Skip to content

Commit 1756666

Browse files
fix(lint): a widget filter keyed on a dataset dimension publishes, and a measure filter key is refused with its own message (#30006) (#30560)
domain:server#3 · os-bill · session_01Utp6CbPHd9hdK2yxiVhr84 <!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgStX19XXyG1z2iVcTwB91bW)_ Fixes #30006 Clause-②: yes (widening) Metadata-protocol: none Skills-docs: updated here — content/docs/deployment/validating-metadata.mdx ## 维护者速读 - 改了什么:仪表盘部件的 `filter` 现在可以直接写数据集维度名(如维度 `state` 对应字段 `status`),发布时不再误报"字段不存在";部件和报表两条规则改为共用同一套可接受键,写度量名的过滤键则给出专门的拒收提示。 - 为什么改:运行时数据集查询本来就按维度绑定这个键,发布检查却拒收,作者被迫改写底层字段名;度量名实测在运行时会被编成逐行比较原始列(sum 度量)、坏 SQL(count 度量)或 400(派生度量),所以继续拒收但讲清原因。 - 风险与回滚:只放宽(以前能发布的仍能发布),只动 `@objectstack/lint` 两条规则、说明文字和 `validating-metadata.mdx` 一句话;回滚即撤销本 PR。 - 先看:`packages/lint/src/validate-widget-bindings.ts`(`judgeDatasetFilterKeys` 的维度/度量分支)、`packages/lint/src/validate-report-runtime-filter.ts`、`packages/metadata-protocol/src/protocol.dashboard-dataset-publish-gate.test.ts` 的 #30006 块。 - 两分钟验证:`pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.dashboard-dataset-publish-gate.test.ts`,预期 19 个测试全绿,其中 #30006 块 4 个。 - 需要你做的:无,席位落地。 ## What changes `widget-filter-field-unknown` and `report-filter-field-unknown` now judge a bare filter key against one accept set, held in `judgeDatasetFilterKeys` (`packages/lint/src/validate-widget-bindings.ts`), so the two rules cannot drift: 1. a field of the dataset's object, authored or registry-injected (unchanged, and first: the dataset door binds a key that names a field to that field even where a member shares its name, `dataset-filter-field-binding.ts`); 2. **new for the widget rule:** a dimension of the dataset. The report rule already accepted this through its own local set, which is removed; 3. a bare key naming a **measure** of the dataset gets its own verdict (`kind: 'measure'`) and its own message in both rules, instead of the generic unknown-field verdict with a "Did you mean". The fix line names the measure's column when it aggregates exactly one (`key the filter by the field "amount"`), and points at fields and dimensions otherwise. A measure sharing a field's name is still the field, and accepted. The `os explain` entries for both rule ids state the accept set and why a measure is refused. Rule ids, severities and every previously accepted key are unchanged. `content/docs/deployment/validating-metadata.mdx`: the package-closure paragraph (added by #30231) said "a filter key the object lacks is refused there"; it now reads "a filter key that neither the object nor the dataset's dimensions name is refused there", which is what both rules answer after this change. ### Which outcome for measures, and the evidence Refusal with its own message. The card asked to measure whether a measure filter is a supported run-time shape. Measured on `main` at `fa9c0aa2` with a throwaway probe through `AnalyticsService.queryDataset` (dataset `orders_states` over `orders`: dimension `state` → field `status`; measures `amount_sum` = sum of `amount`, `order_count` = count, `avg_amt` = derived ratio), both strategies: | `runtimeFilter` | NativeSQL | ObjectQL (engine aggregate) | |:--|:--|:--| | `{ state: 'open' }` (dimension) | `WHERE status = $1` | filter `{ status: 'open' }` | | `{ amount_sum: { $gt: 100 } }` (sum measure) | `WHERE amount > $1`, a row-level filter on the raw column, not on the sum | filter `{ amount: { $gt: 100 } }` | | `{ order_count: { $gt: 5 } }` (count measure) | `WHERE * > $1`, broken SQL | filter on the key `*` | | `{ avg_amt: { $gt: 1 } }` (derived measure) | `400 INVALID_FIELD`, listing "Valid filter members: state" | `400 INVALID_FIELD` | | `{ nope: 1 }` (CONTROL) | `400 INVALID_FIELD` | `400 INVALID_FIELD` | So the door binds a dimension name the way the author means it, and binds a measure name to something other than the measure: the raw column for a sum or avg, no column for a count, nothing at all for a derived or duration measure. The door's own refusal lists only dimensions as valid filter members. Accepting measure keys would publish filters that silently filter rows when the author meant to filter on the total. A measure key was already refused by both rules (as an unknown field), so the new message refuses nothing that published before: no narrowing. ## Tests - `packages/lint/src/validate-widget-bindings.test.ts`, new block `widget-filter-field-unknown — dataset dimensions and measures (#30006)`: a dimension key is accepted (bare and under `$or` / `$not`); a sum measure key is refused at its path with the measure message and its column in the fix; a count and a derived measure are refused with the no-single-column message; a measure sharing a field's name is accepted; CONTROL: an unknown bare key and a dotted key are still refused as unknown. The existing hint pin now reads "a field …, a dimension of that dataset, or a relationship path …". - `packages/lint/src/validate-report-runtime-filter.test.ts`: a report `runtimeFilter` on a measure name is refused with the measure message; the twin-parity test now includes the dimension key `state` and the measure key `hours_sum`, and both rules answer them alike. - `packages/metadata-protocol/src/protocol.dashboard-dataset-publish-gate.test.ts`, new block `#30006`, through the real save and publish path (draft PUT, then publish): a widget filter on a dimension name publishes; a report `runtimeFilter` on a measure name is refused `422 INVALID_METADATA` at `reports[0].runtimeFilter.amount_sum` with the measure message, and nothing active is stored; the widget twin refuses the same measure key; CONTROL: an unknown key beside the dimension key is still refused as unknown, as the only issue. <details><summary>Test runs on the merged head (after merging main, which brought #30231); the docs-only commit b498e10 changes no test input</summary> ``` pnpm --filter @objectstack/lint typecheck exit 0 pnpm --filter @objectstack/lint test Test Files 195 passed (195) · Tests 8097 passed (8097) pnpm --filter @objectstack/metadata-protocol typecheck exit 0 pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.dashboard-dataset-publish-gate.test.ts Test Files 1 passed (1) · Tests 19 passed (19) pnpm --filter @objectstack/spec check:liveness exit 0 ``` </details> <details><summary>Ablation (one, on the dimension accept)</summary> Committed head `a8b06ca5`. `scripts/ablation-replace.mjs` replaced `if (bare && dimensions.has(field)) return;` with a guard that never fires, carrying the marker `ABLATION_30006`; `@objectstack/lint` was rebuilt and `ablation-dist-preflight.mjs` found the marker in 4 built files, because the door test reads `@objectstack/lint` from `dist/`. Result: 7 tests red, every one a dimension-key assertion. In lint: the new widget dimension test, the report rule's dimension test and both of its `validate` / `build` gate tests. At the door: the #29900 dimension CONTROL, the new widget dimension test, and the new CONTROL board, which carries the dimension key. Restore: blob equal to HEAD, `git diff HEAD` empty, lint rebuilt, preflight `--absent` found the marker in none of 20 built files, and the tree was clean. </details> ## Acceptance notes - `skills/**` teaches widget `filter` and report `runtimeFilter` keys that are fields of the dataset's object, which both rules accept unchanged, and no page teaches a measure name as a filter key; the one docs sentence the dimension accept widens is updated here (seat-widened file surface, that sentence only). - A dotted `cube.member` spelling (`orders_states.state`) is not given the dimension accept: the card is about bare keys, and dotted keys keep resolving as relationship paths. ## Verification Derived gates (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`) on head `b498e10f`: 87 families (the docs commit added the documentation families), each run and recorded as `COMMAND :: exit CODE`, all 87 exit 0; `--ran` reconciliation: 87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN. On the code head `a82a3a45` the same derivation was 58 families, all exit 0. The `os-dev-report` comment on #30006 carries the record. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (`packages/spec` untouched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` is CI's. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CbPHd9hdK2yxiVhr84)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 739c972 commit 1756666

8 files changed

Lines changed: 284 additions & 36 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
fix(lint): a widget filter keyed on a dataset dimension publishes, and a filter key naming a dataset measure is refused with its own message
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: none
9+
Skills-docs: updated here — content/docs/deployment/validating-metadata.mdx
10+
11+
`widget-filter-field-unknown` and `report-filter-field-unknown` now judge a bare filter key against one accept set, held in `judgeDatasetFilterKeys`, so the two rules cannot drift: a field of the dataset's object (authored or registry-injected), then a dimension of the dataset.
12+
13+
- **Widened:** a dashboard widget's own `filter` keyed on a dataset dimension's name (`{ state: 'open' }` for a dimension `state` over the field `status`) was refused at publish as an unknown field, although the dataset query binds it through the dimension's field. It now publishes, as the same key on a report's `runtimeFilter` already did.
14+
- **Refused with its own message:** a bare key naming a dataset measure was already refused by both rules as an unknown field. It is now refused as a measure: a filter compares each row before aggregation, so the dataset query would compare the measure's raw column (`{ amount_sum: { $gt: 100 } }` compiles to `amount > 100`, not a filter on the total), or no column at all for a count, derived or duration measure. The fix line names the measure's column when it reads exactly one. A measure that shares a field's name is still the field, and accepted.
15+
16+
No key that published before is refused now.

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -748,9 +748,10 @@ no package provenance (#9612); a provenance it cannot read narrows nothing at al
748748
A dashboard or report write also keeps the base object of each dataset it binds
749749
(a widget's `dataset`; a report's `dataset` and each joined block's), because the
750750
datasets are never narrowed and the dataset query resolves a filter against that
751-
object whichever package ships it: a filter key the object lacks is refused there
752-
just as it is on a dataset inside the closure (#30005). No other write type keeps
753-
it, so a flow or a list view never reaches an outside object through a dataset.
751+
object whichever package ships it: a filter key that neither the object nor the
752+
dataset's dimensions name is refused there just as it is on a dataset inside the
753+
closure (#30005). No other write type keeps it, so a flow or a list view never
754+
reaches an outside object through a dataset.
754755
That changes what a rule can **resolve**, never which rules run, and nothing at
755756
this door walks `packages[]`. Two rules are told the closure is in force. A flow
756757
whose trigger object is outside it is refused with `flow-trigger-unknown-object`

‎packages/lint/src/rule-explanations.ts‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -485,11 +485,16 @@ const WIDGET_FILTER_FIELD_UNKNOWN_EXPLANATION: RuleExplanation = {
485485
'renders successfully either way: nothing reports the miss.',
486486
'Each key is resolved on the object graph against the dataset\'s object — a dotted ' +
487487
'`relationship.field` path hop by hop, a bare name against the authored and registry-injected ' +
488-
'columns — and the verdict names the hop that failed. This rule judges the KEYS; ' +
489-
'`filter-token-unknown` judges the values in the same subtree. Not reported: an object this ' +
490-
'stack does not define, or one with no readable field map.',
491-
'To fix it, filter on a field of the dataset\'s object, or on a `relationship[.relationship].field` ' +
492-
'path: every hop must be a relationship field (lookup, master_detail, user or tree; ADR-0021 ' +
488+
'columns, then against the dataset\'s dimensions, which the dataset query binds through each ' +
489+
'dimension\'s field — and the verdict names the hop that failed. A bare key naming one of the ' +
490+
'dataset\'s measures is refused with its own message: a filter compares each row before ' +
491+
'aggregation, so the dataset query would compare the measure\'s raw column (or no column at ' +
492+
'all, for a count, derived or duration measure), never the aggregated value. ' +
493+
'This rule judges the KEYS; `filter-token-unknown` judges the values in the same subtree. ' +
494+
'Not reported: an object this stack does not define, or one with no readable field map.',
495+
'To fix it, filter on a field of the dataset\'s object, on a dimension of the dataset, or on a ' +
496+
'`relationship[.relationship].field` path: every hop must be a relationship field ' +
497+
'(lookup, master_detail, user or tree; ADR-0021 ' +
493498
'derives every join from the object graph), and the prefix must be declared in the dataset\'s ' +
494499
'`include` (`widget-filter-field-not-included` otherwise). The verdict\'s "Did you mean" names ' +
495500
'the nearest field of the object the miss landed on.',
@@ -5570,8 +5575,9 @@ const REPORT_FILTER_FIELD_UNKNOWN_EXPLANATION: RuleExplanation = {
55705575
'own.',
55715576
'Each key is judged as `widget-filter-field-unknown` judges a widget filter key — a dotted ' +
55725577
'`relationship.field` path hop by hop, a bare name against the authored and registry-injected ' +
5573-
'columns — and one more name is accepted: a bare key naming a dimension of the bound dataset, ' +
5574-
'which the dataset query binds through that dimension. Not reported: a `dataset` that resolves to ' +
5578+
'columns, then against the dimensions of the bound dataset, which the dataset query binds ' +
5579+
'through each dimension\'s field; a bare key naming a measure of the dataset is refused with its ' +
5580+
'own message, as the twin refuses it. Not reported: a `dataset` that resolves to ' +
55755581
'nothing (`chart-dataset-unknown` reports that once), an object this stack does not define, or ' +
55765582
'one with no readable field map.',
55775583
'To fix it, filter on a field of the dataset\'s object, on a dimension of the dataset, or on a ' +

‎packages/lint/src/validate-report-runtime-filter.test.ts‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,23 @@ describe('report-filter-field-unknown (#29900)', () => {
123123
expect(run(s)).toEqual([]);
124124
});
125125

126+
it('refuses a bare key naming a measure, with its own message (#30006)', () => {
127+
const findings = run(stack({ runtimeFilter: { hours_sum: { $gt: 10 } } }));
128+
expect(findings).toHaveLength(1);
129+
const [f] = findings;
130+
expect(f.rule).toBe(REPORT_FILTER_FIELD_UNKNOWN);
131+
expect(f.path).toBe('reports[0].runtimeFilter.hours_sum');
132+
expect(f.message).toContain('names a measure of dataset "orch_ds2"');
133+
expect(f.hint).toContain('key the filter by the field "hours"');
134+
});
135+
126136
it('accepts what the dashboard twin accepts on the same dataset, and refuses what it refuses', () => {
127-
// The twin judges the field half; a dimension-name key is the report's one
128-
// addition and is excluded here.
129-
const keys = ['status', 'created_at', 'owner.region', 'no_such_field', 'owner.regionn', 'hours.total'];
137+
// [#30006] One accept set: the dimension key and the measure key are in the
138+
// list, and the two rules answer them alike.
139+
const keys = [
140+
'status', 'created_at', 'owner.region', 'state',
141+
'no_such_field', 'owner.regionn', 'hours.total', 'hours_sum',
142+
];
130143
for (const key of keys) {
131144
const s = stack({ runtimeFilter: { [key]: 1 } }, {
132145
dashboards: [{ name: 'd', widgets: [{ id: 'w', type: 'metric', dataset: 'orch_ds2', values: ['hours_sum'], filter: { [key]: 1 } }] }],

‎packages/lint/src/validate-report-runtime-filter.ts‎

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,21 @@
1717
*
1818
* - `report-filter-field-unknown` (`error`) — a key of the report's
1919
* `runtimeFilter` (or of a joined block's) resolves to no column on the bound
20-
* dataset's object graph, and names no dimension of that dataset.
20+
* dataset's object graph, and names no dimension of that dataset — or it names
21+
* one of the dataset's MEASURES, which no filter can key (#30006).
2122
*
2223
* The judgement is {@link judgeDatasetFilterKeys}, the function the dashboard
2324
* twin calls, so the two positions accept the same keys: every authored filter
2425
* shape (`$and` / `$or` / `$not`, the `{ field, operator }` rule, the
2526
* `[field, op, value]` triple), dotted relationship paths, and the three skips
2627
* every field-existence rule in this package takes (an object this stack does
2728
* not define, one with no readable field map, a registry-injected system
28-
* column). One addition, never a refusal the twin does not make: a bare key
29-
* that names a DIMENSION of the bound dataset is accepted, because the dataset
30-
* door resolves a bare `where` key through the cube's members first
31-
* (`dataset-filter-field-binding.ts`, «a key naming only a dimension still
32-
* resolves through that dimension»).
29+
* column). A bare key that names a DIMENSION of the bound dataset is
30+
* accepted, because the dataset door resolves a bare `where` key through the
31+
* cube's members first (`dataset-filter-field-binding.ts`, «a key naming only a
32+
* dimension still resolves through that dimension»); a bare key that names a
33+
* MEASURE is refused with its own message. Both live in the shared judgement
34+
* since #30006, so the dashboard twin accepts and refuses the same keys.
3335
*
3436
* ## Which dataset a filter is judged against
3537
*
@@ -51,7 +53,7 @@
5153

5254
import { indexServedObjectGraph, recordsOf } from './object-graph.js';
5355
import type { AnyRec } from './page-walk.js';
54-
import { indexResolvableDatasets, judgeDatasetFilterKeys } from './validate-widget-bindings.js';
56+
import { indexResolvableDatasets, judgeDatasetFilterKeys, measureFilterKeyFix } from './validate-widget-bindings.js';
5557

5658
export const REPORT_FILTER_FIELD_UNKNOWN = 'report-filter-field-unknown';
5759

@@ -91,23 +93,18 @@ export function validateReportRuntimeFilters(stack: AnyRec): ReportRuntimeFilter
9193
const dataset = dsName ? datasets.get(dsName) : undefined;
9294
// Unbound or unresolvable: the shape rule and `chart-dataset-unknown` own it.
9395
if (!dsName || !dataset) return;
94-
const dimensions = new Set<string>();
95-
for (const d of recordsOf(dataset.dimensions)) {
96-
const n = strName(d.name);
97-
if (n) dimensions.add(n);
98-
}
9996
for (const verdict of judgeDatasetFilterKeys(graph, dataset, filter, path)) {
100-
if (verdict.kind !== 'unknown') continue;
101-
if (dimensions.has(verdict.field)) continue; // the cube binds a dimension name
97+
if (verdict.kind === 'not-included') continue; // the twin's second id, not this rule's
10298
findings.push({
10399
severity: 'error',
104100
rule: REPORT_FILTER_FIELD_UNKNOWN,
105101
where,
106102
path: verdict.path,
107103
message: verdict.message,
108-
hint:
109-
`Filter on a field of "${verdict.object}" (dataset "${dsName}"), a dimension of that dataset, ` +
110-
`or a relationship path whose prefix its \`include\` declares.`,
104+
hint: verdict.kind === 'measure'
105+
? measureFilterKeyFix(verdict)
106+
: `Filter on a field of "${verdict.object}" (dataset "${dsName}"), a dimension of that dataset, ` +
107+
`or a relationship path whose prefix its \`include\` declares.`,
111108
});
112109
}
113110
};

‎packages/lint/src/validate-widget-bindings.test.ts‎

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1506,8 +1506,8 @@ describe('widget-filter-field-unknown (#14148 limb A)', () => {
15061506
// alone, and the object's field list is no longer echoed.
15071507
expect(f.message).toContain('Did you mean "due_date"?');
15081508
expect(f.hint).toBe(
1509-
'Filter on a field of "duly_task" (dataset "duly_workload"), or on a relationship path whose prefix that ' +
1510-
"dataset's `include` declares.",
1509+
'Filter on a field of "duly_task" (dataset "duly_workload"), a dimension of that dataset, or a relationship ' +
1510+
'path whose prefix its `include` declares.',
15111511
);
15121512
expect(f.hint).not.toContain('business_unit');
15131513
});
@@ -1596,6 +1596,63 @@ describe('widget-filter-field-unknown (#14148 limb A)', () => {
15961596
});
15971597
});
15981598

1599+
// [#30006] One accept set for a bare key, shared with `report-filter-field-unknown`:
1600+
// a field, then a dimension of the dataset; a measure is refused with its own
1601+
// message. The dimension `state` is named APART from the field it reads, so
1602+
// "a dimension key is accepted" is a different fact from "a field key is".
1603+
describe('widget-filter-field-unknown — dataset dimensions and measures (#30006)', () => {
1604+
const ds = {
1605+
dimensions: [{ name: 'state', field: 'business_unit' }],
1606+
measures: [
1607+
{ name: 'untouched_over_14d', aggregate: 'count' },
1608+
{ name: 'total_estimate', aggregate: 'sum', field: 'estimate' },
1609+
{ name: 'estimate', aggregate: 'avg', field: 'estimate' },
1610+
{ name: 'estimate_per_task', derived: { op: 'ratio', of: ['total_estimate', 'untouched_over_14d'] } },
1611+
],
1612+
};
1613+
const judge = (filter: unknown) =>
1614+
idsOf(validateWidgetBindings(widgetOwnStack({ filter }, ds)), WIDGET_FILTER_FIELD_UNKNOWN);
1615+
1616+
it('accepts a bare key naming a dimension of the dataset, under `$` combinators too', () => {
1617+
expect(judge({ state: 'emea' })).toEqual([]);
1618+
expect(judge({ $or: [{ state: 'emea' }, { $not: { state: 'apac' } }] })).toEqual([]);
1619+
});
1620+
1621+
it('refuses a key naming a measure, with the measure message and its column as the fix', () => {
1622+
const findings = judge({ total_estimate: { $gt: 100 } });
1623+
expect(findings).toHaveLength(1);
1624+
const [f] = findings;
1625+
expect(f.severity).toBe('error');
1626+
expect(f.path).toBe('dashboards[0].widgets[0].filter.total_estimate');
1627+
expect(f.message).toContain('names a measure of dataset "duly_workload"');
1628+
expect(f.message).toContain('"estimate"');
1629+
expect(f.message).not.toContain('Did you mean');
1630+
expect(f.hint).toContain('key the filter by the field "estimate"');
1631+
});
1632+
1633+
it('refuses a count and a derived measure, which read no single column', () => {
1634+
for (const key of ['untouched_over_14d', 'estimate_per_task']) {
1635+
const [f, ...rest] = judge({ [key]: { $gt: 1 } });
1636+
expect(rest, key).toEqual([]);
1637+
expect(f.message, key).toContain('reads no single column');
1638+
expect(f.hint, key).toContain('a dimension of the dataset');
1639+
}
1640+
});
1641+
1642+
it('a measure sharing a field\'s name is the field: accepted, as the dataset door binds it', () => {
1643+
expect(judge({ estimate: { $gt: 3 } })).toEqual([]);
1644+
});
1645+
1646+
it('CONTROL — an unknown bare key and a dotted dimension-shaped key are still refused as unknown', () => {
1647+
const findings = judge({ $and: [{ no_such_key: 1 }, { 'owner.state': 'x' }] });
1648+
expect(findings.map((f) => f.path)).toEqual([
1649+
'dashboards[0].widgets[0].filter.$and[0].no_such_key',
1650+
'dashboards[0].widgets[0].filter.$and[1].owner.state',
1651+
]);
1652+
expect(findings.every((f) => !f.message.includes('names a measure'))).toBe(true);
1653+
});
1654+
});
1655+
15991656
describe('widget-filter-field-not-included (#14148 limb A, the include clause)', () => {
16001657
it('errors when a resolvable dotted key traverses an UNDECLARED relationship', () => {
16011658
const findings = idsOf(

0 commit comments

Comments
 (0)