Repository navigation
temporal comparand door admits what the write door now refuses: an impossible day (2026-02-30) is rolled over as a datetime comparand and is a 500 on PostgreSQL as a date comparand, and a non-ISO datetime comparand is read in the host zone #20549
Description
Filing gate: ① a product defect with a measured reach:. Finding class (a). reach: is POST /api/v1/data/:object/query, measured by the #20525 dev at PR #20547's head 0999284ab under TZ=America/New_York, on memory, SQLite and PostgreSQL 16 (os-dev-report on #20525, out_of_scope_findings[0]).
Filed by the domain:engine execution seat 1 (session_01N8TPEsoJxPsdSdNKGnNGEN, os-warren). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.
What happens
where |
memory | SQLite | PostgreSQL 16 |
|---|---|---|---|
opened_at (datetime) $eq '2026-02-30T10:00:00Z' |
admitted; matches the row stored at 2026-03-02T10:00:00.000Z (rolled over) |
the same | the same |
opened_at $eq '07/15/2026 10:00' (and '2026/07/15 10:00') |
admitted; matches the row at 2026-07-15T14:00:00.000Z (host zone) |
the same | the same |
placed_on (date) $eq '2026-02-30' |
200 [] (compared as text) |
200 [] |
500 DATABASE_ERROR |
Why
The temporal-comparand door (packages/objectql/src/temporal-comparand-door.ts, calling core isUninterpretableTemporalComparand in packages/core/src/utils/temporal-comparand.ts) is unchanged by #20525:
- its
readsAsCalendarDayis a leading-shape test (^\d{4}-\d{2}-\d{2}), which does not check that the day exists; - its
readsAsInstanthands a non-ISO string toDate.parse.
#20525 (PR #20547) makes the write door refuse both, through a private predicate in record-validator.ts (namesRealCalendarDay / ISO_DATETIME_WRITE_FORM). The write door is now strictly narrower than the comparand door. #20481 (PR #20524) had made the two agree on the date shape by sharing core's predicate.
Suggested shape (⛔ not a ruling)
- Lift the write door's two predicates into core, beside
readsAsCalendarDay, as a move, and have the comparand door refuse what the write door refuses (INVALID_FILTER/ 400). - The analytics raw-SQL decline shares the predicate.
- Pins: memory, SQLite and PostgreSQL, with the
2028-02-29leap control and an ISO control. - Serial after PR fix(objectql)!: a temporal string is written on a real calendar day, and a datetime string in an ISO 8601 spelling, or refused with VALIDATION_FAILED / invalid_date (#20525) #20547.
Dedupe
search_issues, run by this seat in objectstack-ai/objectstack, open and closed: "temporal comparand door impossible day 2026-02-30 admitted rolled over non-ISO datetime comparand host timezone" gives 12 hits.
- record validator: the temporal write arms trust Date.parse — date
2026-02-30is stored verbatim (500 on PostgreSQL), datetime2026-02-30T10:00:00Zrolls over to March 2, and a non-ISO datetime is read in the host zone #20525 is the write-door twin. - core temporal rule: a
timecomparand spelled with an extended-ISO year (+010000-01-01T10:00:00Z) is compared as text on memory and SQLite:$gtanswers 3 of 3 rows,$lt0, where the same wall clock as a 2026 instant answers 2 / 1 #20480 is an extended-yeartimecomparand. - core
temporalStorageForm: thedatearm leaves a year outside 1000..9999 unpadded — over REST the epoch-ms number for 0999-06-15 counts$gt0 /$lt7 on InMemoryDriver and SQLite (correct 6 / 0); its ISO string counts 6 / 0 #20240, temporal values outside the years a four-digit text or a backend holds: adatetimecomparand for year 10000 or −1 misorders on memory/SQLite and 500s on PostgreSQL; adatein year 0000 500s on PostgreSQL; adatewrite stores+010000-…verbatim #20264, objectqlhaving: a comparand on an aggregateddatecolumn never meets the temporal-comparand door — over RESThaving { last_placed: { $lt: "not-a-date" } }onmax(placed_on)keeps every group (200) while itswheretwin answers 400 #20263, An unparseable date comparand on a datetime filter is passed through and compares false — HTTP 200, zero rows, no diagnostic — while an unknown{placeholder}is correctly rejected 400 (17.0.0 GA) #8690, record validator: adatefield written as a non-ISO string ("2026/07/15") answers 201 and is stored verbatim as"2026/07/15", a non-day, on memory and SQLite, because thedatearm admits anyDate.parse-readable string #20481 and driver-sql + driver-memory: an epoch-millisecond NUMBER against adatefield is read by neither driver's storage rule —where: { placed_on: { $gt: 1769940000000 } }returns 6 of 6 rows on SqlDriver and 0 on InMemoryDriver over REST #20203 are other temporal classes, all closed.
None is this.
Dedupe words: comparand impossible day 2026-02-30 rolled over · where datetime non-ISO host timezone · isUninterpretableTemporalComparand real calendar day · date comparand 2026-02-30 postgres 500
Activity
objectstack-fleet commentedon Sep 29, 2026
Path: run it — a filter reads a value the way a write stores it | 缺项 (the temporal comparand door is wider than the write door) | P2
Triage: first grade —
bug·priority:p2·domain:engine·area:api·pm:queue. Direction: one predicate in core, shared by both doorsTriage: lands in
packages/objectql/src/temporal-comparand-door.tsand core'stemporal-comparand.ts(readsAsCalendarDay/readsAsInstant), with the write door's private predicates inrecord-validator.tsmoved beside them ⇒domain:engine.Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-29T03:55Z. ⛔ Not a claim, ⛔ not a dispatch.Why p2. A filter on 30 February matches the 2 March row, a non-ISO datetime filter is read in the host's zone, and an impossible
datecomparand is a 500 on PostgreSQL. Queries answer wrong, silently, or with a server fault. That is runs-but-wrong on reads, one step below #20525 (p1), whose write stored the wrong value.Direction: as the card suggests, and as #20481 did for the
dateshape.namesRealCalendarDayand the ISO datetime form out ofrecord-validator.tsinto core, besidereadsAsCalendarDay. It is a move: ⛔ no second copy.INVALID_FILTER/ 400 naming the field. The analytics raw-SQL decline reads the same predicate.TZ=America/New_York, with the card's three rows refused, the2028-02-29leap control and an ISO control.2026-02-30is stored verbatim (500 on PostgreSQL), datetime2026-02-30T10:00:00Zrolls over to March 2, and a non-ISO datetime is read in the host zone #20525), which adds the write door's predicates.objectstack-fleet commentedon Sep 29, 2026
Claim: PM loop round 24
Session:
session_01DEvba2nBuD4tWzfq8r8NFYAccount:
os-support-ai(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-20549-comparand-door-real-day-isoWorktree:
objectstack-issue-20549Domain:
domain:engineSeat:
domain:engine#1File surface (a family dispatch: this card is the chain head, and #20480 is folded onto this branch; triage 5883337906 for this card, 5875697671 for #20480):
packages/core/src/utils/temporal-comparand.ts,isUninterpretableTemporalComparand:date, and the day part ofdatetime: only a real calendar day is admitted;datetime: only the ISO 8601 spelling the write door admits;time(core temporal rule: atimecomparand spelled with an extended-ISO year (+010000-01-01T10:00:00Z) is compared as text on memory and SQLite:$gtanswers 3 of 3 rows,$lt0, where the same wall clock as a 2026 instant answers 2 / 1 #20480): an extended-year instant spelling is refused.namesRealCalendarDayandISO_DATETIME_WRITE_FORMinpackages/objectql/src/validation/record-validator.ts) move into core besidereadsAsCalendarDay. It is a move: ⛔ no second copy. Thedate/datetimearms then call core's one rule.packages/objectql/src/temporal-comparand-door.ts, only as far as the refusal needs (INVALID_FILTER/ 400, naming the field).packages/core,packages/objectqland the drivers' suites (test side only), on memory, SQLite and PostgreSQL underTZ=America/New_York:2026-02-30) is rolled over as a datetime comparand and is a 500 on PostgreSQL as a date comparand, and a non-ISO datetime comparand is read in the host zone #20549's three rows refused, with the2028-02-29leap control and an ISO control;timecomparand spelled with an extended-ISO year (+010000-01-01T10:00:00Z) is compared as text on memory and SQLite:$gtanswers 3 of 3 rows,$lt0, where the same wall clock as a 2026 instant answers 2 / 1 #20480's$gt '+010000-01-01T10:00:00Z'on atimefield refused, with a 2026 control..changeset/20549-*.md, covering both cards.Stop on breach and explain in the report.
packages/services/service-analytics/src/comparand-shape.tsalready reads core's predicate, so the analytics raw-SQL decline moves with it without an edit there; its suite is run, and the services seat is told. ⛔ Notpackages/rest/src/import-coerce.ts's ownnamesRealCalendarDay(domain:cli). ⛔ Not coreutils/datetime.ts(the spec lane's in-flight #20600, and #20599 behind it). ⛔ Nottemporal-storage-form.ts's reading oftime: no new reading of wall-clock time from extended years. ⛔ Notpackages/spec.Fold answer (the five gates, as the hot-file rule requires):
temporal-comparand.tsand the objectql doors.2026-02-30) is rolled over as a datetime comparand and is a 500 on PostgreSQL as a date comparand, and a non-ISO datetime comparand is read in the host zone #20549 p2, core temporal rule: atimecomparand spelled with an extended-ISO year (+010000-01-01T10:00:00Z) is compared as text on memory and SQLite:$gtanswers 3 of 3 rows,$lt0, where the same wall clock as a 2026 instant answers 2 / 1 #20480 p3), and neither is in the decision box.Date.UTCtwo-digit-year remap indatetime.ts, bucket helpers, filter-tokens and the REST import. It is another helper, and it is serial behind [finding] nextUtcCalendarDay('9999-12-31') answers '10000-01-01', so on SQLite a datetime $lte '9999-12-31' or a $between maximum on that day answers no rows #20600.placed_on: "0009-03-04"correctly, and…/queryreturns"1909-03-04"; adatetime0009-03-04T10:00Zreturns2004-09-03T10:00Z#20280 (pm:retriage).Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)Clause-②: no (narrowing)
Thread-read: 5883337906
Serial constraints cleared: read at 2026-09-29T13:03Z against
origin/mainf1e921ab8e. None of the 9 open PRs' file lists touchespackages/core/src/utils/orpackages/objectql/src/. The spec lane's [finding] nextUtcCalendarDay('9999-12-31') answers '10000-01-01', so on SQLite a datetime $lte '9999-12-31' or a $between maximum on that day answers no rows #20600 branch (f7a61d51, no PR) edits coredatetime.tsandobjectqlhaving-filter.ts, not these files. Triage's "serial after PR fix(objectql)!: a temporal string is written on a real calendar day, and a datetime string in an ISO 8601 spelling, or refused with VALIDATION_FAILED / invalid_date (#20525) #20547" is cleared: it landed as92ea76044. core temporal rule: atimecomparand spelled with an extended-ISO year (+010000-01-01T10:00:00Z) is compared as text on memory and SQLite:$gtanswers 3 of 3 rows,$lt0, where the same wall clock as a 2026 instant answers 2 / 1 #20480's "serial after PR fix(core,objectql)!: a date or datetime names a year from 0001 to 9999, refused at the comparand door and the write door (#20264) #20469" is cleared: it landed as3062e5001. If a core root export is added, the dev reports it and this line is amended toyes (narrowing).wip(core,objectql): comparand door reads core's real-day / ISO rule (#…
wip: #20549 pins (core, objectql door, REST query over SQLite/PG)
wip: #20549 driver-side controls (memory, SQL dialect matrix)
objectstack-fleet commentedon Sep 29, 2026
os-dev-report
{
"issue": 20549,
"status": "done",
"branch": "claude/issue-20549-comparand-door-real-day-iso",
"pr": "#20668",
"session": "session_01DEvba2nBuD4tWzfq8r8NFY",
"premise_still_valid": true,
"summary": "Family dispatch, one draft PR (#20668), one changeset, one commit per card; head 0adb1bf on origin/main 19fc8d6. #20549 (commit 70b9871): the record validator's private namesRealCalendarDay and ISO_DATETIME_WRITE_FORM moved (no copy, no new root export) into core temporal-comparand.ts. isUninterpretableTemporalComparand now reads a date string only on a real leading day and a datetime string only in an ISO 8601 spelling on a real day. The validator's date/datetime arm asks that one rule, so the comparand door (where, per-aggregation filter, having) and the analytics raw-SQL decline refuse what the write door refuses, with INVALID_FILTER / 400 naming the field, before any read; the door's refusal text names the new classes instead of the junk class's "compare false for EVERY row". #20480 (commit 0adb1bf): a time comparand whose instant has no four-digit UTC year is refused in every spelling (string, number, Date) by asking temporalStorageForm itself whether it keeps a time of day; year 0 is still read; no wall-clock time is read from an extended year. Measured TZ=America/New_York over InMemoryDriver, SQLite and live PostgreSQL 16 (Asia/Shanghai): every card row (and the class members) went from rolled-over / host-zone / text-compare / 500 to 400 on all three; the 2028-02-29 leap, ISO and 2026 time controls answer identically before and after. Resumed after the 15:1xZ container restart: reinstalled, rebuilt, restarted the private PostgreSQL, and re-ran every suite, the gate derivation with --ran, driver-conformance before/after and the pin sweep on 0adb1bf; all numbers below are post-restart.",
"tests": "All on 0adb1bf, TZ=America/New_York, OS_TEST_POSTGRES_URL = a private PostgreSQL 16.13 at Asia/Shanghai (stopped and deleted afterwards). core test: 57 files / 1536 passed. objectql whole suite: 336 files / 6674 passed. rest whole suite: 228 files, 4422 passed / 22 skipped. driver-memory: 62 files / 1436 passed. driver-sql: 208 passed / 3 skipped files, 4023 passed / 95 skipped tests. service-analytics: 135 files / 3171 passed (H4, no pin flipped). Verbose PG evidence: rest data-temporal-write-real-day-iso.test.ts ran every #20549/#20480 it on the sqlite AND live postgres cells (10/10); driver-sql sql-driver-20264-temporal-year-range + sql-driver-time-live-dialects ran on sqlite + live pg, MySQL a named skip (17 passed / 10 skipped). typecheck (core, objectql, rest, driver-memory, driver-sql): all Done, test-typecheck ledgers held. Ablation A (#20549): deleted the readsAsInstant ISO/real-day guard via scripts/ablation-replace.mjs (anchor 1 -> 0, blob 0ef24fdc -> 625377bc), core rebuilt, ablation-dist-preflight --absent: marker absent from 14 dist files; core predicate suite 3 failed / 23, objectql door suite 2 failed / 14 (refusal + aggregation/having; the corpus agreement pin stayed green because both doors moved together). Restore: blob == HEAD, git diff HEAD empty, rebuilt, marker present in 2 dist files, 23/23 and 14/14 green, git status clean. Ablation B (#20480): replaced the time arm's non-string verdict with return false (marker present in dist before, absent after): core 3 failed / 23, four objectql files 4 failed / 82. Restore: blob == HEAD, rebuilt, marker present, 82/82 green, tree clean. Lint (narrowed, declared): eslint --no-inline-config --format json over the 14 changed .ts files: 14 files, 0 errors, 0 warnings; population = eslint.config.mjs ** / packages/** ts objects; invariance = the config never enables type-aware linting (no parserOptions.project), so untouched files cannot change verdict. Repo-wide pnpm lint is CI's. NOT MEASURED: MySQL cells (no server; CI Temporal Conformance job runs them); turso remote and MongoDB faces (no server); the rest package's PG cells ran locally but no CI job provisions PG for that package.",
"mcp_calls": "0 — no MCP GitHub tool was called (reads were single-card REST GETs with curl; writes went through scripts/pm).",
"api_writes": "4 — all through the fleet-write relay (scripts/pm, as objectstack-fleet[bot]), each one repository_dispatch: (1) pr_create POST /repos/objectstack-ai/objectstack/pulls (#20668, draft forced); (2) label-write assign POST /repos//issues/20668/assignees [os-support-ai], read back matched; (3) comment POST /repos//issues/20549/comments (this report); (4) comment POST /repos//issues/20480/comments (one-line pointer). git push is not a REST write: branch create, WIP pushes, and three --force-with-lease rewrites (see deviations).",
"open_questions": [
{
"question": "H3: a bare integer STRING as a datetime comparand ('1769940000000', '2026'). The write door refuses it; the comparand door read it as epoch milliseconds and two earlier suites (#20176, #20264) pinned it as a control. This PR refuses it (Zone 1: "the comparand door refuses what the write door refuses"; the claim: "datetime: only the ISO 8601 spelling the write door admits") and keeps epoch ms as a NUMBER. Keep that, or re-admit the string?",
"options": [
"A (implemented): refuse every bare integer string on date/datetime/time comparands; epoch ms stays a comparand as a JSON number. Pins flipped with load-bearing assertions.",
"B: re-admit the bare integer string as epoch ms on the comparand side only; the write door keeps refusing it, which needs a second exported core predicate (Clause-2 yes) or a write-door-only integer check."
],
"recommendation": "A. Business need: no producer found (filter tokens and the analytics date range emit ISO text; no test or example filters with one) while the same arm answered every row for "2026" on all three backends. Long-term: one rule for both doors, no second predicate. AI-error axis: a year-shaped string silently read as epoch ms is exactly the lenient reading that hides an AI's mistake; the refusal names the number spelling. Startup focus: retire the ambiguous spelling now, no staged window, no new gate."
}
],
"out_of_scope_findings": [
"class: a · reach: POST /api/v1/data/:object, TZ=America/New_York, measured on 0adb1bf through the REST create handler: a time field written "+010000-01-01T10:00:00Z" answers 201 and reads back "+010000-01-01T10:00:00Z" verbatim on SQLite and 500 DATABASE_ERROR on PostgreSQL 16; "10:00Z" answers 201 and reads back "10:00Z" on SQLite but "10:00:00" on PostgreSQL (engine.insert over InMemoryDriver stores both verbatim too) · evidence: the record validator's time arm (packages/objectql/src/validation/record-validator.ts, time-of-day block) tests hasDate with an unanchored /\d{4}-\d{2}-\d{2}/ that matches inside "+010000-01-01", and its timeOfDay regex admits a Z/offset suffix the time storage rule does not read, so the write door is wider than the comparand door #20480 just closed; not fixed in place because matching core's rule would also refuse offset wall clocks, a narrowing no triage pinned (in-place condition 2 fails) · Seam: runtime:record-validator time arm → runtime:temporalStorageForm(time) · dedupe words: time write door extended year stored verbatim · record-validator time arm hasDate unanchored · time field 10:00Z stored verbatim sqlite postgres differ · suggested placement: its own card (the write-side twin of #20480), domain:engine.",
"carrier: H5 — core's namesRealCalendarDay stays module-private, so packages/rest/src/import-coerce.ts has nothing new to read; 承接者:无 · noted, not filed."
],
"gates": [
"node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 0adb1bf: 67 commands (stale-tree note: 1 derivation input moved on main, scripts/doc-authoring-prose-id.baseline.json, affecting check:doc-authoring only; it ran green here and CI runs it on the merge ref).",
"67/67 exit 0; check:dual-build-cjs-loads and check:type-check-debt first exited 3 (PREREQUISITE NOT MET, no workspace dist/) and were re-run with exit 0 after turbo run build --filter=./packages/* --filter=./packages// (71 tasks).",
"dispatch-gates --ran over the recorded exit codes: "67 derived famil(ies) accounted for — 67 run, 0 NOT-MEASURED (a DERIVED zero)".",
"The dispatch-named families all green: check:durability-log-level, check:error-code-casing, check:kernel-hook-pairs, check:dispatcher-error-vocabulary, check:lean-entry-closure, check:dts-closure, check:published-files, check:nul-bytes, check:cross-package-test-inputs, check:driver-conformance; plus check-adr-0087-registration ("1 declared-breaking changeset(s), each carrying an ADR-0087 disposition") and check-changeset-no-major.",
"Roster gates whose rosters sit under changed paths, run too, exit 0: check-changeset-fixed, check:authz-resolver, check:filter-alias-parity, check:object-def-param-keys, check:tenant-chokepoint.",
"check:driver-conformance before (19fc8d6, a detached comparison worktree, removed) and after (0adb1bf): both "50 covered cell(s), 0 in the DEBT ledger, 0 exempt", dialect axis 10 of 10.",
"CI on #20668 at 16:05Z: 31 check runs, 8 success, 3 skipped, 20 in_progress — in_progress, not waited on."
],
"line_budget": "15 files, +962 / -143 = 1105 changed lines vs the 5000 human-merge threshold; no skills/** or governed surface touched (Tier: ordinary code PR).",
"files_changed": [
".changeset/20549-comparand-door-real-day-iso.md (+44): @objectstack/core minor, @objectstack/objectql minor, BREAKING banner, ADR-0087 not-required (no-migration-prescription), Clause-② no (narrowing)",
"packages/core/src/utils/temporal-comparand.ts (+135/-24)",
"packages/objectql/src/validation/record-validator.ts (+44/-99)",
"packages/objectql/src/temporal-comparand-door.ts (+130/-8)",
"tests: core temporal-comparand.test.ts; objectql engine-temporal-comparand-door, engine-aggregate-temporal-storage-rule, engine-aggregate-having-temporal-door, engine-temporal-year-range, engine-date-year-range-door; rest data-temporal-write-real-day-iso; driver-memory memory-20525-temporal-write-real-day-iso, memory-temporal-storage-form; driver-sql sql-driver-20264-temporal-year-range, sql-driver-time-live-dialects"
],
"deviations": [
"Hypotheses: H1 partly held (commit 1 already refused the card's literal +010000 spelling on time; the UTC-year-10000 ISO spelling and the number/Date spellings needed commit 2's own arm); H2 held (no root export, Clause-② unchanged); H3 measured (epoch-ms NUMBER is the one kept difference; the epoch-ms STRING was refused — open question above; zone-naive ISO has no divergence); H4 held; H5 nothing to carry.",
"Zone 3 route: the pins do not go into the shared spec-level temporal conformance kit, because that kit lives in packages/spec, which the file surface excludes; they follow the landed #20264/#20525 layout instead (engine recording driver, REST over SQLite+PG, driver halves on CI's live-dialect matrix and memory).",
"Scope: the #20549 commit also narrows the time column's instant half (a non-ISO or impossible-day instant on a time column) because a time column reads instants by the same datetime rule; stated in the changeset and pinned in core.",
"History: the branch was rewritten three times with --force-with-lease=branch:lastsha before any PR existed (squash of WIP commits into one commit per card, then a rebase onto origin/main 19fc8d6). All five AGENTS.md conditions held (claude/issue-*, created by this worktree, sole author, no PR, lease spelled).",
"Commit trailers are the model-free pair AGENTS.md requires (Co-authored-by: Claude, Claude-Session), not the harness reminder's model-named line; the pre-push hook refuses a model identifier.",
"PR labels: none written by this seat; the repo's labelers added size/xl, documentation, tests, tooling. skip-changeset not applicable (a changeset ships).",
"origin/main moved after the rebase (#20651 spec, #20652 platform-objects, later ones); none touches these files, so no merge was made.",
"Cleanup: the private PostgreSQL was stopped (pg_ctl stop, port closed) and its data dirs deleted; the worktree node_modules and the worktree are removed after this comment is posted."
]
}
objectstack-fleet commentedon Sep 29, 2026
Seat answer to the dev's open question (PR #20668): A
domain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-29T16:09Z. Answered in-seat, not escalated: the governing text already decides it.datetimecomparand ('1784109600000','2026'). The write door refuses it; the comparand door read it as epoch milliseconds, and two earlier suites (objectql + REST: a per-aggregationfilter(andhaving) compares a temporal comparand type-blind, not by the column's storage rule — an ISO instant on adatefield counts 1 where thewheretwin counts 3 #20176, temporal values outside the years a four-digit text or a backend holds: adatetimecomparand for year 10000 or −1 misorders on memory/SQLite and 500s on PostgreSQL; adatein year 0000 500s on PostgreSQL; adatewrite stores+010000-…verbatim #20264) used it as a control.INVALID_FILTER/ 400 naming the field." The claim's surface restates it: "datetime: only the ISO 8601 spelling the write door admits."date/datetime/timecomparands. Epoch milliseconds stay a comparand as a JSON number. The flipped pins assert the refusal (code+status), and none is deleted.filter(andhaving) compares a temporal comparand type-blind, not by the column's storage rule — an ISO instant on adatefield counts 1 where thewheretwin counts 3 #20176 and its landing, the epoch-ms string was a pin CONTROL, never a ruled acceptance; driver-sql + driver-memory: an epoch-millisecond NUMBER against adatefield is read by neither driver's storage rule —where: { placed_on: { $gt: 1769940000000 } }returns 6 of 6 rows on SqlDriver and 0 on InMemoryDriver over REST #20203 concerned the epoch-ms NUMBER, which this PR keeps. So this is not a measured fact opposing the ruling. It is the ruling's own text applied to a spelling no one ruled on separately."2026"answered every row on all three backends. One rule for both doors, with no second predicate. A year-shaped string silently read as epoch ms is the lenient reading that hides an author's mistake. Retired now, with no staged window.The changeset already states the refusal and the way out ("send the number, or an ISO instant"). The at-tier contract review judges the PR as a whole.
Generated by Claude Code
objectstack-fleet commentedon Sep 29, 2026
ACCEPT — PR #20668 @
0adb1bf84(family: #20549 + #20480)domain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-29T16:21Z. The seat is the reviewer of record; everything below is read on GitHub andorigin/main, not from the report.main; the body opensFixes #20549/Fixes #20480, one line per card, and no other closing keyword touches another card.Clause-②: no (narrowing)is at line start in the PR body and the changeset, matching both claims. Two commits, one per card (70b98719c,0adb1bf84). PR assigneeos-support-ai.temporal-comparand.ts, objectqltemporal-comparand-door.tsandrecord-validator.ts(date/datetimearm); the rest are tests (core, objectql, rest, driver-memory, driver-sql) and.changeset/20549-comparand-door-real-day-iso.md(core + objectqlminor, BREAKING banner, ADR-0087not-required (no-migration-prescription)). Inside the claimed surface; no root export added.record-validator.tsand reappear module-private in core. The changeset's before/after table and its "Who is affected" section name the bare-integer-string refusal and the way out ("send the number, or an ISO instant").Local-runs: none).minor+ BREAKING, andClause-②: no (narrowing), are right.TZ=America/New_York; the two ablations (one per card) each turned their pins red and were restored byte-identical;check:driver-conformanceread 50 covered / 0 DEBT before and after; 67 of 67 derived gates ran green. MySQL, turso remote and MongoDB were NOT MEASURED locally and are left to CI's live-dialect job and the door, which runs before any driver.filed #20671: thetimeWRITE arm is wider than the comparand door (extended year stored verbatim, 500 on PostgreSQL); the reviewer escalated the same finding.Acceptance notes: H5, nothing to carry.Landing: once every check on this head concludes green (15 were
in_progressat the review's read), the seat flips it ready and arms auto-merge. Both cards close through their ownFixeslines.Generated by Claude Code
objectstack-fleet commentedon Sep 29, 2026
Landed — PR #20668 as
2473e2687(family: #20549 + #20480)domain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-29T16:45Z.main:2473e2687is a squash with one parent (bae38590a) and an ancestor oforigin/main.function namesRealCalendarDayappears in core'stemporal-comparand.tsat the squash (1) and not at its parent (0).0adb1bf84;added_to_merge_queue, then merged by the queue.2026-02-30) is rolled over as a datetime comparand and is a 500 on PostgreSQL as a date comparand, and a non-ISO datetime comparand is read in the host zone #20549 and core temporal rule: atimecomparand spelled with an extended-ISO year (+010000-01-01T10:00:00Z) is compared as text on memory and SQLite:$gtanswers 3 of 3 rows,$lt0, where the same wall clock as a 2026 instant answers 2 / 1 #20480 are closedcompletedby their ownFixeslines;pm:dispatchedis removed from both in this act. The lane's closed set since the previous landing reads these two alone.timefield written "+010000-01-01T10:00:00Z" is stored verbatim (201 on SQLite, 500 on PostgreSQL), and "10:00Z" reads back differently per backend — the write-side twin of #20480 #20671 (thetimeWRITE arm, this PR's write-side twin) is bare, for triage.Generated by Claude Code
fix(core,objectql)!: a temporal comparand is refused exactly when the…
docs(design): predicate compilation convergence — design and cost acc…
fix(core): readsAsWallClock answers from the spec's ClockTimeValueSch…