Skip to content

fix(objectql)!: a string comparand against a boolean field is narrowed to its boolean, or refused 400, at the engine filter door - #21372

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-21333-boolean-comparand-door
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-21333-boolean-comparand-door

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #21333

Clause-②: yes (narrowing)

What this does

A comparand against a declared boolean / toggle field (or a formula returning boolean) is now judged at the engine's one field-aware filter walk, the same walk that judges number comparands:

  • true / false reach the driver as written (the controls);
  • 1 / 0, "1" / "0" and "true" / "false" are narrowed to true / false, copy-on-write, so every driver receives the one boolean each spelling names;
  • any other string ("yes", "TRUE", " true ", "", a {placeholder}) is refused INVALID_FILTER / 400, naming the field and its declared type, before any driver is resolved.

That holds at where (object form and FilterArray sugar), the per-aggregation filter and having, on every verb that collects a filter (find / findOne / count / aggregate / update / delete, plus judgeFilter). Every REST spelling reaches that one walk unchanged (the POST body where, ?filter= JSON, ?$filter=, the filter AST, and bare query parameters), so there is no per-door coercion.

The accepted set is exactly the one the record validator's boolean arm admits on WRITE (record-validator.ts), per the triage ruling (5946403646).

Files

  • packages/spec/src/data/filter-boolean-comparand-declared-type.ts (new, exported from @objectstack/spec/data). This is the contract: BOOLEAN_COMPARAND_SPELLINGS, readBooleanComparand, booleanComparandFieldVerdict / booleanComparandDoorVerdict, booleanComparandRefusalMessage, the reading table, the fixture and the derived BOOLEAN_COMPARAND_DOOR_CASES. It is additive. The judged positions are the number door's lists by identity (pinned).
  • packages/objectql/src/boolean-comparand-declared-type-door.ts (new). This is the boolean arm: the field meta, the routed verdict and the words. ⛔ It walks nothing.
  • packages/objectql/src/number-comparand-declared-type-door.ts. The existing walkCondition asks the boolean arm at every field key the number arm does not judge. judgeFieldSpec now takes the judging arm, so both arms judge at the same positions by construction. ⛔ No second walker.
  • packages/objectql/src/engine.ts. This file only gets [#21333] notes at the four existing call sites (where, lowered where, per-aggregation filter, having). No new call site.
  • Tests: filter-boolean-comparand-declared-type.test.ts (spec) and engine-boolean-comparand-declared-type-door.test.ts (objectql). The number engine suite gets a named partition for its two census rows on f_boolean / f_toggle: they pass the number verdict and are now refused by the boolean arm, and that partition is pinned in the direction it answers.
  • packages/spec/api-surface/data.json, packages/spec/export-origins/data.json: regenerated (26 additions, 0 removals).
  • Two changesets: objectql minor, BREAKING, Clause-②: yes (narrowing), with an ADR-0087 not-required (no-migration-prescription) disposition; and spec minor, additive, Clause-②: yes, with no ADR-0087 marker (it is not a breaking changeset). See the review round below.

The card's table, measured before and after, on both drivers

Two rows (one true, one false). Measured with a one-time harness through engine.find, engine.aggregate (where count and per-aggregation filter count) and findData for all five REST spellings, on InMemoryDriver and SqlDriver/SQLite. The harness used freshly built dists: base 6c5bef5f4, and after on the merged head 1c184d7695. Every cell is a 200 unless it says otherwise.

comparand memory before SQLite before memory after SQLite after
"true" (implicit, $eq, $in, all 5 REST spellings, aggregate count) 0 rows 0 rows 1 (true row) 1 (true row)
$ne "true" / $nin ["true"] 2 rows 2 rows 1 (false row) 1 (false row)
"false" / $ne "false" 0 / 2 0 / 2 1 / 1 1 / 1
"yes" / $ne "yes" / "TRUE" 0 / 2 / 0 0 / 2 / 0 400 INVALID_FILTER 400 INVALID_FILTER
control 1 / "1" / 0 / "0" 0 rows 1 1 1
control $ne 1 / $ne "1" 2 rows 1 1 1
control true / false / $ne true / $in [true] 1 1 1 1
per-aggregation filter: "true" / $ne "true" 0 / 2 0 / 2 1 / 1 1 / 1
having over groupBy f_boolean: "true" / $ne "true" / "yes" no group / both no group / both true group / false group / 400 true group / false group / 400

The after-run had zero mismatches against the card's correct column on either driver. ⚠ The ruling's premise that 1 / 0 / "1" / "0" are "already answered correctly" holds on SQLite only: InMemoryDriver answered them with no row (strict true vs 1). Narrowing every accepted spelling to its boolean is what makes the ruling's own pin ("the card's table answers its correct column on both drivers") true there. That is a measured refinement of the premise, ⛔ not a switch of the ruling.

Mechanism hypotheses (dispatch Zone 2)

  • H1, holds. The number door's walk is shareable. The boolean twin is an arm of walkCondition, not a copied walker, and the four engine sites run both arms with no new call.
  • H2, holds. GET /api/v1/data/:object hands req.query to findData, which folds leftover keys into an implicit where of strings (metadata-protocol protocol.ts, the implicit-filters block) and calls engine.find. The engine door therefore sees "true", and no REST-side coercion is needed. The GET-door rows are pinned through findData in objectql's suite (bare-param spelling included), so no packages/rest pin was added.
  • H3, holds. $ne, $in, $nin (and $between) members follow the same narrowing. The baseline $ne "true" is 2 rows on both drivers, as in the table.
  • H4. The contract could live entirely in objectql without weakening "one door": every surface reaches the one engine walk, and the walk is the only runtime consumer today. It lives in packages/spec/src/data/ anyway, beside the number contract, for three reasons. The record validator's write arm carries the identical accepted set as a literal, and one grammar both sides can import belongs where both can reach it (the parseNumericString precedent). The refusal words and case table are the public contract the door answers in. And a consumer outside objectql (see H5) can read it without importing the engine. The module is additive and exported.
  • H5. Two consumers answer the card's rows wrongly at doors this PR does not touch. They are reported below as out-of-scope findings, ⛔ not widened into.

Reverse verification

The implementation was committed first (HEAD 1c184d7695). The arm was then ablated through scripts/ablation-replace.mjs (anchor const booleanMeta = booleanArmFieldMeta(facts.boolean);, replaced by null; anchor 1 to 0, blob cc4b1198 to b001e9b3), and both engine suites were run:

  • 26 red: every refusal pin and every narrowing pin of the boolean suite (25: the case table's refusals and narrowings, the card's table and refusals, every verb, FilterArray, nested structure, placeholder, judgeFilter, aggregate where, per-aggregation filter, having, and all ten REST-door cases), plus the number suite's boolean-arm partition (1).
  • 37 green: the controls (true / false / null / flag / $field cases reaching the driver unchanged, the by-reference guard), the boolean suite's pure partition guard, and all 36 other number-door pins.
  • Restore proven by the tool: blob after restore equals the HEAD blob cc4b1198, and git diff HEAD is empty.

Tests and gates (all at HEAD 1c184d7695, freshly built dists)

  • @objectstack/spec: test 599 files / 17541 passed (1 todo), test:repo 48 / 849, exit 0. typecheck exit 0. The new test is in tsconfig.test.json's program (--listFiles).
  • @objectstack/objectql: test 363 files / 7301, test:repo 1 / 5, exit 0. typecheck exit 0 (new files in tsconfig.test.json's program).
  • @objectstack/rest: test 254 files / 4805 passed (316 skipped), test:repo 5 / 177 (1 skipped), exit 0. typecheck exit 0.
  • @objectstack/driver-memory: 70 files / 1718, exit 0. typecheck exit 0.
  • @objectstack/driver-sql: 215 files passed (11 skipped) / 3594 passed (202 skipped), exit 0. typecheck exit 0.
  • @objectstack/dogfood: 166 files passed (1 skipped) / 1369 passed (3 skipped), exit 0. typecheck exit 0.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 91 families on this head and all 91 ran; --ran reconciles 91 / 91, 0 unrun. Each family's exit code was captured before any pipe. All were 0 except check:dual-build-cjs-loads, whose first run was PREREQUISITE NOT MET (eight unrelated packages had no dist/; nothing measured); after building those eight it ran to exit 0. The run includes check:dispatcher-error-vocabulary (no new code: INVALID_FILTER through the existing invalidFilterError), check:adr-0087-registration, check:empty-changeset, spec check:generated, check:nul-bytes, check:driver-memory-census and check:test-source-alias.

Acceptance notes

  1. Live-driver pins. The committed pins drive a recording driver. The arm answers before any driver is resolved, and a narrowed comparand is pinned to reach the driver as the byte-identical filter its boolean control does. The per-driver row counts above were measured with a one-time harness, not committed. A committed SQLite cell would be a packages/rest pin (domain:cli). A committed InMemoryDriver cell needs a ruling on the closed check:driver-memory-census. objectql depends on neither driver.
  2. Not ruled, left as written. A number other than 1 / 0, a Date, or an array member against a boolean field passes the verdict and reaches the driver as written (no stored boolean equals 2). The ruling refuses strings only. Whether to close the set is the analogue of the number door's later widening, so it is an open question for the maintainer, ⛔ not done here.
  3. One grammar, two sides, one literal. record-validator.ts's boolean write arm still spells the accepted set as a literal rather than reading BOOLEAN_COMPARAND_SPELLINGS. They are equal today, and the spec test pins the set's content. Carrier: none named.

Out-of-scope findings (for the seat to file; ⛔ not fixed here)

  • RLS using predicates compare a boolean as written. The compiled policy filter is composed after the caller's filter door, by design, so record.flag != 'true' keeps the true row and == 'true' keeps none. Measured after this change through SecurityPlugin's real middleware over a real engine on SqlDriver/SQLite, with engine.find and a member context, on two rows: == true gives the true row, == 'true' gives none, != 'true' gives both, == 'yes' gives none (silently). Reach: exception (security: a policy's exclusion is not applied). No in-repo producer writes such a predicate today.
  • Analytics NativeSQL strategy compiles runtimeFilter itself. POST /api/v1/analytics/dataset/query over SqlDriver/SQLite (AnalyticsServicePlugin composition, NativeSQL answered) gives: {"flag":"true"} 200 count 0 (should be 1), {"flag":{"$ne":"true"}} 200 count 2 (should be 1), {"flag":"yes"} 200 count 0 (the engine door refuses it 400). Reach: public door measured. Seam: spec:booleanComparandDoorVerdict to runtime service-analytics NativeSQL where compilation.

Review round 1 (head 6f74eb444c, after the at-tier contract review FAIL 5948769828)

Section added by the domain:engine#1 seat, from the dev's patch-round report (5949409460 on #21333):

  • The FAIL was on ② alone. Clause-②: no (narrowing) was false. The 26 additive @objectstack/spec/data exports widen the public surface, and the line was the seat's own false declaration on the claim, corrected by the claim amendments 5948515811 and 5948799813. This body's first lines now read Clause-②: yes (narrowing), scripts/pm/clause2-line.mjs's spelling for a diff that widens one surface and narrows another.
  • The changesets (commit 6f74eb444c, the only change in this round; no code moved):
    • objectql: Clause-②: yes (narrowing), still BREAKING. Six sentences are scoped to what was measured: InMemoryDriver and SqlDriver over SQLite, at findData rather than the HTTP route, and "every door that reaches the engine's filter walk". One of them is the sentence the review named.
    • spec: Clause-②: yes. The BREAKING banner, the narrowing arm and the ADR-0087 marker are removed, which is b285508188's shape. Its "before" and "remedy" sentences, which described objectql's engine, are replaced by "What moves for consumers".
  • Gates at 6f74eb444c: 91 derived, 91 run, all exit 0.
    • check-adr-0087-registration lists one declared-breaking changeset (objectql's).
    • check-changeset-no-major's level axis, driven offline with this body's line, exits 0.
    • check-widening-tells --diff exits 0 under --declaration yes, and exits 4 under the old no with 31 tells (26 × T3, 5 × T2). That confirms the review.
    • No suite was re-run, because the code is unchanged since 1c184d7695.
  • The review's escalations:
  • Lane. Triage's answer 5948860364 on objectql: a string comparand against a boolean field answers wrong rows under 200 — "true"/"false" match nothing, $ne "true" returns the true row, and ?f=true / ?f=false on the GET door both answer zero rows #21333: the spec lane lands this PR whole, with the objectql half declared. The domain:engine seat hands the card over without marking this PR ready or enqueueing it.

Generated by Claude Code

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/spec, touching 64 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, packages/spec/src/data/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/data-api.mdx (via INVALID_FILTER (literal, a string literal in BooleanComparandDoorRefusalCase; a string literal in BooleanComparandDoorVerdict; a string literal in booleanComparandDoorVerdict))
  • content/docs/api/error-catalog.mdx (via INVALID_FILTER (literal, a string literal in BooleanComparandDoorRefusalCase; a string literal in BooleanComparandDoorVerdict; a string literal in booleanComparandDoorVerdict))
  • content/docs/data-modeling/field-types.mdx (via returnType (symbol, a field of interface BooleanComparandDoorCaseBase; a field of interface BooleanComparandDoorFieldMeta; a field of interface BooleanComparandDoorFixtureField; a field of interface BooleanComparandRefusalSite))
  • content/docs/data-modeling/formulas.mdx (via returnType (symbol, a field of interface BooleanComparandDoorCaseBase; a field of interface BooleanComparandDoorFieldMeta; a field of interface BooleanComparandDoorFixtureField; a field of interface BooleanComparandRefusalSite))
  • content/docs/data-modeling/validation-rules.mdx (via returnType (symbol, a field of interface BooleanComparandDoorCaseBase; a field of interface BooleanComparandDoorFieldMeta; a field of interface BooleanComparandDoorFixtureField; a field of interface BooleanComparandRefusalSite))
  • content/docs/deployment/validating-metadata.mdx (via INVALID_FILTER (literal, a string literal in BooleanComparandDoorRefusalCase; a string literal in BooleanComparandDoorVerdict; a string literal in booleanComparandDoorVerdict))
  • content/docs/getting-started/common-patterns.mdx (via returnType (symbol, a field of interface BooleanComparandDoorCaseBase; a field of interface BooleanComparandDoorFieldMeta; a field of interface BooleanComparandDoorFixtureField; a field of interface BooleanComparandRefusalSite))
  • content/docs/kernel/contracts/data-engine.mdx (via INVALID_FILTER (literal, a string literal in BooleanComparandDoorRefusalCase; a string literal in BooleanComparandDoorVerdict; a string literal in booleanComparandDoorVerdict))
  • content/docs/protocol/objectql/query-syntax.mdx (via INVALID_FILTER (literal, a string literal in BooleanComparandDoorRefusalCase; a string literal in BooleanComparandDoorVerdict; a string literal in booleanComparandDoorVerdict))

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

  • content/docs/releases/v17/17-1.mdx (via INVALID_FILTER (literal, a string literal in BooleanComparandDoorRefusalCase; a string literal in BooleanComparandDoorVerdict; a string literal in booleanComparandDoorVerdict))
  • content/docs/releases/v17/17-4.mdx (via INVALID_FILTER (literal, a string literal in BooleanComparandDoorRefusalCase; a string literal in BooleanComparandDoorVerdict; a string literal in booleanComparandDoorVerdict))
  • content/docs/releases/v17/17-5.mdx (via INVALID_FILTER (literal, a string literal in BooleanComparandDoorRefusalCase; a string literal in BooleanComparandDoorVerdict; a string literal in booleanComparandDoorVerdict))
  • content/docs/releases/v17/17-6.mdx (via returnType (symbol, a field of interface BooleanComparandDoorCaseBase; a field of interface BooleanComparandDoorFieldMeta; a field of interface BooleanComparandDoorFixtureField; a field of interface BooleanComparandRefusalSite), INVALID_FILTER (literal, a string literal in BooleanComparandDoorRefusalCase; a string literal in BooleanComparandDoorVerdict; a string literal in booleanComparandDoorVerdict))

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
  • 3 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, packages/spec/src/data/index.ts) — pages documenting those are invisible to this run
  • 16 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 139 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 3937ad2f327450c0ce4d4ded30acae815ef98810 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 3937ad2f327450c0ce4d4ded30acae815ef98810

⚠️ 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 3937ad2f327450c0ce4d4ded30acae815ef98810 → 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: 1c184d769519eb21d6b1a4917a8fb36cd1b69561
Local-runs: none

Inputs, these and nothing else: card #21333 (body; comments 5946403646 the ruling, 5946729384 the claim, 5948452642 the dev report, 5948515811 the claim amendment moving Clause-② to yes); PR #21372 (body, the 12-file list at +1701/−17, and the net diff from the merge-base 23365eaedf to the head — the same 12 files, byte-identical set); the head's check-runs (read 2026-10-02T09:03:56Z, quoted below); for precedent only, packages/spec/src/data/filter-number-comparand-declared-type.ts on main, b285508188 (PR #20414) and b05743433b (PR #20545), plus 4a1df19656 (PR #20501, the number door's own objectql landing) for the objectql half's declaration. Not read: the dispatch order or the dispatching seat's conclusions. Nothing built, run or re-run.

Check-runs on the head (35, all completed at the time of writing): 32 success — Auto Label, Build Core, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate and its three shards, Dogfood Verify CLI, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates (the job that carries check-adr-0087-registration and check-changeset-no-major), the four claim/ownership guards, Spec property liveness, Temporal Conformance (live PG + MySQL), Test Core and its six shards, Type Check × 4 plus TypeScript Type Check, filter; 3 skipped by design — Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in); 0 failed, 0 pending. Commit status: Vercel success (ignored build step). Those conclusions are the gate verdicts; nothing below re-derives them.

① Derived judgments

The public surface — right, with three redundancies named, none wrong. 26 additions and 0 removals in api-surface/data.json (9 + 11 + 1 + 1 + 3 + 1 across the six hunks, counted), all from the one new module, all additive, and each one a twin of a number-door export by name and kind: BOOLEAN_COMPARAND_SPELLINGS / readBooleanComparand / BooleanComparandReading ↔ NUMERIC_STRING_PATTERN / readNumericString / NumericStringReading; NON_BOOLEAN_STRING_FORMS / NonBooleanStringForm ↔ NON_NUMERIC_STRING_FORMS / NonNumericStringForm; BOOLEAN_COMPARAND_READING_CASES / BooleanComparandReadingCase ↔ NUMERIC_STRING_GRAMMAR_CASES / NumericStringGrammarCase; the seven BOOLEAN_COMPARAND_DOOR_* constants, the eight BooleanComparandDoor* types, BooleanComparandRefusalSite and the three booleanComparand* functions ↔ their NUMBER_ / Number / number twins one for one. Correctly absent: no parseBooleanComparand (there is no second grammar) and no NON_BOOLEAN_VALUE_FORMS (non-strings pass, by the ruling). Placement in @objectstack/spec/data beside the number contract is what the card's "Where", the ruling and the claim all named, and is b285508188's precedent; the PR's H4 is honest that objectql alone would also keep "one door". Exported that no runtime consumer needs beyond symmetry: BOOLEAN_COMPARAND_DOOR_SCALAR_OPERATORS and BOOLEAN_COMPARAND_DOOR_LIST_OPERATORS (identity aliases of the number door's lists, pinned toBe), and BooleanComparandDoorFieldMeta (structurally NumberComparandDoorFieldMeta; declaredFactsOf hands the SAME meta object to both arms). All three are in-family and harmless. @objectstack/objectql's own surface is unchanged: index.ts at the head re-exports neither door module (checked), so "exports nothing new and nothing less" holds.

The accepted set — right. BOOLEAN_COMPARAND_SPELLINGS is 1→true, 0→false, "1"→true, "0"→false, "true"→true, "false"→false; a boolean passes as written. packages/objectql/src/validation/record-validator.ts:1272 on main admits on WRITE exactly boolean | 0 | 1 | '0' | '1' | 'true' | 'false' — equal, as the PR claims. Every other string is refused with a named form: "TRUE" → letter-case, " true " → padded, "" → empty, {current_user_id} / {today} → placeholder (through classifyFilterToken, so no token resolves to a boolean), "yes" / "on" / "2" / "1.0" → not-a-boolean. The words are filter on 'f' compares a declared boolean field against "yes" at where.f.$ne, which is not a boolean: …, so the field and its declared type are named (toggle and formula … returning boolean included; having says "a boolean aggregated column" and never "declared"), and the engine prefixes find('object'): and raises INVALID_FILTER / 400 through the existing invalidFilterError — no new code, no new vocabulary. Copy-on-write holds at all three levels: judgeFieldSpec copies the spec on the first changed operator and the list on the first changed member, walkCondition copies the node on the first changed key, and a filter with nothing to narrow returns BY REFERENCE (pinned toBe(where) for the controls; the caller's JSON is pinned unchanged after a narrowing; the having guard pins having.flag.$in still ['true', 0]). One neighbouring fact, not a defect: the same validator file's READ-side coerceBooleanFields (:798) reads a wider set (trim, case-fold, "" → false, any non-zero number → true); "one grammar for both sides" is a claim about the write arm only, and both changesets say exactly "when a boolean field is WRITTEN".

Coverage, one door — right; no door in the ruling's list is missed. The arm sits in walkCondition's existing else branch, asked only where the number arm does not judge, and booleanArmFieldMeta is non-null only for boolean / toggle / formula returning boolean. judgeFieldSpec now takes the judging arm, so both arms meet the implicit comparand, the six scalar operators and every $in / $nin / $between member at the same code lines. No second walker: boolean-comparand-declared-type-door.ts is 111 lines of field meta, routed verdict and words, and walks nothing. No new engine call site: engine.ts at the head calls narrowNumberComparands at :1080 (object where), :1169 (lowered FilterArray) and :17238 (per-aggregation filter), and narrowHavingNumberComparands at :17389 (having); the diff adds comments at exactly those four. Reach, each pinned in engine-boolean-comparand-declared-type-door.test.ts: where object and FilterArray ([['f_boolean','=','yes']] refused at where.f_boolean, '!=' narrowed to $ne: true); filter in its three spellings and the bare query parameter through findData (protocol.ts :10078 / :10111 on main confirm leftover query parameters lower into implicit filters of strings, so H2 holds structurally, not only by the dev's reading); the per-aggregation filter (rooted aggregations[1].filter.f_boolean.$ne); having over a groupBy of the field; find / findOne / count / aggregate / update / delete / judgeFilter, each refusing before any read or write, and a narrowed update scope reaching updateMany as { f_boolean: true }. mapRelationConditions runs the walk with lowerOnly and skips both arms, as before; a nested relation condition is judged when the related read runs its own where door.

The operators — right. $ne / $in / $nin / $between follow the arm by the number door's SCALAR_OPERATORS / LIST_OPERATORS, which the contract takes by identity. null: readBooleanComparand(null) is null, so passes (pinned as a reading row and as a passes case). $null / $exists / $empty are not in the scalar list and are never handed to the verdict; the [unjudged] rows pin passes, and isBooleanArmCase in the number suite excludes them explicitly. A { $field } reference is kept by isFieldReference before any operator is read, and each case gets its own copy.

Number-door regressions — none for a non-boolean field, and none reachable for a formula. The only control-flow change is the else of numberComparandFieldVerdict(meta) === 'judged': for every type the boolean arm does not judge, booleanArmFieldMeta is null and the loop continues exactly as before; the spec suite pins the two judged classes disjoint over every FieldType and the four formula return types. At having, boolean: { type } is non-null for any typed column but judges only a boolean-typed one; a numeric column still takes the number arm first. The two census rows on f_boolean / f_toggle ($gt "abc") move from "passes to the driver" to the boolean arm's refusal — a boolean field, by design — and the partition is derived from the contract's verdict, not hand-listed, with the GUARD pinning the table's exact sum. Formula: a formula returning boolean is now judged by the arm AT THE WALK (pinned directly on narrowNumberComparands: "yes" throws, "true" narrows), but no engine path reaches it — the materializable-field door refuses every formula filter first with INVALID_FIELD, pinned in both suites; an unreadable returnType is deferred → null → continue, as before. All 36 other number-door pins are unchanged and the dev's ablation left them green.

Answers that change — a correction under the ruling, correctly named, not a widening. The ruling's accept set names 1 / 0 / "1" / "0" and its pin demands the card's correct column on BOTH drivers. At base, InMemoryDriver answered them 0 rows and $ne 1 with 2: memory-driver.ts:1861 on main hands $eq to the matcher as put(op, store(val)), which compares a stored true with 1 strictly — so "already answers correctly" held on SQLite only (integer affinity). Narrowing those spellings to their boolean admits no value the ruling did not name; it makes the ruling's own pin true on memory. It IS a changed answer for a driver-memory caller ({ flag: 1 }: 0 rows → 1 row) and the objectql changeset names it twice ("matched the right row on SQLite and no row on InMemoryDriver"; "?flag=true and ?flag=1 now return the true row"). PostgreSQL / MySQL base answers were not measured (no live server here; PG reads a text '1' — and 'yes' — as a boolean input, so the base likely MATCHED on PG where SQLite matched nothing); after the arm every driver receives the boolean control, so the unmeasured cells can only converge, and the pins prove the control-equivalence that makes that so.

② Semver level

The Clause-②: line on this head is wrong, three times, and that fails ②. The PR body, .changeset/21333-spec-boolean-comparand-contract.md and .changeset/21333-objectql-boolean-comparand-door.md all read Clause-②: no (narrowing). The diff adds 26 rows to packages/spec/api-surface/data.json, the listing of an exports-addressable subpath: by scripts/pm/check-widening-tells.mjs's own T3 ("a new row in a published entry point's export listing") a no is refused at enqueue, and by scripts/pm/clause2-line.mjs's enumerated readings the one that fits "a diff that widens one surface and narrows another — both facts are true and both are read" is yes (narrowing). Precedent agrees on each half: b285508188 (the spec contract alone, 27 additive exports) → Clause-②: yes, minor, no BREAKING banner; 4a1df19656 (PR #20501, the objectql number door alone, spec untouched) → Clause-②: no (narrowing), BREAKING, minor. This PR is both halves in one, so neither half's line alone is right. The claim amendment 5948515811 already names the no "this seat's false declaration" and promises the patch round; this record is of the head as it stands. What the next head must read, with no code change:

  • PR body: Clause-②: yes (narrowing).
  • objectql changeset: keep minor, the ! summary, the BREAKING banner and the (narrowing) arm — check-adr-0087-registration reads breaking-ness from exactly those three signals — and carry the PR's line, Clause-②: yes (narrowing) (AGENTS.md: a breaking changeset's body "carries the PR's Clause-② line"; check-changeset-no-major reads the VALUE from the PR body alone and the ADR gate reads the ARM from the changeset, so the arm is the gate-read part here).
  • spec changeset: Clause-②: yes, no arm, and the BREAKING banner removed. @objectstack/spec narrows nothing of its own: every existing export is unchanged, no Zod schema moves, and no spec consumer's input is refused differently — the sentence "BREAKING: the module declares a narrowing of what a filter may compare…" would ship into spec's CHANGELOG.md a break that is objectql's. That is b285508188's shape byte for byte. With the banner and arm gone the changeset is non-breaking and the ADR gate's G1 ignores its marker; the marker may stay (its text already describes an additive module) or go.
  • Level: minor on both packages is right — yes takes at least minor, and the objectql narrowing ships as minor under the launch-window convention (check-changeset-no-major.mjs:47).

ADR-0087 markers. objectql, not-required (no-migration-prescription): right — no authorable key, spelling, export or stored shape moves and no stored row is read or rewritten, so there is nothing for objectstack migrate meta to carry; "The remedy" is a one-line fix, not a FROM/TO prescription (precedent b05743433b's body carried an explicit FROM → TO line under this same category and the gate read it [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription); Lint & Repo Gates is green on this head). spec: the marker's "why" is consistent with what the diff publishes (an additive module, new exports only, no Zod change); the error is that the changeset declares breaking at all, above.

Changeset sentences against the diff and the PR body's measurements. Supported, sentence by sentence: the before-rows ("true" / "false" 0 rows on both drivers; $ne "true" / $nin ["true"] 2 rows; "yes" 0 and $ne "yes" 2; 1 / "1" / 0 / "0" 1 row on SQLite and 0 on memory with $ne 1 2 there; per-aggregation filter and having no row / no group and every one under $ne) match the PR body's table cell for cell; the after-rows match; "The accepted set is the one the record validator already admits when a boolean field is WRITTEN" (:1272); "No export or published type changes" (objectql index.ts); "A filter on a formula field is still refused one step earlier, as before"; the spec export list (all 26 present with the described roles, NON_BOOLEAN_STRING_FORMS with its five forms); "the judged positions, which are the number door's lists by identity" (pinned toBe); "the refusal words, inside the 500-character client bound" (pinned at the longest position and front-loaded with 40-character names). Not supported: the spec changeset's BREAKING sentence as a statement about @objectstack/spec (above). Stated beyond the measurement: objectql's "now return the true row on every driver" — measured on memory and SQLite only; it follows for PG / MySQL by mechanism (the driver receives the boolean control), and the same changeset's before-paragraph is correctly scoped to the two measured drivers.

③ Boundary flags

Deviation (1), the pins — accepted for this head; a CI-visible per-driver cell escalated as a follow-up. The committed pins prove, through a recording driver, that every accepted spelling reaches the driver byte-identical to its boolean control after the shared lowering, at every position and every door, and that a refusal reads nothing — so each driver's answer for "true" IS its answer for true. The ruling's pin ("the card's table answers its correct column on both drivers, the GET-door rows included") is met by the dev's one-time harness (both drivers, before and after, 0 mismatches, the table in the PR body and both suite headers) joined to that equivalence, and the claim itself allowed the GET-door pin to live outside rest. What a CI-visible per-driver cell would catch that these pins cannot: (a) a regression in a driver's answer to the boolean CONTROL itself — SqlDriver's boolean bind on SQLite, or the memory matcher against rows whose stored form is 1 / 0 after an import — since the pins take the control's correctness from the one-time measurement; (b) anything between the HTTP socket and findData that reshapes a bare ?f_boolean=true — the pins start at findData, and a RestServer-level cell would start at the route; (c) the PG / MySQL cells, never measured. The number door committed exactly such a cell, packages/rest/src/data-number-comparand-door.test.ts (SQLite always, PG / MySQL named skips), so the precedent includes one. Escalation: the seat routes a packages/rest boolean-door cell to domain:cli as a follow-up card — the dev's third out-of-scope item already names it ("carrier: none") — not as a condition on this PR. The InMemoryDriver cell stays "by construction", exactly as b05743433b's acceptance note had it, and this PR rightly did not open the closed check:driver-memory-census.

Deviations (2) to (4) — accepted. (2) Stopping the dev's own detached spec run by its recorded PGID and re-running everything on the merged head is process hygiene, and every number in the report is at the merged head 1c184d7695. (3) The merge commit carries git's default message without the trailer pair; the pre-push check:commit-card-trailers accepted it, the four content commits carry the pair, and the squash landing will carry the PR's. (4) The model-free Co-Authored-By pair is AGENTS.md's rule; in this repo AGENTS.md wins over the harness reminder.

The open question (a non-string outside the set) — shipping the ruling's letter is right; closing the set is its own card. The ruling refuses strings; 2, -1, 0.5, a bigint, a Date or an array member against a boolean field pass as written (pinned passes; the reading rows unread(2) / unread(-1) say why). That is how the number door shipped: strings only in b285508188, widened to booleans / Dates / arrays in b05743433b (PR #20545) on a separate card with a maintainer-directed ruling. Same two-step here. Escalation: the seat files the follow-up with the dev's recommendation B; one reason for B beyond the dev's four axes: "no stored boolean equals 2" gives an empty 200 on memory and SQLite, but on PostgreSQL boolean = integer is a DATABASE_ERROR 500 — the exact defect shape the number door's non-string card closed.

The two out-of-scope findings — reach evidence sufficient; one family card is the right carrier. RLS: packages/plugins/plugin-security/src/rls-compiler.ts (compileCelToFilter) produces a FilterCondition the security middleware ANDs after the caller-filter door, by design (docs/design/predicate-compilation-convergence.md:74 lists it among the front ends that produce a condition outside the door); measured in-process over a real engine on SqlDriver / SQLite with a member context: != 'true' keeps the true row and == 'yes' keeps none silently — reach "exception: security", with the mitigating census that no in-repo producer writes such a predicate. Analytics: packages/services/service-analytics/src/strategies/native-sql-strategy.ts compiles the dataset runtimeFilter itself; measured through the public door POST /api/v1/analytics/dataset/query with before-numbers ("true" count 0, $ne "true" count 2, "yes" count 0 where the engine door now answers 400). Both are one family — a filter compiled outside the engine door skips its declared-type arms, the family the number door's reports already carried for the save-time door — so one card naming both seams with the dedupe words is right; the seat files it (domain:engine for the family, the security seam's fix landing in plugin-security).

The record-validator.ts literal — acceptance note, not a defect. The write arm's literal (:1272) equals BOOLEAN_COMPARAND_SPELLINGS today and the spec test pins the set's content; the next touch of that arm reads the spec set, as the number arm reads parseNumericString. No card needed; noted because the spec pin would not notice a validator drift — only a validator-side pin would.

Implemented-by: claude/issue-21333-boolean-comparand-door
Reviewed-by: session_017xfMoEjKUuSh2xYB8sCozp

Why FAIL and not PASS: ① every derived judgment is right and the head's 32 gates are green; ② the governance declaration on this head is false three times — no (narrowing) where the diff widens @objectstack/spec/data by 26 exports — and the spec changeset carries a BREAKING banner the diff does not support. The next head needs only the three Clause-② lines and the spec changeset's banner corrected as prescribed under ②, with no code change; ③'s two escalations are follow-up cards, not conditions.

VERDICT: FAIL


Generated by Claude Code

…pec; scope sentences to the measured drivers

The objectql changeset carries the PR's Clause-② line, yes (narrowing), and
keeps its BREAKING banner. Its sentences are now scoped to what was measured
(InMemoryDriver, SqlDriver over SQLite, through findData). The spec changeset
reads Clause-② yes, with no banner, no arm and no ADR-0087 marker: the module
is additive, and the narrowing is objectql's.

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: 6f74eb444c1a4961aac2251b9733ee85e3f7a232
Local-runs: none

Inputs, these and nothing else: card #21333 (body and all eleven comments, among them the ruling 5946403646, the claim 5946729384 and its amendments 5948515811 and 5948799813, the dev reports 5948452642 and 5949409460, triage's answer A 5948860364, the release and hand-off 5949483316, triage's route swap 5949706861 and the adopting claim 5949833323); the previous record 5948769828 on PR #21372 (FAIL on the earlier head its own Head-sha line names, 1c184d7 in short, judged on ② alone); PR #21372 (body, the 12-file list, the net diff from the merge base 23365eaedf to this head, and the diff from that earlier head, 1c184d7, to this head); the head's check-runs; scripts/pm/clause2-line.mjs and scripts/pm/check-widening-tells.mjs on main, read as text for the arm and tell vocabulary. Not read: the dispatch order or the dispatching seat's conclusions. Nothing built, run or re-run; the fetched branch ref was confirmed equal to the head above before any diff was read.

Check-runs on the head (read at 2026-10-02T10:14Z): 42 check-runs, all completed, all on this one sha; 37 success, 5 skipped, 0 failed, 0 in progress. The skipped five are by design: Console Pin Gate, Build Docs, Packed-tarball smoke (opt-in), and one later skipped re-run each of Auto Label and Check PR Size beside their earlier success runs. Lint & Repo Gates, the job that carries check-adr-0087-registration and check-changeset-no-major, is success; so are Governed Surface Queue Guard, both Check Changeset runs, Spec property liveness, Build Core, Test Core and its six shards, the four Type Check jobs plus TypeScript Type Check, Dogfood Regression Gate and its three shards, Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), filter, Check Documentation Links, Flag docs affected by code changes, and the four claim and ownership guards. Those conclusions are the gate verdicts; nothing below re-derives them. The hand-off's "4 still running" have since concluded, all success.

① Derived judgments

The code delta since the previous record's head is empty. 6f74eb444c is one commit, a fast-forward from the earlier head 1c184d7 (ancestor confirmed; the earlier sha is written without a code span here so this record names one head and one only). git diff 1c184d769 6f74eb444c touches exactly two files: .changeset/21333-objectql-boolean-comparand-door.md (+7 / −7) and .changeset/21333-spec-boolean-comparand-contract.md (+3 / −7), +10 / −14 in all, which is what the patch-round report 5949409460 states. No line under packages/** moves: no source, test, barrel or regenerated artifact. The net diff against main at this head is the same 12 files as before, +1697 / −17 (the files API agrees: 12 files, 1697 / 17); the four-line difference from the previous record's +1701 / −17 is exactly the changesets' net −4.

So every ① judgment of record 5948769828 stands on unchanged code: the 26 additive @objectstack/spec/data exports (api-surface/data.json and export-origins/data.json +26 / −0 each, src/data/index.ts +7 barrel lines, nothing removed); the accepted set equal to the record validator's write arm; one door (the boolean arm inside walkCondition's existing else branch, four existing engine.ts call sites gaining notes only, no new walker); the operators; the absence of number-door regressions; and the InMemoryDriver 1 / 0 correction as a correction under the ruling. Not re-derived here, because nothing they were derived from moved.

