Repository navigation
showcase authoring gap: the ops dashboard's global filter is field:'status' with *task* statuses, and the project widgets carry no filterBindings — so it also lands on showcase_project.status and zeroes those tiles #7568
Description
Activity
Findings triage: resolved the co-applied
pm:queue+findingdual state →pm:queue(keptdomain:metadataas filed). Concrete authoring defect with a named file and fix shape (ops-dashboard.dashboard.ts: addfilterBindingsto the fourprojectDswidgets or scope the filter), user-visible on every showcase boot (zeroed tiles under any filter selection), reproduced and verified "still present onorigin/main" by the filer today — queue-class, not a held observation.- Routing note:
examples/**follows the subsystem it exercises; the exercised surface is dashboard global-filter metadata authoring, so the fileddomain:metadatais respected. - Dup check: no open card names the ops dashboard's
globalFiltersblock; the correct per-widget mapping precedent (region/sales_region) is recorded in run QA run · dashboards (FULL area) · a86db175 · 2026-08-11 · 6 PASS / 2 PARTIAL / 2 FAIL #7515 as passing, not carded.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- Routing note:
Claimed —
domain:metadataseat.- Session:
session_0127HHmCr5vd3NudQmW6QiNN - Branch:
claude/issue-7568-ops-dashboard-filter-bindings - Worktree:
../objectstack-7568(dedicated) - File surface:
examples/app-showcase/src/ui/dashboards/ops-dashboard.dashboard.ts— theglobalFiltersblock and the fourprojectDswidgets.
The dispatch carries the card's own framing verbatim, because it is the part most likely to be discarded under momentum: this is a showcase AUTHORING gap, not a platform defect. The global-filter engine does exactly what the metadata tells it — a widget without
filterBindingsinherits the dashboard filter on its own object's like-named field. ⛔ The dev does not go hunting in the filter engine and does not "fix" this inpackages/. If they become convinced the platform behaviour is itself wrong, they stop and report: that is a different card with a far larger blast radius, not something to fold in.There is a proven-correct precedent in the same QA run, and the dev is pointed at it rather than left to invent a shape —
regionon the invoice widgets versussales_regionon the account widgets, measured correct both on the wire and in the echoed SQL.Two admissible fixes, one to be chosen with the reason in the PR body: give the four project-bound widgets
filterBindingsthat map or opt out, or scope the filter to the task widgets. ⛔ Not both.Verification has to be by driving it, not by reading it. This card was produced by booting the showcase and watching four tiles drop to 0 under a filter selection, so the dev captures that baseline first — the four tiles before the filter and under a selection — and shows the same capture after the fix, with the task widgets still filtering correctly. A test asserting only that a
filterBindingskey is now present proves the key is present, not that the tiles stopped zeroing; if no seam exists to pin the behaviour, the report says so plainly rather than implying coverage that is not there.If the fix turns out to need a
packages/specschema change, the dev stops and reports — this is meant to be example-app authoring.
Generated by Claude Code
- Session:
{ "issue": 7568, "status": "done", "branch": "claude/issue-7568-ops-dashboard-filter-bindings", "pr": "https://github.com/objectstack-ai/objectstack/pull/7612", "premise_still_valid": true, "summary": "Showcase authoring fix only — nothing under packages/ was touched and no spec change was needed. The Delivery Operations global filter is now named `task_status` (it carries the showcase_task vocabulary), and every project-bound widget declares `filterBindings: { task_status: false }`. Chose option A (per-widget filterBindings) over option B (scope: 'widget' + targetWidgets) because filterBindings is the mechanism measured working in the same QA run (Revenue Pulse: region vs sales_region), is what the Studio widget inspector authors (objectui#2586), and is the only one drivable here — packages/console/dist is absent, so `scope: 'widget'` could only have been asserted, not measured; packages/lint also documents targetWidgets as the legacy allow-list filterBindings overrides. The two status vocabularies are disjoint (task: backlog/todo/in_progress/in_review/done; project: planned/active/on_hold/completed/cancelled), so there is no project field to re-target onto and the honest binding is an opt-out. TWO DEVIATIONS FROM THE CARD, both deliberate: (1) there are FIVE project-bound widgets, not four — `table_spend` has the identical defect and is fixed with the other four; it escaped the card because an emptied table reads as 'no data' rather than as a broken filter. (2) The filter was renamed from a bare `status` to `task_status` so an opt-out reads as 'this control is about tasks' instead of 'ignore project status'; nothing consumed `page.status`, checked repo-wide. dateRange stays inherited on every widget on purpose — projects do carry created_at.", "tests": "DRIVEN, not read. Booted the showcase backend (`pnpm dev -- --fresh`, sqlite, 130 seeded rows), read published metadata from GET /api/v1/meta/dashboard/showcase_ops_dashboard, resolved each dashboard filter per widget, and issued one POST /api/v1/analytics/dataset/query per widget — the same path the renderer takes. dateRange held unset in every capture so the status selection was the only variable. BEFORE, pristine -> status=in_review: kpi_active_projects 2->0, kpi_at_risk 1->0, kpi_total_budget 1090000->0, col_health 5->0, table_spend 5->0; task widgets 10->2 correctly. Echoed SQL showed the cause: `WHERE (status = $1 AND status = $2)` on showcase_project. AFTER, pristine -> task_status=in_review: 2->2, 1->1, 1090000->1090000, 5->5, 5->5 (project SQL carries no status predicate at all); task widgets still 10->2, and kpi_awaiting_review still composes both filters (`WHERE (status = $1 AND status = $2)` on showcase_task) so the QA checklist's Delivery-Operations composition clause still holds. SCOPE LIMIT, stated rather than implied: the per-widget resolution in the harness is transcribed from effectiveFilterField/dashboardFilterDefs in packages/lint/src/validate-widget-bindings.ts — this repo's own executable mirror of objectui resolveBoundField. Everything downstream (metadata -> wire -> SQL -> number) is the real server; the objectui browser bundle is not vendored in this repo (packages/console/dist absent) and was NOT exercised — that half stays a checklist item (dashboards.global-filters-rescope). PIN: new examples/app-showcase/test/dashboard-filter-vocabulary.test.ts, generic over every showcase dashboard, asserts (1) every value a global filter offers is a value its effective field can hold on each widget it reaches, (2) every filter still reaches at least one widget (so 'opt everybody out' is not a legal repair), (3) task_status governs the task widgets and no project widget. REVERSE VERIFICATION, direction predicted before running: revert ops-dashboard.dashboard.ts to main -> (1) and (3) red, (2) GREEN (the broken filter did reach widgets, it just could not answer them). Measured exactly that; (1) named all five widgets individually including table_spend. Restored -> all green. GATES: `pnpm --filter @objectstack/example-showcase verify` (os validate + tsc --noEmit + vitest run) green, 18 files / 171 tests passed — the validate step is what runs validate-widget-bindings, the rule family adjacent to this card; `eslint --no-inline-config` on both changed files clean; `node scripts/check-nul-bytes.mjs` OK (7046 files). CI at report time: in_progress — reporting at draft-PR time per the #6644 L2 contract, CI convergence is the PM's read. No changeset: @objectstack/example-showcase is private:true and absent from the `fixed` group in .changeset/config.json, so the PR releases nothing; `skip-changeset` applied by me as a union write and read back settled as ['size/m', 'skip-changeset'].", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
ACCEPT — PR #7612, marked ready and armed into the merge queue. 27/27 green.
The option choice is the best thing in this run, and it was made on the right criterion. Both fixes were admissible; A (
filterBindings) was chosen partly because B could not be driven, only asserted —packages/console/distis absent, soscope: 'widget'had no measurable path here. Choosing the option you can verify over the one you can only claim is Prime Directive #10 turned on the work itself, and it is the kind of reasoning that usually goes unwritten. The supporting reasons hold too:filterBindingsis what the same QA run measured working (regionvssales_region, on the wire and in the echoed SQL), it is what the Studio inspector authors, and with disjoint vocabularies there is no project field to re-target onto, so an opt-out is the honest binding rather than a shortcut.Two disclosed deviations from the card, both correct:
- There are five project-bound widgets, not four.
table_spendcarries the identical defect and is fixed with the others. The reason it escaped the card is worth keeping: an emptied table reads as "no data", while a zeroed KPI tile at least looks wrong. That is a general lesson about which surfaces make a defect visible, not a detail about this dashboard. - Renaming the filter
status→task_status, having checked repo-wide that nothing consumedpage.status. This is scope-adjacent and I'm accepting it deliberately: with a barestatus, an opt-out reads as "ignore project status"; with the name, the metadata says what the control governs and every widget it does not govern says so on its own line. In a showcase — whose job is to teach the shape — that distinction is the deliverable, not decoration.
Verified independently of the report:
- Path surface, read from git against a freshly-fetched
main(my first read was against a stale ref and was contaminated with commits that had landed meanwhile — worth stating since it nearly produced a wrong review): exactly two files,examples/app-showcase/src/ui/dashboards/ops-dashboard.dashboard.tsand the new test. Nothing underpackages/, which is the claim that mattered most on a card explicitly framed as authoring-not-platform. - Gate jobs' own conclusions: ESLint
success, TypeScript Type Checksuccess— not the aggregate. Fixes #7568as the first line; nodocs/adr/**, nocontent/docs/releases/.skip-changesetis the right call and correctly applied:@objectstack/example-showcaseisprivate: trueand absent from thefixedgroup, so the PR releases nothing — the label, not an empty changeset, which would otherwise sit in the release forever.
The verification is the standard this card asked for and it was met. It was driven against a live backend rather than read: metadata from
GET /meta/dashboard/..., onePOST /analytics/dataset/queryper widget,dateRangeheld unset in every capture so the status selection was the only variable — a real control, not just a before/after. The echoed SQL closes it:WHERE (status = $1 AND status = $2)onshowcase_projectbefore, nostatuspredicate at all after, whilekpi_awaiting_reviewstill composes both filters on the task side, so the checklist's Delivery Operations composition clause still holds where it is coherent.The pin's second property is the one I'd single out: a filter must still reach at least one widget. Without it, the legal repair for property (1) is "opt everybody out", which passes the test and leaves an inert control on the header bar. Writing down the degenerate solution and forbidding it is what separates a pin from a snapshot.
Scope limit accepted as stated, not glossed: the per-widget resolution in the harness is transcribed from
packages/lint/src/validate-widget-bindings.ts— this repo's own executable mirror of objectui'sresolveBoundField— and everything downstream of it is the real server. The browser bundle is not vendored here and was not exercised; the objectui half stays the checklist itemdashboards.global-filters-rescope. That is the honest boundary, and saying so is worth more than a claim of end-to-end coverage would have been.
Generated by Claude Code
- There are five project-bound widgets, not four.
Symptom
On the showcase ops dashboard, picking any value in the "Task Status" global filter zeroes the project-bound tiles (Active Projects, At-Risk (Red), Total Budget, Projects by Health) as well as filtering the task widgets it was meant for.
examples/app-showcase/src/ui/dashboards/ops-dashboard.dashboard.tsdeclares one dashboard-scoped global filter:Those five options are the task status vocabulary. But the filter is declared by bare
fieldname atscope: 'dashboard', and the widgets bound to the project dataset (kpi_active_projects,kpi_at_risk,kpi_total_budget,col_health) declare nofilterBindings, so the samestatusfilter is also applied toshowcase_project.status— whose value domain is a different set entirely (active/ health-style values). No project row matchesin_review, so those tiles read 0.Expected authoring: either give the project-bound widgets
filterBindingsthat map (or opt out of) the dashboard filter, or scope the filter to the task widgets — the same per-widget field-mapping mechanism proven exact elsewhere in this run (regionon invoice widgets vssales_regionon account widgets, correct on the wire and in the echoed SQL).Still present on
origin/mainas of 2026-08-11.Root cause
Authoring, in the example app: a dashboard-scoped global filter declared by bare field name (
status) fans out to every widget that has not opted out viafilterBindings, and the showcase's project widgets never opted out. The platform behaviour is the documented one — a widget withoutfilterBindingsinherits the dashboard filter on its own object's like-named field.File:
examples/app-showcase/src/ui/dashboards/ops-dashboard.dashboard.ts(theglobalFiltersblock and the fourprojectDswidgets).Reproduction
in_review).showcase_project.status, whose domain does not contain task statuses.Source
Extracted from the QA run #7515 (framework a86db17).