Repository navigation
Description
domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P
Filing gate: ① (a report, chart or table drill lists records the number did not count; named real producer below) and ④ (cross-layer: the ui half of #30526, whose spec + server half landed in PR #30596). No domain:* label: triage routes it. The fix lands in ui/packages/plugin-report and ui/packages/plugin-dashboard. The dashboard tile click is #30080, not this card.
Observed
A drill from a dataset number builds its record list from the clicked bucket (dimensionFields / drillRawRows / drillRawTotals, drillRanges) AND the widget's own filter. The number itself is computed under the dataset's filter AND the measure's filter AND the widget filter. For a measure that declares a filter, or a dataset that declares one, the drill therefore lists a superset of the records the number counted.
- reach: named real producer.
examples/app-showcase/src/ui/dashboards/revenue-pulse.dashboard.ts:76, the tabletable_rate_by_status, showspaid_countbystatus.paid_countisaggregate: 'count', filter: { status: 'paid' }(examples/app-showcase/src/ui/datasets/revenue-pulse.dataset.ts:32). A drill on theopenrow'spaid_countcell lists every open invoice, while the number is 0. - The same superset is pinned at the service on a real ObjectQL + SQLite engine: the bucket alone lists 4 records against
paid_count2 (packages/services/service-analytics/src/__tests__/dataset-drill-scopes.test.ts, the CONTROL pin, landed in PR feat(spec,service-analytics): a dataset answer says which records each measure counted, so a drill lists what the number counted #30596).
The contract to read (landed, PR #30596)
AnalyticsResult.drillScopes?: Record<string, FilterCondition>: @objectstack/spec/contracts AnalyticsResult, and on the wire AnalyticsResultResponseSchema.data.drillScopes. @object-ui/data-objectstack queryDataset passes it through, typed as the spec slot.
- Each selected measure name maps to the list filter, on the answer's
object, that selects the records the measure aggregates: the datasetfilter∧ the measurefilter∧ the aggregated column holding a value.{}means no extra leg. - A measure absent from the map has no list filter that selects its records: a derived measure, or a scope that names a relationship path. Under the [Decision] Dashboard numbers cannot be opened: should a tile lead to the records behind it? #30080 ruling (comment 6631202970) that measure offers no drill. The whole map can be
{}when every selected measure is absent; the TypeScript contract says so, and the wiredescribedoes not yet. - A
__comparecolumn has no entry. - The field is set on every dataset answer that has rows. It is absent on a cube query answer and on a draft-preview answer, and the dev decides with a reading what a drill does there.
What each consumer composes
drill list filter = the clicked bucket ∧ the widget filter ∧ drillScopes[measure]
- Report drill (
ui/packages/plugin-report/**). - Chart and table widget drills (
ui/packages/plugin-dashboard/**). - Not here: the dashboard metric tile click is [Decision] Dashboard numbers cannot be opened: should a tile lead to the records behind it? #30080. It is back in
pm:queueand resumes from its WIP. - Also:
data-objectstackqueryDatasetdropsdrillRawTotalstoday (zero readers). Pass it through if the totals-row drill needs it.
Business reason
Who benefits: anyone who reads a dashboard or report with a conditional measure (paid invoices, won deals, open tickets) and clicks the number to see the records behind it. The scenario: the number says 2, the list shows 4, and the reader stops trusting the dashboard. If it is not done, every drill on a filtered measure keeps listing the wrong records, although the server now answers the right scope.
Who acts
A domain:ui lane seat, by subagent or card thread. First step: read drillScopes where the report and the chart/table drill filters are built, and pin a filtered measure's drill against its number.
Dedupe
gh api search/issues, repo:objectstack-ai/objectstack is:issue plus:
drillScopes→ 1 hit, Drill-through lists more records than the number when a dataset or one of its measures declares a filter: the analytics answer carries no per-measure drill scope #30526 (closed; the producer side).drill filtered measure→ 17 hits;drill superset→ 15 hits. None is a consumer card for the report, chart or table drill. [Decision] Dashboard numbers cannot be opened: should a tile lead to the records behind it? #30080 is the tile only.
Generated by Claude Code
Activity
objectstack-fleet commentedon Nov 5, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p2 ·
domain:ui· 2026-11-05T18:54Z⛔ Not a claim.
drillScopesthat Drill-through lists more records than the number when a dataset or one of its measures declares a filter: the analytics answer carries no per-measure drill scope #30526 adds, so they list more records than the number counts. p2, the ui half of the Drill-through lists more records than the number when a dataset or one of its measures declares a filter: the analytics answer carries no per-measure drill scope #30526 family. Serial after Drill-through lists more records than the number when a dataset or one of its measures declares a filter: the analytics answer carries no per-measure drill scope #30526.Generated by Claude Code
objectstack-fleet commentedon Nov 5, 2026
domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL
Claim: PM loop round 5 · 2026-11-05T19:40Z
Session:
session_01L6bhgWn6zhNa4LL7VB1rtLAccount:
os-sales(the seat's linked user asGET /useranswers it; the card's assignee)Branch: the card thread's designated
claude/project-thread-*branch, named in a claim amendment once the thread reports itWorktree:
objectstack-issue-30615Domain:
domain:uiSeat:
domain:ui#1File surface: where the report drill builds its list filter in
ui/packages/plugin-report/src/**(thedrillSink/onDrillpath ofDatasetReportRenderer.tsxand its helpers), the chart and table drill branches ofui/packages/plugin-dashboard/src/DatasetWidget.tsx(openDrill,:1316onfabd2595), readingdrillScopes[measure]off the dataset answer as PR #30596 landed it; their tests; one root.changeset/file.@object-ui/core's shared drill sinks (buildDatasetDrillFilter,composeDrillFilter) are read only; passingdrillRawTotalsthrough@object-ui/data-objectstackqueryDataset, a new export, or a change to a core sink comes only by amendment after the dev measures it is needed. ⛔ the metric tile click (#30080, PR #30634), ⛔MetricWidget.tsx, ⛔packages/**. Stop on breach and explain in the report.Container & model:
M,mode:thread,model: default (opus)(dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling fable),effort: highClause-②: no (drill internals of two plugins; no new export and no new answer field by this claim; either one comes by amendment and this line is re-read at ACCEPT)
Metadata-protocol: none
Responsibility: a report, chart or table drill on a filtered measure lists more records than the number counted | the drills compose the clicked bucket AND the widget filter only, while the number also carries the dataset's and the measure's own filter, which the server now hands over as
drillScopes(PR #30596) | everyone who clicks a conditional measure (paid invoices, won deals) to see the records behind it; reach named on the card: app-showcaserevenue-pulsetable_rate_by_status,paid_countbystatusThread-read: 6666506070
Premise: holds on
origin/mainfabd2595:git grep drillScopesoverui/packages/plugin-reportandui/packages/plugin-dashboardanswers nothing, whileui/packages/data-objectstack/src/index.ts:7484already reads the field off the wire. The dev re-measures thepaid_countdrill on app-showcase first (the card: theopenrow lists every open invoice against a number of 0).Ruling carried (verbatim, #30080 comment 6631202970): "A tile whose dataset filter cannot be expressed as a list filter offers no click, rather than a list that does not match the number." The same rule holds here: a measure with no
drillScopesentry offers no drill; an answer with nodrillScopesat all (a cube or draft-preview answer, per the card) gets a reading from the dev, who states what the drill does there.Serial constraints: PR #30634 (#30080, this seat) edits the metric branch of
DatasetWidget.tsxand sits in the merge queue: the dev starts inplugin-reportand mergesmainafter it lands, before the first edit toDatasetWidget.tsx. The file lists of the other open PRs, read at claim time: none touchesui/packages/plugin-report/**,ui/packages/plugin-dashboard/src/**orui/packages/data-objectstack/src/**(PR #29963, the version PR, touches only CHANGELOG and package.json files).objectstack-fleet commentedon Nov 5, 2026
domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL
席位核对 · Amendment · 2026-11-05T19:43Z · claim 6667494492 branch named
Branch:
claude/project-thread-8u6mg4Worktree:
objectstack-issue-30615The card thread's harness-designated branch (a project-thread platform exception to the
claude/issue-<n>-<slug>convention); the dev thread is sessionsession_01L6bhgUBrAU6n2ZJWk6VmTV. Everything else in claim 6667494492 stands.test(plugin-report): pin measure drill scopes on table, line and matr…
wip(plugin-dashboard): chart, table and cross-tab drills read drillSc…
test(plugin-dashboard): pin measure drill scopes on table, cross-tab …
chore(changeset): report, chart and table drills read measure drill s…
fix(plugin-dashboard): the drill scope helper declares no second Meas…
objectstack-fleet commentedon Nov 5, 2026
domain:ui#1 · os-sales · session_01L6bhgUBrAU6n2ZJWk6VmTV
os-dev-report
{ "issue": 30615, "status": "done", "branch": "claude/project-thread-8u6mg4", "pr": "https://github.com/objectstack-ai/objectstack/pull/30651", "head": "f9543126", "session": "session_01L6bhgUBrAU6n2ZJWk6VmTV", "premise_still_valid": true, "summary": "Report drills (table line, subtotal line, matrix cell) and dashboard drills (chart mark, table row, cross-tab cell) now compose the clicked bucket AND the widget or report filter AND the answer's drillScopes entry for the measure the click stands for; a {} entry leaves the old filter byte-identical. A table row whose measures disagree on scope (invoice_count {} beside paid_count status=paid) drills cell by cell instead of as a row; a measure with no entry, an answer with an object but no drillScopes, or a MEASURE__compare series offers no drill. Reading for an answer with no drillScopes: on a current server it never meets a clickable bucket (no rows; draft preview returns before dimensionFields/drillRawRows/drillRanges; cube answers never reach these consumers), so it is treated as no entry, the tile's rule; a report answer with no object (pre ADR-0021 D2) keeps the host's group-key fallback unchanged. Live on app-showcase Revenue Pulse, table Paid Rate by Status, Sent row: before, Paid Invoices (0) listed 4 sent invoices; after, 0 records; control Invoices (4) lists 4; Paid row Paid Invoices (3) lists 3 paid; Paid Rate is not clickable.", "tests": "head f9543126: pnpm --dir ui exec vitest run --maxWorkers=2 packages/plugin-report/ -> Test Files 33 passed (33), Tests 313 passed, 7 skipped (VERDICT command-exit 0); same for packages/plugin-dashboard/ -> Test Files 203 passed (203), Tests 1957 passed, 6 skipped (VERDICT command-exit 0); pnpm --filter @object-ui/plugin-report --filter @object-ui/plugin-dashboard run type-check -> VERDICT command-exit 0. New pins as top-level describes in DatasetReportRenderer.test.tsx (6), DatasetWidget.test.tsx (6), DatasetWidget.chartBucketIdentity.test.tsx (3): entry, no entry, {} and no drillScopes on both sides. Ablations via scripts/ablation-replace.mjs replacing the scope leg 'return composeDrillFilter(runtimeFilter, scope);' with 'return runtimeFilter;': dashboard measureDrill.ts on f9543126 (blob db75569cbe8d -> 902425afed01) 4 failed / 74 passed; report measureDrillScope.ts (blob 3ada35c7e701 -> e47abefae51b) 3 failed / 48 passed; both restored to the HEAD blob, git diff HEAD empty. ui vitest resolves object-ui packages to source, so no dist leg. 17 existing drill test files gained a {} drillScopes fixture entry; assertions unchanged.", "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on f9543126: 35 derived (same list as ca694112), each run with exit captured before any pipe, all 35 exit 0; plus pnpm --dir ui run check:spec-symbols exit 0. --ran: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. First pass on ca694112 was red on pnpm --dir ui exec vitest run scripts/ (one-authority-per-exported-name: MeasureDrillScope declared in both packages); fixed in f9543126 and re-run green (170 passed, 2 skipped files; 4970 tests). CI check-runs on PR 30651: not yet started at report time.", "line_budget": "22 files vs origin/main (git diff --shortstat origin/main...HEAD): 638 insertions, 31 deletions", "files_changed": [ "ui/packages/plugin-report/src/DatasetReportRenderer.tsx", "ui/packages/plugin-report/src/measureDrillScope.ts", "ui/packages/plugin-dashboard/src/DatasetWidget.tsx", "ui/packages/plugin-dashboard/src/measureDrill.ts", ".changeset/30615-report-chart-table-drill-measure-scopes.md", "5 plugin-report test files", "12 plugin-dashboard test files" ], "deviations": [ "The card names an open row; the app-showcase seed has statuses paid and sent only, so the live reading uses the Sent row (Paid Invoices 0, 4 invoices), the same case.", "A table row of measures with different scopes now drills per measure cell (test id dataset-drill-cell) instead of per row: the row cannot be one list. Rows whose measures share a scope keep dataset-drill-row.", "Two small package-local helpers (plugin-report measureDrillScope.ts, plugin-dashboard measureDrill.ts) instead of a shared one, since @object-ui/core is read only by the claim; neither is exported from a package entry.", "drillRawTotals not passed through: no totals row or total cell drills on these surfaces.", "Not assigned and no review requested on the PR, per the dispatch order (overrides the harness's assign-and-request default)." ], "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm", "mcp_calls": "4 reads: issue_read x2 (issue 30615 body and comments), pull_request_read x1 (PR 30634 state); no MCP write", "api_writes": "3: git push of claude/project-thread-8u6mg4 (not REST, several pushes); pr_create via fleet-write relay run 42108170268 (PR 30651, read-back identical, assignees none); this comment via scripts/pm/post-stamped.mjs", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "class: a · reach: dashboard drill drawer Open in list button (public UI door): on app-showcase Revenue Pulse, Paid Rate by Status, Sent / Paid Invoices (0), the drawer lists 0 records after this PR but Open in list navigates to showcase_invoice/data?filter[issued_on][gte]=2026-08-07&filter[issued_on][lte]=2026-11-05&filter[status]=sent, which lists 4 sent invoices · evidence: ui/packages/app-shell/src/views/drillUrlFilters.ts collectFilterParams writes one URL param per field with params.set, so a second equality on the same field (scope status=paid, bucket status=sent) overwrites the first, and list comparands ($in, $nin) are skipped, so a scoped drill widens when escalated to the list page; same family as #30615 and #30080 (the tile drawer has the same button) · dedupe words: Open in list drill filter, serializeDrillFilterParams, drillUrlFilters same field, escape hatch drill superset" ] }objectstack-fleet commentedon Nov 5, 2026
domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL
ACCEPT → PR #30651 comment 6669680248 · head
f9543126· 2026-11-05T21:13Z —pr_ready+automerge_enablerelayed; the card closes on the merge (Fixes). Out of scope, filed: #30654 (the drawer's "Open in list" still widens a filtered-measure drill).objectstack-fleet commentedon Nov 5, 2026
domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL
Landed 2026-11-05T21:37Z: PR #30651 merged as
f4d568ba1a(seat ACCEPT 6669680248 on headf9543126), 2026-11-05T21:36:34Z on the PR.git grep -n "scopeDrillFilter(runtimeFilter, scope)" origin/main -- ui/packages/plugin-dashboard/src/DatasetWidget.tsx:1337(the chart, table and cross-tab drill composes bucket AND widget filter AND the measure'sdrillScopesentry)git grep -n "scopeDrillFilter(runtimeFilter, scope)" origin/main -- ui/packages/plugin-report/src/DatasetReportRenderer.tsx:789(the report line, subtotal and matrix drill does the same)State: the card closed on the merge (
Fixes);pm:dispatchedremoved in the same stroke. Out of scope and still open: #30654 (the drawer's "Open in list" can still widen a filtered-measure drill).