Skip to content

fix(runtime)!: refuse the stored-metadata family evaluate shapes and serve write returns at the reader-context seams - #21539

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21454-evaluate-refusals
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21454-evaluate-refusals

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Part of #21454

Clause-②: yes (narrowing)

The follow-on to PR #21513, on the same reader-context seam. #21513 served the stored-metadata-body family's reads (body projected, content hash keyed) at the in-process reader contexts. This PR closes the two positions triage routed to the card's next claim, through the generic data door's own code — no copy:

  • Evaluate-shape refusals (triage 5964426684 item 2, with 5963299937). A filter, sort, grouping or search that would EVALUATE the stored body or a content-hash column of the family, arriving through a reader context (ctx.api.object(...) for action and hook bodies, a handler's ctx.api, and ctx.engine.find), is now refused with the door's own INVALID_FIELD / 400 before the query runs. A count carrying such a predicate — the oracle verb — is refused too (it serves no row). A default search is NARROWED to the door's served field set (the body and content-hash columns removed, judged field by field by the door's own search predicate) rather than refused, so a reader context may still search a family table by its scalar columns exactly as the door serves it; a search that narrows to nothing is refused.
  • Serve what a write verb returns (triage 5964426684 item 4). A write whose return carries the family's body or content hash is served projected and keyed, the same way a read is — a returned row is a serve.

Both are executed through the four refusal predicates the door uses, now exported from @objectstack/metadata-protocol (storedMetadataBodyGroupingRefusal, storedMetadataBodyPredicateRefusal, storedMetadataHashEvaluateRefusal, storedMetadataSearchRefusal). The search narrowing consumes the spec's own resolveSearchFieldResolution and the door's search predicate as the authority on which columns a search may never scan. Field collection for the filter/sort refusals uses @objectstack/plugin-security's collectConditionFields (the door's sibling collector; the door's own collectFilterFieldKeys is internal to protocol.ts, PR #21473's file, and unreachable here). That collector gates on a dotted head and reads a cross-field reference, so it refuses MORE than the door — a dotted or cross-field reference to a family column the door's collector would miss — strictly in the safe direction, never a legitimate scalar-column query.

The engine action verb (ScopedRepo.execute), measured (triage 5964836549 item 1)

