Repository navigation
fix(metadata-protocol): refusals, hints and log lines state each decision in words instead of a tracker number (stage 2) - #20830
Conversation
…ds instead of a tracker number (stage 2, author-visible) The thrown refusals, the stored-type preflight and revert refusals, and the schedule-flow organization hint no longer send the reader to a tracker number: each says what was decided, or loses only the citation where the sentence already said it. Text only: no code, field, status or export moves. The prose-id ledger is recomputed with --census-ledger; only metadata-protocol rows move. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
…each decision in words instead of a tracker number (stage 2, log lines) The kernel:ready index migrations, the seed/API tenancy repair and its receipt, the three migration-skipped warnings, and the protocol's warn and error lines no longer cite a tracker number. Where the sentence already said what was decided, only the citation goes; the three skipped warnings now say what the migration that did not run would have ensured. Two tests that pinned a number now pin the sentence. Text only. The ledger is recomputed; only metadata-protocol rows move. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
…s its tracker number; changeset (stage 2) The last metadata-protocol row leaves the prose-id ledger: the testkit's isolation error already says what it guards, so only the citation goes. The ledger is recomputed with --census-ledger and holds no metadata-protocol row. Changeset: @objectstack/metadata-protocol patch. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 793427441801fea69c2cd1f38a9b98fc64d32086 && git checkout 793427441801fea69c2cd1f38a9b98fc64d32086
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 73155fedcacc215565c4eef6d9899977e0707010 442473234551d7cb909e2a927d1adc446a9b4e35 && git checkout -B drift-repro 73155fedcacc215565c4eef6d9899977e0707010 && git merge --no-ff 442473234551d7cb909e2a927d1adc446a9b4e35
node scripts/docs-audit/affected-docs.mjs --json 73155fedcacc215565c4eef6d9899977e0707010
|
Contract reviewServed-tier: Read-only, at tier, adversarial to the dispatch. Inputs: card #20513 (body and all 17 comments — the census 5900801368, the ruling 5902360492 「20513 A」 A / A, the lane checklist 5902678544, the stage-2 claim 5907730876, the stage-2 Check-runs on ① Derived judgmentsScope against the ruling and the claim — right. The ruling orders stages per package, all three categories, form D, Text only — right, checked line by line. All 108 changed lines in the 11 Accept-set and public surface — nothing moves — right. No schema, route, wire Ledger diff — right, exact. Nine Form D, string by string — right. Every cited card was read: 20 closed
Pins — right, 3 lines in 2 files. ② Semver level
Clause-②: no — right. Nothing an author can write is widened or narrowed; the PR body carries ③ Boundary flagsDev flags (report 5908549372), each answered:
One boundary note, not a FAIL item — a docs page quotes the old sentence. Implemented-by: VERDICT: PASS Generated by Claude Code |
…ommits that decided them (objectstack-ai#20836) Part of objectstack-ai#20596 Clause-②: no ## What changed This is the fifteenth stage of the `domain:services` lane of the dead-citation sweep. It covers `packages/services/service-settings/src/**` and nothing else. By the seat's claim (`5908460751`), it is the largest package left in the lane. Later stages cover the other packages, so this PR says `Part of` and the card stays open. Every comment or docblock site in scope that cited a tracker number answering 404 has been rewritten in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123), by the method of stages 1 to 14 (the latest is PR objectstack-ai#20816, landed as `73155fedc`). That is **11 sites on 11 lines in 9 files, covering 4 numbers**: - 4 census sites (every census site this package has at the base); - 7 sites in test comments, which the census defers. One of their numbers, `objectstack-ai#11318`, stands only in test files here; it was read on its own and answers 404. Each rewritten line now cites the commit in this repository that decided what the line describes, and says in its own words what was decided: **4 distinct commit shas**. No ADR records any of the four decisions (see the per-number table), so ruling C's commit rung applies. No number was dropped. Only comments changed. Every touched source file keeps its line count (11 lines out, 11 in, over 9 files), so no line citation into these files moves. All 11 changed lines carried a dead citation. No code token moves (see the guard below). **No citation number is added.** The only tracker number on an added line is the live `objectstack-ai#10251`, once, in `settings-prebind-read-warning.test.ts:17`. It already stood on that line, and it now sits beside the sha as the convenience link ruling C allows: 「(commit 1ec36b7, PR objectstack-ai#10251)」. `1ec36b730` is that pull request's squash commit. 2 dead sites are left on purpose: a test title and a test assertion message (see the list below). One more file: a `patch` changeset for `@objectstack/service-settings`, because the rewritten prose ships (see Changeset below). ## Census: `service-settings`, before and after **Instrument (A1).** The gate's own `node scripts/check-issue-citations.mjs --census --json`, read-only and unchanged. The count below is its `allocated-but-absent` findings under `packages/services/service-settings/`. Each run counts as a reading only because its board frontier equals the newest issue or pull-request number, read by a separate request just before and just after the run. | reading | tree | board | whole-repo `allocated-but-absent` | service-settings sites | lines | files | numbers | |---|---|---|---|---|---|---|---| | before | base `73155fedc`, run 2026-09-30T09:39:38Z to 09:43:17Z | enumerated, 187 pages, frontier objectstack-ai#20830 (newest objectstack-ai#20830 before and after) | 752 | **4** | 4 | 4 | 3 | | after | head `ac05607d6`, run 10:02:17Z to 10:06:00Z | enumerated, 187 pages, frontier objectstack-ai#20834 (newest objectstack-ai#20834 before and after) | 748 | **0** | 0 | 0 | 0 | The whole-repo drop is 4, exactly this diff's census sites. The `resolves` tally is 33,134 in both runs, and `resolves-as-pull-request` (1,985) and `cross-repo-unjudged` (1,018) did not move either. Neither run was truncated or discarded: both enumerations read 187 pages at the newest frontier. The seat's census counted 4 here at `6bff748b`, and the base agrees: `6bff748b` is an ancestor of the base, and no commit between them touches this package's `src`. **Supplementary instrument, the whole scope.** The census does not read test files or strings, and this stage's scope includes test comments. So a second reading runs the gate's own exported `extractCitations` (whole-file and comment-prose projections) and `namesThisRepository` over every `.ts` file under `service-settings/src` (64 files). It takes its verdicts from the before census's own board reading rather than from a second enumeration: a number is dead when that census reported it `allocated-but-absent`, and alive when the gate's own census-scope extraction judged it and the census did not report it. 10 numbers are covered by neither, because they stand only in test files, or as the second number of an `#A/#B` pair. Each was read on its own through the read-only tools. 1 answers 404 (`objectstack-ai#11318`, on the issue and the pull-request endpoint alike); 6 answer as issues; 3 answer as pull requests (`objectstack-ai#7554`, `objectstack-ai#10251`, `objectstack-ai#5133`). The probe's control: the known issue `objectstack-ai#11352` answers 404 on the pull-request endpoint. | reading | citations | dead | src comment | test comment | src string | test string | |---|---|---|---|---|---|---| | before, `73155fedc` | 534 | **13** | 4 | 7 | 0 | 2 | | after, `ac05607d6` | 523 | **2** | 0 | 0 | 0 | 2 | Its src-comment column equals the census's 4, which is the control on the second instrument. The 519 live citations and 2 cross-repo citations are the same in both readings, and the drop of 11 citations is exactly the rewritten sites. A third, raw reading (every `#` followed by 2 to 6 digits, whatever surrounds it) finds 547 occurrences before and 536 after, the same drop of 11. The 13 tokens beyond the gate's grammar are the same before and after, and none is dead (see Acceptance notes). ## Per-number table Sites and files count every dead occurrence in scope at the base (comments and strings, tests included). `rewritten / left` counts the sites rewritten and the sites left. Each anchor was read in its message and diff, not only its subject. | number | sites / files | rewritten / left | anchor: what it decided | |---|---|---|---| | `objectstack-ai#13279` | 4/4 | 4/0 | `6a180e42d` (PR objectstack-ai#13475): `resolveAuthzContext` raises `AuthzStoreUnavailableError` (`SERVICE_UNAVAILABLE`, 503) when a permission-store read throws, instead of answering an outage as a caller with zero capabilities, and each production transport's fail-closed `catch` re-raises that brand. The settings plugin's `verifiedContextFromRequest` is one of them. Its message names `objectstack-ai#13279` 4 times and its diff 54 times; `git blame` puts `settings-service-plugin.ts:307` in it, and the other three lines were written by `ac9376a74` (PR objectstack-ai#16580), a descendant, which describes that re-raise. Stage 6's anchor, reused by the storage, datasource and analytics stages | | `objectstack-ai#10159` | 3/2 | 3/0 | `1ec36b730` (PR objectstack-ai#10251): a settings write issued before the engine is bound is refused with `SETTINGS_ENGINE_NOT_BOUND` (503). Its message states that every read in any state is unchanged, which is the "left reads open" all three lines describe. The message does not name `objectstack-ai#10159`, but its own diff does, once, in its changeset ("refused loudly instead of resolving successfully while nothing reaches `sys_setting` (objectstack-ai#10159)"), and `settings-prebind-read-warning.test.ts:17` already paired the two numbers. `git blame` puts the three lines in `a24b7fa4d` (PR objectstack-ai#11044), the later read-half fix, a descendant. New to the sweep | | `objectstack-ai#17062` | 3/2 | 2/1 | `50b6f17d4` (PR objectstack-ai#17071): adds the package-local route-ledger conformance guard beside the dogfood live-mount parity gate, and updates the ledger header that had said such a guard was deliberately omitted. Its message does not name `objectstack-ai#17062`; its diff does, on 3 added lines, which are the three sites here (`git blame` puts all three in it). New to the sweep | | `objectstack-ai#11318` | 3/1 | 2/1 | `99ccbb9c8` (PR objectstack-ai#11467): the Settings, AI "Test connection" fallback keeps its mount instruction on all three real-provider branches and gains the cloud-only boundary read from `PLATFORM_CAPABILITY_PROVIDERS.ai`. Its own changeset states that "the embedder hint at the fourth site is deliberately left alone ... and pinned by a contrast test", which is the fence `:339` describes. Its message's trailer names `objectstack-ai#11318` as the issue it answers, its diff names the number 3 times, and `git blame` puts all three lines in it. New to the sweep | Every cited sha matches exactly one commit (`git rev-parse --disambiguate`, count 1 for each of the 4), and all 4 are ancestors of the base (`merge-base --is-ancestor`, exit 0 for each; reverse leg, base against each anchor, exit 1 for each; control legs exit 0: stage 1's landing `422db788a`, and the repository's root commit, which lies deeper than every anchor; the history is complete, `--is-shallow-repository` false, 15,193 commits). Each of the 4 numbers answers 404 on the issues endpoint, which serves pull requests too, read one by one. No ADR, `scripts/adr-anchors/` file or other `docs/` page records the decision of any of the 4: `docs/adr` names none of the numbers, and none of their mechanisms (`AuthzStoreUnavailableError`, `SETTINGS_ENGINE_NOT_BOUND`, `engineBindPending`, the settings route ledger, the AI hint's edition boundary). ## Wordings to check - **Tag swaps in place.** 「[objectstack-ai#13279]」 became 「[commit 6a180e4]」 (`settings-service-plugin.ts:307`); 「(objectstack-ai#13279)」 became 「(commit 6a180e4)」 on 2 lines; 「objectstack-ai#13279's permission-store re-raise」 became 「commit 6a180e4's permission-store re-raise」. These are the forms the storage and datasource stages used for the same sha. - **`objectstack-ai#10159`.** 「is why objectstack-ai#10159's fix deliberately left reads open」 became 「is why commit 1ec36b7's write refusal deliberately left reads open」; 「(objectstack-ai#10159's fix left reads open on purpose)」 became 「(commit 1ec36b7 left reads open on purpose)」; 「(objectstack-ai#10159 / PR objectstack-ai#10251)」 became 「(commit 1ec36b7, PR objectstack-ai#10251)」, with the pull-request number kept as the convenience link beside its own squash commit. - **`objectstack-ai#17062`.** 「Two layers, since objectstack-ai#17062.」 became 「Two layers, since commit 50b6f17.」 The docblock goes on to describe the conformance test that commit added as the second layer. - **Two headers keep the antecedent of the prose below them**, the form stage 14 used: - `settings-route-ledger.conformance.test.ts:4`: 「Settings route-ledger conformance (objectstack-ai#17062)」 became 「Settings route-ledger conformance (the issue behind commit 50b6f17)」, because `:25` of the same docblock says 「(per the issue)」. - `manifests/ai.manifest.test.ts:277`: 「objectstack-ai#11318 —」 became 「The card behind commit 99ccbb9:」, because `:288` 「the very claim this card is about」 and `:329` 「The un-followable form this card retired」 speak of that card. - **`ai.manifest.test.ts:339`.** 「deliberately not edited — objectstack-ai#11318 fences this site out by name」 became 「... — commit 99ccbb9 fences this site out by name」. The commit's own changeset names that site (quoted in the table). ## The 2 sites left - **Test strings, 2 sites on 2 lines**, left as stages 1 to 14 left theirs: - `manifests/ai.manifest.test.ts:292`, a `describe` title (`objectstack-ai#11318`); - `settings-route-ledger.conformance.test.ts:88`, the assertion message a failing run prints (`objectstack-ai#17062`). It is a string, not a comment, and form C does not touch strings. - No operator log string, runtime refusal, quoted maintainer ruling or generated file in this package carries a dead number. - **Outside `src`, listed and left, not edited in this stage:** - the shipping `README.md` names only the live `objectstack-ai#8026`; - `vitest.config.ts` names only live numbers (`objectstack-ai#8020`, `objectstack-ai#8030`, `objectstack-ai#8063`, `objectstack-ai#8104`, `objectstack-ai#10374`); - `tsconfig.json` and `package.json` name none; - the release-owned `CHANGELOG.md` names `objectstack-ai#13279` and `objectstack-ai#10159` on 2 lines, the entries of `6a180e4` and `1ec36b7`, which are this PR's anchors. ## Mechanical guard: no code token moves The guard compares, base `73155fedc` against head, over all 9 touched `.ts` files: - **Reading 1**, the TypeScript parser's leaf nodes (a `forEachChild` walk, so comments are trivia and JSDoc nodes are never visited). String and template literals are therefore read in full. - **Reading 2**, the full token stream in parser context (a `getChildren` walk, so punctuation and keywords are included; JSDoc nodes skipped). Results: - Real run at the final head `ac05607d6`: 9,999 base leaf tokens, **0 files with a token change** on either reading (exit 0). The first commit `ff7ef46f4` gave the same, and no `.ts` path changed after it. - Comment control in `settings-service.ts` (「deliberately left reads open.」 to 「deliberately kept reads open.」): 0 files changed, as expected (exit 0). - Positive control, a code token renamed in `settings-service-plugin.ts` (`isAuthzStoreUnavailableError(err)` to `isAuthzStoreUnavailableErrorX(err)`): DIFFER on the identifier (exit 1). - Positive control, one digit changed inside the kept test title `ai.manifest.test.ts:292` (`objectstack-ai#11318` to `objectstack-ai#11319`): DIFFER on the string literal (exit 1). Every mutation went through `scripts/ablation-replace.mjs` (wrap mode) under a shell trap that restores by absolute path, and each landed (anchor 1 to 0, blob changed). Each restore was proven byte-identical to the HEAD blob (`1248149a428d`, `a2fac9ad1f0b`, `79c2b40a48a6`), with `git diff HEAD` empty and a clean tree afterwards. ## Changeset This change ships bytes, so a `patch` changeset for `@objectstack/service-settings` (`.changeset/20596-service-settings-provenance-anchors.md`) is included. Its body is stages 12 and 13's commit-anchor text, word for word, with the package name changed. Measured on the built package (A3), after a full workspace build in which this package was a cache miss (71 of 71 tasks, 0 cached, at `ff7ef46f4`, which holds every source-line change): `files[]` is `dist`, `README.md` and `CHANGELOG.md`, and the package is not private. - The rewritten `settings-service.ts:685` docblock, on the pre-bind read reporter, is in all four entries: `dist/index.js`, `dist/index.cjs`, `dist/index.d.ts` and `dist/index.d.cts` (once each). - The other three rewrites (`settings-route-ledger.ts:17`, `settings-routes.ts:72`, `settings-service-plugin.ts:307`) are stripped by the bundle, and the other seven sit in test files. - Positive controls, the unchanged line beside each rewrite, land exactly where their neighbours do: the line before `settings-service.ts:685` once in each of the four entries, and the neighbours of the three stripped rewrites 0 everywhere. - A never-written negative phrase appears nowhere in `dist`, and none of the four old numbers is left there. The later commit adds only the changeset. ## Gates (final head `ac05607d6`) - **Citation judging, as CI runs it:** `pnpm check:issue-citations` exits 0 (self-test, 114 cases, 8 batteries). `node scripts/check-issue-citations.mjs` exits 0: 「no issue citations added against 73155fe (4 file(s) read)」. - **Doc authoring:** `pnpm check:doc-authoring` exits 0 (the sibling-package prose-id baseline holds, no growth). - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `ac05607d6` derived 63 commands, the same 63 as at dispatch. - Each ran with its exit code captured before any pipe, and all 63 exit 0; none exited 3. - `--ran`, fed each command with its exit code, reports 63 run, 0 NOT MEASURED (a derived zero), 0 unrun, and exits 0. - The full `turbo run build` above ran first under the shared verify lock, so no gate hit an unbuilt workspace. - **Roster families the derivation lists outside its commands** (their rosters sit in directories this diff touches): `node scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit 0. - **Tests and typecheck, under the verify lock, at `ac05607d6`:** - `pnpm --filter @objectstack/service-settings test`: 33 files pass and 584 tests pass, which is every tracked test file under `src/`, the 5 touched ones included. - `pnpm --filter @objectstack/service-settings typecheck` (`tsc --noEmit`) exits 0, and `tsc --listFiles` puts all 9 touched files in the program. - **Lint, as a proven narrowing:** eslint with inline config disabled, over the 9 touched `.ts` files, gives 9 files, 0 errors and 0 warnings (its `--format json` output). All 9 are in eslint's own population (none reported ignored; `dist/index.js`, the control, reads ignored). `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, as its own lines 327-328 state), so a comment edit here cannot move the verdict on any untouched file. The repo-wide `pnpm lint` is CI's run. - **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of the 10 changed files for control bytes finds none. ## Acceptance notes - **The gate-invisible spellings, grepped as the claim asked** (objectstack-ai#20636, including the `clause #N` position). At the base, `#N-word` is on 0 lines. `#A/#B` is on 11 lines (12 second numbers), and every second number is live: `objectstack-ai#6580`, `objectstack-ai#5094`, `objectstack-ai#11230`, `objectstack-ai#5480`, `objectstack-ai#5932`, `objectstack-ai#6199` and `objectstack-ai#5204` by the census's own judgement, and `objectstack-ai#5133` read on its own as a pull request. `option #N`, `clause #N` and URL-spelled links are on 0 lines. So the claim's 0 / 11 / 0 / 0 hold, and nothing dead hides behind them. The one other raw token beyond the grammar is the colour literal `'#6366f1'` in `manifests/branding.manifest.ts:32`. - **「The card」 phrases.** 38 lines in 18 files under this package's `src` speak of 「the card」 or 「this card」. They carry no number, and neither instrument sees them. The ones whose antecedent this diff would have removed are handled above; the rest are unchanged, as in stages 8 to 14. - **The census instrument did not truncate in this stage.** Both enumerations read 187 pages at the newest frontier. - **Anchors the next stages can reuse**, each checked here: `objectstack-ai#10159` → `1ec36b730`; `objectstack-ai#17062` → `50b6f17d4`; `objectstack-ai#11318` → `99ccbb9c8`; and the reused `objectstack-ai#13279` → `6a180e42d`. - **Base.** The branch is on `main` at `73155fedc`. `main` has since moved two commits (`4b45afaed`, `15b586dcf`). Neither touches `packages/services/service-settings`, `scripts/check-issue-citations.mjs`, `.changeset/config.json` or a path in this diff. `15b586dcf` moves `packages/spec/liveness/**`, a gate input this comment-only diff cannot interact with. No merge was taken; the merge queue rebuilds on the merged generation. --- _Generated by [Claude Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…urce against the deployment's SDUI manifest (objectstack-ai#20852) Part of objectstack-ai#20312 Clause-②: yes (narrowing — on a host that registers a manifest, the runtime metadata save door newly refuses an html page whose source uses a component the manifest does not declare, or whose `requires` disagrees with its source; the new exported `SDUI_MANIFEST_SERVICE` widens `@objectstack/metadata-protocol`) ## Summary Stage ① (the channel) and stage ② (save-time compile and refusal) of ruling 5881821895 (letter A), merged into one `domain:cli` PR by dispatch pointer 5902497015 (ruling 5902378057 on objectstack-ai#20542, A + E). Stage ③ (the load-time report, the ledger row, the describe and the docs) is this card's own follow-on and is not in this PR. - **The channel.** `@objectstack/metadata-protocol` exports one constant service key, `SDUI_MANIFEST_SERVICE = 'sdui-manifest'`. It is a plain key, not a `CoreServiceName` slot, and it needs no spec edit. `os serve` resolves the deployment's manifest once at boot through the CLI's existing `resolveSduiManifest(path.dirname(configPath))` and registers the result under that key with `kernel.registerService`, before any plugin inits (`packages/cli/src/commands/serve.ts`, helper `registerDeploymentSduiManifest` in `packages/cli/src/utils/sdui-manifest.ts`). The protocol reads the key on every publish (`resolveSduiManifest` on the protocol, the `resolveFlowCanonicalizer` pattern) and passes `sduiManifest` into `evaluateRuntimeAuthoringGate`. - **Stage ②.** With a usable manifest, the gate compiles a `kind: 'html'` page's `source` (and the deprecated `'jsx'` spelling) with `@objectstack/sdui-parser`'s `compile()`, the compiler behind the CLI-only `validateJsxPages` rule, imported and not re-implemented. It lives beside the gate's existing `sduiManifest` option, as a gate-local judgement in the same shape as the platform-schedule refusal: `findHtmlPageSourceGaps` in `runtime-authoring-gate.ts`. Compiler errors refuse the publish with the existing `422 INVALID_METADATA` envelope, under the CLI's own rule ids (`jsx-forbidden-tag`, `jsx-unknown-component`, and so on). Each issue's `where` and `message` name the component. Compiler warnings ride `advisories`. A hand-written `requires` that disagrees with the compiled one is refused under `page-requires-disagrees-with-source`, and the message names each namespace: one no manifest component carries, one the source does not use, or one the source uses but the list leaves out. `saveMetaItem` stamps `requires` from the compile (`stampHtmlPageRequires`), on a draft save too. A draft that does not compile, or whose `requires` disagrees, is stored as written, because drafts are not gated (objectstack-ai#4463 D1), and its publish refuses it. No new error code. - **The boot line.** A host that resolves no manifest (`absent`), or resolves an unusable one, registers nothing. It prints one line, `Page source and \`requires\` not validated at save: …`, naming the file and reason or every place looked, and the boot continues. The save door then stores html pages exactly as before. A registered value that has no `components` map gets one warning from the protocol and is never compiled against. ## Declared cross-lane touch `packages/metadata-protocol` (`domain:engine`): `runtime-authoring-gate.ts` (the key, the gate-local compile, the stamp helper), `protocol.ts` (the per-publish read, the argument into `evaluateRuntimeAuthoringGate`, the stamp in `saveMetaItem`), and one export line in `index.ts`. There is no new file under `packages/metadata-protocol/src`: the pins sit in the existing `protocol.runtime-authoring-gate.test.ts`. None of open PR objectstack-ai#20830's one-line text edits is touched. A local merge of its head onto this branch is clean (`git merge-tree` exit 0). **Deviation from the claimed file surface (declared):** `packages/metadata-protocol/package.json` gains `"@objectstack/sdui-parser": "workspace:*"`, and `pnpm-lock.yaml` gains its importer line. The claim says "the compile the save door runs is the existing one, imported". Under pnpm's strict layout that import does not resolve unless the package declares the dependency. The alternative imports are closed: the gate may reach `@objectstack/lint` only through `/runtime`, which must not export `validateJsxPages`, and the wiring guard forbids a registry rule named at the gate. `@objectstack/sdui-parser` has zero dependencies and never executes source, so the kernel boot-path contract in `runtime-lazy-deps.test.ts` (never `typescript` or `sucrase`) is untouched. ## Measurements (measurement came first) **False-refusal rate over stored html pages:** 0 of 3, measured at objectstack `15b586dc` before the refusal was written. The measurement compiled every `kind: 'html'` / `'jsx'` page in the repository with `@objectstack/sdui-parser` `dist` against the pinned console's `sdui.manifest.json` (107 components). That manifest is the one `os serve` hands a served example, through the console copy. The population is the 3 showcase pages, the only authored html pages among the 25 `*.page.*` files. Hand-written `requires`: 0. | page | ok | compiled `requires` | errors | warnings | |:--|:--|:--|:--|:--| | `showcase_capability_map` | true | `['ui']` | 0 | 0 | | `showcase_command_center_jsx` | true | `['ui']` | 0 | 0 | | `showcase_start_here` | true | `['ui']` | 0 | 0 | Control: `compile('PLUGIN-NONEXISTENT tag')` answers `ok: false` with `forbidden-tag` and `unknown-component`. Test fixtures, which are not stored pages: 3 html sources in `metadata-protocol` / `metadata-core` tests use a bare `div`, which this manifest does not declare (the `ui-html-page-div-refused` ledger entry). They would be refused only on a host with a registered manifest, and none of those tests registers one. **The console fallback's `exports` failure (ruling ①).** The subpath specifier `@objectstack/console/dist/sdui.manifest.json` still throws `ERR_PACKAGE_PATH_NOT_EXPORTED` from the CLI. However, the CLI's console leg stopped using that specifier in objectstack-ai#19922: it resolves the console's `package.json` and joins the path to it. With an installed console that carries the manifest, and the `exports` map left as it is (`./package.json` only), `resolveSduiManifest` answers `resolved` (107 components). In this workspace it answers `absent`, only because `packages/console/dist` is not built here. ⇒ `packages/console/package.json` is **not** edited. **Cloud's per-project kernel:** NOT MEASURED. The cloud repository is outside this session's scope, so whether its per-environment kernel serves the same console and can read the same manifest is not read here. In-repo, the only host that registers the key is `os serve`. Dispatch pointer 5902497015 names cloud#2482 as the card that registers the console manifest under this key in each per-env kernel. ## Pins - `packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`, block `html page source compiled at the save door against the SDUI manifest (objectstack-ai#20312)`, 10 cases: - an unknown component answers 422 `INVALID_METADATA` with `where`/`message` naming the component, and nothing persists; - a known component saves, with `requires` stamped from the compile; - an agreeing hand-written `requires` is kept, in compiled order; - a disagreeing one is refused in all three shapes, with each namespace named; - a host with no manifest saves exactly as before, with nothing compiled and nothing stamped; - the key is read per publish: registering, widening and removing it each take effect on the next save; - a draft is ungated, but its publish refuses it; - a clean draft is stamped and publishes; - an unusable registered value gets one warning and is never compiled against; - the exported key's value. - `packages/cli/src/utils/sdui-manifest.test.ts`, block `registerDeploymentSduiManifest`, 5 cases: - resolved registers the manifest and prints nothing; - absent registers nothing and gives one line naming every place looked; - unusable registers nothing and names the file and the reason; - the default resolves beside the project directory; - `os serve` makes one registration, under metadata-protocol's key, from `path.dirname(absolutePath)`. - Ablations (one-shot, through `scripts/ablation-replace.mjs`; restore verified as blob == HEAD with `git diff HEAD` empty): - `findHtmlPageSourceGaps` forced to return `null` turns 4 of 10 red: the unknown component, the three `requires` shapes, the per-publish read, and the draft publish. - Removing the `saveMetaItem` stamp turns 3 of 10 red: stamp, agreeing order, and the draft stamp. ## Acceptance notes - The draft→active promotion (`publishMetaItem`, `publishPackageDrafts`) judges the draft with the manifest but does not re-stamp. The draft's own save stamped it when the host had a manifest. A draft saved on a manifest-less host and then published on one with a manifest is judged but stays unstamped. - `kind: 'react'` pages are not compiled (ADR-0081: real JS, not constrained JSX), so a hand-written `requires` on one is not judged. - A 422's `message` headline carries the finding locators (`pages.NAME.source [jsx-forbidden-tag]`), as every gate refusal does (objectstack-ai#10524). The component is named in `issues[].where` and `issues[].message`. - Stage ③ is not here: the load-time report, `packages/spec/liveness/page.json:9` → `live`, the `page.zod.ts` describe, and the docs. ## Verification All readings are at head `898a5bde` unless noted. It merges `origin/main` at `30839063`, objectstack-ai#20830 included. - `pnpm --filter @objectstack/metadata-protocol test`: 191 files passed, 3 skipped; 2811 tests passed, 19 skipped. Measured at `b3d92e56`, after objectstack-ai#20830 merged; the later merge brought in CI-only files. - `pnpm --filter @objectstack/metadata-protocol typecheck` and `pnpm --filter @objectstack/cli typecheck`: exit 0 at `b3d92e56`. - `pnpm --filter @objectstack/cli exec vitest run --project unit`: 237 files, 3375 tests passed at `b3d92e56`. The integration tier is declared to CI. In its place, `serve.ts`'s boot path was exercised by two real boots, below. - Boot smoke (`examples/app-crm`, `os dev --fresh` on a random port): - **No manifest:** the boot prints the one line naming both places looked, then `Server is ready`. A `PUT /api/v1/meta/page/smoke_page` whose source is an unknown component answers `200`, stored unchanged with no `requires`, as before. - **Manifest beside the served config:** no line is printed. The unknown component answers `422 INVALID_METADATA` with `jsx-forbidden-tag` and `jsx-unknown-component`, where = `page "smoke_page"` plus the tag. `requires: ["ui","plugin-absent"]` answers `422` under `page-requires-disagrees-with-source`, naming `'plugin-absent'`. A known-component page answers `200`, and a GET reads back `requires: ["ui"]`. Both servers were torn down. - Gates: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths) derived 77 commands. All 77 ran at `898a5bde` and every one exited 0. `--ran` over the exit-coded record reports 77 derived, 77 run, 0 NOT-MEASURED, 0 UNRUN: a derived zero, not a claimed one. - An earlier sweep at `ab8ea2b5` found one real finding. `check:test-source-alias` wanted the new `@objectstack/sdui-parser` import aliased to source in `packages/metadata-protocol/vitest.config.ts` (done), and the CLI pin's key import moved to module top. - The other non-zero exits in that sweep were prerequisite exits (3): missing `dist`, and the shallow clone for `check-plugin-teardown-shape --self-test`. Each went green once built or deepened. - `pnpm lint` (the whole repo, `eslint . --no-inline-config`): exit 0 at `898a5bde`. --- _Generated by [Claude Code](https://claude.ai/code/session_01DLAS1QUnHCmaso1hjjiBi5)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #20513
Clause-②: no
Stage 2 of 5 of this lane (
metadata-protocol), under the maintainer's A / A ruling on the card. The card stays open for stages 3-5, so this PR carries no closing keyword. Text only: no errorcode, field name, HTTP status, export or control flow moves. Every changed source line is a string-literal line (108 changed lines in 11.tsfiles, checked line by line against the merge base).What this does
The metadata protocol's refusals, hints and log lines sent the reader to a tracker number for the reason behind them. Each rewritten string now says that reason in words (form D, as the migration-entry rewrite and stage 1 applied it). Where the sentence already stated what was decided, only the citation goes. Where it did not, the decision is added in words:
protocol.tsinsertManyDatarefusal (thrown)insertManyis the partial-success batch insert: an outcome per row, so a bad row neither fails the whole batch nor makes the good rows run theirbeforeInserthooks twice.protocol.tsunknown metadata type refusal (400)additionalTypeswas retired because nothing ever read it.protocol.tsstored non-canonical type refusals on publish and on revert (STORED_TYPE_NOT_CANONICAL)/metaURL door now folds a type to its canonical spelling before it writes, so such a row predates that; the stored migration'sskippedreport "with that same reason" loses only its citation.runtime-authoring-gate.tsschedule-floworganization_idhintplugin.tsthe threekernel:ready"migration skipped" warningssys_settingrow identity on tenant and global rows; the adoption of untenanted seed rows and their autonumber counter.sys-metadata-repository.tshistory-counter abort (error)protocol.tspublish-closure degrade (warn)protocol.tscold-boot org-scoped audit (warn)sys_view_definitionandsys_settingindex migration messages (ADR-0120 D4 stays), the seed/API tenancy repair, its receipt and its skips, the batch-row withhold, the object-existence gate's no-registry warning, the nested-select, overlay and non-canonical-registry refusals, and the live-MySQL testkit errorEach claim was checked against today's code, not only against the cited card:
saveMetaItemfolds the request type before it persists;additionalTypesis aretiredKey()tombstone inpackages/spec; the org-scoped write refusal is live inorgScopedWriteRefusal. One cited number (10382) answers 404 and was read through its landing commitee09d2119; the testkit sentence already says what it guards.Order inside the stage
All 46 id-bearing literals (50 occurrences) fit one PR, under the stop line, so the stage lands whole, in three commits in the ordered sequence:
dc8a1a112author-visible text: 10 literals (thrown refusals, the stored-type refusals, the hint);730cbcaf6log lines: 35 literals, plus the two re-pinned tests;442473234thesrc/-shipped testkit string, and the changeset.Each commit recomputes the ledger, so every commit on the branch is green on
check:doc-authoring.Pins re-pinned: 3 assertion lines in 2 test files
migrations/view-definition-active-index.test.ts342-343 asserted the two numbers in the MySQL degradation line. They now assert the two gaps in words: "an archived view keeps occupying its name slot" and "two same-name ACTIVE shared views (owner NULL)".protocol.batch-row-driver-text.test.ts396 asserted the number in the withhold warning. It now asserts "must not be quoted back on response data", the decision itself.A one-off mutation proves each new pin can fail, run on the committed head with
scripts/ablation-replace.mjs(anchor hit once, disk-verified) under a shell trap. Changing "name slot" gives 1 failed / 27 passed. Changing "(owner NULL)" gives 1 failed / 27 passed. Changing "quoted back" gives 2 failed / 14 passed: the re-pin, and a knock-on in the next test, because the failed test never reached itsmockRestore. After each leg, the blob equals HEAD andgit diff HEADis empty. The tree is clean after the run, and no test file was left behind.No string here is compared byte for byte with a twin in another package. The consumer pins outside the package read unchanged fragments:
runtime'sbatch-row-driver-text-real-driver.integration.test.tsreads the withhold prefix,seed-tenancy-autonumber-split.integration.test.tsreads "backfill skipped", andmeta-field-overlay-lock.test.ts,objectql'sprotocol-meta.test.tsandrest'smeta-unknown-type-read-refusal.test.tsread "is not a metadata type". I ran the threeruntimefiles against the rebuiltdist/, and they passed.Ledger burn-down
scripts/doc-authoring-prose-id.baseline.jsonwas regenerated withnode scripts/check-doc-authoring.mjs --census-ledgerinto a scratch file, so the growth refusal ran against the checked-in baseline, and then copied into place. Onlymetadata-protocolrows moved, and every one of them leaves:metadata-protocol/src/protocol.tsmetadata-protocol/src/migrations/seed-tenancy-backfill.tsmetadata-protocol/src/migrations/view-definition-active-index.tsmetadata-protocol/src/migrations/overlay-index.tsmetadata-protocol/src/migrations/sys-setting-identity-index.tsmetadata-protocol/src/plugin.tsmetadata-protocol/src/migrations/live-mysql-database.testkit.tsmetadata-protocol/src/runtime-authoring-gate.tsmetadata-protocol/src/sys-metadata-repository.tsThe ledger's file count goes from 224 to 215, and other packages' rows moved: 0. The census's 40 messages reconcile with the ledger's 50 occurrences. The gate counts 46 string literals: 45 in the census population plus the testkit string, which the census filed as test-facing. The census folds a
+chain into one message, so 5 two-literal chains make 45 literals into 40 messages. Four literals carry two ids each, which makes 46 literals into 50 occurrences.Verification (head
442473234)@objectstack/runtime...(30/30), then the whole workspace (72/72), then@objectstack/metadata-protocoldirectly. The new sentences are indist/index.js, and the only citations left indist/are docblocks.@objectstack/metadata-protocoltest: Test Files 190 passed, 3 skipped (193); Tests 2792 passed, 19 skipped. The skips are the live MySQL/PostgreSQL files: this container has no server.@objectstack/metadata-protocoltypecheck: exit 0;tsc --listFilesreads 193 of 193 test files.@objectstack/runtime, the three consumer files above: 3 files, 33 tests passed.node scripts/pm/dispatch-gates.mjs --commands(no paths; 13 paths against merge base261c529f0): 70 commands, all exit 0.check:dual-build-cjs-loadsandcheck:type-check-debtfirst answered exit 3 PREREQUISITE NOT MET on the partial build. After the full build (and a direct rebuild of this package, whosedista turbo cache hit had left older than the restored sources), both exit 0;check:dts-closureandcheck:lean-entry-closurewere re-run there too.--ran: 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.check:doc-authoring: sibling-package prose ids hold the baseline, no growth, no burn-down unrecorded.eslint --no-inline-config --format jsonover the 11 touched.tsfiles reports 11 files, 0 errors and 0 warnings. The resolvedparserOptionsfor these files areecmaVersionandsourceTypeonly, with noprojectorprojectService. So no type-aware rule can move an untouched file. The repo-widepnpm lintis CI's.Acceptance notes
protocol.ts. Whichever lands second mergesmainand recomputes the ledger with--census-ledger.//comments in this package still cite numbers. They are out of scope here: comments are the sanctioned home for internal anchors, and a separate card owns stale ones.Generated by Claude Code