Repository navigation
[finding] core bucket keys spell a year below 1000 unpadded (50-06, 49-W52) while the SQL drivers pad it (0050-06), and bucketKeyToCalendarRange answers null for a padded week key such as 0050-W01 #20760
Description
Activity
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p3·domain:engine·area:reports·pm:queue. Direction: every bucket key spells its year with four digits, through one core helper, andbucketKeyToCalendarRangereads what the helper writesTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T03:57Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: the helpers are
@objectstack/core's (bucketDateKey,isoWeekLabelFromCalendarDay), read byobjectql,driver-memoryandservice-analytics⇒domain:engine. The service-analytics callers follow the helper and aren't re-derived. It takes #20599's grade (p3): the same years 0001..0999 family.Why p3. The in-memory and pushed-down paths answer different keys, and a drill-down finds no range, but only for years 0001..0999. No public door was measured.
Direction.
- One writer.
bucketDateKeyandisoWeekLabelFromCalendarDaypad the year to four digits at every granularity, as the SQL drivers'strftimedoes.bucketDateKey's own contract says its label must equal the driver's.bucketKeyAtOrdinalreuses the helper. ⛔ No local padding. - One reader.
bucketKeyToCalendarRangeparses exactly what the helper writes, including the padded week key. ⛔ It does not also accept the unpadded spelling: nothing produces it after this change. - Pins:
- for
0050-06-15, the in-memory face andSqlDriveron SQLite answer the same key at every granularity; - a drill-down from
0050-W01finds its range; - a four-digit year is unchanged (the control).
- for
- Not here. [finding]
/exportwrites adate/datetimecell with a year below 1000 unpadded (0500-01-01→500-01-01), so the export does not re-import #20602 (/export,domain:cli) is the same family on another surface and stays its own card.
- One writer.
- addedarea:reportsBusiness reporting — dashboards, reports, the numbers a manager readsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething isn't workingSomething isn't working
on Sep 30, 2026 objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsClaim: PM loop round 24
Session:session_01DEvba2nBuD4tWzfq8r8NFY
Account:os-support-ai(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-20760-bucket-key-four-digit-year
Worktree:objectstack-issue-20760
Domain:domain:engine
Seat:domain:engine#1
File surface (triage's direction 5903751640):packages/core/src/utils/datetime.ts: one writer and one reader.bucketDateKeyandisoWeekLabelFromCalendarDayspell the year with four digits at every granularity, as the SQL drivers'strftimedoes.bucketKeyToCalendarRangeparses exactly what the writer writes, the padded week key included. ⛔ It does not also accept the unpadded spelling.
- Cross-lane surface, declared here:
packages/services/service-analytics/src/dataset-executor.ts,bucketKeyAtOrdinal, reuses the helper. ⛔ No local padding. The otherservice-analyticscallers follow the helper and are not re-derived. - pins: for
0050-06-15, the in-memory face (objectqlin-memory aggregation, thedriver-memorycube face) andSqlDriveron SQLite answer the same key at every granularity; a drill-down from0050-W01finds its range;0999at every granularity; a four-digit year unchanged (the control). .changeset/20760-*.md.
Stop on breach and explain in the report. ⛔ Not #20602 (
/export,domain:cli). ⛔ No driver SQL change.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5903751640
Serial constraints cleared: read at 2026-09-30T12:02Z againstorigin/main.Clause-②: no: a key's spelling for years 0001..0999 aligns with what the SQL drivers already answer, and the reader drops no key the writer produces. The dev measures whether a published output (an in-memorygroupBykey) changes spelling for a consumer and says so.- PR fix(core,objectql)!: a datetime names a year from 1000 to 9999 at both engine doors; a date keeps 0001..9999 (#20280) #20843 (driver-sql on MySQL reads a year 0..99 back a century late — REST create stores
placed_on: "0009-03-04"correctly, and…/queryreturns"1909-03-04"; adatetime0009-03-04T10:00Zreturns2004-09-03T10:00Z#20280, in the merge queue) edits one comment line indatetime.ts. Whichever lands second mergesmain.
- added 5 commits that reference this issue
on Sep 30, 2026 objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20760,
"status": "done",
"branch": "claude/issue-20760-bucket-key-four-digit-year",
"pr": "#20865",
"session": "session_01DEvba2nBuD4tWzfq8r8NFY (mode:subagent). The newest Claim: on #20760 (5910885597) names this branch; no second claim was posted and the card's assignee was not written.",
"premise_still_valid": true,
"summary": "Draft PR #20865 at head c039770 (BASE c90f9fb; origin/main 05a7547, PR #20843, merged in because it edits datetime.ts). Body line 1 is the closing line for this card, line 2 is Clause-②: no; the stored body read back byte-identical (11486 bytes); PR assignee os-support-ai read back. @objectstack/core: a private bucketKeyYear spells the year of every bucketDateKey key with four digits (year, quarter, month, day via a private bucketDayKey, and the ISO week label), and bucketKeyToCalendarRange spells its bounds with the same day key, so its week arm now answers 0050-W01 -> 0050-01-03..0050-01-10 (null at base) and reads no unpadded key; no export added or removed. service-analytics bucketKeyAtOrdinal computes only the bucket's first UTC instant and hands it to core's bucketDateKey (local bucketKeyAt; the private isoWeekKeyOfUtcMs week copy is deleted). Hypotheses: H1 held (every named caller follows with no edit; the census adds two unpadded writers outside the surface, preview-evaluator bucketDate and dimension-labels formatDateBucket, reported below); H2 held; H3 SQLite pinned padded and PostgreSQL 16.13 measured live through this branch's SqlDriver (10 of 10 cells equal bucketDateKey, 0049-W52 included), MySQL NOT MEASURED (no server in the container); H4 year 0 -> 0000, -1 -> -1, 10000 -> 10000, reader null for the last two, not reached, no refusal added, pinned; H5 held (0050-01-01 -> 0049-W52, pinned). Clause-② measured: every published spelling that moves (bucketDateKey, in-memory groupBy keys, memory cube labels, compareTo merge keys, the reader's padded week ranges, formatDateBucket's display of an in-memory month/day key) now equals what the SQL path already answered; nothing narrows, so the line stands.",
"tests": "Suites at 076ba0c (after the merge and a rebuild of the touched closure; the only later commit edits the driver-sql pin): pnpm --filter @objectstack/core test -> 61 files / 1793 passed; core vitest --project repo 3/48; objectql vitest --project local 345/6779; driver-memory 66/1475; driver-sql 202 passed + 11 skipped (live cells, no URL) / 3266 passed + 188 skipped; service-analytics 142/3282. At c039770 every face's pins plus driver-sql date-bucket and date-bucket-storage: 36 / 252 / 6 / 5 / 57 passed. typecheck exit 0 for core, service-analytics, driver-memory, driver-sql, objectql; tsc --listFiles counts each new test file in a compiled program. Ablation at 3716880 (padding line byte-identical at head): scripts/ablation-replace.mjs mutated 'return year >= 0 ? String(year).padStart(4, ...) : String(year);' -> 'return String(year);' (anchor 1 -> 0, blob fe67aac16ed5 -> 5a14574b2faf); core rebuilt; ablation-dist-preflight --absent green (absent from all 14 built files). RED: core datetime* 101 failed / 151 passed (252); objectql 6/6 failed; driver-memory 5/5 failed; driver-sql 4 failed / 8 passed (the SQL cells stay green, SQLite pads by itself); service-analytics 30 failed / 102 passed (132). Restore: blob == HEAD fe67aac16ed5, git diff HEAD empty, rebuilt, preflight default mode found the marker in 2 built files with the tree clean. GREEN: 252 / 6 / 5 / 12 / 132. Base reading at c90f9fb from a temporary copy of datetime.ts: 0050-06-15 -> 50 / 50-Q2 / 50-06 / 50-06-15 / 50-W24, weeks 0050-W01 / 0049-W52 / 0999-W24 -> null; 2026 control identical on both. Lint narrowed and proven: eslint --no-inline-config --format json over the 9 changed .ts files -> 9 files, 0 errors, 0 warnings, none reported ignored; population read from eslint.config.mjs (all 9 inside it); invariance: the config enables no type-aware linting (no parserOptions.project; eslint.config.mjs states it), so the diff moves no verdict on an untouched file; full pnpm lint is CI's.",
"mcp_calls": "0 — no MCP GitHub tool was called; reads went through unauthenticated REST GET on the public repo.",
"api_writes": "3 — each one fleet-write relay dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executing one op as objectstack-fleet[bot]: pr_create (POST /repos/objectstack-ai/objectstack/pulls, draft forced) -> #20865; assign through scripts/pm/label-write.mjs (POST /repos//issues/20865/assignees); this os-dev-report comment (POST /repos//issues/20760/comments). git push (branch probe and six commits) is not a REST write.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: named producer — SqlDriver's bucket SQL (SQLite strftime('%Y'), PostgreSQL to_char 'YYYY', both measured padded) and since this PR the in-memory faces key a year below 1000 as 0050…; analytics-service resolveDimensionLabels hands a date dimension's key to service-analytics dimension-labels.ts formatDateBucket, which answers year 0050 -> 1970, month 0050-06 -> 50-06, day 0050-06-15 -> 50-06-15 (function level at c039770; quarter and week keys pass through unchanged; 2026 control correct); a public door was not measured · evidence: formatDateBucket's contract is an idempotent re-labeller over bucket keys, and its year recogniser admits only 1000..9999, so a pure-digit 0050 is read as epoch seconds · family: the unpadded-year family of #20602, fold into its family card, no single-point card · dedupe words: formatDateBucket year 0050 1970 · dimension label unpadded year 50-06 · resolveDimensionLabels bucket key year below 1000",
"class: a · reach: named producer — service-analytics preview-evaluator.ts bucketDate over a draft's seed rows: 0050-06-15T10:00Z keys 50 / 50-Q2 / 50-06 / 50-06-15 (function level at c039770) while the published dataset path now keys 0050 / 0050-Q2 / 0050-06 / 0050-06-15; a public door was not measured · evidence: bucketDateKey's four-digit output contract, which this preview face does not delegate to · family: #20602's unpadded-year family, fold into its family card · dedupe words: preview bucketDate unpadded year 50-06 · draft preview bucket key year below 1000",
"carrier: none — driver-mongodb mongodb-aggregation.ts header comment ('a year before 1000 labels 0999 here and 999 in memory') is made false by this PR; comment only, outside the declared surface (stop on breach), so not edited; in the PR's Acceptance notes, not filed",
"carrier: none — @objectstack/verify checkDateBucketParity probes 2024/2025 instants only, so the parity device cannot see a year-below-1000 divergence on any driver; noted in Acceptance notes, not filed (a gate change is the maintainer's)",
"carrier: none — preview-evaluator bucketDate's week key is the Monday's YYYY-MM-DD (0050-06-13), not the ISO week label the runtime writes; pinned as such in week-key-year-below-100.test.ts; vocabulary observation, noted, not filed",
"carrier: none — MySQL date_format '%Y' / '%x' for a year below 1000 is NOT MEASURED here (no MySQL server or Docker daemon in the container; the reference manual gives four digits); the driver-sql live dialect matrix was not extended to this pin because a MySQL datetime below 1000 is #20280's ground"
],
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at c039770: 67 commands, identical to the list derived before the typing fix. All 67 ran at c039770: 65 exited 0 on the first pass; check:dual-build-cjs-loads and check:type-check-debt exited 3 (PREREQUISITE NOT MET: unbuilt dist) and then exited 0 after pnpm exec turbo run build --filter='./packages/' --filter='./packages//' (71/71 tasks, 10 cached). --ran reconciliation over the recorded exit codes: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN. The first run, at 076ba0c, caught check:query-options-erasure red (test surface 236 -> 237, an 'as any' on the driver-sql pin's aggregate options); typed in c039770, the ratchet holds at 236. dispatch-gates warned the tree is 3 commits behind origin/main 660a9b2 (scripts/doc-authoring-prose-id.baseline.json moved); not merged, the derived list is the same.",
"line_budget": "diff vs merge base 05a7547: 10 files, +532 / -63 (595 changed lines, under the 5000 human-merge threshold). No skills/** or governed surface is touched. Source: core datetime.ts and service-analytics dataset-executor.ts; the rest is 5 new pin files, 2 corrected existing test files and 1 changeset.",
"deviations": [
"The PR body's Verification section says check:dual-build-cjs-loads and check:type-check-debt were NOT MEASURED (exit 3). Both were measured after the PR opened: exit 0 at c039770 following a full packages build (71/71). The PR body was not patched; the fix for the seat to write is to replace that bullet with: '67 of 67 exit 0 at c039770; the two prerequisite gates exited 0 after turbo run build --filter=./packages/ --filter=./packages// (71/71); --ran: 67 run, 0 NOT-MEASURED'.",
"The core suite was invoked once as 'pnpm --filter @objectstack/core test -- --maxWorkers=2'; the bare -- dropped the worker flag and the whole local project ran (61/1793), which was the intended scope. Every other run passed flags without --.",
"PostgreSQL was measured on a throwaway PG 16.13 instance started as the postgres user under /var/lib/postgresql/os-issue-20760-probe (the scratchpad's parents are root 0700), on a private socket, port 54761; stopped with pg_ctl and its directory deleted. The harness is a scratch script, not committed.",
"The base reading used a temporary copy of the base datetime.ts inside the worktree (packages/core/src/utils/zz-base-probe-20760*.ts), removed by an EXIT trap in the same command; git status --porcelain read 0 afterwards.",
"Two existing test files beside the faces were edited, inside the declared surface: core datetime-year-below-100.test.ts and service-analytics week-key-year-below-100.test.ts carried comments stating the unpadded spelling as current; the comments are corrected and their lenient key readers now require a four-digit year.",
"Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer; AGENTS.md governs (model-free trailer pair, which the pre-push hook enforces, and the session-URL PR footer), so those were used."
],
"files_changed": [
".changeset/20760-bucket-key-four-digit-year.md",
"packages/core/src/utils/datetime.ts",
"packages/core/src/utils/datetime-bucket-key-four-digit-year.test.ts",
"packages/core/src/utils/datetime-year-below-100.test.ts",
"packages/drivers/driver-memory/src/memory-analytics-four-digit-year.test.ts",
"packages/drivers/driver-sql/src/sql-driver-bucket-key-four-digit-year.test.ts",
"packages/objectql/src/in-memory-aggregation-four-digit-year.test.ts",
"packages/services/service-analytics/src/tests/bucket-key-four-digit-year.test.ts",
"packages/services/service-analytics/src/tests/week-key-year-below-100.test.ts",
"packages/services/service-analytics/src/dataset-executor.ts"
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsACCEPT — PR #20865 @
c03977051domain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-30T13:42Z. The seat is the reviewer of record. Everything below was read on GitHub and onorigin/main, not taken from the report.- Shape: the first line is
Fixes #20760, thenClause-②: no. The changeset is@objectstack/coreand@objectstack/service-analyticspatch. The PR assignee isos-support-ai. 10 files, +532/-63; not governed. - One writer, one reader (triage 5903751640):
bucketDateKeyspells the year of every bucket key with four digits, at year, quarter, month, day and ISO week, through one private helper.bucketKeyToCalendarRangereads exactly those keys.0050-W01now drills to its range (it answerednullbefore), and no unpadded fallback was added.service-analyticsbucketKeyAtOrdinaldelegates to core, and its private week copy is deleted.- No export is added or removed.
- Alignment, not a narrowing: every published spelling that moves now equals what the SQL path already answered: SQLite pinned, and PostgreSQL 16.13 measured live, 10 of 10 cells including
0049-W52. A four-digit year is unchanged. MySQL was not measured locally; its%Yand%xare documented four-digit, and the diff changes no driver SQL. - Contract review: at-tier record 5912503757 on this head, PASS (read-only,
Local-runs: none). It judged that no out-of-scope note gets worse at a public door. - Out of scope, filed: [finding] service-analytics still spells a year below 1000 its own way:
formatDateBucketlabels bucket key0050as 1970 and0050-06as50-06, and the draft-previewbucketDatewrites unpadded keys the published path no longer writes #20867:service-analyticsformatDateBucketlabels0050as 1970, and the draft-previewbucketDatestill writes unpadded keys. Both were already so before this PR. - Carriers, not filed:
- A
driver-mongodbheader comment is now false (comment only):carrier:the file's next editor. checkDateBucketParityprobes 2024/2025 only:carrier: none(a gate change is the maintainer's).
- A
- PR body: the seat corrected its "Gates" bullet from the dev's report: the two prerequisite gates exited 0 after a full build. The head is unchanged.
- Checks on this head: 36
success, 5skipped, all rostered.
Landing: ready and auto-merge in this act.
Generated by Claude Code
- Shape: the first line is
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsLanded — PR #20865 as
856321fa7domain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-30T14:09Z.- Verified on
main:856321fa7is a squash with one parent (f10d802b5) and an ancestor oforigin/main.bucketKeyYearappears inpackages/core/src/utils/datetime.tsat the squash (8 hits) and not at its parent. 10 files, +532/-63, as reviewed. - Route: ACCEPT 5912526760 (posted and read back first), then
pr_readyandautomerge_enableas two separate relay acts at the reviewed headc03977051(PASS 5912503757). The 5 skipped checks were all rostered.added_to_merge_queueat 13:46Z; merged by the queue at 14:08Z. - What is on
mainnow:- every core bucket key spells its year with four digits, at every granularity, equal to the SQL drivers' keys;
bucketKeyToCalendarRangereads exactly those keys, so0050-W01drills to its range;service-analyticsbucketKeyAtOrdinaldelegates to core.
- Follow-up filed: [finding] service-analytics still spells a year below 1000 its own way:
formatDateBucketlabels bucket key0050as 1970 and0050-06as50-06, and the draft-previewbucketDatewrites unpadded keys the published path no longer writes #20867 (service-analyticsformatDateBucketand the draft-previewbucketDate). - Card:
Fixes #20760did not close it through the queue, the sixth time this term. The seat closes it ascompletedin this act and removespm:dispatched.
Generated by Claude Code
- Verified on
- added a commit that references this issue
on Oct 7, 2026
Filing gate: ① a product defect with a named producer. Finding class (b).
reach:named real producers atorigin/maina6866da0c, read by this seat:packages/objectql/src/in-memory-aggregation.ts:323, the engine's in-memorygroupBydate bucket;packages/drivers/driver-memory/src/memory-analytics.ts:1436, the memory cube face's bucket;packages/services/service-analytics/src/analytics-service.ts:2039, the drill-down that turns a grouped row's bucket key back into a calendar range;packages/services/service-analytics/src/dataset-executor.ts(bucketKeyAtOrdinal, andbucketKeyToCalendarRangeat:1006), thecompareToalignment.The readings are the #20599 dev's, at function level on PR #20746's head
da39ddacc(os-dev-reporton #20599,out_of_scope_findings[0]), and the at-tier review 5902551719 confirmed them from the code. ⛔ A public door was not measured.Filed by the
domain:engineexecution seat 1 (session_01DEvba2nBuD4tWzfq8r8NFY,os-support-ai), as its ACCEPT 5902610249 on #20599 recorded. ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim. Reader: triage, then this lane's seat (the helpers are@objectstack/core's).What happens
For an instant on
0050-06-15:SqlDriveron SQLite (strftime('%Y-%m', …))0050-06bucketDateKey(in-memory aggregation, memory cube face)50-0650-W24bucketDateKey,isoWeekLabelFromCalendarDayand service-analyticsbucketKeyAtOrdinalspell every year below 1000 unpadded, at every granularity (50,50-Q2,50-06,50-06-15,49-W52).bucketKeyToCalendarRangereads only\d{4}keys, so it answersnullfor the unpadded key the in-memory face produced.nulleven for a padded SQL key such as0050-W01.So for years 0001..0999:
groupByanswers different keys on the in-memory and pushed-down paths;bucketDateKey's own contract says its label must equal the one the driver's SQL produces for the same instant.Not this
Date.UTC's 1900 remap). It did not touch the key's spelling, which the card excluded by name./exportwrites adate/datetimecell with a year below 1000 unpadded (0500-01-01→500-01-01), so the export does not re-import #20602 (domain:cli,packages/rest/export) 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 (closed, the/importreader) are the same unpadded-year family on other surfaces.Suggested shape (⛔ not a ruling)
Spell the year of every bucket key with four digits, as the
datestorage form does (temporalStorageForm). Make the week arm ofbucketKeyToCalendarRangevalidate against the padded label. Pin 0050 and 0999 at every granularity, in memory against SQLite, with a 2026 control.Dedupe
mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:datecell 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/importreader), temporal values outside the years a four-digit text or a backend holds: adatetimecomparand for year 10000 or −1 misorders on memory/SQLite and 500s on PostgreSQL; adatein year 0000 500s on PostgreSQL; adatewrite stores+010000-…verbatim #20264 (the year range) and coretemporalStorageForm: thedatearm leaves a year outside 1000..9999 unpadded — over REST the epoch-ms number for 0999-06-15 counts$gt0 /$lt7 on InMemoryDriver and SQLite (correct 6 / 0); its ISO string counts 6 / 0 #20240 (the storage form'sdatearm). None carries the bucket keys;Dedupe words:
bucketDateKey unpadded year below 1000·bucketKeyToCalendarRange week 0050-W24 null·in-memory bucket key 50-06 strftime 0050-06Generated by Claude Code