Skip to content

Commit 6be194c

Browse files
committed
fix(lint): filter-token-unknown states the runtime's refusal, FILTER_TOKEN_UNKNOWN / 400
The finding's message said an unresolvable placeholder is sent to the data engine as a literal string, matches no record, and the surface renders empty. The data engine refuses such a query instead (UnknownFilterTokenError in @objectstack/core, FILTER_TOKEN_UNKNOWN / 400). The message, the module header, the severity field's doc and the dataset-references docblock that quoted the old sentence now say so. A test holds the message to the code. Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
1 parent 21070a2 commit 6be194c

4 files changed

Lines changed: 53 additions & 19 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): `filter-token-unknown` states what the runtime does with an unresolvable filter placeholder: the data engine refuses the query with `FILTER_TOKEN_UNKNOWN` / 400
6+
7+
Clause-②: no
8+
9+
The finding's message said the value "is sent to the data engine as a literal string, matches no record, and the surface renders empty". The runtime does not do that. The data engine refuses a query that carries an unresolvable placeholder, `FILTER_TOKEN_UNKNOWN` with HTTP 400, rather than compare it as a literal string. The message now reads:
10+
11+
```text
12+
Filter value "{current_user}" is not a resolvable placeholder. The data engine refuses a query that carries it (`FILTER_TOKEN_UNKNOWN`, 400), so the surface never shows the data the filter meant.
13+
```
14+
15+
Unchanged: the rule id, its severity (`error`), the finding's `path`, its `hint` (the `fix:` line), and which values the rule reports. The message's first sentence is unchanged as well. `os explain filter-token-unknown` already stated the refusal and is unchanged.

‎packages/lint/src/validate-dataset-references.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@
3333
*
3434
* ## Why a dataset's dangling reference is worse than most
3535
*
36-
* `filter-token-unknown`'s own wording states the failure mode it exists to
37-
* prevent: the value is *"sent to the data engine as a literal string, matches
38-
* no record, and the surface renders empty."* A dangling field path produces
39-
* the same outcome from the same node.
36+
* `filter-token-unknown` judges the VALUE at that same node, and the value it
37+
* reports does not fail quietly: the data engine refuses the query
38+
* (`FILTER_TOKEN_UNKNOWN`, 400). A dangling field path there is the quiet
39+
* failure. By the severity note below, every outcome it produces reaches a
40+
* human as a chart that rendered successfully.
4041
*
4142
* A dataset is the semantic layer — dashboards and reports bind its dimensions
4243
* and measures BY NAME (ADR-0021) — and the CONSUMER end of that binding is

‎packages/lint/src/validate-filter-tokens.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,18 @@ describe('validateFilterTokens', () => {
5656
expect(findings[0].hint).toContain('{current_user_id}');
5757
});
5858

