Repository navigation
fix(service-analytics): a windowed dataset answer reports its window in drillRanges - #27169
Conversation
…in drillRanges A timeDimensions dateRange window now rides drillRanges: an entry on every row for an ungrouped windowed dimension, and a narrowed bucket entry when the window's field is also a bucket's. The bounds come from the query's own lowering and the engine's storage form, so a drill list filters by exactly the window the query filtered by. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5msvniAz5h2mP2QABA
…rill case red on the base Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5msvniAz5h2mP2QABA
…lared two-bound arm Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5msvniAz5h2mP2QABA
…rding Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5msvniAz5h2mP2QABA
…case's rows the same with or without the empty-bucket fill Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5msvniAz5h2mP2QABA
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5msvniAz5h2mP2QABA
📓 Docs Drift CheckThis PR changes 2 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 147 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6bd0a82fe3abd7d1df839339343a1edfce5bed03 && git checkout 6bd0a82fe3abd7d1df839339343a1edfce5bed03
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0f1045bcc7a3e424def90abcc70dc849944b00bd b807ecc5fa2574e3dd9d2c6bdc1d9c41d29f157c && git checkout -B drift-repro 0f1045bcc7a3e424def90abcc70dc849944b00bd && git merge --no-ff b807ecc5fa2574e3dd9d2c6bdc1d9c41d29f157c
node scripts/docs-audit/affected-docs.mjs --json 0f1045bcc7a3e424def90abcc70dc849944b00bd
|
席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: Triage seat, under the gap rule (#27062), condition (b). Generated by Claude Code |
Contract reviewServed-tier: Inputs: card #27074 (body, triage 6461017000, claim 6462057514, os-dev-report 6464984887), PR #27169 (body, file list, net diff against merge-base Gate verdicts on the head, read off the check-runs: the eight required contexts conclude ① Derived judgmentsPublished schema (
Contract interface (
Nothing in ① is wrong. The one doubtful point is not a contract claim but a construction detail, carried in ③ (midnight race). ② Semver level
Clause-②: ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Contract reviewServed-tier: Inputs read: card #27074 body and all three comments (triage 6461017000, claim 6462057514, os-dev-report 6464984887); PR #27169 body, file list, all PR comments (docs-drift, seat ACCEPT 6465413400, triage claim 6465890103); net diff vs ① Derived judgments
② Semver level
③ Boundary flagsDev flags (PR body Acceptance notes) and the report's
Reviewer's own flags:
④ Design judgment
Neither finding is a must-fix: the tree delivers the ruling (drill bounds = the query's own, one set of functions, no browser resolver, no new key), the spec text describes the tree, and the suite pins the equality between the two on a real engine. Implemented-by: VERDICT: PASS Generated by Claude Code |
Requested by Leehom · project thread
Fixes #27074
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
selection.timeDimensions[].dateRange,如“本季度”)后,数据集查询的回答在已有的drillRanges里带上这个时间段的边界:没按该日期分组时每行多一条{ field, gte, lt };按同一字段分桶时,首尾两个桶的范围收窄到时间段以内。没有新增键。last_30_days的首尾月份下钻也会列出整月。packages/services/service-analytics/src/drill-window.ts;analytics-service.ts的drillRanges块(窗口与桶按字段合并);src/__tests__/dataset-drill-window.test.ts的真实引擎用例。pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=2 src/__tests__/dataset-drill-window.test.ts,预期 21/21 通过;在main上同一文件 18 条失败。What changes
A dataset query whose selection carries
timeDimensions[].dateRangenow reports that window in the answer's existingdrillRanges, so a drill list filters by exactly the bounds the query filtered by. Nothing underui/, and nothing the query filters changes.stage, windowed onclose_date) getsclose_date → { field, gte, lt }on every row.close_quarterbucket overclose_date, windowed through theclose_datedimension) would have replaced the bucket's bound instead of meeting it.drill-window.ts, no second lowering):lowerDatasetCompareDateRange, this package's registered face in the shareddateRangeconformance kit (a preset throughresolveAnalyticsDateRangeStringin the request's reference zone, an explicit[a, b]throughexplicitDateRangeWindow); the{ $gte, $lt }/{ $gte, $lte }pair both strategies build, throughnormalizeDateRangeWindow(the shared whole-day rule turns a bare-day$lteinto$ltthe next day); then core'stemporalStorageForm, the rule the engine reads each temporal comparand through.{token}placeholders resolve withresolveFilterTokens, as the executor resolves them. The rule for which fields get an entry (datetime→ instants,date→ days, unknown type only under UTC) is the one the bucket ranges already used, now a sharedinstantOfin that block.<=it) or the last supported day (no upper bound). The drill then stays as wide as before.drillRangesdocblock onAnalyticsResult(packages/spec/src/contracts/analytics-service.ts) and its.describetext on the dataset answer schema (packages/spec/src/api/analytics.zod.ts) say what the key now carries; the generatedcontent/docs/references/api/analytics.mdxrow is regenerated. No key, type or export added.Why
drillRangescarries it with no ui edit (seat lean, measured)Every ui drill path hands the row's
drillRanges[i]tobuildDatasetDrillFilter(ui/packages/core/src/utils/dataset-format.ts), which iteratesObject.values(rawRanges)and writes{ [r.field]: { $gte: r.gte, $lt: r.lt } }for each entry whatever its key: the report row drill and pivot cell drill (DatasetReportRenderer.tsx) and the dashboard widget (DatasetWidget.tsx, which also treats a non-emptydrillRangesas drillable). The real-engine half of the new test runs exactly that filter shape againstengine.findand lists what each row counted. So the reach closes with no ui change, and an existing reader sees only more entries in a key it already reads (Clause-②: yes (widening),Metadata-protocol: none,patch).Tests
packages/services/service-analytics/src/__tests__/dataset-drill-window.test.ts, 21 cases, clock frozen at2026-09-09T12:34:56.789Z.queryDatasetover a recorded engine aggregate (ObjectQL face): stage ×this_quarteron adatetimefield inAsia/Shanghaianswersclose_date → { gte: '2026-06-30T16:00:00.000Z', lt: '2026-09-30T16:00:00.000Z' }on every row, and the test reads the very same{ $gte, $lt }off the filter the strategy handed the engine. Month buckets ×last_30_daysare narrowed at both edges with the middle untouched (Shanghaidatetime, and an explicit[2026-07-15, 2026-09-14]on adatefield). A window throughclose_dateover aclose_quarterbucket on the same field narrows that entry and adds none. An explicit bare-day window ondatetimeruns to the next UTC midnight; a{month_start}..{today}token window resolves as the executor resolves it; an instant end and a9999-12-31end get no entry.dateRange(equality-only grouping answers nodrillRanges; a granularity-only entry keeps its bucket range); adatefield serialises calendar days; UTC resolves at UTC midnight; an unknown field type under a non-UTC zone gets no entry;compareTocomputescnt__compareas before and the row drills the primary window.AnalyticsServicePlugin's own composition, native and ObjectQL strategies): ten deals either side of Q3's edges in Shanghai and UTC. For each row, the drill filter the ui builds ({ field: { $gte, $lt } }per entry, ANDed with the row's raw grouped value) is run throughengine.find, and the ids it lists must number exactly what the row counted: stage ×this_quarterondatetime(Shanghai), stage × an explicit bare-day window ondatetime(Shanghai), month ×last_30_days(Shanghai), stage ×this_quarterondate(UTC), and stage ×this_quarterondate(Shanghai, parity only; see Acceptance notes).Red first: with
analytics-service.tsrestored toorigin/main6845035canddrill-window.tsremoved (tree only, restored after withgit diff HEADempty and the blob hash equal), the file reads 18 failed / 3 passed; the three that pass are the base-true controls (nodateRange, the no-spelling ends, the unknown type). Green at head: 21/21.Ablations (
scripts/ablation-replace.mjs, restore proven against the HEAD blob each time; this package's tests import itssrc, so no build is in the path):if (w.gte > r.gte) r.gte = w.gte;(the window's lower edge stops narrowing a bucket): 4 red, the two month-edge unit cases and the month ×last_30_daysreal-engine case on both strategies.datetime(temporalStorageForm(v, 'datetime')): 4 red, everydate-field unit case. The real-enginedatecases stay green under it, as expected: the engine reads an instant comparand on adatecolumn through the same storage rule.Acceptance notes
datefield east of UTC filters the wrong days (finding, not changed here). The query compares adatecolumn with the preset's zone-midnight instants read as their leading UTC day, sothis_quarterinAsia/Shanghaicounts 2026-06-30 and drops 2026-09-30 (measured on SQLite, both strategies; west of UTC the leading day is the right one). The drill reports that same storage-form window, so the list still shows what the row counted, and the real-engine case pins parity only. Which days the query should count is reported to the seat as a separate defect.<=), or9999-12-31(no upper bound), leaves the drill as wide as it was. No measured producer sends either: the report chip sends a preset name or a[from, to]of bare days.datetimeare cut at UTC midnight (ADR-0053 D-D), so the drill reportsT00:00:00.000Zbounds even under Shanghai. That is what the query filters by; whether the explicit arm should follow the reference zone is ADR-0053's recorded open seam, not this card.drillRawTotalshas equality values only), as before; no ui path readsdrillRawTotalstoday.new Date(), as each strategy does for the query. A request straddling the reference zone's midnight could resolve the two a day apart.origin/mainis merged in (clean) and every suite above re-ran on the merged head. Both in-window months of the month ×last_30_daysreal-engine case hold a deal, so its rows are the same with or without that fill. feat(spec): a dataset measure declares its polarity, so a lower-is-better KPI draws a rising delta as bad news #26739 (measure polarity, open) edits other hunks ofanalytics-service.tsand the two spec files.Verification
All at head
b807ecc5(the branch merged withorigin/mainafter #27109 landed), after a fullpnpm build(VERDICT command-exit 0), unless a line says otherwise.Package suites
@objectstack/service-analyticspnpm test: 193 files, 4689 passed / 282 skipped;pnpm typecheckexit 0 (it type-checks the test files). VERDICT command-exit 0.dataset-drill-window.test.ts: 21/21. Red first at6845035c(production files restored, test file kept): 18 failed / 3 passed.@objectstack/specsrc/api/analytics.test.ts+src/contracts/analytics-service.test.ts: 43/43.@objectstack/rest: every test file that posts a dataset query (31 files), 1140 passed / 40 skipped.@objectstack/dogfood(skips PR CI):temporal-storage-e2e.dogfood.test.ts+analytics-timezone.dogfood.test.ts, 10/10.eslint --no-inline-config packages/services/service-analytics/src packages/spec/src/api/analytics.zod.ts packages/spec/src/contracts/analytics-service.ts: exit 0.Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackatb807ecc5: 107 families. Every one run, each exit code captured before any pipe: 107/107 exit 0.--ranreconciliation:✓ dispatch-gates --ran: 107 derived famil(ies) accounted for — 107 run, 0 NOT-MEASURED.check-plugin-teardown-shape --self-testneeds its fixture commit in a shallow clone: fetched withgit fetch --depth=1 origin 621a4876…before the run.Not run locally:
test:repo(the diff touches no repo-project test; CI's Test Core runs it); the full dogfood battery (no!in the title and no(narrowing)arm).Generated by Claude Code