Skip to content

Commit e04759d

Browse files
fix(spec): error-code provenance reads a constant by its initializer, and the ledger lists @objectstack/mcp's exposure codes (#22688)
Fixes #22664 Clause-②: yes `check:error-code-provenance` recognised a constant only when its NAME ended in `_CODE`. That proxy hid the `@objectstack/mcp` stdio data bridge's two exposure constants, and ten others like them. This PR replaces the name rule with an initializer rule: a declared binding of any name whose literal initializer is a registered code is a stamp site. The ledger gains the `@objectstack/mcp` owner key, and one provenance waiver for a shared constant in `@objectstack/types`. No new gate. ## Step 1: the measurement (repo-wide, before any edit) **Instrument and radius.** A scratch script (not committed) that walks the gate's own population, `collectSourceFiles` (2849 files at `origin/main` `ee3ae0360d`: `packages/**/src` non-test `.ts`/`.tsx`, the ledger file excluded), masks comments with the gate's own masker, and reads two things: - every `const|let|var NAME = 'CODE'` declaration (type annotation and `as const` admitted) and every `readonly|static NAME = 'CODE'` class field whose initializer is a registered code, with each binding's in-file references classified by position; - a census of every registered-code literal in code (708), split into the ones at a site the gate already reports (336, keyed by line and code) and the rest (372), classified by the tokens in front of them. **Positive control.** The instrument finds all 37 `*_CODE` declarations the gate's `constdef` already reports, each marked as seen. **Declared constants the gate could not see: 11, in five packages.** | Package | Constant = code | File | How the binding reaches a stamp | Ledger | |:--|:--|:--|:--|:--| | `@objectstack/mcp` | `OBJECT_API_DISABLED` | `stdio-data-bridge.ts:207` | `exposureError(…, OBJECT_API_DISABLED, 404)`, whose body assigns `err.code = code` | **no row** | | `@objectstack/mcp` | `OBJECT_API_METHOD_NOT_ALLOWED` | `stdio-data-bridge.ts:208` | same helper, 405 | **no row** (the card names only the first) | | `@objectstack/types` | `GLOBAL_UNIQUE_CONFIRMATION_REQUIRED` = `UNIQUE_SCOPE_CONFIRMATION_REQUIRED` | `unique-scope-install-gate.ts:336` | imported by `@objectstack/cloud-connection`, stamped `code: GLOBAL_UNIQUE_CONFIRMATION_REQUIRED` in the install seam's 409 | **no row, no waiver** for types; cloud-connection lists it | | `@objectstack/plugin-auth` | `SELF_REGISTRATION_CLOSED`, `EMAIL_DOMAIN_NOT_ALLOWED`, `AUDIENCE_CONFIG_ERROR` = `AUTH_CONFIG_ERROR` | `audience-posture.ts:103/105/107` | `code: CONST` | listed | | `@objectstack/service-analytics` | `DATASET_INVALID`, `READ_SCOPE_COMPILE_FAILED` (two files) | `dataset-refusal.ts:128`, `read-scope-refusal.ts:88`, `read-scope-sql.ts:677` | `err.code = CONST` | listed | | `@objectstack/spec` | `CONNECTOR_UPSTREAM_UNAVAILABLE`, `PLUGIN_UI_REQUIRED_KEY_MISSING` | `connector-provider-errors.ts:31`, `plugin.zod.ts:157` | `readonly code = CONST`; `code: CONST` | listed | Every one of the 11 reaches a stamp position. None is a constant that is only compared against, so widening adds no phantom site on this tree. `let`/`var` and lower-case names: zero on this tree. **The other shapes the brief asked about.** - **Object map.** One map holds registered codes: `EXTERNAL_ERROR_CODES` in `packages/spec/src/shared/external-errors.ts`, three codes, all listed under `@objectstack/spec`. No miss. - **Re-exported or imported constant.** Three constants are imported by another package: - `@objectstack/types` to `@objectstack/cloud-connection`: a stamp, and cloud-connection lists the code. This is the row in the table above. - `@objectstack/metadata-core` to `@objectstack/objectql`: a stamp, listed, with the existing `UPDATE_ID_MISMATCH` waiver. - `@objectstack/objectql` to `@objectstack/plugin-sharing`: a `code:` key in a `warn` log payload, not an emission. So attributing a constant to the package that defines it misses no emission today. The gate's header now declares that bound. - **Class fields** (`readonly code = 'X'`): 39 sites, all 39 listed. The header declares this shape blind, and it is not widened here (see Acceptance notes). ## Step 2: the one rule `STAMP_PATTERNS`' `constdef` limb now reads: - `const|let|var NAME = 'CODE'`, for a binding of any name, with an optional type annotation; and - the old `X_CODE = 'CODE'` limb, kept for a keyword-less `*_CODE` binding (a class `static readonly`, or a second declarator). This makes the new rule a superset of the old one. The pattern names stay `objlit`, `assign`, `constdef`. The registered set is still the filter. **Before and after, whole repo, same tree:** | Tree | Rule | Files | Sites | `constdef` | Listed | Waived | Violations | |:--|:--|--:|--:|--:|--:|--:|--:| | `ee3ae0360d` | old (`*_CODE` name) | 2849 | 337 | 37 | 319 | 18 | 0 | | `ee3ae0360d` + new pattern, ledger untouched | new | 2849 | 348 | 48 | 327 | 18 | **3** (mcp x2, types x1) | | merged head `b6ba2fefe6` | old | 2862 | 337 | 37 | 319 | 18 | 0 (plus 1 waiver problem: the new types waiver matches no site, so it cannot outlive the widening) | | merged head `b6ba2fefe6` | new | 2862 | 348 | 48 | 329 | 19 | 0 | `b6ba2fefe6` is the merge of `origin/main` `243dd3c625`; its source is identical to the head `de2fedcb76`, which adds only the changeset. The site-set diff on `ee3ae0360d`, before against after: **0 sites lost, 11 added**, exactly the 11 in the table above. ## Step 3: the ledger - **`'@objectstack/mcp': ['OBJECT_API_DISABLED', 'OBJECT_API_METHOD_NOT_ALLOWED']`.** The row comment records the reachability reading. `enforceApiExposure` stamps the code on the thrown `McpExposureError` (404/405) before every stdio data verb and the ADR-0101 record resource. The stdio data tools flatten the throw to its message (`errorResult(messageOf(err))` in `mcp-http-tools.ts`), so no MCP result carries the code today. That makes it a `door: 'none'` row under the ledger's "door or no door" ruling. - **`PROVENANCE_WAIVERS` gains `@objectstack/types` → `UNIQUE_SCOPE_CONFIRMATION_REQUIRED`, registered under `@objectstack/cloud-connection`.** This is the existing "shared constructor one package over" class, with the same shape as the `UPDATE_ID_MISMATCH` waiver. Types defines the constant, and its one stamp is cloud-connection's install-seam 409, which that package's row already records. Without this entry, the widened gate reds on `main`. **Lane note.** This ledger change adds another lane's entry (`@objectstack/mcp` is `domain:cli`) and a waiver. Per the spec lane's ledger rule, it owes the seat's contract review before the queue. `packages/mcp/**` is not touched: the widening route needed no rename. ## Pins - **`--self-test`.** Four new cases: - a declared constant of any name is a site; - the keyword-less `*_CODE` limb still matches; - a re-assignment to an undeclared non-`*_CODE` name is not a site; - a comparison is not a site. Battery floors were raised from 4 to 6 and from 2 to 4. - **`check-error-code-provenance.test.ts`:** - the widened `constdef` spellings: a bridge-named constant, `export … as const`, `let`, a typed binding; - the superset limb; - the precision pair; - a new block that drives the bridge's two declarations, verbatim, through the scan and the REAL ledger. Both are seen, both are listed, and the red leg runs with the `@objectstack/mcp` key removed: both sites become violations, and no waiver stands in. The block is copied rather than read, so the suite still reads nothing outside its package. - **The card's pin, run end to end (a one-time ablation, not committed).** The input was the committed tree at `b6ba2fefe6`, clean. `node scripts/ablation-replace.mjs` deleted the whole `@objectstack/mcp` block from the ledger: - the anchor went from 1 to 0, and the blob changed (`bde983d345ac` to `a32024110781`); - the real gate then ran (`tsx scripts/check-error-code-provenance.ts`, which imports the ledger from `src/`, with no `dist` on the path) and printed `FAIL — 2 stamp site(s) of a registered code with no provenance row`, naming `stdio-data-bridge.ts:207` and `:208`, exit 1; - the restore was proven: the blob after restore equals HEAD (`bde983d345ac`), and `git diff HEAD` is empty. ## Clause-② (measured on the built declaration closure) After `pnpm --filter @objectstack/spec build`, `dist/error-code-ledger.zod-*.d.ts` declares `readonly '@objectstack/mcp': readonly ["OBJECT_API_DISABLED", "OBJECT_API_METHOD_NOT_ALLOWED"]` on `ERROR_CODE_LEDGER`'s type. The published type gains a key, which widens the public face. Other readings: - `check:api-surface`: `@objectstack/spec public API surface + factory signatures unchanged ✓`. The snapshot records that an export exists, not what it resolves to. - `check:export-origins`: current (5386 exports across 19 entry points). - `ErrorCode` and `REGISTERED_ERROR_CODES` are unchanged, because both codes were already registered under `@objectstack/rest`. - `PROVENANCE_WAIVERS` changes in value only. The claim's `Clause-②: yes` stands. The changeset is `@objectstack/spec: minor`. There are no type-level consumers of the ledger's key set outside `error-code-ledger.test.ts` (`git grep 'typeof ERROR_CODE_LEDGER'`), and no consumer in objectui. ## Verification (head `de2fedcb76`) - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`, then `pnpm --filter @objectstack/spec typecheck`, under the verify lock: **642 files passed, 19165 tests passed, 1 todo**. Typecheck passed: `tsc --noEmit`, `check:scripts-typecheck` (its program compiles both the gate and its test, counted with `--listFilesOnly`) and `check:test-typecheck`. Lock verdict: `VERDICT command-exit 0`. - `pnpm --filter @objectstack/spec run check:error-code-provenance` (self-test and run): `self-test OK — 3 patterns …; 17 case(s) across 5 batteries`, then `scanned 2862 files; 348 registered-code stamp site(s): 329 listed, 19 waived`, `OK — … (10 waiver(s), all live)`. - The derived gate list, re-derived at this head with no paths (`dispatch-gates.mjs --commands`: 91 commands over the 4 changed files): **all 91 ran and exited 0**. `--ran` reconciliation: `91 derived famil(ies) accounted for — 91 run, 0 NOT-MEASURED`. One family, the `check:pm-dispatch-gates` battery (fast tier, 1825 cases pass), was recorded without an exit code. The list includes `check:error-code-casing`, `check:strictness-ledger`, `check:dispatcher-error-vocabulary`, `check:api-surface`, `check:export-origins`, `check:docs` (225 generated files in sync), `check:error-status-conformance`, `check:nul-bytes`, `check:cross-package-test-inputs`, `check:published-files`, `check-changeset-no-major`, `check-adr-0087-registration` (1 non-breaking changeset) and `check-empty-changeset`. Three of them first refused with PREREQUISITE NOT MET (unbuilt packages): `check:doc-formula-expressions`, `check:lean-entry-closure` and `check:dual-build-cjs-loads`. All three exited 0 after `turbo run build` (73 of 73 tasks, 71 cached). - Lint, narrowed and proven: eslint `--no-inline-config --format json` over the three changed `.ts` files reported 3 files, 0 errors and 0 warnings, and none was ignored (no "File ignored" message). `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`), so this diff cannot move the verdict on any untouched file. Repo-wide `pnpm lint` is CI's. ## Acceptance notes (observations, not filed) - **Class fields** (`readonly code = 'X'`) remain a declared blind shape: 39 sites, all 39 listed under their stamping package. Recognising them would add 39 sites and 0 violations. That is a separate widening for a later card. Carrier: none. - **Helper, ternary and table indirections** stay as the header declares them. The unseen-literal census by context: 212 call or array arguments, 46 comparisons, 27 type positions, 14 ternary or `??` operands, 11 table rows, 9 non-`code` object keys, 7 other. These are raw contexts, not adjudicated stamps: comparisons and types are not emissions, and `z.enum` lists are the vocabulary itself. Some sit in packages that do not list the code, for example plugin-auth's `FORBIDDEN`/`NOT_FOUND` passed to its error helpers. Read-only inference: not filed, and no card was opened. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b4ce5e2 commit e04759d

