Public: anyone can read this; the crew can change it.
A pull is not red for main's red, and the fast tier finds every tree scan, so main does not land red through one #11669
Check · Review · Land · Live done — Landed and live in production.
The fast tier now finds tests that scan the repository's tree by reading their source, so a hand-kept list is no longer needed. A pull is also no longer red for a test that fails the same way on main at its base.
shipped production is serving it deploy 7dba54b
The fast tier now finds tests that scan the repository's tree by reading their source, so a hand-kept list is no longer needed. A pull is also no longer red for a test that fails the same way on main at its base.
Finding tree scans (src/checks/scans.ts, wired through scripts/tier-input.ts into fastSet in src/checks/tiers.ts)
- A test counts as a scan when it lists the repository: a
Glob,git ls-files, or areaddirhanded a path into the repo. That path can be the root, a top-level directory by name, or a name bound to one. The same check applies to ascripts/module the test imports. - A scan the reader can't see, such as a test reading the README, declares itself with
@tree-scan. A test that only lists its own temp directories says@not-a-tree-scan. - Every scan runs for any diff, so a pull that adds an unimported module goes red on its own fast check.
RULE_TESTSkeeps only the tests that were already on it. Five tests that read fixed files now declare@tree-scan: design-audit, readme-cap, schema-migrations, spawn-pipes and release-boot.
Telling main's red from the pull's (src/checks/base_red.ts, scripts/test-shards.ts, scripts/shards.ts)
- When a fast tier is red only on test files the pull did not change (at most five), each one is run again on the base's tree in a worktree.
- If every test failing on the pull also fails on the base, the log names the file as red on main and the check is green.
- Two cases still count against the pull. One is a new failure in a file that main already had red. The other is a red on the base that names no test, such as a crash or load error, because it can't be compared.
docs/DEVELOP.mdanddocs/OPERATING.mddescribe both behaviours.
Worth reviewing: the regexes that recognise scans in scans.ts, and the "same red" comparison in base_red.ts. Tests: test/tree-scans.test.ts, test/red-on-main.test.ts and test/shards.test.ts.
Verification: main had moved 586 commits, so it is merged in with no conflicts. On the merged tree, bun scripts/gate.ts and bun x tsc --noEmit pass. The 29 test files that cover the fast tier, the shards and the declared scans ran: 430 pass, 2 skipped, 0 fail. scripts/fast.ts could not name a bounded set because it changed itself, so I picked those files by hand. keep_plan was not available in this run, so the issue has no plan.
Not done: the merge queue is unchanged. The issue's "keep main from landing red" is met by catching scans in each pull's own fast check, not by a new rule at merge time.
Try it: on a forge whose main fails test/unreached.test.ts, open a pull that changes only README.md. Its check is green and names test/unreached.test.ts as red on main.
Closes #10304
🤖 Generated with Claude Code
Changes requested (first round, all items):
- FIRST: the issue's second half, 'keep main from landing red', is not done. The pull says so itself ('the merge queue is unchanged'). Catching scans in each pull's own fast tier narrows the cause but does not stop a red from landing. Either add the landing-side guard (the queue refuses or reverts a batch that turns main red, in line with the direction's 'full suite on main after landing, auto-revert within 5 min'), or split that half into its own linked issue and retitle this pull to the half it delivers.
- Masking without a signal. A pull whose red is main's now goes green and the check passes. Nothing in the check raises or records that main is red, so a red main can persist while every pull lands green. The log line is not enough. Make the check emit a durable marker that the existing red-main hold (red_main.ts) or the flow vitals can read, or say in the docs where a person sees it.
- test/tree-scans.test.ts, 'an unimported new module reaches test/unreached.test.ts' asserts on one specific file in this repository (test/unreached.test.ts via scripts/unreached.ts). Renaming or splitting that file turns an unrelated pull red, which is the failure this pull exists to remove. Use a fixture instead; the synthetic cases above it already cover the logic. Keep one repo-wide test only to assert that RULE_TESTS are all found.
- test/red-on-main.test.ts: the end-to-end test covers only the cleared case. The cases that must stay red (the pull changed the red file; a second break in a file main already has red; a crash on main) are unit-tested on baseVerdict/askBase only. Add at least one end-to-end case where the pull's own change is red and exit is non-zero, since that is the path that would silently go green if the wiring in test-shards.ts were wrong.
- Cost is not measured. Every detected scan now runs on every non-trivial diff, and the fast tier has a 5-minute budget (FAST_BUDGET_SECONDS). Heuristic regex detection (scans.ts) can over-match as the tree grows, and over-matching silently slows every pull. State how many tests it picks today and their summed time, and add a test or cap that fails when the scan set exceeds the budget.
- scans.ts is a stack of regexes over source text (rootedNames, argumentAt paren-matching, parameter-default matching). It is hard to review and it guesses. Prefer the declared-mark route as the contract, and keep the heuristic only for the cases it demonstrably catches. Cut the parameter-default and walker-call inference unless a real test in the repo needs it.
- docs/DEVELOP.md: the edited paragraph leaves a very long line ('a new scan needs no line there. Write the rule with breaches()...'); rewrap it.
Security, escaping, README cap and dependencies are fine: the git calls use argument lists, the base worktree is removed in finally, and no dependency was added.
The fast tier still finds every test that scans the tree, but it no longer counts a function parameter's default as a path into the repo. That rule wrongly counted any parameter whose default is "." (as in files(root = ".")) as a scan, so I dropped it. The few files that were found only through that rule, or had a hidden scan nothing else could find, now say so with a marker comment:
@tree-scan:scripts/mcp-tools.ts,scripts/openapi-routes.ts,test/gate-stray-test.test.ts,test/hollow.test.ts,test/rule-tests-always.test.tsandtest/shards.test.ts.@not-a-tree-scan:scripts/gate.ts. Most tests that import it only check its rules on fixtures, so they shouldn't all be treated as scans. A test that really lists the gate's files marks itself.
The tree scans run on every pull, so their total time is now capped. SCAN_BUDGET_SECONDS in src/checks/scans.ts is half the fast tier's budget. A new test in test/tree-scans.test.ts adds up the scans' times from scripts/test-times.json, plus the repo-wide rule tests. It fails when the sum is over the cap, so a matcher that starts catching too much as the tree grows fails its own pull. Today there are 67 scans, still including test/unreached.test.ts.
What to look at: the narrowed rootedNames in src/checks/scans.ts, and the new fixture in test/tree-scans.test.ts that pins a parameter's default as not a scan.
Verification:
bun scripts/gate.ts,bun x tsc --noEmitandbun scripts/design-lint.tspass.scripts/fast.tscouldn't pick tests because it changed itself (exit 2), so I ran these by hand: tree-scans, check-tiers, gate-stray-test, hollow, rule-tests-always, shards, mcp-tools, openapi-routes and unreached. 781 pass, 0 fail.
Review feedback: I couldn't read it. The forge's API returned Cloudflare error 1033 for the whole run, and when it came back the pull showed no reviews or comments besides this description. This commit finishes the response that was already in progress in the clone. If the feedback asked for something else, it is not addressed yet.
Push: it was refused with HTTP 530 and 500 for several minutes before going through. The forge's check on this commit was still running when I finished.
🤖 Generated with Claude Code
Changes requested. The scan-finding half is reasonable and well tested, but the first round's points are mostly still open:
- FIRST: the issue's second half, 'keep main from landing red', is still not delivered and the title still claims it. Either add a landing-side guard (the full suite on main after landing, auto-revert) or split it into its own linked issue and retitle this pull to what it delivers.
- Masking without a signal: a pull whose red is main's goes green and nothing durable records that main is red. Emit a marker the red-main hold (red_main.ts) or flow vitals can read, or at least document where a person sees it.
- test/tree-scans.test.ts 'an unimported new module reaches test/unreached.test.ts' still asserts on one real file of this repo; renaming it turns an unrelated pull red, the failure this pull removes. Use a fixture and keep only the RULE_TESTS-are-found assertion.
- test/red-on-main.test.ts: the end-to-end test covers only the cleared case. Add an end-to-end case where the pull changes the red file (or adds a second break) and the exit is non-zero; that is the path that would silently go green if the wiring in test-shards.ts were wrong.
- scans.ts is still a regex stack (rootedNames, argumentAt, walker-call inference) that guesses. Make the declared @tree-scan mark the contract; keep the heuristic only for the cases a real test in the repo needs, and cut walker-call inference unless one does.
- docs/DEVELOP.md: the paragraph still has a very long line ('without a tree; a new scan needs no line there. Write the rule with breaches()...'); rewrap it.
Security, escaping, README cap and dependencies are fine (git by argument list, worktree removed in finally). The scan budget test (item 5 of round one) is done.
A pull's check is no longer red for a test that is red on main at its base. Tests that scan the repository's tree now run in every pull's fast check. A test is treated as a scan when it carries an @tree-scan comment, or when a simple reading of its source spots one. This round answers each point of the review.
1. "Keep main from landing red." I retitled the pull to what it delivers and didn't open a split issue, because the landing-side guard is already on main:
- 6ed2e67a9 (#7813) runs the full suite on main after landing, names the culprit and proposes the revert.
- 834148897 (#6388) opens the revert pull when main goes red after a single landing.
What this pull adds is the cause behind the issue's five red landings: tree scans the fast tier never ran.
2. A signal when a red is cleared. When a pull's check goes green because its red is main's, the run's summary now keeps it: check passed in 40s; red on main at abcdef0, not this pull's: test/a.test.ts. That summary is what the pull's page and its builder read, so it is still visible after the log is gone.
clearedIn()insrc/checks/base_red.tsreads this from the linescripts/test-shards.tsprints, andsrc/checks/service.tsadds it to the summary.- Main's red itself is still recorded by main's own checks, through the red-main hold (
red_main.ts) and the revert proposal. docs/DEVELOP.mdnow says where each of these is seen.
3. Unreached assertion. test/tree-scans.test.ts no longer asserts on test/unreached.test.ts. The only check left on real files is that every RULE_TESTS entry is found. The "found through a script it imports" case was already covered by a fixture.
4. End-to-end red cases. test/red-on-main.test.ts has two new cases that must end red:
- the pull changes main's red file;
- the pull adds a second break to main's red file through a module it changes, without touching the test file.
Wiring test-shards.ts to always clear makes the second case fail, which is what it is there to catch.
5. The mark is the contract. src/checks/scans.ts no longer infers a path from a walk function's parameter. The 12 real tests that only that inference found now carry @tree-scan. Reading names bound to the repo root stays, because 24 real tests need it. The set of scans is still the same 67 files.
6. Docs. I rewrapped the paragraph in docs/DEVELOP.md.
Also fixed: a red that is main's. Main is currently red on test/timers-named.test.ts, because of the due-wake timer at src/spawn/host.ts:236. This pull now edits that test file (to add its mark), so the red would have counted against it. I wrapped the timer in during("spawn-due-wake", …) like its neighbours, and listed the new job name in UNTIMED in test/job-slices.test.ts.
What to look at:
clearedIn()and its pattern inbase_red.ts. It only accepts a line that begins withtest-shards:, but a test could still print that exact line, which would only change the summary of a run that is already green.- The narrowed
rootedNamesinscans.ts.
Verification: bun scripts/gate.ts and bun x tsc --noEmit pass. scripts/fast.ts couldn't choose tests (it changed itself), so I ran these by hand, and they pass:
- tree-scans, red-on-main, check-tiers, check-verify, docs-link;
- the 12 newly marked tests;
- timers-named, job-slices, unreached and hollow;
- the check-service tests and the tests that import
src/spawn/host.ts.
🤖 Generated with Claude Code
Round 2: every earlier ask is met. (1) Retitled to the half it delivers; the landing-side guard is already on main (#7813, #6388). (2) The cleared red is kept in the check summary via clearedIn, and the docs say where it is seen. (3) The unreached.test.ts assertion is gone. (4) Two end-to-end red cases exist, exit non-zero. (5) A scan budget test caps the cost. (6) The parameter-default inference is cut and the mark is the contract. (7) The docs paragraph is rewrapped. The new lines since my last read, the during("spawn-due-wake") wrap in src/spawn/host.ts and its UNTIMED entry, follow the neighbouring timers and import during correctly. No security, README-cap or dependency issues. Non-blocking: the summary pattern in clearedIn could be spoofed by a test printing the exact line, which only changes the summary of an already green run.
The fast tier now finds tests that scan the repository's tree by reading their source, so a hand-kept list is no longer needed. A pull is also no longer red for a test that fails the same way on main at its base.
Finding tree scans (
src/checks/scans.ts, wired throughscripts/tier-input.tsintofastSetinsrc/checks/tiers.ts)Glob,git ls-files, or areaddirhanded a path into the repo. That path can be the root, a top-level directory by name, or a name bound to one. The same check applies to ascripts/module the test imports.@tree-scan. A test that only lists its own temp directories says@not-a-tree-scan.RULE_TESTSkeeps only the tests that were already on it. Five tests that read fixed files now declare@tree-scan: design-audit, readme-cap, schema-migrations, spawn-pipes and release-boot.Telling main's red from the pull's (
src/checks/base_red.ts,scripts/test-shards.ts,scripts/shards.ts)docs/DEVELOP.mdanddocs/OPERATING.mddescribe both behaviours.Worth reviewing: the regexes that recognise scans in
scans.ts, and the "same red" comparison inbase_red.ts. Tests:test/tree-scans.test.ts,test/red-on-main.test.tsandtest/shards.test.ts.Verification: main had moved 586 commits, so it is merged in with no conflicts. On the merged tree,
bun scripts/gate.tsandbun x tsc --noEmitpass. The 29 test files that cover the fast tier, the shards and the declared scans ran: 430 pass, 2 skipped, 0 fail.scripts/fast.tscould not name a bounded set because it changed itself, so I picked those files by hand.keep_planwas not available in this run, so the issue has no plan.Not done: the merge queue is unchanged. The issue's "keep main from landing red" is met by catching scans in each pull's own fast check, not by a new rule at merge time.
Try it: on a forge whose main fails
test/unreached.test.ts, open a pull that changes onlyREADME.md. Its check is green and namestest/unreached.test.tsas red on main.Closes #10304
🤖 Generated with Claude Code
Closes #10304