The reach reading: execute is UNREACHABLE from a served body. The sandbox VM bridge exposes only find / findOne / count / aggregate and the writes to a body — no execute, no sudo, no withRunAs — so a served body can never hand a raw scoped context to a nested action. The seam therefore leaves execute untouched and records the reading (pinned: a body's typeof ctx.api.object(...).execute is undefined).

Where #21520's write-verb refusal attaches

The maintainer approved #21520 option A (an app-authored body may not write the family's tables). That refusal is a SEPARATE card and is NOT implemented here. It attaches on the same write verbs this seam wraps, in serveRepository's write branch, as a throw BEFORE the write runs — it needs no reshaping of this seam. A code comment names that point. Measured on main today (pre-#21520): an elevated body's family-table write is not refused and returns the stored row, so the write-return serve in this PR carries real content; it also covers the host-handler write path, which #21520's body refusal does not reach.

Reverse verification (ablation)

Each new behavior ablated on disk (scripts/ablation-replace.mjs, mutation proven by blob hash + anchor count, restored to the HEAD blob), src-resolved (the pins import the seam by relative path):

  • disabling the evaluate refusal turned the 6 refusal / narrowing pins red; the serve, write-return, scalar and unreachability pins stayed green;
  • disabling the write-return serve turned exactly the write-return pin red; all else green.

Tests and gates

  • New pins: stored-metadata-reader-seam.test.ts (17) against a scoped-API double, and stored-metadata-reader-contexts.pin.test.ts (26) end to end through a booted kernel, for administrator and member alike.
  • @objectstack/runtime suite 4413 passed / 19 skipped; @objectstack/metadata-protocol suite 3151 passed / 19 skipped; both typecheck clean.
  • All 70 dispatch-derived gate families green (check:engine-double-contract pinned-ledger entry added through the gate's own --write).
  • Lint narrowing: the 4 changed TS files lint 0 errors / 0 warnings; the .md and .json have no matching eslint config; eslint.config.mjs enables no type-aware linting and no cross-file import rules, so this diff cannot move the verdict on any untouched file (the farm-wide pnpm lint is CI's).

Generated by Claude Code

claude added 6 commits October 3, 2026 03:39
…metadata reader seam

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…narrowing, execute reach

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…ispatch predicates; record pinned coverage

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/runtime, touching 14 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/metadata-protocol/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/data-api.mdx (via deleteMany (literal, a string literal in WRITE_RETURN_VERBS), searchFields (literal, a string literal in narrowFamilySearch), updateMany (literal, a string literal in WRITE_RETURN_VERBS))
  • content/docs/api/error-catalog.mdx (via searchFields (literal, a string literal in narrowFamilySearch))
  • content/docs/api/error-handling-server.mdx (via updateById (literal, a string literal in WRITE_RETURN_VERBS))
  • content/docs/automation/hook-bodies.mdx (via deleteMany (literal, a string literal in WRITE_RETURN_VERBS), updateMany (literal, a string literal in WRITE_RETURN_VERBS))
  • content/docs/automation/webhooks.mdx (via deleteMany (literal, a string literal in WRITE_RETURN_VERBS), updateMany (literal, a string literal in WRITE_RETURN_VERBS))
  • content/docs/data-modeling/queries.mdx (via searchFields (literal, a string literal in narrowFamilySearch))
  • content/docs/kernel/contracts/data-engine.mdx (via searchFields (literal, a string literal in narrowFamilySearch))
  • content/docs/protocol/kernel/error-handling.mdx (via deleteMany (literal, a string literal in WRITE_RETURN_VERBS), searchFields (literal, a string literal in narrowFamilySearch), updateMany (literal, a string literal in WRITE_RETURN_VERBS))
  • content/docs/protocol/kernel/http-protocol.mdx (via deleteMany (literal, a string literal in WRITE_RETURN_VERBS), updateMany (literal, a string literal in WRITE_RETURN_VERBS))
  • content/docs/protocol/knowledge.mdx (via deleteMany (literal, a string literal in WRITE_RETURN_VERBS), updateMany (literal, a string literal in WRITE_RETURN_VERBS))
  • content/docs/protocol/objectql/query-syntax.mdx (via searchFields (literal, a string literal in narrowFamilySearch))

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

  • content/docs/releases/implementation-status.mdx (via deleteMany (literal, a string literal in WRITE_RETURN_VERBS), updateMany (literal, a string literal in WRITE_RETURN_VERBS))
  • content/docs/releases/v16.mdx (via updateMany (literal, a string literal in WRITE_RETURN_VERBS))
  • content/docs/releases/v17/17-0.mdx (via deleteMany (literal, a string literal in WRITE_RETURN_VERBS), searchFields (literal, a string literal in narrowFamilySearch), updateMany (literal, a string literal in WRITE_RETURN_VERBS))
  • content/docs/releases/v17/17-4.mdx (via updateMany (literal, a string literal in WRITE_RETURN_VERBS))

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

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

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

Which tree this was computed on

This run read content/docs from a5cb5c252ef93a5287b6407d006acbe10344a955 — the merge of head 19b2cb6e50e576813431b22928aa1057ce4de0ef into base fd5a1cd5973983bf8b1ad69a10148a85c74c9137, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a5cb5c252ef93a5287b6407d006acbe10344a955 && git checkout a5cb5c252ef93a5287b6407d006acbe10344a955
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fd5a1cd5973983bf8b1ad69a10148a85c74c9137 19b2cb6e50e576813431b22928aa1057ce4de0ef && git checkout -B drift-repro fd5a1cd5973983bf8b1ad69a10148a85c74c9137 && git merge --no-ff 19b2cb6e50e576813431b22928aa1057ce4de0ef

node scripts/docs-audit/affected-docs.mjs --json fd5a1cd5973983bf8b1ad69a10148a85c74c9137

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1a6fbf7d93397b6361931631e1cc9a6ed3fadb2c
Local-runs: none

Inputs: card #21454 (body and all 19 comments), PR #21539 (body, the 6-file list, the net diff against main from the merge-base ad7c351898), and the check-runs on the head. Read-only throughout: git object reads and GET calls, nothing built, run or re-run. The head's latest run per check name is success or skipped: Check Changeset, Lint and Repo Gates, the five Type Check jobs, Test Core 1 to 6 plus the rollup, Dogfood Regression Gate 1 to 3 plus the rollup, Dogfood Verify CLI, Temporal Conformance, Build Core, Governed Surface Queue Guard, the four claim and serial guards, Check Documentation Links and Flag docs affected are green; Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in) and the re-run Auto Label and Check PR Size jobs are skipped. No failure. ⛔ Classes, doors and roles only below.

① Derived judgments

  1. Public-surface widening, @objectstack/metadata-protocol (index.ts, +14): the four evaluate-refusal predicates the generic data door already runs are re-exported, additive, absent on main. Consumed by the seam, never copied. RIGHT.
  2. Evaluate refusals at the three reader contexts. find, findOne and aggregate pass through serveStoredMetadataRead, which the handler's engine facade find also calls, so a body's object API, a handler's scoped API and the engine handle are all judged; count is judged in its own branch. The predicates are the door's own, in the door's order (search, grouping, body filter and sort, hash filter, sort and grouping). RIGHT.
  3. What the collector reads: where plus the filter alias (the engine folds filter into where on these verbs, so reading both is right), each aggregation's own filter, and orderBy only. orderBy alone is right: the engine refuses the wire-only sort key on find through its option-key gate, so no order can reach it under another spelling. The array-form filter is lowered through the spec's own parser before collection, matching the engine's array door. RIGHT.
  4. Collector choice: collectConditionFields from @objectstack/plugin-security, a dependency @objectstack/runtime already declares; plugin-security declares no dependency on runtime, so no cycle. It gates on a dotted head and reads a cross-field comparand, so it can only refuse more than the door, never a scalar-column query. The door's own collector also catches a dotted head (its comment says so), so the extra reach is the comparand alone. Accepted by triage at 5965718076. RIGHT.
  5. Default-search narrowing is live in composition, not only on the unit double: both seam call sites hand the seam the ObjectQL engine, whose getObject answers the object schema with a record field map, the shape the spec's search resolver reads. The family's body column is a textarea and its hash columns are text, all inside the spec's auto-default searchable set, so the narrowing is load-bearing. Each dropped column is judged by the door's own search predicate; the step order and the empty-set rule are restated, which triage ruled orchestration, not a copy (A, unification carded as security(metadata-protocol,runtime): one stored-metadata search narrowing and one filter-field collector, exported by the door and called by the reader seam (consolidation after PR #21539) #21544). RIGHT.
  6. count: the narrowed query is computed and discarded, and count runs with the caller's query. Right today, because the engine's count admits only context and where and refuses search before any scan; it becomes a gap the day count admits search. Note for security(metadata-protocol,runtime): one stored-metadata search narrowing and one filter-field collector, exported by the door and called by the reader seam (consolidation after PR #21539) #21544's claim: let count consume the guard's return.
  7. aggregate under search and searchFields (both admitted by the engine's aggregate): narrowed, since aggregate is a row-serving read here. RIGHT.
  8. Write-return serve: every write verb the scoped repository exposes (insert, create, update, updateById, delete, deleteById) and the bridge's updateMany, deleteMany and upsert are in the set; a number, null or non-family answer passes by reference. The write's own predicate oracle is [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520's, per triage. RIGHT.
  9. Verb census of the scoped repository against the seam's three lists: find, findOne, aggregate served; count guarded; the six writes served on return; execute untouched by ruling. No row-returning verb is outside the lists. RIGHT.
  10. execute unreachable from a served body: the sandbox VM bridge installs on object() exactly find, findOne, count, aggregate and insert, update, delete, updateMany, deleteMany, upsert; no execute, sudo or withRunAs. The reading holds for a sandboxed body and is pinned end to end. A host handler's scoped API is the raw context served through the seam, where execute passes by delegation; that context is deployer code, which triage ruled outside the family. RIGHT, scoped as the PR states.
  11. Aggregation member on the body or a hash column, a shape the MCP stdio door refuses and the generic data door does not: checked and closed. The engine's aggregate field-type door refuses min and max over a text or textarea target with its own INVALID_FIELD / 400 before any driver runs, and the count-shaped members serve a number, never stored bytes. No escalation.
  12. having: neither the generic door nor the seam collects it; parity with the reference door. An observation for the family census, not a defect of this diff.
  13. Pins: the composed pins assert 400, INVALID_FIELD, the offending column named, and no family content in the answer, for administrator and member, through both a sandboxed body and a host handler's engine handle; the unit pins assert the door's exact envelope and that no read reached the double. The composed default-search pin asserts the answer is served like the door but not the narrowed scan set; the unit pin asserts that set on the double. Acceptable.
  14. scripts/engine-double-contract.pinned.json gains two rows through the gate's own writer; the gate is green on the head. RIGHT.

② Semver level

③ Boundary flags

Implemented-by: claude/issue-21454-evaluate-refusals
Reviewed-by: session_016GiHYRmLSNWTfbX9gVQkpz

VERDICT: FAIL


Generated by Claude Code

@objectstack-fleet objectstack-fleet Bot changed the title fix(runtime): refuse the stored-metadata family evaluate shapes and serve write returns at the reader-context seams fix(runtime)!: refuse the stored-metadata family evaluate shapes and serve write returns at the reader-context seams Oct 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 19b2cb6e50e576813431b22928aa1057ce4de0ef
Local-runs: none

Inputs: card #21454 (body and all 22 comments, the earlier record 5966227401 on head 1a6fbf7d93 and everything after it included), PR #21539 (body, the 6-file list, the net diff against main from merge-base ad7c351898), and the check-runs on the head, read at 2026-10-03T06:47Z. Read-only throughout: git object reads and GET calls; nothing built, run or re-run. Latest run per check name, all 34 complete: 29 success, 5 skipped, 0 failures. The seven required contexts are green (Lint and Repo Gates, which finished at 06:42Z and carries the ADR-0087 and empty-changeset gates; TypeScript Type Check; Test Core 1 to 6 plus the rollup; Dogfood Regression Gate 1 to 3 plus the rollup; Build Core; Temporal Conformance; Governed Surface Queue Guard), with Check Changeset, Dogfood Verify CLI, the four claim and serial guards, Check Documentation Links and Flag docs affected also green; Build Docs, Console Pin Gate, Packed-tarball smoke, and the re-run Auto Label and Check PR Size jobs are skipped. Not governed: none of the six paths is on the register. Draft, Part of #21454. ⛔ Classes, doors and roles only below.

① Derived judgments

The code is byte-identical to head 1a6fbf7d93 (the patch-round diff names only the changeset); every judgment below was re-derived from the diff and the reference files on main, not adopted from the earlier record.

  1. Public-surface widening, @objectstack/metadata-protocol (index.ts, +14): the four evaluate-refusal predicates the generic data door already runs are re-exported from the redaction module; additive, absent on main, consumed by the seam and never copied. RIGHT.
  2. Evaluate refusals at the three reader contexts: find, findOne and aggregate pass through the served read, which the handler's engine facade find also calls, so a body's object API, a handler's scoped API and the engine handle are all judged; count is judged in its own branch and is never served a row. Order: search, body grouping, body filter and sort, hash grouping, filter and sort; the same four predicates in the same order the door runs them. RIGHT.
  3. What the collector reads: where, the filter alias, each aggregation's own filter, and orderBy (object-form field or a bare string entry). The engine folds filter into where on every entry point and refuses the wire-only sort spelling on find, so no predicate or order reaches the engine under a spelling the seam does not read. The array-form filter is lowered through the spec's own parser before collection, matching the engine's array door. RIGHT.
  4. Collector choice: collectConditionFields from @objectstack/plugin-security, a dependency runtime already declares; plugin-security declares no dependency on runtime, so no cycle. It gates on a dotted head and walks cross-field comparands, so it can only refuse more than the door's own collector, never a scalar-column query; under an unrecognised combinator both collectors fail open alike. Accepted by triage at 5965718076. RIGHT.
  5. Default-search narrowing is live in composition: all three seam call sites hand the seam the ObjectQL engine, whose getObject answers the object schema with a record field map, the shape the spec's search resolver reads; the body column is text-like and the hash columns are text, inside the auto-default searchable set, so the narrowing is load-bearing. Each dropped column is judged by the door's own search predicate; an explicit list naming an unscannable column is refused under either spelling; a set that narrows to nothing is refused; an object with no field map passes through, as at the door. The restated step order and empty-set rule were ruled orchestration, not a copy (A; unification carded as security(metadata-protocol,runtime): one stored-metadata search narrowing and one filter-field collector, exported by the door and called by the reader seam (consolidation after PR #21539) #21544). RIGHT.
  6. count: the guard's narrowed query is discarded and count runs the caller's own. Right today: the engine's count admits only context and where and refuses search before any scan, and the oracle shapes on where are refused by the guard. Observation for security(metadata-protocol,runtime): one stored-metadata search narrowing and one filter-field collector, exported by the door and called by the reader seam (consolidation after PR #21539) #21544: let count consume the guard's return, so the day count admits search is covered.
  7. aggregate under search and searchFields (both admitted by the engine's aggregate): narrowed, since aggregate is a row-serving read here. having is collected by neither the door nor the seam: parity with the reference door, an observation for the family census, not a defect of this diff. RIGHT.
  8. Write-return serve: the set covers every write verb the scoped repository exposes (insert, create, update, updateById, delete, deleteById) plus the three bulk names the VM bridge installs (updateMany, deleteMany, upsert), which the host dispatches as a method of the served repository, so any repository implementing them is served too. A number, null or non-family answer passes by reference. Whether a body may write the family at all is [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520's question, and the attach point is named in a code comment. RIGHT.
  9. Verb census of the scoped repository against the seam's three lists: find, findOne, aggregate served; count guarded; the six writes served on return; execute untouched by ruling 5964836549. No row-returning verb is outside the lists. RIGHT.
  10. execute unreachable from a served body: the VM bridge installs on object() exactly find, findOne, count, aggregate and insert, update, delete, updateMany, deleteMany, upsert; no execute, sudo or withRunAs reaches the VM, and the absence is pinned end to end. A host handler's scoped API is the raw context served through the seam, where execute passes by delegation to deployer code, which triage ruled outside the family; a nested sandboxed body reached that way still receives its API at the sandbox's one build point, which serves it. RIGHT, as the PR scopes it.
  11. Pins: 17 unit cases on the scoped double assert the door's exact envelope (code, status, param, field) and that no read reached the double, the lowered array form, both explicit-search spellings, scalar-column passthrough, the narrowed default search keeping the name column and dropping the body and hash columns, the served write return with number and non-family passthrough, and no execute added by the seam. 26 composed cases in a booted kernel (15 carried, 11 new: four refusal cases for each of administrator and member through a sandboxed body and a host handler's engine handle, the narrowed default search for each role, and the typeof execute reading) assert 400, INVALID_FIELD, the offending column named, and no family content in the answer. Acceptable.
  12. scripts/engine-double-contract.pinned.json gains two rows through the gate's own writer, after the double's update and delete were routed through the engine's own dispatch predicates; the gate is green on the head. RIGHT.
  13. Changeset text against the diff: the three contexts named are the three seams; the refused shapes (filter, sort or group on the body or a hash column; an explicit search-field list naming one; a count with such a filter) are the shapes the guard refuses; the narrowed default search, the served write return, the unreachable execute and the four exports are each as the diff has them. RIGHT.

② Semver level

  • Clause-②: yes (narrowing) is line-initial in the PR body (read back from the API) and in the changeset; the gate's clause-② reader takes that spelling as a declared narrowing arm. The yes carries the export widening of @objectstack/metadata-protocol; the (narrowing) carries the accept-set narrowing in @objectstack/runtime: a body's or handler's read of the family's two tables that filters, sorts or groups on the body or a content-hash column, searches one explicitly, or counts with such a predicate, ran before this head and is refused with 400 after it. RIGHT.
  • Levels: @objectstack/metadata-protocol minor for the widening; @objectstack/runtime minor, which is what AGENTS.md's BREAKING (narrowing) ships as under the launch-window convention, exactly as the family's sibling changeset in 713b0fa76d landed. The group resolves to minor. RIGHT.
  • Carriers: the summary line is fix(runtime)!: and the PR title carries the same bang; one **BREAKING** paragraph names, as classes, what the three reader contexts may no longer do on the two tables, the route (scalar-column filters, sorts, groups and searches; bodies read with a plain list, served projected), that a default search is narrowed rather than refused, and that it ships minor under the launch-window convention; exactly one ADR-0087 marker, in the gate's comment form (described here in words), with the category not-required (no-migration-prescription), one of the gate's five categories, and the other four closed on facts (both packages publish; no registry id covers a refused query shape; runtime behaviour, not a declaration). RIGHT.
  • Gates: Check Changeset green on the head (the level axis, driven with the PR's own body); Lint and Repo Gates green on the head, which carries the ADR-0087 registration and empty-changeset gates. The earlier record's prescription (5966227401 ②) is met item by item. No skip-changeset, and none would apply.

③ Boundary flags

Implemented-by: claude/issue-21454-evaluate-refusals
Reviewed-by: session_016GiHYRmLSNWTfbX9gVQkpz

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 06:49
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 06:49
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit 2f837a5 Oct 3, 2026
46 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21454-evaluate-refusals branch October 3, 2026 07:22
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ts that decided them (stage 10 of objectstack-ai#20595) (objectstack-ai#21546)

Part of objectstack-ai#20595
Clause-②: no

## What changed

Stage 10 of the `domain:engine` lane of the dead-citation sweep:
`packages/drivers/driver-turso/**`, comment and docblock prose only, per
the claim (`5965451347`). Stages 1 to 9 landed as `a7d9768ec`,
`d150c3039`, `4bf4e7e70`, `13a24ece2`, `db0cf2231`, `85986144c`,
`48fa7a381`, `c205b6c35` and `c98a72d69`. objectstack-ai#20595 stays open: the other
half of this lane is the packages this stage does not touch
(`drivers/driver-mongodb` 9, `formula` 4, `metadata-fs` 2 on the census
after this stage, 15 in all), plus the test-string sites the card
carries for a widened stage.

Every comment or docblock site in the package that cited a tracker
number answering 404 is rewritten in ruling C+D's form C (record
`5749154545` on objectstack-ai#19123): the ADR when one records the decision,
otherwise the commit in this repository's history that made it. That is
**50 sites on 49 lines in 12 files, covering 13 numbers**:

- **14 census sites** (13 lines, 2 files under `src/`): the whole
`allocated-but-absent` population of the gate's own census in this
package at the base. `turso-driver.ts:2131` carries two of them (objectstack-ai#6076
and objectstack-ai#6075);
- **36 test-comment sites** (36 lines, 10 test files), which the census
defers. They carry 12 numbers: 7 the census itself reads as dead in this
package's `src` (objectstack-ai#6075, objectstack-ai#12380, objectstack-ai#14428, objectstack-ai#14438, objectstack-ai#16570, objectstack-ai#16711, objectstack-ai#17690),
and 5 that stand only in test files here (objectstack-ai#14434, objectstack-ai#17590, objectstack-ai#17876,
objectstack-ai#17879, objectstack-ai#17970), which the board and a single read each settle;
- **no site outside the census glob**: `vitest.config.ts`,
`tsconfig.json`, `package.json`, `README.md` and `LICENSE` cite no dead
number.

No comment-id citation is dead here: the package's two comment ids,
ruling records `5865693155` and `5861435168` (`remote-transport.ts:38`,
`:4152` to `:4153`, `turso-20444-empty-operator.test.ts:8`), both answer
200 (see Census).

**Anchors: 13 numbers by commit, 0 by ADR, 0 by repository qualifier; 12
distinct shas** (objectstack-ai#17690 and objectstack-ai#17876 share `be5c60291`: the squash of PR
objectstack-ai#17876, whose message opens 「Part of」 objectstack-ai#17690). 12 numbers reuse the
anchor stage 4 (`driver-sql`) measured for them; `ca3fd4b1a` (objectstack-ai#14428) is
measured here.

Only comments changed. Every file keeps its line count (49 lines out, 49
in, plus the changeset), so no line citation into any of them moves. No
code token moves (the guard below). All 98 changed lines open with a
comment marker. **No citation number is added**: on every changed line
the numbers on the new text are a subset of those on the old (the only
numbers on `+` lines are objectstack-ai#5181, objectstack-ai#6210 twice, objectstack-ai#6212, objectstack-ai#13878 and objectstack-ai#20987,
each already on its line and each answering 200).

**A `patch` changeset**: all 13 rewritten non-test lines are in the
published `dist` (the `.d.ts` keeps JSDoc on exported members, and
esbuild keeps these comments in the JavaScript), and `dist` is not
byte-identical with the base text (see Changeset).

## H0: the package and its size

The gate's own `node scripts/check-issue-citations.mjs --census --json`
at base `31d2255f5` (the before run below), `allocated-but-absent` per
remaining `domain:engine` package:

| package | before | after this stage |
|---|---|---|
| `drivers/driver-turso` | **14** | **0** |
| `drivers/driver-mongodb` | 9 | 9 |
| `formula` | 4 | 4 |
| `metadata-fs` | 2 | 2 |
| `metadata-core`, `core`, `metadata-protocol`, `objectql`, `metadata`,
`drivers/driver-sql`, `drivers/driver-memory`,
`drivers/driver-sqlite-wasm`, `plugins/plugin-pinyin-search`,
`platform-objects` | 0 each | 0 each |

The lane total goes 29 to 15. `driver-turso` is the largest remaining
package and reads 14, as at stage 9's head census (`99f2cfdf0`), so the
stage went ahead.

## Census: `driver-turso`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count is its `allocated-but-absent` findings under
`packages/drivers/driver-turso/`.

| reading | tree | board | whole-repo `allocated-but-absent` | sites |
lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `31d2255f5`, run 04:20:17Z to 04:23:32Z | enumerated,
194 pages, frontier objectstack-ai#21537, 19,358 records | 144 | **14** | 13 | 2 | 8 |
| after | `841405134`, run 04:32:36Z to 04:35:47Z | enumerated, 194
pages, frontier objectstack-ai#21539, 19,360 records (newest number read before and
after the run: objectstack-ai#21539) | 130 | **0** | 0 | 0 | 0 |

The whole-repo drop is 14, and the two finding sets differ by exactly
the 14 rows of this package, removed; none was added. `resolves`
(35,495), `resolves-as-pull-request` (2,388) and `cross-repo-unjudged`
(1,247) did not move.

The head's later commits are the changeset and one merge of `main`. The
census was run a third time at the head `e89bd10cd` (04:53:10Z to
04:56:16Z, 194 pages, frontier objectstack-ai#21544, 19,365 records, newest objectstack-ai#21543
before and objectstack-ai#21544 after): whole-repo 130, `driver-turso` 0. Against the
after run, 5 rows moved line (all in `packages/spec`, shifted by the
merged `main` commit `48eb9c193`), and the finding multiset with line
numbers set aside is identical. Its `resolves` reads 35,509, 14 more
than above, from the merged `main` commits outside this package.

**Supplementary instrument, the whole package.** The census reads
neither test files nor strings nor files outside `src`. A second reading
runs the gate's own exported `extractCitations` (whole-file and
comment-prose projections) over every tracked file in the package (102)
and classifies each citation with the gate's `classifyCitation` against
one board enumerated by the gate's `enumerateBoard` (194 pages, frontier
objectstack-ai#21537, 19,358 records, read 04:24:21Z to 04:27:29Z), the same board for
both readings. Every one of the 13 numbers was then read on its own over
the issues endpoint (04:29:27Z): **all 13 answer 404**; the numbers that
stay on changed lines (objectstack-ai#5181, objectstack-ai#6210, objectstack-ai#6212, objectstack-ai#13878, objectstack-ai#20987) and the lit
controls objectstack-ai#15280, objectstack-ai#12585 and objectstack-ai#6402 answer 200.

| reading | citations | dead | src comment | test comment | other files
| test string | changelog |
|---|---|---|---|---|---|---|---|
| before, `31d2255f5` | 1,921 | **83** | 14 | 36 | 0 | 6 | 27 |
| after, `841405134` | 1,871 | **33** | 0 | 0 | 0 | 6 | 27 |

The citation count drops by 50, the 50 rewritten sites; no respelling
stays a citation. The live counts did not move (src comment: 522
resolve, 80 as pull requests, 8 cross-repo; test comment: 609, 75 and
6). A third, raw reading (every `#` followed by 2 to 6 digits, whatever
surrounds it, `CHANGELOG.md` aside) counts 1,558 before and 1,509 after,
and the URL-spelled issue links go 7 to 6: also a drop of 50.

**Comment ids.** Every ten-digit run under
`packages/drivers/driver-turso` (its `CHANGELOG.md` aside) was read:
five lines. `5861435168` (the record of ruling B on objectstack-ai#20311) and
`5865693155` (the record of ruling A on objectstack-ai#20399) answer 200 (the control
`5965451347`, the claim, answers 200 too).
`remote-canonical-backfill.ts:357` is an epoch-seconds example, not a
citation.

## Per-number table

`src` counts census sites, `test` the test-comment sites. Every sha
matches exactly one commit (`git rev-parse --disambiguate`, count 1), is
an ancestor of the base `31d2255f5` and of `origin/main` (`git
merge-base --is-ancestor`, exit 0 for all 12 on both; the clone is not
shallow), and resolves over the commits endpoint. The `+` lines carry
exactly these 12 nine-hex spans as new ones. Each commit names the
number it replaces, in its message, its diff or both: 7 in the message
and the diff (objectstack-ai#6075, objectstack-ai#12380, objectstack-ai#16570, objectstack-ai#17590, objectstack-ai#17690, objectstack-ai#17879, objectstack-ai#17970), 3
in the diff alone (objectstack-ai#14428 for `ca3fd4b1a`, objectstack-ai#14438 for `2200f8ec8`,
objectstack-ai#16711 for `7862fb711`), and 3 only in the subject's squash suffix
(objectstack-ai#6076 for `6513c1749`, objectstack-ai#14434 for `93940d492`, objectstack-ai#17876 for `be5c60291`),
where the dead number was that pull request's own and the commit is its
squash. `git blame` at the base puts 24 of the 49 changed lines on their
anchor; the other 25 were written by a commit that cites the number as
an earlier decision (`242eb0ac1`, `9f4a6d55f`, `9bfbacbf8` and
`6bd3231f6` citing objectstack-ai#12380; `3cbcedb62` and `e35c40a52` citing objectstack-ai#14438;
`fa2d3b737` citing objectstack-ai#6075 and objectstack-ai#6076; `5ba2ec3ca` citing objectstack-ai#17690;
`862f12c0b` citing objectstack-ai#17590; `2200f8ec8` citing objectstack-ai#14428; `ca3fd4b1a` citing
objectstack-ai#14434; `7862fb711` citing objectstack-ai#16570; `eb9334915` citing objectstack-ai#17876), and in
each case the anchor is the commit that made the change the sentence
credits to the number. `source` says whether stage 4 already used this
anchor for this number (`reused`) or it was measured here (`measured`).

| number | src | test | anchor | kind | source | what it decided |
|---|---|---|---|---|---|---|
| `objectstack-ai#6075` | 1 | 1 | `d367f03d6` | commit | reused (stage 4, stage 5) |
five drivers' `query` parameters follow `DriverQuery`; its turso diff
narrows `count`'s `query` and leaves `options?: any`, the half-narrowed
state both sites describe. It is the squash of PR objectstack-ai#6210, the live number
kept beside it |
| `objectstack-ai#6076` | 1 | 0 | `6513c1749` | commit | reused (stage 4) |
`IDataDriver`'s `query` parameter becomes `DriverQuery` (that pull
request's squash) |
| `objectstack-ai#12380` | 2 | 6 | `4045b954d` | commit | reused (stage 4) | make the
SQLite `Field.json` codec injective, one encoding across all three
dialects: the local half's codec every site credits |
| `objectstack-ai#14428` | 2 | 3 | `ca3fd4b1a` | commit | measured | `update()` on a
missing id answers `null` on Turso's remote face; it wrote the
`RemoteTransport.update()` docblock and the
`turso-update-missing-id.test.ts` header the sites carry, and declared
that door's record-or-null return |
| `objectstack-ai#14434` | 0 | 1 | `93940d492` | commit | reused (stage 4, stage 5) |
declare the not-found arm on `IDataDriver.update()` (that pull request's
squash) |
| `objectstack-ai#14438` | 2 | 5 | `2200f8ec8` | commit | reused (stage 4) |
`update()` publishes the contract's record-or-null, not `any`, on
`SqlDriver` and on `TursoDriver`'s override (the squash of PR objectstack-ai#15280) |
| `objectstack-ai#16570` | 1 | 1 | `b72226f48` | commit | reused (stage 4) | declare
the `indexes` key `initObjects` / `registerObjectMetadata` already read
|
| `objectstack-ai#16711` | 1 | 3 | `7862fb711` | commit | reused (stage 4, stage 3) |
object-definition parameters declare the keys they are read for, plus
the gate that sees subclass overrides; it wrote
`TursoDriver.initObjects`'s docblock and
`turso-driver-16711-init-objects-param.test.ts` |
| `objectstack-ai#17590` | 0 | 1 | `e04a0aff2` | commit | reused (stage 4, stage 5,
stage 8) | compile `$contains` on a JSON column as a per-dialect
MEMBERSHIP test |
| `objectstack-ai#17690` | 4 | 4 | `be5c60291` | commit | reused (stage 4) | eight
more `IDataDriver` doors publish their declared return type,
`TursoDriver`'s overrides among them; its message records the
transaction door left un-narrowed because each of the two repairs is
「neither of which is an annotation swap」, with the 「+14 further consumer
sites」 measurement, the sentence `turso-driver.ts:3549` cites |
| `objectstack-ai#17876` | 0 | 3 | `be5c60291` | commit | reused (stage 4) | that pull
request's squash, which installed the `ContainsAny` detector |
| `objectstack-ai#17879` | 0 | 6 | `eb9334915` | commit | reused (stage 4) | measure
the `ContainsAny` phantom-leg sweep across eight door pins; its message
records that both repair candidates were measured |
| `objectstack-ai#17970` | 0 | 2 | `47e6601c5` | commit | reused (stage 4) | collapse
`ContainsAny`'s distributivity so union-shaped doors assert |

No ADR or ruling record names any of the 13 numbers as the place their
decision is recorded; ADR-0104 names objectstack-ai#12380 in passing, and `4045b954d`
is the change these sites describe.

## Wordings to check

Most rewrites swap a tag in place (`[#N]` to `[commit SHA]`, `(#N)` to
`(commit SHA)`, `with #N` to `with commit SHA`, stage 1's form). These
say more than the tag:

- **Squash rewrites, stage 4's spellings**: 「objectstack-ai#5181 (PR objectstack-ai#6076), objectstack-ai#6075 (PR
objectstack-ai#6210)」 became 「objectstack-ai#5181 (commit 6513c17), commit d367f03 (PR objectstack-ai#6210)」
(`turso-driver.ts:2131`): the dead pull-request number gives way to its
squash, and the dead card number to the commit that landed its fix with
the live pull-request number kept beside it as a convenience link, stage
4's 「Commit 2200f8e (PR objectstack-ai#15280)」 form. 「objectstack-ai#6075 (PR objectstack-ai#6210) narrowed」
became 「commit d367f03 (PR objectstack-ai#6210) narrowed」
(`turso-driver-options-door.test.ts:21`). 「Since objectstack-ai#13878 (PR objectstack-ai#14434)」
became 「Since objectstack-ai#13878 (commit 93940d4)」
(`turso-update-missing-id.test.ts:20`). 「`ContainsAny` (objectstack-ai#17876)」 became
「`ContainsAny` (commit be5c602)」 on three test lines.
- **Reports**: 「the two measured repairs are in the objectstack-ai#17879 report」
became 「the two measured repairs are in commit eb93349's message」
(three test files), stage 4's spelling; that message records that both
candidates were measured.
- **`pre-` spellings**: 「restoring the pre-objectstack-ai#12380 SQLite `json` branch」
became 「restoring the SQLite `json` branch from before commit 4045b95」
(`remote-transport.ts:2612`), and 「The pre-objectstack-ai#12380 form of the string
'bare'」 became 「The form of the string 'bare' from before commit
4045b95」 (`turso-local-json-backfill-depth-limit.test.ts:72`), stage
4's 「from before commit」 form.
- **A URL-spelled citation**: 「@see
objectstack-ai#12380 (the
injective local codec)」 became 「@see commit 4045b95 (the injective
local codec)」 (`turso-json-column-type-asymmetry.test.ts:84`), stage 4's
「@see commit e04a0af (the membership construct)」 form. The three
`@see` links beside it (objectstack-ai#12738, objectstack-ai#12586, objectstack-ai#11535) resolve and stay.
- **The acceptance-criteria reference**: 「(objectstack-ai#16711 验收口径 item 4)」 became
「(验收口径 item 4 of the card commit 7862fb7 closed)」
(`turso-driver-16711-init-objects-param.test.ts:105`), stage 4's wording
for the same sentence in `driver-sql`'s twin test, verbatim. Neither the
card nor PR objectstack-ai#16816 resolves any more, so 「closed」 rests on stage 4's
reading and on `7862fb711` itself: its diff names objectstack-ai#16711 32 times and
creates this test file, and its message records the TS2353 negative
control this paragraph introduces.
- **Sentence and bullet starts**: where the number opened a sentence, a
bullet or a header, the new text opens with 「Commit」: 「Commit
4045b95's codec still runs」
(`turso-json-column-type-asymmetry.test.ts:259`), 「- Commit b72226f's
`indexes` fix」 (`turso-driver-16711-init-objects-param.test.ts:17`), 「//
Commit 2200f8e — 」 (`turso-driver-update-declared-null.test.ts:3`) and
「⛔ Commit 7862fb7 — this parameter type must declare every key」
(`turso-driver.ts:3689`). 「* commit 4045b95 had to defeat」
(`turso-json-column-type-asymmetry.test.ts:46`) stays lower-case: it
continues 「the measured … exposure that」 from the line above.
- **A tag over a paragraph that says 「this card」**:
`turso-driver-doors-declared-types.test.ts:117` now opens 「[commit
be5c602]」, and its line 119 still says 「every door on this card had
regressed to」. That line carries no number, so it was not touched; it
now reads as the card behind the commit in the tag (`be5c60291`'s
message opens 「Part of」 that card).
- **The commit that wrote the file**:
`turso-driver-update-declared-null.test.ts:3` and `:11` cite
`2200f8ec8`, the commit that created that test file. Its subject is
「feat(driver-sql,driver-turso): `update()` publishes the contract's …
not `any`」, so it is both the override fix the header names and the
`SqlDriver.update()` narrowing line 11 credits.
- No line was reflowed, so some are longer than their block's wrap
(`eslint.config.mjs` declares no line-length rule, and a reflow would
move neighbouring lines and every line citation into the file).

## Sites left

- **In comments (src, test, outside the glob): none.**
- **String literals: 6 test-string sites, 4 numbers, 4 files**: the
`describe` titles at `turso-driver-16711-init-objects-param.test.ts:67`
(objectstack-ai#16711), `turso-driver-update-declared-null.test.ts:85` (objectstack-ai#14438) and
`turso-update-missing-id.test.ts:141` and `:223` (objectstack-ai#14428), and two
assertion-message strings at
`turso-json-column-type-asymmetry.test.ts:266` and `:291` (objectstack-ai#12380). All
four numbers are in this stage's table. Strings are outside this stage's
surface; non-test strings cite none.
- **Outside `src`:** the release-owned `CHANGELOG.md` names dead numbers
on 27 sites (11 numbers); left.

## Mechanical guard: no code token moves

The guard (stages 2 to 9's, copied verbatim from stage 9) compares base
`31d2255f5` against the tree over all 12 touched files, with TypeScript
6.0.3:

- **Reading 1**: the parser's leaf nodes, from a `forEachChild` walk.
Comments are trivia there, and JSDoc is never visited. A leaf that is
not itself a token is re-scanned with trivia skipped.
- **Reading 2**: the full token stream in parser context, from a
`getChildren` walk, JSDoc nodes skipped. String, template and numeric
literals are compared in full on both readings.

Results, at `841405134` (the later commits touch none of the 12 files):

- Real run: 38,590 base tokens, **0 files with a token change** (exit
0).
- Comment control (「Keep it that way」 to 「KEEP it that way」,
`turso-driver.ts`): 0 files changed (exit 0).
- Positive control, an identifier (`export class RemoteTransport` to
`XRemoteTransport`, `remote-transport.ts`): DIFFER on both readings
(exit 1).
- Positive control, a string literal (the assertion message 「… for an
existing id」 to 「… for an existing iD」,
`turso-update-missing-id.test.ts`): DIFFER on both readings (exit 1).
- Positive control, a template literal (the index name 「uniq_ … _v」 to
「uniq_ … _w」, `turso-driver-16711-init-objects-param.test.ts`): DIFFER
on both readings (exit 1).
- Positive control, a numeric literal (`deepArray(1001)` to `1002`,
`turso-local-json-backfill-depth-limit.test.ts`): DIFFER on both
readings (exit 1).

Each mutation went through `scripts/ablation-replace.mjs` (wrap mode,
anchor hit 1 to 0, blob changed) under a shell trap that restores by
absolute path from `HEAD`. Each restore was proven equal to its `HEAD`
blob (`cc15138aba39`, `8deefdb0598b`, `e36d370cb3ed`, `f9142478172e`,
`7343e92b5c7d`), with `git diff HEAD` empty and a clean tree afterwards.

## Changeset: `patch` (`dist` measured)

`files[]` is `dist`, `README.md` and `CHANGELOG.md`, and the package is
not private. In one script under the shared verify lock (VERDICT
command-exit 0, held 104s), at `841405134`: the dependency closure was
built first (`pnpm --filter '@objectstack/driver-turso^...' build`),
then the package's own `build` (tsup and `check-dts-emitted`) ran three
times:

- **Leg 1**, the head text: 6 `dist` files hashed (`index.js`,
`index.mjs`, their sourcemaps, `index.d.ts`, `index.d.mts`). All 13
rewritten non-test lines appear verbatim in `dist`. The 5 docblock lines
on exported members (`remote-transport.ts:1645`, `:2601`, `:2612`;
`turso-driver.ts:3689`, `:3694`) are in all four of `index.js`,
`index.mjs`, `index.d.ts` and `index.d.mts`; the 8 `//` lines in
`turso-driver.ts` are in `index.js` and `index.mjs`.
- **Leg 2**, the base text put back in the 2 non-test touched files (2
of 2 proven equal to their base blob): 4 of the 6 files differ from leg
1 (`index.d.ts`, `index.d.mts`, `index.js`, `index.mjs`); the two
sourcemaps do not. `scripts/ablation-dist-preflight.mjs` finds the base
marker 「[objectstack-ai#14428] A miss answers」 in those 4 built files (exit 0).
- **Leg 3**, after the proven restore (2 of 2 equal to their `HEAD`
blob, `git diff HEAD` empty, porcelain empty): all 6 files are
byte-identical to leg 1, and the preflight's `--absent` reading exits 0
with a clean tree, so the build is deterministic and the difference is
the rewrite.

So the rewrite ships, and
`.changeset/20595-driver-turso-provenance-anchors.md` declares a `patch`
for `@objectstack/driver-turso`, comment text only, with the claim's
`Clause-②: no` line. Every anchor is a commit, so it names no ADR,
repository qualifier or bracketed substitution; it says which published
files carry the reworded text, as measured above. The changeset commit
touches no file under `packages/drivers/driver-turso`.

## Gates (head `e89bd10cd`)

- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at `e89bd10cd` (13 paths against
merge base `49161683f`, 113 changed lines) derived 65 commands. All 65
ran (04:52:21Z to 05:03:07Z, after the workspace build), each exit code
captured before any pipe: 65 exit 0. `--ran` reports 「65 derived, 65
run, 0 NOT-MEASURED, 0 UNRUN」 (a derived zero) and exits 0. The PM's
lead derivation (55 commands, tree `550f4cc2f`) is a subset: the extra
10 are the eight families the `.changeset/` path adds (the ADR-0087
registration and empty-changeset pairs, `check:objectui-changeset`,
`check:pm-changeset-deadline-census` and two release self-tests) and two
gates whose source names
`packages/drivers/driver-turso/src/turso-driver.ts` itself
(`check:object-def-param-keys`, `check:tenant-chokepoint`).
- **Named readings:** `node scripts/check-issue-citations.mjs` exits 0
(「every citation this change adds resolves (or is a declared cross-repo
reference)」: 3 judged across 2 files, the three numbers kept on
`turso-driver.ts:2131`, two of them issues and one a pull request);
`pnpm check:issue-citations` exits 0 (self-test, 173 cases, 9
batteries); `pnpm check:doc-authoring` exits 0 (the sibling-package
prose-id baseline holds, no growth); `pnpm check:nul-bytes` exits 0
(9,888 files, no raw control bytes), and a control-byte grep over the 13
changed files finds none (exit 1). The changeset gates
(`check-changeset-no-major`, `check-adr-0087-registration`,
`check-empty-changeset` with `--base origin/main`, and
`check:changeset-gate-self-tests`) exit 0; `check:object-def-param-keys`
exits 0 (5 override parameter positions compared).

- **Build, tests and typecheck, under the verify lock:** at `e89bd10cd`,
the dependency closure and the package were rebuilt, then `pnpm --filter
@objectstack/driver-turso test`: 88 test files pass (88), 2,365 tests
pass and 33 are skipped (2,398); `pnpm --filter
@objectstack/driver-turso typecheck` (`tsc --noEmit`) exits 0 (VERDICT
command-exit 0, held 151s). `tsc --listFilesOnly` puts all 88 tracked
test files and all 12 changed files in `tsconfig.json`'s program. The
workspace build after the merge (`turbo run build
--filter='./packages/*' --filter='./packages/*/*' --concurrency=2`, 71
of 71 tasks, 1 cached; VERDICT command-exit 0, held 319s) ran before the
gates. No importing package owes a run: the declaration files change
only in comment text.
- **Lint, as a proven narrowing, at `e89bd10cd`:** eslint with inline
config disabled, over the 12 touched `.ts` files plus `dist/index.js` as
the control: 13 results, 0 errors and 1 warning, the control's ignore
notice; none of the 12 is reported ignored. `eslint.config.mjs` never
enables type-aware linting (its lines 327 and 328 say so), so a comment
edit cannot move the verdict on an untouched file. The repo-wide `pnpm
lint` is CI's run.

## Acceptance notes

- **Base and merge.** The dispatch read `origin/main` at `550f4cc2f`; by
the time the worktree was cut, `main` had moved one commit (`31d2255f5`,
`packages/mcp/server.json` only), and the branch was cut there. It
merges `main` once, pinned to `49161683f` (merge `e89bd10cd`, no
conflict). The two commits it brought (`48eb9c193`, `spec`; `49161683f`,
`plugin-sharing` and `plugin-audit`) touch neither
`packages/drivers/driver-turso`, `check-issue-citations.mjs` nor
`dispatch-gates.mjs`; the closure and the workspace were rebuilt after
the merge, before the tests and gates. The net diff against `main` is
the 12 rewritten files (+49/−49) and the changeset (+15).
- **The same dead numbers outside this package**, each left to its own
carrier: `driver-mongodb` (this lane's next stage) names objectstack-ai#14428 in
`src/mongodb-driver.ts` and its tests and objectstack-ai#14434 in
`mongodb-update-missing-id.test.ts`, where `ca3fd4b1a` and `93940d492`
are the anchors; `driver-sqlite-wasm`, which the census reads 0, still
carries objectstack-ai#12380, objectstack-ai#14438, objectstack-ai#16711, objectstack-ai#17690, objectstack-ai#17876 and objectstack-ai#17879 in five test
files (comments and two `describe` titles; the census defers test files,
every anchor is in this table, and the comment sites are the same
sentences this stage rewrote here);
`scripts/check-object-def-param-keys.mjs` and
`.github/workflows/lint.yml` name objectstack-ai#16570 and objectstack-ai#16711 (outside the census
surface); `service-analytics`'s `contains-membership.test.ts` carries
objectstack-ai#17590 in a comment, and `driver-sql`'s test strings carry objectstack-ai#12380 and
objectstack-ai#17590; ADR-0104 names objectstack-ai#12380 (governed); the release pages name objectstack-ai#6075
and objectstack-ai#12380 (release-owned).
- **Wording only:** no line without a number was changed.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… the stored-metadata tables (objectstack-ai#21520) (objectstack-ai#21563)

Fixes objectstack-ai#21520

Clause-②: yes (narrowing)

Executes ruling A (record `5965059068`, maintainer 「同意」): **an
app-authored body may not touch the stored-metadata family's tables**
(`sys_metadata`, `sys_metadata_history`). For an app-authored body the
metadata protocol is their only writer. Two refusals, each carrying
`PERMISSION_DENIED` / 403 and a prescription that names the metadata
API:

1. **Binding.** A hook with a sandboxed `body` whose `object` names a
family table is refused at registration.
2. **Writing.** A sandboxed body's write of a family table through
`ctx.api` is refused before the write runs. This also closes the write
verb's own predicate path, which triage `5965718076` carried onto this
card: a refused write runs nothing, and its answer does not depend on
what it names.

Platform code is outside the refusals: the metadata protocol and its
writers, the platform's code hooks, and host code a deployer registers.

## Census of platform writers (A1), by symbol walk

**Method.** A TypeScript AST walk (the compiler API) over every non-test
source file under `packages/*/src`: 2707 files at `fd5a1cd597`.
- A call counts as a family write when its callee is a write verb
(`insert`, `create`, `update`, `updateById`, `upsert`, `delete`,
`deleteById`, `updateMany`, `deleteMany`, and the bulk spellings) and
its object argument resolves to a family name.
- An object argument resolves when it is a literal, a same-file binding,
or an exported constant (`OVERLAY_TABLE`, `METADATA_HISTORY_OBJECT`).
The receiver `X.object(name)` counts the same way.
- Write calls with a non-literal object argument, in files that name the
family, are listed separately so a dynamic writer is not hidden.

**Readings.**
- 13 resolved family writes: `metadata-protocol`
(`sys-metadata-repository.ts` ×5, `protocol.ts` ×2,
`migrations/recorded-by-sentinel.ts` ×1), `service-datasource`
(`datasource-admin-plugin.ts` ×4) and `plugin-security`
(`permission-set-overlay-discard.ts` ×1). Every one is platform module
code calling the engine directly (`this.engine` / `engine` / `ql`).
- 112 unresolved write calls across 18 family-naming files. All are
module code on an engine or driver receiver.
- `buildSandboxApi` is the only constructor of a body's API. It is
reached only from `buildSandboxContext` (hook bodies) and
`buildActionSandboxContext` (action bodies). No platform writer goes
through it.
- Zero shipped hook or action bodies name a family table in
`packages/**` or `examples/**` (non-test). The only `object:
'sys_metadata'` hits are the platform's own list views in
`metadata-core`, matching the ruling's census.

**A1's assumption measured false: the existing seam is not body-only.**
`serveStoredMetadataReadsThrough` is applied at `buildSandboxApi`
(bodies). It is also applied at `buildActionApi`, which is the `ctx.api`
of a host code action handler as well as of an action body. So a throw
in `serveRepository`'s shared write branch would also refuse deployer
host code. The ruling says the seam refuses bodies only, and triage
`5964836549` put deployer host code outside the family. So the write
refusal is a **separate, body-only layer in the same seam file**:
- `refuseStoredMetadataBodyWrites` layers over the read seam.
- It shares one derived-context walk (`deriveThroughSeam`) with the read
seam, so there is no second walk.
- It is applied only at `buildSandboxApi`.

`serveRepository`'s write branch keeps serving write returns for host
handlers. Its comment now says why the refusal is not attached there.

## The binding point (A2): one place every door shares

Every door a body hook binds through reaches `hookBodyRunnerFactory`'s
per-hook resolver. That resolver is where a `body` becomes a handler, at
registration:
- the boot artifact and an installed artifact, install and rehydrate,
through `bindAppArtifactHandlers` (its explicit runner);
- runtime-authored hooks, through ObjectQLPlugin's metadata-service bind
(boot sync and resync), which use the engine's default body runner
installed by `AppPlugin`. That runner is the same factory.

`bindAppArtifactHandlers` alone would have missed the runtime-authored
door. The refusal is a throw from the resolver, so the binder records it
against the hook and logs it at `error`, or rethrows under `strict`. The
hook is never registered.

**Wildcard.** A `'*'` body hook names no family table, so it binds, but
it admits the family's tables. Its body is therefore not run for a
family table's event: a dispatch-side check in the bound handler. The
bind says so once, at `info`.

**Platform hooks** are code handlers, never bodies, so they never reach
this factory. Pinned: a code hook on `sys_metadata` still binds and
fires, and the metadata door's save still fires a platform code hook.

## Codes (A3): an existing code fits, no new ledger row

Both refusals carry **`PERMISSION_DENIED` / 403**, a member of the
ledger's `ErrorCode` union (the standard catalog).
- The condition is generic: this author context is not permitted this
operation on this table.
- The ledger's admission rule (objectstack-ai#8211, mechanical) sends a generic
permission condition to the standard member rather than to a registered
synonym.
- Precedent: the flow-authoring write gate refuses an authoring write by
authority with the same code.
- What the author does instead is carried in the prescription.

So this is **not** `PENDING LEDGER CODE`, and nothing under
`packages/spec` is edited.

## Reach first (A5), measured as classes before the fix

The pins below were run against the pre-fix `body-runner.ts` (BASE
`fd5a1cd597`, byte-restored to HEAD afterwards, `git diff HEAD` empty).
Every observation is a neutral marker token on a free-text column. No
stored content is read.
- **Binding (unit tier, real ObjectQL + QuickJS):** a body hook
targeting each family table bound and ran on that table's write event,
through all three doors (artifact binder, runtime-authored default
runner, wildcard). Result: 6 red, 2 controls green.
- **Composed kernel:**
- The metadata door's own save ran an explicit family body hook, the
wildcard body hook and a runtime-authored family body hook (all three
markers present on the saved row).
- An elevated action body's insert into `sys_metadata` answered `200`
for the administrator and for a member.
  - Result: 4 red, 3 controls green.

## Pins

- `stored-metadata-body-boundary.test.ts`, 8 cases, binding, on a real
engine:
- refused at registration for each family table, string and list forms,
with code, status and the metadata-API prescription;
- refused and recorded through the artifact binder and through the
runtime-authored default runner;
  - thrown under strict binding;
  - a wildcard binds and never runs on a family table;
- controls: an ordinary hook binds, and a code hook on a family table
binds.
- `stored-metadata-body-writes.test.ts`, 10 cases, writing, on a
counting double:
- every write verb on each family table is refused before it reaches the
store;
- a predicate write answers identically whatever its predicate names,
and runs nothing;
  - reads pass to the read seam;
  - controls: ordinary tables write;
- `sudo`, `withRunAs`, `transaction` and `beginTransaction` refuse the
same way;
  - the layer is idempotent and transparent to the read seam's marker;
  - the read seam alone (a host handler's `ctx.api`) keeps its writes;
- through the real QuickJS sandbox, an action body and a hook body on an
ordinary table are both refused.
- `stored-metadata-body-boundary.pin.test.ts`, 7 cases, composed kernel,
boot paid in `beforeAll`:
- the metadata door's save runs no body bound to a family table, and
still fires the platform code hook;
  - controls: ordinary-table hooks fire, the wildcard among them;
- a runtime-authored family hook is not bound, while a runtime-authored
ordinary hook binds;
- an action body's family writes (an insert, a predicate update, a
history insert) answer `403 PERMISSION_DENIED` and land nothing, for
administrator and member;
  - control: the same body's ordinary write lands.

## Reverse verification (ablation), fix committed first, at `0d8af06c80`

Each leg ran through `scripts/ablation-replace.mjs`: the anchor hit 1 →
0 on disk, the blob changed, and the restore was proven (blob == HEAD,
`git diff HEAD` empty). The pins resolve `body-runner.ts` by relative
path within the package (src), so no dist leg applies.
- **A1**, registration throw disabled: 5 red (the 5 refusal and record
pins), 20 green. The composed "no body ran" pin stayed green because the
dispatch-side check still stops the body: defence in depth, observed as
expected.
- **A2**, dispatch-side check disabled: 2 red (the wildcard unit pin,
and the composed save pin with the wildcard marker present).
- **B**, the body write layer removed from `buildSandboxApi`: 4 red
(both sandbox unit pins, and both composed write pins).

## Tests and gates, at `9a95e459d1` (after merging `origin/main`
`ce532184d1`, which carries objectstack-ai#21539's landed seam)

- `@objectstack/runtime`, `--project local`: Test Files 316 passed,
Tests 4444 passed, 19 skipped. `--project repo`: 3 files, 751 passed.
- `pnpm --filter @objectstack/runtime typecheck`: exit 0.
`check:test-typecheck` is OK with the ledger unchanged, and all three
new test files are in the `tsconfig.test.json` program
(`--listFilesOnly`).
- `dispatch-gates --commands --repo objectstack-ai/objectstack` with no
paths derived 62 families. All 62 were run, each exit 0, and `--ran`
reconciled 62 derived, 62 run, 0 not-measured.
`check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET; after
a full `turbo run build` it measured 106 entries across 66 packages.
- **Lint, a proven narrowing** (`pnpm lint` itself is CI's run):
- population, from eslint's own config: 6 of the 7 changed paths are
linted (the changeset `.md` has no matching configuration);
  - count, from `--format json`: 6 files, 0 errors, 0 warnings;
- invariance: `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`), and its only import rule is per-file
(`no-restricted-imports`), so this diff cannot move the verdict on an
untouched file.
- `check:nul-bytes`: OK. Control-byte self-scan of the 7 changed files:
grep exit 1 (none).

## Acceptance notes

- **The metadata door accepts the hook that the bind then refuses.**
Saving a runtime-authored hook whose `object` is a family table answers
`200` at the metadata door (recorded by the composed pin). The bind then
refuses it and records the refusal at `error`, but nothing refuses it at
save. This is reported to the seat as an authoring-trap finding; it is
not fixed here (the save door and the spec are outside this card's
surface).
- An expected write refusal from an action body is logged by the
existing body-runner catch at `error` (`[BodyRunner] sandboxed action
threw`). That is the runner's existing posture for any body that throws,
and it is unchanged here.
- Flow record nodes and host code are outside this ruling (bodies only).
Action bodies declared on a family object are covered by the write
layer, like any body.
- An earlier head of this branch merged objectstack-ai#21539's head while it was open.
objectstack-ai#21539 has since landed, and `origin/main` was merged on top. The seam
file's objectstack-ai#21539 bytes are identical to the landed squash, so this PR's
diff against `main` is exactly the 7 files listed by the gate
derivation.

## Changeset

`@objectstack/runtime` **minor** (the launch-window convention for
accept-set narrowings), with `Clause-②: yes (narrowing)` and the
ADR-0087 disposition `not-required (no-migration-prescription)`. No
stored metadata shape, authorable key or export moves.

---
_Generated by [Claude
Code](https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ion in words instead of a tracker number (stage 5) (objectstack-ai#21568)

Part of objectstack-ai#20749
Clause-②: no

Stage 5 of the `domain:spec` lane's share of the runtime-string
burn-down (ruling `5902360492`, form D): the rest of class (b), the
protocol 17 → 18 conversion summaries in
`packages/spec/src/conversions/registry.ts`. Every rewritten summary now
states in words what the cited decision was, or drops a citation its
sentence already explained. Text only.

## What changed

- **35 ADR-0087 conversion summaries** (40 tracker ids, 34 distinct
cards): every toMajor-18 summary that carried an id, from
`field-malformed-scale-precision-removed` to
`flow-decision-mode-inclusive-explicit`. A summary is what `os migrate
meta --json` reports under `specChanges` (its chain already runs to
protocol 18, `CHAIN_TERMINUS_MAJOR`), and it becomes the upgrade guide's
"Change" column and the `to` text of `spec-changes.json`'s `converted[]`
once protocol 18 ships, so an author upgrading metadata reads it.
- One `@objectstack/spec` **patch** changeset, `Clause-②: no` (message
text only).
- **No generated file changes** (A2 below): no generator projects a
toMajor-18 summary today.

## Census at the base (A1)

Stage 4's instrument (`convtable.cjs`, byte-identical copy, md5
`9b7539067ffc38598172c692de637db5`) at base `e901c27449` (the worktree
before any edit): 127 conversions, **35 summaries with ids, 40 id
occurrences, 34 distinct cards** — stage 4's split unchanged. The
toMajor-18 conversion that PR objectstack-ai#21547 added carries no id. Under the
~60-card bar, so one stage. The stage-3 census re-run at the same base
agrees (class b: 35 messages / 40 ids; nothing else in class b).

| file:line (base) | id | conversion |
|---|---|---|
| `registry.ts:6551` | objectstack-ai#8321 | `field-malformed-scale-precision-removed`
|
| `registry.ts:6659` | objectstack-ai#8762 | `record-chatter-position-vocabulary` |
| `registry.ts:6777` | objectstack-ai#9198 | `element-input-target-variable-removed` |
| `registry.ts:7024` | objectstack-ai#9220 | `element-filter-removed` |
| `registry.ts:7177` | objectstack-ai#9249 | `element-form-removed` |
| `registry.ts:7356` | objectstack-ai#15178 | `translation-per-app-settings-removed` |
| `registry.ts:7356` | objectstack-ai#19620 | `translation-per-app-settings-removed` |
| `registry.ts:7610` | objectstack-ai#9249 |
`translation-component-submit-label-removed` |
| `registry.ts:7782` | objectstack-ai#3951 | `field-column-lists-canonicalized` |
| `registry.ts:7783` | objectstack-ai#9227 | `field-column-lists-canonicalized` |
| `registry.ts:7909` | objectstack-ai#10414 | `metric-filters-removed` |
| `registry.ts:8104` | objectstack-ai#17296 | `cube-sub-day-granularities-removed` |
| `registry.ts:8237` | objectstack-ai#18612 | `cube-join-sql-and-relationship-removed`
|
| `registry.ts:8502` | objectstack-ai#10054 | `record-highlights-field-icon-removed` |
| `registry.ts:8759` | objectstack-ai#11027 | `page-component-responsive-removed` |
| `registry.ts:8860` | objectstack-ai#11805 | `object-grid-default-sort-removed` |
| `registry.ts:9047` | objectstack-ai#21445 | `object-grid-resizable-columns-removed`
|
| `registry.ts:9250` | objectstack-ai#17260 | `object-kanban-quick-add-removed` |
| `registry.ts:9634` | objectstack-ai#12497 | `permission-allow-restore-purge-removed`
|
| `registry.ts:9637` | objectstack-ai#1883 | `permission-allow-restore-purge-removed`
|
| `registry.ts:9954` | objectstack-ai#6837 | `field-reference-to-alias` |
| `registry.ts:10372` | objectstack-ai#14478 | `hook-timeout-to-timeout-ms` |
| `registry.ts:10413` | objectstack-ai#14478 | `job-timeout-to-timeout-ms` |
| `registry.ts:11017` | objectstack-ai#14478 |
`api-endpoint-cache-ttl-to-cache-ttl-seconds` |
| `registry.ts:11086` | objectstack-ai#14478 |
`dashboard-refresh-interval-to-refresh-interval-seconds` |
| `registry.ts:11447` | objectstack-ai#14478 |
`memory-persistence-auto-save-interval-to-ms` |
| `registry.ts:11671` | objectstack-ai#14478 | `turso-config-timeout-to-timeout-ms` |
| `registry.ts:11761` | objectstack-ai#17063 | `view-page-mount-removed` |
| `registry.ts:11866` | objectstack-ai#17053 | `list-view-sort-string-clause-to-array`
|
| `registry.ts:11868` | objectstack-ai#8221 | `list-view-sort-string-clause-to-array`
|
| `registry.ts:12366` | objectstack-ai#19054 |
`object-tenancy-organization-field-removed` |
| `registry.ts:12476` | objectstack-ai#20085 | `view-item-owner-hidden-removed` |
| `registry.ts:12620` | objectstack-ai#20230 | `view-overlay-owner-hidden-removed` |
| `registry.ts:13214` | objectstack-ai#17321 |
`page-component-filter-record-to-rule-array` |
| `registry.ts:13214` | objectstack-ai#6206 |
`page-component-filter-record-to-rule-array` |
| `registry.ts:13463` | objectstack-ai#20161 | `report-joined-chart-removed` |
| `registry.ts:13728` | objectstack-ai#20221 | `form-layout-inline-grid-to-vertical` |
| `registry.ts:13884` | objectstack-ai#19992 | `currency-config-precision-removed` |
| `registry.ts:13988` | objectstack-ai#20321 | `permission-rls-tags-removed` |
| `registry.ts:14128` | objectstack-ai#15429 | `flow-decision-mode-inclusive-explicit`
|

## Projections (A2)

Neither generator projects a toMajor-18 summary at this base.
`build-spec-changes.ts` and `build-upgrade-guide.ts` both loop `major`
from `MIGRATION_SUPPORT_FLOOR + 1` to `PROTOCOL_MAJOR`, which are 16 and
17 here (`PROTOCOL_VERSION = '17.0.0'`), so `spec-changes.json` carries
one `perMajor` record (16 → 17) and the guide one "Protocol 16 → 17"
table. `check:generated` reads all 15 artifacts up to date on this head
with no regeneration, so no generated file is in the diff. Both
projections will pick these summaries up when protocol 18 ships.

## Delivered: each site, the decision read, the new words (A3)

Every cited card was read through REST with all its comments (30
objectstack cards, objectui#3951, objectstack-ai#6206, objectstack-ai#6837, objectstack-ai#8221). The record
column names the comment the decision was read from. Where a summary
already said why, the citation is dropped and the sentence kept; where
an `(#N, ADR-0049 — …)` opener cited both, the card number goes and the
ADR stays, as stage 4 did. In the
`cube-join-sql-and-relationship-removed` row, `ALIAS` stands for the
angle-bracket placeholder in the source.

| conversion (head line) | cited | decision as read (record) | summary
now reads |
|---|---|---|---|
| `field-malformed-scale-precision-removed` (:6550) | objectstack-ai#8321 | refuse a
malformed scale/precision at the producer (z.number().int().min(0)); a
stored malformed value takes the D2 strip so the row stays loadable;
citation dropped, the sentence already said it (body + ACCEPT
5296942541) | malformed field 'scale'/'precision' declarations
(non-integer or negative) are removed — they were silently unenforced;
the schema now refuses them at authoring |
| `record-chatter-position-vocabulary` (:6658) | objectstack-ai#8762 | the row's
vocabulary converges on the renderer's bottom/right/left: one
vocabulary, no mapping layer; the three old spellings take a conversion
(ruling 5299771841) | record:chatter / record:discussion 'position'
respelled to the renderer's vocabulary — 'sidebar' → 'right', 'inline' →
'bottom', 'drawer' → 'right' (one vocabulary, the renderer's, rather
than a mapping layer between two: the renderer compares only
bottom/right/left, and the old set fell through every branch) |
| `element-input-target-variable-removed` (:6778) | objectstack-ai#9198 | ADR-0049
enforce-or-remove: verdict dead (a declarative hint with zero readers),
retired with tombstones and a D2 conversion (ACCEPT 5311252358 (PR body
verdict)) | text-input/record-picker component prop 'targetVariable'
removed (retired under ADR-0049 enforce-or-remove as a declarative hint
nothing read; the live binding resolves from the page variable whose
`source` names the component id) |
| `element-filter-removed` (:7025) | objectstack-ai#9220 | dead at ELEMENT grain (no
renderer anywhere; Studio excludes it from the palette), so the whole
element retires under ADR-0049, not key by key (verdict 5312176877 +
ACCEPT 5312709553) | the whole 'element:filter' element retired
(ADR-0049 enforce-or-remove at element grain, not key by key — no
renderer for it ever shipped in any repo, so every key was a capability
claim nothing kept; list surfaces own their filtering via a view's
userFilters / the list filter builder). All six props are stripped; the
bare node the conversion leaves is refused by name at the parse, with
the prescription to delete the component |
| `element-form-removed` (:7179) | objectstack-ai#9249 | dead at element grain, the
objectstack-ai#9220 precedent: the whole element retires; the palette already names
object-form as the replacement (dev report 5384430470 (verdict re-taken
in the PR body)) | the whole 'element:form' element retired (ADR-0049
enforce-or-remove at element grain, not key by key — no renderer for it
ever shipped in any repo, so every key was a capability claim nothing
kept; use the object-bound 'object-form' block instead — rendered and
designer-publishable). All six props are stripped; the bare node the
conversion leaves is refused by name at the parse, with the prescription
to delete the component |
| `translation-per-app-settings-removed` (:7358) | objectstack-ai#15178, objectstack-ai#19620 |
objectstack-ai#15178: the bundle type splits, the platform bundle keeps `settings`, a
per-app bundle refuses it (settings is a platform key). objectstack-ai#19620 ruling B:
`settings` leaves the translation item too, because the file door and
the item door are two authoring surfaces of one app metadata type and
accept one shape (ruling 5653315643 (objectstack-ai#15178); ruling 5770445203
(objectstack-ai#19620)) | translation group 'settings' removed from both
application-authored faces, the per-app bundle entry and the registered
translation item: settings copy belongs to the platform, and the two
authoring doors of one application translation type accept one shape. It
is keyed by SettingsManifest.namespace and only platform code declares a
manifest. A per-app bundle entry could only fill gaps the platform's own
bundle left in the one merged served tree, and was overwritten wherever
both defined the key; a stored item OVERRODE the platform copy, because
the runtime-authored layer is read over the shipped bundles. Overrides
now give way to the platform copy, gaps fall back to the manifest
literal, and the group stays on the PLATFORM bundle,
PlatformTranslationData |
| `translation-component-submit-label-removed` (:7613) | objectstack-ai#9249 |
`element:form` retired whole because no renderer for it ever shipped
(dead at element grain), which left `submitLabel` with no carrier (dev
report 5384430470) | translation component-copy key 'submitLabel'
removed (retired rather than re-anchored — its only declared carrier,
'element:form', retired whole because no renderer for it ever shipped,
so the resolver no longer overlays it and a stored string was read by
nothing; the live form surface's submit copy is 'object-form''s
'submitText', localized at its own authoring site, and re-anchoring the
key there would only have added a second place to translate one word) |
| `field-column-lists-canonicalized` (:7787) | objectui#3951, objectstack-ai#9227 |
objectui#3951: the published spec spelling `name` wins and the grid
reader is fixed to read it. objectstack-ai#9227: `inlineColumns` gets a strict
name-keyed element schema (an unknown key is a named rejection at
publish, not a blank cell); `relatedListColumns`, checked in the same
pass, takes field-name strings (ruling 5236150020 (objectui#3951);
ruling 5315735776 + ACCEPT 5317488979 (objectstack-ai#9227)) | inline-grid column
entries respelled 'field' → 'name' (the declared spelling wins, and the
grid renderer now reads 'name' too) and related-list column objects
folded to their child field-name string (both lists were z.any(), so a
mis-keyed column published clean and rendered blank cells; inline
columns now take a strict name-keyed shape and related-list columns
plain field names, so a mis-keyed column is refused at publish) |
| `metric-filters-removed` (:7916) | objectstack-ai#10414 | the remove leg of
enforce-or-remove: zero consumers (measured with a positive control) and
a raw-SQL carrier; retire per the playbook; citation dropped, the
sentence already said it (triage grading 5363699539) | cube metric key
'filters' removed (ADR-0049 — no strategy ever read it: the authored
raw-SQL condition was parsed and dropped, and the query returned the
unfiltered aggregate. Filter at query time with `where`, or use an
ADR-0021 dataset measure's structured `filter`; a metric's own `sql` is
a column reference) |
| `cube-sub-day-granularities-removed` (:8111) | objectstack-ai#17296 | each of
second/minute/hour is residue and removed: no layer outside the enum
names them and `queryDateGranularity` cannot advertise them; ADR-0049
prefers removal with no committed roadmap; citation dropped (dev report
5648182389 + landing 5648923185) | cube dimension granularities 'second'
/ 'minute' / 'hour' removed (ADR-0049 — no backend bucketed them and
none could advertise them: `supports.queryDateGranularity` is a record
over `DateGranularity`, which declares day, week, month, quarter, year.
Offer the coarsest interval that still answers the question) |
| `cube-join-sql-and-relationship-removed` (:8244) | objectstack-ai#18612 | retire
`sql` and `relationship` from CubeJoin: the join is derived from the FK
relationship and no author-supplied ON clause executes; the addendum
adds the D2 strip for persisted artifacts; citation dropped, the
sentence already said it (ruling 5725370783 + addendum 5727426171) |
cube join keys 'sql' and 'relationship' removed (ADR-0049 — neither was
ever read: both strategies synthesise the ON clause as a foreign-key
equality, so an authored join condition was REPLACED under a 200 and a
declared cardinality changed no SQL. Keep `joins.ALIAS.name` alone; the
record KEY is the foreign-key field on the base object) |
| `record-highlights-field-icon-removed` (:8509) | objectstack-ai#10054 | option A:
measured dead (zero read points, not designer-publishable), so it
retires under the ADR-0087 flow; citation dropped (ruling 5364978909) |
record:highlights highlight-field key 'icon' removed (ADR-0049 — no
render path: the highlight chip has no icon slot, the register hook
carries field names only, and the Studio designer publishes the field
list as plain strings, so an authored icon was accepted and drawn by
nothing) |
| `page-component-responsive-removed` (:8766) | objectstack-ai#11027 | ruling B:
retire `page.components[].responsive` (ADR-0049, wired into no renderer)
and repair the texts that redirected authors to it (ruling 5380752244) |
page component key 'responsive' removed (ADR-0049 enforce-or-remove — no
renderer ever applied per-component breakpoint layout overrides, and the
shared ResponsiveConfig shape leaves with its last carrier; use
responsiveStyles (ADR-0065) for breakpoint behaviour that IS applied) |
| `object-grid-default-sort-removed` (:8868) | objectstack-ai#11805 | retire
object-grid `defaultSort` (the strict route per the playbook),
completing the objectui-side direction ruling at the producer (ruling
5404972152) | object-grid component prop 'defaultSort' removed (retired
under ADR-0049 enforce-or-remove as the legacy single-sort second
spelling of 'sort', read only when 'sort' was absent; the pair moves to
sort: [{ field, order }], the array shape every read path honours) |
| `object-grid-resizable-columns-removed` (:9055) | objectstack-ai#21445 | `resizable`
is canonical and `resizableColumns` retires now as a tombstone naming
it: zero writers, so no window (immediate retirement) (triage direction
5958164933) | object-grid component prop 'resizableColumns' removed (the
legacy second spelling of 'resizable', read only when 'resizable' was
absent, retires at once so 'resizable' is the one spelling; the value
moves to 'resizable' when that is absent, and is deleted when it is
present) |
| `object-kanban-quick-add-removed` (:9258) | objectstack-ai#17260 | option B:
`quickAdd` leaves `object-kanban` (accepted and dropped there); this
repo carries the tombstone half (card body (the objectui ruling it
executes, option B) + triage 5620331176) | object-kanban component prop
'quickAdd' removed (retired from the board under ADR-0049
enforce-or-remove — the affordance is gated on a host-supplied
'onQuickAdd' function no producer puts on an object-kanban node, so the
key was accepted and dropped; delete the key — object-kanban offers no
quick-add control) |
| `permission-allow-restore-purge-removed` (:9643) | objectstack-ai#12497, objectstack-ai#1883 |
option B: retire `allowRestore` / `allowPurge`, which gate operations
that do not exist; objectstack-ai#1883 stays open as the M2 anchor, where
undelete/purge ship as feature + RBAC in one batch and the keys return
with it (card body (objectstack-ai#12497); ruling 5421209848 (objectstack-ai#1883)) |
object-permission keys 'allowRestore' and 'allowPurge' removed (ADR-0049
— the `restore`/`purge` operations they claimed to gate have never
existed, so granting the bits delivered nothing; dispatched destructive
lifecycle verbs stay denied fail-closed. The keys return with the M2
lifecycle initiative, which builds undelete and purge together with the
permission bits that gate them) |
| `field-reference-to-alias` (:9962) | objectui#6837 | ruling C:
protocol normalisation belongs to the server and the frontend only
executes the protocol; half 1 (this repo) guarantees the serve path
carries only `reference`, half 2 deletes objectui's legacy fallback arms
(ruling 5475017957 + half-1 pointer 5475055291) | field key
'reference_to' → 'reference' (the legacy objectql runtime dialect for a
lookup/master_detail target; normalising to the protocol is the server's
job and the renderer only executes the protocol, so stored rows must
serve the canonical spelling before objectui deletes its `reference ??
reference_to` fallback arms) |
| `hook-timeout-to-timeout-ms` (:10383) | objectstack-ai#14478 | ruling B: a
duration-shaped number key carries its unit in its name (or a
unit-carrying value), every existing offender renamed under an ADR-0087
conversion, no grandfathered baseline (ruling 5518649320 + population
ruling 5548763981) | hook key 'timeout' → 'timeoutMs' (a duration key
carries its unit in its name, and this one's unit lived only in the
description; the value, milliseconds, is unchanged) |
| `job-timeout-to-timeout-ms` (:10424) | objectstack-ai#14478 | as above (as above) |
job key 'timeout' → 'timeoutMs' (a duration key carries its unit in its
name, and this one's unit lived only in the description; the value,
milliseconds, is unchanged) |
| `api-endpoint-cache-ttl-to-cache-ttl-seconds` (:11028) | objectstack-ai#14478 | as
above (as above) | api endpoint key 'cacheTtl' → 'cacheTtlSeconds' (a
duration key carries its unit in its name, and this one's unit lived
only in the description; the value, seconds, is unchanged, and the key
stays GET-only) |
| `dashboard-refresh-interval-to-refresh-interval-seconds` (:11097) |
objectstack-ai#14478 | as above (as above) | dashboard key 'refreshInterval' →
'refreshIntervalSeconds' (a duration key carries its unit in its name,
and this one's unit lived only in the description; the value, seconds,
is unchanged) |
| `memory-persistence-auto-save-interval-to-ms` (:11458) | objectstack-ai#14478 | as
above (as above) | memory datasource key
'config.persistence.autoSaveInterval' → 'autoSaveIntervalMs', on both
the file and auto arms (a duration key carries its unit in its name, and
this one's unit lived only in the description; the value, milliseconds,
is unchanged) |
| `turso-config-timeout-to-timeout-ms` (:11682) | objectstack-ai#14478 | as above (as
above) | turso datasource key 'config.timeout' → 'config.timeoutMs' (a
duration key carries its unit in its name, and this one's unit lived
only in the description and a .meta() title no parse reads; the value,
milliseconds, is unchanged) |
| `view-page-mount-removed` (:11772) | objectstack-ai#17063 | the maintainer chose to
retire (「撤」) over finishing the objectui render half or parking it: the
`page` member and its mount leave the spec under enforce-or-remove (card
body (the maintainer ruling it records)) | list-view type 'page' and its
`pageName` binding removed (retired rather than finished: the delegating
render half was never built, so a page view fell through to the grid
branch and drew an empty table; ADR-0049 enforce-or-remove) |
| `list-view-sort-string-clause-to-array` (:11877) | objectstack-ai#17053,
objectui#8221 | objectui#8221 option B: the legacy string `sort` is
retired, one spelling platform-wide, the array. objectstack-ai#17053: the spec slot
that produces those documents stops accepting the string objectui now
refuses (triage 5620223775 (objectstack-ai#17053); ruling 5567944420 (objectui#8221))
| the bare string list-view `sort` clause becomes the `{ field, order
}[]` array (one sort orthography platform-wide, the array: objectui
already refuses the string, so the schema stops minting documents its
own consumer refuses) |
| `object-tenancy-organization-field-removed` (:12377) | objectstack-ai#19054 | take
`organizationField` off the authorable surface; its one real use stays a
platform-internal fact; citation dropped, the sentence already said it
(card body (the maintainer ruling it records)) | object
`tenancy.organizationField` removed (ADR-0049 — the stamp-only column
declaration was authorable by every application and declared exactly
once in the whole protocol, on the platform's own credential table; the
divergence moves to a platform-internal table in
@objectstack/metadata-core and stops being a knob) |
| `view-item-owner-hidden-removed` (:12487) | objectstack-ai#20085 | retire both keys
(ADR-0049 enforce-or-remove, zero pull) via the retirement playbook;
citation dropped (triage direction 5826969296) | view item keys
'owner'/'hidden' removed (ADR-0049 — declared on the view item record
and stored verbatim, read by nothing: no view switcher ever filtered on
`hidden`, and no per-user scope ever read `owner`, so a view marked as
one user's was listed for everyone) |
| `view-overlay-owner-hidden-removed` (:12631) | objectstack-ai#20230 | follow
objectstack-ai#20085's disposition for the same key pair on the overlay door: the same
retirement (triage direction 5856621469) | flattened view overlay keys
'owner'/'hidden' removed (ADR-0049 — the view item's pair on the overlay
door, retired the same way: declared, accepted by the write door and
stored verbatim, read by nothing, so a `hidden: true` overlay hid no
view and an `owner` scoped none) |
| `page-component-filter-record-to-rule-array` (:13220) | objectui#6206,
objectstack-ai#17321 | objectui#6206 option B: one filter orthography platform-wide,
the rule array. objectstack-ai#17321 ruling B: a partial D2 conversion of what maps
losslessly; combinator-carrying rows pass through untouched and are
named as a TODO (flattening would silently change what a page selects)
(ruling 5406409590 (objectui#6206); ruling 5644018752 (objectstack-ai#17321)) | a
record-form or single-level AST filter at a converged rule-array door
becomes the `[{ field, operator, value }]` rule array wherever the
mapping is lossless (flat keys → `equals` rules, `{ $op: v }` → the
mapped operator, AST comparisons → one rule each); a filter carrying
`$and` / `$or` / `$not` or any part with no lossless rule spelling is
left exactly as stored — reported as a TODO, which `os migrate meta
--stored` lists — and is not the form its door declares (one filter
orthography platform-wide, the rule array; the migration converts only
what maps losslessly and names the rest, because flattening a combinator
would silently change what a page selects) |
| `report-joined-chart-removed` (:13477) | objectstack-ai#20161 | retire, not build
block charts: the joined arm refuses a container chart, the block key
goes, a non-joined report keeps its live chart; citation dropped (triage
direction 5852548444) | a joined report's 'chart' removed from its
blocks and refused on the container (ADR-0049 enforce-or-remove: the
joined renderer draws each block as a table and never read either, so
the chart parsed and nothing was plotted; a non-joined report keeps its
live 'chart') |
| `form-layout-inline-grid-to-vertical` (:13742) | objectstack-ai#20221 | retire the
`inline` / `grid` arms: multi-column already exists as `columns` and
`inline` is not a record-form layout; citation dropped, the sentence
already said it (triage direction 5855767378) | form 'layout' arms
'inline' and 'grid' rewritten to 'vertical' (ADR-0049 — no renderer ever
gave either a behaviour of its own: every form presentation folded both
to 'vertical'. Multi-column is 'columns', honoured under either layout,
and is left untouched) |
| `currency-config-precision-removed` (:13898) | objectstack-ai#19992 | remove
`currencyConfig.precision`: a currency's decimal places are the
currency's, not a setting; citation dropped, the sentence already said
it (triage 5817146460 (ruling 乙 on objectstack-ai#19910 it executes)) | currency field
key 'currencyConfig.precision' removed (ADR-0049 — no renderer or
runtime ever read it: an amount's decimal places are its currency's ISO
4217 minor unit, derived from the currency itself. Its ISO 4217
contradiction check and the default `2` baked into parse output went
with it; the field-level `precision` is a total digit count and is
untouched) |
| `permission-rls-tags-removed` (:14002) | objectstack-ai#20321 | RETIRE by the
maintainer's criterion (no mainstream platform has the capability);
citation dropped, the sentence already said it (triage verdict
5860425529) | RLS-policy key 'tags' removed (ADR-0049 — nothing ever
read a policy's tags and no mainstream platform tags a row-level policy;
dropping it changes no access decision) |
| `flow-decision-mode-inclusive-explicit` (:14141) | objectstack-ai#15429 | align with
mainstream engines: an edge-branched decision is exclusive (first
match), and taking every true edge must be declared (`mode:
'inclusive'`); the migration writes it explicitly for existing nodes so
authored behaviour is unchanged (ruling C narrows that promise to
sources and artifacts) (ruling 5793803317; ruling C 5863827385) |
edge-branched decision with two or more conditioned out-edges and no
`mode`: `mode: 'inclusive'` written explicitly (the traversal became
exclusive, first match in declaration order, as mainstream engines treat
a decision, and taking every true edge must now be declared; the key
keeps the every-true-edge behaviour those nodes had, and the author
deletes it where the branches partition) |

No site was left in place as unclear; `open_questions` is empty.

## Text only (A4)

Stage 3's AST-skeleton plus string-text tool (`skeleton.cjs`, TypeScript
6.0.3; stage 4's copy with only its TypeScript load path changed to this
worktree, md5 `3b10ec8a7e6284f19def54d35f001698` →
`32b4630d7d0a532ee26239319269fe91`). Leg 1 compares an AST skeleton with
every string's text masked (a `+` chain of string operands reads as one
string, so re-wrapping is invisible); leg 2 compares the text of every
string group, requiring each changed group to carry a tracker id before
and none after, and every other group byte-identical.

- `registry.ts`, base `e901c27449` vs the committed copy at
`9c1d040145`: **1 of 1 SAME**, exit 0 — 62656 tokens both sides, 4955
string groups, 35 changed, every changed group carried an id before and
carries none after; parse diagnostics 0/0.
- Controls on scratch copies of the head file, each mutation counted on
disk first (anchor hits 1, replacement present 1, anchor left 0):
`renameFlowConfigAliases` renamed → DIFF exit 1; `!==` flipped to `===`
→ DIFF exit 1; one summary re-split into two `+` operands → SAME, 0
groups changed, exit 0; the `hook.timeout` surface string (never carried
an id) changed → skeleton SAME, text leg VIOLATION, exit 1. No repo file
was mutated for the controls.
- The edits were applied by a script whose every anchor was asserted to
hit exactly once, inside its own conversion's summary line span, and
verified on disk after the write (36 anchors over 35 conversions). The
conversion-table extractor reads 35 summaries changed, `toMajor` /
`surface` / declaration unchanged on all 127, and 0 ids left in any
summary.

## Pins and quotes (A6)

No test asserts a summary, so no pin moves: nothing under `*.test.*`
reads `.summary` off a conversion (the only summary reads are
`spec-changes.ts` and `build-upgrade-guide.ts`), and every removed
id-bearing fragment was searched across the repo. The hits are other
files' own prose with their own citations (CHANGELOGs,
`migrations/registry.ts` rationale, docblocks, `liveness/*.json` notes,
test titles such as `permission.test.ts:278`, docs prose in
`content/docs/permissions/*.mdx`), not quotes of a summary.
`content/docs/**`: no quote of a changed summary. `skills/**`: none.

## Verification

All builds and tests through `scripts/pm/os-verify-lock.sh` (slot
`issue-20749-s5`), each `VERDICT command-exit 0`:

- `pnpm --filter @objectstack/spec build`, then `check:generated` on the
merged head `9a35e049e5`: "✓ All 15 generated artifacts are up to date".
- The package `test` script (`vitest run --project local
--maxWorkers=2`) on `9a35e049e5`: "Test Files 605 passed (605) / Tests
17904 passed | 1 todo (17905)".
- `test:repo`: the 15 repo-project files that read the conversion
registry, `spec-changes` or the guide, "Test Files 15 passed (15) /
Tests 221 passed (221)". NOT MEASURED:
`scripts/build-schemas-check-mode.test.ts` (88 cases at about 7 s each,
over the foreground cap; it reads only conversion surfaces, which the
proof shows unchanged) and the remaining repo-project files; reason:
wall clock on a shared box. CI runs them.
- `pnpm --filter @objectstack/spec run typecheck` on `9a35e049e5`: exit
0; "check:test-typecheck: OK — 52 file(s) / 246 error(s) / 135 pinned
signature(s) held".
- `pnpm turbo run build --concurrency=2 --filter=./packages/*
--filter=./packages/*/*`: "Tasks: 71 successful, 71 total", for the
gates that read built packages.
- Gates: `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` (no paths) at `9a35e049e5`
derives 79 commands; each ran with its exit code written to disk before
any pipe. Three first exited 3 (PREREQUISITE NOT MET:
`check:doc-formula-expressions`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure` need built packages) and exit 0 after the
build; the dist-reading gates were re-run after it too. `--ran`: "✓
dispatch-gates --ran: 79 derived famil(ies) accounted for — 79 run, 0
NOT-MEASURED".
- `pnpm check:doc-authoring` (self-test and run): exit 0, "17323
customer-facing string(s) across 1250 spec sources clean"; the
sibling-package ledger holds its baseline (`packages/spec` sits outside
it, so no ledger change). `pnpm check:nul-bytes`: exit 0, "no raw ASCII
control bytes".
- Changeset gates with this body as the `--event` payload:
`check-changeset-no-major.mjs --base origin/main --event` exit 0 ("✓
LEVEL AXIS: this PR declares clause-② `no`", declaration line `Clause-②:
no`); `check-partof-closing-keyword.mjs` with `PR_BODY` exit 0 ("no
Part-of/closing-keyword contradiction");
`check-adr-0087-registration.mjs --base origin/main` exit 0 ("adds no
declared-breaking changeset (1 non-breaking changeset(s) seen)");
`check-empty-changeset.mjs --base origin/main` exit 0;
`check-changeset-fixed.mjs` exit 0.
- ESLint, a proven narrowing: `eslint --no-inline-config --format json`
over the one changed TS file reads 1 file, 0 errors, 0 warnings; the
population is read from ESLint's own config (`calculateConfigForFile`
resolves it, `isPathIgnored` false); invariance: `eslint.config.mjs`
enables no type-aware linting (`parserOptions.project` /
`projectService` null for this file, its header at :327-328 says so), so
a string-text edit cannot move an untouched file's verdict. Repo-wide
`pnpm lint` is CI's.

## Acceptance notes

- `origin/main` was merged once (`9a35e049e5`, five commits: objectstack-ai#21539,
objectstack-ai#21473, objectstack-ai#21554, objectstack-ai#21556, objectstack-ai#21557; spec moved only in
`contracts/approval-service.ts` TSDoc); spec rebuilt, `check:generated`,
the spec test project, typecheck and the gate union re-ran on the merged
head. No os-regen deferral was recorded.
- Hot file: no open PR touches `conversions/registry.ts`,
`spec-changes.json` or the upgrade guide (all nine open PRs' file lists
read just before opening this one).
- Census after this PR (stage 3's instrument at `9a35e049e5`): non-test
160 → 125 messages, 358 → 318 ids; class (b) is empty. Left for the
later stages: class (c) conformance-case notes 58 messages / 68 ids (the
`objectstack-ai#5322` selector and the `objectstack-ai#8934` name pin move with their tests), class
(f) internal registry rationale 17 / 33, the test strings 1804 / 1920 in
425 files; `migrations/registry.ts` 50 / 217 stays with objectstack-ai#20234's stage
11. Word-form hits (an id spelled after "PR", "issue" and the like) stay
6, all outside this diff.
- Excluded, untouched: `migrations/registry.ts`, comments anywhere,
classes (c) and (f), test strings, the `.mjs` gate scripts. No gate is
added or loosened.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants