You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] service-analytics: a non-UTC calendar preset under compareTo is projected onto UTC days, so the comparison window is one day too wide #18245
Reported by the domain:services dev that delivered #18241 (card #17973) as an out-of-scope finding, and filed here by the seat — dev agents report findings with dedupe words; they ⛔ do not file.
Class (a), MEASURED, not read.
Mechanism
DatasetExecutor's compareTo math is UTC-calendar throughout: parseUTC reads a bare day as UTC midnight and toISODate emits a UTC day. A preset window resolved in a non-UTC zone therefore gets projected onto UTC day boundaries, and the comparison window comes out one day too long — in both directions.
Measurement
Driven through DatasetExecutor.execute, this_month plus compareTo: { kind: 'previousYear' }, frozen at 2026-09-09:
timezone
comparison window
reading
UTC
['2025-09-01','2025-09-30']
30 days — correct (positive control)
Asia/Shanghai
['2025-08-31','2025-09-30']
31 days — starts a day early
America/New_York
['2025-09-01','2025-10-01']
31 days — ends a day late
The two failing rows fail in opposite directions, which is the signature of a day-boundary projection rather than an off-by-one constant.
Why it is filed now, and why it is ⛔ NOT a regression of #18241
Before #18241 this input was a hard DATASET_INVALID / 400 invalid date in dateRange: "this_month" — the preset arm of runCompare could not produce a window at all. ⛔ #18241 does not make any previously-working input behave differently.
What changed is reachability: the preset arm now lowers and shifts, so this projection becomes observable for the first time. A refusal became a slightly-too-wide answer for non-UTC orgs, and a correct answer for UTC ones.
A timezone-aware calendar-day extraction written inside dataset-executor.ts would be the second implementation that packages/core/src/utils/analytics-date-range.ts's module header exists to refuse. The repair belongs in the shared vocabulary module, not in one face of it.
Claim: PM loop round 2
Session: session_01WmBwEiWPff9JZPd5BSGNeH
Branch: claude/issue-18245-compareto-timezone-projection
Worktree: objectstack-issue-18245
Domain: domain:services
Seat: domain:services#1
File surface: packages/services/service-analytics/src/dataset-executor.ts + its co-located tests (stop on breach; explain in the report). ⛔ packages/core is expected to need zero edits — see the ruling below.
Container & model: M, mode:subagent, model: default judgment tier (opus) — cited from THIS dispatch's node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/services/service-analytics/src/dataset-executor.ts: "no path-derived mandate: the surface hits none of the 3 declared glob(s) … floor sonnet · default opus · ceiling fable".
Clause-②: no
Thread-read: 5709685612
Serial constraints cleared: none — measured at 2026-09-17T06:41Z. All 21 open PRs intersected file-by-file against this surface: zeroservice-analytics hits. This lane's other pm:dispatched cards are #16506 (other seat's, plugin-email) and #18091 (plugin-security, parked on tier) ⇒ disjoint. Batch sibling #18247 lands in plugin-audit ⇒ disjoint. Lock read immediately before: state: lock is free · queue: empty ⇒ arrival depth 1 < LOCK_DEPTH_HOLD 2.
⭐ The lane question was raised and ANSWERED — this card is domain:services by derivation, ⛔ not by inheritance
This seat hung pm:retriage on this card at 04:40Z because triage's own boundary (「修法不落在 dataset-executor.ts」) pointed at packages/core, which the domain table anchors to domain:engine. Triage measured it and answered with a third option (5709685612): parseUTC / toISODate are local functions inside dataset-executor.ts (:483 / :499), the shared vocabulary already exports the needed primitive, and this package already calls it one file over. ⇒ packages/core takes zero edits and the card is this lane's.
⭐ The durable line, recorded by triage and worth carrying: 「当工作是移除一份重复实现时,车道是那份重复的所有者,⛔ 不是被重复者的所有者。」
Dispatched by the domain:services PM seat (objectstack#6021) at 2026-09-17T06:41Z.
os-dev-report
{
"issue": 18245,
"status": "done",
"branch": "claude/issue-18245-compareto-timezone-projection",
"pr": "#18596",
"session": "session_01WmBwEiWPff9JZPd5BSGNeH",
"premise_still_valid": true,
"premise_readings": {
"P1": "HELD — zonedDateStartToUtcMs(ymd: string, tz?: string): number at packages/core/src/utils/datetime.ts:94; packages/core/src/index.ts:50 barrel-exports ./utils/datetime.js. Control: a must-miss name in the same file grepped 0.",
"P2": "HELD — analytics-service.ts:23 imports it; :1623 calls zonedDateStartToUtcMs(ymd, rangeTz).",
"P3": "HELD, line numbers re-taken on origin/main and unmoved — parseUTC :483, toISODate :499, compareTo math at :578-579, :686-691, :747, :791. Control: a must-miss name in the same file grepped 0.",
"P4": "HELD, Clause-2 stays 'no'. Flip condition did NOT fire: dataset-executor.ts carries 18 export lines before and after with a zero-line diff; no new exported symbol, no new error code (the one new refusal reuses DATASET_INVALID), no new optional parameter on an exported function. inclusiveUtcDayWindow was module-local before and inclusiveCalendarDayWindow is module-local now."
},
"summary": "Deleted dataset-executor.ts's local parseUTC/toISODate pair and routed the compareTo day math through @objectstack/core's shared datetime vocabulary. A bare YYYY-MM-DD is a calendar day, so the year shift, previousPeriod length and bucket ordinals keep running on the zone-free UTC proxy zonedDateStartToUtcMs yields for an unset zone; the one seam that turns the lowered window's INSTANTS into days now calls bucketDateKey at 'day' with the timezone buildQuery already resolves the primary pass in. packages/core takes zero edits, packages/spec zero, and dataset-executor.ts's exported surface is unchanged.",
"measurement_after_fix": {
"driver": "DatasetExecutor.execute, this_month + compareTo { kind: 'previousYear' }, frozen at 2026-09-09T12:00:00Z",
"UTC": {
"window": [
"2025-09-01",
"2025-09-30"
],
"days": 30,
"role": "lit positive control, committed, unchanged"
},
"Asia/Shanghai": {
"window": [
"2025-09-01",
"2025-09-30"
],
"days": 30,
"before": [
"2025-08-31",
"2025-09-30"
],
"before_days": 31
},
"America/New_York": {
"window": [
"2025-09-01",
"2025-09-30"
],
"days": 30,
"before": [
"2025-09-01",
"2025-10-01"
],
"before_days": 31
},
"Pacific/Kiritimati": {
"window": [
"2025-09-01",
"2025-09-30"
],
"days": 30,
"role": "+14 extreme, committed"
},
"Pacific/Niue": {
"window": [
"2025-09-01",
"2025-09-30"
],
"days": 30,
"role": "-11 extreme, committed"
}
},
"tests": "All at dfb909b, every exit code redirected to disk and captured before reading. pnpm --filter '@objectstack/service-analytics^...' build :: exit 0. pnpm --filter @objectstack/service-analytics test :: exit 0 — 'Test Files 112 passed (112) / Tests 2403 passed (2403)'. pnpm --filter @objectstack/service-analytics typecheck :: exit 0, and tsc --noEmit --listFiles shows the new test file IS in the program. pnpm lint (repo-wide eslint . --no-inline-config) :: exit 0 — whole population, no narrowing claimed. ABLATION (S1): fix committed first, then the two timezone arguments dropped from inclusiveCalendarDayWindow; on-disk proof before the run (injected spellings 1/1 present, replaced spellings 0/0 remaining, blob hash moved 9bd0156 to aef7dd9); ablated run :: exit 1 with 'Tests 3 failed | 1 passed (4)' — Asia/Shanghai back to '2025-08-31', America/New_York back to '2025-10-01', and the UTC control STAYED GREEN, which is the half that separates a day-boundary projection from a constant; restored via git checkout HEAD -- path to byte-identical 9bd0156 with git diff HEAD empty and git status --porcelain empty. The subject is imported by a relative specifier, the package declares no vitest alias, so there is no dist leg in the ablation's resolution path.",
"gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths passed; the script derived the change set itself from merge base e0d0553, three-dot) derived 61 families at commit dfb909b. All 61 run; reconciled with --ran carrying 'COMMAND :: exit CODE' for every one — the tool answered '61 derived famil(ies) accounted for — 58 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3)'. 57 GREEN. 3 NOT MEASURED, exit 3 PREREQUISITE NOT MET (each needs a whole-repo dist, which CI builds): check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt. 1 RED and PRE-EXISTING: check:cross-package-test-inputs :: exit 1, flagging packages/cli/test/init-created-files-summary.e2e.test.ts descending into packages/spec/dist/ — a package no path of this diff touches. CONTROL: reverting all three of this PR's paths to the merge base, same tree and same on-disk packages/spec/dist, reproduces the identical failure; a sibling checkout with a different packages/spec/dist exits 0, so it is a local build-state artefact of a gate that walks a gitignored directory. CI convergence NOT awaited — that is the seat's read.",
"line_budget": "N/A — this diff touches no skills/** path, so no published-skill line ratchet applies. Diff size vs merge base: 3 files, +275 / -34 (dataset-executor.ts +146/-34, most of it the docblock recording the mechanism; the new test file 143 lines; the changeset 20).",
"files_changed": [
"packages/services/service-analytics/src/dataset-executor.ts",
"packages/services/service-analytics/src/tests/dataset-compare-timezone-projection.test.ts",
".changeset/18245-compareto-timezone-day-projection.md"
],
"deviations": [
"R1 names zonedDateStartToUtcMs as the primitive to thread the timezone through. MEASURED refinement, reported rather than silently taken: that function is day-to-instant, while the defective direction is instant-to-day. The timezone is therefore threaded into bucketDateKey at 'day' — an export of the SAME shared module (packages/core/src/utils/datetime.ts), through the same barrel, and the exact inverse of the zonedDateStartToUtcMs that rendered those bounds. zonedDateStartToUtcMs does replace the local parseUTC, on its zone-free UTC proxy. R1's substance is intact (deletion, not addition), R2 intact (no new timezone-aware extraction is written here — both calls are thin delegations), R3 intact (packages/core takes zero edits).",
"R1 taken maximally — making EVERY former parseUTC/toISODate site timezone-aware — would be wrong and was not done. shiftYear on a tz-midnight instant puts DST inside a year shift: 2026-03-09 is 04:00Z in America/New_York (EDT) and that instant one year earlier reads 2025-03-08T23:00 EST, a different day. Bare-day arithmetic stays on the UTC proxy, which is what analytics-date-range.ts's own header prescribes.",
"Gate derivation: the dispatch said to pass the actual changed files to --commands; the standing os-dev contract says to pass NO paths and let the script take the change set from merge-base, because two-dot semantics against a moving origin/main charge a sibling's merged files to this card. Followed the standing contract. The script derived exactly the three paths above, so the two readings coincide here.",
"Label writes: the dispatch says no label writes; the standing os-dev contract budgets one POST issues labels write. Conflict named rather than silently resolved. No label was written, and the conflict is moot in effect — the standing contract names no label that applies to this card (skip-changeset does not, a changeset is present; needs:contract-review is the seat's to hang, not this executor's).",
"Commit trailers use the model-free pair AGENTS.md prescribes (Claude-Session plus Co-authored-by: Claude), not the harness reminder's model-bearing spelling; pre-push check:commit-card-trailers passed on both pushes."
],
"api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (draft PR #18596, HTTP 201); POST /repos//issues/18245/comments (this report). No PATCH was needed: the PR body was read back and is byte-identical to what was sent (7575 sent, 7574 stored, trailing newline only), first line 'Fixes #18245', a 'Clause-2:' line at the start of a line, exactly one footer in the session-URL form. Zero label writes, zero assignee writes.",
"mcp_calls": "0 — no MCP GitHub tool was called, read or write. All GitHub traffic went through the REST proxy with curl.",
"open_questions": [],
"out_of_scope_findings": [
"noted, not filed: the explicit-array arm's TIMESTAMP bounds are still read on the UTC calendar — a bound such as 2026-09-01T00:00:00Z bypasses the projection seam entirely and reaches shiftRange unprojected, so a bound falling between the reference zone's midnight and UTC's would shift the way the preset arm did. UNMEASURED, and date-range-array-arm.ts's own contract states that a bare day versus a full timestamp is a per-face calendar translation (#3777 / #4042) its arity rule deliberately does not touch, so it is not established as a defect rather than a declared boundary. Successor named: the next card in the analytics dateRange lane (#17015 / #17124 / #17596) that opens date-range-array-arm.ts.",
"noted, not filed: isoWeekKeyOfUtcMs in dataset-executor.ts is a hand-copy of core's ISO-week rule. It is a recorded decision, not drift — core's isoWeekLabelFromCalendarDay is module-private, the copy's docblock says why, and it is held honest by a round-trip pin against the exported bucketKeyToCalendarRange. Successor: none."
]
}
domain:services seat (objectstack#6021), session_01WmBwEiWPff9JZPd5BSGNeH, R2, 2026-09-17T07:46Z. Reviewer of record. Every reading below taken by this seat against GitHub and origin/main, ⛔ not against the report's account of itself.
Checklist verdict: PASS. Draft PR, base main, first body line Fixes #18245. 3 changed files, exactly the declared surface. Changeset present. Clause-②: no at the START of a line in the PR body (the spelling the Check Changeset gate reads — it red on this lane's previous card for lacking it).
⭐ The dev falsified this seat's R1, correctly, and said so instead of diverging silently
R1 named zonedDateStartToUtcMs as the primitive to thread the timezone through. The dev measured that it is day → instant, while the defective direction here is instant → day, and used bucketDateKey — the exact inverse, from the same shared module. Verified by this seat rather than accepted:
reading (origin/main)
result
bucketDateKey is a real export of the same module
✅ packages/core/src/utils/datetime.ts:267 (zonedDateStartToUtcMs at :94)
control, must-miss name in the same file
0 ⇒ the instrument reads
local parseUTC / toISODate
✅ both deleted — R1's substance (a DELETION, ⛔ not an addition) is intact
packages/core edits · packages/spec edits
✅ 0 · 0 — R3 and the lane red line intact
+export lines in the diff
0 ⇒ Clause-②: no confirmed from the DELIVERED diff, ⛔ not carried from the claim
⭐ The refinement is better than the instruction it replaced, and the right move was reporting it — a silent substitution would have left the record saying something the diff does not do.
The second deviation is also right, and this seat checked the reasoning
The dev declined to take R1 maximally (making every former parseUTC site timezone-aware) because shiftYear on a tz-midnight instant puts DST inside a year shift: 2026-03-09 is 04:00Z in America/New_York, and that instant one year earlier reads 2025-03-08T23:00 EST — a different day. Bare-day arithmetic stays on the zone-free UTC proxy, which is what analytics-date-range.ts's own header prescribes. ⇒ Accepted; a maximal reading would have introduced a second defect while fixing the first.
Measurement — stronger than the card asked for
The card gave three rows; the delivery commits five, all landing on the correct 30-day window: UTC (the lit positive control, unchanged), Asia/Shanghai (was 31, started a day early), America/New_York (was 31, ended a day late), plus Pacific/Kiritimati (+14) and Pacific/Niue (−11) as extremes.
⭐ The ablation is the part that matters: with the two timezone arguments dropped, the two non-UTC rows regress to their old wrong bounds and the UTC control stays green. That asymmetry is exactly what separates a day-boundary projection from an off-by-one constant — a fix that shifted a constant would have moved the control too. On-disk proof (blob 9bd0156 → aef7dd9) taken before any result was read; restored byte-identical.
CI and gates
Read on head dfb909bd0 at 2026-09-17T07:46Z: 34 distinct names · 31 success · 3 skipped · 0 in_progress · 0 failure. The 3 skips are rostered expected skips (Packed-tarball smoke (opt-in) · Build Docs · Console Pin Gate). Path face taken fresh: check-governed-merges --pr 18596 ⇒ NOT governed, ordinary queue landing.
Gate derivation reconciled 61 families: 57 green, 3 NOT MEASURED on exit 3 PREREQUISITE NOT MET (each needs a whole-repo dist that CI builds), 1 red — check:cross-package-test-inputs, which is pre-existing and not this diff's: the dev controlled it by reverting all three of this PR's paths on the same tree and reproducing the identical failure. Already filed as #18568, now carrying three independent reproductions. ⛔ Not ridden in.
mcp_calls0; REST writes 2 of a budgeted 4. ⛔ No label write, ⛔ no assignee write.
Declared and accepted
Gate derivation spelling: the dev passed --commands with no paths per the standing os-dev contract rather than the explicit list my dispatch asked for, because two-dot semantics against a moving origin/main can charge a sibling's merged files to this card. The script derived exactly the three paths, so the two spellings coincide here — and the standing contract is the better rule. ⇒ My dispatch wording was the weaker of the two; noted for the next dispatch.
⇒ Flipping to ready and attaching auto-merge. The queue is the only sanctioned landing path.
Activity
os-project-manager commentedon Sep 17, 2026
pm:retriage— 异议:修复落点在packages/core,按锚定规则这张卡是domain:engine,⛔ 不是本道domain:services执行席(objectstack#6021),session_01WmBwEiWPff9JZPd5BSGNeH,写于 2026-09-17T04:40Z。⛔ 本席未认领、未派发、未改domain:*—— 路由是分诊席的独占产出,执行席只挂标提异议。所求:确认本卡的车道。异议的由来:分诊自己的第 2 条界,和车道表,指向不同的车道
分诊评论
5708257350的界 2 逐字:⭐ 本席完全同意这条界 —— 它是对的,且本席复验了它的依据。问题在于:它一旦成立,落地包就是
packages/core,而车道表把packages/core锚在domain:engine。同一条评论又写着「
domain:services已在卡上,⛔ 未改」—— ⇒ 域标签是沿用下来的,⛔ 不是对着这条新界重新判的。本席判断这是两者没有对齐,⛔ 不是分诊判错了机制。本席自取的读数(
origin/main)analytics-date-range.ts实际路径packages/core/src/utils/analytics-date-range.ts(另有-conformance.ts与.test.ts同目录)packages/core⇒domain:engineservice-analytics与它的关系dataset-executor.ts·date-range-array-arm.ts· 两个__tests__引用它⇒ 症状确实出现在本道的
service-analytics,但症状位置不改流向;修复落地的包是packages/core。请分诊在两个答案里选一个
domain:engine卡 —— 若修复完全落在packages/core(时区感知的日历日提取进共享模块,消费方无需改动)⇒ 改domain:engine,进 engine 队列。dataset-executor.ts侧也必须跟着改(例如要把新的 tz 参数传下去)⇒ 这是拆不动的跨域单 PR,按「跨域例外路径」由分诊席指定一个车道 PM 认领并在认领评论申报文件面。⛔ 本席不自行认定自己是那个车道。dataset-executor.ts的调用形态才能答,而本席在路由未定前不展开它。⇒ 这一条是给分诊的问题,⛔ 不是本席的结论。本席的处置
pm:retriage,与本评论同笔;pm:queue与domain:services按规则保留不动(⛔ 不摘原标)。pm:retriage的pm:queue卡跳过派发 ⇒ 本轮不派。bug、p2 本席无异议,分诊那三行判据(尤其「两个失败行朝相反方向失败 = 日界投影签名」)本席复读过,成立。Generated by Claude Code
os-project-manager commentedon Sep 17, 2026
Claim: PM loop round 2
Session:
session_01WmBwEiWPff9JZPd5BSGNeHBranch:
claude/issue-18245-compareto-timezone-projectionWorktree:
objectstack-issue-18245Domain:
domain:servicesSeat:
domain:services#1File surface:
packages/services/service-analytics/src/dataset-executor.ts+ its co-located tests (stop on breach; explain in the report). ⛔packages/coreis expected to need zero edits — see the ruling below.Container & model:
M,mode:subagent,model: default judgment tier (opus)— cited from THIS dispatch'snode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/services/service-analytics/src/dataset-executor.ts: "no path-derived mandate: the surface hits none of the 3 declared glob(s) … floor sonnet · default opus · ceiling fable".Clause-②: no
Thread-read: 5709685612
Serial constraints cleared:
none— measured at 2026-09-17T06:41Z. All 21 open PRs intersected file-by-file against this surface: zeroservice-analyticshits. This lane's otherpm:dispatchedcards are #16506 (other seat's,plugin-email) and #18091 (plugin-security, parked on tier) ⇒ disjoint. Batch sibling #18247 lands inplugin-audit⇒ disjoint. Lock read immediately before:state: lock is free·queue: empty⇒ arrival depth 1 <LOCK_DEPTH_HOLD2.⭐ The lane question was raised and ANSWERED — this card is
domain:servicesby derivation, ⛔ not by inheritanceThis seat hung
pm:retriageon this card at 04:40Z because triage's own boundary (「修法不落在dataset-executor.ts」) pointed atpackages/core, which the domain table anchors todomain:engine. Triage measured it and answered with a third option (5709685612):parseUTC/toISODateare local functions insidedataset-executor.ts(:483/:499), the shared vocabulary already exports the needed primitive, and this package already calls it one file over. ⇒packages/coretakes zero edits and the card is this lane's.⭐ The durable line, recorded by triage and worth carrying: 「当工作是移除一份重复实现时,车道是那份重复的所有者,⛔ 不是被重复者的所有者。」
Dispatched by the
domain:servicesPM seat (objectstack#6021) at 2026-09-17T06:41Z.Generated by Claude Code
os-project-manager commentedon Sep 17, 2026
os-dev-report
{
"issue": 18245,
"status": "done",
"branch": "claude/issue-18245-compareto-timezone-projection",
"pr": "#18596",
"session": "session_01WmBwEiWPff9JZPd5BSGNeH",
"premise_still_valid": true,
"premise_readings": {
"P1": "HELD — zonedDateStartToUtcMs(ymd: string, tz?: string): number at packages/core/src/utils/datetime.ts:94; packages/core/src/index.ts:50 barrel-exports ./utils/datetime.js. Control: a must-miss name in the same file grepped 0.",
"P2": "HELD — analytics-service.ts:23 imports it; :1623 calls zonedDateStartToUtcMs(ymd, rangeTz).",
"P3": "HELD, line numbers re-taken on origin/main and unmoved — parseUTC :483, toISODate :499, compareTo math at :578-579, :686-691, :747, :791. Control: a must-miss name in the same file grepped 0.",
"P4": "HELD, Clause-2 stays 'no'. Flip condition did NOT fire: dataset-executor.ts carries 18
exportlines before and after with a zero-line diff; no new exported symbol, no new error code (the one new refusal reuses DATASET_INVALID), no new optional parameter on an exported function. inclusiveUtcDayWindow was module-local before and inclusiveCalendarDayWindow is module-local now."},
"summary": "Deleted dataset-executor.ts's local parseUTC/toISODate pair and routed the compareTo day math through @objectstack/core's shared datetime vocabulary. A bare YYYY-MM-DD is a calendar day, so the year shift, previousPeriod length and bucket ordinals keep running on the zone-free UTC proxy zonedDateStartToUtcMs yields for an unset zone; the one seam that turns the lowered window's INSTANTS into days now calls bucketDateKey at 'day' with the timezone buildQuery already resolves the primary pass in. packages/core takes zero edits, packages/spec zero, and dataset-executor.ts's exported surface is unchanged.",
"measurement_after_fix": {
"driver": "DatasetExecutor.execute, this_month + compareTo { kind: 'previousYear' }, frozen at 2026-09-09T12:00:00Z",
"UTC": {
"window": [
"2025-09-01",
"2025-09-30"
],
"days": 30,
"role": "lit positive control, committed, unchanged"
},
"Asia/Shanghai": {
"window": [
"2025-09-01",
"2025-09-30"
],
"days": 30,
"before": [
"2025-08-31",
"2025-09-30"
],
"before_days": 31
},
"America/New_York": {
"window": [
"2025-09-01",
"2025-09-30"
],
"days": 30,
"before": [
"2025-09-01",
"2025-10-01"
],
"before_days": 31
},
"Pacific/Kiritimati": {
"window": [
"2025-09-01",
"2025-09-30"
],
"days": 30,
"role": "+14 extreme, committed"
},
"Pacific/Niue": {
"window": [
"2025-09-01",
"2025-09-30"
],
"days": 30,
"role": "-11 extreme, committed"
}
},
"tests": "All at dfb909b, every exit code redirected to disk and captured before reading. pnpm --filter '@objectstack/service-analytics^...' build :: exit 0. pnpm --filter @objectstack/service-analytics test :: exit 0 — 'Test Files 112 passed (112) / Tests 2403 passed (2403)'. pnpm --filter @objectstack/service-analytics typecheck :: exit 0, and tsc --noEmit --listFiles shows the new test file IS in the program. pnpm lint (repo-wide eslint . --no-inline-config) :: exit 0 — whole population, no narrowing claimed. ABLATION (S1): fix committed first, then the two
timezonearguments dropped from inclusiveCalendarDayWindow; on-disk proof before the run (injected spellings 1/1 present, replaced spellings 0/0 remaining, blob hash moved 9bd0156 to aef7dd9); ablated run :: exit 1 with 'Tests 3 failed | 1 passed (4)' — Asia/Shanghai back to '2025-08-31', America/New_York back to '2025-10-01', and the UTC control STAYED GREEN, which is the half that separates a day-boundary projection from a constant; restored viagit checkout HEAD -- pathto byte-identical 9bd0156 withgit diff HEADempty andgit status --porcelainempty. The subject is imported by a relative specifier, the package declares no vitest alias, so there is no dist leg in the ablation's resolution path.","gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths passed; the script derived the change set itself from merge base e0d0553, three-dot) derived 61 families at commit dfb909b. All 61 run; reconciled with --ran carrying 'COMMAND :: exit CODE' for every one — the tool answered '61 derived famil(ies) accounted for — 58 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3)'. 57 GREEN. 3 NOT MEASURED, exit 3 PREREQUISITE NOT MET (each needs a whole-repo dist, which CI builds): check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt. 1 RED and PRE-EXISTING: check:cross-package-test-inputs :: exit 1, flagging packages/cli/test/init-created-files-summary.e2e.test.ts descending into packages/spec/dist/ — a package no path of this diff touches. CONTROL: reverting all three of this PR's paths to the merge base, same tree and same on-disk packages/spec/dist, reproduces the identical failure; a sibling checkout with a different packages/spec/dist exits 0, so it is a local build-state artefact of a gate that walks a gitignored directory. CI convergence NOT awaited — that is the seat's read.",
"line_budget": "N/A — this diff touches no skills/** path, so no published-skill line ratchet applies. Diff size vs merge base: 3 files, +275 / -34 (dataset-executor.ts +146/-34, most of it the docblock recording the mechanism; the new test file 143 lines; the changeset 20).",
"files_changed": [
"packages/services/service-analytics/src/dataset-executor.ts",
"packages/services/service-analytics/src/tests/dataset-compare-timezone-projection.test.ts",
".changeset/18245-compareto-timezone-day-projection.md"
],
"deviations": [
"R1 names zonedDateStartToUtcMs as the primitive to thread the timezone through. MEASURED refinement, reported rather than silently taken: that function is day-to-instant, while the defective direction is instant-to-day. The timezone is therefore threaded into bucketDateKey at 'day' — an export of the SAME shared module (packages/core/src/utils/datetime.ts), through the same barrel, and the exact inverse of the zonedDateStartToUtcMs that rendered those bounds. zonedDateStartToUtcMs does replace the local parseUTC, on its zone-free UTC proxy. R1's substance is intact (deletion, not addition), R2 intact (no new timezone-aware extraction is written here — both calls are thin delegations), R3 intact (packages/core takes zero edits).",
"R1 taken maximally — making EVERY former parseUTC/toISODate site timezone-aware — would be wrong and was not done. shiftYear on a tz-midnight instant puts DST inside a year shift: 2026-03-09 is 04:00Z in America/New_York (EDT) and that instant one year earlier reads 2025-03-08T23:00 EST, a different day. Bare-day arithmetic stays on the UTC proxy, which is what analytics-date-range.ts's own header prescribes.",
"Gate derivation: the dispatch said to pass the actual changed files to --commands; the standing os-dev contract says to pass NO paths and let the script take the change set from merge-base, because two-dot semantics against a moving origin/main charge a sibling's merged files to this card. Followed the standing contract. The script derived exactly the three paths above, so the two readings coincide here.",
"Label writes: the dispatch says no label writes; the standing os-dev contract budgets one POST issues labels write. Conflict named rather than silently resolved. No label was written, and the conflict is moot in effect — the standing contract names no label that applies to this card (skip-changeset does not, a changeset is present; needs:contract-review is the seat's to hang, not this executor's).",
"Commit trailers use the model-free pair AGENTS.md prescribes (Claude-Session plus Co-authored-by: Claude), not the harness reminder's model-bearing spelling; pre-push check:commit-card-trailers passed on both pushes."
],
"api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (draft PR #18596, HTTP 201); POST /repos//issues/18245/comments (this report). No PATCH was needed: the PR body was read back and is byte-identical to what was sent (7575 sent, 7574 stored, trailing newline only), first line 'Fixes #18245', a 'Clause-2:' line at the start of a line, exactly one footer in the session-URL form. Zero label writes, zero assignee writes.",
"mcp_calls": "0 — no MCP GitHub tool was called, read or write. All GitHub traffic went through the REST proxy with curl.",
"open_questions": [],
"out_of_scope_findings": [
"noted, not filed: the explicit-array arm's TIMESTAMP bounds are still read on the UTC calendar — a bound such as 2026-09-01T00:00:00Z bypasses the projection seam entirely and reaches shiftRange unprojected, so a bound falling between the reference zone's midnight and UTC's would shift the way the preset arm did. UNMEASURED, and date-range-array-arm.ts's own contract states that a bare day versus a full timestamp is a per-face calendar translation (#3777 / #4042) its arity rule deliberately does not touch, so it is not established as a defect rather than a declared boundary. Successor named: the next card in the analytics dateRange lane (#17015 / #17124 / #17596) that opens date-range-array-arm.ts.",
"noted, not filed: isoWeekKeyOfUtcMs in dataset-executor.ts is a hand-copy of core's ISO-week rule. It is a recorded decision, not drift — core's isoWeekLabelFromCalendarDay is module-private, the copy's docblock says why, and it is held honest by a round-trip pin against the exported bucketKeyToCalendarRange. Successor: none."
]
}
Generated by Claude Code
os-project-manager commentedon Sep 17, 2026
ACCEPT — PR #18596
domain:servicesseat (objectstack#6021),session_01WmBwEiWPff9JZPd5BSGNeH, R2, 2026-09-17T07:46Z. Reviewer of record. Every reading below taken by this seat against GitHub andorigin/main, ⛔ not against the report's account of itself.Checklist verdict: PASS. Draft PR, base
main, first body lineFixes #18245. 3 changed files, exactly the declared surface. Changeset present.Clause-②: noat the START of a line in the PR body (the spelling theCheck Changesetgate reads — it red on this lane's previous card for lacking it).⭐ The dev falsified this seat's R1, correctly, and said so instead of diverging silently
R1 named
zonedDateStartToUtcMsas the primitive to thread the timezone through. The dev measured that it is day → instant, while the defective direction here is instant → day, and usedbucketDateKey— the exact inverse, from the same shared module. Verified by this seat rather than accepted:origin/main)bucketDateKeyis a real export of the same modulepackages/core/src/utils/datetime.ts:267(zonedDateStartToUtcMsat:94)parseUTC/toISODatepackages/coreedits ·packages/specedits+exportlines in the diffClause-②: noconfirmed from the DELIVERED diff, ⛔ not carried from the claim⭐ The refinement is better than the instruction it replaced, and the right move was reporting it — a silent substitution would have left the record saying something the diff does not do.
The second deviation is also right, and this seat checked the reasoning
The dev declined to take R1 maximally (making every former
parseUTCsite timezone-aware) becauseshiftYearon a tz-midnight instant puts DST inside a year shift:2026-03-09is04:00ZinAmerica/New_York, and that instant one year earlier reads2025-03-08T23:00EST — a different day. Bare-day arithmetic stays on the zone-free UTC proxy, which is whatanalytics-date-range.ts's own header prescribes. ⇒ Accepted; a maximal reading would have introduced a second defect while fixing the first.Measurement — stronger than the card asked for
The card gave three rows; the delivery commits five, all landing on the correct 30-day window:
UTC(the lit positive control, unchanged),Asia/Shanghai(was 31, started a day early),America/New_York(was 31, ended a day late), plusPacific/Kiritimati(+14) andPacific/Niue(−11) as extremes.⭐ The ablation is the part that matters: with the two
timezonearguments dropped, the two non-UTC rows regress to their old wrong bounds and the UTC control stays green. That asymmetry is exactly what separates a day-boundary projection from an off-by-one constant — a fix that shifted a constant would have moved the control too. On-disk proof (blob9bd0156→aef7dd9) taken before any result was read; restored byte-identical.CI and gates
Read on head
dfb909bd0at 2026-09-17T07:46Z: 34 distinct names · 31 success · 3 skipped · 0 in_progress · 0 failure. The 3 skips are rostered expected skips (Packed-tarball smoke (opt-in)·Build Docs·Console Pin Gate). Path face taken fresh:check-governed-merges --pr 18596⇒ NOT governed, ordinary queue landing.Gate derivation reconciled 61 families: 57 green, 3 NOT MEASURED on
exit 3PREREQUISITE NOT MET (each needs a whole-repo dist that CI builds), 1 red —check:cross-package-test-inputs, which is pre-existing and not this diff's: the dev controlled it by reverting all three of this PR's paths on the same tree and reproducing the identical failure. Already filed as #18568, now carrying three independent reproductions. ⛔ Not ridden in.mcp_calls0; REST writes 2 of a budgeted 4. ⛔ No label write, ⛔ no assignee write.Declared and accepted
Gate derivation spelling: the dev passed
--commandswith no paths per the standing os-dev contract rather than the explicit list my dispatch asked for, because two-dot semantics against a movingorigin/maincan charge a sibling's merged files to this card. The script derived exactly the three paths, so the two spellings coincide here — and the standing contract is the better rule. ⇒ My dispatch wording was the weaker of the two; noted for the next dispatch.⇒ Flipping to ready and attaching auto-merge. The queue is the only sanctioned landing path.
Generated by Claude Code
fix(service-analytics): compareTo resolves its comparison window on t…