Skip to content

fix(spec)!: a number other than 1 / 0, a Date or an array compared against a boolean field is refused like a non-boolean string (#21382) - #21404

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21382-boolean-comparand-non-string
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21382-boolean-comparand-non-string

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21382
Clause-②: yes (narrowing)

What this changes

One verdict, widened, as triage directed on the card (grade 5949762416, inheriting the number door's direction 5877498426): the published boolean-comparand verdict now refuses any comparand against a declared boolean / toggle field (or a formula returning boolean) that is outside its accepted set. The engine's boolean arm consumes that verdict and nothing else; it carries no second rule. This is the boolean twin of PR #20545 (b05743433b) on the number door, and it follows that PR's shape and words where the two contracts have the same parts.

  • The verdict, packages/spec/src/data/filter-boolean-comparand-declared-type.ts. booleanComparandDoorVerdict(field, comparand) answers door-refusal (INVALID_FILTER / 400) for a number other than 1 / 0 (2, -1, 0.5, NaN), a Date and an array, at a scalar slot or as a list member. The string rule is byte-for-byte what PR fix(objectql)!: a string comparand against a boolean field is narrowed to its boolean, or refused 400, at the engine filter door #21372 published. The accepted set is unchanged: true, false, 1, 0, "true", "false", "1", "0"; null is still the null test.
  • A bigint is read as the number it names: 1n / 0n narrow like 1 / 0, and any other bigint is refused as a number. See The bigint reading below for why this is the only reading that gives one answer at every position.
  • What still passes: a boolean, null, a { $field } reference, and every value outside the comparand-type door's accepted set (undefined, a plain object, a Map). That door already refuses those on every field, in its own words (see Objects below).
  • Three additive exports: NON_BOOLEAN_VALUE_FORMS (number, date, array) and the types NonBooleanValueForm / NonBooleanComparandForm. The refusal's form widens from NonBooleanStringForm to NonBooleanComparandForm, on BooleanComparandDoorVerdict, BooleanComparandRefusalSite and BooleanComparandDoorRefusalCase. The site's value was already unknown; it now carries a non-string. booleanComparandDoorVerdict's signature is unchanged (relevant to A boolean comparand is judged only at the engine door: the RLS compile seam and analytics NativeSQL pass a string against a declared boolean field as written (the family of #21333) #21376, which will consume it).
  • Three refusal clauses. None names a backend, because PostgreSQL's server error was measured at where only. The engine evaluates the per-aggregation filter and having itself, and this contract has no driver-bound site flag. A Date renders as Date(ISO) and a non-finite number by name, not as JSON (NaN would otherwise print as null, the null test).
  • The derived case table gains a value group: -1 at $ne on every judged field, and 2 and a Date at every judged position of f_boolean. An array sits at every judged position except the equality slots, where the comparand-shape door refuses one first. Beside them are four passing rows. The 2 / -1 reading rows, and a new 0.5 row, now derive refusals. freshComparand copies a Date and an array per filter, as the number table does. No bigint row is in either table, so every filter a suite builds survives JSON.stringify.
  • The arm, packages/objectql/src/boolean-comparand-declared-type-door.ts: comments only. The walk already routed every comparand to the verdict and carried the refused value as written. number-comparand-declared-type-door.ts (the shared walk) is untouched.
  • Tests: the spec contract suite, the objectql arm suite, and a new REST three-dialect cell packages/rest/src/data-boolean-comparand-door.test.ts, beside data-number-comparand-door.test.ts. In that cell SQLite always runs, and PostgreSQL / MySQL are named skips without their URL.
  • Generated: packages/spec/api-surface/data.json and export-origins/data.json, three added lines each, regenerated by gen:api-surface / gen:export-origins after check:generated named exactly those two.
  • Changesets, one per published package moved, both BREAKING minor with !, a Clause-②: yes (narrowing) line and one ADR-0087 marker (not-required (no-migration-prescription)): .changeset/21382-spec-boolean-comparand-non-string.md and .changeset/21382-objectql-boolean-comparand-non-string.md.

Clause-②, measured

node scripts/pm/check-widening-tells.mjs on git diff 69a12a0952...HEAD:

  • --declaration no: exit 4, three T3 tells. These are the three new rows in packages/spec/api-surface/data.json (lines 469, 505, 507).
  • --declaration yes: exit 0.

So this PR declares Clause-②: yes (narrowing). It adds an export listing row, and what it changes in behaviour is a narrowing. No T1 / T2 / T4 tell fired.

Before and after

A declared boolean field, two rows (rt true, rf false), through engine.find / engine.aggregate. The probe is a scratch script against freshly built dists, not a committed test. A where cell reads implicit, $eq, $ne, and a $in member beside false. Before: base 69a12a0952. After: this branch, spec and objectql source as at 196afd75cd. PostgreSQL ran on a private PostgreSQL 16.14 cluster in this container, started for the run and stopped after.

position comparand before: memory · SQLite · PostgreSQL 16 after, all three
where 2, -1, 0.5, a Date (the card) no row, no row, both, the false row · the same · 500 DATABASE_ERROR at every slot 400 INVALID_FILTER at every slot
per-aggregation filter the same count 0, 0, 2, 1 on all three 400
having on a groupBy of the field the same no group, no group, both, the false group on all three 400
where an array [true] as a $in member (the card) the false row (200) · a driver 400 · a driver 400 400, in the contract's words
per-aggregation filter / having the same count 1 / the false group on all three: the member silently dropped 400
all three [true] at implicit / $eq / $ne 400 (the comparand-shape door) the same
where 2n (in-process) no row · no row · 500 400
where 1n (in-process) no row · the true row · the true row ($ne 1n: both rows on memory) the true row on all three
all three { "a": 1 } 400 (the comparand-type door) the same, same words
all three true, 1, "true" (the controls) the true row, count 1, the true group the same

Premise check, and the dispatch's hypotheses

  • H1 holds. On 69a12a0952, readBooleanComparand returned null for every non-string except 1 / 0, the verdict answered passes, and the reading rows read unread(2) / unread(-1). The card's table reproduces on memory, SQLite and PostgreSQL (rows above). The number contract's non-string branch was the template.
  • H2 holds: no walk change. judgeBooleanComparand already passed value: comparand and form: verdict.form straight through, so widening the verdict reaches where (both spellings), the per-aggregation filter and having with no code change in objectql. The shared walk file is not in the diff.
  • H3: the after-column is re-measured on memory, SQLite and PostgreSQL 16.14 (above). MySQL is NOT MEASURED, because no MySQL server is available in this container. The REST cell's MySQL leg is a named skip.
  • H4 holds. A formula returning boolean is still refused one door earlier, by the unmaterializable-field door, with INVALID_FIELD / 400. The arm suite's NAMED DIVERGENCE test drives every formula row, including the new value row on f_formula_boolean.

The bigint reading

The ruling does not name bigint. Measured on the base, 1n against the boolean field gave different answers per driver at where: no row on InMemoryDriver, the true row on SQLite and PostgreSQL. 2n gave PostgreSQL's 500. That is the card's defect class, through an in-process caller. The comparand-type door rewrites a bigint to its number. It runs AFTER this door on the object spelling of where and on a per-aggregation filter, and BEFORE it on the FilterArray spelling and at having. So:

  • passing a bigint leaves the divergence;
  • refusing every bigint would refuse 1n on one spelling and narrow it on the other;
  • reading it as the number it names gives one answer at every position.

This is not a widening. A bigint passed the verdict before, so it was accepted, and now it is either narrowed correctly or refused. The arm suite pins it on both spellings and at all three positions.

Objects: served by the comparand-type door, deliberately

Triage's list names objects. On the base, an object comparand against the boolean field is already refused with INVALID_FILTER / 400 by the comparand-type door, on all three drivers and at all three positions (table above). The widened verdict passes such a value rather than refusing it a second time. The reason is the one PR #20545 measured: the engine runs the two doors in a different order per position, so a second refusal would answer one mistake with two sets of words. The arm suite pins it: for a plain object, undefined and a Map, the engine's message equals the comparand-type door's own message.

Tests (at 196afd75cd, the final head)

Each heavy run went through scripts/pm/os-verify-lock.sh with exit codes from the lock's VERDICT command-exit lines.

  • @objectstack/spec: filter-boolean-comparand-declared-type.test.ts 34/34. --project local, run as two --shard halves to fit the container's foreground cap: 300 files / 8983 passed + 1 todo, and 299 files / 8580 passed (599 files in all, 0 failed). typecheck exit 0; check:test-typecheck holds its ledger (52 files / 246 errors, none added).
  • @objectstack/objectql: engine-boolean-comparand-declared-type-door.test.ts 34/34, and engine-number-comparand-declared-type-door.test.ts still green (its boolean-arm partition reads the widened verdict). --project local as two shards: 182 files / 3583 passed, and 182 files / 3777 passed. typecheck exit 0 (ledger 40 / 234, none added).
  • @objectstack/rest: data-boolean-comparand-door.test.ts with OS_TEST_POSTGRES_URL set: 6 passed, 3 skipped (the MySQL leg). The SQLite and PostgreSQL legs both ran. --project local (no live URL): 255 files, 4808 passed, 322 skipped. typecheck exit 0 (test layer 0 / 0).
  • Lint, a proven narrowing (the repo-wide pnpm lint is CI's):
    1. Population, from eslint.config.mjs itself: all 5 touched lintable files answer isPathIgnored false. The other 4 changed files are JSON / Markdown, outside its globs.
    2. eslint --no-inline-config --format json over them: 5 files, 0 errors, 0 warnings.
    3. The config never enables type-aware linting (no parserOptions.project, no projectService), so this diff cannot move any untouched file's verdict.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands (no paths) at 196afd75cd derives 90 families; all 90 ran with exit 0, and --ran reconciles 90 derived / 90 run / 0 NOT-MEASURED. check:dual-build-cjs-loads first answered PREREQUISITE NOT MET, because the workspace was not fully built. It was re-run after a full turbo run build, and exited 0.

Ablation (reverse verification)

From the committed head 196afd75cd, with scripts/ablation-replace.mjs in wrap mode. The mutation lives only while the tool's trap is armed. It removes the widening at its one routing line in the verdict: if (form === null) return { verdict: 'passes' }; gains || (globalThis as Record[string, unknown]).ABLATION_21382 === undefined (angle brackets spelled as square brackets here), so every non-string passes again. The string rule is untouched.

  • Mutated leg: anchor 1 to 0, blob 41a24373 to 064410ff. After pnpm --filter @objectstack/spec build, ablation-dist-preflight found the marker in 4 built files (exit 0). Results:
    • spec suite: 3 failed / 31 passed (the non-string verdict, the bigint reading, the value group);
    • objectql arm suite: 5 failed / 29 passed (the GUARD's form set, and the where, per-aggregation filter, having and bigint tests);
    • REST cell: 4 failed / 2 passed / 3 skipped (the refusal tests on both the SQLite and PostgreSQL legs).
    • The control tests stayed green on every suite. The direction is the expected one: red.
  • Restore leg: blob back to 41a24373 equal to HEAD, git diff HEAD empty, and git status --porcelain empty for the whole tree. After a rebuild, ablation-dist-preflight --absent found the marker absent from all 230 built files (exit 0). Results: spec 34/34, objectql 34/34, REST 6 passed / 3 skipped.

Acceptance notes

  • comparandPreview is a private copy in the boolean module. The number module's copy is private, and PR feat(spec)!: FieldSchema refuses a select / radio with neither options nor picklist #21390 holds that file, so this PR does not touch it. Moving both copies into filter-comparand-refusal-text.ts is a consolidation for whichever PR next touches both. No carrier is named.
  • Two comments outside this claim's file surface still describe the string half only: engine.ts around the narrowNumberComparands call ("any other string is refused"), and the shared walk's [#21333] header section. Both are incomplete, not false. No carrier is named.
  • The REST cell's live legs are red-capable and un-run in CI (no job provisions OS_TEST_POSTGRES_URL for packages/rest), as with the number cell. This PR carries their local PostgreSQL run.

Generated by Claude Code

claude added 4 commits October 2, 2026 11:04
…Date and an array

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
…/ MySQL where provisioned

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
…boolean value-form exports

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Oct 2, 2026
@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 20 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/objectql/src/boolean-comparand-declared-type-door.ts, packages/spec/api-surface/data.json, packages/spec/export-origins/data.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

5 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 booleanComparandDoorVerdict))
  • content/docs/api/error-catalog.mdx (via INVALID_FILTER (literal, a string literal in booleanComparandDoorVerdict))
  • content/docs/deployment/validating-metadata.mdx (via INVALID_FILTER (literal, a string literal in booleanComparandDoorVerdict))
  • content/docs/kernel/contracts/data-engine.mdx (via INVALID_FILTER (literal, a string literal in booleanComparandDoorVerdict))
  • content/docs/protocol/objectql/query-syntax.mdx (via INVALID_FILTER (literal, 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 booleanComparandDoorVerdict))
  • content/docs/releases/v17/17-4.mdx (via INVALID_FILTER (literal, a string literal in booleanComparandDoorVerdict))
  • content/docs/releases/v17/17-5.mdx (via INVALID_FILTER (literal, a string literal in booleanComparandDoorVerdict))
  • content/docs/releases/v17/17-6.mdx (via INVALID_FILTER (literal, 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/objectql/src/boolean-comparand-declared-type-door.ts, packages/spec/api-surface/data.json, packages/spec/export-origins/data.json) — pages documenting those are invisible to this run
  • 2 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 85986144c2ef6f379955137677c5cbfb00e194d2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 98e607fd9432e24aa77959002eee0a6876fe2f0a — the merge of head 196afd75cd74fabf2b4e995ad1172540943c82bb into base 85986144c2ef6f379955137677c5cbfb00e194d2, 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 98e607fd9432e24aa77959002eee0a6876fe2f0a && git checkout 98e607fd9432e24aa77959002eee0a6876fe2f0a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 85986144c2ef6f379955137677c5cbfb00e194d2 196afd75cd74fabf2b4e995ad1172540943c82bb && git checkout -B drift-repro 85986144c2ef6f379955137677c5cbfb00e194d2 && git merge --no-ff 196afd75cd74fabf2b4e995ad1172540943c82bb

node scripts/docs-audit/affected-docs.mjs --json 85986144c2ef6f379955137677c5cbfb00e194d2

⚠️ 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 85986144c2ef6f379955137677c5cbfb00e194d2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Oct 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 196afd75cd74fabf2b4e995ad1172540943c82bb
Local-runs: none

Isolated at-tier review of PR #21404 (card #21382), written 2026-10-02T12:19Z. Inputs: the card's body and its four comments (grade 5949762416, unlock 5950643740, claim 5950727570, report 5951925961), #20502's direction 5877498426, commits b05743433b and 9f13c949b0 as git objects, the PR body, its file list and the net diff of the head against main (merge-base 69a12a0952), the check-runs on the head, and scripts/pm/clause2-line.mjs / scripts/pm/check-widening-tells.mjs on main. Nothing was built, run or re-run; the branch was fetched into a ref owned by this review and its sha confirmed equal to the head above.

① Derived judgments

Every accept-set and public-surface change the diff implies, each judged:

  1. The ruled narrowing — right. booleanComparandDoorVerdict now answers door-refusal (INVALID_FILTER / 400) for a number other than 1 / 0 (2, -1, 0.5, NaN, Infinity), a Date (valid or invalid) and an array, at a scalar slot or as a list member, with the new forms number / date / array (nonBooleanValueForm, asked only after readBooleanComparand answered null). That is what triage 5949762416 directs, inheriting 5877498426.
  2. The accepted set is unchanged — right. BOOLEAN_COMPARAND_SPELLINGS is byte-identical on base and head (1, 0, "1", "0", "true", "false"); a boolean still passes (tested one line earlier now, same answer); null reaches nonBooleanValueForm, which answers null for it, so it passes as the null test. The spec suite pins all three, and the arm suite pins true / 1 / null reaching the recording driver unchanged at all three positions.
  3. The string rule is byte-for-byte — right. readBooleanComparand on head differs from base by exactly one inserted line (the bigint branch); every string path, the five string FORM_SENTENCE clauses and BOOLEAN_COMPARAND_REFUSAL_TAIL are unchanged. booleanComparandRefusalMessage swaps shapePreview for comparandPreview, which hands every string to shapePreview, so a string refusal renders identically. The spec suite drives every refused reading row through the verdict and asserts the same form.
  4. Objects stay the comparand-type door's refusal, in its words — right, as b05743433b did. nonBooleanValueForm answers null for a plain object, undefined, a Map, a Symbol and a { $field } reference; normalizeFilterComparandTypes (filter-comparand-type.ts) refuses the first four on every field with INVALID_FILTER / 400 naming the field, and the arm suite pins that the engine's message for { a: 1 }, undefined and a Map equals that door's own, on both spellings. The number verdict makes the same choice for the same reason: the two doors run in a different order per position, so a second refusal would answer one mistake in two sets of words.
  5. Arrays at the equality slots — right. assertListComparandShapes runs before the arm at every position (engine.ts 1028, inside the FilterArray lowering, 17197, 17334), so an array at implicit / $eq / $ne keeps the shape door's words everywhere; the verdict refuses it too, and the case table places its array rows only where the shape door is silent, as the number table does.
  6. bigint — a comparand the ruling's letter does not name; judged the consistent reading, not a widening. The ordering the dev states is verified in engine.ts on main: the comparand-type door, which rewrites a bigint within plus or minus 2^53 to its number, runs AFTER the arm on the object where (line 1080, then 1090) and on the per-aggregation filter (17238, then 17268), and BEFORE it on the FilterArray spelling (filter.zod.ts 2824 inside the lowering, then engine.ts 1169) and at having (17351, then narrowHavingNumberComparands). So since 9f13c949b0 landed, 1n on the FilterArray spelling and at having already arrived at this door as 1 and narrowed to true, while on the other two positions it reached the driver as 1 unnarrowed (the dev's measured split: no row on memory, the true row on SQLite and PostgreSQL). Reading a bigint as Number(bigint) makes the two remaining positions agree with the two that already answered true. Accept-set direction per value: 1n / 0n were not refused before and are not refused now (their meaning is fixed, not widened); every other in-range bigint was accepted (it reached the driver; 2n was a PostgreSQL 500) and is now refused, a narrowing; no spelling enters BOOLEAN_COMPARAND_SPELLINGS (the map is unchanged, and no T2 tell fired), and the 1 this door reads is the one spelling the write side admits, arriving through the platform-wide rewrite filter-comparand-type.ts already applies. Refusing every bigint here would refuse 1n on two positions and narrow it on the other two, the card's own defect class; passing it keeps the measured split. Not an unruled widening and not a second spelling; no escalation. Recorded here so the landing seat sees it beside the ruling, which stands as written.
    Residual, below the verdict line: a bigint OUTSIDE plus or minus 2^53 against a boolean field now gets this door's number clause on the object where and the per-aggregation filter, and the comparand-type door's precision clause on the FilterArray spelling and at having (on the base the precision clause spoke everywhere, because the arm passed every bigint). Status and code agree (400 INVALID_FILTER, the field named); only the clause splits, for a value no producer writes, which the spec suite pins only at the verdict (2n ** 64n). The next head on this door can close it by letting nonBooleanValueForm answer null for a bigint outside the exact range, so the comparand-type door speaks alone, as the number verdict passes every bigint. Not owed before landing.
  7. No second rule in the door — right. judgeBooleanComparand is byte-identical on base and head; boolean-comparand-declared-type-door.ts changes in doc comments only (14 added, 4 removed, all comment lines); number-comparand-declared-type-door.ts and engine.ts are not in the diff. The arm already carried value: comparand and form: verdict.form through, so the widened verdict reaches all four call sites with no engine change.
  8. The public surface — right, and declared. Three additive exports (NON_BOOLEAN_VALUE_FORMS, NonBooleanValueForm, NonBooleanComparandForm), listed in api-surface/data.json (lines 469, 505, 507) and export-origins/data.json. The refusal form widens from NonBooleanStringForm to NonBooleanComparandForm on BooleanComparandDoorVerdict, BooleanComparandRefusalSite and BooleanComparandDoorRefusalCase; booleanComparandDoorVerdict's signature is unchanged. On main no file outside spec / objectql imports the verdict or these types, and no switch over form exists; A boolean comparand is judged only at the engine door: the RLS compile seam and analytics NativeSQL pass a string against a declared boolean field as written (the family of #21333) #21376 is unlanded and will consume the widened type from its first head. The compile-break for an exhaustive consumer is named in the spec changeset (② below).
  9. The pins — every ruled cell is pinned. Memory: the arm suite's recording driver, at where (both spellings, inside $and / $or / $not, and judgeFilter), the per-aggregation filter and having, for 2, -1, 0.5, a Date (scalar and as a $nin member), [true] as a $in member and at $gt, each asserting the arm's clause and zero driver reads; the controls true / 1 at all three positions and null unchanged; 1n / 0n / 2n on both spellings and at all three positions. SQLite: the new REST cell, always on, through POST /api/v1/data/:object/query and engine.find / engine.aggregate, at all three positions (having on both the native and the rows path), for 2, -1, 0.5, [true] and a Date (in-process), with true / 1 / "true" / $ne 1 / a $in member 1 / $eq 0 as controls, each refusal asserting zero reads. PostgreSQL: the same cell's live leg, env-gated and un-run in CI (the body and the file header say so); the report carries its local run (6 passed, 3 skipped, both SQL legs ran). MySQL: a named skip, stated NOT MEASURED. The spec suite pins the verdict for every form on boolean, toggle and formula returning boolean, the three clauses, the rendering of Date(ISO) / NaN / -Infinity, the value group's position coverage and the unchanged accepted set. The dev's ablation of the one routing line turned 3 / 5 / 4 tests red across the three suites with the controls green; read, not re-run.
  10. No text left false. The base's module header ("Everything else passes this verdict — null, a number other than 1 / 0, a bigint, a Date, an array …") and the base test name ("passes … a non-string outside the accepted set — answered as written") were false after the change and are rewritten in the diff; a sweep of the head for the base's phrasings finds only past-tense descriptions in the boolean module and the number module's own header, which describes its own verdict and is still true.

② Semver level

  • Clause-②: yes (narrowing) — right. PR body line 2 and both changesets carry it in the one spelling clause2-line.mjs reads (key, colon, value token, arm in parentheses). Measured correctly: the three added rows in packages/spec/api-surface/data.json are T3 tells under check-widening-tells (a no is refused at exit 4; a yes is never blocked), and the behaviour change is a narrowing, so the arm is (narrowing). The arm differs from b05743433b's no (narrowing), which added the same three-export shape; under the reader on main today this PR's line is the one that passes, and the precedent's would not.
  • Level: both changesets minor, ! in the PR title and both changeset titles, a BREAKING banner, and exactly one ADR-0087 marker not-required (no-migration-prescription), a category check-adr-0087-registration.mjs lists. minor for a breaking narrowing is the launch-window convention check-changeset-no-major.mjs states (the BREAKING banner is the carrier of breaking-ness until GA). The Check Changeset check-run on the head is success.
  • Sentences against the diff. spec: the three forms, the bigint reading, the three exports, the form widening on the three named types, the site value carrying a non-string, the new clauses and the Date(ISO) / non-finite rendering, the value case group with no array at the equality slots, the 0.5 reading row, the FROM → TO line and the "Unchanged" paragraph all match the diff. objectql: "comments only" and "no export or published type of this package changes" match (14 added / 4 removed, all doc comments); its before / after table names InMemoryDriver, SQLite and PostgreSQL 16 only. Neither changeset, the PR body nor either test header claims a MySQL measurement: the body states MySQL NOT MEASURED, and the REST cell's MySQL leg is a named skip.
  • The widened form union is a compile-break for a consumer that switches over it exhaustively; the spec changeset says so in words, and under the convention it rides the BREAKING banner, not a major. No such consumer exists on main; A boolean comparand is judged only at the engine door: the RLS compile seam and analytics NativeSQL pass a string against a declared boolean field as written (the family of #21333) #21376 will meet the widened type from the start.

③ Boundary flags

Report 5951925961 carries no open_questions; its deviations and findings, each answered:

  1. Container restart at about 11:36 UTC; the spec --project local run killed (exit 137). Redone after the restart at the same head with the tree verified clean against the remote: spec local (two shards), objectql local (two shards), rest local, all three typechecks, the lint narrowing, check:dual-build-cjs-loads after a full workspace build, and the dispatch-gates re-derivation (90 derived / 90 run / 0 unrun). Accepted; the head's own check-runs are the gate verdicts and are read below.
  2. Sharded suites. Two --shard halves per package, 599 and 364 files together. Accepted: a shard pair partitions the same file set, and Test Core runs the required suite on the head.
  3. bigint. Answered in ① item 6: the consistent reading, not a widening; the out-of-range clause split is a residual for the next head on this door. No escalation.
  4. Attribution. All four commits carry the model-free trailer pair AGENTS.md prescribes and no model identifier; the PR body carries the session-URL footer alone. Correct: the repo rule outranks the harness reminder, as the report says.
  5. The measured Clause-② arm differs from b05743433b's. Answered in ②: measured, and the right line under the reader on main.
  6. The optional REST cell added; no InMemoryDriver test consumer (check:driver-memory-census). Accepted: the memory row is pinned by the arm suite's recording driver, which asserts the refusal lands before any driver is resolved, and the REST cell carries the SQL dialects. The census gate ran exit 0 among the dev's 90 families and sits in Lint & Repo Gates on the head.
  7. Finding, noted and not filed: comparandPreview duplicated in the number and boolean modules. Right not to file: no class (a / b / c) under Directive chore: version packages #10, and PR feat(spec)!: FieldSchema refuses a select / radio with neither options nor picklist #21390 holds the number module. Note for whoever consolidates: the two copies are not identical (the boolean copy adds the non-finite number branch), so the shared helper in filter-comparand-refusal-text.ts must take the superset.
  8. Finding, noted and not filed: two string-half comments outside the claim's surface. Judged for falsity, sentence by sentence. engine.ts 1076 to 1079 (the [#21333] note at the narrowNumberComparands call) says the boolean arm narrows the six spellings "and any other string is refused INVALID_FILTER / 400 — the one place a bare query parameter's "true" becomes true": every clause still holds, and it nowhere says a non-string passes. number-comparand-declared-type-door.ts 156 to 166 (## [#21333] …and the boolean arm, at all three positions) says the same of strings and nothing of non-strings: still true. Neither sentence is false, so neither is owed before landing; the next PR that touches either file may complete it with the [#21382] half. Nothing is filed by this review.
  9. premise_still_valid: true is confirmed by the base reading: readBooleanComparand on 69a12a0952 answered null for every non-string but 1 / 0, so the verdict passed them to the drivers as written.

Check-runs on the head, read 2026-10-02T12:18Z (34 runs): 26 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)), 0 red. Of the seven required contexts, Lint & Repo Gates, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL) and Governed Surface Queue Guard are success; Check Changeset, Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Type Check · workspace, Test Core (2/6) and Test Core (4/6) are success. Still in_progress at that read, and NOT reported as passed here: TypeScript Type Check, Test Core (1/6), Test Core (3/6), Test Core (5/6), Test Core (6/6). This verdict judges the contract (①, ② and ③); the landing pre-check that every check is green is the landing seat's, taken on the head's check-runs once those five conclude.

Implemented-by: claude/issue-21382-boolean-comparand-non-string
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 12:26
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 12:27
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 45efcfa Oct 2, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21382-boolean-comparand-non-string branch October 2, 2026 13:16
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/l tests tooling

Projects

None yet

2 participants