Skip to content

fix(core): the JSON-column refusal reads true on every face and reaches a REST caller whole - #21213

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-21067-json-refusal-under-bound
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-21067-json-refusal-under-bound

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #21067

Clause-②: no

What this changes

jsonColumnOperatorRefusalText (packages/core/src/utils/json-column-operator-refusal.ts) is the one builder of the INVALID_FILTER / 400 refusal a filter gets for a scalar comparison or text operator on a multi-value or JSON field. It is rewritten once, for both of the card's reasons together (seat 2's carried note folded in: one rewrite, not two passes):

  1. Under the REST bound. The withheld message was 748 characters. The /data door cuts a 4xx message of 500 or more to 499 plus an ellipsis, so the wire ended …Refused rather than compiled because the answ…, and no caller read the sentence saying the field and the operator were withheld. It is now 486 characters, one constant text.
  2. True on every face. The old reason named driver-sql's storage ("a field this driver stores as a JSON TEXT column") and the two wrong answers SQL used to give. Both are untrue on the engine's per-aggregation filter and on driver-memory, which print the same text. The reason is now the field's declaration: it aims a scalar comparison or text operator at a multi-value or JSON field, which it cannot test for one member.
  3. The null comparand's repair (added at the PM's note on this card). The refused set also catches { f: null }, $eq: null and $ne: null, which ask whether the field has a value; $contains cannot express that. One constant clause, with no branch on the comparand, names the presence spellings: For no value, use "$null" or "$empty". Both answer on a multi-value or JSON field on every face (they are outside the refused set), and the REST pin now proves $null: true, $null: false and $empty: true answer on both the where and the per-aggregation faces.

The new message, in full:

A constraint in this filter WAS NOT APPLIED: it aims a scalar comparison or text operator at a multi-value or JSON field, which it cannot test for one member. Use "$contains" for membership ({ "FIELD": { "$contains": "a" } }), or an $or of "$contains" for any-of ({ "$or": [{ "FIELD": { "$contains": "a" } }, { "FIELD": { "$contains": "b" } }] }). For no value, use "$null" or "$empty". The field and the operator are withheld from the message; the full diagnostic is in the server log.

