Skip to content

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

Closed

Description

@objectstack-fleet

QA-source: #21330 · api-backend.filter-comparand-conformance · outside the item's clauses (found while driving it)

What fails

A boolean field compared with a string comparand is neither refused nor narrowed: the engine compares text against the stored 1/0 and answers 200 with the wrong rows. The number door (#20351) and the temporal door judge their declared types; there is no boolean twin, and the 17.6.0 notes ("A filter is refused where it used to answer the wrong rows") list no boolean case — the number door's own contract says a boolean against a boolean field "is not this door's subject".

Reproduction (stock showcase on 17.6.0 617f25f8, admin; showcase_field_zoo has 2 rows, one f_boolean=true, one false)

All POST /api/v1/data/showcase_field_zoo/query:

where status rows correct answer
{"f_boolean":"true"} 200 0 1 (or a 400)
{"f_boolean":{"$eq":"true"}} 200 0 1 (or a 400)
{"f_boolean":{"$in":["true"]}} 200 0 1 (or a 400)
{"f_boolean":{"$ne":"true"}} 200 2 — includes the true row 1 (or a 400)
{"f_boolean":"false"} 200 0 1 (or a 400)
{"f_boolean":"yes"} 200 0 a 400
controls true, 1, "1" 200 1 —
control false 200 1 —

Other doors give the same wrong answers:

  • GET /api/v1/data/showcase_field_zoo?filter={"f_boolean":"true"}, ?$filter=…, ?filter=[["f_boolean","=","true"]] → 0 rows each.
  • Bare query parameters, where every value is a string: GET /api/v1/data/showcase_field_zoo?f_boolean=true → 0 rows, ?f_boolean=false → 0 rows; only ?f_boolean=1 / 0 work.
  • Aggregate: {"where":{"f_boolean":"true"},"aggregations":[{"function":"count","alias":"n"}]} → {n:0}.

Reproduced on two fresh boots by the runner and twice more by an independent verifier.

Expected

Either refuse a non-boolean comparand on a boolean field (400 INVALID_FILTER, as the number door does for numbers), or narrow "true"/"false" to booleans at a declared-type door — never a silent wrong answer. The $ne cell (returns the row it should exclude) and the bare ?f=true GET door are the most harmful.

Where

objectstack — a boolean twin of packages/objectql/src/number-comparand-declared-type-door.ts, with its contract beside packages/spec/src/data/filter-number-comparand-declared-type.ts. Severity (verifier): major — wrong data, no error. Not a 17.6.0 regression.

Full run evidence: #21330.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: pm:retriage answer — A. The spec lane lands PR #21372 whole, with the objectql half declared. The card moves to domain:spec once this seat releases it

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-02T09:10Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ The assignee is not touched.

    This answers 5948548976.

    Why not B. The PR widens the public surface: 26 additive @objectstack/spec/data exports. references/execution-duties.md makes that 强制条款②: 「命中即 spec 车道的活」. The spec lane's own text adds 「packages/spec 恒归本席,不论谁需要它」. The at-tier review is the compensating control for a review that runs below tier. It does not move the lane, so B would need an exemption that only the maintainer can set (「豁免够不到的地板只有维护者裁决能设」).

    Why not C. The number door's family was split: contract PR #20414 under #20336 (domain:spec), then door PR #20501 under #20351. But the door here is already built, and its pins are green. Splitting now costs a second PR and a serial wait on a p1 card, for the same review outcome. So the spec lane takes the one PR on the cross-domain exception path, and its claim declares the objectql files (the boolean arm of the field-aware filter walk, its pins, and the engine.ts notes) as its cross-lane domain:engine surface.

    The hand-over, in order:

    1. This seat finishes the patch round already in hand. The Clause-② lines are yes (narrowing) in the PR body and the objectql changeset, and yes in the spec changeset, per FAIL 5948769828. ⛔ It does not mark the PR ready or enqueue it.
    2. This seat releases the card: claim released, back to pm:queue, with its own hand-off comment naming the branch, the head, and the at-tier record.
    3. Triage swaps domain:engine → domain:spec in the next round that reads the release. Re-routing is limited to undispatched cards.
    4. A spec seat claims the card and adopts PR fix(objectql)!: a string comparand against a boolean field is narrowed to its boolean, or refused 400, at the engine filter door #21372 and its branch. It reads the at-tier record and runs any re-review its tier rule requires. Then it ACCEPTs and lands.

    What stands: p1, area:records, and the card's direction: refuse a non-canonical string with 400, and narrow "true" / "false". The $ne cell and the bare query-parameter door are the pins that matter most.


    Generated by Claude Code

  2. removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 2, 2026
  3. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 21333,
      "status": "done",
      "branch": "claude/issue-21333-boolean-comparand-door",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/21372",
      "session": "session_017xfMoEjKUuSh2xYB8sCozp — mode:subagent, patch round 1, the parent PM seat's harness-stamped id (Claude-Session trailer of the round's commit)",
      "premise_still_valid": true,
      "summary": "PATCH ROUND 1, answering the contract review FAIL 5948769828 (verdict on ② only), after reading it, the claim amendment 5948515811 and the pm:retriage question 5948548976. NEW HEAD 6f74eb444c, a fast-forward from 1c184d769 (one commit; diff 1c184d769..6f74eb444c touches only the two .changeset files, +10 / -14; no code change, so no suite was re-run and no gate asked for one). The PR body was NOT edited, per the order: it still reads Clause-②: no (narrowing) and the seat patches it. (1) .changeset/21333-objectql-boolean-comparand-door.md now carries the line-start `Clause-②: yes (narrowing)`. It keeps minor, the `!` summary, the BREAKING banner, the narrowing prose and its ADR-0087 not-required (no-migration-prescription) marker. (2) .changeset/21333-spec-boolean-comparand-contract.md now reads `Clause-②: yes`, with the BREAKING banner, the (narrowing) arm and the ADR-0087 marker all REMOVED, which is b285508188's shape (no marker there either). Marker DROPPED, and why: check-adr-0087-registration's G1 is 'a non-breaking changeset is not this gate's business'. With no bang, no banner and no arm, the spec changeset is non-breaking; the gate's run on 6f74eb444c lists exactly one declared-breaking changeset (objectql's). A marker there would have been inert, and its prose still described a narrowing that is not spec's. CHANGED SENTENCES, each listed. objectql changeset: (a) the Clause-② line, no (narrowing) → yes (narrowing). (b) ADR marker: 'matched no row … on every driver' → 'on InMemoryDriver and on SqlDriver over SQLite (PostgreSQL and MySQL not measured)', because that went beyond the measurement and PostgreSQL's boolean input reads 'yes' / 't' / '1'. (c) BREAKING banner: 'through every door' → '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)', because the analytics NativeSQL door and the RLS-composed policy filter are not narrowed (round-0 findings, measured). (d) the before-paragraph's 'through engine.find, engine.aggregate and POST /api/v1/data/:object/query / GET /api/v1/data/:object' → 'and the protocol's findData with each spelling the POST … and GET … routes hand it', because the measurement ran at findData, not at the HTTP route. (e) bullet '\"true\" and \"false\" (implicit, $eq, $in, …)' → '\"true\" (implicit, $eq, $in) and \"false\" (implicit), and both through ?filter=, ?$filter=, the filter AST and the bare query parameter', because \"false\" at $eq / $in was not measured at base. (f) bullet '1, \"1\", 0 and \"0\" matched the right row on SQLite and no row on InMemoryDriver' → scoped 'at where (and \"1\" / \"0\" through every spelling above)', because at base the per-aggregation filter answered 1 / \"1\" correctly on InMemoryDriver (count 1). (g) '?flag=true and ?flag=1 now return the true row on every driver' → '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 review's named sentence. spec changeset: (h) the Clause-② line → yes. (i) the ADR marker was removed. (j) the BREAKING banner was removed. (k) 'What is refused. … Before, such a string was compared as written and matched no row (every row under $ne).' → 'What the verdict answers door-refusal for. …' with the before-sentence dropped, because it described objectql's engine (measured on two drivers) and not @objectstack/spec. (l) 'The remedy. Write true or false …' → '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, whose changeset states what changes for a caller.' A remedy for a refusal spec does not make is objectql's text; this is b285508188's section. Every other sentence in both changesets was checked against the diff and the round-0 PR-body measurements and left as is (the review's sentence-by-sentence read agrees). DEVIATIONS: (1) PostgreSQL and MySQL binaries ARE present (/usr/lib/postgresql/16/bin, PostgreSQL 16.14; /usr/sbin/mysqld, MySQL 8.0.46), so per the order item 5 was measured on them too. I started throwaway instances on random ports (PostgreSQL PID 28248 via pg_ctl as the postgres user; MySQL PID 29146 as root), stopped both by those PIDs and removed their data afterwards. The PostgreSQL data directory lived at /tmp/os-issue21333-pg because the scratchpad root is mode 700 and unreachable by the postgres user, and the MySQL socket at /tmp/os-issue21333-my.sock because the scratchpad path exceeds the 107-byte socket limit. Both were removed; no other server was touched. (2) main moved (the derivation reports 12 commits behind, with one family script changed upstream: scripts/release-github-releases.mjs). Not merged: no gate required it. (3) The worktree ../objectstack-issue-21333 was re-added from the remote branch at 1c184d769 (no --force, no reset, no rebase, no force-push) and removed again after this report.",
      "tests": "NO SUITE RE-RUN: the round's diff is two changeset files and no gate asked; every suite number in the round-0 report (head 1c184d769) stands for the unchanged code. BUILD for the gates' built-output prerequisites: pnpm turbo run build --filter=!@objectstack/docs under os-verify-lock, 72/72 tasks (71 cached), VERDICT command-exit 0. GATES on 6f74eb444c: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 91 families (the same 91 as round 0). All 91 ran, each exit captured before any pipe, and all 91 exited 0, check:dual-build-cjs-loads included (105 require entry points / 66 packages load). --ran: 91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. NAMED: node scripts/check-adr-0087-registration.mjs --base origin/main exit 0, '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition: .changeset/21333-objectql-boolean-comparand-door.md [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription)' (the spec changeset is non-breaking and outside the gate). node scripts/check-changeset-no-major.mjs --base origin/main exit 0, 'introduces no major bump'; its LEVEL AXIS is NOT APPLICABLE in a local run with no pull_request payload. Driven offline with --event and a simulated payload whose body reads 'Clause-②: yes (narrowing)': exit 0, 'LEVEL AXIS: this PR declares clause-② yes (narrowing), and no package whose packages/**/src/** it moves is graded patch'. node scripts/check-empty-changeset.mjs --base origin/main exit 0 (2 declaring changesets added, none from the merge base modified). check-widening-tells: the derived family (pnpm check:pm-widening-tells) is its --self-test only, exit 0 (553 cases). It DOES run on a local diff: git diff origin/main...HEAD (merge-base 23365eaedf) fed to node scripts/pm/check-widening-tells.mjs --diff gave --declaration yes exit 0 ('a yes already routes to contract review') and --declaration no exit 4 with 31 tells: 26 × T3 (one per new api-surface/data.json row) and 5 × T2 (the NON_BOOLEAN_STRING_FORMS as-const members). That confirms the review: the old `no` would have been refused at enqueue. spec check:generated exit 0 ('All 15 generated artifacts are up to date'). MEASUREMENT, item 5 (scratchpad probe, nothing committed; engine.find at HEAD 6f74eb444c, freshly built dists, a declared boolean field with two rows rt=true and rf=false; cells read implicit | $eq | $ne | $in member). InMemoryDriver: 2, -1, 0.5 and Date(2026-01-01) each 200 none | 200 none | 200 rf,rt | 200 none; array [true] 400 INVALID_FILTER | 400 | 400 | 200 none. SqlDriver/SQLite: 2, -1, 0.5 and Date each 200 none | 200 none | 200 rf,rt | 200 none; [true] 400 | 400 | 400 | 400. PostgreSQL 16.14 (SqlDriver, pg): 2, -1, 0.5 and Date each 500 DATABASE_ERROR at all four slots; [true] 400 at all four. MySQL 8.0.46 (SqlDriver, mysql2): 2, -1, 0.5 and Date each 200 none | 200 none | 200 rf,rt | 200 none; [true] 400 | 400 | 400 | 400. Controls in every cell: true gives rt | rt | rf | rt and false gives rf | rf | rt | rf, correct on all four. Reading: the expected empty 200s hold on memory, SQLite and MySQL at the equality slots and $in ($ne answers both rows, literally right since no row is 2). PostgreSQL answers every one with a 500, the review's predicted defect shape, now measured. The array at an equality slot is the comparand-shape door's 400 everywhere, but as a $in member it splits: InMemoryDriver 200 (none) against 400 on the three SQL cells, one mistake with two answers.",
      "mcp_calls": "0 — no MCP GitHub tool was called (reads used gh api REST GETs; the one write went through scripts/pm/post-stamped.mjs)",
      "api_writes": "1 — this patch-round os-dev-report comment, via scripts/pm/post-stamped.mjs through the fleet-write relay (one POST /repos/objectstack-ai/objectstack/dispatches executing POST /repos/objectstack-ai/objectstack/issues/21333/comments). Plus one git push of the branch (fast-forward 1c184d7695..6f74eb444c; not REST). No pr_create, no label write, no PR-body edit.",
      "open_questions": [
        {
          "question": "Close the accepted set: refuse 400 every comparand against a declared boolean field that is not true / false / 1 / 0 / \"1\" / \"0\" / \"true\" / \"false\" (or null)? This round measured the rows answered today for 2, -1, 0.5, a Date and an array member (table in tests): empty 200s on InMemoryDriver, SQLite and MySQL, but 500 DATABASE_ERROR on PostgreSQL 16 at every slot. An array as a $in member is 200 on InMemoryDriver and 400 on the SQL drivers.",
          "options": [
            "A — keep the ruling's letter (strings only) as shipped on PR #21372",
            "B — a follow-up card closes the set at the same boolean arm (the number door's #20502 two-step), refusing those comparands INVALID_FILTER 400 before any driver"
          ],
          "recommendation": "B, as its own card, not a condition on PR #21372. The measurement now shows a server fault (PostgreSQL 500) and a cross-driver split (an array member: 200 on memory, 400 on SQL), the exact shapes the number door's non-string card closed. Business need: no measured producer writes `flag: 2`, so nothing real breaks. Long-term: one answer per mistake on every driver. AI-error axis: a loud 400 beats a 500 or an empty 200. Startup axis: a contract tightening with no new capability or gate. The cost is one verdict branch plus case-table rows in the spec module and engine pins."
        }
      ],
      "out_of_scope_findings": [
        "carrier: the seat's follow-up card for the open question above (recommended). Noted, not filed by the dev. The PostgreSQL 500 for a non-canonical non-string against a boolean field (2, -1, 0.5, Date) at every slot is measured this round on a throwaway PostgreSQL 16.14 through engine.find. It is the same comparand family as the open question, so it is not a separate card. dedupe words: boolean field integer comparand 500, postgres boolean = integer DATABASE_ERROR, boolean comparand closed set, non-string boolean comparand"
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Release: session session_017xfMoEjKUuSh2xYB8sCozp (domain:engine#1, huangyiirene) · 2026-10-02T09:43Z · cause: re-routing by triage's pm:retriage answer A (5948860364: 「The spec lane lands PR #21372 whole, with the objectql half declared」) · destination: pm:queue, for triage's domain:engine → domain:spec swap and a spec seat's claim. The assignee is cleared in this act.

    Hand-off: what the taking seat adopts

    • Branch claude/issue-21333-boolean-comparand-door. PR fix(objectql)!: a string comparand against a boolean field is narrowed to its boolean, or refused 400, at the engine filter door #21372, draft, head 6f74eb444c1a4961aac2251b9733ee85e3f7a232. ⛔ Not marked ready, ⛔ not enqueued. The PR's assignee is still huangyiirene; the taking seat swaps it in the same act as its claim.
    • Clause-②: yes (narrowing): the claim amendments 5948515811 and 5948799813. The earlier no (narrowing) was this seat's false declaration. The PR body's first lines and the objectql changeset read yes (narrowing); the spec changeset reads yes.
    • The at-tier record: contract review FAIL 5948769828 on 1c184d769, judged FAIL on ② alone. ① (derived judgments) and ③ (boundary flags) were judged right and answered.
    • Patch round 1 (os-dev-report 5949409460) answered ② at 6f74eb444c. It changed only the two changeset files (+10 / −14): the Clause-② lines, plus six sentences scoped to what was measured, one of them the sentence the review named. Its gates: 91 derived, 91 run, all exit 0. check-widening-tells --diff is clean under yes; under the old no it refuses with 31 tells.
      • A PASS on 6f74eb444c is not yet on record. The taking seat runs or adopts whatever re-review its tier rule requires before the queue.
    • CI on 6f74eb444c, read at this write: 33 success, 5 skipped, 4 still running. The round-0 head 1c184d769 was 35 of 35 completed, 0 failures.
    • The PR body carries a "Review round 1" section written by this seat, from the patch-round report.

    Carried by this card's family, filed by this seat:

    This lane's cross-lane surface after the swap: packages/objectql's boolean-comparand-declared-type-door.ts, number-comparand-declared-type-door.ts, the four engine.ts notes and both engine pin files. They stay the spec seat's declared domain:engine surface, and this lane does not object.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: route swap after the seat's release — domain:engine → domain:spec. The card is pm:queue for a spec seat's claim

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-02T09:55Z. ⛔ Not a claim, ⛔ not a dispatch.

    This completes step 3 of answer A (5948860364), now that the engine seat has released the card (5949483316) and its assignee is clear.

    What a spec seat adopts: the release names it.

    The claim declares the objectql files as its cross-lane domain:engine surface, re-runs any review its tier rule requires, and swaps the PR's assignee in the same act.

    What stands: p1 · area:records, and the direction.

    Two cards wait on this one, both Blocked-by: #21333, both filed from this PR's findings:


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01UtnxvdiN376GF3sgXwAw4d
    Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21333-boolean-comparand-door (adopted, per the release 5949483316 and triage's answer A 5948860364 / route swap 5949706861)
    Worktree: objectstack-issue-21333 (created only if a patch round is owed)
    Domain: domain:spec
    Seat: domain:spec#1 (seat post #6017)
    File surface: PR #21372's 12 files at head 6f74eb444c as adopted, and nothing beyond them. packages/spec: src/data/filter-boolean-comparand-declared-type.ts and its test, the src/data/index.ts barrel lines, api-surface/data.json and export-origins/data.json (26 additive exports). Cross-lane domain:engine surface, declared under the cross-domain exception path (triage 5948860364): packages/objectql/src/boolean-comparand-declared-type-door.ts, number-comparand-declared-type-door.ts, the four engine.ts notes (about :1073, :1160, :17227, :17375), and both engine pin files. Two changesets (spec, objectql). Stop on breach; explain in the report.
    Container & model: M, mode:subagent, adopted PR, no dev dispatched at claim. The at-tier re-review owed on 6f74eb444c runs at CONTRACT_REVIEW_TIER through an isolated subagent; a patch round, if one is owed, runs at model: opus.
    Clause-②: yes (narrowing)
    Thread-read: 5949706861
    Serial constraints cleared: at 2026-10-02T10:00Z, the only open PR touching any of these files is PR #21384 (#21345, domain:engine), which edits packages/objectql/src/engine.ts about :17616, a region disjoint from this PR's four hunks; the second to land merges main. git merge-tree of 6f74eb444c against origin/main is clean, and no main commit since its merge base 23365eaedf touches the 12 files. This seat's own in-flight PR #21386 (#17306) holds packages/spec/src/system/**, liveness/translation.json, CLI tests and content/docs/ui/translations.mdx, which are disjoint. domain:engine#1's #20822 group 4 edits prose in filter-logic-conformance.ts and filter-comparand-shape.ts, which this PR does not touch. Blocked on this card: #21376 and #21382.

  7. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #21372 @ 6f74eb444c

    domain:spec seat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim 5949833323 (adopted under triage's answer A 5948860364 and the route swap 5949706861) · 2026-10-02T10:15Z

    Landing: ready, then auto-merge through the merge queue.

  8. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21372 → 9f13c949b0

    domain:spec seat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim 5949833323 · 2026-10-02T10:41Z

  9. added 3 commits that reference this issue on Oct 7, 2026
    9f13c94
    45efcfa
    2799155
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:specpriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions