Repository navigation
feat(types)!: retire ObjectChartSchema xAxisField / yAxisFields / aggregation on the object-chart node (objectui#10608) - #10734
Conversation
…regation on the object-chart node
The three are the list-view chart block's vocabulary; the list-view relays
translate that block before composing an object-chart node, and nothing on the
node's own render path reads them. Both published faces declared them anyway,
so a node written with them type-checked, parsed green, and drew no category
axis and no series.
Each is now a `?: never` tombstone on the TS face and a retirementTombstone()
on the zod mirror, refused by name with the spec spelling as its remedy
(`xAxis: { field }` / `yAxis: [{ field }]` / `aggregate: { field, function,
groupBy }`). The docblocks that taught the old spelling on this node are
rewritten. The list-view carriers are untouched.
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 judgments
② 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>
…ies type and names chartType (objectui#10584) (objectstack-ai#10768) Fixes objectstack-ai#10584 Clause-②: no. The accept set does not move; this is a `.describe` and docblock wording fix. `yes` would be owed only if a named producer turned the card to widening, and then the dev stops and reports. ## What changed - `packages/types/src/zod/objectql.zod.ts`, the `series` member of `ObjectChartSchema`: the `.describe()` no longer calls the element "the arm ChartRendererProps declares". It now says the copy is that `{ dataKey }` arm minus the arm's per-series `type`, which this copy does not declare, and that this copy's per-series family override is `chartType` (`bar` | `line` | `area`). A short comment above the member points at the TS twin for the ground. - `packages/types/src/objectql.ts`, the `series` member docblock: "this copy has not taken up" (which read as a lag) now says the omission is deliberate. Taking `type` up widens a published accept set and waits for a named producer that writes it on an `object-chart` node. The docblock also states what a `type` written anyway meets on each face (measured below). - `.changeset/10584-object-chart-series-describe.md`: `@object-ui/types` `patch`, since the `.describe()` string is published text. Both edits stay inside the `ObjectChartSchema` block, which is the file surface the claim names. No member, type or schema node changes. ## Premise check on `origin/main` at `0896838deb` - **H1 holds.** The zod `.describe()` said "the arm ChartRendererProps declares", while both arms of `ChartRendererProps.schema.series` carry `type?: string` (objectui#8086). The TS docblock already named the omission ("every member of it except `type` ... this copy has not taken up"). It now says the omission is deliberate. - **H2 holds.** PR objectstack-ai#10734 (objectui#10608) and PR objectstack-ai#10601 (objectui#10518) are both on `main`, so this card's serial predecessors on the block have landed. The text was re-read on `main`; the card's line numbers were not used. - **H3: no pin.** `git grep` for the old describe text ("ChartRendererProps declares", "plotted series in the renderer", "has not taken up") matches only the two source files. No test, generated artifact or doc asserts the string, and no describe-verdict pin (`INTERNAL` / `AUTHORABLE` prefix) reads it. ## Stop condition: producer census for a per-series `type` on `object-chart` No producer found, so the work did not stop. Each reading has a control that hits: - **objectui (in-repo):** every `type: 'object-chart'` literal outside tests is in one of the relays: app-shell `ObjectView`, plugin-view `ObjectView`, plugin-list `ListView`, plugin-dashboard `DashboardRenderer` and `DashboardGridLayout`. Each writes `series` as `{ dataKey, label }`, and none writes `type`. app-shell forwards `viewDef.chart.series` verbatim, but the spec's `ListChartConfigSchema` is a `strictObject` with no `series` member, so no conforming stored view carries one. `chartConfigPresentation` emits no `series`. The only `{ dataKey, type }` literals are in `plugin-charts` tests of the `chart` renderer. - **objectstack showcase (at `16c5a33`):** `command-center.page.ts` writes `object-chart` nodes with no `series`. `renewals-pipeline.page.ts` writes `series={[{ name: 'total', label: 'Invoice value' }]}` on the ObjectChart react block: the `{ name }` arm, with no `type`. - **hotcrm (shallow clone at `2f7b232`):** zero `object-chart` / `ObjectChart` hits, and zero `series` in the 15 files under `src/**/dashboards` and `src/**/reports`. Control: 34 chart `type:` literals across those files (five dashboards and two reports). ## What each face does with a per-series `type` (measured, unchanged by this PR) Measured on the rebuilt `dist` at HEAD `5dc9a71e02`, with the same readings on the base build: - **zod mirror:** `ObjectChartSchema.safeParse` of `series: [{ dataKey: 'margin', type: 'line' }]` gives `success: true`, and the parsed series is `[{"dataKey":"margin"}]` (the plain `z.object` element strips `type`). `{ dataKey, chartType: 'line' }` keeps `chartType`. `{ label }` alone is still `invalid_type` at `series.0.dataKey`. The base build gives the same result for each. - **TS face:** `tsc --strict` on a literal typed `ObjectChartSchema` gives `TS2353` ("'type' does not exist in type ...") for `{ dataKey, type }` and compiles `{ dataKey, chartType }`. - **`dist` diff, base vs head:** the non-comment lines of `objectql.d.ts` are byte-identical. `zod/objectql.zod.d.ts` and `zod/index.zod.d.ts` differ only in union-member emission order on members this PR does not touch (`"json" | "csv" | "xlsx"` versus `"json" | "xlsx" | "csv"`, `position` and `operator` unions). `zod/objectql.zod.js` differs only in the comment and the describe string. ## Verification (all on HEAD `5dc9a71e02`, working tree clean) - `pnpm --filter @object-ui/types build`: `VERDICT command-exit 0`. The dependency closure `@object-ui/types^...` has no build script, so it is empty. - `pnpm --filter @object-ui/types type-check`: `VERDICT command-exit 0` (the script name is echoed as `type-check`). - `pnpm exec vitest run --maxWorkers=2 packages/types/`: `Test Files 245 passed (245)`, `Tests 5327 passed (5327)`, exit 0. - `pnpm exec vitest run --maxWorkers=2 packages/plugin-charts/`: `Test Files 82 passed (82)`, `Tests 964 passed (964)`, exit 0. Direction: the downstream consumer of `@object-ui/types` named in the dispatch, run to confirm no fixture reads the describe string. - Out-of-package tests that parse `objectql.ts` / `objectql.zod.ts` from disk (app-shell `relayRungCensus-7559`, `chartConfigForward-7891`; plugin-grid, plugin-kanban, plugin-tree and react census pins; and eight `scripts/__tests__` suites): `Test Files 15 passed (15)`, `Tests 602 passed (602)`, exit 0. - ESLint `--no-inline-config --format json` on the two touched sources: exit 0, 2 files linted, 0 errors, and 35 `no-explicit-any` warnings, none on an edited line. Type-aware linting is not enabled (`eslint.config.js` has no `parserOptions.project` / `projectService`), so this diff cannot move a verdict on any untouched file. - `node scripts/check-changeset-presence.mjs` 0 · `node scripts/check-changeset-no-major.mjs` 0 · `pnpm check:new-line-citations` 0 (`VERDICT new-cross-file-line-citations: 0 new citation(s)`) · `pnpm check:control-bytes` 0 · `pnpm check:spec-symbols` 0 · `pnpm check:test-path-roots` 0 · `pnpm check:changeset-claims` 0 (report-only). The pending objectui#8086 changeset's sentence "that copy of the internal arm does not carry `type`, and its type is unchanged" is still true. - `pnpm check:component-surface-parity` 0 · `check:designer-field-key-parity` 0 · `check:doc-types` 0 · `check:installed-pin-claims` 0 · `check:handler-key-reads` 0 · `check:action-forward-parity` 0. - `node scripts/check-governed-queue-guard.mjs --test` on the three paths: `NOT GOVERNED`. - **NOT MEASURED: `pnpm check:doc-examples`**, exit 2, prerequisite not met. It needs the dist of about 30 workspace packages, and that full build is CI's. This diff adds no `@example` block or fence (zero added lines match), so the gate's population is unchanged. - **NOT RUN: Spec Main Shape Gate.** The touched member is not spec-derived: the `series` element is a local copy of `ChartRendererProps`' arm and binds no `@objectstack/spec` symbol. The emitted `objectql.d.ts` is non-comment byte-identical, so compiling against any spec cannot move. ## Acceptance notes - **Kept verbatim: the `INTERNAL (relay-composed)` prefix.** It is objectui#7946's ruled verdict. The census above measured a channel it does not name: the ObjectChart react block. The spec's `react-blocks` entry has `schemaType: 'object-chart'` and `schema: ChartConfigSchema`, and it lists `series` in `dataProps`. `ChartSeriesSchema` is the `{ name }` arm and carries the per-series `type`. The showcase's `renewals-pipeline` page writes `series={[{ name, label }]}` through that block onto an `object-chart` node, and the published zod face refuses that node (`invalid_value` at `chartType`, `invalid_type` at `series.0.dataKey`). That belongs to the same family as objectui#10518 / objectui#10608 (the `object-chart` copy against the spec's ChartConfig contract), and it is reported to the seat rather than acted on here. It does not change this card's answer. On this node the spec's per-series `type` rides the `{ name }` arm, and the spec refuses `dataKey` by name, so a `{ dataKey, type }` entry is not spec-authorable on `object-chart` either. - The describe names `chartType` because it is this copy's declared per-series override. It is not a new authoring recommendation: the member stays `INTERNAL`. - No pin was added. The dispatch owed one only if the wording was already pinned, and it was not. Refs: objectui#8086 · objectui#7946 · PR objectstack-ai#10554 --- _Generated by [Claude Code](https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #10608
Clause-②: yes
What changes
ObjectChartSchema.xAxisField,yAxisFieldsandaggregationare retired from theobject-chartnode on both published faces (ADR-0049). They are the LIST-VIEW chart block's vocabulary; the list-view relays translate that block before they compose anobject-chartnode, and nothing on the node's own render path reads them.packages/types/src/objectql.ts): each member is now?: never, with a docblock that names the spec spelling and a@deprecatedtag.packages/types/src/zod/objectql.zod.ts): each member is aretirementTombstone(), so one guidance string is both the parse-time issue message and the.describe()metadata. The refusal isinvalid_typeat the key's own path and names the remedy:xAxis: { field: 'status' },yAxis: [{ field: 'amount' }],aggregate: { field, function, groupBy }.xAxisKeybullet (which said the author-facing spelling on this node isxAxisField), thexAxisKeymember doc, theyAxismember's note aboutyAxisFields, and the mirror'sxAxisKey.describe()text. The interface docblock gains a short section on the retirement.minorfor@object-ui/types, with the breaking semantics stated. It is notmajor, because of the version-alignment rule.packages/types/src/__tests__/object-chart-legacy-axis-keys-retired-10608.test.ts.Precedent followed. This follows the package's
?: never+retirementTombstone()convention (tombstone.zod.ts). The nearest case isChatbotSchema.userAvatar/height(objectui#7703, pinned inchatbot-dark-keys-retired-7703.test.ts): a dead spelling that sits beside a live one is refused, with the live one as the remedy.ObjectKanbanSchema.groupField(objectui#7322) is the other node-local case. Deleting the members was not the route.BaseSchemais.passthrough(), so an undeclared key is KEPT, not refused. Pin block (d) measures that on a misspelling.Measured before retiring (the card's stop condition)
The stop condition was a real producer that writes these keys ON an
object-chartnode. None was found.H1, declaration. Confirmed on
origin/main5c61e5241, the branch point. main had moved past the dispatch'sf9c06ef6a, but this block is unchanged. The three members are on the TS interface and on the mirror, and the docblocks cited above teach the old spelling.H2, no reader. Measured with a render probe, not a grep. The probe was a throwaway test file, deleted and not committed. It used a real
SchemaRendererwith the package's own registrations and mockedAdvancedChartImplto capture its props. The static rows werestatus: open, n: 3andstatus: done, n: 5.xAxisKeypropseriespropxAxisField: 'status',yAxisFields: ['n'],aggregation: 'sum', staticdataxAxis: { field: 'status' },yAxis: [{ field: 'n' }], staticdatastatusdataKey: 'n'objectName: 'task'chart-missing-category-axisfindonceaggregate: { field: 'n', function: 'sum', groupBy: 'status' }+objectName: 'task'statusaggregateonceI also read
normalizeChartSchema's axis and series sections in full. It resolves the category fromxAxisKey, thenxAxis.field, then a barexAxisstring, and it derives series fromyAxis[].field. It names none of the three keys. In the non-test sources ofplugin-charts,components,reactandcore, the three names appear only in comments.H3, producers.
object-chartcomposers: app-shellObjectView(both chart branches), plugin-viewObjectView, plugin-listListView,DashboardRendererandDashboardGridLayout. Each one translates the keys, and none spreads them onto the node it builds. The two dashboard sites do spreadchartConfigPresentation(widget.chartConfig), but that function's whitelist (read inpackages/core/src/utils/chart-presentation.ts) emits none of the three.object-chartregistration'sinputsadvertise none of the three.examples/**,content/**,apps/**,skills/**: there are zero hits forxAxisFieldoryAxisFields. Theaggregationhits belong to the report schema's field aggregation and to the grid'saggregations, not to this node.skills/**teaches none of the three onobject-chart, so no governed row is split out.main16c5a33: the three showcase pages that build anobject-chart/ObjectChartwritedataset/dimensions/values,yAxis: [{ field, stepSize }], andxAxis={{ field }}withaggregate. They write none of the three keys. The onlyxAxisField/yAxisFieldshits are spec, lint and doc text about removing the keys from list charts and dashboard widgets.main2f7b232(shallow clone): zero hits forxAxisField,yAxisFields,object-chartorObjectChart. The positive control,chartType, does hit.H4.
objectql.zod.ts#ObjectChartSchemais inSPEC_DERIVED_PAIRS(zod-mirror-parity.test.ts). This diff adds no spec symbol reference and removes no spec crossing.H5. The door is
safeValidateSchema. The pin asserts the refusal there by code + path, and checks that the message carries the remedy.H6. No census row moved.
object-chart-undeclared-keys-8885,widget-schema-anchors-7946andzod-mirror-parityread the mirror's shape keys, and the tombstones stay declared. All three pass unchanged.Verification, at
856ecc953Reverse verification. The fix was committed first. I then checked out
objectql.tsandobjectql.zod.tsfrom the base5c61e5241and confirmed the change on disk: the tombstone text had 0 hits andxAxisField: z.string()had 1 hit in the mirror;xAxisField?: neverhad 0 hits andxAxisField?: stringhad 1 hit in the interface.tsc -p tsconfig.test.jsonexited 2 with 6 errors: 3 TS2344 on theEqualassertions and 3 TS2578 for unused@ts-expect-errordirectives.git checkout HEAD --. Both blob hashes equal HEAD's, andgit diff HEADis empty.Cross-package check against the rebuilt
.d.ts. After the dependency-closure build, a temporary file in plugin-charts typedxAxisField: 'status'on anObjectChartSchemaliteral. It failed with TS2322 ("Type 'string' is not assignable to type 'undefined'"), and thexAxis: { field }control line compiled. The file was then removed and the tree is clean.Consumers (all DOWNSTREAM of
@object-ui/types). Their dependency closures were built first withturbo run build --concurrency=2.pnpm exec vitest run packages/types/pnpm --filter @object-ui/types type-checktsconfig.test.jsonprogram; counted with--listFiles)pnpm --filterplugin-charts, plugin-view, plugin-dashboard, plugin-listrun type-checkpnpm exec vitest run packages/plugin-charts/pnpm exec vitest run packages/plugin-view/pnpm exec vitest run packages/plugin-dashboard/Gates (each exit code captured before any pipe):
node scripts/check-changeset-presence.mjsnode scripts/check-changeset-no-major.mjspnpm check:new-line-citationspnpm check:control-bytespnpm check:spec-symbolspnpm check:test-path-rootsnode scripts/check-governed-queue-guard.mjs --test(the 4 paths)pnpm check:component-surface-parity·check:designer-field-key-parity·check:doc-types·check:handler-key-reads·check:prompt-keys·check:installed-pin-claims·check:action-forward-parity·check:registry-bare-names·check:changeset-claims·check:pending-changeset-literals·check:unreferenced-sourcespnpm check:doc-examples·check:doc-snippets·check:skill-examples--no-inline-config, the 3 touched.tsfilesThe ESLint run was narrowed. The population is the repository-root
eslint.config.js; there is no package-level config underpackages/types. The--format jsonoutput counted 3 files. The config has noprojectService/parserOptions.project, so type-aware linting is off, and this diff cannot move the verdict on any untouched file.NOT MEASURED: the Spec Main Shape Gate. Reason: running it locally needs
@objectstack/specbuilt from objectstackmainas a tarball, plus a virtual-store injection and a typecheck of the whole repo. The only objectstack checkout here is shared and has no built spec. This diff adds or moves no spec symbol reference, only three local members. The gate's workflow runs on every pull request without a paths filter, so CI takes the reading.Acceptance notes
ObjectViewand plugin-listListViewstill readxAxisField/yAxisFields/aggregationfrom a list view's chart config and translate them. On the authoring door, the spec'schartblock (dataset-only) already refuses them, and the legacyoptions.chartbag admits them. Pin block (e) holds that boundary as a control. Whether that legacy path should also be retired is a separate question. Carrier: none named.objectNamevariant drew the refusal with 0 leaf calls. My probe saw the same refusal after onefindcall. That does not change the verdict (the legacy keys still aggregate nothing and bind no axis), and it is recorded here rather than chased.aggregationrefusal lists the spec'sfunctionvocabulary in its message. Pin block (c) re-derives that list fromChartAggregateSchemaon every run, so the prose cannot drift from the spec silently.5c61e5241when this PR was opened. objectui#10710 edits the same two files atObjectDataTableSchemaand the import line, and this PR's hunks are theObjectChartSchemablock only. objectui#10584 is held serial behind this card on the same block.Generated by Claude Code