Repository navigation
feat(meta): the /meta item read serves the version token, If-None-Match: * pins a first write, and the 409 carries currentVersion - #22126
Conversation
…ch: * pins a first write; the 409 carries currentVersion (WIP) Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
…t's currentVersion at the door (WIP) Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
…ead (WIP) Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
…r the read token and the conflict body (WIP) Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
… reference docs and the strictness ledger counts Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 31 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 143 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 4f51f8374f71df82b70ea92fd6c83d733e6ec14a && git checkout 4f51f8374f71df82b70ea92fd6c83d733e6ec14a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8fc50b7647d30db2a0837877cf251c1163a3239e e2a28fadf1c02a48c965f0303776872e1a323b79 && git checkout -B drift-repro 8fc50b7647d30db2a0837877cf251c1163a3239e && git merge --no-ff e2a28fadf1c02a48c965f0303776872e1a323b79
node scripts/docs-audit/affected-docs.mjs --json 8fc50b7647d30db2a0837877cf251c1163a3239e
|
…ta-read-version-token
…ETADATA_CONFLICT; the changeset grades the two 400 refusals as a narrowing Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
…ta-read-version-token
…@objectstack/types; the alternate-spelling read pin names the version head read Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: PR #22126 (card #22114), branch Check-runs on the head (REST check-runs API, both reads): first read at 2026-10-08T02:54:41Z — 39 runs, 33 ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
… verdict (objectstack-ai#22032 pass 3) (objectstack-ai#22151) Part of objectstack-ai#22032 Clause-②: no (narrowing) This is pass 3 of objectstack-ai#22032: a field option's `visibleWhen`. Pass 4 (the object's own action predicates) stays fenced, and the card stays open for it. ## What changes **The object save door gives the build's verdict on a field option's `visibleWhen`.** `formulas.mdx` says "the same `validateExpression` validator backs `os build` and metadata registration". After pass 2 the object door ran the whole field walk except the per-option loop, which kept its own guard. So an object whose option carried `visibleWhen: 'amount > 1'` (a bare field reference) still saved with a 200, while `os build` refused it at error. The server's option check cannot evaluate such a predicate and fails open (logged, allowed through), so the gate it declares is never enforced (read from `evaluateOptionVisibility` in `packages/objectql/src/validation/rule-validator.ts`). - **The lift is the guard, nothing else (H1 held).** On `origin/main` `8fc50b7647` the loop read `for (const [oi, opt] of (objectWrite ? [] : recordsOf(f.options)).entries())` (`validate-expressions.ts:2072`), under a `[objectstack-ai#22032] FENCED on an object write (pass 3 …)` comment. It now reads `recordsOf(f.options)`. On an object write the loop runs at the build's own position in the field walk, so the door gets both of the option's checks: - `check(optionWhere, opt.visibleWhen, objectName, 'record')`; - `refuseFieldTraversal(optionWhere, 'option visibleWhen', …)`, the refusal of a read through a reference field on `record` or `previous`. - **`current_user` keeps the build's two verdicts (H2).** An option's evaluator binds the acting user (ADR-0068 D1), so the build accepts `current_user` on an option and refuses it on the field-rule slots one level up. The door now gives both verdicts as the build does. The showcase's role gate, `'org_admin' in current_user.positions`, still saves on an option, and the same text on the field's own `visibleWhen` is refused at both doors. Both are pinned. - **No registry change (H3 re-verified).** The `validateStackExpressions` entry declares `runtimeTypes: ['flow', 'action', 'hook', 'object']` (`authoring-rules.ts`), and `runtimeAuthoringRulesFor('object')` (`runtime-gate.ts`) dispatches it. `runtime-gate.ts` is untouched. - **Docblocks made true.** `StackExpressionOptions.runtimeWriteType` now names four admitted passes and one fenced pass. `AuthoringRuleContext.runtimeWriteType` in `authoring-rules.ts`, the one line that reaches a built `.d.ts`, names the per-option pass. The function-head comment and the field walk's two comments move with it. In `authoring-rules.ts` the registry entry gains a `[objectstack-ai#22032, pass 3]` measurement comment, as passes 1 and 2 added theirs. Comments in four sibling test files are corrected so that none of them still says option `visibleWhen` is fenced. - **The door's verdict is the build's finding (H4).** The door's 422 issue and `runAuthoringRules('build', …)` give the same rule (`expression-invalid`), location (`object 'fx_option' · field 'province' option 'zj' visibleWhen`), path, message and hint. The pins compare these key by key. - **No code change in `packages/metadata-protocol`.** Only its test file gains the door-level pins. ## Pins - **Lint door:** `packages/lint/src/runtime-gate.object-option-visibility-writes.test.ts` (new, 14 tests). - LIT: one refused body per finding the pass gives. These are a bare `amount > 1`, an unregistered `sqrt(record.amount) > 1`, an unknown field `record.amont > 1`, a syntax error `record.country ==`, and the traversal refusal through `record.account` and through `previous.account`. Each is located at the option and asserted on its named subject. - CONTROL (the still-accepted cases): the `record.country` cascade, the showcase's `current_user.positions` role gate, a grant check plus role gate (`current_user.can(…) && …`), and a reference compared as a value (`record.account != null`). Each is clean at the door and at the build. - CONTRAST: `current_user` on the option and on the field's own `visibleWhen` in one body. The build and the door both give exactly one finding, at the field slot. - PARITY: for each refused body, the door's findings equal the build's. - The differential: a stored sibling's broken options are not this write's to answer for. - **The fence (enumeration pin)** in `packages/lint/src/runtime-gate.object-formula-writes.test.ts`. The fenced site is now pass 4's alone (an action `visible`). The option `visibleWhen` site moves to the lifted sites, beside the validation rule and the `requiredWhen`. The build flags all four sites. The object door flags the three lifted sites in the build's order, and `runStackExpressionPasses` on an object write returns exactly the build's findings for the admitted passes. - **Protocol door:** a new pass-3 block in `packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`, through the real `saveMetaItem`, `publishMetaItem` and `publishPackageDrafts`. - (a) A bare reference, an unregistered function and a read through a reference field are each refused on an active save. The answer is a 422 `INVALID_METADATA` carrying the build's located finding, and nothing lands. - (a) The card-shaped body is refused on a draft's promotion and on a package draft publish (`outcome: 'refused'`, `failed` naming the object with `INVALID_METADATA`, the row left a draft). The draft saves themselves still succeed. - (b) The showcase's cascade and its `current_user` role gate still save, and the row lands active. The role gate rides every refused body too, which each yield exactly one finding. - (d) For each refused body, the door and `os build` give the same finding on rule, where, path, message and hint. ## Reverse verification (one-off, from committed HEAD `23ce6c494c`) - **What was mutated.** `scripts/ablation-replace.mjs` (wrap mode) put the guard back on the option loop. The new text was `const ablationFence22032p3 = objectWrite;` followed by `for (const [oi, opt] of (ablationFence22032p3 ? [] : recordsOf(f.options)).entries()) {`. The anchor was hit once, 1 to 0, and the blob went `879fcb828037` to `73bd026d1554`. The outer script carried `trap restore EXIT INT TERM` on the absolute path. - **Rebuild and dist proof.** `@objectstack/lint` was rebuilt, and `ablation-dist-preflight` found the marker in 4 built files. - **Lint suites (source): 9 failed and 14 passed, as predicted.** - Red: the 6 LIT tests, PARITY, and the two fence tests that assert the lifted sites. - Green: the 4 CONTROL tests, CONTRAST, the differential, the registry test, the build-flags-each-site test and the six formula-door tests. - **Protocol pass-3 block (dist-mediated): 6 failed and 1 passed, as predicted.** Red: the three (a) saves, (a) on promotion, (a) on package publish, and (d). Green: (b). - **Restore.** The tool restored the file: blob `879fcb828037` equals HEAD, and `git diff HEAD` is empty. The whole tree had 0 changed paths. Lint was rebuilt, and `--absent` found the marker gone from all 14 built files. Both suites went green again: lint 23 of 23, and the protocol file 99 of 99. ## Measurements - **Corpus first: the stop condition was not met (H5).** Every object this tree ships was judged before the door changed. That is every `*.object.ts` under `packages/**` and `examples/**` (111 files) plus the two `app-multi-package` sub-stacks: 118 objects in 18 groups. Each was judged at the raw shape and at the `ObjectSchema.parse` shape (0 parse failures), with its own group as context. - 5 option predicates on 2 fields of 1 object, all on `showcase_cascade`: `province`'s four `record.country` cascades (`zj`, `gd`, `ca`, `tx`) and `tier`'s `restricted` role gate (`'org_admin' in current_user.positions`). - At base `8fc50b7647`: 0 build errors and 0 build warnings for the option pass, through `validateStackExpressions` and through `runAuthoringRules('build')`, at both shapes. There were 0 door expression findings over all 118 objects. - Non-vacuity: the 5 sites are judged. Mutating one cascade to a bare `country` and the role gate to `sqrt(record.amount) > 1`, in a copy, gave 2 build errors at those two options. - At head `23ce6c494c`, and again at the merged heads `06d3cad963` and `3c1d3262ee`: 0 door errors and 0 door advisories over every object, through `runRuntimeAuthoringRules` with type `object`, at both shapes. The harness passes each object's own group as context, so at `3c1d3262ee` every one of those saves is an update and also runs the stored-universe pass that objectstack-ai#22118 added. - Positive control in the same harness (an option `visibleWhen: 'amount > 1'`): 1 build error at every head. The door gave 0 at base and 1 at head. - **Which doors newly answer 422.** The active publish save, a draft's promotion, and a package draft publish. Each was measured through the real methods above. A draft save stays ungated, measured by the same pins. ## Clause-② (measured) - **Accept set: narrowing.** An object write in publish mode answered 200 for an option `visibleWhen` the validator refuses. It now answers 422 on the three doors above. - **Built entry declarations.** In `@objectstack/lint` one doc comment moves (`AuthoringRuleContext.runtimeWriteType`). `StackExpressionOptions` and `runStackExpressionPasses` are not in the built declarations. No exported signature moves. - **Changeset.** `.changeset/22032-object-save-door-option-visible-when.md` covers `@objectstack/lint` and `@objectstack/metadata-protocol` as `minor`. It carries `fix(lint)!`, the `Clause-②: no (narrowing)` line, a BREAKING section with the remedy, and ADR-0087 `not-required (no-migration-prescription)`. `@objectstack/metadata-protocol` is listed as passes 1 and 2 listed it, although no code moves there: its save, promotion and package-publish doors are where the BREAKING behaviour can be seen. `.changeset/pre.json` is absent on `origin/main` (read at `8fc50b7647`, 2026-10-08T01:54Z, at `ef1fcb26a2`, 2026-10-08T02:40Z, and at `7ef50a4fbb`, 2026-10-08T03:39Z), so the bump is `minor` with the BREAKING banner, as in passes 1 and 2. The changeset says it supersedes the earlier objectstack-ai#22032 entries' line that option `visibleWhen` is not judged at this door. ## Merges - **`06d3cad963`** merges `origin/main` `ef1fcb26a2` (PR objectstack-ai#22103), through `scripts/pm/os-regen-merge.sh`. It was clean. - **`3c1d3262ee`** merges `origin/main` `7ef50a4fbb` (parents `06d3cad963` and `7ef50a4fbb`), through `scripts/pm/os-regen-merge.sh`. That brought PR objectstack-ai#22129 (objectstack-ai#21982), PR objectstack-ai#22094, PR objectstack-ai#22134, PR objectstack-ai#22133 (objectstack-ai#22118, the gate's object-write baseline keeps the written item's stored self), PR objectstack-ai#22126 and PR objectstack-ai#22140. - `validate-expressions.ts` auto-merged. PR objectstack-ai#22129's edit is in the flow `script` / `subflow` region; the option loop's lift is unchanged. - One conflict: `packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`, one hunk. Both sides had added a new top-level `describe` block at the same place, after the pass-2 block. It was resolved by stacking: the pass-3 block first, then objectstack-ai#22118's stored-self block, and every line of both was kept. Against `7ef50a4fbb` the file shows only this branch's lines; against `06d3cad963` it shows only objectstack-ai#22118's 114 lines. - No generated path was touched by this branch. The rerun of the script took `origin/main`'s side of every generated path main moved, and that left nothing to commit. - This branch's own changes are the same before and after the merge: for each of the 9 files, the added and removed lines against the merge base are identical. The delta against `7ef50a4fbb` is 9 files, +467 / −45. - **The interaction with objectstack-ai#22118, measured.** objectstack-ai#22118 adds a third, stored-universe pass on an update into a context collection. A finding whose path names no entry is judged as one on the written item. The expression rule's paths are `where` strings, so an option finding is always judged that way. - A probe ran the real gate (`runRuntimeAuthoringRules`, type `object`) over 20 cases, against this branch's lint source before the merge (`06d3cad963`) and after it (`3c1d3262ee`). The cases are a create, an update over a stored self that is broken and over one that is clean, and a stored sibling that is broken, each for three refused bodies, plus two still-accepted bodies. - The verdicts are identical, case for case. Re-saving a broken option is still refused, including over a broken stored self, because re-saving a row is writing it. A clean save over a broken stored self passes. A broken sibling is never charged. - The pass-3 pins (differential, PARITY, LIT, CONTROL, CONTRAST) and objectstack-ai#22118's pins all pass at `3c1d3262ee`: lint 48 of 48 across the three files, and the protocol file 103 of 103. - This matches the code. The option pass reads only the written object's own field index, and binds `record` and `previous`, never `parent`, so the stored universe has nothing extra to offer it. ## Tests and gates (all at `3c1d3262ee`, the merge of `origin/main` `7ef50a4fbb`) - **`main` moved during the run (H6).** PR objectstack-ai#22103 landed as `ef1fcb26a2` and touched two files this pass edits, `authoring-rules.ts` and `runtime-gate.object-writes.test.ts`, in other hunks. It was merged with `scripts/pm/os-regen-merge.sh` as a merge commit. The merge was clean, and no generated path was taken from either side. After the merge: `pnpm install --frozen-lockfile`, a full turbo build (72 tasks), and `@objectstack/spec check:generated` ("All 15 generated artifacts are up to date"). objectstack-ai#22118 and objectstack-ai#21982 had not landed at that read (2026-10-08T02:40Z). Both have since landed, and they are merged at `3c1d3262ee` (see Merges). After that merge the same sequence ran: a full turbo build (72 tasks) and `check:generated` ("All 15 generated artifacts are up to date"). - **`@objectstack/lint`:** 125 files and 5729 tests passed. `typecheck` exit 0, with its test-typecheck included (`--listFiles`: the five touched or new lint test files are in the `tsconfig.test.json` program). - **`@objectstack/metadata-protocol`:** 221 files passed and 3 skipped; 28260 tests passed and 19 skipped. `typecheck` exit 0 (`--listFiles`: the door test file is in the program). - **Consumer readings.** Every package was built at the head named. - `@objectstack/objectql`, 8 files and 282 tests passed: `publish-package-drafts-response-conformance`, `save-meta-response-conformance`, `publish-meta-response-conformance`, `plugin.integration`, `engine-field-predicate-fault`, `engine-option-permission-predicate`, `validation/rule-validator.option-visibility` and `engine`. - `@objectstack/rest`, 11 files and 197 tests passed: every `meta-object-*` file and `meta-publish-package-scope`. - `@objectstack/cli`, 3 files and 16 tests passed, run as `--project integration` because the tier predicate puts them there: `validate-field-predicate-traversal` (which asserts an option `visibleWhen` `expression-invalid` finding), `authoring-rule-command-parity` and `verify-author-time-stage`. The three nightly-tier `*.e2e` files that assert `expression-invalid` are left to CI. - The search for other consumers covered every test file in the repository carrying `visibleWhen`, matched against the save-door entry points. Only the two packages above save an option `visibleWhen` through a door. - **Gates.** `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 63 commands, the same set at `23ce6c494c`, `06d3cad963` and `3c1d3262ee`. At `3c1d3262ee` all 63 exit 0, each exit code captured before any pipe. `--ran` reconciles 63 derived, 63 run, 0 NOT-MEASURED and 0 UNRUN, with an exit code recorded for each. The printed artifact-roster block names 51 families, and all 51 ran at `3c1d3262ee`, each with exit 0. That is 47 in the same battery, one (`check:engine-double-contract`) already among the 63, and the three PR-scoped ones (`check-partof-closing-keyword`, `check-closing-target-claim`, `check-single-claim-paths`) run with this PR's number and this body. The same 63 also ran at `06d3cad963`, all exit 0. At `23ce6c494c`, before the merge, the same 63 ran: 61 exited 0 on the first run. `check:dual-build-cjs-loads` and `check:lean-entry-closure` first exited 3 (PREREQUISITE NOT MET: no full build) and exited 0 after the full build. - **ESLint, narrowed to the 8 touched TypeScript files** (`--no-inline-config --format json`): 8 files, 0 errors and 0 warnings. Each file is matched by `eslint.config.mjs` (`--print-config`), and none was ignored. The config enables no type-aware linting (no `parserOptions.project`), so this diff cannot move the verdict on any untouched file. ## Acceptance notes - **Out of this pass, reported for the seat: an option `visibleWhen` reading `parent` gets no verdict at the build, so none at the door either.** - Measured at `23ce6c494c` with scratch tests that were deleted afterwards. Through the real `saveMetaItem`, an object whose option carries `visibleWhen: "parent.status == 'closed'"` saved with success, and the row landed `active`. `runAuthoringRules('build')` gave 0 findings. - The server's option check (`evaluateValidationRules`, authenticated caller, the option picked) then logged `failed to evaluate (authenticated caller) — allowed through`, with `Unknown variable: parent`, and admitted the value. The option evaluator binds `record`, `previous`, the user and permissions only. - This is a gap in the build, which the door now mirrors. This card's contract is parity with the build, so it is not changed here. - **A code comment names an old spelling.** The comment above the option loop calls the showcase's legal usage `'admin' in current_user.positions`. The showcase now writes `'org_admin' in current_user.positions`, and the pins use that spelling. The comment is not changed here: per the contract review, it rides pass 4. - **The pending objectstack-ai#22032 changesets.** Pass 1's and pass 2's changesets each list option `visibleWhen` under "Unchanged", which was true at their heads. This pass's changeset says it supersedes that line rather than editing them, the same way pass 2 left pass 1's changeset alone. Per the contract review, reconciling those lines rides pass 4. - `formulas.mdx` could name the object save door. That would be a docs addition, not a correction of a false line. - **Contract review.** Triage's grade asks for one per pass. A PASS is on record for head `06d3cad963` (`6051573476`). The head has since moved to `3c1d3262ee` by the merge above, so the review for this head is the seat's. --- _Generated by [Claude Code](https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…nd unpinned package-less draft save is accepted (objectstack-ai#22128) (objectstack-ai#22185) Fixes objectstack-ai#22128 Clause-②: yes (widening) The fix restores ADR-0008's stated behaviour: an unpinned save is last-writer-wins. The `packageId` describe now states what the code already does. `@objectstack/metadata-protocol` is `minor`, because `SysMetadataRepository.headAt` is one added public method on an exported class. It is the only change to the package's public surface. The precedent is `.changeset/21986-metadata-protocol-declines-stored-row-public.md`: one added method, `yes (widening)`, `minor`. `@objectstack/rest` and `@objectstack/spec` stay `patch`. The contract review (`6053073603`) found the earlier declaration of `no` wrong on this one point. Triage's `no` came before the fix had a shape. A widening is not a break, so no ADR-0087 disposition marker applies: `check-adr-0087-registration` reads `1 non-breaking changeset(s)`. ## Direction (triage `6049925243`, binding) > One function resolves "the draft head for this address". The save's expected-parent read, the repository's lock comparison and the read's served `version` all call it. > > It resolves a package-less draft over a package-bound active row the way the repository already does. That inheritance is objectstack-ai#11087's fix, pinned by `sys-metadata-repository.draft-package-inherit.test.ts`. > > ⛔ Not by dropping the inheritance: that re-opens objectstack-ai#11087 (drafts lose their package). > > The `packageId` describe, which says absent means env-local, is aligned in the same PR to state the inheritance. The seat's rider `6051335917` adds two items: `?package=all` on the item read, and the `metadata_conflict` spelling in the publish route's description. Patch round 1 adds a third member of the class, which the contract review escalated: the credential carry-forward. See [the section below](#the-credential-carry-forward-patch-round-1). ## What was wrong (measured before the fix) The new real-route test `packages/rest/src/meta-draft-head-package-inheritance.test.ts` was committed first (`eb42ce01e`). It ran against a `dist/` built at the base `7ef50a4fb`, which already carries PR objectstack-ai#22126: ``` pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/meta-draft-head-package-inheritance.test.ts Tests 6 failed | 1 passed (7) step 3: {"error":"view/case_grid has been modified since you loaded it. Expected parent null but current is hmac-sha256:1de899d7…","code":"METADATA_CONFLICT",…}: expected 409 to be 200 ?state=draft after step 2: expected null to be 'hmac-sha256:1de899d7…' ?package=all read: expected null to be 'hmac-sha256:6df7b221…' ?state=draft&package=all: {"error":"No pending draft exists for view/case_grid.","code":"NO_DRAFT"}: expected 404 to be 200 ``` The one case that passed is the env-local control, which is meant to pass both before and after. The three readers resolved three different rows: | reader | before | after | |---|---|---| | the save's expected parent (`saveMetaItem`, through `storedHeadAt`) | `repo.get` at the key it was handed. With no package, that is the package-unbound row (`package_id IS NULL`). | `repo.headAt`, which runs `resolveWriteHead` | | the read's `version` (`readVersionToken`, through `storedHeadAt`) | the same `repo.get`. The REST read door forwarded `?package=all` verbatim, so the key was `package_id = 'all'`. | the same `repo.headAt`. `all` is folded first, as the save door folds it. | | the repository's lock (`SysMetadataRepository.put`) | the active row's package, inherited for a draft that names no package (objectstack-ai#11087), else the named key, with orphan-draft adoption as a fallback | `resolveWriteHead`, the same code moved into one function | ## The change - **The one resolution: `SysMetadataRepository.resolveWriteHead`** (private, `packages/metadata-protocol/src/sys-metadata-repository.ts`). For a write of `state` that names `packageId`, it returns the row the write upserts (`null` for a create) and the binding a create stamps. It contains objectstack-ai#11087's draft inheritance, which reads the active row across the caller's org and the env-wide scope, and the orphan-draft adoption, both unchanged. It lives in the repository because `put`'s lock and stamp already live there, and the protocol already calls the repository. Calling it from the protocol adds no new layer dependency. Keeping it in the protocol would have made the repository call up into its caller. - `put` calls it inside its transaction, for the lock and the stamp. The inheritance lookup now runs inside that transaction too. It used to run just before it. - `SysMetadataRepository.headAt(ref, { state, packageId })` is the new public method. It answers `{ head, packageId }`. `head` is the row the write upserts, served as `get` serves a row, so its `hash` is the version `lockAccepts` accepts. `packageId` is the binding that write targets. - **The callers rewired.** - `ObjectStackProtocolImplementation.storedHeadAt` now asks `repo.headAt`, not `repo.get`. Its callers are unchanged: - `saveMetaItem`'s expected-parent read, for both the `draft` and the `publish` mode. Unpinned, the parent is the head. Pinned, the token is judged against the head (`storedParentForToken`). - `readVersionToken`, the item read's `version`, for both `?state=draft` and the uncached plain read. - `storedBodyForCarryForward` (patch round 1). Both of its reads now ask `repo.headAt`: the row the save overwrites, and with no draft yet the active row in the package the draft is stamped into. - REST: `metaItemPackageBinding(raw)` (`packages/rest/src/rest-server.ts`) is the one reading of `?package=` on the three `/meta/:type/:name` doors. Before, `PUT` and `POST …/publish` each had an inline copy, and `GET` had none. `all` and the empty value name no package. `GET`'s cache bypass (`packageScoped`) still reads the raw parameter, so `?package=all` keeps the uncached arm and keeps serving `version`. - **Spec, descriptions only.** - `SaveMetaItemRequestSchema.packageId` now states the inheritance. It also states that the same row is the one the parent-version resolution reads and the one the item read serves as `version`. The protocol reference is regenerated (`gen:docs`, through `check:generated --fix`). - The `POST /:type/:name/publish` route description now says 409 `METADATA_CONFLICT`. - `GetMetaItemResponseSchema.version`'s describe (from objectstack-ai#22114) was re-read against the fix and still holds: "the STORED row a save to this item compares against, at this read's scope". It is unchanged. One behaviour moves on `GET /meta/:type/:name?package=all`: the served content. Before, `all` was a package address, so the read preferred a row bound to a package literally named `all`, then the package-less row. Now the read serves what a read with no `?package=` serves. That includes the draft the package-less draft save stored in the item's package, which before answered `404 NO_DRAFT`. objectui's editor sends this read (read at objectui `9990f9e`): `ResourceEditPage.tsx` scopes its draft and layered reads with the router's raw `?package=`, which does not fold `all` (its own docblock says so), while its saves fold it. ## The credential carry-forward (patch round 1) **The defect.** A save of a type whose read withholds a credential carries the stored credential back into a body that returns as it was served. Today those types are `datasource` (built in) and `flow` (with the automation plugin). The carry-forward is objectstack-ai#8154's write-path inverse. `storedBodyForCarryForward` compared the served body against `repo.get` at the key the caller named. For a package-less draft save of a package-owned item, neither the inherited draft nor the package-bound active row sits at that key. So the comparison fell to the code layer, nothing was carried, and the stored credential was persisted away. **Measured on the real route** (the rest harness above, better-sqlite3), at head `07c229054`, before this round's fix: - The setup is a package-owned legacy datasource row whose `config.url` holds a userinfo credential. It was written straight to the store, because the save door now refuses such a URL, so a row like this only exists from before the gates. - `GET /meta/datasource/warehouse` serves it redacted. - That body is saved back with `PUT …?mode=draft` and no `?package=`, twice: 200, 200. - Each time, a system read of the stored draft row (bound to the package) shows `config.url` with no credential. - The env-local control kept the credential in both drafts. `meta-draft-head-package-inheritance.test.ts` went `1 failed | 8 passed (9)` on the pre-fix `dist/`. The failure is `expected [ false ] to deeply equal [ true ]`, on whether the first stored draft holds the credential. With the fix and `metadata-protocol` rebuilt (`grep -c 'target.packageId' dist/index.js` → 1), the file is green. Neither the PR body nor any comment carries a credential value: the pins assert a boolean. **The fix** is the same resolution (`4e63f1122`). `storedBodyForCarryForward` reads `repo.headAt` at the save's own state. With no draft yet, it reads `repo.headAt(active, packageId)` with the binding that call answered. The code-layer fallback and its `objectstack-ai#20552` reason are unchanged. An active save's carry-forward, and the authoring gate's `restoredCredentialPathsFor`, read the exact key as before, because `headAt` adds nothing for `active`. ## Pins `packages/rest/src/meta-draft-head-package-inheritance.test.ts`: the real `RestServer` routes, better-sqlite3 `:memory:`, the real `sys_metadata*` objects. Each pin reads the store back. - Triage's sequence: `the measured sequence: active save in a package, a package-less draft, the same draft again with no If-Match → 200, 200, 200` - Triage's read pin: `?state=draft after the package-less draft serves that draft's version (the save receipt's), not null` - Triage's controls: - `control: a stale If-Match is still refused 409, with the inherited draft's token as currentVersion`. It also covers `If-None-Match: *` refused while the inherited draft exists. - `control: an env-local item with no package is unchanged — unbound rows, unpinned saves accepted, the read serves the receipt's token` - Rider pins (`?package=all`): - `active: the read's version is the token the save compares against; If-None-Match: * is refused only because a row is there` - `active, no row at the env-local address: the read serves null, and If-None-Match: * creates it` - `draft of a package-owned item: ?state=draft&package=all serves the inherited draft and its token, and the save accepts it` - Carry-forward pins (patch round 1), describe `[objectstack-ai#22128] the credential carry-forward reads the row the draft save overwrites`: - `package-owned: the redacted read saved back as a package-less draft, twice, keeps the stored credential` - `control: env-local, the same round trip keeps the stored credential, as before` `packages/metadata-protocol/src/sys-metadata-repository.draft-package-inherit.test.ts`, new describe `SysMetadataRepository.headAt — the row put locks against (objectstack-ai#22128)`, 6 cases. They also pin the `packageId` binding `headAt` answers. - the inherited draft is the head, `get` at the named key is not, `put` refuses parent `null` and accepts the head's version; - the orphan draft is the head; - an explicit package never resolves another package's row; - an active address inherits nothing; - an org-scoped draft address resolves across the ADR-0005 reach; - a brand-new item drafted first has no head and the package-less binding. The objectstack-ai#11087 inheritance pins in that file are unchanged and green. ## Tests and gates Every reading below is at head `ea3c748f8` (patch round 1). `origin/main` was not merged again, because the branch still merges cleanly. No file of this diff overlaps the 11 commits `main` has gained since `959c209d5`. `@objectstack/metadata-protocol` was rebuilt before the real-route runs, because `@objectstack/rest`'s tests resolve it through `exports` to `dist/`. - **The real-route pins.** `meta-draft-head-package-inheritance.test.ts`, `meta-item-version-token-occ.test.ts` and `meta-publish-package-scope.test.ts` → `Test Files 3 passed (3) · Tests 42 passed (42)`, run at `4e63f1122`, whose code `ea3c748f8` carries unchanged. That covers this card's 9 pins plus objectstack-ai#22114's OCC pins and the `PUT`/publish `?package=` parity pins. - Before round 0's fix, the first 7 pins were `6 failed | 1 passed`, as quoted above. - Before round 1's fix, the carry-forward pin was `1 failed | 8 passed (9)`, as stated in its section. - **`@objectstack/metadata-protocol`.** - `vitest run --maxWorkers=2` → `Test Files 221 passed | 3 skipped (224) · Tests 28259 passed | 19 skipped`. That includes `protocol.metadata-redaction.test.ts` and `protocol.credential-channel.test.ts`. - `typecheck` → exit 0. - **`@objectstack/rest`.** - `vitest run --project local --maxWorkers=2` → `Test Files 262 passed (262) · Tests 4938 passed | 326 skipped`. - `typecheck` (`tsc --noEmit && check:test-typecheck`) → exit 0. - **Downstream consumer, `@objectstack/objectql`.** The 54 test files that name `saveMetaItem` / `getMetaItem` / `SysMetadataRepository` → `54 passed · 683 tests passed`. `protocol-meta.test.ts`'s `findOne` pins are among them. - **`@objectstack/spec` (descriptions only, no source change since round 0).** - Round 0's readings at `07c229054`: the 4 test files that name `SaveMetaItemRequestSchema` or the REST route table → `4 passed`, and `typecheck` → exit 0. - `check:generated` was re-run at this head and is green. - **The changeset gates.** - `check-changeset-no-major` was run with a `pull_request` event carrying this body. It answered `LEVEL AXIS: this PR declares clause-② yes (widening), and it grades a package whose packages/**/src/** it moves at minor or above`, exit 0. - `check-adr-0087-registration` → `1 non-breaking changeset(s)`, exit 0. - **Gates.** - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `ea3c748f8` derived 113 commands, the same set as round 0. Each was run with its exit code captured before any pipe. `--ran` gives: `113 derived famil(ies) accounted for — 111 run, 2 NOT-MEASURED`. - NOT MEASURED (exit 3, prerequisite not met, both need a full-repo build, CI runs them): - `pnpm --filter @objectstack/spec run check:skill-examples` (no `client-react` dist); - `pnpm check:dual-build-cjs-loads`. - The derivation flagged the tree as 11 commits behind `origin/main`. Two gate-source files differ there (`scripts/engine-double-contract.pinned.json`, which adds no entry for this diff's files, and a removed audit probe). - The 48-family artifact-roster block was run too, including `check:error-code-provenance`, `check:error-code-casing`, `check:engine-double-contract`, `check:spec-changes` and `check:meta-url-spelling`. 45 exit 0. The three PR-context gates are `check-partof-closing-keyword`, `check-closing-target-claim` and `check-single-claim-paths`. They were run with `PR_NUMBER=22185` and this body, and each exits 0. ## Acceptance notes Each item below is noted, not filed. - **The same `?package=all` reading on three more `/meta` doors (measured).** On the real stack, with a view whose active row is stored in package `com.probe.pkg`: - `GET /meta/view/case_grid/layers` → 200, overlay `live`. `GET /meta/view/case_grid/layers?package=all` → **404**. - `GET /meta/view` → `["case_grid"]`. `GET /meta/view?package=all` → `[]`. The layered read (`rest-server.ts`, `layeredPackageId = req.query?.package || undefined`), the list read and the book lookup each forward the literal `all`. objectui's editor (`ResourceEditPage.tsx` at objectui `9990f9e`) scopes its layered and draft reads with the router's raw `?package=`. These doors are outside this card's declared surface, so they are not changed here. The seat filed them as objectstack-ai#22188. `metaItemPackageBinding` is the fold they would share. - **Lowercase `metadata_conflict` elsewhere.** The spelling persists in `packages/client/src/index.ts` (four docblocks), `packages/cli/src/commands/meta/delete.ts:115` (a description string), `content/docs/concepts/metadata-lifecycle.mdx:147` and `docs/qa/platform-checklist/areas/studio-authoring.json:374,376`. The rider named only the route description. `sys_metadata_audit`'s `code: 'metadata_conflict'` is the audit vocabulary's data value and is correct as it stands. - `put`'s draft-inheritance lookup now runs inside its transaction, beside the existing-row read. It used to run just before the transaction. The predicates are unchanged. --- _Generated by [Claude Code](https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… cited decision in words instead of a tracker number (stage 29) (objectstack-ai#22376) Part of objectstack-ai#20749 Clause-②: no Stage 29 of this card: the next area of class (e), the test strings shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513. This stage takes the second name-ordered group of the test files directly in `packages/spec/src/`: 15 files, `root-entry-migrations-split.pin.test.ts` through `stack.test.ts`. 12 of them carried 57 tracker ids in test titles (59 ids), one of which sits in a declared describe label rather than a literal title. All 57 now either state what their record decided, in words (form D), or drop the number where the title already says it. Text only: no assertion, identifier, test count, code comment, file name or non-test file changes. No file directly in `src/` carries an id in a string any more (`type-alias-convention.pin.test.ts`, which sorts after the group, reads 0); the card stays open for the subdirectory files named under Acceptance notes. ## Census (re-taken first) The instrument is stage 28's `census28.cjs`, byte-identical (md5 `31d8488b5194b8d3048e3fcaec0efaed`, the value stage 28 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 stage 27's landing `f7b8a5932b`, 191 / 200 in 53 files (titles 162 / 168, other 29 / 32) at `aa09db58c9`, and 73 / 76 in 24 files at stage 28's landing `b7e01fbbd`. | reading | messages / ids | files | |:--|--:|--:| | the group, base `43fc50051` | 57 / 59 (titles 56 / 58, other 1 / 1) | 12 of 15 | | class (e) whole, base `43fc50051` | 73 / 76 (titles 61 / 64, other 12 / 12) | 24 | | class (e) whole, this head | 16 / 17 (titles 5 / 6, other 11 / 11) | 12 | | the group, this head | 0 / 0 | 0 of 15 | The group reads 57 / 59, stage 28's landing figure: no re-cut. The census at `3f80f1716` (the dispatch base) and at stage 28's landing read the same per file as at `43fc50051`. Per file, messages at base: root-entry-migrations-split.pin 1, stack-artifact-crossref 11 (12 ids), stack-artifact-packages 3, stack-cross-reference-envelope 2 (one title, one declared label), stack-dev-logins 4, stack-email-template-locale-floor 6, stack-inline-action-crossref 13, stack-json-stage-package-body 4, stack-refusal-envelopes 2, stack-requires 4 (5 ids), stack-top-level-strict 5, stack 2. `stack-conversions-record`, `stack-duplicate-action-key` and `stack-provenance` carry no id in any literal. Controls: - Pathspec: the 15 named paths hit the control word `describe(` in 15 of 15 files, and a nonsense word in none; the census scanned 15 of 15. - Lit outside the group, at base and head: `ai/build-progress.test.ts` 2 / 2 and `ui/dashboard.test.ts` 2 / 2. - Lit at base and dark at head, inside the group: `stack-artifact-crossref` 11 / 12, `stack-inline-action-crossref` 13 / 13, `stack-requires` 4 / 5 and `stack-cross-reference-envelope` 2 / 2 each read 0. - Dark at both ends: `stack-conversions-record` has no id anywhere; `stack-duplicate-action-key` and `stack-provenance` carry an id in one comment line each and read 0. The 15 group files read 0 at head while 55 of their comment lines still carry ids (112 id-bearing lines at base, comments included, 14 files). - Planted, in a scratch tree: an id in a describe title, a plus-chain title, an expect message, an it.each row name, a template title and a declared string each read once (6 / 6); a comment, a six-digit number, an HTML entity and a markdown heading read 0. ## What changed 57 literals, one line each, in 12 files: +57 / -57. Every file keeps its line count. - 6 state the decision in words: "a modal target names a page, only" for the inline and the embedded modal-target refusals (stack-inline-action-crossref 133 and 330); "it was never built by a stack producer" for the hand-built stack object composeStacks refuses before either artifact-scoped rule runs (stack-artifact-crossref 380); "compatibility with the multi-package artifact" (stack-artifact-crossref 270); "unchanged from before the dangling-`objectName` refusal" (stack-inline-action-crossref 399); and the declared label "hooks[].object (the hook-ownership rule)", the rule ADR-0130 states as "the split must follow hook ownership" (stack-cross-reference-envelope 91). - 4 get their subject back where the number was the only subject: "the no-floor report: warn-once bookkeeping" and "the floor guard's declared scope boundary" (stack-email-template-locale-floor 159 and 179), "the strict top-level door: the accept side does not move" and "defineStack surfaces the unknown-top-level-key refusal" (stack-top-level-strict 173 and 225). - 47 drop a number the title already explains, with its connector: a `#N — ` or `#N ` prefix, a `[#N] ` prefix, or a trailing `(#N)`. That includes the twin title `stack-artifact-packages.test.ts:355`, which read `does not warn about an undeclared composition rule (objectstack-ai#5005 rule 3)` and now reads `does not warn about an undeclared composition rule`, the same text stage 28 gave its copy at `compose-stacks-manifest-preserve.test.ts:308`. ## Cited records 26 distinct ids. 25 answer 200 and were read: the body and the comment thread as the API serves it, with the rulings and landings that the rewritten titles state read in full. objectstack-ai#10485 answers 404; its decision was read from its landing commit `35ad101bc` (the `themes` carrier retired under ADR-0049 enforce-or-remove; `app.branding` is the one colour surface) and from the unknown-key strictness ledger, which records the maintainer's 2026-08-21 disposition as retiring the authorable surface, the phrase the title keeps verbatim. The comments counter of four records differs from what the comments endpoint serves (objectstack-ai#14122 serves 21 of 22, objectstack-ai#17614 5 of 6, objectstack-ai#16449 0 of 5, objectstack-ai#18056 5 against a counter of 4); everything served was read, and objectstack-ai#16449's decision is in its body. What each record decided, as the titles now say it: - objectstack-ai#6739, ruling A (2026-08-09): a `type: 'modal'` target names a page, only; the object fallback is consumer leniency and enters retirement. - objectstack-ai#20367, ruling B: one authoring shape; `defineStack` and `composeStacks` stamp a provenance mark and `composeStacks` refuses an unmarked input with `STACK_PROVENANCE_MISSING` / 422. - objectstack-ai#7456, Option A: the embedded `objectName` gets the registered rule's existence check, and only a dangling name newly refuses. - objectstack-ai#19926, ruling A: `packages: null` is malformed, not absent. - objectstack-ai#19784, ruling C: a non-array concat key is refused with the envelope. - objectstack-ai#5005 (2026-08-04): same value passes, a conflict throws a prescriptive error, an undeclared top-level key warns. - objectstack-ai#8687, Shape B: the top-level stack door is strict, and the lint yields to it. - objectstack-ai#14122 / objectstack-ai#18202: one artifact carries N packages; the artifact-scoped permission and seed references resolve against the artifact, a stack that does not opt in is untouched. - objectstack-ai#17518, ruling A prime: the artifact-stage and record-stage package bodies are declared beside the assembled one. - objectstack-ai#6889 and objectstack-ai#7397: inline page-element actions and object-embedded actions get the registered action rule's target cross-references. - objectstack-ai#17063: the `type: 'page'` list-view mount is retired. objectstack-ai#17556: an app declares its own `devHint` / `devLogins`. objectstack-ai#17614 and objectstack-ai#18056: the no-`en-US`-floor report and its declared scope. objectstack-ai#14552 and objectstack-ai#15963: every `defineStack` refusal carries an ADR-0112 envelope; objectstack-ai#16449 registers each shipped code in the ledger. objectstack-ai#3265 and objectstack-ai#3308: `requires` is validated at the producer and the camelCase aliases are removed. objectstack-ai#14153 and objectstack-ai#20332: trigger capability validation and its `triggers` without `automation` pair. objectstack-ai#20646: the root entry stops carrying the migration registries. objectstack-ai#2611: named import mappings are wired into the import path. ## Readers 173 needles (each old literal, a 24-character window around each id, and the text left and right of each id) were searched with `git grep -F` over the tracked tree at the base outside the 15 files, with a lit control (`composeStacks` in `stack.zod.ts`, hit) and a dark control (a nonsense string, no hit). One needle hit, one line: a `//` comment in `packages/qa/dogfood/test/fixtures/override-composite-fixture.ts` that quotes the tail of one title with its id. It is a comment, not a reader. The only reader of the declared label is the file's own `describe(row.label, ...)`. No group file reads a test name (`currentTestName`, `expect.getState`, snapshots: 0). The 15 file names appear elsewhere only in prose and comment lines, in `packages/spec/test-typecheck-debt.json` (keyed by file and error text, not by title) and in two `dispatch-gates.mjs` / `check-changeset-no-major.mjs` fixtures that name a path or a changeset file. Every old literal occurs exactly once across the 15 files (its own site). ## Text-only proof - `textonly28.cjs` (stage 28's, md5 `957eff6b3837d762b8e03d070155930a`), per file, base text against head text: both texts with every string token masked are byte-equal (code and comments), the string-token counts are equal, and every changed token sits in a describe / it / test title position or on a declared line, carried an id at base and carries no `#` plus digits at head. 15 / 15 files SAME; 57 changed tokens, 56 titles and 1 declared label (run with `--declared 91`); per-file counts exactly as predicted in writing at 2026-10-08T22:57Z, before the edit. The 3 untouched files read SAME with 0 changed. - 17 / 17 controls on scratch copies of the head files behave as predicted: an identifier rename, a numeric literal, a comment edit, a non-title string given an id, a rewritten title given a new id, a title that was id-free at base edited, one rewritten title reverted to base (SAME, 0 changed), an it.each title given an id, an expect message changed, a title re-split into a plus chain, a template title given an id, a test added, an id appended to a rewritten title in a many-title file, an untouched file (SAME, 0 changed), the declared label run without `--declared` (refused), the declared line plus another changed string (refused) and the declared line alone (SAME, 2 changed). - `git diff -U0`: 57 plus and 57 minus lines, each the planned line; 0 added lines carry `#` plus digits; control-byte scan of the 12 files and of this body: 0 hits. ## Tests - The 15 files with `--project local --project repo` and the JSON reporter, at the base (same worktree, before the edit) and at the head `65b42b7d1`: 405 / 405 passed in 15 files, 108 suites each side; per-file test count and status sequence identical in 15 / 15; 229 full names changed, 0 mismatches against the plan (each head name equals the base name with the planned replacement applied); 0 duplicate full names either side; names with `#` plus digits 229 at base, 0 at head. - spec `vitest run --project local`: 626 files passed, 18740 tests passed | 1 todo, exit 0. - spec `typecheck`: `VERDICT command-exit 0`; `check:test-typecheck` 52 files / 246 errors / 135 pinned signatures held, unchanged; `tsc --listFilesOnly -p tsconfig.test.json` holds 15 of 15 group files. - turbo build of all packages before the gates: `Tasks: 71 successful, 71 total`. Heavy runs went through `scripts/pm/os-verify-lock.sh` (slot `dev-20749-s29`). - No reverse verification or ablation applies: no assertion, gate or reader moved. ## Gates All at `65b42b7d1`. The dispatch list (77 commands, derived at `3f80f1716`) equals the list re-derived from the real change set at this head, 12 paths against merge base `43fc50051` (114 changed lines): nothing added, nothing dropped. All 77 exit 0 and `dispatch-gates --ran` reads "77 derived, 77 run, 0 NOT-MEASURED, 0 UNRUN". Also run, exit 0: the five roster families that share a directory with the diff (`check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`) and `check:generated` ("All 15 generated artifacts are up to date", measured against the dist the full build wrote at this head). ESLint, narrowed and proven: 15 files linted with `--no-inline-config`, 0 errors, 0 warnings; the population read from ESLint's own config is 15 configured, 0 ignored; no file has `parserOptions.project` or `projectService`, so type-aware linting is not enabled and this diff cannot move the verdict of a file it did not touch. ## Packaging and landing - No changeset: `skip-changeset`. `npm pack --dry-run --json --ignore-scripts` in `packages/spec` lists 2069 files, none of the 12 changed files and no `*.test.ts`; controls `src/stack.zod.ts`, `dist/index.mjs` and `package.json` are present; an old and a new title phrase read 0 files in `dist/`, the control `Unrecognized key` reads 42. - Not governed: `check-governed-merges --test` on the 12 paths reads 0 of 12; 114 changed lines. - `git merge-tree` onto `origin/main` `6a53564b9`: clean. None of the 17 open PRs touches any of the 15 files (file lists read; 6 of them touch `packages/spec/src`, so the scan sees spec files). ## Acceptance notes - Class (e) is not finished: 16 messages / 17 ids remain in 12 files, all in subdirectories. By directory: `ui/` 9 / 9 in 7 files, `automation/` 2 / 3 in 1, `ai/` 2 / 2 in 1, and `api/`, `contracts/` and `kernel/` 1 / 1 in 1 each. Their plan is the cutting seat's: stage 27's landing sets the needles stage apart (one stage, with an at-tier review) and keeps `ui/`'s four colour literals as decided, and this stage measured neither. - Regrowth for stage 30's plan: five subdirectory test files carry 6 messages / 7 ids that were added after stage 27's landing. The files are older; the ids entered their strings in three commits. `api/meta-item-response-shapes.test.ts` 1 / 1 (a title, from objectstack-ai#22126); `automation/builtin-node-config.test.ts` 2 / 3 (two titles, from objectstack-ai#22259, which re-added ids to a title stage 10 had rewritten); `kernel/manifest.test.ts` 1 / 1, `ui/action-description.test.ts` 1 / 1 (titles) and `ui/view-submit-redirect-url.test.ts` 1 / 1 (an `other` string, to be needle-checked there), all three from objectstack-ai#22125. Nothing regrew since stage 28's landing: the per-file census at `b7e01fbbd` equals the one at `43fc50051`. Test files are outside `check:doc-authoring`'s ledgered leg, so nothing refused these on arrival. Not edited here. - Code comments in the 15 files still carry live ids (55 lines). Comments are not this card's share. Carrier: none. - `origin/main` moved from `43fc50051` to `6a53564b9` while this ran; one commit touches `packages/spec` (`api/error-code-ledger.zod.ts`, none of the 15 files), so nothing was merged into the branch. --- _Generated by [Claude Code](https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn)_ 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>
Fixes #22114
The client half, objectstack-ai/objectui#11773 (PR objectstack-ai/objectui#11826), is unlocked by the published release that carries this change, not by this merge.
Clause-②: yes (narrowing)
yesis for the widened read member, request header and conflict field. The(narrowing)arm is for two request shapes onPUT /meta/:type/:namethat were answered200and are now refused400 VALIDATION_ERROR: anIf-None-Matchvalue other than*, andIf-None-MatchbesideIf-Match. The changeset carries the BREAKING line with the remedy (send*alone, never besideIf-Match), and the ADR-0087 dispositionnot-required (no-migration-prescription): no key, export, response field or stored shape moves, no first-party sender putsIf-None-Matchon aPUT, and which precondition the client meant is a choice no conversion entry can derive. The level staysminor, because.changeset/pre.jsonis absent onorigin/main(51290bca2).What changes
ADR-0008 says clients pass
If-Matchwith the version they read, and a client can send only a token it was served. Before this PR, only a save receipt served one. So the first save after a load could not be pinned, and two editors who each loaded and saved once overwrote each other. This PR closes the three gaps the card names, at the/metadoor. Spec end and runtime end land together.GET /meta/:type/:namenow carries aversionmember, declared onGetMetaItemResponseSchema. It is the keyed token of the stored row that a save to this item compares against, at the read's scope (the caller's organization partition and?package=) and lifecycle (?state=draftreads the draft row, the plain read the active row). It comes from the same producer as the save receipt'sversionand covers the same row, so a read after a save serves the receipt's token byte for byte.nullmeans no stored row is there, so the next save is a create.If-None-Match: *onPUT /meta/:type/:namesaves only where no row of the target lifecycle exists. Once a row exists it is refused409 METADATA_CONFLICT. This is theparentVersion: nullpin thatSaveMetaItemRequestSchemaalready declared, now reachable over HTTP. A save with neither header is last-writer-wins, as before./metaitem write doors now carriescurrentVersionbeside today's unchanged sentence: the token the sentence names, ornullwhen no row of the target lifecycle exists. The body shape is declared asMetadataConflictErrorSchema, and the SDK exposes the field aserr.details.currentVersionwith no client change.Where each half lands
@objectstack/specGetMetaItemResponseSchema.version(string, null or absent);MetadataConflictErrorSchemaplus its two type aliases; theparentVersiondescribe now namesIf-None-Match: *; the receipts'versiondescribes are corrected (below). Generated artifacts regenerated (authorable surface, json-schema manifest, api-surface, export origins, declaration map, reference docs, strictness-ledger counts).@objectstack/metadata-protocolstoredHeadAt, is shared by the save door's token check and the read'sversion(readVersionToken), so no second token derivation exists.getMetaItemservesversionon the draft branch and on the active branch.metadataConflictRefusal, the single builder all four item doors (save, publish, rollback, reset) answer through, now also setscurrentVersion.@objectstack/restPUT /meta/:type/:namereads its pin throughmetaSavePreconditionPin:If-Matchas before,If-None-Match: *as the expect-no-row pin. A comment on the cached arm records why its ETag is not the token.@objectstack/typesMETADATA_CONFLICTarm instructuredCodeAnswerserializescurrentVersioninto the flat ADR-0112 body. This is outside the claim's declared file surface, and the reason is the repo's own rule:error-response.tssays a new bespoke code arm "belongs instructuredCodeAnswer, where both doors read it", and that function moved to@objectstack/types. The arm keys on the code and on a statedcurrentVersion, so aMETADATA_CONFLICTthat states none keeps today's body byte for byte.No objectui change. No change to
packages/clienteither: the read type is the spec's ownGetMetaItemResponse, and the flat conflict body already reacheserr.details.Decisions taken, with the measurement behind each
versionnames: the address of the save, not the row whose content was served. A read falls back from the caller's organization to the environment-wide row (ADR-0005), and from a package's own row to the package-less one (ADR-0048). A save does not fall back; it writes its own partition. A token for a row the save would not overwrite would be refused by that save every time. So such a read servesnull, which is the honest create pin. This is pinned at the protocol: an env-wide row read under an organization servesnull, andparentVersion: nullis then honoured once and refused once.versionis absent:lock, and the schema's existing contract sends OCC readers to the uncached path.?preview=draft, a render path that mixes two lifecycles.ETagstays the cache validator and is not the token. That validator issimpleHashover the scope (organization, locale) plus the served bytes, folded with the caller's field-visibility fingerprint (ADR-0106 D3) and the public-form intake fingerprint. The version token moves with none of locale, visibility or served-but-unstored bytes. An item served from code has a validator but no token.getMetaItemCached's own comment forbids hashing "a version marker" instead of the content, withget-meta-item-cached-etag-scope.test.ts§3 as its pin. So triage's "anETagequal to it where the door sets one" cannot hold on the one branch that sets one without breaking that validator. The body member satisfies the card's "a body member, anETag, or both". No new ETag is minted on the uncached arms: a strong validator shared across locale and mask variants would be false, and the runtime dispatcher serves the same envelope with no such header. The pin ismeta-item-version-token-occ.test.ts: the cached arm has noversionand its ETag is not token-shaped.If-None-Matchtakes*alone, and never besideIf-Match.rest-server.tsreadif-none-matchonly on the cachedGET. The adapters andplugin-hono-serverread it nowhere. The SDK sends it only fromgetCached(aGET). objectui'suseETagCachehas zero in-repo callers.*value is refused400 VALIDATION_ERROR, because a write-unless-the-head-is-one-of-these condition is one this door does not evaluate. Silently dropping a precondition the caller asked for would write that caller unguarded.If-MatchplusIf-None-Match: *is refused400. Under RFC 9110 §13.2.2 both are evaluated, so the pair can never hold, and a409would send the caller round a re-read that serves a token it would pair with*again.versiondescribes are corrected (seat round 1).SaveMetaItemResponseSchema.version,PublishMetaItemResponseSchema.versionand the package publish door'spublished[].version(PublishPackageDraftsResponseSchema, the samereceiptVersiontoken, so the same falsehood in the same file) said "Content hash … currently emitted assha256:" and "409metadata_conflict". They now say the token is the keyedhmac-sha256:digest of the stored content hash, never the hash itself, and that the conflict is409 METADATA_CONFLICT. Only description text moves. The reference docs are regenerated; no spec test reads these describes, so none is pinned.currentVersionis relayed asnull, not dropped.nullis the answer "no row". The arm therefore reads presence, not truthiness. The record-levelCONCURRENT_UPDATEarm reads truthiness, and its producer never statesnull.Tests
All pins are at the HTTP door on the real stack (better-sqlite3
:memory:, the realsys_metadata*objects, the realObjectStackProtocolImplementation, the real routes). Every refusal pin asserts the ADR-0112codeand the HTTP status, and reads the store to show the refused write did not land.packages/rest/src/meta-item-version-token-occ.test.ts:?state=draftserves the draft receipt'sversion, which survivesGetMetaItemResponseSchema.parse.currentVersionequals the winner's receipt and the next read'sversion, the sentence is unchanged, andMetadataConflictErrorSchemaparses the body. The loser re-pins fromcurrentVersionand is accepted.If-None-Match: *: a first draft gets 200. Once a draft exists it gets 409, withcurrentVersionequal to the draft's token.If-Matchgets 409 withcurrentVersion: null, andIf-None-Match: *writes the next draft.If-None-Match: *pins the create. An item registered in code servesversion: null.currentVersiontoo.version.If-None-MatchbesideIf-Match, or set to an entity-tag, a weak wildcard, a list, or empty: 400VALIDATION_ERROR, nothing written. A wildcard with surrounding whitespace is still the wildcard.Other suites:
packages/metadata-protocol/src/protocol.served-content-hash.test.ts: read token equals receipt token, keyed, never the stored hash;nullat an empty save address; noversiononpreviewDrafts; the refusal statescurrentVersion.packages/rest/src/error-response-structured-arm-door-parity.test.ts: two parity cases (a token, andnull) plus a control with nocurrentVersion, which keeps the passthrough body.packages/rest/src/error-response-sandbox-arm-message.test.ts: the arm joins the derived census.packages/spec/src/api/meta-item-response-shapes.test.ts: both schemas at runtime and at the type level.Readings (repository
objectstack-ai/objectstack)Round 1, on the merged head.
origin/main51290bca2was merged throughbash scripts/pm/os-regen-merge.shas merge commit0bb0202bc, with parents8f04870ef(this branch) and51290bca2(main). It was a clean merge: main touched none of this diff's files, and step 2 kept the branch's bytes of the 8 routed artifacts main did not move. After it came the describe fix (3950502c6) and the docs regeneration (54ae811d4).gen:schemadid not run in MERGE state.54ae811d4.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 113 families against merge base51290bca2. All 113 ran, each exit code captured before any pipe.PREREQUISITE NOT MET(nothing measured):check:skill-examples(noclient/client-reactdist) andcheck:dual-build-cjs-loads(37 packages with no dist). The recreated worktree had built only this diff's closure. Both read exit 0 once the battery's own full build had run.--ranreconciliation:113 derived, 113 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero.check:adr-0087-registrationreads1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … [BREAKING+clause-②-narrowing] not-required (no-migration-prescription).check:changeset-no-majorreads nomajorbump.check:generatedreports all 15 artifacts up to date.54ae811d4, under the verify lock,--maxWorkers=2, after rebuilding the closure on the merged tree:@objectstack/specmeta-item-response-shapes.test.ts+protocol.test.ts: 197 tests passed.@objectstack/metadata-protocolprotocol.served-content-hash.test.ts: 27 passed.@objectstack/restmeta-item-version-token-occ.test.ts: 15 passed;--project repo: 5 files, 191 passed and 1 skipped.@objectstack/specand@objectstack/resttypecheck: exit 0..changeset/pre.jsonis absent atorigin/main51290bca2, read at this push. The contents API answers 404, against a 200 for.changeset/config.jsonat the same ref as the control.Round 0, on the pre-merge head (base
54ace18c6).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat head8f04870ef: 113 families, all run there, each exit code captured before any pipe, 113 of 113 exit 0. The--ranreconciliation reads113 derived, 113 run, 0 NOT-MEASURED, 0 UNRUN("a DERIVED zero — all 113 recorded an exit code"). The list includescheck:dispatcher-error-vocabulary,check:route-envelope,check:durability-log-level,check:engine-double-contract,check:query-options-erasure,check:spec-parsed-alias,check:adr-0087-registration,check:changeset-no-major,check:nul-bytes,check:type-check-coverage/-debt, and the speccheck:*set includingcheck:generated(all 15 artifacts up to date).--maxWorkers=2under the shared verify lock, with each package's built dependency closure.12a0d8525:@objectstack/types: test and typecheck exit 0.@objectstack/metadata-protocol: typecheck exit 0; test 221 files passed and 3 skipped, 28243 tests passed.@objectstack/rest: typecheck exit 0, includingcheck:test-typecheck(0 debt);--project local261 files and 4929 tests passed.e5e7817b8:@objectstack/spec: typecheck exit 0; test 622 files and 18583 tests passed.@objectstack/client: typecheck exit 0.@objectstack/runtime: the 7/metaitem-read and parity suites, 812 tests passed. The dispatcher's item read serves the same protocol envelope, so it carriesversiontoo.8f04870ef:@objectstack/rest--project repo5 files passed. Its first run reddened the sandbox-arm census onMETADATA_CONFLICT, which is the census working as designed; the arm now has its row.pnpm lintis CI's).eslint --no-inline-config --format jsonover the 9 changed.tsfiles: 9 files, 0 errors, 0 warnings, with no ignore-pattern warning, so all 9 are inside the config's population.eslint --print-configshows noparserOptions.projectorprojectService, andeslint.config.mjsstates it never enables type-aware linting. So this diff cannot move a verdict on an untouched file.scripts/ablation-replace.mjs(anchor hit 1 to 0, blob changed), and the dist legs throughscripts/ablation-dist-preflight.mjs(marker present in 2 built files, then absent after the restore and rebuild). The tree readsgit diff HEADempty afterwards.If-None-Match: *(source):meta-item-version-token-occ.test.ts2 failed of 15.version(rebuiltmetadata-protocoldist): the REST door test 5 failed of 15;protocol.served-content-hash.test.ts1 failed of 27.@objectstack/typesarm never firing (rebuilttypesdist): 9 failed of 73 across the door test and the parity test..changeset/pre.json: absent atorigin/main15ec50e52, read before opening this PR. The changeset is a plainminorfor@objectstack/spec,@objectstack/metadata-protocol,@objectstack/restand@objectstack/types.origin/main(15ec50e52). Neither commit touches a file this diff touches, generated shards included. It was not merged; the queue rebuilds onmain.Acceptance notes
findOnefor the save address's head. This includes the cached arm's internalgetMetaItemcall, whose envelope discards it. A read that served no stored row makes no extra call.GET /meta/:type/:name/layersdoes not serveversion. It is the diagnostic view, and Studio's designers edit from?state=draft.PUT /metareads neitherIf-MatchnorIf-None-Match, and no host in this repository routes/metawrites to it (measured).handleMetadataRequest(deps, path, _context, method, body, query)(packages/runtime/src/domains/meta.ts:874) takes no header argument.PUTbranch (:1274) callsprotocol.saveMetaItem({ type, name, item, organizationId, writeFace: 'meta-dispatch', ...packageId })(:1430–:1434), with noparentVersionand nomode.if-match,if-none-matchorparentVersion.objectstack serveandos dev, mountcreateRestApiPlugin(packages/cli/src/commands/serve.ts:4508–:4511) and thencreateDispatcherPlugin(:4519–:4535).${prefix}/metaroute (0 matches). Its own comment says "the standalone /os devserver mounts ONLY the explicit routes here" (packages/runtime/src/dispatcher-plugin.ts:1283–:1285). So/metawrites on those hosts are the REST door's.${prefix}/*catch-all that does reachdomains/meta.tsis@objectstack/hono'screateHonoApp(packages/adapters/hono/src/index.ts:725,:739). No app, example or package in this repository calls it.dispatcher-plugin.ts:1321). The cloud runtime is not in this session, so whether a cloud host'sPUT /metareaches the catch-all beforeRestServeris NOT MEASURED. This is for the seat to file separately if it is reached.GET /meta/:type/:name?package=allhands the protocol the literalall, while the save door drops it. So such a read servesversion: nullfor a package-less row the save would write. This is a read-only observation.PUT /meta/view/NAME?mode=draft, sent with noIf-Match, over an item whose active row is package-bound, answers409 METADATA_CONFLICT. The save door's head read asks for the package-unbound draft, while the repository bound the draft to the active row's package. This PR's read token follows the same head read (storedHeadAt). So on that path, until finding(metadata-protocol): a second package-less draft save of a package-owned item is refused 409 METADATA_CONFLICT with no If-Match — the save door's head read and the repository's draft package inheritance read different rows #22128 lands,?state=draftservesversion: nullwhile a draft exists. The describe stays as written: the change finding(metadata-protocol): a second package-less draft save of a package-owned item is refused 409 METADATA_CONFLICT with no If-Match — the save door's head read and the repository's draft package inheritance read different rows #22128 carries makes it true there too, at the one shared head read.Generated by Claude Code