diff --git a/.changeset/11717-action-key-requires-membership-reach.md b/.changeset/11717-action-key-requires-membership-reach.md new file mode 100644 index 0000000000..af934a01bd --- /dev/null +++ b/.changeset/11717-action-key-requires-membership-reach.md @@ -0,0 +1,5 @@ +--- +'@object-ui/core': patch +--- + +The action key inventory lists `requiresMembershipReach`, the key `@objectstack/spec` 17.7.0 adds to `ActionSchema` (objectui#11717), so an action carrying it is no longer reported as having an unknown key. The spec's parse lowers it into `visible`; the action runner does not read it. diff --git a/.changeset/11717-bump-prose.md b/.changeset/11717-bump-prose.md new file mode 100644 index 0000000000..54e9d48226 --- /dev/null +++ b/.changeset/11717-bump-prose.md @@ -0,0 +1,4 @@ +--- +--- + +objectui#11717: test re-pins, ledgers and source comments that the `@objectstack/*` 17.7.0 bump made stale. Each measured fact was re-read on 17.7.0 before its stamp moved, and nothing that ships changes behaviour here. diff --git a/.changeset/11717-capability-view-all-audit-log.md b/.changeset/11717-capability-view-all-audit-log.md new file mode 100644 index 0000000000..cc494b9dea --- /dev/null +++ b/.changeset/11717-capability-view-all-audit-log.md @@ -0,0 +1,6 @@ +--- +'@object-ui/fields': patch +'@object-ui/i18n': patch +--- + +The capability picker labels `view_all_audit_log`, the platform capability `@objectstack/spec` 17.7.0 adds to `PLATFORM_CAPABILITIES` (objectui#11717). Without the label it showed the registry's English text in every locale. All ten locale packs carry the label. diff --git a/.changeset/11717-dashboard-widget-chart-measure-arity.md b/.changeset/11717-dashboard-widget-chart-measure-arity.md new file mode 100644 index 0000000000..b1d93b68f2 --- /dev/null +++ b/.changeset/11717-dashboard-widget-chart-measure-arity.md @@ -0,0 +1,10 @@ +--- +'@object-ui/types': minor +--- + +`DashboardWidgetSchema` chains `@objectstack/spec`'s `checkDashboardWidgetChartMeasureArity` after the metric-family check (objectui#11334, objectui#11417), so `safeValidateSchema` and `objectui validate` now refuse what the spec refuses since 17.7.0. The refusal is one `custom` issue at `values`, with the spec's own message: + +- two or more measures with no `dimensions` on a chart type that has no rendering for them without a dimension (`pie`, `donut`, `funnel`, `scatter`, `radar`, `treemap`, `sankey`); +- two or more measures on `pie`, `donut`, `funnel`, `treemap` or `sankey`, even with a dimension. These draw one series and dropped every measure after the first. + +This narrows the accepted documents (breaking for a document of either shape). The fix: use `table` or a bar-family type for several measures, or give each measure its own widget. A widget stored before the refusal still renders, and the dashboard's dropped-measure warning still names what it drops. The widget editor's measure picker asks the same door, so it stops offering a second measure on these widgets. diff --git a/.changeset/11717-element-text-retired-variants.md b/.changeset/11717-element-text-retired-variants.md new file mode 100644 index 0000000000..797c03fda2 --- /dev/null +++ b/.changeset/11717-element-text-retired-variants.md @@ -0,0 +1,5 @@ +--- +'@object-ui/components': minor +--- + +`element:text` drops the two pre-convergence `variant` spellings `heading` and `subheading`, which `@objectstack/spec` 17.7.0 retires (objectstack#21015, objectui#11717). The registry `inputs` enum is the contract's nine values again: `h1` to `h6`, `body`, `caption` and `overline`. The html tier therefore refuses `heading` / `subheading` with `invalid-enum`, as `objectui validate` and the spec do. A stored node that still carries one renders as `body`, the renderer's answer for any value outside the contract. Migration: `heading` -> `h2`, `subheading` -> `h3`. `os migrate meta --from 17` lists the edits. diff --git a/.changeset/11717-master-detail-sort-field-refused.md b/.changeset/11717-master-detail-sort-field-refused.md new file mode 100644 index 0000000000..d18ca4a47c --- /dev/null +++ b/.changeset/11717-master-detail-sort-field-refused.md @@ -0,0 +1,5 @@ +--- +'@object-ui/plugin-form': patch +--- + +`@objectstack/spec` 17.7.0 retires `sortField` on an `object-master-detail-form` `details` entry (objectstack-ai/objectstack#21589), so `objectui validate` now refuses an entry that writes it, at that member, with the spec's retired-key message (objectui#11717). The form never read it: the line-position field is derived from the child object (objectui#11070). The `details` registration description and the `MasterDetailDetailConfig` docblock now say so. The type is unchanged: it already left the key off. diff --git a/.changeset/11717-objectstack-17-7-0.md b/.changeset/11717-objectstack-17-7-0.md new file mode 100644 index 0000000000..c5060fd595 --- /dev/null +++ b/.changeset/11717-objectstack-17-7-0.md @@ -0,0 +1,14 @@ +--- +'@object-ui/types': patch +'@object-ui/plugin-dashboard': patch +'@object-ui/console': patch +--- + +objectui now resolves `@objectstack/*` 17.7.0 (objectui#11717). Two declared `@objectstack/spec` floors move, each because the package's published code now imports something an older release does not export: + +- `@object-ui/types`: `^17.6.0` to `^17.7.0`. Its zod mirrors chain `checkDashboardWidgetChartMeasureArity` and `checkPageRequiresKind`, which the spec first exports in 17.7.0. +- `@object-ui/plugin-dashboard`: `^17.5.0` to `^17.6.0`. `DatasetWidget` reads `DASHBOARD_WIDGET_MULTI_MEASURE_TYPES`, which the spec first exports in 17.6.0, instead of restating it. What the widget draws does not change. + +No other declared range moves. + +- `@object-ui/console`: the bundle inlines the 17.7.0 packages, so its client-side validation answers as a 17.7.0 server does, refusals included. The first screen is heavier: the eager closure grows by 183,008 gzipped bytes against a `main` build, nearly all of it in the `vendor-objectstack` chunk, and the bundle budget is re-pinned over the new reading on a maintainer ruling. It comes down again by whatever objectstack#22044 recovers. diff --git a/.changeset/11717-page-requires-kind.md b/.changeset/11717-page-requires-kind.md new file mode 100644 index 0000000000..3e6132d355 --- /dev/null +++ b/.changeset/11717-page-requires-kind.md @@ -0,0 +1,5 @@ +--- +'@object-ui/types': minor +--- + +The `page` node and the spec page-kind nodes (`record` / `home` / `utility`) chain `@objectstack/spec`'s `checkPageRequiresKind` (objectui#11717), as they already chain `checkPageSourceCompleteness`. A page that writes `requires` on a kind the platform does not compile at save is now refused at `requires` with the spec's own message, as the spec refuses it since 17.7.0. That covers `react`, `full` and `slotted` pages, and a page with no `kind`, an empty list included. `requires` stays legal on `html` and `jsx` pages, where the platform derives it from the compiled source. This narrows the accepted documents. The fix: delete the key. diff --git a/.changeset/11717-spec-row-describes.md b/.changeset/11717-spec-row-describes.md new file mode 100644 index 0000000000..67cad88ef9 --- /dev/null +++ b/.changeset/11717-spec-row-describes.md @@ -0,0 +1,6 @@ +--- +'@object-ui/plugin-gantt': patch +'@object-ui/plugin-timeline': patch +--- + +The `object-gantt` `markers` input and the `object-timeline` `items` and `mapping` inputs open their descriptions with the `@objectstack/spec` 17.7.0 row's own describe text, which is true of these renderers (objectui#11168's rule, followed at objectui#11717). The renderer-specific sentences after it are unchanged. diff --git a/apps/console/src/__tests__/preview-samples-registry-resolvable.test.ts b/apps/console/src/__tests__/preview-samples-registry-resolvable.test.ts index ce00ee4d78..e39137f803 100644 --- a/apps/console/src/__tests__/preview-samples-registry-resolvable.test.ts +++ b/apps/console/src/__tests__/preview-samples-registry-resolvable.test.ts @@ -21,7 +21,7 @@ * is therefore perfectly valid metadata. `{ type: 'heading' }` parsed clean * through every spec release while resolving to nothing: `heading` has never * been registered in this repo (the nearest real things are `element:text` - * with `variant: 'heading'`, which renders an h2, and the `ui/typography` + * with a heading `variant` such as `h2` (`heading` until 17.7.0 retired it), and the `ui/typography` * primitives, which never go through the registry at all). Both `heading` * nodes in the `page` sample fell through to the ComponentRegistry fallback, * so the gallery's page card — the worked example of a composed page — showed 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 9423e95109..8433facf42 100644 --- a/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts +++ b/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts @@ -972,7 +972,7 @@ const OBJECTUI_11111_LEDGER_CAPS = { */ const UNJUDGED_SPEC_BLOCKS: Record = { 'ai:chat_window': - 'NOT REGISTERED, DELIBERATELY. `packages/components/src/renderers/placeholders.tsx` omits it from `PROTOCOL_COMPONENTS` in so many words — the floating chat overlay (plugin-chatbot) is the canonical entry point, an inline page-level chat window is not a supported surface, and a page schema referencing it should raise a loud unknown-type error rather than draw a placeholder. `app-shell/src/views/metadata-admin/previews/block-types.ts` records the same refusal. objectui#8176.', + 'RETIRED UPSTREAM. `@objectstack/spec` 17.7.0 retired it by name (objectstack#21504, landed by objectstack#21531): out of `PageComponentType`, named in `RETIRED_PAGE_COMPONENT_TYPES`, and its row `retiredComponentProps` — a `never` that refuses every props bag, `{}` included, with a message that names the floating chat overlay as the AI chat entry point — so there is no authoring surface for either direction to judge. Until that release it stood here as NOT REGISTERED, DELIBERATELY: `PROTOCOL_COMPONENTS` in `@object-ui/components` `renderers/placeholders.tsx` omits it in so many words, which the retirement now says upstream too. objectui#11717, ledger objectui#8176.', 'app:launcher': 'EMPTY SPEC SHAPE. `ComponentPropsMap[app:launcher]` declares no props at all, so there is nothing for either direction to judge. It IS registered — `app-shell/src/views/app-launcher-renderer.tsx` registers it propless for exactly this reason, and says so — but by `@object-ui/app-shell`, which this file does not import; the empty shape is the load-bearing half either way. objectui#8176.', 'cloud-connection:panel': @@ -1285,7 +1285,7 @@ const UNPUBLISHED_EXEMPTIONS: Record = { // five extra (`showPagination`, `defaultSort`, `defaultFilters`, // `resizableColumns`, `title`) are the same class by the same test, so they are // carved out with it. `defaultSort` has since been harvested — spec 17.3.0 - // tombstoned it (see the note below), leaving nine. Trimming back to exactly five is a one-line reversal + // tombstoned it (see the note below), and `resizableColumns` followed at 17.7.0, leaving eight. Trimming back to exactly five is a one-line reversal // (delete the entry, declare the input); publishing first and withdrawing later // is not, which is why the exemption is the direction taken while the card is // open. @@ -1324,8 +1324,22 @@ const UNPUBLISHED_EXEMPTIONS: Record = { */ 'object-grid.defaultFilters': '@deprecated in ObjectGridSchema ("Use filter instead"); GA describes it as the "Legacy base-filter fallback, read only when `filter` is absent. Prefer `filter`". Read as back-compat, deliberately not published — the canonical `filter` IS declared. Same ruled carve-out class as the five the ruling enumerated, measured on this branch — objectui#4648 (maintainer 2026-08-16).', - 'object-grid.resizableColumns': - '@deprecated in ObjectGridSchema ("Moved to top-level resizable"); GA describes it as the "Alternate spelling of `resizable`". Read as back-compat, deliberately not published — the canonical `resizable` IS declared. Same ruled carve-out class as the five the ruling enumerated, measured on this branch — objectui#4648 (maintainer 2026-08-16).', + /* + * `object-grid.resizableColumns` WAS THE TENTH TOMBSTONE HARVESTED HERE — + * `@objectstack/spec` 17.7.0 (objectstack#21445, PR objectstack#21463), the way + * `defaultSort` above was at 17.3.0. + * + * 17.7.0 converted the key to a tombstone: `safeParse` of + * `{ resizableColumns: true }` fails `invalid_type` `expected: 'never'` at that + * path, with a message that opens "`object-grid` property `resizableColumns` + * was removed in @objectstack/spec 17.7.0 (ADR-0049) — it was the legacy + * second spelling of `resizable`", and `{ resizable: true }` parses. So the + * key left the AUTHORABLE set while staying listed, and the dangling and + * stale checks reported it exactly as designed; deleting the entry is the only + * way to green (objectui#11717). ⛔ Not resolved by declaring the input: + * `resizable`, the canonical spelling, is declared already. Retiring the + * renderer's own read of the old spelling is objectui#6152's. + */ 'object-grid.title': '@deprecated in ObjectGridSchema ("Use label instead"); GA describes it as the "Fallback for `label` (the renderer reads `label || title`)". Read as back-compat, deliberately not published — the canonical `label` IS declared. Same ruled carve-out class as the five the ruling enumerated, measured on this branch — objectui#4648 (maintainer 2026-08-16).', @@ -1671,7 +1685,6 @@ const GA_PENDING_UNPUBLISHED_KEYS = [ 'object-grid.showSearch', 'object-grid.showPagination', 'object-grid.defaultFilters', - 'object-grid.resizableColumns', 'object-grid.title', ]; @@ -3034,7 +3047,7 @@ const MEMBER_PINS: Record = { }, 'object-master-detail-form.details': { file: 'packages/plugin-form/src/__tests__/masterDetailDetailsMembers-8071.test.tsx', - pins: 'Members are detail-collection OBJECTS (`MasterDetailDetailConfig`), and unlike this block\'s parent keys the read site is THIS block: `MasterDetailForm` is the key\'s only reader. Each member renders as its own section in AUTHORED order, headed by `title` (fallback `Line Items`), with `columns` reaching the line grid in authored order. ⭐ The sharp row: four members reach the grid through ONE hand-written object under the grid\'s camelCase keys (objectui#11610): `minRows`, `maxRows` and `addLabel` by the same name, and `amountField` RENAMED to the grid\'s `totalField` (the CHILD column summed; the detail\'s own `totalField`, the PARENT field, is not forwarded), beside `columns` and the DERIVED `sortField` (objectui#11070 round 9 retired the detail\'s `sortField` member; row 2c pins that a written one is read by nothing). That object is pinned as a SORTED KEY SET of exactly six, with two off-list members (one in the grid\'s own spelling, `allowAdd`) asserted not forwarded and an all-empty control beside it. `inlineMode: \'form\'` makes a list whose Add opens the full form (labelled `Add` when unset), and in grid mode `formFields` wider than `columns` offers the row form; each arm is the other\'s control. `childObject` and `relationshipField` address the atomic batch (each line a create on the child, linked by `{ $ref: 0 }`) and the edit-mode read (`{ $filter: { FK: recordId }, $top: 500 }` per collection). `totalField` receives the sum of the lines\' `amountField` on the PARENT leg, and a collection without one puts nothing there. With `{ childObject }` alone the FK and the columns are DERIVED from the child object, the one member behaviour the spec\'s description promises, and the batch writes the derived FK. `details: []` is the non-vacuity control: no section, no grid, a one-leg batch. The line grid is the REAL `LineItemsField`, wrapped only to record the props it is handed. Since `@objectstack/spec` 17.6.0 (objectstack-ai/objectstack#21215) the spec row is an array of CLOSED entries — `childObject` required, `columns` the spec\'s inline grid column, an undeclared member key refused by name — and `@object-ui/types` takes that row by reference, so both published faces give the spec\'s verdict on an entry (`packages/types/src/__tests__/object-master-detail-form-details-entry-11396.test.ts`). `MasterDetailDetailConfig` is DERIVED from that entry (objectui#11396) and held `Equal` to it at compile time in the pinned file, with ONE stated fork: 17.6.0 declares `sortField`, which this block does not read (row 2c); objectstack `main` has since retired it (objectstack-ai/objectstack#21589, unreleased after 17.6.0), and the fork closes at the bump to the first release carrying that. The registration declares `of: \'object\'`, derived from the now single-kind member contract. A declaration fixes an entry\'s SHAPE, not what this block DOES with a member, so the read site is still the whole member contract. New file (objectui#8071 slice 18).', + pins: 'Members are detail-collection OBJECTS (`MasterDetailDetailConfig`), and unlike this block\'s parent keys the read site is THIS block: `MasterDetailForm` is the key\'s only reader. Each member renders as its own section in AUTHORED order, headed by `title` (fallback `Line Items`), with `columns` reaching the line grid in authored order. ⭐ The sharp row: four members reach the grid through ONE hand-written object under the grid\'s camelCase keys (objectui#11610): `minRows`, `maxRows` and `addLabel` by the same name, and `amountField` RENAMED to the grid\'s `totalField` (the CHILD column summed; the detail\'s own `totalField`, the PARENT field, is not forwarded), beside `columns` and the DERIVED `sortField` (objectui#11070 round 9 retired the detail\'s `sortField` member; row 2c pins that a written one is read by nothing). That object is pinned as a SORTED KEY SET of exactly six, with two off-list members (one in the grid\'s own spelling, `allowAdd`) asserted not forwarded and an all-empty control beside it. `inlineMode: \'form\'` makes a list whose Add opens the full form (labelled `Add` when unset), and in grid mode `formFields` wider than `columns` offers the row form; each arm is the other\'s control. `childObject` and `relationshipField` address the atomic batch (each line a create on the child, linked by `{ $ref: 0 }`) and the edit-mode read (`{ $filter: { FK: recordId }, $top: 500 }` per collection). `totalField` receives the sum of the lines\' `amountField` on the PARENT leg, and a collection without one puts nothing there. With `{ childObject }` alone the FK and the columns are DERIVED from the child object, the one member behaviour the spec\'s description promises, and the batch writes the derived FK. `details: []` is the non-vacuity control: no section, no grid, a one-leg batch. The line grid is the REAL `LineItemsField`, wrapped only to record the props it is handed. Since `@objectstack/spec` 17.6.0 (objectstack-ai/objectstack#21215) the spec row is an array of CLOSED entries — `childObject` required, `columns` the spec\'s inline grid column, an undeclared member key refused by name — and `@object-ui/types` takes that row by reference, so both published faces give the spec\'s verdict on an entry (`packages/types/src/__tests__/object-master-detail-form-details-entry-11396.test.ts`). `MasterDetailDetailConfig` is DERIVED from that entry (objectui#11396) and held `Equal` to it at compile time in the pinned file, minus ONE key, `sortField`, which this block does not read (row 2c): a fork through 17.6.0, closed by `@objectstack/spec` 17.7.0, whose entry refuses an authored `sortField` with its retired-key message (objectstack-ai/objectstack#21589, objectui#11717). The registration declares `of: \'object\'`, derived from the now single-kind member contract. A declaration fixes an entry\'s SHAPE, not what this block DOES with a member, so the read site is still the whole member contract. New file (objectui#8071 slice 18).', }, 'object-master-detail-form.fields': { file: 'packages/plugin-form/src/__tests__/topLevelFieldsWarnCoverage-8847.test.tsx', @@ -4929,7 +4942,6 @@ describe('registry `inputs` vs `@objectstack/spec` ComponentPropsMap (repo-wide) 'defaultFilters', 'fields', 'pageSize', - 'resizableColumns', 'selectable', 'showPagination', 'showSearch', @@ -5182,7 +5194,7 @@ describe('registry `inputs` vs `@objectstack/spec` ComponentPropsMap (repo-wide) } }); - it('the nine tombstoned keys are recognised, not exempted — and not published either', () => { + it('the ten tombstoned keys are recognised, not exempted — and not published either', () => { // The pin the harvest leaves behind (objectui#3809). Deleting eight // exemptions is only half the change: the derived assertions above would go // green just as readily if a future edit RE-EXEMPTED one of these keys, or @@ -5192,8 +5204,9 @@ describe('registry `inputs` vs `@objectstack/spec` ComponentPropsMap (repo-wide) // the same reason the `#3808 / #3830` and `rc.6 record_picker` pins next door // are written by name. // - // Six upstream retirements, nine keys, several facts each (eight through - // 17.4.0; `object-kanban.quickAdd` joined at 17.5.0). The list is + // Seven upstream retirements, ten keys, several facts each (eight through + // 17.4.0; `object-kanban.quickAdd` joined at 17.5.0, and + // `object-grid.resizableColumns` at 17.7.0). The list is // pin-dependent by construction and that is the point: it is the measurement // (`@objectstack/spec@17.0.0`, and the same eight on the rc.6 that preceded // it — this change was verified on both), so a pin that un-retires one of @@ -5212,6 +5225,9 @@ describe('registry `inputs` vs `@objectstack/spec` ComponentPropsMap (repo-wide) // (objectui#11073): objectui#8285's ruled retirement, which emptied // `LAZY_BLOCK_RULED_CARVE_OUTS` in the same change. ['object-kanban', 'quickAdd'], + // The tenth, harvested at the 17.7.0 bump (objectui#11717): + // objectstack#21445 retired the legacy second spelling of `resizable`. + ['object-grid', 'resizableColumns'], ]; for (const [type, key] of HARVESTED) { diff --git a/apps/console/src/preview-samples.ts b/apps/console/src/preview-samples.ts index 4042e4ac45..6b4d4b0abe 100644 --- a/apps/console/src/preview-samples.ts +++ b/apps/console/src/preview-samples.ts @@ -69,8 +69,8 @@ export const SAMPLES: Record> = { // • in-body headings are `element:text` with a heading variant — `h3` // renders an h3, which is what the old `level: 3` asked for. The variant // names the level, the same nine values `ui:text` takes (objectui#7450); - // `heading` / `subheading` are the pre-convergence spellings, not - // authored here. (A heading is a VARIANT of element:text, never a type; + // `heading` / `subheading` are the pre-convergence spellings, refused + // since `@objectstack/spec` 17.7.0 and not authored here. (A heading is a VARIANT of element:text, never a type; // `{ type: 'heading' }` is the near-miss the original made.) // • the rule is `element:divider`. The old bare `separator` did resolve — // `ui:separator` claims the bare name — but it is not a page block type, diff --git a/e2e/live/ci/backend.env b/e2e/live/ci/backend.env index 790433aa58..4e4a47263b 100644 --- a/e2e/live/ci/backend.env +++ b/e2e/live/ci/backend.env @@ -14,7 +14,7 @@ # # This file declares exactly ONE @objectstack pin, and that is the whole # design: the commit is derived from it, see below. -OBJECTSTACK_VERSION=17.6.0 +OBJECTSTACK_VERSION=17.7.0 # There is deliberately no OBJECTSTACK_REF key here. The objectstack-ai/objectstack # commit the showcase app metadata is sparse-checked-out from is DERIVED at diff --git a/packages/app-shell/src/views/metadata-admin/celAuthoring.ts b/packages/app-shell/src/views/metadata-admin/celAuthoring.ts index d62c47f26b..848797a677 100644 --- a/packages/app-shell/src/views/metadata-admin/celAuthoring.ts +++ b/packages/app-shell/src/views/metadata-admin/celAuthoring.ts @@ -428,7 +428,7 @@ function loadRowCanon(): Promise { * not re-derived — and the engine's own message ships with it. * * ⚠️ The reason is NOT that the engine's message refuses a `record.` - * rewrite. Measured against the installed 17.6.0, it mostly PRESCRIBES one: the + * rewrite. Measured against the installed 17.7.0, it mostly PRESCRIBES one: the * `current_user` text CONTAINS "To gate on record state, rewrite the predicate * against `record`." — one of the three remedies it offers, and NOT where it * ends (measured: `includes` true, `endsWith` false; every covered slot's text diff --git a/packages/app-shell/src/views/metadata-admin/clientValidation.flowNodeTypes.test.ts b/packages/app-shell/src/views/metadata-admin/clientValidation.flowNodeTypes.test.ts index 3d402c1872..46039d49c8 100644 --- a/packages/app-shell/src/views/metadata-admin/clientValidation.flowNodeTypes.test.ts +++ b/packages/app-shell/src/views/metadata-admin/clientValidation.flowNodeTypes.test.ts @@ -40,9 +40,14 @@ import { validateMetadataDraft } from './clientValidation'; * `connectorConfig` naming the connector and the action (objectui#11073). That * is a refusal about the NODE's config, addressed to `nodes.N.connectorConfig`, * ⛔ not a re-closed `type` enum — the next case pins exactly that — so the - * type-only fixture carries the block its type requires. + * type-only fixture carries the block its type requires. 17.7.0 does the same + * for `approval` (objectstack#21893): the build doors judge its `config` + * against `ApprovalNodeConfigSchema`, which requires `approvers`, and refuse it + * at `nodes.N.config.approvers` — so that fixture carries one approver + * (objectui#11717). */ const REQUIRED_SIBLING: Record> = { + approval: { config: { approvers: [{ type: 'user', value: 'usr_approver' }] } }, connector_action: { connectorConfig: { connectorId: 'slack', actionId: 'post_message' } }, }; const flowWithNodeType = (type: unknown, extra: Record = REQUIRED_SIBLING[type as string] ?? {}) => ({ @@ -77,6 +82,15 @@ describe('flow node `type` is an open string — the shim premise, re-measured', expect(paths).not.toContain('nodes.0.type'); }); + it('refuses an `approval` with no `approvers` at that config key — never at `type` (17.7.0)', async () => { + // The same shape as the row above, one release later (objectstack#21893). + const res = await validateMetadataDraft('flow', flowWithNodeType('approval', {})); + expect(res.ok).toBe(false); + const paths = res.issues.map((i) => i.path); + expect(paths).toEqual(['nodes.0.config.approvers']); + expect(paths).not.toContain('nodes.0.type'); + }); + it('still rejects an EMPTY node type, and that issue was never suppressed', async () => { const res = await validateMetadataDraft('flow', flowWithNodeType('')); expect(res.ok).toBe(false); diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.declaredDefault.test.tsx b/packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.declaredDefault.test.tsx index 16f7255268..2ba1551de2 100644 --- a/packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.declaredDefault.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.declaredDefault.test.tsx @@ -381,7 +381,7 @@ describe('boolean: a declared defaultValue seeds the control (objectui#8451, arm // ⇒ `screen.waitForInput` is the offline undeclared boolean that sentence // says does not exist — and it is undeclared CORRECTLY, so ⛔ do not read // this as an objectui#9277-class omission waiting to be declared. Measured - // on the installed `@objectstack/spec` (17.6.0): `waitForInput` is typed + // on the installed `@objectstack/spec` (17.7.0): `waitForInput` is typed // `z.boolean().optional()` with no `.default(...)`, so an omitted key // materialises nothing and there is no spec answer for a declaration to // mirror. Declaring one here would invent a default the runtime does not diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.escalationOff-11660.test.tsx b/packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.escalationOff-11660.test.tsx index 118ec69c38..487fe3ef5a 100644 --- a/packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.escalationOff-11660.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.escalationOff-11660.test.tsx @@ -183,6 +183,16 @@ const SOURCES: Array<{ name: 'offline' | 'engine'; labels: Labels; install: () = ]; const gateBox = (l: Labels) => screen.queryByLabelText(l.gate) as HTMLInputElement | null; +/** + * A sub-field's control by its label, read the way `getByText` reads a label — + * its own text, without the required marker (`SchemaForm`'s `*`). Since + * `@objectstack/spec` 17.7.0 judges an approval node's config against + * `ApprovalNodeConfigSchema` (objectstack#21893), the inspector marks the + * escalation block's required `timeoutHours`, and a whole-label match on the + * plain text stopped resolving (objectui#11717). + */ +const controlLabelled = (text: string) => + screen.getByLabelText(new RegExp(`^${text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\*?$`)); const subLabels = (l: Labels) => [l.timeout, l.action, l.escalateTo, l.notify]; const shownSubFields = (l: Labels) => subLabels(l).filter((label) => screen.queryByText(label) !== null); const retainedNotices = () => screen.queryAllByTestId('inactive-retained'); @@ -322,11 +332,11 @@ for (const source of SOURCES) { // ON, authored control by control through the inspector. gateBox(l)!.click(); expect(gateBox(l)!.checked, 'switching on draws the switch on').toBe(true); - fireEvent.change(screen.getByLabelText(l.timeout), { target: { value: '24' } }); + fireEvent.change(controlLabelled(l.timeout), { target: { value: '24' } }); await userEvent.click(screen.getByRole('combobox', { name: l.action })); await userEvent.click(await screen.findByRole('option', { name: l.reassign })); fireEvent.change(escalateToInput(l), { target: { value: 'sre_lead' } }); - (screen.getByLabelText(l.notify) as HTMLInputElement).click(); + (controlLabelled(l.notify) as HTMLInputElement).click(); const onConfig = configOf(latest())!; const authored = { enabled: true, timeoutHours: 24, action: 'reassign', escalateTo: 'sre_lead', notifySubmitter: false }; diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.requiredMarkers-10948.test.tsx b/packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.requiredMarkers-10948.test.tsx index dea4ab09e8..d1b341c8a3 100644 --- a/packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.requiredMarkers-10948.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.requiredMarkers-10948.test.tsx @@ -124,7 +124,10 @@ describe('each seeded kind: markers exactly where the spec requires the key (obj ['wait', ['Wait for', 'Duration'], []], ['boundary_event', ['Attached to', 'Event type'], ['Interrupting', 'Error code']], ['loop', [], ['Collection', 'Item variable']], - ['approval', [], ['Approvers']], + // `approval` marked nothing through 17.6.0. 17.7.0 judges its config against + // `ApprovalNodeConfigSchema` (objectstack#21893), which requires `approvers` + // and each approver's `type` (objectui#11717). + ['approval', ['Approvers', 'Type'], ['Value']], ['assignment', [], ['Assignments']], ['end', [], ['Outcome']], ['try_catch', [], ['Error variable']], 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 69facfc728..65b9c938b1 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 @@ -279,14 +279,19 @@ describe('page palette ↔ spec PageComponentType coverage', () => { } }); - it('ai:chat_window is not offered — it has no inline renderer', () => { + it('ai:chat_window is not offered — it has no inline renderer, and the spec retired it', () => { // The palette used to offer it WITH a config panel while // `components/renderers/placeholders.tsx` deliberately excluded it to force // a loud error: an author dragged a block Studio advertised and got a red // "Unknown component type" box. expect((BLOCK_TYPE_META as any)['ai:chat_window']).toBeUndefined(); expect(blockHasConfig('ai:chat_window')).toBe(false); - expect(PALETTE_EXCLUSIONS['ai:chat_window']).toBeTruthy(); + // Excluded by the spec now, not by this palette: `@objectstack/spec` 17.7.0 + // dropped it from `PageComponentType` (objectstack#21504), so it needs no + // exclusion entry, and "every exclusion names a real spec type" would refuse + // one (objectui#11717). + expect(specNames).not.toContain('ai:chat_window'); + expect(PALETTE_EXCLUSIONS['ai:chat_window']).toBeUndefined(); }); it('element:button offers an action editor — without it the button is inert', () => { diff --git a/packages/app-shell/src/views/metadata-admin/previews/__tests__/canvas-display-meta.test.tsx b/packages/app-shell/src/views/metadata-admin/previews/__tests__/canvas-display-meta.test.tsx index f70e50512c..d8c315802e 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/__tests__/canvas-display-meta.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/previews/__tests__/canvas-display-meta.test.tsx @@ -176,17 +176,24 @@ describe('canvas chrome for a renderable-but-unoffered spelling (#5837)', () => // icon it has not earned. // `element:form` was a fourth probe here until `@objectstack/spec` 17.3.0 // retired it from `PageComponentType` and objectui dropped its now-stale - // palette exclusion with it (objectui#7122). Three probes still exercise - // the same property on three different exclusion reasons, so the - // counter-probe keeps its force. - for (const type of ['element:text_input', 'element:record_picker', 'ai:chat_window']) { + // palette exclusion with it (objectui#7122). Two probes still exercise + // the same property on two different exclusion reasons, so the + // counter-probe keeps its force. `ai:chat_window` was the third until + // `@objectstack/spec` 17.7.0 retired it from `PageComponentType` the same way + // and its palette exclusion went with it (objectui#11717); it stays a probe + // below, as the retired type a stored page may still carry. + for (const type of ['element:text_input', 'element:record_picker']) { expect(PALETTE_EXCLUSIONS[type], `${type} is not an exclusion any more`).toBeTruthy(); expect(resolveBlockDisplayMeta(type), `${type} borrowed display meta`).toBeUndefined(); expect(resolveBlockTone(type), `${type} borrowed a tone`).toBe(BLOCK_CATEGORY_TONE.misc); } - // And an outright unknown type, which is what the fallback exists for. - expect(resolveBlockDisplayMeta('zz:not-a-block')).toBeUndefined(); - expect(resolveBlockTone('zz:not-a-block')).toBe(BLOCK_CATEGORY_TONE.misc); + // And an outright unknown type, which is what the fallback exists for — a + // retired spec type included. + for (const type of ['zz:not-a-block', 'ai:chat_window']) { + expect(PALETTE_EXCLUSIONS[type], `${type} is an exclusion`).toBeUndefined(); + expect(resolveBlockDisplayMeta(type), `${type} borrowed display meta`).toBeUndefined(); + expect(resolveBlockTone(type), `${type} borrowed a tone`).toBe(BLOCK_CATEGORY_TONE.misc); + } }); }); diff --git a/packages/app-shell/src/views/metadata-admin/previews/__tests__/element-text-variant-options-7450.test.ts b/packages/app-shell/src/views/metadata-admin/previews/__tests__/element-text-variant-options-7450.test.ts index f2e549a8e2..cdcc4db612 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/__tests__/element-text-variant-options-7450.test.ts +++ b/packages/app-shell/src/views/metadata-admin/previews/__tests__/element-text-variant-options-7450.test.ts @@ -9,9 +9,9 @@ * shows, flagged, in `InspectorSelectField`. So it offers exactly the nine * values `ui:text` publishes (`TextSchema.variant` in the `@object-ui/types` * Zod mirror), each one a value the installed `@objectstack/spec` accepts on - * `element:text`, and it no longer offers `heading` / `subheading`: the - * contract still accepts that pair, but the ruling retires it in a later spec - * release, so the designer stops writing it now. + * `element:text`, and it no longer offers `heading` / `subheading`: the ruling + * retired that pair, and `@objectstack/spec` 17.7.0 refuses it (objectui#11717), + * so the designer does not write it. * * Whether each label key exists in both locale tables, and is the key its * position implies, is `block-config-i18n.test.ts`'s question. This file asks diff --git a/packages/app-shell/src/views/metadata-admin/previews/__tests__/exclusion-reason-truthfulness.test.ts b/packages/app-shell/src/views/metadata-admin/previews/__tests__/exclusion-reason-truthfulness.test.ts index 67df6ff5e0..bb79ec4d0f 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/__tests__/exclusion-reason-truthfulness.test.ts +++ b/packages/app-shell/src/views/metadata-admin/previews/__tests__/exclusion-reason-truthfulness.test.ts @@ -208,14 +208,20 @@ describe('objectui#6071 — PALETTE_EXCLUSIONS reasons that claim "no renderer"' } }); - it('the ledger still contains a no-renderer claim to check (guards a vacuous loop)', () => { - // If this fails, every reason was reworded away from the claim. That may be - // fine — but then this file is no longer guarding anything, and that should - // be a decision rather than a silent green. + it('the ledger\'s no-renderer population is EMPTY, and why (guards a vacuous loop)', () => { + // This row asserted the population was non-empty, and said that an empty one + // "should be a decision rather than a silent green". It emptied by a + // retirement, not a rewording: its last member, `ai:chat_window`, left the + // ledger when `@objectstack/spec` 17.7.0 retired the type upstream + // (objectui#11717), because an exclusion must name a real spec type. So the + // emptiness is asserted here, with its cause, rather than left silent: the + // loop below runs over nothing until a new reason claims "no renderer", and + // that reason turns this row red, so the guard is re-armed by name. expect( claimingNoRenderer.map(([type]) => type), - 'no exclusion claims "no renderer" any more — this guard has nothing left to check', - ).not.toEqual([]); + 'an exclusion claims "no renderer" again — the loop below checks it now; restore the non-empty guard', + ).toEqual([]); + expect(PALETTE_EXCLUSIONS['ai:chat_window']).toBeUndefined(); }); it('no exclusion whose reason claims "no renderer" actually has one', () => { 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 ca4b630528..9ef081f287 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 @@ -258,11 +258,11 @@ export const BLOCK_CONFIG: Record = { kind: 'select', // The nine `ui:text` publishes and `element:text` renders, in the order // both registrations declare them (objectui#7450). `heading` and - // `subheading` are not offered: the installed contract still accepts both - // and the renderer still draws them, but the ruling retires them in a later - // spec release, so the designer stops writing them now. A block that - // already carries one keeps it; `InspectorSelectField` shows a stored - // value its options do not offer as a flagged row rather than blanking it. + // `subheading` are not offered: the designer stopped writing them ahead of + // the ruling's retirement, and `@objectstack/spec` 17.7.0 now refuses both + // (objectui#11717). A block that already carries one keeps it on screen; + // `InspectorSelectField` shows a stored value its options do not offer as + // a flagged row rather than blanking it. options: [ { value: 'h1', label: 'engine.inspector.pageBlock.option.variant.h1' }, { value: 'h2', label: 'engine.inspector.pageBlock.option.variant.h2' }, diff --git a/packages/app-shell/src/views/metadata-admin/previews/block-types.ts b/packages/app-shell/src/views/metadata-admin/previews/block-types.ts index 71cdbd310e..399f06a4a7 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/block-types.ts +++ b/packages/app-shell/src/views/metadata-admin/previews/block-types.ts @@ -122,9 +122,10 @@ export const BLOCK_TYPE_META: Record> = { // `ai:chat_window` is deliberately NOT in the palette: the floating chat // overlay (plugin-chatbot) is the canonical entry point and there is no // inline page-level renderer — `components/renderers/placeholders.tsx` - // documents that exclusion. The palette used to offer it (with a config - // panel), so an author dragged a block Studio advertised and got a red - // "Unknown component type" box (#2943). See PALETTE_EXCLUSIONS. + // documents that exclusion, and `@objectstack/spec` 17.7.0 retired the type + // (objectstack#21504). The palette used to offer it (with a config panel), so + // an author dragged a block Studio advertised and got a red "Unknown component + // type" box (#2943). 'ai:suggestion': { label: 'AI suggestion', labelKey: 'engine.pageBlockCanvas.type.aiSuggestion', category: 'ai', Icon: Sparkles }, // Elements @@ -179,10 +180,16 @@ export const PALETTE_EXCLUSIONS: Record = { // shell's own profile affordance is a React slot and never this block type, // and nothing anywhere registered `element:form`. // - // No renderer, by decision. No `ai:` namespace registration exists anywhere — - // `components/renderers/placeholders.tsx` keeps `ai:chat_window` out on - // purpose so a referencing schema fails loudly. - 'ai:chat_window': 'no inline renderer — the floating chat overlay (plugin-chatbot) is canonical', + // ⛔ `ai:chat_window` is NOT a missing entry either — it is RETIRED UPSTREAM. + // `@objectstack/spec` 17.7.0 dropped it from `PageComponentType` and refuses + // it by name (objectstack#21504): its row refuses every props bag with a + // message naming the floating chat overlay as the AI chat entry point. That + // is the decision this entry recorded (no inline renderer; the overlay in + // plugin-chatbot is canonical), now made upstream, so the entry went with the + // type, as `user:profile` / `element:form` did (objectui#11717). The rest of + // the decision stands where it lives: `components/renderers/placeholders.tsx` + // still keeps `ai:chat_window` out so a referencing schema fails loudly, and + // the palette still does not offer it (`block-config.test.ts`). // Renders fine — excluded because it is not PAGE CONTENT, not because it is // unrenderable. Both types have a registered renderer under `namespace: // 'element'` (`components/renderers/basic/text-input.tsx:161`, diff --git a/packages/app-shell/src/views/metadata-admin/previews/flow-canvas-seeds.saveErrors-10948.test.tsx b/packages/app-shell/src/views/metadata-admin/previews/flow-canvas-seeds.saveErrors-10948.test.tsx index 54cea89d58..0bbf2e9562 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/flow-canvas-seeds.saveErrors-10948.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/previews/flow-canvas-seeds.saveErrors-10948.test.tsx @@ -2,7 +2,7 @@ /** * objectui#10948 — what the designer's live `FlowSchema` pass says about a node - * the author has ADDED but not yet configured, on the installed spec (17.6.0). + * the author has ADDED but not yet configured, on the installed spec (17.7.0). * * Spec 17.5.0 carries objectstack#20316 (a node config key its executor * contract requires, left out, and a decision branch with no `label`, are diff --git a/packages/components/src/renderers/basic/__tests__/element-text-published-variants-7450.test.tsx b/packages/components/src/renderers/basic/__tests__/element-text-published-variants-7450.test.tsx index 5edb071008..9caaa72ba2 100644 --- a/packages/components/src/renderers/basic/__tests__/element-text-published-variants-7450.test.tsx +++ b/packages/components/src/renderers/basic/__tests__/element-text-published-variants-7450.test.tsx @@ -24,7 +24,8 @@ * - the nine render the way `ui:text` renders them: the same heading element * for `h1`-`h6`, the same class for all nine, and a paragraph (not * `ui:text`'s inline ``) for `body` / `caption` / `overline`; - * - the two spellings the contract still accepts render exactly as they did; + * - the two pre-convergence spellings, retired by the spec in 17.7.0, are + * refused at every objectui gate (objectui#11717); * - absence still means `body` here, beside a `ui:text` control that still * does not synthesise it (objectui#6942); * - every objectui authoring gate accepts exactly the installed contract's set @@ -35,9 +36,10 @@ * * The nine come from the `@object-ui/types` Zod mirror (`TextSchema`), the * accept set from `@objectstack/spec`'s `ElementTextPropsSchema`. The one - * restated fact is which two spellings are the pre-convergence pair: when the - * spec release that retires them lands (objectstack#17109's mechanism), the - * first test goes red and names the follow-up. + * restated fact is which two spellings are the pre-convergence pair, now + * named as the retired pair: the spec release that retires them + * (objectstack#17109's mechanism) landed in 17.7.0, the first test went red as + * it promised, and the follow-up it named is done (objectui#11717). */ import { describe, it, expect } from 'vitest'; import { render } from '@testing-library/react'; @@ -106,10 +108,10 @@ describe('objectui#7450 — the two vocabularies, read where they live', () => { expect(HEADINGS).toEqual(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']); expect(PARAGRAPHS).toEqual(['body', 'caption', 'overline']); expect(CONTRACT).toEqual(expect.arrayContaining(PUBLISHED)); - // Red here means the spec release that retires the pair has landed: drop - // them from the registry `inputs` enum and from `VARIANT_CLASS` / - // `VARIANT_TAG` in `elements.tsx`, and move this expectation to `[]`. - expect(PRE_CONVERGENCE).toEqual(['heading', 'subheading']); + // The spec release that retires the pair landed in 17.7.0 (objectui#11717): + // they left the registry `inputs` enum and `VARIANT_CLASS` / `VARIANT_TAG` + // in `elements.tsx`, and this expectation moved to `[]`, as it instructed. + expect(PRE_CONVERGENCE).toEqual([]); }); }); @@ -141,17 +143,25 @@ describe('objectui#7450 — the nine render the way ui:text renders them', () => }); }); -describe('objectui#7450 — the spellings the contract still accepts keep their heading', () => { - it('heading renders the h2 it always rendered, with its old class', () => { - const element = elementText('heading'); - expect(element.tag).toBe('h2'); - expect(withoutAlign(element.classes)).toEqual(['font-semibold', 'text-2xl', 'tracking-tight']); +const RETIRED_PAIR = ['heading', 'subheading'] as const; + +describe('objectui#11717 — the retired pair is refused at every objectui gate, as the contract refuses it', () => { + it.each(RETIRED_PAIR)('the installed contract refuses %s on element:text', (variant) => { + expect(ElementTextPropsSchema.safeParse({ content: CONTENT, variant }).success).toBe(false); + expect(declaredVariantEnum('element:text')).not.toContain(variant); + }); + + it.each(RETIRED_PAIR)('the html tier refuses %s with invalid-enum', (variant) => { + const { diagnostics } = compile(``, htmlTierManifest()); + expect(diagnostics.filter((d) => d.severity === 'error').map((d) => d.code)).toEqual(['invalid-enum']); + }); + + it.each(RETIRED_PAIR)('objectui validate (safeValidateSchema) refuses %s on element:text', (variant) => { + expect(safeValidateSchema({ type: 'element:text', properties: { content: CONTENT, variant } }).success).toBe(false); }); - it('subheading renders the h3 it always rendered, with its old class', () => { - const element = elementText('subheading'); - expect(element.tag).toBe('h3'); - expect(withoutAlign(element.classes)).toEqual(['font-medium', 'text-foreground', 'text-lg']); + it.each(RETIRED_PAIR)('a stored %s renders as body, the read site\'s answer for a value outside the contract', (variant) => { + expect(elementText(variant).html).toBe(elementText('body').html); }); }); diff --git a/packages/components/src/renderers/basic/elements.tsx b/packages/components/src/renderers/basic/elements.tsx index 03740bf203..2162032400 100644 --- a/packages/components/src/renderers/basic/elements.tsx +++ b/packages/components/src/renderers/basic/elements.tsx @@ -104,17 +104,19 @@ const ALIGN_CLASS = { * renderers for all nine and asserts they agree, so this map cannot drift from * `ui:text`'s without going red. * - * ## `heading` / `subheading` — still accepted, still rendered, not offered + * ## `heading` / `subheading` — retired with the spec, in 17.7.0 * - * The installed contract still ACCEPTS the two pre-convergence spellings. The - * ruling retires them in a LATER spec release, through the generic value-level - * retirement mechanism (objectstack#17109), and no objectui face refuses a - * value before the spec does. So they render exactly as they always have - * (`heading` an `

`, `subheading` an `

`, with their old classes), and a - * document the spec accepts keeps its heading. The Studio inspector stops - * offering them; the registry `inputs` below keep accepting them, for the - * reason written there. Migration: `heading` -> `h2`, `subheading` -> `h3`, or - * the level the document means. + * Through 17.6.0 the contract still ACCEPTED the two pre-convergence + * spellings, and no objectui face refuses a value before the spec does, so they + * rendered as they always had (`heading` an `

`, `subheading` an `

`). + * `@objectstack/spec` 17.7.0 retires them through the value-level retirement + * mechanism (objectstack#17109; objectstack#21015 refuses both by name, and + * `os migrate meta` rewrites them to `h2` / `h3`), so they left this map and + * the registry `inputs` below at that bump (objectui#11717), as + * `__tests__/element-text-published-variants-7450.test.tsx` instructed. A + * stored value outside the contract renders as `body`, as the read site says. + * Migration: `heading` -> `h2`, `subheading` -> `h3`, or the level the document + * means. * * ## Absence is `body` here, and only here * @@ -126,10 +128,7 @@ const ALIGN_CLASS = { * materialises the same value this renderer reads. Converging the vocabulary * moves what absence means on neither side. */ -type PublishedTextVariant = NonNullable; -/** The two spellings the installed contract still accepts, pending objectstack#17109's release. */ -type AcceptedLegacyTextVariant = 'heading' | 'subheading'; -type ElementTextVariant = PublishedTextVariant | AcceptedLegacyTextVariant; +type ElementTextVariant = NonNullable; const VARIANT_CLASS: Record = { h1: 'text-4xl font-semibold tracking-tight', @@ -141,8 +140,6 @@ const VARIANT_CLASS: Record = { body: 'text-sm text-foreground', caption: 'text-xs text-muted-foreground', overline: 'text-xs font-medium uppercase tracking-widest text-muted-foreground', - heading: 'text-2xl font-semibold tracking-tight', - subheading: 'text-lg font-medium text-foreground', }; const VARIANT_TAG: Record = { @@ -155,8 +152,6 @@ const VARIANT_TAG: Record; * published values below. The paragraph above records how this map was first * built, not which way the dependency runs now — `element:text` takes this * map's nine, `overline` included, with this map's class for each, and this - * map is the vocabulary that does not move. `element:text` still also accepts - * `heading` / `subheading`, because the installed `@objectstack/spec` does; - * their retirement is a later spec release, through the value-level mechanism - * of objectstack-ai/objectstack#17109, never this file. The defaulting + * map is the vocabulary that does not move. `element:text` also accepted + * `heading` / `subheading` while the installed `@objectstack/spec` did; the + * spec retired them in 17.7.0, through the value-level mechanism of + * objectstack-ai/objectstack#17109, and `element:text` dropped them with it + * (objectui#11717) — never this file. The defaulting * asymmetry stays: an absent `variant` is not `body` here (objectui#6942, see * ABSENCE IS NOT `body` below), while `element:text` renders it as `body`. * diff --git a/packages/core/src/actions/actionKeys.ts b/packages/core/src/actions/actionKeys.ts index f4712ea363..9b0fab5323 100644 --- a/packages/core/src/actions/actionKeys.ts +++ b/packages/core/src/actions/actionKeys.ts @@ -299,6 +299,16 @@ export const SPEC_ACTION_KEYS = [ 'refreshAfter', 'requiredPermissions', 'requiresFeature', + // Added to `ActionSchema` in @objectstack/spec 17.7.0 (objectui#11717): the + // organization endpoint (a `MEMBERSHIP_REACH` row) whose membership-grade gate + // the action follows, which the spec's parse LOWERS into `visible` over + // `current_user.positions` (the same transform that lowers `requiresFeature` + // above). Listed for the reason `description` is — this array restates the + // spec's declared keys, so an action carrying it is not reported as having an + // unknown key. `ActionDef` does NOT declare it and the runner does not read + // it; whether it should, beside `requiresFeature`, is not this inventory's + // question. + 'requiresMembershipReach', 'resultDialog', 'shortcut', 'successMessage', diff --git a/packages/core/src/evaluator/optionRules.ts b/packages/core/src/evaluator/optionRules.ts index 0d0aa7f4ea..68cd57ca65 100644 --- a/packages/core/src/evaluator/optionRules.ts +++ b/packages/core/src/evaluator/optionRules.ts @@ -118,7 +118,7 @@ export function resolveVisibleOptions( * This is deliberately NOT the lenient renderer fallback AGENTS.md #0.1 * forbids. That rule governs metadata the contract REFUSES ("if the metadata * is off-spec, fix it at the producer"); an empty label is metadata the - * contract ACCEPTS. Measured on the installed `@objectstack/spec` 17.6.0, + * contract ACCEPTS. Measured on the installed `@objectstack/spec` 17.7.0, * `SelectOptionSchema`: * * { value: 'low', label: 'Low' } -> ACCEPT (lit control) diff --git a/packages/data-objectstack/src/object-metadata-write-guard.derivation.test.ts b/packages/data-objectstack/src/object-metadata-write-guard.derivation.test.ts index efb4de951e..1149982605 100644 --- a/packages/data-objectstack/src/object-metadata-write-guard.derivation.test.ts +++ b/packages/data-objectstack/src/object-metadata-write-guard.derivation.test.ts @@ -103,13 +103,13 @@ describe('the four target states, measured against the contract rather than asse * objectui#11253 — the choice set, and the one claim the guard's docblock makes * about the server for it. * - * `FieldSchema` cannot answer "which types need options" yet: the door the - * maintainer's ruling A on objectstack#20827 closes has not shipped. The - * contract DOES already state the rule, at author time: ADR-0078's - * `field/choice-without-options` in `checkFieldCompleteness`, which the ruling - * names as the rule the door adopts. So the set is derived from THAT, keeping - * only the `error`-severity findings (the `warning` one is `checkboxes`, which - * the ruling does not name). + * The set is derived from ADR-0078's author-time rule, + * `field/choice-without-options` in `checkFieldCompleteness`, which the + * maintainer's ruling A on objectstack#20827 names as the rule its door adopts, + * keeping only the `error`-severity findings (the `warning` one is + * `checkboxes`, which the ruling does not name). The door itself shipped in + * `@objectstack/spec` 17.7.0, and the block below requires `FieldSchema`'s + * answer to equal this one (objectui#11717). */ function deriveChoiceTypesRequiringOptions(): string[] { return FieldType.options.filter((type) => @@ -146,22 +146,43 @@ describe('CHOICE_TYPES_REQUIRING_OPTIONS — derived from the installed spec (ob }); }); -describe('the installed server still ACCEPTS a choice with no options (objectui#11253)', () => { - // The guard's docblock says its choice refusal is deliberately AHEAD of the - // server, by ruling. That is a claim about the installed artifact, so it is - // measured here rather than left in prose (AGENTS.md #9). +describe('the installed server refuses a choice with no options one layer down (objectui#11253)', () => { + // The guard's docblock says its choice refusal forecloses nothing the server + // would take. That is a claim about the installed artifact, so it is measured + // here rather than left in prose (AGENTS.md #9). // - // ⭐ When this goes red, the door has shipped and the pin has reached it. Then - // rewrite the guard's docblock paragraph on the choice refusal into the - // relationship form ("refused one layer down"), and turn this block into the - // refusal pin the relationship rule has above: refused at `options`. + // Until `@objectstack/spec` 17.7.0 this block pinned the opposite — the server + // still ACCEPTED a choice with no options, and the guard was ahead of it by + // ruling — and it named what to do when it went red: the door shipped + // (objectstack#21390), so the guard's docblock paragraph collapsed into the + // relationship form, and this block became the refusal pin the relationship + // rule has above: refused at `options` (objectui#11717). for (const type of CHOICE_TYPES_REQUIRING_OPTIONS) { - it(`\`${type}\` with no \`options\` still parses through \`ObjectSchema\``, () => { - const result = ObjectSchema.safeParse({ name: 'account', label: 'A', fields: { stage: { type, label: 'L' } } }); - expect(result.success).toBe(true); + for (const [label, extra] of [['no `options`', {}], ['an EMPTY `options` list', { options: [] }]] as const) { + it(`the contract refuses a \`${type}\` with ${label}, at \`options\``, () => { + const result = ObjectSchema.safeParse({ name: 'account', label: 'A', fields: { stage: { type, label: 'L', ...extra } } }); + expect(result.success).toBe(false); + if (result.success) return; + expect(result.error.issues.map((issue) => issue.path.join('.'))).toContain('fields.stage.options'); + }); + } + + it(`CONTROL — a \`${type}\` with one option, or a shared \`picklist\`, is ACCEPTED by the same schema`, () => { + for (const extra of [{ options: [{ label: 'Open', value: 'open' }] }, { picklist: 'stage_values' }]) { + const result = ObjectSchema.safeParse({ name: 'account', label: 'A', fields: { stage: { type, label: 'L', ...extra } } }); + expect(result.success, JSON.stringify(extra)).toBe(true); + } }); } + it('the server-refused choice set is the guard\'s list: `FieldSchema` and `checkFieldCompleteness` give one answer', () => { + const refusedAtOptions = FieldType.options.filter((type) => { + const result = FieldSchema.safeParse({ type, label: 'L' }); + return !result.success && result.error.issues.some((issue) => issue.path.join('.') === 'options'); + }); + expect([...refusedAtOptions].sort()).toEqual([...CHOICE_TYPES_REQUIRING_OPTIONS].sort()); + }); + it('CONTROL — the same schema on the same document DOES refuse a target-less lookup', () => { const result = ObjectSchema.safeParse({ name: 'account', label: 'A', fields: { stage: { type: 'lookup', label: 'L' } } }); expect(result.success).toBe(false); diff --git a/packages/data-objectstack/src/object-metadata-write-guard.ts b/packages/data-objectstack/src/object-metadata-write-guard.ts index 40289f88bc..264d8ab6fb 100644 --- a/packages/data-objectstack/src/object-metadata-write-guard.ts +++ b/packages/data-objectstack/src/object-metadata-write-guard.ts @@ -55,36 +55,31 @@ * live Zod pass, on the stated ground that a schema issue on a draft the server * ACCEPTS would dead-bolt Save with no on-screen editor able to clear it. * - * The two invariants stand in different relations to the server, and the - * dead-bolt argument is made separately for each: - * - * - The relationship refusal forecloses nothing the server would have taken: - * the same body is refused one layer down with a 422 on - * `fields.NAME.reference`, so the guard only moves an identical refusal - * earlier, where it can name the field while it is still on screen. - * - The choice refusal is deliberately AHEAD of the server. The installed - * `@objectstack/spec` still accepts a choice with no option source, and the - * ruling closes the client order first on purpose — its execution parameter - * (2) reads "if Studio saves a `select` before its options exist, the - * objectui side changes its order first (a card in that lane) and the door - * closes after". So this refusal does hold a body the server would store - * today. It is not a dead-bolt, for the reason the ruling gives and the - * reason the editors give: the ruling makes the same body a server refusal - * once the door closes (a stored row is named and every later save of that - * object is refused until it gains options), and the message names the - * field on every surface that holds one. The Studio data page and the - * metadata-admin object editor carry an options editor in that field's - * inspector, so the hold clears on screen, including for an object that - * already stores such a field (both pinned by their - * `*.choiceWithoutOptions-11253` suites). plugin-designer's drawer has no - * options editor, so it stops offering the choice types; a stored `select` + * Neither invariant forecloses anything the server would have taken, and that + * is the whole of the dead-bolt argument for both: + * + * - The relationship refusal: the same body is refused one layer down with a + * 422 on `fields.NAME.reference`, so the guard only moves an identical + * refusal earlier, where it can name the field while it is still on screen. + * - The choice refusal: the same body is refused one layer down at + * `fields.NAME.options`, an empty list included, since `@objectstack/spec` + * 17.7.0 shipped the door (objectstack#21390, the ruling's own door). Until + * that release the installed spec still accepted a choice with no option + * source and this refusal was deliberately AHEAD of the server, by the + * ruling's execution parameter (2): "if Studio saves a `select` before its + * options exist, the objectui side changes its order first (a card in that + * lane) and the door closes after". The door has closed, the order changed + * first, and the refusal collapsed into the relationship form at + * objectui#11717's bump. The editors still clear it on screen: the Studio + * data page and the metadata-admin object editor carry an options editor in + * that field's inspector (both pinned by their + * `*.choiceWithoutOptions-11253` suites); plugin-designer's drawer has no + * options editor, so it stops offering the choice types, a stored `select` * keeps its type there and can be retyped or removed, and a stored `radio` * is among the types that page carries through unedited and sends to * metadata-admin, as it already does for a target-less `master_detail`. - * `object-metadata-write-guard.derivation.test.ts` re-measures "still - * accepted by the installed server" on every run; when the door ships and - * the pin moves, that reading turns red, and this paragraph collapses into - * the relationship one above. + * `object-metadata-write-guard.derivation.test.ts` re-measures "refused one + * layer down" for both invariants on every run. * * ⛔ **NOT strip-and-report-saved.** Dropping the offending field and reporting * success would trade a visible refusal for an invisible deletion — @@ -106,7 +101,7 @@ * - the choice set from the same members run through `checkFieldCompleteness` * (ADR-0078's author-time rule, which the ruling's door adopts), keeping * those it reports as `field/choice-without-options` at `error` severity. - * `FieldSchema` cannot answer this one yet, because the door has not shipped. + * The door itself shipped in 17.7.0, and the pin holds the two answers equal. * * A spec release that changes either set turns that pin red instead of leaving * a guard that quietly stopped covering the contract. diff --git a/packages/fields/src/widgets/CapabilityMultiSelectField.tsx b/packages/fields/src/widgets/CapabilityMultiSelectField.tsx index a47f13ee1f..eb5dc93380 100644 --- a/packages/fields/src/widgets/CapabilityMultiSelectField.tsx +++ b/packages/fields/src/widgets/CapabilityMultiSelectField.tsx @@ -141,6 +141,8 @@ const CURATED_CAPABILITY_LABELS = new Set([ 'setup_write', 'studio_access', 'manage_sharing', + // `@objectstack/spec` 17.7.0 added it to `PLATFORM_CAPABILITIES` (objectui#11717). + 'view_all_audit_log', ]); export function CapabilityMultiSelectField({ diff --git a/packages/fields/src/widgets/useFieldTranslation.ts b/packages/fields/src/widgets/useFieldTranslation.ts index f49387dcc5..2ca676b43c 100644 --- a/packages/fields/src/widgets/useFieldTranslation.ts +++ b/packages/fields/src/widgets/useFieldTranslation.ts @@ -230,6 +230,7 @@ const FIELD_DEFAULTS: Record = { 'capability.label.setup_write': 'Write Settings', 'capability.label.studio_access': 'Studio Access', 'capability.label.manage_sharing': 'Manage Sharing', + 'capability.label.view_all_audit_log': 'View All Audit Log', }; export const useFieldTranslation = createSafeTranslation( diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index fc554ce332..b94cab4d2e 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -25,6 +25,7 @@ const ar = { setup_write: "كتابة الإعدادات", studio_access: "الوصول إلى Studio", manage_sharing: "إدارة المشاركة", + view_all_audit_log: "عرض سجل التدقيق بالكامل", }, group: { platform: 'منصة', diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index b09df99b95..a1af01a70f 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -25,6 +25,7 @@ const de = { setup_write: "Einstellungen schreiben", studio_access: "Studio-Zugriff", manage_sharing: "Freigaben verwalten", + view_all_audit_log: "Gesamtes Audit-Protokoll anzeigen", }, group: { platform: 'Plattform', diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 39db2df11c..bd221e6869 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -36,6 +36,7 @@ const en = { setup_write: 'Write Settings', studio_access: 'Studio Access', manage_sharing: 'Manage Sharing', + view_all_audit_log: 'View All Audit Log', }, }, // objectui#3546 slice six — the read-only facet summary + Studio deep-link a diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index 780103c370..23c8528dfe 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -25,6 +25,7 @@ const es = { setup_write: "Escritura de la configuración", studio_access: "Acceso a Studio", manage_sharing: "Gestionar el uso compartido", + view_all_audit_log: "Ver todo el registro de auditoría", }, group: { platform: 'Plataforma', diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index c8e089c382..bd25f8a8b6 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -25,6 +25,7 @@ const fr = { setup_write: "Écriture des paramètres", studio_access: "Accès à Studio", manage_sharing: "Gérer le partage", + view_all_audit_log: "Voir tout le journal d'audit", }, group: { platform: 'Plateforme', diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index e00195d263..ee1a83027b 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -25,6 +25,7 @@ const ja = { setup_write: "設定の書き込み", studio_access: "Studio へのアクセス", manage_sharing: "共有の管理", + view_all_audit_log: "すべての監査ログを表示", }, group: { platform: 'プラットフォーム', diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index f7878a797b..073eb75de8 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -25,6 +25,7 @@ const ko = { setup_write: "설정 쓰기", studio_access: "Studio 접근", manage_sharing: "공유 관리", + view_all_audit_log: "모든 감사 로그 보기", }, group: { platform: '플랫폼', diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index 130f10d88d..4948b93f60 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -25,6 +25,7 @@ const pt = { setup_write: "Gravação de configurações", studio_access: "Acesso ao Studio", manage_sharing: "Gerenciar compartilhamento", + view_all_audit_log: "Ver todo o registro de auditoria", }, group: { platform: 'Plataforma', diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index 4c3c5cfaa9..d77801eb3a 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -25,6 +25,7 @@ const ru = { setup_write: "Запись настроек", studio_access: "Доступ к Studio", manage_sharing: "Управление общим доступом", + view_all_audit_log: "Просмотр всего журнала аудита", }, group: { platform: 'Платформа', diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 651cd12f43..6c0f2e2e77 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -35,6 +35,7 @@ const zh = { setup_write: '保存设置', studio_access: '访问 Studio', manage_sharing: '管理共享', + view_all_audit_log: '查看全部审计日志', }, }, // objectui#3546 切片六 —— 权限集记录上六个授权面的只读摘要与 Studio 深链 diff --git a/packages/plugin-calendar/src/ObjectCalendar.tsx b/packages/plugin-calendar/src/ObjectCalendar.tsx index 78e6b41efe..9e548e2e4f 100644 --- a/packages/plugin-calendar/src/ObjectCalendar.tsx +++ b/packages/plugin-calendar/src/ObjectCalendar.tsx @@ -1247,7 +1247,7 @@ export const ObjectCalendar: React.FC = ({ * * `@objectstack/spec`'s `CalendarConfigSchema` is a `strictObject` whose ONE * required key is `startDateField`; `titleField` is optional. Re-measured on - * the installed 17.6.0, three legs: `{}` and `{ titleField: 't' }` both fail + * the installed 17.7.0, three legs: `{}` and `{ titleField: 't' }` both fail * `invalid_type` at `startDateField`, and `{ startDateField: 'd' }` parses * CLEAN. The spec's own note on that schema names THIS renderer as the * reason — `resolveTitle` above takes an explicit `titleField` when present diff --git a/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx b/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx index b154eef8fd..a999757729 100644 --- a/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx +++ b/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx @@ -179,7 +179,7 @@ const LEDGERED_OTHER_CARD_READS: readonly string[] = []; * `ObjectCalendarSchema` and must stay so. * * ⚠️ The ground is NOT that the spec singles these two out. MEASURED on - * installed `@objectstack/spec` 17.6.0: `ComponentPropsMap['object-calendar']` + * installed `@objectstack/spec` 17.7.0: `ComponentPropsMap['object-calendar']` * is STRICT and declares exactly ten flat members — `calendar` `data` * `defaultView` `filter` `loading` `locale` `navigation` `objectName` `sort` * `staticData` (nine through 17.4.0; 17.5.0 added `navigation`) — @@ -351,20 +351,22 @@ describe('objectui#8651 — every key read off the node is declared on that sche /* ── 3. `calendar`, the container the spec names ───────────────────────────── */ -describe('objectui#8651 — `calendar` is declared; the spec sets the KEY, objectui sets the SHAPE', () => { - it('⚠️ the spec declares the KEY but NOT its shape — the slot refuses nothing', () => { - // The grounds, stated the way the instrument returns them. An earlier cut - // of this card said the spec declares the container and that "no conforming - // author could write" the alias spellings inside it. The first half is - // true; the second is false, and this row is why. +describe('objectui#8651 — `calendar` is declared; the spec sets the KEY, and since 17.7.0 its SHAPE too', () => { + it('the spec declares the KEY and, since 17.7.0, its shape — the slot is the list view\'s strict calendar block', () => { + // The grounds, stated the way the instrument returns them. Through 17.6.0 + // this row pinned the slot as `z.unknown()`: a nonsense key, the alias + // spelling and a number all parsed inside it. `@objectstack/spec` 17.7.0 + // types it (objectstack#21464 stage 2, `7d674dfc`) as the list view's own + // `calendar` member, `CalendarConfigSchema`, so each of those is refused now + // and this row flipped at that bump (objectui#11717). const oc = (ComponentPropsMap as unknown as Record)['object-calendar']; const inside = (value: unknown) => oc.safeParse({ objectName: 'duly_task', calendar: value }).success; expect(inside({ startDateField: 'kickoff' })).toBe(true); // known-accepted control - expect(inside({ [CONTROL_KEY]: 'x' })).toBe(true); // ⇒ the slot is not strict - expect(inside({ dateField: 'kickoff' })).toBe(true); // so the alias IS writable - expect(inside(42)).toBe(true); // it is `z.unknown()` + expect(inside({ [CONTROL_KEY]: 'x' })).toBe(false); // ⇒ the slot is strict + expect(inside({ dateField: 'kickoff' })).toBe(false); // so the alias is NOT writable + expect(inside(42)).toBe(false); // it is an object, not `z.unknown()` // CONTROL, same instrument, one level out: the element's props schema IS - // strict, so the reading above is about this SLOT and not a dead parser. + // strict too, so the readings above are about this SLOT's own shape. expect(oc.safeParse({ objectName: 'duly_task', [CONTROL_KEY]: 'x' }).success).toBe(false); }); diff --git a/packages/plugin-dashboard/package.json b/packages/plugin-dashboard/package.json index 9253a87ae7..306325b777 100644 --- a/packages/plugin-dashboard/package.json +++ b/packages/plugin-dashboard/package.json @@ -31,7 +31,7 @@ "@object-ui/permissions": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^17.5.0", + "@objectstack/spec": "^17.6.0", "lucide-react": "^1.43.0" }, "peerDependencies": { diff --git a/packages/plugin-dashboard/src/DatasetWidget.tsx b/packages/plugin-dashboard/src/DatasetWidget.tsx index d998dc62fa..568db4f43b 100644 --- a/packages/plugin-dashboard/src/DatasetWidget.tsx +++ b/packages/plugin-dashboard/src/DatasetWidget.tsx @@ -95,6 +95,7 @@ import { resolveFilterPlaceholders, computeMetricDelta } from './utils'; import { metricAccentTextClass } from './colorVariants'; import { DrillDownDrawer } from './DrillDownDrawer'; import { specDefaultWidgetType } from './widgetDispatch'; +import { DASHBOARD_WIDGET_MULTI_MEASURE_TYPES } from '@objectstack/spec/ui'; type Row = Record; // One RESULT-side totals grouping (the response's `totals[]`), named apart from @@ -388,29 +389,30 @@ const METRIC_TYPES = new Set(['metric', 'kpi', 'gauge', 'solid-gauge', 'bullet'] * The chart families that draw a DIMENSIONLESS widget's measures: one mark per * measure, the measures' labels on the category axis (objectui#11261). * - * The four families the spec names, read off the family a widget renders AS - * (`CHART_TYPE_MAP`). The metric-family arity refusal and its ADR-0087 entry - * (`dashboard-widget-metric-family-multi-measure-refused`) send an author who - * wants several numbers in one widget here: "`type: 'table'` renders a row of - * measures, and the chart families (`bar` / `line` / `area` / `combo`) render - * one mark per measure". `table` / `pivot` are that sentence's other half - * (`isTable`). + * The spec's own list, imported, never restated (objectui#11334 placement + * ruling 5925828806): `DASHBOARD_WIDGET_MULTI_MEASURE_TYPES` is the one list of + * the types that render several measures with no dimension — `table` / `pivot` + * and the chart families `bar` / `column` / `horizontal-bar` / `line` / `area` / + * `combo` — and the spec's chart measure-arity check refuses two or more + * measures on a dimensionless widget of any other type. It is read here off the + * family a widget renders AS (`CHART_TYPE_MAP`), so a `stacked-bar` draws as a + * `bar` does; `table` / `pivot` are no `CHART_TYPE_MAP` family, and take + * `isTable` instead. * * `column` and `horizontal-bar` are the `bar` family drawn in another - * orientation, so the same sentence covers them and nothing is invented by - * drawing them (objectui#8894, triage's answer on the card): `column` draws - * the measures' labels along the bottom as `bar` does, and `horizontal-bar` - * runs them down the left with one bar across per measure. `CHART_TYPE_MAP` - * keeps both under their own names, which is why they are listed here. + * orientation (objectui#8894, triage's answer on the card): `column` draws the + * measures' labels along the bottom as `bar` does, and `horizontal-bar` runs + * them down the left with one bar across per measure. * - * The spec's text names no other family, so a dimensionless one (pie, donut, - * funnel, scatter, radar, treemap, sankey) keeps the tile it always rendered: - * its display semantics for several measures are not invented here, and the - * spec is to refuse that shape at its door instead (objectstack#20958). Until - * that refusal is installed, the tile says which measures it drops (see - * `renderedMeasures` in the component). + * A dimensionless widget of any other family (pie, donut, funnel, scatter, + * radar, treemap, sankey) keeps the tile it always rendered: its display + * semantics for several measures are not invented here, and since + * `@objectstack/spec` 17.7.0 the spec refuses that shape at its door, which + * objectui's door follows (objectui#11717). A stored widget from before that + * refusal still says which measures it drops (see `renderedMeasures` in the + * component). */ -const MEASURE_AXIS_CHART_FAMILIES = new Set(['bar', 'column', 'horizontal-bar', 'line', 'area', 'combo']); +const MEASURE_AXIS_CHART_FAMILIES: ReadonlySet = new Set(DASHBOARD_WIDGET_MULTI_MEASURE_TYPES); /** * The chart families that draw ONE series, whatever the widget declares @@ -437,9 +439,10 @@ const MEASURE_AXIS_CHART_FAMILIES = new Set(['bar', 'column', 'horizontal-bar', * change the drawn chart or this set names its family. * * Triage's ruling on objectui#11417 is that the spec refuses two or more - * measures on these five types with a dimension too (objectstack#21293). Until - * that refusal reaches objectui, every door accepts the shape, and the - * dropped-measure diagnostic below is what says so. + * measures on these five types with a dimension too (objectstack#21293). Since + * `@objectstack/spec` 17.7.0 the spec's door and objectui's refuse the shape + * (objectui#11717); for a widget stored before that, the dropped-measure + * diagnostic below is what says so. */ const SINGLE_SERIES_CHART_FAMILIES = new Set(['pie', 'donut', 'funnel', 'treemap', 'sankey']); diff --git a/packages/plugin-dashboard/src/__tests__/DatasetWidget.singleSeriesMeasures-11417.test.tsx b/packages/plugin-dashboard/src/__tests__/DatasetWidget.singleSeriesMeasures-11417.test.tsx index 539bf762d2..60bc5eacca 100644 --- a/packages/plugin-dashboard/src/__tests__/DatasetWidget.singleSeriesMeasures-11417.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/DatasetWidget.singleSeriesMeasures-11417.test.tsx @@ -7,12 +7,13 @@ * * The chart branch of `DatasetWidget` hands every declared measure to the * shared chart renderer as a series, and the renderer's arms for those five - * families bind `series[0]` and read no other series. Every door accepts the - * document (objectui's `DashboardWidgetSchema` and `@objectstack/spec` alike), - * the query runs both measures, and the chart looks finished while answering a + * families bind `series[0]` and read no other series. Every door accepted the + * document (objectui's `DashboardWidgetSchema` and `@objectstack/spec` alike) + * until 17.7.0, the query runs both measures, and the chart looks finished while answering a * narrower question than its metadata asked. Triage's ruling on the card: the - * spec refuses the shape (objectstack#21293), and until that refusal reaches - * objectui the widget's existing diagnostic reports the dropped measure. + * spec refuses the shape (objectstack#21293, in `@objectstack/spec` 17.7.0, which + * objectui's door follows since objectui#11717), and for a widget stored before + * that refusal the widget's existing diagnostic reports the dropped measure. * * The pins are triage's: for each of the five types with a dimension and two * measures, the diagnostic names the dropped measure; one measure stays silent @@ -172,19 +173,22 @@ const renderChart = async (widget: Record, rows: Row[] = ROWS) describe('objectui#11417 — a single-series chart with a dimension says which measure it drops', () => { it.each(SINGLE_SERIES_TYPES)('SUBJECT (triage pin): a dimensioned `%s` with two measures names the dropped one', async (type) => { const doc = widgetOf(type); - // Every door accepts it, so the diagnostic is the only voice until the - // spec's refusal (objectstack#21293) reaches objectui. When the pin moves - // past that refusal these two go red ON PURPOSE: the mirror then follows the - // refusal (triage's item 2 on objectui#11417), and this pin becomes one - // about a STORED document, as objectui#8894's stored-metric pin is. + // A STORED document now, as objectui#8894's stored-metric pin is. Since + // `@objectstack/spec` 17.7.0 (objectstack#21293) the spec refuses the shape at + // `values`, and objectui's door refuses it with the spec's own issue (the + // mirror chains `checkDashboardWidgetChartMeasureArity`, objectui#11717, + // triage's item 2 on objectui#11417). These two asserted the acceptance until + // the pin moved past that refusal; they flipped on purpose. A widget stored + // before the refusal still renders, and the diagnostic still names the drop. + const spec = SpecDashboardWidgetSchema.safeParse(doc); + expect(spec.success, 'the spec\'s door refuses a dimensioned two-measure single-series widget').toBe(false); + const specIssue = spec.error!.issues.find((i) => i.path.join('.') === 'values')!; + expect(specIssue.code).toBe('custom'); + const mirror = DashboardWidgetSchema.safeParse(doc); + expect(mirror.success, 'objectui\'s door refuses it too').toBe(false); expect( - SpecDashboardWidgetSchema.safeParse(doc).success, - 'the spec\'s door refuses this now: objectstack#21293 has reached the pin — see the comment above', - ).toBe(true); - expect( - DashboardWidgetSchema.safeParse(doc).success, - 'objectui\'s door refuses this now — see the comment above', - ).toBe(true); + mirror.error!.issues.map((i) => ({ code: i.code, path: i.path.join('.'), message: i.message })), + ).toContainEqual({ code: 'custom', path: 'values', message: specIssue.message }); await renderChart(doc); const msg = measureWarning(); expect(msg).toBeDefined(); diff --git a/packages/plugin-dashboard/src/__tests__/DatasetWidget.unrenderedMeasures-8894.test.tsx b/packages/plugin-dashboard/src/__tests__/DatasetWidget.unrenderedMeasures-8894.test.tsx index c735d45482..82e6a83f19 100644 --- a/packages/plugin-dashboard/src/__tests__/DatasetWidget.unrenderedMeasures-8894.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/DatasetWidget.unrenderedMeasures-8894.test.tsx @@ -211,13 +211,17 @@ describe('objectui#8894 — the dropped measures speak', () => { expect(measureWarning()).toBeDefined(); }); - it('SUBJECT (triage pin): a dimensionless pie with two measures warns and names the dropped one', async () => { + it('SUBJECT (triage pin): a STORED dimensionless pie with two measures warns and names the dropped one', async () => { // A widget that declares no dimension renders as a tile on a type the // spec's text gives no dimensionless rendering of, and drops the same - // measures. Every door accepts this document today, so the diagnostic is - // the only thing that speaks until the spec refuses the shape. + // measures. Since `@objectstack/spec` 17.7.0 the spec refuses the shape and + // objectui's door follows it (`checkDashboardWidgetChartMeasureArity`, + // objectui#11717), so — as for the stored metric below — it reaches the + // renderer only as a row stored before that narrowing, and the diagnostic + // is what speaks at the dashboard. This line asserted the acceptance until + // the mirror attached; it flipped on purpose. const doc = { id: 'bare_pie', type: 'pie', dataset: 'sales', values: ['revenue', 'cost'] }; - expect(DashboardWidgetSchema.safeParse(doc).success, 'the door accepts it — the diagnostic is the only voice').toBe(true); + expect(DashboardWidgetSchema.safeParse(doc).success, 'the door refuses it — only a stored row reaches the renderer').toBe(false); await renderTile(doc, [{ revenue: 510000, cost: 120000 }], '510000'); const msg = measureWarning(); expect(msg).toContain('renders 1 of its 2 declared measures: "revenue".'); diff --git a/packages/plugin-dashboard/src/__tests__/WidgetConfigPanel.measureDoor-8894.test.tsx b/packages/plugin-dashboard/src/__tests__/WidgetConfigPanel.measureDoor-8894.test.tsx index a13d1e049b..b3cbc39974 100644 --- a/packages/plugin-dashboard/src/__tests__/WidgetConfigPanel.measureDoor-8894.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/WidgetConfigPanel.measureDoor-8894.test.tsx @@ -31,13 +31,16 @@ * same document, read off the spec's own parse: equal text proves the panel * surfaces the door's verdict rather than a restatement of it. * - * ## OWED TO objectui#11334 — a bounded ledger, not a skip + * ## The chart families: objectui#11334 * - * The spec's dimensionless measure-arity check puts more types into FAMILY than - * objectui's door refuses, because the mirror does not re-attach it yet. Those - * types are booked in `OWED_TO_OBJECTUI_11334` below, as in the types package's - * test: their rows assert what the panel does today, and a cap row requires the - * set the panel fails to refuse to equal the ledger exactly. + * The spec's chart measure-arity check (`checkDashboardWidgetChartMeasureArity`, + * `@objectstack/spec` 17.7.0) puts more types into FAMILY than the metric + * family: two measures with no `dimensions` on `pie`, `funnel` and the rest. + * objectui#11717 chained it on objectui's door, and the panel asks that door, + * so it refuses them with no edit of its own. Until then those types were booked + * in an `OWED_TO_OBJECTUI_11334` ledger; the ledger is struck, its rows now + * assert the refusal, and the cap row requires the set the panel fails to + * refuse to be EMPTY. */ import * as React from 'react'; @@ -46,6 +49,7 @@ import { render, screen, cleanup, within } from '@testing-library/react'; import { ChartTypeSchema as SpecChartTypeSchema, DashboardWidgetSchema as SpecDashboardWidgetSchema, + checkDashboardWidgetChartMeasureArity, } from '@objectstack/spec/ui'; import { WidgetConfigPanel } from '../WidgetConfigPanel'; import type { WidgetDatasetCatalogEntry } from '../dataset-catalog'; @@ -85,44 +89,18 @@ const FAMILY = SPEC_TYPES.filter((t) => specArityIssue(t, TWO) !== undefined); const OTHERS = SPEC_TYPES.filter((t) => !FAMILY.includes(t)); /** - * ⚠️ OWED TO objectui#11334 — the FAMILY types the panel does NOT refuse yet, - * booked rather than skipped. - * - * The spec this repository resolves refuses two or more measures on these types - * when the widget declares no `dimensions` - * (`checkDashboardWidgetDimensionlessMeasureArity`, objectstack `11d28c17`, - * #21053). The panel asks objectui's door (`measureRefusal`), and the door's - * mirror does not re-attach that check: objectstack `main` renamed its export - * (`32d57690`), and a static import of the name the resolved spec ships fails - * the `Spec Main Shape Gate`. objectui#11334 owns the mirror; the panel follows - * it with no edit of its own. - * - * Booked by objectui#11438 ruling A″ (record 5968177777), which applies - * objectui#11111 decision 3 = B (record 5902351047) to the bump. Each listed - * type's rows assert TODAY's behaviour (the add control is offered, and no - * refusal is shown under a stored pair the spec refuses), and the cap row - * requires the FAMILY types the panel fails to refuse to EQUAL this list. A new - * difference is red, and so is a listed type the panel starts refusing: the - * entries go stale, by name, when the mirror attaches, and objectui#11334 - * strikes them in that change. - * - * EXPIRES when objectui resolves an `@objectstack/spec` carrying `32d57690`, or - * 2026-11-02, whichever is first. That resolution is caught by name in - * `packages/types/src/__tests__/spec-object-refinements-7715.test.ts`, whose - * census books the same check; the date is read by objectui#11334, not by a - * clock. + * The FAMILY types the spec's chart measure-arity check refuses with no + * dimension (its dimensionless arm), read by calling the exported check with a + * collecting context — the types the struck `OWED_TO_OBJECTUI_11334` ledger held. */ -const OWED_TO_OBJECTUI_11334 = ['pie', 'donut', 'funnel', 'scatter', 'treemap', 'sankey', 'radar']; - -/** The reason every owed row prints when it fails. */ -const OWED_REASON = - 'OWED TO objectui#11334: the dimensionless measure-arity check is not re-attached by the mirror yet. ' + - 'Booked by objectui#11438 ruling A″ (record 5968177777), applying objectui#11111 decision 3 = B ' + - '(record 5902351047). Expires when objectui resolves an @objectstack/spec carrying 32d57690, or ' + - '2026-11-02, whichever is first.'; - -/** The FAMILY types the panel refuses today: FAMILY minus the ledger. */ -const MIRRORED = FAMILY.filter((t) => !OWED_TO_OBJECTUI_11334.includes(t)); +const DIMENSIONLESS_ARM = FAMILY.filter((type) => { + const issues: unknown[] = []; + checkDashboardWidgetChartMeasureArity( + { id: 'sales_tile', type, values: TWO }, + { addIssue: (issue: unknown) => issues.push(issue) } as unknown as Parameters[1], + ); + return issues.length === 1; +}); const renderPanel = (config: Record, datasets: WidgetDatasetCatalogEntry[] | undefined = catalog) => render(); @@ -142,7 +120,7 @@ describe('objectui#8894 — the family is read off the spec\'s rule, and it is n }); describe('objectui#8894 — no second measure is offered on a metric-family widget', () => { - it.each(MIRRORED)('SUBJECT: `%s` with one measure chosen offers no add control (catalog)', (type) => { + it.each(FAMILY)('SUBJECT: `%s` with one measure chosen offers no add control (catalog)', (type) => { renderPanel({ id: 'rev_tile', type, dataset: 'sales_pipeline', values: ONE }); expect(valuesField().getByTestId('dataset-name-chip-revenue')).toBeInTheDocument(); expect(addControl()).toBeNull(); @@ -150,7 +128,7 @@ describe('objectui#8894 — no second measure is offered on a metric-family widg expect(valuesField().queryByTestId('dataset-names-refusal')).toBeNull(); }); - it.each(MIRRORED)('SUBJECT: `%s` with one measure chosen offers no add control (free text, no catalog)', (type) => { + it.each(FAMILY)('SUBJECT: `%s` with one measure chosen offers no add control (free text, no catalog)', (type) => { renderPanel({ id: 'rev_tile', type, dataset: 'sales_pipeline', values: ONE }, undefined); expect(addControl()).toBeNull(); }); @@ -168,7 +146,7 @@ describe('objectui#8894 — no second measure is offered on a metric-family widg }); describe('objectui#8894 — measures the door refuses are reported at once, in the door\'s words', () => { - it.each(MIRRORED)('SUBJECT: a stored `%s` with two measures shows the spec\'s own refusal under them', (type) => { + it.each(FAMILY)('SUBJECT: a stored `%s` with two measures shows the spec\'s own refusal under them', (type) => { renderPanel({ id: 'sales_tile', type, dataset: 'sales_pipeline', values: TWO }); const shown = valuesField().getByTestId('dataset-names-refusal'); expect(shown).toHaveAttribute('role', 'alert'); @@ -192,31 +170,40 @@ describe('objectui#8894 — measures the door refuses are reported at once, in t }); }); -describe('objectui#11334 — OWED: what the panel does today for the types the spec refuses and the door accepts', () => { - it.each(OWED_TO_OBJECTUI_11334)('OWED TO objectui#11334: `%s` with one measure chosen still offers the add control (catalog)', (type) => { +describe('objectui#11334 — the dimensionless arm: the panel refuses what the door refuses', () => { + // Formerly the OWED TO objectui#11334 rows, which asserted what the panel did + // while the door accepted these widgets (the add control offered, no refusal + // shown). The door refuses them now, so each row asserts the refusal. + it('the arm is read off the spec\'s check and is not vacuous: it holds `pie`, and not the metric family\'s `kpi`', () => { + expect(DIMENSIONLESS_ARM).toContain('pie'); + expect(DIMENSIONLESS_ARM).not.toContain('kpi'); + }); + + it.each(DIMENSIONLESS_ARM)('`%s` with one measure chosen and no dimension offers no add control (catalog)', (type) => { renderPanel({ id: 'rev_tile', type, dataset: 'sales_pipeline', values: ONE }); expect(valuesField().getByTestId('dataset-name-chip-revenue')).toBeInTheDocument(); - expect(addControl(), OWED_REASON).not.toBeNull(); - expect(valuesField().queryByTestId('dataset-names-refusal'), OWED_REASON).toBeNull(); + expect(addControl()).toBeNull(); + expect(valuesField().queryByTestId('dataset-names-refusal')).toBeNull(); }); - it.each(OWED_TO_OBJECTUI_11334)('OWED TO objectui#11334: `%s` with one measure chosen still offers the add control (free text, no catalog)', (type) => { + it.each(DIMENSIONLESS_ARM)('`%s` with one measure chosen and no dimension offers no add control (free text, no catalog)', (type) => { renderPanel({ id: 'rev_tile', type, dataset: 'sales_pipeline', values: ONE }, undefined); - expect(addControl(), OWED_REASON).not.toBeNull(); + expect(addControl()).toBeNull(); }); - it.each(OWED_TO_OBJECTUI_11334)('OWED TO objectui#11334: a stored `%s` with two measures and no dimension shows no refusal, though the spec refuses it', (type) => { - expect(specArityIssue(type, TWO), OWED_REASON).toBeDefined(); + it.each(DIMENSIONLESS_ARM)('a stored `%s` with two measures and no dimension shows the spec\'s own refusal under them', (type) => { renderPanel({ id: 'sales_tile', type, dataset: 'sales_pipeline', values: TWO }); - expect(valuesField().queryByTestId('dataset-names-refusal'), OWED_REASON).toBeNull(); + const shown = valuesField().getByTestId('dataset-names-refusal'); + expect(shown).toHaveAttribute('role', 'alert'); + expect(shown.textContent).toBe(specArityIssue(type, TWO)!.message); expect(valuesField().getByTestId('dataset-name-chip-revenue')).toBeInTheDocument(); expect(valuesField().getByTestId('dataset-name-chip-deal_count')).toBeInTheDocument(); - expect(addControl(), OWED_REASON).not.toBeNull(); + expect(addControl()).toBeNull(); }); }); -describe('objectui#11334 — the cap: the panel fails to refuse EXACTLY the ledgered FAMILY types', () => { - it('the FAMILY types whose stored two-measure widget shows no spec refusal equal the ledger', () => { +describe('objectui#11334 — the cap: the panel fails to refuse NO FAMILY type (the struck ledger stays empty)', () => { + it('every FAMILY type\'s stored two-measure widget shows the spec\'s own refusal', () => { const unrefused = FAMILY.filter((type) => { const { unmount } = renderPanel({ id: 'sales_tile', type, dataset: 'sales_pipeline', values: TWO }); const shown = valuesField().queryByTestId('dataset-names-refusal'); @@ -224,11 +211,10 @@ describe('objectui#11334 — the cap: the panel fails to refuse EXACTLY the ledg unmount(); return !refused; }); - expect([...unrefused].sort(), OWED_REASON).toEqual([...OWED_TO_OBJECTUI_11334].sort()); + expect(unrefused).toEqual([]); }); - it('the ledger leaves the metric family judged: the mirrored rows are not vacuous', () => { - expect(MIRRORED.length).toBeGreaterThan(0); - expect(MIRRORED).toContain('kpi'); + it('the metric family is still judged beside the chart arm: `kpi` is in FAMILY', () => { + expect(FAMILY).toContain('kpi'); }); }); diff --git a/packages/plugin-designer/src/MetadataFieldsPage.tsx b/packages/plugin-designer/src/MetadataFieldsPage.tsx index 5f5a5faef7..f1d54621ff 100644 --- a/packages/plugin-designer/src/MetadataFieldsPage.tsx +++ b/packages/plugin-designer/src/MetadataFieldsPage.tsx @@ -184,7 +184,7 @@ function isDesignerAuthorableType(raw: unknown): raw is DesignerFieldType { * ## Why reading the retired spelling is a RENAME here and not a laundering * * `@objectstack/spec` treats the two as one key under two spellings and says - * so in the refusal itself. Measured on the installed 17.6.0: + * so in the refusal itself. Measured on the installed 17.7.0: * * FieldSchema.safeParse({ type: 'lookup', label: 'L', referenceTo: 'account' }) * => success = false diff --git a/packages/plugin-detail/src/index.tsx b/packages/plugin-detail/src/index.tsx index ee796dc5c1..ca1a806cb5 100644 --- a/packages/plugin-detail/src/index.tsx +++ b/packages/plugin-detail/src/index.tsx @@ -953,7 +953,7 @@ ComponentRegistry.register('alert', RecordAlertRenderer, { // through `pickLocalized`, which is exactly what these descriptions teach. // Declaring the map arm therefore adds no shape the block does not already // honour; it stops the manifest gate warning `type-mismatch` on the - // recommended write. (The row DOES exist as of the installed 17.6.0 — read + // recommended write. (The row DOES exist as of the installed 17.7.0 — read // for `visible` below, objectui#9100 — so the "no entry" reading is stale; // these two arms are unaffected either way.) { name: 'title', type: ['string', 'object'], description: 'Accepts an inline translation map ({ en, "zh-CN", … })' }, @@ -961,7 +961,7 @@ ComponentRegistry.register('alert', RecordAlertRenderer, { // objectui#9100 — the spec accepts three arms here and the renderer now // resolves all three, so a single `'string'` was the declaration-narrower- // than-the-contract family of objectui#4581, one layer up. Measured on the - // INSTALLED `@objectstack/spec` 17.6.0 (`dist/ui/index.d.ts`, the + // INSTALLED `@objectstack/spec` 17.7.0 (`dist/ui/index.d.ts`, the // `ComponentPropsMap['record:alert']` row): `visible` is // `boolean | string | { dialect: 'cel'|'cron'|'template', source?, … }`, // and `renderers/record-alert.tsx` hands whichever arrives to diff --git a/packages/plugin-form/src/MasterDetailForm.tsx b/packages/plugin-form/src/MasterDetailForm.tsx index 647d58e1b8..037d45a411 100644 --- a/packages/plugin-form/src/MasterDetailForm.tsx +++ b/packages/plugin-form/src/MasterDetailForm.tsx @@ -78,21 +78,20 @@ import type { ObjectMasterDetailFormProps } from '@objectstack/spec/ui'; * `__tests__/masterDetailDetailsMembers-8071.test.tsx` holds the two equal at * compile time. * - * ⛔ ONE fork, stated rather than absorbed: at 17.6.0 the spec entry declares - * `sortField`, and this form reads no such member. objectui#11070 round 9 - * retired the authored override: the child field the grid stamps with each - * line's position is DERIVED from the child object (`deriveDetail` picks its - * `position` / `sort_order` / … field) and carried on the resolved entry, and - * row 2c of that test pins that a written `sortField` is read by nothing. - * objectstack has since retired the member on its `main` — a `retiredKey()` + * ONE key is left off, and it is no longer a fork: `sortField`. This form + * reads no such member — objectui#11070 round 9 retired the authored override: + * the child field the grid stamps with each line's position is DERIVED from + * the child object (`deriveDetail` picks its `position` / `sort_order` / … + * field) and carried on the resolved entry, and row 2c of that test pins that a + * written `sortField` is read by nothing. Through 17.6.0 the spec entry still + * declared it; `@objectstack/spec` 17.7.0 retires it — a `retiredKey()` * tombstone on the entry (objectstack-ai/objectstack#21589, landed by PR - * objectstack-ai/objectstack#21632 as `6ec54f00`), unreleased after 17.6.0 — - * so the fork closes at objectui's bump to the first `@objectstack/spec` - * release that carries it; the omission below then subtracts a key the entry - * no longer offers, and holds on both shapes. Until that bump the member is - * left OFF this face, so the authoring face declares no key the renderer does - * not honour. That omission is the whole of what is hand-written about the - * shape. + * objectstack-ai/objectstack#21632 as `6ec54f00`) — so both published zod faces + * refuse an authored `sortField` with the spec's retired-key message + * (objectui#11717). The entry still LISTS the key as that tombstone, and the + * omission below keeps it off this face rather than offer a member whose only + * legal value is absence. That omission is the whole of what is hand-written + * about the shape. * * Where each member lands — the renderer's reading, which the spec's * description does not carry: diff --git a/packages/plugin-form/src/__tests__/formSectionGroupReference-7051.test.tsx b/packages/plugin-form/src/__tests__/formSectionGroupReference-7051.test.tsx index 2456fc3714..89bd32a95e 100644 --- a/packages/plugin-form/src/__tests__/formSectionGroupReference-7051.test.tsx +++ b/packages/plugin-form/src/__tests__/formSectionGroupReference-7051.test.tsx @@ -448,7 +448,7 @@ describe('objectui#7051 — resolveSectionGroupReferences', () => { /** * ⭐ The second axis. The card asks that group-owned presentation keys beside * `group`, and `group` on a wizard step, not be SILENTLY accepted. Measured - * against the installed `@objectstack/spec` (17.6.0): both are refused at PARSE + * against the installed `@objectstack/spec` (17.7.0): both are refused at PARSE * — a stronger door than the lint that objectui#8497 concluded owns EXISTENCE * checking, and a different one. So authored metadata carrying either never * reaches this renderer at all; the renderer's own reports above exist for the diff --git a/packages/plugin-form/src/__tests__/masterDetailDetailsMembers-8071.test.tsx b/packages/plugin-form/src/__tests__/masterDetailDetailsMembers-8071.test.tsx index 087734855f..29f096437f 100644 --- a/packages/plugin-form/src/__tests__/masterDetailDetailsMembers-8071.test.tsx +++ b/packages/plugin-form/src/__tests__/masterDetailDetailsMembers-8071.test.tsx @@ -20,15 +20,15 @@ * objectui#11396): the spec row is an array of CLOSED entries — `childObject` * required, an undeclared key refused by name, `columns` the spec's inline grid * column — and `MasterDetailDetailConfig` is derived from that entry by - * reference, with ONE stated fork: `@objectstack/spec` 17.6.0 declares - * `sortField`, which this block does not read (objectui#11070 round 9 retired - * the authored override; row 2c below pins that a written one reaches - * nothing); objectstack `main` has since retired it too - * (objectstack-ai/objectstack#21589, unreleased after 17.6.0), and the fork - * closes at objectui's bump to the first release carrying that. The registration - * declares `of: 'object'`, derived from that single-kind member contract. The + * reference, with ONE key left off: `sortField`, which this block does not read + * (objectui#11070 round 9 retired the authored override; row 2c below pins that + * a written one reaches nothing). That was a fork through 17.6.0, which + * declared the member; `@objectstack/spec` 17.7.0 retires it too + * (objectstack-ai/objectstack#21589, a `retiredKey()` tombstone the entry still + * lists), so the fork is closed (objectui#11717). The registration declares + * `of: 'object'`, derived from that single-kind member contract. The * compile-time block at the end of this file holds the config equal to the - * spec's entry minus that fork and spells the member list out, so the member + * spec's entry minus that key and spells the member list out, so the member * count is re-derived by the compiler rather than by this sentence. A * declaration fixes an entry's SHAPE, not what this block DOES with a member, * so the read site is still the whole member contract, and every row below is @@ -485,19 +485,18 @@ type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B type SpecDetailEntry = NonNullable[number]; /** - * The config IS the spec's entry minus the one stated fork, by reference + * The config IS the spec's entry minus `sortField`, by reference * (objectui#11396): a member the spec adds, drops or retypes lands on the * config with no hand edit, and a hand-written restatement that drifts is red - * here before it is red anywhere else. It holds on both spec shapes this - * repository is compiled against: 17.6.0, where the entry declares `sortField` - * as a string, and objectstack `main`, where the entry carries a `retiredKey()` - * tombstone for it (objectstack-ai/objectstack#21589, unreleased after 17.6.0) - * — the subtraction removes the key either way. ⛔ No row here pins which of - * the two shapes the spec has; the `Spec Main Shape Gate` compiles this file - * against `main`, so such a row is red there the day the two differ. The - * forward tripwire for the retirement is the runtime pin in `@object-ui/types` - * (`object-master-detail-form-details-entry-11396.test.ts`), which records - * `sortField` as ACCEPTED on 17.6.0 and flips at the bump. + * here before it is red anywhere else. The installed 17.7.0 entry carries a + * `retiredKey()` tombstone for `sortField` (objectstack-ai/objectstack#21589); + * 17.6.0 declared it as a string, and the subtraction removes the key either + * way. ⛔ No row here pins which of the two shapes the spec has; the `Spec Main + * Shape Gate` compiles this file against objectstack `main`, so such a row is + * red there the day the two differ. The runtime pin in `@object-ui/types` + * (`object-master-detail-form-details-entry-11396.test.ts`) records + * `sortField` as REFUSED, with the spec's retired-key message, since the + * 17.7.0 bump (objectui#11717). */ const derivedFromSpec: Equal> = true; @@ -526,6 +525,6 @@ const memberList: Equal< /** The lit control for the directive below: the same literal without the retired member compiles. */ const authoredDetail: MasterDetailDetailConfig = { childObject: 'po_step', amountField: 'qty' }; -// @ts-expect-error objectui#11070 round 9: `sortField` is retired as a detail member; the sort field is derived from the child object. This face leaves it off on purpose (objectui#11396), on 17.6.0 where the spec declares it and on objectstack main where the spec retired it too. +// @ts-expect-error objectui#11070 round 9: `sortField` is retired as a detail member; the sort field is derived from the child object. This face leaves it off on purpose (objectui#11396), and the spec retired it too in 17.7.0 (objectstack-ai/objectstack#21589). const retiredSortField: MasterDetailDetailConfig = { childObject: 'po_step', sortField: 'line_no' }; void [derivedFromSpec, columnsAreTheGridColumn, memberList, authoredDetail, retiredSortField]; diff --git a/packages/plugin-form/src/index.tsx b/packages/plugin-form/src/index.tsx index fef9d7f94f..c387e7910e 100644 --- a/packages/plugin-form/src/index.tsx +++ b/packages/plugin-form/src/index.tsx @@ -560,7 +560,7 @@ ComponentRegistry.register('object-master-detail-form', MasterDetailFormRenderer of: 'object', required: true, description: - 'The child collections, one OBJECT per entry, judged by the spec\'s closed `details` entry: `childObject` (required) names the child object; `relationshipField` is the FK on the child back to the parent, derived from the child\'s metadata when omitted; `columns` are the line grid\'s columns as the spec\'s inline grid column objects (`{ name, … }`, never a bare field name), derived from the child\'s fields when omitted; `formFields` names the per-row form\'s fields; `inlineMode` is `grid` (editable cells) or `form` (list + per-row form); `amountField` is the CHILD column summed and `totalField` the PARENT field that sum is saved to; `title`, `minRows`, `maxRows` and `addLabel` dress the section and its grid. An undeclared member key is refused by name. The entry\'s `sortField` is read by nothing here — the line-position field is derived from the child object (objectui#11070) — and objectstack retires the key on its `main` (objectstack-ai/objectstack#21589, after 17.6.0).', + 'The child collections, one OBJECT per entry, judged by the spec\'s closed `details` entry: `childObject` (required) names the child object; `relationshipField` is the FK on the child back to the parent, derived from the child\'s metadata when omitted; `columns` are the line grid\'s columns as the spec\'s inline grid column objects (`{ name, … }`, never a bare field name), derived from the child\'s fields when omitted; `formFields` names the per-row form\'s fields; `inlineMode` is `grid` (editable cells) or `form` (list + per-row form); `amountField` is the CHILD column summed and `totalField` the PARENT field that sum is saved to; `title`, `minRows`, `maxRows` and `addLabel` dress the section and its grid. An undeclared member key is refused by name. `sortField` is not a member: the line-position field is derived from the child object (objectui#11070), and since `@objectstack/spec` 17.7.0 the entry refuses an authored `sortField` with the spec\'s retired-key message (objectstack-ai/objectstack#21589).', }, { name: 'recordId', type: 'string', description: 'The parent record to load in `edit` mode. Leave unset for `create`.' }, // TWO values, not the six `object-form` declares (objectui#5939). A bare diff --git a/packages/plugin-gantt/src/index.tsx b/packages/plugin-gantt/src/index.tsx index 38398f1004..5a8c9ba069 100644 --- a/packages/plugin-gantt/src/index.tsx +++ b/packages/plugin-gantt/src/index.tsx @@ -187,7 +187,7 @@ ComponentRegistry.register('object-gantt', ObjectGanttRenderer, { { name: 'holidays', type: 'array', of: 'string', description: 'Additional non-working dates for the working calendar, ISO `yyyy-mm-dd` strings; folded into a Set for the duration math. A non-empty list turns the working calendar on by itself, without `skipWeekends`, and the day view folds each listed date\'s column out of its axis.' }, { name: 'persistLayout', type: 'boolean', description: 'Opt OUT of layout and filter-chip persistence — only an explicit `false` disables it; the storage key is `objectName:viewName`. The object half is `objectName`, else `data.object`, and `gantt` on inline rows that name neither. With `false` the toolbar has no save-layout button and nothing is stored.' }, { name: 'viewName', type: 'string', description: 'Layout-persistence scope, the second half of the `objectName:viewName` storage key (renderer default `\'default\'`). Two gantts on the same object with different names keep separate saved layouts, and a gantt restores only the layout saved under its own name.' }, - { name: 'markers', type: 'array', description: 'Extra vertical reference lines drawn like the Today marker ({ date, label?, color? }). `date` places the line, and a date outside the chart\'s range draws none; `label` is the text drawn against it; `color` is its CSS colour, the theme\'s primary colour when omitted.' }, + { name: 'markers', type: 'array', description: 'Extra vertical reference lines drawn like the Today marker — each `{ date, label?, color? }`: `date` places the line (a date outside the drawn range draws none), `label` is drawn against it, `color` paints it. `color` is a CSS colour, the theme\'s primary colour when omitted.' }, { name: 'criticalPath', type: 'boolean', description: 'Seed the critical-path highlight ON; the toolbar toggle stays available either way' }, { name: 'showBaselines', type: 'boolean', description: 'Render the planned-vs-actual baseline bars — ON unless an explicit `false` disables it. A task draws one only when `gantt.baselineStartField` and `gantt.baselineEndField` give it both baseline dates.' }, { name: 'readOnly', type: 'boolean', description: 'Disable every write path on this gantt and lock the record drawer. A task click still opens the record, read-only.' }, diff --git a/packages/plugin-gantt/src/readme-navigation-example.test.ts b/packages/plugin-gantt/src/readme-navigation-example.test.ts index 9060060c41..8f85c70379 100644 --- a/packages/plugin-gantt/src/readme-navigation-example.test.ts +++ b/packages/plugin-gantt/src/readme-navigation-example.test.ts @@ -79,8 +79,9 @@ * * WARNING: `declaredMembers()` cannot derive that key's retirement. When this * was written the spec in the lockfile (17.4.0) still DECLARED `view` as a live - * member. The installed `@objectstack/spec` 17.6.0 lands it as a tombstone, - * as 17.5.0 did (objectui#11073 measured it, objectui#11438 again: a `never` + * member. The installed `@objectstack/spec` 17.7.0 lands it as a tombstone, + * as 17.5.0 and 17.6.0 did (objectui#11073 measured it, objectui#11438 and + * objectui#11717 again: a `never` * schema, and `{ view: 'detail' }` is * refused at `view` with "`view.list.navigation.view` was removed in * @objectstack/spec 17.5.0") -- but a tombstone is still a KEY of the shape, diff --git a/packages/plugin-kanban/src/index.tsx b/packages/plugin-kanban/src/index.tsx index fc767e71eb..871ed97a6e 100644 --- a/packages/plugin-kanban/src/index.tsx +++ b/packages/plugin-kanban/src/index.tsx @@ -549,8 +549,8 @@ export const ObjectKanbanRenderer: React.FC<{ schema: any; [key: string]: any }> * ## Why these keys were added * * `@objectstack/spec`'s `ComponentPropsMap['object-kanban']` carries FIFTEEN - * top-level keys on the installed 17.6.0 pin, fourteen of them live, the - * same set as 17.5.0: that release + * top-level keys on the installed 17.7.0 pin, fourteen of them live, the + * same set as 17.5.0 and 17.6.0: 17.5.0 * added `navigation` and turned `quickAdd` into a tombstone (typed `never`, * refused by name — objectui#8285's ruled retirement, landed). This list * published three until objectui#8186 added `filter`. ⚠️ The shape carried diff --git a/packages/plugin-timeline/src/index.tsx b/packages/plugin-timeline/src/index.tsx index 89b3327cca..0a8d17cdfb 100644 --- a/packages/plugin-timeline/src/index.tsx +++ b/packages/plugin-timeline/src/index.tsx @@ -429,13 +429,13 @@ const TIMELINE_OBJECT_NAME_DESCRIPTION = 'Object this timeline binds to. Optiona const TIMELINE_CONFIG_DESCRIPTION = 'Timeline configuration, the author face — the same block `ListViewSchema.timeline` declares: { startDateField, endDateField, titleField, groupByField, colorField, scale }. The flat top-level spellings beside it are the runtime handoff, not a second authoring spelling. On entries composed from records, `startDateField` is the entry’s date, `endDateField` the end printed after it, `titleField` the title, `groupByField` the field whose value heads each group (without it, entries group into date buckets), and `colorField` the field whose option colour, or the value itself when it is a colour literal, paints the marker. Each outranks the flat key of the same binding, and `titleField` / `startDateField` outrank `mapping`’s `title` / `date`. `scale` sets the axis unit of the gantt branch, which on this block draws authored `items` only.'; const TIMELINE_LIMIT_DESCRIPTION = 'Maximum number of records loaded onto the rail (row cap); lowered to the query\'s top-level `$top` (renderer default 100). A timeline renders one rail with no pagination control, so this is the author\'s window rather than a page size. A value that is not a positive integer is ignored with a console warning, and the default cap applies.'; const TIMELINE_DATA_DESCRIPTION = 'Pre-fetched records — read FIRST as the rail\'s row source, ahead of the data-scope binding and the fetch, and composed into entries through the same `timeline` field bindings a fetched row takes; authoring it suppresses the object query entirely. Distinct from `items`, which is the already-composed entry shape and wins over this key when both are written.'; -const TIMELINE_ITEMS_DESCRIPTION = 'Static inline entries — read ahead of every record source, `data` above included, and bypasses the object query entirely (the renderer becomes a pass-through). Each element is objectui\'s declared timeline element, `@object-ui/types`\'s `TimelineFeedItem` (`variant` absent / `vertical` / `horizontal`) or `TimelineGanttItem` (`variant: \'gantt\'`), the arm this node\'s `variant` selects. They are drawn as written: the `timeline` block’s field bindings, `mapping` and `descriptionField` do not apply to them.'; +const TIMELINE_ITEMS_DESCRIPTION = 'Static inline entries — read ahead of every record source, `data` above included, and bypasses the object query entirely (the renderer becomes a pass-through). Each entry is the kind this node\'s `variant` selects: a feed entry `{ time?, title, description?, variant?, icon?, content?, className? }` (`variant` absent / `vertical` / `horizontal`), or a gantt row `{ label, items? }` (`variant: \'gantt\'`) whose bars are `{ title?, startDate?, endDate?, variant? }`, each date a string or epoch milliseconds. These are `@object-ui/types`\'s `TimelineFeedItem` and `TimelineGanttItem`. They are drawn as written: the `timeline` block’s field bindings, `mapping` and `descriptionField` do not apply to them.'; const TIMELINE_DATE_FORMAT_DESCRIPTION = 'How each entry\'s date is rendered (renderer default `short`): `short` / `long` are locale-formatted, `iso` is the locale-free machine form.'; const TIMELINE_ROW_LABEL_DESCRIPTION = 'Header label for the gantt row column — read by the gantt branch only, which on this block needs authored `items`. Without it the header shows the default label.'; const TIMELINE_MIN_DATE_DESCRIPTION = 'Pin the gantt axis start (ISO `yyyy-mm-dd`) instead of deriving it from the rows; only a non-empty value is honoured. A value that is not a date, or a `minDate` after `maxDate`, refuses the chart with a diagnostic naming it.'; const TIMELINE_MAX_DATE_DESCRIPTION = 'Pin the gantt axis end (ISO `yyyy-mm-dd`) instead of deriving it from the rows; only a non-empty value is honoured. A value that is not a date, or a `maxDate` before `minDate`, refuses the chart with a diagnostic naming it.'; const TIMELINE_DESCRIPTION_FIELD_DESCRIPTION = 'Field rendered as each entry\'s description (renderer default `description`). It is declared flat because the `timeline` block has no member for it, and `mapping.description` outranks it when both are written.'; -const TIMELINE_MAPPING_DESCRIPTION = 'Record-to-entry field mapping ({ title, date, description, variant }) — the objectui-side binding record read BETWEEN the `timeline` block and the flat fallbacks. Its `variant` member (the field whose value picks each marker colour, renderer default `variant`) is the only spelling that binding has. On the vertical rail a colour that `timeline.colorField` resolves for an entry outranks that marker colour.'; +const TIMELINE_MAPPING_DESCRIPTION = 'Record-to-entry field mapping `{ title?, date?, description?, variant? }`, each a field name — the binding record read BETWEEN the `timeline` block and the flat fallbacks. Its `variant` member (the field whose value picks each marker colour, renderer default `variant`) is the only spelling that binding has. On the vertical rail a colour that `timeline.colorField` resolves for an entry outranks that marker colour.'; /** * The one input list both tags publish (`object-timeline` and `view:timeline`): diff --git a/packages/types/package.json b/packages/types/package.json index ea9735bd5d..5134df1eb1 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -93,7 +93,7 @@ "directory": "packages/types" }, "dependencies": { - "@objectstack/spec": "^17.6.0", + "@objectstack/spec": "^17.7.0", "zod": "^4.6.1" }, "devDependencies": { diff --git a/packages/types/src/__tests__/chartconfig-aria-retired-4044.test.ts b/packages/types/src/__tests__/chartconfig-aria-retired-4044.test.ts index 5d1b453ebb..41028feff3 100644 --- a/packages/types/src/__tests__/chartconfig-aria-retired-4044.test.ts +++ b/packages/types/src/__tests__/chartconfig-aria-retired-4044.test.ts @@ -30,7 +30,7 @@ * - **TypeScript.** `complex.ts` re-typed `chartConfig` as `any`, so an * authored `aria` compiled. The member now takes `aria` from the spec's own * input type, which is the spec's retired-key type (the branded `[REMOVED]` - * mark at the pinned 17.6.0 and on objectstack `main`, bare `undefined` + * mark at the pinned 17.7.0 and on objectstack `main`, bare `undefined` * through 17.5.0; both * spellings are `retired-key-type.ts`'s), so an authored value is a compile * error. diff --git a/packages/types/src/__tests__/dashboard-aria-retired-contract-twins.test.ts b/packages/types/src/__tests__/dashboard-aria-retired-contract-twins.test.ts index 53561d9f76..0ced39f3f1 100644 --- a/packages/types/src/__tests__/dashboard-aria-retired-contract-twins.test.ts +++ b/packages/types/src/__tests__/dashboard-aria-retired-contract-twins.test.ts @@ -29,7 +29,7 @@ * objectui#9736 closed that half. The interface now extends the spec's own * `Dashboard` input type (`Omit< Dashboard, … >` over the exclusion list its * mirror reads), so it inherits the spec's tombstone as a DECLARED member typed - * as the spec's retired key (the branded `[REMOVED]` mark at the pinned 17.6.0 + * as the spec's retired key (the branded `[REMOVED]` mark at the pinned 17.7.0 * and on objectstack `main`, bare `undefined` through 17.5.0) — `aria` is back in the declared key * set, but only as a refusal, * and an authored value is a compile error: the same verdict the Zod twin gives @@ -52,7 +52,7 @@ describe('the TS interface declares `aria` only as the spec tombstone (objectui# it('`aria` is declared, typed as the spec\'s retired key; the neighbours it stood beside still are', () => { // Type-level pins, erased at runtime. `aria` is a DECLARED key again — the // spec projection carries the tombstone — and its type admits no value: - // the branded `[REMOVED]` mark at the pinned 17.6.0 and on objectstack + // the branded `[REMOVED]` mark at the pinned 17.7.0 and on objectstack // `main`, bare `undefined` through 17.5.0 (both spellings are // `retired-key-type.ts`'s, objectui#11330). const ariaDeclared: 'aria' extends Declared ? true : false = true; diff --git a/packages/types/src/__tests__/dashboard-widget-metric-measure-door-8894.test.ts b/packages/types/src/__tests__/dashboard-widget-metric-measure-door-8894.test.ts index 0bcd31844a..9aab4e39f7 100644 --- a/packages/types/src/__tests__/dashboard-widget-metric-measure-door-8894.test.ts +++ b/packages/types/src/__tests__/dashboard-widget-metric-measure-door-8894.test.ts @@ -43,23 +43,33 @@ * dimension, the shape the spec's refusal points authors at), and the family * derivation itself is checked for being non-vacuous in both directions. * - * ## OWED TO objectui#11334 — a bounded ledger, not a skip + * ## The chart families: objectui#11334 and objectui#11417 * * FAMILY is read by effect, so a second spec rule at `values` joins it: the - * dimensionless measure-arity check (objectstack `11d28c17`, #21053) refuses two - * measures with no `dimensions` on more types than the metric family, and - * objectui's mirror does not re-attach it yet. Those types are booked in - * `OWED_TO_OBJECTUI_11334` below: their rows assert today's difference, and a cap - * row requires the set objectui fails to refuse to equal the ledger exactly. + * chart measure-arity check (`checkDashboardWidgetChartMeasureArity`, the + * dimensionless check of objectstack `11d28c17` under the name `32d57690` gave + * it, which `@objectstack/spec` 17.7.0 ships) refuses two measures with no + * `dimensions` on more types than the metric family. objectui#11717 chains it + * after the metric-family check, so every FAMILY type is refused on both faces + * with the spec's own issue. Until then those types were booked in an + * `OWED_TO_OBJECTUI_11334` ledger (objectui#11438 ruling A″, record 5968177777); + * the ledger is struck, its rows now assert the refusal, and the cap rows + * require the set objectui fails to refuse to be EMPTY. + * + * The same check's single-series arm (objectui#11417) refuses two measures on + * five of those types WITH a dimension too. Both arms are read off the spec's + * exported check itself (`chartArmIssues`), never off a list written here. * * ⛔ Not pinned here, deliberately: the TypeScript face. `values` stays * `string[]` on the type, as it does on the spec's own inferred type — the * card rules out a TypeScript narrowing. */ import { describe, it, expect } from 'vitest'; +import type { z } from 'zod'; import { ChartTypeSchema as SpecChartTypeSchema, DashboardWidgetSchema as SpecDashboardWidgetSchema, + checkDashboardWidgetChartMeasureArity, } from '@objectstack/spec/ui'; import { DashboardWidgetSchema } from '../zod/complex.zod'; import { safeValidateSchema } from '../zod/index.zod'; @@ -98,40 +108,28 @@ const FAMILY = SPEC_TYPES.filter((t) => specArityIssue(widget(t, TWO)) !== undef const OTHERS = SPEC_TYPES.filter((t) => !FAMILY.includes(t)); /** - * ⚠️ OWED TO objectui#11334 — the FAMILY types whose refusal objectui's doors do - * NOT mirror yet, booked rather than skipped. - * - * The spec this repository resolves refuses two or more measures on these types - * when the widget declares no `dimensions` - * (`checkDashboardWidgetDimensionlessMeasureArity`, objectstack `11d28c17`, - * #21053). objectui's mirror does not re-attach that check: objectstack `main` - * renamed its export (`32d57690`), and a static import of the name the resolved - * spec ships fails the `Spec Main Shape Gate`. objectui#11334 owns the mirror. - * - * Booked by objectui#11438 ruling A″ (record 5968177777), which applies - * objectui#11111 decision 3 = B (record 5902351047) to the bump. Each listed - * type's rows assert TODAY's difference (the spec refuses; objectui's door - * accepts), and the cap rows require the FAMILY types objectui fails to refuse - * to EQUAL this list on both faces. A new difference is red, and so is a listed - * type objectui starts refusing: the entries go stale, by name, when the mirror - * attaches, and objectui#11334 strikes them in that change. - * - * EXPIRES when objectui resolves an `@objectstack/spec` carrying `32d57690`, or - * 2026-11-02, whichever is first. That resolution is caught by name in - * `spec-object-refinements-7715.test.ts`, whose census books the same check; - * the date is read by objectui#11334, not by a clock. + * The issues the spec's chart measure-arity check raises on a widget, read by + * calling the exported check itself with a collecting context. The metric + * family is the sibling check's, so this answers for the chart arms alone. */ -const OWED_TO_OBJECTUI_11334 = ['pie', 'donut', 'funnel', 'scatter', 'treemap', 'sankey', 'radar']; - -/** The reason every owed row prints when it fails. */ -const OWED_REASON = - 'OWED TO objectui#11334: the dimensionless measure-arity check is not re-attached by the mirror yet. ' + - 'Booked by objectui#11438 ruling A″ (record 5968177777), applying objectui#11111 decision 3 = B ' + - '(record 5902351047). Expires when objectui resolves an @objectstack/spec carrying 32d57690, or ' + - '2026-11-02, whichever is first.'; +const chartArmIssues = (doc: Record): unknown[] => { + const issues: unknown[] = []; + checkDashboardWidgetChartMeasureArity(doc, { addIssue: (issue: unknown) => issues.push(issue) } as unknown as z.RefinementCtx); + return issues; +}; -/** The FAMILY types whose refusal objectui's doors mirror today: FAMILY minus the ledger. */ -const MIRRORED = FAMILY.filter((t) => !OWED_TO_OBJECTUI_11334.includes(t)); +/** + * The FAMILY types the chart check refuses with no dimension — its + * dimensionless arm (objectui#11334). These are the types the struck + * `OWED_TO_OBJECTUI_11334` ledger held. + */ +const DIMENSIONLESS_ARM = FAMILY.filter((t) => chartArmIssues(widget(t, TWO)).length === 1); +/** The metric family proper: FAMILY minus the chart check's dimensionless arm. */ +const METRIC = FAMILY.filter((t) => !DIMENSIONLESS_ARM.includes(t)); +/** The types the chart check refuses two measures on WITH a dimension — its single-series arm (objectui#11417). */ +const SINGLE_SERIES_ARM = SPEC_TYPES.filter((t) => chartArmIssues(widget(t, TWO, { dimensions: ['stage'] })).length === 1); +/** The dimensionless-arm types a dimension rescues: the single-series arm's complement within it. */ +const RESCUED_BY_A_DIMENSION = DIMENSIONLESS_ARM.filter((t) => !SINGLE_SERIES_ARM.includes(t)); /** Does an objectui verdict carry the spec's own arity issue for `doc` at `path`? */ const refusesAsSpec = ( @@ -164,7 +162,7 @@ describe('objectui#8894 — the family is read off the spec\'s own rule, and it }); describe('objectui#8894 — face 1: `DashboardWidgetSchema` refuses a second measure on the metric family', () => { - it.each(MIRRORED)('`%s` with two measures is refused at `values`, with the spec\'s own issue', (type) => { + it.each(FAMILY)('`%s` with two measures is refused at `values`, with the spec\'s own issue', (type) => { const doc = widget(type, TWO); const specIssue = specArityIssue(doc)!; const r = DashboardWidgetSchema.safeParse(doc); @@ -203,7 +201,7 @@ describe('objectui#8894 — face 1: `DashboardWidgetSchema` refuses a second mea }); describe('objectui#8894 — face 2: the published door (`safeValidateSchema`) refuses it inside a dashboard', () => { - it.each(MIRRORED)('`%s` with two measures is refused at `widgets.0.values`, with the spec\'s own issue', (type) => { + it.each(FAMILY)('`%s` with two measures is refused at `widgets.0.values`, with the spec\'s own issue', (type) => { const w = widget(type, TWO); const specIssue = specArityIssue(w)!; const r = safeValidateSchema(dashboardNode(w)); @@ -220,38 +218,113 @@ describe('objectui#8894 — face 2: the published door (`safeValidateSchema`) re }); }); -describe('objectui#11334 — OWED: the spec refuses these widgets and objectui\'s doors accept them today', () => { - it.each(OWED_TO_OBJECTUI_11334)('OWED TO objectui#11334: `%s` with two measures and no dimension: the spec refuses at `values`, `DashboardWidgetSchema` accepts', (type) => { +describe('objectui#11334 — the dimensionless arm: the spec refuses these widgets and objectui\'s doors refuse them with its issue', () => { + // Formerly the OWED TO objectui#11334 rows, which asserted the difference (the + // spec refuses, objectui accepts). The mirror attaches the check, so each row + // now asserts the refusal itself, on both faces. + it('the arm is read off the spec\'s check and is not vacuous: it holds `pie`, and the metric family is outside it', () => { + expect(DIMENSIONLESS_ARM).toContain('pie'); + expect(METRIC.length).toBeGreaterThan(0); + for (const t of METRIC) expect(chartArmIssues(widget(t, TWO)), t).toEqual([]); + }); + + it.each(DIMENSIONLESS_ARM)('`%s` with two measures and no dimension: the spec refuses at `values`, `DashboardWidgetSchema` refuses with its issue', (type) => { const doc = widget(type, TWO); - expect(specArityIssue(doc), OWED_REASON).toBeDefined(); - expect(DashboardWidgetSchema.safeParse(doc).success, OWED_REASON).toBe(true); + const specIssue = specArityIssue(doc); + expect(specIssue).toBeDefined(); + const r = DashboardWidgetSchema.safeParse(doc); + expect(r.success).toBe(false); + expect(envelope(r.error!.issues as Issue[])).toContainEqual({ code: 'custom', path: 'values', message: specIssue!.message }); }); - it.each(OWED_TO_OBJECTUI_11334)('OWED TO objectui#11334: `%s` with two measures and no dimension: the published door accepts it inside a dashboard', (type) => { + it.each(DIMENSIONLESS_ARM)('`%s` with two measures and no dimension: the published door refuses it inside a dashboard with the spec\'s issue', (type) => { const w = widget(type, TWO); - expect(specArityIssue(w), OWED_REASON).toBeDefined(); - expect(safeValidateSchema(dashboardNode(w)).success, OWED_REASON).toBe(true); + const specIssue = specArityIssue(w); + expect(specIssue).toBeDefined(); + const r = safeValidateSchema(dashboardNode(w)); + expect(r.success).toBe(false); + expect(envelope(r.error!.issues as Issue[])).toContainEqual({ code: 'custom', path: 'widgets.0.values', message: specIssue!.message }); + }); + + it('SUBJECT (the card\'s pin): a dimensionless two-measure `pie` is ONE issue at `values`, the spec\'s own, on both faces', () => { + const doc = widget('pie', TWO); + const specIssue = specArityIssue(doc)!; + expect(specIssue.message).toContain('with no `dimensions`'); + const mirror = DashboardWidgetSchema.safeParse(doc); + expect(mirror.success).toBe(false); + expect(envelope(mirror.error!.issues as Issue[]).filter((i) => i.path === 'values')).toEqual([ + { code: 'custom', path: 'values', message: specIssue.message }, + ]); + const published = safeValidateSchema(dashboardNode(doc)); + expect(published.success).toBe(false); + expect(envelope(published.error!.issues as Issue[])).toContainEqual({ code: 'custom', path: 'widgets.0.values', message: specIssue.message }); + }); + + it('CONTROL: one dimension takes the same `pie` out of the dimensionless arm — what refuses it then is the single-series arm, and a `radar` it rescues parses', () => { + // The card asked for the one-dimension `pie` to parse as this control. 17.7.0 + // refuses it anyway, by the single-series arm (objectui#11417, pinned below), so + // the control is stated per arm: the dimensionless refusal is gone, and a type + // the single-series arm does not hold parses whole with the same dimension. + const pie = widget('pie', TWO, { dimensions: ['stage'] }); + const pieIssue = specArityIssue(pie)!; + expect(pieIssue.message).not.toContain('with no `dimensions`'); + expect(envelope(DashboardWidgetSchema.safeParse(pie).error!.issues as Issue[]).map((i) => i.message)).not.toContain(specArityIssue(widget('pie', TWO))!.message); + expect(RESCUED_BY_A_DIMENSION).toContain('radar'); + const radar = widget('radar', TWO, { dimensions: ['stage'] }); + expect(SpecDashboardWidgetSchema.safeParse(radar).success).toBe(true); + expect(DashboardWidgetSchema.safeParse(radar).success).toBe(true); + expect(safeValidateSchema(dashboardNode(radar)).success).toBe(true); + }); +}); + +describe('objectui#11417 — the single-series arm: a dimension does not rescue a second measure on these types', () => { + it('the arm is read off the spec\'s check and is not vacuous: it holds `pie`, and every member is in the dimensionless arm too', () => { + expect(SINGLE_SERIES_ARM).toContain('pie'); + expect(RESCUED_BY_A_DIMENSION.length).toBeGreaterThan(0); + for (const t of SINGLE_SERIES_ARM) expect(DIMENSIONLESS_ARM, t).toContain(t); + }); + + it.each(SINGLE_SERIES_ARM)('`%s` with one dimension and two measures is refused at `values` on both faces, with the spec\'s own issue', (type) => { + const doc = widget(type, TWO, { dimensions: ['stage'] }); + const specIssue = specArityIssue(doc); + expect(specIssue).toBeDefined(); + const mirror = DashboardWidgetSchema.safeParse(doc); + expect(mirror.success).toBe(false); + expect(envelope(mirror.error!.issues as Issue[])).toContainEqual({ code: 'custom', path: 'values', message: specIssue!.message }); + const published = safeValidateSchema(dashboardNode(doc)); + expect(published.success).toBe(false); + expect(envelope(published.error!.issues as Issue[])).toContainEqual({ code: 'custom', path: 'widgets.0.values', message: specIssue!.message }); + }); + + it.each(SINGLE_SERIES_ARM)('CONTROL: `%s` with one dimension and ONE measure parses on both faces', (type) => { + const doc = widget(type, ONE, { dimensions: ['stage'] }); + expect(SpecDashboardWidgetSchema.safeParse(doc).success).toBe(true); + expect(DashboardWidgetSchema.safeParse(doc).success).toBe(true); + expect(safeValidateSchema(dashboardNode(doc)).success).toBe(true); + }); + + it.each(RESCUED_BY_A_DIMENSION)('CONTROL: `%s` with one dimension and two measures parses on both faces — the arm names five types, not the family', (type) => { + const doc = widget(type, TWO, { dimensions: ['stage'] }); + expect(SpecDashboardWidgetSchema.safeParse(doc).success).toBe(true); + expect(DashboardWidgetSchema.safeParse(doc).success).toBe(true); + expect(safeValidateSchema(dashboardNode(doc)).success).toBe(true); }); }); -describe('objectui#11334 — the cap: objectui fails to refuse EXACTLY the ledgered FAMILY types', () => { - it('face 1: the FAMILY types `DashboardWidgetSchema` does not refuse with the spec\'s own issue equal the ledger', () => { +describe('objectui#11334 — the cap: objectui fails to refuse NO FAMILY type (the struck ledger stays empty)', () => { + it('face 1: no FAMILY type goes unrefused by `DashboardWidgetSchema` with the spec\'s own issue', () => { const unrefused = FAMILY.filter((t) => { const doc = widget(t, TWO); return !refusesAsSpec(DashboardWidgetSchema.safeParse(doc), doc, 'values'); }); - expect([...unrefused].sort(), OWED_REASON).toEqual([...OWED_TO_OBJECTUI_11334].sort()); + expect(unrefused).toEqual([]); }); - it('face 2: the FAMILY types the published door does not refuse with the spec\'s own issue equal the ledger', () => { + it('face 2: no FAMILY type goes unrefused by the published door with the spec\'s own issue', () => { const unrefused = FAMILY.filter((t) => { const w = widget(t, TWO); return !refusesAsSpec(safeValidateSchema(dashboardNode(w)), w, 'widgets.0.values'); }); - expect([...unrefused].sort(), OWED_REASON).toEqual([...OWED_TO_OBJECTUI_11334].sort()); - }); - - it('the ledger leaves the metric family judged: the mirrored rows are not vacuous', () => { - expect(MIRRORED.length).toBeGreaterThan(0); + expect(unrefused).toEqual([]); }); }); diff --git a/packages/types/src/__tests__/imported-defaults-8317.test.ts b/packages/types/src/__tests__/imported-defaults-8317.test.ts index aff774ee18..4a9261a4b2 100644 --- a/packages/types/src/__tests__/imported-defaults-8317.test.ts +++ b/packages/types/src/__tests__/imported-defaults-8317.test.ts @@ -136,6 +136,8 @@ import { checkPageSourceCompleteness, checkDashboardWidgetStageOrder, checkDashboardWidgetMetricMeasureArity, + checkDashboardWidgetChartMeasureArity, + checkPageRequiresKind, } from '@objectstack/spec/ui'; import { FieldSchema as SpecFieldSchema, @@ -457,15 +459,17 @@ describe('the import boundary strips every imported default (objectui#8317)', () it('the walker docblock\'s `lazy` count is re-derived, not quoted', () => { // The `lazy` arm is the one place the identity property cannot hold: it // must rebuild without forcing the getter, so a clean subtree behind a - // `z.lazy` is rebuilt anyway. The module's docblock names FIVE such + // `z.lazy` is rebuilt anyway. The module's docblock names SIX such // nodes (the fourth, `ViewSchema`'s form-field group, since objectui#7928; - // the fifth, under `FieldSchema.relatedListFilter`, since objectui#11070) + // the fifth, under `FieldSchema.relatedListFilter`, since objectui#11070; + // the sixth, the form-field group under the `object-form` row's + // `sections[].fields[]`, since the 17.7.0 bump, objectui#11717) // and says the exception costs nothing extra for every owner but the // ones `REBUILT_CLEAN` names (objectui#10872 batch 2), because each // other owner sits inside a schema that is being rebuilt regardless. // Both halves are measured here, so a spec bump or an import that moves // either one is red rather than quietly making the docblock false. - expect(walk(IMPORTED.map(([, s]) => s)).lazies).toBe(5); + expect(walk(IMPORTED.map(([, s]) => s)).lazies).toBe(6); const lazyOwners = IMPORTED.filter(([, s]) => walk([s]).lazies > 0); expect(lazyOwners.length, 'no schema owns a lazy — the count above found them elsewhere').toBeGreaterThan(0); // The clean owners are exactly the named set: rebuilt (so NOT the spec's @@ -671,6 +675,12 @@ describe('the import boundary strips every imported default (objectui#8317)', () // re-attached by objectui's `DashboardWidgetSchema` under the same ruling. ['checkDashboardWidgetStageOrder', checkDashboardWidgetStageOrder], ['checkDashboardWidgetMetricMeasureArity', checkDashboardWidgetMetricMeasureArity], + // objectui#11717: the two `@objectstack/spec` 17.7.0 checks the mirrors re-attach + // under the same ruling — the chart measure-arity check on `DashboardWidgetSchema` + // (objectui#11334 / objectui#11417) and the page `requires` ⇄ `kind` check on + // `PageNodeSchema` and `PageKindNodeSchema`. + ['checkDashboardWidgetChartMeasureArity', checkDashboardWidgetChartMeasureArity], + ['checkPageRequiresKind', checkPageRequiresKind], ]); const isSpecModule = (m: string): boolean => diff --git a/packages/types/src/__tests__/object-form-properties-bag-10859-b4.test.ts b/packages/types/src/__tests__/object-form-properties-bag-10859-b4.test.ts index 27f9c991fc..cd8124da11 100644 --- a/packages/types/src/__tests__/object-form-properties-bag-10859-b4.test.ts +++ b/packages/types/src/__tests__/object-form-properties-bag-10859-b4.test.ts @@ -269,11 +269,17 @@ describe('the bag is judged by the spec row (objectui#10859 batch 4)', () => { const keysOf = (schema: unknown) => Object.keys((schema as { shape: Record }).shape).sort(); // The same members, read off the installed spec on every run. expect(keysOf(bag)).toEqual(keysOf(SpecObjectFormPropsSchema)); - // The row carries no spec default, so the import boundary hands the spec's - // own export back untouched (objectui#8317: a no-op on a clean subtree); - // the bag is what that export resolves to — the spec exports each row - // behind a lazy facade — and it answers every probe exactly as the spec does. - expect(stripImportedDefaults(SpecObjectFormPropsSchema)).toBe(SpecObjectFormPropsSchema); + // Since `@objectstack/spec` 17.7.0 the row reaches a spec default — its + // `sections[].fields[]` take the page-block section shape, which carries + // one — so the import boundary hands back its stripped derivation, not the + // spec's own export (objectui#8317; re-measured at the bump, objectui#11717). + // Through 17.6.0 the row carried none, and this line asserted the spec's + // export came back untouched. The bag IS that derivation, memoised to one + // crossing, and it answers every probe exactly as the spec does. + const crossed = stripImportedDefaults(SpecObjectFormPropsSchema); + expect(crossed).not.toBe(SpecObjectFormPropsSchema); + expect(stripImportedDefaults(SpecObjectFormPropsSchema)).toBe(crossed); + expect(bag).toBe(crossed); for (const probe of [ {}, { objectName: 'order', mode: 'create' }, diff --git a/packages/types/src/__tests__/object-form-unmirrored-members-6152.test.ts b/packages/types/src/__tests__/object-form-unmirrored-members-6152.test.ts index e0aa8677ef..1771266706 100644 --- a/packages/types/src/__tests__/object-form-unmirrored-members-6152.test.ts +++ b/packages/types/src/__tests__/object-form-unmirrored-members-6152.test.ts @@ -79,9 +79,19 @@ const ROWS: ReadonlyArray<{ key: string; valid: unknown; wrong: unknown }> = [ { key: 'formType', valid: 'wizard', wrong: 'carousel' }, { key: 'sections', + // `visibleWhen` rides the STANDALONE section: since `@objectstack/spec` 17.7.0 + // a group-referencing section takes its presentation from the object's + // `fieldGroups` entry and refuses `visibleWhen` beside `group` + // (objectui#11717 re-judged this fixture; it wrote the pair together). valid: [ - { name: 'basics', label: 'Basics', columns: 2, fields: ['name', { name: 'email', type: 'email' }] }, - { group: 'contact_info', pane: 'secondary', visibleWhen: { dialect: 'cel', source: 'record.kind == "b2b"' } }, + { + name: 'basics', + label: 'Basics', + columns: 2, + fields: ['name', { name: 'email', type: 'email' }], + visibleWhen: { dialect: 'cel', source: 'record.kind == "b2b"' }, + }, + { group: 'contact_info', pane: 'secondary' }, ], wrong: 'basics', }, diff --git a/packages/types/src/__tests__/object-master-detail-form-details-entry-11396.test.ts b/packages/types/src/__tests__/object-master-detail-form-details-entry-11396.test.ts index 9758ac91c5..e1d1cc4898 100644 --- a/packages/types/src/__tests__/object-master-detail-form-details-entry-11396.test.ts +++ b/packages/types/src/__tests__/object-master-detail-form-details-entry-11396.test.ts @@ -36,23 +36,22 @@ * own props row gives it, read live in the same run, at the same path under * `properties`. * - * ## The fork, pinned as ACCEPTED so that closing it is a deliberate change + * ## The fork, closed: `sortField` is refused with the spec's retired-key message * - * At 17.6.0 the spec entry declares `sortField`; `MasterDetailForm` reads no - * such member (objectui#11070 round 9 retired the authored override, and + * Through 17.6.0 the spec entry declared `sortField` while `MasterDetailForm` + * read no such member (objectui#11070 round 9 retired the authored override, and * `plugin-form`'s `masterDetailDetailsMembers-8071.test.tsx` row 2c pins that a - * written one reaches nothing). So `objectui validate` ACCEPTS an entry carrying - * `sortField` while the renderer ignores it. objectstack has since retired the - * key on its `main` with a `retiredKey()` tombstone on the entry + * written one reaches nothing), so `objectui validate` ACCEPTED an entry carrying + * it while the renderer ignored it — and this row pinned that acceptance as the + * forward tripwire. `@objectstack/spec` 17.7.0 carries the retirement * (objectstack-ai/objectstack#21589, PR objectstack-ai/objectstack#21632, - * `6ec54f00`), unreleased after 17.6.0. The row below is the forward tripwire: - * it records the 17.6.0 verdict, and reds at objectui's bump to the first - * `@objectstack/spec` release that carries the retirement — the cue to flip it - * to a refusal and drop the `Omit` on `MasterDetailDetailConfig`. It is a - * RUNTIME row on purpose: the `Spec Main Shape Gate` compiles this repository - * against objectstack `main`, so a compile-time row pinning the 17.6.0 shape - * would be red there today. - */ + * `6ec54f00`): a `retiredKey()` tombstone on the entry. So the row is flipped at + * objectui's bump to that release (objectui#11717): both published faces refuse + * an entry carrying `sortField` at that member, with the spec's own message. + * `MasterDetailDetailConfig` keeps its `Omit` of the key: the spec entry still + * LISTS it (as a tombstone), and the TypeScript face leaves it off rather than + * offer a member whose only legal value is absence. + */ import { describe, it, expect } from 'vitest'; import { @@ -84,7 +83,7 @@ export type detailsEntryIsTheSpecs = [ /* ── Runtime ──────────────────────────────────────────────────────────────── */ -type Issue = { code: string; path: readonly PropertyKey[]; keys?: readonly string[] }; +type Issue = { code: string; path: readonly PropertyKey[]; keys?: readonly string[]; message?: string }; /** A node authored in the spec's `{ type, properties }` form, with ONE detail entry. */ const nodeWith = (entry: unknown) => ({ @@ -112,7 +111,7 @@ const PROBES = [ { name: 'no `childObject`', entry: { title: 'Lines' } }, { name: 'a bare field-name column', entry: { childObject: 'po_line', columns: ['qty'] } }, { name: 'a column with an undeclared key', entry: { childObject: 'po_line', columns: [{ name: 'qty', bogusKey: 1 }] } }, - { name: 'the fork: `sortField`, declared by the spec and read by nothing', entry: { childObject: 'po_line', sortField: 'line_no' } }, + { name: 'the retired `sortField` (a tombstone since 17.7.0)', entry: { childObject: 'po_line', sortField: 'line_no' } }, ] as const; describe('an `object-master-detail-form` `details` entry is judged by the spec\'s closed entry (objectui#11396)', () => { @@ -154,12 +153,20 @@ describe('an `object-master-detail-form` `details` entry is judged by the spec\' ]); }); - it('ACCEPTS `sortField` on 17.6.0 — the forward tripwire for objectstack#21589, which retired the key on main after 17.6.0', () => { - // When this reds, the installed spec carries the retirement: flip this - // row to a refusal and drop the `Omit` on `MasterDetailDetailConfig` in - // `@object-ui/plugin-form`. - const result = parse(nodeWith({ childObject: 'po_line', sortField: 'line_no' })); - expect(result.success, JSON.stringify(result.error?.issues)).toBe(true); + it('REFUSES `sortField` at that member with the spec\'s retired-key message — objectstack#21589, released in 17.7.0', () => { + // Flipped at the bump (objectui#11717): this row asserted the 17.6.0 + // acceptance as the forward tripwire for the retirement. + const entry = { childObject: 'po_line', sortField: 'line_no' }; + const result = parse(nodeWith(entry)); + expect(result.success).toBe(false); + const spec = SpecObjectMasterDetailFormPropsSchema.safeParse(nodeWith(entry).properties); + expect(spec.success).toBe(false); + const specIssue = spec.error!.issues.find((issue) => issue.path.join('.') === 'details.0.sortField')!; + expect(specIssue.message).toContain('`details[].sortField` was removed'); + const atMember = issuesUnder(result, ['properties', 'details', 0, 'sortField']); + expect(atMember.map((issue) => [issue.code, issue.path, issue.message])).toEqual([ + [specIssue.code, [], specIssue.message], + ]); }); }); diff --git a/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts b/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts index 7f39466c1d..c38f993904 100644 --- a/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts +++ b/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts @@ -337,7 +337,7 @@ const NAMED_VIEW_ABSENT_CONTROL = 'stickyHeader'; /** * The protocol's own member set for a named list view — `ObjectListViewSchema`'s - * shape, off `@objectstack/spec` as installed (17.6.0). ⛔ Not a copy of the + * shape, off `@objectstack/spec` as installed (17.7.0). ⛔ Not a copy of the * objectui face and ⛔ not a hand list: it is read off the schema object, so a * spec bump that moves a key moves this. */ diff --git a/packages/types/src/__tests__/record-details-top-level-9040.test.ts b/packages/types/src/__tests__/record-details-top-level-9040.test.ts index 222bdf1a85..2b283e86a5 100644 --- a/packages/types/src/__tests__/record-details-top-level-9040.test.ts +++ b/packages/types/src/__tests__/record-details-top-level-9040.test.ts @@ -174,7 +174,7 @@ type _KeyofStillResolvesOnThisInterface = Expect< /** * The contract's face, untouched by this removal: the tombstone accepts nothing. * Its type is the spec's retired-key type — the branded `[REMOVED]` mark at the - * pinned 17.6.0 and on objectstack `main`, + * pinned 17.7.0 and on objectstack `main`, * bare `undefined` through 17.5.0 — and the helper accepts * exactly those two spellings (objectui#11330). */ diff --git a/packages/types/src/__tests__/registered-type-arms-10859-b2.test.ts b/packages/types/src/__tests__/registered-type-arms-10859-b2.test.ts index 5ad98bc0dc..57fa9eb69d 100644 --- a/packages/types/src/__tests__/registered-type-arms-10859-b2.test.ts +++ b/packages/types/src/__tests__/registered-type-arms-10859-b2.test.ts @@ -337,11 +337,19 @@ describe('the spec-row arms read the row by reference (objectui#10859)', () => { // transcribed list that could drift from it. expect(keysOf(bag)).toEqual(keysOf(row)); expect(keysOf(bag).length).toBeGreaterThan(5); - // Neither row carries a spec default, so the import boundary hands the - // spec's own export back untouched; the bag is what that export resolves - // to (the spec exports each row behind a lazy facade), and it answers - // every probe exactly as the spec does. - expect(stripImportedDefaults(row)).toBe(row); + // Since `@objectstack/spec` 17.7.0 each row reaches a spec default (the + // `object-metric` row through `drillDown.report`, the spec's report + // definition; the `object-master-detail-form` row through the page-block + // section shape its `sections[].fields[]` take), so the import boundary + // hands back its stripped derivation, not the spec's own export + // (objectui#8317; re-measured at the bump, objectui#11717). Through 17.6.0 + // neither row carried one, and this line asserted the spec's export came + // back untouched. The bag IS that derivation, memoised to one crossing, and + // it answers every probe exactly as the spec does. + const crossed = stripImportedDefaults(row); + expect(crossed).not.toBe(row); + expect(stripImportedDefaults(row)).toBe(crossed); + expect(bag).toBe(crossed); for (const probe of [{}, { objectName: 'order' }, { inventedKey10859: 1 }, { objectName: 7 }]) { expect(bag.safeParse(probe).success, JSON.stringify(probe)).toBe(row.safeParse(probe).success); } diff --git a/packages/types/src/__tests__/select-option-spec-extension-7014.test.ts b/packages/types/src/__tests__/select-option-spec-extension-7014.test.ts index d18c4dcbc5..466b771db8 100644 --- a/packages/types/src/__tests__/select-option-spec-extension-7014.test.ts +++ b/packages/types/src/__tests__/select-option-spec-extension-7014.test.ts @@ -182,7 +182,16 @@ describe('FieldSchema declares `rows` on the four multiline editor types', () => }); describe('`rows` is TYPE-GATED — declared is not the same as declared everywhere', () => { - const base = (type: string) => ({ name: 'body', type, label: 'Body' }); + // A `select` carries one option: since `@objectstack/spec` 17.7.0 a `select` + // with no option source is refused at `options` (objectstack#21390), and this + // block's control is about `rows` alone, so the document is otherwise legal + // (objectui#11717). + const base = (type: string) => ({ + name: 'body', + type, + label: 'Body', + ...(type === 'select' ? { options: [{ label: 'Open', value: 'open' }] } : {}), + }); for (const type of ['text', 'select'] as const) { it(`control: \`${type}\` without \`rows\` is accepted`, () => { diff --git a/packages/types/src/__tests__/spec-object-refinements-7715.test.ts b/packages/types/src/__tests__/spec-object-refinements-7715.test.ts index 57d18b3205..2fa9070a35 100644 --- a/packages/types/src/__tests__/spec-object-refinements-7715.test.ts +++ b/packages/types/src/__tests__/spec-object-refinements-7715.test.ts @@ -114,6 +114,33 @@ describe('objectui#7715 — the Page source-completeness refusal reaches objectu }); }); +describe('objectui#11717 — the Page `requires` ⇄ `kind` refusal reaches objectui', () => { + it.each([ + ['an absent `kind` (the spec default, `full`)', { requires: ['ui'] }], + ['a `react` page', { kind: 'react', source: 'export default () => null', requires: ['ui'] }], + ['a `slotted` page, with an EMPTY list', { kind: 'slotted', requires: [] }], + ])('%s: the spec refuses at `requires`, and objectui refuses with the spec\'s own issue', (_label, extra) => { + const spec = SpecPageSchema.safeParse(specPage(extra)); + expect(spec.success).toBe(false); + const specIssue = spec.error!.issues.find((i) => i.path.join('.') === 'requires'); + expect(specIssue?.code).toBe('custom'); + + for (const r of [PageNodeSchema.safeParse(pageNode(extra)), safeValidateSchema(pageNode(extra))]) { + expect(r.success).toBe(false); + const issues = r.error!.issues.map((i) => ({ code: i.code, path: i.path.join('.'), message: i.message })); + expect(issues).toContainEqual({ code: 'custom', path: 'requires', message: specIssue!.message }); + } + }); + + it('controls: `requires` on a compiled `html` page, and a page that writes no `requires`, pass on both doors', () => { + for (const extra of [{ kind: 'html', source: '
x
', requires: ['ui'] }, { kind: 'react', source: 'x' }]) { + expect(SpecPageSchema.safeParse(specPage(extra)).success).toBe(true); + expect(PageNodeSchema.safeParse(pageNode(extra)).success).toBe(true); + expect(safeValidateSchema(pageNode(extra)).success).toBe(true); + } + }); +}); + // ── The census: every object-level check the spec runs is accounted for ───── /** @@ -130,33 +157,32 @@ const SITES = [ { site: 'DashboardWidgetSchema (complex.zod.ts)', spec: SpecDashboardWidgetSchema, - // Both added to the spec object at 17.5.0 and attached by objectui#11073: each reads only - // `type`, `options.stageOrder` / `values` and `id`, the spec's own fields on this node. - attached: ['checkDashboardWidgetStageOrder', 'checkDashboardWidgetMetricMeasureArity'], - // OWED TO objectui#11334. A BOOKING, not a measured structural refusal like the - // `checkListViewPageMount` entry the `ListViewSchema` row once held: objectstack - // `11d28c17` (#21053) added this check to the spec object, and objectui's mirror - // does not re-attach it yet. A static import of it compiles against the spec this - // repository resolves and fails the `Spec Main Shape Gate`, because objectstack - // `main` renamed the export (`32d57690`, #21293 / #21425) and no longer exports - // this name; objectui#11531 measured that attach on the bump's trunk and reverted - // it (its row 3). Nothing in this file measures that reason, for the Shape Gate - // compiles against objectstack `main`, which no run of this file sees. - // Booked by objectui#11438 ruling A″ (record 5968177777), which applies - // objectui#11111 decision 3 = B (record 5902351047) to the bump; group 5's - // booking of objectui#11536 has the same shape. - // Expires when objectui resolves an `@objectstack/spec` carrying `32d57690`, or - // 2026-11-02, whichever is first. The condition is enforced here and the date - // is read by objectui#11334, not by a clock: that resolution drops this export - // name, so the census test that matches every exported `check*` name against - // these lists turns red by name, and objectui#11334 strikes this entry as it - // re-attaches the renamed check. Capped by this row's own count equality and - // that census's exact name match: no check beyond the entries listed here is - // admitted. - notAttachable: ['checkDashboardWidgetDimensionlessMeasureArity'], + // The first two were added to the spec object at 17.5.0 and attached by objectui#11073; + // the third is `checkDashboardWidgetChartMeasureArity` (the 17.6.0 dimensionless check, + // renamed and given its single-series arm by objectstack `32d57690` in 17.7.0), attached + // by objectui#11717 for objectui#11334 / objectui#11417. Each reads only `type`, + // `options.stageOrder` / `dimensions` / `values` and `id`, the spec's own fields on this + // node. + attached: [ + 'checkDashboardWidgetStageOrder', + 'checkDashboardWidgetMetricMeasureArity', + 'checkDashboardWidgetChartMeasureArity', + ], + // EMPTY since objectui#11717. It held `checkDashboardWidgetDimensionlessMeasureArity`, + // OWED TO objectui#11334 (booked by objectui#11438 ruling A″, record 5968177777), until + // objectui resolved an `@objectstack/spec` carrying `32d57690`: 17.7.0 exports the check + // under its new name only, and the mirror attaches it above. + notAttachable: [], }, { site: 'SpecDashboardFields → DashboardComponentSchema (complex.zod.ts)', spec: SpecDashboardSchema, attached: [], notAttachable: [] }, - { site: 'SpecPageFields → PageNodeSchema (layout.zod.ts)', spec: SpecPageSchema, attached: ['checkPageSourceCompleteness'], notAttachable: [] }, + { + site: 'SpecPageFields → PageNodeSchema (layout.zod.ts)', + spec: SpecPageSchema, + // `checkPageRequiresKind` joined the spec object at 17.7.0 (objectstack#21459) and was + // attached by objectui#11717: it reads `kind` and `requires`, both carried by reference. + attached: ['checkPageSourceCompleteness', 'checkPageRequiresKind'], + notAttachable: [], + }, { site: 'ListViewSchema (objectql.zod.ts)', spec: SpecListViewSchema, diff --git a/packages/types/src/__tests__/twins-spec-by-reference-9736.test.ts b/packages/types/src/__tests__/twins-spec-by-reference-9736.test.ts index af0d81fb26..65e55dcba7 100644 --- a/packages/types/src/__tests__/twins-spec-by-reference-9736.test.ts +++ b/packages/types/src/__tests__/twins-spec-by-reference-9736.test.ts @@ -160,7 +160,10 @@ describe('spec tombstones surface on the twin as a refusal — the verdict the m it('the Page control: the same document without `assignedProfiles`, and the admitted spec keys, pass both faces', () => { const bare: PageNodeSchema = { type: 'page' }; - const admitted: PageNodeSchema = { type: 'page', source: 'pages/home.tsx', requires: ['crm'] }; + // `kind: 'html'`: since `@objectstack/spec` 17.7.0 `requires` is admitted only on + // the kinds the platform compiles at save (`checkPageRequiresKind`, chained on + // this mirror by objectui#11717), and an absent `kind` is `full`. + const admitted: PageNodeSchema = { type: 'page', kind: 'html', source: 'pages/home.tsx', requires: ['crm'] }; expect(PageMirror.safeParse(bare).success).toBe(true); expect(PageMirror.safeParse(admitted).success).toBe(true); }); diff --git a/packages/types/src/field-types.ts b/packages/types/src/field-types.ts index 5e38ebd29b..30da5a5c9d 100644 --- a/packages/types/src/field-types.ts +++ b/packages/types/src/field-types.ts @@ -25,7 +25,7 @@ import type { Field as SpecField } from '@objectstack/spec/data'; * sibling field names, or `{ field, param }` entries mapping a sibling onto the * remote query parameter a dependent lookup filters by. * - * Measured on the installed `@objectstack/spec` (17.6.0), `FieldSchema` declares + * Measured on the installed `@objectstack/spec` (17.7.0), `FieldSchema` declares * `dependsOn` as an OPTIONAL ARRAY of `string | { field, param? }` — never a * bare string. That is deliberately narrower than `DependsOnInput` (`form.ts`), * the shape the widget prop `FieldWidgetComponentProps.dependsOn` and @@ -288,7 +288,7 @@ export interface MarkdownFieldMetadata extends BaseFieldMetadata { * annotated literal rejected. Follows the `TextareaFieldMetadata` precedent. * * A DECLARED spec key as of `@objectstack/spec` 17.3.0, which implements that - * same ruling. Measured on the installed `@objectstack/spec` 17.6.0: + * same ruling. Measured on the installed `@objectstack/spec` 17.7.0: * `FieldSchema` ACCEPTS `rows` on all four of textarea/markdown/html/ * richtext, as an integer of at least 1 — a non-integer answers * `invalid_type` and 0 answers `too_small`, so "declared" does not mean "any @@ -572,7 +572,7 @@ export interface SelectOptionMetadata extends SelectOptionBase { * * A DECLARED `SelectOptionSchema` key as of `@objectstack/spec` 17.3.0, * which implements the objectui#6153 half of that ruling. Measured on the - * installed `@objectstack/spec` 17.6.0: an option carrying `description` + * installed `@objectstack/spec` 17.7.0: an option carrying `description` * is ACCEPTED, as a string (a non-string answers `invalid_type`; the empty * string is valid), and a field whose `options` carry it parses whole. ⇒ it * may now be written into authored object metadata, which is the point of diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index c315f48a76..74ef991f26 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -4115,7 +4115,7 @@ export interface ObjectCalendarSchema extends BaseSchema { * (the spec's own `ObjectCalendarPropsSchema`) declares * `z.array(z.unknown()).optional()`, described *"Pre-fetched records — skips * the internal fetch"*. - * MEASURED on the installed artifact at `@objectstack/spec` 17.6.0 — the + * MEASURED on the installed artifact at `@objectstack/spec` 17.7.0 — the * version this repository's `pnpm-lock.yaml` resolves — through the published * `@objectstack/spec/ui` entry point: the provider block returns * `success=false` with `expected: 'array'` at `path: ['data']`, the array diff --git a/packages/types/src/record-components.ts b/packages/types/src/record-components.ts index 3e43567ed4..1619229998 100644 --- a/packages/types/src/record-components.ts +++ b/packages/types/src/record-components.ts @@ -107,7 +107,7 @@ export interface RecordDetailsComponentProps { * It was `number` here until objectui#8604, which is the wrong PRIMITIVE * TYPE, not merely a wider range: `{ columns: 2 }` compiled locally and the * contract refused it at publish with `invalid_value` at `columns` (measured - * on the installed pin, 17.6.0, against a control — `columns: '2'` — that + * on the installed pin, 17.7.0, against a control — `columns: '2'` — that * parses green on the same instrument). Contract-first (Commandment #0.1): * the code moves to the contract's spelling, and today's `columns: 2` * authors are the defect surfacing rather than collateral damage. @@ -334,7 +334,7 @@ export interface RecordHighlightsComponentProps { * `RecordHighlightsProps.fields[]`'s object arm declares exactly * `name`/`label`/`type`/`readonly` and carries a `never` catchall, i.e. it is * `$strict`: an unlisted key is REFUSED, not stripped, and the refusal takes - * the WHOLE document with it. Measured on the installed pin, 17.6.0, + * the WHOLE document with it. Measured on the installed pin, 17.7.0, * `RecordHighlightsProps.safeParse({ fields: [{ name: 'x', icon: 'star' }] })` * is RED with `invalid_union` at `fields.0`. So `{ name: 'amount', icon: * 'dollar-sign' }` type-checked here and was refused at the door — a green @@ -366,7 +366,7 @@ export interface RecordHighlightsComponentProps { * `z.enum(['horizontal','vertical'])` behind a `.default('horizontal')`). * * It offered a third value, `grid`, until objectui#9187, and the contract - * never accepted it: measured on the installed pin, 17.6.0, + * never accepted it: measured on the installed pin, 17.7.0, * `RecordHighlightsProps.safeParse({ fields: ['name'], layout: 'grid' })` is * RED with `invalid_value` at `layout`. So `{ layout: 'grid' }` type-checked * here and was refused at the door — a green local build and a rejection at diff --git a/packages/types/src/zod/complex.zod.ts b/packages/types/src/zod/complex.zod.ts index 41e1da2294..de64001451 100644 --- a/packages/types/src/zod/complex.zod.ts +++ b/packages/types/src/zod/complex.zod.ts @@ -20,6 +20,7 @@ import { z } from 'zod'; import { aliasKeyRefusal, handlerKeyRefusal, retiredNodeType, retirementTombstone } from './tombstone.zod.js'; import { ChartTypeSchema as SpecChartTypeSchema, + checkDashboardWidgetChartMeasureArity, checkDashboardWidgetMetricMeasureArity, checkDashboardWidgetStageOrder, DashboardSchema as SpecDashboardSchema, @@ -1510,18 +1511,29 @@ export const DashboardWidgetSchema = specFieldsExcept(stripImportedDefaults(Spec // ⭐ THE SPEC'S OBJECT-LEVEL CHECKS, re-attached (objectui#7715, ruling B1; objectui#11073). // // `specFieldsExcept` rebuilds a fresh object from the spec's `.shape`, so it drops - // every check the spec attached to the OBJECT. `@objectstack/spec` 17.5.0 attaches two - // to `DashboardWidgetSchema` and exports both by name, so the one the spec runs is - // attached here instead of restated. Both are ATTACHABLE: each reads only `type`, - // `options.stageOrder` / `values` and `id`, and on this node those are the spec's own - // fields (`type` adds objectui's closed `list` / `custom` extensions, which the spec's - // checks judge as the non-funnel, non-metric types they are; an absent `type` resolves - // to the spec's default inside the check, as on the spec). This is objectui#9111's - // criterion: a non-funnel widget carrying `options.stageOrder` is refused here as the - // server refuses it. `__tests__/spec-object-refinements-7715.test.ts` re-derives the - // split from the spec object's own check count. + // every check the spec attached to the OBJECT. The spec attaches three to + // `DashboardWidgetSchema` and exports each by name, so the one the spec runs is + // attached here instead of restated. All three are ATTACHABLE: each reads only `type`, + // `options.stageOrder` / `dimensions` / `values` and `id`, and on this node those are + // the spec's own fields (`type` adds objectui's closed `list` / `custom` extensions, + // which the spec's checks judge as the non-funnel, non-metric, undeclared types they + // are; an absent `type` resolves to the spec's default inside the check, as on the + // spec). This is objectui#9111's criterion: a non-funnel widget carrying + // `options.stageOrder` is refused here as the server refuses it. + // `__tests__/spec-object-refinements-7715.test.ts` re-derives the split from the spec + // object's own check count. + // + // The third, `checkDashboardWidgetChartMeasureArity` (objectui#11334, objectui#11417), + // chained after the metric-family check as the spec orders them, so a metric-family + // widget keeps its one issue. Two arms: with no `dimensions`, two or more measures only + // on a type in `DASHBOARD_WIDGET_MULTI_MEASURE_TYPES`; on `pie` / `donut` / `funnel` / + // `treemap` / `sankey`, one measure whatever the dimension. A type outside the spec's + // `ChartTypeSchema` (objectui's `list` / `custom`) is not this check's to judge, by the + // check's own guard. Pinned on both doors by + // `__tests__/dashboard-widget-metric-measure-door-8894.test.ts`. .superRefine(checkDashboardWidgetStageOrder) - .superRefine(checkDashboardWidgetMetricMeasureArity); + .superRefine(checkDashboardWidgetMetricMeasureArity) + .superRefine(checkDashboardWidgetChartMeasureArity); /** * Global Filter Schema — a dashboard-level filter definition: `@objectstack/spec/ui`'s diff --git a/packages/types/src/zod/imported-defaults.ts b/packages/types/src/zod/imported-defaults.ts index 0fa9694db1..73bea69544 100644 --- a/packages/types/src/zod/imported-defaults.ts +++ b/packages/types/src/zod/imported-defaults.ts @@ -137,7 +137,10 @@ const walk = (schema: z.ZodType): z.ZodType => { // container crosses the boundary, not only the slot. objectui#11070 added a // FIFTH by importing the spec's `FieldSchema` for nine `FormFieldSchema` // members: the one under `FieldSchema.relatedListFilter`, walked for the - // same reason. + // same reason. `@objectstack/spec` 17.7.0 added a SIXTH (objectui#11717): the + // recursive form-field group under `ObjectFormPropsSchema.sections[].fields[]`, + // reached from the `object-form` and `object-master-detail-form` rows, whose + // `sections` take the page-block section shape since that release. // Each sits inside a schema that carries a default anyway, so the exception // costs no extra rebuild. objectui#10872 batch 2 had ONE named exception, // `ElementDataSourceSchema` (the `element:number` arm's `dataSource`), whose diff --git a/packages/types/src/zod/layout.zod.ts b/packages/types/src/zod/layout.zod.ts index b126c32bcd..f8a995e685 100644 --- a/packages/types/src/zod/layout.zod.ts +++ b/packages/types/src/zod/layout.zod.ts @@ -22,6 +22,7 @@ import { PageSchema as SpecPageSchema, PageTypeSchema as SpecPageTypeSchema, PageVariableSchema as SpecPageVariableSchema, + checkPageRequiresKind, checkPageSourceCompleteness, } from '@objectstack/spec/ui'; import { BaseSchema, SchemaNodeSchema, specFieldsExcept } from './base.zod.js'; @@ -1324,16 +1325,22 @@ export const PageNodeSchema = BaseSchema.extend(SpecPageFields.shape).extend({ // shadowed that tombstone and accepted the key under a description that // called it access control, which nothing ever enforced. }) - // ⭐ THE SPEC'S OBJECT-LEVEL CHECK, re-attached (objectui#7715, ruling B1). + // ⭐ THE SPEC'S OBJECT-LEVEL CHECKS, re-attached (objectui#7715, ruling B1). // {@link SpecPageFields} rebuilds a fresh object from the spec's `.shape`, so - // it drops the one check the spec's `PageSchema` carries on the OBJECT: an - // `html` / `react` / `jsx` page with no non-empty `source` renders nothing and - // is refused at `source`. The spec exports that check (objectstack#16489) and - // it is attached here as-is: it reads `kind` and `source`, and this node - // carries both by reference — neither is in {@link PAGE_SPEC_EXCLUDED} nor - // overridden above. `__tests__/spec-object-refinements-7715.test.ts` re-derives - // the count, so a check the spec adds to `PageSchema` later reddens there. - .superRefine(checkPageSourceCompleteness); + // it drops the two checks the spec's `PageSchema` carries on the OBJECT: + // - an `html` / `react` / `jsx` page with no non-empty `source` renders + // nothing and is refused at `source` (`checkPageSourceCompleteness`, + // objectstack#16489); + // - `requires` is refused at `requires` on a page whose `kind` the platform + // does not compile at save — anything but `html` / `jsx`, an absent `kind` + // included (`checkPageRequiresKind`, objectstack#21459, the spec's 17.7.0). + // The spec exports both and both are attached here as-is: they read `kind`, + // `source` and `requires`, and this node carries all three by reference — + // none is in {@link PAGE_SPEC_EXCLUDED} nor overridden above. + // `__tests__/spec-object-refinements-7715.test.ts` re-derives the count, so a + // check the spec adds to `PageSchema` later reddens there. + .superRefine(checkPageSourceCompleteness) + .superRefine(checkPageRequiresKind); /** * The spec page KINDS a node may carry in `type` (objectui#11440): `record`, @@ -1381,8 +1388,8 @@ const { type: _pageNodeTypeLiteral, ...PAGE_NODE_MEMBERS } = PageNodeSchema.shap * member is {@link PageNodeSchema}'s own, by reference, and that node already * takes the spec's `PageSchema` fields by reference through * {@link SpecPageFields}. Its four refusals (`actions`, `breadcrumbs`, - * `maxWidth`, `padding`), the `body` refusal and the spec's object-level - * `source` check come with it. `regions` stays the node's own + * `maxWidth`, `padding`), the `body` refusal and the spec's two object-level + * checks (`source`, and `requires` by `kind`) come with it. `regions` stays the node's own * {@link PageNodeRegionSchema}, so each region component is judged by this * union at every depth, as on `page`. * @@ -1392,7 +1399,7 @@ const { type: _pageNodeTypeLiteral, ...PAGE_NODE_MEMBERS } = PageNodeSchema.shap export const PageKindNodeSchema: PageKindNodeSchemaType = BaseSchema.extend({ ...PAGE_NODE_MEMBERS, type: PAGE_KIND_NODE_TYPE.describe('The spec page kind — `record`, `home` or `utility` (`@objectstack/spec` `PageTypeSchema`)'), -}).superRefine(checkPageSourceCompleteness); +}).superRefine(checkPageSourceCompleteness).superRefine(checkPageRequiresKind); /** * The TYPE of {@link PageKindNodeSchema}, written out BY REFERENCE to the diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index fead84b19b..27aee11ead 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -1615,8 +1615,9 @@ const KanbanConfig = stripImportedDefaults(SpecKanbanConfigSchema).partial().ext * about the protocol; the corrected mechanism, re-derived by RUNNING * `@objectstack/spec` 17.4.0 (the version in the lockfile then) rather than * reading it — RE-RUN on 17.5.0 (objectui#11073), whose answer moved, as the - * last bullet says, and RE-RUN again on the installed 17.6.0 (objectui#11438), - * which answers every bullet below as 17.5.0 did: + * last bullet says, RE-RUN on 17.6.0 (objectui#11438), and again on the + * installed 17.7.0 (objectui#11717), each of which answers every bullet below as + * 17.5.0 did: * * - `CalendarConfigSchema`'s `strictObject` options carry `surface` and * `history` and NOTHING ELSE. There is no `aliases` entry, so upstream holds @@ -1639,8 +1640,8 @@ const KanbanConfig = stripImportedDefaults(SpecKanbanConfigSchema).partial().ext * not say which end of the range it binds … Write the one you mean") and * prescribes neither. `endField` still draws no hint, and a one-char typo * still resolves by distance (`titleFeld` → `titleField`) — all three - * measured on 17.5.0, and again on the - * installed 17.6.0 (objectui#11438). + * measured on 17.5.0, again on 17.6.0 (objectui#11438), and again on the + * installed 17.7.0 (objectui#11717). * * ⇒ through 17.4.0 a generic typo-distance suggester picked the wrong sibling. * It was not a declaration, it contradicted no declaration, and ⛔ no upstream @@ -1836,31 +1837,33 @@ const CalendarConfig = stripImportedDefaults(SpecCalendarConfigSchema).partial() * renderer drops — `plugin-calendar/src/index.tsx` names that as this * gate's own failure mode one layer in. * - * ⚠️ THE MEMBER LIST IS objectui's OWN, and the spec does NOT supply it. - * MEASURED on the installed `@objectstack/spec` 17.6.0 (the same answer - * 17.5.0 and 17.4.0 gave): - * `ComponentPropsMap['object-calendar'].calendar` is NOT `CalendarConfigSchema` - * — it is `z.unknown().optional()` (wrapper chain `["optional","unknown"]`, and - * not the same object reference), so at THIS position the protocol accepts - * everything: a nonsense key, a wrong-typed member, even `calendar: 42` all - * parse. `CalendarConfigSchema` is the strict object the spec uses for a LIST - * VIEW's calendar block, which is a different position — four keys through - * 17.4.0, five since 17.5.0 declared `allDayField` (objectui#11073). - * - * ⇒ what the protocol settles here is the KEY, not its SHAPE. The shape below - * is objectui's, chosen as exactly the five members `ObjectCalendar.tsx`'s - * events pass destructures out of the resolved config. Through 17.4.0 that was - * the spec's four plus objectui's own `allDayField`, the same objectui-local - * lane objectui#8466 took for the FLAT spelling of this vocabulary, on this same - * interface, for the same renderer; since 17.5.0 it is the list view block's - * five exactly. - * - * That makes this mirror STRICTER than the protocol at this position, which is - * the sanctioned direction and not the forbidden one: objectui#8327's triage - * ruling forbids accepting what the platform REFUSES, and under `BaseSchema`'s - * `.passthrough()` — which already admitted this key unexamined — a declaration - * can only narrow. The same asymmetry `filter` and `sort` already carry on this - * block. + * ⚠️ THE MEMBER LIST IS objectui's OWN. Through 17.6.0 the spec did NOT + * supply one: `ComponentPropsMap['object-calendar'].calendar` was + * `z.unknown().optional()` (measured on 17.4.0, 17.5.0 and 17.6.0), so at THIS + * position the protocol accepted everything — a nonsense key, a wrong-typed + * member, even `calendar: 42`. `@objectstack/spec` 17.7.0 types it + * (objectstack#21464 stage 2, `7d674dfc`). MEASURED on the installed 17.7.0: + * the slot is the list view's own `calendar` member (wrapper chain + * `["optional","object"]`, a strict copy of `CalendarConfigSchema`, not the + * same object reference), so a nonsense key, the alias `dateField`, + * `calendar: 42` and `calendar: { startDateField: 42 }` are all refused there + * now. `CalendarConfigSchema` is the strict object the spec uses for a LIST + * VIEW's calendar block — four keys through 17.4.0, five since 17.5.0 declared + * `allDayField` (objectui#11073). + * + * The shape below is still objectui's, chosen as exactly the five members + * `ObjectCalendar.tsx`'s events pass destructures out of the resolved config. + * Through 17.4.0 that was the spec's four plus objectui's own `allDayField`, the + * same objectui-local lane objectui#8466 took for the FLAT spelling of this + * vocabulary, on this same interface, for the same renderer; since 17.5.0 it is + * the list view block's five exactly. + * + * Through 17.6.0 that made this mirror STRICTER than the protocol at this + * position, the sanctioned direction (objectui#8327's triage ruling forbids + * accepting what the platform REFUSES). ⚠️ Since 17.7.0 it is not, for KEYS: + * the `.passthrough()` below admits an unexamined key the spec now refuses. + * The objectui#11717 bump records that difference by row and leaves the + * declaration as it is; it narrows nothing on its own. * * ⛔ `.passthrough()` is kept, so this declaration refuses no KEY that parses * today: a `calendar` block carrying `defaultView`, or any other unexamined diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ec4074459..100a923a20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,7 +35,7 @@ importers: version: link:packages/test-support '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@playwright/test': specifier: ^1.62.1 version: 1.62.1 @@ -273,13 +273,13 @@ importers: version: link:../../packages/types '@objectstack/client': specifier: ^17.3.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@objectstack/lint': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@tailwindcss/postcss': specifier: ^4.3.3 version: 4.3.3 @@ -417,7 +417,7 @@ importers: version: link:../../packages/types '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) fumadocs-core: specifier: 16.15.4 version: 16.15.4(@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.43.0(react@19.2.8))(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.2.0)(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.6.5) @@ -775,13 +775,13 @@ importers: version: link:../types '@objectstack/formula': specifier: ^17.5.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@objectstack/lint': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@objectstack/spec': specifier: ^17.6.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@sentry/react': specifier: ^10.70.0 version: 10.70.0(react@19.2.8) @@ -872,7 +872,7 @@ importers: dependencies: '@objectstack/spec': specifier: ^17.3.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) better-auth: specifier: ^1.7.2 version: 1.7.2(@opentelemetry/api@1.9.1)(better-sqlite3@12.9.0)(mongodb@7.2.0(socks@2.8.9))(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.2.0)(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) @@ -903,7 +903,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.1.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@tailwindcss/postcss': specifier: ^4.3.3 version: 4.3.3 @@ -971,7 +971,7 @@ importers: devDependencies: '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@types/react': specifier: 19.2.18 version: 19.2.18 @@ -1007,7 +1007,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@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) @@ -1191,10 +1191,10 @@ importers: version: link:../types '@objectstack/formula': specifier: ^17.5.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@objectstack/spec': specifier: ^17.6.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) devDependencies: '@object-ui/test-support': specifier: workspace:* @@ -1250,10 +1250,10 @@ importers: version: link:../types '@objectstack/client': specifier: ^17.3.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@objectstack/spec': specifier: ^17.5.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) devDependencies: '@object-ui/test-support': specifier: workspace:* @@ -1293,7 +1293,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.6.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) lucide-react: specifier: ^1.43.0 version: 1.43.0(react@19.2.8) @@ -1397,7 +1397,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) lucide-react: specifier: ^1.43.0 version: 1.43.0(react@19.2.8) @@ -1438,7 +1438,7 @@ importers: devDependencies: '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@types/react': specifier: 19.2.18 version: 19.2.18 @@ -1619,7 +1619,7 @@ importers: version: link:../test-support '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@types/react': specifier: 19.2.18 version: 19.2.18 @@ -1661,7 +1661,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@radix-ui/react-slot': specifier: ^1.3.3 version: 1.3.3(@types/react@19.2.18)(react@19.2.8) @@ -1751,8 +1751,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^17.5.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + specifier: ^17.6.0 + version: 17.7.0(ai@7.0.65(zod@4.6.5)) lucide-react: specifier: ^1.43.0 version: 1.43.0(react@19.2.8) @@ -1822,7 +1822,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.4.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -1874,7 +1874,7 @@ importers: version: link:../i18n '@objectstack/spec': specifier: ^17.5.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) lucide-react: specifier: ^1.43.0 version: 1.43.0(react@19.2.8) @@ -1999,7 +1999,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.6.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) lucide-react: specifier: ^1.43.0 version: 1.43.0(react@19.2.8) @@ -2060,7 +2060,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.5.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) lucide-react: specifier: ^1.43.0 version: 1.43.0(react@19.2.8) @@ -2130,7 +2130,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.5.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@tanstack/react-virtual': specifier: ^3.14.9 version: 3.14.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -2304,7 +2304,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@types/react': specifier: 19.2.18 version: 19.2.18 @@ -2481,7 +2481,7 @@ importers: version: link:../test-support '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@types/node': specifier: ^26.2.0 version: 26.2.0 @@ -2529,7 +2529,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -2599,7 +2599,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) lucide-react: specifier: ^1.43.0 version: 1.43.0(react@19.2.8) @@ -2672,7 +2672,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -2737,7 +2737,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.1.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) react: specifier: 19.2.8 version: 19.2.8 @@ -2802,7 +2802,7 @@ importers: devDependencies: '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@tailwindcss/postcss': specifier: ^4.3.3 version: 4.3.3 @@ -2844,13 +2844,13 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) packages/test-support: devDependencies: '@objectstack/spec': specifier: ^17.0.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + version: 17.7.0(ai@7.0.65(zod@4.6.5)) '@types/node': specifier: ^26.2.0 version: 26.2.0 @@ -2864,8 +2864,8 @@ importers: packages/types: dependencies: '@objectstack/spec': - specifier: ^17.6.0 - version: 17.6.0(ai@7.0.65(zod@4.6.5)) + specifier: ^17.7.0 + version: 17.7.0(ai@7.0.65(zod@4.6.5)) zod: specifier: ^4.6.1 version: 4.6.5 @@ -4226,26 +4226,26 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@objectstack/client@17.6.0': - resolution: {integrity: sha512-ZvhtjximG+nQNeC8v0Ed/FRdI5xcGQLJzxUtKlqvJJ6I3ueqBmOs8zHSoCa1vyMbctjBuzLRKxUeM9cUn9LTuA==} + '@objectstack/client@17.7.0': + resolution: {integrity: sha512-PFnr58tb1usCSMP+jNyuMBn9a6fEcaVFOiGRox9B88VQvttXo99akGzI8utFX8DsEobLQbh9YZBtXR51VI+JQg==} engines: {node: '>=22.0.0'} - '@objectstack/core@17.6.0': - resolution: {integrity: sha512-mLKGKYnbmp6ZNVZ26sC1ftDOIpKqiPjujJ+9avVDM06eQhL+O9r9nPFFjx+4RNfH7z+6r7T2LOu6nPdpRPaUWA==} + '@objectstack/core@17.7.0': + resolution: {integrity: sha512-2/NqNk41eNo0nPgBsClefme2HwtLhwCdxKZER+XTOD345KAb4L0suyu2vGxHAN82MGkfNu4ticZzrRAHDT7tYQ==} engines: {node: '>=22.0.0'} - '@objectstack/formula@17.6.0': - resolution: {integrity: sha512-5RpDNHLYJxqaivpDtM+QpltC5WlLRzwm58G9Y1us+6waZNnrV61eCDwV8JvaCsluneugcCVxp7oSDfjM8xZjAA==} + '@objectstack/formula@17.7.0': + resolution: {integrity: sha512-jf7SA/8QawVEKmQhwxjtyGxz+DIX+3V7M/Kt3z79jMhby5tjh5vwTbJI3PdyWCKQ1pS0epO6sSK7MddwLbEJ9Q==} - '@objectstack/lint@17.6.0': - resolution: {integrity: sha512-72LncQPEk1BnO3HWdtp+3Q9tik2lntQ8/WNfxtB5jqPo/9XQqDLjYaxCXmN1+mLtEkjk1ph/g8mHaSGORq9/4g==} + '@objectstack/lint@17.7.0': + resolution: {integrity: sha512-ddCHHRJKRSEirb74UNWd8DwdpqvQtMzsKH/bBiUslwXlYucQJPlQMd+ZVYN4V0IJoeD+IpD5el3P6S5H/wqPuQ==} engines: {node: '>=22.0.0'} - '@objectstack/sdui-parser@17.6.0': - resolution: {integrity: sha512-mMJxAXscqVTruSdRdzBfC1EufAXfX2jNDuyo1k0XNLkClLdJAgjmQBZtyMARUSEbGg33PC30gPx9IiBt7qYSFA==} + '@objectstack/sdui-parser@17.7.0': + resolution: {integrity: sha512-BaEt37YGK2lBgBXrM+w/tVI+kz5Gz6xhbmCDwgdeGCj8qKALNO8l/oQh/3qdsf6pLIBMfIi+IMqryOICgqwUdw==} - '@objectstack/spec@17.6.0': - resolution: {integrity: sha512-i5il5q0vIYjYrkHznsCXuO8C6Ftrs2RU4MxRHWrW3TsR7t/VUoe3sSfVLHDaudV+LfVVUDD9QHsWex1MCdckrA==} + '@objectstack/spec@17.7.0': + resolution: {integrity: sha512-PmKFK2C+NHiVoCAC+hYifJJWKiiJxZxN/FlpCQKwqPwajPxCcEUk0oVH0Af0yG3AFRQVCXxwIgEHGGeAa8Z4tA==} engines: {node: '>=22.0.0'} peerDependencies: ai: ^7.0.0 @@ -4253,8 +4253,8 @@ packages: ai: optional: true - '@objectstack/types@17.6.0': - resolution: {integrity: sha512-yH4B4YZt0cdSkenjifKY5o8svaV+PMPCdV0Tm0FMo3w1o6ShhF5gQK/PvU9lwFaRsEAB9SuJ70nCURfIAVoLsw==} + '@objectstack/types@17.7.0': + resolution: {integrity: sha512-7EIBYXZsZrBMihmmhwfcafI7F8WUvWNRAylJsZ25mBO7okQLhL4J3GX4wYBiyAJUmroIF3R9lCi+noaoWj5E5g==} engines: {node: '>=22.0.0'} '@open-draft/deferred-promise@2.2.0': @@ -12328,33 +12328,33 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@objectstack/client@17.6.0(ai@7.0.65(zod@4.6.5))': + '@objectstack/client@17.7.0(ai@7.0.65(zod@4.6.5))': dependencies: - '@objectstack/core': 17.6.0(ai@7.0.65(zod@4.6.5)) - '@objectstack/spec': 17.6.0(ai@7.0.65(zod@4.6.5)) + '@objectstack/core': 17.7.0(ai@7.0.65(zod@4.6.5)) + '@objectstack/spec': 17.7.0(ai@7.0.65(zod@4.6.5)) transitivePeerDependencies: - ai - '@objectstack/core@17.6.0(ai@7.0.65(zod@4.6.5))': + '@objectstack/core@17.7.0(ai@7.0.65(zod@4.6.5))': dependencies: - '@objectstack/spec': 17.6.0(ai@7.0.65(zod@4.6.5)) - '@objectstack/types': 17.6.0(ai@7.0.65(zod@4.6.5)) + '@objectstack/spec': 17.7.0(ai@7.0.65(zod@4.6.5)) + '@objectstack/types': 17.7.0(ai@7.0.65(zod@4.6.5)) zod: 4.6.5 transitivePeerDependencies: - ai - '@objectstack/formula@17.6.0(ai@7.0.65(zod@4.6.5))': + '@objectstack/formula@17.7.0(ai@7.0.65(zod@4.6.5))': dependencies: '@marcbachmann/cel-js': 8.0.0 - '@objectstack/spec': 17.6.0(ai@7.0.65(zod@4.6.5)) + '@objectstack/spec': 17.7.0(ai@7.0.65(zod@4.6.5)) transitivePeerDependencies: - ai - '@objectstack/lint@17.6.0(ai@7.0.65(zod@4.6.5))': + '@objectstack/lint@17.7.0(ai@7.0.65(zod@4.6.5))': dependencies: - '@objectstack/formula': 17.6.0(ai@7.0.65(zod@4.6.5)) - '@objectstack/sdui-parser': 17.6.0 - '@objectstack/spec': 17.6.0(ai@7.0.65(zod@4.6.5)) + '@objectstack/formula': 17.7.0(ai@7.0.65(zod@4.6.5)) + '@objectstack/sdui-parser': 17.7.0 + '@objectstack/spec': 17.7.0(ai@7.0.65(zod@4.6.5)) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) sucrase: 3.35.1 @@ -12362,18 +12362,18 @@ snapshots: transitivePeerDependencies: - ai - '@objectstack/sdui-parser@17.6.0': {} + '@objectstack/sdui-parser@17.7.0': {} - '@objectstack/spec@17.6.0(ai@7.0.65(zod@4.6.5))': + '@objectstack/spec@17.7.0(ai@7.0.65(zod@4.6.5))': dependencies: pg-connection-string: 2.14.0 zod: 4.6.5 optionalDependencies: ai: 7.0.65(zod@4.6.5) - '@objectstack/types@17.6.0(ai@7.0.65(zod@4.6.5))': + '@objectstack/types@17.7.0(ai@7.0.65(zod@4.6.5))': dependencies: - '@objectstack/spec': 17.6.0(ai@7.0.65(zod@4.6.5)) + '@objectstack/spec': 17.7.0(ai@7.0.65(zod@4.6.5)) transitivePeerDependencies: - ai diff --git a/scripts/__tests__/check-eager-closure-budget.test.ts b/scripts/__tests__/check-eager-closure-budget.test.ts index d2aa8e66cb..42b89794db 100644 --- a/scripts/__tests__/check-eager-closure-budget.test.ts +++ b/scripts/__tests__/check-eager-closure-budget.test.ts @@ -808,10 +808,11 @@ describe('ceiling sensitivity, judged live (objectui#5924)', () => { // UP to 3563.2 on the ruled raise for `@objectstack/*` 17.5.0, // objectui#11088 decision 1 = A, objectui#11101 down to 3552.8 on the first // payback of that raise, objectui#11438 down to 3306.8 on the second, when - // `@objectstack/*` 17.6.0's spec root shed its migration chain) — a rendering - // derived in the test would agree with the renderer by construction and pin - // nothing. - expect(result.message).toContain('3306.8'); + // `@objectstack/*` 17.6.0's spec root shed its migration chain, + // objectui#11717 UP to 3506.9 on the ruled raise for `@objectstack/*` + // 17.7.0, ruling 6027998226) — a rendering derived in the test would agree + // with the renderer by construction and pin nothing. + expect(result.message).toContain('3506.9'); }); it('is exactly one regression wide, from either side of the line', () => { @@ -1641,7 +1642,7 @@ describe('main', () => { // about the FIXTURE while the gate under test behaved correctly. The number // this case is actually about is "the report's chunk count, echoed". expect(outputs.closure_chunks).toBe(String(fixture.files.length)); - expect(outputs.closure_gzip_kb).toBe('3306.8'); + expect(outputs.closure_gzip_kb).toBe('3506.9'); }); it('exits 1 — a verdict about the BUNDLE — when over budget', () => { @@ -2619,8 +2620,9 @@ describe('the prose attached to the baselines (objectui#7046)', () => { * * objectui#10996 is the first re-baseline to meet it, and re-pinned it to the * shape it left: `squashMerge` null, one commit carried. objectui#11073 and - * objectui#11101, and objectui#11438 after them, each re-pinned the constant - * onto a branch tip of their own and left the same shape. ⛔ Still exact and positional — a back-fill of the + * objectui#11101, and objectui#11438 and objectui#11717 after them, each + * re-pinned the constant onto a branch tip of their own and left the same + * shape. ⛔ Still exact and positional — a back-fill of the * squash reds here again, and is re-pinned to two strings the same way. */ it('records what each baseline carries as data, so the pin cannot go vacuous', () => { diff --git a/scripts/__tests__/check-lockfile-integrity.test.ts b/scripts/__tests__/check-lockfile-integrity.test.ts index 08f41d675e..1518e18c83 100644 --- a/scripts/__tests__/check-lockfile-integrity.test.ts +++ b/scripts/__tests__/check-lockfile-integrity.test.ts @@ -89,17 +89,17 @@ describe('green on main — the negative control', () => { * * ⛔ It cannot be produced by running the resolver, and that impossibility is * the card's central claim: every workspace range on `@objectstack/*` is a - * floating `^17.x` that a fresh resolve satisfies with the newest release, 17.6.0 today - * (objectui#11438; 17.4.0 when this sample was written), so no fresh resolve can pick 17.2.0. The sample is therefore a transformation of the real lockfile + * floating `^17.x` that a fresh resolve satisfies with the newest release, 17.7.0 today + * (objectui#11717; 17.4.0 when this sample was written), so no fresh resolve can pick 17.2.0. The sample is therefore a transformation of the real lockfile * reproducing the identity facts measured on PRs #7053 / #7058 — the family * down to 17.2.0, and `@objectstack/spec` resolved at BOTH versions. */ function sampleA(): string { let out = realLock; for (const pkg of ['client', 'core', 'formula', 'lint', 'sdui-parser']) { - out = out.replace(new RegExp(`(@objectstack/${pkg}@)17\\.6\\.0`, 'g'), '$117.2.0'); + out = out.replace(new RegExp(`(@objectstack/${pkg}@)17\\.7\\.0`, 'g'), '$117.2.0'); } - const specKey = " '@objectstack/spec@17.6.0(ai@7.0.65(zod@4.6.5))':"; + const specKey = " '@objectstack/spec@17.7.0(ai@7.0.65(zod@4.6.5))':"; expect(out, 'the spec snapshot key moved — rebuild this sample before trusting it').toContain( specKey, ); @@ -123,8 +123,8 @@ function sampleA(): string { function sampleB(): string { let out = realLock; const forked = [ - " '@objectstack/spec@17.6.0(ai@7.0.65(zod@4.6.5))':", - " '@objectstack/formula@17.6.0(ai@7.0.65(zod@4.6.5))':", + " '@objectstack/spec@17.7.0(ai@7.0.65(zod@4.6.5))':", + " '@objectstack/formula@17.7.0(ai@7.0.65(zod@4.6.5))':", " ai@7.0.65(zod@4.6.5):", ]; for (const key of forked) { @@ -190,7 +190,7 @@ describe('cause 2 — a declared dependency forks with no version moving (object expect(spec?.headCount).toBe(2); // This is #8326's exact bundling mechanism — two real paths, nothing // dedupes them — arriving with no version change anywhere. - expect(spec?.head.every((k) => k.includes('@17.6.0'))).toBe(true); + expect(spec?.head.every((k) => k.includes('@17.7.0'))).toBe(true); }); }); diff --git a/scripts/check-eager-closure-budget.mjs b/scripts/check-eager-closure-budget.mjs index 425381a5c1..318790082a 100644 --- a/scripts/check-eager-closure-budget.mjs +++ b/scripts/check-eager-closure-budget.mjs @@ -417,12 +417,13 @@ import { isEntrypoint } from './invoked-as.mjs'; * header for both builds and the three control rows that show the bytes * LEFT rather than moved. * - * Headroom above {@link BASELINE} is 24,091 bytes — 0.26x - * {@link REGRESSION_THIS_GATE_MUST_CATCH_BYTES}, on the pair objectui#11438 - * re-pinned when the `@objectstack/*` 17.6.0 bump paid back the rest of the - * spec-root part of objectui#11073's raise; the header's objectui#11101 entry - * says why that band is under the 0.50x design point, and objectui#11438's - * entry kept it to the byte. ⚠️ That + * Headroom above {@link BASELINE} is 45,972 bytes — 0.50x + * {@link REGRESSION_THIS_GATE_MUST_CATCH_BYTES}, on the pair objectui#11717 + * re-pinned under the ruling recorded in the header's fourth raise; before it, + * the pair objectui#11438 re-pinned when the `@objectstack/*` 17.6.0 bump paid + * back the rest of the spec-root part of objectui#11073's raise carried 24,091 + * bytes (0.26x), and the header's objectui#11101 entry says why that band sat + * under the 0.50x design point. ⚠️ That * is arithmetic on two * constants in this file, so it stays true while they do — it is NOT what the * closure has left today, which is smaller by every byte the payload has @@ -782,8 +783,82 @@ import { isEntrypoint } from './invoked-as.mjs'; * ceiling came down the same way, by the 250,108 bytes that chunk lost, with * {@link PER_CHUNK_BASELINE}. The other three per-chunk rows did not move, * and neither did {@link REGRESSION_THIS_GATE_MUST_CATCH_BYTES}. + * + * ## ⚠️ RAISED A FOURTH TIME, ON EXPLICIT MAINTAINER RULING (objectui#11717) + * + * From 3,410,284 over a 3,386,193 baseline to 3,637,000 over 3,591,028, the + * latter measured on `c1e32e96`: objectui#11717's branch with `main` at + * `9b673e54` merged in, the tree that carries `@objectstack/*` 17.7.0 into this + * repository. The ruling is record 6027998226 on objectui#11717, the + * maintainer's answer to decision request 6024630321, selected verbatim: + * + * 「A 抬预算+回收卡 (Recommended)」 + * + * In the ruling's own words: the eager-closure ceiling, {@link BASELINE} and + * the `vendor-objectstack` row of {@link PER_CHUNK_BASELINE} / its per-chunk + * ceiling are raised in one commit of PR objectui#11742, in the objectui#11088 + * shape, with the headroom kept at half of + * {@link REGRESSION_THIS_GATE_MUST_CATCH_BYTES}, and a payback card is filed in + * the same stroke. + * + * ⛔ WHAT THE BYTES ARE — one container, one instrument, two holds of + * `scripts/pm/os-verify-lock.sh`: `pnpm exec turbo run build + * --filter=!@object-ui/site --concurrency=2` in each tree (which builds the + * console), then `pnpm check:eager-closure`, reading + * `apps/console/dist/eager-closure.json`. The control is `9b673e54`, the `main` + * commit this branch last merged, built the same way in a worktree of its own + * with `@objectstack/*` 17.6.0 resolved. Both builds weigh 331 of 2451 chunks. + * + * | chunk | `9b673e54` | `c1e32e96` | delta | + * |-------------------------------|------------:|------------:|---------:| + * | `vendor-objectstack` | 1,421,565 | 1,604,474 | +182,909 | + * | `ui-components` | 283,061 | 283,079 | +18 | + * | `framework` | 74,774 | 74,785 | +11 | + * | `src` | 176,172 | 176,181 | +9 | + * | `i18n-locale-en` | 44,592 | 44,620 | +28 | + * | everything else (326 names) | 1,407,856 | 1,407,889 | +33 | + * | ⇒ aggregate | 3,408,020 | 3,591,028 | +183,008 | + * + * `vendor-objectstack` is the whole of it: the chunk the `@objectstack/*` + * packages land in, grown by the 17.7.0 release, while every other row nets + * +99 bytes across the bump's own edits. ⛔ Not attributed below the chunk by a + * build: the build records no module-level sizes for this chunk. The reading + * below the chunk, recorded on objectui#11717, is a byte diff of the two + * installed releases by entry file, not of what this chunk keeps after + * tree-shaking. Among the spec entries this repository's shipped code imports, + * `shared` grew most (+152,445 gzipped bytes as a standalone file, behind an + * unchanged list of exports), then the browser root, `browser/system`, `ui` + * and `browser/kernel` entries. + * + * WHAT THEY BUY: the console's client-side validation answering as the 17.7.0 + * server does — its new refusals (the chart measure-arity check on a dashboard + * widget, `requires` by page `kind`, a master-detail `sortField`, the retired + * keys and values refused by name) and its schemas — so the window in which an + * author, or an AI, writes in the console what the server rejects closes; and + * the cards this bump unblocks (objectui#11717 lists them). ⛔ It is NOT a new + * console capability. + * + * ⭐ PAYBACK: objectstack#22044, filed in the same stroke as the ruling. When it + * lands, this ceiling and {@link BASELINE} come DOWN together by the amount it + * recovers, the way objectui#11101 and objectui#11438 lowered the third raise: + * ⛔ not re-derived as a new reading plus half a regression, and ⛔ not rounded. + * + * Headroom 45,972 bytes = 0.50x {@link REGRESSION_THIS_GATE_MUST_CATCH_BYTES}: + * 3,591,028 + 45,568 = 3,636,596, rounded to the nearest thousand, the rounding + * every raise above used (the two paybacks, by rule, did not round). ⛔ The + * ceiling did not move alone: over the retired 3,386,193 baseline, 3,637,000 + * would carry 250,807 bytes, 2.75x the regression, and both the unit test's constant-vs-constant guard and + * {@link evaluateHeadroomSensitivity} red on it — so {@link BASELINE} moved in + * the same commit. + * + * ⛔ What moved with it, and what did not. `vendor-objectstack`'s per-chunk + * ceiling fired on the same build (its row above) and moved in this commit, + * with {@link PER_CHUNK_BASELINE}, under the same ruling. The other three + * per-chunk rows passed and did not move. Not + * {@link REGRESSION_THIS_GATE_MUST_CATCH_BYTES}. No exemption was added and no + * import was made lazy. */ -export const MAX_EAGER_CLOSURE_GZIP_BYTES = 3_410_284; +export const MAX_EAGER_CLOSURE_GZIP_BYTES = 3_637_000; /** * The measurement the ceiling above was derived from. Exported so the two @@ -796,6 +871,22 @@ export const BASELINE = Object.freeze({ /** * `emitEagerClosureReport`'s `eagerGzipBytes` on this commit. * + * `c1e32e96` is objectui#11717's branch tip: `main` at `9b673e54` merged into + * the branch that carries `@objectstack/*` 17.7.0 and the bump's own edits, + * re-pinned under the ruling recorded in the fourth raise on + * {@link MAX_EAGER_CLOSURE_GZIP_BYTES} (its table and its `9b673e54` control + * are there, ⛔ not restated here). It is a branch tip for the reason the + * paragraphs below give for `2ba091c` and `4acbea07`: the pull request's own + * diff is what moved the figure. Measured by `pnpm exec turbo run build + * --filter=!@object-ui/site --concurrency=2` (exit 0), reading + * `apps/console/dist/eager-closure.json` through `pnpm check:eager-closure`, + * under `scripts/pm/os-verify-lock.sh`, in the same container as the + * `9b673e54` control build. The commit that re-pins this file reaches no + * bundler input, so it cannot move the figure it pins. + * + * ⚠️ The paragraph below describes the reading this one superseded, `2ba091c` + * (3,386,193 across 330 of 2450 chunks), kept as its provenance. + * * `2ba091c` is objectui#11438's branch tip: `main` at `2d576e46` with * `@objectstack/*` 17.6.0 resolved and the bump's own edits, the second * payback recorded on {@link MAX_EAGER_CLOSURE_GZIP_BYTES} (its table and its @@ -842,10 +933,10 @@ export const BASELINE = Object.freeze({ * and not from `main` once it is squash-merged — the dead end objectui#9355 * added `squashMerge` below to route around. */ - gzipBytes: 3_386_193, - chunks: 330, - totalChunks: 2450, - commit: '2ba091c', + gzipBytes: 3_591_028, + chunks: 331, + totalChunks: 2451, + commit: 'c1e32e96', /** * The squash merge that carried the reading above onto `main`, recorded when @@ -853,8 +944,8 @@ export const BASELINE = Object.freeze({ * (objectui#9355): a sha that DOES resolve, so a reader re-checking the * measurement gets a handle rather than a dead end. * - * `null` since objectui#10996, and `null` again after objectui#11101 and - * objectui#11438, deliberately. This field can only ever be BACK-FILLED: a squash sha does + * `null` since objectui#10996, and `null` again after objectui#11101, + * objectui#11438 and objectui#11717, deliberately. This field can only ever be BACK-FILLED: a squash sha does * not exist until the pull request merges, so the change that re-pins the * field above ⛔ cannot write its own here. ⛔ Do not guess one: a wrong sha in * this position is worse than an absent one, because it RESOLVES, and a @@ -1376,7 +1467,15 @@ export const PER_CHUNK_GZIP_CEILINGS = Object.freeze({ // this chunk lost (the table under objectui#11438's entry on // MAX_EAGER_CLOSURE_GZIP_BYTES). Headroom 18,199 bytes = 0.20x // REGRESSION_THIS_GATE_MUST_CATCH_BYTES over the baseline below. - 'vendor-objectstack': 1_439_157, + // + // ⚠️ RAISED by objectui#11717 under ruling 6027998226 (the fourth raise on + // MAX_EAGER_CLOSURE_GZIP_BYTES): `@objectstack/*` 17.7.0 lands in THIS + // chunk, +182,909 gzipped bytes on the two builds tabled there, and the + // retired 1,439,157 fired on it. Headroom 18,526 bytes = 0.20x + // REGRESSION_THIS_GATE_MUST_CATCH_BYTES over the baseline below — the band + // the two paybacks kept — rounded to the nearest thousand. It comes down with + // the aggregate when objectstack#22044 pays back what it recovers. + 'vendor-objectstack': 1_623_000, // ⭐ LOWERED, and RE-KEYED, by objectui#7479 — this line used to read // `'i18n-locales': 465_000` and budget TEN catalogues. Nine of them are // `import()`ed on demand now, so the chunk that name pointed at no longer @@ -1447,10 +1546,13 @@ export const PER_CHUNK_GZIP_CEILINGS = Object.freeze({ * per file, and saying so is the point — a comment that names one commit for * three numbers taken on two is the drift objectui#6631 is open about: * - * - `vendor-objectstack` — `2ba091c` (objectui#11438), the 17.6.0 bump's - * build the aggregate also reads, re-measured when the spec's root entry - * shed its migration chain and this chunk's ceiling came down with the - * aggregate's. It supersedes `4acbea07` (objectui#11101), the payback build + * - `vendor-objectstack` — `c1e32e96` (objectui#11717), the 17.7.0 bump's + * build the aggregate also reads, re-measured when this chunk's ceiling + * fired and went up with the aggregate's under ruling 6027998226. It + * supersedes `2ba091c` (objectui#11438), the 17.6.0 bump's build, + * re-measured when the spec's root entry shed its migration chain and this + * chunk's ceiling came down with the aggregate's, which superseded + * `4acbea07` (objectui#11101), the payback build * re-measured when this chunk's group stopped claiming `import()`ed * modules, which superseded `048e7f6` (objectui#11073, the `@objectstack/*` * 17.5.0 raise), which superseded `34a1578ef` (objectui#7122, the 17.3.0 @@ -1514,10 +1616,10 @@ export const PER_CHUNK_GZIP_CEILINGS = Object.freeze({ * objectui#9251, and `ui-components` from objectui#9251 until objectui#10996 * re-pinned the aggregate onto a `main` commit; today neither does — and * WHICH ONE IS LATER flips every time either side is re-baselined, so read the - * commit names, never a direction asserted here. As of objectui#11438, - * `vendor-objectstack` SHARES the aggregate's reading: BASELINE's `2ba091c`, - * the 17.6.0 bump's build, re-pinned both, as objectui#11101's `4acbea07` and - * objectui#11073's `048e7f6` had before it. The aggregate is the later reading for the + * commit names, never a direction asserted here. As of objectui#11717, + * `vendor-objectstack` SHARES the aggregate's reading: BASELINE's `c1e32e96`, + * the 17.7.0 bump's build, re-pinned both, as objectui#11438's `2ba091c`, + * objectui#11101's `4acbea07` and objectui#11073's `048e7f6` had before it. The aggregate is the later reading for the * other keys: `bbf6b02d9` (2026-09-13, objectui#9251) for `ui-components`, * and `3f775eeb8` for `framework`. ⚠️ `i18n-locale-en`'s commit was `755d34a5f` when it * was taken and the aggregate has moved on since, which is exactly why the two @@ -1578,12 +1680,13 @@ export const PER_CHUNK_GZIP_CEILINGS = Object.freeze({ * measurement with no ceiling weighs nothing. */ export const PER_CHUNK_BASELINE = Object.freeze({ - // `2ba091c`, the same build as BASELINE above (objectui#11438), re-pinned - // with its ceiling by the second payback. It supersedes objectui#11101's - // `4acbea07` reading of 1,670,964, which superseded objectui#11073's + // `c1e32e96`, the same build as BASELINE above (objectui#11717), re-pinned + // with its ceiling by the fourth raise. It supersedes objectui#11438's + // `2ba091c` reading of 1,420,958 (re-pinned with its ceiling by the second + // payback), which superseded objectui#11101's `4acbea07` reading of 1,670,964, which superseded objectui#11073's // `048e7f6` reading of 1,703,690 (landed on `main` as `81f8498`), which // superseded objectui#7122's `34a1578ef` reading of 1,235,029. - 'vendor-objectstack': 1_420_958, + 'vendor-objectstack': 1_604_474, // `755d34a5f` (objectui#7479) — the SAME console build as // BASELINE above, so the two are directly comparable, and the same instrument // and container as the control build it is subtracted from. It supersedes diff --git a/scripts/check-installed-spec-pin-claims.mjs b/scripts/check-installed-spec-pin-claims.mjs index ea9e11c178..c45139d1ea 100644 --- a/scripts/check-installed-spec-pin-claims.mjs +++ b/scripts/check-installed-spec-pin-claims.mjs @@ -478,6 +478,14 @@ export const LEDGER = [ class: "historical", why: "Four lines about the release, not the install. Two ask whether the installed spec carries \"the 17.5.0 set\" / \"the loaded 17.5.0 blocks\" (the version names a record SET, as the 17.1.0 entry above does). Two are `pins` prose whose `pinned` is an unrelated verb (\"pinned narrowly\", \"pinned as THE RESIDUAL LEG\") beside a WHEN sentence (`allDayField` \"since `@objectstack/spec` 17.5.0\"; `redactFields` \"until `@objectstack/spec` 17.5.0 declared it\"). None restamps (objectui#11438).", }, + { + file: "apps/console/src/__tests__/registry-inputs-spec-parity.test.ts", + package: "@objectstack/spec", + version: "17.6.0", + sites: 1, + class: "historical", + why: "The `object-master-detail-form.details` pins prose: \"Since `@objectstack/spec` 17.6.0 (objectstack-ai/objectstack#21215) the spec row is an array of CLOSED entries\", the release the closed entry landed in, and \"a fork through 17.6.0, closed by `@objectstack/spec` 17.7.0\", the release that still declared `sortField`. `pinned` on that line is the verb in \"the pinned file\". Neither restamps (objectui#11717).", + }, { file: "apps/console/src/components/FormPage.tsx", package: "@objectstack/spec", @@ -870,6 +878,22 @@ export const LEDGER = [ class: "stale", why: "\"measured on the installed 17.0.0-rc.6: 18 exports entries, 17 of them reached by this repository's own imports, across 29 packages\" \u2014 three counts, all re-measurable against the resolved artifact.", }, + { + file: "packages/plugin-grid/src/index.tsx", + package: "@objectstack/spec", + version: "17.6.0", + sites: 1, + class: "historical", + why: "\"the row declares it since 17.6.0\" on the `keyboardNavigation` input's comment \u2014 the release `object-grid` gained the member; the line's `pinned` is the verb in \"is pinned there\". Permanently true (objectui#11717).", + }, + { + file: "scripts/check-eager-closure-budget.mjs", + package: "@objectstack/spec", + version: "17.6.0", + sites: 1, + class: "historical", + why: "One line naming the 17.6.0 bump as an EVENT: \"the pair objectui#11438 re-pinned when the `@objectstack/*` 17.6.0 bump paid back the rest of the spec-root part\", in the MAX docblock's account of the headroom the fourth raise superseded. It dates a measurement anchored to a named commit and does not restamp. The second line this entry once counted moved to 17.7.0 when objectui#11717 re-pinned BASELINE onto `c1e32e96` under ruling 6027998226.", + }, ]; // ── The run ──────────────────────────────────────────────────────────────────