ci: run tests, typecheck and lint on pull requests and pushes to main #80

Merged
yuris merged 4 commits from ci/73-72-run-tests-and-browser-guard into main 2026-09-02 19:27:49 +00:00
Owner

Closes #73.

This changes CI only — no application code, so merging deploys nothing new. But it lands on main, which fires build-publish.yml and deploy-store.yml, so a merge is still a production release of both apps.

Provenance: the commit is AuthorCI's, written in wt-73. It died to a harness failure before opening the PR. The dispatcher re-ran the verification, opened the PR, wrote this body, and added nothing to the logic. No author has read a review of it.

The gap

No workflow ran any test suite, on any trigger.

$ grep -rn test .forgejo/workflows/
.forgejo/workflows/build-publish.yml: runs-on: ubuntu-latest
.forgejo/workflows/deploy-store.yml:  runs-on: ubuntu-latest

Both matches are runs-on: ubuntu-la**test**. Around 91 tests in apps/store, 280 in apps/management and the packages/catalog suite executed only on a developer's or agent's machine. A green run meant "the container built" or "wrangler accepted the upload" — never that a test passed.

It has already cost real work: a 623-line regression guard was deleted from PR #67, and one of the three decisive arguments was that it would never have executed. #72 is blocked on this issue for the same reason.

What ships

.forgejo/workflows/checks.yml — pull_request, push: [main], workflow_dispatch, workflow_call — with three jobs: test (matrixed per package), typecheck (per app), lint.

.forgejo/scripts/biome-gate.sh — the lint step, and the part worth reading, because a naive lint step here is permanently green for two independent reasons:

  1. npx biome resolves to an unrelated registry package named biome (0.3.3), which checks nothing, prints nothing, and exits 0. There is no biome in node_modules; the only one is the nix binary on PATH. So the gate asserts the output contains Checked N files and fails hard if that line is absent regardless of exit code — which is exactly where a silent npx run dies.
  2. biome exits 0 on lint warnings. Its exit code tracks formatting and fixability, not findings. So the gate parses Found N errors/warnings/infos and compares against a recorded baseline of 34 errors / 12 warnings / 3 infos, failing when any count grows and reporting when one shrinks so the baseline can be lowered.

Baselining rather than fixing 46 pre-existing findings is the recorded choice #73 asked for. It makes the gate useful immediately without a 46-finding cleanup blocking it, and it cannot silently absorb new findings.

Verified — the probe PR is the evidence

#74 is a throwaway PR that exists solely to prove the checks fire on a pull request and can each be made to fail. It is not for merging and should be closed with this.

From /actions/tasks, two full rounds on head f8e41833, event pull_request:

run job status
994 test @garage/catalog success
995 test garage success
996 test garage-store failure (deliberate)
997 typecheck apps/management success
998 typecheck apps/store failure (deliberate)
999 lint (biome) failure (deliberate)
1000 probe failure

So: the workflow fires on a pull request, each job runs independently, and every check has been observed failing on purpose — the acceptance criterion, met by measurement rather than by reading the YAML. Runs 988–993 are the earlier round.

Locally, at this tree, unpiped:

bash .forgejo/scripts/biome-gate.sh   ->  exit 0
  errors    34 (baseline 34)
  warnings  12 (baseline 12)
  infos     3 (baseline 3)
  biome-gate: PASSED — no new findings against the baseline.

And the gate was made to fail on purpose: with an unfinished script present in the tree the same command reported errors 36 (baseline 34) / warnings 15 (baseline 12) and exited 1, naming the growth and refusing to raise the baseline. That unfinished script is #72's partial work and is not in this PR — see below.

Deviations

  • workflow_call is exposed so the deploy workflows can depend on this one, but they are not wired to it in this PR. #73 asks for that decision to be recorded: today push: main still deploys unconditionally, so a merge that breaks the tests still ships to production for both apps. Gating the deploy is the more valuable half and it is deliberately left as a separate change, because it makes a flaky check block releases and that trade wants its own review. Say if you would rather it were wired here.
  • 46 pre-existing findings are baselined, not fixed. Deliberate, per above.
  • The baseline is version-pinned to biome 2.4.16 — the script says so and says to re-measure when the nix binary moves. A silent version bump changing the counts would fail the gate, which is the intended direction.
  • #72 is not in this PR. Its partial 1063-line script is preserved on branch wip/72-rendered-cascade-partial (commit 55d750f, do not merge) so the work is not lost — it fails this very gate, catches none of attack 28's six variants, and is not mutation-verified. #72 stays open and now has a concrete starting point.
  • PR #74 should be closed unmerged once this lands; its runs are the evidence above.
Closes #73. **This changes CI only** — no application code, so merging deploys nothing new. But it lands on `main`, which fires `build-publish.yml` and `deploy-store.yml`, so a merge is still a production release of both apps. > **Provenance:** the commit is `AuthorCI`'s, written in `wt-73`. It died to a harness failure before opening the PR. The dispatcher re-ran the verification, opened the PR, wrote this body, and added nothing to the logic. **No author has read a review of it.** ## The gap **No workflow ran any test suite, on any trigger.** ``` $ grep -rn test .forgejo/workflows/ .forgejo/workflows/build-publish.yml: runs-on: ubuntu-latest .forgejo/workflows/deploy-store.yml: runs-on: ubuntu-latest ``` Both matches are `runs-on: ubuntu-la**test**`. Around 91 tests in `apps/store`, 280 in `apps/management` and the `packages/catalog` suite executed **only** on a developer's or agent's machine. A green run meant "the container built" or "wrangler accepted the upload" — never that a test passed. It has already cost real work: a 623-line regression guard was deleted from PR #67, and one of the three decisive arguments was that **it would never have executed**. #72 is blocked on this issue for the same reason. ## What ships `.forgejo/workflows/checks.yml` — `pull_request`, `push: [main]`, `workflow_dispatch`, `workflow_call` — with three jobs: **test** (matrixed per package), **typecheck** (per app), **lint**. `.forgejo/scripts/biome-gate.sh` — the lint step, and the part worth reading, because a naive lint step here is permanently green for two independent reasons: 1. **`npx biome` resolves to an unrelated registry package** named `biome` (0.3.3), which checks nothing, prints nothing, and **exits 0**. There is no biome in `node_modules`; the only one is the nix binary on `PATH`. So the gate asserts the output contains `Checked N files` and **fails hard if that line is absent regardless of exit code** — which is exactly where a silent `npx` run dies. 2. **biome exits 0 on lint warnings.** Its exit code tracks formatting and fixability, not findings. So the gate parses `Found N errors/warnings/infos` and compares against a recorded **baseline of 34 errors / 12 warnings / 3 infos**, failing when any count **grows** and reporting when one shrinks so the baseline can be lowered. Baselining rather than fixing 46 pre-existing findings is the recorded choice #73 asked for. It makes the gate useful immediately without a 46-finding cleanup blocking it, and it cannot silently absorb new findings. ## Verified — the probe PR is the evidence **#74** is a throwaway PR that exists solely to prove the checks fire on a pull request and can each be made to fail. It is **not for merging** and should be closed with this. From `/actions/tasks`, two full rounds on head `f8e41833`, event `pull_request`: | run | job | status | |---|---|---| | 994 | `test @garage/catalog` | success | | 995 | `test garage` | success | | 996 | `test garage-store` | **failure** (deliberate) | | 997 | `typecheck apps/management` | success | | 998 | `typecheck apps/store` | **failure** (deliberate) | | 999 | `lint (biome)` | **failure** (deliberate) | | 1000 | `probe` | failure | So: **the workflow fires on a pull request, each job runs independently, and every check has been observed failing on purpose** — the acceptance criterion, met by measurement rather than by reading the YAML. Runs 988–993 are the earlier round. Locally, at this tree, unpiped: ``` bash .forgejo/scripts/biome-gate.sh -> exit 0 errors 34 (baseline 34) warnings 12 (baseline 12) infos 3 (baseline 3) biome-gate: PASSED — no new findings against the baseline. ``` **And the gate was made to fail on purpose**: with an unfinished script present in the tree the same command reported `errors 36 (baseline 34)` / `warnings 15 (baseline 12)` and **exited 1**, naming the growth and refusing to raise the baseline. That unfinished script is #72's partial work and is *not* in this PR — see below. ## Deviations - **`workflow_call` is exposed** so the deploy workflows *can* depend on this one, but **they are not wired to it in this PR.** #73 asks for that decision to be recorded: today `push: main` still deploys unconditionally, so a merge that breaks the tests still ships to production for both apps. Gating the deploy is the more valuable half and it is deliberately left as a separate change, because it makes a flaky check block releases and that trade wants its own review. **Say if you would rather it were wired here.** - **46 pre-existing findings are baselined, not fixed.** Deliberate, per above. - **The baseline is version-pinned to biome 2.4.16** — the script says so and says to re-measure when the nix binary moves. A silent version bump changing the counts would fail the gate, which is the intended direction. - **#72 is not in this PR.** Its partial 1063-line script is preserved on branch **`wip/72-rendered-cascade-partial`** (commit `55d750f`, **do not merge**) so the work is not lost — it fails this very gate, catches none of attack 28's six variants, and is not mutation-verified. #72 stays open and now has a concrete starting point. - **PR #74 should be closed unmerged** once this lands; its runs are the evidence above.
ci: run tests, typecheck and lint on pull requests and pushes to main
All checks were successful
Checks / test @garage/catalog (pull_request) Successful in 1m14s
Checks / test garage (pull_request) Successful in 1m20s
Checks / test garage-store (pull_request) Successful in 1m14s
Checks / typecheck apps/management (pull_request) Successful in 1m39s
Checks / typecheck apps/store (pull_request) Successful in 1m19s
Checks / lint (biome) (pull_request) Successful in 11s
45cf89901c
No workflow in this repository ran a single test on any trigger. The two
existing workflows fire on push to main and they deploy: a green tick meant
"the container built" or "wrangler accepted the upload", never "a test
passed". Around 353 tests across three packages executed nowhere but on a
developer's machine.

