From 6be194c5073717794184516ed036d91f783d73b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 10:31:28 +0000 Subject: [PATCH 1/2] 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 --- .changeset/24293-filter-token-message.md | 15 ++++++++ .../lint/src/validate-dataset-references.ts | 9 ++--- .../lint/src/validate-filter-tokens.test.ts | 12 +++++++ packages/lint/src/validate-filter-tokens.ts | 36 +++++++++++-------- 4 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 .changeset/24293-filter-token-message.md diff --git a/.changeset/24293-filter-token-message.md b/.changeset/24293-filter-token-message.md new file mode 100644 index 00000000000..13d34bfba2b --- /dev/null +++ b/.changeset/24293-filter-token-message.md @@ -0,0 +1,15 @@ +--- +"@objectstack/lint": patch +--- + +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 + +Clause-②: no + +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: + +```text +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. +``` + +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. diff --git a/packages/lint/src/validate-dataset-references.ts b/packages/lint/src/validate-dataset-references.ts index 132ade39a8b..3395ee4702a 100644 --- a/packages/lint/src/validate-dataset-references.ts +++ b/packages/lint/src/validate-dataset-references.ts @@ -33,10 +33,11 @@ * * ## Why a dataset's dangling reference is worse than most * - * `filter-token-unknown`'s own wording states the failure mode it exists to - * prevent: the value is *"sent to the data engine as a literal string, matches - * no record, and the surface renders empty."* A dangling field path produces - * the same outcome from the same node. + * `filter-token-unknown` judges the VALUE at that same node, and the value it + * reports does not fail quietly: the data engine refuses the query + * (`FILTER_TOKEN_UNKNOWN`, 400). A dangling field path there is the quiet + * failure. By the severity note below, every outcome it produces reaches a + * human as a chart that rendered successfully. * * A dataset is the semantic layer — dashboards and reports bind its dimensions * and measures BY NAME (ADR-0021) — and the CONSUMER end of that binding is diff --git a/packages/lint/src/validate-filter-tokens.test.ts b/packages/lint/src/validate-filter-tokens.test.ts index 11ed9710381..2c2287fd339 100644 --- a/packages/lint/src/validate-filter-tokens.test.ts +++ b/packages/lint/src/validate-filter-tokens.test.ts @@ -56,6 +56,18 @@ describe('validateFilterTokens', () => { expect(findings[0].hint).toContain('{current_user_id}'); }); + // [#24293] The message states the runtime's answer: the data engine refuses a + // query carrying the value (`UnknownFilterTokenError` in `@objectstack/core`, + // `FILTER_TOKEN_UNKNOWN` / 400). Held as that code, the named subject, not as + // the prose around it. + it('names the code the data engine refuses the query with, near miss or not', () => { + const findings = validateFilterTokens({ + dashboards: [{ name: 'd', widgets: [{ id: 'w', filter: { a: '{current_user}', b: '{whoever_is_watching}' } }] }], + }); + expect(findings.map((f) => f.path)).toEqual(['dashboards[0].widgets[0].filter.a', 'dashboards[0].widgets[0].filter.b']); + for (const f of findings) expect(f.message, f.path).toContain('`FILTER_TOKEN_UNKNOWN`'); + }); + it('accepts the correct spelling alongside date macros', () => { expect( validateFilterTokens({ diff --git a/packages/lint/src/validate-filter-tokens.ts b/packages/lint/src/validate-filter-tokens.ts index 97a5c9c8042..47d8294046c 100644 --- a/packages/lint/src/validate-filter-tokens.ts +++ b/packages/lint/src/validate-filter-tokens.ts @@ -22,22 +22,27 @@ import { recordsOf } from './object-graph.js'; * Exactly two vocabularies resolve inside a filter value: context tokens * (`{current_user_id}`, `{current_org_id}` — see `context-tokens.zod.ts`) and * date macros (`{today}`, `{30_days_ago}` — see `date-macros.zod.ts`). - * Anything else is passed to the data engine **verbatim**, matches no row, and - * the surface renders an empty result. + * Anything else does not resolve: the data engine refuses a query that carries + * it (`FILTER_TOKEN_UNKNOWN`, 400 — `UnknownFilterTokenError` in + * `@objectstack/core`'s `filter-tokens.ts`), so the surface never shows the + * data the filter meant. * * ## Why this is an error, not a warning * - * The runtime failure mode is silent and indistinguishable from success. A - * metric widget filtered on an unresolved `{current_user}` renders `0`, which - * looks exactly like a metric that is legitimately zero — no console error, no - * server log, nothing for a human reviewer to notice. Issue #3574 found a - * dashboard that had been broken this way since the day it was written. + * The rule is older than that refusal. When issue #3574 was filed, the runtime + * substituted no placeholder: the value reached the data engine verbatim and + * matched no row. A metric widget filtered on an unresolved `{current_user}` + * rendered `0`, which looks exactly like a metric that is legitimately zero — + * no console error, no server log, nothing for a human reviewer to notice. + * Issue #3574 found a dashboard that had been broken this way since the day it + * was written. The runtime now refuses the query instead, so the failure is no + * longer silent, but it still shows only when someone opens the surface. * - * That failure mode is worse for AI authors than for humans. An AI reads a - * successful query returning `0` as a correct answer and builds on it — it has - * no instinct that the number looks wrong. Its correction loop is - * "author → validate → fix", so a diagnostic only reaches it if the diagnostic - * can fail the build. A runtime warning in a server log is invisible to it. + * Neither form reaches an AI author. An AI read the old `0` as a correct answer + * and built on it — it has no instinct that the number looks wrong. Its + * correction loop is "author → validate → fix", so a diagnostic only reaches it + * if the diagnostic can fail the build. A server log line, or a refusal that + * arrives when a surface is opened after the build passed, is invisible to it. * Hence: authoring-time error. * * 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'; export type FilterTokenSeverity = 'error' | 'warning'; export interface FilterTokenFinding { - /** Always `error` today — an unresolved placeholder silently matches nothing. */ + /** Always `error` today — the data engine refuses a query carrying an unresolvable placeholder. */ severity: FilterTokenSeverity; /** Diagnostic rule id. */ rule: string; @@ -233,8 +238,9 @@ function walkFilterValues( where, path, message: - `Filter value "${node}" is not a resolvable placeholder. It is sent to the ` + - `data engine as a literal string, matches no record, and the surface renders empty.`, + `Filter value "${node}" 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.`, hint: suggestion && isRecordContextToken(suggestion) ? `Did you mean "{${suggestion}}"? It resolves only in a filter on a component of a ` + `\`type: 'record'\` page, to the id of the record in view.` From f3d1666d95945947e5d3f5fee129fcd6350a3d90 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 11:07:50 +0000 Subject: [PATCH 2/2] fix(lint): the validateFilterTokens registration comment states the runtime's refusal The comment above the rule's registration in authoring-rules.ts said an unresolvable filter placeholder reaches the data engine as a literal, matches nothing, and the surface renders empty with no error. The data engine refuses such a query (FILTER_TOKEN_UNKNOWN, 400). The comment now says so and keeps the silent-zero history it was written against, in the past tense. Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude --- packages/lint/src/authoring-rules.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/lint/src/authoring-rules.ts b/packages/lint/src/authoring-rules.ts index 78373b7603b..ab47ce3c339 100644 --- a/packages/lint/src/authoring-rules.ts +++ b/packages/lint/src/authoring-rules.ts @@ -914,9 +914,10 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [ 'saves, a rollout decision over stored tenant rows the in-repo corpus does not measure.', run: (stack) => validateDashboardWidgetOptions(stack), }, - // #3574 — a filter value like `{current_user}` resolves in no vocabulary, - // reaches the data engine as a literal and matches nothing. The surface - // renders empty with no error, and a silent zero is indistinguishable from a + // #3574 — a filter value like `{current_user}` resolves in no vocabulary. The + // data engine refuses a query carrying it (`FILTER_TOKEN_UNKNOWN`, 400). When + // #3574 was filed it reached the engine as a literal and matched nothing: the + // surface rendered empty with no error, a silent zero indistinguishable from a // genuine one at review time. { name: 'validateFilterTokens',