From 38c84358d8a42f459d95e4d44df07825acb928a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 15:17:46 +0000 Subject: [PATCH 1/2] retire(studio): drop the global_nav designer surfaces with the rc.6 spec bump (objectstack#7100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `global_nav` was an `ACTION_LOCATIONS` member no running-app surface ever rendered — the console's ⌘K palette reads no action metadata at all — and the Studio designer previewed a mock command-palette frame for it anyway. @objectstack/spec 17.0.0-rc.6 retired the member (objectstack#6888, maintainer ruling 2026-08-09 direction 2); this removes the designer surfaces that outlived it, plus the bump that makes them stop compiling. - ActionPreview.tsx: the mock ⌘K placement frame is gone. The metadata strip still echoes the declared token, deliberately — reporting what a draft says is honest; the frame CLAIMED the platform renders it. - ActionDefaultInspector.tsx: the `LOCATION_LABELS` entry is gone. The map is `Record`, so the retirement arrived as a compile error (objectui#3017's mechanism firing as designed). - block-config.ts + both locale tables: the palette option and its now-orphaned i18n key. - components' action:bar doc comment aligned (its enum is `[...ACTION_LOCATIONS]` and followed on its own). - ActionEngine.getActionsForLocation is UNCHANGED: narrowing it would be a second rejection point beside the schema's. The type and `ActionLocationSchema` are the enforcement, both pinned. The bump (all 37 `@objectstack/*` declarations, rc.5 -> rc.6) rides along because the two cannot compile apart, and its fallout is repaired here: the `…Input` alias retirement (types/theme, types/spec-report, core/ActionRunner), the five dead i18n re-exports, the `I18nLabel` widening at 8 render sites, `.extend` -> `.safeExtend` on a now-refined `GlobalFilterSchema`, and two parity lists re-pointed. NOT green: the bump's remaining surface is out of this card's scope and filed — objectui#4165 (contract conflict, blocks), #4167 (adaptation remainder), #4163 (I18nLabel consumer story), #4164 (name reclaim available). Part of objectstack-ai/objectstack#7100 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3 --- .changeset/global-nav-studio-retire-rc6.md | 35 ++++ apps/console/package.json | 4 +- apps/site/package.json | 2 +- package.json | 2 +- packages/app-shell/package.json | 6 +- packages/app-shell/src/layout/AppSidebar.tsx | 21 +- .../app-shell/src/layout/UnifiedSidebar.tsx | 16 +- .../src/views/metadata-admin/i18n.ts | 9 +- .../inspectors/ActionDefaultInspector.tsx | 10 +- .../inspectors/DashboardDefaultInspector.tsx | 19 +- .../inspectors/DashboardWidgetInspector.tsx | 33 ++- .../metadata-admin/previews/ActionPreview.tsx | 20 +- .../previews/DashboardPreview.tsx | 11 +- .../ActionPreview.locations.test.tsx | 113 +++++++++++ .../previews/__tests__/block-config.test.ts | 41 +++- .../metadata-admin/previews/block-config.ts | 5 +- packages/auth/package.json | 2 +- packages/collaboration/package.json | 2 +- packages/components/package.json | 2 +- .../src/renderers/action/action-bar.tsx | 9 +- packages/core/package.json | 4 +- packages/core/src/actions/ActionRunner.ts | 25 ++- packages/data-objectstack/package.json | 4 +- packages/fields/package.json | 2 +- packages/layout/package.json | 2 +- packages/mobile/package.json | 2 +- packages/plugin-charts/package.json | 2 +- packages/plugin-chatbot/package.json | 2 +- packages/plugin-dashboard/package.json | 2 +- packages/plugin-detail/package.json | 2 +- packages/plugin-form/package.json | 2 +- packages/plugin-gantt/package.json | 2 +- packages/plugin-grid/package.json | 4 +- packages/plugin-list/package.json | 4 +- packages/plugin-map/package.json | 2 +- packages/plugin-report/package.json | 2 +- packages/plugin-timeline/package.json | 2 +- packages/plugin-tree/package.json | 2 +- packages/plugin-view/package.json | 2 +- packages/providers/package.json | 2 +- packages/react/package.json | 2 +- .../hooks/__tests__/useActionEngine.test.ts | 74 +++++++ packages/runner/package.json | 2 +- packages/sdui-parser/package.json | 2 +- packages/types/package.json | 2 +- .../page-nav-misc-spec-parity.test.ts | 37 +++- .../spec-ui-schema-reexports.test.ts | 14 +- packages/types/src/index.ts | 16 +- packages/types/src/spec-report.ts | 16 +- packages/types/src/theme.ts | 23 ++- packages/types/src/zod/complex.zod.ts | 13 +- pnpm-lock.yaml | 190 +++++++++--------- 52 files changed, 628 insertions(+), 196 deletions(-) create mode 100644 .changeset/global-nav-studio-retire-rc6.md create mode 100644 packages/app-shell/src/views/metadata-admin/previews/__tests__/ActionPreview.locations.test.tsx diff --git a/.changeset/global-nav-studio-retire-rc6.md b/.changeset/global-nav-studio-retire-rc6.md new file mode 100644 index 0000000000..e3269a1189 --- /dev/null +++ b/.changeset/global-nav-studio-retire-rc6.md @@ -0,0 +1,35 @@ +--- +"@object-ui/app-shell": minor +"@object-ui/components": minor +"@object-ui/types": minor +"@object-ui/core": minor +--- + +Retire the `global_nav` Studio designer surfaces, and track the `@objectstack` family at `17.0.0-rc.6` (objectstack#7100 / objectstack#6888). + +## The retirement + +`global_nav` was an `ACTION_LOCATIONS` member no running-app surface ever rendered. The console's ⌘K palette (`app-shell/src/chrome/CommandPalette.tsx`) builds its groups from nav items, objects, dashboards, pages, reports, recent items, record search and theme; it holds no reference to `global_nav`, to `actionRendersAt`, or to any action-metadata source. An action declaring `locations: ['global_nav']` therefore never reached a user. + +The Studio designer previewed it anyway — a mock frame reading `⌘K · Command palette` with the author's button inside it. That is the sharp edge the maintainer's 2026-08-09 ruling on objectstack#6888 named: an authoring tool promising a surface the product does not have teaches authors, and every AI copying this corpus, to declare dead metadata. `@objectstack/spec` `17.0.0-rc.6` retired the member (7 members → 6) with a named rejection message; this release removes the designer surfaces that outlived it. + +- `metadata-admin/previews/ActionPreview.tsx` — the mock command-palette placement frame is gone. The metadata strip above it still ECHOES whatever `locations` the draft declares, deliberately: reporting what a (possibly stale) draft says is honest, whereas the frame CLAIMED the platform renders it. +- `metadata-admin/inspectors/ActionDefaultInspector.tsx` — the `global_nav` entry is gone from `LOCATION_LABELS`. That map is typed `Record< ActionLocation, string >`, so the retirement reached it as a compile error rather than as a silently stale dropdown — the mechanism objectui#3017 installed, firing as designed. +- `metadata-admin/previews/block-config.ts` — the `record:quick_actions` location dropdown no longer offers it, and both locale tables drop the now-orphaned `…option.location.global_nav` key. +- `@object-ui/components`' `action:bar` doc comment is aligned. The component's published enum is `[...ACTION_LOCATIONS]`, so it followed the retirement on its own; only the prose was stale. + +`@object-ui/core`'s `ActionEngine.getActionsForLocation` is **unchanged and still answers a literal string match**. Narrowing it to the six live members would put a second rejection point beside the schema's — the tolerant-consumer shape the strict-contract rule forbids, inverted. Enforcement stays where it belongs: the parameter type is now six-membered so no type-correct caller can spell the retired value, and `ActionLocationSchema` rejects it by name at authoring and publish time. + +## The dependency move + +All 37 `@objectstack/*` declarations across 30 `package.json` files move from `^17.0.0-rc.5` to `^17.0.0-rc.6`, and `pnpm-lock.yaml` resolves one copy of each family package at rc.6. The siblings move with `spec` because `client` / `formula` / `lint` pin it **exactly** — leaving them behind would keep two copies of the spec in the tree, the split brain objectui#3560 called out. + +Bumping the pin and repairing the fallout cannot be split: at rc.5 the `Record< ActionLocation, string >` above is missing a key, at rc.6 it has an excess one. + +## Breaking, in FROM → TO form + +- **`@object-ui/types`' `Theme` now binds the spec's `Theme`, not `ThemeInput`.** rc.6 retired every `…Input` alias and moved the bare name onto the `z.input` side (`X` = `z.input`, `XParsed` = `z.infer`). The runtime shape and this package's exported name are unchanged — `Theme` was, and still is, the AUTHORING shape where `mode` is optional. Re-pointing at `ThemeParsed` would have been the silent swap. +- **`SpecReport` / `SpecReportChart` re-point to `ReportParsed` / `ReportChartParsed`, and `SpecReportInput` / `SpecReportChartInput` to `Report` / `ReportChart`.** Same rename, same rule: each local alias keeps the SIDE it had at rc.5. +- **`@object-ui/types` no longer re-exports `I18nObject`, `LocaleConfig`, `PluralRule`, `DateFormat` or `NumberFormat`** — all five were retired by rc.6. They were dead re-exports here: nothing in this repo imported them from `@object-ui/types` (`@object-ui/i18n`'s formatter vocabulary in `utils/spec-formatters.ts` is locally declared and never bound the spec symbols). `I18nLabel` survives and is unchanged as a name. +- **`I18nLabel` itself widened from `string` to `string | Record< string, string >`** — rc.6 folded the retired `I18nObject`'s per-locale map into it and ships `resolveI18nLabel(label, locale)` as the shared resolver. Every read in this repo that lands in a text slot now goes through that resolver, so an inline map renders its locale instead of `[object Object]`. Reads the compiler cannot see are audited separately in objectui#4163. +- **`@object-ui/types`' `GlobalFilterSchema` derives via `.safeExtend`, not `.extend`.** rc.6's `GlobalFilterSchema` carries a refinement and zod 4 refuses `.extend()` on a refined object outright, which threw at module load. `.safeExtend` is zod's prescribed replacement and KEEPS the refinement, so the spec's cross-field rule now also runs on this package's dialect — which is the intended behaviour, since the pinned divergences widen individual fields and were never meant to switch off a whole-object rule. diff --git a/apps/console/package.json b/apps/console/package.json index bfe7e8aa01..9560915984 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -89,8 +89,8 @@ "@object-ui/providers": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/client": "^17.0.0-rc.5", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/client": "^17.0.0-rc.6", + "@objectstack/spec": "^17.0.0-rc.6", "@tailwindcss/postcss": "^4.3.3", "@tailwindcss/typography": "^0.5.20", "@testing-library/jest-dom": "^7.0.0", diff --git a/apps/site/package.json b/apps/site/package.json index 498a84231b..9dbb3fdcd6 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -33,7 +33,7 @@ "@object-ui/plugin-view": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "fumadocs-core": "16.14.1", "fumadocs-mdx": "15.2.2", "fumadocs-ui": "16.14.1", diff --git a/package.json b/package.json index 9d9245721a..8de2c98847 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "devDependencies": { "@changesets/cli": "^2.31.1", "@eslint/js": "^10.0.1", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "@playwright/test": "^1.62.1", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^7.0.0", diff --git a/packages/app-shell/package.json b/packages/app-shell/package.json index e533fdde8a..99d94c4087 100644 --- a/packages/app-shell/package.json +++ b/packages/app-shell/package.json @@ -49,9 +49,9 @@ "@object-ui/providers": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/formula": "^17.0.0-rc.5", - "@objectstack/lint": "^17.0.0-rc.5", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/formula": "^17.0.0-rc.6", + "@objectstack/lint": "^17.0.0-rc.6", + "@objectstack/spec": "^17.0.0-rc.6", "@sentry/react": "^10.69.0", "jsonc-parser": "^3.3.1", "lucide-react": "^1.29.0", diff --git a/packages/app-shell/src/layout/AppSidebar.tsx b/packages/app-shell/src/layout/AppSidebar.tsx index 02937540d6..f0aff1e347 100644 --- a/packages/app-shell/src/layout/AppSidebar.tsx +++ b/packages/app-shell/src/layout/AppSidebar.tsx @@ -63,6 +63,13 @@ import { useRecentItems } from '../hooks/useRecentItems'; import { useFavorites } from '../hooks/useFavorites'; import { useNavPins } from '../hooks/useNavPins'; import { resolveI18nLabel, matchAppBySegment, appRouteSegment } from '../utils'; +// Two resolvers, two vocabularies — the alias is what keeps them apart. +// `resolveI18nLabel` above is objectui's own and resolves a TRANSLATION-KEY ref +// (`{ key, defaultValue, params }`) through i18next. `resolveInlineI18nLabel` is +// the spec's `resolveI18nLabel`, new in @objectstack/spec 17.0.0-rc.6, and +// resolves the INLINE per-locale map (`{ en: …, 'zh-CN': … }`) that the same +// release folded into `I18nLabel`. Neither accepts the other's shape. +import { resolveI18nLabel as resolveInlineI18nLabel } from '@objectstack/spec/ui'; import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n'; import { useAppContextSelectors } from './ContextSelectors'; @@ -143,7 +150,7 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri const isWorkspaceAdmin = useIsWorkspaceAdmin(); const navigate = useNavigate(); const location = useLocation(); - const { t } = useObjectTranslation(); + const { t, language } = useObjectTranslation(); const { objectLabel: resolveNavObjectLabel, viewLabel: resolveNavViewLabel } = useObjectLabel(); // Swipe-from-left-edge gesture to open sidebar on mobile @@ -514,15 +521,23 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri {visibleAreas.map((area) => { const AreaIcon = getIcon(area.icon); const isActiveArea = area.id === activeArea?.id; + // `NavigationArea.label` is the spec's `I18nLabel`, which + // @objectstack/spec 17.0.0-rc.6 widened from plain `string` + // to `string | Record` (the inline + // per-locale map, folded in from the retired `I18nObject`). + // Rendering it raw would print `[object Object]` for the + // map form, so it goes through the spec's own shared + // resolver rather than a local guess. + const areaLabel = resolveInlineI18nLabel(area.label, language); return ( setActiveAreaId(area.id)} > - {area.label} + {areaLabel} ); diff --git a/packages/app-shell/src/layout/UnifiedSidebar.tsx b/packages/app-shell/src/layout/UnifiedSidebar.tsx index 50afa55901..fe5383cd0b 100644 --- a/packages/app-shell/src/layout/UnifiedSidebar.tsx +++ b/packages/app-shell/src/layout/UnifiedSidebar.tsx @@ -49,6 +49,10 @@ import { useFavorites } from '../hooks/useFavorites'; import { useNavPins } from '../hooks/useNavPins'; import { useNavActionDispatch } from '../hooks/useNavActionDispatch'; import { resolveI18nLabel, matchAppBySegment, appRouteSegment } from '../utils'; +// Aliased to keep it apart from objectui's own `resolveI18nLabel` above: this is +// the spec's resolver (new in @objectstack/spec 17.0.0-rc.6) for the INLINE +// per-locale map form of `I18nLabel`, not for a translation-key ref. +import { resolveI18nLabel as resolveInlineI18nLabel } from '@objectstack/spec/ui'; import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n'; // useObjectLabel provides appLabel/appDescription for convention-based // i18n lookup — `{ns}.apps.{name}.label` resolves to the translated label @@ -152,7 +156,7 @@ function isOverviewGroup(item: NavigationItem): boolean { export function UnifiedSidebar({ activeAppName }: UnifiedSidebarProps) { const { isMobile, setOpenMobile } = useSidebar(); const location = useLocation(); - const { t } = useObjectTranslation(); + const { t, language } = useObjectTranslation(); const { objectLabel: resolveNavObjectLabel, dashboardLabel: resolveNavDashboardLabel, navGroupLabel: resolveNavGroupLabel, viewLabel: resolveNavViewLabel } = useObjectLabel(); const { context, currentAppName } = useNavigationContext(); const { user, activeOrganization } = useAuth(); @@ -506,15 +510,21 @@ export function UnifiedSidebar({ activeAppName }: UnifiedSidebarProps) { {visibleAreas.map((area) => { const AreaIcon = getIcon(area.icon); const isActiveArea = area.id === activeArea?.id; + // Same as AppSidebar: `NavigationArea.label` is the spec's + // `I18nLabel`, widened in @objectstack/spec 17.0.0-rc.6 to + // `string | Record`, so the inline + // per-locale form has to be resolved before it reaches a + // text slot or it renders as `[object Object]`. + const areaLabel = resolveInlineI18nLabel(area.label, language); return ( setActiveAreaId(area.id)} > - {area.label} + {areaLabel} ); diff --git a/packages/app-shell/src/views/metadata-admin/i18n.ts b/packages/app-shell/src/views/metadata-admin/i18n.ts index 00ed90875d..5ba584270e 100644 --- a/packages/app-shell/src/views/metadata-admin/i18n.ts +++ b/packages/app-shell/src/views/metadata-admin/i18n.ts @@ -661,7 +661,10 @@ const ENGINE_STRINGS_EN: Record = { 'engine.inspector.pageBlock.option.location.record_related': 'Record related', 'engine.inspector.pageBlock.option.location.list_toolbar': 'List toolbar', 'engine.inspector.pageBlock.option.location.list_item': 'List item', - 'engine.inspector.pageBlock.option.location.global_nav': 'Global nav', + // `…option.location.global_nav` retired with the location itself + // (objectstack#6888, @objectstack/spec 17.0.0-rc.6). Its only reader was the + // dropdown option removed from `previews/block-config.ts`; a key kept past its + // option is dead vocabulary that the next author reads as a live surface. 'engine.inspector.pageBlock.field.ai:input.agentName': 'Agent', 'engine.inspector.pageBlock.placeholder.ai:input.agentName': 'agent name', 'engine.inspector.pageBlock.field.ai:input.placeholder': 'Input placeholder', @@ -2392,7 +2395,9 @@ const ENGINE_STRINGS_ZH: Record = { 'engine.inspector.pageBlock.option.location.record_related': '记录关联列表', 'engine.inspector.pageBlock.option.location.list_toolbar': '列表工具栏', 'engine.inspector.pageBlock.option.location.list_item': '列表行', - 'engine.inspector.pageBlock.option.location.global_nav': '全局导航', + // `…option.location.global_nav` retired with the location itself — see the + // matching note in the `en` table above. Removed from BOTH tables in the same + // edit so the two key sets stay identical. 'engine.inspector.pageBlock.field.ai:input.agentName': '智能体', 'engine.inspector.pageBlock.placeholder.ai:input.agentName': '智能体名称', 'engine.inspector.pageBlock.field.ai:input.placeholder': '输入框占位提示', diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/ActionDefaultInspector.tsx b/packages/app-shell/src/views/metadata-admin/inspectors/ActionDefaultInspector.tsx index 137357d4f1..319b06c2ac 100644 --- a/packages/app-shell/src/views/metadata-admin/inspectors/ActionDefaultInspector.tsx +++ b/packages/app-shell/src/views/metadata-admin/inspectors/ActionDefaultInspector.tsx @@ -119,8 +119,15 @@ const PARAM_TYPE_OPTS = [ * the spec ADDS is a missing-key error here rather than a silently absent * dropdown entry, and one it REMOVES is an excess-property error. * + * That mechanism is what fired here: `@objectstack/spec` 17.0.0-rc.6 retired + * `global_nav` from `ACTION_LOCATIONS` (objectstack#6888), and the entry that + * used to sit at the end of this map — `'Global nav / command palette'` — + * became a TS2353 excess property the moment the resolved vocabulary dropped to + * six. It is removed rather than re-typed; the six below are the whole + * vocabulary. + * * Insertion order is the display order — an authoring-friendly grouping - * (record → list → global), deliberately not the spec's declaration order. + * (record → list), deliberately not the spec's declaration order. */ const LOCATION_LABELS: Record = { record_header: 'Record header', @@ -129,7 +136,6 @@ const LOCATION_LABELS: Record = { record_related: 'Record · related list', list_toolbar: 'List toolbar', list_item: 'List · row', - global_nav: 'Global nav / command palette', }; const LOCATIONS: Array<{ value: ActionLocation; label: string }> = ( diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/DashboardDefaultInspector.tsx b/packages/app-shell/src/views/metadata-admin/inspectors/DashboardDefaultInspector.tsx index 48f0714b02..83a4aa1627 100644 --- a/packages/app-shell/src/views/metadata-admin/inspectors/DashboardDefaultInspector.tsx +++ b/packages/app-shell/src/views/metadata-admin/inspectors/DashboardDefaultInspector.tsx @@ -45,6 +45,14 @@ import { SchemaForm } from '../SchemaForm'; import { getDashboardForm, getDashboardSchema } from '../dashboard-schema'; import { mergeServerFields } from '../mergeServerFields'; import { t } from '../i18n'; +// `DashboardWidget.title` is the spec's `I18nLabel`, which @objectstack/spec +// 17.0.0-rc.6 widened from plain `string` to `string | Record` +// (the inline per-locale map, folded in from the retired `I18nObject`). Every +// read below lands in a text slot — a ` {w?.type} {!readOnly && ( diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/DashboardWidgetInspector.tsx b/packages/app-shell/src/views/metadata-admin/inspectors/DashboardWidgetInspector.tsx index d2eceb6890..64b6517b4d 100644 --- a/packages/app-shell/src/views/metadata-admin/inspectors/DashboardWidgetInspector.tsx +++ b/packages/app-shell/src/views/metadata-admin/inspectors/DashboardWidgetInspector.tsx @@ -33,6 +33,9 @@ import type { DashboardWidgetSchema } from '@object-ui/types'; import { resolveDashboardFilterDefs, type DashboardFilterDef } from '@object-ui/core'; import type { MetadataInspectorProps } from '../inspector-registry'; import { t, tFormat } from '../i18n'; +// The spec's `I18nLabel` resolver (new in @objectstack/spec 17.0.0-rc.6), +// aliased apart from objectui's same-named translation-KEY resolver. +import { resolveI18nLabel as resolveInlineI18nLabel } from '@objectstack/spec/ui'; import { InspectorCheckboxField, InspectorReorderButtons, moveArray } from './_shared'; import { InspectorComboField, type InspectorComboOption } from './InspectorComboField'; import { DatasetNamesEditor } from './ReportDefaultInspector'; @@ -183,7 +186,11 @@ export function DashboardWidgetInspector({ function moveWidget(to: number) { onPatch({ widgets: moveArray(widgetsAll, index, to) }); if (widget.id) { - onSelectionChange?.({ kind: 'widget', id: widget.id, label: widget.title ?? undefined }); + onSelectionChange?.({ + kind: 'widget', + id: widget.id, + label: resolveInlineI18nLabel(widget.title, locale), + }); } } @@ -195,7 +202,9 @@ export function DashboardWidgetInspector({ {t('engine.inspector.widget.kind', locale)}
- {widget.title || selection.label || `Widget ${index + 1}`} + {resolveInlineI18nLabel(widget.title, locale) || + selection.label || + `Widget ${index + 1}`}
@@ -221,12 +230,30 @@ export function DashboardWidgetInspector({
+ {/* The ONE authoring — not display — read of `widget.title`, and the only + site in this change where following the rc.6 widening mechanically + would have destroyed data. `I18nLabel` now admits an inline per-locale + map, and this is a single-line text input: resolving the map into it + and writing `e.target.value` straight back would silently collapse + every other locale the author wrote, on the first keystroke. So the + input stays the plain-string editor it has always been, and a + map-valued title is shown resolved and READ-ONLY instead of being + flattened. Nothing in any corpus can hit this path yet — `I18nLabel` + was plain `string` through 17.0.0-rc.5, so no stored widget title can + be a map — which is exactly why the conservative branch is safe to + take now and why authoring the map form is follow-up work + (objectui#4163, part 2) rather than a guess made here. */} patchWidget({ title: e.target.value })} disabled={readOnly} + readOnly={widget.title != null && typeof widget.title !== 'string'} /> diff --git a/packages/app-shell/src/views/metadata-admin/previews/ActionPreview.tsx b/packages/app-shell/src/views/metadata-admin/previews/ActionPreview.tsx index c85fb82dcf..83f24fcf03 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/ActionPreview.tsx +++ b/packages/app-shell/src/views/metadata-admin/previews/ActionPreview.tsx @@ -578,14 +578,18 @@ function PlacementPreview({ locations, label, icon, variant, iconOnly }: { )} - {locations.includes('global_nav') && ( - -
-
⌘K · Command palette
-
{label}
-
- - )} + {/* No `global_nav` frame. The location was retired from the spec's + `ACTION_LOCATIONS` in @objectstack/spec 17.0.0-rc.6 (objectstack#6888, + maintainer ruling 2026-08-09 direction 2) because no running-app + surface ever rendered it: the console's ⌘K palette + (`chrome/CommandPalette.tsx`) builds its groups from nav items, + objects, dashboards, pages, reports, recent items, record search and + theme, and reads no action metadata at all. The frame that used to + stand here drew the author a mock "⌘K · Command palette" preview, so + the designer promised a surface the product does not have — the + ADR-0078 "declares, 'renders', reports success, and does nothing" + shape, which is the sharpest harm the ruling names. Pinned negatively + in `__tests__/ActionPreview.locations.test.tsx`. */} ); } diff --git a/packages/app-shell/src/views/metadata-admin/previews/DashboardPreview.tsx b/packages/app-shell/src/views/metadata-admin/previews/DashboardPreview.tsx index e1b6ad6e4a..f84ed669ae 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/DashboardPreview.tsx +++ b/packages/app-shell/src/views/metadata-admin/previews/DashboardPreview.tsx @@ -23,6 +23,9 @@ import type { MetadataPreviewProps } from '../preview-registry'; import { PreviewShell, PreviewErrorBoundary, PreviewMessage } from './PreviewShell'; import { uniqueId, appendArray } from '../inspectors/_shared'; import { t as tr } from '../i18n'; +// The spec's own `I18nLabel` resolver, aliased so it is never confused with +// objectui's same-named translation-KEY resolver in `app-shell/src/utils`. +import { resolveI18nLabel as resolveInlineI18nLabel } from '@objectstack/spec/ui'; import { AddWidgetPicker } from './AddWidgetPicker'; import { WIDGET_TYPE_META } from './widget-types'; @@ -62,10 +65,14 @@ export function DashboardPreview({ onSelectionChange({ kind: 'widget', id: widgetId, - label: w?.title || widgetId, + // `DashboardWidget.title` became `string | Record` in + // @objectstack/spec 17.0.0-rc.6 (`I18nLabel` absorbed the inline + // per-locale map). `SelectionChange.label` is a `string`, so the map + // form is resolved here rather than stringified at the render site. + label: resolveInlineI18nLabel(w?.title, locale) || widgetId, }); }, - [onSelectionChange, widgets], + [onSelectionChange, widgets, locale], ); const handleReorder = React.useCallback( diff --git a/packages/app-shell/src/views/metadata-admin/previews/__tests__/ActionPreview.locations.test.tsx b/packages/app-shell/src/views/metadata-admin/previews/__tests__/ActionPreview.locations.test.tsx new file mode 100644 index 0000000000..09a656cf9f --- /dev/null +++ b/packages/app-shell/src/views/metadata-admin/previews/__tests__/ActionPreview.locations.test.tsx @@ -0,0 +1,113 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * objectstack#6888 / objectstack#7100 — the placement preview draws a frame for + * every action location the product actually renders, and for no other. + * + * ## What this replaces + * + * `ActionPreview` used to close its `PlacementPreview` with a `global_nav` + * frame drawing the author a mock command palette: + * + * ⌘K · Command palette + * [ New Task ] + * + * No such surface exists. The console's palette (`chrome/CommandPalette.tsx`) + * builds its groups from nav items, objects, dashboards, pages, reports, recent + * items, record search and theme; it holds no reference to `global_nav`, to + * `actionRendersAt`, or to any action-metadata source at all. So an author who + * declared `locations: ['global_nav']`, saw it previewed and shipped it got a + * button that rendered nowhere — the ADR-0078 "declares, 'renders', reports + * success, and does nothing" shape, arriving through the location vocabulary. + * The maintainer's 2026-08-09 ruling on #6888 (direction 2) retired the member; + * @objectstack/spec 17.0.0-rc.6 shipped that retirement. + * + * ## Why the assertions are paired + * + * Deleting the frame makes any "does not render global_nav" check pass forever, + * including if `PlacementPreview` were deleted whole or silently stopped + * rendering. So each negative pin here sits next to a positive one over the SAME + * render: a live location must still draw its frame in the same tree where the + * retired one must not appear. A vacuous pass then requires both halves to fail + * at once, which no plausible edit does. + */ + +import * as React from 'react'; +import { describe, it, expect, afterEach } from 'vitest'; +import { render, screen, within, cleanup } from '@testing-library/react'; +import { ACTION_LOCATIONS } from '@objectstack/spec/ui'; + +import { ActionPreview } from '../ActionPreview'; + +afterEach(cleanup); + +function renderWithLocations(locations: string[]) { + return render( + , + ); +} + +/** + * The "Where it appears" section — the PLACEMENT simulation, and the only + * surface this change is about. + * + * Scoping matters and is not incidental. Higher up, the metadata strip echoes + * every token the draft declares as a chip under "Locations:", so an unscoped + * `queryByText('global_nav')` matches the ECHO and can never distinguish it + * from a frame. The echo is correct and deliberately untouched: it reports what + * this (possibly stale) draft says, which is honest, whereas the frame CLAIMED + * the platform renders it. That distinction is the whole retirement, so the + * test has to be able to see it. + */ +function placement() { + const heading = screen.getByText('Where it appears'); + // Section renders:
{title}
{children}
+ return within(heading.parentElement!.parentElement!); +} + +describe('ActionPreview placement frames', () => { + it('draws a frame for a live location', () => { + renderWithLocations(['record_header']); + expect(placement().getByText('record_header')).toBeTruthy(); + }); + + it('does NOT draw a frame for the retired global_nav location', () => { + // Both halves over one render: `record_header` proves the placement preview + // is alive and drawing, `global_nav` proves the retired frame is gone. + renderWithLocations(['record_header', 'global_nav']); + expect(placement().getByText('record_header')).toBeTruthy(); + expect(placement().queryByText('global_nav')).toBeNull(); + }); + + it('renders no command-palette mock for a global_nav-only action', () => { + renderWithLocations(['global_nav']); + // The mock frame's caption was literally "⌘K · Command palette". Asserting + // the visible string rather than a testid is deliberate: the harm the + // ruling names is what the AUTHOR SEES, so the pin is on what is displayed. + expect(screen.queryByText(/Command palette/i)).toBeNull(); + expect(placement().queryByText('global_nav')).toBeNull(); + // The section header still renders (it is gated on `locations.length`, not + // on how many frames draw), and its body is now empty. That is the truthful + // answer for a stale draft whose only location the platform retired — it + // appears nowhere — so it is pinned rather than left to look like a bug. + expect(screen.getByText('Where it appears')).toBeTruthy(); + }); + + it('the vocabulary this preview draws from no longer declares global_nav', () => { + // The upstream half of the same fact. Without it, a future spec that + // re-added the member would leave the pins above passing while the designer + // silently stopped previewing a location the product does declare. + expect(ACTION_LOCATIONS as readonly string[]).not.toContain('global_nav'); + expect(ACTION_LOCATIONS.length).toBe(6); + }); +}); diff --git a/packages/app-shell/src/views/metadata-admin/previews/__tests__/block-config.test.ts b/packages/app-shell/src/views/metadata-admin/previews/__tests__/block-config.test.ts index c48cbf5280..0f202c2514 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/__tests__/block-config.test.ts +++ b/packages/app-shell/src/views/metadata-admin/previews/__tests__/block-config.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { PageComponentType, RecordDetailsProps } from '@objectstack/spec/ui'; +import { ACTION_LOCATIONS, PageComponentType, RecordDetailsProps } from '@objectstack/spec/ui'; import { BLOCK_CONFIG, blockHasConfig, type PlaceholderSpec } from '../block-config'; import { BLOCK_TYPE_META, PALETTE_EXCLUSIONS } from '../block-types'; import { t } from '../../i18n'; @@ -243,4 +243,43 @@ describe('page palette ↔ spec PageComponentType coverage', () => { ); expect(orphanPanels, 'these expose a config panel but cannot be authored').toEqual([]); }); + + describe("record:quick_actions `location` — the designer's action-location dropdown", () => { + const locationField = () => + BLOCK_CONFIG['record:quick_actions'].find((f) => f.name === 'location') as + | { options?: Array<{ value: string }> } + | undefined; + + // POSITIVE half. Without it the negative pin below passes on an empty + // option list, which is how a deleted dropdown reads as a passing test. + it('offers exactly the locations the spec declares', () => { + const offered = (locationField()?.options ?? []).map((o) => o.value); + expect([...offered].sort()).toEqual([...ACTION_LOCATIONS].sort()); + }); + + // NEGATIVE pin, converted from the coverage the removed option used to + // carry. `global_nav` was retired from `ACTION_LOCATIONS` in + // @objectstack/spec 17.0.0-rc.6 (objectstack#6888, maintainer ruling + // 2026-08-09 direction 2): no running-app surface ever rendered it, and the + // console's ⌘K palette reads no action metadata at all. An option the + // schema now rejects by name must not be offerable, or the designer teaches + // authors — and every AI copying this corpus — to write metadata that fails + // to parse. + it('does NOT offer the retired global_nav location', () => { + const offered = (locationField()?.options ?? []).map((o) => o.value); + expect(offered.length, 'option list is empty — the pin would be vacuous').toBeGreaterThan(0); + expect(offered).not.toContain('global_nav'); + expect(ACTION_LOCATIONS as readonly string[]).not.toContain('global_nav'); + }); + + // The i18n side of the same removal: an option key kept past its option is + // dead vocabulary the next author reads as a live surface, so BOTH locale + // tables lost `…option.location.global_nav`. `t()` returns the key + // unchanged on a miss, which is exactly "this locale has no translation". + it('has no leftover translation for the retired option in either locale', () => { + const key = 'engine.inspector.pageBlock.option.location.global_nav'; + expect(t(key, 'en-US')).toBe(key); + expect(t(key, 'zh-CN')).toBe(key); + }); + }); }); diff --git a/packages/app-shell/src/views/metadata-admin/previews/block-config.ts b/packages/app-shell/src/views/metadata-admin/previews/block-config.ts index 8972f627c0..8996c6699a 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/block-config.ts +++ b/packages/app-shell/src/views/metadata-admin/previews/block-config.ts @@ -472,7 +472,10 @@ export const BLOCK_CONFIG: Record = { { value: 'record_related', label: 'engine.inspector.pageBlock.option.location.record_related' }, { value: 'list_toolbar', label: 'engine.inspector.pageBlock.option.location.list_toolbar' }, { value: 'list_item', label: 'engine.inspector.pageBlock.option.location.list_item' }, - { value: 'global_nav', label: 'engine.inspector.pageBlock.option.location.global_nav' }, + // No `global_nav` option: the location was retired from the spec's + // `ACTION_LOCATIONS` in @objectstack/spec 17.0.0-rc.6 (objectstack#6888). + // Offering it here would let the designer author a value the schema now + // rejects by name. Pinned negatively in `__tests__/block-config.test.ts`. ], }, ], diff --git a/packages/auth/package.json b/packages/auth/package.json index e2b9b7f2fe..1453a46e19 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -38,7 +38,7 @@ }, "dependencies": { "@object-ui/types": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "better-auth": "^1.6.26" }, "devDependencies": { diff --git a/packages/collaboration/package.json b/packages/collaboration/package.json index 50814228d3..5a43c3c6a7 100644 --- a/packages/collaboration/package.json +++ b/packages/collaboration/package.json @@ -41,7 +41,7 @@ "@object-ui/types": "workspace:*" }, "devDependencies": { - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "@types/react": "19.2.18", "react": "19.2.8", "typescript": "^6.0.3", diff --git a/packages/components/package.json b/packages/components/package.json index 16fcff4251..012a656582 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -40,7 +40,7 @@ "@object-ui/react-runtime": "workspace:*", "@object-ui/sdui-parser": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "@radix-ui/react-accordion": "^1.2.20", "@radix-ui/react-alert-dialog": "^1.1.23", "@radix-ui/react-aspect-ratio": "^1.1.15", diff --git a/packages/components/src/renderers/action/action-bar.tsx b/packages/components/src/renderers/action/action-bar.tsx index f13c30d297..b06e2e9e12 100644 --- a/packages/components/src/renderers/action/action-bar.tsx +++ b/packages/components/src/renderers/action/action-bar.tsx @@ -15,8 +15,13 @@ * `maxVisible` threshold are grouped into an overflow "More" dropdown. * * This is the "bridge" component that connects ActionSchema metadata to the UI, - * enabling server-driven action rendering at list_toolbar, record_header, - * list_item, record_more, record_related, and global_nav locations. + * enabling server-driven action rendering at every location the spec declares: + * list_toolbar, list_item, record_header, record_more, record_related and + * record_section. (`global_nav` used to close that list; it was retired from + * `ACTION_LOCATIONS` in @objectstack/spec 17.0.0-rc.6 — objectstack#6888 — as a + * location no running-app surface ever rendered. The enum this component + * publishes is `[...ACTION_LOCATIONS]`, so it followed the retirement on its + * own; only this prose had to be aligned.) * * @example * ```tsx diff --git a/packages/core/package.json b/packages/core/package.json index 74e37d70f2..3efa776478 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -32,8 +32,8 @@ }, "dependencies": { "@object-ui/types": "workspace:*", - "@objectstack/formula": "^17.0.0-rc.5", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/formula": "^17.0.0-rc.6", + "@objectstack/spec": "^17.0.0-rc.6", "lodash": "^4.18.1", "zod": "^4.4.3" }, diff --git a/packages/core/src/actions/ActionRunner.ts b/packages/core/src/actions/ActionRunner.ts index 719f65113e..68373203e1 100644 --- a/packages/core/src/actions/ActionRunner.ts +++ b/packages/core/src/actions/ActionRunner.ts @@ -22,7 +22,7 @@ */ import type { RunnableActionType } from '@object-ui/types'; -import type { ActionInput as SpecActionInput } from '@objectstack/spec/ui'; +import type { Action as SpecActionInput } from '@objectstack/spec/ui'; import { ExpressionEvaluator } from '../evaluator/ExpressionEvaluator'; import { hasDeclaredPredicate } from '../evaluator/declaredPredicate'; import { globalUndoManager, type UndoableOperation } from './UndoManager'; @@ -244,14 +244,21 @@ export interface ActionDef { // hand-copied: a hand-written duplicate of a spec shape is a second contract // that drifts silently, which is the failure this whole issue is about. // - // Derived from `z.input` (`ActionInput`), not `z.infer` (`Action`), and the - // difference is load-bearing rather than stylistic. `ActionSchema` is a - // `ZodPipe` whose transforms narrow the authored shape — `visible` is authored - // as `string | { dialect, source }` but INFERS to the object form alone. This - // runner consumes authored/stored rows, which #3903 established are rehydrated - // UNPARSED, so it sees the input shape. Deriving from `Action` would have - // type-errored the raw-string predicate that `ActionEngine` explicitly - // supports and that `ActionEngine.visibility.test.ts` pins. + // Derived from `z.input`, not `z.infer`, and the difference is load-bearing + // rather than stylistic. `ActionSchema` is a `ZodPipe` whose transforms narrow + // the authored shape — `visible` is authored as `string | { dialect, source }` + // but INFERS to the object form alone. This runner consumes authored/stored + // rows, which #3903 established are rehydrated UNPARSED, so it sees the input + // shape. Deriving from the `z.infer` side would type-error the raw-string + // predicate that `ActionEngine` explicitly supports and that + // `ActionEngine.visibility.test.ts` pins. + // + // WHICH SPEC NAME carries `z.input` changed under us in `@objectstack/spec` + // 17.0.0-rc.6: up to rc.5 it was `ActionInput` (with `Action` = `z.infer`), + // and rc.6 retired every `…Input` alias and moved the bare name onto the input + // side (`Action` = `z.input`, `ActionParsed` = `z.infer`). The import above is + // therefore `Action as SpecActionInput` — the local alias keeps naming the + // side this file needs, so every derivation below is unchanged in meaning. // // `shortcut` and `bulkEnabled` land here as `undefined`, not as a usable type // — that is correct and deliberate. Spec 17 retired both as `retiredKey()` diff --git a/packages/data-objectstack/package.json b/packages/data-objectstack/package.json index 176c4e6b43..0715bcb71a 100644 --- a/packages/data-objectstack/package.json +++ b/packages/data-objectstack/package.json @@ -32,8 +32,8 @@ "dependencies": { "@object-ui/core": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/client": "^17.0.0-rc.5", - "@objectstack/spec": "^17.0.0-rc.5" + "@objectstack/client": "^17.0.0-rc.6", + "@objectstack/spec": "^17.0.0-rc.6" }, "devDependencies": { "tsup": "^8.5.1", diff --git a/packages/fields/package.json b/packages/fields/package.json index 1a75f4dc61..a40cafe3f6 100644 --- a/packages/fields/package.json +++ b/packages/fields/package.json @@ -37,7 +37,7 @@ "@object-ui/providers": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "clsx": "^2.1.1", "lucide-react": "^1.29.0", "react-markdown": "^10.1.0", diff --git a/packages/layout/package.json b/packages/layout/package.json index 0fc6ccbb58..2a5add518c 100644 --- a/packages/layout/package.json +++ b/packages/layout/package.json @@ -42,7 +42,7 @@ "react-router-dom": "^6.0.0 || ^7.0.0" }, "devDependencies": { - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "@vitejs/plugin-react": "^6.0.5", "react-router-dom": "^7.18.2", "vite": "^8.2.1", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 498b988acf..5eb4658521 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -40,7 +40,7 @@ "@object-ui/types": "workspace:*" }, "devDependencies": { - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "@types/react": "19.2.18", "react": "19.2.8", "typescript": "^6.0.3", diff --git a/packages/plugin-charts/package.json b/packages/plugin-charts/package.json index 35355a4547..23a1c46bf0 100644 --- a/packages/plugin-charts/package.json +++ b/packages/plugin-charts/package.json @@ -44,7 +44,7 @@ "react-dom": "^18.0.0 || ^19.0.0" }, "devDependencies": { - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "@types/react": "19.2.18", "@types/react-dom": "19.2.4", "@vitejs/plugin-react": "^6.0.5", diff --git a/packages/plugin-chatbot/package.json b/packages/plugin-chatbot/package.json index 5f67079a6c..1432a348d4 100644 --- a/packages/plugin-chatbot/package.json +++ b/packages/plugin-chatbot/package.json @@ -38,7 +38,7 @@ "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", "@radix-ui/react-slot": "^1.3.3", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "@radix-ui/react-use-controllable-state": "^1.2.6", "ai": "^7.0.56", "class-variance-authority": "^0.7.1", diff --git a/packages/plugin-dashboard/package.json b/packages/plugin-dashboard/package.json index b44b7c022f..4fcf51d219 100644 --- a/packages/plugin-dashboard/package.json +++ b/packages/plugin-dashboard/package.json @@ -43,7 +43,7 @@ }, "devDependencies": { "@object-ui/plugin-charts": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "@types/react-grid-layout": "^2.1.0", "@vitejs/plugin-react": "^6.0.5", "react-grid-layout": "^2.2.4", diff --git a/packages/plugin-detail/package.json b/packages/plugin-detail/package.json index 9ca27ce13c..90c7eba797 100644 --- a/packages/plugin-detail/package.json +++ b/packages/plugin-detail/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@object-ui/i18n": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "lucide-react": "^1.29.0" }, "peerDependencies": { diff --git a/packages/plugin-form/package.json b/packages/plugin-form/package.json index 9dfeb7efec..2bd1f08031 100644 --- a/packages/plugin-form/package.json +++ b/packages/plugin-form/package.json @@ -28,7 +28,7 @@ "@object-ui/permissions": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "lucide-react": "^1.29.0" }, "peerDependencies": { diff --git a/packages/plugin-gantt/package.json b/packages/plugin-gantt/package.json index 9f38fe7369..02f46495c4 100644 --- a/packages/plugin-gantt/package.json +++ b/packages/plugin-gantt/package.json @@ -38,7 +38,7 @@ "@object-ui/plugin-detail": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "lucide-react": "^1.29.0", "sonner": "^2.0.7" }, diff --git a/packages/plugin-grid/package.json b/packages/plugin-grid/package.json index d265529c26..2aa5bdee67 100644 --- a/packages/plugin-grid/package.json +++ b/packages/plugin-grid/package.json @@ -29,7 +29,7 @@ "@object-ui/permissions": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "@tanstack/react-virtual": "^3.14.9", "exceljs": "^4.4.0", "lucide-react": "^1.29.0" @@ -40,7 +40,7 @@ }, "devDependencies": { "@object-ui/data-objectstack": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "@vitejs/plugin-react": "^6.0.5", "msw": "^2.15.0", "typescript": "^6.0.3", diff --git a/packages/plugin-list/package.json b/packages/plugin-list/package.json index d21556228c..fdcf00d853 100644 --- a/packages/plugin-list/package.json +++ b/packages/plugin-list/package.json @@ -42,7 +42,7 @@ "@object-ui/permissions": "workspace:^", "@object-ui/react": "workspace:^", "@object-ui/types": "workspace:^", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, @@ -55,7 +55,7 @@ "@object-ui/permissions": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "@types/react": "19.2.18", "@types/react-dom": "19.2.4", "@vitejs/plugin-react": "^6.0.5", diff --git a/packages/plugin-map/package.json b/packages/plugin-map/package.json index 0b575bd7e3..68ef4142c5 100644 --- a/packages/plugin-map/package.json +++ b/packages/plugin-map/package.json @@ -35,7 +35,7 @@ "@object-ui/core": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "lucide-react": "^1.29.0", "maplibre-gl": "^6.2.0", "react-map-gl": "^8.1.2", diff --git a/packages/plugin-report/package.json b/packages/plugin-report/package.json index 6158f6e033..10316f63d9 100644 --- a/packages/plugin-report/package.json +++ b/packages/plugin-report/package.json @@ -43,7 +43,7 @@ "react-dom": "^18.0.0 || ^19.0.0" }, "devDependencies": { - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "@types/node": "^26.1.2", "@types/react": "19.2.18", "@types/react-dom": "19.2.4", diff --git a/packages/plugin-timeline/package.json b/packages/plugin-timeline/package.json index 8e0fe5e8a5..ef8d156438 100644 --- a/packages/plugin-timeline/package.json +++ b/packages/plugin-timeline/package.json @@ -37,7 +37,7 @@ "@object-ui/mobile": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "class-variance-authority": "^0.7.1", "zod": "^4.4.3" }, diff --git a/packages/plugin-tree/package.json b/packages/plugin-tree/package.json index eacd49c26a..62dc8843ab 100644 --- a/packages/plugin-tree/package.json +++ b/packages/plugin-tree/package.json @@ -36,7 +36,7 @@ "@object-ui/i18n": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "lucide-react": "^1.29.0" }, "peerDependencies": { diff --git a/packages/plugin-view/package.json b/packages/plugin-view/package.json index c7095b4e3d..e8217ba256 100644 --- a/packages/plugin-view/package.json +++ b/packages/plugin-view/package.json @@ -31,7 +31,7 @@ "@object-ui/plugin-grid": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "class-variance-authority": "^0.7.1", "lucide-react": "^1.29.0" }, diff --git a/packages/providers/package.json b/packages/providers/package.json index 1ea4937ed7..59a457403a 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -31,7 +31,7 @@ }, "dependencies": { "@object-ui/types": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5" + "@objectstack/spec": "^17.0.0-rc.6" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", diff --git a/packages/react/package.json b/packages/react/package.json index e32b903772..4db2939c2b 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -34,7 +34,7 @@ "@object-ui/data-objectstack": "workspace:*", "@object-ui/i18n": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "react-hook-form": "^7.84.0" }, "peerDependencies": { diff --git a/packages/react/src/hooks/__tests__/useActionEngine.test.ts b/packages/react/src/hooks/__tests__/useActionEngine.test.ts index 4bfcd9ca27..87cb88368b 100644 --- a/packages/react/src/hooks/__tests__/useActionEngine.test.ts +++ b/packages/react/src/hooks/__tests__/useActionEngine.test.ts @@ -4,6 +4,7 @@ import { describe, it, expect, vi } from 'vitest'; import { renderHook, act } from '@testing-library/react'; +import { ActionLocationSchema } from '@objectstack/spec/ui'; import { useActionEngine } from '../useActionEngine'; describe('useActionEngine', () => { @@ -28,6 +29,14 @@ describe('useActionEngine', () => { locations: ['list_item'], }, { + // Deliberately STALE, in the same posture as the `bulkEnabled` and + // `shortcut` keys this fixture also keeps: `global_nav` was retired from + // the spec's `ACTION_LOCATIONS` in @objectstack/spec 17.0.0-rc.6 + // (objectstack#6888), so metadata like this can no longer be authored. + // Kept rather than deleted because the coverage that matters now is the + // INVERSE — see 'does not offer an action declaring the retired + // global_nav location' below. Deleting the entry would delete the only + // input that can prove the retired location leaks nowhere. name: 'global_search', type: 'script', target: 'true', @@ -36,6 +45,16 @@ describe('useActionEngine', () => { }, ]; + /** The six locations `ACTION_LOCATIONS` declares as of 17.0.0-rc.6. */ + const LIVE_LOCATIONS = [ + 'list_toolbar', + 'list_item', + 'record_header', + 'record_more', + 'record_related', + 'record_section', + ] as const; + describe('getActionsForLocation', () => { it('returns actions for list_toolbar', () => { const { result } = renderHook(() => @@ -75,6 +94,61 @@ describe('useActionEngine', () => { const actions = result.current.getActionsForLocation('record_related'); expect(actions.length).toBe(0); }); + + // `global_nav` was retired from `ACTION_LOCATIONS` in @objectstack/spec + // 17.0.0-rc.6 (objectstack#6888) because no running-app surface ever + // rendered it. `sampleActions` still declares it, so this pins what a stale + // declaration can and cannot do: it must surface at NONE of the six live + // locations. + it('does not offer an action declaring the retired global_nav location', () => { + const { result } = renderHook(() => + useActionEngine({ actions: sampleActions }), + ); + + for (const location of LIVE_LOCATIONS) { + const names = result.current + .getActionsForLocation(location) + .map((a) => a.name); + expect(names, `global_search leaked into ${location}`).not.toContain( + 'global_search', + ); + } + }); + + // The OTHER half, and it is deliberately NOT the symmetric one. Asking the + // engine for `'global_nav'` still returns the stale action, and that is + // correct: `ActionEngine.getActionsForLocation` is a literal string match + // over whatever a host registered, not a vocabulary filter. Narrowing it to + // the six live members would be renderer-side enforcement of a spec + // vocabulary — precisely the tolerant/lenient consumer shape AGENTS.md #0.1 + // forbids, and it would put a SECOND rejection point beside the schema's. + // + // Enforcement lives in two places instead, both pinned here: + // 1. the TYPE — `getActionsForLocation(location: ActionLocation)` is now + // six-membered, so no type-correct caller can spell the retired value + // at all (the cast below is what a stale caller would have to write); + // 2. the SCHEMA — `ActionLocationSchema` rejects it by name at authoring + // and publish time, with the retirement message pointing at the fix. + // Between them, metadata carrying `global_nav` cannot be saved, and no live + // caller can ask for it. This test was written expecting the engine to + // return `[]` and measured otherwise; the measurement is what it now pins. + it('leaves the retired location to the schema, not to a runtime filter', () => { + const { result } = renderHook(() => + useActionEngine({ actions: sampleActions }), + ); + + const retired = result.current.getActionsForLocation( + 'global_nav' as (typeof LIVE_LOCATIONS)[number], + ); + expect(retired.map((a) => a.name)).toEqual(['global_search']); + + const rejected = ActionLocationSchema.safeParse('global_nav'); + expect(rejected.success).toBe(false); + expect(rejected.error?.issues?.[0]?.message).toContain( + 'was removed from `ACTION_LOCATIONS`', + ); + expect(ActionLocationSchema.safeParse('record_header').success).toBe(true); + }); }); describe('getBulkActions', () => { diff --git a/packages/runner/package.json b/packages/runner/package.json index 74417bc6ed..c692a4734b 100644 --- a/packages/runner/package.json +++ b/packages/runner/package.json @@ -37,7 +37,7 @@ "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "@tailwindcss/postcss": "^4.3.3", "@types/react": "19.2.18", "@types/react-dom": "19.2.4", diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json index aaf926c97e..f6f51f7259 100644 --- a/packages/sdui-parser/package.json +++ b/packages/sdui-parser/package.json @@ -33,6 +33,6 @@ "devDependencies": { "@object-ui/core": "workspace:*", "@object-ui/react": "workspace:*", - "@objectstack/spec": "^17.0.0-rc.5" + "@objectstack/spec": "^17.0.0-rc.6" } } diff --git a/packages/types/package.json b/packages/types/package.json index e8178996c7..ebf2a9058a 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -90,7 +90,7 @@ "directory": "packages/types" }, "dependencies": { - "@objectstack/spec": "^17.0.0-rc.5", + "@objectstack/spec": "^17.0.0-rc.6", "zod": "^4.4.3" }, "devDependencies": { diff --git a/packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts b/packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts index 6509dd657d..dcdc04b802 100644 --- a/packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts +++ b/packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts @@ -561,8 +561,9 @@ describe('renamed local dialects do not collide with a spec export (objectui#307 ['PageRegion', 'PageNodeRegion'], ['PageRegionSchema', 'PageNodeRegionSchema'], ['ResponsiveConfig', 'MobileResponsiveConfig'], - ['WidgetManifest', 'RuntimeWidgetManifest'], - ['WidgetSource', 'RuntimeWidgetSource'], + // `WidgetManifest` / `WidgetSource` moved OUT of this list on the + // 17.0.0-rc.6 bump — see the block below. The tripwire fired for the second + // time, the same way objectui#3363 recorded the first. ])('the spec still owns `%s`, which is why the rename to `%s` happened', (owned) => { // The other direction of the tripwire: if the spec RETIRES one of these, // the local dialect can take the natural name back. A workaround should not @@ -586,6 +587,38 @@ describe('renamed local dialects do not collide with a spec export (objectui#307 * * The third did not move — see the `OfflineConfig` block below. */ + /** + * THE TRIPWIRE FIRED A SECOND TIME, on the @objectstack/spec 17.0.0-rc.6 bump + * (objectstack#7100). rc.6 retired the whole widget-manifest vocabulary — + * `WidgetManifest`, `WidgetSource`, `WidgetProperty`, `WidgetEvent`, + * `WidgetLifecycle` and their `…Schema`/`…Parsed` twins — so the spec stopped + * owning the two names that forced this package's `Runtime…` prefixes, and + * both rows moved from the "spec still owns" list above to this one. + * + * NOT reclaimed here, deliberately. The reclaim is available, not required: + * `RuntimeWidgetManifest`/`RuntimeWidgetSource` are exported names, so taking + * the bare names back is a cross-package rename with its own review, and this + * PR is a `global_nav` retirement that the bump merely rides under. What the + * rows below do is keep the vacancy PINNED — if the spec re-publishes either + * name while objectui holds it, that is a live collision, and this goes red. + * The unlock is recorded rather than taken: objectui#4164. + */ + it.each([ + ['WidgetManifest', 'RuntimeWidgetManifest'], + ['WidgetSource', 'RuntimeWidgetSource'], + ])( + 'the spec no longer owns `%s`, so `%s` keeps its prefix by choice, not by force (objectstack#7100)', + (vacated) => { + expect( + names, + `spec owns '${vacated}' again. The rc.6 retirement that vacated it has ` + + `been undone upstream, so the prefix on the local dialect is load-bearing ` + + `again — move this row back to the "spec still owns" list, and re-triage ` + + `any reclaim planned in objectui#4164.`, + ).not.toContain(vacated); + }, + ); + it.each([ ['GestureType', 'TouchGestureType'], ['GestureConfig', 'TouchGestureConfig'], diff --git a/packages/types/src/__tests__/spec-ui-schema-reexports.test.ts b/packages/types/src/__tests__/spec-ui-schema-reexports.test.ts index aa3dc9f78e..43175daeee 100644 --- a/packages/types/src/__tests__/spec-ui-schema-reexports.test.ts +++ b/packages/types/src/__tests__/spec-ui-schema-reexports.test.ts @@ -92,11 +92,15 @@ const DROPPED_SCHEMA_EXPORTS = [ 'AriaPropsSchema', // I18n 'I18nLabelSchema', - 'I18nObjectSchema', - 'LocaleConfigSchema', - 'PluralRuleSchema', - 'DateFormatSchema', - 'NumberFormatSchema', + // `I18nObjectSchema`, `LocaleConfigSchema`, `PluralRuleSchema`, + // `DateFormatSchema` and `NumberFormatSchema` were removed from this list on + // the @objectstack/spec 17.0.0-rc.6 bump (objectstack#7100): the spec RETIRED + // all five, so each row asserted "this name is not re-exported from + // @object-ui/types" about a name that no longer exists anywhere — vacuously + // true forever, which is what the guard above this list exists to catch. + // `I18nLabelSchema` SURVIVES rc.6 and stays denied; the release folded the + // per-locale record form into `I18nLabel` itself (`string` → + // `string | Record`) and ships `resolveI18nLabel` for it. // Responsive Design 'SpecResponsiveConfigSchema', 'BreakpointColumnMapSchema', diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 6b77dbef79..f6b60305ea 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1188,13 +1188,19 @@ export type { // ============================================================================ // v2.0.7 Spec UI Types — I18n // ============================================================================ +// `I18nObject`, `LocaleConfig`, `PluralRule`, `DateFormat` and `NumberFormat` +// (and their `…Schema` values) were retired from `@objectstack/spec/ui` in +// 17.0.0-rc.6. Nothing in this repo imported them from here — the only names +// that looked like consumers are `Intl.NumberFormat` call sites and +// `@object-ui/i18n`'s own locally declared formatter vocabulary in +// `utils/spec-formatters.ts`, which never bound the spec symbols. So these five +// were dead re-exports and are dropped rather than re-declared locally. +// `I18nLabel` SURVIVES rc.6 and stays bound by reference: it is the authoring +// label union (`string | Record`) that `ActionParam.label` and +// friends are typed against, and rc.6 additionally publishes the inline-map +// half as `InlineLocaleMap` plus the shared `resolveI18nLabel` resolver. export type { I18nLabel, - I18nObject, - LocaleConfig, - PluralRule, - DateFormat, - NumberFormat, } from '@objectstack/spec/ui'; // ============================================================================ diff --git a/packages/types/src/spec-report.ts b/packages/types/src/spec-report.ts index 1fb7483475..512e5b333f 100644 --- a/packages/types/src/spec-report.ts +++ b/packages/types/src/spec-report.ts @@ -40,11 +40,19 @@ * when translating a Report column into an ObjectQL `AggregationNode`. */ +// `@objectstack/spec` 17.0.0-rc.6 retired every `…Input` alias and moved the +// bare name onto the `z.input` side: `Report`/`ReportChart` are now +// `z.input` and `ReportParsed`/`ReportChartParsed` carry the +// `z.infer` shape these bindings used to read under the bare name. Both pairs +// below are therefore re-pointed so that each local alias keeps the meaning it +// had at rc.5 — `SpecReport`/`SpecReportChart` stay PARSED, `SpecReportInput`/ +// `SpecReportChartInput` stay INPUT. Following the rename by name alone (`X` → +// `X`) would have silently swapped the two. import type { - Report as SpecReportType_, - ReportInput as SpecReportInputType_, - ReportChart as SpecReportChartType_, - ReportChartInput as SpecReportChartInputType_, + ReportParsed as SpecReportType_, + Report as SpecReportInputType_, + ReportChartParsed as SpecReportChartType_, + ReportChart as SpecReportChartInputType_, } from '@objectstack/spec/ui'; import { diff --git a/packages/types/src/theme.ts b/packages/types/src/theme.ts index 7acb5e5682..57fa904f65 100644 --- a/packages/types/src/theme.ts +++ b/packages/types/src/theme.ts @@ -81,24 +81,31 @@ import type { * by reference (objectstack#4115) rather than restated. * * The interface this replaces was member-for-member identical to the spec's - * `ThemeInput` (verified by the mutual-assignability probe in + * authoring theme shape (verified by the mutual-assignability probe in * `__tests__/page-nav-misc-spec-parity.test.ts`), so the only thing the copy * added was a second place to drift from — under the spec's own symbol name, * which is what makes such a copy read as canonical to the next reader. * - * `ThemeInput`, not `Theme`: the spec's `z.infer` is the - * PARSED shape, where `mode` is required because `.default('auto')` has already - * run. Everything in objectui that carries a `Theme` is on the authoring side — - * theme JSON as stored, edited and handed to `ThemeProvider` — so the input - * shape (`mode` optional) is the one that is true here. This is the + * **The `z.input` shape, not the parsed one**, and in `@objectstack/spec` + * 17.0.0-rc.6 that shape is spelled `Theme`. Up to rc.5 the spec published + * three names — `Theme` (= `z.infer`), `ThemeParsed` (= `z.infer`) and + * `ThemeInput` (= `z.input`) — and this binding took `ThemeInput`. rc.6 retired + * every `…Input` alias and made the bare name the input: `Theme` is now + * `z.input` and `ThemeParsed` is the `z.infer` side. So + * `ThemeInput` → `Theme` is a rename that preserves this binding's meaning + * exactly; re-pointing at `ThemeParsed` would be the silent semantic change. + * The reason the input side is the true one here is unchanged: `mode` is + * required in the parsed shape because `.default('auto')` has already run, and + * everything in objectui that carries a `Theme` is on the authoring side — + * theme JSON as stored, edited and handed to `ThemeProvider`. This is the * `.default()`/`z.input` rule from objectui#3169: writing side → input type. * * This is the canonical JSON shape for a theme. It can be serialized, stored, * and applied at runtime via ThemeProvider. */ -export type { ThemeInput as Theme } from '@objectstack/spec/ui'; +export type { Theme } from '@objectstack/spec/ui'; -import type { ThemeInput as Theme } from '@objectstack/spec/ui'; +import type { Theme } from '@objectstack/spec/ui'; // ============================================================================ // ObjectUI Component Schemas (UI rendering) diff --git a/packages/types/src/zod/complex.zod.ts b/packages/types/src/zod/complex.zod.ts index 15ca470ffd..6b9922511a 100644 --- a/packages/types/src/zod/complex.zod.ts +++ b/packages/types/src/zod/complex.zod.ts @@ -327,8 +327,19 @@ export const DashboardWidgetSchema = specFieldsExcept(SpecDashboardWidgetSchema. * that object form, which the spec's `string | number | boolean` rejects. * * Drift guard: `__tests__/report-chart-query-spec-parity.test.ts`. + * + * `.safeExtend`, not `.extend`, since @objectstack/spec 17.0.0-rc.6: the spec's + * `GlobalFilterSchema` now carries a refinement, and zod 4 refuses `.extend()` + * on a refined object outright — *"Cannot overwrite keys on object schemas + * containing refinements. Use `.safeExtend()` instead."* — which threw at + * MODULE LOAD and took six `@object-ui/types` suites down with it before this + * file ran a single test. `.safeExtend` is zod's own prescribed replacement and + * KEEPS the refinement (measured: one check before, one check after), so the + * spec's cross-field rule still runs on objectui's dialect. That is the + * behaviour we want — the three divergences below widen individual FIELDS, and + * were never meant to switch off a whole-object rule. */ -export const GlobalFilterSchema = SpecGlobalFilterSchema.extend({ +export const GlobalFilterSchema = SpecGlobalFilterSchema.safeExtend({ options: z.array(z.union([ z.string(), z.object({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e36ecee63..2af93336f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,8 +35,8 @@ importers: specifier: ^10.0.1 version: 10.0.1(eslint@10.8.0(jiti@2.7.0)) '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@playwright/test': specifier: ^1.62.1 version: 1.62.1 @@ -267,11 +267,11 @@ importers: specifier: workspace:* version: link:../../packages/types '@objectstack/client': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@tailwindcss/postcss': specifier: ^4.3.3 version: 4.3.3 @@ -405,8 +405,8 @@ importers: specifier: workspace:* version: link:../../packages/types '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) fumadocs-core: specifier: 16.14.1 version: 16.14.1(@mdx-js/mdx@3.1.1)(@tanstack/react-router@1.170.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.29.0(react@19.2.8))(next@16.3.0(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.1.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(zod@4.4.3) @@ -721,14 +721,14 @@ importers: specifier: workspace:* version: link:../types '@objectstack/formula': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@objectstack/lint': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@sentry/react': specifier: ^10.69.0 version: 10.69.0(react@19.2.8) @@ -818,8 +818,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) better-auth: specifier: ^1.6.26 version: 1.6.26(@opentelemetry/api@1.9.1)(better-sqlite3@12.9.0)(mongodb@7.2.0)(next@16.3.0(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.1.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.10) @@ -911,8 +911,8 @@ importers: version: link:../types devDependencies: '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@types/react': specifier: 19.2.18 version: 19.2.18 @@ -947,8 +947,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@radix-ui/react-accordion': specifier: ^1.2.20 version: 1.2.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -1131,11 +1131,11 @@ importers: specifier: workspace:* version: link:../types '@objectstack/formula': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) lodash: specifier: ^4.18.1 version: 4.18.1 @@ -1193,11 +1193,11 @@ importers: specifier: workspace:* version: link:../types '@objectstack/client': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) devDependencies: tsup: specifier: ^8.5.1 @@ -1230,8 +1230,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -1347,8 +1347,8 @@ importers: version: 3.6.0 devDependencies: '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@vitejs/plugin-react': specifier: ^6.0.5 version: 6.0.5(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.10)(yaml@2.9.0)) @@ -1372,8 +1372,8 @@ importers: version: link:../types devDependencies: '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@types/react': specifier: 19.2.18 version: 19.2.18 @@ -1547,8 +1547,8 @@ importers: version: 3.10.1(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.6)(react@19.2.8)(redux@5.0.1) devDependencies: '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@types/react': specifier: 19.2.18 version: 19.2.18 @@ -1589,8 +1589,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@radix-ui/react-slot': specifier: ^1.3.3 version: 1.3.3(@types/react@19.2.18)(react@19.2.8) @@ -1708,8 +1708,8 @@ importers: specifier: workspace:* version: link:../plugin-charts '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@types/react-grid-layout': specifier: ^2.1.0 version: 2.1.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -1817,8 +1817,8 @@ importers: specifier: workspace:* version: link:../i18n '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) lucide-react: specifier: ^1.29.0 version: 1.29.0(react@19.2.8) @@ -1936,8 +1936,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) lucide-react: specifier: ^1.29.0 version: 1.29.0(react@19.2.8) @@ -1991,8 +1991,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) lucide-react: specifier: ^1.29.0 version: 1.29.0(react@19.2.8) @@ -2052,8 +2052,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@tanstack/react-virtual': specifier: ^3.14.9 version: 3.14.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -2193,8 +2193,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@types/react': specifier: 19.2.18 version: 19.2.18 @@ -2232,8 +2232,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) lucide-react: specifier: ^1.29.0 version: 1.29.0(react@19.2.8) @@ -2382,8 +2382,8 @@ importers: version: 3.6.0 devDependencies: '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@types/node': specifier: ^26.1.2 version: 26.1.2 @@ -2427,8 +2427,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -2482,8 +2482,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) lucide-react: specifier: ^1.29.0 version: 1.29.0(react@19.2.8) @@ -2546,8 +2546,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -2580,8 +2580,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) react: specifier: 19.2.8 version: 19.2.8 @@ -2614,8 +2614,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) react: specifier: 19.2.8 version: 19.2.8 @@ -2688,8 +2688,8 @@ importers: version: 1.0.7(tailwindcss@4.3.3) devDependencies: '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) '@tailwindcss/postcss': specifier: ^4.3.3 version: 4.3.3 @@ -2727,14 +2727,14 @@ importers: specifier: workspace:* version: link:../react '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) packages/types: dependencies: '@objectstack/spec': - specifier: ^17.0.0-rc.5 - version: 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + specifier: ^17.0.0-rc.6 + version: 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) zod: specifier: ^4.4.3 version: 4.4.3 @@ -4080,26 +4080,26 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@objectstack/client@17.0.0-rc.5': - resolution: {integrity: sha512-E1wiOK90d6CbiYtbQ0157oA8BKTBnf6JfizZ0vcH7gh89rrCCeL6dIsmo4SQyOYh+ZWtBRJMttrhLCLm35LBXQ==} + '@objectstack/client@17.0.0-rc.6': + resolution: {integrity: sha512-gdHhcY3JTRRmjlosIUP2WE4wG0ciwfYWu+HqyvtjFHVnhuNOoDOuzsKy2M/CppeOaKDay21Bs/HNH9ljGRQOnQ==} engines: {node: '>=22.0.0'} - '@objectstack/core@17.0.0-rc.5': - resolution: {integrity: sha512-kw8A9ERMscZwEnOXGDFmnUW0QdYcEhvsiHW6H4HgBQYJEqkbWXjBaS0ujbWxbn/hT5A+pEAiVQc2RcDVx/yHYA==} + '@objectstack/core@17.0.0-rc.6': + resolution: {integrity: sha512-78VndPhlxUXAupQVJ04YDdFbyIYcMJxw1I+Q7dlUuf+NGrlP7VzWwAQZm98dRH3q+M5wEvvV5t0myLhmVEpmKQ==} engines: {node: '>=22.0.0'} - '@objectstack/formula@17.0.0-rc.5': - resolution: {integrity: sha512-50DI3S/A8cyR4amwROxOcYD1r4om7bo/3Gf6MugEfWXfejzLpc9EUS3maIlYiW0orAmo20SHA81Q4cB+qGUxGw==} + '@objectstack/formula@17.0.0-rc.6': + resolution: {integrity: sha512-7vK6WgjeaO4rijaI4qZucRLsYGOPnrO/vuSd6wJCr1MpsH9go1yWLVte8itF0mTU0PhqXcnwtPHYCsxYnu4Y1Q==} - '@objectstack/lint@17.0.0-rc.5': - resolution: {integrity: sha512-0huaGN11ub7iqjuRzmiyUoh0Sju1U8tpVkp062ilOOF2bf0xzxgSVUdQrJlFNjV4+gzLJnkbqha1/4Y8Tr4dNA==} + '@objectstack/lint@17.0.0-rc.6': + resolution: {integrity: sha512-xZsa8DB2h76M0b6R4pFNnVZ1IzwGPxRI2vZhsFa5qNMiEWOLTc+WGVB76X18syzh0hhSMf69BR0G1G40/nOa5Q==} engines: {node: '>=22.0.0'} - '@objectstack/sdui-parser@17.0.0-rc.5': - resolution: {integrity: sha512-gi61YonxZnXQ5gPMa7CShz8d3Yg1ZGYC24jbDtOerbgOlBowc36Qrp3UKIL2JjW6PBQJJxUY+Y0h4lDwAEs/BA==} + '@objectstack/sdui-parser@17.0.0-rc.6': + resolution: {integrity: sha512-bao+TMFCGwu73ilvoJCrXLHWBvXhzHo/um1IHFJPcLXSRyee0Mea8rDy6Q2Ks04/VhYOza4M9rG1npoLk5DqQA==} - '@objectstack/spec@17.0.0-rc.5': - resolution: {integrity: sha512-9AFBwdwCaFPUFxGAShRMvW3UQAW+Gz0ET1CEJEHL5f2Qh/Bu7hGMBT7NY8fhYqfajAroe08jZGx3JMOFgS2v5Q==} + '@objectstack/spec@17.0.0-rc.6': + resolution: {integrity: sha512-xDdZVm7KtpcKZVyt0LSaGQ8OtAgaIZnPqug5zPkndySFGQKdjPtiANUfVVX4H/L25HdEXbzodFZwBbPYR37zFQ==} engines: {node: '>=22.0.0'} peerDependencies: ai: ^7.0.0 @@ -12447,32 +12447,32 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@objectstack/client@17.0.0-rc.5(ai@7.0.56(zod@4.4.3))': + '@objectstack/client@17.0.0-rc.6(ai@7.0.56(zod@4.4.3))': dependencies: - '@objectstack/core': 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) - '@objectstack/spec': 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + '@objectstack/core': 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) + '@objectstack/spec': 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/core@17.0.0-rc.5(ai@7.0.56(zod@4.4.3))': + '@objectstack/core@17.0.0-rc.6(ai@7.0.56(zod@4.4.3))': dependencies: - '@objectstack/spec': 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + '@objectstack/spec': 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) zod: 4.4.3 transitivePeerDependencies: - ai - '@objectstack/formula@17.0.0-rc.5(ai@7.0.56(zod@4.4.3))': + '@objectstack/formula@17.0.0-rc.6(ai@7.0.56(zod@4.4.3))': dependencies: '@marcbachmann/cel-js': 8.0.0 - '@objectstack/spec': 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + '@objectstack/spec': 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/lint@17.0.0-rc.5(ai@7.0.56(zod@4.4.3))': + '@objectstack/lint@17.0.0-rc.6(ai@7.0.56(zod@4.4.3))': dependencies: - '@objectstack/formula': 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) - '@objectstack/sdui-parser': 17.0.0-rc.5 - '@objectstack/spec': 17.0.0-rc.5(ai@7.0.56(zod@4.4.3)) + '@objectstack/formula': 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) + '@objectstack/sdui-parser': 17.0.0-rc.6 + '@objectstack/spec': 17.0.0-rc.6(ai@7.0.56(zod@4.4.3)) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) sucrase: 3.35.1 @@ -12480,9 +12480,9 @@ snapshots: transitivePeerDependencies: - ai - '@objectstack/sdui-parser@17.0.0-rc.5': {} + '@objectstack/sdui-parser@17.0.0-rc.6': {} - '@objectstack/spec@17.0.0-rc.5(ai@7.0.56(zod@4.4.3))': + '@objectstack/spec@17.0.0-rc.6(ai@7.0.56(zod@4.4.3))': dependencies: zod: 4.4.3 optionalDependencies: From db9dad96173ead776a58a863433ddb98f0512c2c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 17:15:21 +0000 Subject: [PATCH 2/2] fix(deps): repair the rc.6 fallout the first pass missed, and pin the one conflict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second pass over the @objectstack/spec 17.0.0-rc.6 bump. The first commit fixed every COMPILE error; a full `vitest run` (14615 tests) then surfaced the runtime half. All mechanical, each pinned; the one non-mechanical case is left as a tripwire rather than decided. - types/zod/complex.zod.ts: `GlobalFilterSchema` composes by spreading the spec's `.shape`. rc.6 closed BOTH extension doors on a now-refined schema — `.extend()` throws at module load, `.safeExtend()` types the three overridden keys as `never` (TS2322, which only `apps/site`'s build caught). The spread preserves rc.5 behaviour exactly and decides nothing: the refinement-vs-stored-`{preset}` conflict is objectui#4165, pinned by a new tripwire that reds from either side. The pin isolates the refinement by widening `defaultValue` first — the bare spec schema rejects `{preset}` on the OLD field type and the refinement never runs, so the obvious shorter assertion would have pinned the wrong thing. - core/actionKeys.ts: `description` joins SPEC_ACTION_KEYS (rc.6 added it to `ActionSchema`). Inventory only — `ActionDef` is untouched. - metadata-admin flow tests x2: `FlowNodeSchema` became pipe-wrapped, so `.shape` read `undefined`. Access-path repair (walk the wrapper); the blocks themselves are unchanged upstream, and a non-vacuity guard now says so. - clientValidation.optOuts: INVERTED — rc.6 makes `SharingRuleSchema` declare the seven ADR-0010 envelope keys, so the create door now ACCEPTS a stamped `_packageId`. Upstream convergence, pinned as accept, with a bogus-key case proving the door is still a door. - clientValidation.viewDiagnostics: fixture supplies a `label`; rc.6's widened `I18nLabel` made every label a LEAF union, which #3606's root-union expansion does not reach. The resulting bare `Invalid input` is recorded as its own pin (objectui#4163), not papered over. - components/record-picker parity: fixture drops `displayField` — an ADR-0087 D2 tombstone refused by name, which was failing all three `filter` assertions. - console registry parity: `SPEC_SHAPE_EMPTY_ON_THE_PIN` deleted exactly as its own self-clearing test instructed (rc.6 lands `PageContainerProps`), with a tombstone assertion so the deletion cannot be silently undone. - i18n residue ratchet: matches `const { t, … } = useObjectTranslation(` by pattern. Both sidebars legitimately also take `language` now, to resolve the spec's inline per-locale labels. Still red, filed, NOT fixed here: 2 console-parity exemption-list entries and `check:spec-symbols`' 13 name collisions (objectui#4167) — each is per-symbol triage, not a mechanical edit — and objectui#4165, which blocks ready. Part of objectstack-ai/objectstack#7100 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3 --- .../registry-inputs-spec-parity.test.ts | 54 +++++++------ .../clientValidation.optOuts.test.ts | 34 ++++++++- .../clientValidation.viewDiagnostics.test.ts | 37 ++++++++- ...ow-node-config.spec-reconciliation.test.ts | 40 +++++++++- .../flow-canvas-seeds.spec-parse.test.tsx | 33 +++++++- .../record-picker-inputs-spec-parity.test.ts | 19 ++++- packages/core/src/actions/actionKeys.ts | 9 +++ .../residue-namespaces-3546.test.tsx | 13 +++- .../report-chart-query-spec-parity.test.ts | 75 +++++++++++++++++++ packages/types/src/zod/complex.zod.ts | 49 +++++++++--- 10 files changed, 309 insertions(+), 54 deletions(-) diff --git a/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts b/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts index 57b5870914..d1b8c3d571 100644 --- a/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts +++ b/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts @@ -265,24 +265,24 @@ const EXPECTED_WITHOUT_INPUTS = [ 'nav:menu', ]; -/** - * Covered blocks whose spec props schema legitimately resolves to ZERO keys on - * the pinned `@objectstack/spec`, so the non-empty probe guard below must not - * judge them (objectui#4027). +/* + * `SPEC_SHAPE_EMPTY_ON_THE_PIN` — DELETED on the @objectstack/spec 17.0.0-rc.6 + * bump (objectstack#7100), exactly as it was designed to be. * - * The guard exists to catch a BROKEN reader — a `specTopLevelKeys` that stopped - * resolving `.shape` and returned `[]` for everything. These three return `[]` - * for a real reason instead: rc.5 still maps them to `EmptyProps`, while - * objectstack#5775 / PR objectstack#6281 replaced that with the shared - * `PageContainerProps` upstream. Without this carve-out the guard would report - * "spec shape did not resolve" for a shape that resolved perfectly well and is - * simply empty — a false accusation against the probe. + * It carved `page:footer` / `page:section` / `page:sidebar` out of the + * non-empty probe guard because rc.5 still mapped all three to `EmptyProps`, + * so their shapes resolved to `{}` for a real reason rather than a broken + * reader (objectui#4027). objectstack#5775 / PR objectstack#6281 replaced that + * with the shared `PageContainerProps` upstream, and rc.6 is where this repo + * resolves it: `children` now appears in each of the three shapes. * - * Self-clearing, and pinned as such by `the empty-shape carve-out still - * describes an empty shape` below: once the pin moves, `children` appears in - * each shape and that assertion fails until this list is deleted. + * The list was self-clearing by construction, and its companion test — `the + * empty-shape carve-out still describes an empty shape` — is what fired, + * carrying its own instruction ("delete it from SPEC_SHAPE_EMPTY_ON_THE_PIN"). + * Both the list and that test are gone; the plain non-empty guard now covers + * all three again, which is the state the carve-out was always temporary + * against. */ -const SPEC_SHAPE_EMPTY_ON_THE_PIN = ['page:footer', 'page:section', 'page:sidebar']; /** * Off-spec top-level inputs ACCEPTED for now, each with the reason. @@ -561,24 +561,20 @@ describe('registry `inputs` vs `@objectstack/spec` ComponentPropsMap (repo-wide) // would return `[]`, every input would read as off-spec, and the failure // would look like a repo-wide regression instead of a broken probe. An // empty result here means "fix the reader", not "fix the inputs". - for (const type of covered.filter((t) => !SPEC_SHAPE_EMPTY_ON_THE_PIN.includes(t))) { + for (const type of covered) { expect(specTopLevelKeys(type).length, `${type} spec shape did not resolve`).toBeGreaterThan(0); } }); - it('the empty-shape carve-out still describes an empty shape', () => { - // The carve-out above cannot be allowed to outlive its cause: these three - // resolve to `{}` because the PINNED rc.5 still maps them to `EmptyProps`, - // not because the probe is broken. The moment the pin carries - // `PageContainerProps`, `children` appears in the shape and this assertion - // fails — which is the instruction to delete the list and let the plain - // non-empty guard cover all three again. - for (const type of SPEC_SHAPE_EMPTY_ON_THE_PIN) { - expect(covered, `${type} no longer declares inputs — the carve-out is dead`).toContain(type); - expect( - specTopLevelKeys(type), - `${type}'s spec shape is no longer empty — delete it from SPEC_SHAPE_EMPTY_ON_THE_PIN`, - ).toEqual([]); + it('the three former carve-out blocks now resolve a real shape', () => { + // The tombstone of `SPEC_SHAPE_EMPTY_ON_THE_PIN` (see above). Keeping one + // assertion on the three names is what stops the deletion being silently + // undone by a future pin that regresses them to `EmptyProps` — the guard + // above would then read as "the probe is broken" for all three at once, + // which is the misdiagnosis objectui#4027 was filed about. + for (const type of ['page:footer', 'page:section', 'page:sidebar']) { + expect(covered, `${type} no longer declares inputs`).toContain(type); + expect(specTopLevelKeys(type), `${type} regressed to an empty spec shape`).toContain('children'); } }); diff --git a/packages/app-shell/src/views/metadata-admin/clientValidation.optOuts.test.ts b/packages/app-shell/src/views/metadata-admin/clientValidation.optOuts.test.ts index 8ba4b20b60..68a29714b3 100644 --- a/packages/app-shell/src/views/metadata-admin/clientValidation.optOuts.test.ts +++ b/packages/app-shell/src/views/metadata-admin/clientValidation.optOuts.test.ts @@ -109,13 +109,41 @@ describe('validateMetadataDraft("sharing_rule") — objectui#3561', () => { */ it('does not gate the edit door — a stored body may carry `_packageId`', async () => { const stored = { ...SHARING_RULE, _packageId: 'crm_pkg' }; - // Authoring door: strict, so the stamped key is an unrecognized key. + + // AUTHORING DOOR — INVERTED on the @objectstack/spec 17.0.0-rc.6 bump, and + // the inversion is upstream convergence rather than a regression here. + // + // This assertion used to read `expect(create.ok).toBe(false)`, on the stated + // premise that `SharingRuleSchema` "declares NONE of the seven ADR-0010 + // envelope keys" — so a stamped `_packageId` reached the strict object as an + // unrecognized key. rc.6 made the schema declare all seven (`_lock`, + // `_lockDocsUrl`, `_lockReason`, `_lockSource`, `_packageId`, + // `_packageVersion`, `_provenance`), the way `ActionSchema` has since rc.2. + // A stamped body is therefore legal input now, and the create door accepts + // it — which is the behaviour the envelope was always supposed to have. + // + // Pinned as accept, with the strictness it does NOT cost asserted right + // below: the boundary this file guards is "does the client stay no stricter + // than the server", and that still holds. const create = await validateMetadataDraft('sharing_rule', stored, undefined, { mode: 'create', }); - expect(create.ok).toBe(false); + expect(create.ok).toBe(true); + + // …and the door is still a door. A genuinely unknown key is still refused, + // so the assertion above pins "the envelope became legal", not "the schema + // stopped being strict" — without this, reverting the schema to a permissive + // object would leave the pin above green. + const bogus = await validateMetadataDraft( + 'sharing_rule', + { ...SHARING_RULE, notAKeyAnySchemaDeclares: 1 }, + undefined, + { mode: 'create' }, + ); + expect(bogus.ok).toBe(false); + // Edit door: no client gate, so the server stays authoritative and the - // client cannot report a stored body as broken. + // client cannot report a stored body as broken. Unchanged by rc.6. const edit = await validateMetadataDraft('sharing_rule', stored, undefined, { mode: 'edit' }); expect(edit.ok).toBe(true); expect(edit.issues).toEqual([]); diff --git a/packages/app-shell/src/views/metadata-admin/clientValidation.viewDiagnostics.test.ts b/packages/app-shell/src/views/metadata-admin/clientValidation.viewDiagnostics.test.ts index 2649eef86e..24b43ec250 100644 --- a/packages/app-shell/src/views/metadata-admin/clientValidation.viewDiagnostics.test.ts +++ b/packages/app-shell/src/views/metadata-admin/clientValidation.viewDiagnostics.test.ts @@ -198,11 +198,44 @@ describe('view edit path — union diagnostics are expanded to the selected memb }); it('leaves non-union failures exactly as they were', async () => { - // `page` has a plain object schema — no union, so the mapping is untouched. - const res = await validateMetadataDraft('page', { name: 'p', type: 42 }); + // `page` has a plain object schema — no ROOT union, so the mapping is + // untouched. The draft carries a valid `label`: see the pin below for why + // that is now load-bearing rather than incidental. + const res = await validateMetadataDraft('page', { name: 'page_x', label: 'Page X', type: 42 }); expect(res.ok).toBe(false); expect(res.issues.length).toBeGreaterThan(0); expect(res.issues.every((i) => i.message !== 'Invalid input')).toBe(true); + // The specific message is what "exactly as they were" means here. + expect(res.issues.some((i) => /Invalid option/.test(i.message))).toBe(true); + }); + + /** + * A LEAF union still reports `Invalid input`, and rc.6 created leaf unions + * where there were none — recorded here rather than fixed (objectui#4163). + * + * objectui#3606's expansion is about a ROOT union: it picks the member the + * draft was aiming at so the author reads that member's diagnostics instead of + * zod's aggregate `Invalid input`. It does not, and was never asked to, reach + * a union nested at a FIELD. + * + * That distinction cost nothing until now because the fields in question were + * scalars. @objectstack/spec 17.0.0-rc.6 widened `I18nLabel` from `string` to + * `string | Record< string, string >`, so every `label` in the vocabulary + * became a leaf union — and an author who simply OMITS a required label now + * reads `Invalid input` where they used to read a typed message naming the + * field. It is a small regression, it is upstream in origin, and repairing it + * means teaching the mapper about leaf unions, which is a diagnostics design + * change rather than a bump repair. + * + * Pinned so the gap is measured rather than remembered: when the mapper learns + * leaf unions, this test goes red and should be deleted along with the note. + */ + it('a leaf union (rc.6 `I18nLabel`) still yields the bare `Invalid input` — objectui#4163', async () => { + const res = await validateMetadataDraft('page', { name: 'page_x', type: 'record' }); + expect(res.ok).toBe(false); + const labelIssue = res.issues.find((i) => /(^|\.)label$/.test(String(i.path ?? ''))); + expect(labelIssue, 'expected a diagnostic on `label`').toBeTruthy(); + expect(labelIssue!.message).toBe('Invalid input'); }); }); diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.spec-reconciliation.test.ts b/packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.spec-reconciliation.test.ts index 1a9dea7ded..2c34aa927b 100644 --- a/packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.spec-reconciliation.test.ts +++ b/packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.spec-reconciliation.test.ts @@ -85,6 +85,33 @@ function unwrapped(schema: unknown): unknown { return cur; } +/** + * The object shape behind a schema that may be wrapped in a pipe/effect. + * + * `unwrap()` above only walks `.optional()`/`.default()`; it cannot get past a + * `ZodPipe` or a refinement wrapper, which expose neither `.shape` nor + * `.unwrap`. `FlowNodeSchema` became one of those in @objectstack/spec + * 17.0.0-rc.6, so the direct `FlowNodeSchema.shape` this file used to read went + * `undefined` and every assertion below died on `Cannot read properties of + * undefined`. The BLOCKS themselves are untouched — `connectorConfig`, + * `waitEventConfig` and `boundaryConfig` are all still declared, verified by + * walking the wrapper — so this is an access-path repair, not a changed + * expectation. + */ +function objectShape(schema: unknown, depth = 0): Record | null { + const s = schema as Record | undefined; + if (!s || depth > 8) return null; + if (s.shape) return s.shape as Record; + const def = (s._def ?? s.def) as Record | undefined; + if (!def) return null; + if (def.shape) return def.shape as Record; + for (const key of ['in', 'out', 'innerType', 'schema', 'left', 'right']) { + const found = def[key] ? objectShape(def[key], depth + 1) : null; + if (found) return found; + } + return null; +} + /** A field render-gated behind the never-matching `__legacy__` controller. */ function isLegacyGated(f: FlowConfigField): boolean { return f.showWhen?.field === '__legacy__'; @@ -217,7 +244,16 @@ describe('sibling-block forms ↔ FlowNodeSchema blocks (framework#4278 ratchet) // no feature detection: the hand-written groups for wait / connector_action // / boundary_event must edit exactly the keys the spec block declares — // #4161 / #4210 verified them by hand once; this keeps them verified. - const FlowNodeSchema = spec.FlowNodeSchema as { shape: Record }; + const flowNodeShape = objectShape(spec.FlowNodeSchema); + + it('the spec still exposes the FlowNode block shape this suite reads', () => { + // Guards the assertions below from passing vacuously if the walk stops + // resolving — the failure mode rc.6 produced, seen one level up. + expect(flowNodeShape, 'could not resolve FlowNodeSchema’s object shape').toBeTruthy(); + expect(Object.keys(flowNodeShape ?? {})).toEqual( + expect.arrayContaining(['waitEventConfig', 'connectorConfig', 'boundaryConfig']), + ); + }); const BLOCKS: ReadonlyArray<{ type: string; block: string }> = [ { type: 'wait', block: 'waitEventConfig' }, @@ -231,7 +267,7 @@ describe('sibling-block forms ↔ FlowNodeSchema blocks (framework#4278 ratchet) .filter((f) => f.path[0] === block) .map((f) => f.path[1]!), )].sort(); - const blockKeys = zodKeys(unwrapped(FlowNodeSchema.shape[block])); + const blockKeys = zodKeys(unwrapped(flowNodeShape?.[block])); expect( blockKeys.filter((k) => !formKeys.includes(k)), diff --git a/packages/app-shell/src/views/metadata-admin/previews/flow-canvas-seeds.spec-parse.test.tsx b/packages/app-shell/src/views/metadata-admin/previews/flow-canvas-seeds.spec-parse.test.tsx index 4dca3a8744..7fed76bfa7 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/flow-canvas-seeds.spec-parse.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/previews/flow-canvas-seeds.spec-parse.test.tsx @@ -74,6 +74,35 @@ function unwrapped(schema: ZodLike | undefined): ZodLike | undefined { return cur; } +/** + * The object shape behind a schema that may be wrapped in a pipe/effect. + * + * `unwrapped()` walks `.optional()`/`.default()` only, and cannot get past a + * `ZodPipe` or refinement wrapper — which exposes neither `.shape` nor + * `.unwrap`. `FlowNodeSchema` became one in @objectstack/spec 17.0.0-rc.6, so + * the `FlowNodeSchema.shape?.[block]` reads below silently became `undefined` + * and `liveKeys` failed on its own "expected a Zod object schema" guard. The + * blocks are unchanged upstream, so this restores the access path rather than + * the expectation. Same repair, same reason, as + * `inspectors/flow-node-config.spec-reconciliation.test.ts`. + */ +function objectShape(schema: unknown, depth = 0): Record | null { + const s = schema as Record | undefined; + if (!s || depth > 8) return null; + if (s.shape) return s.shape as Record; + const def = (s._def ?? s.def) as Record | undefined; + if (!def) return null; + if (def.shape) return def.shape as Record; + for (const key of ['in', 'out', 'innerType', 'schema', 'left', 'right']) { + const found = def[key] ? objectShape(def[key], depth + 1) : null; + if (found) return found; + } + return null; +} + +/** `FlowNodeSchema`'s block shape, resolved through any wrapper (see above). */ +const flowNodeShape = objectShape(FlowNodeSchema); + /** * Keys a Zod object schema declares as LIVE authoring surface. `retiredKey()` * tombstones stay in the shape so their rejection can carry the upgrade @@ -144,8 +173,8 @@ describe('designer node seeds ↔ spec FlowNodeSchema (#3316)', () => { for (const block of BLOCKS) { const seeded = extras[block]; if (!seeded || typeof seeded !== 'object') continue; - const retired = retiredKeys(FlowNodeSchema.shape?.[block] as ZodLike | undefined); - const live = liveKeys(FlowNodeSchema.shape?.[block] as ZodLike | undefined); + const retired = retiredKeys(flowNodeShape?.[block]); + const live = liveKeys(flowNodeShape?.[block]); for (const key of Object.keys(seeded as Record)) { if (retired.includes(key)) offenders.push(`${type}.${block}.${key} (retired in spec)`); else if (!live.includes(key)) offenders.push(`${type}.${block}.${key} (not declared by spec)`); diff --git a/packages/components/src/__tests__/record-picker-inputs-spec-parity.test.ts b/packages/components/src/__tests__/record-picker-inputs-spec-parity.test.ts index decbc08d2e..115affd955 100644 --- a/packages/components/src/__tests__/record-picker-inputs-spec-parity.test.ts +++ b/packages/components/src/__tests__/record-picker-inputs-spec-parity.test.ts @@ -54,8 +54,20 @@ const inputNames = () => inputs().map((i) => i.name); const input = (name: string) => inputs().find((i) => i.name === name); const filterDescription = () => input('filter')?.description ?? ''; -/** A minimal spec-valid props object, so a `filter` probe fails only on `filter`. */ -const withFilter = (filter: unknown) => ({ object: 'account', displayField: 'name', filter }); +/** + * A minimal spec-valid props object, so a `filter` probe fails only on `filter`. + * + * `displayField: 'name'` used to be the second key here, and it is deliberately + * NOT re-spelled to a survivor: @objectstack/spec 17.0.0 retired + * `element:record_picker`'s `displayField` as an ADR-0087 D2 tombstone (#5775), + * and rc.6 is where this repo first resolves that. A tombstone is refused BY + * NAME, so the old fixture stopped being "minimal and valid" and started being + * "valid except for one retired key" — which made all three `filter` assertions + * below fail on `displayField` instead. `object` alone is a complete valid + * shape, and keeping the fixture at the true minimum is what stops it drifting + * into the same trap again. + */ +const withFilter = (filter: unknown) => ({ object: 'account', filter }); describe('element:record_picker — registry inputs vs @objectstack/spec', () => { it('is registered with a non-empty `inputs` surface', () => { @@ -84,7 +96,6 @@ describe('element:record_picker — registry inputs vs @objectstack/spec', () => // consumer before it was declared here. const undeclared = ElementRecordPickerPropsSchema.safeParse({ object: 'account', - displayField: 'name', notASpecKey: 1, } as never); expect(undeclared.success).toBe(true); @@ -158,7 +169,7 @@ describe('element:record_picker — registry inputs vs @objectstack/spec', () => expect(input('filter')).toBeDefined(); expect(input('filter')?.defaultValue).toBeUndefined(); expect( - ElementRecordPickerPropsSchema.safeParse({ object: 'account', displayField: 'name' }).data, + ElementRecordPickerPropsSchema.safeParse({ object: 'account' }).data, ).not.toHaveProperty('filter'); }); diff --git a/packages/core/src/actions/actionKeys.ts b/packages/core/src/actions/actionKeys.ts index e747e95fcc..2358e7af4d 100644 --- a/packages/core/src/actions/actionKeys.ts +++ b/packages/core/src/actions/actionKeys.ts @@ -172,6 +172,15 @@ export const SPEC_ACTION_KEYS = [ 'bulkEnabled', 'component', 'confirmText', + // Added to `ActionSchema` in @objectstack/spec 17.0.0-rc.6. Listed here purely + // because this array's contract is "every property the spec's `ActionSchema` + // declares" — restating the spec, not adopting a feature. `ActionDef` does NOT + // declare `description` and is not changed: whether the runner or any renderer + // should READ it is a separate question, and `actionKeys.pin.test.ts`'s + // `ActionDef` half stays green without it. What this entry buys is that an + // action carrying a `description` is no longer reported as having an unknown + // key, which is the only thing the inventory is consulted for. + 'description', 'disabled', 'errorMessage', 'execute', diff --git a/packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx b/packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx index d62f9d905c..6070cfd60f 100644 --- a/packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx +++ b/packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx @@ -770,7 +770,18 @@ describe('objectui#3546 slice seven — the ratchet residue', () => { CREATE_WORKSPACE, ]) { expect(sourceOf(rel), `${rel} no longer binds the pack hook`).toContain('useObjectTranslation'); - expect(sourceOf(rel), `${rel} stopped destructuring t`).toContain('const { t } = useObjectTranslation();'); + // `t` must come out of the hook's destructuring — but it need not be the + // ONLY thing destructured. The literal `const { t } = …` this used to + // match broke the moment a file legitimately also took `language`, which + // both sidebars now do to resolve the spec's inline per-locale + // `I18nLabel` (widened in @objectstack/spec 17.0.0-rc.6). What this + // suite actually depends on is that `t` is i18next's, bound from this + // hook, so the pattern asserts exactly that and stays blind to which + // siblings ride along. + expect( + /const \{[^}]*\bt\b[^}]*\} = useObjectTranslation\(/.test(sourceOf(rel)), + `${rel} stopped destructuring t from useObjectTranslation()`, + ).toBe(true); } // RecordDetailView takes it from @object-ui/react's re-export, with language. expect(sourceOf(RECORD_DETAIL)).toContain('const { t, language } = useObjectTranslation();'); diff --git a/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts b/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts index fbca9a317c..f25a2f2584 100644 --- a/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts +++ b/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts @@ -29,6 +29,9 @@ */ import { describe, it, expect } from 'vitest'; +// Needed to isolate the rc.6 `GlobalFilterSchema` refinement from the field type +// it hides behind — see the objectui#4165 pin below. +import { z } from 'zod'; import { createRequire } from 'node:module'; import { readFileSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; @@ -149,6 +152,78 @@ describe('GlobalFilterSchema pinned divergences', () => { }).success).toBe(false); }); + /** + * OPEN CONFLICT, pinned rather than resolved — objectui#4165. + * + * The divergence above used to be about a FIELD TYPE, and only that: the spec + * typed `defaultValue` as `string | number | boolean`, objectui widened the key + * to `z.any()`, and widening the key was ENOUGH — nothing else in the spec had + * an opinion about the object form. + * + * @objectstack/spec 17.0.0-rc.6 added a whole-object refinement that refuses + * `{ preset }` BY NAME, with a message enumerating the three spellings a + * `type: 'date'` filter may use (a preset name, an ISO date, a date-macro + * token). A refinement is not a field type, so widening the key no longer + * escapes it: any derivation that CARRIES the refinement refuses the object + * form regardless of how `defaultValue` is typed. + * + * Read the two assertions below in that light, because the obvious shorter + * version is a trap. `SpecGlobalFilterSchema.safeParse({ …, defaultValue: { + * preset } })` does fail — but it fails with `invalid_union` on the FIELD, the + * same way it failed at rc.5, and the refinement never runs. Asserting that + * would pin the OLD behaviour while reading like a pin on the new one. The + * refinement is only observable once the field admits the object, so it is + * isolated here by widening the key first — which is exactly the shape + * objectui's own dialect has. + * + * The conflict this leaves is a producer/consumer one with three possible + * answers (follow the spec and migrate stored dashboards; keep the divergence; + * the spec's refinement is an upstream defect), and it is filed rather than + * guessed — the deciding half belongs to the spec owner. Until it is ruled, + * `GlobalFilterSchema` is composed by spreading the spec's `.shape`, carrying + * the FIELDS by reference and leaving the refinement behind: the exact + * behaviour this schema had before rc.6, so the bump changes nothing here. The + * spread is not a preference — rc.6 closed both other doors (`.extend()` + * throws at module load, `.safeExtend()` types the three overrides as `never`), + * so some spelling had to change, and this is the one that decides nothing. + * + * This test is the tripwire on that standstill and goes red from EITHER side: + * if objectui stops accepting the object form (resolved consumer-side), or if + * the refinement is withdrawn or reworded upstream (resolved producer-side). + */ + it('does NOT carry the spec rc.6 refinement that refuses `{ preset }` (objectui#4165)', () => { + const stored = { field: 'created_at', type: 'date', defaultValue: { preset: 'last_7_days' } }; + + // objectui side: still accepts what its own normalizer writes. + expect(GlobalFilterSchema.safeParse(stored).success).toBe(true); + + // Spec side, refinement ISOLATED from the field type: widen `defaultValue` + // exactly as objectui's dialect does, but keep the spec's checks (that is + // what `.safeExtend` preserves). What refuses the value now can only be the + // rc.6 refinement, and asserting its MESSAGE is what separates "the spec's + // `defaultValue` is narrow" (true since forever) from "the spec has a rule + // about this exact shape" (new in rc.6, and the reason #4165 exists). + const specWithRefinement = SpecGlobalFilterSchema.safeExtend({ + defaultValue: z.any().optional(), + }); + const refused = specWithRefinement.safeParse(stored); + expect(refused.success).toBe(false); + expect( + refused.error?.issues?.some((i) => /is not a value a .*date.* filter can resolve/.test(i.message)), + 'the spec no longer refuses `{ preset }` with its rc.6 refinement message — ' + + 'either the refinement was withdrawn or its wording changed. Re-read objectui#4165 ' + + 'before touching this: the standstill it documents may be over.', + ).toBe(true); + + // …and the bare preset NAME, which the refinement's message points authors + // at, is already legal on both sides. That is why #4165's leading candidate + // is a rewrite rather than a capability loss. + const bare = { field: 'created_at', type: 'date', defaultValue: 'last_7_days' }; + expect(specWithRefinement.safeParse(bare).success).toBe(true); + expect(SpecGlobalFilterSchema.safeParse(bare).success).toBe(true); + expect(GlobalFilterSchema.safeParse(bare).success).toBe(true); + }); + it('keeps `optionsFrom.labelField` optional', () => { expect(GlobalFilterSchema.safeParse({ field: 'owner', optionsFrom: { object: 'users', valueField: 'id' }, diff --git a/packages/types/src/zod/complex.zod.ts b/packages/types/src/zod/complex.zod.ts index 6b9922511a..d3cdebd911 100644 --- a/packages/types/src/zod/complex.zod.ts +++ b/packages/types/src/zod/complex.zod.ts @@ -328,18 +328,45 @@ export const DashboardWidgetSchema = specFieldsExcept(SpecDashboardWidgetSchema. * * Drift guard: `__tests__/report-chart-query-spec-parity.test.ts`. * - * `.safeExtend`, not `.extend`, since @objectstack/spec 17.0.0-rc.6: the spec's - * `GlobalFilterSchema` now carries a refinement, and zod 4 refuses `.extend()` - * on a refined object outright — *"Cannot overwrite keys on object schemas - * containing refinements. Use `.safeExtend()` instead."* — which threw at - * MODULE LOAD and took six `@object-ui/types` suites down with it before this - * file ran a single test. `.safeExtend` is zod's own prescribed replacement and - * KEEPS the refinement (measured: one check before, one check after), so the - * spec's cross-field rule still runs on objectui's dialect. That is the - * behaviour we want — the three divergences below widen individual FIELDS, and - * were never meant to switch off a whole-object rule. + * ## Spread, not `.extend` — and NOT `.safeExtend` either (objectui#4165) + * + * @objectstack/spec 17.0.0-rc.6 put a refinement on `GlobalFilterSchema`, and + * that closed BOTH extension doors on a schema whose whole purpose here is to + * override three keys: + * + * - `.extend()` — what this used to be — now throws outright: *"Cannot + * overwrite keys on object schemas containing refinements. Use + * `.safeExtend()` instead."* At MODULE LOAD, taking six `@object-ui/types` + * suites down before any of them ran a test. + * - `.safeExtend()` — zod's own suggested replacement — runs, but is "safe" + * precisely in the sense that it will not let you REPLACE an existing key's + * type. It types every incompatible override as `never`, so `options`, + * `optionsFrom` and `defaultValue` — exactly the three divergences above — + * stop compiling (TS2322, `is not assignable to type 'never'`). + * + * So the spread below is not a style choice: it is the only spelling that keeps + * this schema the shape it has been since objectstack#4115. It composes the + * spec's fields BY REFERENCE (`.shape`, so a spec field change still lands + * here) and replaces the three, which is byte-for-byte what `.extend()` + * produced before rc.6. + * + * What it does NOT do is carry the spec's new refinement across, and that is + * deliberate and TEMPORARY. The refinement rejects `defaultValue: + * { preset: 'last_7_days' }` on a `type: 'date'` filter — the exact object form + * `@object-ui/core`'s `normalizeDateDefault` PRODUCES and stored dashboards + * carry (framework#4475), and the stated reason for the third divergence. That + * is a producer/consumer conflict, not a mechanical repair: adopting the rule + * means changing what objectui writes and migrating stored dashboards, and + * declining it permanently means objectui accepts metadata the platform + * refuses. Neither is a call to make in passing, so this preserves the status + * quo EXACTLY — nothing about this schema's behaviour changes on the rc.6 bump — + * and the ruling is tracked in objectui#4165. `__tests__/report-chart-query- + * spec-parity.test.ts` pins the gap explicitly so it cannot fade into folklore. + * + * Drift guard: `__tests__/report-chart-query-spec-parity.test.ts`. */ -export const GlobalFilterSchema = SpecGlobalFilterSchema.safeExtend({ +export const GlobalFilterSchema = z.object({ + ...SpecGlobalFilterSchema.shape, options: z.array(z.union([ z.string(), z.object({