Skip to content

Commit 834da6f

Browse files
baozhoutaoclaude
andauthored
fix(lint): resolve dotted dashboard filter fields on the object graph and drop the SYSTEM_FIELDS union (#14437)
* wip(lint): migrate dashboard-filter-field-unknown onto the object-graph seam (#14275) * fix(lint): resolve dotted dashboard filter fields on the object graph and drop the SYSTEM_FIELDS union (#14275) * fix(lint): keep the tracker id out of the dashboard-filter finding messages (#14275) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 63f3b43 commit 834da6f

5 files changed

Lines changed: 545 additions & 69 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
lint: `dashboard-filter-field-unknown` resolves dotted dashboard-filter fields on the object graph, and answers system columns per object
6+
7+
A dashboard-level filter (`dateRange`, or a `globalFilters[]` entry) is ANDed into
8+
**every** widget's analytics query, so its effective field — after any
9+
`filterBindings` re-target — has to resolve on each bound widget's dataset object.
10+
The rule that enforces that shipped with two holes, and this closes both by
11+
migrating the check onto the shared `resolveFieldPath` / `joinablePrefixes` seam
12+
the widget's own `filter` keys already use one position over.
13+
14+
- **Dotted paths are no longer skipped.** The branch carried
15+
`if (field.includes('.')) continue;`, accurate when nothing in the package could
16+
walk relationship hops and false since the object-graph seam landed. A filter
17+
re-targeted to `account.signed_at` was unjudged whether or not `account` existed,
18+
whether or not `signed_at` existed on it, and whether or not `account` was
19+
declared in the dataset's `include`. It is now walked hop by hop, and a miss
20+
names **which** hop failed.
21+
- **System columns are resolved per object, not through the flat union.** The old
22+
test was `objectFields.has(field) || SYSTEM_FIELDS.has(field)`, which answers
23+
"could this be a system column *anywhere*". On an `ownership: 'none'` object the
24+
platform injects no `owner_id`, and on `systemFields: { audit: false }` no
25+
`created_at` — both were answered as resolvable and are now reported.
26+
27+
New error id **`dashboard-filter-field-not-included`**: the effective field
28+
resolves, but its relationship prefix is not declared in the bound dataset's
29+
`include`, so ADR-0021 compiles no join and the column is out of the broadcast
30+
query's reach. It mirrors `widget-filter-field-not-included` one level down, and is
31+
its own id because the fix is a different edit (declare the join, versus point the
32+
filter at something real).
33+
34+
This **narrows the accept set of a shipped gating rule**. Both new answers are
35+
error-tier, so like the rule's other errors they fail `os validate` / `os build`
36+
and the runtime publish gate for `dashboard` writes. Measured over the shipped
37+
dashboard corpus — the three example apps plus the platform's own
38+
`system_overview` — the change is 0 findings before and 0 after; the
39+
`dashboard-filter-field-unprovisioned` warning is unchanged and now travels with
40+
the verdict, so it answers a dotted path landing on an ADR-0015 `external` object
41+
too.

packages/lint/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export {
2424
WIDGET_LEGACY_ANALYTICS_UNRENDERABLE,
2525
DASHBOARD_FILTER_FIELD_UNKNOWN,
2626
DASHBOARD_FILTER_FIELD_UNPROVISIONED,
27+
// [#14275] The `include` clause on a DASHBOARD-level filter's effective
28+
// field, now that it is resolved on the object graph rather than skipped
29+
// whenever it was dotted.
30+
DASHBOARD_FILTER_FIELD_NOT_INCLUDED,
2731
// [#14148] The widget's OWN two references, at the same site: the keys of its
2832
// presentation-scope `filter` (resolved on the #14105 object-graph seam, with
2933
// the ADR-0021 `include` clause its `runtimeFilter` really is subject to) and

packages/lint/src/system-fields-consumers.test.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -451,12 +451,15 @@ const LEDGER: Record<string, LedgerRow> = {
451451
'prevent. The blank-column consequence belongs to the surface that RENDERS the anchor ' +
452452
'(validate-page-field-bindings, #8340), not to the bundle that names it.',
453453
},
454-
'validate-widget-bindings.ts': {
455-
kind: 'rule',
456-
reach: ['direct'],
457-
asksProvenance: true,
458-
why: 'Blanket .has read site. Provenance wired by #8340.',
459-
},
454+
// [#14275] `validate-widget-bindings.ts` LEFT the population. Its one blanket
455+
// `.has` read site was the `dashboard-filter-field-unknown` branch, which now
456+
// resolves through `resolveFieldPath` and therefore through the PER-OBJECT
457+
// `injectedColumnsFor` instead — the distinction this ledger's own header
458+
// calls "the two differ exactly where it matters". Its #8116 obligation did
459+
// not lapse with the row: the rule still asks the provenance question, now
460+
// gated by the verdict's `injected` marker. Recorded here rather than by a
461+
// silent deletion because a shrinking population is the one direction this
462+
// census cannot distinguish from a broken analyzer.
460463
};
461464

462465
// ── Fixtures: a module set whose right answer is known ────────────────────

0 commit comments

Comments
 (0)