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