main has not moved under the diff. The merge base is 23365eaedf; 18 commits have landed on origin/main since, and git log over the 12 paths from the merge base to origin/main is empty. The PR's mergeable state reads clean. The round-1 deviation "main moved, not merged" therefore costs nothing; the queue merges it.

One note, not a condition. The engine suite's header table (engine-boolean-comparand-declared-type-door.test.ts, the row for "true" / "false") states both strings at implicit, $eq, $in and every REST spelling as 0 rows before the arm, while the patch round scoped the objectql changeset to "false" (implicit) precisely because "false" at $eq / $in was not measured at base. That is a comment inside a test, unchanged since round 0 and shipped to no consumer; acceptance-note class. The next touch of the file scopes it to match the changeset.

② Semver level

The three Clause-② lines are right on this head. Read under clause2-line.mjs's own reader as text: the key must open the line, the value token yes or no must be the first thing after the colon, and the arm is the first token of a parenthetical opened next, from the closed pair widening / narrowing.

  • PR body, line 3: Clause-②: yes (narrowing), declared yes, arm narrowing. The two later mentions of the key in the body (the Files bullet and a Review-round bullet) sit mid-line after other text, which the reader never takes as a declaration; the first key-initial line governs, and it is line 3.
  • objectql changeset: Clause-②: yes (narrowing), declared yes, arm narrowing.
  • spec changeset: Clause-②: yes, declared yes, no arm.
    The diff's two facts: it widens the public surface (26 new rows in a published entry point's export listing, the shape check-widening-tells.mjs names T3, "the PUBLIC SURFACE grows") and it narrows an accept set (a string other than "true" / "false" / "1" / "0" against a declared boolean field becomes INVALID_FILTER / 400). The reader's enumerated spelling for a diff that does both is yes (narrowing), so the PR body and the breaking changeset are right. The spec changeset narrows nothing of its own: every existing export is unchanged, no Zod schema moves, and nothing @objectstack/spec accepts is refused differently. yes with no arm is right there, and the arm would have declared a breaking changeset the diff does not support. This is the previous record's prescription, line for line.

