From a910dd7121642d7e74b1febd792ebd24433f3223 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 07:14:37 +0000 Subject: [PATCH 1/4] chore(deps): pin @objectstack/* to 17.1.0 and adapt the surfaces it moves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lockfile refresh only — every manifest already declared `^17.0.0`, which admits 17.1.0, so no dependency range changed. spec, client, formula and lint move in lockstep (a split resolution is what produced the dual-version spec graph that reddened check:spec-symbols in this repo's history). Green-keeping, each forced by the new build and nothing else: - data-objectstack: identity alias row for `icontains`, a canonical VIEW_FILTER_OPERATORS member as of 17.1.0 and itself a member of VALID_AST_OPERATORS. Unmapped, an authored `icontains` reaches the wire raw — the shape that shipped an unfiltered query in objectstack#3948. - app-shell palette: drop the now-stale `element:filter` exclusion, exclude the newly-declared `record:discussion` as the same renderer as the offered `record:chatter`. Nothing the palette offers changes. - check-spec-symbol-derivation DEBT: `ReferenceRailEntry`, which 17.1.0 started exporting under a name objectui already used. The shapes disagree on `icon` (spec's schema is $strict and refuses it), so reconciling is a contract call — filed as #5494. - check-action-forward-parity KNOWN_GAPS: `onSuccess` x4 declared surfaces. ActionRunner has honoured it all along; 17.1.0 supplied the missing half by declaring it authorable, so it became owed. Forwarding it is capability wiring — filed as #5493. check:eager-closure is NOT addressed here and is left red deliberately: the closure grows 129,270 gzipped bytes (3,871,997 to 4,001,267), 41,267 over a ceiling the maintainer ruled on today in #5468 — "the absolute aggregate ceiling stays as shipped", and "gate-strength policy is the maintainer's". Refs #5328 --- .changeset/spec-pin-17-1-0-5328.md | 12 ++ .../metadata-admin/previews/block-types.ts | 16 +- packages/data-objectstack/src/index.ts | 9 ++ pnpm-lock.yaml | 145 ++++++++++-------- scripts/check-action-forward-parity.mjs | 19 +++ scripts/check-spec-symbol-derivation.mjs | 18 +++ 6 files changed, 152 insertions(+), 67 deletions(-) create mode 100644 .changeset/spec-pin-17-1-0-5328.md diff --git a/.changeset/spec-pin-17-1-0-5328.md b/.changeset/spec-pin-17-1-0-5328.md new file mode 100644 index 0000000000..b8785ce57d --- /dev/null +++ b/.changeset/spec-pin-17-1-0-5328.md @@ -0,0 +1,12 @@ +--- +'@object-ui/data-objectstack': minor +'@object-ui/app-shell': minor +--- + +Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves. + +The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history. + +**A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. + +**The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. 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 56ce7fc355..340ea56750 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 @@ -153,10 +153,24 @@ export const PALETTE_EXCLUSIONS: Record = { 'user:profile': 'shell singleton — lives in the app shell header', // No renderer, by decision. 'ai:chat_window': 'no inline renderer — the floating chat overlay (plugin-chatbot) is canonical', - 'element:filter': 'no renderer — list surfaces own filtering (userFilters / filter builder)', 'element:form': 'no renderer — use the object-bound `object-form` block', 'element:record_picker': 'no renderer — record picking is a field widget, not a page block', 'element:text_input': 'no renderer — bare inputs belong to a form, not a page block', + // Renders fine — excluded to keep ONE palette entry per renderer, not because + // it is unauthorable. `record:chatter` and `record:discussion` are the same + // renderer under two names (plugin-detail/src/index.tsx:589), and the palette + // already offers it above as 'Chatter feed'. Entered when `@objectstack/spec` + // 17.1.0 added `record:discussion` to `PageComponentType` (objectui#5328); + // objectui itself has emitted the type all along + // (synth/buildDefaultPageSchema.ts:731), so nothing new became authorable — + // only the spec's enum caught up. + // + // ⚠️ The pair is offered under the LEGACY name: `public-blocks.ts` records + // `record:chatter` as "`record:discussion` under a Salesforce-familiar name", + // i.e. this exclusion hides the canonical spelling and offers the alias. + // Flipping which one the palette offers changes what authors drag, so it is a + // decision rather than a pin-bump edit — filed as objectui#5495. + 'record:discussion': 'same renderer as the offered `record:chatter` — one palette entry per renderer', }; export const CATEGORY_LABEL_EN: Record = { diff --git a/packages/data-objectstack/src/index.ts b/packages/data-objectstack/src/index.ts index 9a041a4585..3d74a13047 100644 --- a/packages/data-objectstack/src/index.ts +++ b/packages/data-objectstack/src/index.ts @@ -99,6 +99,15 @@ export const FILTER_OPERATOR_ALIASES: Record = { notin: 'nin', nin: 'nin', contains: 'contains', + // Case-insensitive contains. A canonical `VIEW_FILTER_OPERATORS` member that + // arrived with `@objectstack/spec` 17.1.0 (objectui#5328), and an IDENTITY row + // like `contains` above: `icontains` is itself a member of + // `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the + // AST takes — nothing is translated and no case-sensitivity is lost. + // Declared rather than left to the `?? op` fall-through on the rule this + // table's test states: the AST gate accepting a spelling is not the driver + // compiling it into a WHERE clause. + icontains: 'icontains', not_contains: 'notcontains', notcontains: 'notcontains', starts_with: 'startswith', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 694c746965..37d9cdc7f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,7 +36,7 @@ importers: version: 10.0.1(eslint@10.8.1(jiti@2.7.0)) '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@playwright/test': specifier: ^1.62.1 version: 1.62.1 @@ -274,10 +274,10 @@ importers: version: link:../../packages/types '@objectstack/client': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@tailwindcss/postcss': specifier: ^4.3.3 version: 4.3.3 @@ -412,7 +412,7 @@ importers: version: link:../../packages/types '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) fumadocs-core: specifier: 16.14.4 version: 16.14.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.31.0(react@19.2.8))(next@16.3.1(@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.4.3) @@ -761,13 +761,13 @@ importers: version: link:../types '@objectstack/formula': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@objectstack/lint': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@sentry/react': specifier: ^10.70.0 version: 10.70.0(react@19.2.8) @@ -861,7 +861,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) better-auth: specifier: ^1.6.28 version: 1.6.28(@opentelemetry/api@1.9.1)(better-sqlite3@12.9.0)(mongodb@7.2.0(socks@2.8.9))(next@16.3.1(@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) @@ -957,7 +957,7 @@ importers: devDependencies: '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@types/react': specifier: 19.2.18 version: 19.2.18 @@ -993,7 +993,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@radix-ui/react-accordion': specifier: ^1.2.20 version: 1.2.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -1177,10 +1177,10 @@ importers: version: link:../types '@objectstack/formula': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) lodash: specifier: ^4.18.1 version: 4.18.1 @@ -1239,10 +1239,10 @@ importers: version: link:../types '@objectstack/client': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) devDependencies: tsup: specifier: ^8.5.1 @@ -1276,7 +1276,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -1383,7 +1383,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -1430,7 +1430,7 @@ importers: devDependencies: '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@types/react': specifier: 19.2.18 version: 19.2.18 @@ -1605,7 +1605,7 @@ importers: devDependencies: '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@types/react': specifier: 19.2.18 version: 19.2.18 @@ -1647,7 +1647,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@radix-ui/react-slot': specifier: ^1.3.3 version: 1.3.3(@types/react@19.2.18)(react@19.2.8) @@ -1766,7 +1766,7 @@ importers: version: link:../plugin-charts '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@types/react-grid-layout': specifier: ^2.1.0 version: 2.1.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -1875,7 +1875,7 @@ importers: version: link:../i18n '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) lucide-react: specifier: ^1.31.0 version: 1.31.0(react@19.2.8) @@ -1997,7 +1997,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) lucide-react: specifier: ^1.31.0 version: 1.31.0(react@19.2.8) @@ -2052,7 +2052,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) lucide-react: specifier: ^1.31.0 version: 1.31.0(react@19.2.8) @@ -2113,7 +2113,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@tanstack/react-virtual': specifier: ^3.14.9 version: 3.14.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -2257,7 +2257,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@types/react': specifier: 19.2.18 version: 19.2.18 @@ -2296,7 +2296,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) lucide-react: specifier: ^1.31.0 version: 1.31.0(react@19.2.8) @@ -2446,7 +2446,7 @@ importers: devDependencies: '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@types/node': specifier: ^26.2.0 version: 26.2.0 @@ -2491,7 +2491,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -2546,7 +2546,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) lucide-react: specifier: ^1.31.0 version: 1.31.0(react@19.2.8) @@ -2610,7 +2610,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -2644,7 +2644,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) react: specifier: 19.2.8 version: 19.2.8 @@ -2678,7 +2678,7 @@ importers: version: link:../types '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) react: specifier: 19.2.8 version: 19.2.8 @@ -2752,7 +2752,7 @@ importers: devDependencies: '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@tailwindcss/postcss': specifier: ^4.3.3 version: 4.3.3 @@ -2791,13 +2791,13 @@ importers: version: link:../react '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) packages/test-support: devDependencies: '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -2809,7 +2809,7 @@ importers: dependencies: '@objectstack/spec': specifier: ^17.0.0 - version: 17.0.0(ai@7.0.65(zod@4.4.3)) + version: 17.1.0(ai@7.0.65(zod@4.4.3)) zod: specifier: ^4.4.3 version: 4.4.3 @@ -4084,8 +4084,8 @@ packages: react: 19.2.8 react-dom: 19.2.8 - '@mongodb-js/saslprep@1.4.13': - resolution: {integrity: sha512-E3Sv4eCYAlKYUTx8S3ioQcDUscOif+8zZ5OnW1IzJ+Tt+EO+ke8mn+Y3FX6N1H79picwbdOavVOb1jPi2EOyrg==} + '@mongodb-js/saslprep@1.5.0': + resolution: {integrity: sha512-Hk1SKJCMcCos38+vqDnZzlIo4XRj9yCGzYkjB4LcqpeXRIYfia1UWTz+VrueLxoU+uSRJzgkufxoRZg8gi52YA==} '@mswjs/interceptors@0.41.9': resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} @@ -4166,26 +4166,26 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@objectstack/client@17.0.0': - resolution: {integrity: sha512-pfapHGMXA0/nKohGNg6tXTZ9AuKa3lN7ZRrvb6N8h0+PEZsJLsGP13C2nb3Hc6T9NzCl4uO1nDMMSRYcLe++Ug==} + '@objectstack/client@17.1.0': + resolution: {integrity: sha512-pgd7JTIxN0TBDKs0bYuYWCJrPS8+H1sDby7vdvdYPBr4uaNBcwXQD3BWvSpaNyU11S8EtwWK5RQnqVYZARZEpA==} engines: {node: '>=22.0.0'} - '@objectstack/core@17.0.0': - resolution: {integrity: sha512-byJCvD92jvepzzcvydmQfSyjb5Qiu+r+RJEKn/qlRwYXFpMG9CbJovglBHyyCFRzbpWKc70z88FGrj5vlr25gQ==} + '@objectstack/core@17.1.0': + resolution: {integrity: sha512-4R+rmaNWNmW6K2hMK2GihCAYwLPJr9csqpq0Rmegs79ElXPRy93JNoVy5LG3WotVnIvUdPlGCWogdRhc4lRhqw==} engines: {node: '>=22.0.0'} - '@objectstack/formula@17.0.0': - resolution: {integrity: sha512-PhJcfhZBMZTmerGeFJfVuhntvhvPrZhXd8MxmBl8wJjOr0CpTsr25WsNxq86lwKlTkq9PRscSbIW8fxCiTRV9g==} + '@objectstack/formula@17.1.0': + resolution: {integrity: sha512-u54fhrTxkUySKHuRwA4XjFi8i4SEYemMI9dlA+GGubguwtoE27gTvH5J4CTWcrVb3oN9Cf+3Aymw3yrfFWirkA==} - '@objectstack/lint@17.0.0': - resolution: {integrity: sha512-U34gEiEK7VFF/By5RXRrbU69hhX0uekhfp9H5nhz+thr2zOcRjpL8FoAH+mwAZvv3bz/mWeVw0t40KiR5PIlww==} + '@objectstack/lint@17.1.0': + resolution: {integrity: sha512-qHh6oRHsjUGSzd8X3hiKQeV0jqFg2J98YxAArdaWV14AzJkYXnmQNv7bzLAdVyV1VWbnp/nPWprUVc3FTzjzBg==} engines: {node: '>=22.0.0'} - '@objectstack/sdui-parser@17.0.0': - resolution: {integrity: sha512-hHcjBsHH9B+WFHoqwBF5qV75Su9W0LXVFeWoTMJ+1mkdNyD5pKSQH7cWx81pTAkJ8yFTKlKUk74sLBHZIIHwPg==} + '@objectstack/sdui-parser@17.1.0': + resolution: {integrity: sha512-D6LuWbi6MlHrrQSX+4/MXx+n4M79PZtgOa9uOgyMVd/JoTVR/waWrIu2qrhkKP7TX/P9KNGql2ahR3fOPJ2txQ==} - '@objectstack/spec@17.0.0': - resolution: {integrity: sha512-65rmDnj6WKnIATvEg9/coDYSVUgBTtym/FysEApaK2W/k4ub0DP7dQ9VUmLB4HEbJxo0ahzD1tFObkXYEIT7Fw==} + '@objectstack/spec@17.1.0': + resolution: {integrity: sha512-gyF7knX3mqTEZSYeFIW/HBP0yFcjEzyXCS/p5pwH4yuJ4R2uw7mE3ItJs187CxZIFhFC2PTluRri3C/1Yaa9Og==} engines: {node: '>=22.0.0'} peerDependencies: ai: ^7.0.0 @@ -7152,6 +7152,9 @@ packages: dompurify@3.4.13: resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} + dompurify@3.4.14: + resolution: {integrity: sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -9397,6 +9400,9 @@ packages: pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -12399,7 +12405,7 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - '@mongodb-js/saslprep@1.4.13': + '@mongodb-js/saslprep@1.5.0': dependencies: sparse-bitfield: 3.0.3 optional: true @@ -12455,32 +12461,32 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@objectstack/client@17.0.0(ai@7.0.65(zod@4.4.3))': + '@objectstack/client@17.1.0(ai@7.0.65(zod@4.4.3))': dependencies: - '@objectstack/core': 17.0.0(ai@7.0.65(zod@4.4.3)) - '@objectstack/spec': 17.0.0(ai@7.0.65(zod@4.4.3)) + '@objectstack/core': 17.1.0(ai@7.0.65(zod@4.4.3)) + '@objectstack/spec': 17.1.0(ai@7.0.65(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/core@17.0.0(ai@7.0.65(zod@4.4.3))': + '@objectstack/core@17.1.0(ai@7.0.65(zod@4.4.3))': dependencies: - '@objectstack/spec': 17.0.0(ai@7.0.65(zod@4.4.3)) + '@objectstack/spec': 17.1.0(ai@7.0.65(zod@4.4.3)) zod: 4.4.3 transitivePeerDependencies: - ai - '@objectstack/formula@17.0.0(ai@7.0.65(zod@4.4.3))': + '@objectstack/formula@17.1.0(ai@7.0.65(zod@4.4.3))': dependencies: '@marcbachmann/cel-js': 8.0.0 - '@objectstack/spec': 17.0.0(ai@7.0.65(zod@4.4.3)) + '@objectstack/spec': 17.1.0(ai@7.0.65(zod@4.4.3)) transitivePeerDependencies: - ai - '@objectstack/lint@17.0.0(ai@7.0.65(zod@4.4.3))': + '@objectstack/lint@17.1.0(ai@7.0.65(zod@4.4.3))': dependencies: - '@objectstack/formula': 17.0.0(ai@7.0.65(zod@4.4.3)) - '@objectstack/sdui-parser': 17.0.0 - '@objectstack/spec': 17.0.0(ai@7.0.65(zod@4.4.3)) + '@objectstack/formula': 17.1.0(ai@7.0.65(zod@4.4.3)) + '@objectstack/sdui-parser': 17.1.0 + '@objectstack/spec': 17.1.0(ai@7.0.65(zod@4.4.3)) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) sucrase: 3.35.1 @@ -12488,10 +12494,11 @@ snapshots: transitivePeerDependencies: - ai - '@objectstack/sdui-parser@17.0.0': {} + '@objectstack/sdui-parser@17.1.0': {} - '@objectstack/spec@17.0.0(ai@7.0.65(zod@4.4.3))': + '@objectstack/spec@17.1.0(ai@7.0.65(zod@4.4.3))': dependencies: + pg-connection-string: 2.14.0 zod: 4.4.3 optionalDependencies: ai: 7.0.65(zod@4.4.3) @@ -15456,6 +15463,10 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 + dompurify@3.4.14: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -17732,7 +17743,7 @@ snapshots: monaco-editor@0.55.1: dependencies: - dompurify: 3.4.13 + dompurify: 3.4.14 marked: 14.0.0 mongodb-connection-string-url@7.0.2: @@ -17743,7 +17754,7 @@ snapshots: mongodb@7.2.0(socks@2.8.9): dependencies: - '@mongodb-js/saslprep': 1.4.13 + '@mongodb-js/saslprep': 1.5.0 bson: 7.3.2 mongodb-connection-string-url: 7.0.2 optionalDependencies: @@ -18105,6 +18116,8 @@ snapshots: pend@1.2.0: {} + pg-connection-string@2.14.0: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} diff --git a/scripts/check-action-forward-parity.mjs b/scripts/check-action-forward-parity.mjs index 46897832d4..c7377ec1fe 100644 --- a/scripts/check-action-forward-parity.mjs +++ b/scripts/check-action-forward-parity.mjs @@ -314,6 +314,25 @@ export const KNOWN_GAPS = { }, ]) ), + ...Object.fromEntries( + ["action:button", "action:icon", "action:group", "action:menu"].map((surface) => [ + `${surface}:onSuccess`, + { + reason: + "Newly OWED by the `@objectstack/spec` 17.1.0 pin bump (objectui#5328), not newly " + + "dropped: the runner has honoured it all along (ActionRunner.ts:1197 reads " + + "`action.onSuccess` and :1198 runs the chained defs), and 17.1.0 supplied the missing " + + "half by declaring the key authorable on `ActionSchema` — 0 occurrences in 17.0.0's " + + "`dist/**/*.d.ts`, 53 in 17.1.0. Both halves of the owed-set therefore hold for the " + + "first time on the bump. Forwarding it is capability WIRING, the same class the pin " + + "bump defers to its dependants (#5074 `viewMode`, #5042 `ListMapConfigSchema`), so it " + + "is filed rather than ridden in: objectui#5493. `element:button` is absent from this " + + "list because `onSuccess` is not on spec's `InlineActionSchema` pick list, so that " + + "surface never owed it.", + issue: 5493, + }, + ]) + ), }; // ── Opaque spreads ─────────────────────────────────────────────────────────── diff --git a/scripts/check-spec-symbol-derivation.mjs b/scripts/check-spec-symbol-derivation.mjs index 9233b59fa6..e84e943117 100644 --- a/scripts/check-spec-symbol-derivation.mjs +++ b/scripts/check-spec-symbol-derivation.mjs @@ -417,6 +417,24 @@ const DEBT = { "NavigationItem", "NavigationItemSchema", ], + // Entered by the `@objectstack/spec` 17.1.0 pin bump (objectui#5328), and it is + // worth naming how this collision differs from the three above: objectui did + // not fork a spec name here. The local `ReferenceRailEntry` predates the + // release and did not move — 17.1.0 started EXPORTING the name (absent from + // 17.0.0's `dist/` entirely), so the collision arrived from upstream. The + // gate's own framing, "a fresh fork fails on the PR that writes it", does not + // fit a PR that wrote no fork. + // + // Not burnable in that PR either, because the two shapes disagree on one key: + // spec's `ReferenceRailEntrySchema` is `$strict` over + // {objectName, relationshipField, title, limit, displayField}, while the local + // interface adds `icon` — which the renderer reads and the strict schema + // REFUSES at save. Importing/deriving retires `icon`; renaming to a dialect + // keeps it unsaveable. Either is a contract call, so it is filed rather than + // guessed: objectui#5494. + "@object-ui/plugin-detail": [ + "ReferenceRailEntry", + ], }; // Files under these paths are not objectui's own authored surface. From 64dbef33497e87ea5014d05a45169d3e7c3f23fa Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 07:44:55 +0000 Subject: [PATCH 2/4] fix(spec-pin): adapt the four surfaces 17.1.0 moves beyond the palette MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All four are forced by the new contract and nothing else. - plugin-view: map the newly-canonical `icontains` onto the builder's `containsCaseInsensitive` (the id that authors the spec's `$icontains`). The table is keyed by ViewFilterOperator, so this was a compile error, not a silent gap. Deliberately not folded onto `contains`, which would rewrite a case-insensitive filter as case-sensitive on the next save. - test-support tombstone judge: 17.1.0 retires `element:filter` WHOLLY — it leaves PageComponentType while its props schema stays in ComponentPropsMap with every key tombstoned. That is a retired block, not the "broken probe" the old floor assumed, so the floor now applies to blocks the enum still offers. The control key is searched for rather than taken from found[0], which had made it depend on sort order. - console registry parity: delete the two `targetVariable` exemptions, which went dangling-and-stale exactly as that file's docblock predicted they would at the next pin; pin the five `record:*` blocks 17.1.0 adds to ComponentPropsMap (37 entries to 42) using the same pin-dependent shape GA_ONLY_BLOCKS already established; exempt `record:reference_rail.entries`, an array of objects a flat scalar `inputs` carrier cannot express, whose editor also needs #5494's `icon` question settled first. Refs #5328 --- .changeset/spec-pin-17-1-0-5328.md | 3 + .../registry-inputs-spec-parity.test.ts | 106 ++++++++++++++---- .../src/config/view-config-utils.ts | 8 ++ .../src/__tests__/spec-tombstones.test.ts | 51 +++++++-- 4 files changed, 139 insertions(+), 29 deletions(-) diff --git a/.changeset/spec-pin-17-1-0-5328.md b/.changeset/spec-pin-17-1-0-5328.md index b8785ce57d..6866fbe309 100644 --- a/.changeset/spec-pin-17-1-0-5328.md +++ b/.changeset/spec-pin-17-1-0-5328.md @@ -1,5 +1,6 @@ --- '@object-ui/data-objectstack': minor +'@object-ui/plugin-view': minor '@object-ui/app-shell': minor --- @@ -9,4 +10,6 @@ The pin itself is a lockfile refresh — every manifest already declared `^17.0. **A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. +**A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. + **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. 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 dd13ad72ae..ee15edf5f1 100644 --- a/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts +++ b/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts @@ -284,6 +284,37 @@ const GA_ONLY_BLOCKS = [ 'object-metric', ]; +/** + * The five `record:*` blocks `@objectstack/spec` 17.1.0 adds to + * `ComponentPropsMap` and `17.0.0` does not carry at all (objectui#5328; + * the map goes from 37 entries to 42, and these are the five). + * + * Exactly the same shape as `GA_ONLY_BLOCKS` above, and for the same reason: + * this repo has registered all five with `inputs` for far longer than the spec + * has described them — `plugin-detail/src/index.tsx` registers `alert` (:686), + * `history` (:662) and `reference_rail` (:675), and `quick_actions` / + * `discussion` alongside them — so what moved at the pin bump is the SPEC's + * side, not this repo's. They enter `covered` the moment the installed spec + * carries them, and the reverse direction then asks each for the keys it does + * not publish. Only `record:reference_rail` had one: `entries`, exempted below. + */ +const MINOR_17_1_BLOCKS = [ + 'record:alert', + 'record:discussion', + 'record:history', + 'record:quick_actions', + 'record:reference_rail', +]; + +/** + * Does the installed `@objectstack/spec` carry the 17.1.0 record set? + * + * Same observable-fact reasoning as `specCarriesGaBlocks` below, including the + * `every` rather than `some`: the five arrived in one release, so a + * half-carried state is a broken premise rather than an in-between pin. + */ +const specCarries171Blocks = MINOR_17_1_BLOCKS.every((type) => type in ComponentPropsMap); + /** * Does the installed `@objectstack/spec` carry the GA element set? * @@ -335,6 +366,7 @@ const PINNED_EXPECTED_COVERED = [ const EXPECTED_COVERED = [ ...PINNED_EXPECTED_COVERED, ...(specCarriesGaBlocks ? GA_ONLY_BLOCKS : []), + ...(specCarries171Blocks ? MINOR_17_1_BLOCKS : []), ].sort(); /** @@ -522,14 +554,17 @@ const UNPUBLISHED_EXEMPTIONS: Record = { * regression: the mechanism is now self-clearing. A key upstream retires after * this change enters the shape as a tombstone, leaves the accepted set on * arrival, and any exemption covering it goes dangling-and-stale in the same - * run — no issue needed, no filter to remember. Two of the entries still below - * are already queued for it: objectstack `origin/main` tombstones + * run — no issue needed, no filter to remember. + * + * THAT PREDICTION HAS NOW RUN ONCE, AND IT HELD. The paragraph used to say two + * entries below were queued for it: objectstack `origin/main` tombstoned * `targetVariable` on BOTH `element:text_input` and `element:record_picker` - * (measured on `main` @ `23abe2782`; both keys are still LIVE in the installed - * 17.0.0, whose tombstone set is the same eight rc.6 carried), so the pin that - * carries those retirements will name both entries here. Deleting them is - * the fix — objectui#3834's "should we publish an intent-only key" question is - * answered upstream by then, in the negative. + * while the installed 17.0.0 still carried both as live. The 17.1.0 pin + * (objectui#5328) delivered those retirements, all three directions named the + * two entries in the same run, and deleting them was the entire fix — + * objectui#3834's "should we publish an intent-only key" question having been + * answered upstream, in the negative. The mechanism needed no maintenance to + * do that, which is the property worth keeping. * * DO NOT resolve a tombstone red by declaring the input. That publishes a key * the contract rejects by name and fails the forward direction immediately; @@ -544,19 +579,22 @@ const UNPUBLISHED_EXEMPTIONS: Record = { // exemption` demanded its deletion. It is now pinned as DECLARED, by name, // alongside #3808's four at the bottom of this file. - // ── targetVariable — the spec's own "declarative hint" (2 keys) ──────────── - // Zero read points repo-wide (`grep -rn targetVariable packages/ apps/` is - // empty), and that is by design, not drift: the spec's describe says the live - // binding resolves via the variable whose `source` equals the component id, - // which is exactly what `usePageVariableBinding(schema?.id)` does - // (`text-input.tsx:60`). So publishing it is neither a fix nor a defect — it - // is a judgement about whether to publish an intent-only key, with a concrete - // risk on the publish side (an author who writes only `targetVariable` and no - // variable `source` gets an input that writes nowhere, silently). - 'element:text_input.targetVariable': - "Spec's own declarative hint with zero read points repo-wide; the live binding is the reverse lookup in usePageVariableBinding(schema.id) (text-input.tsx:60). Whether to publish an intent-only key is an open judgement: objectui#3834.", - 'element:record_picker.targetVariable': - "Spec's own declarative hint with zero read points repo-wide; the live binding is the reverse lookup by component id, as on element:text_input. Whether to publish an intent-only key is an open judgement: objectui#3834.", + // TWO targetVariable ENTRIES DELETED HERE — objectui#5328, and they died + // exactly the way the docblock above said they would. + // + // `element:text_input.targetVariable` and `element:record_picker.targetVariable` + // were exempted as the spec's own intent-only "declarative hint" with zero read + // points repo-wide, pending objectui#3834's question of whether to publish such + // a key at all. The `@objectstack/spec` 17.1.0 pin answered it upstream, in the + // negative: both keys arrived as ADR-0087 D2 tombstones, so they left the + // accepted set and the exemptions covering them went dangling-and-stale in the + // same run — named by `every unpublished-key exemption names a key the spec + // really declares`, `carries no stale unpublished-key exemption` and `the + // tombstoned keys are recognised, not exempted`, all three at once. + // + // Deleting them is the whole fix. The tombstone judge recognises both keys now, + // which is a stronger statement than an exemption ever was: the contract itself + // rejects them by name. // FIVE GA-PENDING ENTRIES DELETED HERE — objectui#4668, and they too were // designed to die exactly this way. @@ -655,6 +693,34 @@ const UNPUBLISHED_EXEMPTIONS: Record = { '@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.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).', + + // ── record:reference_rail.entries — a nested collection, newly JUDGED ────── + // (1 key) + // The gap is not new; being GATED is. `@objectstack/spec` 17.1.0 added + // `record:reference_rail` to `ComponentPropsMap` (37 entries to 42), so this + // file began judging a block it had never covered — the registration in + // `plugin-detail/src/index.tsx:675` has always published `hideEmpty` and only + // `hideEmpty`. Nothing about the renderer or its inputs changed on the pin + // (objectui#5328). + // + // `entries` is an ARRAY OF OBJECTS — `{objectName, relationshipField, title, + // limit, displayField}` per item — and `inputs` is a flat carrier of scalar + // fields (`type: 'string' | 'number' | 'boolean' | 'enum'`). The same + // "unpublishable in a flat carrier" reading `page:tabs.type` carried, except + // here the carrier cannot express the SHAPE rather than colliding on a name. + // + // DO NOT resolve this by declaring a scalar input for it: a string field + // standing in for a list of related-object bindings recommends a write the + // renderer cannot honour, which is this gate's own failure mode one layer in + // (the `page:tabs.alwaysShowStrip` note above). + // + // Also blocked on a real contract question, so it is not merely unbuilt: the + // item shape is `ReferenceRailEntrySchema`, whose `$strict` object REFUSES the + // `icon` key this repo's own local `ReferenceRailEntry` declares and the + // renderer reads. Publishing an entries editor means first deciding whether + // `icon` survives — objectui#5494. + 'record:reference_rail.entries': + 'An array of {objectName, relationshipField, title, limit, displayField} objects; `inputs` is a flat scalar carrier and cannot express it. Newly judged rather than newly missing — @objectstack/spec 17.1.0 added record:reference_rail to ComponentPropsMap, and the registration (plugin-detail/src/index.tsx:675) has always published only `hideEmpty`. An entries editor also needs the `icon` divergence settled first: objectui#5494.', }; /** diff --git a/packages/plugin-view/src/config/view-config-utils.ts b/packages/plugin-view/src/config/view-config-utils.ts index 70cc6e7cb1..00958188a1 100644 --- a/packages/plugin-view/src/config/view-config-utils.ts +++ b/packages/plugin-view/src/config/view-config-utils.ts @@ -45,6 +45,14 @@ const CANONICAL_TO_BUILDER: Record = { 'equals': 'equals', 'not_equals': 'notEquals', 'contains': 'contains', + // Case-insensitive contains, canonical in `VIEW_FILTER_OPERATORS` as of + // `@objectstack/spec` 17.1.0 (objectui#5328). The builder HAS an equivalent + // — `containsCaseInsensitive`, which authors the spec's `$icontains` + // (filter-builder.tsx:160, objectui#4023) — so this is a real row and not a + // `null`: mapping it to `contains` would quietly rewrite a case-insensitive + // filter into a case-sensitive one the next time the view was saved, the + // same folding the `is_null` note below refuses. + 'icontains': 'containsCaseInsensitive', 'not_contains': 'notContains', 'starts_with': 'startsWith', 'ends_with': 'endsWith', diff --git a/packages/test-support/src/__tests__/spec-tombstones.test.ts b/packages/test-support/src/__tests__/spec-tombstones.test.ts index 4023e5e1bb..39f4fa7e8e 100644 --- a/packages/test-support/src/__tests__/spec-tombstones.test.ts +++ b/packages/test-support/src/__tests__/spec-tombstones.test.ts @@ -36,7 +36,7 @@ import { describe, it, expect } from 'vitest'; import { z } from 'zod'; -import { ComponentPropsMap } from '@objectstack/spec/ui'; +import { ComponentPropsMap, PageComponentType } from '@objectstack/spec/ui'; import { RETIRED_DESCRIPTION_PREFIX, @@ -234,11 +234,27 @@ describe('spec-tombstone judge — against the installed @objectstack/spec', () // EVERY key would satisfy the loop above while proving nothing. An // authorable key of the same schema, carrying a value it accepts, must come // back clean at its own path. - const [controlType] = found[0]; - const controlKey = authorableShapeKeys( - ComponentPropsMap[controlType as keyof typeof ComponentPropsMap], - ).find((key) => shapeMemberTypeName(ComponentPropsMap[controlType as keyof typeof ComponentPropsMap], key) === 'string'); - expect(controlKey, `${controlType} has no authorable string key to use as a control`).toBeTruthy(); + // + // The control type is SEARCHED for rather than taken as `found[0]`: a + // WHOLLY retired block (see the narrowing test below) has no authorable key + // to offer, so pinning the control to whichever type sorts first made this + // assertion depend on alphabetical order. `@objectstack/spec` 17.1.0 made + // that concrete — `element:filter` retired entirely and sorts first + // (objectui#5328). Any type carrying both a tombstone and an authorable + // string key serves the control equally well. + const control = found + .map(([type]) => type) + .map((type) => { + const schema = ComponentPropsMap[type as keyof typeof ComponentPropsMap]; + return [type, authorableShapeKeys(schema).find((key) => shapeMemberTypeName(schema, key) === 'string')] as const; + }) + .find(([, key]) => key !== undefined); + expect( + control, + 'no tombstoned block has an authorable string key to use as a control — every candidate ' + + 'is wholly retired, so the cross-check below would prove nothing', + ).toBeTruthy(); + const [controlType, controlKey] = control as readonly [string, string]; expect( issuePaths(controlType, { [controlKey as string]: 'a string' }), `${controlType}.${controlKey} is authorable and string-typed, yet the contract rejected it`, @@ -252,9 +268,26 @@ describe('spec-tombstone judge — against the installed @objectstack/spec', () authorableShapeKeys(schema).length, `${type} authorable set did not narrow below its listed set`, ).toBeLessThan(listedShapeKeys(schema).length); - // Not narrowed to nothing, either: a block whose every key read as - // retired would be a broken probe, not a retired block. - expect(authorableShapeKeys(schema).length, `${type} has no authorable key left`).toBeGreaterThan(0); + // Not narrowed to nothing — but only for a block the spec still OFFERS. + // The reasoning was "a block whose every key read as retired would be a + // broken probe, not a retired block", and `@objectstack/spec` 17.1.0 + // supplied the case that separates the two: `element:filter` left + // `PageComponentType` altogether while its props schema stayed in + // `ComponentPropsMap` with every key tombstoned (objectui#5328). That is a + // retired BLOCK, and reading zero authorable keys off it is the correct + // answer rather than a broken probe — the tombstones are how a consumer + // pinned to the old enum still gets told the block is gone. + // + // So the floor is asserted where it still discriminates: a type the enum + // continues to offer must keep at least one authorable key, because THAT + // is the shape a broken probe would produce. + const stillOffered = (PageComponentType.options as readonly string[]).includes(type); + if (stillOffered) { + expect( + authorableShapeKeys(schema).length, + `${type} is still offered by PageComponentType yet has no authorable key left`, + ).toBeGreaterThan(0); + } } }); }); From ffc2d527b15f07c5dd228a8c2d82d866b5ca4c5a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 08:01:55 +0000 Subject: [PATCH 3/4] fix(spec-pin): pin the operator and action-key inventories to 17.1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three inventories that enumerate a spec vocabulary, all named by their own ratchets on the bump. - plugin-list `mapOperator`: explicit `icontains` arm. The emitted spelling equals the input, so `default` already returned it — but that file's header records relying on the AST set happening to accept a raw view spelling as exactly how it stopped discriminating in #3641, so the arm is written out and pinned in EXPECTED_AST_TARGET. - plugin-list tab presets: VIEW_FILTER_OPERATORS is 20, not 19. - core `SPEC_ACTION_KEYS`: add `onSuccess`. Diagnostic only — the list feeds warnOnUnknownActionKeys, so without it an author writing the key the spec now declares gets told it is unknown. The forwarding gap stays open and tracked (#5493). Refs #5328 --- .changeset/spec-pin-17-1-0-5328.md | 6 ++++++ packages/core/src/actions/actionKeys.ts | 8 ++++++++ packages/plugin-list/src/ListView.tsx | 7 +++++++ .../src/__tests__/UserFilters.tabPresetOperators.test.tsx | 4 +++- .../src/__tests__/filter-operator-ast-parity.test.ts | 1 + 5 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.changeset/spec-pin-17-1-0-5328.md b/.changeset/spec-pin-17-1-0-5328.md index 6866fbe309..7716d93219 100644 --- a/.changeset/spec-pin-17-1-0-5328.md +++ b/.changeset/spec-pin-17-1-0-5328.md @@ -1,6 +1,8 @@ --- '@object-ui/data-objectstack': minor +'@object-ui/plugin-list': minor '@object-ui/plugin-view': minor +'@object-ui/core': minor '@object-ui/app-shell': minor --- @@ -10,6 +12,10 @@ The pin itself is a lockfile refresh — every manifest already declared `^17.0. **A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause. +The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating. + +`@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately. + **A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. diff --git a/packages/core/src/actions/actionKeys.ts b/packages/core/src/actions/actionKeys.ts index ad7906dc0a..6da7334c13 100644 --- a/packages/core/src/actions/actionKeys.ts +++ b/packages/core/src/actions/actionKeys.ts @@ -238,6 +238,14 @@ export const SPEC_ACTION_KEYS = [ 'name', 'newTabUrl', 'objectName', + // Declared by `ActionSchema` as of @objectstack/spec 17.1.0 (objectui#5328). + // Listing it here is a DIAGNOSTIC statement only — `KNOWN_ACTION_KEYS` feeds + // `warnOnUnknownActionKeys`, so without this row an author writing the key the + // spec now accepts would be warned it is unknown. It says nothing about the + // key being forwarded: the four declared action surfaces still drop it before + // the runner, tracked as KNOWN_GAPS in check-action-forward-parity.mjs and + // filed as objectui#5493. + 'onSuccess', 'openIn', 'opensInNewTab', 'order', diff --git a/packages/plugin-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx index ae343afda9..77d204dd06 100644 --- a/packages/plugin-list/src/ListView.tsx +++ b/packages/plugin-list/src/ListView.tsx @@ -181,6 +181,13 @@ export function mapOperator(op: string) { case 'equals': case 'eq': return '='; case 'notequals': case 'ne': case 'neq': return '!='; case 'contains': return 'contains'; + // Canonical in `VIEW_FILTER_OPERATORS` as of @objectstack/spec 17.1.0 + // (objectui#5328), and an explicit arm rather than a `default` fall-through + // even though the emitted spelling is identical: `icontains` is its own + // member of `VALID_AST_OPERATORS`, so the raw passthrough happens to be + // accepted today, and relying on that is the exact slack this file's own + // header records as how it stopped discriminating in #3641. + case 'icontains': return 'icontains'; case 'notcontains': return 'notcontains'; case 'startswith': return 'startswith'; case 'endswith': return 'endswith'; diff --git a/packages/plugin-list/src/__tests__/UserFilters.tabPresetOperators.test.tsx b/packages/plugin-list/src/__tests__/UserFilters.tabPresetOperators.test.tsx index 69438d8f1c..85ee6d7e08 100644 --- a/packages/plugin-list/src/__tests__/UserFilters.tabPresetOperators.test.tsx +++ b/packages/plugin-list/src/__tests__/UserFilters.tabPresetOperators.test.tsx @@ -122,7 +122,9 @@ describe('UserFilters tab presets — the reported defect (#3470)', () => { describe('UserFilters tab presets — lowering is structural, not translated', () => { it('reads both vocabularies from the spec', () => { // Guards every it.each below against silently passing on an empty list. - expect(VIEW_FILTER_OPERATORS.length).toBe(19); + // 19 through @objectstack/spec 17.0.0; 20 as of 17.1.0, which adds the + // canonical `icontains` (objectui#5328). + expect(VIEW_FILTER_OPERATORS.length).toBe(20); expect(VALID_AST_OPERATORS.size).toBeGreaterThan(0); }); diff --git a/packages/plugin-list/src/__tests__/filter-operator-ast-parity.test.ts b/packages/plugin-list/src/__tests__/filter-operator-ast-parity.test.ts index 40ad46fac7..53b68dedfe 100644 --- a/packages/plugin-list/src/__tests__/filter-operator-ast-parity.test.ts +++ b/packages/plugin-list/src/__tests__/filter-operator-ast-parity.test.ts @@ -81,6 +81,7 @@ const EXPECTED_AST_TARGET: Record = { equals: '=', // case 'equals' not_equals: '!=', // case 'notequals' contains: 'contains', // case 'contains' + icontains: 'icontains', // case 'icontains' not_contains: 'notcontains', // case 'notcontains' starts_with: 'startswith', // case 'startswith' ends_with: 'endswith', // case 'endswith' From 4ad75fdfe7ef8766b3b7924df13039dc0bef4e57 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 00:06:17 +0000 Subject: [PATCH 4/4] chore(perf-budget): re-baseline the eager-closure ceiling for the 17.1.0 pin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer ruling on objectui#5531: option A, raise the ceiling permanently together with the headroom assertion that guards it. MAX_EAGER_CLOSURE_GZIP_BYTES 3,960,000 -> 4,086,000 BASELINE.gzipBytes 3,881,609 -> 4,005,911 (52 of 508 chunks) BASELINE.commit 77f846a8b -> 4c1623c0c Both constants move in ONE commit by necessity, not preference: the guard is `MAX - BASELINE < REGRESSION_THIS_GATE_MUST_CATCH_BYTES`, so raising MAX alone leaves headroom near 200 KB and fails the gate's own unit test. That failure is the guard working. Headroom is 80,089 bytes = 2.00% of the baseline, holding the header's existing ~2% discipline, and 80,089 < 91,136 with 11,047 to spare. The substantive property is preserved, not just the inequality: an 89 KiB repeat of objectui#5266 measured from the new baseline lands at 4,097,047, still over the ceiling, so the gate is exactly as sensitive as it was. REGRESSION_THIS_GATE_MUST_CATCH_BYTES is deliberately UNCHANGED. That is the gate's sensitivity and the ruling did not touch it; a ceiling that rises while sensitivity relaxes is a gate retiring itself. The baseline was re-measured on this branch after merging origin/main (4c1623c0c), not copied from the earlier 4,001,689 reading — main moved 62 commits in between and carried the closure up by 4,222 bytes of its own. Header prose updated so the comments and the constants agree. Refs #5328 --- .changeset/spec-pin-17-1-0-5328.md | 2 + scripts/check-eager-closure-budget.mjs | 53 +++++++++++++++++++------- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/.changeset/spec-pin-17-1-0-5328.md b/.changeset/spec-pin-17-1-0-5328.md index 7716d93219..261be17ee2 100644 --- a/.changeset/spec-pin-17-1-0-5328.md +++ b/.changeset/spec-pin-17-1-0-5328.md @@ -19,3 +19,5 @@ The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s **A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved. **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes. + +**The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes. diff --git a/scripts/check-eager-closure-budget.mjs b/scripts/check-eager-closure-budget.mjs index c84bb2c411..4e40ad84a1 100644 --- a/scripts/check-eager-closure-budget.mjs +++ b/scripts/check-eager-closure-budget.mjs @@ -24,6 +24,10 @@ * `advancedChunks` deliberately routes vendor and workspace code into named * chunks, so MOST regressions land outside `index-*.js`. * + * That table is the MOTIVATING measurement and stays pinned to `77f846a8b`; it + * is not the current reading. {@link BASELINE} carries today's, and the ceiling + * section below does the arithmetic against it. + * * ## Where the number comes from * * `apps/console/vite.config.ts` (`emitEagerClosureReport`) writes @@ -41,20 +45,43 @@ * Two constraints pin it from both sides: * * - It must PASS on today's `main`. A gate that lands red is a gate someone - * disables, and this one is landing as a replacement for a gate nobody could - * fail. Headroom above the current 3,881,609 bytes: 78,391 (2.02%). + * disables, and this one replaced a gate nobody could fail. Headroom above + * the current 4,005,911 bytes: 80,089 (2.00%). * - The headroom must stay SMALLER than the regression the gate exists to - * catch. objectui#5266 was 89 KiB = 91,136 bytes; 78,391 < 91,136, so this + * catch. objectui#5266 was 89 KiB = 91,136 bytes; 80,089 < 91,136, so this * ceiling would have failed on that change. Widening the headroom past ~89 * KiB would leave the gate green through a repeat of its own motivating * incident. * - * This is a truthful CURRENT-STATE ceiling, not a target. 3.7 MB gzipped before + * ## Why this number moved once (objectui#5328, maintainer ruling on #5531) + * + * It was 3,960,000 over a 3,881,609 baseline measured on `77f846a8b`. Pinning + * `@objectstack/spec` and its three siblings to 17.1.0 put the closure 41,689 + * bytes over that ceiling: the release is ~930 KB larger uncompressed, and + * essentially all of it lands in `vendor-objectstack-*.js`. That is REAL added + * payload, not a measurement artefact, and it is larger than the #5266 + * regression this gate was sized to catch — the gate did its job. + * + * The re-baseline was therefore escalated rather than taken by the seat doing + * the bump, because #5468 had ruled days earlier that the aggregate ceiling + * "stays as shipped" and that gate-strength policy is the maintainer's. The + * maintainer ruled option A on #5531: raise it, permanently, together with the + * headroom assertion that guards it. Both constants move in ONE commit — raising + * the ceiling alone leaves headroom at ~200 KB and fails the test below, which + * is the guard working, not an obstacle to route around. + * + * What did NOT move: {@link REGRESSION_THIS_GATE_MUST_CATCH_BYTES}. That is the + * gate's sensitivity, the ruling did not touch it, and re-baselining must never + * become an excuse to widen it — a ceiling that rises while the sensitivity + * relaxes is a gate quietly retiring itself. + * + * This is a truthful CURRENT-STATE ceiling, not a target. 3.8 MB gzipped before * first render is a bad payload, and the honest long-term line is far below it — * but lowering the line is a separate decision with its own work behind it - * (objectui#5324 names the candidates: per-chunk budgets, a ratchet against - * `main`, or actually cleaving the closure). Nothing here should be read as a - * finding that 3.79 MB is acceptable. + * (objectui#5324 names the candidates; objectui#5490 is the ruled follow-up + * adding per-chunk budgets so `vendor-objectstack` cannot grow unnoticed inside + * aggregate headroom again). Nothing here should be read as a finding that + * 3.82 MB is acceptable. * * ## Raising it * @@ -70,9 +97,9 @@ import { pathToFileURL } from 'node:url'; /** * Ceiling for the console eager closure, in gzipped bytes. See the header for - * how this number was chosen; measured 3,881,609 on `77f846a8b`. + * how this number was chosen; measured 4,005,911 on `4c1623c0c`. */ -export const MAX_EAGER_CLOSURE_GZIP_BYTES = 3_960_000; +export const MAX_EAGER_CLOSURE_GZIP_BYTES = 4_086_000; /** * The measurement the ceiling above was derived from. Exported so the two @@ -83,10 +110,10 @@ export const MAX_EAGER_CLOSURE_GZIP_BYTES = 3_960_000; */ export const BASELINE = Object.freeze({ /** `emitEagerClosureReport`'s `eagerGzipBytes` on this commit. */ - gzipBytes: 3_881_609, - chunks: 58, - totalChunks: 507, - commit: '77f846a8b', + gzipBytes: 4_005_911, + chunks: 52, + totalChunks: 508, + commit: '4c1623c0c', }); /**