Skip to content

fix(metadata-protocol): refusals, hints and log lines state each decision in words instead of a tracker number (stage 2) - #20830

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-20513-stage2-metadata-protocol
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-20513-stage2-metadata-protocol

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 error code, field name, HTTP status, export or control flow moves. Every changed source line is a string-literal line (108 changed lines in 11 .ts files, 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:

Where Cited The sentence now says
protocol.ts insertManyData refusal (thrown) 3172 insertMany is the partial-success batch insert: an outcome per row, so a bad row neither fails the whole batch nor makes the good rows run their beforeInsert hooks twice.
protocol.ts unknown metadata type refusal (400) 8586 A plugin cannot declare a type either: additionalTypes was retired because nothing ever read it.
protocol.ts stored non-canonical type refusals on publish and on revert (STORED_TYPE_NOT_CANONICAL) 7894, 8957 The /meta URL door now folds a type to its canonical spelling before it writes, so such a row predates that; the stored migration's skipped report "with that same reason" loses only its citation.
runtime-authoring-gate.ts schedule-flow organization_id hint 6153 The author's value wins, and the engine fills only an organization the run resolved, so for a schedule run the author is the one source.
plugin.ts the three kernel:ready "migration skipped" warnings 5839, 8629, 8686 What the migration that did not run would have ensured: view-name uniqueness among ACTIVE rows only; the NULL-safe sys_setting row identity on tenant and global rows; the adoption of untenanted seed rows and their autonumber counter.
sys-metadata-repository.ts history-counter abort (error) 4867 The old path answered 1 because it took a failed read for an empty table.
protocol.ts publish-closure degrade (warn) 10377 Validation falls back to the LIVE declarations without the batch's own drafts in the closure.
protocol.ts cold-boot org-scoped audit (warn) 6190, 6992 The write refusal it points at covers declared types only; the trailing "See" keeps ADR-0005.
everything else: the overlay, sys_view_definition and sys_setting index 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 error 6418, 8725, 8629, 5839, 6417, 8686, 9451, 9261, 8502, 3770, 4196, 6190, 4432, 10382 The sentence already said what was decided; only the citation goes.

Each claim was checked against today's code, not only against the cited card: saveMetaItem folds the request type before it persists; additionalTypes is a retiredKey() tombstone in packages/spec; the org-scoped write refusal is live in orgScopedWriteRefusal. One cited number (10382) answers 404 and was read through its landing commit ee09d2119; 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:

  1. dc8a1a112 author-visible text: 10 literals (thrown refusals, the stored-type refusals, the hint);
  2. 730cbcaf6 log lines: 35 literals, plus the two re-pinned tests;
  3. 442473234 the src/-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.ts 342-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.ts 396 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 its mockRestore. After each leg, the blob equals HEAD and git diff HEAD is 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's batch-row-driver-text-real-driver.integration.test.ts reads the withhold prefix, seed-tenancy-autonumber-split.integration.test.ts reads "backfill skipped", and meta-field-overlay-lock.test.ts, objectql's protocol-meta.test.ts and rest's meta-unknown-type-read-refusal.test.ts read "is not a metadata type". I ran the three runtime files against the rebuilt dist/, and they passed.

Ledger burn-down

scripts/doc-authoring-prose-id.baseline.json was regenerated with node scripts/check-doc-authoring.mjs --census-ledger into a scratch file, so the growth refusal ran against the checked-in baseline, and then copied into place. Only metadata-protocol rows moved, and every one of them leaves:

File Occurrences before after (file, id) pairs before after
metadata-protocol/src/protocol.ts 15 0 (row leaves) 11 0
metadata-protocol/src/migrations/seed-tenancy-backfill.ts 14 0 (row leaves) 3 0
metadata-protocol/src/migrations/view-definition-active-index.ts 7 0 (row leaves) 3 0
metadata-protocol/src/migrations/overlay-index.ts 4 0 (row leaves) 2 0
metadata-protocol/src/migrations/sys-setting-identity-index.ts 4 0 (row leaves) 1 0
metadata-protocol/src/plugin.ts 3 0 (row leaves) 3 0
metadata-protocol/src/migrations/live-mysql-database.testkit.ts 1 0 (row leaves) 1 0
metadata-protocol/src/runtime-authoring-gate.ts 1 0 (row leaves) 1 0
metadata-protocol/src/sys-metadata-repository.ts 1 0 (row leaves) 1 0
whole ledger 861 811 572 546

The 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)

  • build: turbo over @objectstack/runtime... (30/30), then the whole workspace (72/72), then @objectstack/metadata-protocol directly. The new sentences are in dist/index.js, and the only citations left in dist/ are docblocks.
  • @objectstack/metadata-protocol test: 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-protocol typecheck: exit 0; tsc --listFiles reads 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 base 261c529f0): 70 commands, all exit 0. check:dual-build-cjs-loads and check:type-check-debt first answered exit 3 PREREQUISITE NOT MET on the partial build. After the full build (and a direct rebuild of this package, whose dist a turbo cache hit had left older than the restored sources), both exit 0; check:dts-closure and check:lean-entry-closure were 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.
  • narrowed lint: eslint --no-inline-config --format json over the 11 touched .ts files reports 11 files, 0 errors and 0 warnings. The resolved parserOptions for these files are ecmaVersion and sourceType only, with no project or projectService. So no type-aware rule can move an untouched file. The repo-wide pnpm lint is CI's.
  • NOT MEASURED locally (CI's): the live PG/MySQL files, whose messages changed only ahead of the pinned MySQL statement lead-in; the Test Core shards; Dogfood; the workspace type-check lanes.

Acceptance notes


Generated by Claude Code

…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 23 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via additionalTypes (literal, a string literal in refuseUnmintableMetaType))
  • content/docs/kernel/contracts/data-engine.mdx (via insertManyData (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/services-checklist.mdx (via assembleMetadataProtocol (symbol, a top-level function))
  • content/docs/plugins/adding-a-metadata-type.mdx (via additionalTypes (literal, a string literal in refuseUnmintableMetaType))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-1.mdx (via publishPackageDrafts (symbol, a method of class ObjectStackProtocolImplementation), additionalTypes (literal, a string literal in refuseUnmintableMetaType))
  • content/docs/releases/v17/17-4.mdx (via insertManyData (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/releases/v17/17-5.mdx (via insertManyData (symbol, a method of class ObjectStackProtocolImplementation))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 73155fedcacc215565c4eef6d9899977e0707010 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 793427441801fea69c2cd1f38a9b98fc64d32086 — the merge of head 442473234551d7cb909e2a927d1adc446a9b4e35 into base 73155fedcacc215565c4eef6d9899977e0707010, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 73155fedcacc215565c4eef6d9899977e0707010 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 442473234551d7cb909e2a927d1adc446a9b4e35
Local-runs: none

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 os-dev-report 5908549372), PR #20830 (body, 13-file list, the net diff origin/main...442473234 at merge base 261c529f0), PR #20795 at df67985b0 and its record 5906164285 as the prior application of the same ruling, the head's check-runs, the 22 cited cards (21 readable; 10382 through its landing commit ee09d2119), and the head's own source through git show / git grep. Nothing built, run or re-run. The PR head was confirmed as 442473234551d7cb909e2a927d1adc446a9b4e35 before the read and had not moved.

Check-runs on 442473234, read once for this record and not polled: 31 check-runs — 13 completed / success (Check Changeset, Check PR Size, Part-of PR must not also close its card, the claim and single-writer guards, Flag docs affected by code changes, Check Documentation Links, Governed Surface Queue Guard, Type Check · source gates, Dogfood Verify CLI, Auto Label, filter), 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke — path-filtered), 15 in_progress (Test Core 1-6, Dogfood Regression Gate 1-3, Build Core, Temporal Conformance (live PG + MySQL), Lint & Repo Gates, Type Check · workspace / debt ledger / consumer gates), 0 failure. Their conclusions are the gate verdicts: the runs still going carry check:doc-authoring (Lint & Repo Gates), the three re-pinned assertions (Test Core) and the live-server leg the dev did not measure (Temporal Conformance). This record judges the contract on the diff, the cards and the head's source; a failure among those runs, if one lands, is a new fact for the adopting session, not one this record has read.

① Derived judgments

Scope against the ruling and the claim — right. The ruling orders stages per package, all three categories, form D, --census-ledger in the same PR, author-visible text first and log lines last; the claim scopes stage 2 to packages/metadata-protocol/src/**, the pins, the ledger and one patch changeset, and forbids wire code / field / status changes, comment or docblock edits (#20595) and other packages' ledger rows. The 13-file list is exactly that surface: 9 source files, 2 test files, the ledger, the changeset. No governed path. "Author-visible first" is honoured inside the stage as commit order (dc8a1a112 the refusals and the hint, 730cbcaf6 the log lines, 442473234 the testkit string), and a stage that lands whole has nothing left to split.

Text only — right, checked line by line. All 108 changed lines in the 11 .ts files (54 removed, 54 added) are inside string literals: template pieces in the five migration files and runtime-authoring-gate.ts, the '…' + chains in protocol.ts and the testkit, the three ctx.logger.warn('…') literals in plugin.ts, the console.error template in sys-metadata-repository.ts. The one-line throw new Error('insertManyData requires…') changes only its literal; the three rejoined boundaries (seed-tenancy object(s): + untenanted, view-definition cannot. + MySQL, and the catch-all whose trailing (#5839 / #6417). line folded into the sentence before it — hence 6 added / 7 removed on that file) move no non-literal token. err.code, err.status, code: 'STORED_TYPE_NOT_CANONICAL', organizationId, every logger.* / console.* call shape and every export are context lines. The two test-file changes are the 3 pin lines only. Comments and docblocks are untouched: the only tracker ids left outside comments in the 9 files on the head are trailing // [#3770], // [#7823], // [#7539] comments on code lines, which the ledger does not read.

Accept-set and public surface — nothing moves — right. No schema, route, wire code, HTTP status, field name, default or export changes; a caller sees different prose in the same envelopes. No test outside the package asserts any of the 22 removed numbers (a git grep of the head over every *.test.ts finds one trailing // [#8502] comment beside an unchanged assertion, nothing else), and the consumer fragments the dev names — the withhold prefix, "backfill skipped", "is not a metadata type" — are unchanged on the head.

Ledger diff — right, exact. Nine packages/metadata-protocol/src/** rows leave and nothing else moves. I reconciled every removed occurrence against a citation deleted in the diff: protocol.ts 15 (#6190 ×3 — the NOT_OVERRIDABLE "See", the cold-boot audit body and its "See" line; #7894 ×2 and #8957 ×2 — the publish and revert refusals; #8502, #3770, #4196, #3172, #4432, #8586, #10377, #6992 once each); seed-tenancy-backfill.ts 14 (#8686 ×9, #9451 ×4, #9261 ×1); view-definition-active-index.ts 7 (#5839 ×3, #6417 ×3, #8725 ×1); overlay-index.ts 4 (#6418 ×3, #8725 ×1); sys-setting-identity-index.ts 4 (#8629 ×4); plugin.ts 3 (#5839, #8629, #8686); runtime-authoring-gate.ts 1 (#6153); sys-metadata-repository.ts 1 (#4867); the testkit 1 (#10382). 50 occurrences, 26 (file, id) pairs, 9 files — the whole-ledger deltas 861 to 811, 572 to 546, 224 to 215 follow. check:doc-authoring inside Lint & Repo Gates is the mechanical confirmation, pending at read time.

Form D, string by string — right. Every cited card was read: 20 closed completed issues, one merged PR (#6153, landed as bdc8e709a), and #10382 through ee09d2119 (each live-MySQL suite's database derived from its own file path; the drop database in afterAll is why sharing one is unsafe — what the testkit sentence still says). No cited decision has been reversed. The 11 literals that gained words, each judged against the card and the head's code:

Pins — right, 3 lines in 2 files. view-definition-active-index.test.ts now asserts "an archived view keeps occupying its name slot" and "two same-name ACTIVE shared views (owner NULL)"; both phrases are contiguous in the head's joined template (…occupying its + name slot, and two same-name ACTIVE shared views (owner NULL) or environment-level …). protocol.batch-row-driver-text.test.ts asserts "must not be quoted back on response data", present in the head's chain. The dev's mutation legs (1 / 27, 1 / 27, 2 / 14 with the mockRestore knock-on) are its own measurement; what this record verified is that each pinned phrase is on the head.

② Semver level

.changeset/20513-metadata-protocol-runtime-strings-state-the-decision.md: '@objectstack/metadata-protocol': patch — right. The package is released (17.5.0, not private); the diff publishes changed prose in that one package and nothing else — no key, export, envelope code, status or default moves — so this is a fix-class patch, never skip-changeset. The body is CHANGELOG-fit: it names each message family, says which sentences gained words and what they now say, and closes "Text only: no error code, field name, status or behaviour changes"; each claim in it matches a string in the diff (the insertMany description, additionalTypes retired having never been read, the fold-before-write door, "a schedule resolves none", the failed read taken for an empty table). No model identifier in the changeset, the three commit trailers (Claude-Session plus a model-free Co-authored-by) or the PR body (session-URL footer). Check Changeset is success.

Clause-②: no — right. Nothing an author can write is widened or narrowed; the PR body carries Clause-②: no at line start and the changeset repeats it.

③ Boundary flags

Dev flags (report 5908549372), each answered:

  1. Census reconciliation (46 literals / 50 occurrences / 40 messages) — holds, recomputed independently. The census lists 40 messages for this package with 49 per-message-deduplicated id occurrences, plus the testkit string filed as test-facing. Five messages hold two id-bearing literals each (the MySQL view degradation, the ambiguous-organization skip with its #9261 NOTE, the publish and revert refusals, the cold-boot audit): 40 + 5 = 45, plus the testkit = 46. Four literals carry two ids (#6418, #8725; #6417, #8725; #5839 / #6417; #6190 / #6992): 46 + 4 = 50; the census's 49 is the ledger's 50 less the testkit, the audit's second #6190 having been folded by the per-message dedupe. Exact.
  2. 3 re-pinned assertions and their mutation legs — verified in ①. The 2-failed leg is the failing test never reaching warn.mockRestore(), a knock-on inside the same file, not a second pin.
  3. No cross-package byte-parity twin, nothing held — accepted. A git grep of the head over every test file finds no assertion on any of the 22 numbers outside the package, and no test compares these sentences with another package's copy; the driver-sql stage's held rows are a different family. The claim's "no other package's ledger rows" holds: the ledger diff touches nine rows, all under packages/metadata-protocol/src/.
  4. In-file twin pair rewritten alike — verified. The publish (about 18670) and revert (about 20876) refusals carry the identical parenthetical; no test on the head asserts the rewritten parenthetical, and the fragments tests do read (STORED_TYPE_NOT_CANONICAL, the re-author remedy) are unchanged.
  5. PR fix(runtime,metadata-protocol): the /automation write doors keep the packaged-base lock the /meta door keeps (#20679) #20817 also edits protocol.ts — accepted as stated. Whichever lands second merges main and recomputes with --census-ledger; the growth refusal makes a stale ledger red, so the order cannot go wrong silently. Carrier: the seat that lands the second one.
  6. The out-of-scope note on recordSeedTenancyReceipt — yes, a sentence this PR rewrote makes that claim. The shared prefix at seed-tenancy-backfill.ts about 1149 — "the seed/API tenancy repair ran and rewrote stored rows, but this deployment has NO durable record that it did" — lost its (#8686) in this diff and heads all three not-recorded lines. The code comment at about 1538 says the receipt is written for every applied run "including one where every stamp failed (objectsStamped === 0)", and a failed stamp skips that object's counter merge, so on such a run nothing was rewritten and the prefix over-claims — only when every stamp failed AND the receipt could not be written. The words pre-date this PR; the ruling's rewrite takes the citation out of a sentence that already states the decision, and the stage-1 record treated the same class as a wording note. Not a FAIL item. Carrier: the seat, as a finding or a rider on the next PR that touches this file — derive the prefix from result.objectsStamped, or say "ran" and let the receipt's own objectsStamped say what moved.
  7. Live PG / MySQL NOT MEASURED locally — CI's; Temporal Conformance is in_progress at read time. The three live files pin only the unchanged MySQL statement lead-in.

open_questions: none in the stage-2 report; none raised here. The card's lane children (#20749, #20751, #20753, Blocked-by: #20513) are unaffected by this stage.

One boundary note, not a FAIL item — a docs page quotes the old sentence. content/docs/api/error-catalog.mdx about 695 reproduces the unknown-metadata-type refusal verbatim as a sample 400, including "since #8586 retired 'additionalTypes'"; it is the only page under content/docs that quotes any of the 46 rewritten literals. The Docs Drift Check on this PR lists that page (via the additionalTypes literal) and is advisory by its own text; AGENTS.md's Documentation Guardrails bind no code PR to it, and the claim forbids riders outside packages/metadata-protocol/src/**. Carrier: the docs-accuracy-audit scoped by the drift comment, or a docs-only PR — the example should read the sentence the runtime now emits.

Implemented-by: claude/issue-20513-stage2-metadata-protocol
Reviewed-by: session_01DEvba2nBuD4tWzfq8r8NFY

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 10:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit fe463b4 Sep 30, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20513-stage2-metadata-protocol branch September 30, 2026 10:42
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants