Skip to content

feat(lint)!: refuse a widget filterBindings key that names no declared dashboard filter - #27527

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-m56rgp
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-m56rgp

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #27445

Triage-ref: 6491434802
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — skills/objectstack-ui/rules/dashboards.md already teaches that a filterBindings key is the declared filter's name (defaulting to field); nothing there teaches an undeclared key, so no skills/** edit. Docs updated here: content/docs/ui/dashboards.mdx, content/docs/deployment/validating-metadata.mdx.

维护者速读

  • 改了什么:新增 lint 规则 dashboard-filter-binding-unknown(error)。部件 filterBindings 的键若不是该仪表盘已声明的过滤器名,os validate / os build / os lint 和元数据发布门(422 INVALID_METADATA)都会拒绝;草稿照常保存,_diagnostics 给出同样的结论。消息写明部件 id、错误的键、已声明的过滤器,并给出 Did you mean。
  • 为什么改:运行时按已声明过滤器的名字去读绑定,未声明的键永远不会被读到。拼错的退出({ regoin: false })会让部件仍被那个过滤器筛选,数字悄悄变了。
  • 风险与回滚:BREAKING(收窄),ADR-0087 登记 D3 条目 ui-dashboard-filter-binding-unknown-refused,无 D2(盘点没有任何生产者写出未声明的键)。已存的 sys_metadata 仪表盘照常加载,下次保存/发布才会被拒。回滚即 revert。
  • 先看:packages/lint/src/validate-widget-bindings.ts 的 (a-1) 段。
  • 两分钟验证:cd packages/lint && pnpm exec vitest run src/validate-widget-bindings.test.ts -t "dashboard-filter-binding-unknown",预期全部通过。
  • 需要你做的:无,席位落地(需合同复核)。

What changes

One new error id in validateWidgetBindings. For every widget, each key of filterBindings must name a filter the dashboard declares. The declared names are the ones the rule's existing dashboardFilterDefs builds, the same set objectui's resolveDashboardFilterDefs builds at render time: each globalFilters[] entry under its name (its field when unnamed), and dateRange only when the dashboard declares dateRange. A key outside that set is reported at dashboards[i].widgets[j].filterBindings.<key>:

widget "kpi_deals" binds filterBindings.regoin, but dashboard "deal_board" declares no filter named regoin (declared: dateRange, region), so the binding is never read

with the fix Rename the key to a declared filter name, or delete the entry. Did you mean "region"? (a dateRange key on a dashboard with no dateRange gets "Declare dateRange on the dashboard, or delete the dateRange entry."). The check runs before the dataset limb because it needs no dataset, and it is not suppressible.

Why error, not the chart-field-unknown warning tier. #15463 demoted chart-field-unknown because its three keys only re-dress the same numbers. A filterBindings key decides which rows the widget counts: a misspelled opt-out leaves the widget scoped by the filter the author opted it out of, and a misspelled re-target leaves the filter on its default field. Triage 6491434802 graded it a refusal at draft and publish.

Route. The rule rides validateWidgetBindings, which is already on the dashboard runtime-gate path with per-rule surfaces (the 2026-08-15 ruling's class), so the publish door refuses it with no new wiring and a stored row is never judged on load. No spec superRefine: DashboardWidgetSchema cannot see its dashboard's filters, and a parse refusal would move the load path.

  • packages/lint: the rule and DASHBOARD_FILTER_BINDING_UNKNOWN (barrel-exported), its os explain entry in rule-explanations.ts, the module docblock bullet.
  • packages/spec: DashboardWidgetSchema.filterBindings TSDoc and .describe() name the rule and the valid keys (content/docs/references/ui/dashboard.mdx regenerated); ADR-0087 D3 entry 18.ui-dashboard-filter-binding-unknown-refused (no conversion: only the author knows which filter a misspelled key meant).
  • Docs: a row in the "Where a Filter's field Resolves" table and a paragraph under Per-Widget Filter Bindings (dashboards.mdx); a paragraph in validating-metadata.mdx.
  • Changeset 27445-dashboard-filter-binding-unknown.md: @objectstack/lint minor, @objectstack/spec minor, marker registered ui-dashboard-filter-binding-unknown-refused.

Census (Phase 1)

Measured on main 273137d2 and the public HotCRM clone at 47a0666:

  • Producers: 0 offenders. Showcase showcase_revenue_pulse (keys dateRange, region; declares both) and showcase_ops_dashboard (task_status; declared); platform system_overview (created_at; an unnamed globalFilters entry whose name defaults to its field); HotCRM executive_dashboard, sales_dashboard, crm_overview_dashboard, service_dashboard (dateRange, owner_id, lead_source, type, priority; all declared, the unnamed entries by field). docs/** and skills/** snippets are widget-array fragments. So no D2.
  • dateRange is valid only when declared: lint's dashboardFilterDefs, objectui's resolveDashboardFilterDefs and @objectstack/verify's dashboard-window proof all build the dateRange def only when dash.dateRange is set, so an undeclared one was inert everywhere.
  • Studio's widget inspector writes filterBindings rows only for declared filter defs. Renaming or removing a global filter does not prune existing widget bindings; that dashboard's next save is now refused naming the stale key, which the widget's generic Filter Bindings form row can delete.

Tests

  • packages/lint/src/validate-widget-bindings.test.ts (new describe, 12 cases): the card's body (exact message, path and fix), re-target spelling with one finding per key, declared keys including dateRange pass, an unnamed globalFilters entry bound by field, a custom name replaces the field as the key, dateRange only when declared, a dashboard with no filters, judged with no dataset and not suppressible, silent without filterBindings, gates validate and build, the os explain entry.
  • packages/lint/src/runtime-gate.test.ts: the door accept-set pin goes from five ids to six, and a new case pins both halves at the door (a declared binding publishes; regoin is refused at its path with the declared list and the Did you mean).
  • packages/metadata-protocol/src/protocol.draft-publish-verdict.test.ts: a seventh case on the real save/read/publish seams (the draft saves, reads valid: false with the publish's words, the publish answers 422 INVALID_METADATA); and a stored-row pair: a row written with the authoring gate off loads with { regoin: false } intact, and its next save is refused 422 naming the key and (declared: region). The stub engine's matcher learned $in, which the active read's type clause uses.
  • Ablation: with the rule's check replaced by continue, 10 lint cases go red; restored, green.

Verification

Head 4759433d, off main 273137d2.

  • Under os-verify-lock: build of the @objectstack/lint... and @objectstack/metadata-protocol... closures exit 0; @objectstack/spec check:generated exit 0 (after gen:docs for the describe); @objectstack/spec test (750 files, 22313 tests) and test:repo (86 files, 1419 tests) exit 0; @objectstack/lint test (177 files, 7602 tests) and typecheck exit 0; @objectstack/metadata-protocol test (279 files passed, 4 skipped; 21777 tests) and typecheck exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands: 107 derived families, each run with its exit recorded. 103 exit 0 on the first pass; four exit 3 for prerequisites (check-plugin-teardown-shape --self-test needed its pinned fixture commit fetched; check:skill-examples, check:dual-build-cjs-loads and check:lean-entry-closure need a full build). After git fetch origin 621a4876 and a full turbo run build under the lock (11m38s on a shared box), all four exit 0. dispatch-gates --ran: 107 derived, 107 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0.
  • ESLint on the changed source and test files: 0 errors.

Human Minimum

Each item is one rule from
CONTRIBUTING.md › Human Minimum,
which links the AGENTS.md text that states it.

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (No formatter ran. ESLint on the changed files: 0 errors; pnpm lint is CI's run.)
  • 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 touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (The 维护者速读 block is in Chinese by the repo's dispatch rule.)
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzeTeGTp7gp3ECrLPMf

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 6 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/analytics.mdx (via DashboardWidgetSchema (symbol, a top-level const))
  • content/docs/frontend/plugins/plugin-dashboard.mdx (via DashboardWidgetSchema (symbol, a top-level const))

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

  • content/docs/releases/v15.mdx (via DashboardWidgetSchema (symbol, a top-level const))
  • content/docs/releases/v16.mdx (via DashboardWidgetSchema (symbol, a top-level const), 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
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 166 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 3604371fd229511aa028cc660415f15bc8f74a10 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 792b6163161bd51e8cb244658913882716951759 — the merge of head 4759433d4c2601f6e4a20f1d2c105391232cc7f8 into base 3604371fd229511aa028cc660415f15bc8f74a10, 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 792b6163161bd51e8cb244658913882716951759 && git checkout 792b6163161bd51e8cb244658913882716951759
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3604371fd229511aa028cc660415f15bc8f74a10 4759433d4c2601f6e4a20f1d2c105391232cc7f8 && git checkout -B drift-repro 3604371fd229511aa028cc660415f15bc8f74a10 && git merge --no-ff 4759433d4c2601f6e4a20f1d2c105391232cc7f8

node scripts/docs-audit/affected-docs.mjs --json 3604371fd229511aa028cc660415f15bc8f74a10

⚠️ 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 3604371fd229511aa028cc660415f15bc8f74a10 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 4759433d4c2601f6e4a20f1d2c105391232cc7f8 · 2026-10-29T10:55Z

Triage seat, under gap rule (b) (#27062): this head is over 60 minutes old with no record and no claim. The review runs at CONTRACT_REVIEW_TIER. If the lane claims or posts a record on this head first, triage withdraws.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4759433d4c2601f6e4a20f1d2c105391232cc7f8
Local-runs: none
Scope: packages/spec/src/migrations/entries/semantic/18.ui-dashboard-filter-binding-unknown-refused.ts, packages/spec/src/ui/dashboard.zod.ts + packages/lint/src/validate-widget-bindings.ts, packages/lint/src/authoring-rules.ts, ui/packages/core/src/utils/dashboard-filters.ts, packages/verify/src/dashboard-window.ts, ui/packages/app-shell/src/views/metadata-admin/inspectors/DashboardWidgetInspector.tsx, packages/spec/scripts/build-migration-registry.ts, packages/spec/scripts/build-upgrade-guide.ts, packages/spec/src/migrations/semantic-relevance.test.ts, skills/objectstack-ui/rules/dashboards.md

Inputs read: card #27445 body and both comments (triage 2026-10-29T08:09Z, claim 09:31Z); PR #27527 body, file list (12 paths), net diff vs main; the head's check-runs re-fetched twice (gh api GET) — final state 41 completed/success, 9 completed/skipped (Lint UI, Test UI, UI Published Artifacts, the three dogfood/temporal jobs and the three opt-in jobs — the diff touches no ui/** path, so the path-filtered UI jobs skipping is the expected shape), 0 pending, 0 failed. Among the green runs: Lint & Repo Gates, every Lint · family, Check Changeset, Governed Surface Queue Guard, PR-body guards, Spec property liveness, all four Type Check families, Test Core 1–8 and the aggregate, Build Core, Build Docs. The rest of the diff (changeset, three docs pages, packages/lint rule/explanations/barrel/tests, packages/metadata-protocol test) is the lane seat's ACCEPT at TIER_DEFAULT; it was READ here only where the hits files cite it.

① Derived judgments

  • Accept-set narrowing, dashboard type, every authoring door — RIGHT. validateWidgetBindings is registered in packages/lint/src/authoring-rules.ts (line 924) with tier: 'gating', input: 'parsed', commands: ALL, surfaces: CLI_AND_RUNTIME, runtimeTypes: ['dashboard']. The new (a-1) limb pushes severity: 'error' under that rule, so it reaches os validate / os build / os lint and the runtime publish door (422 INVALID_METADATA) with no new wiring, exactly as the PR claims and as the 2026-08-15 ruling's class (per-rule surfaces) implies. The runtime-gate.test.ts accept-set pin moves from five ids to six; chart-field-unknown stays out. Correct.
  • Stored rows load unchanged — RIGHT. The rule rides the authoring path only; no load-path or getMetaItem code is touched; the metadata-protocol test pins a row written with the gate off loading with { regoin: false } intact and its next save refused.
  • Public surface of @objectstack/lint — RIGHT, additive: one new exported constant DASHBOARD_FILTER_BINDING_UNKNOWN (barrel at index.ts:43), one new RULE_EXPLANATIONS entry. No existing id, message or export changes.
  • Public surface of @objectstack/spec — RIGHT, non-structural: DashboardWidgetSchema.filterBindings keeps z.record(z.string(), z.union([z.string(), z.literal(false)])).optional(); only the TSDoc and .describe() text change. The regenerated content/docs/references/ui/dashboard.mdx carries the new describe in both tables where the widget shape is rendered; no stale copy of the old describe text exists anywhere else in the head tree (grepped). check:generated is inside the green Lint & Repo Gates family.
  • ADR-0087 D3 registration, no D2 — RIGHT. One semantic entry; the registry (packages/spec/src/migrations/registry.ts) and the upgrade-guide pages (content/docs/protocol-upgrade/) are gitignored, generated at build/docs-build from the entries directory, so no committed artifact was owed. The census premise (0 producers) is re-measured below and holds.
  • No spec superRefine — RIGHT as a derived judgment: a DashboardSchema-level refine COULD see both widgets and globalFilters, but a parse refusal would judge stored rows on load, which the ruling (and the metadata-protocol pin) forbid. The lint route is the one that keeps the load path open.

② Semver level

Changeset .changeset/27445-dashboard-filter-binding-unknown.md: @objectstack/lint: minor, @objectstack/spec: minor; summary feat(lint)!: …; body carries **BREAKING**, Clause-②: no (narrowing), Metadata-protocol: narrowed, and adr-0087: registered ui-dashboard-filter-binding-unknown-refused — the marker's id resolves to the new entry file, whose filename 18.ui-dashboard-filter-binding-unknown-refused.ts is the one shardNameFor(18, id) derives (the registry builder refuses a mismatch). minor on lint is right (a new export); minor on spec matches house practice for a narrowing that adds a D3 entry and changes a describe (peers: 26413-now-anchor-extra-key-refused, 26601-dotted-add-days-offset-refused, 26646-field-group-visible-when-judged). The PR body's Clause-②: no (narrowing) and Metadata-protocol: narrowed lines agree with the changeset. Check Changeset, PR-body guards (which runs check-adr-0087-registration.mjs --base) and Governed Surface Queue Guard are green.

Clause-②: no (narrowing) — the line matches what the diff publishes: nothing is widened; one accept set is narrowed at every dashboard authoring door.

③ Boundary flags

  • Human Minimum item 3 left unchecked (pnpm lint is CI's run) — answered by the head: every Lint · check-run and Lint & Repo Gates are success.
  • Skills-docs: none — verified against skills/objectstack-ui/rules/dashboards.md lines 203–224 on the head: the skill teaches name as "the stable key filterBindings references. Defaults to field" and both examples bind declared names (region, task_status). Nothing teaches an undeclared key. Holds.
  • Claim comment flagged the branch name as measured (claude/project-thread-m56rgp, not claude/issue-27445-slug) — recorded as-is in Implemented-by:; no action.
  • Census premise (0 offending producers at main and HotCRM) — re-measured here, read-only: showcase showcase_revenue_pulse (declares dateRange + region; keys dateRange, region), showcase_ops_dashboard (declares dateRange + task_status; key task_status), platform system_overview (unnamed globalFilters entry field: 'created_at'; key created_at). HotCRM clone at 1f1ea12 (newer than the dev's 47a0666): five dashboards, every binding key (dateRange, owner_id, lead_source, type) is declared on its own dashboard; UNDECLARED is empty for all five. No other filterBindings producer exists under examples/**, apps/**, ui/apps/** or packages/*/src/**; docs/**, content/docs/** and skills/** hits are widget-array fragments. The "no D2" disposition stands.
  • Docs-drift bot named content/docs/data-modeling/analytics.mdx and content/docs/frontend/plugins/plugin-dashboard.mdx via the DashboardWidgetSchema anchor — neither mentions filterBindings, so the new rule falsifies nothing there. Outside hits; noted for the lane seat only.
  • open_questions: none in the PR body or the card.

④ Design judgment

packages/spec/src/ui/dashboard.zod.ts — a TSDoc + .describe() change on DashboardWidgetSchema.filterBindings, judged against the rule it names and the runtime it describes.

  • "Every key must name a filter the dashboard declares — a globalFilters entry's name (its field when unnamed), or dateRange when the dashboard declares dateRange": matches dashboardFilterDefs in validate-widget-bindings.ts (lines 652–672: dateRange def only when dash.dateRange is an object; each globalFilters entry keyed f.name || f.field) and the live renderer ui/packages/core/src/utils/dashboard-filters.ts resolveDashboardFilterDefs (lines 525–560: identical construction, const name = f.name || f.field). GlobalFilterSchema.field is required and name optional (lines 1593, 1617), so lint's skip of a fieldless entry is unreachable on a parsed dashboard — no declared filter can be missed.
  • "The runtime reads a binding by the declared filter's name, so any other key is never read": resolveBoundField reads widget.filterBindings?.[def.name] (dashboard-filters.ts:697); @objectstack/verify's window proof reads only filterBindings.dateRange and only after if (!dash.dateRange) … not-windowed (dashboard-window.ts:444–461); the Studio inspector renders and writes a row per filterDefs entry keyed def.name (DashboardWidgetInspector.tsx:404–427). All three consumers agree with the describe; none reads an undeclared key.
  • "refuses it at os validate / os build and at the metadata publish door": true by the authoring-rules.ts registration above.
  • Break attempts: a globalFilters entry named dateRange on a dashboard with no dateRange — both lint and runtime set byName('dateRange'), so the key is declared in both; duplicate names — later wins in both; scope: 'widget' + targetWidgets — the filter is still declared and an explicit binding still wins at runtime, so accepting the key is right; dateRange: {} — truthy object in both, declared in both. No divergence found. Holds. No finding.

packages/spec/src/migrations/entries/semantic/18.ui-dashboard-filter-binding-unknown-refused.ts — judged against the registry builder and tests that consume it, and against the rule it registers.

  • Shape: every required SemanticMigration field present (id, surface, replacement, reason, acceptanceCriteria); export const entry initializer ends with }; on its own line; no conversionIds (correct — there is no D2); no relevantWhen (correct — the relevance table in semantic-relevance.test.ts pins exactly 21 protocol-18 questions and this entry adds none). migrations.test.ts "acceptance criteria never silence" holds (both non-empty). Test Core 1–8 green.
  • Content: reason cites widget.filterBindings?.[def.name] in the dashboard renderer — read at dashboard-filters.ts:697, true; "the schema types filterBindings as an open record" — true (z.record); "dashboard-filter-field-unknown judges only the EFFECTIVE field of each declared filter" — true (effectiveFilterField iterates dashFilterDefs, lines 933–950); the load-path statement matches the metadata-protocol pin. acceptanceCriteria is measurable (os validate reports no finding; each dashboard publishes) and states the measured baseline.
  • Should-fix (non-blocking): surface carries backticks around globalFilters, field and dateRange while build-upgrade-guide.ts:135 renders s.surface inside a code span, so the generated upgrade-guide line's code span closes early. This is endemic — the great majority of the 516 entries on main do the same, and only 18.visibility-predicate-has-argument-refused.ts avoids it by comment — so it is house practice, not a defect this PR introduces; noted for a follow-up sweep, not for this head.
  • Break attempts: id/filename correspondence (enforced by parseEntry) — matches; the id is unique among the 516 entries (listed). No finding at must-fix.

Verdict basis. Both hits files hold against the cited code in the live tree and against the card's scenario ({ no_such_filter: 'status' } / { regoin: false } published clean before; refused at draft-read and publish now, stored rows untouched). Gate verdicts: all 41 completed check-runs on the head succeeded, none pending, the 9 skips are path-filtered UI/opt-in jobs on a diff with no ui/** path. No must-fix.

Implemented-by: claude/project-thread-m56rgp
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-29T11:10Z

Head 4759433d4c2601f6e4a20f1d2c105391232cc7f8. Card #27445, claim 6493063447, triage 6491434802.

  • Contract review: owed at CONTRACT_REVIEW_TIER, narrowing face. Triage served it: record 6495021350 at this head is a PASS with no must-fix. The new error id dashboard-filter-binding-unknown refuses { no_such_filter: 'status' } and { regoin: false } at the draft read and at publish. Stored rows are untouched.
  • Should-fix: backticks in the D3 entry's surface close the upgrade guide's code span early. The review grades this house practice (most of the ~516 entries do it), not a defect this PR introduces. The seat filed the sweep as [finding] the generated upgrade guide wraps each semantic entry's surface in a single-backtick code span, so a surface that carries its own backticks (most of the ~516 entries) renders broken #27532: fix it once, in build-upgrade-guide.ts. It does not hold this head.
  • Seat's read of the rest (TIER_DEFAULT):
    • The changeset is @objectstack/lint minor and @objectstack/spec minor, with Clause-②: no (narrowing), Metadata-protocol: narrowed and the marker registered ui-dashboard-filter-binding-unknown-refused. It covers Before, Now, Who sees it, Stored rows and the remedy, and it matches the diff.
    • The prose in validating-metadata.mdx and dashboards.mdx states the rule as the code enforces it: a globalFilters name, or its field when the filter has no name, or dateRange only when declared. The rule is not suppressible.
    • The PR body is the dev's pr-body.md.
  • Dev readings:
    • lint and metadata-protocol closure builds and the spec check:generated exit 0.
    • spec test passes 750 files and 22313 tests; test:repo passes 86 files and 1419 tests.
    • lint test passes 177 files and 7602 tests; metadata-protocol test passes 279 files and 21777 tests.
    • Derived gates: 107 run, 0 NOT-MEASURED, all exit 0.
    • Ablation: with the check removed, 10 lint cases go red.
    • Census: 0 offenders on main (273137d2) and in hotcrm (47a0666).
  • Gates: check-runs on this head are 42 success, 13 skipped and 0 red. mergeable_state is clean.
  • Landing: check-governed-merges --pr 27527: NOT governed, +364/−6. Tier S. pr_ready + automerge_enable in this act. The seat removes needs:contract-review.

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 protocol:ui size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dashboards: a widget filterBindings key naming an undeclared dashboard filter is accepted at draft and publish (silently inert)

2 participants