fix(fabrika): scope build check's unvalidated list to the surface that ran (#5288) - #5300
Conversation
…t ran (#5288) `unvalidated` was computed from the third file class alone, so `--surface code` over `["a.ts", "README.md"]` greened with an empty list: the markdown had a validator, just not the one that ran, and an empty list affirmatively reads as "nothing uncovered". `--surface plan` did the same to code files. `notCoveredBy(surface, files)` reports what this surface's validators did not read, which closes both cases with one rule. Disclosing is not validating — the code surface names the markdown it skipped and still does not scan it, because a surface that guessed at file classes is what the anchor exists to refuse. The `DiffClasses` bucket is renamed `unvalidatable`, so the tree-level fact and the per-verdict one no longer share a word. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No preview deploy
|
|
review-code: PASS @ efd6fae — merge-ready Verified PR #5300 against the acceptance criteria of #5288, one at a time:
Independent verification — re-run, not taken on the body's word
Ruling on the fork, and on the disagreement with the triaged body
Invariant check — is Within the diff's reach, yes: an empty list now requires that every changed file matched a class this surface's validators open. Two residual paths where a green can still under-report — both verified pre-existing at the parent commit, neither introduced nor worsened here, both outside the ACs:
Neither blocks this PR. Both deserve a follow-up — #5301 is the natural home or a sibling to it. Rename (the root-cause half) — complete. Deviations audit — literal
Run-evidence bundle: PENDING for head Read the PR head (§HEAD): all files under review sourced from §CP: not-control-plane, proven ordinary on both axes ( All criteria pass. This PR is merge-ready. review-code does not merge — Verdict-written: 2026-08-10T06:45:05Z |
|
review-skill: PASS @ efd6fae — merge-ready Reviewed-head: @ efd6fae Skill-class gate over Acceptance criteria (of #5288, as they land on the contract surface)
Skill rigor
Advisory — non-blocking, not a condition of this PASS
I am not failing on it. The substance is documented twice inside the same file (the body text and a dedicated #5288 grounding entry), so the contract itself is correct and a reader of the body is not misled — only a reader who stops at the header. That is the same disposition the stale Fork and triage-disagreement ruling — both are ruled in full on the Deviations — literal Read the PR head (§HEAD): all files under review sourced from §CP: not-control-plane, proven ordinary on both axes — All criteria and rigor checks pass. This PR is merge-ready. review-skill does not merge — Verdict-written: 2026-08-10T06:46:07Z |
fabrika build check --surface codereturned green over a diff like["a.ts", "README.md"]while listing nothing as skipped. The markdown was classified, so it never tripped the unclassified-diff refusal, but no code runner reads markdown —lint:worktreefilters.mdout by extension — and an emptyunvalidated: []affirmatively reads as "nothing uncovered".--surface plandid the same to code files. A green now lists every changed file this surface's validators did not read, so an empty list means the whole diff was checked and nothing weaker.Fixes #5288
The fork, and which side this takes
#5287's disclosure named two candidate directions — scan both classes, or list the unscanned class — and this takes the second: disclose, do not widen.
--surfaceis an anchor, not a classifier. Running the markdown validators under--surface codewould make the surface guess at file classes, which the anchor exists to refuse, and it has no coherent mirror: covering the symmetric--surface plancase that way would meanbuild check --surface planspawningpnpm typecheck. Acceptance criterion 2 asks for one mechanism across both cases, and per-surface disclosure is the only shape that gives one. Acceptance criterion 1 already permits either ("validated or explicitly named as not covered").This is also what the field was documented to be.
contract.mdsaidunvalidated"lists the changed files this verdict does not cover" and then narrowed it to the third class; the implementation followed the narrowing. Scoping the computation to the surface makes the code match the sentence — no new key, no new exit code, no third vocabulary.What changed
notCoveredBy(surface, files)reports the changed files this surface's validators do not open, off a smallCOVERSmap. It is a superset of the third class, and the extra members are markdown undercodeand code underplan— one rule, both cases.packages/fabrika-cli/src/build/check-verb.ts.unvalidatedand the stderr scope note both read from it. The note names the surface now (--surface code does not validate — NOT covered by this verdict: README.md), because "no surface validates" is no longer true of everything in the list.DiffClasses.unvalidatedis renamedunvalidatable. The tree-level fact (no surface covers this file, which drives exit22) and the per-verdict one shared a word, and that collision is how a markdown file sat outside the disclosure while the field's own docs said otherwise.contract.mdis amended to match: the scoped meaning, the disclosure-is-not-validation rule and its remedy (a second run, not a wider surface), the new scope-note row, an example with both classes, and the build check --surface code greens a diff whose markdown it never scans #5288 grounding line.Exit codes are untouched — no refusal is added, and
22keeps its exact trigger.Regression coverage
Three
runChecktests pin the defect, and they were proven to fail against the pre-fix computation by restoring it (const unvalidated = classifyDiff(files).unvalidatable;) and re-running: all three failed withexpected [] to deeply equal [...], which is the defect's exact signature.--surface codeover["apps/web/src/App.tsx", "README.md"]→ green withunvalidated: ["README.md"]and the stderr line asserted verbatim.--surface planover["apps/web/src/App.tsx", "plans/epic.md"]→ green withunvalidated: ["apps/web/src/App.tsx"], andpnpm typecheck --forceasserted not spawned, so the mirrored case is disclosed rather than validated.--surface coderun stays green over markdown carrying a machine-local path — the prose validators would red it, which is what proves the file was named but not scanned.Plus five
notCoveredByunit tests, including the empty-only-when-fully-read case and diff-order reporting.Verification
pnpm typecheck— 31/31 tasks, 0 cached, resolved against this worktree.pnpm lint:worktree— clean.pnpm vitest runinpackages/fabrika-cli— 196 files, 2833 tests, all passing.Deviations
Reviewer-suggested alternative declined — Said: triage's read on build check --surface code greens a diff whose markdown it never scans #5288 was that disclose-only "leaves mixed diffs permanently unscannable for leaks and dead links, so it is honest but not sufficient on its own." Did: shipped disclose-only anyway. Why: the gap triage names is real but it is a different defect —
surfaceMismatchrefuses--surface prosewhenever the diff holds any code file, so a mixed diff has no runnable prose surface at all. That is a routing rule, not the disclosure bug this issue scopes, and fixing it here would change which surfaces a diff admits, which no acceptance criterion asks for. Acceptance criterion 2's "same mechanism" requirement also rules out the validate-both shape, since its mirror would have--surface planrunningpnpm typecheck. Disposition: follow-up build check has no runnable surface that scans a mixed code+markdown diff's prose #5301 filed for the unscannable-mixed-diff routing; for the reviewer to judge whether it belongs in this PR instead.Widened the fix shape — Said: the issue asks that the skipped class be named. Did: also renamed the
DiffClassesbucketunvalidated→unvalidatable, touching the exported interface fix(fabrika): refuse a build check diff no surface validates, never green it #5287 landed hours ago. Why: leaving both the structural bucket and the per-verdict list calledunvalidatedin one file reproduces the exact confusion that produced this bug — a reader grepsclassifyDiff().unvalidatedand concludes the JSON field means the same thing. Disposition: no action needed; contained tocheck-verb.tsand its test, no other consumer in the repo.Known defect left unfixed — Said:
packages/fabrika-cli/src/build/codes.ts:7still documents the group as adding its own12-21, which is stale now that22and23are allocated; fix(fabrika): refuse a build check diff no surface validates, never green it #5287 left it for the next lane touching that file. Did: left it. Why: this fix allocates no exit code, socodes.tsis not in the diff, and opening it solely for a comment would widen the reviewed surface past the issue. Disposition: for the reviewer to judge — still owned by the next PR that editscodes.ts.No ADR — Said: ADR
0265was pre-assigned for this lane. Did: wrote none, leaving0265unused. Why: the change makes an existing documented contract true rather than deciding anything new;contract.mdcarries the reasoning at the surface it governs. Disposition: no action needed —0265is free for another lane.