Repository navigation
feat(types,plugin-dashboard): dashboard header and globalFilters state the spec member on both faces (objectui#7759 group A) - #10744
Conversation
…e the spec member on both faces (objectui#7759 group A) Both keys are declared by @objectstack/spec's DashboardSchema, so under the card's ruling (principle 1) the zod mirror and the TypeScript declaration both follow the spec, both ways. Each key was DISJOINT: ledgered in KnownDrift and WiderThanDeclared. - header: the declaration drops its hand-written restatement and inherits the spec's DashboardHeader through the Omit<Dashboard, ...> projection (actions[].label is I18nLabel, actionUrl required, actionType the ActionType enum). DashboardRenderer resolves the action label to the active language before rendering it or using it as the bundle fallback; an inline per-locale label used to reach React as a button child and throw. - globalFilters: the mirror's GlobalFilterSchema is the spec's schema by reference. It no longer accepts the bare-string option shorthand, a label-less option, an optionsFrom without labelField, an array filter, or an unknown key; it now accepts the inline per-locale option label it refused. The runtime bare-string lift in @object-ui/core is untouched. The pair leaves WiderThanDeclared whole and keeps only `widgets` in KnownDrift. Claude-Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW
…ocument the spec shape (objectui#7759 group A) The dashboard docs follow the implementation: a header action's label is a string or an inline per-locale map and its actionUrl is required; an optionsFrom names labelField; objectui's validator refuses the bare-string option shorthand, as the platform does. One changeset for the group: @object-ui/types minor (the validator's globalFilters accept set narrows and the header interface narrows actionUrl / actionType), @object-ui/plugin-dashboard patch (the renderer resolves a per-locale header action label). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW
…up-a-dashboard-locale
… the spec type (objectui#7759) check:spec-symbols refuses a local declaration under a name @objectstack/spec exports; `DashboardHeaderAction` is one. The alias is read off DashboardComponentSchema, so it is named DashboardNodeHeaderAction. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
Red check that is not this PR's: The same check is red on Generated by Claude Code |
Contract reviewServed-tier: ① Derived judgmentsMeasured on a detached worktree at the head (removed afterwards), objectui Accept-set / public-surface changes, each named and judged
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…e html tier's intrinsic elements, marked tier html — div stays out (objectui#10735) (objectstack-ai#10753) Fixes objectstack-ai#10735 Clause-②: yes — the published manifest's declared vocabulary grows. Executes the maintainer's ruling A on objectstack#20112 (batch objectstack-ai#225 item 1, 「其他同意」): the published `sdui.manifest.json` declares the html tier's registered intrinsic set exactly as the registry declares it, with each tag's `inputs` and child slot; `div` stays undeclared. Base `origin/main` 5ad3b88; head 6417e8d (round 2: wording only, one commit on top of 5bd6270). ## What changed - `packages/core/src/registry/html-tier-intrinsics.ts` (new): `HTML_TIER_INTRINSICS` — the 37 tags of `html-elements.tsx`'s `TAGS`, plus `span`, `table`, `label` and the seven sectioning tags of `semantic.tsx` (47). Exported from `@object-ui/core` beside `PUBLIC_BLOCKS`. - `packages/core/src/registry/Registry.ts`: `getPublicConfigs()` appends the roster's registered tags LAST, each stamped `tier: 'html'` in the projection (never written onto the registration); `PublicComponentConfig.tier` admits `'html'`. `PUBLIC_BLOCKS` is untouched. - `packages/sdui-parser`: `ManifestComponent.tier?: 'html'`; `manifestFromConfigs` writes exactly `'html'` or omits the key (every curated entry serialises byte-identically); `generateBlockList` sections the html tier under its own count; `RegistryConfigLike.tier` admits `'html'`. `gen-manifest.ts` header updated, behaviour unchanged. - `packages/components/src/renderers/layout/react-page.tsx`: the `kind:'react'` scope builder skips `tier === 'html'` entries, so no `P` / `A` / `Img` wrapper is injected (file-surface amendment, forced by the card's ⛔ on silent widening — see H2). - Tests: new pins in core, sdui-parser, console; `react-page-scope.test.tsx` and `container-declaration-census.test.tsx` updated; `public-contract.test.ts` prose amended (two ledger passages that said curating a sectioning tag would newly widen the manifest). - Docs: `content/docs/guide/react-pages.md` (the html tier is not injected on the react tier), `content/docs/guide/plugin-development.md` (the component-level `tier: 'html'` marker). - `.changeset/10735-html-tier-manifest.md`: `minor` for `@object-ui/core`, `@object-ui/sdui-parser`, `@object-ui/components`. - Round 2 (contract review, wording): the react-page skip line is quoted as it now reads — `if (!tag || cfg.isContainer || cfg.tier === 'html') continue;` — in `content/docs/guide/react-pages.md` ("skips every container and every html-tier entry"; frontmatter lines 1–4 md5 unchanged, 9c5f94a7…), in both `public-contract.test.ts` passages, in the `container-declaration-census` pin, and in `layout-containers-declare-containment.test.tsx` (comment-only, previously untouched). `git grep -F` of the old line at head: 2 hits, both inside `.changeset/6740-flex-is-container.md` — its original paragraph and the dated note that marks it. The 10735 changeset now says "200 ledgered findings" (10 ledger rows whose counts sum to 200). - `.changeset/6740-flex-is-container.md` (pre-existing, `@object-ui/components: patch`): reconciled in the objectui#10533 dated-note form, scoped "at this change" — `⚠️ **Dated note, 2026-09-27 — the skip line quoted above has since gained a third arm — objectui#10735.**` — frontmatter byte-identical (md5 ce888ebd… before and after). `check-changeset-overwrite` REPORTS the modification (report-only, exit 0) and did not refuse it. - ⛔ Not touched: `html-elements.tsx`, `div.tsx`, `PUBLIC_BLOCKS`, the registration-uniqueness test. ## H1 — the census (measured from `ComponentRegistry.getKnownTypes()` after `@object-ui/components` + `apps/console/src/register-plugins.ts` load, in the console vitest project) Instrument for "is an HTML tag": the keys of `JSX.IntrinsicElements` in `@types/react` (178 names, HTML and SVG; lit control: `div`, `p`, `svg` in, `object-grid` out). Registry: 485 known keys, 208 bare. Intersection: **64** bare registrations carry an element name. - **(a) `html-elements.tsx` `TAGS` — 37, all registered:** h1 h2 h3 h4 h5 h6 p a blockquote pre strong em b i u small mark sub sup del ins abbr ul ol li dl dt dd figure figcaption img hr br time address cite q. - **(b) registered elsewhere, declared — 10:** `span` (basic/span.tsx; inputs className, children), `table` (complex/table.tsx; caption, footer, columns, data, className — no child slot), `label` (form/label.tsx; text required, className — no child slot), and `aside main header nav footer section article` (layout/semantic.tsx; className, children; `isContainer`). - **(c) excluded — 17:** - `div` — deprecated on the JSON surface in favour of `box` (objectui#3965, PR objectui#6878); kept out by the ruling. - `code`, `summary`, `object` — bare namespace fallbacks of `field:code` / `field:summary` / `field:object` (inputs `[]`, no child slot); `view` — `plugin-view`'s bare key (SVG name coincidence).⚠️ The ruling's list names `code` as "registered elsewhere"; measured, the only `code` registration is the field widget (a code editor / `pre` block reading `value`). Declaring it would teach a tag whose children the runtime drops, so it is NOT declared — see Acceptance notes and the report's open question. - `kbd` — a `ui` component reading `keys` / `label`, not named by the ruling; not declared (additive later on a named need; retracting is a narrowing). - `form input textarea select switch` — JSON-surface form components; the tier excludes form controls by design (the exclusion list in `html-elements.tsx`). `progress`, `dialog` — `ui` components under tag names, not passthroughs, not named. - `button text image html` — already curated in `PUBLIC_BLOCKS`; they reach the manifest from there, unstamped. The (c) list is not prose: `apps/console/src/__tests__/html-tier-manifest.test.ts` holds every HTML-named bare registration to one of `PUBLIC_BLOCKS`, `HTML_TIER_INTRINSICS` or its exclusion ledger `EXCLUDED_HTML_NAMED` (13 entries, each with a reason, each pinned live), so a registration that newly squats on a tag name lands there by absence. ## H2 — the fork, the consumer map, and the marker decision **Premise measured true:** `getPublicConfigs()` through `manifestFromConfigs` is the sole source of the published manifest — objectui's `apps/console/dev/manifest-dump.tsx` / `scripts/dump-public-manifest.mjs`, and objectstack's `scripts/gen-sdui-manifest-node.mjs` (which imports the 16 registry modules from objectui's built tree at `.objectui-sha` and calls exactly `ComponentRegistry.getPublicConfigs()` then its lockstep copy's `manifestFromConfigs`). The manifest is ONE flat `components` map with no tier marker; `compile()` derives `allowedTags = Object.keys(manifest.components)`. So listing the html tags in `PUBLIC_BLOCKS` would have widened the curated JSON-surface vocabulary silently — the fork the card names. **Marker chosen: a per-entry `tier: 'html'` on `ManifestComponent`, carried from a projection-only stamp in `getPublicConfigs()`.** Why not a second registry read or a second manifest section: the objectstack generator calls `getPublicConfigs()` and nothing else, and `compile()` reads only `components` — either alternative needs an objectstack-side change before the gate accepts a single html tag, and the second-section shape would leave the gate refusing the tags until its lockstep copy was ported. With the per-entry marker the objectstack gate accepts the tags with NO reader change: it ignores the extra key. Consumers, and what each does with the marker: | reader | reads | change | |---|---|---| | objectstack `packages/lint` `validateJsxPages` → `compile()` | keys as whitelist; `validateTree` reads `inputs` / `namespace` | none — accepts the tags; ignores `tier` | | objectstack `scripts/check-sdui-manifest.mjs` | `components[k].type === k`, count, sha | none | | objectstack `scripts/gen-sdui-manifest-node.mjs` | `getPublicConfigs()` + lockstep `manifestFromConfigs` | none to run;⚠️ its lockstep copy of `packages/sdui-parser/src/{index,types,codegen}.ts` needs this port for the marker to reach the tracked file — until then the tags are declared there WITHOUT `tier`, which the gate treats identically | | objectui `react-page.tsx` `buildComponentScope` | `getPublicConfigs()` | skips `tier === 'html'` (else `P` / `A` / `Img` wrappers would be injected) | | objectui `generateBlockList` | manifest | sections the html tier; curated count unchanged (59) | | objectui `generateDts` | manifest | emits the tags into `JSX.IntrinsicElements` (the html-tier authoring surface) | | objectui `page.tsx` html-tier compile | `getKnownTypes()`, not the manifest | none (already accepted these tags) | | console census tests | `getPublicConfigs()` | `container-declaration-census` reads the curated contract as the unstamped entries; `public-contract` coverage lists filter by `PUBLIC_BLOCKS` and are unchanged | Reverse verification that consumers read the REBUILT `.d.ts`: a scratch console file assigning `tier: 'json'` to a `ManifestComponent` and to a `PublicComponentConfig` — `tsc --noEmit` exit 2, `TS2322: Type '"json"' is not assignable to type '"html"'` and `… to type '"html" | "public" | "internal" | undefined'`; the `'html'` assignments compiled. Probe removed; `git status` clean. ## H3 — the regenerated artefacts Generated with the repo's own generator (`buildArtifacts` in `packages/sdui-parser/scripts/gen-manifest.ts`) over the console registration graph with the 16 plugin modules eager-imported in `manifest-dump.tsx` order, once on the base tree and once on head: | artefact | base | head | |---|---|---| | `sdui.manifest.json` components | 59 | **106** (+47 added, 0 removed, 0 curated entries changed) | | `div` / `code` / `kbd` present | no | no | | entries with `tier: 'html'` / without | 0 / 59 | 47 / 59 | | `sdui-intrinsics.d.ts` intrinsic entries | 59 | 106 (+253 lines, −0) | | `sdui-blocks.md` | 65 lines, one table | 119 lines: `# SDUI public blocks (59)` unchanged + `## html tier intrinsic elements (47)` | Sample head entries: `a` → inputs className, href, target, rel, title, children (slot); `br` → className only (void, no slot); `label` → text (required), className (no slot). The base run's manifest differs from objectstack's tracked `sdui.manifest.json` only because the pin f8a9d0f predates this base (`cmp` differs at byte 1183) — the regeneration at the next pin bump is the `domain:cli` lane's step. ## Pins — red on base, green on head Two mutation legs (implementation files checked out at base, tests at head; trap-guarded restores to `HEAD`, proven by `git diff HEAD` empty and blob-hash equality — "RESTORE PROVEN" printed twice): - Leg 1 — `Registry.ts`, sdui-parser `index.ts` / `types.ts` / `codegen.ts`, `react-page.tsx` at base: `Test Files 5 failed (5)`, `Tests 17 failed | 61 passed (78)` — every behavioural pin red (the roster-only pins stay green by design). - Leg 2 — `react-page.tsx` alone at base: `Tests 1 failed | 8 passed (9)` — `a PascalCased html-tier tag is an unknown identifier on this tier` red (an `Img` wrapper was injected). - Green on head 5bd6270 (round 1) after the restores: `Test Files 9 passed (9)`, `Tests 114 passed (114)` over the five pin files plus `registration-uniqueness.test.tsx` (unchanged, green), `public-tier.test.ts`, `tier.test.ts`, `public-contract.test.ts`. Round 2 (head 6417e8d): the three re-quoted test files plus `react-page-scope` and `html-tier-manifest` re-run — `public-contract`, `container-declaration-census`, `layout-containers-declare-containment`, `react-page-scope`, `html-tier-manifest` — `Test Files 5 passed (5)`, `Tests 89 passed (89)`. Broader affected suites at head 5bd6270: console `html-tier-manifest`, `public-contract`, `public-block-binding-reach`, `record-block-record-reach`; components `react-page-scope`, `container-declaration-census`, `container-declaration-ratchet`, `layout-containers-declare-containment`, `registration-uniqueness`; plugin-tree `registration.publicTier-10064` — `Test Files 10 passed`, `Tests 137 passed` (plus the scratch generator run). ## Gates (exit captured before any pipe; verdict lines quoted) - Build: `turbo run build --filter="@object-ui/console^..." --concurrency=2` under the verify lock — `Tasks: 34 successful, 34 total`, `VERDICT command-exit 0`. - Type-check (`pnpm run type-check`, script echoed): `@object-ui/core` exit 0, `@object-ui/sdui-parser` exit 0, `@object-ui/components` exit 0, `@object-ui/console` exit 0. `--listFiles` counts the new / edited test files inside their programs (1, 1, 2, 1). - Lint, CI-parity: `turbo run lint` over the four packages — `Tasks: 5 successful, 5 total`, 0 errors; eslint `--format json` over the 14 touched files: 0 errors, 22 warnings, all pre-existing `no-explicit-any` lines not introduced here. - `check:control-bytes` — `✅ check-control-bytes: OK (scanned 8928 tracked text file(s))`. - `check:new-line-citations` — `VERDICT new-cross-file-line-citations: 0 new citation(s)`. - `check-changeset-presence` — round 2: `✅ 15 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)`; `changeset:check` — `✅ No changeset declares a major bump` and `✅ All workspace packages are in the changeset fixed group`. - Round 2 text gates on head 6417e8d: `check:control-bytes` `✅ OK (scanned 8928 tracked text file(s))`; `check:new-line-citations` `VERDICT new-cross-file-line-citations: 0 new citation(s)`; `check-changeset-claims` exit 0 (2 bodies judged, no negation; the re-read list is unchanged); `check-changeset-overwrite` exit 0 report-only — `1 changeset(s) added, 1 modified, 0 deleted`, the modified one being `.changeset/6740-flex-is-container.md` (the dated note above); `check-doc-links` — `Links are valid across 17 scan roots.` - `check:unreferenced-sources` OK; `check:pending-changeset-literals` `✅ No test source names a pending changeset`; `check:test-path-roots` OK; `check:doc-types` `✅ Every documented component type is registered`; `check:doc-fences` OK; `check:registry-bare-names` `✓ no new bare-name collision`; `check:esm-specifiers` (specifier leg) OK; `check:component-surface-parity` report-only exit 0. - `check:changeset-claims` (report-only): 13 pending changesets name a file this change touches; each re-read — the "forwards exactly seven keys per input" claims stay true (this adds a component-level key, no input key), and "none of the 8 is in the curated public contract" stays true (the seven now enter the manifest as html-tier entries, not curated). - NOT MEASURED locally, declared to CI: `check:readme-exports` (exit 1 = 8 self-imports unjudgeable because `packages/cli` and `packages/plugin-ai` are outside the built closure; 538 judged, 0 fabricated), `check:doc-snippets` (exit 2 PRECONDITION NOT MET, same unbuilt packages; the two edited guides gained prose only), `check:eager-closure` and `check:sdui-registration-pins` (need a console build), repo-wide `pnpm lint` (per-package eslint via turbo covered the four touched packages exactly as CI runs them; `eslint.config.js` declares no type-aware `project`, so this diff moves no untouched file's verdict). ## Serial Claim read: no open PR touches `gen-manifest.ts`, `public-blocks.ts` or `html-elements.tsx`. Round 1: `origin/main` fetched into the private ref `refs/issue-10735/main` (01700dc, four commits past base), `git merge-tree --write-tree HEAD refs/issue-10735/main` exit 0 (tree 368f01d). Round 2, before the push of 6417e8d: main re-fetched at 704e05b (nine commits past base: objectstack-ai#10741, objectstack-ai#10574, objectstack-ai#10736, objectstack-ai#10745, objectstack-ai#10749, objectstack-ai#10734, objectstack-ai#10733, objectstack-ai#10744, objectstack-ai#10752), `merge-tree --write-tree` exit 0 (tree 0d9bcb9); the set of files main moved since base is disjoint from this branch's files (`comm -12` empty), so no merge commit was needed. ## Acceptance notes - **`div` on the html tier is now split across two faces.** The published contract refuses it (`forbidden-tag`), while the renderer's declaration still reads `deprecated: { surfaces: ['json'] }` (objectui#4000) and the console's html-tier compile (`page.tsx`, `getKnownTypes()`) still accepts and renders it. The ruling's item 3 parks the renderer-side retirement in these notes: the reconciliation is a separate card (declare the `html` surface on `div`'s deprecation with `box` as the replacement, and decide whether the console compile should read the same roster). - **"Refused with `box` named as the replacement" has no mechanism today.** The gate's refusal is the parser's generic `forbidden-tag` text (the tag is not an allowed component); nothing in the manifest or the parser names `box` for an absent `div`. `Registry.deprecationFor('div', 'json').replacement` names it, but the gate never reaches the registry. Not implemented here (a new contract shape); raised in the report's open questions. - **`code`:** the `html-elements.tsx` header and ADR-0081 §2 both say `code` is "registered elsewhere"; the only registration is `field:code`'s bare fallback. On an html page today `code` resolves to that widget and drops its text — a reproducible runtime defect independent of this card; reported as an out-of-scope finding. - **`label` and `table` are declared as registered**, so the HTML-shaped `label` with text children draws `missing-required-prop` (`text`) and `not-a-container`, and a `table` authored with element children draws `not-a-container`; the registered spellings (`text="…"`, `columns` / `data`) are clean. Pinned. - **objectstack side, for the `domain:cli` lane:** regenerate `sdui.manifest.json` at the pin; port `packages/sdui-parser/src/{index,types,codegen}.ts` to the lockstep copy so the marker reaches the tracked file; the ledger's 200 `a` / `div` rows go stale (the `a` rows) or stay refused (the `div` rows, to be rewritten to `box` as the ruling says). - `semantic.tsx`'s comment ("none of these seven tags is in the curated `PUBLIC_BLOCKS` contract, so there is no injected identifier") remains literally true and its conclusion holds on two grounds now; not edited (no registration change). ## Pending changesets re-read (round 2, head 6417e8d) The `changeset-claim-re-read` bot lists 12 pending changesets naming files this diff touches (13 in round 1, `6740` now being one this change modifies). Each re-read paragraph by paragraph against head: - `5905-componentinput-inputtype-tombstone.md` — still true: `packages/sdui-parser/src/index.ts` still forwards exactly seven keys per input (`name`, `type`, `of`, `required`, `enum`, `binding`, `description`); this change's `tier` write sits at component level, beside `type` / `namespace` / `isContainer`, and adds no per-input key. - `5905-componentinput-retire-constraint-keys.md` — still true, same seven-keys-per-input claim, same reason. - `7493-componentinput-retire-label-defaultvalue-advanced.md` — still true, same seven-keys-per-input claim; `min` / `max` / `step` / `placeholder` / `label` / `defaultValue` / `advanced` still reach no manifest. - `6067-component-meta-derive-from-canonical.md` — still true: it describes the pre-6067 shape of `Registry.ts` in the past tense; at head `ComponentMeta` is still `CanonicalComponentMeta & RegistryComponentMetaExtras`, and `RegistryComponentMetaExtras.tier` is unchanged (`'public' | 'internal'`). The `'html'` value lives only on the `PublicComponentConfig` projection. - `componentinput-reexport-4972.md` — still true: `Registry.ts` still re-exports `ComponentInput` from `@object-ui/types` (`export type { ComponentInput } from '@object-ui/types'`). - `default-children-retired-5051.md` — still true: none of the three `ComponentMeta` declarations offers `defaultChildren`; this change adds no `ComponentMeta` key. - `6764-container-declaration-census.md` — still true as written: `react-page.tsx` drops containers from the react scope and reads `getPublicConfigs()`, and none of the 8 is in the CURATED public contract. The seven sectioning tags now ride that read as html-tier entries, stamped `tier: 'html'` and skipped, which leaves the claim's operative words ("curated public contract") intact; not corrected. - `9280-record-highlights-entry-icon-retired.md` — still true: `gen-manifest.ts` still serialises registry `inputs` into `sdui.manifest.json` and `sdui-intrinsics.d.ts` (the header now also names the html tier's stamped entries; behaviour unchanged). - `components-react-page-published-dts-text-5666.md` — still true: `react-pages.md`'s injected-scope note and §Styling still say plain HTML with inline `style`, not Tailwind; the guide's edited sentence quotes the skip line and adds "and every html-tier entry", nothing about styling. - `page-source-tailwind-framing-5461.md` — still true: `react-page.tsx` keeps both styling comments (the header note and the `buildComponentScope` note); this change adds a comment inside the loop and removes none. - `page-source-tailwind-prose-retraction-5469.md` — still true: `react-pages.md` §Styling wording is unchanged. - `palette-canonical-discussion-5495.md` — still true: `public-blocks.ts` still records `record:chatter` as the same block "under a Salesforce-familiar name"; this change adds a header paragraph and touches no roster entry. - `6740-flex-is-container.md` — CORRECTED (prose-only): its paragraph quotes the react-page skip line as `if (!tag || cfg.isContainer) continue;`, which this PR changed; a dated note in the objectui#10533 form (`⚠️ **Dated note, 2026-09-27 — the skip line quoted above has since gained a third arm — objectui#10735.**`) records the current line and keeps the entry as the reading of that change. Frontmatter byte-identical, md5 ce888ebde4729564f67bca69a7928743 before and after. `check-changeset-overwrite` reports the modification (report-only, exit 0). --- _Generated by [Claude Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #7759
Clause-②: yes
This PR covers group A of objectui#7759 only: the
header/globalFiltersrow of thecomplex.zod.ts#DashboardComponentSchemapair. The card stays open for group C (FormSchema.layout), which this PR does not touch.Dev session:
https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW(PM seatdomain:spec#1, claim comment 5851963524).Determination per key (ruling 5617465269)
@objectstack/specdeclares both keys, so principle 1 governs both: the zod mirror and the TypeScript declaration both follow the spec, in both directions.The stop condition did not fire on either key. It covers the case where principle 1 and the objectui#4580 route point opposite ways. Here the spec types every nested label involved as
I18nLabel(a string or an inline per-locale map), which is exactly the objectui#4580 form. So the two routes agree on the label, and principle 1 alone decides the remaining members.headerDashboardSchemahasheader: DashboardHeaderSchema.optional(). InDashboardHeaderActionSchema,labelisI18nLabelSchema,actionUrlisz.string()(required), andactionTypeisWidgetActionTypeSchema.optional(), the spec'sActionTypelabel: string,actionUrl?: string,actionType?: string'header'omission and inherits the spec's member through the Omit projection its other spec-owned keys useglobalFiltersDashboardSchemahasglobalFilters: z.array(GlobalFilterSchema).optional(). InGlobalFilterSchema, eachoptionsitem is a strict{ value, label: I18nLabelSchema }pair. InGlobalFilterOptionsFromSchema,labelFieldisz.string()(required) andfilterisFilterConditionSchema.optional()SpecGlobalFilter[], already by reference (objectui#4032). Mirror: the spec's shape spread into a localz.object, withoptionsandoptionsFromrestatedGlobalFilterSchemaisstripImportedDefaults(SpecGlobalFilterSchema), the spec's schema whole. The TypeScript face is unchangedThe pair is also in the ledger's
SPEC_DERIVED_PAIRS.grep evidence. Measured on the installed pin,
@objectstack/spec17.4.0, against itssrc/ui/dashboard.zod.ts:main(8d1f7ab): the same file, read withgit show, declares the same shape for both schemas. The only difference is an added.meta({ title })on several members, which does not change the input type.Measured on BASE
5c61e5241, before any edit: both keys were DISJOINTI measured with a throwaway probe (deleted afterwards). It checked the type face with
@ts-expect-errorlegs and ransafeParseon the mirror:label: { en: … }actionUrlactionType: 'bogus'options: ['EMEA'](bare-string shorthand)labeloptionsFromwith nolabelFieldoptionsFrom.filteras an arrayFilterConditiontype has an index signature, so it admits an array; the spec's runtime schema refuses one)label: { en: … }Premise check (the dispatch's H1–H5)
headeronly.globalFiltersmeasured WIDER for a different reason: the objectui-local option shorthand and the relaxedoptionsFrom. Its locale arm ran the other way: the mirror refused the map, which puts it on theKnownDriftside. The card body's group-A table inherits the same wording. The row was correct to exist; only its description was wrong.globalFiltersis declared asSpecGlobalFilter[].headerwas omitted from the spec projection and re-declared locally, and the spec does declare it (see the table above).DashboardComponentSchemamirror line isglobalFilters: z.array(GlobalFilterSchema). Thez.array(z.any())line belongs toDashboardConfigSchema, a different pair, which this PR leaves alone.mainagree on both shapes.KnownDriftkeepswidgetsfor this pair, because it still measures as drift.What changed
packages/types/src/complex.ts:DashboardComponentSchemaextends the Omit projection overDASHBOARD_SPEC_EXCLUDEDalone. The hand-writtenheaderis deleted, and the docblock records what it was and why it went.packages/types/src/zod/complex.zod.ts:GlobalFilterSchema = stripImportedDefaults(SpecGlobalFilterSchema). The localoptions/optionsFromrestatement and its delegatedsuperRefineare gone. The spec's own object-level refinement (the datedefaultValuecheck) arrives with the schema. TheSpecDashboardFieldsdocblock no longer calls the filter config "deliberately looser than spec's".packages/plugin-dashboard/src/DashboardRenderer.tsx: principle 1 requires the read site to implement every arm the spec declares.tActionLabelnow collapses theI18nLabelto the active language withpickLocalizedbefore using it, either as button text or as thedashboardActionLabelfallback. TheActionDefbuilt for each header action carries the resolved label too; that memo depends onlanguagerather than on a memoised resolver's identity (AGENTS.md [WIP] Enhance every detail of the designer #10). ThehandleClickfallbackactionUrl || labelwas unreachable, because the guard above it already returns whenactionUrlis empty; it is nowexecuteAction(actionUrl).Objects are not valid as a React child (found: object with keys {en, zh-CN}). The mirror accepted that exact document.packages/types/src/__tests__/zod-mirror-parity.test.ts:WiderThanDeclaredloses the pair's entry whole, and a departure note replaces it.WIDER_ARMSloses the pair's two rows.KnownDriftfor the pair goes from'header' | 'widgets' | 'globalFilters'to'widgets'.WiderThanDeclared, and from 46 / 86 to 46 / 84 forKnownDrift. Each move is recorded in the sentence beside its figure.packages/types/src/__tests__/dashboard-header-global-filters-spec-7759.test.ts, with checks on both faces for each key:Equalrows showing the declaration and the mirror input each equal the spec member, plus a control row showing the old restatement is not the spec member;@ts-expect-errorlegs for each refused spelling;safeParselegs that assert the issue envelope (codepluspath);scopedefault is authored, and a control document.twins-spec-by-reference-9736.test.ts:headeris no longer a twin-only omission. The case now asserts it equals the spec member; theslotshalf stays as it was.p1-spec-alignment.test.ts: the fixture{ label: 'Refresh', actionType: 'refresh' }was legal only under the removed restatement, and the spec and the mirror both refuse it. It is replaced by a spec-legal action that uses a locale-map label, plus an@ts-expect-errorleg forrefresh.report-chart-query-spec-parity.test.ts: the "pinned divergences" block, which asserted the local schema accepts what the spec refuses, now asserts both schemas give the same verdict on the same literals. The case that pinned the old composition is replaced by one that shows the date refinement still applies when the options are spec-legal.content/docs/plugins/plugin-dashboard.mdxgives header actions the spec's shape.content/docs/guide/dashboard-filters.mdsays objectui's own validator refuses what the platform refuses, and thatlabelFieldis required.packages/plugin-dashboard/README.mdexample comment addslabelField. Before this PR it taught anoptionsFromthe platform refuses at publish..changeset/7759-dashboard-header-global-filters-spec.md:@object-ui/typesminor, because the validator'sglobalFiltersaccept set narrows and theheaderinterface narrowsactionUrl/actionType.@object-ui/plugin-dashboardpatch, for the renderer fix. The changeset lists the breaking semantics in full.Accept-set delta (why Clause-② is yes)
invalid_typeatglobalFilters.N.options.M;invalid_unionat….options.M.label;optionsFromwith nolabelFieldis refused:invalid_typeat….optionsFrom.labelField;optionsFrom.filteris refused;unrecognized_keysatglobalFilters.N, where before it was stripped without a message.labelmay be an inline per-locale map. The spec has always allowed it, and the filter bar already resolves it withpickLocalized.header.actions[].actionUrlis required, andactionTypeis limited to the spec'sActionType.header.actions[].labelacceptsI18nLabel.@object-ui/core'snormalizeFilterOptionsstill converts a STORED bare-string option on read and logs its deprecation warning, on the objectstack#7917 option-② window ("the spec stays strict; the renderer's lift retires behind a survey"). This is the split objectui#4165 established: the schema refuses the legacy spelling and the read path lifts it. The filter bar also still falls back tovalueFieldwhenlabelFieldis absent. Renderers never parse through the mirror; the only runtime callers ofsafeValidateSchemaare the CLI'svalidate/check, which is the authoring door.File surface beyond the claim, declared
The claim listed
complex.ts,complex.zod.ts, this pair's ledger rows, one pin and one changeset. Measurement took this PR further, as the dispatch allows ("若实测表明…按生产者侧修,落点与理由写进报告和 PR 正文"):DashboardRenderer.tsxand its test. Aligning the declaration with the spec is the forced consequence: without the read-site change, a spec-legal document the mirror accepts would still crash the renderer, and principle 1 requires the read site to implement the spec's arm. objectui#9092 / PR fix(types): restore the inline-locale declared face on group A's three pairs #9364 set the same precedent forplugin-grid.packages/typespins listed above. Each one pinned the removed restatement or divergence.The PM should re-derive serial constraints on
packages/plugin-dashboard/src/DashboardRenderer.tsx; I did not scan other open PRs (dev contract).Tests and gates at head
70d26a8The final union ran after the last commit, and after merging
main, which brought in PR #10730 cleanly.pnpm --filter @object-ui/types type-checkpnpm exec vitest run packages/types/pnpm --filter @object-ui/core type-check/plugin-dashboard/plugin-designer/plugin-listturbo run build --concurrency=2, and the rebuilttypesdist was checked for the changepnpm exec vitest run packages/core/pnpm exec vitest run packages/plugin-dashboard/pnpm exec vitest run packages/plugin-designer/pnpm exec vitest run examples/schema-catalog/retired-view-dashboard-key-9533/app-generatorplus app-shelldashboard-schema/DashboardWidgetInspectoreslint --no-inline-config --format jsonon the 9 changed TS filesnode scripts/check-changeset-presence.mjs/check-changeset-no-major.mjspnpm check:new-line-citationspnpm check:control-bytes/check:spec-symbols/check:test-path-rootscheck:spec-symbolsfirst caught my local alias namedDashboardHeaderAction, a spec export name; it was renamedDashboardNodeHeaderActionpnpm check:doc-types/doc-snippets/doc-examples/doc-fences/doc-example-ids/docs-route-closure/readme-exports/doc-example-readers/shell-escape-residuepnpm check:changeset-claims/pending-changeset-literals/installed-pin-claims/handler-key-readsnode scripts/check-governed-queue-guard.mjs --testover the 13 branch pathsConsumer direction. The dependents checked were
core,plugin-dashboard,plugin-designerandplugin-list: the packages the census found importingDashboardComponentSchema.plugin-list's hit is a comment.examples/schema-catalogand the CLI / app-shell dashboard files cover the validator's accept set.Cross-package reverse check. I pasted a header action with
actionType: 'refresh'into a probe inplugin-dashboard's test project. It gaveTS2322against the rebuiltpackages/types/dist, which shows the consumer reads the new.d.ts. The probe was then removed.Reverse verification on committed HEAD
Each mutation went through
ablation-replace.mjs, and each restore was proven the same way: the blob equals HEAD andgit diff HEADis empty. The expected direction was registered before each run.GlobalFilterSchemaback to the local override (blob72d0b6a85b32toe52ba4acbdfe).tsc -p tsconfig.test.jsonexit 2. The failures: the pin's mirror and localEqualrows,assertionDriftMatchesLedgerandassertionWiderMatchesLedger(both namingcomplex.zod.ts#DashboardComponentSchema), andassertionWiderLedgerRecordsEveryKey(namingglobalFilters).72d0b6a85b32.headerrestored (blobace7e75f2adetocfb61c54e999).tscexit 2, with 10 errors. They cover: the pin's declaredEqualrow and its locale-map literal; two unused@ts-expect-errorlegs; the twins pin;p1-spec-alignment; and the three ledger reconciliations, which name the pair and the keyheader.tActionLabelpasses the authored label through unresolved. vitest: 2 failed / 8 passed, withObjects are not valid as a React child. Restored.NOT MEASURED
scripts/spec-main-shape-gate.mjs). Reason: its workflow builds@objectstack/specfrom objectstackmain, injects it, and runsTURBO_FORCE=true pnpm type-checkacross the whole workspace. That is farm-scale, and CI owns it. Mitigation: a source comparison of both touched spec schemas, pin 17.4.0 againstmain8d1f7ab, shows no shape difference.pnpm check:spec-floors. A prerequisite failure, not a finding: all 9 findings were[no-artifact]for packages outside the built closure. This diff does not move any@objectstack/specrange.pnpm lintand the fullpnpm testfarm. Left to CI. The ESLint run above is a narrowed run. Three facts make it a measurement: the population is eslint's own**/*.{ts,tsx}config; the file count, 9, comes from the--format jsonoutput; andeslint.config.jsdeclares noparserOptions/projectService, and no customeslint-rules/*rule reads the filesystem, so type-aware linting is off and this diff cannot move a verdict in an untouched file.Acceptance notes
normalizeFilterOptions, the bare-string lift in@object-ui/core, remains. For a validated document it is now a read-path tolerance only, and its retirement stays on objectstack#7917's schedule. It is not touched here.labelFieldfallback kept.DashboardFilterBar'sfrom.labelField ? … : …fallback tovalueFieldis unreachable for a validated document and was left in place for stored ones; nobody is assigned to carry it.namefallback kept. TheheaderActionDefsfallbackname: a.actionUrl || labelsurvives only for a stored action with noactionUrl, which the mirror has always refused. It was left in place, matching the click guard.globalFiltersdifference "inline-locale widening". Measured, it is the local shorthand, and the locale arm ran the other way (see H1). This is recorded so the group-C round does not inherit the wording.Claude-Session:trailer. The later commits carry a model-freeCo-Authored-By: Claude/Claude-Session:pair. History was not rewritten.FormSchema.layout) is untouched and remains on the card, which stays open.Generated by Claude Code