From 60ed1ad43c2d2dcb27db738b2d76194bac8a87c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 15:15:59 +0000 Subject: [PATCH 01/19] chore(deps): resolve @objectstack/* 17.7.0 in pnpm-lock.yaml (objectui#11717) Regenerated with pnpm only, never by hand: every @objectstack/* specifier was raised to ^17.7.0 for one `pnpm install`, every manifest was restored byte for byte to HEAD, and a second `pnpm install` reconciled the importers back to their declared ranges. The seven packages objectui resolves (spec, types, core, client, formula, lint, sdui-parser) move 17.6.0 -> 17.7.0; no importer specifier line and no other package moves. Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- pnpm-lock.yaml | 134 ++++++++++++++++++++++++------------------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a365539d9..13a7fadecf 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) @@ -1752,7 +1752,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) @@ -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 @@ -2596,7 +2596,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) @@ -2669,7 +2669,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 @@ -2734,7 +2734,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 @@ -2799,7 +2799,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 @@ -2841,13 +2841,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 @@ -2862,7 +2862,7 @@ importers: dependencies: '@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)) zod: specifier: ^4.6.1 version: 4.6.5 @@ -4223,26 +4223,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 @@ -4250,8 +4250,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': @@ -12325,33 +12325,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 @@ -12359,18 +12359,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 From c62fd612a1e1952c8c786d07756d81028cb5f06e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:37:34 +0000 Subject: [PATCH 02/19] test(tooling): re-key the lockfile-integrity samples and the live-e2e backend pin on 17.7.0 (objectui#11717) check-lockfile-integrity.test.ts builds its samples from the real lockfile's @objectstack/* snapshot keys and asked to be rebuilt when they moved; they are re-keyed on 17.7.0. e2e/live/ci/backend.env must equal the resolved @objectstack/spec (ci-cd-pipeline-doc.test.ts): OBJECTSTACK_VERSION=17.7.0, whose @objectstack/cli@17.7.0 tag resolves to the release commit 4e4e881427. Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- e2e/live/ci/backend.env | 2 +- scripts/__tests__/check-lockfile-integrity.test.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) 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/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); }); }); From a1164374220d71254c0a5f0d6c62e1c998b61b8e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:37:35 +0000 Subject: [PATCH 03/19] types,plugin-dashboard: re-attach the spec's 17.7.0 object-level checks; the objectui#11334 mirror and objectui#11417's single-series arm (objectui#11717) DashboardWidgetSchema chains checkDashboardWidgetChartMeasureArity after the metric-family check, by import (objectui#11334 rulings 5925828806 and 5956401275). The page node and the page-kind node chain checkPageRequiresKind beside checkPageSourceCompleteness (objectui#7715 ruling B1). The 7715 census names both and its notAttachable booking is struck. The three OWED TO objectui#11334 bookings are struck by flipping their rows to the refusal: the census entry, and the two 8894 ledgers, whose cap rows now require the unrefused set to be empty. The dimensionless two-measure pie is pinned on both faces; its one-dimension control is stated per arm, because 17.7.0 refuses that pie by the single-series arm, which is pinned with its controls. PR objectui#11433's two door assertions and the dimensionless pie pin in DatasetWidget.unrenderedMeasures-8894 flip to the stored-document form. DatasetWidget reads DASHBOARD_WIDGET_MULTI_MEASURE_TYPES instead of restating it. Floors: @object-ui/types ^17.7.0, @object-ui/plugin-dashboard ^17.6.0. Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- ...17-dashboard-widget-chart-measure-arity.md | 10 + .changeset/11717-objectstack-17-7-0.md | 11 ++ .changeset/11717-page-requires-kind.md | 5 + packages/plugin-dashboard/package.json | 2 +- .../plugin-dashboard/src/DatasetWidget.tsx | 47 ++--- ...Widget.singleSeriesMeasures-11417.test.tsx | 38 ++-- ...setWidget.unrenderedMeasures-8894.test.tsx | 12 +- ...idgetConfigPanel.measureDoor-8894.test.tsx | 111 +++++------ packages/types/package.json | 2 +- ...rd-widget-metric-measure-door-8894.test.ts | 185 ++++++++++++------ .../spec-object-refinements-7715.test.ts | 76 ++++--- packages/types/src/zod/complex.zod.ts | 34 ++-- packages/types/src/zod/layout.zod.ts | 31 +-- pnpm-lock.yaml | 4 +- 14 files changed, 355 insertions(+), 213 deletions(-) create mode 100644 .changeset/11717-dashboard-widget-chart-measure-arity.md create mode 100644 .changeset/11717-objectstack-17-7-0.md create mode 100644 .changeset/11717-page-requires-kind.md 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-objectstack-17-7-0.md b/.changeset/11717-objectstack-17-7-0.md new file mode 100644 index 0000000000..67e4d31f06 --- /dev/null +++ b/.changeset/11717-objectstack-17-7-0.md @@ -0,0 +1,11 @@ +--- +'@object-ui/types': patch +'@object-ui/plugin-dashboard': 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. 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/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..e9f2a3bd7e 100644 --- a/packages/plugin-dashboard/src/__tests__/WidgetConfigPanel.measureDoor-8894.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/WidgetConfigPanel.measureDoor-8894.test.tsx @@ -31,21 +31,26 @@ * 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'; import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup, within } from '@testing-library/react'; +import type { z } from 'zod'; import { ChartTypeSchema as SpecChartTypeSchema, DashboardWidgetSchema as SpecDashboardWidgetSchema, + checkDashboardWidgetChartMeasureArity, } from '@objectstack/spec/ui'; import { WidgetConfigPanel } from '../WidgetConfigPanel'; import type { WidgetDatasetCatalogEntry } from '../dataset-catalog'; @@ -85,44 +90,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 z.RefinementCtx, + ); + return issues.length === 1; +}); const renderPanel = (config: Record, datasets: WidgetDatasetCatalogEntry[] | undefined = catalog) => render(); @@ -142,7 +121,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 +129,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 +147,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 +171,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 +212,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/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__/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__/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/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/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/pnpm-lock.yaml b/pnpm-lock.yaml index 13a7fadecf..291d792fb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1751,7 +1751,7 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^17.5.0 + specifier: ^17.6.0 version: 17.7.0(ai@7.0.65(zod@4.6.5)) lucide-react: specifier: ^1.43.0 @@ -2861,7 +2861,7 @@ importers: packages/types: dependencies: '@objectstack/spec': - specifier: ^17.6.0 + specifier: ^17.7.0 version: 17.7.0(ai@7.0.65(zod@4.6.5)) zod: specifier: ^4.6.1 From 7f05f8ea554bff213700c87c9e3cc715b3787921 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:37:57 +0000 Subject: [PATCH 04/19] docs(spec-pin): re-measure and restamp the installed-pin claims on 17.7.0; ledger four historical sites (objectui#11717) check:installed-pin-claims reported 22 findings naming 17.6.0. Each fact was re-read on the installed 17.7.0 before its stamp moved: SelectOptionSchema's blank label, CalendarConfigSchema's three legs, object-calendar's ten strict members, FieldSchema dependsOn / rows / option description / referenceTo, record:details columns, record:highlights icon and layout, record:alert visible, waitForInput's missing default, the lint field-rule root messages, and the retired-key brand. Three lines name the 17.6.0 release as an event and are ledgered historical (plugin-grid keyboardNavigation, two eager-closure lines), as is the console parity row that dates the closed details entry. Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- .changeset/11717-bump-prose.md | 4 ++++ .../src/views/metadata-admin/celAuthoring.ts | 2 +- ...FlowNodeInspector.declaredDefault.test.tsx | 2 +- ...low-canvas-seeds.saveErrors-10948.test.tsx | 2 +- packages/core/src/evaluator/optionRules.ts | 2 +- .../plugin-calendar/src/ObjectCalendar.tsx | 2 +- .../src/MetadataFieldsPage.tsx | 2 +- packages/plugin-detail/src/index.tsx | 4 ++-- .../formSectionGroupReference-7051.test.tsx | 2 +- .../src/readme-navigation-example.test.ts | 5 ++-- packages/plugin-kanban/src/index.tsx | 4 ++-- .../chartconfig-aria-retired-4044.test.ts | 2 +- ...hboard-aria-retired-contract-twins.test.ts | 4 ++-- .../object-view-unmirrored-keys-7779.test.ts | 2 +- .../record-details-top-level-9040.test.ts | 2 +- packages/types/src/field-types.ts | 6 ++--- packages/types/src/objectql.ts | 2 +- packages/types/src/record-components.ts | 6 ++--- scripts/check-installed-spec-pin-claims.mjs | 24 +++++++++++++++++++ 19 files changed, 54 insertions(+), 25 deletions(-) create mode 100644 .changeset/11717-bump-prose.md 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/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/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/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/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/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-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/__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-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/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__/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/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/scripts/check-installed-spec-pin-claims.mjs b/scripts/check-installed-spec-pin-claims.mjs index ea9e11c178..36fa67e053 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: 2, + class: "historical", + why: "Two lines 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\" and \"BASELINE's `2ba091c`, the 17.6.0 bump's build, re-pinned both\". Each dates a measurement anchored to a named commit; neither restamps (objectui#11717).", + }, ]; // ── The run ────────────────────────────────────────────────────────────────── From 7b220cfd1278b196d6fd5edd658fa6962e678cf9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:37:58 +0000 Subject: [PATCH 05/19] app-shell: ai:chat_window is retired upstream; its palette exclusion leaves with the type (objectui#11717) @objectstack/spec 17.7.0 drops ai:chat_window from PageComponentType and refuses it by name (objectstack#21504). block-config.test.ts's 'every exclusion names a real spec type' refused the stale entry. It goes the way user:profile / element:form did at 17.3.0, and the probes that read it move to the unknown-type branch. Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- .../previews/__tests__/block-config.test.ts | 9 ++++++-- .../__tests__/canvas-display-meta.test.tsx | 21 ++++++++++++------- .../metadata-admin/previews/block-types.ts | 21 ++++++++++++------- 3 files changed, 35 insertions(+), 16 deletions(-) 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/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`, From 5d6c2a64bfa740c2633f7861c36a093dfb5696c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:37:58 +0000 Subject: [PATCH 06/19] components: element:text drops the retired heading / subheading variants, as 17.7.0 refuses them (objectui#11717) element-text-published-variants-7450.test.ts went red as it promised to and named the follow-up: drop the pair from the registry inputs enum and from VARIANT_CLASS / VARIANT_TAG, and move the expectation to []. Done. The rows that pinned the old rendering flip to the refusal on every gate (contract, html tier, safeValidateSchema) and to the read site's body answer for a stored value. Prose that said the contract still accepts the pair is corrected. Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- .../11717-element-text-retired-variants.md | 5 ++ apps/console/src/preview-samples.ts | 4 +- .../element-text-variant-options-7450.test.ts | 6 +-- .../metadata-admin/previews/block-config.ts | 10 ++-- ...ment-text-published-variants-7450.test.tsx | 44 ++++++++++------ .../src/renderers/basic/elements.tsx | 52 ++++++++----------- .../components/src/renderers/basic/text.tsx | 9 ++-- 7 files changed, 70 insertions(+), 60 deletions(-) create mode 100644 .changeset/11717-element-text-retired-variants.md 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/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/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/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/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`. * From 622e461e3bc2fc9662a05072ae2152b84c302595 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:37:58 +0000 Subject: [PATCH 07/19] fields,i18n: label view_all_audit_log, the capability 17.7.0 adds (objectui#11717) Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- .changeset/11717-capability-view-all-audit-log.md | 6 ++++++ packages/fields/src/widgets/CapabilityMultiSelectField.tsx | 2 ++ packages/fields/src/widgets/useFieldTranslation.ts | 1 + packages/i18n/src/locales/ar.ts | 1 + packages/i18n/src/locales/de.ts | 1 + packages/i18n/src/locales/en.ts | 1 + packages/i18n/src/locales/es.ts | 1 + packages/i18n/src/locales/fr.ts | 1 + packages/i18n/src/locales/ja.ts | 1 + packages/i18n/src/locales/ko.ts | 1 + packages/i18n/src/locales/pt.ts | 1 + packages/i18n/src/locales/ru.ts | 1 + packages/i18n/src/locales/zh.ts | 1 + 13 files changed, 19 insertions(+) create mode 100644 .changeset/11717-capability-view-all-audit-log.md 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/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 6589bfd9df..b425a8d39d 100644 --- a/packages/fields/src/widgets/useFieldTranslation.ts +++ b/packages/fields/src/widgets/useFieldTranslation.ts @@ -218,6 +218,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 02a764627f..cc60adf91a 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 dad896ce81..eb34ee39da 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 032a6e5a8c..0530ff3fdc 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 15de671462..3e5a28870d 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 805edd3f12..c444d66c78 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 7d8f5a8e4b..768db9b396 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 0f99c3e2e6..64070dda2e 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 88f919aca1..dcdb30d0f7 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 a4bce464cc..ff7aa935da 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 c683b85f40..c89cbe25fd 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 深链 From fc0d45dfab94155e900437cfcd7c5300b6c86f7b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:37:58 +0000 Subject: [PATCH 08/19] test(app-shell): follow 17.7.0's approval node contract in three flow pins (objectui#11717) objectstack#21893 judges an approval node's config against ApprovalNodeConfigSchema at the build doors. The type-only fixture carries the approvers its contract requires, with a new pin for the refusal's location; the required-marker measurement for a seeded approval now reads Approvers and Type; the escalation round-trip matches its labels without the required marker the inspector now draws on timeoutHours. Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- .../clientValidation.flowNodeTypes.test.ts | 16 +++++++++++++++- ...lowNodeInspector.escalationOff-11660.test.tsx | 14 ++++++++++++-- ...wNodeInspector.requiredMarkers-10948.test.tsx | 5 ++++- 3 files changed, 31 insertions(+), 4 deletions(-) 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.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']], From 4eca34dceea07a0f6f12f2f5e4eaaf41001b3793 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:37:58 +0000 Subject: [PATCH 09/19] data-objectstack: the choice-without-options door shipped in 17.7.0; the guard's paragraph collapses into the relationship form (objectui#11717) The objectui#11253 tripwire went red and said what to do: the server refuses a select / radio with no option source at fields.NAME.options, so the guard's docblock no longer calls that refusal ahead of the server, and the block becomes the refusal pin, with controls and an equality between FieldSchema's answer and the guard's list. select-option-spec-extension-7014's select control carries one option so it stays about rows alone. Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- ...ct-metadata-write-guard.derivation.test.ts | 57 +++++++++++++------ .../src/object-metadata-write-guard.ts | 51 ++++++++--------- .../select-option-spec-extension-7014.test.ts | 11 +++- 3 files changed, 72 insertions(+), 47 deletions(-) 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/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`, () => { From b10c5f4cff1d65c3f6edf6343be2fd0b81adaea4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:38:13 +0000 Subject: [PATCH 10/19] =?UTF-8?q?types:=20the=2017.7.0=20import-boundary?= =?UTF-8?q?=20readings=20=E2=80=94=20three=20rows=20now=20carry=20spec=20d?= =?UTF-8?q?efaults,=20a=20sixth=20z.lazy,=20two=20new=20chained=20checks?= =?UTF-8?q?=20(objectui#11717)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit object-form, object-metric and object-master-detail-form rows reach a spec default since 17.7.0 (sections[].fields[] take the page-block section shape; object-metric drillDown.report is ReportSchema), so stripImportedDefaults hands back a stripped derivation; the two by-reference pins re-pin to 'the bag IS that derivation'. The walker docblock names the sixth lazy node, and the 8317 census lists the two newly chained checks. object-form-unmirrored-members moves visibleWhen off the group-referencing section, which 17.7.0 refuses. Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- .../__tests__/imported-defaults-8317.test.ts | 16 +++++++++++++--- ...object-form-properties-bag-10859-b4.test.ts | 16 +++++++++++----- ...object-form-unmirrored-members-6152.test.ts | 14 ++++++++++++-- .../registered-type-arms-10859-b2.test.ts | 18 +++++++++++++----- packages/types/src/zod/imported-defaults.ts | 5 ++++- 5 files changed, 53 insertions(+), 16 deletions(-) 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__/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/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 From 616920fabf810258ef9320b92727159d29f9cd61 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:38:13 +0000 Subject: [PATCH 11/19] types,plugin-calendar: 17.7.0 types the object-calendar calendar slot; flip the key-not-shape pin and restate the mirror's position (objectui#11717) objectstack#21464 stage 2 (7d674dfc) makes ComponentPropsMap['object-calendar'] .calendar the list view's strict calendar block. calendarUnionReads-8651's 'the slot refuses nothing' row flips: the control key, the dateField alias and 42 are refused now. The ObjectCalendarSchema.calendar docblock is re-measured, and it says the mirror's .passthrough() now admits an unexamined key the spec refuses. That difference is reported by row on the PR, not changed here. Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- .../calendarUnionReads-8651.test.tsx | 24 ++++---- packages/types/src/zod/objectql.zod.ts | 61 ++++++++++--------- 2 files changed, 45 insertions(+), 40 deletions(-) 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/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 From 6c5baf938955469d98869c3f92bbc777a251f848 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:38:13 +0000 Subject: [PATCH 12/19] plugin-gantt,plugin-timeline: open three descriptions with the 17.7.0 row's own describe (objectui#11717) Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- .changeset/11717-spec-row-describes.md | 6 ++++++ packages/plugin-gantt/src/index.tsx | 2 +- packages/plugin-timeline/src/index.tsx | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 .changeset/11717-spec-row-describes.md 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/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-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`): From 58062fd3b3115a5b09d38bc8f8009217523aceed Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:38:13 +0000 Subject: [PATCH 13/19] core: inventory requiresMembershipReach, the ActionSchema key 17.7.0 adds (objectui#11717) Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- .../11717-action-key-requires-membership-reach.md | 5 +++++ packages/core/src/actions/actionKeys.ts | 10 ++++++++++ 2 files changed, 15 insertions(+) create mode 100644 .changeset/11717-action-key-requires-membership-reach.md 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/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', From c7d4420c3a75fd9c1a9c9b98cde234f77fd5b0d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:38:13 +0000 Subject: [PATCH 14/19] =?UTF-8?q?test(console):=20three=20registry=20parit?= =?UTF-8?q?y=20rows=2017.7.0=20moves=20=E2=80=94=20ai:chat=5Fwindow=20reti?= =?UTF-8?q?red=20upstream,=20object-grid.resizableColumns=20harvested,=20t?= =?UTF-8?q?he=20sortField=20fork=20closed=20(objectui#11717)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- .../registry-inputs-spec-parity.test.ts | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts b/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts index 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) { From 9586d0e7a3962a39889b82621ee9faea4b071a43 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:38:13 +0000 Subject: [PATCH 15/19] plugin-form,types: sortField on a master-detail details entry is refused with the spec's retired-key message (objectui#11624 rows, objectui#11717) 17.7.0 carries 6ec54f00 (objectstack#21589). The objectui#11396 types pin's forward tripwire flips to the refusal on both published faces, with the spec's message at the member. The MasterDetailDetailConfig docblock, the 8071 test's header and compile-time block, and the details registration description stop calling the retirement unreleased. The Omit stays: the entry still lists the key as a tombstone. Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- .../11717-master-detail-sort-field-refused.md | 5 ++ packages/plugin-form/src/MasterDetailForm.tsx | 27 +++++----- .../masterDetailDetailsMembers-8071.test.tsx | 39 +++++++------- packages/plugin-form/src/index.tsx | 2 +- ...er-detail-form-details-entry-11396.test.ts | 51 +++++++++++-------- 5 files changed, 67 insertions(+), 57 deletions(-) create mode 100644 .changeset/11717-master-detail-sort-field-refused.md 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/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__/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/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], + ]); }); }); From 47b0ff99dd33c5ad92cd7153c7dd5c35acc2f4e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:54:49 +0000 Subject: [PATCH 16/19] test(plugin-dashboard): type the collecting context off the spec check, not off zod (objectui#11717) @object-ui/plugin-dashboard declares no zod dependency, so `import type { z }` failed its tsconfig.test.json compile (TS2307). The context's type is read off checkDashboardWidgetChartMeasureArity's own parameter instead. Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- .../src/__tests__/WidgetConfigPanel.measureDoor-8894.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 e9f2a3bd7e..b3cbc39974 100644 --- a/packages/plugin-dashboard/src/__tests__/WidgetConfigPanel.measureDoor-8894.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/WidgetConfigPanel.measureDoor-8894.test.tsx @@ -46,7 +46,6 @@ import * as React from 'react'; import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup, within } from '@testing-library/react'; -import type { z } from 'zod'; import { ChartTypeSchema as SpecChartTypeSchema, DashboardWidgetSchema as SpecDashboardWidgetSchema, @@ -98,7 +97,7 @@ 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 z.RefinementCtx, + { addIssue: (issue: unknown) => issues.push(issue) } as unknown as Parameters[1], ); return issues.length === 1; }); From 50ed869cfab397fd92f83ed04da7697283639e8a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:58:33 +0000 Subject: [PATCH 17/19] docs(console): the preview-samples note stops naming the retired heading variant as live (objectui#11717) Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- .../src/__tests__/preview-samples-registry-resolvable.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From c7bbce48162794bd09c9291469af9eca0a1774ac Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 20:05:28 +0000 Subject: [PATCH 18/19] test(types,app-shell): two rows the 17.7.0 moves reach in CI shard 7 (objectui#11717) - twins-spec-by-reference-9736: the Page control writes `requires`, which `checkPageRequiresKind` now refuses on a page with no `kind` (the spec default `full`); the control carries `kind: 'html'`, where the key is legal. - exclusion-reason-truthfulness: the no-renderer population emptied when `ai:chat_window`, its last member, left PALETTE_EXCLUSIONS with the type's upstream retirement. The row that required it non-empty now asserts it is empty and why, and goes red the day a new reason claims "no renderer". Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- .../exclusion-reason-truthfulness.test.ts | 18 ++++++++++++------ .../twins-spec-by-reference-9736.test.ts | 5 ++++- 2 files changed, 16 insertions(+), 7 deletions(-) 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/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); }); From b680973fd84b694f6b67d70684b286ebeebfc6e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 00:45:55 +0000 Subject: [PATCH 19/19] chore(console): raise the eager-closure line a fourth time for @objectstack/* 17.7.0, on maintainer ruling A (objectui#11717) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ruling is record 6027998226 on objectui#11717, verbatim 「A 抬预算+回收卡 (Recommended)」, answering decision request 6024630321. One commit, in the objectui#11088 shape: - BASELINE re-pinned to the merged head c1e32e96: 3,591,028 gzipped bytes across 331 of 2451 chunks, squashMerge null. - MAX_EAGER_CLOSURE_GZIP_BYTES 3,410,284 to 3,637,000, which is 3,591,028 + 45,568 rounded to the nearest thousand. Headroom is 45,972 bytes = 0.50x the regression. - vendor-objectstack: PER_CHUNK_BASELINE 1,420,958 to 1,604,474 and its ceiling 1,439,157 to 1,623,000 (headroom 18,526 = 0.20x). The retired ceiling fired on this build. - A new header section names the ruling and gives the chunk table against a 9b673e54 control built the same way. The aggregate grows by +183,008, of which vendor-objectstack is +182,909. It says what the bytes buy and names the payback card objectstack#22044. REGRESSION_THIS_GATE_MUST_CATCH_BYTES is untouched. No exemption was added and no import was made lazy. The other three per-chunk rows passed and did not move. The unit test's rendered literal moves 3306.8 to 3506.9. The pin-claims ledger entry for this file drops from two historical 17.6.0 sites to one, because the provenance line now cites the 17.7.0 build. The bump changeset gains the @object-ui/console bullet, following the 17.6.0 precedent. Claude-Session: https://claude.ai/code/session_01XgevVHGcN8dnegiiY3v75V Co-authored-by: Claude --- .changeset/11717-objectstack-17-7-0.md | 3 + .../check-eager-closure-budget.test.ts | 16 +- scripts/check-eager-closure-budget.mjs | 155 +++++++++++++++--- scripts/check-installed-spec-pin-claims.mjs | 4 +- 4 files changed, 143 insertions(+), 35 deletions(-) diff --git a/.changeset/11717-objectstack-17-7-0.md b/.changeset/11717-objectstack-17-7-0.md index 67e4d31f06..c5060fd595 100644 --- a/.changeset/11717-objectstack-17-7-0.md +++ b/.changeset/11717-objectstack-17-7-0.md @@ -1,6 +1,7 @@ --- '@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: @@ -9,3 +10,5 @@ objectui now resolves `@objectstack/*` 17.7.0 (objectui#11717). Two declared `@o - `@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/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/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 36fa67e053..c45139d1ea 100644 --- a/scripts/check-installed-spec-pin-claims.mjs +++ b/scripts/check-installed-spec-pin-claims.mjs @@ -890,9 +890,9 @@ export const LEDGER = [ file: "scripts/check-eager-closure-budget.mjs", package: "@objectstack/spec", version: "17.6.0", - sites: 2, + sites: 1, class: "historical", - why: "Two lines 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\" and \"BASELINE's `2ba091c`, the 17.6.0 bump's build, re-pinned both\". Each dates a measurement anchored to a named commit; neither restamps (objectui#11717).", + 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.", }, ];