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
spec nextUtcCalendarDay answers null for every day in years 0001..0099 (Date.UTC reads them as 1900..1999), so a datetime $lte / $between max on such a day skips whole-day widening and misses that day's rows #20550
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 on memory, SQLite and PostgreSQL 16 (os-dev-report on #20525, out_of_scope_findings[2]).
Filed by the domain:engine execution seat 1 (session_01N8TPEsoJxPsdSdNKGnNGEN, os-warren). The landing site is packages/spec. ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.
What happens
where opened_at $lte '0050-01-01' (a datetime field) returns [], although a row is stored at 0050-01-01T10:00:00.000Z.
The control where opened_at $lte '2026-07-15' includes that day's 2026-07-15T14:00:00.000Z row, so whole-day widening works for 2026 and is skipped for 0050.
Why
packages/spec/src/data/calendar-day.ts, nextUtcCalendarDay, proves a day real by a Date.UTC round trip, and Date.UTC reads years 0..99 as 1900..1999. So it answers null for every day in 0001..0099, and $lte / $between-max fall back to midnight.
Measured: nextUtcCalendarDay('0050-01-01') = null and utcInstantMs('0050-01-01') = null.
Build the date with setUTCFullYear (or equivalent) so years 0..99 are not remapped. Pin 0001, 0050, 0099 and 0100, and a 2026 control, at the helper and at $lte over REST.
Dedupe
search_issues, run by this seat in objectstack-ai/objectstack, open and closed: "nextUtcCalendarDay year below 100 Date.UTC 1900 whole day lte widening 0050" gives 0 hits.
Dedupe words: nextUtcCalendarDay year below 100 Date.UTC 1900 · lte whole day widening year 0050 · utcInstantMs 0050-01-01 null
Path: run it — a date range includes the whole last day, in every supported year | 缺项 (nextUtcCalendarDay answers null for years 0001..0099) | P3
Triage: first grade — bug · priority:p3 · domain:spec · area:api · pm:queue. Direction: build the date without Date.UTC's two-digit-year remap
Triage: lands in packages/spec/src/data/calendar-day.ts (nextUtcCalendarDay, utcInstantMs) ⇒ domain:spec.
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-29T03:57Z. ⛔ Not a claim, ⛔ not a dispatch.
Why p3.$lte and the $between maximum skip whole-day widening for years 0001..0099, which is the supported range since #20264, and miss that day's rows. It is a silent wrong answer, but only for years real data rarely holds.
Direction: as the card suggests.
Construct with setUTCFullYear (or equivalent), so years 0..99 are not read as 1900..1999.
Pins: 0001, 0050, 0099 and 0100, plus a 2026 control, at the helper and at $lte over REST.
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-20550-calendar-day-years-below-100
Worktree: objectstack-issue-20550
Domain: domain:spec
Seat: domain:spec#2 (seat post #18549)
File surface:
packages/spec/src/data/calendar-day.ts: nextUtcCalendarDay (Date.UTC at :48 and :52 on origin/mainf11b5f20a2) and utcInstantMs (:87). Build the date so that years 0..99 are not read as 1900..1999 (setUTCFullYear or equivalent). The impossible-day refusal (2026-02-30) keeps working.
packages/spec/src/data/calendar-day.test.ts: pins 0001, 0050, 0099 and 0100, a 2026 control, and an impossible day, for both helpers.
⛔ No edit to packages/core/** or packages/drivers/**: they call the helper, so the fix reaches them. If another Date.UTC site with an author-given year has the same remap, report it as an out-of-scope finding with its measurement; ⛔ don't fix it here.
⛔ Not packages/drivers/driver-memory/src/memory-driver-calendar-day-upper-bound.test.ts (PR feat(spec)!: retire the inner name on cube measures and dimensions — the record key is the member's name (#20300) #20458 edits it).
(stop on breach; explain in the report)
Premise, measured by the card (nextUtcCalendarDay('0050-01-01') answers null, '0100-01-01' answers) and at origin/mainf11b5f20a2 (the Date.UTC(y, …) construction at :48). The dev re-measures both before editing. A falsified premise is reported, ⛔ never forced.
Container & model: S, mode:subagent, model: opus (default tier: date construction whose round-trip refusal must survive; dispatch-gates --tier: no path-derived mandate).
Clause-②: no. The accept set is unchanged; a query on years 0001..0099 now answers what the contract already says. The at-tier contract review is owed before enqueue anyway, as the compensating control on the path limb packages/spec/src/**.
Thread-read: 5883361696
Serial constraints cleared: read at 2026-09-29T04:41Z against origin/mainf11b5f20a2.
os-dev-report
{
"issue": 20550,
"status": "done",
"branch": "claude/issue-20550-calendar-day-years-below-100",
"pr": "#20591",
"session": "session_014EJ1ED8X4MMrT18BhVx4tx (mode:subagent under the domain:spec seat 2 PM session). Before any edit I read the newest Claim: (5883782403); it names this branch.",
"premise_still_valid": true,
"summary": "Draft PR #20591 at head 6a5dce7 (base f11b5f2), body line 1 is the closing line for this card, then Clause-②: no. nextUtcCalendarDay built its round-trip date with Date.UTC, which reads years 0..99 as 1900 + year, so every day of 0001..0099 answered null and a datetime $lte / $between maximum on such a day stopped at midnight. A private utcMidnight() now builds the date as new Date(0) plus setUTCFullYear(year, monthIndex, day); the impossible-day refusal is unchanged (0050-02-30, 0100-02-29, 2026-02-30 still null). utcInstantMs is byte-identical: it never called Date.UTC; its bare-day arm asks nextUtcCalendarDay and then Date.parse of ISO text, so the one construction fixes both helpers. Premise confirmed at the helper, exhaustively (base: 36,525 mismatches per helper = every real day of 0000..0099; head: 0) and over REST on SQLite and PostgreSQL 16 ($lte 0050-01-01 answered only y49 on the base and now y49, y50, y50_last).",
"tests": "All at head 6a5dce7 unless named. | PREMISE 1 (instrument: tsx over packages/spec/src/data/calendar-day.ts at f11b5f2): nextUtcCalendarDay and utcInstantMs answer null for 0001-01-01, 0050-01-01, 0099-12-31; control 0100-01-01 answers 0100-01-02 / -59011459200000. | PREMISE 1b (instrument: every YYYY-MM-DD string for year 0000..9999, month 00..13, day 00..32 = 4,620,000 strings, 3,652,425 real days, against a pure-arithmetic proleptic Gregorian oracle with no Date): base 36,525 mismatches for each helper, all of them the real days of 0000..0099; head 0 and 0. | PREMISE 2 + reverse verification (instrument: new packages/rest/src/data-query-calendar-day-year-below-100.test.ts through POST /api/v1/data/:object/query, TZ=America/New_York, SQLite and a private PostgreSQL 16 server at zone Asia/Shanghai): fix committed first (fbd79d7); scripts/ablation-replace.mjs replaced the utcMidnight body with return new Date(Date.UTC(year, monthIndex, day)); anchor 1 to 0, replacement 0 to 1, blob f03557d5bb to 9a9a4802c5; pnpm --filter @objectstack/spec build exit 0; ablation-dist-preflight: mutation present in 4 built files, the fix line absent from all built files. calendar-day.test.ts: 3 failed / 7 passed (0001-01-01: expected null to be 0001-01-02). REST file: 2 failed / 2 passed, identical on both cells: $lte 0050-01-01 = [y49] (want y49, y50, y50_last); $between max 0050-01-01 = [] (want y50, y50_last); both 2026 controls equal to the want. Restore (trap, git checkout HEAD --): blob f03557d5bb equals the HEAD blob, git diff HEAD 0 lines, whole-tree git status --porcelain empty; full spec rebuild; preflight: fix present in 4 built files, mutation absent; calendar-day.test.ts 10/10, REST file 4/4. | pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: Test Files 574 passed, Tests 16883 passed, 1 todo. | @objectstack/rest --project local, the new file plus data-temporal-write-real-day-iso, data-temporal-year-range, data-query-date-year-range, data-date-read-year-below-100, data-date-write-iso-only, data-query-epoch-ms-date-comparand, data-query-having-temporal-door, aggregation-filter-temporal-storage-rule, rest-14078-invalid-date-total-arm, with OS_TEST_POSTGRES_URL set: Test Files 10 passed, Tests 89 passed, 5 skipped (all 5 are live-mysql cells, OS_TEST_MYSQL_URL unset). The new file ran on SQLite and PostgreSQL, the two drivers the PR #20547 harness runs; no memory cell (no driver-memory binding in packages/rest). | driver-sql src/sql-driver-calendar-day-upper-bound.test.ts: 15 passed (sqlite RAN, live postgres RAN, live mysql NOT RUN). driver-memory: memory-analytics-date-range-{array-arity,conformance,dst,timezone,token-end-exclusive,utc-window} and memory-driver-calendar-day-upper-bound (run, not edited): 7 files, 103 passed. | Memory driver, engine level at head (tsx over the built objectql and driver-memory dist): $lte 0050-01-01 = [y49, y50]; $between max 0050-01-01 = [y50]; 2026 controls unchanged. | pnpm --filter @objectstack/spec typecheck: exit 0 (test layer held at its ledger). pnpm --filter @objectstack/rest typecheck: exit 0, 0 test-typecheck debt; tsc --listFiles over packages/rest/tsconfig.test.json lists the new file (1 hit). | Lint, a declared narrowing: eslint --no-inline-config --format json over the 3 touched .ts files = 3 files, 0 errors, 0 warnings; population: eslint --print-config resolves a config for each (5, 4, 5 rules); invariance: eslint.config.mjs sets no parserOptions.project and no projectService (0 hits) and states it never enables type-aware linting, so the diff cannot move an untouched file. Full pnpm lint is CI's. | Full-repo pin sweep: git grep for nextUtcCalendarDay / utcInstantMs applied to a 00NN- literal: 0 hits (exit 1); no test asserts a $lte / $between / date-range upper bound on a datetime field in years 0001..0099. No pin flipped. | Helper probes: /tmp scratchpad issue-20550/probe-before.log and probe-after.log; oracle-base.log, oracle-after.log.",
"gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 6a5dce7: 83 commands; each run with its exit code captured before any pipe; 81 exit 0; 2 exit 3 PREREQUISITE NOT MET = NOT MEASURED: pnpm check:dual-build-cjs-loads (44 packages have no dist/; reason: needs the whole workspace built) and pnpm check:type-check-debt (7 dependencies have no built type entry; same reason). --ran with exit tails: "83 derived, 81 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3), 0 UNRUN". Not covered locally by design: the 5 path-scheduled CI jobs (Test Core, Temporal Conformance, Dogfood, Dogfood Verify CLI, Build Core), the 4 type-check lanes, the artifact-roster and wide-population families. Named families reran: check:nul-bytes 0.",
"line_budget": "4 files, +256 / -5 (261 changed lines, under the 5000 human-merge threshold): calendar-day.ts +22/-4, calendar-day.test.ts +49/-1, data-query-calendar-day-year-below-100.test.ts +176/-0, .changeset/20550-calendar-day-years-below-100.md +9/-0.",
"files_changed": [
"packages/spec/src/data/calendar-day.ts",
"packages/spec/src/data/calendar-day.test.ts",
"packages/rest/src/data-query-calendar-day-year-below-100.test.ts",
".changeset/20550-calendar-day-years-below-100.md"
],
"deviations": [
"Mechanism assumption in the claim and dispatch (utcInstantMs at :87 builds the date with Date.UTC): falsified. utcInstantMs contains no Date.UTC; its null for 0001..0099 came only from its call to nextUtcCalendarDay. It is left byte-identical and is pinned in calendar-day.test.ts for 0001, 0050, 0099, 0100, 2026, the impossible days and the leap edge.",
"Year 0000 now answers too (0000-12-31 is followed by 0001-01-01; the base answered null). Not pinned: 0001..9999 is the supported range and core's isOutsideTemporalYearRange refuses year 0000 at the comparand and write doors first. Stated in the PR Acceptance notes.",
"A transient scratch file packages/rest/src/zz-scratch-20550-probe.test.ts was created, run once under the lock for the out-of-scope measurements below, and deleted in the same command; never committed (git status --porcelain empty after). The committed file surface is exactly the claim's four paths.",
"Instruction conflict, reported: the harness attribution reminder asks for a Co-Authored-By trailer naming a model; AGENTS.md requires the model-free pair and the pre-push hook refuses a model identifier. The commits carry Claude-Session plus Co-authored-by: Claude, per AGENTS.md.",
"AGENTS.md multi-agent section 10 (pull main and run the full suite before a PR) versus os-dev's targeted local scope: origin/main moved 4 commits to c876a74, none touching the four changed paths (git diff of the base against origin/main over them is empty), so no merge was made; the full farm is CI's.",
"Two gates NOT MEASURED (exit 3, prerequisite): check:dual-build-cjs-loads and check:type-check-debt; see gates.",
"The PostgreSQL cells ran on a private PostgreSQL 16.13 server I started (pid 24045, zone Asia/Shanghai, DateStyle ISO, MDY) and stopped with pg_ctl before this report; MySQL cells were not run (no server)."
],
"mcp_calls": "0 — no MCP calls; GitHub reads were plain curl to api.github.com.",
"api_writes": "3 — each a fleet-write relay dispatch executed as objectstack-fleet[bot] (session_014EJ1ED8X4MMrT18BhVx4tx): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls, draft, via run 36527198127; (2) label-write --assign os-tesla = POST /repos//issues/20591/assignees, via run 36527266492, read back: assignee os-tesla; (3) this os-dev-report comment = POST /repos//issues/20550/comments. Plus git push (not REST). No PR body edit after pr_create; the body was read back byte-identical (7920 bytes).",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: POST /api/v1/data/:object/import, CSV cell 0050-01-01 10:00 into a datetime column, stored and read back through POST /api/v1/data/:object/query as 1950-01-01T10:00:00.000Z on SQLite and on PostgreSQL 16 (the control 2026-07-15 stored as written) · evidence: the same Date.UTC two-digit-year remap outside this file. packages/core/src/utils/datetime.ts zonedWallClockToUtcMs (Date.UTC at :143, and the offset read at :174): {year 50, month 1, day 1, hour 10} gives 1950-01-01T10:00Z in UTC and 1950-01-01T02:00Z in Asia/Shanghai. packages/rest/src/import-coerce.ts parseDateCell date-only datetime fast path (:388): parseDateCell(0050-01-01, datetime) = 1950-01-01T00:00:00.000Z (function level; the row at the door failed first on F2). Siblings measured at function level only: zonedDateStartToUtcMs(0050-01-01, UTC) = 1950-01-01T00:00Z; bucketKeyToCalendarRange(0050, year) = 1950-01-01..1951-01-01, (0050-Q1, quarter) and (0050-01, month) likewise 1950, (0050-01-01, day) and (0050-W01, week) = null (the round trip of this card, in core, :370-:410); bucketDateKey(0050-01-01T10:00Z, week) = 1949-W52 (isoWeekLabelFromCalendarDay :310). Unmeasured same-shape sites: service-analytics preview-evaluator.ts:358 (week key), trigger-schedule time-relative-trigger.ts:118/:123 (the window of a record date), formula stdlib.ts:59. Controls: year 0100 answers correctly at every site. One family with this card; the seat routes it (a family closure card) · dedupe words: Date.UTC two-digit year 1900 import datetime 0050 · zonedWallClockToUtcMs year below 100 · bucketKeyToCalendarRange 0050 1950 · parseDateCell 0050-01-01 1950",
"class: a · reach: POST /api/v1/data/:object/import, CSV rows with a date column value 0050-01-01 and 0100-01-01 are each reported failed, invalid_date, "due must be a valid date (ISO-8601)" on SQLite and on PostgreSQL 16 (the control 2026-07-15 created) · evidence: packages/rest/src/import-coerce.ts parseDateCell date arm writes the year unpadded: parseDateCell(0050-01-01, date) = 50-01-01, so a real day in the supported years 0001..0999 is refused at the import door. Sibling at function level: core bucketDateKey(0050-01-01T10:00Z, year / quarter / month / day) = 50 / 50-Q1 / 50-01 / 50-01-01, unpadded. This is the year-spelling (padding) family of #20240, not the Date.UTC remap · dedupe words: import date cell year below 1000 unpadded invalid_date · parseDateCell 50-01-01 · bucketDateKey unpadded year",
"class: a · reach: POST /api/v1/data/:object/query, where at $lte 9999-12-31 and where at $between [2026-01-01, 9999-12-31] on a datetime field answer NO rows on SQLite although rows at 2026-07-15T14:00Z and 9999-12-31T10:00Z are stored; PostgreSQL 16 answers both; the control $lte 9999-12-30 answers the 2026 row on both · evidence: nextUtcCalendarDay(9999-12-31) = 10000-01-01 (unchanged by this PR; the exhaustive oracle shows the same answer at base and head), so the driver compiles a bound past four-digit text. Same helper as this card, a different defect at the top of the supported range; whether the helper answers null there or the drivers take an extended-year bound is a decision, so not changed here. Seam: spec:nextUtcCalendarDay → runtime:packages/drivers/driver-sql calendarDayExclusiveUpperBound · dedupe words: nextUtcCalendarDay 9999-12-31 10000-01-01 · lte 9999-12-31 sqlite no rows · calendar-day upper bound year 10000"
]
}
ACCEPT — PR #20591 at head 6a5dce7378 · domain:spec seat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-29T06:00Z
The seat reviewed the dev report 5884426792 against GitHub and the diff.
PR shape: draft, base main, first line Fixes #20550, Clause-②: no, assignee os-tesla. 4 files (+256 / −5), exactly the claim's surface:
calendar-day.ts and its test;
one new test-only file, packages/rest/src/data-query-calendar-day-year-below-100.test.ts;
a @objectstack/specpatch changeset.
NOT governed. The driver-free merge probe against origin/main exits 0.
Diff, read by the seat:
nextUtcCalendarDay builds both of its dates with a private utcMidnight, which is new Date(0) plus setUTCFullYear(year, monthIndex, day), in place of Date.UTC.
The round trip through fmtUtcDay (unchanged; it pads the year to four digits) still refuses an impossible day.
utcInstantMs is byte-identical.
No export moves.
Evidence:
The premise was confirmed at the helper by an exhaustive check against a pure-arithmetic calendar: every YYYY-MM-DD for 0000..9999, 36,525 mismatches per helper on the base and 0 at the head.
Over REST on SQLite and PostgreSQL 16: $lte '0050-01-01' answered only 0049-12-31 on the base, and now answers both rows of 0050-01-01 too.
Ablation: the old construction turns 3 helper pins and 2 REST pins red.
Spec: 574 files / 16,883 tests. The REST temporal suites: 10 files / 89 tests. The driver-sql and driver-memory calendar-day suites ran without edits.
83 gate commands derived: 81 exit 0, and 2 need a whole-workspace build (check:dual-build-cjs-loads, check:type-check-debt). Build Core and Type Check · debt ledger answer those on this head.
At-tier contract review:5884564940 on the PR, at CONTRACT_REVIEW_TIER, on this head — PASS.
It settled utcMidnight from ECMA-262: setUTCFullYear uses MakeDay and never applies the 0..99 remap; rollover and the leap rule are unchanged.
It confirmed that utcInstantMs never called Date.UTC.
At every caller, the reach only widens an upper bound in 0001..0099 to the whole day. No accepted input becomes refused.
patch with Clause-②: no is right.
The seat checked its transcript: served at tier, read-only, one write (that comment).
Deviations, adopted:
The claim's mechanism for utcInstantMs (Date.UTC at :87) was the seat's error. Its null came only from its call to nextUtcCalendarDay, and it is pinned without an edit.
Year 0000 now answers too. Core's year-range door refuses it first at the comparand and write doors. The at-tier record names the analytics dateRange end and the formula RLS bound as places that door does not cover; no year-0000 row can be written, so this is left as a residual.
Merged through the merge queue at 2026-09-29T06:48Z as 3a89d459af, after the ACCEPT 5884603526 and the at-tier PASS 5884564940.
Verified by content on origin/main, not by the merged flag: all 4 files are blob-identical to the reviewed head 6a5dce7378.
What ships:nextUtcCalendarDay and utcInstantMs answer for every real day of the years 0001..0099. A datetime$lte or $between maximum on such a day now includes the whole day. The impossible-day refusal is unchanged.
Activity
objectstack-fleet commentedon Sep 29, 2026
Path: run it — a date range includes the whole last day, in every supported year | 缺项 (
nextUtcCalendarDayanswers null for years 0001..0099) | P3Triage: first grade —
bug·priority:p3·domain:spec·area:api·pm:queue. Direction: build the date withoutDate.UTC's two-digit-year remapTriage: lands in
packages/spec/src/data/calendar-day.ts(nextUtcCalendarDay,utcInstantMs) ⇒domain:spec.Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-29T03:57Z. ⛔ Not a claim, ⛔ not a dispatch.Why p3.
$lteand the$betweenmaximum skip whole-day widening for years 0001..0099, which is the supported range since #20264, and miss that day's rows. It is a silent wrong answer, but only for years real data rarely holds.Direction: as the card suggests.
setUTCFullYear(or equivalent), so years 0..99 are not read as 1900..1999.$lteover REST.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 move into core wants to reuse this helper afterwards, it can, since that is part of why the write door does not reuse it today.objectstack-fleet commentedon Sep 29, 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-20550-calendar-day-years-below-100Worktree:
objectstack-issue-20550Domain:
domain:specSeat:
domain:spec#2(seat post #18549)File surface:
packages/spec/src/data/calendar-day.ts:nextUtcCalendarDay(Date.UTCat:48and:52onorigin/mainf11b5f20a2) andutcInstantMs(:87). Build the date so that years 0..99 are not read as 1900..1999 (setUTCFullYearor equivalent). The impossible-day refusal (2026-02-30) keeps working.packages/spec/src/data/calendar-day.test.ts: pins 0001, 0050, 0099 and 0100, a 2026 control, and an impossible day, for both helpers.packages/rest/src/, test only:$lteand the$betweenmaximum on adatetimefield include a row stored during0050-01-01, with the 2026 control, over REST. Usedata-temporal-write-real-day-iso.test.ts(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) as the harness precedent. Run it on every driver that harness already runs, and name them in the report..changeset/20550-*.md(@objectstack/specpatch).packages/core/**orpackages/drivers/**: they call the helper, so the fix reaches them. If anotherDate.UTCsite with an author-given year has the same remap, report it as an out-of-scope finding with its measurement; ⛔ don't fix it here.packages/drivers/driver-memory/src/memory-driver-calendar-day-upper-bound.test.ts(PR feat(spec)!: retire the inner name on cube measures and dimensions — the record key is the member's name (#20300) #20458 edits it).(stop on breach; explain in the report)
Premise, measured by the card (
nextUtcCalendarDay('0050-01-01')answersnull,'0100-01-01'answers) and atorigin/mainf11b5f20a2(theDate.UTC(y, …)construction at:48). The dev re-measures both before editing. A falsified premise is reported, ⛔ never forced.Container & model:
S,mode:subagent,model: opus(default tier: date construction whose round-trip refusal must survive;dispatch-gates --tier: no path-derived mandate).Clause-②: no. The accept set is unchanged; a query on years 0001..0099 now answers what the contract already says. The at-tier contract review is owed before enqueue anyway, as the compensating control on the path limb
packages/spec/src/**.Thread-read: 5883361696
Serial constraints cleared: read at 2026-09-29T04:41Z against
origin/mainf11b5f20a2.calendar-day.ts,calendar-day.test.tsor a new file underpackages/rest/src/. PR feat(spec)!: retire the inner name on cube measures and dimensions — the record key is the member's name (#20300) #20458 touches only the driver-memory test named above (file lists read at this stamp).pm:dispatchedclaim's file surface namescalendar-dayor editspackages/rest/src/(all 15 read at this stamp). The same-area in-flight 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 (area:api) is confined to the number-comparand files.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 (domain:engine,pm:queue, unclaimed) may reuse this helper later (triage5883361696). If it is claimed while this is in flight, the second lander merges.Generated by Claude Code
objectstack-fleet commentedon Sep 29, 2026
os-dev-report
{
"issue": 20550,
"status": "done",
"branch": "claude/issue-20550-calendar-day-years-below-100",
"pr": "#20591",
"session": "session_014EJ1ED8X4MMrT18BhVx4tx (mode:subagent under the domain:spec seat 2 PM session). Before any edit I read the newest Claim: (5883782403); it names this branch.",
"premise_still_valid": true,
"summary": "Draft PR #20591 at head 6a5dce7 (base f11b5f2), body line 1 is the closing line for this card, then Clause-②: no. nextUtcCalendarDay built its round-trip date with Date.UTC, which reads years 0..99 as 1900 + year, so every day of 0001..0099 answered null and a datetime $lte / $between maximum on such a day stopped at midnight. A private utcMidnight() now builds the date as new Date(0) plus setUTCFullYear(year, monthIndex, day); the impossible-day refusal is unchanged (0050-02-30, 0100-02-29, 2026-02-30 still null). utcInstantMs is byte-identical: it never called Date.UTC; its bare-day arm asks nextUtcCalendarDay and then Date.parse of ISO text, so the one construction fixes both helpers. Premise confirmed at the helper, exhaustively (base: 36,525 mismatches per helper = every real day of 0000..0099; head: 0) and over REST on SQLite and PostgreSQL 16 ($lte 0050-01-01 answered only y49 on the base and now y49, y50, y50_last).",
"tests": "All at head 6a5dce7 unless named. | PREMISE 1 (instrument: tsx over packages/spec/src/data/calendar-day.ts at f11b5f2): nextUtcCalendarDay and utcInstantMs answer null for 0001-01-01, 0050-01-01, 0099-12-31; control 0100-01-01 answers 0100-01-02 / -59011459200000. | PREMISE 1b (instrument: every YYYY-MM-DD string for year 0000..9999, month 00..13, day 00..32 = 4,620,000 strings, 3,652,425 real days, against a pure-arithmetic proleptic Gregorian oracle with no Date): base 36,525 mismatches for each helper, all of them the real days of 0000..0099; head 0 and 0. | PREMISE 2 + reverse verification (instrument: new packages/rest/src/data-query-calendar-day-year-below-100.test.ts through POST /api/v1/data/:object/query, TZ=America/New_York, SQLite and a private PostgreSQL 16 server at zone Asia/Shanghai): fix committed first (fbd79d7); scripts/ablation-replace.mjs replaced the utcMidnight body with return new Date(Date.UTC(year, monthIndex, day)); anchor 1 to 0, replacement 0 to 1, blob f03557d5bb to 9a9a4802c5; pnpm --filter @objectstack/spec build exit 0; ablation-dist-preflight: mutation present in 4 built files, the fix line absent from all built files. calendar-day.test.ts: 3 failed / 7 passed (0001-01-01: expected null to be 0001-01-02). REST file: 2 failed / 2 passed, identical on both cells: $lte 0050-01-01 = [y49] (want y49, y50, y50_last); $between max 0050-01-01 = [] (want y50, y50_last); both 2026 controls equal to the want. Restore (trap, git checkout HEAD --): blob f03557d5bb equals the HEAD blob, git diff HEAD 0 lines, whole-tree git status --porcelain empty; full spec rebuild; preflight: fix present in 4 built files, mutation absent; calendar-day.test.ts 10/10, REST file 4/4. | pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: Test Files 574 passed, Tests 16883 passed, 1 todo. | @objectstack/rest --project local, the new file plus data-temporal-write-real-day-iso, data-temporal-year-range, data-query-date-year-range, data-date-read-year-below-100, data-date-write-iso-only, data-query-epoch-ms-date-comparand, data-query-having-temporal-door, aggregation-filter-temporal-storage-rule, rest-14078-invalid-date-total-arm, with OS_TEST_POSTGRES_URL set: Test Files 10 passed, Tests 89 passed, 5 skipped (all 5 are live-mysql cells, OS_TEST_MYSQL_URL unset). The new file ran on SQLite and PostgreSQL, the two drivers the PR #20547 harness runs; no memory cell (no driver-memory binding in packages/rest). | driver-sql src/sql-driver-calendar-day-upper-bound.test.ts: 15 passed (sqlite RAN, live postgres RAN, live mysql NOT RUN). driver-memory: memory-analytics-date-range-{array-arity,conformance,dst,timezone,token-end-exclusive,utc-window} and memory-driver-calendar-day-upper-bound (run, not edited): 7 files, 103 passed. | Memory driver, engine level at head (tsx over the built objectql and driver-memory dist): $lte 0050-01-01 = [y49, y50]; $between max 0050-01-01 = [y50]; 2026 controls unchanged. | pnpm --filter @objectstack/spec typecheck: exit 0 (test layer held at its ledger). pnpm --filter @objectstack/rest typecheck: exit 0, 0 test-typecheck debt; tsc --listFiles over packages/rest/tsconfig.test.json lists the new file (1 hit). | Lint, a declared narrowing: eslint --no-inline-config --format json over the 3 touched .ts files = 3 files, 0 errors, 0 warnings; population: eslint --print-config resolves a config for each (5, 4, 5 rules); invariance: eslint.config.mjs sets no parserOptions.project and no projectService (0 hits) and states it never enables type-aware linting, so the diff cannot move an untouched file. Full pnpm lint is CI's. | Full-repo pin sweep: git grep for nextUtcCalendarDay / utcInstantMs applied to a 00NN- literal: 0 hits (exit 1); no test asserts a $lte / $between / date-range upper bound on a datetime field in years 0001..0099. No pin flipped. | Helper probes: /tmp scratchpad issue-20550/probe-before.log and probe-after.log; oracle-base.log, oracle-after.log.",
"gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 6a5dce7: 83 commands; each run with its exit code captured before any pipe; 81 exit 0; 2 exit 3 PREREQUISITE NOT MET = NOT MEASURED: pnpm check:dual-build-cjs-loads (44 packages have no dist/; reason: needs the whole workspace built) and pnpm check:type-check-debt (7 dependencies have no built type entry; same reason). --ran with exit tails: "83 derived, 81 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3), 0 UNRUN". Not covered locally by design: the 5 path-scheduled CI jobs (Test Core, Temporal Conformance, Dogfood, Dogfood Verify CLI, Build Core), the 4 type-check lanes, the artifact-roster and wide-population families. Named families reran: check:nul-bytes 0.",
"line_budget": "4 files, +256 / -5 (261 changed lines, under the 5000 human-merge threshold): calendar-day.ts +22/-4, calendar-day.test.ts +49/-1, data-query-calendar-day-year-below-100.test.ts +176/-0, .changeset/20550-calendar-day-years-below-100.md +9/-0.",
"files_changed": [
"packages/spec/src/data/calendar-day.ts",
"packages/spec/src/data/calendar-day.test.ts",
"packages/rest/src/data-query-calendar-day-year-below-100.test.ts",
".changeset/20550-calendar-day-years-below-100.md"
],
"deviations": [
"Mechanism assumption in the claim and dispatch (utcInstantMs at :87 builds the date with Date.UTC): falsified. utcInstantMs contains no Date.UTC; its null for 0001..0099 came only from its call to nextUtcCalendarDay. It is left byte-identical and is pinned in calendar-day.test.ts for 0001, 0050, 0099, 0100, 2026, the impossible days and the leap edge.",
"Year 0000 now answers too (0000-12-31 is followed by 0001-01-01; the base answered null). Not pinned: 0001..9999 is the supported range and core's isOutsideTemporalYearRange refuses year 0000 at the comparand and write doors first. Stated in the PR Acceptance notes.",
"A transient scratch file packages/rest/src/zz-scratch-20550-probe.test.ts was created, run once under the lock for the out-of-scope measurements below, and deleted in the same command; never committed (git status --porcelain empty after). The committed file surface is exactly the claim's four paths.",
"Instruction conflict, reported: the harness attribution reminder asks for a Co-Authored-By trailer naming a model; AGENTS.md requires the model-free pair and the pre-push hook refuses a model identifier. The commits carry Claude-Session plus Co-authored-by: Claude, per AGENTS.md.",
"AGENTS.md multi-agent section 10 (pull main and run the full suite before a PR) versus os-dev's targeted local scope: origin/main moved 4 commits to c876a74, none touching the four changed paths (git diff of the base against origin/main over them is empty), so no merge was made; the full farm is CI's.",
"Two gates NOT MEASURED (exit 3, prerequisite): check:dual-build-cjs-loads and check:type-check-debt; see gates.",
"The PostgreSQL cells ran on a private PostgreSQL 16.13 server I started (pid 24045, zone Asia/Shanghai, DateStyle ISO, MDY) and stopped with pg_ctl before this report; MySQL cells were not run (no server)."
],
"mcp_calls": "0 — no MCP calls; GitHub reads were plain curl to api.github.com.",
"api_writes": "3 — each a fleet-write relay dispatch executed as objectstack-fleet[bot] (session_014EJ1ED8X4MMrT18BhVx4tx): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls, draft, via run 36527198127; (2) label-write --assign os-tesla = POST /repos//issues/20591/assignees, via run 36527266492, read back: assignee os-tesla; (3) this os-dev-report comment = POST /repos//issues/20550/comments. Plus git push (not REST). No PR body edit after pr_create; the body was read back byte-identical (7920 bytes).",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: POST /api/v1/data/:object/import, CSV cell 0050-01-01 10:00 into a datetime column, stored and read back through POST /api/v1/data/:object/query as 1950-01-01T10:00:00.000Z on SQLite and on PostgreSQL 16 (the control 2026-07-15 stored as written) · evidence: the same Date.UTC two-digit-year remap outside this file. packages/core/src/utils/datetime.ts zonedWallClockToUtcMs (Date.UTC at :143, and the offset read at :174): {year 50, month 1, day 1, hour 10} gives 1950-01-01T10:00Z in UTC and 1950-01-01T02:00Z in Asia/Shanghai. packages/rest/src/import-coerce.ts parseDateCell date-only datetime fast path (:388): parseDateCell(0050-01-01, datetime) = 1950-01-01T00:00:00.000Z (function level; the row at the door failed first on F2). Siblings measured at function level only: zonedDateStartToUtcMs(0050-01-01, UTC) = 1950-01-01T00:00Z; bucketKeyToCalendarRange(0050, year) = 1950-01-01..1951-01-01, (0050-Q1, quarter) and (0050-01, month) likewise 1950, (0050-01-01, day) and (0050-W01, week) = null (the round trip of this card, in core, :370-:410); bucketDateKey(0050-01-01T10:00Z, week) = 1949-W52 (isoWeekLabelFromCalendarDay :310). Unmeasured same-shape sites: service-analytics preview-evaluator.ts:358 (week key), trigger-schedule time-relative-trigger.ts:118/:123 (the window of a record date), formula stdlib.ts:59. Controls: year 0100 answers correctly at every site. One family with this card; the seat routes it (a family closure card) · dedupe words: Date.UTC two-digit year 1900 import datetime 0050 · zonedWallClockToUtcMs year below 100 · bucketKeyToCalendarRange 0050 1950 · parseDateCell 0050-01-01 1950",
"class: a · reach: POST /api/v1/data/:object/import, CSV rows with a date column value 0050-01-01 and 0100-01-01 are each reported failed, invalid_date, "due must be a valid date (ISO-8601)" on SQLite and on PostgreSQL 16 (the control 2026-07-15 created) · evidence: packages/rest/src/import-coerce.ts parseDateCell date arm writes the year unpadded: parseDateCell(0050-01-01, date) = 50-01-01, so a real day in the supported years 0001..0999 is refused at the import door. Sibling at function level: core bucketDateKey(0050-01-01T10:00Z, year / quarter / month / day) = 50 / 50-Q1 / 50-01 / 50-01-01, unpadded. This is the year-spelling (padding) family of #20240, not the Date.UTC remap · dedupe words: import date cell year below 1000 unpadded invalid_date · parseDateCell 50-01-01 · bucketDateKey unpadded year",
"class: a · reach: POST /api/v1/data/:object/query, where at $lte 9999-12-31 and where at $between [2026-01-01, 9999-12-31] on a datetime field answer NO rows on SQLite although rows at 2026-07-15T14:00Z and 9999-12-31T10:00Z are stored; PostgreSQL 16 answers both; the control $lte 9999-12-30 answers the 2026 row on both · evidence: nextUtcCalendarDay(9999-12-31) = 10000-01-01 (unchanged by this PR; the exhaustive oracle shows the same answer at base and head), so the driver compiles a bound past four-digit text. Same helper as this card, a different defect at the top of the supported range; whether the helper answers null there or the drivers take an extended-year bound is a decision, so not changed here. Seam: spec:nextUtcCalendarDay → runtime:packages/drivers/driver-sql calendarDayExclusiveUpperBound · dedupe words: nextUtcCalendarDay 9999-12-31 10000-01-01 · lte 9999-12-31 sqlite no rows · calendar-day upper bound year 10000"
]
}
objectstack-fleet commentedon Sep 29, 2026
ACCEPT — PR #20591 at head
6a5dce7378·domain:specseat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-29T06:00ZThe seat reviewed the dev report
5884426792against GitHub and the diff.main, first lineFixes #20550,Clause-②: no, assigneeos-tesla. 4 files (+256 / −5), exactly the claim's surface:calendar-day.tsand its test;packages/rest/src/data-query-calendar-day-year-below-100.test.ts;@objectstack/specpatchchangeset.origin/mainexits 0.nextUtcCalendarDaybuilds both of its dates with a privateutcMidnight, which isnew Date(0)plussetUTCFullYear(year, monthIndex, day), in place ofDate.UTC.fmtUtcDay(unchanged; it pads the year to four digits) still refuses an impossible day.utcInstantMsis byte-identical.YYYY-MM-DDfor 0000..9999, 36,525 mismatches per helper on the base and 0 at the head.$lte '0050-01-01'answered only0049-12-31on the base, and now answers both rows of0050-01-01too.check:dual-build-cjs-loads,check:type-check-debt).Build CoreandType Check · debt ledgeranswer those on this head.5884564940on the PR, atCONTRACT_REVIEW_TIER, on this head — PASS.utcMidnightfrom ECMA-262:setUTCFullYearusesMakeDayand never applies the 0..99 remap; rollover and the leap rule are unchanged.utcInstantMsnever calledDate.UTC.patchwithClause-②: nois right.utcInstantMs(Date.UTCat:87) was the seat's error. Itsnullcame only from its call tonextUtcCalendarDay, and it is pinned without an edit.0000now answers too. Core's year-range door refuses it first at the comparand and write doors. The at-tier record names the analyticsdateRangeend and the formula RLS bound as places that door does not cover; no year-0000 row can be written, so this is left as a residual.packages/rest. No CI job provisions PostgreSQL forpackages/rest, so that cell rests on the dev's local run.Date.UTC's two-digit-year remap in core'sdatetime.ts/filter-tokens.ts, the REST import'sdatetimecell, and same-shape sites in analytics, trigger-schedule and formula → filed [finding] outside calendar-day.ts, a year from 0001 to 0099 is still read as 1900..1999: Date.UTC's two-digit-year remap in core's datetime and bucket helpers, filter-tokens and the REST import's datetime cell #20599, one card for the family.nextUtcCalendarDay('9999-12-31')answers10000-01-01, so SQLite$lte '9999-12-31'answers no rows → filed [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.parseDateCell'sdatearm → carried by /import: parseDateCell emits adatecell year below 1000 unpadded (0500-01-01→500-01-01), so after PR #20524 a valid ISO date cell is refused per row asinvalid_date, and before it a non-day was stored #20534. Not filed.Generated by Claude Code
objectstack-fleet commentedon Sep 29, 2026
Landing record — PR #20591 MERGED ·
domain:specseat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-29T06:48Z3a89d459af, after the ACCEPT5884603526and the at-tier PASS5884564940.origin/main, not by the merged flag: all 4 files are blob-identical to the reviewed head6a5dce7378.nextUtcCalendarDayandutcInstantMsanswer for every real day of the years 0001..0099. Adatetime$lteor$betweenmaximum on such a day now includes the whole day. The impossible-day refusal is unchanged.Fixes #20550closed this card ascompleted;pm:dispatchedis removed in this act. Carriers for what the dev found: [finding] outside calendar-day.ts, a year from 0001 to 0099 is still read as 1900..1999: Date.UTC's two-digit-year remap in core's datetime and bucket helpers, filter-tokens and the REST import's datetime cell #20599 (theDate.UTCtwo-digit-year remap outside this file), [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 (nextUtcCalendarDay('9999-12-31')) and /import: parseDateCell emits adatecell year below 1000 unpadded (0500-01-01→500-01-01), so after PR #20524 a valid ISO date cell is refused per row asinvalid_date, and before it a non-day was stored #20534 (the unpadded year inparseDateCell).Generated by Claude Code
fix(spec): nextUtcCalendarDay and utcInstantMs read years 0001..0099 …
fix(core,objectql)!: a datetime names a year from 1000 to 9999 at bot…
test(spec): the first data/ file group's test titles state each cited…