59+
// [#24293] The message states the runtime's answer: the data engine refuses a
60+
// query carrying the value (`UnknownFilterTokenError` in `@objectstack/core`,
61+
// `FILTER_TOKEN_UNKNOWN` / 400). Held as that code, the named subject, not as
62+
// the prose around it.
63+
it('names the code the data engine refuses the query with, near miss or not', () => {
64+
const findings = validateFilterTokens({
65+
dashboards: [{ name: 'd', widgets: [{ id: 'w', filter: { a: '{current_user}', b: '{whoever_is_watching}' } }] }],
66+
});
67+
expect(findings.map((f) => f.path)).toEqual(['dashboards[0].widgets[0].filter.a', 'dashboards[0].widgets[0].filter.b']);
68+
for (const f of findings) expect(f.message, f.path).toContain('`FILTER_TOKEN_UNKNOWN`');
69+
});
70+
5971
it('accepts the correct spelling alongside date macros', () => {
6072
expect(
6173
validateFilterTokens({

‎packages/lint/src/validate-filter-tokens.ts‎

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,27 @@ import { recordsOf } from './object-graph.js';
2222
* Exactly two vocabularies resolve inside a filter value: context tokens
2323
* (`{current_user_id}`, `{current_org_id}` — see `context-tokens.zod.ts`) and
2424
* date macros (`{today}`, `{30_days_ago}` — see `date-macros.zod.ts`).
25-
* Anything else is passed to the data engine **verbatim**, matches no row, and
26-
* the surface renders an empty result.
25+
* Anything else does not resolve: the data engine refuses a query that carries
26+
* it (`FILTER_TOKEN_UNKNOWN`, 400 — `UnknownFilterTokenError` in
27+
* `@objectstack/core`'s `filter-tokens.ts`), so the surface never shows the
28+
* data the filter meant.
2729
*
2830
* ## Why this is an error, not a warning
2931
*
30-
* The runtime failure mode is silent and indistinguishable from success. A
31-
* metric widget filtered on an unresolved `{current_user}` renders `0`, which
32-
* looks exactly like a metric that is legitimately zero — no console error, no
33-
* server log, nothing for a human reviewer to notice. Issue #3574 found a
34-
* dashboard that had been broken this way since the day it was written.
32+
* The rule is older than that refusal. When issue #3574 was filed, the runtime
33+
* substituted no placeholder: the value reached the data engine verbatim and
34+
* matched no row. A metric widget filtered on an unresolved `{current_user}`
35+
* rendered `0`, which looks exactly like a metric that is legitimately zero —
36+
* no console error, no server log, nothing for a human reviewer to notice.
37+
* Issue #3574 found a dashboard that had been broken this way since the day it
38+
* was written. The runtime now refuses the query instead, so the failure is no
39+
* longer silent, but it still shows only when someone opens the surface.
3540
*
36-
* That failure mode is worse for AI authors than for humans. An AI reads a
37-
* successful query returning `0` as a correct answer and builds on it — it has
38-
* no instinct that the number looks wrong. Its correction loop is
39-
* "author → validate → fix", so a diagnostic only reaches it if the diagnostic
40-
* can fail the build. A runtime warning in a server log is invisible to it.
41+
* Neither form reaches an AI author. An AI read the old `0` as a correct answer
42+
* and built on it — it has no instinct that the number looks wrong. Its
43+
* correction loop is "author → validate → fix", so a diagnostic only reaches it
44+
* if the diagnostic can fail the build. A server log line, or a refusal that
45+
* arrives when a surface is opened after the build passed, is invisible to it.
4146
* Hence: authoring-time error.
4247
*
4348
* The near-miss spellings this catches are not hypothetical. Each is a correct
@@ -103,7 +108,7 @@ export const FILTER_TOKEN_UNKNOWN = 'filter-token-unknown';
103108
export type FilterTokenSeverity = 'error' | 'warning';
104109

105110
export interface FilterTokenFinding {
106-
/** Always `error` today — an unresolved placeholder silently matches nothing. */
111+
/** Always `error` today — the data engine refuses a query carrying an unresolvable placeholder. */
107112
severity: FilterTokenSeverity;
108113
/** Diagnostic rule id. */
109114
rule: string;
@@ -233,8 +238,9 @@ function walkFilterValues(
233238
where,
234239
path,
235240
message:
236-
`Filter value "${node}" is not a resolvable placeholder. It is sent to the ` +
237-
`data engine as a literal string, matches no record, and the surface renders empty.`,
241+
`Filter value "${node}" is not a resolvable placeholder. The data engine refuses a ` +
242+
`query that carries it (\`FILTER_TOKEN_UNKNOWN\`, 400), so the surface never shows ` +
243+
`the data the filter meant.`,
238244
hint: suggestion && isRecordContextToken(suggestion)
239245
? `Did you mean "{${suggestion}}"? It resolves only in a filter on a component of a ` +
240246
`\`type: 'record'\` page, to the id of the record in view.`

0 commit comments

Comments
 (0)