Bang, banner and ADR-0087 marker. objectql: fix(objectql)!: carries the bang, the BREAKING banner, the (narrowing) arm and exactly one adr-0087 marker (the HTML comment), not-required (no-migration-prescription). Right: no authorable key, spelling, export or stored shape moves, no stored row is read or rewritten, and the remedy is the one-line fix the changeset's own "The remedy" paragraph gives, so there is nothing for a migration to carry. spec: feat(spec): with no bang, no banner, no arm and no marker. Right for a non-breaking additive changeset; a marker there would be outside the ADR gate's business, and the removed marker text described objectql's narrowing, not spec's. Lint & Repo Gates is success on this head, and that conclusion is the verdict of both gates that read these signals.

Level. minor on both packages. AGENTS.md's changeset rule: yes takes at least minor; the objectql narrowing is BREAKING and ships as minor under the launch-window convention for accept-set narrowings, which the changeset names in its banner; check-changeset-no-major reads the PR body's yes and finds no package whose source moves graded patch. Green.

Every sentence the patch round added or rescoped, against the PR body's measurements (the card's table, driven through engine.find, engine.aggregate and findData for all five REST spellings, on InMemoryDriver and SqlDriver over SQLite, before and after):

  • objectql marker, "matched no row (and every row under $ne) on InMemoryDriver and on SqlDriver over SQLite (PostgreSQL and MySQL not measured)": the rows "yes" 0 and $ne "yes" 2 on both drivers. The PostgreSQL and MySQL string cells are unmeasured (the round-1 runs on those servers measured non-string comparands and the boolean controls). Supported, and the scoping is load-bearing: PostgreSQL reads the text 'yes' as a boolean input, so its before-answer may have differed.
  • BREAKING banner, "through every door that reaches the engine's filter walk (engine.find / findOne / count / aggregate / update / delete, and every spelling the data API hands it)": the body's verb list and REST-spelling list; it correctly leaves out the two doors that compile filters themselves (the RLS seam and analytics NativeSQL), measured unchanged in round 0.
  • "the protocol's findData with each spelling the POST and GET routes hand it": the harness ran at findData, not at the HTTP route. Supported; it narrows the earlier claim.
  • ""true" (implicit, $eq, $in) and "false" (implicit), and both through ?filter=, ?$filter=, the filter AST and the bare query parameter, matched no row on either driver": the table's rows 1 and 3 read under its preamble (every comparand through the five REST spellings), and the card's own reproduction adds ?f_boolean=false answering 0 rows. Supported; "false" at $eq / $in is no longer claimed.
  • "1, "1", 0 and "0" at where (and "1" / "0" through every spelling above) matched the right row on SQLite and no row on InMemoryDriver": row 5, rescoped to where because the per-aggregation filter answered 1 correctly on memory at base. Supported.
  • "Measured on InMemoryDriver and on SqlDriver over SQLite, ?flag=true and ?flag=1 now return the true row; any other driver receives the same narrowed boolean by mechanism (PostgreSQL and MySQL not measured)": the after-columns of rows 1 and 5. The sentence the previous record named ("on every driver") is fixed. The mechanism clause is also now better than mechanism: the round-1 report measured the boolean controls true / false correct on all four drivers (memory, SQLite, PostgreSQL 16, MySQL 8), and the committed pins hold a narrowed comparand byte-identical to its control at the driver.
  • spec, "What the verdict answers door-refusal for": matches booleanComparandDoorVerdict (a string whose reading is not a boolean returns door-refusal with INVALID_FILTER / 400; the scalar and list operators are the number door's by identity). "The four accepted ones" are "1", "0", "true", "false". The dropped before-sentence was objectql's measurement, not spec's. Right.
  • spec, "What moves for consumers. Nothing in this package refuses or narrows a filter, and every existing export is unchanged. The door that applies the verdict ships in the same release in @objectstack/objectql": index.ts and both generated listings are add-only, and both changesets ride one PR into one Version Packages run. Right.
    Every other sentence in both changesets is byte-identical to the head the previous record read sentence by sentence, and nothing under them moved.

The PR body's added "Review round 1" section is consistent with the delta: "six sentences" (the objectql delta is the Clause-② line plus six sentences, counted above); "26 × T3, 5 × T2" matches the tell vocabulary (26 export-listing rows; NON_BOOLEAN_STRING_FORMS has five as-const members) and is the dev's local reading, not re-run here, and under yes that gate blocks nothing in any case; "one declared-breaking changeset" is what the spec changeset's shape implies. The Files bullet describing the two changesets matches their frontmatter and bodies.

③ Boundary flags

The previous record's two escalations are closed by filing, as the hand-off (5949483316) and triage's route swap (5949706861) both carry them. #21376 takes the RLS compile seam and analytics NativeSQL as one family card, both seams named, as the previous record asked. #21382 takes the non-string half (the dev's recommendation B), with the round-1 findings folded in as the same comparand family: PostgreSQL answering 500 DATABASE_ERROR for 2 / -1 / 0.5 / a Date at every slot, and an array $in member splitting 200 on memory against 400 on the SQL drivers. That dedupe is right. Both carry Blocked-by: #21333, which is right: the RLS and analytics fix consumes booleanComparandDoorVerdict from this PR's spec module, and the number door's precedent made its non-string card serial after the door. Neither is a condition on this PR. The two cards are read from those two comments, written by two seats, within this review's input set; the cards themselves were not opened.

The packages/rest boolean-door cell, reclassified from a follow-up card to an Acceptance note: the reclassification is right. The filing gate: a card needs a reproducible defect, a contract violation or a metadata-authoring trap, with a measured reach; anything else is an Acceptance note. The cell is none of the three. After this head the measured table has zero mismatches against the card's correct column on both drivers; the committed pins prove, through a recording driver, that each accepted spelling reaches the driver byte-identical to its boolean control and that a refusal reads nothing; and the round-1 measurement now shows the boolean controls correct on PostgreSQL and MySQL too, which closes the one unmeasured gap the previous record named for such a cell. A missing CI cell has no user reach of its own. The previous record over-filed it; the releasing seat's note ("test coverage with no user-reachable reach") is the correct class.

Round-1 deviations, accepted. (1) Throwaway PostgreSQL and MySQL instances were started outside the scratchpad for the measurement, stopped by their PIDs and removed; no repository file was touched, and the committed diff is the two changesets alone. (2) main not merged: covered under ①. (3) The worktree was re-added from the remote branch and removed after the report. The non-string measurement itself was a scratchpad probe, nothing committed, and its findings are #21382's evidence, not this PR's.

Unchanged flags. The record-validator.ts write-arm literal stays an acceptance note, equal to BOOLEAN_COMPARAND_SPELLINGS today. The adopting claim 5949833323 declares Clause-②: yes (narrowing), consistent with the PR body and the amendments, and the PR's assignee now reads the adopting seat's account.

Why PASS: ① the code is unchanged since the head whose derived judgments the previous record found right, and main has not moved under the 12 files; ② the three Clause-② lines, the bang, banner and marker on each changeset, the minor levels and every rescoped sentence now match the diff and the measurements, and the two gates that read them are green on this head; ③ both escalations are filed as follow-up cards, and the one reclassification is right under the filing gate. Nothing remains that a next head would need to change.

Implemented-by: claude/issue-21333-boolean-comparand-door
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 10:16
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 10:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 9f13c94 Oct 2, 2026
44 checks passed
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 protocol:data size/xl tests tooling

Projects

None yet

3 participants