Observation-class, found while widening Rule 3's hoisted-const pass in PR #13104. Latent, not live — measured zero instances in tree today, so nothing is currently escaping.
The shape
scripts/check-doc-authoring.mjs has two places that ask "what type does this const declare", and PR #13104 only fixed one of them.
collectTextSinkConsts() now routes its type anchors through a declaredTypeText() helper that reads the annotation or a trailing satisfies when there is no annotation. Its sibling, inStrictOptions(), still reads the annotation alone:
if(ts.isVariableDeclaration(p)){constnm=p.name.getText();return/_STRICT_OPTIONS$/.test(nm)||/\bStrictObjectOptions\b/.test(p.type ? p.type.getText() : '');}inStrictOptions() is the position test: it climbs from a guidance: / guidanceSets: / history: / aliases: / retiredForms: / surface: property up to whichever encloses it first, and decides whether that property is really a StrictObjectOptions key whose value is printed at a refusing author.
With satisfies, the climb passes through a SatisfiesExpression — which is neither a CallExpression nor a VariableDeclaration, so the loop continues — and lands on the VariableDeclaration, where p.type is undefined. Result: false. A prescription written like this would not be recognised as customer-facing prose:
constSOMETHING_OPTIONS={surface: 'this shape',guidance: {legacyKey: 'removed in protocol 17 (#1234). Delete it.'},}satisfiesStrictObjectOptions;The failure mode is the one this whole file is a monument to: no flag, silently.
Why it is latent
Measured on main @ e9c1055ea2:
satisfies StrictObjectOptions — zero occurrences under packages/. Every options table in the tree uses either the annotation or the *_STRICT_OPTIONS name, both of which inStrictOptions() already recognises.- The
satisfies spelling is live for the sibling type: WIDGET_GUIDANCE_SETS (packages/spec/src/ui/dashboard.zod.ts) is written as const satisfies readonly KeySetGuidance[]. So the spelling is idiomatic here and the next options table written that way is plausible, not hypothetical.
Why it was left out of PR #13104
Scope. That card's defect class is the cross-module blind spot in collectTextSinkConsts(); this is a spelling gap in a different function with a different job, and folding it in would have blurred what that PR's ablation proves. Zero live instances also means there is no red to demonstrate today, so the fix would ship with no reverse verification available beyond a planted fixture.
Suggested direction
Reuse the helper PR #13104 already added — declaredTypeText(decl, ts) — at the VariableDeclaration branch of inStrictOptions(), and add the --self-test case that the gate's own extend-the-detector rule requires. That makes the two type anchors read the same way, which is the property that made this gap possible in the first place.
Not filed as a bug against Rule 3's verdicts: no current string is misclassified.
Generated by Claude Code
Observation-class, found while widening Rule 3's hoisted-const pass in PR #13104. Latent, not live — measured zero instances in tree today, so nothing is currently escaping.
The shape
scripts/check-doc-authoring.mjshas two places that ask "what type does this const declare", and PR #13104 only fixed one of them.collectTextSinkConsts()now routes its type anchors through adeclaredTypeText()helper that reads the annotation or a trailingsatisfieswhen there is no annotation. Its sibling,inStrictOptions(), still reads the annotation alone:inStrictOptions()is the position test: it climbs from aguidance:/guidanceSets:/history:/aliases:/retiredForms:/surface:property up to whichever encloses it first, and decides whether that property is really aStrictObjectOptionskey whose value is printed at a refusing author.With
satisfies, the climb passes through aSatisfiesExpression— which is neither aCallExpressionnor aVariableDeclaration, so the loop continues — and lands on theVariableDeclaration, wherep.typeisundefined. Result:false. A prescription written like this would not be recognised as customer-facing prose:The failure mode is the one this whole file is a monument to: no flag, silently.
Why it is latent
Measured on
main@e9c1055ea2:satisfies StrictObjectOptions— zero occurrences underpackages/. Every options table in the tree uses either the annotation or the*_STRICT_OPTIONSname, both of whichinStrictOptions()already recognises.satisfiesspelling is live for the sibling type:WIDGET_GUIDANCE_SETS(packages/spec/src/ui/dashboard.zod.ts) is writtenas const satisfies readonly KeySetGuidance[]. So the spelling is idiomatic here and the next options table written that way is plausible, not hypothetical.Why it was left out of PR #13104
Scope. That card's defect class is the cross-module blind spot in
collectTextSinkConsts(); this is a spelling gap in a different function with a different job, and folding it in would have blurred what that PR's ablation proves. Zero live instances also means there is no red to demonstrate today, so the fix would ship with no reverse verification available beyond a planted fixture.Suggested direction
Reuse the helper PR #13104 already added —
declaredTypeText(decl, ts)— at theVariableDeclarationbranch ofinStrictOptions(), and add the--self-testcase that the gate's own extend-the-detector rule requires. That makes the two type anchors read the same way, which is the property that made this gap possible in the first place.Not filed as a bug against Rule 3's verdicts: no current string is misclassified.
Generated by Claude Code