Repository navigation
feat(spec, metadata-protocol): each _drafts row carries the draft body's own label, or null - #22323
Conversation
…y's own label, or null ListDraftsResponseSchema gains a required, nullable `label` (I18nLabel): the draft body's own top-level label, as authored. SysMetadataRepository reads it off the row listDrafts already holds; the protocol passes it through. A body that declares none reads null, never the machine name. Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
… authored, null when absent, the only body member on the header Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
…changeset Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 140 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 347b3754f4f14d93827bcb46dd6335a63f890887 && git checkout 347b3754f4f14d93827bcb46dd6335a63f890887
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 440bed63e731117bc194166fea3eec3d923ad2c6 98a5aef8dcd71f0cd0c5ac6a866b1c133df06866 && git checkout -B drift-repro 440bed63e731117bc194166fea3eec3d923ad2c6 && git merge --no-ff 98a5aef8dcd71f0cd0c5ac6a866b1c133df06866
node scripts/docs-audit/affected-docs.mjs --json 440bed63e731117bc194166fea3eec3d923ad2c6
|
|
CI on
Generated by Claude Code |
…afts-header-label
Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs read: card #22200 (body and all 5 comments: triage ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged:
② Semver levelChangeset Clause-②: yes (widening) — agreed. ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…or before the checks that judge its body, on every kernel topology (objectstack-ai#22338) Fixes objectstack-ai#22220 Clause-②: no ## What changes `saveMetaItem`'s package door (`refusePackagedBaseOverride`) is now asked on every kernel topology, at the position it already had on an environment kernel: after the code-only and organization-scope refusals, before the item lock and before every check that reads the body or the store. It used to sit behind `environmentId !== undefined`. A host-config kernel (the CLI's assembler, the showcase's boot shape, `OS_MODE=off`) met the same predicate only at `SysMetadataRepository.assertAllowed`, the first statement of `repo.put`, which is the method's last act. So on that kernel every refusal in between answered first. - `packages/metadata-protocol/src/protocol.ts`: the `environmentId` wrapper around the door is removed. The `_lock` gate's guard `packagedBaseRefusal(...) === null` (its hand-written deferral to the door on host-config) always answered "no refusal" once the door runs first on every kernel, so it is removed. The invariant comment on that gate is extended, and the door's call site records why no acceptance set moves. Two TSDoc paragraphs that said the door is environment-only are corrected. - New pins: `packages/metadata-protocol/src/protocol.package-door-before-gates.test.ts` (one table over both kernel topologies, `code` + `status` per row). - Three downstream test files pinned the old host-config ordering and are updated (below). - `scripts/engine-double-contract.pinned.json`: the new pin file's `findOne` double, recorded by `check-engine-double-contract.mjs --write`. - `.changeset/22220-package-door-before-gates.md`: `@objectstack/metadata-protocol` patch. ## Door table, live (base `4e4111ca05` vs head `9e763ec0bc`, both built and booted before the `main` merge) Seeded admin, default composition, `OS_METADATA_WRITABLE` unset. Bodies: **served** = the `GET` item; **gate-refused** = served plus one autonumber field whose format names a missing field (`autonumber-references-unknown-field`); **spec-refused** = served plus an undeclared top-level key (`unrecognized_keys`). Every cell is `status code`. `examples/app-crm`, `PUT /api/v1/meta/object/crm_account` (packaged under `com.example.crm`; `object` is `allowOrgOverride: false`). Environment kernel = `pnpm dev:crm -- --fresh` (`env_local`). Host-config kernel = the same stack under `OS_MODE=off` (the lightweight assembler, `environmentId` undefined; confirmed by the repository's sentence on the base row). | body, mode | env kernel, base | env kernel, head | host-config, base | host-config, head | |:--|:--|:--|:--|:--| | served, publish | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | | served, draft | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | | gate-refused, publish | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | **422 INVALID_METADATA** | 403 NOT_OVERRIDABLE | | gate-refused, draft | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | | spec-refused, publish | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | **422 INVALID_METADATA** | 403 NOT_OVERRIDABLE | | spec-refused, draft | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | **422 INVALID_METADATA** | 403 NOT_OVERRIDABLE | | `permission/crm_sales_user`, spec-refused, publish | not measured | 403 NOT_OVERRIDABLE | **422 INVALID_METADATA** | 403 NOT_OVERRIDABLE | | `position/sales_rep`, spec-refused, publish | not measured | 403 NOT_OVERRIDABLE | **422 INVALID_METADATA** | 403 NOT_OVERRIDABLE | | control: env-local `zz_local_obj`, gate-refused, publish | 422 INVALID_METADATA | 422 INVALID_METADATA | 422 INVALID_METADATA | 422 INVALID_METADATA | | control: env-local `zz_local_obj`, spec-refused, publish | 422 INVALID_METADATA | 422 INVALID_METADATA | 422 INVALID_METADATA | 422 INVALID_METADATA | At head the host-config 403 carries the environment kernel's sentence: the two `crm_account` gate-refused publish bodies compare byte-equal. At base the host-config 403s carried the repository's sentence (`'object' is not allowOrgOverride in the registry ...`), and a packaged `permission`'s plain publish carried plugin-security's lock sentence. The environment kernel's code path is unchanged by this diff, which is why its two unmeasured base cells are left unmeasured rather than inferred. The card's own composition, `examples/app-showcase` (`pnpm dev -- --fresh`, host-config), `PUT /api/v1/meta/object/showcase_task`: base answered 422 for gate-refused publish and for spec-refused publish and draft, 403 for the rest; head answers 403 `NOT_OVERRIDABLE` for all six rows, and the env-local controls answer 422 `INVALID_METADATA`. ## Door table, unit pins (both kernels; base = `protocol.ts` at the merge base, head = this branch) Measured through the real `saveMetaItem` over an engine double (the new pin file's harness). "gate reached" = `assertRuntimeAuthoringRules` was called. | request | env base | env head | host-config base | host-config head | |:--|:--|:--|:--|:--| | object, served body, publish | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE, gate reached | 403 NOT_OVERRIDABLE | | object, gate-refused, publish | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 422 INVALID_METADATA, gate reached | 403 NOT_OVERRIDABLE | | object, gate-refused, draft | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE, gate reached | 403 NOT_OVERRIDABLE | | object, spec-refused, publish | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 422 INVALID_METADATA | 403 NOT_OVERRIDABLE | | object, spec-refused, draft | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 422 INVALID_METADATA | 403 NOT_OVERRIDABLE | | object, `?package=` naming its read-only package, gate-refused, publish | 403 ITEM_LOCKED | 403 ITEM_LOCKED | 422 INVALID_METADATA, gate reached | 403 ITEM_LOCKED | | object, fields dropped (destructive), publish | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 409 DESTRUCTIVE_CHANGE | 403 NOT_OVERRIDABLE | | position, spec-refused, publish | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 422 INVALID_METADATA | 403 NOT_OVERRIDABLE | | permission, spec-refused, publish | 403 NOT_OVERRIDABLE | 403 NOT_OVERRIDABLE | 422 INVALID_METADATA | 403 NOT_OVERRIDABLE | | control: env-local object, gate-refused, publish | 422, gate reached | 422, gate reached | 422, gate reached | 422, gate reached | | control: env-local object, spec-refused, publish | 422 INVALID_METADATA | 422 INVALID_METADATA | 422 INVALID_METADATA | 422 INVALID_METADATA | | control: packaged view (`allowOrgOverride: true`), spec-refused | 422 INVALID_METADATA | 422 INVALID_METADATA | 422 INVALID_METADATA | 422 INVALID_METADATA | Registry-wide (a packaged `pkg_TYPE` with a spec-refused body, publish, every type in `DEFAULT_METADATA_TYPE_REGISTRY`): at base the host-config kernel answered differently from the environment kernel for 15 types (`object` 409 `DESTRUCTIVE_CHANGE`; `hook`, `seed`, `mapping`, `page`, `app`, `action`, `dataset`, `datasource`, `doc`, `book`, `permission`, `position`, `tool`, `skill` 422 `INVALID_METADATA`). At head both kernels give the same envelope for every type, and every type the door governs (`allowOrgOverride: false`, `allowRuntimeCreate: true`) answers 403 `NOT_OVERRIDABLE`. The other 13 types answered alike on both kernels at base and at head. ## The window the door now precedes (M2) Refusals `saveMetaItem` could answer on a host-config kernel between the door's position and `repo.put`, for a packaged `allowOrgOverride: false` save: - the ADR-0010 `_lock` gate, 403 `ITEM_LOCKED` (already deferred to the door by hand; that guard is removed); - the ADR-0029 D9.9 package mismatch, 422 `OBJECT_OVERLAY_PACKAGE_MISMATCH`; - the destructive diff, 409 `DESTRUCTIVE_CHANGE` (measured above); - the layered-envelope refusal, 422 `INVALID_METADATA`; the save-name refusal, 400 `VALIDATION_ERROR`; - the flow conversion conflict, 409 `FLOW_CONVERSION_CONFLICT`; - the spec-conformance parse, 422 `INVALID_METADATA`, draft and publish (measured); - the stored-hook body refusal, 400 `VALIDATION_ERROR`; - the runtime authoring gate, 422 `INVALID_METADATA`, publish only (measured); - the domain plugins' authoring gates: plugin-security's permission-set lock (403 `NOT_OVERRIDABLE`, its own error class and sentence; measured live) and its object posture gate R1 (403, wire `code` `PERMISSION_DENIED`, `declaredCode` `owd_widening_forbidden`). The view-container collision refusal also sits there but judges only `view`, which allows overlays, so the door never precedes it. ADR-0070 D1 (`WRITABLE_PACKAGE_REQUIRED`) judges only `runtime-only` writes, which the door never refuses. ## Mechanism assumptions, as measured - **M1** reproduced at base on both compositions above. The authoring-gate body was built from what `main` refuses (`autonumber-references-unknown-field`); no pass-4 lift was needed. - **M2** the spec parse also answers 422 ahead of the door on host-config, in draft and publish mode; the full window is listed above. - **M3** the predicate is `refusePackagedBaseOverride`'s own, already topology-independent (`packagedBaseRefusal` asks it on every topology for the `/automation` doors). It and `assertAllowed` read the same registry-derived `allowOrgOverride` set, the same `OS_METADATA_WRITABLE` hatch and the same `isWritablePackage` (`package-writability.ts`), and both throw the same `readOnlyBaseOverrideError` for a named read-only base. They differ only in the fallback sentence for a type with no ADR-0126 regime row. - **M4** read `0b997ea447`. The door's input is `isArtifactBacked`, the same input the repository's intent uses, so stack-declared positions are refused by the door on both kernels (measured live for `position/sales_rep`). The `security`-domain types are `permission`, `position` and `capability`; `capability` is code-only and answers the code-only refusal ahead of the door, unchanged. - **M5** held: a draft is still not judged by the authoring gate; env-local bodies still answer 422 on both kernels; a packaged `view` and a packaged object with the hatch open are still judged by the gates. ## What does not move (Clause-② measurement, on this head) - **Accept sets.** The door refuses exactly when `artifactBacked && !isOverlayAllowed(type)`. `repo.put` runs `assertAllowed` with intent `override-artifact` whenever `artifactBacked`, and that refuses on the same predicate, on every topology. `saveMetaItem` has no success return before `repo.put` (the one `return` in that window is inside a closure). So a request the door refuses was refused before, and a request it admits meets the same checks as before. The unit tables above show every measured row refused at base and at head. - **Built entry declarations.** `dist/index.d.ts` and `dist/index.d.cts` of `@objectstack/metadata-protocol`, built from this head and from the merge base's `protocol.ts` (`fe98cc63a4`): the only differences are the two TSDoc paragraphs corrected above. No declaration line changes. - **Wire.** For a packaged `allowOrgOverride: false` save on a host-config kernel that one of the checks above refused, the answer is now 403 `NOT_OVERRIDABLE` (or 403 `ITEM_LOCKED` for a named read-only base) with the door's sentence, which is what an environment kernel already answered. ## Downstream pins that encoded the old host-config order Each relied on the host-config kernel skipping the door. Each now reaches the check it tests the way an environment kernel always had to. - `packages/objectql/src/protocol-destructive.test.ts`: saved a destructive edit of a packaged object with no `environmentId` "to bypass the overlay opt-in gate". It now opens `OS_METADATA_WRITABLE=object`, the one route by which a packaged object's write reaches the destructive diff. Expectations unchanged. - `packages/rest/src/meta-object-owd-gate.test.ts`: the lint-before-R1 order case and the two R1 refusals drove a packaged-object overlay with the hatch shut. They now open the hatch (the path R1's docblock names). Expectations unchanged. Two comments that said the door was environment-scoped are corrected. - `packages/plugins/plugin-security/src/packaged-permission-set-lock-gate.test.ts`: the hatch-closed case pinned the lock's error class on host-config. With the hatch closed the door now answers first there, as on an environment kernel, so the case asserts the same `NOT_OVERRIDABLE` / 403 envelope and that the class is not the lock's. The hatch-open cases still pin the lock's class. ## Verification - Reverse verification: the new pin file run against the merge base's `protocol.ts` (swapped on disk, blob-verified, restored by trap to the HEAD blob): 23 failed / 26 passed of 49, every failure a host-config row or a registry row; at head 49/49. - `pnpm --filter @objectstack/metadata-protocol exec vitest run`: 222 files passed, 3 skipped; 28329 tests passed (at `9e763ec0bc`, before the `main` merge, which touched only the seed loader in this package). - At `81a42b1203`, after `git merge origin/main` and a rebuild: the new pin file 49/49; `objectql` `protocol-destructive.test.ts` 7/7; `plugin-security` `packaged-permission-set-lock-gate.test.ts` 7/7; `rest` `meta-object-owd-gate.test.ts` 14/14; the five `qa/dogfood` files that drive `saveMetaItem` 27/27. `typecheck` (with `check:test-typecheck` where the package has it) green for `metadata-protocol`, `objectql`, `plugin-security` and `rest`. - Before the merge: `objectql` full `--project local` (383 files passed; the 3 failures were `protocol-destructive.test.ts`, updated above), `plugin-security` full (181 files passed; the 1 failure was the lock-gate case updated above), the 35 `runtime` files and 24 `rest` files that call `saveMetaItem` (all green except the 3 `meta-object-owd-gate` cases updated above). - Gates: `node scripts/pm/dispatch-gates.mjs --commands` derives 78 commands on `81a42b1203`. 70 ran on `32d94c2d00` (the merge commit; the one later commit only adds the ledger row); the 8 that row adds, the changeset gates and the ratchet families re-ran on `81a42b1203`. 77 exited 0; `--ran` reconciles 78 derived, 77 run, 1 unrun. `check:engine-double-contract` first exited 1 for the missing ledger row and exits 0 with it recorded. `check:type-check-debt` (a repository-wide re-measure) hit a 400 s local timeout: **NOT MEASURED**, left to CI. - Lint, narrowed: ESLint's own config matches 5 of the 7 changed paths (the changeset and the JSON ledger answer "no matching configuration"); `--format json` reports 5 files, 0 errors, 0 warnings. This config enables no type-aware linting, so the diff cannot move a verdict on an untouched file. ## Serial notes - PR objectstack-ai#22319 edits `saveMetaItem`'s flow canonicalization and spec-parse region; this diff stays out of those lines. - PR objectstack-ai#22323 edits `protocol.ts` around the drafts listing and `sys-metadata-repository.ts`; no overlap with this diff. ## Acceptance notes - `packages/metadata-protocol/src/protocol.read-lock-flags-write-door.test.ts`'s `hostConfigDoor` docblock still says `saveMetaItem` skips its package door on host-config. The test measures `repo.put` directly and stays correct; only that sentence is now stale. Not edited here (outside the claimed file surface). - `packaged-base-regime.ts` and `sys-metadata-repository.ts` describe the protocol door as environment-scoped (incomplete now, not false). Not edited here, for the same reason. - ADR-0005 §"Whitelist enforcement" still says single-kernel deployments keep "any type writable". The repository's `assertAllowed` has refused these writes on those kernels since before this change; this diff moves no acceptance set, so it reverses no ADR decision. The ADR text is stale relative to shipped behaviour, not to this change. - `deleteMetaItem` keeps its own `environmentId`-scoped removal door; this card is about the save door only. --- _Generated by [Claude Code](https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…n in words instead of a tracker number (stage 30) (objectstack-ai#22414) Part of objectstack-ai#20749 Clause-②: no Stage 30 of this card: the class (e) remainder, the test strings shipped under the `packages/spec/src` subdirectories, as ruled in `5902360492` on objectstack-ai#20513. The census at the base reads 17 messages / 18 ids in 12 files. This stage rewrites 7 of them (6 titles and 1 expect message, 8 ids) in 5 files: each now states what its record decided, or drops the number where the title already says it. The other 10 messages / 10 ids stay, 4 because earlier stages decided they are not citations and 6 because an assertion matches the string against text this claim does not let the stage edit; both groups are named under "What stays". Text only: no assertion, identifier, test count, code comment, file name or non-test file changes. No file is deferred. ## Census (re-taken first) The instrument is stage 28's `census28.cjs`, byte-identical (md5 `31d8488b5194b8d3048e3fcaec0efaed`, the value stages 28 and 29 published): an AST walk over the `packages/spec/src` test files, one message per folded string (a lone literal, a template, or a plus chain) that matches the gate's id pattern, a title when the folded root is argument 0 of a describe / it / test / suite / bench call, comments never read. It was run against the three published readings before it was trusted, and all three reproduce exactly: 128 messages / 130 ids in 37 files at `f7b8a5932b`, 191 / 200 in 53 files at `aa09db58c9`, 73 / 76 in 24 files at `b7e01fbbd`. | reading | messages / ids | files | |:--|--:|--:| | base `11d119ab1` | 17 / 18 (titles 6 / 7, other 11 / 11) | 12 | | stage 29's landing `0ef9029da` | 17 / 18, per file equal to the base | 12 | | this head | 10 / 10 (titles 0 / 0, other 10 / 10) | 7 | | the 5 edited files, this head | 0 / 0 | 0 of 5 | Stage 29's ACCEPT carried 16 / 17 (ui 9 / 9 in 7 files, automation 2 / 3, ai 2 / 2, api 1 / 1, contracts 1 / 1, kernel 1 / 1). The base reads 1 / 1 more, all in `ui`: the title `carries no ruling date and no tracker id (objectstack-ai#22093)` at `view-submit-redirect-url.test.ts:341`, which PR objectstack-ai#22322 (`ad381fd94`, landed 2026-10-09T00:35Z) added after stage 29's head. So `ui` reads 10 / 10 in 7 files, and nothing else moved. The census at `origin/main` `e75dceddd` (8 commits past the base, none touching the 12 files) reads the same 17 / 18 in the same 12 files, so nothing regrew while this stage ran. The reading is within one message of the claim's, so there was no re-cut. Per file, messages at base: `ai/build-progress` 2, `api/meta-item-response-shapes` 1, `automation/builtin-node-config` 2 (3 ids), `contracts/approval-service` 1, `kernel/manifest` 1, `ui/action-description` 1, `ui/component-props-unknown-members.pin` 1, `ui/dashboard-chart-structure-refusal` 2, `ui/dashboard` 2, `ui/notification` 1, `ui/strictness-batch14` 1, `ui/view-submit-redirect-url` 2. Controls: - Pathspec: the 12 named paths hit the control word `describe(` in 12 of 12 files and a nonsense word in none; the census scanned 12 of 12. - Planted, in a scratch tree: an id in a describe title, a plus-chain title, an expect message, a template literal, a cross-repo spelling and a ledger-style string each read once (6 / 6); a comment, a six-digit colour, an HTML entity, a two-digit number and a hex colour with a letter read 0. - Lit and dark inside the group: the 5 edited files read 1, 2, 1, 1 and 2 messages at base and 0 at the head; the 7 untouched files read the same at both ends. ## Deferral At the census (2026-10-09T03:03Z) 17 PRs were open; at the re-scan before opening this PR (04:13Z), 13. Every file list was read through REST (605 and 593 rows). None touches any of the 12 files: lit control `api/protocol.test.ts` (PR objectstack-ai#22323) found, dark control 0. Of the four PRs the claim named, objectstack-ai#22380 has landed and objectstack-ai#22315, objectstack-ai#22323 and objectstack-ai#22215 are open; none of them touches a file in this group. Deferred files: none. ## What changed 7 literals, one line each, in 5 files: +7 / -7. Every file keeps its line count. - `api/meta-item-response-shapes.test.ts:226`: the `[objectstack-ai#22114] ` prefix goes; the title already says what objectstack-ai#22126 landed, that the read serves the version token and the 409 carries the current one as data. - `automation/builtin-node-config.test.ts:434`: "a CEL envelope beside literals; the `{token}` dialect retired". objectstack-ai#14149's ruling A made an assignment value a CEL envelope beside literals, and objectstack-ai#19939 retires the `{token}` dialect in flow value slots; the title already stated both, so only the two numbers go. - `automation/builtin-node-config.test.ts:485`: the `[objectstack-ai#19939] ` prefix goes from the REFUSES title. - `kernel/manifest.test.ts:681` and `ui/action-description.test.ts:235`: the trailing `(objectstack-ai#22093)` goes. objectstack-ai#22093 decided that author-visible help and refusals carry no service-interface name, ruling date or foreign example id, and both titles already say what their bodies pin. - `ui/view-submit-redirect-url.test.ts:341`: the trailing `(objectstack-ai#22093)` goes from the title. - `ui/view-submit-redirect-url.test.ts:118`: the expect message `states the rule, not its ruling date (objectstack-ai#22093)` drops the number. It is an assertion's failure message, so it was needle-checked first (below) and is the one declared non-title string. All seven are "drop a number the title already explains". None needed a rewrite in new words, because each title already carried the decision. ## What stays, and why 10 messages / 10 ids in 7 files, none edited. Four are CSS hex colours, not citations. `colors: ['objectstack-ai#111', 'objectstack-ai#222']` at `ui/dashboard-chart-structure-refusal.test.ts:94` and `palette: ['objectstack-ai#111', 'objectstack-ai#222']` at `ui/dashboard.test.ts:124` are fixture input the schema under test reads. Stage 21's ACCEPT (`6001279159`, decision A) kept them by file and line, and every later stage carried them forward. Six are strings that an assertion matches against text outside this stage's edit surface. Moving one at the same strength means editing a non-test source docblock (and, for the first two, its generated reference page) or the assertion that matches it. The claim forbids both and says to stop and report, so none is touched; `open_questions` in the report carries the decision. - `ai/build-progress.test.ts:236` `'cloud#2172'` and `:237` `'objectui#7388 block 2'`: `toContain` over the source text of `ai/build-progress.zod.ts` (docblock lines 8, 27 and 85), which `content/docs/references/ai/build-progress.mdx` renders. - `contracts/approval-service.test.ts:274` `'objectstack-ai#16495'`: `toContain` over the docblock above `continueRestoredRun` in `contracts/approval-service.ts` (line 999). - `ui/notification.test.ts:123` `'// [objectstack-ai#4610]'`: the locator of the tombstone note in `ui/notification.zod.ts:94`; the file's own `toMatch(/^\[objectstack-ai#4610\]/)` at `:134` reads the same note. - `ui/strictness-batch14.test.ts:395` `'objectstack-ai#5015'`: `toContain` over `ui/notification.zod.ts` and `ui/sharing.zod.ts`. - `ui/component-props-unknown-members.pin.test.ts:322` `ruling: 'decision card objectstack-ai#21704, fork 4, letter B (record 5979239990)'`: the file's own assertion at `:417` matches the value with `/objectstack-ai#21704/`. Stage 20's ACCEPT (`5998488373`) kept it for this reason and sent it to the needles' stage. Readers of the seven rewritten strings: none. `git grep -F` at HEAD over the tracked tree outside the 12 files, with the full literal, a 24-character window around each id, and the text on each side of each id (29 needles over all 17 sites): the only hits are the readers of the kept strings named above, the lit control (`composeStacks` in `stack.zod.ts`) hits and the dark control does not. The same needles searched inside the 12 files, outside each literal's own span: the only hits are two code comments beside `:322`. The five short needles (`cloud#2172`, `objectstack-ai#16495`, `// [objectstack-ai#4610]`, `objectstack-ai#5015`, `objectstack-ai#21704`) fall under the script's 12-character floor, so their readers were confirmed by direct `git grep -F` with a dark control. ## Cited records Read with their comments as the API serves them: objectstack-ai#22114 (8 of 8 comments; landed as PR objectstack-ai#22126), objectstack-ai#14149 (12 of 12; ruling A `5507504961`, landed as PR objectstack-ai#15113), objectstack-ai#19939 (15 of 15; pass 1 landed as PR objectstack-ai#22259, the card stays open), objectstack-ai#22093 (17 of 17; PRs objectstack-ai#22125, objectstack-ai#22309 and objectstack-ai#22322), and the four PRs themselves. objectstack-ai#19939 is still open: its pass 1 refuses the `{token}` dialect in flow value slots and keeps two spellings (the date macros and `{$User.*}`) until CEL can write them. The describe's PRESERVATION test still accepts those two, and the title keeps the record's own verb, "retired", as PR objectstack-ai#22259 wrote it. The title and the test body say the same thing the record says. ## Verification At head `8885dbf1c` (one commit on base `11d119ab1`): - **Text only.** `textonly28.cjs` (stage 28's, md5 `957eff6b3837d762b8e03d070155930a`) on all 12 base copies against their heads: 12 / 12 SAME. 7 changed tokens, as predicted in writing at 2026-10-09T03:08Z before any edit or test run: 6 titles and 1 declared string (`--declared 118`). Every other string token, identifier, number, punctuation mark and comment is byte-equal. 16 controls, expectations written in the script before the first run, 16 / 16 as predicted: an identifier rename, a numeric literal, a comment edit, an undeclared expect message, a rewritten title given a new id, an id-free title edited, one title reverted to base (SAME, 0 changed), a declared label without `--declared`, a declared line plus another changed string, the declared line alone (SAME, 2 changed), a title re-split into a plus chain, a test added, an untouched file (SAME, 0 changed), an id appended to a rewritten title, a kept needle rewritten, a kept hex colour rewritten. The two controls that mutate a string beside the declared line fail at the mutated line, not at 118. - **Tests, 12 files, base and head.** `--project local --project repo` with the JSON reporter, 618 tests in 88 suites each side, all passed. Per-file test count and status sequence identical in 12 / 12. 23 full names changed (4 + 14 + 1 + 3 + 1), 0 mismatches against the plan. Names carrying `#` plus digits: 23 at base, 0 at head. Duplicate full names: 3 and 3, the same three `[object Object]` it.each rows at both ends. - **Full spec unit tier at the head**, under the verify lock: `Test Files 626 passed (626)`, `Tests 18743 passed | 1 todo (18744)`. - **Build and typecheck**, under the verify lock: `turbo run build` over `packages/*` and `packages/*/*`, `Tasks: 71 successful, 71 total`; `@objectstack/spec` `typecheck` exit 0 with `check:test-typecheck` holding 52 files / 246 errors / 135 pinned signatures, the same figures as stage 29; the 12 files are all in the `tsconfig.test.json` program. - **Gates.** `dispatch-gates.mjs --commands` at the head derives 79 (stage 29's 77 plus `check:authorable-surface` and `check:yaml-examples`); all 79 exit 0, and `--ran` reconciles 79 derived, 79 run, 0 NOT-MEASURED. The five artifact-roster families that keep their roster in a directory one of the paths is in (`check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`) and `check:generated` (all 15 artifacts up to date) also exit 0. - **ESLint**, `--no-inline-config`, 12 files: 0 errors, 0 warnings. Population from ESLint's own config: 12 configured, 0 ignored, 0 with a type-aware parser option, so this diff cannot move the verdict of a file it does not touch. - **Skip-changeset.** `npm pack --dry-run --json --ignore-scripts` in `packages/spec`: 2069 files, 0 `*.test.ts`, 0 of the 5 edited files; controls `src/stack.zod.ts`, `dist/index.mjs` and `package.json` present. The rewritten expect message occurs in 0 files of `dist/`; the control `Unrecognized key` occurs in 42. Nothing published changes. - **Governed.** `check-governed-merges.mjs --test` on the 5 paths: 0 of 5, not governed; 14 changed lines. - **Merge.** `git merge-tree --write-tree` onto `origin/main` `e75dceddd`: clean. - **Bytes.** 0 added lines carry `#` plus digits; 0 control bytes in the changed files. Declared narrowing: the 12-file base and head comparison ran outside `os-verify-lock.sh`, after three queue turns (about 28 minutes) ended without a grant. It is a 12-file run with two workers; the workspace build, the typecheck and the full unit tier all ran under the lock. The gates are `check:*` runs, which do not use the lock. ## Acceptance notes - **Regrowth continues.** Since stage 27's landing, four PRs (objectstack-ai#22125, objectstack-ai#22126, objectstack-ai#22259 and objectstack-ai#22322) added 7 messages / 8 ids to test strings in files that already existed, one of them to a title objectstack-ai#22322 wrote while stripping a ruling date from a describe. Test files sit outside `check:doc-authoring`'s ledgered leg, and the ruling adds no gate, so the per-stage census is the only instrument. An observation about the burn-down's denominator, not a class a / b / c finding. - **Comments are untouched.** Code comments in these files still cite ids (for example `// ─── assignment (objectstack-ai#14149) ───` at `builtin-node-config.test.ts:432`); comments are objectstack-ai#20234's share. --- _Generated by [Claude Code](https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn)_ Co-authored-by: Claude <noreply@anthropic.com>
…afts-header-label-m81
The merge of origin/main kept the branch's side of the generated content/docs/references/api/protocol.mdx and dropped main's: the submitBehavior description no longer carries a ruling date. Regenerated from the merged source after taking main's side, so the page now carries both main's description and this branch's _drafts label rows. Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs read, at 2026-10-09T11:07Z: card #22200 (body and all 6 comments: triage ① Derived judgments
② Semver levelChangeset Clause-②: yes (widening) — agreed. ③ Boundary flags
Implemented-by: VERDICT: PASS |
Fixes #22200
Clause-②: yes (widening: a new field on a published response schema)
What changes
Each row of the pending-drafts list (
GET /api/v1/meta/_drafts, the runtime'sGET /metadata/_drafts,client.meta.listDrafts()) now carrieslabel: the draft body's own top-levellabel, as authored, ornullwhen the body declares none. It is never the machine name standing in for a missing label.packages/spec/src/api/protocol.zod.ts):ListDraftsResponseSchemarows gainlabel: I18nLabelSchema.nullable(), required on the wire, with a describe that says it is the body's own label and never a name fallback.packages/metadata-protocol/src/sys-metadata-repository.ts):SysMetadataRepository.listDraftsreads the label off thesys_metadatarow it already fetches (draftBodyLabel), so there is no second query. The declared return gainslabel: I18nLabel | null.packages/metadata-protocol/src/protocol.ts):ObjectStackProtocolImplementation.listDraftsdeclares the same member and passes the repository's rows through unchanged. The docblock on the publish batch's closure read, which listed the six header members, now lists seven.The maintainer's direction on objectstack-ai/objectui#11862, quoted by the card: 「所有地方以标签为主,机器名只作为次要信息」. The producer carries the label; consumers do not each fetch one. Triage's grade, quoted: "Each
_draftsrow carries the draft body's own label, ornull"; "Add a row whose draft body declares no label and readsnull, never a fallback."Measurements behind the shape (each PM hypothesis, measured on this branch's base
238222d8c)H1: holds, with one refinement.
listDraftscallsengine.find('sys_metadata', { where, context })with no field projection, so the whole row is in hand. The body is NOT a column of its own:sys_metadatahas nolabelcolumn (packages/metadata-core/src/objects/sys-metadata.object.ts); the label lives inside themetadatatextarea column, stored as JSON text (an already-parsed object on a JSON-column dialect). So the producer parsesrow.metadataonce per draft row, through the samestoredRowBodyreaderrowToItemuses. No second query.Label spelling per metadata type, read from
getMetadataTypeSchema(type)over everyDEFAULT_METADATA_TYPE_REGISTRYentry (builtpackages/spec/dist,z.toJSONSchema, input side):labelshapestringI18nLabel(string or inline locale map)labelkeynull)labelis judged byI18nLabelSchema)No registered type spells its display label
titleor nests it. Off-registry:connector,sharing_ruleandwebhookspell itlabel(string);analytics_cubespells ittitleand its schema refuseslabelwith guidance totitle, so a cube draft readsnull(see Acceptance notes).No ADR-0087 conversion rewrites a top-level
label(packages/spec/src/conversions/registry.ts: the onlylabelentries are the nesteddatasource.external.labelremoval and fixtures), so the stored spelling is already the canonical one and reading it raw needs no conversion replay.H2: holds; no third file moves. Both faces serve the protocol's return whole:
packages/rest/src/rest-server.tsGET ${metaPath}/_draftsends inres.json(result), andpackages/runtime/src/domains/meta.ts_draftsends indeps.success(data).packages/client/src/index.tsmeta.listDraftstypes its answer as the spec'sListDraftsResponse.packages/restandpackages/runtimeare untouched.H3: holds. Seven types declare
labelasI18nLabelSchema(string or inline locale map such as{ en, 'zh-CN' }). What the producer does with each shape:I18nLabel(resolveI18nLabelin@objectstack/spec/ui, or objectui'spickLocalized).null:null.null.I18nLabelSchema.safeParseis the judge, so the declared field never carries a shape its own type rules out. Draft saves are schema-validated (resolveOverlaySchemainsaveMetaItem), so this is reachable only for rows stored before a type's schema was enforced on save, or for a type with no registered schema.null, and the draft stays listed. This islockHead's answer for the same bytes, in the same file, for the same reason: a header listing must not become a parse failure. The draft stays visible and discardable, while every read of its body still fails loudly.Landing and surface
The landing matches the claim's surface, plus one test file outside it, named here with its reason:
packages/objectql/src/sys-metadata-repository-list-drafts.test.tsis a test ofSysMetadataRepository.listDraftsthat lives inpackages/objectql. Its finding:/meta/_draftsserves DRAFT object schemas unmasked — the one ADR-0106 outlet left uncovered #6599 disclosure pin asserted exactly six header keys, so it goes red on any seventh. It now names seven keys. Its fixture now carries the body in themetadatacolumn the repository actually reads, beside the two older spellings, so the whole-payload sweep covers the real column. It asserts the label arrives while every field-level secret stays off the wire. Its docblock said the routes had "no capability gate"; both routes now gate onmayReadPendingDrafts, so that sentence is corrected.packages/metadata-protocol(domain:engine), declared by the seat on [PM seat] domain:engine — ⏳ vacant #6367.Tests
Readings at head
317b208be. Every run went throughscripts/pm/os-verify-lock.shon a shared box. The full report comment on the card carries the rest.@objectstack/spectests:local, all 8 shards: 625 files, 18713 passed, 1 todo, 0 failed;repo, shard 1/2: 27 files, 399 passed. The report states the reading for shard 2/2, or NOT MEASURED with the reason;typecheck(tsc, scripts and the test layer) exits 0.@objectstack/metadata-protocoltests: the whole suite has 221 files passed and 3 skipped, with 28287 tests passed and 19 skipped.typecheckexits 0.@objectstack/objectql:sys-metadata-repository-list-drafts.test.tshas 7 passed.typecheck(with the test layer) exits 0.@objectstack/rest: the 18 test files that name_draftshave 906 tests, all passed.packages/restitself is unchanged.node scripts/pm/dispatch-gates.mjs --commandswas re-derived after the change and gave 113 families. All 113 were run, every one exited 0, and--ranreports "113 derived famil(ies) accounted for — 113 run, 0 NOT-MEASURED".pnpm --filter @objectstack/spec check:generated --fixregenerated onlycontent/docs/references/api/protocol.mdx, andcheck:docsre-checked green.What the new pins cover:
@objectstack/spec: the schema pin "ListDraftsResponseSchema declares the pending-drafts body" now carries a string label, an inline-locale-map label and a no-label row readingnull. It also pins that a row omittinglabelis refused atdrafts.0.label.@objectstack/metadata-protocol(sys-metadata-repository-14938-list-drafts-updated-at.test.ts, which shares its pinned engine double): the declaration-keyedCONFORMStable gainslabel, so every existing case also checks it. New#22200cases cover:put(..., { state: 'draft' })and read back;null, not the name;null;nullwhile the draft stays listed;ObjectStackProtocolImplementation.listDraftspassing the label through, its response parsed byListDraftsResponseSchemawith every member preserved, and exactly seven header keys with no body residue.scripts/ablation-replace.mjs: the producer linelabel: draftBodyLabel(row),was replaced by the forbidden fallbacklabel: row.name ?? null,. All 10#22200cases went red and the 8#14938cases stayed green. The file was restored to its HEAD blob andgit diff HEADwas empty.ListDraftsResponseliteral withoutlabel, planted in a metadata-protocol test file, failstscwithTS2741: Property 'label' is missing. So the consumer reads the rebuilt spec.d.ts. The file was restored to its HEAD blob.Acceptance notes (observations; not filed)
analytics_cubedrafts readlabel: null.CubeSchemaspells its display nametitleand refuseslabelwith guidance totitle. This PR reads onlylabel, the field the ruling names. A per-type display-key mapping would be a second vocabulary for the producer to keep in step, so it is not added here. Reach is not measured: no cube draft producer was found in this repository. Carrier: none.label('') is carried as''. It is the body's own value and passesI18nLabelSchema. Whether a reader treats it as absent is the reader's choice.MetadataDraftHeaderin@object-ui/data-objectstackfollows after the pin bump that carries this field. No objectui change is made here.Generated by Claude Code