Follow-up from review of #497. Deferred there because replacing the shim's generation mechanism is a design change, not a fix to the feature under review.
RuleContext is declared twice: once as a real interface in src/formats/rules.ts, and once as text inside a template literal in src/helpers/rules-shim.ts. That second copy is what generateRulesDts() writes to .archgate/rules.d.ts in every governed project, and it is what a rule author's editor uses for autocomplete and type errors while writing .rules.ts.
The two copies are kept in sync entirely by hand, and nothing in bun run validate verifies the second one.
Reproduction
Plant a flatly wrong signature in the shim's template literal — a wrong parameter type plus two types that do not exist anywhere in the codebase:
readYAML(path: number, bogus: Nonexistent): Promise<WrongType>;
Then run the full gate:
| Gate |
Result |
bun run typecheck (tsc --build) |
passes |
bun run lint (oxlint --deny-warnings) |
passes |
bun run check (incl. ARCH-022/rulecontext-shim-parity) |
passes, 5/5 |
bun run validate |
exit 0 |
Nothing fails. tsc type-checks code, and this interface is a string — so to the compiler it is inert text.
Why the existing rule does not cover it
ARCH-022/rulecontext-shim-parity (added in #497) compares member names only. readYAML is present in both surfaces, so the planted signature passes. The rule's own description and the ADR both say full signature/JSDoc parity remains a manual review item; this issue is about how wide that manual surface actually is.
Empirically it does not hold: three separate findings in #497's review were this exact drift, after readYAML's content was narrowed from unknown to YamlValue mid-review:
The surface
One-line type change; six hand-maintained descriptions of it:
| Surface |
Audience |
Machine-checked |
src/formats/rules.ts |
the compiler |
yes |
src/helpers/rules-shim.ts |
rule authors' editors, via .archgate/rules.d.ts |
member names only |
docs/src/content/docs/reference/rule-api.mdx |
English readers |
no |
docs/src/content/docs/nb/reference/rule-api.mdx |
Norwegian readers |
page parity only (GEN-002) |
docs/src/content/docs/pt-br/reference/rule-api.mdx |
Portuguese readers |
page parity only (GEN-002) |
docs/public/llms-full.txt |
LLM consumers |
generated, content unverified |
Suggested approach
Derive the shim from the real interface instead of transcribing it. src/formats/rules.ts is already the single source of truth for these types; generateRulesDts() should read them from there and emit the ambient form (strip export, prepend declare) rather than restating them. Drift then becomes structurally impossible rather than review-dependent.
Two things to settle when implementing:
- Extraction mechanism. The ambient shim needs the type declarations plus their JSDoc, with
export removed. Note ctx.ast() cannot parse these out of the source: Bun.Transpiler erases type-only declarations before the tree is built, which is why rulecontext-shim-parity uses a regex over raw text. A generator has the same constraint and would need the TypeScript AST or a text-level extractor.
- Which types travel. The shim currently mirrors a specific set (
RuleContext, RuleConfig, RuleSet, and the types they reference). Deriving it needs an explicit rule for what is in scope, or the ambient file grows to everything exported from rules.ts.
Smaller alternative if that is too invasive: extend rulecontext-shim-parity to compare normalized signature text per member, not just names. Cheaper, and it would have caught every finding above — but it stays a text comparison, so it constrains drift rather than preventing it, and JSDoc prose stays unchecked.
Scope and severity
This is not a security or runtime-behavior issue. The engine's behavior comes from src/formats/rules.ts and the actual implementations; the shim only affects the type hints rule authors see in their editor. The realistic worst case is that Archgate ships .archgate/rules.d.ts files that disagree with the runtime, and a rule author gets misleading autocomplete or a spurious type error with nothing failing anywhere.
Not a blocker for #497, which is green and approved.
Related
Follow-up from review of #497. Deferred there because replacing the shim's generation mechanism is a design change, not a fix to the feature under review.
RuleContextis declared twice: once as a real interface insrc/formats/rules.ts, and once as text inside a template literal insrc/helpers/rules-shim.ts. That second copy is whatgenerateRulesDts()writes to.archgate/rules.d.tsin every governed project, and it is what a rule author's editor uses for autocomplete and type errors while writing.rules.ts.The two copies are kept in sync entirely by hand, and nothing in
bun run validateverifies the second one.Reproduction
Plant a flatly wrong signature in the shim's template literal — a wrong parameter type plus two types that do not exist anywhere in the codebase:
Then run the full gate:
bun run typecheck(tsc --build)bun run lint(oxlint --deny-warnings)bun run check(incl.ARCH-022/rulecontext-shim-parity)bun run validateNothing fails.
tsctype-checks code, and this interface is a string — so to the compiler it is inert text.Why the existing rule does not cover it
ARCH-022/rulecontext-shim-parity(added in #497) compares member names only.readYAMLis present in both surfaces, so the planted signature passes. The rule's own description and the ADR both say full signature/JSDoc parity remains a manual review item; this issue is about how wide that manual surface actually is.Empirically it does not hold: three separate findings in #497's review were this exact drift, after
readYAML'scontentwas narrowed fromunknowntoYamlValuemid-review:contentasunknown— feat(engine): add ctx.readYAML and ctx.checkCase rule helpers #497 (comment)readJSON" while its own adjacent example said "no cast needed" — feat(engine): add ctx.readYAML and ctx.checkCase rule helpers #497 (comment)The surface
One-line type change; six hand-maintained descriptions of it:
src/formats/rules.tssrc/helpers/rules-shim.ts.archgate/rules.d.tsdocs/src/content/docs/reference/rule-api.mdxdocs/src/content/docs/nb/reference/rule-api.mdxdocs/src/content/docs/pt-br/reference/rule-api.mdxdocs/public/llms-full.txtSuggested approach
Derive the shim from the real interface instead of transcribing it.
src/formats/rules.tsis already the single source of truth for these types;generateRulesDts()should read them from there and emit the ambient form (stripexport, prependdeclare) rather than restating them. Drift then becomes structurally impossible rather than review-dependent.Two things to settle when implementing:
exportremoved. Notectx.ast()cannot parse these out of the source:Bun.Transpilererases type-only declarations before the tree is built, which is whyrulecontext-shim-parityuses a regex over raw text. A generator has the same constraint and would need the TypeScript AST or a text-level extractor.RuleContext,RuleConfig,RuleSet, and the types they reference). Deriving it needs an explicit rule for what is in scope, or the ambient file grows to everything exported fromrules.ts.Smaller alternative if that is too invasive: extend
rulecontext-shim-parityto compare normalized signature text per member, not just names. Cheaper, and it would have caught every finding above — but it stays a text comparison, so it constrains drift rather than preventing it, and JSDoc prose stays unchecked.Scope and severity
This is not a security or runtime-behavior issue. The engine's behavior comes from
src/formats/rules.tsand the actual implementations; the shim only affects the type hints rule authors see in their editor. The realistic worst case is that Archgate ships.archgate/rules.d.tsfiles that disagree with the runtime, and a rule author gets misleading autocomplete or a spurious type error with nothing failing anywhere.Not a blocker for #497, which is green and approved.
Related