Skip to content

fix(layering): stop double-reporting contracts-authority violations - #1746

Merged
thymikee merged 4 commits into
mainfrom
claude/dedupe-contracts-authority-check-90624a
Aug 11, 2026
Merged

fix(layering): stop double-reporting contracts-authority violations#1746
thymikee merged 4 commits into
mainfrom
claude/dedupe-contracts-authority-check-90624a

Conversation

@thymikee

@thymikee thymikee commented Aug 11, 2026

Copy link
Copy Markdown
Member

Three findings, all in the layering guard's wiring. Each commit stands alone.

1. fix — a rule was reported twice

main() spread checkContractsImplementationAuthority(sources) twice, so every R11 contracts-implementation-authority finding was printed and ::error-annotated twice: inflated headline count, duplicated file:line rows, and GitHub double-annotating the same line. Pre-existing.

Proof: planted one violation (setTimeout in packages/contracts/src/back-mode.ts) and ran the guard on both sides. The probe was reverted; it is not part of this PR.

violations annotations
before 2 2
after 1 1

The pair was byte-identical in file, line, and message — double-reporting, not two overlapping rules.

2. refactor — rules are registered, not hand-spread

The duplicate survived because nothing enumerated the rules: main() held a hand-written array of spreads, and every policy test calls its rule function directly — bin-alias-fast-path.test.ts:1 says as much: "independently of the check.ts wiring that turns it into a violation." A duplicated entry is the mild failure; a dropped one retires a rule while the run still prints OK.

Rules now live in LAYERING_RULES, a Readonly<Record<LayeringRuleId, LayeringRule>> over a shared LayeringContext, executed once via Object.values(...).flatMap(...). Call sites and order are unchanged, so grouped output and the success line are byte-identical.

failure what stops it
duplicate registration unrepresentable — an object holds a key once; oxlint no-dupe-keys rejects it at source
dropped wiring missing key in Record<LayeringRuleId, …>, plus the catalog test
uncatalogued rule catalog test

One small regression test remains, through the production interface. Note: tsconfig.json's include is ["src", "test", "scripts/help-conformance-command-validator.ts"], so scripts/layering/** is not typechecked — true of every policy there. The Record's exhaustiveness is an editor signal, not a CI gate, which is why the catalog assertion is load-bearing rather than belt-and-braces. Closing that properly needs a scripts/ typecheck project; too big for this PR.

Mutation-verified: dropping an entry ✖, duplicating a key ✖ (oxlint), registering an uncatalogued rule ✖, restored ✔.

This replaces an earlier AST-based wiring guard after review. That version reconstructed the array's shape from TypeScript syntax, so a const-defined or aliased rule was invisible to it and a helper named checkX was a false positive — naming and syntax became part of the interface, all to detect a mistake rather than prevent it. Net −133 LOC against that approach.

3. test — two suites had never run

Found while verifying the above: check:layering named its 14 test files one at a time, so registration was manual — and twice it was missed. Both halves of the R16 record cutover shipped with tests that have never run:

dormant suite tests
record-runtime-mechanics-policy.test.ts 2
record-runtime-registry-policy.test.ts 1

Their policies are live in the guard; only the tests were dormant, and all three pass — the coverage was simply never collected. Fixed by globbing the directory the way mutation:test already globs its own, which makes the filesystem the enumeration.

Same defect as #1, one level up: a hand-maintained list with nothing checking it against reality.

Reviewer notes

  • refactor(layering): one parametrized runtime-command-cutover gate (ADR 0019 §8) #1745 rewrites this same check.ts assembly and the package.json test command. Its rebase must preserve the single registration and the directory glob. The catalog test fails loudly if the merge drops a rule.
  • Out of scope, filed separately: the guard's rule-ID namespace has two collisions — R11 is both contracts-implementation-authority and package-boundaries; R13 is both device-inventory-cutover and platform-package-substrate. Every other number is 1:1. The success line's "R11 holds 17 workspace package(s)…" describes package-boundaries, so reading it to learn what R11 covers gives the wrong rule.
  • Also noticed: scripts/integration-progress-model.test.ts is run by nothing (passes when run). No glob to fold it into; needs a deliberate home.

main()'s violation list spread checkContractsImplementationAuthority(sources)
twice, so every R11 contracts-implementation-authority finding was printed and
::error-annotated twice on a red run — inflating the headline violation count
and producing duplicate GitHub annotations on the same file:line.

Verified by planting a `setTimeout` call in a contracts production source: the
rule reported 2 identical violations before and 1 after, with the extra
annotation gone. `pnpm check:layering` stays green (136/136 policy tests).

Nothing in the suite covers main()'s assembly of the violation list — the
policy tests all call their rule functions directly — so neither a duplicated
nor a dropped entry there is currently detectable.
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 2.18 MB 2.18 MB 0 B
JS gzip 712.0 kB 712.0 kB 0 B
npm tarball 838.3 kB 838.1 kB -120 B
npm unpacked 2.92 MB 2.92 MB -649 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 25.3 ms 25.0 ms -0.3 ms
CLI --help 61.2 ms 60.9 ms -0.2 ms

Top changed chunks: no changes in the largest emitted chunks.

The duplicate this branch removed survived because nothing enumerates the
guard's rules: main()'s violation list is hand-written, and the per-policy
tests call their rule functions directly, never seeing the wiring. A lost
spread is the dangerous version of the same gap — the rule stops being
enforced and the run still prints OK.

Make the file's own bindings the oracle: every in-scope `check*` value, local
or imported, must be spread into main()'s violation list exactly once. That
covers both directions plus a third case — a policy written and never wired
in. Fails closed if main() or the array is renamed, so the instrument cannot
pass by finding nothing.

Test-only rather than an R17 inside the guard: a self-referential rule is
defeated by dropping its own spread, which is exactly the failure it exists
to catch.

Verified by mutating the real check.ts in both directions (re-planting the
duplicate, then dropping checkZeroDepJobs) — each turns the run red, and the
restored file is green at 143/143.
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-11 18:22 UTC

check:layering named its 14 test files one by one, so adding a policy test
meant remembering to register it — and twice nobody did. Both halves of the
R16 record cutover shipped with tests that have never run:

  scripts/layering/record-runtime-mechanics-policy.test.ts  (2 tests)
  scripts/layering/record-runtime-registry-policy.test.ts   (1 test)

Their policies are live in the guard; only the tests were dormant. All three
pass, so nothing had rotted — the coverage was simply never being collected.

Glob the directory the way mutation:test already globs its own, which makes
the filesystem the enumeration and retires the registration step. 143 -> 146
tests, still green.

This is the same defect as the duplicate spread this branch opened with, one
level up: a hand-maintained list with nothing checking it against reality.
@thymikee

Copy link
Copy Markdown
Member Author

Reviewed exact head 390d13a6d611066ca808bb89fbfd965e9872cd34.

