Skip to content

fix(service-analytics): a preset dateRange on a date field counts the reference zone's calendar days, east of UTC too - #27271

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-s7v4ey
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-s7v4ey

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread
Fixes #27172

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:报表的预设日期范围(如“本季度”)落在 date 类型字段上时,按参考时区的日历日计数;datetime 字段仍按时刻比较。两条查询策略、钻取范围、草稿预览共用同一处取界。
  • 为什么改:东八区等 UTC 以东的租户,“本季度”会多算 6 月 30 日、漏掉 9 月 30 日,报表数字静默出错(西半球恰好正确)。
  • 风险与回滚:只影响“预设范围 × 已声明为 date 的字段”;显式起止日期、datetime 字段、类型未知字段行为不变。回滚即还原本 PR。
  • 先看:packages/services/service-analytics/src/dataset-executor.ts(新增 dateRangeWindowBounds,复用既有 inclusiveCalendarDayWindow)、strategies/native-sql-strategy.ts 与 strategies/objectql-strategy.ts 的预设分支、drill-window.ts。
  • 两分钟验证:pnpm --filter @objectstack/service-analytics exec vitest run src/__tests__/dataset-drill-window.test.ts,预期 26 条全过,其中上海 date 字段 this_quarter 两条策略都只列 7/1–9/30 的记录。
  • 需要你做的:无,席位落地。

What changes

A preset dateRange (this_quarter, last_30_days, …) resolves to the reference zone's midnight instants. On a date column those instants were compared as they are, and the column reads an instant by its leading UTC day (temporalStorageForm), so east of UTC the window slid one day early: in Asia/Shanghai, this_quarter counted 2026-06-30 and dropped 2026-09-30, on both strategies (measured red below). West of UTC the leading UTC day happens to be the zone's own day.

