@@ -68,7 +68,22 @@ export type CodeStampShape =
6868 /** `readonly code = CONST` — the same, through a resolved constant. */
6969 | 'classconst'
7070 /** `code: 'X'` in an object literal. */
71- | 'objlit' ;
71+ | 'objlit'
72+ /**
73+ * [#9223] `code: CONST` in an object literal — the same indirection
74+ * `classconst` follows, in the shape that stamps most of this repo's codes.
75+ * Missing from the scan until #9223, which is why the rows carrying it were
76+ * all added at once: `objlit` demanded a quoted literal, so a constant in an
77+ * object literal matched NOTHING and was not even reported as unresolved.
78+ */
79+ | 'objlitconst'
80+ /**
81+ * [#9223] `` code: `A_${x}_B` `` — a code built by interpolation. No source
82+ * scan can evaluate one, so the gate reports it under its FAMILY identity
83+ * (`${…}` → `*`, e.g. `APPROVAL_*_FAILED`) and a row must classify it. The
84+ * only verdict that can honestly cover a family is `runtime-pinned`.
85+ */
86+ | 'objlittemplate' ;
7287
7388/**
7489 * Where the stamped code can end up. `dispatcher` is the door this card is
@@ -85,8 +100,15 @@ export type CodeStampShape =
85100 * — so a code stamped on a thrown value and passed through remains exactly the
86101 * reachability question this table answers. Both were spelled `sendError` until
87102 * #9098; that collision is what let the door's hole read as closed.
103+ *
104+ * [#9223] `plugin-route` is a THIRD door, surfaced by the widened scan: a plugin
105+ * that mounts its own Hono routes answers refusals with its own `c.json({
106+ * success: false, error: { code, message } })` and passes through neither the
107+ * dispatcher's `errorFromThrown` nor `packages/rest`'s doors. The envelope is
108+ * still an ADR-0112 `error.code` on a wire with a live reader, so a code that
109+ * reaches it is a registration question exactly like the other two.
88110 */
89- export type CodeDoor = 'dispatcher' | 'rest' | 'none' ;
111+ export type CodeDoor = 'dispatcher' | 'rest' | 'plugin-route' | ' none';
90112
91113export type CodeVerdict =
92114 /**
@@ -114,7 +136,25 @@ export type CodeVerdict =
114136 * `MONGODB_MULTI_TENANT_UNSUPPORTED` was UNregistered by #8035 for exactly
115137 * this reason, and "host boot matching is not wire vocabulary".
116138 */
117- | 'boot-refusal' ;
139+ | 'boot-refusal'
140+ /**
141+ * [#9223] The site builds its code by INTERPOLATION, so no source scan can
142+ * say which codes it produces or whether they are registered — and a named
143+ * test does that job at runtime instead, by enumerating the family and
144+ * parsing each member against the closed union.
145+ *
146+ * This is the one verdict that does not decide reachability; it records a
147+ * DIVISION OF LABOUR, and it is deliberately hard to misuse:
148+ * `check-dispatcher-error-vocabulary` refuses it on any shape other than
149+ * `objlittemplate` (on a literal it would be an exemption from the registry
150+ * check — the very hole this gate exists to close) and fails when the
151+ * {@link UnregisteredCodeSite.pin} file does not exist.
152+ *
153+ * ⛔ Not a place to park a template nobody checks. If no runtime pin covers
154+ * the family, the fix is to stamp a LITERAL code per branch — then the scan
155+ * checks it like any other and the door can narrow it.
156+ */
157+ | 'runtime-pinned' ;
118158
119159export interface UnregisteredCodeSite {
120160 /** The literal as it is stamped. */
@@ -126,6 +166,13 @@ export interface UnregisteredCodeSite {
126166 readonly verdict : CodeVerdict ;
127167 /** Why this verdict — the evidence, not a restatement of the verdict. */
128168 readonly why : string ;
169+ /**
170+ * [#9223] Required by `runtime-pinned`, meaningless otherwise: the
171+ * repo-relative test that does at runtime what the scan cannot do
172+ * statically. The gate checks that this file EXISTS — a deleted pin would
173+ * otherwise leave the row asserting a guarantee nothing provides.
174+ */
175+ readonly pin ?: string ;
129176}
130177
131178/**
@@ -146,6 +193,48 @@ export const UNREGISTERED_CODE_SITES: readonly UnregisteredCodeSite[] = [
146193 // unswept producer lands here as an `unclassified-site` finding and gets a
147194 // new row (then a spec-lane registration, then the row comes out again). ──
148195
196+ // ── pending registration, found by #9223's widened scan ────────────────
197+ {
198+ code : 'UNIQUE_SCOPE_CONFIRMATION_REQUIRED' ,
199+ file : 'packages/cloud-connection/src/marketplace-install-local-plugin.ts' ,
200+ shape : 'objlitconst' ,
201+ door : 'plugin-route' ,
202+ verdict : 'pending-registration' ,
203+ why :
204+ 'Returned as `error.code` by the marketplace install seam when the ADR-0120 D5e posture gate ' +
205+ 'stops an install, and READ off the wire: `packages/cli/src/commands/package/install.ts` ' +
206+ "branches on `res.body?.error?.code === 'UNIQUE_SCOPE_CONFIRMATION_REQUIRED'` to print the " +
207+ 'per-index decision list. That the seam speaks REGISTERED vocabulary is measurable rather ' +
208+ 'than assumed: every sibling code in the same file (PLUGIN_MANIFEST_INVALID, ' +
209+ 'MARKETPLACE_UNAVAILABLE, INVALID_REQUEST, RESOURCE_NOT_FOUND, CLOUD_FETCH_FAILED, …) is in ' +
210+ 'the ledger already, which is why the scan never reported them — this one member is the gap. ' +
211+ 'Invisible until #9223 for one reason only: it is stamped through a constant ' +
212+ '(GLOBAL_UNIQUE_CONFIRMATION_REQUIRED, `packages/types/src/unique-scope-install-gate.ts`) in ' +
213+ 'an object literal, the shape `objlit` could not see. ⇒ the spec lane registers it.' ,
214+ } ,
215+
216+ // ── runtime-pinned: an interpolated family, checked where it can be ─────
217+ {
218+ code : 'APPROVAL_*_FAILED' ,
219+ file : 'packages/rest/src/rest-server.ts' ,
220+ shape : 'objlittemplate' ,
221+ door : 'rest' ,
222+ verdict : 'runtime-pinned' ,
223+ pin : 'packages/rest/src/rest-approvals-wire-codes.test.ts' ,
224+ why :
225+ "Three approvals route factories (`decisionRoute`, `flowMoveRoute`, `threadRoute`) spell the " +
226+ 'terminal 500 catch\'s code as a template — `` `APPROVAL_${action.toUpperCase()}_FAILED` `` and ' +
227+ 'two siblings — so the family, not a literal, is what exists in source. #8885 registered all ' +
228+ 'nine codes the family produces, and its pin is what keeps that true: it enumerates the ' +
229+ 'registered `POST /approvals/requests/:id/<action>` routes and asserts the code each catch arm ' +
230+ "would generate parses against ApiErrorSchema's closed union, mirroring the production " +
231+ "template exactly (single-occurrence `.replace('-', '_')` included). So a tenth action route " +
232+ 'whose generated code nobody registers fails THERE, mechanically. This row records that ' +
233+ 'division of labour instead of letting the scan imply it checked something it cannot: #9223 ' +
234+ 'widened the scan enough to SEE the template, and seeing it is what makes the pin an ' +
235+ 'accounted-for half rather than a local habit in one package.' ,
236+ } ,
237+
149238 // ── sandbox-authored: outside any ledger, by design ────────────────────
150239 // (no source site — the producer is tenant code; see SANDBOX_AUTHORED_LIMB)
151240
@@ -187,6 +276,61 @@ export const UNREGISTERED_CODE_SITES: readonly UnregisteredCodeSite[] = [
187276 '`error.code`. This is the row that shows why verdicts are DECLARED: it is written exactly ' +
188277 'like FLOW_FAILED and a documented catch one layer up makes it unreachable.' ,
189278 } ,
279+ {
280+ code : 'YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER' ,
281+ file : 'packages/plugins/plugin-auth/src/auth-manager.ts' ,
282+ shape : 'objlitconst' ,
283+ door : 'none' ,
284+ verdict : 'foreign-vocabulary' ,
285+ why :
286+ "better-auth's own APIError vocabulary — the vendor's `YOU_ARE_NOT_ALLOWED_TO_*` family, " +
287+ 'thrown as `new APIError(\'FORBIDDEN\', { message, code })` so the remove-member guard\'s ' +
288+ 'refusal SET stays byte-for-byte the vendor\'s (see the contract note in ' +
289+ '`remove-member-permission-guard.ts`; only the envelope differs). It cannot reach this door, ' +
290+ 'by the same route the IMPERSONATION_ROTATION_FAILED row documents and re-verified here: ' +
291+ '`domains/auth.ts` catches everything the auth service throws and answers ' +
292+ '`deps.error(INTERNAL_ERROR_MESSAGE, 500)` — unconditionally, never `errorFromThrown` (#5085). ' +
293+ 'So the string never lands in an ADR-0112 `error.code`.' ,
294+ } ,
295+ {
296+ code : 'OS_METADATA_CONVERTED' ,
297+ file : 'packages/spec/src/conversions/apply.ts' ,
298+ shape : 'objlitconst' ,
299+ door : 'none' ,
300+ verdict : 'foreign-vocabulary' ,
301+ why :
302+ 'The ADR-0087 D4 conversion-notice vocabulary, not an error vocabulary at all: ' +
303+ '`applyConversions` PASSES this code to an `onNotice` callback in a structured ' +
304+ 'ConversionNotice, and the loader, `validate` and the MCP deprecations surface consume that ' +
305+ 'shape. Nothing is thrown and no envelope is built. Same class as the INVALID_SCREEN_INPUT ' +
306+ 'row — a result envelope that merely spells itself `code`.' ,
307+ } ,
308+ {
309+ code : 'OS_METADATA_CONVERSION_CONFLICT' ,
310+ file : 'packages/spec/src/conversions/apply.ts' ,
311+ shape : 'objlitconst' ,
312+ door : 'none' ,
313+ verdict : 'foreign-vocabulary' ,
314+ why :
315+ 'The conflict twin of the OS_METADATA_CONVERTED row: handed to `onConflict` when a rename ' +
316+ "target is a live name owned by something else, so the conversion refuses to rewrite it and " +
317+ 'surfaces a loud diagnostic instead (ADR-0078: never silent). A callback payload, not a ' +
318+ 'thrown error and not a response body.' ,
319+ } ,
320+ {
321+ code : 'ERR_BULK_PER_ROW_HOOK_LIMIT' ,
322+ file : 'packages/spec/src/data/bulk-write-hook-conformance.ts' ,
323+ shape : 'objlitconst' ,
324+ door : 'none' ,
325+ verdict : 'foreign-vocabulary' ,
326+ why :
327+ 'The declaring source rules on this itself, in the doc comment on the constant: ' +
328+ '"Deliberately an `ERR_`-prefixed operational code on the thrown error\'s own property bag, ' +
329+ 'NOT an ADR-0112 wire code … minting a member of it by side effect is the exact ' +
330+ '`declared != enforced` shape that vocabulary exists to prevent." It is RETURNED in a ' +
331+ '`BulkPerRowHookBudgetVerdict` by a function documented pure and total (no throw); the engine ' +
332+ 'raises, the contract decides.' ,
333+ } ,
190334 {
191335 code : 'SQLITE_ERROR' ,
192336 file : 'packages/spec/src/migrations/entries/semantic/18.driver-sql-unresolvable-where-column-refused.ts' ,
0 commit comments