Repository navigation
Description
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.tsformatDateBucket, called byanalytics-serviceresolveDimensionLabelsfor a date dimension's key;packages/services/service-analytics/src/preview-evaluator.tsbucketDate, 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)
formatDateBucketrecognises every key core'sbucketDateKeywrites, the four-digit year from 0001 included, and relabels nothing it did not write.bucketDatedelegates tobucketDateKeyfor its keys, asbucketKeyAtOrdinalnow does (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). ⛔ No second padding.- Its week key is the Monday's
YYYY-MM-DD, not the ISO week label the runtime writes, and a test pins that. Whether preview adopts the runtime's week vocabulary is the taker's to measure and say.
- Its week key is the Monday's
- Serial: after 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, which is the writer these readers must match.
- Same family, other surface: [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), which stays its own card.
Dedupe
mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:
- 「formatDateBucket year 0050 1970 dimension label unpadded year preview bucketDate below 1000」: 3 hits. [finding] core bucket keys spell a year below 1000 unpadded (
50-06,49-W52) while the SQL drivers pad it (0050-06), andbucketKeyToCalendarRangeanswersnullfor a padded week key such as0050-W01#20760 (open) is the core writer and reader, whose PR produced this finding. [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 (closed) is the instant for 0001..0099. Analytics: a timeDimensions-bucketed query drops the bucket label from rows and fields — trend charts get counts with no x-axis #4033 (closed) is an unrelated bucket-label drop. None covers these two producers.
Dedupe words: formatDateBucket year 0050 1970 · dimension label unpadded year 50-06 · preview bucketDate unpadded year below 1000
Generated by Claude Code
Activity
objectstack-fleet commentedon Sep 30, 2026
Triage: first grade —
bug·priority:p3·domain:services·area:reports·pm:blockedon #20760. Direction as the card: the readers match core's one writerTriage 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.
formatDateBucketrecognises every keybucketDateKeywrites, the four-digit years from 0001 included, and relabels nothing it did not write.bucketDatedelegates tobucketDateKey. ⛔ No second padding.0050,0050-06and0050-06-15label as written;0050-06-15equal the published path's;State
Blocked-by: #20760
objectstack-fleet commentedon Sep 30, 2026
Claim: PM loop round 1 · 2026-09-30T23:04Z
Session:
session_01XY5uCwTjZj7884yYtyur4HAccount:
os-justin(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-20867-analytics-year-keysWorktree:
objectstack-issue-20867Domain:
domain:servicesSeat:
domain:services(seat 1, seat post #6021)Unlock:
Blocked-by: #20760is met. #20760 is closed, and its PR #20865 landed as856321faonmain. This act moves the card frompm:blockedstraight topm:dispatched.File surface:
packages/services/service-analytics/src/dimension-labels.ts(formatDateBucket) andpreview-evaluator.ts(bucketDate, delegating to core'sbucketDateKey), with their pins inservice-analytics/src/__tests__/..changeset/20867-*.md.analytics-service.ts,strategies/native-sql-strategy.ts,strategies/objectql-strategy.ts,strategies/filter-normalizer.tsandread-scope-sql.ts(security(analytics): on the native-SQL strategy an inferred cube's relationship path reads the related object without that object's read admission or its row scope #20933, p0, and PR fix(service-analytics)!: the nested-relation filter gets the engine's answer on every analytics face — the related object read as the caller, capped (#20887) #20916 / #20802 analytics half (domain:services): the cube read and the analytics read scope answer{ relation: { field: value } }as the engine seam now serves it — as the caller, capped, one answer on every face #20887), norfield-read-admission.tsandplugin.ts(security(analytics): a field the caller may only see masked is answered unmasked as a grouped or filtered member on the native-SQL strategy; the published field reader has no masked-for-this-caller answer #20935, p0, thedomain:specseat). If the fix needs one of them, the dev stops and reports, and this card serializes behind that holder.packages/objectql/src/**and notpackages/spec/src/**: the engine's predicates and the compatibility table are called, not changed. If one is not exported, the dev stops and reports.packages/core/src/**:bucketDateKeyis core's one writer, and it is called, not changed. ⛔ No second padding.(stop on a breach outside these; explain in the report)
Container & model:
S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable). Two readers match one writer, with the pins triage named. The contract review runs atCONTRACT_REVIEW_TIER.Clause-②: no
no: a label and a preview key are corrected to what the published path already writes, and nothing is widened. The dev measures it, and the PR's line 2 carries the measured grammar.Thread-read: 5912878801
Serial constraints cleared:
domain:services): the cube read and the analytics read scope answer{ relation: { field: value } }as the engine seam now serves it — as the caller, capped, one answer on every face #20887, this seat) hold the service, the strategies, the normalizer and the read scope. security(analytics): a field the caller may only see masked is answered unmasked as a grouped or filtered member on the native-SQL strategy; the published field reader has no masked-for-this-caller answer #20935 (p0, the spec seat) holds the gate and the plugin wiring. [finding] analytics NativeSQL answers a multi-value dimension one group per serialized array on SQLite and 500 on PostgreSQL, and acount_distinctover a JSON-stored field 2 / 500; the engine door #20808 adds does not see it #20912 (claimed in the same act) holds the structured-JSON door and the dataset compiler. None of those files is on this surface.Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
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
objectstack-fleet commentedon Sep 30, 2026
ACCEPT: #20867 · PR #20971 at
25ba5918·domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-09-30T23:40ZThe dev report is
5921673071.Checklist, read on GitHub rather than from the report:
main, 5 files, +236 / −64. Every path is on the claim5921235126:service-analytics/src/dimension-labels.tsandpreview-evaluator.ts;week-key-year-below-100pin updated;None of the held files (
analytics-service.ts, the strategies, the normalizer, the read scope, the gate, the plugin wiring), and nocore,specorobjectqlpath. Line 1 isFixes #20867, and line 2 isClause-②: no, measured. Assignedos-justin.5912878801):formatDateBucketreturns a keybucketDateKeywrites, at the stated granularity, as written:0050,0050-06and0050-06-15label as written, where they read1970,50-06and50-06-15. What counts as such a key is core'sbucketKeyToCalendarRange, not a second pattern.bucketDateisbucketDateKeyitself. There is no second padding.2026-W25, where it wrote the Monday's date for every year). The changeset states it, and states its consequence: a weeklycompareToin the preview now merges onto its week, as the published path does.@objectstack/service-analyticspatch. Neither function is on the package entry, no accepted input is refused, and the outputs now equal the published path's. Whetherpatchfits the preview's week-label change is the at-tier review's to judge.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.Findings, line by line: three edges, each with no named producer. → Acceptance notes.
weekstill labelled as its own day;bucketDateechoing 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 #20867closed the card.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Landed ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T00:15ZPR #20971 merged through the merge queue as
525b8139onorigin/main, andFixes #20867closed this cardcompleted.5921853583. It also judged the preview's week-key change apatch.git patch-id --stableequals that of the reviewed head25ba5918against its basef6ccca4a.What now holds: the analytics readers of a date-bucket key follow core's one writer,
bucketDateKey.0050,0050-06and0050-06-15, where it was1970,50-06and50-06-15.bucketDateKeyitself. Itsweekkey is the ISO label, as the published path's is, so a weeklycompareToin the preview merges onto its week.For the release list:
@objectstack/service-analyticsships apatch(Clause-②: no).Carried elsewhere (PR #20971's Acceptance notes): three edges, each with no named producer:
weekis still labelled as its own day;bucketDateechoes a raw value for a granularity outside the five.In the same act, this seat removes
pm:dispatchedand the assignee.Generated by Claude Code
fix(service-analytics): a date-bucket key's two readers match core's …