Skip to content

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

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20760-bucket-key-four-digit-year
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20760-bucket-key-four-digit-year

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

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


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/core, @objectstack/service-analytics, touching 10 documentable anchor(s).

⛔ 1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via bucketKeyToCalendarRange (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 31 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 660a9b247e824f7747d63f3b778dccc9cb4751d6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 797ddf6877b1f295674ad786897d24eafe4cfccb — the merge of head c03977051a1869f6dd3e8c66e9f4dc49abbab0df into base 660a9b247e824f7747d63f3b778dccc9cb4751d6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 660a9b247e824f7747d63f3b778dccc9cb4751d6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c03977051a1869f6dd3e8c66e9f4dc49abbab0df
Local-runs: none

PR #20865 for card #20760, branch claude/issue-20760-bucket-key-four-digit-year; the head was confirmed unchanged at review time. Inputs: the card body and its three comments (triage 5903751640, claim 5910885597, os-dev-report 5912276902), the PR body and file list, the net diff against main (merge base 05a7547c9f, 10 files, +532 / -63), PR #20746 (#20599, a6866da0c) as the prior fix in this family, and the head's check-runs. Nothing was built, run or re-run.

Check-runs on the head, as read by this act (shortly before 2026-09-30T13:40Z): 39 check-runs; 28 completed success, 5 completed skipped (Build Docs, Console Pin Gate, Packed-tarball smoke, and the re-run Auto Label and Check PR Size), 6 in_progress with no conclusion yet (Check Changeset on its second run, Test Core 1/6, 5/6 and 6/6, Lint & Repo Gates, Type Check · workspace). No check-run had failed. Concluded success: the first Check Changeset run, Build Core, Test Core 2/6 to 4/6, Type Check source gates, consumer gates and debt ledger, Temporal Conformance (live PG + MySQL), all three Dogfood gates and Dogfood Verify CLI, the Governed Surface Queue Guard, Check Documentation Links, Flag docs affected, and the PR-automation guards. Combined commit status: one context, success. The verdict below is on the contract; landing still needs every check green, which this record does not assert.

① Derived judgments

Triage's direction (5903751640), judged against the diff:

  • One writer — right. bucketDateKey spells year, quarter, month and day through a new module-private bucketKeyYear(year) (String(year).padStart(4, '0') for a non-negative year, plain spelling otherwise) and bucketDayKey; isoWeekLabelFromCalendarDay spells the week-year through the same helper. git grep padStart(4 over the non-test sources of core, objectql, driver-memory and service-analytics finds only bucketKeyYear and the pre-existing temporalStorageForm date rule: no local padding anywhere. isoWeekLabelUtc, the reader's validator, delegates to isoWeekLabelFromCalendarDay, so core carries one week derivation.
  • One reader — right. bucketKeyToCalendarRange keeps its \d{4} pattern on every arm; the week arm's label check now compares the padded key against the padded label the writer gives the reconstructed Monday, so 0050-W01 answers 0050-01-03 up to 0050-01-10 (pinned, with 0049-W52 and 0999-W24). Every bound is spelled by bucketDayKey, the writer's own day spelling. No unpadded fallback: 50, 50-Q2, 50-06, 50-06-15, 49-W52 and 999-W24 are pinned null.
  • bucketKeyAtOrdinal delegates to core — right. Every arm now computes only the UTC instant the ordinal's bucket starts at and hands it to bucketDateKey through a private bucketKeyAt. Each inverse checked against bucketOrdinalOfDay: year is the year itself; quarter is floor(o/4) and o - 4y in 0..3, month 3q + 1; month is floor(o/12) and o - 12y in 0..11; week is 7o - 3 days, the Monday, since the ordinal is floor((ms + 3 days) / 7 days); day is o days. calendarDayAt delegates too. The deleted private isoWeekKeyOfUtcMs had one source caller and one test-comment mention at main, both edited here; a git grep for a -W label spelling over the four packages' non-test sources finds only core's one line. No second week derivation remains. The only other text that moves is the unreachable invariant error's wording (no {granularity} bucket for no calendar day), same condition, same envelope — right.
  • Public surface — right. No export added or removed in either package: bucketKeyYear, bucketDayKey and bucketKeyAt are module-private, and BucketGranularity was already a core root export through export *.

Published spellings that move (the dev's six), each judged:

  1. bucketDateKey for years 0000..0999 at every granularity: 50 becomes 0050, 50-Q2 becomes 0050-Q2, 50-06 becomes 0050-06, 50-06-15 becomes 0050-06-15, 49-W52 becomes 0049-W52; 1000..9999 is byte-identical (2026 control pinned). This is the contract the function's own docblock states, that its label equals the driver's SQL label, and SQLite's strftime('%Y') is pinned padded in the new driver-sql test. Right.
  2. In-memory groupBy keys: objectql bucketDateValue is a one-line delegate, unchanged; pinned at every granularity for a date value and a stored instant on 0050-06-15, plus 0050-01-01 keying 0049-W52. Right. The side effect the dev named is confirmed: having-filter's declared date class for a day bucket is unchanged code, and its value now conforms to YYYY-MM-DD for those years. Not a surface change.
  3. Memory cube labels: driver-memory aggregateWithTimeBuckets delegates, unchanged; pinned at every granularity. Right.
  4. compareTo merge keys: bucketKeyAtOrdinal now mints 0050-06 and 0049-W52, equal to the grouped key (pinned against bucketDateKey for 0001, 0050, 0050-01-01, 0999 and 2026); alignedCompareBucketKey restates 0049-06 as 0050-06 and 0049-W24 as 0050-W24 (pinned). Right.
  5. The reader's week ranges for a padded key below 1000: null becomes a range; pinned through queryDataset's drillRanges for 0050-W01, 0049-W52, 0999-W24 and the 2026 control. Right.
  6. The display relabel (formatDateBucket, unchanged code): see ③. Not worse, not a surface change.

A narrowing a consumer can see: none. No input the reader accepted before is refused now; the unpadded spelling never matched its \d{4} patterns, and the week arm's null for 0050-W01 was a defect inside its declared accept set, not a boundary. The in-repo readers of a bucket key are the two core arms, alignedCompareBucketKey through the reader, and formatDateBucket; a git grep for -Q / -W key patterns over non-test package sources finds no other reader, and no caller reads an unpadded key. A caller outside this repo that relied on 50-06 relied on a spelling the pushed-down path never produced; the changeset states what moved. No driver SQL changes: the file list touches no driver source, and driver-sql gains a test only.

Pins against triage's list: for 0050-06-15 the in-memory face and SQLite agree at year, quarter, month and day through a datetime and a date column (the driver-sql pin); week has no SQL cell on SQLite because the driver refuses it and the engine buckets it in memory (sql-driver.ts states so), which the pin's header says — right. 0050-W01 drills to its range — pinned. A four-digit year unchanged — pinned in every file. 0999 at every granularity — pinned. The dev's ablation, read only: mutating the padding line to String(year) reddened 101 core, 6 objectql, 5 driver-memory, 4 driver-sql (the in-memory cells; the SQL cells stayed green because SQLite pads on its own) and 30 service-analytics cases, then restored green. Consistent with the pins' shape.

② Semver level

Changeset .changeset/20760-bucket-key-four-digit-year.md: @objectstack/core: patch and @objectstack/service-analytics: patch — right. Both are bug fixes in released packages toward a stated contract, and the body names the FROM and TO spellings and the reader's new answer, which is what an upgrading consumer greps. No changeset for objectql, driver-memory or driver-sql — right: their sources are untouched (test files only) and their behaviour moves through core, which the fixed group in .changeset/config.json versions together with them. skip-changeset is not used and would be wrong here. The first Check Changeset run concluded success.

The declaration line: the PR body's second line declares no with no arm — right. Nothing widens an accept set or a public surface (no export, no new key shape, the reader's patterns unchanged), and nothing narrows (no accepted input is refused). no with no arm is the reading scripts/pm/clause2-line.mjs gives a fix of this shape.

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; check:dual-build-cjs-loads and check:type-check-debt first exit 3 on an unbuilt dist, then 0 after the full packages build, 71/71; --ran 67 derived, 67 run, 0 NOT MEASURED, 0 unrun) matches the report's gates field and is exactly the replacement its deviations[0] asked for. Right.

③ Boundary flags

open_questions is empty. Dev flags, each answered:

  • H3 — SQLite and PostgreSQL 16.13 measured padded, MySQL NOT MEASURED. Answered, not a FAIL: the diff changes no driver SQL; MySQL's date_format %Y and %x are documented four-digit; the card's required SQL pin is SQLite, which is committed; the PostgreSQL reading is the dev's uncommitted harness, declared. The head's Temporal Conformance (live PG + MySQL) check concluded success, but its date-bucket-parity.ts fixture probes 2024 and 2025 instants only, so it does not measure this family. Escalated as a residual for driver-sql on MySQL reads a year 0..99 back a century late — REST create stores placed_on: "0009-03-04" correctly, and …/query returns "1909-03-04"; a datetime 0009-03-04T10:00Z returns 2004-09-03T10:00Z #20280's ground and the live dialect matrix, where a MySQL date in 0001..0999 could be pinned.
  • H4 — years 0, -1 and 10000, no refusal added. Right. -1 keys -1, 10000 keys 10000, and the reader answers null for both: pinned. Year 0 keying 0000 follows from padStart by inspection and is stated in the body but not pinned; acceptable, since neither engine door reaches any of the three (date 0001..9999, datetime 1000..9999) and a refusal would have been outside the direction.
  • Two existing test files edited (datetime-year-below-100.test.ts, week-key-year-below-100.test.ts): comments corrected and their key readers made strict ((\d+)-W to (\d{4})-W; numbers() reads NaN for a non-four-digit year). Right, and in the direction's spirit: a lenient reader beside the faces would have been an unpadded fallback in test form. Inside the pins surface.
  • One merge of main (47c272769a merges 05a7547c9f, fix(core,objectql)!: a datetime names a year from 1000 to 9999 at both engine doors; a date keeps 0001..9999 (#20280) #20843, the datetime floor). The diff against main is exactly the PR's own 10 files, and the head's datetime.ts carries fix(core,objectql)!: a datetime names a year from 1000 to 9999 at both engine doors; a date keeps 0001..9999 (#20280) #20843's floor comment and states the floor in the new range sentences. Right.
  • Attribution deviation: the branch's commits carry the model-free Co-authored-by and Claude-Session trailer pair AGENTS.md requires. Right.
  • Out-of-scope notes — does this PR make any worse at a public door?
    • formatDateBucket (service-analytics, unchanged; resolveDimensionLabels applies it to a date dimension's rows when queryDataset resolves labels). For an in-memory year key the display was 1970 before (50 read as epoch seconds) and is 1970 after (0050 reads as the number 50, the same path): unchanged, and wrong on both sides exactly as it already was for the SQL key. For month and day it now shows 50-06 and 50-06-15, what it already showed for the SQL path's 0050-06; the dev measured the base display of the in-memory key as 1950-06. Not worse at any granularity; the year arm's 1000..9999 recogniser is the unpadded-year family of [finding] /export writes a date / datetime cell with a year below 1000 unpadded (0500-01-01 → 500-01-01), so the export does not re-import #20602 and stays for that family card. drillRanges are computed before the relabel (analytics-service.ts reads the key first), so the drill-down is unaffected.
    • preview-evaluator.ts bucketDate (the draft preview, unchanged): still spells 50, 50-Q2, 50-06, 50-06-15, and its week key is the Monday's YYYY-MM-DD, a vocabulary of its own at every year. Its keys never matched the reader before and do not now; no preview output changes. Not worse. Note that triage's premise that the service-analytics callers follow the helper was false for this writer, so the preview and the runtime dataset path now differ in spelling for years below 1000 where they agreed on the in-memory path before (and never agreed with the SQL path). Escalated: a card for the seat to carry (fold into [finding] /export writes a date / datetime cell with a year below 1000 unpadded (0500-01-01 → 500-01-01), so the export does not re-import #20602's family as the report proposes, or its own), not a rider on this PR.
    • driver-mongodb mongodb-aggregation.ts header comment ("labels '0999' here and '999' in memory") is now false; comment only, outside the surface. Since Mongo's %Y pads, that face now agrees with the helper. Left for the file's next editor. Not worse.
    • checkDateBucketParity probes 2024 and 2025 only: a pre-existing blind spot, unchanged; a gate change is the maintainer's. Not worse.
    • The live dialect matrix not extended: see H3.

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: claude/issue-20760-bucket-key-four-digit-year
Reviewed-by: session_01DEvba2nBuD4tWzfq8r8NFY

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 13:43
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 856321f Sep 30, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20760-bucket-key-four-digit-year branch September 30, 2026 14:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants