fix(catalog): settle the two guard collisions before Phase E inherits them - #89
Merged
Conversation
… them Both were found by implementation sessions, both were worked around rather than resolved, and both would have recurred in Phase G. Maintainer decisions 2026-08-05. D9 - reading an installed dependency's version. FR-029/T063 require the picomatch version be read at runtime and never transcribed; ADR-0013/FR-002 forbid a dynamic loader and the guard bans import.meta.resolve anywhere in the adapter. Read naively those collide. They do not: the distinction the guard protects is dynamic MODULE LOADING, not filesystem access. Walking up to node_modules/picomatch/package.json and reading its version field invokes no resolver, imports no module, and cannot load code, while still taking the value from the installed artifact rather than a literal in our source. Recorded as package-boundary.md §6 with the distinction tabulated, because to the session that implemented it this looked like a route around the guard rather than a path through it, and the next reader deserves the reasoning rather than an inference from what happened to be committed. The ADR-0016 obligation stays attached: a read that silently returns undefined and one that returns 4.0.5 are indistinguishable from a green suite alone. D10 - source-scan false positives. The scans match `from '...'` and `import '...'` textually rather than resolving them, so a file that STATES a rule is indistinguishable from one that BREAKS it. This fired on the string 'bulk-import' - ADR-0015's own plugin name - and on prose ending in "from " before a quote. Two sessions renamed around it, which leaves the trap armed for the next writer. EXCLUDED_FROM_SCAN now carries the consumer's two boundary guards, each of which must name the very thing it forbids: the schema file it pins by hash, and the adapter package it proves is never imported. The patterns are deliberately NOT loosened - a scan that misses a real edge is worse than one that occasionally over-matches, and the exclusion list is the intended relief valve. The pinned-set assertion caught this change and failed until updated deliberately, which is the guard working exactly as designed; it now reads five files and records why the two were added. Gates: bun test 1504 pass / 0 fail, typecheck clean, check:deps ok, check:freeze-hashes ok over both frozen trees, adr lint 20 records 0/0. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Documents dependency-version discovery and expands source-scan exclusions for consumer boundary guards.
Changes:
- Defines permitted runtime manifest reading for
picomatch. - Adds and pins two consumer guard exclusions.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
specs/010-catalog-backstage/contracts/package-boundary.md |
Records dependency-version lookup decision. |
packages/adapters/catalog-backstage/test/source-scan.ts |
Extends scan exclusions and rationale. |
packages/adapters/catalog-backstage/test/no-dynamic-loader.test.ts |
Pins the expanded exclusion set. |
Suppressed comments (1)
packages/adapters/catalog-backstage/test/source-scan.ts:66
package-boundary.md§4 is about dependency allowlists, not these source-scan exclusions, so this reference directs maintainers to unrelated rationale. Remove it or replace it with the actual exclusion-policy section.
// never imported. See package-boundary.md §4.
Comment on lines
+67
to
+68
| 'packages/catalog-envelope/test/no-core-schema-change.test.ts', | ||
| 'packages/catalog-envelope/test/no-adapter-import.test.ts', |
| * false positive go away — a scan that misses a real edge is worse than one | ||
| * that occasionally over-matches, and this list is the intended relief valve. | ||
| * | ||
| * @see specs/010-catalog-backstage/contracts/package-boundary.md §4 |
|
|
||
| --- | ||
|
|
||
| ## §6. Reading an installed dependency's version — permitted, and why it is not loader behaviour |
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.
Two guard collisions, both found by implementation sessions, both worked around rather than resolved, and both certain to recur in Phase G. Maintainer decisions 2026-08-05, recorded so the next reader gets the reasoning rather than an inference from what happened to be committed.
D9 — reading an installed dependency's version
FR-029 / T063 require the
picomatchversion be read at runtime, never transcribed, so the glob engine recorded in a snapshot is the engine that actually ran. ADR-0013 / FR-002 forbid a dynamic loader, and the guard bansimport.meta.resolveanywhere in the adapter.Read naively those collide. They don't — the distinction the guard protects is dynamic module loading, not filesystem access:
import.meta.resolve, dynamicimport(),require.resolvenode_modules/<dep>/package.jsonas a fileWalking up to
node_modules/picomatch/package.jsonand reading itsversiontakes the value from the installed artifact rather than a literal in our source, while invoking no resolver and loading no code. Recorded aspackage-boundary.md§6.The ADR-0016 obligation stays attached: a read that silently returns
undefinedand one that returns4.0.5are indistinguishable from a green suite alone, so the assertion against the lockfile must be observed failing.D10 —
source-scan.tsfalse positivesThe scans match
from '…'andimport '…'textually rather than resolving them, so a file that states a rule is indistinguishable from one that breaks it. It fired on:'bulk-import'— ADR-0015's own plugin name — read as a side-effectingimport '…'… frombefore a quotePhase C and Phase D each renamed around it, which leaves the trap armed for the next writer.
EXCLUDED_FROM_SCANnow carries the consumer's two boundary guards. Each must name the very thing it forbids — the schema file it pins by hash, and the adapter package it proves is never imported — so neither was scannable without an entry.The patterns are deliberately not loosened. A scan that misses a real edge is worse than one that occasionally over-matches, and the exclusion list is the intended relief valve.
The guard caught this change
The pinned-set assertion — "an exclusion list that can grow without anyone noticing is the same defect the scans are guarding against" — failed on my own edit until updated deliberately. That is the design working on the person extending it, and it is why the list is pinned rather than merely commented.
Checks
bun test1504 pass / 0 fail ·typecheckclean ·check:deps→core-has-no-adapter-deps: ok·check:freeze-hashes→ ok over both frozen trees ·adr lint20 records, 0 errors.Standing
ADR-0014 rung 1 only. No rung-2 or rung-3 claim, no release scheduled or prepared. ADR-0012 gate 3 recorded as observed; gate 4 unmet and not yet testable.
With this, every decision Phase E inherits has been made.