4 files changed

Lines changed: 164 additions & 17 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec): the error-code ledger records `@objectstack/mcp` as an emitter of `OBJECT_API_DISABLED` and `OBJECT_API_METHOD_NOT_ALLOWED` (#22664)
6+
7+
Clause-②: yes
8+
9+
- **`ERROR_CODE_LEDGER` gains the `@objectstack/mcp` owner key.** The MCP stdio data bridge stamps both exposure codes onto the error it throws when an object's `enable` block refuses a data verb: `OBJECT_API_DISABLED` (404) for `apiEnabled: false`, and `OBJECT_API_METHOD_NOT_ALLOWED` (405) for an `apiMethods` whitelist that does not grant the verb. The published type of `ERROR_CODE_LEDGER` gains the key. `ErrorCode` and `REGISTERED_ERROR_CODES` are unchanged, because both codes were already registered under `@objectstack/rest`.
10+
- **`PROVENANCE_WAIVERS` gains one entry:** `@objectstack/types` → `UNIQUE_SCOPE_CONFIRMATION_REQUIRED`, registered under `@objectstack/cloud-connection`. `@objectstack/types` defines the shared constant `GLOBAL_UNIQUE_CONFIRMATION_REQUIRED`. The one stamp is the marketplace install seam's 409 in `@objectstack/cloud-connection`, whose owner key already lists the code.
11+
12+
No code is added, renamed or removed, and no schema moves.

‎packages/spec/scripts/check-error-code-provenance.test.ts‎

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ import {
2222
deriveFindings,
2323
type StampSite,
2424
} from './check-error-code-provenance';
25-
import { ERROR_CODE_LEDGER, type ProvenanceWaiver } from '../src/api/error-code-ledger.zod';
25+
import {
26+
ERROR_CODE_LEDGER,
27+
PROVENANCE_WAIVERS,
28+
type ProvenanceWaiver,
29+
} from '../src/api/error-code-ledger.zod';
2630

2731
const HERE = path.dirname(fileURLToPath(import.meta.url));
2832

@@ -64,6 +68,29 @@ describe('STAMP_PATTERNS (published, one pin per pattern)', () => {
6468
.toEqual([{ code: 'REGISTERED_ONE', pattern: 'constdef', line: 1 }]);
6569
});
6670

71+
it('constdef: a DECLARED constant of any name is a site — the binding, not its name, says "code"', () => {
72+
// The shape the `*_CODE` name rule could not see: a constant named after
73+
// the code it holds (the stdio bridge), exported, `as const`, `let`, typed.
74+
expect(scanSourceText("const OBJECT_API_DISABLED = 'REGISTERED_ONE';", registered))
75+
.toEqual([{ code: 'REGISTERED_ONE', pattern: 'constdef', line: 1 }]);
76+
expect(scanSourceText("export const SELF_REGISTRATION_CLOSED = 'REGISTERED_ONE' as const;", registered))
77+
.toEqual([{ code: 'REGISTERED_ONE', pattern: 'constdef', line: 1 }]);
78+
expect(scanSourceText("let code = 'REGISTERED_ONE';", registered))
79+
.toEqual([{ code: 'REGISTERED_ONE', pattern: 'constdef', line: 1 }]);
80+
expect(scanSourceText("const DATASET_INVALID: RegisteredErrorCode = 'REGISTERED_ONE';", registered))
81+
.toEqual([{ code: 'REGISTERED_ONE', pattern: 'constdef', line: 1 }]);
82+
});
83+
84+
it('constdef keeps the keyword-less *_CODE limb, so the widening is a superset of the name rule', () => {
85+
expect(scanSourceText("static readonly MY_CODE = 'REGISTERED_ONE';", registered))
86+
.toEqual([{ code: 'REGISTERED_ONE', pattern: 'constdef', line: 1 }]);
87+
});
88+
89+
it('constdef reads a declaration: a re-assignment to a non-*_CODE name and a comparison are not sites', () => {
90+
expect(scanSourceText("status = 'REGISTERED_ONE';", registered)).toEqual([]);
91+
expect(scanSourceText("if (e.code === 'REGISTERED_ONE') return;", registered)).toEqual([]);
92+
});
93+
6794
it('an unregistered code is out of population — the dispatcher-vocabulary gate owns it', () => {
6895
expect(scanSourceText("return { code: 'NOT_IN_ANY_LEDGER' };", registered)).toEqual([]);
6996
});
@@ -150,6 +177,48 @@ describe('deriveFindings — the reconciliation, both directions', () => {
150177
});
151178
});
152179

