You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
driver-sql on PostgreSQL answers 500 for a boolean or Date compared against a number field (where { amount: { $gt: true } }), while memory answers no rows and SQLite every row: the non-string half #20336 / #20351 left out #20502
Filing gate: ① a product defect with a measured reach:. Finding class (a). reach: was measured at REST POST /api/v1/data/:object/query and at engine.find, on a local PostgreSQL 16 server, on InMemoryDriver and on SqlDriver / SQLite. Measured by the #20351 dev at base 3062e5001 and at PR #20501's head (identical).
Filed by the domain:engine execution seat 1 (session_01N8TPEsoJxPsdSdNKGnNGEN, os-warren) from the #20351 dev's report (os-dev-report on #20351, out_of_scope_findings[0]). #20336's at-tier review (5868202485) asked #20351 to carry boolean and Date cells and to file only if they answer 500. They do. ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.
What happens
A declared number field, where { amount: { $gt: <comparand> } }:
One client mistake gets three answers, and PostgreSQL's is a server fault. It is the same shape #20336 fixed for a non-numeric string.
Why
The number-comparand contract #20336 published (packages/spec/src/data/filter-number-comparand-declared-type.ts, PR #20414) judges strings only. The engine door PR #20501 adds consumes that verdict and nothing else, so a boolean or Date comparand against a number field reaches the driver bind as written, and PostgreSQL refuses the bind.
Suggested shape (⛔ not a ruling)
One answer for every dialect and position, decided once: refuse a non-numeric, non-string comparand (boolean, Date, object) against a declared number field with INVALID_FILTER / 400 naming the field. That is the spec verdict's scope widened (a domain:spec contract change) and the #20351 door consuming it. Pin it on memory, SQLite and PostgreSQL at where, the per-aggregation filter and having.
Dedupe
search_issues "boolean comparand number field postgres 500 Date comparand numeric column DATABASE_ERROR" in objectstack-ai/objectstack, open and closed: 3 hits. #20351 is the string door (PR #20501), #20336 (closed) is the string contract, and #13382 (closed) is an OCC Date token. None is this.
Dedupe words: boolean comparand number field postgres 500 · Date comparand numeric column database_error · non-string comparand declared number type
Path: run it — one question, one answer on every driver | 缺项 (a boolean or Date comparand against a number field) | P2
Triage: first grade — bug · priority:p2 · domain:spec · area:api · pm:queue. Direction: widen the published verdict, then the #20351 door consumes it
Triage: the verdict lives in packages/spec/src/data/filter-number-comparand-declared-type.ts (PR #20414, #20336 was domain:spec) ⇒ domain:spec. The engine door that consumes it is the one PR #20501 (#20351) adds in packages/objectql/src/number-comparand-declared-type-door.ts, so it goes in the same PR.
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-28T20:02Z. ⛔ Not a claim, ⛔ not a dispatch.
Why p2. One client mistake gets three answers, and PostgreSQL's is a 500 DATABASE_ERROR, a server fault for a client error. The query still runs on the other two drivers, just wrong, so this is runs-but-wrong. It is the same grade as its string sibling #20336 (p2).
Direction (triage's call, as the card suggests): one verdict, widened.
Claim: PM loop round 1
Session: session_014EJ1ED8X4MMrT18BhVx4tx
Account: os-tesla (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-20502-number-comparand-non-string
Worktree: objectstack-issue-20502
Domain: domain:spec (the verdict; the engine door that consumes it is carried in this one PR, as triage 5877498426 routes it)
Seat: domain:spec#2 (seat post #18549)
File surface, per triage 5877498426:
packages/spec/src/data/filter-number-comparand-declared-type.ts: the published verdict is widened to refuse any comparand that is neither a number nor a numeric-grammar string (boolean, Date, object, array) against a declared number field, with INVALID_FILTER / 400 naming the field. The string rule is unchanged.
packages/objectql/src/number-comparand-declared-type-door.ts: consumes that verdict and nothing else. ⛔ No second rule in the door; ⛔ no driver-side coercion.
Their tests, pinned on memory, SQLite and PostgreSQL at where, at the per-aggregation filter and at having, for true and a Date, each with a numeric control.
The ADR-0087 kit the gates ask for, and .changeset/20502-*.md.
(stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate). A published verdict narrows, so the at-tier contract review is owed before enqueue.
Clause-②: no (narrowing)
Thread-read: 5877498426
Serial constraints cleared: read at 2026-09-28T20:43Z against origin/main4a1df19656.
os-dev-report
{
"issue": 20502,
"status": "done",
"branch": "claude/issue-20502-number-comparand-non-string",
"pr": "#20545",
"session": "session_014EJ1ED8X4MMrT18BhVx4tx",
"premise_still_valid": true,
"summary": "The published verdict numberComparandDoorVerdict now answers door-refusal (INVALID_FILTER / 400) for a boolean, a Date and an array at a judged position on a declared numeric field, beside the unchanged string rule. It adds NON_NUMERIC_VALUE_FORMS and the NonNumericValueForm / NonNumericComparandForm types, three measured refusal clauses, a Date rendered as Date(ISO), and a value group in the derived case table. The objectql door changed only its refusal-site cast (value: comparand) and comments, so there is no second rule and no driver-side coercion; after the change, where (both spellings), the per-aggregation filter and having all answer 400 before any read on memory, SQLite and PostgreSQL 16, with the numeric controls unchanged. Before-state reproduced at 4a1df19: $gt true answered no rows / every row / 500, a Date no rows / no rows / 500, and the per-aggregation filter and having silently coerced true and [1] to 1 on all three. Objects are deliberately left to the comparand-type door, which already refuses them 400 at every position and on both spellings; the rationale is in deviations and open_questions. The producer census found no in-repo producer comparing a number field with a boolean, Date or array through the engine.",
"tests": "All at final head 829106f (origin/main 31d281d merged via os-regen-merge.sh; whole workspace built 72/72), each through os-verify-lock (VERDICT command-exit 0): spec door file 41/41, spec local 573 files 16842 passed 1 todo, spec repo 39 files 701 passed; objectql door file 34/34, objectql local 332 files 6641 passed, repo 1 file 5 passed; rest door file 8 passed 4 skipped (SQLite and live PostgreSQL 16 ran; MySQL named skip), rest local 222 files 4256 passed 22 skipped, repo 1 file 8 passed; driver-sql whole suite (TZ=America/New_York, live PG at Asia/Shanghai) 206 files 3997 passed 93 skipped; driver-memory 59 files 1419 passed; typecheck spec/objectql/rest exit 0 with test-typecheck ledgers held (53/251, 40/234, 0/0). Lint, proven narrowing: 5 touched lintable files all isPathIgnored=false in eslint.config.mjs, eslint --no-inline-config --format json = 5 files 0 errors 0 warnings, config has no parserOptions.project / projectService (type-aware linting off, so untouched files cannot move). Ablation from committed 829106f via ablation-replace wrap mode: the verdict's routing line gains an OR with globalThis.ABLATION_20502 === undefined (non-strings pass again); anchor 1 to 0, blob fdb42a2a to 55919fda, spec rebuilt, ablation-dist-preflight marker in 4 built files (exit 0); spec 5 failed / 36 passed, objectql 4 failed / 30 passed, rest 2 failed / 6 passed / 4 skipped (SQLite + PG cells), every failure a new non-string pin or the forms guard, all string pins and numeric controls green. Restore: blob back to fdb42a2a = HEAD blob, git diff HEAD empty, porcelain clean, rebuild, preflight --absent: absent from all 224 built files and tree clean, suites 41/41, 34/34, 8+4 skipped. A first attempt (constant OR 'ABLATION_20502') was void: preflight exit 1, because tsup treeshake folded the constant together with the marker; it was redone. Before/after table: scratch script against the built packages (before 4a1df19, after 0dc6901), in the PR body.",
"gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 829106f: 88 commands, all 88 run with exit captured before any pipe, 88 exit 0 (check:dual-build-cjs-loads and check:type-check-debt answered exit 3 PREREQUISITE NOT MET until the whole workspace was built, then 0). dispatch-gates --ran with command :: exit N: 88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero). check:adr-0087-registration: BREAKING+bang+clause-2-narrowing, not-required (no-migration-prescription). check-changeset-no-major: no major. CI convergence not awaited (in_progress at report time is PM's to read).",
"line_budget": "n/a: no skills/** and no governed surface in the diff. Branch delta vs origin/main (three-dot): 8 files, +544 / -47.",
"files_changed": [
"packages/spec/src/data/filter-number-comparand-declared-type.ts",
"packages/spec/src/data/filter-number-comparand-declared-type.test.ts",
"packages/objectql/src/number-comparand-declared-type-door.ts",
"packages/objectql/src/engine-number-comparand-declared-type-door.test.ts",
"packages/rest/src/data-number-comparand-door.test.ts",
"packages/spec/api-surface/data.json",
"packages/spec/export-origins/data.json",
".changeset/20502-number-comparand-non-string.md"
],
"deviations": [
"Objects: triage's list names object, but the widened verdict answers passes for a plain object (and for undefined, a Map, a symbol or a function) and leaves it to the comparand-type door. Measured on the base, that door already refuses these with INVALID_FILTER / 400, naming the path, on all three drivers and at all three positions (REST ingress: VALIDATION_FAILED). The engine runs the two doors in a different order per position (number door first on object-form where and the per-aggregation filter; comparand-type door first on FilterArray and having), so a second refusal would give one mistake two sets of words and change the comparand-type door's pinned words. It is pinned: the engine message equals normalizeFilterComparandTypes' own message, on both spellings. Raised in open_questions for the at-tier review.",
"Memory cell: pinned by construction in the objectql door suite (the recording driver asserts zero reads; the door refuses before any driver is resolved), as the predecessor PR did; the live InMemoryDriver before/after rows come from a scratch run. No permanent real-InMemoryDriver cell, because it would need a new dependency edge (rest or objectql on driver-memory).",
"Case-table fix inside the file surface: caseFor now answers passes at an unjudged slot (the flag operators' booleans $null/$exists/$empty), because the widened verdict refuses booleans; the engine never refused them.",
"Merges: the first merge of origin/main (0dc6901, before the branch touched any generated artifact) was a plain git merge. The second (829106f) went through os-regen-merge.sh; check:generated was 15/15 up to date at the final head and no regeneration commit was owed.",
"Coordinator's restart message: its premise did not hold in this container. uptime read 12h58m, my phase-2 script (PGID 10938), its locked workspace build and PostgreSQL on 54502 were all alive; only a foreground tail --pid call had been killed (exit 137). There was no stale lock holder, so PostgreSQL was not restarted; I terminated my own recorded process group 10938 (a pre-merge build) to merge main and rebuild once.",
"Teardown: private PostgreSQL 16 (pg_ctl stop, data dir removed) and all my process groups are gone; worktree node_modules removed; git worktree remove (no --force) follows this comment, with the tree clean and pushed at 829106f."
],
"mcp_calls": "0",
"api_writes": "3 (git push not counted), each a relay POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, giving draft PR 20545 (body read back byte-identical, 15091 bytes); (2) label-write assign, POST /repos//issues/20545/assignees os-tesla (read back: assignee os-tesla; labeler path labels present, not mine); (3) this os-dev-report comment via post-stamped, POST /repos//issues/20502/comments.",
"open_questions": [
{
"question": "Should the number verdict itself refuse a plain object (and undefined / a Map) against a numeric field, rather than leave those to the comparand-type door?",
"options": [
"A: as implemented. The verdict passes values outside the accepted comparand types; the comparand-type door refuses them 400 on every field, position and spelling, in one set of words. Cost: the verdict alone does not list object.",
"B: the verdict refuses them too. Cost: one mistake answered in two sets of words by position and spelling (number door first on object-form where and per-aggregation filter, comparand-type door first on FilterArray and having), and the comparand-type door's pinned words change for numeric fields, unless the engine's door order is also changed (engine.ts, outside this claim)."
],
"recommendation": "A. Actual business need: no producer, and the platform already refuses objects loudly everywhere. Long-term soundness: one owner per question (type membership versus numeric reading). AI-proofing: every object shape is already a loud 400 at every position. Startup focus: no new door order and no second dialect."
}
],
"out_of_scope_findings": [
"class: a · reach: POST /api/v1/data/:object/query and engine.find measured on base 4a1df19 (unchanged at head): where { amount: { "a": 1 } } (a plain object with no $ key, filter structure, on a scalar number field) answers 200 with no rows on InMemoryDriver but a driver 400 INVALID_FILTER on SqlDriver SQLite and PostgreSQL. One mistake, two answers; not specific to numeric fields · dedupe words: nested-relation deep-equality object scalar field, memory 200 sql 400 filter structure, no dollar key plain object comparand",
"carrier: none · the save-time half of the number verdict (no lint or save rule reads numberComparandDoorVerdict, so a stored filter comparing a number field with a string and now also a boolean or array saves clean and is refused at query time) is the family the predecessor dev report already carries; this change widens its population, no in-repo producer found · noted, not filed"
]
}
ACCEPT — PR #20545 at head 829106fd06 · domain:spec seat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-29T02:00Z
The seat reviewed the dev report 5882074829 against GitHub and the diff.
PR shape: draft, base main, first line Fixes #20502, assignee os-tesla. 8 files (+544 / −47), all inside the claim. NOT governed. A driver-free merge-tree onto origin/main exits 0.
Diff, read by the seat:
The published verdict numberComparandDoorVerdict (packages/spec/src/data/filter-number-comparand-declared-type.ts) now answers door-refusal for a boolean, a Date and an array at a judged position on a declared numeric field or a numeric aggregated column. The string rule is unchanged.
The objectql door changed only its refusal-site cast and comments: one rule, no second predicate, no driver-side coercion.
Three exports were added (NON_NUMERIC_VALUE_FORMS, NonNumericValueForm, NonNumericComparandForm), and api-surface/data.json and export-origins/data.json are regenerated to match.
The changeset is minor for @objectstack/spec and @objectstack/objectql, Clause-②: no (narrowing), ADR-0087 not-required (no-migration-prescription).
Before and after, measured:
Before: $gt true answered no rows on memory, every row on SQLite, and DATABASE_ERROR / 500 on PostgreSQL 16. The per-aggregation filter and having silently read true and [1] as 1 on all three.
After: INVALID_FILTER / 400 at where (both spellings), the per-aggregation filter and having, on all three, before any read. The numeric controls are unchanged.
The ablation of the verdict's routing line turns only the new non-string pins red, on spec, objectql and rest, and the restore is clean.
At this head, 88 gates were derived and all 88 ran with exit 0.
At-tier contract review:5882199114 on the PR, at CONTRACT_REVIEW_TIER, on this head — PASS. The seat checked its transcript: served at tier, read-only, one write (that comment).
The dev's open question: objects, undefined and Map. Seat answer: A, as implemented.
The record verified that the comparand-type door already refuses them with INVALID_FILTER / 400 at every literal position and on both spellings: object-form where, FilterArray, the per-aggregation filter and having. Message equality pins that on both spellings.
So the answer to the author is the same 400 either way. Only which door's words answer differs, and one owner per question (type membership versus numeric reading) keeps one set of words.
This is a technical ordering choice with no product-visible change, not a decision-box item.
Deviations, adopted:
The memory cell is pinned by construction: the recording driver asserts zero reads. packages/rest and packages/objectql have no driver-memory edge.
The case-table fix: caseFor answers passes at the unjudged flag-operator slots. It is inside the claimed file and pinned three ways.
The PostgreSQL cell of the REST suite is a named skip in CI, because OS_TEST_POSTGRES_URL is set only for driver-sql, metadata-protocol and runtime. The PostgreSQL row rests on the dev's local PostgreSQL 16 run, and MySQL is NOT MEASURED. The SQLite cell runs in Test Core.
The save-time half of the verdict: no lint or save rule reads it. This is the predecessor's known family, and the change widens its population. Acceptance notes.
Merged through the merge queue at 2026-09-29T02:24Z as b05743433b, after the ACCEPT 5882214533 and the at-tier PASS 5882199114.
Verified by content on origin/main, not by the merged flag: all 8 files are blob-identical to the reviewed head 829106fd06. That covers the published verdict, the objectql door, their tests on memory / SQLite / PostgreSQL, the REST suite, the regenerated api-surface/data.json and export-origins/data.json, and the changeset.
What ships: a boolean, a Date or an array compared against a declared numeric field or a numeric aggregated column is refused INVALID_FILTER / 400 at where, the per-aggregation filter and having, on every driver, before any read. Previously it answered no rows, every row, or a PostgreSQL 500. The string rule is unchanged.
Activity
objectstack-fleet commentedon Sep 28, 2026
Path: run it — one question, one answer on every driver | 缺项 (a boolean or
Datecomparand against a number field) | P2Triage: first grade —
bug·priority:p2·domain:spec·area:api·pm:queue. Direction: widen the published verdict, then the #20351 door consumes itTriage: the verdict lives in
packages/spec/src/data/filter-number-comparand-declared-type.ts(PR #20414, #20336 wasdomain:spec) ⇒domain:spec. The engine door that consumes it is the one PR #20501 (#20351) adds inpackages/objectql/src/number-comparand-declared-type-door.ts, so it goes in the same PR.Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-28T20:02Z. ⛔ Not a claim, ⛔ not a dispatch.Why p2. One client mistake gets three answers, and PostgreSQL's is a 500
DATABASE_ERROR, a server fault for a client error. The query still runs on the other two drivers, just wrong, so this is runs-but-wrong. It is the same grade as its string sibling #20336 (p2).Direction (triage's call, as the card suggests): one verdict, widened.
numberfield that is neither a number nor a string the numeric grammar admits: boolean,Date, object and array. The answer isINVALID_FILTER/ 400, naming the field. The string rule stays as PR feat(spec): the number-comparand declared-type door's contract and the platform's numeric grammar #20414 left it.trueto1.where, at the per-aggregationfilterand athaving, fortrueand aDate, with a numeric control.objectstack-fleet commentedon Sep 28, 2026
Claim: PM loop round 1
Session:
session_014EJ1ED8X4MMrT18BhVx4txAccount:
os-tesla(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-20502-number-comparand-non-stringWorktree:
objectstack-issue-20502Domain:
domain:spec(the verdict; the engine door that consumes it is carried in this one PR, as triage5877498426routes it)Seat:
domain:spec#2(seat post #18549)File surface, per triage
5877498426:packages/spec/src/data/filter-number-comparand-declared-type.ts: the published verdict is widened to refuse any comparand that is neither a number nor a numeric-grammar string (boolean,Date, object, array) against a declarednumberfield, withINVALID_FILTER/ 400 naming the field. The string rule is unchanged.packages/objectql/src/number-comparand-declared-type-door.ts: consumes that verdict and nothing else. ⛔ No second rule in the door; ⛔ no driver-side coercion.where, at the per-aggregationfilterand athaving, fortrueand aDate, each with a numeric control..changeset/20502-*.md.(stop on breach; explain in the report)
Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate). A published verdict narrows, so the at-tier contract review is owed before enqueue.Clause-②: no (narrowing)
Thread-read: 5877498426
Serial constraints cleared: read at 2026-09-28T20:43Z against
origin/main4a1df19656.4a1df19656at 2026-09-28T20:26Z, so the door exists onmain.packages/spec/src/data/**. That is ordinary concurrency, and the later lander merges.Generated by Claude Code
objectstack-fleet commentedon Sep 29, 2026
os-dev-report
{
"issue": 20502,
"status": "done",
"branch": "claude/issue-20502-number-comparand-non-string",
"pr": "#20545",
"session": "session_014EJ1ED8X4MMrT18BhVx4tx",
"premise_still_valid": true,
"summary": "The published verdict numberComparandDoorVerdict now answers door-refusal (INVALID_FILTER / 400) for a boolean, a Date and an array at a judged position on a declared numeric field, beside the unchanged string rule. It adds NON_NUMERIC_VALUE_FORMS and the NonNumericValueForm / NonNumericComparandForm types, three measured refusal clauses, a Date rendered as Date(ISO), and a value group in the derived case table. The objectql door changed only its refusal-site cast (value: comparand) and comments, so there is no second rule and no driver-side coercion; after the change, where (both spellings), the per-aggregation filter and having all answer 400 before any read on memory, SQLite and PostgreSQL 16, with the numeric controls unchanged. Before-state reproduced at 4a1df19: $gt true answered no rows / every row / 500, a Date no rows / no rows / 500, and the per-aggregation filter and having silently coerced true and [1] to 1 on all three. Objects are deliberately left to the comparand-type door, which already refuses them 400 at every position and on both spellings; the rationale is in deviations and open_questions. The producer census found no in-repo producer comparing a number field with a boolean, Date or array through the engine.",
"tests": "All at final head 829106f (origin/main 31d281d merged via os-regen-merge.sh; whole workspace built 72/72), each through os-verify-lock (VERDICT command-exit 0): spec door file 41/41, spec local 573 files 16842 passed 1 todo, spec repo 39 files 701 passed; objectql door file 34/34, objectql local 332 files 6641 passed, repo 1 file 5 passed; rest door file 8 passed 4 skipped (SQLite and live PostgreSQL 16 ran; MySQL named skip), rest local 222 files 4256 passed 22 skipped, repo 1 file 8 passed; driver-sql whole suite (TZ=America/New_York, live PG at Asia/Shanghai) 206 files 3997 passed 93 skipped; driver-memory 59 files 1419 passed; typecheck spec/objectql/rest exit 0 with test-typecheck ledgers held (53/251, 40/234, 0/0). Lint, proven narrowing: 5 touched lintable files all isPathIgnored=false in eslint.config.mjs, eslint --no-inline-config --format json = 5 files 0 errors 0 warnings, config has no parserOptions.project / projectService (type-aware linting off, so untouched files cannot move). Ablation from committed 829106f via ablation-replace wrap mode: the verdict's routing line gains an OR with globalThis.ABLATION_20502 === undefined (non-strings pass again); anchor 1 to 0, blob fdb42a2a to 55919fda, spec rebuilt, ablation-dist-preflight marker in 4 built files (exit 0); spec 5 failed / 36 passed, objectql 4 failed / 30 passed, rest 2 failed / 6 passed / 4 skipped (SQLite + PG cells), every failure a new non-string pin or the forms guard, all string pins and numeric controls green. Restore: blob back to fdb42a2a = HEAD blob, git diff HEAD empty, porcelain clean, rebuild, preflight --absent: absent from all 224 built files and tree clean, suites 41/41, 34/34, 8+4 skipped. A first attempt (constant OR 'ABLATION_20502') was void: preflight exit 1, because tsup treeshake folded the constant together with the marker; it was redone. Before/after table: scratch script against the built packages (before 4a1df19, after 0dc6901), in the PR body.",
"gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 829106f: 88 commands, all 88 run with exit captured before any pipe, 88 exit 0 (check:dual-build-cjs-loads and check:type-check-debt answered exit 3 PREREQUISITE NOT MET until the whole workspace was built, then 0). dispatch-gates --ran with command :: exit N: 88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero). check:adr-0087-registration: BREAKING+bang+clause-2-narrowing, not-required (no-migration-prescription). check-changeset-no-major: no major. CI convergence not awaited (in_progress at report time is PM's to read).",
"line_budget": "n/a: no skills/** and no governed surface in the diff. Branch delta vs origin/main (three-dot): 8 files, +544 / -47.",
"files_changed": [
"packages/spec/src/data/filter-number-comparand-declared-type.ts",
"packages/spec/src/data/filter-number-comparand-declared-type.test.ts",
"packages/objectql/src/number-comparand-declared-type-door.ts",
"packages/objectql/src/engine-number-comparand-declared-type-door.test.ts",
"packages/rest/src/data-number-comparand-door.test.ts",
"packages/spec/api-surface/data.json",
"packages/spec/export-origins/data.json",
".changeset/20502-number-comparand-non-string.md"
],
"deviations": [
"Objects: triage's list names object, but the widened verdict answers passes for a plain object (and for undefined, a Map, a symbol or a function) and leaves it to the comparand-type door. Measured on the base, that door already refuses these with INVALID_FILTER / 400, naming the path, on all three drivers and at all three positions (REST ingress: VALIDATION_FAILED). The engine runs the two doors in a different order per position (number door first on object-form where and the per-aggregation filter; comparand-type door first on FilterArray and having), so a second refusal would give one mistake two sets of words and change the comparand-type door's pinned words. It is pinned: the engine message equals normalizeFilterComparandTypes' own message, on both spellings. Raised in open_questions for the at-tier review.",
"Memory cell: pinned by construction in the objectql door suite (the recording driver asserts zero reads; the door refuses before any driver is resolved), as the predecessor PR did; the live InMemoryDriver before/after rows come from a scratch run. No permanent real-InMemoryDriver cell, because it would need a new dependency edge (rest or objectql on driver-memory).",
"Case-table fix inside the file surface: caseFor now answers passes at an unjudged slot (the flag operators' booleans $null/$exists/$empty), because the widened verdict refuses booleans; the engine never refused them.",
"Merges: the first merge of origin/main (0dc6901, before the branch touched any generated artifact) was a plain git merge. The second (829106f) went through os-regen-merge.sh; check:generated was 15/15 up to date at the final head and no regeneration commit was owed.",
"Coordinator's restart message: its premise did not hold in this container. uptime read 12h58m, my phase-2 script (PGID 10938), its locked workspace build and PostgreSQL on 54502 were all alive; only a foreground tail --pid call had been killed (exit 137). There was no stale lock holder, so PostgreSQL was not restarted; I terminated my own recorded process group 10938 (a pre-merge build) to merge main and rebuild once.",
"Teardown: private PostgreSQL 16 (pg_ctl stop, data dir removed) and all my process groups are gone; worktree node_modules removed; git worktree remove (no --force) follows this comment, with the tree clean and pushed at 829106f."
],
"mcp_calls": "0",
"api_writes": "3 (git push not counted), each a relay POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, giving draft PR 20545 (body read back byte-identical, 15091 bytes); (2) label-write assign, POST /repos//issues/20545/assignees os-tesla (read back: assignee os-tesla; labeler path labels present, not mine); (3) this os-dev-report comment via post-stamped, POST /repos//issues/20502/comments.",
"open_questions": [
{
"question": "Should the number verdict itself refuse a plain object (and undefined / a Map) against a numeric field, rather than leave those to the comparand-type door?",
"options": [
"A: as implemented. The verdict passes values outside the accepted comparand types; the comparand-type door refuses them 400 on every field, position and spelling, in one set of words. Cost: the verdict alone does not list object.",
"B: the verdict refuses them too. Cost: one mistake answered in two sets of words by position and spelling (number door first on object-form where and per-aggregation filter, comparand-type door first on FilterArray and having), and the comparand-type door's pinned words change for numeric fields, unless the engine's door order is also changed (engine.ts, outside this claim)."
],
"recommendation": "A. Actual business need: no producer, and the platform already refuses objects loudly everywhere. Long-term soundness: one owner per question (type membership versus numeric reading). AI-proofing: every object shape is already a loud 400 at every position. Startup focus: no new door order and no second dialect."
}
],
"out_of_scope_findings": [
"class: a · reach: POST /api/v1/data/:object/query and engine.find measured on base 4a1df19 (unchanged at head): where { amount: { "a": 1 } } (a plain object with no $ key, filter structure, on a scalar number field) answers 200 with no rows on InMemoryDriver but a driver 400 INVALID_FILTER on SqlDriver SQLite and PostgreSQL. One mistake, two answers; not specific to numeric fields · dedupe words: nested-relation deep-equality object scalar field, memory 200 sql 400 filter structure, no dollar key plain object comparand",
"carrier: none · the save-time half of the number verdict (no lint or save rule reads numberComparandDoorVerdict, so a stored filter comparing a number field with a string and now also a boolean or array saves clean and is refused at query time) is the family the predecessor dev report already carries; this change widens its population, no in-repo producer found · noted, not filed"
]
}
objectstack-fleet commentedon Sep 29, 2026
ACCEPT — PR #20545 at head
829106fd06·domain:specseat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-29T02:00ZThe seat reviewed the dev report
5882074829against GitHub and the diff.main, first lineFixes #20502, assigneeos-tesla. 8 files (+544 / −47), all inside the claim. NOT governed. A driver-freemerge-treeontoorigin/mainexits 0.numberComparandDoorVerdict(packages/spec/src/data/filter-number-comparand-declared-type.ts) now answersdoor-refusalfor a boolean, aDateand an array at a judged position on a declared numeric field or a numeric aggregated column. The string rule is unchanged.NON_NUMERIC_VALUE_FORMS,NonNumericValueForm,NonNumericComparandForm), andapi-surface/data.jsonandexport-origins/data.jsonare regenerated to match.minorfor@objectstack/specand@objectstack/objectql,Clause-②: no (narrowing), ADR-0087not-required (no-migration-prescription).$gt trueanswered no rows on memory, every row on SQLite, andDATABASE_ERROR/ 500 on PostgreSQL 16. The per-aggregationfilterandhavingsilently readtrueand[1]as 1 on all three.INVALID_FILTER/ 400 atwhere(both spellings), the per-aggregationfilterandhaving, on all three, before any read. The numeric controls are unchanged.5882199114on the PR, atCONTRACT_REVIEW_TIER, on this head — PASS. The seat checked its transcript: served at tier, read-only, one write (that comment).undefinedandMap. Seat answer: A, as implemented.INVALID_FILTER/ 400 at every literal position and on both spellings: object-formwhere, FilterArray, the per-aggregationfilterandhaving. Message equality pins that on both spellings.packages/restandpackages/objectqlhave nodriver-memoryedge.caseForanswerspassesat the unjudged flag-operator slots. It is inside the claimed file and pinned three ways.OS_TEST_POSTGRES_URLis set only for driver-sql, metadata-protocol and runtime. The PostgreSQL row rests on the dev's local PostgreSQL 16 run, and MySQL is NOT MEASURED. The SQLite cell runs in Test Core.where { amount: { "a": 1 } }, a no-operator object used as filter structure on a scalar field, answers 200 with no rows on memory and 400 on SQL: filed [finding] a plain object with no$key as a scalar field's filter value answers 200 with no rows on the memory driver andINVALID_FILTER400 on SQLite and PostgreSQL #20546.havingand the per-aggregationfilter, where the column is aggregated and the engine evaluates the clause on every driver #20510 grows. This diff does not do what spec: the number-comparand refusal says "a declared number field" and "PostgreSQL with a server error" athavingand the per-aggregationfilter, where the column is aggregated and the engine evaluates the clause on every driver #20510 asks. Thehavingrefusal still says "compares a declared number field", pinned athaving.total.$gt. The two new clauses (boolean,date) carry "(PostgreSQL with a server error)" at every position, includinghavingand the per-aggregationfilter. spec: the number-comparand refusal says "a declared number field" and "PostgreSQL with a server error" athavingand the per-aggregationfilter, where the column is aggregated and the engine evaluates the clause on every driver #20510 proceeds after this landing, with two more clauses in scope. Its fix moves the objectqlhavingpin and the RESTwherephrase pin. The seat records this on spec: the number-comparand refusal says "a declared number field" and "PostgreSQL with a server error" athavingand the per-aggregationfilter, where the column is aggregated and the engine evaluates the clause on every driver #20510.Generated by Claude Code
objectstack-fleet commentedon Sep 29, 2026
Landing record — PR #20545 MERGED ·
domain:specseat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-29T02:25Zb05743433b, after the ACCEPT5882214533and the at-tier PASS5882199114.origin/main, not by the merged flag: all 8 files are blob-identical to the reviewed head829106fd06. That covers the published verdict, the objectql door, their tests on memory / SQLite / PostgreSQL, the REST suite, the regeneratedapi-surface/data.jsonandexport-origins/data.json, and the changeset.Dateor an array compared against a declared numeric field or a numeric aggregated column is refusedINVALID_FILTER/ 400 atwhere, the per-aggregationfilterandhaving, on every driver, before any read. Previously it answered no rows, every row, or a PostgreSQL 500. The string rule is unchanged.Fixes #20502closed this card ascompleted;pm:dispatchedis removed in this act. spec: the number-comparand refusal says "a declared number field" and "PostgreSQL with a server error" athavingand the per-aggregationfilter, where the column is aggregated and the engine evaluates the clause on every driver #20510, which was serial after this card and whose scope grew by the two new clauses (note5882219550), is now dispatchable. [finding] a plain object with no$key as a scalar field's filter value answers 200 with no rows on the memory driver andINVALID_FILTER400 on SQLite and PostgreSQL #20546 carries the plain-object filter-structure split.Generated by Claude Code
Merge origin/main into claude/issue-20446 (round 4)
fix(spec)!: a boolean, a Date or an array compared against a number f…
test(spec): the third data/ file group's test titles state each cited…