fix(lint): the architecture gate fails closed on an edge it cannot see#120
Merged
Conversation
28 of 136 source files matched no glob in architecture.config.json, so they joined no module group and no domain/layer/plane rule applied to them. The storage and streams packages were unclassified entirely, including their entrypoint and server files, which boot the deployed runtime. Six workspace specifiers were also missing from tsconfig.depcruise.json. Those resolve through node_modules to the package exports map, which points at built dist; dist is excluded, so the cruiser dropped the edge and could not rule on it. Aliasing them to source makes 12 previously invisible edges visible. All of them turn out to be clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
A new file under packages/ joined no module group and no rule applied to it, so a plane violation in it passed with a green gate. That was the default for every file added to the repo. A missing paths entry did the same thing by a different route: the import resolved to excluded dist and the edge vanished. lint-architecture-coverage.mjs now fails when a source file is unclassified or a workspace specifier is unaliased. It is a check rather than a default classification because neither can be guessed: a plane is a judgement about when code runs, and a file wrongly defaulted to shared would also weaken the rules applied to files importing it (ADR-0005). It imports the cruisers own exclude list and shares its normalizeGlob, so the two cannot disagree about what is checked. The cruiser also cruised files its exclude list meant to skip: .test. missed .vitest.ts and vitest.config missed vitest.conformance.deployed.config.ts. Proven with a deliberate control: a new public file importing an execution module is red when unclassified and red on the plane rule once classified. Removing one paths entry turns the cruiser green on that same real violation, which the coverage check catches. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
commit: |
wmadden
approved these changes
Jul 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
pnpm lint:depsdid not guard newly added files. A new file underpackages/matched no glob inarchitecture.config.json, so it joined no module group and no domain/layer/plane rule applied to it. A layering violation landed with a green gate. This makes the gate fail closed and proves the fix fires.Found during the SPI-inversion project (#117), which fixed classification for its own new file and deliberately left the mechanism alone.
The audit found more than expected
28 of 136 source files (21%) had no architecture rule applied to them — not just hypothetical future files. The
storageandstreamspackages were unclassified entirely, includingstorage-server.ts(callsBun.serve) and*-entrypoint.ts— execution-plane runtime code with zero plane rules. Also unclassified: 6 files in1-extensions/target, and 4 public files incomposer-prisma-cloud/src.Classifying them surfaced no hidden violations. The code was fine; only the lint was blind.
Cause #2 works differently than reported
A missing
pathsentry doesn't fail to resolve. It resolves successfully through the node_modules symlink to the package'sexportsmap → builtdist→ which the cruiser excludes. The edge is silently dropped.This matters: it means a dependency-cruiser
couldNotResolverule does not catch it (tried, discarded). Six specifiers were affected — including@internal/storageand@internal/streams— and aliasing them made 12 previously invisible edges visible (540 → 552 deps). All clean.Why a check, not a glob default
A glob-based default classification was considered and rejected.
storage/src/testing.tsre-exportsBun.serveboot code, so it is execution — but a path-based default would call itshared. A wrong default is worse than no rule: a file mislabeledsharedalso makescontrol → itedges pass when they should fail. That is a false-negative factory, and it contradicts ADR-0005 (we don't guess).Both causes are the same shape — an edge the cruiser cannot see — so both live in one check,
scripts/lint-architecture-coverage.mjs:packages/must match a glob inarchitecture.config.json.tsconfig.depcruise.json.The check imports the cruiser's own
excludelist and shares itsnormalizeGlob. If it used its own copies it could report a file as classified while the cruiser still applied no rule.Proven with a control
controlpathsentry, violation still correctly classifiedThe third is the decisive one: it proves cause #2 is independently sufficient. With a real, correctly-classified
control → executionviolation present, the cruiser alone says✔ no dependency violations found.The control caught a real bug in this PR. The first version of the check used
git ls-files, which lists only tracked files — so a brand-new file was invisible to it. It printed "135 files checked" and passed while the probe sat right there: the exact blind spot being closed, reintroduced in the fix for it. Now uses--others --exclude-standard. A fix that can't be shown to fire is the same bug again.All controls removed. 10 unit tests lock the behavior in.
Also fixed
The cruiser cruised files its
excludelist meant to skip:\.test\.missed.vitest.ts, andvitest\.configmissedvitest.conformance.deployed.config.ts— a third instance of the same enumeration bug.tsconfig.depcruise.jsonis now a lint-staged trigger, since dropping apathsentry hides violations.Review focus
The 28 plane classifications are judgement calls, made using the
cronpackage as precedent. Worth your eyes, particularlystorage-testing.ts/streams-testing.tsasexecution(they re-export server boot code) — two independent agents disagreed on exactly that call.Verification
pnpm lint:deps— green (213 modules, 552 deps, 135 files classified, 171 specifiers checked)pnpm test:scripts— 121/121 pass🤖 Generated with Claude Code