P2 — replace the AST wiring guard with a construction that makes duplicate registration impossible. Keep the one-line dedupe and the scoped scripts/layering/*.test.ts discovery change: the glob is appropriately limited to policy suites and correctly wakes the two dormant record suites. The planted mutations are also valid for the current syntax—they catch both restoring the duplicate and dropping checkZeroDepJobs.

The 112-LOC check-wiring.ts, however, creates a parallel compiler/schema for one local array shape. It recognizes only top-level FunctionDeclarations and imports whose local names match check[A-Z]; a const-defined or aliased/non-check rule is invisible, while a helper named checkX becomes a false positive. That makes naming and syntax part of the interface without making the failure class impossible.

Please replace it with a typed, keyed rule registry plus a uniform adapter (for example, each entry consumes a shared layering context and returns violations), then execute the registry values once. Unique object keys make duplicate registration impossible, exhaustiveness can be checked against the rule-id catalog, and lint/typechecking exposes dropped wiring without reconstructing TypeScript syntax. Keep one small regression through that production interface.

#1745 rewrites the same check.ts assembly and package.json test command, so its expected post-merge rebase must preserve both the single registration and directory glob. Layering, lint, fallow, typecheck, and the completed device lanes are green; Coverage, Integration, FreeRange, and macOS remain pending.

Replaces the AST wiring guard with a construction that cannot express the
defect, per review on #1746.

The parser was the wrong instrument: it reconstructed one array's shape from
TypeScript syntax, so it only recognised top-level function declarations and
imports whose local name matched /^check[A-Z]/. A const-defined or aliased
rule was invisible to it, a helper named checkX was a false positive, and
naming and syntax became part of the interface — all to detect a mistake
rather than prevent it.

Rules now live in a keyed table over a shared context, executed once via
Object.values. An object cannot hold a key twice, so double registration is
unrepresentable rather than merely detected, and oxlint's no-dupe-keys
rejects the attempt at the source. LayeringRuleId makes a missing key a type
error, and LAYERING_RULE_IDS gives the catalog to check exhaustiveness
against. Call sites and order are unchanged, so grouped output and the
success line are byte-identical.

One regression test remains, through the production interface: scripts/ is
outside tsconfig.json's `include`, so the Record's exhaustiveness is an
editor signal rather than a CI gate, and the catalog assertion is what fails
the build when wiring goes missing.

Verified by mutation: dropping an entry and registering an uncatalogued one
both fail the test, a duplicated key fails oxlint, and re-planting the
original contracts violation reports it exactly once. Net -133 LOC.
@thymikee

Copy link
Copy Markdown
Member Author

P2 addressed in 01da41ec2 — the AST guard is gone, replaced by a keyed rule table.

Your read of check-wiring.ts was right, including a case I hadn't considered: a rule defined as a const or bound under a non-check alias was invisible to the matcher, so the instrument had a silent false-negative in exactly the direction that matters. Detecting the mistake was the wrong goal.

What's there nowLAYERING_RULES, a Readonly<Record<LayeringRuleId, LayeringRule>> over a shared LayeringContext, executed once via Object.values(...).flatMap(...). Call sites and order are unchanged, so grouped output and the success line are byte-identical. Net −133 LOC (−220/+87).

failure what stops it
duplicate registration unrepresentable — an object holds a key once; oxlint no-dupe-keys rejects it at source
dropped wiring missing key in Record<LayeringRuleId, …>, plus the catalog test
uncatalogued rule catalog test

One correction worth your attention. Typecheck can't expose dropped wiring here: tsconfig.json's include is ["src", "test", "scripts/help-conformance-command-validator.ts"], so nothing under scripts/layering/** is typechecked — true of every existing policy in that directory, not just this file. The Record exhaustiveness is an editor signal, not a CI gate. That's why I kept the catalog assertion rather than treating the test as belt-and-braces; it is the only thing that fails the build when a key goes missing. Happy to add a scripts/ project to the typecheck lane as a follow-up if you'd like that gap closed properly — it's a bigger change than this PR should carry.

Lint does gate the duplicate direction, verified directly: a planted duplicate key errors under oxlint . --deny-warnings.

Mutation-verified, since the construction is only worth what it catches:

mutation result
drop 'zero-dep-job-closure' entry ✖ test fails
duplicate the 'back-edges' key ✖ oxlint no-dupe-keys
register an uncatalogued 'ghost-rule' ✖ test fails
re-plant the original setTimeout in contracts 1 violation, 1 annotation
restored ✔ green, 141/141, guard OK

Dedupe and the scoped glob are untouched, as you asked. Noted on #1745 — its rebase needs to land the single registration and keep scripts/layering/*.test.ts; the catalog test will fail loudly if the merge drops a rule, which is the point.

🤖 Addressed by Claude Code

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 11, 2026
@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed exact head 01da41e. The prior AST parallel-compiler issue is fixed: the syntax scanner is deleted, and production now constructs layering enforcement through one typed, keyed LAYERING_RULES registry over a shared context, executed once via Object.values(...).flatMap(...). Duplicate wiring is structurally impossible (and rejected by no-dupe-keys); the catalog/registry production exports are checked directly. Rule order and calls preserve the one-line contracts-authority dedupe, and check:layering still discovers scripts/layering/*.test.ts, including the formerly dormant record suites. The drop/duplicate/uncatalogued planted checks are non-vacuous. No source finding remains.

Stack note: #1745 currently rewrites the same assembly and package script, so its eventual rebase must preserve this registry and glob while registering its consolidated cutover rule. Current head is mergeable, but authoritative CI is still in progress; code-review clean, with merge readiness gated on those checks. No label yet.

@thymikee
thymikee merged commit 057ab1c into main Aug 11, 2026
29 of 30 checks passed
@thymikee
thymikee deleted the claude/dedupe-contracts-authority-check-90624a branch August 11, 2026 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant