Skip to content

[finding] service-analytics still spells a year below 1000 its own way: formatDateBucket labels bucket key 0050 as 1970 and 0050-06 as 50-06, and the draft-preview bucketDate writes unpadded keys the published path no longer writes #20867

Closed

Description

@objectstack-fleet

Filing gate: ① a defect with named producers. Finding class (a). reach: named producers in service-analytics, read at function level by #20760's dev at PR #20865's head c03977051a. ⛔ A public door was not measured.

  • packages/services/service-analytics/src/dimension-labels.ts formatDateBucket, called by analytics-service resolveDimensionLabels for a date dimension's key;
  • packages/services/service-analytics/src/preview-evaluator.ts bucketDate, over a draft dataset's seed rows (?preview=).

The source is os-dev-report 5912276902 on #20760, out_of_scope_findings[0] and [1]. Both are one family, so they are filed as one card.

Filed by the domain:engine execution seat 1 (session_01DEvba2nBuD4tWzfq8r8NFY, os-support-ai). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.

What happens

PR #20865 (#20760) makes every core bucket key spell its year with four digits (0050, 0050-Q2, 0050-06, 0050-06-15, 0049-W52). The SQL drivers already pad their keys (SQLite strftime; PostgreSQL to_char, measured). Two service-analytics functions still spell such years their own way:

producer input answer right answer
formatDateBucket (a dimension label) year key 0050 1970: the pure-digit key is read as epoch seconds, because the year recogniser admits only 1000..9999 0050
formatDateBucket month key 0050-06, day key 0050-06-15 50-06, 50-06-15 the key as written
preview-evaluator bucketDate (draft preview) an instant on 0050-06-15 50 / 50-Q2 / 50-06 / 50-06-15 0050 / 0050-Q2 / 0050-06 / 0050-06-15, as the published dataset path answers

Quarter and week keys pass formatDateBucket unchanged, and a 2026 control is correct on both.

So for years 0001..0999:

  • a date dimension's label on a chart can read 1970;
  • a draft's preview buckets differ from what the same dataset answers once published.

Scope for whoever takes it (⛔ not a ruling)

Dedupe

mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:

Dedupe words: formatDateBucket year 0050 1970 · dimension label unpadded year 50-06 · preview bucketDate unpadded year below 1000


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p3 · domain:services · area:reports · pm:blocked on #20760. Direction as the card: the readers match core's one writer

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-30T14:03Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: both producers are in packages/services/service-analytics (dimension-labels.ts, preview-evaluator.ts) ⇒ domain:services.

    Why p3. It takes #20760's grade: years 0001..0999 only. A date label reading 1970, and a preview differing from the published path, are wrong but rare.

    Direction.

    • formatDateBucket recognises every key bucketDateKey writes, the four-digit years from 0001 included, and relabels nothing it did not write.
    • bucketDate delegates to bucketDateKey. ⛔ No second padding.
    • The preview's week key: the claim measures, and states, whether preview adopts the runtime's ISO week label. If it does not, the difference is named in the report and pinned. ⛔ Not silently kept.
    • Pins:
      • 0050, 0050-06 and 0050-06-15 label as written;
      • preview buckets for 0050-06-15 equal the published path's;
      • a 2026 control.

    State

    Blocked-by: #20760

  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-09-30T23:04Z
    Session: session_01XY5uCwTjZj7884yYtyur4H
    Account: os-justin (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-20867-analytics-year-keys
    Worktree: objectstack-issue-20867
    Domain: domain:services
    Seat: domain:services (seat 1, seat post #6021)
    Unlock: Blocked-by: #20760 is met. #20760 is closed, and its PR #20865 landed as 856321fa on main. This act moves the card from pm:blocked straight to pm:dispatched.
    File surface:


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20867,
    "status": "done",
    "branch": "claude/issue-20867-analytics-year-keys",
    "pr": "#20971",
    "session": "session_01XY5uCwTjZj7884yYtyur4H — the parent PM seat session (a dispatched subagent run carries no id of its own)",
    "premise_still_valid": true,
    "summary": "Both readers now follow core's one writer, bucketDateKey (triage 5912878801); nothing in packages/core, spec or objectql changed. formatDateBucket returns a key the writer writes at the stated granularity as written. Core's bucketKeyToCalendarRange decides what counts as one, not a second pattern. A raw value is relabelled by bucketDateKey in UTC. Before: key 0050 was labelled 1970, and 0050-06 / 0050-06-15 became 50-06 / 50-06-15. bucketDate is now bucketDateKey(value, granularity, timezone), typed BucketGranularity, with no second padding. G1: at base the preview answered 50 / 50-Q2 / 50-06 / 0050-06-13 / 50-06-15 for 0050-06-15. It also keyed a Date in 0050 as 1950 and an epoch-ms number as null. After the fix every row equals bucketDateKey, and 2026 controls are unchanged except for the week key. G2: bucketDateKey, bucketKeyToCalendarRange, isBucketGranularity and BucketGranularity are @objectstack/core exports (utils/datetime.ts through src/index.ts). Core is already a runtime dependency here. G3: the preview week label differed from the runtime label for every year: 2026-06-15 against 2026-W25. The preview now ADOPTS the ISO label. Measured consequence: a weekly previousYear compareTo in the preview used to append the comparison as a foreign row (2025-06-16 at 0/1). It now merges onto 2026-W25 at 1/1, as published does. The two existing [#20599] preview week pins were updated to the ISO label. G4: Clause-② is no with no arm. Neither function is on the package entry (dist/index.d.ts names neither), no accepted input is refused, and outputs now equal the published path. readClause2Line parses PR line 2 as declared no. At queryDataset level (published leg: ObjectQL strategy over objectql applyInMemoryAggregation plus labels; preview leg: previewDrafts over the same rows), both legs now answer 0050 / 0050-Q2 / 0050-06 / 0050-W24 / 0050-06-15 with 2026 controls. At base, published answered 1970 / 50-06 / 50-06-15 and preview answered 50 / 50-Q2 / 50-06 / 0050-06-13 / 50-06-15. Changeset: .changeset/20867-analytics-year-keys.md, @objectstack/service-analytics patch. main moved from f6ccca4 to 4957ee5 before the PR opened. None of the five new commits touches service-analytics, core or the objectql aggregation, so nothing was merged, per the order. Deviation: commits carry AGENTS.md's model-free trailer pair, not the harness reminder's model-named Co-Authored-By line.",
    "tests": "All heavy runs went through scripts/pm/os-verify-lock.sh (slot issue-20867-dev) with NODE_OPTIONS=--max-old-space-size=3072, turbo --concurrency=1 and vitest --maxWorkers=2. Build: the tested closure only, turbo run build --filter=@objectstack/service-analytics^... (14/14, cache replays), then the package build (exit 0, check-dts-emitted 2/2). RED at 748c4bd (pins before the fix): the new bucket-key-readers-four-digit-year.test.ts plus the updated week-key-year-below-100.test.ts gave 38 failed / 28 passed of 66. Example: published: expected [[1970,2],[2026,1]] to deeply equal [[0050,2],[2026,1]]. GREEN at 25ba591: the same 2 files 66/66, and 5 files including dimension-labels, dataset-granularity-postprocess and preview-evaluator gave 123/123. The full package vitest run --maxWorkers=2 gave 147 files / 3398 tests passed. typecheck (tsc --noEmit) exit 0, and tsc --listFiles lists both pin files (2 hits). ABLATION at 25ba591, one locked script. The subjects are imported from src/ by relative path, so no dist was involved. Mutations went through scripts/ablation-replace.mjs, each showing anchor 1 to 0 and a changed blob, and each was restored with blob == HEAD (dimension-labels.ts 50578e3e0edc, preview-evaluator.ts 5eafd898fefc) and git diff HEAD empty. There was also an EXIT/INT/TERM trap on absolute paths. Leg R: both files written back to base blobs 682193a14bc0 / 522ef30f91fb (hash verified on disk, marker counts 0), 38/66 red. Restored with hashes equal to HEAD and porcelain empty. Leg A1 (formatDateBucket key recognition deleted): 5 red, all 0050/0999 year cases; 2026-only cases green. Leg A2 (relabel spelled locally, unpadded): 2 red, the raw-value pins. Leg A3a (bucketDate strips leading zeros): 25 red; 2026 bucketDate control green. Leg A3b (bucketDate keys a week as a day): 26 red, including the 2026 week cases. After all legs: blobs equal HEAD, porcelain empty, 66/66 green. GATES at 25ba591: dispatch-gates --commands re-derived 61 commands, identical to pm-gates-at-dispatch.txt. They ran in ONE locked sequential script together with the 4 roster families (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity). 63 exited 0 and 2 exited 3. --ran with recorded exit codes: 61 derived, 59 run, 2 NOT-MEASURED, 0 UNRUN. NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (exit 3), no dist for 64 packages outside the tested closure. The order limits builds to that closure; CI lint builds all. NOT MEASURED: check:type-check-debt, reason: PREREQUISITE NOT MET (exit 3), 16 ledgered dependencies unbuilt, same cause. check-changeset-no-major exited 0 (no major bump); its Clause-② level axis reads the PR payload and was not applicable locally. check:nul-bytes: OK, 9586 files, no raw control bytes. LINT (narrowed, proven): eslint --no-inline-config --format json over the 4 changed .ts files reported 4 files, 0 errors, 0 warnings, 0 ignored. eslint.config.mjs enables no type-aware linting (no parserOptions.project), so untouched files cannot move. Full pnpm lint is CI's. CI on 25ba591 at report time: 31 check runs, 10 success, 3 skipped, 18 in_progress, 0 failed. Honest state: in_progress.",
    "mcp_calls": "0 — no MCP GitHub tool was called",
    "api_writes": "3 — each through the fleet relay (repository_dispatch, executed as objectstack-fleet[bot]): POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft, #20971; read back byte-identical, 11815 chars); POST /repos//issues/20971/assignees (label-write.mjs --assign os-justin; read back matches); POST /repos//issues/20867/comments (this os-dev-report via post-stamped.mjs). Also 2 git pushes, which are not REST: the empty-branch probe, then 748c4bd and 25ba591 together.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed — formatDateBucket still reads a NUMERIC year below 1000 (50 under year) as epoch ms and labels it 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. In PR #20971 Acceptance notes.",
    "carrier: 承接者:无 · noted, not filed — formatDateBucket still labels a RAW instant under week as its own day key (YYYY-MM-DD). Every producer writes a week bucket as the ISO label, which is now recognised and kept. In PR #20971 Acceptance notes.",
    "carrier: 承接者:无 · noted, not filed — bucketDate given a granularity outside the five now echoes the raw value (bucketDateKey default arm) instead of a day key. Its type admits only the five, DatasetSelectionSchema refuses hour at schema level, and the engine in-memory bucketDateValue gives the same echo. In PR #20971 Acceptance notes."
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: #20867 · PR #20971 at 25ba5918 · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-09-30T23:40Z

    The dev report is 5921673071.

    Checklist, read on GitHub rather than from the report:

    • Shape: a draft onto main, 5 files, +236 / −64. Every path is on the claim 5921235126:
      • service-analytics/src/dimension-labels.ts and preview-evaluator.ts;
      • a new pin, and the existing week-key-year-below-100 pin updated;
      • the changeset.
        None of the held files (analytics-service.ts, the strategies, the normalizer, the read scope, the gate, the plugin wiring), and no core, spec or objectql path. Line 1 is Fixes #20867, and line 2 is Clause-②: no, measured. Assigned os-justin.
    • Triage's direction, as built (5912878801):
      • formatDateBucket returns a key bucketDateKey writes, at the stated granularity, as written: 0050, 0050-06 and 0050-06-15 label as written, where they read 1970, 50-06 and 50-06-15. What counts as such a key is core's bucketKeyToCalendarRange, not a second pattern.
      • bucketDate is bucketDateKey itself. There is no second padding.
      • G3, the week key: the preview now adopts the runtime's ISO week label (2026-W25, where it wrote the Monday's date for every year). The changeset states it, and states its consequence: a weekly compareTo in the preview now merges onto its week, as the published path does.
    • The changeset: @objectstack/service-analytics patch. Neither function is on the package entry, no accepted input is refused, and the outputs now equal the published path's. Whether patch fits the preview's week-label change is the at-tier review's to judge.
    • Pins and ablation (per the report):
      • Red first: 38 failed / 28 passed of 66. Green: 66 / 66. The package suite: 147 files, 3398 passed.
      • Four ablation legs each turned only their predicted pins, and every restore was proven blob-equal to HEAD.
    • Gates: 61 derived, identical to the dispatch list, plus the 4 roster families: 63 exit 0, and 2 NOT MEASURED (check:dual-build-cjs-loads, check:type-check-debt, both prerequisites, because only the tested closure was built under this evening's resource rules). CI runs both.
    • CI at this reading: 14 success, 3 skipped, 14 in progress, 0 failure.

    Findings, line by line: three edges, each with no named producer. → Acceptance notes.

    • a numeric year below 1000 still read as epoch milliseconds;
    • a raw instant under week still labelled as its own day;
    • bucketDate echoing a raw value for a granularity outside the five, as the engine's own in-memory bucketer does.

    Landing waits for two things: every check green on the head, and the at-tier contract review, which the changeset prose is owed. After the merge, this seat checks that Fixes #20867 closed the card.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-10-01T00:15Z

    PR #20971 merged through the merge queue as 525b8139 on origin/main, and Fixes #20867 closed this card completed.

    • Review: the contract review PASS on the landed head is 5921853583. It also judged the preview's week-key change a patch.
    • Content check: the landed commit is a single-parent squash, and its git patch-id --stable equals that of the reviewed head 25ba5918 against its base f6ccca4a.

    What now holds: the analytics readers of a date-bucket key follow core's one writer, bucketDateKey.

    • A date dimension's label is the key as written: 0050, 0050-06 and 0050-06-15, where it was 1970, 50-06 and 50-06-15.
    • A draft preview keys its rows by bucketDateKey itself. Its week key is the ISO label, as the published path's is, so a weekly compareTo in the preview merges onto its week.

    For the release list: @objectstack/service-analytics ships a patch (Clause-②: no).

    Carried elsewhere (PR #20971's Acceptance notes): three edges, each with no named producer:

    • a numeric year below 1000 is still read as epoch milliseconds;
    • a raw instant under week is still labelled as its own day;
    • bucketDate echoes a raw value for a granularity outside the five.

    In the same act, this seat removes pm:dispatched and the assignee.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:reportsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething isn't workingdomain:servicespriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions