From 6e7b285518329d7c693fad47d517e6b30935815d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 19:55:27 +0000 Subject: [PATCH 1/3] docs(spec): say where a dashboard global filter's field resolves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `globalFilters[].field` described itself only as "Field name to filter on", which left the generated reference page silent on the one thing authors get wrong: the name resolves against the object behind each bound widget's dataset (`dataset.object`), not against that dataset's declared `dimensions`. The rewritten `.describe()` states the resolution target, scopes the claim to the authoring layer, and names the rule that already enforces it (`dashboard-filter-field-unknown`, severity error). The mirrored TSDoc carries the second half the one-line form cannot: the separation is a statement about the authorable surface only, NOT a claim that an object field can never serve as a dimension — the analytics query API does accept one ad hoc without the dataset declaring it. Wording aligned with the hand-written page's `## Global Filters` section. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x --- packages/spec/src/ui/dashboard.zod.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/spec/src/ui/dashboard.zod.ts b/packages/spec/src/ui/dashboard.zod.ts index fa67cfc806..8da9be3fc4 100644 --- a/packages/spec/src/ui/dashboard.zod.ts +++ b/packages/spec/src/ui/dashboard.zod.ts @@ -800,8 +800,21 @@ export const GlobalFilterSchema = lazySchema(() => strictObject({ */ name: z.string().optional().describe('Stable filter name (variable key); defaults to field'), - /** Field name to filter on */ - field: z.string().describe('Field name to filter on'), + /** + * Field name to filter on — at the authoring layer it resolves against the + * object behind each bound widget's dataset (`dataset.object`), not against + * that dataset's declared `dimensions`; enforced by the lint rule + * `dashboard-filter-field-unknown` (severity error). + * + * `dimensions` is a separate namespace, the one `widgets[].dimensions[]` + * selects from BY NAME (`widget-dimension-unknown`, also severity error). + * That separation is a statement about the AUTHORABLE SURFACE only — it is + * NOT a claim that an object field can never serve as a dimension: the + * analytics query API does accept an object's own field as an ad-hoc + * dimension without the dataset declaring it, and `widget-dimension-unknown` + * is what holds that line for authored dashboards. + */ + field: z.string().describe('Field name to filter on — at the authoring layer it resolves against the object behind each bound widget\'s dataset (`dataset.object`), not against that dataset\'s declared `dimensions`; enforced by the lint rule `dashboard-filter-field-unknown` (severity error)'), /** * Source object for i18n label resolution (#7804): when set, this filter's From 6139ec910e9b4be3cefc656b00569f1b11552566 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 20:38:54 +0000 Subject: [PATCH 2/3] docs(spec): regenerate the dashboard reference page, add the changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `content/docs/references/ui/dashboard.mdx` is a build product of the spec's docs build — its line 6 says so — so the new `globalFilters[].field` description reaches it by regeneration, never by hand. Two rows carry the sentence because two sections render the same `GlobalFilterSchema`: `### Nested Shape: Dashboard.globalFilters[number]` and the top-level `## GlobalFilter` → `### Properties`. No other reference page moved: the identically-spelled `field` describe on the list-view schema is a different surface and was deliberately left alone. Changeset is `patch` on `@objectstack/spec`: the string is a published datum, not a comment — it is the `description` of the property in the shipped JSON Schema and the runtime `.description` on the Zod schema in `dist`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x --- ...board-global-filter-field-resolution-target.md | 15 +++++++++++++++ content/docs/references/ui/dashboard.mdx | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 .changeset/dashboard-global-filter-field-resolution-target.md diff --git a/.changeset/dashboard-global-filter-field-resolution-target.md b/.changeset/dashboard-global-filter-field-resolution-target.md new file mode 100644 index 0000000000..68c61febec --- /dev/null +++ b/.changeset/dashboard-global-filter-field-resolution-target.md @@ -0,0 +1,15 @@ +--- +"@objectstack/spec": patch +--- + +`Dashboard.globalFilters[].field` now describes WHERE the name resolves, instead of only that it is a field name. + +The key's `.describe()` read `Field name to filter on` — true, but silent on the one thing authors get wrong. A dashboard global filter's `field` resolves against the object behind each bound widget's dataset (`dataset.object`), never against that dataset's declared `dimensions`; `widgets[].dimensions[]` selects from that second, separate namespace by name. Readers who assumed the two were the same namespace concluded a filter needs a matching dataset dimension, which is not so. + +The rewritten description states the resolution target, scopes the claim to the authoring layer, and names the rule that already enforces it — `dashboard-filter-field-unknown`, `severity: 'error'` in `@objectstack/lint`'s widget-binding validator. It is a statement about a rule that already fires, not a suggestion. + +The mirrored TSDoc carries the half a one-line description cannot: the separation holds for the **authorable surface** only. It is NOT a claim that an object field can never serve as a dimension — the analytics query API does accept an object's own field as an ad-hoc dimension without the dataset declaring it, and `widget-dimension-unknown` (also `severity: 'error'`) is what holds that line for authored dashboards. + +**What moves for consumers.** The string is a published datum, not a comment: it is the `description` of the `field` property in the shipped JSON Schema (`json-schema/ui/GlobalFilter.json`, `json-schema/ui/Dashboard.json`, `json-schema/objectstack.json`) and the runtime `.description` on the Zod schema in `dist`, so anything that renders schema descriptions — editor hovers, generated reference pages, prompt builders — shows the new sentence. No accept set moves: the key stays `z.string()`, nothing that parses today stops parsing, and no validation behaviour changes. + +The same wording already stands on the hand-written page (`content/docs/ui/dashboards.mdx` → **Where a Filter's `field` Resolves**); the generated reference page now agrees with it rather than trailing it. diff --git a/content/docs/references/ui/dashboard.mdx b/content/docs/references/ui/dashboard.mdx index d78c62471b..4173838ab8 100644 --- a/content/docs/references/ui/dashboard.mdx +++ b/content/docs/references/ui/dashboard.mdx @@ -97,7 +97,7 @@ const result = DashboardSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | optional | Stable filter name (variable key); defaults to field | -| **field** | `string` | ✅ | Field name to filter on | +| **field** | `string` | ✅ | Field name to filter on — at the authoring layer it resolves against the object behind each bound widget's dataset (`dataset.object`), not against that dataset's declared `dimensions`; enforced by the lint rule `dashboard-filter-field-unknown` (severity error) | | **object** | `string` | optional | Object whose `fields..` translation-bundle entry resolves this filter's field label and option labels | | **label** | `string \| Record` | optional | Display label for the filter | | **type** | `Enum<'text' \| 'select' \| 'date' \| 'number' \| 'lookup'>` | optional | Filter input type | @@ -275,7 +275,7 @@ Widget configuration — declared query keys + open renderer extras | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | optional | Stable filter name (variable key); defaults to field | -| **field** | `string` | ✅ | Field name to filter on | +| **field** | `string` | ✅ | Field name to filter on — at the authoring layer it resolves against the object behind each bound widget's dataset (`dataset.object`), not against that dataset's declared `dimensions`; enforced by the lint rule `dashboard-filter-field-unknown` (severity error) | | **object** | `string` | optional | Object whose `fields..` translation-bundle entry resolves this filter's field label and option labels | | **label** | `string \| Record` | optional | Display label for the filter | | **type** | `Enum<'text' \| 'select' \| 'date' \| 'number' \| 'lookup'>` | optional | Filter input type | From 95663cfc11ee5fd17d81885c14aff70150adbee4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 20:53:57 +0000 Subject: [PATCH 3/3] docs(spec): disarm the `object` adjacency in the `field` docblock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two adjacent docblocks in `GlobalFilterSchema` each say "the object the field lives on" about a different resolution: `field` resolves a COLUMN against the bound widget's `dataset.object`, while the sibling `object` key resolves a translation-bundle KEY. They do not collide, but a reader who meets both may infer that setting `object` re-points the filter — the same reader-facing shape this docblock was just rewritten to fix. One clause in the `field` TSDoc closes it at the point of confusion. The `object` docblock is untouched, and the `.describe()` string is byte-unchanged, so the generated reference page and the changeset need no edit. The citation is anchored by SYMBOL, not by line: the exported rule-id constant `DASHBOARD_FILTER_FIELD_UNKNOWN`, the enclosing `validateWidgetBindings`, and a verbatim snippet measured to grep to exactly one hit in that file. A `path:NNN` prose anchor would have started rotting on the next edit to the file it names. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x --- packages/spec/src/ui/dashboard.zod.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/spec/src/ui/dashboard.zod.ts b/packages/spec/src/ui/dashboard.zod.ts index 8da9be3fc4..c2e7086b9e 100644 --- a/packages/spec/src/ui/dashboard.zod.ts +++ b/packages/spec/src/ui/dashboard.zod.ts @@ -806,6 +806,14 @@ export const GlobalFilterSchema = lazySchema(() => strictObject({ * that dataset's declared `dimensions`; enforced by the lint rule * `dashboard-filter-field-unknown` (severity error). * + * The sibling `object` key does not move this: it names the object a + * translator's bundle entry is keyed by and is read for LABEL resolution + * only. `dashboard-filter-field-unknown` (exported as + * `DASHBOARD_FILTER_FIELD_UNKNOWN`) resolves against the widget's own + * dataset — `validateWidgetBindings` seeds its base with + * `const datasetObject = typeof dataset.object === 'string'` — and never + * reads a filter-level `object`. + * * `dimensions` is a separate namespace, the one `widgets[].dimensions[]` * selects from BY NAME (`widget-dimension-unknown`, also severity error). * That separation is a statement about the AUTHORABLE SURFACE only — it is