Repository navigation
Commit 856321f
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.
- **One writer: `@objectstack/core` `bucketDateKey` and its ISO week
label.** A private `bucketKeyYear(year)` spells the year of a bucket key
with four digits, and every arm goes through it: `year`, `quarter`,
`month`, `day` (through a private `bucketDayKey`) and the week label
(`isoWeekLabelFromCalendarDay`). A year from 1000 to 9999 is spelled as
before. No export is added or removed.
- **One reader: `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, so `0050-W01` answered `null`. Its bounds are now
spelled by the writer's own day key (`fmt` is `bucketDayKey`), so the
two share one spelling. It does not accept the unpadded spelling
(`50-06`, `49-W52`): nothing writes it after this change.
- **`service-analytics` `bucketKeyAtOrdinal` (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's
`bucketDateKey` spells the key (a local `bucketKeyAt`, which
`calendarDayAt` now delegates to). No second padding.
## Before and after (function level)
Core at base `c90f9fb6e2`, read from a temporary copy of the base
`datetime.ts`, against this branch:
| instant | base | this branch |
|:--|:--|:--|
| `0050-06-15T10:00Z` | `50` · `50-Q2` · `50-06` · `50-06-15` · `50-W24`
| `0050` · `0050-Q2` · `0050-06` · `0050-06-15` · `0050-W24` |
| `0050-01-01T10:00Z` | `50` · `50-Q1` · `50-01` · `50-01-01` · `49-W52`
| `0050` · `0050-Q1` · `0050-01` · `0050-01-01` · `0049-W52` |
| `0999-06-15T10:00Z` | `999` · `999-Q2` · `999-06` · `999-06-15` ·
`999-W24` | `0999` · `0999-Q2` · `0999-06` · `0999-06-15` · `0999-W24` |
| `2026-06-15T10:00Z` (control) | `2026` · `2026-Q2` · `2026-06` ·
`2026-06-15` · `2026-W25` | identical |
`bucketKeyToCalendarRange(key, 'week')`: `0050-W01`, `0049-W52` and
`0999-W24` answered `null` at base. They now answer
`0050-01-03`..`0050-01-10`, `0049-12-27`..`0050-01-03` and
`0999-06-10`..`0999-06-17`. `2026-W01` answers
`2025-12-29`..`2026-01-05` on 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:
- `objectql` `in-memory-aggregation.ts` `bucketDateValue` is a thin
delegate (pinned);
- `objectql` `having-filter.ts` names the helper in a comment only; its
`day` bucket's `date` class now holds a real `YYYY-MM-DD`;
- `driver-memory` `memory-analytics.ts` `aggregateWithTimeBuckets`
delegates (pinned);
- `service-analytics` `analytics-service.ts`, the drill ranges, calls
`bucketKeyToCalendarRange` (pinned through `queryDataset`);
- `service-analytics` `dataset-executor.ts`: `calendarDayAt` delegates,
`alignedCompareBucketKey` reads through the reader (pinned), and
`bucketKeyAtOrdinal` is H2;
- `core` `compensated-sum.ts` names the helper in a comment only.
Two `service-analytics` writers call no helper and spell the year
unpadded: `preview-evaluator.ts` `bucketDate` and `dimension-labels.ts`
`formatDateBucket`. Both are outside the declared surface (Acceptance
notes).
- **H2: held.** `bucketKeyAtOrdinal` built its own keys at every
granularity. It now calls the helper (above).
- **H3: SQLite and PostgreSQL pad, measured; MySQL is NOT MEASURED.**
- SQLite 3.53.4: pinned below. The keys equal `bucketDateKey` at `year`,
`quarter`, `month` and `day` through a `datetime` and a `date` column.
SQLite buckets `week` in memory, not in SQL.
- PostgreSQL 16.13: measured live on a throwaway local server. This
branch's `SqlDriver` ran `initObjects`, `create` and `aggregate` over a
`datetime` and a `date` column holding 0050-06-15, 0050-01-01,
0999-06-15 and 2026-06-15. The keys equal `bucketDateKey` at all five
granularities, 0 mismatches in 10 cells, `0049-W52` from `IYYY"-W"IW`
included. This was a scratch harness, not committed (Acceptance notes).
- MySQL: NOT MEASURED. This container has no MySQL server (no binary, no
Docker daemon). The reference manual gives `%Y` and `%x` as four digits.
- No dialect was found whose bucket SQL does not pad, and no driver
changes.
- **H4: read at function level, not reached, and no refusal added.**
- Year 0 keys `0000`, as SQLite's `strftime('%Y')` does.
- Year −1 keys `-1`, never the padded fragment `00-1`. SQLite answers
`-001`.
- Year 10000 keys `10000`. SQLite answers NULL.
- The reader answers `null` for both the −1 and the 10000 key.
- Both engine doors refuse these years for a `date` and a `datetime`
value. The shape is pinned in the core file below.
- **H5: held.** Early January 0050 keys `0049-W52`, and `0049-W52` spans
`0049-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:
- core `datetime-bucket-key-four-digit-year.test.ts`:
- the writer at every granularity, for 0001, 0050, 0999 and 2026, as a
`Date` and as epoch ms too;
- the Asia/Shanghai week-year;
- the reader round-tripping every written key;
- the week ranges above (`0050-W01` included);
- the unpadded spelling answering `null`;
- H4.
- objectql `in-memory-aggregation-four-digit-year.test.ts`: the engine's
in-memory `groupBy` at every granularity.
- driver-memory `memory-analytics-four-digit-year.test.ts`: the memory
cube face at every granularity.
- driver-sql `sql-driver-bucket-key-four-digit-year.test.ts`:
`SqlDriver` on SQLite against core's `bucketDateKey`, for 0050-06-15.
- service-analytics `bucket-key-four-digit-year.test.ts`:
- `bucketKeyAtOrdinal` against the grouped key at every granularity;
- `alignedCompareBucketKey` restating `0049-06` / `0049-W24` as
`0050-06` / `0050-W24`;
- a `queryDataset` drill-down from `0050-W01` finding
`0050-01-03`..`0050-01-10`.
Two existing files had comments stating the unpadded spelling as current
(`datetime-year-below-100.test.ts` in core,
`week-key-year-below-100.test.ts` in 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.
- **Mutation.** `scripts/ablation-replace.mjs` turned `return year >= 0
? String(year).padStart(4, '0') : String(year);` into `return
String(year);` (anchor 1 → 0, blob `fe67aac16ed5` → `5a14574b2faf`).
- **It reached `dist/`.** Core was rebuilt, and `ablation-dist-preflight
--absent` passed: the marker was absent from all 14 built files.
objectql and service-analytics read core from `dist/`.
- **Red:**
- core `datetime*`: 101 failed / 151 passed of 252;
- objectql: 6 of 6 failed;
- driver-memory: 5 of 5 failed;
- driver-sql: 4 failed / 8 passed. The 8 are the SQL cells, green
because SQLite pads on its own; only the in-memory cells go red;
- service-analytics: 30 failed / 102 passed of 132.
- **Restore.** The blob equals HEAD's `fe67aac16ed5` and `git diff HEAD`
is empty. Core was rebuilt, and preflight in default mode found the
marker in 2 built files with the tree clean.
- **Green:** 252 / 6 / 5 / 12 / 132.
## Verification (head `c03977051a`)
`main` at `05a7547c9f` (#20843) was merged in. It edits one comment in
`datetime.ts`, and this branch's two range sentences now state its
`datetime` floor.
- **Suites** (`vitest run`, at `076ba0c599`, after the merge and a
rebuild of the touched closure):
- core: `local` 61 files / 1793 passed, `repo` 3 / 48;
- objectql: `local` 345 / 6779;
- driver-memory: 66 / 1475;
- driver-sql: 202 passed, 11 skipped (the live cells, which have no URL
here) / 3266 tests;
- service-analytics: 142 / 3282.
- **At `c03977051a`,** which changes only the driver-sql pin: every
face's pins plus driver-sql's `date-bucket` and `date-bucket-storage`
suites pass, 36 / 252 / 6 / 5 / 57.
- **Typecheck:** `typecheck` exits 0 for all five packages. `tsc
--listFiles` shows each new test file in a compiled program.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands` derived 67
families at `c03977051a`, and all 67 ran there. 67 of 67 exited 0.
`--ran` reconciles: 67 derived, 67 run, 0 NOT MEASURED, 0 unrun.
- `check:dual-build-cjs-loads` and `check:type-check-debt` first exited
3 (PREREQUISITE NOT MET: only the touched closure was built). Both
exited 0 after `pnpm exec turbo run build --filter='./packages/*'
--filter='./packages/*/*'` (71/71 tasks). The seat corrected this bullet
from `os-dev-report` 5912276902; the head is unchanged.
- The first run caught one `as any` on the driver-sql pin's aggregate
options (`check:query-options-erasure`, test surface 236 → 237). It was
typed in `c03977051a`, and the ratchet holds at 236.
- **Lint (narrowed, proven).** `eslint --no-inline-config --format json`
ran over the 9 changed `.ts` files: 9 files, 0 errors, 0 warnings, none
reported as ignored. The config covers all 9. It enables no type-aware
linting (no `parserOptions.project` anywhere, stated in
`eslint.config.mjs` itself), so this diff cannot move a verdict on an
untouched file. The full `pnpm lint` is CI's.
## Clause-② measured
Six published outputs change spelling for a year below 1000:
- `bucketDateKey`;
- the in-memory `groupBy` keys;
- the memory cube labels;
- the `compareTo` merge keys;
- `bucketKeyToCalendarRange`, which now answers for a padded week key;
- the display label `formatDateBucket` gives an in-memory month or day
key: it was `1950-06`, a wrong century, and is now `50-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-②:
no` stands.
## Acceptance notes
- **`dimension-labels.ts` `formatDateBucket` (service-analytics).** It
relabels a date dimension's rows when labels resolve. For a padded key
below 1000 it answers `year` `0050` → `1970`, reading a pure-digit key
as epoch seconds because its year check admits only 1000..9999. It
answers `month` `0050-06` → `50-06` and `day` `0050-06-15` → `50-06-15`.
Function level. It is the same unpadded-year family as #20602, it sits
outside the declared surface, and it is reported for the family card.
- **`preview-evaluator.ts` `bucketDate` (service-analytics, the draft
preview).** It spells the year unpadded at `year` / `quarter` / `month`
/ `day`. Its `week` key is the Monday's `YYYY-MM-DD` (pinned so), not
the ISO label. Outside the surface.
- **`driver-mongodb` `mongodb-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/verify` `checkDateBucketParity`.** It probes 2024 and
2025 instants only, so the parity device cannot see this family on any
driver. No gate change here.
- **The live dialect matrix (driver-sql
`live-dialect-matrix.testkit.ts`).** It is not extended to this pin. A
MySQL `datetime` below 1000 is #20280's ground, and its reading is not
measured here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent f10d802 commit 856321f
10 files changed
Lines changed: 532 additions & 63 deletions
File tree
- .changeset
- packages
- core/src/utils
- drivers
- driver-memory/src
- driver-sql/src
- objectql/src
- services/service-analytics/src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
Lines changed: 112 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
158 | 158 | | |
159 | 159 | | |
160 | 160 | | |
161 | | - | |
162 | | - | |
163 | | - | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
164 | 165 | | |
165 | | - | |
| 166 | + | |
166 | 167 | | |
167 | 168 | | |
168 | 169 | | |
| |||
242 | 243 | | |
243 | 244 | | |
244 | 245 | | |
245 | | - | |
246 | | - | |
247 | | - | |
| 246 | + | |
| 247 | + | |
248 | 248 | | |
249 | 249 | | |
250 | 250 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
302 | 302 | | |
303 | 303 | | |
304 | 304 | | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
305 | 312 | | |
306 | 313 | | |
307 | 314 | | |
| |||
319 | 326 | | |
320 | 327 | | |
321 | 328 | | |
322 | | - | |
| 329 | + | |
323 | 330 | | |
324 | | - | |
| 331 | + | |
325 | 332 | | |
326 | | - | |
| 333 | + | |
327 | 334 | | |
328 | | - | |
| 335 | + | |
329 | 336 | | |
330 | 337 | | |
331 | 338 | | |
| |||
337 | 344 | | |
338 | 345 | | |
339 | 346 | | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
340 | 374 | | |
341 | 375 | | |
342 | 376 | | |
| |||
347 | 381 | | |
348 | 382 | | |
349 | 383 | | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
350 | 388 | | |
351 | 389 | | |
352 | 390 | | |
| |||
361 | 399 | | |
362 | 400 | | |
363 | 401 | | |
364 | | - | |
| 402 | + | |
365 | 403 | | |
366 | 404 | | |
367 | 405 | | |
| |||
398 | 436 | | |
399 | 437 | | |
400 | 438 | | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
401 | 446 | | |
402 | 447 | | |
403 | 448 | | |
404 | 449 | | |
405 | 450 | | |
406 | 451 | | |
407 | | - | |
408 | | - | |
409 | | - | |
410 | | - | |
411 | | - | |
| 452 | + | |
412 | 453 | | |
413 | 454 | | |
414 | 455 | | |
| |||
Lines changed: 65 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
0 commit comments