From fa21e4bd9cb8ed5e11c010d68ee1c73643879781 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 19:19:34 +0000 Subject: [PATCH 1/2] fix(core): raise declared @objectstack/spec floor to ^17.3.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@object-ui/core` declared `@objectstack/spec: ^17.2.0` in `dependencies` (consumer-facing), while `normalizeListViewSchema` folds objectui's legacy toolbar flags onto the `userActions` keys `group` / `hideFields` / `rowColor`, which the protocol adopted only in 17.3.0. Any consumer resolution landing on 17.0.0 / 17.1.0 / 17.2.0 satisfied the declared range and got a normalizer whose output the spec refuses BY NAME. Re-measured against published artifacts, not the workspace copy: every published 17.x installed into its own isolated consumer project, the fold's real output parsed against that install's own `./ui` entry. 17.0.0 / 17.1.0 / 17.2.0 REFUSED refused-keys=[group, hideFields, rowColor] 17.3.0 / 17.4.0 ACCEPTED An undeclared firing-control key was refused by ALL FIVE versions, so the contrast measures those three keys and not the harness. 17.3.0 is the FIRST accepting version across the whole published 17.x stable line, not the first one that happened to work. A second, independent key family lands on the same floor: `ListViewSchema` gained `pageName` in 17.3.0, so the `page` view fixture this package already pins is refused before it and accepted from it. `scripts/check-spec-range-floors.mjs` cannot hold this: its criterion is symbol PRESENCE, and `UserActionsConfigSchema` is exported by every version above. The floor is held instead by a new pin carrying firing controls that prove its comparator can redden. The standing instruction in the objectui#5435 pin — read the file reddening on a resolved 17.2.x as "the declared floor is too low" — is discharged in place. Refs objectui#9012 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w --- .changeset/9012-core-spec-floor.md | 42 ++++ packages/core/package.json | 2 +- ...e-list-view.declaredSpecFloor-9012.test.ts | 185 ++++++++++++++++++ ...ist-view.foldOutputAuthorable-5435.test.ts | 12 +- 4 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 .changeset/9012-core-spec-floor.md create mode 100644 packages/core/src/utils/__tests__/normalize-list-view.declaredSpecFloor-9012.test.ts diff --git a/.changeset/9012-core-spec-floor.md b/.changeset/9012-core-spec-floor.md new file mode 100644 index 0000000000..63097dc114 --- /dev/null +++ b/.changeset/9012-core-spec-floor.md @@ -0,0 +1,42 @@ +--- +'@object-ui/core': patch +--- + +Raise `@object-ui/core`'s declared `@objectstack/spec` floor from `^17.2.0` to +`^17.3.0` (objectui#9012) — the old range admitted a spec that refuses this +package's own output. + +`normalizeListViewSchema` folds objectui's legacy toolbar flags onto the +`userActions` keys `group` / `hideFields` / `rowColor`, which the protocol +adopted in 17.3.0 (objectui#5435). `@objectstack/spec` was declared in +`dependencies` — consumer-facing — as `^17.2.0`, so any resolution landing on +17.0.0 / 17.1.0 / 17.2.0 satisfied the declared range and got a normalizer whose +output is refused **by name** at the view save gate. + +Measured against the published artifacts rather than the workspace copy: each +published 17.x was installed into its own isolated consumer project and the +fold's real output parsed against that install's own `./ui` entry. + + 17.0.0 / 17.1.0 / 17.2.0 REFUSED refused-keys=[group, hideFields, rowColor] + 17.3.0 / 17.4.0 ACCEPTED + +An undeclared firing-control key was refused by all five versions, so the +contrast is about those three keys and not about the harness. 17.3.0 is the +FIRST published version that accepts — verified across the entire published 17.x +stable line (17.0.0, 17.1.0, 17.2.0, 17.3.0, 17.4.0), not by taking the first +version that happened to work. + +A second, independent key family lands on the same floor: `ListViewSchema` gained +`pageName` in 17.3.0, and the `page` view fixture this package already pins is +refused by 17.0.0 / 17.1.0 / 17.2.0 (`refused-keys=[pageName]` plus an +`invalid_value` on `type`) and accepted from 17.3.0. + +No runtime behaviour changes: on the 17.4.0 every install resolves today, this is +the same normalizer. What changes is the declared contract — the range no longer +claims to work against specs that refuse its output. + +`scripts/check-spec-range-floors.mjs` was green before and after, and would be +green at any floor here: its criterion is symbol PRESENCE, and +`UserActionsConfigSchema` is exported by every version above. The floor is held +instead by `normalize-list-view.declaredSpecFloor-9012.test.ts`, which carries +firing controls proving its comparator can redden. diff --git a/packages/core/package.json b/packages/core/package.json index fb341c8ce4..54cca7cc9c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -33,7 +33,7 @@ "dependencies": { "@object-ui/types": "workspace:*", "@objectstack/formula": "^17.0.0", - "@objectstack/spec": "^17.2.0" + "@objectstack/spec": "^17.3.0" }, "devDependencies": { "@object-ui/test-support": "workspace:*", diff --git a/packages/core/src/utils/__tests__/normalize-list-view.declaredSpecFloor-9012.test.ts b/packages/core/src/utils/__tests__/normalize-list-view.declaredSpecFloor-9012.test.ts new file mode 100644 index 0000000000..c01cb464aa --- /dev/null +++ b/packages/core/src/utils/__tests__/normalize-list-view.declaredSpecFloor-9012.test.ts @@ -0,0 +1,185 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#9012 — `@object-ui/core`'s DECLARED `@objectstack/spec` floor must + * admit only specs that accept what this package's own fold produces. + * + * The sibling pin `normalize-list-view.foldOutputAuthorable-5435.test.ts` proves + * the fold's output is authorable against the RESOLVED spec. That is a fact + * about the lockfile, not about what a consumer installs: `dependencies` + * declared `^17.2.0`, and a consumer resolution landing on 17.2.x — a sibling + * pinning it exactly, an `overrides` entry, an offline mirror a minor behind — + * satisfied that range. This file pins the other half, the DECLARED floor. + * + * ## The measurement (objectui#9012, re-derived against published artifacts) + * + * Each published 17.x was installed into its own isolated consumer project + * (`npm install @objectstack/spec@VERSION`, no workspace resolution anywhere) + * and the fold's real output parsed against that install's own `./ui` entry: + * + * 17.0.0 UserActionsConfigSchema declares 8 keys REFUSED group/hideFields/rowColor + * 17.1.0 UserActionsConfigSchema declares 8 keys REFUSED group/hideFields/rowColor + * 17.2.0 UserActionsConfigSchema declares 8 keys REFUSED group/hideFields/rowColor + * 17.3.0 UserActionsConfigSchema declares 11 keys ACCEPTED + * 17.4.0 UserActionsConfigSchema declares 11 keys ACCEPTED + * + * A firing control (`zzUndeclared`) was refused by ALL FIVE, so "ACCEPTED" is + * not the reading of a schema that accepts everything, and "REFUSED" is not the + * reading of one that refuses everything. + * + * ⭐ SECOND, INDEPENDENT ROUTE to the same floor. `ListViewSchema` gained + * `pageName` in the same release, and `type: 'page'` with it. The fixture + * `normalize-list-view.pageResidual-8429.test.ts` asserts the spec ACCEPTS is + * refused by 17.0.0 / 17.1.0 / 17.2.0 (`refused-keys=['pageName']` plus an + * `invalid_value` on `type`) and accepted from 17.3.0. So the floor below is + * not propped up by one key family: two independent ones land on it. + * + * ⇒ 17.3.0 is the FIRST published version that accepts, verified across the + * whole published 17.x stable line rather than by taking the first version that + * happened to work. The line is exactly 17.0.0, 17.1.0, 17.2.0, 17.3.0, 17.4.0 + * — there is no unexamined gap between the last refusing and first accepting + * release. + * + * ## Why this is a test and not a gate extension + * + * `scripts/check-spec-range-floors.mjs` judges SYMBOL PRESENCE in the floor's + * published artifact, and `UserActionsConfigSchema` / `ListViewSchema` are + * exported by every version above — so that gate is green at `^17.2.0` and + * would stay green at any floor. The requirement here is BEHAVIOURAL (which + * KEYS the symbol declares), deliberately outside that gate's criterion; its + * header argues at length why a behaviour-based criterion there would over-name + * symbols. Teaching the gate this class is objectui#9012's fourth question and + * is filed separately rather than smuggled in here. + */ + +import { readFileSync } from 'node:fs'; + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { ListViewSchema as SpecListViewSchema, UserActionsConfigSchema } from '@objectstack/spec/ui'; + +import { normalizeListViewSchema } from '../normalize-list-view.js'; + +/** + * The first published `@objectstack/spec` that accepts this package's fold + * output. Measured, not assumed — see the docblock above. + */ +const REQUIRED_FLOOR = '17.3.0'; + +/** `[major, minor, patch]` of a plain `X.Y.Z`, or `null`. */ +const parseVersion = (v: string): [number, number, number] | null => { + const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim()); + return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null; +}; + +/** The minimum version a `^X.Y.Z` range admits. */ +const floorOf = (range: string): string => range.trim().replace(/^[\^~>=\s]+/, ''); + +/** Whether every version `range` admits is at or above `required`. */ +const admitsOnlyAtOrAbove = (range: string, required: string): boolean => { + const low = parseVersion(floorOf(range)); + const need = parseVersion(required); + if (!low || !need) return false; + for (let i = 0; i < 3; i += 1) { + if (low[i] !== need[i]) return low[i] > need[i]; + } + return true; +}; + +const manifest = JSON.parse( + readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'), +) as { name: string; dependencies?: Record }; + +/** The widest `userActions` block the fold can produce — all seven flags. */ +const LEGACY_VIEW = { + name: 'my_view', + label: 'My View', + type: 'grid', + columns: [{ field: 'name' }], + showSearch: true, + showSort: true, + showFilters: true, + showDensity: true, + showGroup: false, + showHideFields: true, + showColor: true, +} as const; + +/** The three keys adopted in 17.3.0 that made the old floor wrong. */ +const ADOPTED_KEYS = ['group', 'hideFields', 'rowColor'] as const; + +describe('the DECLARED spec floor admits only specs that accept the fold (objectui#9012)', () => { + beforeEach(() => { + // #8372's undrawable-kind warning is developer-facing noise here. + vi.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('the comparator, proven able to redden before it is trusted', () => { + it('refuses the floor this card replaced, and every floor below it', () => { + // ⭐ FIRING CONTROLS. Without these, `admitsOnlyAtOrAbove` returning true + // for the live manifest is equally consistent with a predicate that + // returns true for everything. + expect(admitsOnlyAtOrAbove('^17.2.0', REQUIRED_FLOOR)).toBe(false); + expect(admitsOnlyAtOrAbove('^17.1.0', REQUIRED_FLOOR)).toBe(false); + expect(admitsOnlyAtOrAbove('^17.0.0', REQUIRED_FLOOR)).toBe(false); + expect(admitsOnlyAtOrAbove('^16.99.99', REQUIRED_FLOOR)).toBe(false); + + // ... and accepts the floor that measured clean, and later ones. + expect(admitsOnlyAtOrAbove('^17.3.0', REQUIRED_FLOOR)).toBe(true); + expect(admitsOnlyAtOrAbove('^17.4.0', REQUIRED_FLOOR)).toBe(true); + + // A range it cannot reason about must not read as satisfied. + expect(admitsOnlyAtOrAbove('*', REQUIRED_FLOOR)).toBe(false); + expect(admitsOnlyAtOrAbove('workspace:*', REQUIRED_FLOOR)).toBe(false); + }); + }); + + describe('⭐ the declared range', () => { + it('declares the spec as a consumer-facing dependency at or above the measured floor', () => { + const declared = manifest.dependencies?.['@objectstack/spec']; + + // `dependencies`, not `devDependencies`: a dev range floors nothing for + // anybody, which is why this pin reads that field by name. + expect(declared, '@objectstack/spec must stay a runtime dependency').toBeTruthy(); + expect( + admitsOnlyAtOrAbove(declared as string, REQUIRED_FLOOR), + `declared "${declared}" admits a spec below ${REQUIRED_FLOOR}, which refuses this package's own fold output`, + ).toBe(true); + }); + }); + + describe('the requirement stays anchored to the spec, not to a number in this file', () => { + it('still emits exactly the keys the measurement was taken on', () => { + const out = normalizeListViewSchema(LEGACY_VIEW) as Record; + const ua = out.userActions as Record; + + // If the fold stops emitting these, REQUIRED_FLOOR is answering a + // question nobody is asking any more and this pin must be revisited. + for (const key of ADOPTED_KEYS) expect(Object.keys(ua)).toContain(key); + }); + + it('requires a spec that declares every key the floor was chosen for', () => { + const specKeys = Object.keys(UserActionsConfigSchema.shape); + for (const key of ADOPTED_KEYS) expect(specKeys).toContain(key); + + // The second, independent route to the same floor: `pageName` on a whole + // ListView document (objectui#8429's fixture, refused before 17.3.0). + const page = SpecListViewSchema.safeParse({ + name: 'account_page', + type: 'page', + pageName: 'crm_welcome', + columns: [], + }); + expect(page.success, '`pageName` on a `page` view must be authorable').toBe(true); + }); + }); +}); diff --git a/packages/core/src/utils/__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts b/packages/core/src/utils/__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts index 0f65d349db..98cb370b9a 100644 --- a/packages/core/src/utils/__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts +++ b/packages/core/src/utils/__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts @@ -48,9 +48,15 @@ * empties, this pin reddens and gets updated deliberately. * * ⚠️ Requires `@objectstack/spec >= 17.3.0` (the release that adopted the - * three). `@object-ui/core` still declares `^17.2.0`; if this file ever reddens - * on a resolved 17.2.x, the reading is that the declared floor is too low, not - * that the fold regressed. + * three). That is now also what `@object-ui/core` DECLARES: objectui#9012 + * measured the refusal against every published 17.x and raised the + * `dependencies` floor from `^17.2.0` to `^17.3.0`, so a resolution this + * package admits can no longer land on a spec that refuses the fold. The + * standing instruction that used to live here — "if this file ever reddens on a + * resolved 17.2.x, the reading is that the declared floor is too low, not that + * the fold regressed" — has been DISCHARGED, not deleted: it was read exactly + * that way. The declared floor itself is pinned by + * `normalize-list-view.declaredSpecFloor-9012.test.ts`. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; From 24f89ec4683bf9a7ca6d64478530022b685254c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 19:31:41 +0000 Subject: [PATCH 2/2] fix(core): sync pnpm-lock specifier with the raised spec floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pnpm-lock.yaml` records each importer's DECLARED specifier beside the resolved version, so raising `packages/core`'s range without regenerating the lockfile left the two disagreeing. CI installs with `--frozen-lockfile`, which refuses that mismatch before any job does its own work — every check on the first push failed with one root cause: specifiers in the lockfile don't match specifiers in package.json: * 1 dependencies are mismatched: - @objectstack/spec (lockfile: ^17.2.0, manifest: ^17.3.0) Regenerated with `pnpm install --lockfile-only`. The diff is exactly one line — the `specifier:` for `packages/core`'s `@objectstack/spec`. The resolved `version:` beside it is unchanged at `17.4.0`, which is the direct evidence that this moves no resolution for anybody: the floor is a declaration, and every install already landed above it. `pnpm install --frozen-lockfile` now exits 0, and `scripts/check-lockfile-integrity.mjs` reports clean. Refs objectui#9012 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d06147cc97..1f9e1bfcc6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1184,7 +1184,7 @@ importers: specifier: ^17.0.0 version: 17.4.0(ai@7.0.65(zod@4.4.3)) '@objectstack/spec': - specifier: ^17.2.0 + specifier: ^17.3.0 version: 17.4.0(ai@7.0.65(zod@4.4.3)) devDependencies: '@object-ui/test-support':