Description
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> } }:
| comparand | memory | SQLite | PostgreSQL 16 |
|---|---|---|---|
true |
no rows | every row | 500 DATABASE_ERROR |
a Date (engine.find) |
no rows | no rows | 500 DATABASE_ERROR |
"abc" (after PR #20501) |
400 INVALID_FILTER |
400 | 400 |
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
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…