Skip to content

feat(spec,lint): the ui vocabularies admit what the renderers implement (objectui#2945) - #4070

Merged
os-zhuang merged 5 commits into
mainfrom
claude/spec-vocab-2945-additions
Jul 30, 2026
Merged

feat(spec,lint): the ui vocabularies admit what the renderers implement (objectui#2945)#4070
os-zhuang merged 5 commits into
mainfrom
claude/spec-vocab-2945-additions

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Additions-only follow-up to the vocabulary audit (objectstack-ai/objectui#2901, objectstack-ai/objectui#2945). Companion to #4061, which deletes the orphans; this one closes the gaps in the opposite direction.

Nothing here narrows a vocabulary. No accepted value stops being accepted, so no already-stored metadata and no in-flight payload changes meaning. That is why this is safe while the actual consolidation stays blocked on #3948.

ChartTypeSchema admits combo

The taxonomy could not name the one chart family the rest of chart.zod.ts is written for:

  • ChartSeriesSchema.type overrides a series' chart type — its own doc comment says "combo charts";
  • ChartSeriesSchema.yAxis binds a series to the left or right axis, which is only meaningful for mixed marks.

objectui's renderer draws it distinctly — mixed bar/line/area, dual axes, per-series type resolution (AdvancedChartImpl.tsx:819) — and had to carry combo in a local fork of this list, in a set whose comment claims to be "every value the spec's ChartTypeSchema admits".

This satisfies the taxonomy's own admission rule (in the NOTE below the enum): only families the default renderer draws distinctly. combo does; it is not a variant that falls back to a base family.

WidgetActionTypeSchema is ActionType

The two lists had drifted apart by one member, form, and the disagreement was backwards.

A dashboard header or widget action button dispatches through the same ActionRunner that implements form. objectui's DashboardRenderer routes everything except a raw url into it, deliberately — the code comment records that the previous modal/script allow-list meant a flow header action fell through to a warn and never dispatched (#3528).

So the narrower enum rejected at validation exactly what the shared dispatcher then executes at runtime. Derived rather than restated: the next type the runner implements needs one edit, not two.

dashboard.zod.tsaction.zod.ts is a new import edge with no cycle (action.zod.ts imports neither dashboard.zod.ts nor anything that reaches it).

ListChartConfigSchema.chartType is ChartTypeSchema.extract([...])

Same five members as before — a de-duplication, not a widening. A member renamed in the chart taxonomy now fails at build time instead of leaving a second list quietly disagreeing with the first. (.extract() resolves correctly through the lazySchema proxy — verified by execution, and by the guard.)

view.zod.tschart.zod.ts is likewise cycle-free (chart.zod.ts imports only i18n.zod and lazy-schema).

@objectstack/lint's chart-family set derives from the taxonomy

validate-widget-bindings decides which widgets need a chartConfig measure mapping from a hand-written list of families. Its omissions fail in the worst direction: an unlisted family reads as "not a chart", so a widget missing its measure mapping passes validation instead of being reported.

combo was exactly that case. Proven rather than asserted — pinning the old hand-written list back:

AssertionError: expected a warning for chart type 'combo': expected [] to have a length of 1 but got +0

The set is now the taxonomy minus an explicit MEASURE_EXEMPT_CHART_TYPES (single-value: gauge/solid-gauge/metric/kpi/bullet; tabular: table/pivot), so a family added to the spec is covered without editing the rule.

Guards

packages/spec/src/ui/vocabulary-derivation.test.ts asserts both derivations still hold — a restated list fails silently, since it keeps validating, just not what the other list says:

  • the list-chart subset admits exactly its five members, all of them taxonomy members, still rejects donut and combo, accepts each of the five, and defaults to bar;
  • WidgetActionTypeSchema's options equal ActionType's exactly, form parses, an unknown type still throws.

The lint suite now walks every multi-series family in the taxonomy rather than a list of its own.

One item from the audit deliberately not done

The audit also proposed promoting navigation into ActionType. I did not, and the reason is the audit's own thesis:

  • ActionRunner.executeNavigation is a strictly weakerexecuteUrl — no ${param.X} / ${ctx.X} interpolation, no apiBase promotion for /api/... paths, no openIn — differing only by a replace option;
  • its one live producer is the SDUI element:buttonaction prop, and ElementButtonPropsSchemadoes not model an action prop at all (page block properties are z.record(z.string(), z.unknown()), so it is neither validated nor stripped today).

Promoting the name would add a second spelling of navigate to a closed authorable vocabulary — members cannot be removed later, per the audit's own rule — without closing the gap that actually exists, which is the undeclared element:button.action. Worth its own issue; not worth a one-way door here.

Verification

  • @objectstack/spec: 6944 tests across 267 files, green. tsc --noEmit clean.
  • @objectstack/lint: 544 tests across 37 files, green. tsc --noEmit clean for the touched file (two remaining diagnostics in this worktree are @objectstack/formula having no local dist types, in files this PR does not touch).
  • The lint guard was confirmed to fail before the fix, as quoted above.

Refs objectstack-ai/objectui#2945, objectstack-ai/objectui#2901, #3528, #4061

🤖 Generated with Claude Code

@vercel

vercelBot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredJul 30, 2026 8:51am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests protocol:ui tooling size/m and removed documentation Improvements or additions to documentation tests protocol:ui tooling labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec.

105 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via packages/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via @objectstack/lint, packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/lint, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

os-zhuangand others added 4 commits July 30, 2026 16:36
…nt (objectui#2945)
Additions-only follow-up to the vocabulary audit. Nothing narrows, so no
already-stored metadata changes meaning.
- ChartTypeSchema admits `combo`. The taxonomy could not name the one
family the rest of chart.zod.ts is written for: ChartSeriesSchema.type
exists to override a series' type ("combo charts", per its own doc
comment) and ChartSeriesSchema.yAxis binds left/right, which only means
something for mixed marks. objectui draws it distinctly and carried
`combo` in a local fork whose comment claimed to mirror this list.
- WidgetActionTypeSchema is ActionType. They disagreed by `form`, and
backwards: a widget/header action dispatches through the same
ActionRunner that implements `form` (DashboardRenderer routes everything
but a raw `url` into it, deliberately — #3528), so the narrower enum
rejected at validation what the dispatcher executes.
- ListChartConfigSchema.chartType is ChartTypeSchema.extract([...]) —
same five members, a de-duplication rather than a widening.
- @objectstack/lint's chart-family set derives from the taxonomy. Its
omissions failed in the worst direction: an unlisted family read as
"not a chart", so a widget with no chartConfig measure mapping PASSED
validation. Pinning the old hand-written list back reproduces exactly
that for `combo` — zero findings — which is the new test's teeth.
ActionType deliberately does NOT gain `navigation`: executeNavigation is a
strictly weaker executeUrl (no ${param} interpolation, no apiBase
promotion, no openIn) differing only by `replace`, and its one live
producer is the SDUI element:button `action` prop that
ElementButtonPropsSchema does not model at all. Adding the name would put
a second spelling of "navigate" into a closed vocabulary without closing
the real gap.
spec 6944 tests / 267 files and lint 544 / 37 green; tsc clean on both.
Refs objectstack-ai/objectui#2945, objectstack-ai/objectui#2901, #3528
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ce docs regenerate
Two gates fired on the taxonomy addition, both correctly.
- app-showcase's coverage test requires a gallery widget for every
distinctly-renderable ChartType, so admitting `combo` without
demonstrating it failed. The Chart Gallery now shows one: task count as
bars on the left axis, avg progress as a line on the right — the
configuration series[].type / series[].yAxis exist for. Rows below it
shift down by 4 to keep the 12-column layout non-overlapping.
- content/docs/references/ui/{chart,dashboard,report}.mdx regenerated for
the new chart-type member and the two derived enums.
check:docs, check:api-surface and check:authorable-surface pass; the
showcase coverage suite is 13/13.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`view.zod.ts` now imports `chart.zod.ts` (for the extracted list-chart
subset), so the taxonomy joins that skill's transitive reference set.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rt type
Fifth generated artefact tracking the spec surface: the react-blocks
contract and its reference table inline the ChartType union.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@xuyushun441-sys
xuyushun441-sysforce-pushed the claude/spec-vocab-2945-additions branch from bd8ffb8 to f93da17CompareJuly 30, 2026 08:36
check-i18n-coverage freezes each example's untranslated-declared-string
count and fails on growth; the `combo` widget's title took app-showcase
from 456 to 457. Translated in both locales the example claims to support,
which is the remedy the bundle's own `globalActions` comment prescribes.
The gallery's other 20 widget titles predate that ratchet and stay part of
the frozen baseline — this does not ratchet the number up.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@os-zhuang
os-zhuang merged commit 1bd2795 into mainJul 30, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/spec-vocab-2945-additions branch July 30, 2026 09:04
xuyushun441-sys pushed a commit that referenced this pull request Jul 30, 2026
…tionSchema (objectui#2997)
element:button's renderer reads `properties.action` and dispatches it
through the ActionRunner — it is the only thing making a standalone-page
button interactive — and ElementButtonPropsSchema did not declare it.
It is authored anyway: cloud's service-tenant pages carry five sites across
the billing and pricing funnel, each `{type:'navigation', to: …}`, each cast
`as any`.
An undeclared prop is STRIPPED, not ignored — safeParse returned success
with no `action` in `data`. Harmless only because page block `properties`
are still z.record(z.string(), z.unknown()); the moment that is tightened
to validate against ComponentPropsMap, every one of those buttons loses its
action at save with a green parse.
InlineActionSchema is derived, not a new dialect. ActionSchema is
z.object(…).refine(…).refine(…), so `.pick()` is unavailable on the export;
the object half is now a factory both build from, preserving lazySchema's
deferral. The inline schema picks the twelve fields element:button actually
forwards, sharing their describe() text, the ActionType vocabulary and the
target-required refinement.
`name`/`label` are optional — a registry entry needs an identity and a menu
label, an inline action has neither (the button supplies its own label).
Registry-only concerns are excluded, as are icon/variant (the button has its
own) and body (a page button running a sandboxed script is a separate
decision).
`navigation` and `to` become normalizing aliases folding to `url`/`target`,
the VIEW_FILTER_OPERATOR_ALIASES pattern: cloud's pages keep validating
while output is always canonical, and the aliases can be tombstoned once
producers migrate. `url` is also the better target — its runner path has
${param} interpolation, apiBase promotion and popup-safe openIn; the
navigation path has none of those.
NOT done: promoting `navigation` into ActionType bare (declined in #4070) —
it names the type while leaving the prop undeclared and `to` homeless, and
adds a permanent synonym to a vocabulary whose members cannot be removed.
Nothing narrowed; all generated surfaces gain entries only. spec suite
7038/271 green, tsc clean, all twelve check:* gates pass.
Refs objectstack-ai/objectui#2997, objectstack-ai/objectui#2945, #4070
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
xuyushun441-sys pushed a commit that referenced this pull request Jul 30, 2026
…tionSchema (objectui#2997)
element:button's renderer reads `properties.action` and dispatches it
through the ActionRunner — it is the only thing making a standalone-page
button interactive — and ElementButtonPropsSchema did not declare it.
It is authored anyway: cloud's service-tenant pages carry five sites across
the billing and pricing funnel, each `{type:'navigation', to: …}`, each cast
`as any`.
An undeclared prop is STRIPPED, not ignored — safeParse returned success
with no `action` in `data`. Harmless only because page block `properties`
are still z.record(z.string(), z.unknown()); the moment that is tightened
to validate against ComponentPropsMap, every one of those buttons loses its
action at save with a green parse.
InlineActionSchema is derived, not a new dialect. ActionSchema is
z.object(…).refine(…).refine(…), so `.pick()` is unavailable on the export;
the object half is now a factory both build from, preserving lazySchema's
deferral. The inline schema picks the twelve fields element:button actually
forwards, sharing their describe() text, the ActionType vocabulary and the
target-required refinement.
`name`/`label` are optional — a registry entry needs an identity and a menu
label, an inline action has neither (the button supplies its own label).
Registry-only concerns are excluded, as are icon/variant (the button has its
own) and body (a page button running a sandboxed script is a separate
decision).
`navigation` and `to` become normalizing aliases folding to `url`/`target`,
the VIEW_FILTER_OPERATOR_ALIASES pattern: cloud's pages keep validating
while output is always canonical, and the aliases can be tombstoned once
producers migrate. `url` is also the better target — its runner path has
${param} interpolation, apiBase promotion and popup-safe openIn; the
navigation path has none of those.
NOT done: promoting `navigation` into ActionType bare (declined in #4070) —
it names the type while leaving the prop undeclared and `to` homeless, and
adds a permanent synonym to a vocabulary whose members cannot be removed.
Nothing narrowed; all generated surfaces gain entries only. spec suite
7038/271 green, tsc clean, all twelve check:* gates pass.
Refs objectstack-ai/objectui#2997, objectstack-ai/objectui#2945, #4070
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…tionSchema (objectui#2997) (#4135)
element:button's renderer reads `properties.action` and dispatches it
through the ActionRunner — it is the only thing making a standalone-page
button interactive — and ElementButtonPropsSchema did not declare it.
It is authored anyway: cloud's service-tenant pages carry five sites across
the billing and pricing funnel, each `{type:'navigation', to: …}`, each cast
`as any`.
An undeclared prop is STRIPPED, not ignored — safeParse returned success
with no `action` in `data`. Harmless only because page block `properties`
are still z.record(z.string(), z.unknown()); the moment that is tightened
to validate against ComponentPropsMap, every one of those buttons loses its
action at save with a green parse.
InlineActionSchema is derived, not a new dialect. ActionSchema is
z.object(…).refine(…).refine(…), so `.pick()` is unavailable on the export;
the object half is now a factory both build from, preserving lazySchema's
deferral. The inline schema picks the twelve fields element:button actually
forwards, sharing their describe() text, the ActionType vocabulary and the
target-required refinement.
`name`/`label` are optional — a registry entry needs an identity and a menu
label, an inline action has neither (the button supplies its own label).
Registry-only concerns are excluded, as are icon/variant (the button has its
own) and body (a page button running a sandboxed script is a separate
decision).
`navigation` and `to` become normalizing aliases folding to `url`/`target`,
the VIEW_FILTER_OPERATOR_ALIASES pattern: cloud's pages keep validating
while output is always canonical, and the aliases can be tombstoned once
producers migrate. `url` is also the better target — its runner path has
${param} interpolation, apiBase promotion and popup-safe openIn; the
navigation path has none of those.
NOT done: promoting `navigation` into ActionType bare (declined in #4070) —
it names the type while leaving the prop undeclared and `to` homeless, and
adds a permanent synonym to a vocabulary whose members cannot be removed.
Nothing narrowed; all generated surfaces gain entries only. spec suite
7038/271 green, tsc clean, all twelve check:* gates pass.
Refs objectstack-ai/objectui#2997, objectstack-ai/objectui#2945, #4070
Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:uisize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-zhuang