The triage rule (6465987107): a date field compares calendar days in the reference zone (ADR-0053), a datetime compares instants, and the executor's existing whole-day rule is used, not a new resolver.

  • The existing rule, reused. inclusiveCalendarDayWindow in packages/services/service-analytics/src/dataset-executor.ts already projects a lowered window onto the calendar days it covers in the reference zone (through bucketDateKey, the exact inverse of the zonedDateStartToUtcMs the preset resolver renders bounds with); compareTo shifts exactly that projection. No new resolver and no new calendar arithmetic.
  • One spelling for every face. New dateRangeWindowBounds(window, timezone, dateColumn) beside it: a resolved preset on a date column becomes the inclusive day pair { $gte: first, $lte: last }; on any other column it stays { $gte, $lt } on the instants; an explicit [a, b] stays { $gte, $lte } bound for bound. It is called by NativeSQLStrategy (compile), ObjectQLStrategy (execute and the SQL echo), drillWindowBounds (so drill and count stay on one lowering, as fix(service-analytics): a windowed dataset answer reports its window in drillRanges #27169 set up) and the draft preview's evaluateAnalyticsQueryOverRows.
  • Which column is date. New declaredDateColumn(ctx, target) in strategies/filter-normalizer.ts, asked of the same declaredFieldType hook and the same storage target as declaredDatetimeLowering. A column whose type the host cannot name answers false and keeps the instants it was filtered by before.
  • The [gte, lt) drill bound for a date window now reads 2026-07-01 .. 2026-10-01 in Shanghai, the same days the count uses.

Bounded in-place fix, declared: the draft preview face (preview-evaluator.ts, inside the claim's file surface) had the same defect, measured red on 4d3a077f (Shanghai counted Jun 30 and dropped Sep 30); it takes the same helper, pinned by its own test.

Tests

  • dataset-drill-window.test.ts: the real-engine case "stage × this_quarter on the date field, Shanghai" now asserts the ids (won: d2 d3 d6 d8, lost: d10 d4) instead of parity only, on both strategies; added Los Angeles × date (west, unchanged days) and Los Angeles × datetime (instants: d2 out, d5 in), each also checking the drill lists exactly what the row counted. A unit case pins the engine bounds ({ $gte: '2026-07-01', $lte: '2026-09-30' }) and the drill range for Shanghai and Los Angeles.
  • New preview-preset-date-field.test.ts: the draft preview on a date field selects Jul 1 through Sep 30 in Shanghai, Los Angeles and UTC; a datetime field keeps the zone's instants.

Acceptance notes

  • driver-memory's cube face (MemoryAnalyticsService) is outside this claim's file surface and has the mirror-image defect: measured in-process on this branch, this_quarter on a date field (declared or not) answers Jul 1 and Sep 30 in Shanghai (right, by text-prefix ordering of a bare day against the instant) but Sep 30 and Oct 1 in Los Angeles (drops Jul 1, counts Oct 1). Reported to the seat for its own card; not touched here.
  • An explicit [a, b] window keeps the engine's UTC calendar-day reading on a datetime column (ADR-0053 D-D), unchanged and still pinned.
  • A field whose type the host cannot name keeps the instants (the drill already omits its window under a non-UTC zone).

Verification

All at head c9135bdb on this branch, base origin/main 4d3a077f.

Red first, on base sources (fix restored to origin/main 4d3a077, tests from this branch)
dataset-drill-window.test.ts: Tests 3 failed | 23 passed (26)
  x [#27172] a date field counts and drills the zone's calendar days (Asia/Shanghai):
      received { $gte: '2026-06-30T16:00:00.000Z', $lt: '2026-09-30T16:00:00.000Z' }
  x stage x this_quarter on the date field, Shanghai (native)    won +d1, lost -d4
  x stage x this_quarter on the date field, Shanghai (objectql)  won +d1, lost -d4
preview-preset-date-field.test.ts (preview-evaluator.ts at 4d3a077f): Tests 1 failed | 1 passed
  Asia/Shanghai: expected [jul01, sep30], received [jul01, jun30]
Green at c9135bd
pnpm --filter @objectstack/service-analytics test       Test Files 195 passed (195) · Tests 4705 passed | 282 skipped   VERDICT command-exit 0
pnpm --filter @objectstack/service-analytics typecheck  VERDICT command-exit 0
eslint --no-inline-config on the 8 touched files         exit 0
Ablation, once (scripts/ablation-replace.mjs; restored to the HEAD blob 41872f43db26, git diff HEAD empty)

Mutation in dateRangeWindowBounds: if (!dateColumn) return { $gte: window.start, $lt: window.end }; became if (dateColumn || !dateColumn) … (the date arm never taken). Anchor 1 to 0, blob 41872f43db26 to 6b76ccd50391.

Tests 4 failed | 24 passed (28)
  x a date field selects Jul 1 through Sep 30, east and west of UTC (preview)
  x [#27172] a date field counts and drills the zone's calendar days, east and west of UTC
  x stage x this_quarter on the date field, Shanghai (native)
  x stage x this_quarter on the date field, Shanghai (objectql)

The Los Angeles date and datetime cases stay green under the mutation, as expected: west of UTC the leading UTC day was already the zone's day.

Derived gates (dispatch-gates --commands, 60 families) at c9135bd
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-tenant-audit-census.mjs :: exit 0
node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 3
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

check:dual-build-cjs-loads exit 3 is PREREQUISITE NOT MET (it reads every package's dist; only this package's dependency closure was built): NOT MEASURED, CI's. check-plugin-teardown-shape --self-test first exited 3 on the shallow clone (its pinned fixture commit was unreachable); after fetching that commit it exits 0, as recorded. Reconciliation, dispatch-gates --ran: 60 derived, 59 run, 1 NOT-MEASURED (derived from the recorded exit 3), 0 UNRUN.


Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-analytics, touching 7 documentable anchor(s).

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

  • content/docs/releases/v14.mdx (via generateSql (symbol, a method of class ObjectQLStrategy))
  • content/docs/releases/v17/17-5.mdx (via generateSql (symbol, a method of class ObjectQLStrategy))

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

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 56 of 208 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 39ea1d67e5c272b41871cfeada3630971d10b81b → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 39ea1d67e5c272b41871cfeada3630971d10b81b

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · domain:server#3 · session_01Utp6CXc5hequf76uJfzrdK · head c9135bdb · 2026-10-28T14:30Z

  • Shape: 9 files, +236/−26, all under packages/services/service-analytics/** plus one patch changeset; no governed surface, no packages/spec, under the 3,000-line threshold. Matches claim 6469850256's surface; the preview-evaluator face is a declared, bounded in-place fix of the same defect (measured red on base).
  • Premise, measured: the triage rule 6465987107 ("the executor's existing whole-day rule is used, not a new resolver") holds in the diff: dateRangeWindowBounds delegates to the existing inclusiveCalendarDayWindow; no new calendar arithmetic. Count (both strategies), drill and draft preview call the one helper, so drill and count stay on one lowering, as fix(service-analytics): a windowed dataset answer reports its window in drillRanges #27169 set up.
  • Gates: all 48 check-runs on c9135bdb are success (34) or skipped (14), including the required set; mergeable_state: clean. The dev's local derived gates: 59/60 exit 0; check:dual-build-cjs-loads exited 3 (NOT MEASURED locally, green in CI).
  • Tests read: the Shanghai real-engine case asserts ids on both strategies, not just parity; Los Angeles date (west, unchanged) and datetime (instants) are pinned; the unit pins the engine bounds and the drill range. Red-first on base: 3 + 1 failures; one ablation 4 red, restored.
  • Accepted deviations: WIP-prefixed commit subjects (squash title carries the real subject); the dev did not request a review via MCP (the order forbids it).
  • Out of scope: the driver-memory mirror (west of UTC) → filed driver-memory analytics: a preset dateRange on a date field counts the wrong calendar days west of UTC (Los Angeles this_quarter drops Jul 1, counts Oct 1) #27272.
  • Declarations: Clause-②: no · Metadata-protocol: none, matching the diff (no accept-set or protocol change).
  • Landing: ready → auto-merge through the queue.

Generated by Claude Code

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

Development

Successfully merging this pull request may close these issues.

analytics: a preset dateRange on a date-typed field counts the wrong calendar days east of UTC (Shanghai this_quarter counts Jun 30, drops Sep 30)

2 participants