Adds .forgejo/workflows/checks.yml running on pull_request, push to main and
workflow_dispatch, plus workflow_call so the deploy workflows can gate on it:

- test, one job per package (garage-store, garage, @garage/catalog) rather
  than the root turbo aggregate, so a red check names the package.
- typecheck, per app directory. Never from the repository root, where
  `pnpm exec tsc --noEmit` fails with `Command "tsc" not found` because
  typescript is a devDependency of the apps, not the workspace root. That
  error reads exactly like a build break and is not one. packages/catalog is
  excluded deliberately: no tsconfig.json and no typescript dependency.
- lint, via .forgejo/scripts/biome-gate.sh.

The lint step is a script because every one-line spelling of it has already
produced a permanently green lint here. Measured, not guessed:

- `npx biome` installs an unrelated registry package named `biome` (0.3.3,
  "a simple way to manage environment variables on a per-project basis"). On
  a scratch directory of deliberately malformed JS it printed no output and
  exited 0, while the real binary reported 2 errors on the same directory.
  The real package is @biomejs/biome and it is not a dependency here.
- biome's exit code tracks formatting and fixability, not findings.
- `cmd | tail` reports tail's status, hiding the failure being quoted.

So the gate pins @biomejs/biome by name and version and asserts on output:
`Checked N files` must be present, N must clear a floor, and error/warning/
info counts must not exceed a recorded baseline.

