Repository navigation
fix(spec): nextUtcCalendarDay and utcInstantMs read years 0001..0099 as written, not as 1900..1999 (#20550) - #20591
Conversation
…as written, not as 1900..1999 Date.UTC reads a year from 0 to 99 as 1900 + year, so the round trip that proves a bare day real failed for every day of those years and both helpers answered null: a datetime $lte or $between maximum on such a day skipped whole-day widening. The date is now built with setUTCFullYear. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…y in year 0050, over REST on SQLite and PostgreSQL Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…shows each Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d8ca92ed06496e3d9e03a613dc1b61b4d63515e6 && git checkout d8ca92ed06496e3d9e03a613dc1b61b4d63515e6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c876a7426d930e9e8d310fdffb1f64ae836cdced 6a5dce7378eb31968ef97e55a579d87ff1b5cdf8 && git checkout -B drift-repro c876a7426d930e9e8d310fdffb1f64ae836cdced && git merge --no-ff 6a5dce7378eb31968ef97e55a579d87ff1b5cdf8
node scripts/docs-audit/affected-docs.mjs --json c876a7426d930e9e8d310fdffb1f64ae836cdced |
Contract reviewServed-tier: ① Derived judgmentsScope read first. Head confirmed two ways:
② Semver level
③ Boundary flags
The three out-of-scope findings, each verified read-only and judged:
Check-runs on this head, one read at 2026-09-29T05:46Z, 32 runs, no failure: 13 success (Check Changeset, Check PR Size, Governed Surface Queue Guard, the three claim guards, Spec property liveness, Type Check · source gates, docs link and drift checks, Auto Label, filter), 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke), 16 in progress and NOT concluded: Lint & Repo Gates (every Implemented-by: VERDICT: PASS Generated by Claude Code |
…stant is built from parts (wallClockToUtcMs) (objectstack-ai#20746) Fixes objectstack-ai#20599 Clause-②: yes ## What changes `Date.UTC(year, …)` and `new Date(year, …)` read a year from 0 to 99 as 1900 + year (ECMA-262 `MakeFullYear`). Core built UTC instants from parts that way, so every day of 0001..0099, inside the supported range 0001..9999, landed in the 1900s with no error. - **`@objectstack/core` gains one root export, `wallClockToUtcMs(parts: WallClockParts): number`.** It is `Date.UTC` without the remap, built as `new Date(0)`, then `setUTCFullYear`, then `setUTCHours`. `month` is 1-12. Every component rolls over past its end the way `Date.UTC` rolls it, and a `NaN` component gives `NaN`. It sits beside `zonedWallClockToUtcMs` in `utils/datetime.ts`, and the root barrel's `export *` carries it, so `packages/core/src/index.ts` is untouched. - **Every site the census below confirms now builds through it.** There is no per-site copy: - core: `zonedWallClockToUtcMs` (the wall clock and the zone-offset read), and through it `zonedDateStartToUtcMs`; `isoWeekLabelFromCalendarDay`; `bucketKeyToCalendarRange`; and `filter-tokens` (`proxyDay`, `startOfPeriod`, `daysInMonth`, `addMonthsClamped`); - `service-analytics`: `preview-evaluator.ts` `bucketDate`'s week key, and `dataset-executor.ts` `isoWeekKeyOfUtcMs`. The census found the second one; the card did not list it; - `trigger-schedule`: `time-relative-trigger.ts` `startOfUtcDay` / `endOfUtcDay`. - **The offset read also takes the zone's era.** `Intl`'s `year` part is an ERA year, so 1 BC reads `1`. A wall clock early on 0001-01-01 in a zone west of UTC probes the offset in year 0. Without the era, that probe reads a year late and the answer is garbage. `America/New_York` `0001-01-01 00:00` is pinned: it gives `0001-01-01T04:56:02.000Z`. - **`filter-tokens` spells its day with core's `temporalStorageForm` `date` rule.** Before, a hand-rolled `ymd()` wrote the year unpadded. That spelling was unreachable for 0001..0099 while `Date.UTC` threw those years into the 1900s. This change makes it reachable: `{1976_years_ago}` would have become `50-09-30`, which names no day. So the fix pads it, and a macro step into 0100..0999 is padded too (`{720000_days_ago}` gives `0055-06-15`). This is a mandatory fix under the "a shipped defect this change touches" rule, and it is not the bucket-key padding family (see "Not in this PR"). - `packages/rest/src/import-coerce.ts` is **unchanged** (H3). Its door is fixed through core. ## Census (before any fix, at `origin/main` `4dfff176b9`) `git grep -n "Date\.UTC("` and `git grep -nE "new Date\(\s*[^)\"'\x60]*,"` over non-test tracked files, all packages and scripts. That gave 49 `Date.UTC(` lines and 6 multi-argument `new Date(` lines. Every multi-argument `new Date(` hit is a comment, or a single-argument call caught by the pattern. | site (line at `4dfff176b9`) | can a year below 100 reach it? | disposition | |---|---|---| | core `datetime.ts:143` `zonedWallClockToUtcMs` wall clock | **yes**: the `POST /import` `datetime` cell, measured below | helper | | core `datetime.ts:174` offset read | **yes**: once the wall clock keeps year 50, the probe instant is in year 50 (and in year 0 for 0001 west of UTC) | helper + era | | core `datetime.ts:314` / `:317` ISO-week label | **yes**: `bucketDateKey(week)` of a stored year-50 instant (in-memory aggregation, analytics) | helper | | core `datetime.ts:374`–`:414` `bucketKeyToCalendarRange` | **yes**: a padded key such as `0050` from a SQL driver's bucket expression, drilled | helper | | core `filter-tokens.ts:198` `proxyDay`, `:215`–`:219` `startOfPeriod` | no: derived from `now`, the clock, at every caller (`filterTokenContextFrom(ctx, new Date())` or unset) | helper (the family closes) | | core `filter-tokens.ts:225` `daysInMonth` | reachable, benign: a month's length is the same in Y and 1900 + Y for Y in 1..99 | helper | | core `filter-tokens.ts:243` `addMonthsClamped` | **yes**: `{N_months_ago}` / `{N_years_ago}`; the grammar's N is unbounded (`DATE_MACRO_PARAM_RE`) | helper | | `service-analytics` `preview-evaluator.ts:367` week key | **yes**: a preview row in year 50 | helper | | `service-analytics` `dataset-executor.ts:841` `isoWeekKeyOfUtcMs` | **yes**: `compareTo` alignment on a week ordinal in year 50 | helper | | `trigger-schedule` `time-relative-trigger.ts:118` / `:123` | **yes**: `offsetDays` / `withinDays` are unbounded ints in spec, so an offset reaches 1..99 | helper | | `formula` `stdlib.ts:59` `calendarDayUtc` | no: reads `now()` only (the pinned evaluation clock) | unchanged; `formula` depends on `spec` alone and cannot import core | | `formula` `stdlib.ts:102` `addMonthsUtc` | reachable, benign: day count only, same as `daysInMonth` | unchanged | | `examples/app-todo` `task.functions.ts:47` | benign, the same day-count shape | unchanged | | `service-messaging` `preference-resolver.ts:359` | no: `nowMs` clock | unchanged | | `service-sms` `sms-daily-quota.ts:141` | no: `now` clock | unchanged | | `driver-mongodb` `mongodb-pipeline-evaluator.testkit.ts:92` / `:147` / `:151` | test model, imported only by tests | unchanged (Acceptance notes) | | spec `calendar-day.ts:170`, rest `import-coerce.ts:453`, `driver-sql` `sql-driver.ts:6892` / `:6893` / `:6989` / `:15303`, `driver-turso` `:71` | comments | none | | spec `filter-number-comparand-declared-type.ts:909` | the constant 2026 | none | | `scripts/**` (`check-osv-exemptions`, `pm/*`, `qa/qa-rollup`, `sync-release-index-currency`) | tooling; fixed 2026 constants or 2020s dates read from files | none | ## Reach, measured at the door The in-process route harness drives `POST /api/v1/data/:object/import` and reads back through `POST /api/v1/data/:object/query` over ObjectQL and SqlDriver. The base reading restores `packages/core/src/utils/{datetime,filter-tokens}.ts` to `4dfff176b9` and rebuilds core; core is the only package on this door that the diff touches. The PostgreSQL 16.13 server ran at `timezone=Asia/Shanghai`. | | SQLite, base | PostgreSQL 16, base | SQLite and PostgreSQL 16, this branch | |---|---|---|---| | `0050-01-01 10:00`, no zone | `1950-01-01T10:00:00.000Z`, ok 2 / errors 0 | same | `0050-01-01T10:00:00.000Z` | | `0050-01-01 10:00`, Asia/Shanghai | `1950-01-01T02:00:00.000Z` | same | `0050-01-01T01:54:17.000Z` (LMT +08:05:43) | | `2026-07-15 10:00` control | `2026-07-15T10:00:00.000Z` / `…02:00:00.000Z` | same | same as base | | export, then import, `0001` / `0050` / `0100` at `…-01-01T10:00Z`, export as written | refused, ok 0 / errors 3 (`1-01-01 …`, `50-01-01 …`, `100-01-01 …` unpadded) | same | same: the export's padding is PR objectstack-ai#20688's | | the same, with the year padded as PR objectstack-ai#20688's export writes it | `1901-01-01T10:00Z`, `1950-01-01T10:00Z`, `0100` exact; Asia/Shanghai: `1950-01-01T10:05:43Z` | same | all three exact, both zones | ## Mechanism hypotheses (zone 2), as measured - **H1 held**, and one site more: the root is core's `datetime.ts`, at the listed lines. The offset read also needed the zone's era for a year-0 probe. - **H2 held**: - a new core root export, used by core, `service-analytics` and `trigger-schedule`; - `rest` needs no import (H3); - `formula` cannot import core, and needs no change: its two sites are clock-only or benign; - no existing export fits: `zonedWallClockToUtcMs` with no zone equals the helper, but only through its documented fallback. - **H3 held.** PR objectstack-ai#20601's bare-day `datetime` path goes through `Date.parse`. A cell with a time goes through `zonedWallClockToUtcMs`, which is now correct, so `import-coerce.ts` is untouched. - **H4 partly falsified.** The `Date.UTC` half is gone: `bucketDateKey('0050-01-01T10:00Z', week)` is week 52 of 0049, not of 1949. `bucketKeyToCalendarRange` spans padded keys (`0050`, `0050-Q4`, `0050-12`, `0050-01-01`) in their own years. The round trip from `bucketDateKey` to `bucketKeyToCalendarRange` still does **not** hold below year 1000, at any granularity: - `bucketDateKey` spells those years unpadded (`50`, `50-Q1`, `50-01`, `50-01-01`, `49-W52`), and the range function reads only `\d{4}`; - the week arm validates against the unpadded label, so even a padded SQL key `0050-W01` answers `null`; - that is the unpadded-key family, which the dispatch excluded (out-of-scope finding 1). - **H5 held.** The rollover is load-bearing at Q4's and December's end (month 13), a day key's end (day 32) and `daysInMonth` (day 0). The helper rolls identically. Nine rollover cases are pinned in 0001..0099, plus four 2026 cases equal to `Date.UTC`. ## Pins Each file runs its zone-free sites on a UTC host and on an Asia/Shanghai host (`process.env.TZ`, asserted to have taken). - `packages/core/src/utils/datetime-year-below-100.test.ts` (160 cases): the helper, rollover and `NaN`; `zonedWallClockToUtcMs` and `zonedDateStartToUtcMs` with no zone, UTC, Asia/Shanghai and America/New_York; `bucketDateKey` week in UTC and Asia/Shanghai; `bucketKeyToCalendarRange` year, quarter, month and day, plus the 2026 week control. - `packages/core/src/utils/filter-tokens-year-below-100.test.ts` (26): year and month macros into 0001, 0050 and 0099 in UTC and Asia/Shanghai, the February-29 clamp in years 48, 50 and 100, and day steps padded. - `packages/services/service-analytics/src/__tests__/week-key-year-below-100.test.ts` (42): the preview `bucketDate` week, and the `compareTo` ordinals from `bucketOrdinalOfDay` and `bucketKeyAtOrdinal`. - `packages/triggers/trigger-schedule/src/time-relative-window-year-below-100.test.ts` (20): `offsetDays` and `withinDays` windows, and the claim scope. - `packages/rest/src/import-datetime-year-below-100.test.ts` (74). This is the `domain:cli` seat's round-trip pin, in their `export-date-year-pad.test.ts` harness pattern: - `parseDateCell` on two hosts; - `POST /import` of a CSV with no zone, UTC and Asia/Shanghai; - rows created, sent through `GET /export` (csv and json) and re-imported into a fresh stack, under no zone, Asia/Shanghai and America/New_York, for 0001, 0050, 0099, 0100 and 2026. - The round trip pads an exported year below 1000 to four digits before re-importing, as PR objectstack-ai#20688's export will write it. That half is PR objectstack-ai#20688's (`Blocked-by: objectstack-ai#20599`); once it lands, the step changes nothing, and PR objectstack-ai#20688 can drop its own `dt: undefined` exclusion for 0001 and 0050. Every expected instant is spelled as an ISO string, never computed by the code under test. ## Reverse verification (committed first, rebuilt, and checked in `dist/`) - **Mutate.** `node scripts/ablation-replace.mjs` replaced the helper's body with `Date.UTC(parts.year, …)`: anchor 1 → 0, blob `fda5c68ba9bb` → `9d0def6aaa50`. Then `pnpm --filter @objectstack/core build`, and `ablation-dist-preflight.mjs @objectstack/core 'Date.UTC(parts.year'` said the marker is present in 2 built files. Result: core **120 failed / 66 passed**, service-analytics **28 / 14**, trigger-schedule **12 / 8**, rest **38 / 36**. For example, `expected '1950-01-01T00:00:00.000Z' to be '0050-01-01T00:00:00.000Z'`, preview week `expected '1949-12-26' to be '0049-12-27'`, and window `gte '1950-09-30T00:00:00.000Z'`. Every 0100, 2026, `NaN` and padding control stayed green. The direction was red, as predicted. - **Restore.** The blob equals HEAD `fda5c68ba9bb`, and `git diff HEAD` is empty. After a rebuild, the preflight with `--absent 'Date.UTC(parts.year'` finds the marker in none of 14 files, and the tree is clean. Result: **186 / 42 / 20 / 74 passed**. - **Base leg.** With core's two files at `4dfff176b9` (blob match: yes), rest's pin file gave **38 failed / 36 passed**. Exactly the 0001, 0050 and 0099 rows failed; `imports every row` stayed green, which is the silent `ok`. ## Tests and gates (after the final commit, `da39ddacc0`) - `pnpm --filter PKG test`: - core 60 files / 1728 tests; - service-analytics 140 / 3266; - trigger-schedule 8 / 170; - rest 229 / 4461 passed and 55 skipped; - objectql 337 / 6687 (a consumer of `bucketDateKey` and the filter tokens). - The non-SQL temporal suite under `TZ=America/New_York`, asserted to have taken, all green: core, formula 42 / 1240, driver-memory 65 / 1470, driver-mongodb 29 / 661 (172 skipped), service-analytics. - `pnpm --filter PKG typecheck` is green for core, service-analytics, trigger-schedule and rest. Each program's `--listFiles` includes the new test files. - `node scripts/pm/dispatch-gates.mjs --commands`: 64 commands, all exit 0. `check:dual-build-cjs-loads` and `check:type-check-debt` first answered `PREREQUISITE NOT MET` (exit 3), and answered 0 after `turbo run build --filter='./packages/*' --filter='./packages/*/*'`. `--ran`: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN. - eslint, narrowed: - population: the 10 changed `.ts` files, all matched by `eslint.config.mjs`'s `files` globs; - count: `--format json` read 10 files, 0 errors and 0 warnings; - invariance: `--print-config` shows no `parserOptions.project` or `projectService`. That is, no type-aware linting, and the only cross-file inputs are two baselines this diff does not touch, so no untouched file's verdict can move. - **NOT MEASURED: the live `driver-sql` leg of Temporal Conformance.** This container has no MySQL server, and no `driver-sql` source imports a changed helper. CI runs it. ## Not in this PR - spec `calendar-day.ts`, which PR objectstack-ai#20591 already corrected. - The unpadded year in bucket keys and the export (objectstack-ai#20534 / objectstack-ai#20602), and MySQL's read-back (objectstack-ai#20280). objectstack-ai#20602 and objectstack-ai#20280 are not addressed here. - PR objectstack-ai#20688 stays drafted by the `domain:cli` seat. This PR removes the `Blocked-by` cause. ## Acceptance notes - **Out-of-scope finding 1, reported to the seat and not filed from here.** `bucketDateKey`, `isoWeekLabelFromCalendarDay` and service-analytics `bucketKeyAtOrdinal` spell a year below 1000 unpadded. SQL drivers' bucket expressions pad it (`strftime('%Y')`), so the in-memory and pushed-down keys differ for those years, and `bucketKeyToCalendarRange`'s week arm answers `null` even for a padded key. This belongs to the unpadded-year family of objectstack-ai#20602. - `formula` keeps a private calendar-day copy (`stdlib.ts:59`), reached only through `now()`, and a day-count use of `Date.UTC` (`:102`). Both read right for 1..99, so they are unchanged. `examples/app-todo` has the same day-count shape. - `driver-mongodb`'s `mongodb-pipeline-evaluator.testkit.ts` reads an ISO string through `Date.UTC` and would model a year-50 instant in 1950. It is a test model, not product; noted with no carrier. - The forward direction `calendarPartsInTz` reads `Intl`'s era year too. It matters only for an instant whose local day is before 0001-01-01, which is outside the supported range. --- _Generated by [Claude Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20550
Clause-②: no
nextUtcCalendarDay(packages/spec/src/data/calendar-day.ts) proves a bareYYYY-MM-DDis a real day by building it and reading it back. It built the date withDate.UTC(y, mo - 1, d), andDate.UTCreads a year from 0 to 99 as 1900 + year, so0050-01-01was built as1950-01-01, the round trip failed, and the helper answerednullfor every day of the years 0001..0099.utcInstantMsasks the same helper about a bare day, so it answerednullfor them too. Adatetime$lteor$betweenmaximum on such a day then skipped whole-day widening (ADR-0053 D-D) and stopped at the day's first instant.The accept set is unchanged: a query on years 0001..0099 now answers what the contract already says (the claim's
Clause-②: no).The change
packages/spec/src/data/calendar-day.ts: a privateutcMidnight(year, monthIndex, day)builds the date asnew Date(0)plussetUTCFullYear(year, monthIndex, day), which takes the year as written and rolls the month and day over exactly asDate.UTCdoes.nextUtcCalendarDayuses it for both the round trip and the next day. The impossible-day refusal is unchanged in kind:2026-02-30,0050-02-30and0100-02-29still roll over and are refused by the round trip.utcInstantMsis byte-identical. Its bare-day arm already delegated the "is this a real day" question tonextUtcCalendarDayand then read the instant withDate.parseof ISO text, which reads year 0050 correctly; its timestamp arm never touchedDate.UTC. So the one construction fixes both helpers. No other helper in the file builds a date.packages/core/**orpackages/drivers/**: every caller imports these helpers from@objectstack/spec(core re-exports them).Verification (final head
6a5dce7378, basef11b5f20a2)Premise, measured before editing:
tsximportingpackages/spec/src/data/calendar-day.tsatf11b5f20a2.nextUtcCalendarDay('0050-01-01')=null,utcInstantMs('0050-01-01')=null; the same for0001-01-01and0099-12-31. Control:'0100-01-01'answers'0100-01-02'and-59011459200000.YYYY-MM-DDwith year 0000..9999, month 00..13 and day 00..32 (4,620,000 strings, 3,652,425 real days), compared with a pure-arithmetic proleptic Gregorian oracle that uses noDate. Base: 36,525 mismatches for each helper, exactly every real day of the years 0000..0099. Head: 0 mismatches for either helper.packages/rest/src/data-query-calendar-day-year-below-100.test.tsthroughPOST /api/v1/data/:object/query, process inAmerica/New_York, withcalendar-day.ts's construction put back toDate.UTC(reverse verification below). SQLite and PostgreSQL 16 answered the same:where opened_at$lte '0050-01-01'y49y49,y50,y50_last$between ['0050-01-01', '0050-01-01']y50,y50_last$lte '2026-07-15'(control)c26,y49,y50,y50_last,y50_next$between ['2026-07-15', '2026-07-15'](control)c26Rows:
y49=0049-12-31T10:00Z,y50=0050-01-01T10:00Z(the card's row),y50_last=0050-01-01T23:59:59.999Z,y50_next=0050-01-02T00:00Z,c26=2026-07-15T14:00Z(the card's control),c26_next=2026-07-16T00:00Z. The next day's midnight stays out in every cell.Reverse verification (fix committed first, mutation through
scripts/ablation-replace.mjs,traprestore to theHEADblob): theutcMidnightbody was replaced byreturn new Date(Date.UTC(year, monthIndex, day));; the anchor fell 1 to 0 and the replacement rose 0 to 1 on disk;pnpm --filter @objectstack/spec build;scripts/ablation-dist-preflight.mjsfound the mutation in 4 built files and the fix's line in none. Thencalendar-day.test.ts: 3 failed / 7 passed (0001-01-01: expected null to be '0001-01-02'); the REST file: 2 failed / 2 passed, the table's base column, identical on both cells. Restore: blobf03557d5bbequalsHEAD, whole-treegit status --porcelainempty, a full spec rebuild, the preflight found the fix in 4 built files and the mutation in none, and both files went green again (10/10, 4/4).Tests at
6a5dce7378:pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 574 files, 16,883 passed, 1 todo.@objectstack/rest, the new file with the existing temporal suites (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 a private PostgreSQL 16 server (zoneAsia/Shanghai) asOS_TEST_POSTGRES_URL: 10 files, 89 passed, 5 skipped. The 5 skips are MySQL cells (OS_TEST_MYSQL_URLunset). The new file ran on SQLite and PostgreSQL, the two drivers the 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 harness runs.driver-sqlsql-driver-calendar-day-upper-bound.test.ts(SQLite and PostgreSQL ran, MySQL not run): 15 passed.driver-memory: the sixmemory-analytics-date-range-*suites andmemory-driver-calendar-day-upper-bound.test.ts(run, not edited): 7 files, 103 passed.pnpm --filter @objectstack/spec typecheckandpnpm --filter @objectstack/rest typecheck: exit 0;tsc --listFilesoverpackages/rest/tsconfig.test.jsonincludes the new test file.eslint --no-inline-config --format jsonover the three touched.tsfiles: 3 files, 0 errors, 0 warnings.eslint --print-configresolves a config for each of them, andeslint.config.mjsenables no type-aware linting (noparserOptions.project, noprojectService), so the diff cannot move any untouched file's verdict.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands: 83 commands, 81 exit 0. Two were NOT MEASURED,PREREQUISITE NOT MET(exit 3), because they need the whole workspace built:pnpm check:dual-build-cjs-loads(44 packages withoutdist/) andpnpm check:type-check-debt(7 dependencies without a built type entry). CI runs both.Full-repo pin sweep: no test asserts
nullfrom either helper for a year below 100 (git grepfor both names applied to a00NN-literal: 0 hits), and no suite asserts a$lte/$between/ date-range upper bound on adatetimefield in those years. So no pin had to change.Acceptance notes
0000-12-31is followed by0001-01-01), where the base answerednull. It is not pinned: 0001..9999 is the supported range, and@objectstack/core'sisOutsideTemporalYearRangerefuses year 0000 at the comparand and write doors before either helper is asked. The helper does not keep a second copy of that range.@objectstack/driver-memoryhas no binding inpackages/rest):$lte '0050-01-01'answersy49,y50; the$betweenmaximum answersy50; the 2026 controls are unchanged.Date.UTCconstructions read an author-given year below 100 as 1900 + year, inpackages/core/src/utils/datetime.tsandpackages/rest/src/import-coerce.ts. For example, the import door stores a CSVdatetimecell0050-01-01 10:00as1950-01-01T10:00:00.000Z. Separately,nextUtcCalendarDay('9999-12-31')answers'10000-01-01', so on SQLite adatetime$lte '9999-12-31'answers no rows. That is unchanged by this PR and measured in the report.Generated by Claude Code