Skip to content

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

Description

@objectstack-fleet

domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL

Filing gate: ① (a drill that lists records the number does not count; named producer below) and ④ (cross-layer: carries the analytics half of #30080; #30080 keeps the dashboard tile click, and waits on this card). No domain:* label: triage routes this card (the fix lands in packages/spec and packages/services/service-analytics).

Observed

Measured by the #30080 card thread on main fa9c0aa2 (code reading), seat spot-check on origin/main:

  • A dataset tile's or cell's number is computed under three filters at once: the dataset's own filter, the measure's own filter, and the widget's runtime filter (packages/services/service-analytics/src/dataset-executor.ts: combineFilters(compiled.filter, selection.runtimeFilter), and splitMeasuresByFilter runs one supplementary query per filtered measure).
  • The client sees only the last one. The dataset answer's drill sidecars (dimensionFields, drillRawRows, drillRanges in packages/spec/src/api/analytics.zod.ts, ADR-0021 D2) describe the bucket, not the dataset or measure filter; the DataSource interface has no read of a dataset definition, and ui/packages/plugin-dashboard has no metadata client.
  • So every drill surface builds its list filter from the bucket plus the widget filter only (the report drill in ui/packages/plugin-report/src/DatasetReportRenderer.tsx, the dashboard chart and table drill openDrill in ui/packages/plugin-dashboard/src/DatasetWidget.tsx). A drill on a filtered measure lists more records than the number says.

reach: named real producer: examples/app-showcase/src/ui/datasets/revenue-pulse.dataset.ts:32 declares paid_count (aggregate: 'count', filter: { status: 'paid' }), drawn by revenue-pulse.dashboard.ts:76 (table_rate_by_status). A filtered measure is the spec's own conditional-count spelling, so any author can write it today.

Wanted

  • The dataset answer carries a per-measure drill scope beside the existing drill sidecars: for each measure, the list filter that selects exactly the records it counts (the dataset's filter ∧ the measure's filter), in the same vocabulary drillRanges uses. When it cannot be expressed as a list filter (a derived measure, a relation path the list cannot filter on), the scope is absent for that measure.
  • Drill surfaces compose bucket ∧ widget filter ∧ that scope, and offer no drill for a measure whose scope is absent — the rule the maintainer ruled for tiles on [Decision] Dashboard numbers cannot be opened: should a tile lead to the records behind it? #30080: "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."

Governing text: ADR-0021 D2 (the server answers the drill bounds; the client never restates dataset semantics); the ruling on #30080 (comment 6631202970). Precedent for the shape: drillRanges (#20700) and the timeDimensions drill bounds (#27074) — the server sends what the drill needs.

Who acts, and in what order

  1. The spec seat: the answer field in packages/spec/src/api/analytics.zod.ts (Clause-②: yes (widening), a published API answer; contract review at its tier).
  2. The server seat: service-analytics serves it (and the data-objectstack passthrough if it filters sidecars).
  3. ui: [Decision] Dashboard numbers cannot be opened: should a tile lead to the records behind it? #30080 consumes it for the tile click. The switch of the existing report / chart / table drills to the scope is a ui card the landing seat files when this lands.

Business reason: a manager clicks "Paid Invoices: 12" and must see those 12 invoices, not every invoice of the period; a drill that does not match the number teaches people to stop trusting the dashboard.

Dedupe: drill measure filter, drill scope dataset filter, drillRanges, measure filter drill-through (is:issue, open and closed): 67 / 51 / 10 / 12 hits, none open on this gap (closest closed: #27074, #20700, #20644).


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p1 · domain:spec · 2026-11-05T15:01Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. added
    bugSomething isn't working
    domain:specDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    priority:p1High: required for production / M2
    pm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-set
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 5, 2026
  3. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

    Claim: PM loop round 1 · 2026-11-05T15:43Z
    Session: session_01L6bhgVugtCCwBs98FeR27P (seat); the dev is a card thread the project's coordinator opens
    Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: the card thread's designated claude/project-thread-* branch, named in a claim amendment once the thread reports it
    Worktree: objectstack-issue-30526
    Domain: domain:spec
    Seat: domain:spec#2
    File surface: packages/spec/src/api/analytics.zod.ts (a per-measure drill scope beside dimensionFields / drillRawRows / drillRanges), packages/spec/src/contracts/analytics-service.ts if the contract carries it, packages/services/service-analytics/src/** (the dataset executor and analytics service serve it), ui/packages/data-objectstack/src/index.ts only if its dataset-answer mapping drops sidecars (the analytics mapping only; ⛔ the upload adapter, #25921 domain:ui#8), their tests, the regenerated spec artifacts and liveness rows, one changeset. ⛔ The drill surfaces that consume it (ui/packages/plugin-report/**, ui/packages/plugin-dashboard/**: #30080 and a ui card the landing seat files), skills/**, docs/adr/**, .claude/**, AGENTS.md (stop on breach; explain in the report)
    Container & model: M, mode:thread (p1; build-heavy: packages/spec/src/** plus another package), model: default (no path-derived mandate; a published API answer widened ⇒ the seat runs a CONTRACT_REVIEW_TIER review before the queue), effort: xhigh (p1)
    Clause-②: yes (widening)
    Metadata-protocol: none
    Responsibility: packages/services/service-analytics computes each number under the dataset filter ∧ the measure filter ∧ the runtime filter but answers drill sidecars for the bucket only, and packages/spec declares no slot for the rest | no platform path: the client has no read of a dataset definition (ADR-0021 D2: the server answers the drill bounds) | every author of a filtered measure (named producer: examples/app-showcase/src/ui/datasets/revenue-pulse.dataset.ts:32 paid_count, drawn by revenue-pulse.dashboard.ts:76), every reader who drills it
    Thread-read: 6661303021
    Premise: holds, read at origin/main 86ae2cac: analytics.zod.ts declares dimensionFields (L214), drillRawRows (L222), drillRanges (L235) and no per-measure scope; dataset-executor.ts L1282 combineFilters(compiled.filter, selection.runtimeFilter) and L1480 combineFilters(baseFilter, compiled.measureFilters[m]). Maintainer ruling on #30080 (6631202970): 「其他全部同意」, option A: "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."
    Serial constraints cleared: no open pm:dispatched claim names analytics.zod.ts, contracts/analytics-service.ts, service-analytics or the data-objectstack analytics mapping (read of every open claim's File surface:); #25921 (domain:ui#8) holds the data-objectstack upload adapter, another region; #30080 (domain:ui, pm:blocked) waits on this card

  4. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

    席位核对 · Amendment · 2026-11-05T15:45Z · claim 6662301380 branch named

    Branch: claude/project-thread-mg9ioh

    The card thread's harness-designated branch (a project-thread platform exception to the claude/issue-<n>-<slug> convention); the dev thread is session session_01L6bhgXexAhb1ogpnymn5Jb. Everything else in claim 6662301380 stands.


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

    席位核对 · Amendment · 2026-11-05T15:45Z · claim 6662301380 branch named

    Branch: claude/project-thread-mg9ioh

    The card thread's harness-designated branch (a project-thread platform exception to the claude/issue-<n>-<slug> convention); the dev thread is session session_01L6bhgXexAhb1ogpnymn5Jb. Everything else in claim 6662301380 stands.


    Generated by Claude Code

  6. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-sales · session_01L6bhgXexAhb1ogpnymn5Jb
    os-dev-report
    Stamp: 2026-11-05T17:13Z

    {
      "issue": 30526,
      "status": "done",
      "branch": "claude/project-thread-mg9ioh",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30596",
      "head": "ba62840b",
      "session": "session_01L6bhgXexAhb1ogpnymn5Jb",
      "premise_still_valid": true,
      "summary": "Premise held on origin/main: the number is computed under dataset filter AND measure filter AND runtimeFilter, the answer's drill sidecars bound only the bucket, the client has no other read of the dataset definition, and data-objectstack's queryDataset hand-picks its return keys, so an unlisted sidecar was dropped. One vertical PR: the spec slot AnalyticsResult.drillScopes (measure name to FilterCondition, also on AnalyticsResultResponse), served by service-analytics queryDataset on both faces from the same token-resolved filters the numbers used (resolveSelectionTokens now runs once in answerDataset), and passed through by data-objectstack. A scope is dataset filter AND measure filter AND the aggregated column not null (both instants for a duration measure), {} when nothing applies, and absent (no drill) for a derived measure or a scope naming a relationship path. Consumers (#30080 and the ui card the seat files) are not touched; the PR lists what each must compose: bucket AND widget filter AND drillScopes[measure].",
      "tests": "New packages/services/service-analytics/src/__tests__/dataset-drill-scopes.test.ts: 20 passed (20), unit tier on the revenue-pulse shape (paid_count, paid_rate) plus a real ObjectQL + SqlDriver SQLite tier on both faces with and without a widget filter, where the composed drill lists exactly as many records as each count and the sum adds up; CONTROL: the bucket alone lists 4 against paid_count 2; contract-vs-wire: the service answer through JSON parsed by AnalyticsResultResponseSchema keeps every scope. Spec pins in api/analytics.test.ts and contracts/analytics-service.test.ts (52 passed together); data-objectstack queryDataset.test.ts pins plus a compile-time identity pin. Ablation via scripts/ablation-replace.mjs: result.drillScopes = measureDrillScopes({ became void measureDrillScopes({, anchor 1 to 0, blob bca6a0582f42 to 98dc47836851: 17 failed, 3 passed (20); the 3 green are the two direct walk tests and the record-list CONTROL (engine.find refuses a dotted key), none of which goes through the service; restored, blob == HEAD, git diff HEAD empty. Suites at ba62840b (and identically at 25fc5696 before the main merge), each VERDICT command-exit 0: spec Test Files 815 passed (815), Tests 25648 passed, 1 todo, typecheck exit 0; service-analytics 211 passed (211), 4996 passed, 300 skipped, typecheck exit 0; data-objectstack 115 passed (115), 1319 passed, type-check exit 0; consumers from git grep drillRanges: ui core 233 passed (233), plugin-dashboard 202 passed (202), plugin-report 33 passed (33).",
      "gates": "spec build && check:generated: 14 of 14 up to date after gen:docs (the one artifact proved stale; content/docs/references/api/analytics.mdx committed). check:test-typecheck needs NODE_OPTIONS=--max-old-space-size=8192 (at its 4096 default, tsc dies without diagnostics, an OOM, not a verdict). dispatch-gates --commands at ba62840b: 110 commands, each run with its exit code recorded before any pipe; first pass 108 exit 0 and 2 exit 3 (check:skill-examples, check:dual-build-cjs-loads: missing dists of 7 packages), rerun after turbo build of those 7: exit 0. --ran: 110 derived, 110 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. eslint on the 10 changed source files: 0 errors (data-objectstack +1 and +3 no-explicit-any warnings in the files' existing cast pattern). CI on #30596: in_progress at report time.",
      "dogfood_battery": "n/a — no ! in the PR title and Clause-② is (widening) only",
      "mcp_calls": "reads only, 0 writes: mcp__github__issue_read on #30526 (card body and comments) and get_comments on #30430 (an os-dev-report precedent), mcp__github__pull_request_read x2 (#30484 as a body precedent, #30596 read-back). Not GitHub: subscribe_pr_activity on #30596 (platform default).",
      "api_writes": "2 relay writes: pr_create via scripts/pm/fleet-write/dispatch.mjs (repository dispatch, run 42072224591, read-back 14770 bytes sent, 14770 stored, identical, no assignees); this os-dev-report via scripts/pm/post-stamped.mjs (issue comment on #30526). Plus git push of the harness branch.",
      "open_questions": [
        {
          "question": "The spec build admits one new dropped-refinement site, api/AnalyticsResultResponse data.drillScopes.valueType.lazy (header totals 251 to 252 and 1019 to 1020). Accept it?",
          "options": [
            "A accept: it is the same FilterConditionSchema lazy refinement the ledger already admits at api/AnalyticsQueryRequest where.lazy, and the build prints the entry",
            "B teach refinement-projection's closed list the FilterCondition lazy pattern first: a public-contract decision of its own, outside this card"
          ],
          "recommendation": "A, because the slot reuses the one filter vocabulary both the dataset and the record list speak, and B would change the projection for every FilterCondition slot at once"
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: c · reach: service only, HTTP door not measured — AnalyticsService.queryDataset on ObjectQL + SqlDriver (SQLite) · evidence: a dataset measure whose filter is the nested-relation form { account: { tier: 'gold' } } fails the whole answer with the engine's refusal at aggregations[0].filter.account (the nested-relation form, which the engine serves in where only), while DatasetSchema accepts that measure; repro: the real-engine rig in dataset-drill-scopes.test.ts with the nested-relation DATASET filter moved onto a measure · dedupe words: nested-relation measure filter, aggregation filter where only, dataset measure relation filter",
        "carrier: none · noted, not filed — data-objectstack queryDataset also drops drillRawTotals (hand-picked return keys); zero ui readers today (git grep drillRawTotals -- ui/: 0 hits)"
      ],
      "deviations": [
        "PR body attribution link names this card thread (cmsg_01D5bWFY3rFzgJcytD5gD4Uu6W6tm24ezCZ9jmVrXt12YA), not the seat thread the brief named: the platform writes the session's own thread link.",
        "PR body correction for the seat (dev writes the body once): in Verification, ablation block, the sentence 'Every service pin went red on both tiers, the two CONTROL pins and the contract-vs-wire pin included. The 3 that stayed green call namesRelationshipPath / measureDrillScopes directly and never go through the service.' should read 'Every service pin went red on both tiers, the bucket CONTROL and the contract-vs-wire pin included. The 3 that stayed green are the two direct walk tests and the record-list CONTROL (engine.find refuses a dotted key), none of which goes through the service.'"
      ]
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

    ACCEPT → PR #30596 comment 6664731315 · head ba62840b7d742525f60c41a225359ea3887b3a72 · contract review PASS 6664662638 · 2026-11-05T17:31Z

  8. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

    Landed · PR #30596 · merge 55d9dd448f00d1498ba3ae830368306aa0dd8d8a · 2026-11-05T18:05Z

    Reading Probe Answer
    Queue membership GET /repos/objectstack-ai/objectstack/issues/30596/timeline added_to_merge_queue 17:38:24Z → merged + removed_from_merge_queue 18:01:48Z
    origin/main by content git grep -c drillScopes origin/main -- packages/spec/src/api/analytics.zod.ts packages/services/service-analytics/src/drill-scope.ts ui/packages/data-objectstack/src/index.ts 1 · 1 · 5

    State: the card is closed completed by Fixes #30526, and pm:dispatched is removed in this stroke. The consumer half is filed as #30615 (report, chart and table drills compose bucket ∧ widget filter ∧ drillScopes[measure]). #30080 (the dashboard tile click) goes back to pm:queue from its WIP. The dev's out-of-scope class c finding (a nested-relation measure filter is accepted by DatasetMeasureSchema but refused by the engine in an aggregation) has reach measured at the service only; the seat measures the HTTP door before filing it.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:specDomain lane — seat card indexed by label:pm:seatpm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-setpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions