Found by measurement while porting dashboard-widget-options for #12810 — the port tripped it, and the workaround is in that PR with its reasoning at the site. Recorded because the workaround is a per-site choice and the underlying hole is not.
The shape
The repo has two gates over code-shaped values, and they are designed as a pair that hands work to each other:
scripts/check-dispatcher-error-vocabulary.mjs — its objlitconst shape matches code: followed by a SCREAMING_SNAKE identifier, then reduces that constant to a literal. Its literal grammar is [A-Za-z][A-Za-z0-9_]*.scripts/check-error-code-casing.mjs — owns the lowercase sweep. Every one of its patterns requires a QUOTED lowercase literal next to the token code, and its grammar is [a-z][a-z0-9_]*.
Neither grammar admits a hyphen. So for a KEBAB-case code, the two spellings behave in opposite and equally wrong ways:
- as an inline literal (
code: 'some-diagnostic') — matches no pattern in either gate. Invisible to both. This is the state of all six existing parser diagnostic codes in packages/sdui-parser/src/validate.ts (unknown-component, unknown-prop, not-a-container, inert-expression, type-mismatch, invalid-enum), and both gates are green on them today. - through a constant (
code: SOME_DIAGNOSTIC) — the objlitconst shape matches the constant NAME, then literalCodeValues refuses the kebab VALUE, so the site is reported as unresolved-constant. Measured: exactly 1 finding, naming the file.
Why the constant form is the bad half
unresolved-constant findings are pushed unconditionally in reconcile(); unlike a resolved site, there is no declaration row in packages/runtime/src/dispatcher-error-vocabulary.ts that can classify one. The gate's own remedy text says "Resolve it, or teach resolveConstant() the spelling" — but resolving is impossible for a value the grammar excludes, so the only escape is editing the gate.
So the incentive the gate pair currently creates is backwards: it pushes authors toward the inline literal, which is the form NEITHER gate can see, and penalizes the form that names the code once and exports it.
Why this is not just cosmetic
The vocabulary gate's declared bound is that a value it cannot reduce is REPORTED, never dropped. That bound is honoured for the constant form and quietly violated for the literal form — not by a bug in either gate, but because a kebab literal falls outside both grammars, so no pattern fires and nothing says so. A run that reports "22 code-stamping sites found, 22 classified" is telling the truth about the values it can spell and saying nothing about a whole naming convention this repo actually uses.
What a fix would have to decide
Not obvious, and deliberately not decided here:
- whether kebab-case DIAGNOSTIC codes (a build/authoring vocabulary) should be in either gate's population at all, or explicitly declared out of it once, in one place, instead of being out by accident of two regexes;
- if in: which gate owns them, and whether widening
literalCodeValues to admit hyphens would newly resolve other constants across the repo into sites that then need declaration rows. That blast radius was not measured and should be before anyone widens the grammar.
The cheap and possibly correct answer is the first: state once that kebab-case is a separate vocabulary, so the silence becomes declared rather than accidental.
Found by measurement while porting
dashboard-widget-optionsfor #12810 — the port tripped it, and the workaround is in that PR with its reasoning at the site. Recorded because the workaround is a per-site choice and the underlying hole is not.The shape
The repo has two gates over code-shaped values, and they are designed as a pair that hands work to each other:
scripts/check-dispatcher-error-vocabulary.mjs— itsobjlitconstshape matchescode:followed by a SCREAMING_SNAKE identifier, then reduces that constant to a literal. Its literal grammar is[A-Za-z][A-Za-z0-9_]*.scripts/check-error-code-casing.mjs— owns the lowercase sweep. Every one of its patterns requires a QUOTED lowercase literal next to the tokencode, and its grammar is[a-z][a-z0-9_]*.Neither grammar admits a hyphen. So for a KEBAB-case code, the two spellings behave in opposite and equally wrong ways:
code: 'some-diagnostic') — matches no pattern in either gate. Invisible to both. This is the state of all six existing parser diagnostic codes inpackages/sdui-parser/src/validate.ts(unknown-component,unknown-prop,not-a-container,inert-expression,type-mismatch,invalid-enum), and both gates are green on them today.code: SOME_DIAGNOSTIC) — theobjlitconstshape matches the constant NAME, thenliteralCodeValuesrefuses the kebab VALUE, so the site is reported asunresolved-constant. Measured: exactly 1 finding, naming the file.Why the constant form is the bad half
unresolved-constantfindings are pushed unconditionally inreconcile(); unlike a resolved site, there is no declaration row inpackages/runtime/src/dispatcher-error-vocabulary.tsthat can classify one. The gate's own remedy text says "Resolve it, or teachresolveConstant()the spelling" — but resolving is impossible for a value the grammar excludes, so the only escape is editing the gate.So the incentive the gate pair currently creates is backwards: it pushes authors toward the inline literal, which is the form NEITHER gate can see, and penalizes the form that names the code once and exports it.
Why this is not just cosmetic
The vocabulary gate's declared bound is that a value it cannot reduce is REPORTED, never dropped. That bound is honoured for the constant form and quietly violated for the literal form — not by a bug in either gate, but because a kebab literal falls outside both grammars, so no pattern fires and nothing says so. A run that reports "22 code-stamping sites found, 22 classified" is telling the truth about the values it can spell and saying nothing about a whole naming convention this repo actually uses.
What a fix would have to decide
Not obvious, and deliberately not decided here:
literalCodeValuesto admit hyphens would newly resolve other constants across the repo into sites that then need declaration rows. That blast radius was not measured and should be before anyone widens the grammar.The cheap and possibly correct answer is the first: state once that kebab-case is a separate vocabulary, so the silence becomes declared rather than accidental.