Skip to content

fix(service-analytics): a date-bucket key's two readers match core's writer for a year below 1000, and the draft preview keys a week by its ISO label (#20867) - #20971

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20867-analytics-year-keys
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20867-analytics-year-keys

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20867
Clause-②: no

What changes

Triage direction 5912878801, as ruled: the two service-analytics readers of a date-bucket key match @objectstack/core's one writer, bucketDateKey. Nothing in packages/core, packages/spec or packages/objectql moves.

  • dimension-labels.ts formatDateBucket (the dimension labels queryDataset resolves after grouping). A string that is a key the writer writes at the stated granularity is returned as written. What counts as one is decided by core's one reader of those keys, bucketKeyToCalendarRange, not by a second pattern here. A raw value (epoch ms, ISO text, Date) is relabelled by bucketDateKey itself, in UTC, so no label here spells a year of its own. A week or unstated granularity keeps labelling a raw value as its own day key, as before. The numeric-year branch (a driver answering 2026 as a number) is unchanged.
  • preview-evaluator.ts bucketDate (the draft preview, queryDataset with previewDrafts). It is now bucketDateKey(value, granularity, timezone), typed with core's BucketGranularity. No second padding. Its caller passes the cube dimension's single granularity as typed, where it used to pass String(...) of it.

bucketDateKey, bucketKeyToCalendarRange, isBucketGranularity and BucketGranularity are exports of @objectstack/core (src/utils/datetime.ts, re-exported by src/index.ts), a runtime dependency of this package that dataset-executor.ts and analytics-service.ts already import them from.

Before and after

Base f6ccca4a44, head 25ba591849. Function level, TZ=UTC, read with a scratch tsx script over the package's own sources and core's built dist. The reference column is bucketDateKey.

input driver reference before after
key 0050 formatDateBucket(key, 'year') 0050 1970 0050
key 0050-06 formatDateBucket(key, 'month') 0050-06 50-06 0050-06
key 0050-06-15 formatDateBucket(key, 'day') 0050-06-15 50-06-15 0050-06-15
key 0999 formatDateBucket(key, 'year') 0999 1970 0999
keys 0050-Q2, 0050-W24, 0049-W52 formatDateBucket as written as written as written
raw 0050-06-15T10:00:00.000Z formatDateBucket(raw, year / quarter / month / day) 0050 · 0050-Q2 · 0050-06 · 0050-06-15 50 · 50-Q2 · 50-06 · 50-06-15 0050 · 0050-Q2 · 0050-06 · 0050-06-15
2026 keys and raw values (control) formatDateBucket unchanged unchanged
0050-06-15T10:00:00.000Z bucketDate year / quarter / month / week / day 0050 · 0050-Q2 · 0050-06 · 0050-W24 · 0050-06-15 50 · 50-Q2 · 50-06 · 0050-06-13 · 50-06-15 as the reference
0050-01-01T10:00:00.000Z bucketDate week 0049-W52 0049-12-27 0049-W52
0999-06-15T10:00:00.000Z bucketDate year / month 0999 · 0999-06 999 · 999-06 as the reference
2026-06-15T10:00:00.000Z (control) bucketDate week 2026-W25 2026-06-15 2026-W25
2026-06-15T10:00:00.000Z (control) bucketDate year / quarter / month / day 2026 · 2026-Q2 · 2026-06 · 2026-06-15 the same the same
epoch ms of 2026-06-15T10Z / 0050-06-15T10Z bucketDate, every granularity the key null (the empty bucket) the key
Date of 0050-06-15T10Z bucketDate year / day 0050 · 0050-06-15 1950 · 1950-06-15 as the reference

And at the queryDataset level, over the same three rows (0050-06-15, 0050-06-15T10:00:00.000Z, 2026-06-15). The published leg is the ObjectQL strategy over the engine's in-memory grouping (@objectstack/objectql applyInMemoryAggregation) followed by the dimension labels. The preview leg is previewDrafts over the same rows as the seed draft.

granularity published, before preview, before both, after
year 1970:2 · 2026:1 50:2 · 2026:1 0050:2 · 2026:1
quarter 0050-Q2:2 · 2026-Q2:1 50-Q2:2 · 2026-Q2:1 0050-Q2:2 · 2026-Q2:1
month 50-06:2 · 2026-06:1 50-06:2 · 2026-06:1 0050-06:2 · 2026-06:1
week 0050-W24:2 · 2026-W25:1 0050-06-13:2 · 2026-06-15:1 0050-W24:2 · 2026-W25:1
day 50-06-15:2 · 2026-06-15:1 50-06-15:2 · 2026-06-15:1 0050-06-15:2 · 2026-06-15:1

The preview's week key (triage's third bullet): measured, and adopted

  • Measured at base: the preview wrote the Monday's YYYY-MM-DD (2026-06-15) where the runtime writes the ISO week label (2026-W25), for every year, the 2026 control included.
  • Adopted: bucketDate delegating to bucketDateKey writes the ISO week label. The difference is not kept.
  • Why it matters beyond the label: the preview runs the same DatasetExecutor, whose compareTo alignment reads a key through bucketKeyToCalendarRange, and that reader answers null for a Monday date at week. Measured with one row on 2026-06-15 and one on 2025-06-16, a previousYear comparison over the window 2026-06-15..2026-06-21:
    • published, before and after: one row, 2026-W25, current 1, compare 1;
    • preview, before: two rows, 2026-06-15 (1 / 0) and 2025-06-16 (0 / 1), the comparison appended as a foreign row;
    • preview, after: one row, 2026-W25 (1 / 1), as published.
  • The two existing preview week pins (week-key-year-below-100.test.ts, [[finding] outside calendar-day.ts, a year from 0001 to 0099 is still read as 1900..1999: Date.UTC's two-digit-year remap in core's datetime and bucket helpers, filter-tokens and the REST import's datetime cell #20599]) pinned the Monday date. They now read the ISO label for the same instants, and the Monday each week starts on stays in a comment.

Clause-② measured

no, with no arm. The package's public entry (src/index.ts, exports . only) exports neither function, so no export is added, removed or retyped. What changes is published output: the labels resolveDimensionLabels / queryDataset give a key or a raw value in 0001..0999, and the preview's keys. Each now equals what the published path answers or the writer writes. No input that was answered before is refused now. The public door parses timeDimensions[].granularity and dateGranularity against DatasetSelectionSchema (read at schema level: hour refused, week accepted), so bucketDate's narrowed parameter type admits every value that door lets through. The line parses as no under scripts/pm/clause2-line.mjs readClause2Line.

Pins

src/__tests__/bucket-key-readers-four-digit-year.test.ts (new), every expected key spelled literally, 0050 / 0050-01-01 / 0999 with a 2026 control:

  • 0050, 0050-06 and 0050-06-15 label as written, and so does every key the writer gives each instant at each granularity;
  • a raw value in 0050 is relabelled with the year in four digits;
  • bucketDate equals the writer for ISO text, epoch ms and a Date, and in Asia/Shanghai (0049-W52 in UTC is 0050-W01 there);
  • a draft preview keys 0050-06-15 as the published path does, at every granularity, with the 2026 control.

Committed red first (748c4bd5b9): 38 failed / 28 passed of 66 across the new file and the two updated preview week pins. The fix is 25ba591849.

Ablation

At 25ba591849, one locked script. The subjects are imported from src/ by relative path, so no build is involved. Every mutation went through scripts/ablation-replace.mjs (anchor 1 to 0, blob changed) and was restored by it: the blob equals HEAD's (dimension-labels.ts 50578e3e0edc, preview-evaluator.ts 5eafd898fefc) and git diff HEAD is empty. Pins run: the new file and week-key-year-below-100.test.ts, 66 tests.

leg mutation red
R both files written back to base f6ccca4a44 (blobs 682193a14bc0 / 522ef30f91fb on disk) 38 / 66, as at the red commit
A1 formatDateBucket's key recognition deleted 5: the 0050 / 0999 year key reads 1970 (the key pin, three every-granularity pins, and the published leg at year); the 2026-only cases stay green
A2 its relabel spelled locally, year unpadded 2: the two raw-value pins; keys still pass through the recogniser
A3a bucketDate strips the year's leading zeros 25; the 2026 bucketDate control green
A3b bucketDate keys a week as a day 26, the six 2026 week cases included

After all legs: both blobs equal HEAD's, git status --porcelain empty, 66 / 66 green.

Verification (head 25ba591849)

  • Suites (under the shared lock, --maxWorkers=2): @objectstack/service-analytics vitest run, 147 files / 3398 tests passed. typecheck (tsc --noEmit) exits 0, and tsc --listFiles lists both pin files, so the typecheck covers them.
  • Build: the tested dependency closure, turbo run build --filter='@objectstack/service-analytics^...' --concurrency=1 (14 / 14), then the package itself (exit 0; check-dts-emitted 2 / 2). dist/index.d.ts names neither function.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands re-derived at this head: 61 commands, identical to the dispatch-time list. All 61 ran in one locked sequential script, with the four roster families the derivation marks (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity). 63 exited 0 and 2 exited 3. --ran, with each exit code recorded: 61 derived, 59 run, 2 NOT MEASURED, 0 unrun.
    • NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt, both PREREQUISITE NOT MET (exit 3). They read every workspace package's built output, and this run built only the tested closure, as the dispatch's resource rule requires. CI's lint job builds the whole closure before them.
    • check-changeset-no-major exited 0 ("no major bump"). Its Clause-② level axis reads the PR payload, so it had no input locally.
  • Lint (narrowed, proven): eslint --no-inline-config --format json over the 4 changed .ts files. The JSON reports 4 files, 0 errors, 0 warnings, and none ignored. eslint.config.mjs enables no type-aware linting (no parserOptions.project, stated in the config itself), so this diff cannot move a verdict on an untouched file. The full pnpm lint is CI's.

Acceptance notes

  • Not measured: an HTTP request through POST /analytics/dataset/query. The readings above are at function level and at queryDataset level, over the engine's in-memory grouping. No SQL driver ran; SQLite and PostgreSQL already pad their keys (measured in PR 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). The door's refusal of hour is read from DatasetSelectionSchema.safeParse, not from the route.
  • Not measured locally: the two gates above, and CI's Test Core, Dogfood, Build Core and Temporal Conformance jobs and the workspace type-check lanes.
  • Unchanged, noted: formatDateBucket still reads a NUMERIC year below 1000 (50 under year) as epoch milliseconds (1970). No named producer answers a numeric year key: SQLite strftime, MySQL date_format, PostgreSQL to_char and MongoDB $dateToString all answer text, and the text key 0050 is now recognised.
  • Unchanged, noted: a raw instant under week still labels as its own day key (YYYY-MM-DD). A week bucket's key is the ISO label on every producer, and that key is recognised and kept.
  • Changed at function level only: bucketDate given a granularity outside the five now answers bucketDateKey's echo of the raw value, where it answered the day key. Its type admits only the five, and DatasetSelectionSchema refuses hour (schema-level reading), so no public path reaches it. It is also the answer the engine's in-memory grouping gives, since bucketDateValue delegates to the same writer.
  • Same family, other surface: [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 (/export) remains its own card and is not addressed here.

Generated by Claude Code

…ter for a year below 1000 (red)

formatDateBucket labels every key bucketDateKey writes as written (0050,
0050-06, 0050-06-15), and bucketDate delegates to bucketDateKey, so a draft
preview keys 0050-06-15 as the published path does, the ISO week label
included. A 2026 control on each. Red at this commit: the fix follows.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
…r for a year below 1000, and the preview keys a week by its ISO label

formatDateBucket labels a key bucketDateKey writes as written, recognised by
core's bucketKeyToCalendarRange (0050 was labelled 1970, 0050-06 became
50-06), and relabels a raw value through bucketDateKey in UTC. bucketDate
delegates to bucketDateKey: no second padding, and the draft preview now
writes the runtime's ISO week label, so a drafted chart keys a row as the
published path does.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 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
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 10 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 4957ee5ef0e660fc9ee4525d83f13aa32ef8e969 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from fa02d5f3ab4b27c4d6803d99756680f5cea0c222 — the merge of head 25ba591849c1aaad8956e0dd21423189c7aed620 into base 4957ee5ef0e660fc9ee4525d83f13aa32ef8e969, 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 fa02d5f3ab4b27c4d6803d99756680f5cea0c222 && git checkout fa02d5f3ab4b27c4d6803d99756680f5cea0c222
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4957ee5ef0e660fc9ee4525d83f13aa32ef8e969 25ba591849c1aaad8956e0dd21423189c7aed620 && git checkout -B drift-repro 4957ee5ef0e660fc9ee4525d83f13aa32ef8e969 && git merge --no-ff 25ba591849c1aaad8956e0dd21423189c7aed620

node scripts/docs-audit/affected-docs.mjs --json 4957ee5ef0e660fc9ee4525d83f13aa32ef8e969

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 25ba591849c1aaad8956e0dd21423189c7aed620
Local-runs: none

Inputs, and nothing else: card #20867 (its body and all four comments: triage 5912878801, claim 5921235126, os-dev-report 5921673071, ACCEPT 5921695101); PR #20971 (its body, its file list, the net diff of the head against the merge-base f6ccca4a44 with origin/main); the check-runs on the head, latest run per name. The dispatching seat's conclusions were read as claims to test, never as inputs. Read-only: nothing built, run, re-run or ablated; every source line cited below was read out of git objects at the head and at the merge-base.

① Derived judgments

  1. Public surface: nothing moves — right. @objectstack/service-analytics is published (private unset, files ships dist), its exports map is . only, and src/index.ts at the head re-exports from dimension-labels.ts only resolveDimensionLabels, pickDisplayField, createOrderLabelResolver, withLabelFetchCache and the types DimensionLabelDeps, FieldMetaLite, OrderLabelResolver; nothing from preview-evaluator.ts. formatDateBucket and bucketDate are reachable only inside the package (a tree-wide grep at the head finds no importer outside packages/services/service-analytics; the hits are ADR and design prose and CHANGELOG history). No published symbol is added, removed or retyped.
  2. formatDateBucket: a string key is recognised by core's reader and returned as written — right. The recogniser is bucketKeyToCalendarRange(value, granularity) !== null, core's one reader of the keys bucketDateKey writes; it answers null for an unpadded 50-06 or 49-W52, so nothing is relabelled that the writer did not write (triage's first bullet). For a key in 1000..9999 the output is byte-identical to the base: the removed year branch answered String(y) for the same string, a quarter, month or day key round-tripped through the parse to its own spelling, and an ISO week label failed the parse and was returned unchanged. For 0001..0999, 0050, 0050-06, 0050-06-15 now label as written where the base answered 1970 (pure digits read as epoch seconds), 50-06, 50-06-15. The signature (value: unknown, granularity?: DateGranularity | string): unknown is unchanged and every non-key still falls through to the parse path, so no input is refused.
  3. formatDateBucket: the raw-value relabel is bucketDateKey(d, labelGranularity) in UTC — right. For 1000..9999 the bytes equal the removed local spelling (bucketKeyYear is a four-wide padStart, a no-op at four digits; the month and day padding is the same two-wide pad). Below 1000 the year is four digits. week, an unstated and an off-vocabulary granularity still label a raw value as its own day key (labelGranularity falls to 'day'), as before. No timezone is passed, so the module's UTC-only contract (ADR-0053 D2's seam note: this is a re-labeller of already-bucketed values) is kept — no accepted ADR is reversed.
  4. The numeric year branch (y in 1000..9999 → String(y)) is kept as-is — right, and flagged. A numeric 50 under year still reads as epoch ms and labels 1970. Judged in ③ (F1).
  5. bucketDate is one call, bucketDateKey(value, granularity, timezone) — right. Triage's second bullet, met literally: no second padding, no second week rule. Runtime deltas, for every year: (i) the week key is the ISO label YYYY-Www, no longer the Monday's YYYY-MM-DD; (ii) a finite number is bucketed as epoch ms (the base answered null, the empty bucket); (iii) a Date is read directly (the base re-parsed String(date), which lands a year below 100 in the 1900s). Below 1000 the year is four digits. The timezone rule is the same helper the removed code called (calendarPartsInTzOrUtc, now inside the writer): unset, 'UTC' or invalid keeps UTC. Each delta lands the preview on the published path's own answer: @objectstack/objectql's bucketDateValue, the in-memory grouping's bucketer, is bucketDateKey(value, granularity, timezone) at the head (packages/objectql/src/in-memory-aggregation.ts), and the drivers' bucket SQL is what that writer's docblock binds its labels to.
  6. The parameter narrowing string → BucketGranularity is not an accept-set narrowing — right. The one production caller, evaluateAnalyticsQueryOverRows, feeds bucketDate from granByDim (the query's timeDimensions[].granularity, typed TimeUpdateInterval) or from dim.granularities[0] (the same enum); TimeUpdateInterval is z.enum(DateGranularity.options) and DateGranularity is ['day', 'week', 'month', 'quarter', 'year'] (packages/spec/src/data/query.zod.ts); the dataset door's dateGranularity is DateGranularity. So every value a door admits is one of the five, hour is refused at the enum before either reader runs, and the removed String(...) at the caller was a type-loosening with no runtime effect. The narrowing compiles on the head: Type Check · workspace and TypeScript Type Check are success.
  7. The two updated [#20599] pins keep their subject — right. week-key-year-below-100.test.ts still pins that a day in 0001..0099 lands in its own ISO week and never the 1900s one (0050-01-01 → 0049-W52; 20:00 UTC on the Sunday 0050-01-02 → 0049-W52 in UTC and 0050-W01 in Asia/Shanghai, where it is already Monday); only the key's spelling moves from the Monday's date to the label, and the Monday stays in a comment. Re-spelled, not weakened.
  8. The new pin file is the right shape — right. bucket-key-readers-four-digit-year.test.ts spells the writer's reference keys literally for 0050-06-15, 0050-01-01 (whose week rolls to 0049-W52), 0999 and a 2026 control; pins formatDateBucket as-written for every key at every granularity and the raw-value relabels; pins bucketDate equal to the writer for ISO text, epoch ms and a Date, and in Asia/Shanghai; and pins both queryDataset legs (published: the ObjectQL strategy over applyInMemoryAggregation then labels; preview: previewDrafts) answering the same rows at all five granularities. The red-first commit 748c4bd5b9 is an ancestor of the head (the branch is two commits: the red pin, then the fix). @objectstack/objectql is a declared devDependency of the package, so the test's import is a declared read.
  9. File surface and scope — right. Five files, all on the claim 5921235126: the two readers, their pins under src/__tests__/, .changeset/20867-analytics-year-keys.md. None of the held files (analytics-service.ts, the strategies, the normalizer, read-scope-sql.ts, field-read-admission.ts, plugin.ts); no path under packages/core, packages/spec or packages/objectql; no governed surface; the head repo is the base repo; +236 / −64. PR line 1 is Fixes #20867, line 2 is Clause-②: no.

Nothing in the diff is judged wrong.

② Semver level

  • The package publishes, so a changeset is owed and one is present: @objectstack/service-analytics patch; no skip-changeset label. Check Changeset is success on the head.
  • Widening: none. No door accepts a value it refused; no export is added.
  • Narrowing: none. No input any door admitted is refused now; no export is removed or retyped on the entry (①.1, ①.6).
  • The one judgment the ACCEPT left to this review — the preview's week key moving from the Monday's date to the ISO label, for every year — is a patch, not a minor and not a break: the key vocabulary is the published path's declared output contract (bucketDateKey's docblock: an output contract, not a display choice; ADR-0037's continuity of numbers across the publish boundary), the published leg already answered 2026-W25 for the same dataset before this PR, and the preview's Monday spelling was a divergence from that contract that no consumer could depend on without already breaking at publish. The changeset states the change and its consequence (a weekly compareTo in the preview now merges onto its week, as published does), so an upgrading reader is told.
  • The changeset prose names the four behaviour deltas (four-digit year, ISO week label, epoch-ms bucketed, a Date below 1000 keyed by its own year), carries no tracker number and no model identifier, and does not edit any RELEASE-OWNED file.

Clause-②: no, no arm — right. Both limbs of the question the line answers (an accept set widened, a public surface expanded) are no on the readings above; no (narrowing) arm is owed because nothing admitted before is refused now. no at patch is a consistent pair.

③ Boundary flags

open_questions: [] — none to answer. Every dev flag (os-dev-report 5921673071 out_of_scope_findings, PR Acceptance notes, the declared deviation) and the triage direction's open bullet:

  • F1 — a NUMERIC year below 1000 under year still labels 1970 (unchanged): answered, agreed as a note, not filed. No producer answers a numeric year key: the in-memory writer returns a string, and the drivers' bucket expressions (strftime, to_char, date_format, $dateToString) answer text; the branch's own comment names the only producer class it exists for (a driver casting 2026 to a number), and a numeric 50 is indistinguishable from 50 epoch-ms by construction. Not escalated.
  • F2 — a raw instant under week still labels as its own day key (unchanged): answered, agreed. A week bucket reaches the labeller as the ISO label on every producer, and that label is now recognised and kept; the raw-value fallback is the week → YYYY-MM-DD line the function's header has always documented. Not escalated.
  • F3 — bucketDate with a granularity outside the five echoes the raw value: answered, agreed. Unreachable through the type (BucketGranularity), unreachable through the door (①.6), and equal to the engine's in-memory bucketer's own answer (same delegate). Not escalated.
  • F4 — not measured: an HTTP request through POST /analytics/dataset/query: answered. The change sits below the door and touches no request parsing; the queryDataset-level readings over both legs are the right altitude for a label and key change; the door's enum was read at schema level (①.6); CI's Test Core and Dogfood lanes exercise the door on the head (verdicts below).
  • F5 — not measured locally: check:dual-build-cjs-loads, check:type-check-debt: answered by CI. Type Check · debt ledger runs pnpm check:type-check-debt (lint.yml) — success; Build Core runs pnpm check:dual-build-cjs-loads (ci.yml) — success.
  • F6 — triage's third bullet, the preview's week vocabulary: answered. Measured at base (the Monday's date for every year, the 2026 control included), adopted, its consequence stated in the changeset and the PR body, pinned in both test files. Not silently kept.
  • F7 — [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 (/export) stays its own card: agreed, not on this surface.
  • F8 — the declared deviation (commit trailers carry the repo's model-free trailer pair, not the harness reminder's model-named line): conforming, read on the head commit; AGENTS.md's rule is the model-free pair, so this is not a deviation from the repo's rule.
  • Independence note: the dispatching seat and this record share one session id — a dispatched subagent run carries no id of its own. The record was rendered by an isolated contract-review subagent of that seat, which read the card, the PR, the diff and the check-runs and none of the seat's conclusions as input, and is adopted by that seat.

Check-runs on the head, latest run per name, read at 2026-09-30T23:55Z: 34 names, every run on head 25ba591849. The seven required contexts: Lint & Repo Gates = success; TypeScript Type Check = success; Test Core = success; Dogfood Regression Gate = success; Build Core = success; Temporal Conformance (live PG + MySQL) = success; Governed Surface Queue Guard = success. Success (31): Auto Label, Build Core, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Dogfood Regression Gate (1/3), Dogfood Regression Gate (2/3), Dogfood Regression Gate (3/3), Dogfood Verify CLI, filter, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Temporal Conformance (live PG + MySQL), Test Core, Test Core (1/6), Test Core (2/6), Test Core (3/6), Test Core (4/6), Test Core (5/6), Test Core (6/6), The card this PR closes must claim this branch, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check. Skipped by their own paths filters (3): Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in). Nothing in progress, nothing red.

Implemented-by: claude/issue-20867-analytics-year-keys
Reviewed-by: session_01XY5uCwTjZj7884yYtyur4H

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 23:56
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 525b813 Oct 1, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20867-analytics-year-keys branch October 1, 2026 00:14
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/m tests tooling

Projects

None yet

2 participants