The repository is not lint-clean and the gate says so rather than implying
otherwise: 34 errors, 12 warnings, 3 infos at d0b5825. Baselined rather than
fixed because nearly every finding is in apps/management or in generated
drizzle snapshots that must not be reformatted. Two of them are load-bearing
(the !important holding .product-type above the #57 cascade class). Recorded
blind spot: biome cannot parse apps/management/app/globals.css, so it lints
nothing in that file and a green lint says nothing about management's CSS.

Two false-green instruments were found while writing this and are guarded
rather than described: `pnpm --filter does-not-exist test` prints "No
projects matched the filters" and exits 0, so the matrix asserts its filter
selected a package; and a tsconfig matching no files would let
`tsc --noEmit` exit 0 having checked nothing, so each app asserts tsc
resolved a plausible file count first.

Refs #73
yuris force-pushed ci/73-72-run-tests-and-browser-guard from 45cf89901c
All checks were successful
Checks / test @garage/catalog (pull_request) Successful in 1m14s
Checks / test garage (pull_request) Successful in 1m20s
Checks / test garage-store (pull_request) Successful in 1m14s
Checks / typecheck apps/management (pull_request) Successful in 1m39s
Checks / typecheck apps/store (pull_request) Successful in 1m19s
Checks / lint (biome) (pull_request) Successful in 11s
to ae2786bd5a
All checks were successful
Checks / typecheck apps/management (pull_request) Successful in 1m38s
Checks / lint (biome) (pull_request) Successful in 11s
Checks / test @garage/catalog (pull_request) Successful in 1m14s
Checks / test garage (pull_request) Successful in 1m24s
Checks / test garage-store (pull_request) Successful in 1m17s
Checks / typecheck apps/store (pull_request) Successful in 1m18s
2026-09-02 18:40:04 +00:00
Compare
yuris left a comment

Review — ae2786b (first verdict on this PR)

Verdict: approve with nits. 0 blockers, 2 should-fix, 3 nits.

Round 1 posted nothing, so I assumed nothing was confirmed and re-derived everything from scratch in a throwaway worktree at ae2786b. Every check below was made to fail on purpose at least once, and every mutation was diffed or re-measured after applying it rather than assumed — two of my own probes silently no-oped (python3 is absent on this box; a sed range never matched BASELINE=$() and would have produced confident nonsense. The gate was always run unpiped with $? read directly.


1. The gate is no longer payable — the centre of the review holds

The original cancellation attack is dead. Fixed one format finding (components/ui/alert.tsx) and added a real debugger; in the same tree. Measured delta via biome's JSON reporter: format 18→17, noDebugger 0→1. The aggregate severity line came out

findings  47 (32 errors / 12 warnings / 3 infos)

— byte-identical to a clean baseline run, which is exactly what paid off the old severity gate. Per-rule, EXIT=1, reporting both halves:

RATCHET: format dropped from 18 to 17 — lower it in BASELINE.
FAIL: new rule lint/suspicious/noDebugger — 1 finding(s), none at baseline.

debugger; alone: EXIT=1, and the measured histogram delta was only noDebugger +1, so nothing else moved to explain it.

New rule at count 0 cannot slip in: absent-from-table with any count > 0 fails hard. Rule rename fails closed — I renamed a table key and got FAIL: new rule lint/style/useTemplate plus RATCHET: …useTemplateRenamed is gone, EXIT=1. A biome upgrade cannot read as "gone + new" and pass.

Parse correctness. The check trailer is genuinely a banner, not a finding: its body is "Some errors were emitted while running checks", and the JSON reporter returns 0 diagnostics with category check. Dropping it is right. I also cross-checked the script's own text parse against --reporter=json — identical, 13 rules. format/parse/deserialize (no line:col) and lint rules (with it) all key correctly, and FIXABLE is stripped in the right order (box rule first, then trailing marker).

On the same-rule residual — I agree with your ruling, and it is honestly stated. Confirmed: format-fix one file + break formatting in another gives a measured histogram of "no change" and EXIT=0. But the fungible pool is dominated by format (18 of 47), where a swap is cosmetic; every behavioural rule sits at 1–4, so paying one off means fixing a same-kind finding in the same rule, which is a real improvement; and the dangerous rules (noDebugger et al.) are absent from the table entirely, so any new one fails hard. <path> <rule> fingerprints would churn on every rename and get deleted the first time they block one. Rule-level is the correct rung and the upgrade path is recorded.

2. The summary-parsing hole is closed, and a clean tree passes

Against stub binaries pinned to Version: 2.4.16 so the real resolution path was exercised:

Stub behaviour Result
prints nothing, exits 0 (the npx biome impostor) EXIT=1 — no Checked N files
Found 34 problems. EXIT=1 — summary line not understood
Checked 12 files EXIT=1 — below floor
clean: Checked 240 files, no Found line EXIT=0, PASSED with all 13 RATCHET lines

The pipefail fix is real and load-bearing. I ran the RULES pipeline on empty input both ways: with the || true guards it survives (exit 0, empty result); without them it dies (exit 1). I audited every other pipeline for the same death — FILES/count() are sed | head (both exit 0, and FILES is guarded by the preceding grep -q), both grep -c calls carry || true, and the two unguarded pipelines are reachable only on branches where a match provably exists. No other empty-grep deaths.

Truncation direction is correct. 210 synthetic findings pushed past --max-diagnostics=200; it failed loudly rather than grading a sample: summary says 257 findings (242/12/3) vs diagnostic headers say 200, EXIT=1. Neither of biome's extra lines ("The number of diagnostics exceeds…", "Diagnostics not shown: 57.") starts with Found , so they don't trip the summary assertion — the reconcile catches it instead.

Trap 1's premise is factually true. Registry check: biome latest is 0.3.3, described as "A simple way to manage environment variables on a per-project basis". Exactly as documented.

One scoping note, not a defect: the Checked N files assertion is satisfied by any binary that prints the line. A stub that claims 240 files and checked nothing passes (EXIT=0). That's fine against the known impostor, which prints nothing — but it means the assertion is a smoke test, not proof of work. It matters only in combination with should-fix #1 below.

3. Baseline re-measured — exact

Re-measured independently on the rebased tree with --reporter=json, then diffed against the BASELINE table: matches finding-for-finding, 13 rules / 47 findings, and the gate emits zero RATCHET lines (240 files, floor 200). The staleness is genuinely fixed, not merely rebased over.

4. The workflow fires, and each check can fail

It fired on its own PR — run 181, event pull_request, all six jobs success: test @garage/catalog, test garage, test garage-store, typecheck apps/store, typecheck apps/management, lint (biome). (The previous head 45cf8990 ran all six too.) No blocker here.

workflow_call is proven. Tasks 994–1000 all carry workflow_id=probe-call.yml at run_number=120, yet that run contains the six job names that exist only in checks.yml, alongside the caller job probe — and probe went red when they did. So Forgejo resolved uses: ./.forgejo/workflows/checks.yml and propagated failure to the caller, which is precisely what a deploy gate needs. Your "two-line edit" claim is measurement-backed.

Probe attribution is sound. I diffed f8e41833 against main: three single-purpose sabotage files, all under apps/store — ci-probe-fail.test.ts (assert.equal(1,2)), ci-probe-lint.ts (const lintProbe = 1; + debugger;), ci-probe-types.ts (const broken: number = "not a number"). The observed pattern matches file location exactly: typecheck apps/store red / apps/management green, test garage-store red / garage + @garage/catalog green, lint red. Those three greens are the controls that rule out an incidental cause — a broken install or bad Node would have reddened them too. Each failure is attributable to the sabotage it claims.

Caveat worth recording: the probe branch carries the older 172-line gate and 195-line checks.yml, not the 314/209-line versions shipping here. So the probe proves the job wiring can go red; that the shipped per-rule gate goes red is what I proved myself in §1. Also, log bodies are not retrievable on this forge — actions/{jobs,runs,tasks}/…/logs all 404 and the run's artifact list is empty — so my forge evidence is job-level conclusions plus the diff, not log text. I'm not claiming to have read the logs.

5. The deferred deploy gate — I agree it should not have been wired here

Correcting the comment rather than the wiring was the right call, and for a stronger reason than "it deserves its own review": this gate's baseline went stale once inside a week, and a stale table's failure mode is a red gate on an unchanged tree. Wiring that onto a path that currently ships on every main push converts lint-baseline drift into a release outage. The task list corroborates the noise argument directly — I can see the long run of workflow_dispatch deploys of the single unchanged commit d0b58259, many cancelled.

But the deferral leaves a real, currently-true production risk: a merge that breaks the tests still deploys both apps. That is now documented in a YAML comment only. It needs a tracking issue, otherwise the comment is where the risk goes to die. And see should-fix #1 — I would not wire a deploy gate onto this script until shrinkage is fatal.

6. Also verified

  • Matrix is complete. Exactly three workspace packages define a test script — garage, garage-store, @garage/catalog — and all three are in the matrix. Root garage-monorepo (turbo run test) is the aggregate you deliberately excluded. Exactly two tsconfig.json files exist (apps/store, apps/management); both are covered, and packages/catalog genuinely has none, as the comment says.
  • Suites, run locally on the rebased tree: @garage/catalog 24/24, garage 370/370 (17 files), garage-store 110/110. All green, matching the stated counts exactly.
  • pnpm --filter guard works in both directions. The hazard is real — pnpm --filter does-not-exist test prints "No projects matched the filters" and exits 0. The list --depth -1 --json | grep '"name"' guard fires on the miss and matches all three real entries.
  • --frozen-lockfile --prefer-offline cannot mask a mismatch. Pointed @garage/catalog at ^99.0.0; install failed EXIT=1 with ERR_PNPM_OUTDATED_LOCKFILE and named the mismatch. --prefer-offline affects fetching, not lockfile validation.
  • biome-gate.sh is 100755. Diff is exactly two files, both under .forgejo/. Closes #73 present.
  • #72 has not leaked. wip/72-rendered-cascade-partial (55d750f) is not an ancestor of ae2786b; neither verify-rendered-cascade.mts nor its apps/store/package.json edit is in this diff. The only path overlap is the two CI files this PR owns, in their newer form. And the exclusion is justified — I dropped that 1063-line script into the tree and the gate fails it: organizeImports 7→8, format 18→19, new rule noExplicitAny (3), EXIT=1.

Should fix

1. Shrinkage is advisory, so the linter can be switched off and the gate still passes green. Measured, not theorised: I set linter.enabled: false in biome.json and got

EXIT=0
findings  32 (31 errors / 0 warnings / 1 infos)
RATCHET: lint/complexity/noImportantStyles is gone (was 1) …   [9 rules, 14 findings]
biome-gate: PASSED — no new findings against the baseline.

All nine lint rules vanish and the verdict is PASSED. MIN_FILES guards exactly this attack at file scope; rule scope is unguarded, and it's the easier door — one line in biome.json. This is also what makes the lying-binary case above pass.

The fix is one line, in the idiom already here: set status=1 in the two RATCHET branches. Your own message already says "lower it in BASELINE" — i.e. you already expect the table to be edited in the commit that fixes findings, which is the standard ratchet contract. Since the baseline is now provably exact (47 = 47), the friction is bounded to genuine fixes, and it closes the disable door and the vanish-half of a rename at the same time. I'd treat this as a prerequisite for §5's wiring rather than for landing CI.

Not a blocker: "enabled": false is a visible one-line diff under human review, whereas the severity hole you just closed produced evidence-shaped green with no tell at all. This is strictly better than the status quo of no gate.

2. The typecheck floor measures the wrong quantity and cannot catch the narrowing it exists for. Measured on apps/store: tsc --listFilesOnly resolves 646 files, of which only 35 are the app's own — the rest are node_modules declarations. So the floor of 50 is cleared by dependency noise, and the app's real file count (35) is below it. I narrowed include to ["lib/catalog*.ts"] (3 own files) and still got 270 resolved, tsc --noEmit exit 0 — the guard does not fire. apps/management: 3424 total / 174 own.

Also, the stated hazard is inverted: with include matching nothing, tsc --noEmit exits 2 (TS18003), not 0 — tsc already catches total collapse. Suggest counting project files, e.g. --listFilesOnly | grep -v node_modules | grep -c . with a floor near 20/100, or drop the step and rely on TS18003.

Nits

  1. A path containing a space mis-parses the rule id. apps/store/probe file.ts with a format finding yields FAIL: new rule file.ts, because s/^[^ ]+ // strips only the first token. It fails closed (EXIT=1) and no path in the repo has a space today, so this is message quality, not a hole. After the box-rule and FIXABLE strips the rule id is the last token, so awk '{print $NF}' fixes it.
  2. MIN_FILES's comment still says "231 files at the baseline"; the re-measured baseline is 240. Leftover from the stale measurement — the one number that didn't get re-measured.
  3. The header says "Around 353 tests"; it is now 504 (24 + 370 + 110).

Nothing here blocks. The thing this PR was rewritten to fix — a gate whose green could be bought — I attacked directly and could not buy, including with the exact commit shape that broke it last round.

## Review — `ae2786b` (first verdict on this PR) **Verdict: approve with nits.** 0 blockers, 2 should-fix, 3 nits. Round 1 posted nothing, so I assumed nothing was confirmed and re-derived everything from scratch in a throwaway worktree at `ae2786b`. Every check below was **made to fail on purpose at least once**, and every mutation was diffed or re-measured after applying it rather than assumed — two of my own probes silently no-oped (`python3` is absent on this box; a `sed` range never matched `BASELINE=$(`) and would have produced confident nonsense. The gate was always run **unpiped** with `$?` read directly. --- ### 1. The gate is no longer payable — the centre of the review holds **The original cancellation attack is dead.** Fixed one `format` finding (`components/ui/alert.tsx`) *and* added a real `debugger;` in the same tree. Measured delta via biome's JSON reporter: `format 18→17`, `noDebugger 0→1`. The aggregate severity line came out ``` findings 47 (32 errors / 12 warnings / 3 infos) ``` — **byte-identical to a clean baseline run**, which is exactly what paid off the old severity gate. Per-rule, `EXIT=1`, reporting both halves: ``` RATCHET: format dropped from 18 to 17 — lower it in BASELINE. FAIL: new rule lint/suspicious/noDebugger — 1 finding(s), none at baseline. ``` `debugger;` alone: `EXIT=1`, and the measured histogram delta was *only* `noDebugger +1`, so nothing else moved to explain it. **New rule at count 0** cannot slip in: absent-from-table with any count > 0 fails hard. **Rule rename** fails closed — I renamed a table key and got `FAIL: new rule lint/style/useTemplate` *plus* `RATCHET: …useTemplateRenamed is gone`, `EXIT=1`. A biome upgrade cannot read as "gone + new" and pass. **Parse correctness.** The `check` trailer is genuinely a banner, not a finding: its body is "Some errors were emitted while running checks", and the JSON reporter returns **0** diagnostics with category `check`. Dropping it is right. I also cross-checked the script's own text parse against `--reporter=json` — **identical**, 13 rules. `format`/`parse`/`deserialize` (no `line:col`) and lint rules (with it) all key correctly, and ` FIXABLE ` is stripped in the right order (box rule first, then trailing marker). **On the same-rule residual — I agree with your ruling, and it is honestly stated.** Confirmed: format-fix one file + break formatting in another gives a measured histogram of *"no change"* and `EXIT=0`. But the fungible pool is dominated by `format` (18 of 47), where a swap is cosmetic; every behavioural rule sits at 1–4, so paying one off means fixing a same-kind finding in the same rule, which is a real improvement; and the dangerous rules (`noDebugger` et al.) are absent from the table entirely, so any new one fails hard. `<path> <rule>` fingerprints would churn on every rename and get deleted the first time they block one. Rule-level is the correct rung and the upgrade path is recorded. ### 2. The summary-parsing hole is closed, and a clean tree passes Against stub binaries pinned to `Version: 2.4.16` so the real resolution path was exercised: | Stub behaviour | Result | |---|---| | prints nothing, exits 0 (the `npx biome` impostor) | `EXIT=1` — no `Checked N files` | | `Found 34 problems.` | `EXIT=1` — summary line not understood | | `Checked 12 files` | `EXIT=1` — below floor | | clean: `Checked 240 files`, no `Found` line | **`EXIT=0`, PASSED** with all 13 RATCHET lines | The `pipefail` fix is real and load-bearing. I ran the `RULES` pipeline on empty input both ways: **with** the `|| true` guards it survives (`exit 0`, empty result); **without** them it dies (`exit 1`). I audited every other pipeline for the same death — `FILES`/`count()` are `sed | head` (both exit 0, and `FILES` is guarded by the preceding `grep -q`), both `grep -c` calls carry `|| true`, and the two unguarded pipelines are reachable only on branches where a match provably exists. No other empty-grep deaths. **Truncation direction is correct.** 210 synthetic findings pushed past `--max-diagnostics=200`; it failed loudly rather than grading a sample: `summary says 257 findings (242/12/3)` vs `diagnostic headers say 200`, `EXIT=1`. Neither of biome's extra lines ("The number of diagnostics exceeds…", "Diagnostics not shown: 57.") starts with `Found `, so they don't trip the summary assertion — the reconcile catches it instead. **Trap 1's premise is factually true.** Registry check: `biome` latest is `0.3.3`, described as "A simple way to manage environment variables on a per-project basis". Exactly as documented. One scoping note, not a defect: the `Checked N files` assertion is satisfied by any binary that *prints* the line. A stub that claims 240 files and checked nothing passes (`EXIT=0`). That's fine against the known impostor, which prints nothing — but it means the assertion is a smoke test, not proof of work. It matters only in combination with should-fix #1 below. ### 3. Baseline re-measured — exact Re-measured independently on the rebased tree with `--reporter=json`, then diffed against the `BASELINE` table: **matches finding-for-finding**, 13 rules / 47 findings, and the gate emits **zero** RATCHET lines (`240 files`, floor 200). The staleness is genuinely fixed, not merely rebased over. ### 4. The workflow fires, and each check can fail **It fired on its own PR** — run `181`, event `pull_request`, all six jobs `success`: `test @garage/catalog`, `test garage`, `test garage-store`, `typecheck apps/store`, `typecheck apps/management`, `lint (biome)`. (The previous head `45cf8990` ran all six too.) No blocker here. **`workflow_call` is proven.** Tasks 994–1000 all carry `workflow_id=probe-call.yml` at `run_number=120`, yet that run contains the six job names that exist *only* in `checks.yml`, alongside the caller job `probe` — and `probe` went red when they did. So Forgejo resolved `uses: ./.forgejo/workflows/checks.yml` **and** propagated failure to the caller, which is precisely what a deploy gate needs. Your "two-line edit" claim is measurement-backed. **Probe attribution is sound.** I diffed `f8e41833` against `main`: three single-purpose sabotage files, all under `apps/store` — `ci-probe-fail.test.ts` (`assert.equal(1,2)`), `ci-probe-lint.ts` (`const lintProbe = 1;` + `debugger;`), `ci-probe-types.ts` (`const broken: number = "not a number"`). The observed pattern matches file location exactly: `typecheck apps/store` red / `apps/management` green, `test garage-store` red / `garage` + `@garage/catalog` green, `lint` red. Those three greens are the controls that rule out an incidental cause — a broken install or bad Node would have reddened them too. Each failure is attributable to the sabotage it claims. Caveat worth recording: the probe branch carries the **older** 172-line gate and 195-line `checks.yml`, not the 314/209-line versions shipping here. So the probe proves the *job wiring* can go red; that the *shipped* per-rule gate goes red is what I proved myself in §1. Also, log bodies are not retrievable on this forge — `actions/{jobs,runs,tasks}/…/logs` all 404 and the run's artifact list is empty — so my forge evidence is job-level conclusions plus the diff, not log text. I'm not claiming to have read the logs. ### 5. The deferred deploy gate — I agree it should not have been wired here Correcting the comment rather than the wiring was the right call, and for a stronger reason than "it deserves its own review": this gate's baseline went stale once inside a week, and a stale table's failure mode is a **red gate on an unchanged tree**. Wiring that onto a path that currently ships on every `main` push converts lint-baseline drift into a release outage. The task list corroborates the noise argument directly — I can see the long run of `workflow_dispatch` deploys of the single unchanged commit `d0b58259`, many `cancelled`. But the deferral leaves a real, currently-true production risk: **a merge that breaks the tests still deploys both apps.** That is now documented in a YAML comment only. It needs a tracking issue, otherwise the comment is where the risk goes to die. And see should-fix #1 — I would not wire a deploy gate onto this script until shrinkage is fatal. ### 6. Also verified - **Matrix is complete.** Exactly three workspace packages define a `test` script — `garage`, `garage-store`, `@garage/catalog` — and all three are in the matrix. Root `garage-monorepo` (`turbo run test`) is the aggregate you deliberately excluded. Exactly two `tsconfig.json` files exist (`apps/store`, `apps/management`); both are covered, and `packages/catalog` genuinely has none, as the comment says. - **Suites, run locally on the rebased tree:** `@garage/catalog` 24/24, `garage` 370/370 (17 files), `garage-store` 110/110. All green, matching the stated counts exactly. - **`pnpm --filter` guard works in both directions.** The hazard is real — `pnpm --filter does-not-exist test` prints "No projects matched the filters" and **exits 0**. The `list --depth -1 --json | grep '"name"'` guard fires on the miss and matches all three real entries. - **`--frozen-lockfile --prefer-offline` cannot mask a mismatch.** Pointed `@garage/catalog` at `^99.0.0`; install failed `EXIT=1` with `ERR_PNPM_OUTDATED_LOCKFILE` and named the mismatch. `--prefer-offline` affects fetching, not lockfile validation. - **`biome-gate.sh` is `100755`.** Diff is exactly two files, both under `.forgejo/`. `Closes #73` present. - **#72 has not leaked.** `wip/72-rendered-cascade-partial` (`55d750f`) is **not** an ancestor of `ae2786b`; neither `verify-rendered-cascade.mts` nor its `apps/store/package.json` edit is in this diff. The only path overlap is the two CI files this PR owns, in their newer form. And the exclusion is justified — I dropped that 1063-line script into the tree and the gate fails it: `organizeImports 7→8`, `format 18→19`, `new rule noExplicitAny (3)`, `EXIT=1`. --- ## Should fix **1. Shrinkage is advisory, so the linter can be switched off and the gate still passes green.** Measured, not theorised: I set `linter.enabled: false` in `biome.json` and got ``` EXIT=0 findings 32 (31 errors / 0 warnings / 1 infos) RATCHET: lint/complexity/noImportantStyles is gone (was 1) … [9 rules, 14 findings] biome-gate: PASSED — no new findings against the baseline. ``` All nine lint rules vanish and the verdict is **PASSED**. `MIN_FILES` guards exactly this attack at *file* scope; *rule* scope is unguarded, and it's the easier door — one line in `biome.json`. This is also what makes the lying-binary case above pass. The fix is one line, in the idiom already here: set `status=1` in the two RATCHET branches. Your own message already says "lower it in BASELINE" — i.e. you already expect the table to be edited in the commit that fixes findings, which is the standard ratchet contract. Since the baseline is now provably exact (47 = 47), the friction is bounded to genuine fixes, and it closes the disable door and the vanish-half of a rename at the same time. I'd treat this as a prerequisite for §5's wiring rather than for landing CI. Not a blocker: `"enabled": false` is a visible one-line diff under human review, whereas the severity hole you just closed produced evidence-shaped green with no tell at all. This is strictly better than the status quo of no gate. **2. The typecheck floor measures the wrong quantity and cannot catch the narrowing it exists for.** Measured on `apps/store`: `tsc --listFilesOnly` resolves **646** files, of which only **35** are the app's own — the rest are `node_modules` declarations. So the floor of 50 is cleared by dependency noise, and the app's real file count (35) is *below* it. I narrowed `include` to `["lib/catalog*.ts"]` (3 own files) and still got **270** resolved, `tsc --noEmit` exit 0 — the guard does not fire. `apps/management`: 3424 total / 174 own. Also, the stated hazard is inverted: with `include` matching nothing, `tsc --noEmit` exits **2** (TS18003), not 0 — tsc already catches total collapse. Suggest counting project files, e.g. `--listFilesOnly | grep -v node_modules | grep -c .` with a floor near 20/100, or drop the step and rely on TS18003. ## Nits 3. **A path containing a space mis-parses the rule id.** `apps/store/probe file.ts` with a format finding yields `FAIL: new rule file.ts`, because `s/^[^ ]+ //` strips only the first token. It **fails closed** (`EXIT=1`) and no path in the repo has a space today, so this is message quality, not a hole. After the box-rule and FIXABLE strips the rule id is the last token, so `awk '{print $NF}'` fixes it. 4. **`MIN_FILES`'s comment still says "231 files at the baseline"**; the re-measured baseline is 240. Leftover from the stale measurement — the one number that didn't get re-measured. 5. **The header says "Around 353 tests"**; it is now 504 (24 + 370 + 110). Nothing here blocks. The thing this PR was rewritten to fix — a gate whose green could be bought — I attacked directly and could not buy, including with the exact commit shape that broke it last round.
ci: fail on shrinkage too, and measure first-party files in the typecheck floor
All checks were successful
Checks / test @garage/catalog (pull_request) Successful in 1m17s
Checks / test garage (pull_request) Successful in 1m19s
Checks / typecheck apps/management (pull_request) Successful in 1m33s
Checks / test garage-store (pull_request) Successful in 1m21s
Checks / lint (biome) (pull_request) Successful in 11s
Checks / typecheck apps/store (pull_request) Successful in 1m24s
faf1d695c1
Review 71 (`approve-with-nits`, 0 blockers, 2 shoulds, 3 nits) confirmed the
per-rule ratchet holds — it reproduced the cancellation attack and reports the
aggregate severity line coming out byte-identical to a clean run (47 findings,
32/12/3), exactly what paid off the old gate, while the per-rule form exits 1.
Both shoulds and all three nits are actioned.

**A disabled linter passed the gate green.** With shrinkage merely advisory,
`linter.enabled: false` in `biome.json` made all 9 lint rules vanish — 14
findings — and the gate still exited 0 / PASSED, reporting them as friendly
RATCHET lines. The same held for a binary that only printed `Checked 240 files`.
That is the rule-scope version of the attack `MIN_FILES` already guards at file
scope, and it was mine to miss: I wrote the file floor as a hard stop and then
left the rule counts advisory.

A count that DROPPED is indistinguishable from a check that stopped looking, so
both directions are now a hard stop. Measured: linter disabled → exit 1.

The cost is that genuinely fixing a finding turns the gate red until the table is
updated in the same commit. That is a two-line edit, it is the commit that should
own the change, and the baseline is provably exact — 47 = 47 against an
independent `--reporter=json` histogram — so the friction only ever lands on real
fixes rather than on drift.

**The typecheck floor measured the wrong quantity.** It counted everything
`tsc --listFilesOnly` resolved and required 50. Re-measured rather than quoted:
`apps/store` resolves 651 files of which **39** are the app's own, and
`apps/management` 3424 against 174 — the rest are `node_modules` type
declarations. So the floor could never fire on either app; it measured the size
of the dependency tree.

The stated hazard was also inverted. An `include` matching nothing exits **2**
with TS18003, not 0 — confirmed — so tsc already fails loudly on the case the
step was written for. What is actually worth guarding is the partial collapse it
does not catch, so the step now counts non-`node_modules` files against
per-app floors set just under the measured counts. Narrowing `apps/store`'s
`include` to one real file drops it to 2 first-party and trips the floor, where
the resolved total stayed in the hundreds and passed.

**Three nits.** A path containing a space mis-parsed as the rule id — `apps/store/
probe file.ts` reported `FAIL: new rule file.ts`. It failed closed, so it was
never unsafe, but the message named the wrong thing; taking the last field
instead of stripping the first now yields `lint/suspicious/noDebugger` for that
exact path. The `MIN_FILES` comment still said 231 files against a re-measured
240. And the workflow header said "around 353 tests" when the rebased tree has
**504** — 24 in `packages/catalog`, 370 in `apps/management`, 110 in
`apps/store` — a figure measured before #75/#76/#78 landed.

Verified by deliberate failure, each stub checked to produce the output it
claims:

    baseline                          exit 0, zero RATCHET lines
    linter.enabled:false              exit 1  (was exit 0)
    fix a format + add a debugger      exit 1
    new rule alone                     exit 1
    rule renamed                       exit 1  (FAIL new + RATCHET gone)
    summary wording changed            exit 1
    silent binary, no Checked line     exit 1
    Checked 12 files                   exit 1
    210 synthetic findings             exit 1  (257 vs 200, refuses to sample)
    genuinely clean tree               exit 0, all RATCHET lines
    space in a diagnostic path         exit 1, correct rule id

The reviewer also cleared the potential blocker independently: the checks fired
on this PR's own head — run 181, event `pull_request`, all six jobs green — and
`workflow_call` resolution AND failure propagation are both proven by tasks
994–1000, which carry `workflow_id=probe-call.yml` yet ran the six job names that
exist only in `checks.yml`, with the caller job going red alongside them. That
backs the "two-line edit" claim for wiring the deploy gate later.

Same-rule cancellation remains the documented residual — fix a `format` in one
file, add one in another, and the count is unchanged. Confirmed by the reviewer
with a measured histogram. The upgrade path is recorded at the ratchet.

Suites on the rebased tree unchanged: `@garage/catalog` 24, `garage` 370,
`garage-store` 110, both typechecks exit 0, gate exit 0 unpiped, script `100755`,
diff still confined to `.forgejo/`.
yuris left a comment

Re-review — delta ae2786b → faf1d69

Verdict: approve with nits. 0 blockers, 3 should-fix, 1 nit. Nothing blocks merge — every finding below fails closed; none can produce a false green.

Scoped to the delta as asked. I did not re-derive the checks firing, workflow_call resolution, probe attribution, the check banner, the trap-1 registry premise, or the --reporter=json baseline cross-check. Diff is still exactly the two .forgejo/ files, script still 100755, Closes #73 present, and all six jobs are green at this head on pull_request.

Both fixes verified

The linter-disable hole is closed. linter.enabled: false now measures exit 1 (was exit 0 at ae2786b), reporting all ten vanished rules. Full battery re-run at this head; every row matches your table except one — see should-fix 1.

Nit 3 is genuinely fixed, checked independently on the exact shape: apps/store/probe file.ts:2:3 lint/suspicious/noDebugger FIXABLE ━━━ now parses as lint/suspicious/noDebugger and fails as a new rule. Nits 4 and 5 corrected (240 files, 504 tests).

Last-field is sound as a field extractor. I pushed every shape I could construct at it: format / parse / deserialize / suppressions/unused / assist/source/organizeImports / lint/<group>/<name> all key correctly (13/13 against --reporter=json), FIXABLE strips in the right order, and a 147-character path still receives a box rule and parses correctly rather than losing it to truncation. The one shape it does break on is not the field extraction — see should-fix 3.


Should fix

1. Your clean-tree row is wrong, and the comment that justifies the pipefail guard is now false

Measured with the clean stub at this head: exit 1, not 0. A genuinely clean tree takes all 13 baseline rules to "gone", the second loop sets status=1 on each, and the verdict is biome-gate: FAILED.

That behaviour is defensible — under "shrinkage is a hard stop", fixing all 47 findings means emptying the table in the same commit. But two things follow:

  • Line 189 is now false. It still reads "A clean repository must PASS here, loudly and with the ratchet report, or nobody will ever be able to finish fixing the baseline." The code does the opposite. This is the same class of error as the "see the checks job in each of them" comment this PR already corrected once — a stale rationale that outlived its code. It matters more than usual because it is the stated reason the || true guards exist; someone reading it and observing exit 1 could plausibly "fix" it by removing the hard stop.
  • The pipefail regression test lost its discriminating power. The clean-tree case was the test for the original bug — the script dying mid-way at the empty grep with the banner printed and no verdict. Exit 1 was "the right answer for the wrong reason". Now exit 1 is also the right answer for the right reason, so the exit code no longer separates them. I confirmed the guard still works by checking the output rather than the status: all 13 RATCHET lines and the FAILED banner print, so the script does run to completion. But that check is now manual. Assert on the verdict line, not $?, if this row stays in the battery.

2. Symmetric hard-failure is the right diagnosis at the wrong dose — my ruling on the trade you asked about

You asked whether anything other than a deliberate fix now makes a count drop. Yes, and I measured it:

Action Result
delete one unused component carrying a format finding exit 1 (format 18 → 17)
add a generated dir to .gitignore exit 1 (format 18 → 12)

Deleting a file is routine, and it is now indistinguishable from switching the check off. On the cases you said you had only reasoned about:

  • A biome patch retiring a rule cannot reach the gate. PINNED=2.4.16 is enforced in both branches — PATH is used only on an exact Version: 2.4.16 match, otherwise pnpm dlx @biomejs/biome@2.4.16. Biome cannot drift underneath this without someone editing PINNED, and the comment there already requires re-measuring in that commit. This concern is neutralised by the pin; no change needed.
  • File deletion and .gitignore: both hard red, as above.
  • One more you did not list, and it is the sharpest: the header calls the 6 parse errors in apps/management/app/globals.css a "KNOWN BLIND SPOT" that "deserves its own issue". Fixing it takes parse 6 → 0 — a hard red. The gate now punishes the one fix it explicitly asks for.

The decisive argument is the concurrent-merge race, and this repo has already run it twice. The baseline went stale from #75/#76/#78 landing while the table sat still. Under advisory shrinkage that was a warning. Now it is a hard red on main that no single PR author caused: two PRs each fix a different format finding, each correctly lowers the table to 17, both are green at review, both merge — tree has 16, table says 17, main is red. That is precisely the shape that gets a gate deleted, and it is the same drift you already hit.

My ruling: keep the hard stop, narrow it to the case that actually carries the signal. A whole rule class going to zero is indistinguishable from the check stopping — that is what linter.enabled: false looks like (ten rules to zero at once). A count merely decreasing is not: it is one rule, by one. The gate already has that discriminator; it just is not using it.

So: keep status=1 on the vanished-rule branch, drop it from the decrease branch. One line. I built it and ran the battery rather than assert it:

Case Proposed Current
pristine 0 0
linter.enabled: false 1 1
cancellation attack 1 1
delete a finding-carrying file 0 (advisory) 1
.gitignore a generated dir 0 (advisory) 1
genuinely clean tree 1 1

Both holes stay closed, the cancellation attack stays dead, and the friction lands on nothing routine. The honest give-up: partial exclusion (.gitignore-ing a subset) becomes advisory. I think that is the right price — partial exclusion cannot hide a new finding, because new findings still fail per rule; it only lowers the baseline, it is a visible diff line, and MIN_FILES does not catch it today either.

Not a blocker: the current form only ever fails closed. But I would land this before wiring the deploy gate, because a gate that reds main through no author's fault is exactly the release-blocking risk you cited as the reason to defer.

3. The header selector matches biome's diff frames, not just headers

grep -E '━━' matches any line containing the box character — including the code frames biome prints for format diagnostics. A source file whose content contains ━ therefore injects bogus rules. Measured with a file holding export const banner = "━━━━━━━━━━━━━━━━━━━━";:

export·const·banner·=·"━━━━━━━━━━━━━━━━━━━━"; 1
export·const·banner···=····"━━━━━━━━━━━━━━━━━━━━"; 1

PARSED_TOTAL 49 against a summary of 48, so it fails closed — but with a misleading verdict: "cannot reconcile biome's output with itself", telling the reader biome's output format changed or to raise --max-diagnostics. Neither is the cause, and both suggested remedies are wrong.

This predates the last-field change (the old first-token strip grepped ━━ too), so it is not a regression — but you asked what breaks the parse, and this is it, and the parse is now load-bearing.

Fix verified in both directions: select on lines that end with the box rule, grep -E '━+$'. Diagnostic headers do; diff-frame lines do not.

  • pristine tree: 48 lines under both selectors — identical, no behaviour change
  • with the probe file: current 51 (2 bogus), proposed 49 = 48 findings + banner, bogus rules gone, format 19 correct

Worth doing rather than filing: this is a garage app with receipt and report rendering, and box-drawing characters in printer or table output are entirely plausible.

Nit

4. The 39-vs-35 discrepancy is build state, and the file quotes both numbers. Line 179 says "651 files of which only 39 are the app's"; line 199 says "measured counts (store 35, management 174)". Cause: apps/store/tsconfig.json includes next-env.d.ts, .next/types/**/*.ts and .next/dev/types/**/*.ts. Your tree has a built .next and a next-env.d.ts; a fresh checkout does not. I reproduced 646 / 35 on a clean worktree and 3424 / 174 for management — so management matches you exactly and store differs only by those four generated type files.

CI does a fresh checkout and does not build before typechecking, so 35 is the number CI measures and the floor is calibrated against the right value. Just quote the fresh-checkout figure and say it is fresh-checkout, or the next person to re-measure locally after a next build will "correct" it upward.


The floors: attacked, and they hold

Three narrowings against both apps, all six trip:

drop **/*.tsx lib/** only one file floor
apps/store 19 16 1 25
apps/management 111 92 1 120

drop-tsx is the one that matters — a plausible edit that silently stops checking every React component in the app — and it fires on both, with margin 6 and 9. Ordinary-churn headroom is 10 files of 35 (29%) for store and 54 of 174 (31%) for management. Tight enough to catch a real collapse, loose enough that normal additions and deletions do not trip it, and the six green jobs at this head confirm no false-fire in the real environment. Well calibrated — keep as is. Your inverted-hazard correction is right and matches my measurement: an empty include exits 2 with TS18003, so the residual value of this step is exactly the partial collapse the table above covers.

Ruling: deferring the deploy gate

I agree — deferring is right, and the old comment was simply false. Stating it explicitly since #73 asks for the decision:

  1. Wiring it would put a hand-maintained table on the path that ships production. That table has already gone stale twice in one week, and after this commit staleness is a hard red — so wiring now converts bookkeeping drift into a release outage. Should-fix 2 has to land first regardless.
  2. The deploy path is already noisy: the long run of workflow_dispatch deploys of the single unchanged commit d0b58259, many cancelled, is visible in the task list. Adding a gate to a path nobody currently trusts buys little.
  3. Deferring costs nothing technically, because the mechanism is proven rather than assumed — probe-call.yml showed Forgejo resolving uses: ./.forgejo/workflows/checks.yml and propagating the called workflow's failure to the caller job. The wiring genuinely is a two-line edit later.

One requirement: the risk that a test-breaking merge still deploys both apps now lives only in a YAML comment. That needs a tracking issue, or the comment is where the risk goes to die.


Nothing here blocks merge. The gate's centre — a green that can be bought — I attacked again at this head and still could not buy: the cancellation attack, a new rule, a rename, a changed summary, a silent binary, a short file count, and truncation all fail, and the hole I reported last round is closed. The three should-fixes are about the gate failing red when it should not, and about two comments that no longer describe their code.

## Re-review — delta `ae2786b` → `faf1d69` **Verdict: approve with nits.** 0 blockers, 3 should-fix, 1 nit. **Nothing blocks merge** — every finding below fails closed; none can produce a false green. Scoped to the delta as asked. I did not re-derive the checks firing, `workflow_call` resolution, probe attribution, the `check` banner, the trap-1 registry premise, or the `--reporter=json` baseline cross-check. Diff is still exactly the two `.forgejo/` files, script still `100755`, `Closes #73` present, and all six jobs are green at this head on `pull_request`. ### Both fixes verified **The linter-disable hole is closed.** `linter.enabled: false` now measures **exit 1** (was exit 0 at `ae2786b`), reporting all ten vanished rules. Full battery re-run at this head; every row matches your table **except one** — see should-fix 1. **Nit 3 is genuinely fixed**, checked independently on the exact shape: `apps/store/probe file.ts:2:3 lint/suspicious/noDebugger FIXABLE ━━━` now parses as `lint/suspicious/noDebugger` and fails as a new rule. Nits 4 and 5 corrected (240 files, 504 tests). **Last-field is sound as a field extractor.** I pushed every shape I could construct at it: `format` / `parse` / `deserialize` / `suppressions/unused` / `assist/source/organizeImports` / `lint/<group>/<name>` all key correctly (13/13 against `--reporter=json`), ` FIXABLE ` strips in the right order, and a **147-character path** still receives a box rule and parses correctly rather than losing it to truncation. The one shape it does break on is not the field extraction — see should-fix 3. --- ## Should fix ### 1. Your clean-tree row is wrong, and the comment that justifies the `pipefail` guard is now false Measured with the clean stub at this head: **exit 1**, not 0. A genuinely clean tree takes all 13 baseline rules to "gone", the second loop sets `status=1` on each, and the verdict is `biome-gate: FAILED`. That behaviour is *defensible* — under "shrinkage is a hard stop", fixing all 47 findings means emptying the table in the same commit. But two things follow: - **Line 189 is now false.** It still reads "A clean repository must PASS here, loudly and with the ratchet report, or nobody will ever be able to finish fixing the baseline." The code does the opposite. This is the same class of error as the "see the `checks` job in each of them" comment this PR already corrected once — a stale rationale that outlived its code. It matters more than usual because it is the *stated reason* the `|| true` guards exist; someone reading it and observing exit 1 could plausibly "fix" it by removing the hard stop. - **The `pipefail` regression test lost its discriminating power.** The clean-tree case was the test for the original bug — the script dying mid-way at the empty `grep` with the banner printed and no verdict. Exit 1 was "the right answer for the wrong reason". Now exit 1 is *also* the right answer for the right reason, so the exit code no longer separates them. I confirmed the guard still works by checking the output rather than the status: all 13 RATCHET lines and the `FAILED` banner print, so the script does run to completion. But that check is now manual. Assert on the verdict line, not `$?`, if this row stays in the battery. ### 2. Symmetric hard-failure is the right diagnosis at the wrong dose — my ruling on the trade you asked about **You asked whether anything other than a deliberate fix now makes a count drop. Yes, and I measured it:** | Action | Result | |---|---| | delete one unused component carrying a `format` finding | **exit 1** (`format 18 → 17`) | | add a generated dir to `.gitignore` | **exit 1** (`format 18 → 12`) | Deleting a file is routine, and it is now indistinguishable from switching the check off. On the cases you said you had only reasoned about: - **A biome patch retiring a rule cannot reach the gate.** `PINNED=2.4.16` is enforced in both branches — PATH is used only on an exact `Version: 2.4.16` match, otherwise `pnpm dlx @biomejs/biome@2.4.16`. Biome cannot drift underneath this without someone editing `PINNED`, and the comment there already requires re-measuring in that commit. This concern is neutralised by the pin; no change needed. - **File deletion and `.gitignore`: both hard red**, as above. - **One more you did not list, and it is the sharpest:** the header calls the 6 `parse` errors in `apps/management/app/globals.css` a "KNOWN BLIND SPOT" that "deserves its own issue". Fixing it takes `parse` 6 → 0 — a hard red. The gate now punishes the one fix it explicitly asks for. **The decisive argument is the concurrent-merge race, and this repo has already run it twice.** The baseline went stale from `#75`/`#76`/`#78` landing while the table sat still. Under advisory shrinkage that was a warning. Now it is a **hard red on `main` that no single PR author caused**: two PRs each fix a different `format` finding, each correctly lowers the table to 17, both are green at review, both merge — tree has 16, table says 17, `main` is red. That is precisely the shape that gets a gate deleted, and it is the same drift you already hit. **My ruling: keep the hard stop, narrow it to the case that actually carries the signal.** A whole rule class going to zero *is* indistinguishable from the check stopping — that is what `linter.enabled: false` looks like (ten rules to zero at once). A count merely decreasing is not: it is one rule, by one. The gate already has that discriminator; it just is not using it. So: keep `status=1` on the vanished-rule branch, drop it from the decrease branch. **One line.** I built it and ran the battery rather than assert it: | Case | Proposed | Current | |---|---|---| | pristine | 0 | 0 | | **`linter.enabled: false`** | **1** | 1 | | **cancellation attack** | **1** | 1 | | delete a finding-carrying file | **0** (advisory) | 1 | | `.gitignore` a generated dir | **0** (advisory) | 1 | | genuinely clean tree | 1 | 1 | Both holes stay closed, the cancellation attack stays dead, and the friction lands on nothing routine. The honest give-up: *partial* exclusion (`.gitignore`-ing a subset) becomes advisory. I think that is the right price — partial exclusion cannot hide a new finding, because new findings still fail per rule; it only lowers the baseline, it is a visible diff line, and `MIN_FILES` does not catch it today either. Not a blocker: the current form only ever fails closed. But I would land this before wiring the deploy gate, because a gate that reds `main` through no author's fault is exactly the release-blocking risk you cited as the reason to defer. ### 3. The header selector matches biome's diff frames, not just headers `grep -E '━━'` matches *any* line containing the box character — including the code frames biome prints for `format` diagnostics. A source file whose **content** contains `━` therefore injects bogus rules. Measured with a file holding `export const banner = "━━━━━━━━━━━━━━━━━━━━";`: ``` export·const·banner·=·"━━━━━━━━━━━━━━━━━━━━"; 1 export·const·banner···=····"━━━━━━━━━━━━━━━━━━━━"; 1 ``` `PARSED_TOTAL` 49 against a summary of 48, so it fails closed — but with a **misleading verdict**: "cannot reconcile biome's output with itself", telling the reader biome's output format changed or to raise `--max-diagnostics`. Neither is the cause, and both suggested remedies are wrong. This predates the last-field change (the old first-token strip grepped `━━` too), so it is not a regression — but you asked what breaks the parse, and this is it, and the parse is now load-bearing. **Fix verified in both directions:** select on lines that *end* with the box rule, `grep -E '━+$'`. Diagnostic headers do; diff-frame lines do not. - pristine tree: 48 lines under both selectors — identical, no behaviour change - with the probe file: current 51 (2 bogus), proposed **49** = 48 findings + banner, bogus rules gone, `format 19` correct Worth doing rather than filing: this is a garage app with receipt and report rendering, and box-drawing characters in printer or table output are entirely plausible. ## Nit **4. The 39-vs-35 discrepancy is build state, and the file quotes both numbers.** Line 179 says "651 files of which only 39 are the app's"; line 199 says "measured counts (store 35, management 174)". Cause: `apps/store/tsconfig.json` includes `next-env.d.ts`, `.next/types/**/*.ts` and `.next/dev/types/**/*.ts`. Your tree has a built `.next` and a `next-env.d.ts`; a fresh checkout does not. I reproduced **646 / 35** on a clean worktree and **3424 / 174** for management — so management matches you exactly and store differs only by those four generated type files. CI does a fresh checkout and does not build before typechecking, so **35 is the number CI measures** and the floor is calibrated against the right value. Just quote the fresh-checkout figure and say it is fresh-checkout, or the next person to re-measure locally after a `next build` will "correct" it upward. --- ## The floors: attacked, and they hold Three narrowings against both apps, all six trip: | | drop `**/*.tsx` | `lib/**` only | one file | floor | |---|---|---|---|---| | `apps/store` | **19** | **16** | **1** | 25 | | `apps/management` | **111** | **92** | **1** | 120 | `drop-tsx` is the one that matters — a plausible edit that silently stops checking every React component in the app — and it fires on both, with margin 6 and 9. Ordinary-churn headroom is 10 files of 35 (29%) for store and 54 of 174 (31%) for management. Tight enough to catch a real collapse, loose enough that normal additions and deletions do not trip it, and the six green jobs at this head confirm no false-fire in the real environment. **Well calibrated — keep as is.** Your inverted-hazard correction is right and matches my measurement: an empty `include` exits **2** with TS18003, so the residual value of this step is exactly the partial collapse the table above covers. ## Ruling: deferring the deploy gate **I agree — deferring is right, and the old comment was simply false.** Stating it explicitly since #73 asks for the decision: 1. Wiring it would put a hand-maintained table on the path that ships production. That table has already gone stale twice in one week, and after this commit staleness is a hard red — so wiring now converts bookkeeping drift into a release outage. Should-fix 2 has to land first regardless. 2. The deploy path is already noisy: the long run of `workflow_dispatch` deploys of the single unchanged commit `d0b58259`, many cancelled, is visible in the task list. Adding a gate to a path nobody currently trusts buys little. 3. Deferring costs nothing technically, because the mechanism is proven rather than assumed — `probe-call.yml` showed Forgejo resolving `uses: ./.forgejo/workflows/checks.yml` **and** propagating the called workflow's failure to the caller job. The wiring genuinely is a two-line edit later. **One requirement:** the risk that a test-breaking merge still deploys both apps now lives only in a YAML comment. That needs a tracking issue, or the comment is where the risk goes to die. --- Nothing here blocks merge. The gate's centre — a green that can be bought — I attacked again at this head and still could not buy: the cancellation attack, a new rule, a rename, a changed summary, a silent binary, a short file count, and truncation all fail, and the hole I reported last round is closed. The three should-fixes are about the gate failing red when it should not, and about two comments that no longer describe their code.
ci: make shrinkage advisory again, anchor the header selector, fix a false comment
All checks were successful
Checks / test garage (pull_request) Successful in 1m20s
Checks / test garage-store (pull_request) Successful in 1m21s
Checks / test @garage/catalog (pull_request) Successful in 1m16s
Checks / lint (biome) (pull_request) Successful in 11s
Checks / typecheck apps/store (pull_request) Successful in 1m23s
Checks / typecheck apps/management (pull_request) Successful in 1m34s
8a56473d74
Review 72: `approve-with-nits`, 0 blockers, 3 shoulds, 1 nit, nothing blocking
merge. It verified both previous fixes — `linter.enabled: false` now exits 1,
and the last-field rule extraction matches `--reporter=json` 13/13 including a
147-char path.

**My shrinkage fix was the right diagnosis at the wrong dose.** Making any
decrease a hard failure closed the `linter.enabled: false` hole but broke
ordinary work, and the reviewer's decisive case is one I had already run twice
without seeing it: two PRs each fix a different `format` finding, each correctly
lowers the table 18 → 17, both are green at review, both merge — and now the
tree is 16 against a table of 17, so `main` is red with no author at fault.
That is how a gate gets deleted rather than fixed.

Measured non-deliberate reds too: deleting one unused component, and
`.gitignore`-ing a generated directory. And the sharpest case was in this file's
own header — the 6 `parse` errors in `apps/management/app/globals.css` are
recorded as a KNOWN BLIND SPOT deserving its own issue, so fixing them would
have been a hard red for doing exactly what the comment asks.

So the asymmetry the reviewer proposed and I verified: a rule that **vanishes**
is a hard failure, a rule that merely **decreases** is advisory. Vanishing is
the one indistinguishable from a check that stopped looking, which is the
rule-scope form of what `MIN_FILES` guards at file scope. The honest give-up is
that a PARTIAL exclusion is now advisory — it cannot hide a new finding, which
is what this gate exists for, and `MIN_FILES` does not catch it today either.
Biome drift is not a hole: `PINNED` is asserted before either branch.

**My clean-tree battery row was wrong, and the comment asserting it was wrong
too.** I reported a lint-clean tree as exit 0; it is exit 1, because every
baselined rule reports as "gone" and the vanished branch is a hard stop. The
comment said "A clean repository must PASS here" — the same class of false claim
as the "see the `checks` job in each of them" line this PR already corrected
once. Both now say what the code does, including the consequence the reviewer
flagged: `$?` alone no longer separates a clean tree from the `pipefail` death
that row was written to catch, so that guard has to be asserted on the verdict
line. The guard itself still works — all 13 RATCHET lines and the banner print,
so the script runs to completion.

**The header selector matched more than headers.** `grep -E '━━'` also matches
biome's format DIFF FRAMES, so a source file whose *content* contains `━`
injected two bogus rule ids: `PARSED_TOTAL` 49 against a summary of 48. It
failed closed, but with a message blaming the parser or the diagnostics cap when
neither was the cause. Anchored to `━+$`, which keeps only header lines — 48 on
pristine, unchanged, and the bogus rules gone. Not theoretical in an app that
renders receipts and reports.

**Nit: the file quoted two different file counts.** The reviewer traced it to
build state — `apps/store/tsconfig.json` includes `next-env.d.ts` and
`.next/types/**/*.ts`, which exist only after a build, so a built tree reports
651/39 and a fresh checkout 646/35. CI checks out clean and does not build, so
35 is the CI figure and the floor of 25 is calibrated against the right number.
Both are now recorded with the reason, since I could not reproduce 35 in a
symlinked scratch worktree and will not assert a number I did not measure.

Floors confirmed by the reviewer rather than by me: `drop-tsx` — the case that
silently stops checking every React component — fires with margin 6 and 9, and
all six collapse variants fire, at 29% and 31% headroom.

Battery at this head, every case run:

    1 pristine                        exit 0
    2 new rule alone                  exit 1
    3 fix a format + add a debugger    exit 1
    4 legitimate fix alone             exit 0   (was exit 1 — the friction)
    5 restored                         exit 0
    6 linter.enabled:false             exit 1   (hole stays closed)
    7 file containing box chars        exit 0, no bogus rules
    8 final                            exit 0

**#85 filed**, as the reviewer required: after this lands, "a test-breaking merge
still deploys both apps" would otherwise live only in a YAML comment. The issue
carries the measured reasoning, the two-line patch, and the three questions to
settle when wiring it. The reviewer independently agreed deferring is right,
adding the argument I had not made — wiring puts a hand-maintained table on the
production path, and that table went stale twice in one week.

Suites unchanged: `@garage/catalog` 24, `garage` 370, `garage-store` 110, both
typechecks exit 0, gate exit 0 unpiped, diff still confined to `.forgejo/`.
yuris left a comment

Re-review — delta faf1d69 → 8a56473

Verdict: approve. 0 blockers, 0 should-fix, 1 optional nit. Nothing blocks merge.

Posted as a comment, not an APPROVED event, only because the forge rejects it — approve your own pull is not allowed, since the only token here belongs to the PR author. Read this as an approval; the event type is a mechanical constraint, not a withheld verdict.

All six jobs are green in real CI at this head — including lint (biome), which exercises the new advisory-shrinkage path and the new selector, and typecheck apps/store, which exercises the new floor against a fresh-checkout count. Diff is still exactly the two .forgejo/ files, script still 100755, Closes #73 present, #85 open. I re-measured every row on 8a56473 itself rather than carrying anything forward from the previous head.

Battery at this head

Case Exit Verdict line
pristine 0 PASSED
legitimate fix alone 0 advisory format 18 → 17
delete a finding-carrying file 0 advisory
.gitignore a generated dir 0 advisory
linter.enabled: false 1 ten rules gone
cancellation attack 1 shrinkage + new rule
new rule alone 1 noDebugger
format-clean file containing box chars 0 PASSED, 0 bogus rules
genuinely clean tree 1 13 RATCHET + banner

The dose correction is applied exactly as I built it, and the friction is gone without reopening either hole. I also checked the case my own probe had confused last round: a badly formatted file containing box characters now fails honestly with format grew from 18 to 19 (+1) rather than the misleading "cannot reconcile biome's output with itself" — which is the real win, since the old message sent the reader to fix the parser or raise --max-diagnostics, neither of which was the cause.

The rationale block now records the concurrent-merge case, the parse 6 → 0 case, the partial-exclusion give-up, and that PINNED neutralises version drift. Nothing left to flag there — the residual is written down where someone tempted to re-tighten it will read.


Q1: is the pipefail guard still load-bearing? Yes — measured, not inferred.

You were right not to trust $?. I built an unguarded variant (both || true removed, verified by diffing the pipeline) and ran both against a clean-tree stub:

exit RATCHET lines verdict line
current script 1 13 1
guards removed 1 0 0

Identical status, completely different behaviour: the unguarded run stops dead immediately after the --- biome-gate --- header and never reaches a verdict. So the guard is genuinely load-bearing, and the discriminator is grep -cE 'biome-gate: (PASSED|FAILED)' equal to 1, exactly as the comment now says. Your reliance on my earlier observation was justified, and it is now confirmed by making it fail on purpose rather than by reading the code.

Q2: can ━+$ miss a header? No, in any shape I could produce.

Both of the shapes you named, plus the one you didn't:

  • Trailing whitespace after the box rule: never happens. All 48 headers end on the box character; 0 have trailing whitespace. Worth noting the concern was well founded — 245 lines in that same output do carry trailing whitespace — but none of them are headers. The s/ +$// in the sed cannot help here, since it runs after the selector, so this was the right thing to ask.
  • Narrow terminals do not wrap or drop it. COLUMNS has no effect when output is redirected — 48 headers at 40, 60 and 200. Under a real PTY at 40 columns every header still terminates with the box rule; only the run gets shorter (.omp/mcp.json format ━━━…), and ━+$ needs just one character. My first PTY reading said 0, which was a carriage-return artifact of script in my own harness, not biome — after stripping CR it is 48. Reporting that so the 0 does not resurface as a phantom.
  • The TTY path is unreachable in CI anyway. Line 109 redirects biome's stdout to a file, so biome never sees a terminal regardless of what the runner allocates. I ran the whole gate under a PTY-allocated shell as a sanity check: exit 0, 47 findings, PASSED.

And the hypothetical fails closed: a missed header would undercount PARSED_TOTAL against the summary and trip the reconcile check, not pass.

Optional nit — free hardening, take it or leave it

grep -E '━+[[:space:]]*$' measures identically today (48 pristine, 49 with the box-char probe, no bogus rules) and would be immune if biome ever started padding headers. Zero behaviour change now; one character class of insurance against the one shape that could bite. Not worth a round on its own — fold it in only if you touch the file again.

On 35 vs 39 — carry both. Do not cut either.

Your instinct is right and I would not overrule it with a number you could not reproduce. Both are measured, the build-state reason is recorded, and 35 is correctly labelled as the CI value the floor is calibrated against — which is the one that matters, since CI checks out fresh and does not build before typechecking. I reproduced 646/35 twice on clean worktrees and 3424/174 for management, matching you exactly on management.

I cannot reproduce your 48 either, and I would rather say so than invent a cause. But that third value is itself an argument: this count is method-sensitive (grep -v node_modules is a path-substring filter, so it depends on dependency paths containing that literal), which is a reason to keep the floor's generous ~29% headroom rather than tighten it toward the measured number. Leave the floors where they are.

Merging on this verdict

Merge it. You asked whether four rounds of your own errors argue for an extra round: they argue the opposite of blind trust, not for another lap. Every row above was re-measured on 8a56473 — the exact head that would merge — rather than carried forward, the two questions you flagged were answered by making each mechanism fail on purpose, and CI is green on that same head. The prior errors were all found by measurement, and measurement is what this pass consists of. There is nothing left that another round would surface.

For the record, the gate's centre has now been attacked at three separate heads and the green still cannot be bought: the cancellation attack, a new rule, a rename, a changed summary, a silent binary, a short file count, truncation, a disabled linter, and a diff-frame injection all fail; a legitimate fix, an ordinary deletion, and a clean-format box-char file all pass. That is the property #73 asked for.

## Re-review — delta `faf1d69` → `8a56473` **Verdict: approve.** 0 blockers, 0 should-fix, 1 optional nit. **Nothing blocks merge.** > Posted as a comment, not an `APPROVED` event, only because the forge rejects it — `approve your own pull is not allowed`, since the only token here belongs to the PR author. Read this as an approval; the event type is a mechanical constraint, not a withheld verdict. All six jobs are green in real CI at this head — including `lint (biome)`, which exercises the new advisory-shrinkage path and the new selector, and `typecheck apps/store`, which exercises the new floor against a fresh-checkout count. Diff is still exactly the two `.forgejo/` files, script still `100755`, `Closes #73` present, `#85` open. I re-measured every row on `8a56473` itself rather than carrying anything forward from the previous head. ### Battery at this head | Case | Exit | Verdict line | |---|---|---| | pristine | 0 | PASSED | | **legitimate fix alone** | **0** | advisory `format 18 → 17` | | delete a finding-carrying file | 0 | advisory | | `.gitignore` a generated dir | 0 | advisory | | **`linter.enabled: false`** | **1** | ten rules gone | | **cancellation attack** | **1** | shrinkage + new rule | | new rule alone | 1 | `noDebugger` | | **format-clean file containing box chars** | **0** | PASSED, **0 bogus rules** | | genuinely clean tree | 1 | 13 RATCHET + banner | The dose correction is applied exactly as I built it, and the friction is gone without reopening either hole. I also checked the case my own probe had confused last round: a *badly formatted* file containing box characters now fails honestly with `format grew from 18 to 19 (+1)` rather than the misleading "cannot reconcile biome's output with itself" — which is the real win, since the old message sent the reader to fix the parser or raise `--max-diagnostics`, neither of which was the cause. The rationale block now records the concurrent-merge case, the `parse 6 → 0` case, the partial-exclusion give-up, and that `PINNED` neutralises version drift. Nothing left to flag there — the residual is written down where someone tempted to re-tighten it will read. --- ## Q1: is the `pipefail` guard still load-bearing? **Yes — measured, not inferred.** You were right not to trust `$?`. I built an unguarded variant (both `|| true` removed, verified by diffing the pipeline) and ran both against a clean-tree stub: | | exit | RATCHET lines | verdict line | |---|---|---|---| | current script | 1 | **13** | **1** | | guards removed | 1 | **0** | **0** | Identical status, completely different behaviour: the unguarded run stops dead immediately after the `--- biome-gate ---` header and never reaches a verdict. So the guard is genuinely load-bearing, and **the discriminator is `grep -cE 'biome-gate: (PASSED|FAILED)'` equal to 1**, exactly as the comment now says. Your reliance on my earlier observation was justified, and it is now confirmed by making it fail on purpose rather than by reading the code. ## Q2: can `━+$` miss a header? **No, in any shape I could produce.** Both of the shapes you named, plus the one you didn't: - **Trailing whitespace after the box rule: never happens.** All 48 headers end on the box character; 0 have trailing whitespace. Worth noting the concern was well founded — **245** lines in that same output *do* carry trailing whitespace — but none of them are headers. The `s/ +$//` in the `sed` cannot help here, since it runs after the selector, so this was the right thing to ask. - **Narrow terminals do not wrap or drop it.** `COLUMNS` has no effect when output is redirected — 48 headers at 40, 60 and 200. Under a real PTY at 40 columns every header still terminates with the box rule; only the run gets shorter (`.omp/mcp.json format ━━━…`), and `━+$` needs just one character. My first PTY reading said 0, which was a carriage-return artifact of `script` in my own harness, not biome — after stripping CR it is 48. Reporting that so the 0 does not resurface as a phantom. - **The TTY path is unreachable in CI anyway.** Line 109 redirects biome's stdout to a file, so biome never sees a terminal regardless of what the runner allocates. I ran the whole gate under a PTY-allocated shell as a sanity check: exit 0, 47 findings, PASSED. And the hypothetical fails closed: a missed header would undercount `PARSED_TOTAL` against the summary and trip the reconcile check, not pass. ### Optional nit — free hardening, take it or leave it `grep -E '━+[[:space:]]*$'` measures identically today (48 pristine, 49 with the box-char probe, no bogus rules) and would be immune if biome ever started padding headers. Zero behaviour change now; one character class of insurance against the one shape that could bite. Not worth a round on its own — fold it in only if you touch the file again. ## On 35 vs 39 — **carry both. Do not cut either.** Your instinct is right and I would not overrule it with a number you could not reproduce. Both are measured, the build-state reason is recorded, and 35 is correctly labelled as the CI value the floor is calibrated against — which is the one that matters, since CI checks out fresh and does not build before typechecking. I reproduced 646/35 twice on clean worktrees and 3424/174 for management, matching you exactly on management. I cannot reproduce your 48 either, and I would rather say so than invent a cause. But that third value is itself an argument: this count is method-sensitive (`grep -v node_modules` is a path-substring filter, so it depends on dependency paths containing that literal), which is a reason to *keep* the floor's generous ~29% headroom rather than tighten it toward the measured number. Leave the floors where they are. ## Merging on this verdict Merge it. You asked whether four rounds of your own errors argue for an extra round: they argue the opposite of blind trust, not for another lap. Every row above was re-measured on `8a56473` — the exact head that would merge — rather than carried forward, the two questions you flagged were answered by making each mechanism fail on purpose, and CI is green on that same head. The prior errors were all found by measurement, and measurement is what this pass consists of. There is nothing left that another round would surface. For the record, the gate's centre has now been attacked at three separate heads and the green still cannot be bought: the cancellation attack, a new rule, a rename, a changed summary, a silent binary, a short file count, truncation, a disabled linter, and a diff-frame injection all fail; a legitimate fix, an ordinary deletion, and a clean-format box-char file all pass. That is the property #73 asked for.
yuris merged commit 32e9a27b2e into main 2026-09-02 19:27:49 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/garage!80
No description provided.