180+
describe('@objectstack/mcp: the stdio bridge\'s exposure constants are seen, and their rows hold them', () => {
181+
// The bridge's two declarations, verbatim (`packages/mcp/src/stdio-data-bridge.ts`),
182+
// driven through the scan and the REAL ledger — copied rather than read, so
183+
// this suite still reads nothing outside its package. The repo-wide CI run of
184+
// the gate is what holds the file itself.
185+
const BRIDGE_SOURCE = [
186+
"const OBJECT_API_DISABLED = 'OBJECT_API_DISABLED';",
187+
"const OBJECT_API_METHOD_NOT_ALLOWED = 'OBJECT_API_METHOD_NOT_ALLOWED';",
188+
].join('\n');
189+
const realRegistered = new Set<string>(Object.values(ERROR_CODE_LEDGER).flat());
190+
const bridgeSites = (): StampSite[] =>
191+
scanSourceText(BRIDGE_SOURCE, realRegistered).map((hit) => ({
192+
file: 'packages/mcp/src/stdio-data-bridge.ts',
193+
package: '@objectstack/mcp',
194+
...hit,
195+
}));
196+
197+
it('the scan sees both constants', () => {
198+
expect(bridgeSites().map((s) => `${s.code} (${s.pattern})`)).toEqual([
199+
'OBJECT_API_DISABLED (constdef)',
200+
'OBJECT_API_METHOD_NOT_ALLOWED (constdef)',
201+
]);
202+
});
203+
204+
it('the real ledger lists both under @objectstack/mcp', () => {
205+
const findings = deriveFindings(bridgeSites(), ERROR_CODE_LEDGER, PROVENANCE_WAIVERS);
206+
expect(findings.violations).toEqual([]);
207+
expect(findings.listed).toHaveLength(2);
208+
});
209+
210+
it('RED LEG: with the @objectstack/mcp rows removed the gate reports both sites — no waiver stands in', () => {
211+
const withoutMcp = Object.fromEntries(
212+
Object.entries(ERROR_CODE_LEDGER).filter(([owner]) => owner !== '@objectstack/mcp'),
213+
);
214+
const findings = deriveFindings(bridgeSites(), withoutMcp, PROVENANCE_WAIVERS);
215+
expect(findings.violations.map((v) => `${v.package} → ${v.code}`)).toEqual([
216+
'@objectstack/mcp → OBJECT_API_DISABLED',
217+
'@objectstack/mcp → OBJECT_API_METHOD_NOT_ALLOWED',
218+
]);
219+
});
220+
});
221+
153222
describe('rows the scan cannot see, pinned by hand (the declared helper-call blind spot)', () => {
154223
// The gate's header declares it BLIND to a helper indirection (a
155224
// `makeError(code, …)` call site) and makes widening its patterns a card of

‎packages/spec/scripts/check-error-code-provenance.ts‎

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,26 @@
5555
* `apps/`, not `examples/`, not package `scripts/` trees.
5656
* - The ledger file itself is excluded by name: its waiver tables spell
5757
* `code: '…'` about codes, which is mention, not stamping.
58+
* - A CONSTANT is read at its definition, by its initializer, whatever its
59+
* name (`constdef`). The rule used to be the NAME — a `*_CODE` binding —
60+
* and that proxy missed eleven declared constants in five packages, three
61+
* of them with no provenance record at all (#22664). Two consequences are
62+
* declared rather than left implicit: the site is attributed to the
63+
* package that DEFINES the constant, so a second package that imports it
64+
* and stamps it is not seen as a second emitter (a shared constant one
65+
* package over is the ledger's waiver class:
66+
* `UNIQUE_SCOPE_CONFIRMATION_REQUIRED`, `UPDATE_ID_MISMATCH`); and a
67+
* constant that is only ever COMPARED against is still a site — the
68+
* over-match bound below, measured at zero such constants when the rule
69+
* widened.
5870
* - BLIND, and inheriting the sibling gate's declared blindness rather than
59-
* re-litigating it: a code arriving through a constant NOT named `*_CODE`
60-
* (`GLOBAL_UNIQUE_CONFIRMATION_REQUIRED` in `@objectstack/types` defines a
61-
* registered code and is invisible here), an object-literal or helper
62-
* indirection (`{ code }` shorthand, `makeError(code, …)` call sites), a
63-
* template literal, and a class field. Those shapes DO have recognizers in
64-
* the sibling gate; a registered code reaching a stamp through one of them
65-
* is simply not this gate's finding yet. Widening is a gate-population
66-
* change with an unmeasured blast radius — its own card, never a rider.
71+
* re-litigating it: an object-literal or helper indirection (`{ code }`
72+
* shorthand, `makeError(code, …)` call sites, a `cond ? 'A' : 'B'` or
73+
* `?? 'X'` operand), a template literal, and a class field. Those shapes
74+
* DO have recognizers in the sibling gate; a registered code reaching a
75+
* stamp through one of them is simply not this gate's finding yet.
76+
* Widening is a gate-population change with an unmeasured blast radius —
77+
* its own card, never a rider.
6778
* - OVER-matching is accepted and absorbed by rows/waivers rather than
6879
* heuristics: a TYPE-position literal (`{ code: 'ITEM_LOCKED'; reason:
6980
* string }`) matches the object-literal pattern. That is deliberate — the
@@ -109,10 +120,20 @@ export const STAMP_PATTERNS: ReadonlyArray<{ name: string; re: RegExp }> = [
109120
{ name: 'objlit', re: /\bcode:\s*'([A-Z][A-Z0-9_]*)'/g },
110121
// `err.code = 'X'` — stamped onto a value about to be thrown.
111122
{ name: 'assign', re: /\.code\s*=\s*'([A-Z][A-Z0-9_]*)'/g },
112-
// `X_CODE = 'X'` — a `*_CODE`-named constant's literal initializer (the
113-
// driver-memory `UNIQUE_VIOLATION_CODE` shape). The optional `[^=\n]*?`
114-
// limb admits a type annotation between name and `=`.
115-
{ name: 'constdef', re: /\b[A-Z][A-Z0-9_]*_CODE\s*(?::[^=\n]*?)?=\s*'([A-Z][A-Z0-9_]*)'/g },
123+
// A constant whose literal initializer is a registered code: the binding is
124+
// the site, whatever it is named. Two limbs:
125+
// - `const|let|var NAME = 'X'` — a DECLARED binding of any name (the
126+
// `@objectstack/mcp` stdio bridge's `const OBJECT_API_DISABLED = …`, the
127+
// plugin-auth `SELF_REGISTRATION_CLOSED` shape). The registered set is
128+
// the filter, so the name no longer has to say "code".
129+
// - `X_CODE = 'X'` — a `*_CODE`-named binding with no keyword in front (a
130+
// class `static readonly`, a second declarator), kept so the widening
131+
// is a superset of the name rule it replaced, never a trade.
132+
// The optional `[^=\n]*?` limb admits a type annotation between name and `=`.
133+
{
134+
name: 'constdef',
135+
re: /(?:\b(?:const|let|var)\s+[A-Za-z_$][\w$]*|\b[A-Z][A-Z0-9_]*_CODE)\s*(?::[^=\n]*?)?=\s*'([A-Z][A-Z0-9_]*)'/g,
136+
},
116137
];
117138

118139
export interface StampSite {
@@ -301,8 +322,9 @@ export function deriveFindings(
301322

302323
function printBounds(): void {
303324
console.log('bounds: packages/**/src non-test .ts/.tsx; ledger file excluded (mention, not stamping);');
304-
console.log(`bounds: patterns = ${STAMP_PATTERNS.map((p) => p.name).join(', ')} — blind to non-*_CODE constants,`);
305-
console.log('bounds: helper/shorthand indirections, templates and class fields (see the header; sibling-gate shapes).');
325+
console.log(`bounds: patterns = ${STAMP_PATTERNS.map((p) => p.name).join(', ')} — a constant is read at its definition`);
326+
console.log('bounds: and attributed to the DEFINING package; blind to helper/shorthand/ternary indirections, templates');
327+
console.log('bounds: and class fields (see the header; sibling-gate shapes).');
306328
}
307329

308330
function run(report: boolean): number {
@@ -411,8 +433,8 @@ let selfTestReachedVerdict = false;
411433
// The counts are a FLOOR, not an equality: adding cases is ordinary work and
412434
// must not red. A battery BELOW its floor means cases stopped running.
413435
const SELF_TEST_BATTERIES: Readonly<Record<string, number>> = Object.freeze({
414-
'each published STAMP pattern catches its own spelling': 4,
415-
'the population boundary and comment masking: what is NOT a site': 2,
436+
'each published STAMP pattern catches its own spelling': 6,
437+
'the population boundary and comment masking: what is NOT a site': 4,
416438
'the reconciliation: an unlisted stamper reddens, a listed one is green': 2,
417439
'a waiver admits EXACTLY its (package, code) pair': 2,
418440
'every stale-waiver direction reddens': 3,
@@ -492,13 +514,34 @@ function selfTest(): number {
492514
'constdef admits a type annotation',
493515
scanSourceText(`const MY_CODE: string = '${CODE_A}';`, registered).some((h) => h.pattern === 'constdef'),
494516
);
517+
// The binding is the site whatever it is named — the stdio-bridge shape the
518+
// `*_CODE` name rule could not see.
519+
check(
520+
'constdef catches a declared constant of ANY name',
521+
scanSourceText(`const REFUSAL = '${CODE_A}';`, registered).some((h) => h.pattern === 'constdef'),
522+
);
523+
// …and the keyword-less `*_CODE` limb keeps the widening a superset.
524+
check(
525+
'constdef keeps a keyword-less *_CODE binding',
526+
scanSourceText(`static readonly MY_CODE = '${CODE_A}';`, registered).some((h) => h.pattern === 'constdef'),
527+
);
495528
// Population boundary: a code outside the registered set is the sibling
496529
// gate's subject, never a site here.
497530
battery('the population boundary and comment masking: what is NOT a site');
498531
check(
499532
'a code outside the registered set is out of population',
500533
scanSourceText(`return { code: '${CODE_OUT}' };`, registered).length === 0,
501534
);
535+
// Precision of the widened `constdef`: it reads a DECLARATION, so neither a
536+
// re-assignment to an undeclared non-`*_CODE` name nor a comparison is one.
537+
check(
538+
'a re-assignment to a non-*_CODE name is not a site',
539+
scanSourceText(`status = '${CODE_A}';`, registered).length === 0,
540+
);
541+
check(
542+
'a comparison against a registered code is not a site',
543+
scanSourceText(`if (e.code === '${CODE_A}') return;`, registered).length === 0,
544+
);
502545
// Comment masking: a code quoted in prose is not a site.
503546
check(
504547
'a commented stamp is not a site',

‎packages/spec/src/api/error-code-ledger.zod.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,19 @@ export const ERROR_CODE_LEDGER = {
11271127
'RAW_SQL_UNSUPPORTED',
11281128
'READ_SCOPE_COMPILE_FAILED', // [#5367] RLS read-scope lowering failed fail-closed — a SERVER fault (500), never the caller's
11291129
],
1130+
'@objectstack/mcp': [
1131+
// [#22664] The stdio data bridge's exposure gate (`stdio-data-bridge.ts`,
1132+
// `enforceApiExposure`) stamps the data door's two exposure codes onto the
1133+
// thrown `McpExposureError.code` — 404 for `apiEnabled: false`, 405 with
1134+
// `allowedOperations` for a whitelist that does not grant the verb — before
1135+
// every stdio data verb and the ADR-0101 record resource. The stdio data
1136+
// tools flatten the throw to its message (`errorResult(messageOf(err))`),
1137+
// so no MCP result carries the code today: a `door: 'none'` row under the
1138+
// "door or no door" ruling above. `@objectstack/rest` keeps its own rows
1139+
// for the data door.
1140+
'OBJECT_API_DISABLED',
1141+
'OBJECT_API_METHOD_NOT_ALLOWED',
1142+
],
11301143
'@objectstack/service-datasource': [
11311144
'DATASOURCE_ADMIN_ERROR', // lifecycle/validation refusal from the datasource-admin service
11321145
'EXTERNAL_DATASOURCE_ERROR', // introspection/connection-test refusal from the external-datasource service
@@ -1871,6 +1884,16 @@ export const PROVENANCE_WAIVERS: readonly ProvenanceWaiver[] = [
18711884
'throws are served under the emitting doors\' own registrations (runtime\'s ' +
18721885
'dispatcher exits, rest\'s `mapDataError` — both packages list the code).',
18731886
},
1887+
{
1888+
package: '@objectstack/types',
1889+
code: 'UNIQUE_SCOPE_CONFIRMATION_REQUIRED',
1890+
registeredUnder: '@objectstack/cloud-connection',
1891+
reason: 'Shared constant one package over: `GLOBAL_UNIQUE_CONFIRMATION_REQUIRED` ' +
1892+
'(unique-scope-install-gate.ts) spells the string beside the ADR-0120 D5e posture ' +
1893+
'gate it names, and the one stamp is the marketplace install seam\'s 409 ' +
1894+
'(`marketplace-install-local-plugin.ts`), which answers it on the plugin\'s own Hono ' +
1895+
'route — the cloud-connection row records that door. Types ships no door.',
1896+
},
18741897
{
18751898
package: '@objectstack/core',
18761899
code: 'ANALYTICS_DATE_RANGE_UNRECOGNIZED',

0 commit comments

Comments
 (0)