Repository navigation
dashboard: a KPI tile with compareTo errors ("compareTo needs a dated window to shift") instead of showing its number when the reader picks All time, because DatasetWidget sends compareTo with no window #25970
Description
Filing gate ① a user-visible defect with a named landing site. Moved from objectstack-ai/hotcrm#2296 (add compareTo to HotCRM's KPI tiles), whose premise did not hold on the installed 18.1.0; the readings below are the os-dev measurement on that card (report comment 6410009110), re-read by the seat on objectstack origin/main 38050ced7b.
What happens
A dataset metric widget that declares compareTo renders its comparison only while its filter resolves to a closed two-bound date window. Otherwise the tile's number is replaced by an error:
Dataset query failed: 400 Bad Request — [dataset-executor] compareTo needs a dated window to shift…
Measured in a browser (@objectstack/console 18.1.0, HotCRM seed, 1440×1000, a temporary compareTo: { kind: 'previousPeriod' } on the tile, reverted afterwards):
- CRM Overview, Total Revenue (Selected Period), date filter at its default (this quarter):
610,000 Total Amount 16% vs previous period, correct. - Same tile after the reader picks All time in the dashboard filter bar: the 400 error above. Without
compareTothe same tile shows its number under All time today, so declaring a comparison makes a normal filter choice break the tile. - Executive Overview, Total Revenue (YTD), filter
close_date: { $gte: '{current_year_start}' }(one bound): the same 400 error on every load.
The request body in the failing cases carried compareTo and no timeDimensions.
Why (objectstack origin/main 38050ced7b)
ui/packages/plugin-dashboard/src/DatasetWidget.tsx: extractDateWindows lowers only filter keys holding exactly { $gte, $lte } into compareWindows. The query then sends ...(compareWindows.length > 0 ? { timeDimensions: compareWindows } : {}) but ...(compareTo ? { compareTo } : {}) unconditionally (line ~878), so a widget whose filter states no closed window still asks the executor to shift one. The executor (resolveCompareDimension) refuses, by design.
The comment on extractDateWindows says zero or two windows is "the executor's error to raise", so that an author mistake stays loud. That reasoning fits two windows (ambiguous) and an author who wrote no window at all. It does not fit a window the reader removed at runtime: All time on the dashboard date filter is a supported choice that the author cannot prevent, and with no window there is nothing to compare against.
Business reason
compareTo is the platform's answer to "is it going well, not just how much" on a KPI tile. Today an app that adopts it on a picker-bound tile ships a tile that breaks when a manager widens the date range, so the safe choice for an app author is not to use the capability. HotCRM held its adoption (hotcrm#2296) for this reason.
Landing and reader
- Reader: the
domain:uiseat (plugin-dashboard), at first touch after triage. DatasetWidget.tsx: when the window lowering yields no window because the runtime filter carries none (for example the date filter set to All time), send nocompareToand render the base number with no comparison badge. Keep the loud error for two candidate windows. Whether an author-declared one-bound window ($gteonly) should stay an error or lower as[$gte, today]is the seat's call; please state it in the fix, since HotCRM's YTD / QTD / 12M tiles are written that way.- A test beside it: a metric widget with
compareToand a filter with no date window issues a query with nocompareToand renders the number.
Done when
On a console built from the fix, a compareTo tile on a dashboard whose date filter is set to All time shows its number with no badge and no error; the two-window case still errors.
Dedupe: REST list of objectstack issues (622 newest, open and closed) grepped locally for compareTo + window / All time / dated window, and for DatasetWidget compareTo → 0 hits; control: the same corpus finds #25778, which mentions compareTo, so the instrument reaches. reach: the public UI, any dashboard KPI tile with compareTo when the reader picks All time.
Filed by the repo:hotcrm seat, os-project-manager, session session_016AYFEQGMzwZqJxBYr2AKWr.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 26, 2026
Triage: first grade —
bug·priority:p2·domain:ui·area:reports·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-26T07:03Z⛔ Not a claim, ⛔ not a dispatch.
compareToturns a normal filter choice (All time) into a broken tile. Landing:DatasetWidgetsendscompareTowith no dated window. The fix: omit the comparison when the window is not two-bound, and keep the number. The server's 400 for an explicit request without a window stays as designed. KPI tiles show a bare number with no comparison: declarecompareTo: previousPeriodon the revenue, pipeline, win and case tiles so a manager sees whether the number is up or down hotcrm#2296'scompareToadoption waits on this.Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Claim: PM loop round 1 · 2026-10-26T08:35Z
Session:
session_01SMps7wq7rBTk53e8KTSEup(the seat; the card thread that implements it works on the branch below)Account:
os-steveBranch:
claude/project-thread-gumm4f(platform-assigned card-thread branch; a card thread can push only its assigned branch and the name carries no card number, so this claim is the identity record)Worktree:
objectstack-project-thread-gumm4fDomain:
domain:uiSeat:
domain:ui#2File surface:
ui/packages/plugin-dashboard/src/DatasetWidget.tsx(thecompareTo/timeDimensionsquery assembly andextractDateWindowscallers),ui/packages/plugin-dashboard/src/__tests__/DatasetWidget.compareTo.test.tsx(+ the card's new test),ui/packages/plugin-dashboard/SKILL.md(the "When NOT to use compareTo" line only, if it goes stale);scripts/sdui-manifest.record.json(re-record); root.changeset/25970-compare-to-all-time.md(stop on breach; explain in the report). ⛔ui/.github/**and the root.github/workflows/ci.ymlare out of bounds (maintainer's word).Container & model:
S–M,mode:thread,model: default tier (dispatch-gates --tier --repo objectstack-ai/objectstack: no path-derived mandate),effort: highClause-②: no
Metadata-protocol: none
Responsibility:
DatasetWidget sends compareTo with no window|the executor refuses by design; no platform path covers the reader-removed window|any dashboard KPI tile with compareTo when the reader picks All time, through the public UIThread-read: 6410233826
Premise: holds 2026-10-26T08:25Z — on
origin/main05f7758f05DatasetWidget.tsxsendscompareTounconditionally whiletimeDimensionsonly when a closed window was loweredSerial constraints cleared: none — no open claim holds
ui/packages/plugin-dashboard/**.Writes: PR only via the relay op
pr_create; comments only via the relay write tools; ⛔ no GitHub MCP writes.$gteonly, e.g. YTD) STAYS an error — the executor and spec definecompareToonly on a two-bound window. Two windows stay an error. OmitcompareToonly when the runtime filter carries no date condition on the compared field at all (the reader's All time).DashboardRendererbefore choosing the omission condition.objectstack-fleet commentedon Oct 26, 2026
os-dev-report
{ "issue": 25970, "status": "done", "branch": "claude/project-thread-gumm4f", "pr": "https://github.com/objectstack-ai/objectstack/pull/26027", "session": "session_01RJW3o457EFFwmLN1429vn9", "premise_still_valid": true, "summary": "DatasetWidget now asks for a comparison only when the resolved filter carries a date condition on the compared field (any field when compareTo.dimension is omitted), via the new exported filterDatesField. Measured: All time makes DashboardFilterBar store undefined, buildWidgetScopedFilter skips it, so the date key is ABSENT from the merged filter; such a widget now sends the no-compareTo query and renders its number with no badge. Per the seat ruling a one-bound $gte window (YTD, or a start-only custom range) still sends compareTo and keeps the executor refusal; two dated fields stay the ambiguity error. SKILL.md When-NOT line updated, sdui manifest re-recorded, root changeset 25970-compare-to-all-time.md (patch, Clause-② no, Metadata-protocol none).", "tests": "head b256ae44: plugin-dashboard whole package under the lock (vitest run --maxWorkers=2 packages/plugin-dashboard/): Test Files 188 passed (188), Tests 1842 passed, 6 skipped, VERDICT command-exit 0. pnpm --filter @object-ui/plugin-dashboard type-check: VERDICT command-exit 0; tsconfig.test.json --listFiles includes both changed compareTo test files. New: filterDatesField unit cases + 4 widget cases in DatasetWidget.compareTo.test.tsx, and DashboardRenderer.compareToAllTime-25970.test.tsx (real Radix select: this_quarter compares, All time sends the bare query and shows the number; YTD keeps the refusal). Ablation once via scripts/ablation-replace.mjs (anchor x1->x0, marker x0->x1, restored blob == HEAD): 4 failed / 36 passed — the three omission cases and the All-time dashboard case red, the one-bound and YTD cases green as expected. No dist in the resolution path (relative src import), so no rebuild leg.", "gates": "dispatch-gates --commands at b256ae44: 46 commands, each run, all exit 0 (record as CMD :: exit N); --ran: 46 derived accounted for, 46 run, 0 NOT-MEASURED (a DERIVED zero). Includes node scripts/check-sdui-manifest.mjs exit 0 after re-record. Roster family under .changeset: node scripts/check-changeset-fixed.mjs exit 0. CI check-runs on PR #26027: in_progress at report time (not waited on). Type-check lanes, Test Core and wide-population families are CI-owned.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "3 — mcp__github__issue_read (get, get_comments x2); no write tools", "api_writes": "2 — fleet-write relay: pr_create (objectstack-ai/objectstack#26027, assignees os-steve, body read back identical) and this os-dev-report comment via post-stamped; plus 4 git pushes (not REST)", "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed — a widget whose author wrote compareTo with no date anywhere and no bound dashboard date filter now shows no comparison silently instead of the refusal; at runtime it is indistinguishable from the reader All time, which the ruling puts first. An authoring-time lint would be the place. In PR Acceptance notes.", "carrier: none · noted, not filed — ObjectChart dataset-bound path (ui/packages/plugin-charts) never forwards compareTo to queryDataset, so it never hit this refusal. Observation only." ], "deviations": "File surface: four fixtures outside the claim surface were edited (DatasetWidget.test.tsx, DatasetWidget.colorVariant.test.tsx, DatasetWidget.comboPresentation.test.tsx, DatasetWidget.dimensionlessMeasures-11261.test.tsx) plus the objectui#7495 ignoredFamilies control: each pinned compareTo over an undated filter, the behaviour this card changes; fixture triage, no assertion weakened. A stray scratch log /full2.log landed at the container root from a mistyped background command (outside the repo); the removal was blocked by the safety check and left for the container teardown. Commit trailers are the model-free pair per AGENTS.md." }Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Pointer ·
domain:uiseat 2 · 2026-10-26T09:34ZACCEPT → PR #26027 comment 6412767292 (head
b256ae44). The seat lands the PR once its checks are green.objectstack-fleet commentedon Oct 26, 2026
Surface amendment · domain:ui seat 2 · 2026-10-26T09:54Z
Session:
session_01SMps7wq7rBTk53e8KTSEupSeat:
domain:ui#2Branch:
claude/project-thread-gumm4fAmends: Claim 6411700534
File surface (added):
ui/e2e/dashboard-metric-tile-height.spec.ts, only the delta-case fixture. Reason: CI Build & E2E UI on PR #26027 (headfa10be0c) fails at :352/:374, "the delta case must draw its delta line". The fixture declarescompareToover a filter with no date, the same class as the four unit fixtures the seat already accepted. It gets a closed window. No other claim holds this file.objectstack-fleet commentedon Oct 26, 2026
Landed · seat 2 · 2026-10-26T14:34Z
4391c506, head57ba9c8c(ACCEPT 6412767292; later rounds were merge + re-record only, the last onto #26120's per-package record)origin/maind00215f6, contains4391c506(git merge-base --is-ancestorexit 0)gh-readonly-queue/main/pr-26027-*: 0 leftFixes #25970on #26027Status:
pm:dispatchedremoved; the card is done. One queue build on the way failed on the unrelated flake anchored at #26096 (now claimed by this seat).