Skip to content

fix(lint): a widget filter keyed on a dataset dimension publishes, and a measure filter key is refused with its own message (#30006) - #30560

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-xi54xm
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-xi54xm

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#3 · os-bill · session_01Utp6CbPHd9hdK2yxiVhr84

Requested by Bill · project thread

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 的 lint: the dataset filter-key rules refuse a bare dimension or measure name that the dataset door binds at run time #30006 块。
  • 两分钟验证:pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.dashboard-dataset-publish-gate.test.ts,预期 19 个测试全绿,其中 lint: the dataset filter-key rules refuse a bare dimension or measure name that the dataset door binds at run time #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.
Test runs on the merged head (after merging main, which brought #30231); the docs-only commit b498e10 changes no test input
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
Ablation (one, on the dimension accept)

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.

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

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec untouched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint is CI's.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.

Generated by Claude Code

…a measure key in both filter rules

judgeDatasetFilterKeys now holds the one accept set for a bare filter key
that widget-filter-field-unknown and report-filter-field-unknown share:
a field of the object, then a dimension of the dataset. A bare key naming
a dataset measure gets its own verdict and message: the dataset door
compiles it to a row-level predicate on the measure's column (or to no
column at all), never to a filter on the aggregated value.

Claude-Session: https://claude.ai/code/session_01Utp6CbPHd9hdK2yxiVhr84
Co-authored-by: Claude <noreply@anthropic.com>
…imension accept widens

Claude-Session: https://claude.ai/code/session_01Utp6CbPHd9hdK2yxiVhr84
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 6 documentable anchor(s).

⛔ 2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via validateWidgetBindings (symbol, a top-level function))
  • content/docs/releases/v17/17-1.mdx (via validateWidgetBindings (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 48b3ae6eb7cd234dcdea467fb55dd493a56bb0c2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 41596954acc205e4a91f3e9bb5c4cfeef1bd8bcc — the merge of head b498e10ffbca375b172585ab9e15188e0f17bc9e into base 48b3ae6eb7cd234dcdea467fb55dd493a56bb0c2, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 41596954acc205e4a91f3e9bb5c4cfeef1bd8bcc && git checkout 41596954acc205e4a91f3e9bb5c4cfeef1bd8bcc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 48b3ae6eb7cd234dcdea467fb55dd493a56bb0c2 b498e10ffbca375b172585ab9e15188e0f17bc9e && git checkout -B drift-repro 48b3ae6eb7cd234dcdea467fb55dd493a56bb0c2 && git merge --no-ff b498e10ffbca375b172585ab9e15188e0f17bc9e

node scripts/docs-audit/affected-docs.mjs --json 48b3ae6eb7cd234dcdea467fb55dd493a56bb0c2

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 48b3ae6eb7cd234dcdea467fb55dd493a56bb0c2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…mensions a filter key may also name

Claude-Session: https://claude.ai/code/session_01Utp6CbPHd9hdK2yxiVhr84
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

Contract review

Served-tier: TIER_DEFAULT
Head-sha: b498e10ffbca375b172585ab9e15188e0f17bc9e
Local-runs: none
Scope: packages/lint/src/validate-widget-bindings.ts (judgeDatasetFilterKeys, DatasetFilterKeyVerdict, measureFilterKeyFix, measureColumn, the widget rule's verdict switch), packages/lint/src/validate-report-runtime-filter.ts, packages/lint/src/rule-explanations.ts (the two rule entries), the three test files, .changeset/30006-dataset-filter-key-dimension-measure.md, content/docs/deployment/validating-metadata.mdx (the package-closure paragraph).
Face: no packages/spec/**, skills/**, docs/adr/**, .claude/** or governed path; check-governed-merges NOT governed, 320 lines. Declaration Clause-②: yes (widening) with no spec or governed path ⇒ TIER_DEFAULT. The docs file was added to the file surface by the seat (one sentence made false by the widening). git merge-tree against origin/main clean.

① Derived judgments

  1. One accept set for both rules' bare keys now lives in judgeDatasetFilterKeys: a field of the object first (unchanged), then a dimension of the dataset (new for the widget rule; the report rule's local dimension set is removed, and its strName filter is equivalent to the shared typeof === 'string' test for every non-empty name). A bare key naming a measure gets kind: 'measure'; dotted keys never take either member path. Correct against the card's ask ("one shared accept set … so the two rules cannot drift").
  2. The measure outcome is refusal with its own message, chosen on the dev's run-time measurement on main (sum → row-level predicate on the raw column, count → broken SQL / key *, derived → 400 INVALID_FIELD). The card asked to measure and pick; the pick is evidenced and refuses nothing that published before, because both rules already refused a measure key as unknown. No narrowing.
  3. The report rule's verdict filter moved from kind !== 'unknown' → skip to kind === 'not-included' → skip: the same set plus measure, so the report rule keeps leaving not-included to the widget twin's second id.
  4. The message is one line under the [maintainer] validate: the field-no-consumers warning is one 856-character line, printed by validate, build and dev alike — one-line verdict + rule: id + a pointer to the full reasoning (os explain, which today takes only schema names) #22161 limit and names no tracker id; the fix line names the column only when the measure reads exactly one (derived / duration / missing field → generic fix).
  5. Pins: a dimension key accepted (bare, under $or / $not), sum / count / derived measures refused with the right fix, a measure sharing a field's name accepted, CONTROL unknown and dotted keys still refused, twin parity extended to a dimension and a measure key, and the real draft → publish route at the door (422 INVALID_METADATA at reports[0].runtimeFilter.amount_sum, nothing active stored). The dev's ablation of the dimension accept turned 7 dimension assertions red and was restored clean.
  6. The docs sentence now matches both rules: "a filter key that neither the object nor the dataset's dimensions name is refused there". The os explain entries state the accept set and the measure refusal.

② Semver level

patch on @objectstack/lint, the only package published by the diff (metadata-protocol is touched only in a test file). Clause-②: yes (widening) is right: one accept set widens, nothing narrows. Metadata-protocol: none is right: the CLI already accepted a dimension key on a report, the widget twin catches up, and no shard of the authorable surface moves.

③ Boundary flags

None. A dotted cube.member spelling stays a relationship path, as the acceptance notes state; that is a separate shape and not owed by this card.

Implemented-by: claude/project-thread-xi54xm
Reviewed-by: session_01Utp6CbPxYq1wQmUNWvMZkC

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

维护者速读(席位终稿)

  • 改了什么:仪表盘部件的 filter 现在可以直接写数据集维度名(如维度 state 对应字段 status),发布不再误报「字段不存在」。部件规则和报表规则改为共用一套可接受的键;写度量名的过滤键仍然拒收,但给出专门的说明。
  • 为什么改:运行时的数据集查询本来就按维度绑定这个键,发布检查却拒收,作者只能改写底层字段名(lint: the dataset filter-key rules refuse a bare dimension or measure name that the dataset door binds at run time #30006)。度量名在运行时会被编成逐行比较原始列(sum)、坏 SQL(count)或 400(派生),所以继续拒收,并讲清原因。
  • 风险与回滚:只放宽,以前能发布的仍能发布;度量键以前就被拒,现在只是换了提示。只动 @objectstack/lint 两条规则、说明文字和 validating-metadata.mdx 一句话。回滚即 revert。
  • 先看:packages/lint/src/validate-widget-bindings.ts 里 judgeDatasetFilterKeys 的维度和度量分支;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 条)。
  • 需要你做的:无。技能定档:非受管面、320 行、只放宽,不需人审;席位复核 6663670331(Head-sha b498e10ffbca375b172585ab9e15188e0f17bc9e,PASS),席位落地。

席位意见:同意落地。两条规则从此用同一套键,度量键的取舍有运行时实测撑着。

席位核对

ACCEPT · head b498e10ffbca375b172585ab9e15188e0f17bc9e · card #30006 · 2026-11-05T16:46Z.

  • Shape: draft PR to main, relay author, Fixes #30006 only. 8 files, +284 −36. check-governed-merges.mjs --pr 30560 → exit 0, NOT governed, 320 ≤ 3000. No merge=os-regen path. Merge-tree against origin/main clean.
  • Premise measured: the dev's run-time table on main through AnalyticsService.queryDataset (both strategies) for dimension, sum, count and derived measure keys; ablation of the dimension accept turned 7 dimension assertions red and was restored clean.
  • Gates (check-runs on this head): 69 runs, 59 success, 10 path-gated skipped, 0 failed, 0 in progress (read 2026-11-05T16:46Z).
  • Accepted deviation: none. Not BREAKING (Clause-②: yes (widening)), so no dogfood battery is owed.
  • Out of scope: none filed. Skills-docs: updated here — content/docs/deployment/validating-metadata.mdx (seat-widened file surface, one sentence).
  • Landing: not BREAKING, not governed, ≤ 3000 ⇒ the skill requires no human merge; seat lands it now.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 16:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 1756666 Nov 5, 2026
70 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-xi54xm branch November 5, 2026 17:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lint: the dataset filter-key rules refuse a bare dimension or measure name that the dataset door binds at run time

1 participant