fix(core,service-analytics): a date-bucket key spells its year with four digits, and the reader reads the week key the writer writes (#20760) - #20865
Conversation
…its (#20760) Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
…alytics (#20760) Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
…r spelling (#20760) Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 31 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 797ddf6877b1f295674ad786897d24eafe4cfccb && git checkout 797ddf6877b1f295674ad786897d24eafe4cfccb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 660a9b247e824f7747d63f3b778dccc9cb4751d6 c03977051a1869f6dd3e8c66e9f4dc49abbab0df && git checkout -B drift-repro 660a9b247e824f7747d63f3b778dccc9cb4751d6 && git merge --no-ff c03977051a1869f6dd3e8c66e9f4dc49abbab0df
node scripts/docs-audit/affected-docs.mjs --json 660a9b247e824f7747d63f3b778dccc9cb4751d6
|
Contract reviewServed-tier: PR #20865 for card #20760, branch Check-runs on the head, as read by this act (shortly before 2026-09-30T13:40Z): 39 check-runs; 28 completed ① Derived judgmentsTriage's direction (5903751640), judged against the diff:
Published spellings that move (the dev's six), each judged:
A narrowing a consumer can see: none. No input the reader accepted before is refused now; the unpadded spelling never matched its Pins against triage's list: for ② Semver levelChangeset The declaration line: the PR body's second line declares Review faces: the changeset text is accurate against the diff (SQLite pinned; PostgreSQL named as measured, which is the dev's uncommitted live harness, stated as such in the PR body). The PR body's "Gates" bullet (67 of 67 exit 0; ③ Boundary flags
No governed surface is touched (file list: core and service-analytics sources, test files in core, objectql, driver-memory, driver-sql and service-analytics, one changeset). Six check-runs were still running when read; a red among them is that gate's verdict, not this record's. Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #20760
Clause-②: no
What changes
Triage direction 5903751640, as ruled: one writer, one reader, no local padding. Nothing in any driver's SQL moves.
@objectstack/corebucketDateKeyand its ISO week label. A privatebucketKeyYear(year)spells the year of a bucket key with four digits, and every arm goes through it:year,quarter,month,day(through a privatebucketDayKey) and the week label (isoWeekLabelFromCalendarDay). A year from 1000 to 9999 is spelled as before. No export is added or removed.bucketKeyToCalendarRange. Its patterns already required a four-digit year. The defect was the week arm: it checks a key against the label the writer gives the reconstructed Monday, and that label was unpadded, so0050-W01answerednull. Its bounds are now spelled by the writer's own day key (fmtisbucketDayKey), so the two share one spelling. It does not accept the unpadded spelling (50-06,49-W52): nothing writes it after this change.service-analyticsbucketKeyAtOrdinal(the declared cross-lane line). It spelled every granularity itself and carried a private copy of the ISO week rule (isoWeekKeyOfUtcMs, deleted). It now computes only the UTC instant the ordinal's bucket starts at, and core'sbucketDateKeyspells the key (a localbucketKeyAt, whichcalendarDayAtnow delegates to). No second padding.Before and after (function level)
Core at base
c90f9fb6e2, read from a temporary copy of the basedatetime.ts, against this branch:0050-06-15T10:00Z50·50-Q2·50-06·50-06-15·50-W240050·0050-Q2·0050-06·0050-06-15·0050-W240050-01-01T10:00Z50·50-Q1·50-01·50-01-01·49-W520050·0050-Q1·0050-01·0050-01-01·0049-W520999-06-15T10:00Z999·999-Q2·999-06·999-06-15·999-W240999·0999-Q2·0999-06·0999-06-15·0999-W242026-06-15T10:00Z(control)2026·2026-Q2·2026-06·2026-06-15·2026-W25bucketKeyToCalendarRange(key, 'week'):0050-W01,0049-W52and0999-W24answerednullat base. They now answer0050-01-03..0050-01-10,0049-12-27..0050-01-03and0999-06-10..0999-06-17.2026-W01answers2025-12-29..2026-01-05on both.The zone-2 hypotheses
H1, the callers: held, and the census found two more writers. Each named caller follows the helper with no edit:
objectqlin-memory-aggregation.tsbucketDateValueis a thin delegate (pinned);objectqlhaving-filter.tsnames the helper in a comment only; itsdaybucket'sdateclass now holds a realYYYY-MM-DD;driver-memorymemory-analytics.tsaggregateWithTimeBucketsdelegates (pinned);service-analyticsanalytics-service.ts, the drill ranges, callsbucketKeyToCalendarRange(pinned throughqueryDataset);service-analyticsdataset-executor.ts:calendarDayAtdelegates,alignedCompareBucketKeyreads through the reader (pinned), andbucketKeyAtOrdinalis H2;corecompensated-sum.tsnames the helper in a comment only.Two
service-analyticswriters call no helper and spell the year unpadded:preview-evaluator.tsbucketDateanddimension-labels.tsformatDateBucket. Both are outside the declared surface (Acceptance notes).H2: held.
bucketKeyAtOrdinalbuilt its own keys at every granularity. It now calls the helper (above).H3: SQLite and PostgreSQL pad, measured; MySQL is NOT MEASURED.
bucketDateKeyatyear,quarter,monthanddaythrough adatetimeand adatecolumn. SQLite bucketsweekin memory, not in SQL.SqlDriverraninitObjects,createandaggregateover adatetimeand adatecolumn holding 0050-06-15, 0050-01-01, 0999-06-15 and 2026-06-15. The keys equalbucketDateKeyat all five granularities, 0 mismatches in 10 cells,0049-W52fromIYYY"-W"IWincluded. This was a scratch harness, not committed (Acceptance notes).%Yand%xas four digits.H4: read at function level, not reached, and no refusal added.
0000, as SQLite'sstrftime('%Y')does.-1, never the padded fragment00-1. SQLite answers-001.10000. SQLite answers NULL.nullfor both the −1 and the 10000 key.dateand adatetimevalue. The shape is pinned in the core file below.H5: held. Early January 0050 keys
0049-W52, and0049-W52spans0049-12-27..0050-01-03. This is pinned in core, objectql, service-analytics and the PostgreSQL reading.Pins
One new file beside each face. Every expected key is spelled literally, and each file covers 0050, 0999 and the 2026 control:
datetime-bucket-key-four-digit-year.test.ts:Dateand as epoch ms too;0050-W01included);null;in-memory-aggregation-four-digit-year.test.ts: the engine's in-memorygroupByat every granularity.memory-analytics-four-digit-year.test.ts: the memory cube face at every granularity.sql-driver-bucket-key-four-digit-year.test.ts:SqlDriveron SQLite against core'sbucketDateKey, for 0050-06-15.bucket-key-four-digit-year.test.ts:bucketKeyAtOrdinalagainst the grouped key at every granularity;alignedCompareBucketKeyrestating0049-06/0049-W24as0050-06/0050-W24;queryDatasetdrill-down from0050-W01finding0050-01-03..0050-01-10.Two existing files had comments stating the unpadded spelling as current (
datetime-year-below-100.test.tsin core,week-key-year-below-100.test.tsin service-analytics). Their comments are corrected, and their lenient readers now require the four-digit year.Ablation: red/green on the padding
At
3716880ded. The padding line is byte-identical at the PR head.scripts/ablation-replace.mjsturnedreturn year >= 0 ? String(year).padStart(4, '0') : String(year);intoreturn String(year);(anchor 1 → 0, blobfe67aac16ed5→5a14574b2faf).dist/. Core was rebuilt, andablation-dist-preflight --absentpassed: the marker was absent from all 14 built files. objectql and service-analytics read core fromdist/.datetime*: 101 failed / 151 passed of 252;fe67aac16ed5andgit diff HEADis empty. Core was rebuilt, and preflight in default mode found the marker in 2 built files with the tree clean.Verification (head
c03977051a)mainat05a7547c9f(#20843) was merged in. It edits one comment indatetime.ts, and this branch's two range sentences now state itsdatetimefloor.vitest run, at076ba0c599, after the merge and a rebuild of the touched closure):local61 files / 1793 passed,repo3 / 48;local345 / 6779;c03977051a, which changes only the driver-sql pin: every face's pins plus driver-sql'sdate-bucketanddate-bucket-storagesuites pass, 36 / 252 / 6 / 5 / 57.typecheckexits 0 for all five packages.tsc --listFilesshows each new test file in a compiled program.node scripts/pm/dispatch-gates.mjs --commandsderived 67 families atc03977051a, and all 67 ran there. 67 of 67 exited 0.--ranreconciles: 67 derived, 67 run, 0 NOT MEASURED, 0 unrun.check:dual-build-cjs-loadsandcheck:type-check-debtfirst exited 3 (PREREQUISITE NOT MET: only the touched closure was built). Both exited 0 afterpnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'(71/71 tasks). The seat corrected this bullet fromos-dev-report5912276902; the head is unchanged.as anyon the driver-sql pin's aggregate options (check:query-options-erasure, test surface 236 → 237). It was typed inc03977051a, and the ratchet holds at 236.eslint --no-inline-config --format jsonran over the 9 changed.tsfiles: 9 files, 0 errors, 0 warnings, none reported as ignored. The config covers all 9. It enables no type-aware linting (noparserOptions.projectanywhere, stated ineslint.config.mjsitself), so this diff cannot move a verdict on an untouched file. The fullpnpm lintis CI's.Clause-② measured
Six published outputs change spelling for a year below 1000:
bucketDateKey;groupBykeys;compareTomerge keys;bucketKeyToCalendarRange, which now answers for a padded week key;formatDateBucketgives an in-memory month or day key: it was1950-06, a wrong century, and is now50-06, what it gives the SQL key.Each is now what the SQL path answered already: SQLite pinned, PostgreSQL measured. The reader drops no key it read before, because the unpadded spelling never matched its four-digit patterns. So
Clause-②: nostands.Acceptance notes
dimension-labels.tsformatDateBucket(service-analytics). It relabels a date dimension's rows when labels resolve. For a padded key below 1000 it answersyear0050→1970, reading a pure-digit key as epoch seconds because its year check admits only 1000..9999. It answersmonth0050-06→50-06andday0050-06-15→50-06-15. Function level. It is the same unpadded-year family as [finding]/exportwrites adate/datetimecell with a year below 1000 unpadded (0500-01-01→500-01-01), so the export does not re-import #20602, it sits outside the declared surface, and it is reported for the family card.preview-evaluator.tsbucketDate(service-analytics, the draft preview). It spells the year unpadded atyear/quarter/month/day. Itsweekkey is the Monday'sYYYY-MM-DD(pinned so), not the ISO label. Outside the surface.driver-mongodbmongodb-aggregation.ts. A header comment says a year before 1000 "labels '0999' here and '999' in memory". This PR makes it false. The file is outside the declared surface, so it is left for its next editor.@objectstack/verifycheckDateBucketParity. It probes 2024 and 2025 instants only, so the parity device cannot see this family on any driver. No gate change here.live-dialect-matrix.testkit.ts). It is not extended to this pin. A MySQLdatetimebelow 1000 is driver-sql on MySQL reads a year 0..99 back a century late — REST create storesplaced_on: "0009-03-04"correctly, and…/queryreturns"1909-03-04"; adatetime0009-03-04T10:00Zreturns2004-09-03T10:00Z#20280's ground, and its reading is not measured here.Generated by Claude Code