The diagnostic (server log, and the author-disclosed wire text, see below) shares that reason, names the operator in it (it aims "$in", a scalar comparison or text operator, at …; the bare spelling's operator as =, as before), names the field, and spells the remedy with the field's name, presence clause included. It drops the same storage and SQL history. One refusalReason() and one containsRemedy() (which ends in PRESENCE_REMEDY) serve both texts, so the two cannot drift. The SQL mechanism and the measured wrong answers stay in the builder's docblock.

Unchanged: code, status, the refused operator set (JSON_COLUMN_INCOMPATIBLE_OPERATORS, 27 spellings), the $contains remedy, no new export, no new code.

Faces. Re-derived at cb45469e by name and by text: every face that prints this sentence calls the builder, and none builds its own copy. They are driver-sql jsonColumnOperatorError (sql-driver.ts), objectql having-filter.ts (the judgment and the per-row backstop, three call sites), and driver-memory jsonStoredFieldOperatorError (filter-refusal.ts). No face file changes except one stale docblock in sql-driver.ts. driver-turso's remote-transport.ts and driver-mongodb share phrases with other refusals, not this sentence. No docs page or skill quotes it (content/docs/**, skills/**, docs/**, apps/docs/**, by builder name and by seven distinctive phrases: zero hits).

Measured, before and after

reading BASE cb45469e this PR
withheld message, constant 748 chars 486 chars
its wire error, SQLite and live PostgreSQL 16.14, mapDataError 499 plus an ellipsis, ending Refused rather than compiled because the answ… the whole message
author-disclosed diagnostic, field owners, $in 643 chars 402 chars
that, at POST /api/v1/data/:object/query through the real SecurityPlugin 499 plus an ellipsis, ending Refused rather than compiled beca… the whole diagnostic (measured at 377 before the presence clause; the 402 text is pinned whole through mapDataError)
field-name length from which the diagnostic is cut on the wire every length 27 characters (the presence clause goes first)
field-name length from which the cut takes the any-of example 10 characters 37 characters

A dispatch hypothesis this falsified (H5)

The dispatch read the diagnostic as server-log text the envelope never bounds. Measured, it is also a wire text. driver-sql's 'author' provenance arm (resolveWithheldFilterRefusal) swaps the refusal for the diagnostic when the predicate is the caller's own, and plugin-security marks the caller's verbatim where 'author' (security-plugin.ts, the markFilterSubtreeProvenance(callerWhere, 'author') call). Through POST /api/v1/data/:object/query with a real SecurityPlugin, a member caller sending { owners: { $in: ['u1'] } } received the diagnostic: cut to 500 at BASE, whole (377) at caf0e3c7, before the presence clause raised it to 402. That was a one-off measurement file, run twice and not committed; the BASE leg rebuilt core's dist/ with the old text and restored it, with dist preflight proofs both ways.

The diagnostic was in the rewrite already, because its reason clause was the same untrue driver-sql mechanism seat 2's note names. All four conditions of the bounded in-place fix hold: same defect class (a refusal cut at the envelope), a mechanical fix in the shape triage pinned, the claimed file, and the same gate families. So it is fixed here, not filed. Its content is kept: field and operator named, remedy with the field's name.

Pins

New, compared with the builder's output and the bound's own function (truncateClientMessage / mapDataError from @objectstack/types), never with a copied sentence or a retyped 500. CLIENT_MESSAGE_MAX itself is module-private in packages/types/src/data-error-classification.ts, so it is not imported; truncateClientMessage is already exported there for rest, and nothing new is exported. A sibling that rewords nothing and only calls the builder cannot flip these.

  • packages/core/src/utils/json-column-operator-refusal.test.ts: re-captured hashes and lengths. For every refused spelling, truncateClientMessage(message) returns it unchanged. The message carries the one-member, any-of and no-value remedy and ends with the withheld sentence. Neither text names a storage form or a backend's wrong answer. The diagnostic gives the same reason with the operator named.
  • packages/drivers/driver-sql/src/sql-driver-json-column-refusal-wire-bound.test.ts (new, DIALECT_CELLS): for 14 operators plus bare equality, on a multi-value lookup, a tags field and a json field, mapDataError(err).body.error equals the shared message (unmarked, carrying the presence clause) and the shared diagnostic (author-marked), with the remedy spelling. SQLite always; PostgreSQL and MySQL in CI's Temporal Conformance (live PG + MySQL) job, which runs this package's whole suite with both URLs set.
  • packages/rest/src/aggregation-filter-json-column-refusal.test.ts: through POST /api/v1/data/:object/query, the where twin's body equals the builder's message whole, not just the per-aggregation face's (toBe, plus the any-of remedy, the presence clause and the withheld sentence), on the null-comparand rows too; three new controls show $null: true, $null: false and $empty: true answer with equal counts on both faces.

Flipped, each to the new substance:

  • sql-driver-json-column-operator-refusal.test.ts: 'JSON TEXT column' becomes the operator-named reason, and the remedy is asserted with the field's own name.
  • sql-driver-target-field-provenance.test.ts: the class fragment that survives redaction, 'JSON TEXT column', becomes 'at a multi-value or JSON field'.
  • sql-driver-json-column-refusal-shared-text.test.ts: the docblock's "did not change by one byte" claim.

ADR-0112 code plus status assertions are untouched everywhere.

Reverse verification

At fefb6e1f, BASE's builder was written to disk (tree only, never staged) and its landing checked by grep: old text 1, new 0. Then:

  • sql-driver-json-column-refusal-wire-bound.test.ts (SQLite plus live PostgreSQL 16.14): 90 failed (45 per cell), 1 skipped (MySQL). The red direction, as expected.
  • The core pin: 8 failed, 4 passed.

Restored with git checkout HEAD -- PATH under an EXIT INT TERM trap. The blob is 2f17d7f3, equal to HEAD's, and git diff HEAD is empty.

Verification (on d352319a, the final head: the branch, one merge of origin/main 0d421041, which touches none of these packages, and the presence-clause commit)

The targeted files below ran on d352319a, with OS_TEST_POSTGRES_URL set to a private PostgreSQL 16.14:

  • core: 12 passed
  • driver-sql, the five JSON-column files: 360 passed, 2 skipped (the MySQL cells)
  • rest aggregation-filter-json-column-refusal: 136 passed, 68 skipped (MySQL)
  • driver-memory memory-20444-* and memory-21066-*: 125 passed
  • objectql engine-aggregate-filter-json-column-refusal, engine-aggregate-filter, engine-aggregate-filter-array-membership and engine-cascade-delete-multivalue-probe: 263 passed

Full suites, on the pre-merge commits (before the presence clause; that commit touches only the builder, its unit pin and the two wire pins above, which were re-run):

  • @objectstack/core local: 74 files, 2107 passed
  • @objectstack/driver-sql, SQLite: 208 files passed, 11 skipped; 3469 passed, 192 skipped
  • @objectstack/rest local: 255 files, 4834 passed, 301 skipped

Typecheck: core, driver-sql and rest all pass, and tsc --listFiles confirms the four touched driver-sql test files are in its program.

Gates (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, re-derived and re-run on d352319a): 65 derived, 65 exit 0, 0 NOT MEASURED. --ran with exit codes reconciles to 65 accounted, 0 unrun.

  • check:dual-build-cjs-loads measured this time: 105 require entries across 66 packages load. The first run on 8450f66b was exit 3 PREREQUISITE NOT MET, since a whole-repo build was missing then.
  • check:driver-conformance, before the first edit and after the last commit: 50 covered, 0 DEBT, 0 exempt; dialect axis 8 suites, 0 in the DIALECT ledger. The ledger did not move.

Lint, narrowed:

  1. The population comes from eslint's own config: --format json reports every listed file with 0 warnings, so none was ignored.
  2. On d352319a, the 8 .ts files of this diff were linted, with 0 errors and 0 warnings.
  3. eslint.config.mjs enables no type-aware linting (no parserOptions.project), so this diff cannot move the verdict on any untouched file.

The repo-wide pnpm lint is CI's to run.

Patch round 1 (89209290). Lint & Repo Gates was red at d352319a on pnpm check:live-db-isolation: packages/rest/src/aggregation-filter-json-column-refusal.test.ts:258 use names the literal "$null". The fix is test-only: the presence clause is asserted in two pieces. On 89209290:

  • pnpm check:live-db-isolation: exit 0, 34 live-server files scanned.
  • That test file: 136 passed, 68 skipped, with SQLite and live PostgreSQL 16.14 running and MySQL a named skip.
  • The 65 derived families re-run: 65 exit 0. check:dual-build-cjs-loads gave exit 3 PREREQUISITE NOT MET until another family built the missing dist/, then exit 0 on re-run. --ran reconciles to 65 run, 0 NOT MEASURED.
  • check:live-db-isolation is a declared WIDE-population family, so per-card derivation never names it.

Siblings that print this sentence (both landed before this PR; seat edit)

Both siblings this PR named as later landers merged first, so they are earlier landers now:

This branch is behind both. The merge queue rebuilds it onto main, so their pins run against this text in the merge group.

Acceptance notes

  • The diagnostic's boundary. It names the field four times, so its length grows with the name. It is whole on the wire up to 26-character field names (measured over $in, $startsWith and bare equality). From 27 characters the cut takes the presence clause first, and from 37 the any-of example; the one-member remedy and both names come before both. Field names declare no maximum length, so no text that repeats the name can be bounded. The withheld message, the card's subject, is constant and bounded.
  • A gate false positive, on the test's spelling. check:live-db-isolation's statement-head needle matches the verb USE followed by a quoted operand (STATEMENT in scripts/check-live-db-isolation.mjs). It read the assertion string 'For no value, use "$null" or "$empty".' at packages/rest/src/aggregation-filter-json-column-refusal.test.ts:258 as a MySQL USE naming a database "$null". Fixed in 89209290 by asserting the clause in two pieces; the gate and the product sentence are unchanged.
  • packages/objectql/src/engine-cascade-delete-multivalue-probe.test.ts keeps a test double whose refusal paraphrases the old wording ("is stored as a JSON TEXT column"). It asserts only code and status, so it is not a pin of this text. Left as is.
  • rest's own PostgreSQL and MySQL cells of aggregation-filter-json-column-refusal.test.ts are still provisioned by no CI job (its header says so). The PostgreSQL wire pin that CI does run is the new driver-sql file.
  • The local PostgreSQL leg ran against a private PostgreSQL 16.14 started for this run on a random port (UTC server, so driver-sql's temporal files were not run against it). It was stopped afterwards.

Generated by Claude Code

claude added 7 commits October 1, 2026 15:54
…under the REST bound

The withheld message named driver-sql's storage and its two wrong answers,
untrue where the engine's per-aggregation filter and driver-memory print it,
and ran to 748 characters, so the REST envelope cut it before the any-of
example ended and before the sentence saying the field and the operator were
withheld. One reason, true on every face, now serves both texts; the message
is 461 characters.

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
…lope on every dialect cell

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
…d the whole-message wire

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
…n, the bare spelling's as =

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
…nder the REST bound

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/drivers/driver-sql/src/sql-driver.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/drivers/driver-sql/src/sql-driver.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 862f12c0b9b36a6192f6ffe55f4052fa7dbaf34a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d30af816a70872f385ecf4c9c7dcee87a46a8945 — the merge of head 892092904e52f682c650166856a9d210ba240c39 into base 862f12c0b9b36a6192f6ffe55f4052fa7dbaf34a, 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 d30af816a70872f385ecf4c9c7dcee87a46a8945 && git checkout d30af816a70872f385ecf4c9c7dcee87a46a8945
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 862f12c0b9b36a6192f6ffe55f4052fa7dbaf34a 892092904e52f682c650166856a9d210ba240c39 && git checkout -B drift-repro 862f12c0b9b36a6192f6ffe55f4052fa7dbaf34a && git merge --no-ff 892092904e52f682c650166856a9d210ba240c39

node scripts/docs-audit/affected-docs.mjs --json 862f12c0b9b36a6192f6ffe55f4052fa7dbaf34a

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

…a null comparand needs

A null comparand ({ f: null }, $eq: null, $ne: null) on a multi-value or
JSON field is refused with the same text, and $contains cannot express a
no-value test. One constant clause names $null and $empty; the message is
486 characters, still whole under the REST bound.

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Lint & Repo Gates red at head d352319a6: check:live-db-isolation misreads a test literal as live DDL. Read at 2026-10-01T17:33Z. domain:engine#1 · session_017xfMoEjKUuSh2xYB8sCozp.

  • The verdict line: packages/rest/src/aggregation-filter-json-column-refusal.test.ts:258 use names the literal "$null". That line asserts the new presence clause, 'For no value, use "$null" or "$empty".'. The gate's statement-head needle (scripts/check-live-db-isolation.mjs, the create database|drop database|create schema|drop schema|use alternation followed by a quoted operand) reads use "$null" inside a string as a MySQL USE that names a database by literal.
  • It is this PR's, and it is the test's spelling, not the product text. The refusal's words are right; only how the test spells its expectation trips the gate. Step 🔗 Broken links detected in documentation #163 failed, so the 30 later steps of this job did not run (NOT MEASURED).
  • Next: a patch round on this branch by its original dev, at the seat's next free subagent slot. Line 258 asserts the presence spellings without the use "…" adjacency, or compares against the builder's output as the line above it already does. The gate is not edited, and the one false positive goes to the PR's Acceptance notes. The at-tier review then runs on the new head. This PR stays draft.

Generated by Claude Code

…ve-db-isolation USE needle

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 892092904e52f682c650166856a9d210ba240c39
Local-runs: none

Inputs: card #21067 (body and all six comments, the two os-dev-reports included), PR #21213 (body, nine-file list, net diff against main), and the check-runs on the head. Read-only: origin/main was read with git show / git grep; the lengths and hashes below are my own arithmetic over the template text as the diff spells it, not a run of repo code.

Check-runs on the head, read at 2026-10-01T18:04Z: all seven required contexts completed / success — Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. Every other check is success or skipped (path-filtered); the second Check Changeset run, in progress at my first reading, had completed success by this one. No governed surface in the file list; 444 changed lines; head repo equals base repo; the PR is draft, by design.

① Derived judgments

  • The words are true on every face — right. At origin/main four faces print the builder's output and none builds its own copy: driver-sql jsonColumnOperatorError (sql-driver.ts), objectql having-filter.ts (three call sites), driver-memory jsonStoredFieldOperatorError (filter-refusal.ts) and, since 862f12c0b, driver-turso RemoteTransport.jsonColumnOperator (remote-transport.ts). The new reason names the field's declaration (a multi-value or JSON field) and no storage form, so it holds where no JSON TEXT exists. The remedies hold on each face: $contains, $null and $empty are outside JSON_COLUMN_INCOMPATIBLE_OPERATORS, every gate judges the operator alone (assertOperatorAppliesToColumn, the memory gate, the engine's jsonStored backstop, the turso gate), and each face carries a $null arm and an $empty arm. The presence clause's premise is right too: a null comparand reaches the gate as = / $eq / $ne before any IS NULL lowering (the three gate positions in sql-driver.ts), and the REST family at origin/main already carries the implicit equality with null and $eq null rows the strengthened pin now holds to the whole message. The spec's $empty description (null or the empty list for multi-value types) matches the builder's docblock.
  • The bound holds — right. truncateClientMessage (packages/types/src/data-error-classification.ts) returns a message unchanged only when its length is under 500. The message is one constant text of 486 characters — SHA-256 f0a54a98…dfd36f7, the hash the core pin carries — for all 27 spellings and the bare one. The diagnostics for members are 406 / 416 / 418 ($in / $between / bare), again the pinned hashes. The field-name thresholds the docblock and the acceptance note state reproduce: whole on the wire up to 26 characters, the any-of example surviving to 36, both worst-cased over $startsWith / $icontains.
  • Code, status and the refused set unchanged — right. The diff edits only the two text expressions and adds three module-private helpers (refusalReason, containsRemedy, PRESENCE_REMEDY); JSON_COLUMN_INCOMPATIBLE_OPERATORS, withheldFilterError and every constructor are untouched. ADR-0112 code / status assertions stand everywhere.
  • No export widened — right. No export is added. truncateClientMessage and mapDataError are already exported from @objectstack/types at origin/main; markFilterSubtreeProvenance from @objectstack/spec/data; the dialect testkit's DIALECT_CELLS / declareDialectCell / LIVE_CELL_TIMEOUT_MS exist. Each new import is a declared dependency of its package.
  • The in-place diagnostic fix — right, inside the card. The diagnostic shared the untrue driver-sql mechanism clause that seat 2's carried note (5929942555) asked this card to remove, and driver-sql's 'author' provenance arm puts it on the wire, so it is the same defect class (a refusal cut at the envelope), in the claimed file, under the same gate families. The dev measured it rather than assumed it (H5 falsified) and said so in the body.
  • The pins sweep — complete. At origin/main the only pins of the old wording outside the builder are the two driver-sql tests the diff flips (sql-driver-json-column-operator-refusal.test.ts, sql-driver-target-field-provenance.test.ts). Every other pin compares with the builder's output: driver-memory (memory-20444-*, memory-21066-*), driver-turso (turso-local-remote-json-column-parity, remote-transport-compile-refusal-seam), driver-sql (21009-*, shared-text), rest. The objectql cascade probe's test double paraphrases the old reason but asserts only code / status (read: lines 321–509 assert counts, codes and $contains rewrites). No page under content/docs, docs, skills or apps/docs quotes the sentence.
  • Two statements in the diff are wrong, neither load-bearing. The builder's new docblock says "Three faces print the sentence below" and the sql-driver.ts docblock edit names only the engine and driver-memory; at origin/main there are four, driver-turso's remote face being the fourth (the PR body's "Siblings" section knows this; the docblocks do not). The product text names no face, so behaviour is unaffected. A touch-up on the next edit of those comments, not a blocker.

② Semver level

  • The changeset .changeset/21067-json-refusal-under-bound.md declares patch for @objectstack/core, @objectstack/driver-sql, @objectstack/driver-memory, @objectstack/objectql and carries the body's Clause-② declaration, which reads no. Right: a fix to a released package is patch, never skip-changeset; nothing an author can write is added, removed or renamed; the accept set is unchanged (same refused set, same code and status, no new export), so there is no widening or narrowing arm to declare. The message prose changes, and the changeset tells a client matching on the old words to match on code: "INVALID_FILTER" instead — the right migration note for prose that was never the contract.
  • One inconsistency, harmless: by the changeset's own reasoning (list the faces whose printed text moves) @objectstack/driver-turso belongs beside driver-memory and objectql. All four listed packages and driver-turso sit in the single fixed group of .changeset/config.json, so every one of them bumps together whatever the list says; no published version is affected. rest is rightly absent (tests only).

③ Boundary flags

  • Dev deviations (os-dev-reports 5936816305 and 5937293044): (1) the presence clause added after PR open under the PM's mid-task note, with the body edit owed to the seat — the PR body now carries the v2 numbers (486 / 402, the patch-round-1 paragraph), answered. (2) main not re-merged after 0d421041; merge-tree clean; every sibling pin compares with the builder, so the queue's rebuilt generation runs them against this text — acceptable. (3) the bare -- vitest trap ran the whole core suite at default workers — the fuller run, no gap. (4) a private PostgreSQL started outside the scratchpad with a brief, reverted permission change on two scratchpad parents; PIDs recorded and stopped — within §8. (5) the door-measurement test file ran twice and was deleted, never committed — not in the nine-file list, confirmed. (6) the changeset's package list — judged in ②.
  • open_questions: empty in both reports. None to answer.
  • out_of_scope_findings, all "noted, not filed" — each answered: (i) the author-disclosed diagnostic is whole on the wire only to 26-character field names — right to note rather than file: the card's subject is the constant message, which is bounded; the diagnostic's drop order loses the presence clause first and the any-of example second while the one-member remedy and both names stay; field names declare no maximum (regex only, field.zod.ts), so no text repeating the name can be bounded; not one of Prime Directive chore: version packages #10's three filing classes. (ii) the objectql probe double — verified above, right to leave. (iii) rest's PostgreSQL / MySQL cells unprovisioned in CI — pre-existing and stated in that file's header; the CI-run PostgreSQL wire pin is the new driver-sql file in Temporal Conformance (live PG + MySQL), which is green on this head. (iv) check:live-db-isolation is a declared WIDE-population family — an observation about derivation reach, right. (v) that gate's statement-head needle reading a prose assertion as a MySQL USE — the remedy was a test-spelling split beside the builder-equality assertion, not a gate edit and not a route-around; the product sentence is unchanged; a gate false positive on prose is not a runtime defect, a contract violation or a metadata-authoring trap, so noting it is within the rule. The seat may file it as a tooling card at its discretion; nothing here waits on that.
  • Carried for the seat, not blocking: the two face-count docblocks (①, last bullet) and the changeset's driver-turso omission (②). Both are comment or metadata prose with no behavioural effect; either can ride a later edit of those files.
  • No governed surface, no release-adjacent act, no fork head. Nothing escalated.

Implemented-by: claude/issue-21067-json-refusal-under-bound
Reviewed-by: session_017xfMoEjKUuSh2xYB8sCozp

VERDICT: PASS

Reviewed and written at 2026-10-01T18:06Z.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants