Repository navigation
refactor(types,plugin-kanban): retire ColumnWidthConfig / ColumnWidthConfigSchema (objectui#10582) - #10730
Conversation
…ConfigSchema No schema key references the type since the kanban arm's `columnWidths` retired with that arm (objectui#8802), and its one reader, `useColumnWidths`, was removed by objectui#8522. ADR-0049 enforce-or-remove: the interface, its Zod mirror, both `@object-ui/types` barrel exports and the `plugin-kanban` re-exports go; the mirror-parity ledger drops its pair and the population constant follows the registry. The kanban retirement docblock in complex.ts no longer says the type survives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW
Neither `@object-ui/types` entry point exports `ColumnWidthConfig` / `ColumnWidthConfigSchema`, `complex.ts` declares neither, and no TypeScript source under packages/, apps/ or examples/ declares or exports either name. Each reading carries a lit control on the sibling `CardTemplate` pair that did not retire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW
…eaking) Co-Authored-By: Claude Opus 5.5 <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:
This PR otherwise waits on its remaining CI and on an at-tier contract review before it leaves draft. Generated by Claude Code |
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…se the DrillDownConfig members they never read (objectui#10685) (objectstack-ai#10710) Fixes objectstack-ai#10685 Clause-②: no — a per-block narrowing, as the ruling recorded for objectui#9002 ## What this does This applies objectui#9002's ruling B (comment `5643445104`, a per-block refusal) to the sibling blocks of `object-metric`, as triage `5837818071` directed. Each block refuses, by name, the `DrillDownConfig` members it can never honour; the shared `DrillDownConfig` keeps every member for the blocks that read them. The seat's round-2 ruling (option A) widened the claim's file surface by `packages/types/src/objectql.ts` and `packages/types/src/zod/objectql.zod.ts`, so the `object-data-table` half shuts BOTH doors here, and merging this PR completes the card. - **`@object-ui/types`** adds two per-block shapes beside `ObjectMetricDrillDownConfig` in `data-display.ts`, exported on the root entry and on the `@object-ui/types/data-display` subpath: - `ObjectPivotDrillDownConfig`: `DrillDownConfig` plus a `mode?: never` tombstone (the objectstack-ai#10681 idiom: a `REFUSED BY NAME` docblock and `@deprecated`, naming `object-data-table` as the block that reads `mode`). - `ObjectDataTableDrillDownConfig`: `DrillDownConfig` plus `filter?: never`, `maxRows?: never` and `report?: never` tombstones, and `target?: 'drawer' | 'dialog'`. Each tombstone names the blocks that do read the key. - **TypeScript door.** `ObjectDataTableSchema.drillDown` in `objectql.ts` is typed with `ObjectDataTableDrillDownConfig`. The data table's component prop is anchored `Equal` to that schema by the two objectui#6576 pins, so the widget's prop refuses exactly what the schema refuses. - **Zod door.** The `object-data-table` mirror's `drillDown` in `objectql.zod.ts` is the shared `DrillDownConfigSchema` extended so that `filter`, `maxRows` and `report` are `retirementTombstone` arms whose messages name the key and the blocks that read it, and `target` is `z.enum(['drawer', 'dialog'])` with an error naming the refused `'navigate'`. `enabled`, `mode`, `title` and `columns` parse exactly as before. Nothing else in `objectql.zod.ts` changes. This is the door `objectui validate` and `objectui check` run (`safeValidateSchema`), so a stored JSON table config carrying one of the four is now refused there. - **`@object-ui/plugin-dashboard`**: `ObjectPivotTableProps.schema` gains `drillDown?: ObjectPivotDrillDownConfig`, and the component reads its drill config through that type instead of through `any`. - **Docblocks**: the shared `DrillDownConfig.mode` docblock names only `object-data-table` (none of charts, pivot tables or metric cards reads it) and says what each value does there; the `DrillDownConfig` class docblock no longer says "`target` is honoured by all of them", and the declared-keys pin's comment no longer names the table among the `navigateOnly` deliverers. - **`object-chart`: no type change** (see H4). Its refused set is pinned. - **Pins**: `drill-down-per-block-10685.test.ts` in `@object-ui/types` (one `describe` per block, `@ts-expect-error` per refused member with the read members as controls, plus a RUNTIME `describe` over the zod door), and `ObjectPivotTable.drillDownRefusal-10685.test.tsx` in `@object-ui/plugin-dashboard` (the published component's prop). The two 6576 anchor pins are re-pointed at the table shape, and the 7352 mirror pin's table leg runs over the values the table now accepts, with a non-vacuity check that the host-synthesised table configs stay in it. - **Changesets**: `10685-drilldown-per-block.md` (`@object-ui/types` `minor` with a breaking banner, `@object-ui/plugin-dashboard` `patch`) opens with the banner "Breaking for authored metadata, graded `minor` by this repo's convention": `objectui validate` / `safeValidateSchema` now refuse a stored `object-data-table` config carrying `drillDown.filter`, `.maxRows`, `.report` or `target: 'navigate'` that parsed green before, none was ever read, and the migration is to delete the key or write `'drawer'` / `'dialog'`. `minor` because the change narrows the published validator's accept set and the published `ObjectDataTableSchema` type, the grade every pending zod-door refusal in this repo carries (6881, 6951, 6972, 7322, 7963, 8801, 7352); objectui#9002's `patch` was graded on a TypeScript-door-only refusal, which does not describe this change. `@object-ui/plugin-dashboard` stays `patch`: a React-prop narrowing with no runtime door, the 9002 shape. The changeset also says `object-pivot`'s refusal is at the TypeScript door only (no zod mirror exists for it). H6: `.changeset/7352-drill-down-config-mirror.md` and `.changeset/6576-widget-schema-anchors.md` each have the falsified sentence scoped "at this change" plus a `⚠️ Dated note, 2026-09-25 … objectui#10685`; frontmatter md5 before and after: 7352 `ecedb10c5189b2c8841ea736b67c3cc6` both, 6576 `b25dbc26fe50c14d0b6f0cc8fe6cb9e1` both. ## H1: the table, re-measured per block Read sites were re-read on `origin/main` `5c61e524` this round. The one-member-at-a-time runtime probe (rendered DOM, data source calls and `openRecordList` calls compared against a control, with and without a `DrillNavigationProvider`) was run in round 1 at `bc97f9247` and is recorded in report comment `5839013332`; it was not re-run, and no read site moved since. | block | reads | never reads | |:--|:--|:--| | `object-chart` | `enabled` (`isDrillEnabled`), `filter` (`computeDrillFilter`), `title` (`resolveDrillTitle`), `target` including `'navigate'` (`openRecordList`), `maxRows` (drawer `pageSize`), `columns` | `mode`, `report`: already refused by the spec's `ChartDrillDown` type and by the zod mirror (`unrecognized_keys`) | | `object-pivot` | `enabled`, `filter`, `title`, and `target`, `columns`, `maxRows`, `report` through `DrillDownDrawer` | `mode` | | `object-data-table` | `enabled` (`isDrillEnabled`), `mode` (`'record'` opens the row; `'filter'` turns the drill off), a non-template `title`, `columns` (the record drawer's field list), `target` in two arms (`'dialog'`, anything else drawn as a drawer) | `filter`, `maxRows`, `report`; `target: 'navigate'` was drawn as a drawer | `@object-ui/core`'s `drill-down.ts` helpers read `enabled`, `filter` and `title` only; `DrillNavigationContext` carries one handler, `openRecordList(objectName, filter)`; the `data-table` renderer the table spreads its node into never names `drillDown`; `RecordDetailDrawer` takes `fields`, `title` and a two-armed `target`. The card's runtime-door bullet also names `ChartSchema.drillDown`. That is the plain `chart` node, whose renderer `ChartRenderer.tsx` reads no `drillDown`; it is outside this card's three blocks and deliberately untouched, the plain-`chart` remainder. ## H2: can never honour, or not yet honoured (one line each) - `object-pivot` · `mode`: never. Every pivot click point is an aggregated bucket (a cell, a row or column header, a total), so the pivot always drills through; `mode` chooses drill-to-record for a clicked ROW, and a pivot has none. This is the metric's reasoning in `5643445104`. - `object-chart` · `mode`, `report`: never, by the contract. Its drill type is `@objectstack/spec`'s `ChartDrillDown`, which declares neither; the spec's schema refuses both by name ("A chart segment is always an aggregate"). Already refused before this card; only pinned here. - `object-data-table` · `filter`, `maxRows`, `report`: never. All three configure a drilled LIST (its scope, its row cap, the report that replaces it). The table drills to the RECORD its row already is, `RecordDetailDrawer` lists nothing, and the block's `mode: 'filter'` arm is pinned as "ignored", so there is no list drill for them to act on. A row that drills through to a list of another object would need a drill-target member `DrillDownConfig` does not have: a new capability, not an unread member. - `object-data-table` · `target: 'navigate'`: refused, not honoured (H3). ## H3: `navigate` on the data table `object-chart` honours `navigate` by calling `openRecordList(objectName, merged filter)`, the object's LIST page scoped by the drill filter. `DrillNavigationContext` has no record-level handler and `@object-ui/app-shell`'s `useOpenRecordList` builds only the list route. The table's click point is one record, so the list page is the wrong destination, and a "navigate to this record" arm needs a new host seam, not a small hunk. So the table's shape refuses `navigate` on both doors and an author gets a type error, or a `validate` refusal, instead of a drawer. ## H4: where the shape differs from PR objectui#10681 - **`object-chart` keeps its spec-bound type.** `ObjectChartSchema.drillDown` is already per-block (the spec's `ChartDrillDown`), pinned `Equal` to that symbol by `object-chart-undeclared-keys-8885.test.ts`. A tombstoned local type would unbind it from the spec. A fresh `mode` or `report` literal is already a compile error and the zod mirror already refuses both by name. - **Registration sentences: none added.** The `object-pivot` and `object-data-table` registrations advertise no `drillDown` input (adding one widens the designer palette, a shape decision). `object-chart`'s description is pinned by `packages/plugin-charts/src/index.test.ts` to list exactly the six spec keys and NOT `mode` or `report`. - **Zod door narrowed for the table only**, because a runtime validator (`safeValidateSchema`, run by `objectui validate`) reads that block's drill members and accepted all four; the round-1 probe showed the same door refuses a bad value on `enabled`, so it does reach `drillDown`. `object-pivot` has no zod mirror, so its refusal is TypeScript-only and the changeset says so. ## Verification (measured at `430223947f` = `a1217b515` merged with `origin/main` `5c61e524`; head `64d43b6d67` = that head + a second merge of `origin/main` `41ae65b26` + the changeset commit) - **Round 3 delta and its gates (head `64d43b6d67`).** Against `430223947f` the 14 files of this card differ in two places only: the changeset (the `minor` grade and its banner) and one line of `index.ts` that `main`'s own objectstack-ai#10730 deleted (a retired `ColumnWidthConfig` export); the other 12 are byte-identical, and the branch's diff against `main` is still exactly these 14 files. Re-run at this head, each exit 0: `check-changeset-presence` (11 source files of 2 released packages, 1 changeset); `changeset:check`; `check-changeset-no-major` (the `Changeset Bump Policy` step); `check:changeset-claims` (36 pending changesets, unchanged reading); `check:pending-changeset-literals`; `check:new-line-citations` (`0 new citation(s)`); `check:control-bytes` OK; `check-changeset-overwrite` (report-only: it reports the 6576 and 7352 edits, the intended shape). No suite, type-check or ablation was re-run: the delta is a changeset plus `main`'s already-green commits merged with a clean `merge-tree`, and none of the measured sources moved. - Closure build `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-dashboard^...' run build`, under the verify lock: `VERDICT command-exit 0`. `dist/data-display.d.ts` and `dist/objectql.d.ts` carry `ObjectDataTableDrillDownConfig` (2 hits each), `dist/index.d.ts` carries `ObjectPivotDrillDownConfig`. - `pnpm --filter @object-ui/types run type-check` then `pnpm --filter @object-ui/plugin-dashboard run type-check`: `VERDICT command-exit 0`. `tsc --listFilesOnly` on each `tsconfig.test.json` lists the four types pins and the two plugin-dashboard pins, and plugin-dashboard's program reads `@object-ui/types` through `dist/data-display.d.ts` and `dist/objectql.d.ts`. - `pnpm exec vitest run --maxWorkers=2 packages/types/ packages/plugin-dashboard/` from the repo root: `Test Files 378 passed (378)`, `Tests 6562 passed (6562)`, `VERDICT command-exit 0`. - Gates, each exit 0: `check-changeset-presence` (11 source files of 2 released packages, 1 changeset); `check:new-line-citations` (`0 new citation(s)`); `check:control-bytes` OK; `changeset:check` (no `major`); `check:pending-changeset-literals`; `check:changeset-claims` (36 pending changesets name a touched file; the two whose prose is about drill-down, 7363 and 8885, were read and stay true, and the rest describe other regions of these files); `check:spec-symbols`; `check:self-import`; `check:esm-specifiers`; `check:test-path-roots`; `check:vi-mock-specifiers`; `check:handler-key-reads`; `check:unreferenced-sources`; `check:metadata-write-doors`; `type-check:coverage`; `check:component-surface-parity`; `check:doc-types`; `check-governed-queue-guard --test` over the 14 paths: NOT GOVERNED. - NOT MEASURED: `check:readme-exports` exits 1 on a prerequisite (385 README self-imports unjudgeable because other packages' `dist/index.d.ts` are not built); this diff edits no README. Left to CI. - ESLint was run on the 11 touched source and test files, not the whole repo. The narrowing is a measurement, for three reasons: the config is the root `eslint.config.js` and neither package has its own; `--format json` reports 11 files, 0 errors and 80 warnings (all `no-explicit-any`, none on a line this branch added, by intersecting message lines with the branch's added-line set); `--print-config` shows empty `parserOptions`, so there is no type-aware linting and the diff cannot move any untouched file's verdict. Under `--no-inline-config` the same run shows 1 error, the block-disabled `SpecFormField` alias in `index.ts` that is on `main` and outside this diff; the package `lint` scripts are `eslint .`, which honours the disable. Full `pnpm lint` is left to CI. - CI on this head: `Inert vi.mock Specifier Check` is red on `main` since `f9c06ef6a` (PR objectui#10729), filed as objectui#10732. Confirmed locally in a no-install probe worktree at `5c61e524`: exactly one hit, `apps/console/src/__tests__/filterContextTokensSweep-10666.test.tsx`, outside this surface; in an installed worktree the specifier is found through `node_modules` and the checker passes. Not touched here. ## Red on base, then green on head The five touched sources (`data-display.ts`, `index.ts`, `objectql.ts`, `zod/objectql.zod.ts`, `ObjectPivotTable.tsx`) were swapped to their `origin/main` `5c61e524` blobs with the HEAD pins in place, then restored to HEAD (every blob equal to `HEAD`, `git diff HEAD` empty, types dist rebuilt, markers back to 2 and 1). - Types test program (`tsc -p tsconfig.test.json`): exit 2. `TS2305`/`TS2724` on the two new type names imported by the per-block pin and the 6576 pin, and nine `TS2578 Unused '@ts-expect-error'` on the per-block pin's refusals (the pivot's two, the table's seven). - Vitest on the per-block pin and the 7352 mirror pin: `Tests 4 failed | 59 passed (63)`, the four failures being the zod describe's `filter`, `maxRows`, `report` and `target` rows. - Types dist built from base sources (markers 0 and 0), then the plugin-dashboard test program: exit 2, `TS2305` on the 6576 anchor's `ObjectDataTableDrillDownConfig` import and two `TS2578` on the pivot pin. ## Ablations (each through `ablation-replace.mjs`: anchor hit once, blob changed, command run, restored to the HEAD blob with an empty `git diff HEAD`) - **TS wiring.** `ObjectDataTableSchema.drillDown` back to the shared `DrillDownConfig`, with a JSDoc marker `ABLATED_10685_TS1` on the line. Types program exit 2: `TS2344` on `_SchemaTakesTheTableShape`, `TS2578` on `nodeWithFilter` and `nodeWithNavigate`, `TS2344` on the 6576 pin's `assertionDrillDownDeclared`, plus `zod-mirror-parity.test.ts` going red with `objectql.zod.ts#ObjectDataTableSchema` (the repo's TS/zod parity pin sees the two doors disagree: a second, independent instrument). Dist leg: types rebuilt, marker count in `dist/objectql.d.ts` 1, plugin-dashboard program exit 2 (`TS2344` on `ObjectDataTable.schemaAnchor-6576`'s `Equal`); after the restore rebuild the marker count is 0 and `ObjectDataTableDrillDownConfig` is back to 2. A first pass of this leg read the dist marker with the wrong grep (tsc emits the `import()` type with single quotes) and got 0; the downstream red was the same in both passes, and the marker form above is the evidence. - **Each table tombstone alone** (`never` to `DrillDownConfig['KEY']`): `filter` gives `TS2578` on `withFilter` and `nodeWithFilter`; `maxRows` on `withMaxRows`; `report` on `withReport`; `target` widened back to the shared union gives `TS2578` on `withNavigate` and `nodeWithNavigate`. Each also turns `zod-mirror-parity` red as above. - **Pivot tombstone** neutralised (marker `ABLATED_10685_PIVOT`): types program `TS2578` on `withMode` and `handedAcross`; dist marker 1; plugin-dashboard program exit 2 with two `TS2578` on the pivot pin; marker 0 after the restore rebuild. - **Chart binding** (`ObjectChartSchema.drillDown` to the shared type): `TS2578` on the chart pin's `withMode` and `withReport`, plus the 8885 pin's `TS2344` and `TS2578`. - **Zod door** back to the shared mirror: `Tests 4 failed | 59 passed (63)`, the four refusal rows. **Each zod member alone** (the tombstone renamed away, or `target` widened to include `navigate`): exactly one failure each, its own row, `62 passed`. Direction: red as predicted in every leg, with one increase (the parity pin) beyond the prediction. ## Serial `origin/main` `5c61e524` and PR objectui#8941's head `bb7d5e718` (`claude/pr-7058-lucide-react-1.41.0`) were fetched into private refs; `git merge-tree --write-tree` exits 0 against both. PR objectui#10707 and PR objectui#10643 have merged; objectui#10657 has no open PR and no branch (its test is on `main`). `main` had moved in three of this PR's files (the `filter` docblocks in `objectql.ts`, the `masked` column in `data-display.ts`, the `filter` describes in `objectql.zod.ts`), so `main` was merged in (`430223947f`, a merge commit, no conflicts). In round 3 `main` had moved again in one file (`index.ts`, objectstack-ai#10730's retired export), so `origin/main` `41ae65b26` was merged in the same way (`4f2f7a530f`, no conflicts). At head `64d43b6d67`, `merge-tree --write-tree` exits 0 against `origin/main` `41ae65b26` and against the three open PRs that touch these files: PR objectui#10734 (`856ecc953`, `ObjectChartSchema` regions of `objectql.ts` / `objectql.zod.ts`, disjoint), PR objectui#10714 (`218f642484`, `data-display.ts`) and PR objectui#8941 (`bb7d5e718`, `objectql.ts`). The branch diff against `main` is exactly the 14 files of this card. ## Acceptance notes - **`mode: 'filter'` on `object-data-table`** turns the row drill off, as `ObjectDataTable.drill.test.tsx` pins, instead of drilling through. The rewritten docblock says so. Value-level, outside the card's member set; recorded here only. - **`maxRows`** reaches each drill list as the drill table's `pageSize` (`DrillDownDrawer` and the chart's drawer), a page size rather than the "Hard cap on rows fetched" the shared docblock promises. Observation, outside this card. - **`object-chart`, non-fresh values.** A value typed as the shared `DrillDownConfig` is still assignable to `ObjectChartSchema.drillDown`, because the spec type omits `mode` and `report` rather than tombstoning them. No producer in the tree hands a typed shared config across. - **Spec guidance text.** `@objectstack/spec`'s `ChartDrillDownSchema` refusal text for `mode` calls it "a TABLE / PIVOT / METRIC drill key"; on `object-pivot` and `object-metric` it is now refused. Reported to the seat in round 1 for the spec repository; not touched here. - **PR assignee.** Set to `os-elon-musk` in round 1 (read back matching); found empty at the start of this round and set again to match the card's assignee, per the dispatch rule. Session: `https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN` --- _Generated by [Claude Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…e the spec member on both faces (objectui#7759 group A) (objectstack-ai#10744) Part of objectstack-ai#7759 Clause-②: yes This PR covers group A of objectui#7759 only: the `header` / `globalFilters` row of the `complex.zod.ts#DashboardComponentSchema` pair. The card stays open for group C (`FormSchema.layout`), which this PR does not touch. Dev session: `https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW` (PM seat `domain:spec#1`, claim comment 5851963524). ## Determination per key (ruling 5617465269) `@objectstack/spec` declares 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. | key | does the spec declare it? | before | after | |---|---|---|---| | `header` | **yes**. `DashboardSchema` has `header: DashboardHeaderSchema.optional()`. In `DashboardHeaderActionSchema`, `label` is `I18nLabelSchema`, `actionUrl` is `z.string()` (required), and `actionType` is `WidgetActionTypeSchema.optional()`, the spec's `ActionType` | mirror: the spec's member, by reference. Declaration: a hand-written restatement whose actions were typed `label: string`, `actionUrl?: string`, `actionType?: string` | the declaration drops the extra `'header'` omission and inherits the spec's member through the Omit projection its other spec-owned keys use | | `globalFilters` | **yes**. `DashboardSchema` has `globalFilters: z.array(GlobalFilterSchema).optional()`. In `GlobalFilterSchema`, each `options` item is a strict `{ value, label: I18nLabelSchema }` pair. In `GlobalFilterOptionsFromSchema`, `labelField` is `z.string()` (required) and `filter` is `FilterConditionSchema.optional()` | declaration: `SpecGlobalFilter[]`, already by reference (objectui#4032). Mirror: the spec's shape spread into a local `z.object`, with `options` and `optionsFrom` restated | the mirror's `GlobalFilterSchema` is `stripImportedDefaults(SpecGlobalFilterSchema)`, the spec's schema whole. The TypeScript face is unchanged | The pair is also in the ledger's `SPEC_DERIVED_PAIRS`. **grep evidence.** Measured on the installed pin, `@objectstack/spec` 17.4.0, against its `src/ui/dashboard.zod.ts`: ``` grep -nE 'header: DashboardHeaderSchema|globalFilters: z.array\(GlobalFilterSchema\)|label: I18nLabelSchema|actionUrl: z.string|labelField: z.string|filter: FilterConditionSchema' src/ui/dashboard.zod.ts ``` - **Pin (17.4.0):** the command hits all six members. - **objectstack `main` (`8d1f7ab`):** the same file, read with `git 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 DISJOINT I measured with a throwaway probe (deleted afterwards). It checked the type face with `@ts-expect-error` legs and ran `safeParse` on the mirror: | spelling | mirror (safeParse) | declaration (tsc) | |---|---|---| | header action `label: { en: … }` | accepted | refused | | header action with no `actionUrl` | refused | accepted | | header action `actionType: 'bogus'` | refused | accepted | | filter `options: ['EMEA']` (bare-string shorthand) | accepted | refused | | filter option with no `label` | accepted | refused | | `optionsFrom` with no `labelField` | accepted | refused | | `optionsFrom.filter` as an array | accepted | accepted (the declared `FilterCondition` type has an index signature, so it admits an array; the spec's runtime schema refuses one) | | unknown key inside a filter | accepted, and stripped without a message | n/a | | filter option `label: { en: … }` | **refused** | accepted | ## Premise check (the dispatch's H1–H5) - **H1: partly false.** The ledger docblock said both keys "carry the inline-locale widening one level down". That holds for `header` only. `globalFilters` measured WIDER for a different reason: the objectui-local option shorthand and the relaxed `optionsFrom`. Its locale arm ran the other way: the mirror refused the map, which puts it on the `KnownDrift` side. The card body's group-A table inherits the same wording. The row was correct to exist; only its description was wrong. - **H2: held.** `globalFilters` is declared as `SpecGlobalFilter[]`. `header` was omitted from the spec projection and re-declared locally, and the spec does declare it (see the table above). - **H3: held.** The `DashboardComponentSchema` mirror line is `globalFilters: z.array(GlobalFilterSchema)`. The `z.array(z.any())` line belongs to `DashboardConfigSchema`, a different pair, which this PR leaves alone. - **H4: held.** The pin and objectstack `main` agree on both shapes. - **H5: held.** Both rows were retired because the pair now measures clean in both directions. `KnownDrift` keeps `widgets` for this pair, because it still measures as drift. ## What changed - **`packages/types/src/complex.ts`**: `DashboardComponentSchema` extends the Omit projection over `DASHBOARD_SPEC_EXCLUDED` alone. The hand-written `header` is deleted, and the docblock records what it was and why it went. - **`packages/types/src/zod/complex.zod.ts`**: `GlobalFilterSchema = stripImportedDefaults(SpecGlobalFilterSchema)`. The local `options` / `optionsFrom` restatement and its delegated `superRefine` are gone. The spec's own object-level refinement (the date `defaultValue` check) arrives with the schema. The `SpecDashboardFields` docblock 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. `tActionLabel` now collapses the `I18nLabel` to the active language with `pickLocalized` before using it, either as button text or as the `dashboardActionLabel` fallback. The `ActionDef` built for each header action carries the resolved label too; that memo depends on `language` rather than on a memoised resolver's identity (AGENTS.md objectstack-ai#10). The `handleClick` fallback `actionUrl || label` was unreachable, because the guard above it already returns when `actionUrl` is empty; it is now `executeAction(actionUrl)`. - **Red-first on BASE source:** both new renderer cases threw `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`**: - `WiderThanDeclared` loses the pair's entry whole, and a departure note replaces it. `WIDER_ARMS` loses the pair's two rows. - `KnownDrift` for the pair goes from `'header' | 'widgets' | 'globalFilters'` to `'widgets'`. - The header figures move from 7 / 9 / 9 (split 5 / 4 / 0 / 0) to 6 / 7 / 7 (5 / 2 / 0 / 0) for `WiderThanDeclared`, and from 46 / 86 to 46 / 84 for `KnownDrift`. Each move is recorded in the sentence beside its figure. - **New pin, `packages/types/src/__tests__/dashboard-header-global-filters-spec-7759.test.ts`**, with checks on both faces for each key: - `Equal` rows 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-error` legs for each refused spelling; - `safeParse` legs that assert the issue envelope (`code` plus `path`); - a check that no `scope` default is authored, and a control document. - **Existing pins whose premise this PR removes** (fixture triage): - `twins-spec-by-reference-9736.test.ts`: `header` is no longer a twin-only omission. The case now asserts it equals the spec member; the `slots` half 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-error` leg for `refresh`. - `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. - **Docs** (AGENTS.md objectstack-ai#2; ruling principle 3): - `content/docs/plugins/plugin-dashboard.mdx` gives header actions the spec's shape. - `content/docs/guide/dashboard-filters.md` says objectui's own validator refuses what the platform refuses, and that `labelField` is required. - The `packages/plugin-dashboard/README.md` example comment adds `labelField`. Before this PR it taught an `optionsFrom` the platform refuses at publish. - **`.changeset/7759-dashboard-header-global-filters-spec.md`**: `@object-ui/types` **minor**, because the validator's `globalFilters` accept set narrows and the `header` interface narrows `actionUrl` / `actionType`. `@object-ui/plugin-dashboard` **patch**, for the renderer fix. The changeset lists the breaking semantics in full. ## Accept-set delta (why Clause-② is yes) - **Validator, narrowed:** - the bare-string option shorthand is refused: `invalid_type` at `globalFilters.N.options.M`; - a label-less option is refused: `invalid_union` at `….options.M.label`; - an `optionsFrom` with no `labelField` is refused: `invalid_type` at `….optionsFrom.labelField`; - an array `optionsFrom.filter` is refused; - an unknown key in a filter is now refused by name with `unrecognized_keys` at `globalFilters.N`, where before it was stripped without a message. - **Validator, widened:** an option `label` may be an inline per-locale map. The spec has always allowed it, and the filter bar already resolves it with `pickLocalized`. - **Interface, narrowed:** `header.actions[].actionUrl` is required, and `actionType` is limited to the spec's `ActionType`. - **Interface, widened:** `header.actions[].label` accepts `I18nLabel`. - **Runtime, deliberately unchanged:** `@object-ui/core`'s `normalizeFilterOptions` still 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 to `valueField` when `labelField` is absent. Renderers never parse through the mirror; the only runtime callers of `safeValidateSchema` are the CLI's `validate` / `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.tsx` and 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 objectstack-ai#9364 set the same precedent for `plugin-grid`. - **The three existing `packages/types` pins listed above.** Each one pinned the removed restatement or divergence. - **Three docs files.** They follow the implementation. 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 `70d26a8` The final union ran after the last commit, and after merging `main`, which brought in PR objectstack-ai#10730 cleanly. | command | reading | |---|---| | `pnpm --filter @object-ui/types type-check` | exit 0 (VERDICT command-exit 0) | | `pnpm exec vitest run packages/types/` | 242 files / 5294 tests passed | | `pnpm --filter @object-ui/core type-check` / `plugin-dashboard` / `plugin-designer` / `plugin-list` | exit 0 each, script name echoed. The dependency closure was built first with `turbo run build --concurrency=2`, and the rebuilt `types` dist was checked for the change | | `pnpm exec vitest run packages/core/` | 173 / 3504 passed | | `pnpm exec vitest run packages/plugin-dashboard/` | 136 / 1283 passed | | `pnpm exec vitest run packages/plugin-designer/` | 24 / 175 passed | | `pnpm exec vitest run examples/schema-catalog/` | 34 / 2198 passed | | CLI `retired-view-dashboard-key-9533` / `app-generator` plus app-shell `dashboard-schema` / `DashboardWidgetInspector` | 4 files / 77 tests passed | | `eslint --no-inline-config --format json` on the 9 changed TS files | 9 files, 0 errors, 21 warnings; the same files at BASE carry the same 21 | | `node scripts/check-changeset-presence.mjs` / `check-changeset-no-major.mjs` | exit 0 / exit 0 | | `pnpm check:new-line-citations` | exit 0: "0 new citation(s)" | | `pnpm check:control-bytes` / `check:spec-symbols` / `check:test-path-roots` | exit 0 each. `check:spec-symbols` first caught my local alias named `DashboardHeaderAction`, a spec export name; it was renamed `DashboardNodeHeaderAction` | | `pnpm check:doc-types` / `doc-snippets` / `doc-examples` / `doc-fences` / `doc-example-ids` / `docs-route-closure` / `readme-exports` / `doc-example-readers` / `shell-escape-residue` | exit 0 each; doc snippets "676 of 676 block(s) judged, 0 failed" (after the gate's own scoped build) | | `pnpm check:changeset-claims` / `pending-changeset-literals` / `installed-pin-claims` / `handler-key-reads` | exit 0 each | | `node scripts/check-governed-queue-guard.mjs --test` over the 13 branch paths | NOT GOVERNED | **Consumer direction.** The dependents checked were `core`, `plugin-dashboard`, `plugin-designer` and `plugin-list`: the packages the census found importing `DashboardComponentSchema`. `plugin-list`'s hit is a comment. `examples/schema-catalog` and 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 in `plugin-dashboard`'s test project. It gave `TS2322` against the rebuilt `packages/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 and `git diff HEAD` is empty. The expected direction was registered before each run. - **A. Mirror: `GlobalFilterSchema` back to the local override** (blob `72d0b6a85b32` to `e52ba4acbdfe`). - `tsc -p tsconfig.test.json` exit 2. The failures: the pin's mirror and local `Equal` rows, `assertionDriftMatchesLedger` and `assertionWiderMatchesLedger` (both naming `complex.zod.ts#DashboardComponentSchema`), and `assertionWiderLedgerRecordsEveryKey` (naming `globalFilters`). - vitest: 6 failed / 48 passed. - Restored to `72d0b6a85b32`. - **B. Declaration: hand-written `header` restored** (blob `ace7e75f2ade` to `cfb61c54e999`). - `tsc` exit 2, with 10 errors. They cover: the pin's declared `Equal` row and its locale-map literal; two unused `@ts-expect-error` legs; the twins pin; `p1-spec-alignment`; and the three ledger reconciliations, which name the pair and the key `header`. - The runtime legs stayed 9 / 9 passed, as registered: the mirror does not move under this mutation. - Restored. - **C. Renderer: `tActionLabel` passes the authored label through unresolved.** vitest: 2 failed / 8 passed, with `Objects are not valid as a React child`. Restored. ## NOT MEASURED - **Spec Main Shape Gate** (`scripts/spec-main-shape-gate.mjs`). Reason: its workflow builds `@objectstack/spec` from objectstack `main`, injects it, and runs `TURBO_FORCE=true pnpm type-check` across the whole workspace. That is farm-scale, and CI owns it. Mitigation: a source comparison of both touched spec schemas, pin 17.4.0 against `main` `8d1f7ab`, 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/spec` range. - **Repo-wide `pnpm lint` and the full `pnpm test` farm.** 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 json` output; and `eslint.config.js` declares no `parserOptions` / `projectService`, and no custom `eslint-rules/*` rule reads the filesystem, so type-aware linting is off and this diff cannot move a verdict in an untouched file. ## Acceptance notes - **Runtime lift kept.** `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. - **`labelField` fallback kept.** `DashboardFilterBar`'s `from.labelField ? … : …` fallback to `valueField` is unreachable for a validated document and was left in place for stored ones; nobody is assigned to carry it. - **`name` fallback kept.** The `headerActionDefs` fallback `name: a.actionUrl || label` survives only for a stored action with no `actionUrl`, which the mirror has always refused. It was left in place, matching the click guard. - **Card body.** The card body's group-A table calls the `globalFilters` difference "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. - **Commit trailers.** The first commit carries only the `Claude-Session:` trailer. The later commits carry a model-free `Co-Authored-By: Claude` / `Claude-Session:` pair. History was not rewritten. - **Scope.** Group C (`FormSchema.layout`) is untouched and remains on the card, which stays open. --- _Generated by [Claude Code](https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #10582
Clause-②: yes
What this does
Deletes
ColumnWidthConfig(packages/types/src/complex.ts) and its Zod mirrorColumnWidthConfigSchema(packages/types/src/zod/complex.zod.ts), together with both@object-ui/typesbarrel exports (src/index.ts,src/zod/index.zod.ts) and the@object-ui/plugin-kanbanre-exports (src/types.ts,src/index.tsx). This is ADR-0049 enforce-or-remove on a declaration that no key references and nothing reads. The governing direction is triage's written retire direction on the card (first grade, comment5831964491), not a maintainer ruling.complex.tsno longer says the type survives. It now saysColumnWidthConfiglost its one reader to objectui#8522 and is deleted here, and it points at the pin.zod-mirror-parity.test.ts: thecomplex.zod.ts#ColumnWidthConfigSchemapair leavesMIRRORS,Declaredand the two import lists.EXPECTED_MIRROR_PAIRSfollows the registry down by one, which is what the census failure message instructs ("update EXPECTED_MIRROR_PAIRS ... and nothing else"). The header sentence that names the pair under objectui#7664 is history and stays as it is.packages/types/src/__tests__/column-width-config-retired-10582.test.ts..changeset/10582-retire-column-width-config.md:minorfor@object-ui/typesand@object-ui/plugin-kanban, with the breaking semantics and a migration section. Nomajor.Premise, re-measured on
origin/main5c61e5241(in-repo evidence only)git grep ColumnWidthConfigover the whole tracked tree. Outside.changeset/, it hits only the declaration, the mirror, the four re-exports, docblocks and the parity ledger. Per directory, case-insensitive, with a lit control on the same instrument:content/: 0 hits (controlKanbanSchema: 19);examples/: 0 hits (controlKanbanSchema: 3);apps/: 0 hits (control@object-ui/types: 32);skills/: 0 hits (control@object-ui/types: 17). There is no governed row to split out.defaultWidthappears only in the type and the mirror. No schema key references either name. The one key that did, the'kanban'arm'scolumnWidths, retired with that arm (objectui#8802).one-authority-per-exported-name-6273.test.tshas no ledger row for the name. It has one historical comment inside the objectui#7664 narrative, and that comment stays true as history. The file is not edited.@object-ui/plugin-kanban@17.6.0exportsColumnWidthConfigfrom its entry. There it is the package's own declaration, andKanbanSchema.columnWidthsis typed with it.@object-ui/types@17.6.0exports neither name (lit control:BaseSchemaappears in 42 files of the same tarball). The pair entered@object-ui/typesin this unreleased window with objectui#7664, so for that package it leaves before any version publishes it. The changeset says so.ActionCallback/ActionCallbackSchema. There, a standalone retired pair is DELETED rather than tombstoned, and its absence is pinned (action-callback-retired-7068.test.ts). A tombstone (?: never/retirementTombstone()) exists for a member key under.passthrough(). A standalone type has no such hatch: an importer gets TS2305, "has no exported member", which is the loud refusal. The objectui#8522 retirement ofuseColumnWidths(PR objectui#10549) was a plain deletion too. I found no tombstone-export convention for retired type names in this repo.The pin, and proof that it can fail
The pin has three readings, each with a lit control on the sibling
CardTemplate/CardTemplateSchemathat did not retire:ColumnWidthConfigSchemaname isinneithercomplex.zod.tsnor the@object-ui/types/zodbarrel, checked at runtime.complex.tsdeclares noColumnWidthConfig, and neither entry file (index.ts,zod/index.zod.ts) exports either name. Both are read off the AST, so the docblocks that tell the story do not count.packages/,apps/orexamples/declares or exports either name. A re-declaration in another package would type-check cleanly, and only this reading sees it.Each mutation leg went through
ablation-replace.mjs. Its anchor had to hit, the landing was proven by the blob hash, and the restore was proven by blob == HEAD and an emptygit diff HEAD. The pin imports relativesrcpaths, not a packageexportsspecifier, so no dist build sits between the mutation and the reading.export const ColumnWidthConfigSchemaincomplex.zod.tsexport interface ColumnWidthConfiginplugin-kanban/src/types.tspackages/plugin-kanban/src/types.ts: ColumnWidthConfigColumnWidthConfigto theindex.tsexport listThe first A1 attempt was a null run. The replacement contained the anchor, so the tool refused because the anchor count could not drop. It restored the file and measured nothing. The leg was rerun with a disjoint anchor, and the table records that rerun.
Reverse check on the consumer side: the
@object-ui/typesdist was rebuilt, and its oneColumnWidthConfighit is the docblock sentence. I re-plantedColumnWidthConfiginplugin-kanban/src/types.ts's re-export list and ranpnpm --filter @object-ui/plugin-kanban type-check. It went red witherror TS2305: Module '"@object-ui/types"' has no exported member 'ColumnWidthConfig', and the restore was proven. This shows the green type-check reads the rebuilt.d.ts, not a cached one.Verification, on HEAD
1f612da39Each exit code was captured before any pipe.
pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-kanban^...' build: exit 0 (13 of 47 projects, including@object-ui/types).pnpm --filter @object-ui/types type-check: exit 0.pnpm --filter @object-ui/plugin-kanban type-check: exit 0.pnpm exec vitest run packages/types/: exit 0, 241 files and 5283 tests passed.pnpm exec vitest run packages/plugin-kanban/: exit 0, 64 files and 420 tests passed.pnpm exec vitest runover the sixscripts/__tests__suites that read a touched file (one-authority-per-exported-name-6273,check-handler-key-read-sites,check-prompt-component-keys,docs-build-trigger,vite-objectstack-spec-dist,vite-types-zod-lazy): exit 0, 6 files and 235 tests passed.node scripts/check-changeset-presence.mjs: exit 0.node scripts/check-changeset-no-major.mjs: exit 0.node scripts/check-changeset-fixed.mjs: exit 0.pnpm check:new-line-citations: exit 0, with the verdict line reading 0 new citations.pnpm check:control-bytes: exit 0.pnpm check:spec-symbols: exit 0.pnpm check:test-path-roots: exit 0.pnpm check:handler-key-reads: exit 0.pnpm check:doc-types: exit 0.node scripts/check-governed-queue-guard.mjs --testover the 9 changed paths: exit 0, NOT GOVERNED.--no-inline-configon the 8 touched.ts/.tsxfiles: 1 error and 37 warnings. Every one of them is pre-existing. The per-file counts are identical when the same command lints the base blob of each file through--stdin, and the new pin has none. The 1 error is theno-restricted-importsfinding on theFormFieldre-export inpackages/types/src/index.ts, and it is present on the base blob.Acceptance notes
Pending changesets that this retirement supersedes.
check:changeset-claims(report-only) asked for 35 pending bodies to be re-read. Four of them name the type:ColumnWidthConfigstays exported from@object-ui/typesand from this package." That was true of that change.@object-ui/types.This PR's own entry states the supersession in the same release section, for both packages. It edits none of those four files, because they are outside the claimed file surface. If the seat prefers an in-body dated note on the objectui#8522 entry, following the precedent where objectui#8522 added one to objectui#7742, it is a one-paragraph, prose-only edit.
Shared ledger. objectui#10608 may also delete rows from
zod-mirror-parity.test.tsand moveEXPECTED_MIRROR_PAIRS. Whichever PR lands second mergesmain(no rebase, no force-push) and takes the constant from the registry, as the census message says. PR objectui#10710 editspackages/types/src/index.tsin a separate hunk.Spec Main Shape Gate: not applicable. Neither deleted declaration is spec-derived: both are plain local declarations with no
@objectstack/specreference, so objectui's compile against objectstackmaincannot move with this diff.check:readme-exportsnot run. No README names either symbol (whole-tree grep above), and the gate needs every package built.check:test-path-rootsclassifies the pin's four filesystem reads as "unknown", because they go through a parameter or a loop binding. Every one of them is rooted at the pin's ownimport.meta.url, which is the same shape as theaction-callback-retired-7068walk.Dev session:
https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW(seatdomain:spec#1, claim comment5851641050).Generated by Claude Code