Filing unassigned; found while implementing #7991 (the packages/qa/downstream-contract source alias). Not fixed there — the card's ruling is that a problem surfaced by aliasing is a finding in its own right, and this one changes a shared gate's semantics plus two other packages' registry entries.
The fact, measured
scripts/check-test-source-alias.mjs reads an alias replacement with asPath(), which takes the last string literal in the expression:
functionasPath(raw){conststrings=[...raw.matchAll(/(['"`])((?:[^\\]|\\.)*?)\1/g)].map((m)=>m[2]);
...
returnstrings[strings.length-1];}For the path.resolve(__dirname, '../../spec/src/index.ts') form that is the whole answer. For the template-literal form — the one the gate's own two reference configs use for subpath rules —
{find: /^@objectstack\/spec\/([a-z-]+)$/,replacement: `${path.resolve(__dirname,'../../spec/src')}/$1/index.ts`}the only string literal delimiter reached is the backtick, so asPath returns the entire template body, ${path.resolve(__dirname, '../../spec/src')}/$1/index.ts. pointsAtSource() looks for a src path segment and there is none in that text (spec/src' is followed by a quote, not a separator), so a config that aliases every namespace correctly reads to the gate as aliasing nothing.
Evidence: changing only the SPELLING moves the verdict
packages/services/service-knowledge and packages/plugins/plugin-audit both alias spec subpaths to source with the template form today. Rewriting only the spelling — same resolution, no behaviour change:
replacement: path.join(path.resolve(__dirname,'..','..'),'spec/src/$1/index.ts')
makes the gate declare their registry entries stale:
x @objectstack/plugin-audit: registry entry is STALE — no longer unaliased: @objectstack/spec.
Narrow the entry to exactly: ["@objectstack/objectql"]
x @objectstack/service-knowledge: registry entry is STALE — no longer unaliased: @objectstack/spec.
Narrow the entry to exactly: ["@objectstack/objectql"]
So @objectstack/spec sits in both entries because of how the replacement is written, not because anything resolves through dist. (Both experiments were reverted; nothing in PR for #7991 touches those two configs.)
Direction, and why it still matters
Fail-closed. The gate never reads an unaliased package as safe, so its core guarantee is intact and this is not a #7991-shaped false green. What it does cost:
- The shrink-only registry over-states the remaining work — at least two of the 62 entries name a dependency that is already aliased.
- A dev dispatched to remediate either package finds the aliases already correct and the gate still red, with the failure text prescribing the alias they already have. The only thing that moves it is a spelling change; nothing in the message says so.
- It quietly penalises the one-rule-for-all-namespaces form. That form exists because hand-listing namespaces goes stale, and both configs carry a paragraph explaining why they use it.
Fix options (not chosen here)
- A — teach
asPath the template form: concatenate the literal chunks and treat each ${...} as an opaque prefix, so the tail /$1/index.ts joins the spec/src chunk. Keeps every existing config as written; the two entries then shrink to ['@objectstack/objectql'] in the same PR (the gate audits both directions, so it names them). - B — prescribe the readable spelling in the gate header and convert the two configs; same registry shrink, no gate change.
A is the contract-first shape: the configs are correct, the reader is what cannot see them. Either way the change is one PR and its own card — it edits a shared gate plus two packages' entries, so it should not ride a per-package alias fix.
Prior art (searched before filing)
Generated by Claude Code
Filing unassigned; found while implementing #7991 (the
packages/qa/downstream-contractsource alias). Not fixed there — the card's ruling is that a problem surfaced by aliasing is a finding in its own right, and this one changes a shared gate's semantics plus two other packages' registry entries.The fact, measured
scripts/check-test-source-alias.mjsreads an alias replacement withasPath(), which takes the last string literal in the expression:For the
path.resolve(__dirname, '../../spec/src/index.ts')form that is the whole answer. For the template-literal form — the one the gate's own two reference configs use for subpath rules —the only string literal delimiter reached is the backtick, so
asPathreturns the entire template body,${path.resolve(__dirname, '../../spec/src')}/$1/index.ts.pointsAtSource()looks for asrcpath segment and there is none in that text (spec/src'is followed by a quote, not a separator), so a config that aliases every namespace correctly reads to the gate as aliasing nothing.Evidence: changing only the SPELLING moves the verdict
packages/services/service-knowledgeandpackages/plugins/plugin-auditboth alias spec subpaths to source with the template form today. Rewriting only the spelling — same resolution, no behaviour change:makes the gate declare their registry entries stale:
So
@objectstack/specsits in both entries because of how the replacement is written, not because anything resolves throughdist. (Both experiments were reverted; nothing in PR for #7991 touches those two configs.)Direction, and why it still matters
Fail-closed. The gate never reads an unaliased package as safe, so its core guarantee is intact and this is not a #7991-shaped false green. What it does cost:
Fix options (not chosen here)
asPaththe template form: concatenate the literal chunks and treat each${...}as an opaque prefix, so the tail/$1/index.tsjoins thespec/srcchunk. Keeps every existing config as written; the two entries then shrink to['@objectstack/objectql']in the same PR (the gate audits both directions, so it names them).A is the contract-first shape: the configs are correct, the reader is what cannot see them. Either way the change is one PR and its own card — it edits a shared gate plus two packages' entries, so it should not ride a per-package alias fix.
Prior art (searched before filing)
vitest.config.tslets a stale@objectstack/coredist decide its verdicts — #7668 fixed one, nothing stops the next #7849 / PR ci(test-alias): gate that a unit test judges source, not a sibling's dist #7966 — the gate and the registry.packages/qa/downstream-contract— the repo's backward-compatibility gate for@objectstack/specreports 14/14 green against a spec change that breaks its own fixture, because it readsspec/dist#7991 — the card this was found under; its config uses thepath.joinspelling deliberately, with a comment pointing here.attach-requires-parent-editc3: #4757 multi-delete pin can't execute against the prebuilt@objectstack/core(stale-dist / CI) #7668 / PR test(service-storage): resolve@objectstack/corefrom source so a stale dist can't decide a pin (#7668) #7778 — the anchored-regex constraint the template form implements.Generated by Claude Code