docs(spec): globalFilters[].field describes where the name resolves — the dataset object, not its dimensions - #16968
Conversation
`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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
`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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 2 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 131 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3d13f56e4a0e23552bf8935ad606eedbe5b65413 && git checkout 3d13f56e4a0e23552bf8935ad606eedbe5b65413
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ee4a59b6c22a3a894b4cfd73755fe26a2a08922a 95663cfc11ee5fd17d81885c14aff70150adbee4 && git checkout -B drift-repro ee4a59b6c22a3a894b4cfd73755fe26a2a08922a && git merge --no-ff 95663cfc11ee5fd17d81885c14aff70150adbee4
node scripts/docs-audit/affected-docs.mjs --json ee4a59b6c22a3a894b4cfd73755fe26a2a08922a
|
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…d line stops reading as an addition The two tells the card reproduced fire on added LINES and could not see that a line replaced an equivalent one: PR #16941 (T2, a form `description:` prose rewrite on a file with no closed set in it) and PR #16968 (T1, a Zod key whose `.describe()` text grew, keys 32 -> 32). Neither moved an accept set, and the C5 row neither raised could be cleared except by declaring `Clause-②: yes` on a change that does not widen. `tellsInFile` now reads a REPLACEMENT BUDGET per change block, per tell kind: each removed line carrying a member or key of kind K buys one added line of kind K the right not to be reported, spent in patch order, so a block that adds more than it removed still reports the surplus with its own file:line. The unit is the change block, never the hunk, so an unrelated removal three context lines away cannot pay for a real addition. Openers and prose fragments pay for nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
Fixes #16256
Clause-②: no
One
.describe()string is rewritten so it says where the name resolves. The key staysz.string(): no accept set moves, no arm is added, no bound changes, no validationbehaviour changes. The published
descriptiontext is the whole diff — a 拉回已声明契约restatement, 常规档. The fence held: saying it correctly needed no type change and no
new arm, so there was nothing to stop and report.
What
Dashboard.globalFilters[].fielddescribed itself only asField name to filter on. True,but 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 declareddimensions. The generated reference row is the only place on the reference page thatexplains this key, so it now carries the resolution target.
The reference page is a build product — its line 6 says
AUTO-GENERATED — DO NOT EDIT— sothe source edit is the
.describe()string inpackages/spec, and the page reaches its newtext by regeneration. Nothing under
content/docs/references/was hand-edited.The wording, and why it is shaped this way
Triage made one nuance binding. Quoted verbatim, unmodified (per AGENTS.md, a Chinese ruling
is not translated in place):
The one-line
.describe()blocks the misreading by scoping the claim rather than bydenying the lower layer: it opens with at the authoring layer. It therefore never asserts
anything about the analytics query layer, so it cannot state that layer's truth backwards.
The mirrored TSDoc then carries the half a table cell cannot, in as many words: the
separation is a statement about the authorable surface only, and it is NOT a claim that
an object field can never serve as a dimension.
Aligned with PR #16266, not a second phrasing
Three carriers state this one rule (#16167's hand-written page via PR #16266, #16268's
example on that page, and this card's generated row). #16266 has landed; its sentence on
content/docs/ui/dashboards.mdxunder Where a Filter'sfieldResolves reads:and its lower-layer disclaimer reads:
The new describe reuses that noun phrase — the object behind each bound widget's dataset
(
dataset.object) — verbatim, and the TSDoc reuses the disclaimer's substance. No secondspelling was invented.
Before / after
Source,
packages/spec/src/ui/dashboard.zod.ts(:803-:804onorigin/main97adce2fa):after: a 13-line TSDoc plus
Which generated rows moved
The card cites
content/docs/references/ui/dashboard.mdx:97. That number has moved, and ontoday's
mainthe old string appears in that page twice. Both occurrences render thesame
GlobalFilterSchema, so both moved — and only those two:origin/main:100### Nested Shape: Dashboard.globalFilters[number]— the row the card names:278## GlobalFilter→### Properties— the same schema rendered as its own top-level sectionpnpm --filter @objectstack/spec gen:docsregenerated 228 files andgit statusreportedexactly one modified path. Notably not swept in:
packages/spec/src/ui/view.zod.ts:657carries the identical
.describe('Field name to filter on')on a different schema, and itsgenerated rows in
view.mdx,component.mdxandpage.mdxare byte-unchanged. A tree-widesearch-and-replace on that string would have been the wrong instrument; the edit was anchored
to one exact source block whose occurrence count was asserted to be 1 before writing.
The enforcement, located by rule id
Both rules were found by id, not by the line numbers the card cites (triage said it had
not verified them). They happen to still hold on
97adce2fa:severitydashboard-filter-field-unknownpackages/lint/src/validate-widget-bindings.ts:430:901and:918'error'at:900and:917widget-dimension-unknownpackages/lint/src/validate-widget-bindings.ts:401:1073'error'at:1072Both are
severity: 'error', which is the point: the new sentence states a rule that isalready enforced, not a suggestion.
packages/lintwas read only — it is untouched bythis diff.
Changeset — decided with both halves
files[]? Yes.packages/spec'sfiles[]is['dist','json-schema','liveness','prompts','llms.txt','README.md','src/**/*.zod.ts','CHANGELOG.md','api-surface','spec-changes.json'], andsrc/ui/dashboard.zod.tsmatchessrc/**/*.zod.ts.distjson-schema.describe()(Stable filter name (variable key); defaults to field)//comment in the same object literal (The binding runs the other way…)The
distcolumn does not discriminate: those bundles are unminified and retain sourcecomments, so the negative control appears there too.
json-schemais the discriminatingreading, and it is a clean pair — only
.describe()text reaches it. The new sentence landsin
json-schema/ui/GlobalFilter.json,json-schema/ui/Dashboard.jsonandjson-schema/objectstack.json, all insidefiles[], plus the runtime.descriptionon theZod schema in
dist. ⇒patchon@objectstack/spec.Verification
Run in the dedicated worktree
/home/user/objectstack-issue-16256, offorigin/main97adce2fa. Heavy runs went throughscripts/pm/os-verify-lock.sh; every exit code wascaptured before any pipe.
pnpm --filter @objectstack/spec buildVERDICT command-exit 0pnpm --filter @objectstack/spec gen:docsVERDICT command-exit 0—Generated 228 filespnpm --filter @objectstack/spec run check:docspnpm --filter @objectstack/spec run check:generatedpnpm --filter @objectstack/spec run check:authorable-surfacepnpm --filter @objectstack/spec typecheckVERDICT command-exit 0— test layer compiles, 54 files / 261 errors / 145 pinned signatures held, shrink-only ledger unchangedpnpm --filter @objectstack/spec testVERDICT command-exit 0—Test Files 467 passed (467),Tests 13100 passed (13100)pnpm lint(whole repo,eslint . --no-inline-config)VERDICT command-exit 0, re-run at final commit6139ec910pnpm check:nul-bytesgit statusafter the docs buildGate families were derived mechanically rather than transcribed:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat6139ec910derived 94 families over the 3 changed paths; all 94 were run, and--ranreconciliation reports94 derived famil(ies) accounted for — 94 run, 0 NOT-MEASURED.Two honest caveats on that sweep:
pnpm check:dual-build-cjs-loadsexited 3 —PREREQUISITE NOT MET, naming 87 packageswith no
dist/. Nothing was measured; it needs a whole-repopnpm build, which does notfit this seat's foreground budget. Declared to CI, and it reads no path in this diff.
node scripts/check-adr-0087-registration.mjs --self-testfirst exited 1 on anenvironment failure (the container's commit-signing server answered 503, so the
temp-repo commit the self-test makes could not be written). Retried: exit 0,
332 assertions over real temp git repos. Not a diff finding.Four other families first exited on
PREREQUISITE NOT METfor unbuilt packages(
check:doc-formula-expressions,check:doc-security-posture,check:skill-examples,check:docs-transcript-drift); afterturbo run build --filter=@objectstack/lint --filter=@objectstack/formula --filter=@objectstack/client-react --filter=@objectstack/client(
VERDICT command-exit 0, 34/34 tasks) all four re-ran green.Not governed: the register prints
docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md,and this diff touches none of them.
验收备注
packages/spec/src/ui/view.zod.ts:657carries the sameField name to filter ondescribeon the list-view filter schema. Whether that surface wants the same treatment is a separate
reading on a separate card — noted, deliberately not filed and deliberately not touched.
docs/notes/airtable-dashboard-analysis.mdx:347quotes the old describe line inside adesign-note code block. It is a historical note, not a carrier of the rule — left alone.
Generated by Claude Code