Skip to content

fix(types)!: DashboardComponentSchema refuses a duplicate globalFilters key, as the spec does - #29507

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

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #29284

Clause-②: yes (narrowing)
Metadata-protocol: none

维护者速读

  • 改了什么:@object-ui/types 发布的仪表盘校验器 DashboardComponentSchema 的 globalFilters 改为直接引用 spec 的同名字段,于是同一仪表盘里两个全局筛选用了同一个键(name,没有 name 时用 field)会被拒绝;另顺手修正一行表单视图注释。
  • 为什么改:spec 自 dashboards: two globalFilters with the same name are accepted; the board draws one control and its value filters the other filter's field #29023 起拒绝这种重复键,但 ui 镜像自己重建了这个数组,把数组上的拒绝规则丢了,结果 ui 校验通过、平台发布却拒绝,两边口径不一致。
  • 风险与回滚:只收紧 ui 镜像对一种已被平台拒绝的写法的判断,元素校验、TypeScript 类型和 DASHBOARD_SPEC_EXCLUDED 都不变;回滚即 revert 本 PR。
  • 先看:ui/packages/types/src/zod/complex.zod.ts(globalFilters: 那一行和上方说明),ui/packages/types/src/__tests__/dashboard-global-filters-duplicate-key-29284.test.ts,.changeset/29284-dashboard-mirror-global-filters-by-reference.md。
  • 两分钟验证:在 ui/ 下运行 pnpm exec vitest run packages/types/src/__tests__/dashboard-global-filters-duplicate-key-29284.test.ts,预期 6 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

  • Mechanism (a): the spec's slot by reference. DashboardComponentSchema.globalFilters is now StrippedSpecDashboard.shape.globalFilters, the globalFilters member of stripImportedDefaults(SpecDashboardSchema), instead of a local z.array(GlobalFilterSchema).optional(). The spec attaches its duplicate-key refusal (refuseDuplicateGlobalFilterNames, dashboards: two globalFilters with the same name are accepted; the board draws one control and its value filters the other filter's field #29023) to that ARRAY, so taking the slot carries it, with the spec's own issue. The array's element is still the exported GlobalFilterSchema (same object, pinned), so the precondition the card names for (a) holds and no element check moves. stripImportedDefaults clones by def, so the array's checks survive the walk (its own docblock).
  • globalFilters stays in DASHBOARD_SPEC_EXCLUDED: that list is published API and the TypeScript twin in complex.ts restates the key over the same element type. The list's docblock now says why the key is re-added and how.
  • No spec export: the wrapper stays unexported, and the spec-object-refinements-7715 SITES table is untouched, because the refinement rides the slot and is not an object-level check the mirror attaches by identifier.
  • Ride-along (ui(types): DashboardComponentSchema rebuilds globalFilters itself, so spec's duplicate-filter-key refusal (#29023) does not reach the published ui mirror #29284 comment, spec seat's review of PR feat(spec)!: retire FormView drawerSide / drawerWidth — no form view opens in a drawer #29287): ui/packages/types/src/objectql.ts's FormView key taxonomy no longer lists drawerSide/drawerWidth as [spec-aligned]. They are tombstones on the spec form view and the React ObjectForm block's own props. Comment only. The dashboard.zod.ts docblock sentence the card mentions is domain:spec's and is not touched here.
  • Changeset: patch for @object-ui/types, both declaration lines, **BREAKING** banner, ADR-0087 not-required (already-registered ui-dashboard-global-filter-duplicate-name-refused).

Tests

  • New ui/packages/types/src/__tests__/dashboard-global-filters-duplicate-key-29284.test.ts, 6 cases: refused by name at globalFilters[1].name; refused by field fallback at globalFilters[1].field; for both, the mirror's custom issues equal the spec door's (code, path, message); distinct keys parse on both; the slot's element is the published GlobalFilterSchema.
  • pnpm --filter @object-ui/types test: 387 files, 10462 tests passed (head db3d4622 before the main merge, and again on the final head 4bbe9510 after rebuilding the @object-ui/types closure: 387 files, 10462 tests passed).
  • turbo run type-check --filter=@object-ui/types: exit 0 (tsc --noEmit, examples, test config).

Acceptance notes

  • Ablation (scripts/ablation-replace.mjs, test import resolves ../zod/complex.zod from source, so no dist leg): the slot line put back to z.array(GlobalFilterSchema).optional().describe('Dashboard-level filters') reds 4 of 6 cases (the two refusals and the two spec-equality cases); the distinct-keys and element-identity cases stay green, as expected. Restore proven by blob equal to HEAD and an empty git diff HEAD.
  • The slot's description is now the spec's (Global filters that apply to all widgets in the dashboard. …) instead of Dashboard-level filters; nothing pins the old string.
  • Serial: the spec seat's PR fix(spec)!: refuse a dashboard globalFilters entry keyed dateRange beside a declared dateRange #29370 edits the same DashboardComponentSchema tail (.superRefine(checkDashboardReservedDateRangeGlobalFilters)) and the SITES row. This diff touches neither: its hunks are the docblock above DASHBOARD_SPEC_EXCLUDED, the SpecDashboardFields line and the globalFilters line.

Verification

Final head 4bbe9510 (branch merged with origin/main 86d36e99; neither merge touched a changed file). node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 34 families; each ran with its exit code recorded, and --ran answered: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN (exit 0).

Gate notes
  • All 34 derived families exited 0 on 03269aa6 (the changeset commit); pnpm --dir ui exec vitest run scripts/: 169 files passed, 2 skipped; 4921 tests passed.
  • Re-run on the final head 4bbe9510, all exit 0: check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main (this body as the event payload), check-closing-keyword-parity, check-empty-changeset --base origin/main, check-sdui-manifest, check:nul-bytes, check:type-check-debt, check:cross-package-test-inputs.
  • pnpm exec turbo run type-check --filter=@object-ui/types: exit 0.
  • Dogfood battery (owed by the (narrowing) arm), head 571c0eb8 (one main merge before the final head; that merge touched no file of this diff), closure build then pnpm --filter @objectstack/dogfood test under the lock: VERDICT command-exit 1, Test Files 2 failed, 395 passed, 3 skipped (400); Tests 4 failed, 3261 passed, 28 skipped (3293). The four failures are flow-runas.dogfood.test.ts (three runAs cases; first assertion: trigger HTTP 404 RECORD_NOT_FOUND in runas_note) and declarative-endpoint-anonymous-guest.dogfood.test.ts (one case). They reproduce when the two files run alone (4 failed, 8 passed). Not this diff: the dogfood closure build contains no @object-ui/* package, and no file under packages/qa/dogfood, packages/verify or packages/plugins imports @object-ui/types. Reported to the seat as a finding.
  • Serial: PR fix(spec)!: refuse a dashboard globalFilters entry keyed dateRange beside a declared dateRange #29370 (open, head 6b7710c9) touches the same file with disjoint hunks. A three-way git merge-file of complex.zod.ts, spec-object-refinements-7715.test.ts and objectql.ts (ours, origin/main as base, theirs) answers 0 conflicts on all three, re-checked on the final head.

🤖 Generated with Claude Code · https://claude.ai/code/session_01QsR2jUAK3FofY5VJPyS8fr


Generated by Claude Code

…slot by reference

The mirror rebuilt globalFilters as z.array(GlobalFilterSchema), which kept
every element check and dropped the spec array's own refinement: two global
filters under one key passed the published mirror while every spec door
refuses them. Take StrippedSpecDashboard.shape.globalFilters instead.

Also corrects the FormView key taxonomy docblock: drawerSide/drawerWidth are
tombstones on the spec form view and live on as ObjectForm's own props.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jUAK3FofY5VJPyS8fr
…ardComponentSchema

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jUAK3FofY5VJPyS8fr
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026

Copy link
Copy Markdown
Collaborator

Contract-review-claim: session_01QsR2jV5sBSzCYNEDgpYLJw · head 4bbe951 · 2026-11-02T16:55Z


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4bbe95100e19e92e4d3b427b18cb63755940aaf6
Local-runs: none
Scope: ui/packages/types/src/zod/complex.zod.ts, ui/packages/types/src/objectql.ts, ui/packages/types/src/tests/dashboard-global-filters-duplicate-key-29284.test.ts, .changeset/29284-dashboard-mirror-global-filters-by-reference.md + packages/spec/src/ui/dashboard.zod.ts (L1685–1689, L1793–1801), ui/packages/types/src/zod/imported-defaults.ts (the walk's array arm L248–251, the memo L109–170), packages/spec/src/conversions/refused-shapes.ts (L564), packages/spec/src/migrations/entries/semantic/18.ui-dashboard-global-filter-duplicate-name-refused.ts, packages/spec/src/ui/view.zod.ts (L5195–5197), scripts/check-changeset-no-major.mjs (L55–100), PR #29370's hunks on the same files

Judged against card #29284 (the spec seat's should-fix from PR #29162) and the claim 6580476126. Read-only review of the committed tree; nothing was built or run here.

① Derived judgments

  • The spec's refusal reaches the mirror by the slot, with the spec's own issue. dashboard.zod.ts L1798–1801 attaches refuseDuplicateGlobalFilterNames with .superRefine on the z.array(GlobalFilterSchema) itself, before .optional(); on zod 4 that is a check on the array's def. The strip walk's array arm (imported-defaults.ts L248–251) rebuilds through cloneWithDef, which carries def.checks, and returns the spec's own node when the element is unchanged; the walk is memoised per spec schema (L109–170). So StrippedSpecDashboard.shape.globalFilters is the spec's slot with its refinement, and its element is the same object as the exported GlobalFilterSchema — which test case 6 pins by identity (toBe), not by shape. Break attempts: two filters with distinct names over one field stay accepted on both doors (case 5); an absent globalFilters parses on both (the .optional() is outside the refinement on both). Holds.
  • Byte-equal issues. The spec refinement adds path: [index, key] relative to the array, so under the mirror the full path is ['globalFilters', 1, 'name'] (or 'field'), the same as the spec door's; cases 3–4 compare code/path/message for both keys. Holds.
  • Reach reading matches mine. Only a dashboard that repeats a filter key changes answer, and every spec door already refuses it since dashboards: two globalFilters with the same name are accepted; the board draws one control and its value filters the other filter's field #29023; the TypeScript type of globalFilters, the DASHBOARD_SPEC_EXCLUDED list and the element checks do not move. The one visible side effect the author names — the slot's description string is now the spec's — has no pin on the old string. Holds.
  • Ablation signature (slot line back to z.array(GlobalFilterSchema).optional()): 4 of 6 red, cases 5 and 6 green — exactly the defect's shape.
  • Field security: none in reach; nothing withheld from this record.

② Semver level

③ Boundary flags

  • Not Tier H. No governed path (ui/packages/types, a changeset, a test). The seat lands.
  • Dogfood battery reading is red, not from this diff, and not yet attributed to main. The (narrowing) arm owes the battery; on 571c0eb8 it answered 4 failures in flow-runas.dogfood.test.ts and declarative-endpoint-anonymous-guest.dogfood.test.ts, deterministic alone. The dogfood closure holds no @object-ui/* package and nothing under packages/qa/dogfood, packages/verify or packages/plugins imports @object-ui/types, so the diff cannot reach them; but the reading was taken on a branch head, not compared with a clean origin/main, and this PR's CI runs no Dogfood gate for its path set (the gate is selector-skipped). Landing note for the seat: before enqueue, show those two files on a clean origin/main checkout (green ⇒ a branch-environment artefact to name; red ⇒ main's defect, file it) and record which in the ACCEPT. Not a must-fix on the diff.
  • Serial with PR fix(spec)!: refuse a dashboard globalFilters entry keyed dateRange beside a declared dateRange #29370 (spec(dashboard): a globalFilters entry named dateRange parses and silently replaces the built-in date range's binding #29213). Its hunks on complex.zod.ts are the import block (L20) and the schema tail (L1741, .superRefine(checkDashboardReservedDateRangeGlobalFilters)), plus the SITES row in spec-object-refinements-7715.test.ts; this PR's hunks are L1646–1690 and L1703–1716 and it leaves the SITES table alone, which is right because the refinement rides the slot and is not attached by identifier. Disjoint; whichever lands second merges main.
  • The new test imports DashboardSchema from @objectstack/spec/ui directly (a cross-package test input); check:cross-package-test-inputs ran exit 0 on the head.

④ Design judgment

ui/packages/types/src/zod/complex.zod.ts — judged against the card and dashboard.zod.ts.

  • Taking the slot by reference rather than exporting the refinement by identifier is the better of the card's two routes: no spec export, no SITES row, and the issue is the spec's rather than a re-spelling. The new StrippedSpecDashboard const makes the slot and SpecDashboardFields share one visible walk. The DASHBOARD_SPEC_EXCLUDED bullet now states why the key stays excluded (published list; the TS twin restates it) and how it is re-added, with both issue ids; the slot line's two-line comment points at that bullet. Holds.

ui/packages/types/src/objectql.ts — judged against view.zod.ts.

  • drawerSide / drawerWidth are retiredKey tombstones on the spec form view (L5195–5197, removed in 18.3.0 under ADR-0049) and ObjectForm's own props (objectql.ts L2243, L2249); moving them from [spec-aligned] to [ObjectUI extension] with that sentence is true on this head. Comment only. Holds.

Test and changeset — 6 cases with the identity pin; the changeset body carries the full Before / Now / Fix and the entry id. Holds.

No must-fix found. No should-fix. One landing note under ③ (attribute the dogfood red to main or to the branch environment before enqueue).

Implemented-by: claude/project-thread-t0oa5x
Reviewed-by: session_01QsR2jV5sBSzCYNEDgpYLJw

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#5 · zhuangjianguo · session_01QsR2jQjvWkZ7is6GXbC6DB

席位核对 — ACCEPT · domain:ui#5 · head 4bbe9510 · 2026-11-02T17:08Z

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 17:09
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 5651647 Nov 2, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-t0oa5x branch November 2, 2026 17:47
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

Projects

None yet

2 participants