Skip to content

Commit f8e3e9a

Browse files
os-warrenclaude
andauthored
fix(core): raise declared @objectstack/spec floor to ^17.3.0 (#9032)
* fix(core): raise declared @objectstack/spec floor to ^17.3.0 `@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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w * fix(core): sync pnpm-lock specifier with the raised spec floor `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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2b10ca0 commit f8e3e9a

5 files changed

Lines changed: 238 additions & 5 deletions

File tree

‎.changeset/9012-core-spec-floor.md‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
'@object-ui/core': patch
3+
---
4+
5+
Raise `@object-ui/core`'s declared `@objectstack/spec` floor from `^17.2.0` to
6+
`^17.3.0` (objectui#9012) — the old range admitted a spec that refuses this
7+
package's own output.
8+
9+
`normalizeListViewSchema` folds objectui's legacy toolbar flags onto the
10+
`userActions` keys `group` / `hideFields` / `rowColor`, which the protocol
11+
adopted in 17.3.0 (objectui#5435). `@objectstack/spec` was declared in
12+
`dependencies` — consumer-facing — as `^17.2.0`, so any resolution landing on
13+
17.0.0 / 17.1.0 / 17.2.0 satisfied the declared range and got a normalizer whose
14+
output is refused **by name** at the view save gate.
15+
16+
Measured against the published artifacts rather than the workspace copy: each
17+
published 17.x was installed into its own isolated consumer project and the
18+
fold's real output parsed against that install's own `./ui` entry.
19+
20+
17.0.0 / 17.1.0 / 17.2.0 REFUSED refused-keys=[group, hideFields, rowColor]
21+
17.3.0 / 17.4.0 ACCEPTED
22+
23+
An undeclared firing-control key was refused by all five versions, so the
24+
contrast is about those three keys and not about the harness. 17.3.0 is the
25+
FIRST published version that accepts — verified across the entire published 17.x
26+
stable line (17.0.0, 17.1.0, 17.2.0, 17.3.0, 17.4.0), not by taking the first
27+
version that happened to work.
28+
29+
A second, independent key family lands on the same floor: `ListViewSchema` gained
30+
`pageName` in 17.3.0, and the `page` view fixture this package already pins is
31+
refused by 17.0.0 / 17.1.0 / 17.2.0 (`refused-keys=[pageName]` plus an
32+
`invalid_value` on `type`) and accepted from 17.3.0.
33+
34+
No runtime behaviour changes: on the 17.4.0 every install resolves today, this is
35+
the same normalizer. What changes is the declared contract — the range no longer
36+
claims to work against specs that refuse its output.
37+
38+
`scripts/check-spec-range-floors.mjs` was green before and after, and would be
39+
green at any floor here: its criterion is symbol PRESENCE, and
40+
`UserActionsConfigSchema` is exported by every version above. The floor is held
41+
instead by `normalize-list-view.declaredSpecFloor-9012.test.ts`, which carries
42+
firing controls proving its comparator can redden.

‎packages/core/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"dependencies": {
3434
"@object-ui/types": "workspace:*",
3535
"@objectstack/formula": "^17.0.0",
36-
"@objectstack/spec": "^17.2.0"
36+
"@objectstack/spec": "^17.3.0"
3737
},
3838
"devDependencies": {
3939
"@object-ui/test-support": "workspace:*",
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectui#9012 — `@object-ui/core`'s DECLARED `@objectstack/spec` floor must
11+
* admit only specs that accept what this package's own fold produces.
12+
*
13+
* The sibling pin `normalize-list-view.foldOutputAuthorable-5435.test.ts` proves
14+
* the fold's output is authorable against the RESOLVED spec. That is a fact
15+
* about the lockfile, not about what a consumer installs: `dependencies`
16+
* declared `^17.2.0`, and a consumer resolution landing on 17.2.x — a sibling
17+
* pinning it exactly, an `overrides` entry, an offline mirror a minor behind —
18+
* satisfied that range. This file pins the other half, the DECLARED floor.
19+
*
20+
* ## The measurement (objectui#9012, re-derived against published artifacts)
21+
*
22+
* Each published 17.x was installed into its own isolated consumer project
23+
* (`npm install @objectstack/spec@VERSION`, no workspace resolution anywhere)
24+
* and the fold's real output parsed against that install's own `./ui` entry:
25+
*
26+
* 17.0.0 UserActionsConfigSchema declares 8 keys REFUSED group/hideFields/rowColor
27+
* 17.1.0 UserActionsConfigSchema declares 8 keys REFUSED group/hideFields/rowColor
28+
* 17.2.0 UserActionsConfigSchema declares 8 keys REFUSED group/hideFields/rowColor
29+
* 17.3.0 UserActionsConfigSchema declares 11 keys ACCEPTED
30+
* 17.4.0 UserActionsConfigSchema declares 11 keys ACCEPTED
31+
*
32+
* A firing control (`zzUndeclared`) was refused by ALL FIVE, so "ACCEPTED" is
33+
* not the reading of a schema that accepts everything, and "REFUSED" is not the
34+
* reading of one that refuses everything.
35+
*
36+
* ⭐ SECOND, INDEPENDENT ROUTE to the same floor. `ListViewSchema` gained
37+
* `pageName` in the same release, and `type: 'page'` with it. The fixture
38+
* `normalize-list-view.pageResidual-8429.test.ts` asserts the spec ACCEPTS is
39+
* refused by 17.0.0 / 17.1.0 / 17.2.0 (`refused-keys=['pageName']` plus an
40+
* `invalid_value` on `type`) and accepted from 17.3.0. So the floor below is
41+
* not propped up by one key family: two independent ones land on it.
42+
*
43+
* ⇒ 17.3.0 is the FIRST published version that accepts, verified across the
44+
* whole published 17.x stable line rather than by taking the first version that
45+
* happened to work. The line is exactly 17.0.0, 17.1.0, 17.2.0, 17.3.0, 17.4.0
46+
* — there is no unexamined gap between the last refusing and first accepting
47+
* release.
48+
*
49+
* ## Why this is a test and not a gate extension
50+
*
51+
* `scripts/check-spec-range-floors.mjs` judges SYMBOL PRESENCE in the floor's
52+
* published artifact, and `UserActionsConfigSchema` / `ListViewSchema` are
53+
* exported by every version above — so that gate is green at `^17.2.0` and
54+
* would stay green at any floor. The requirement here is BEHAVIOURAL (which
55+
* KEYS the symbol declares), deliberately outside that gate's criterion; its
56+
* header argues at length why a behaviour-based criterion there would over-name
57+
* symbols. Teaching the gate this class is objectui#9012's fourth question and
58+
* is filed separately rather than smuggled in here.
59+
*/
60+
61+
import { readFileSync } from 'node:fs';
62+
63+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
64+
import { ListViewSchema as SpecListViewSchema, UserActionsConfigSchema } from '@objectstack/spec/ui';
65+
66+
import { normalizeListViewSchema } from '../normalize-list-view.js';
67+
68+
/**
69+
* The first published `@objectstack/spec` that accepts this package's fold
70+
* output. Measured, not assumed — see the docblock above.
71+
*/
72+
const REQUIRED_FLOOR = '17.3.0';
73+
74+
/** `[major, minor, patch]` of a plain `X.Y.Z`, or `null`. */
75+
const parseVersion = (v: string): [number, number, number] | null => {
76+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
77+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
78+
};
79+
80+
/** The minimum version a `^X.Y.Z` range admits. */
81+
const floorOf = (range: string): string => range.trim().replace(/^[\^~>=\s]+/, '');
82+
83+
/** Whether every version `range` admits is at or above `required`. */
84+
const admitsOnlyAtOrAbove = (range: string, required: string): boolean => {
85+
const low = parseVersion(floorOf(range));
86+
const need = parseVersion(required);
87+
if (!low || !need) return false;
88+
for (let i = 0; i < 3; i += 1) {
89+
if (low[i] !== need[i]) return low[i] > need[i];
90+
}
91+
return true;
92+
};
93+
94+
const manifest = JSON.parse(
95+
readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'),
96+
) as { name: string; dependencies?: Record<string, string> };
97+
98+
/** The widest `userActions` block the fold can produce — all seven flags. */
99+
const LEGACY_VIEW = {
100+
name: 'my_view',
101+
label: 'My View',
102+
type: 'grid',
103+
columns: [{ field: 'name' }],
104+
showSearch: true,
105+
showSort: true,
106+
showFilters: true,
107+
showDensity: true,
108+
showGroup: false,
109+
showHideFields: true,
110+
showColor: true,
111+
} as const;
112+
113+
/** The three keys adopted in 17.3.0 that made the old floor wrong. */
114+
const ADOPTED_KEYS = ['group', 'hideFields', 'rowColor'] as const;
115+
116+
describe('the DECLARED spec floor admits only specs that accept the fold (objectui#9012)', () => {
117+
beforeEach(() => {
118+
// #8372's undrawable-kind warning is developer-facing noise here.
119+
vi.spyOn(console, 'warn').mockImplementation(() => {});
120+
});
121+
122+
afterEach(() => {
123+
vi.restoreAllMocks();
124+
});
125+
126+
describe('the comparator, proven able to redden before it is trusted', () => {
127+
it('refuses the floor this card replaced, and every floor below it', () => {
128+
// ⭐ FIRING CONTROLS. Without these, `admitsOnlyAtOrAbove` returning true
129+
// for the live manifest is equally consistent with a predicate that
130+
// returns true for everything.
131+
expect(admitsOnlyAtOrAbove('^17.2.0', REQUIRED_FLOOR)).toBe(false);
132+
expect(admitsOnlyAtOrAbove('^17.1.0', REQUIRED_FLOOR)).toBe(false);
133+
expect(admitsOnlyAtOrAbove('^17.0.0', REQUIRED_FLOOR)).toBe(false);
134+
expect(admitsOnlyAtOrAbove('^16.99.99', REQUIRED_FLOOR)).toBe(false);
135+
136+
// ... and accepts the floor that measured clean, and later ones.
137+
expect(admitsOnlyAtOrAbove('^17.3.0', REQUIRED_FLOOR)).toBe(true);
138+
expect(admitsOnlyAtOrAbove('^17.4.0', REQUIRED_FLOOR)).toBe(true);
139+
140+
// A range it cannot reason about must not read as satisfied.
141+
expect(admitsOnlyAtOrAbove('*', REQUIRED_FLOOR)).toBe(false);
142+
expect(admitsOnlyAtOrAbove('workspace:*', REQUIRED_FLOOR)).toBe(false);
143+
});
144+
});
145+
146+
describe('⭐ the declared range', () => {
147+
it('declares the spec as a consumer-facing dependency at or above the measured floor', () => {
148+
const declared = manifest.dependencies?.['@objectstack/spec'];
149+
150+
// `dependencies`, not `devDependencies`: a dev range floors nothing for
151+
// anybody, which is why this pin reads that field by name.
152+
expect(declared, '@objectstack/spec must stay a runtime dependency').toBeTruthy();
153+
expect(
154+
admitsOnlyAtOrAbove(declared as string, REQUIRED_FLOOR),
155+
`declared "${declared}" admits a spec below ${REQUIRED_FLOOR}, which refuses this package's own fold output`,
156+
).toBe(true);
157+
});
158+
});
159+
160+
describe('the requirement stays anchored to the spec, not to a number in this file', () => {
161+
it('still emits exactly the keys the measurement was taken on', () => {
162+
const out = normalizeListViewSchema(LEGACY_VIEW) as Record<string, unknown>;
163+
const ua = out.userActions as Record<string, unknown>;
164+
165+
// If the fold stops emitting these, REQUIRED_FLOOR is answering a
166+
// question nobody is asking any more and this pin must be revisited.
167+
for (const key of ADOPTED_KEYS) expect(Object.keys(ua)).toContain(key);
168+
});
169+
170+
it('requires a spec that declares every key the floor was chosen for', () => {
171+
const specKeys = Object.keys(UserActionsConfigSchema.shape);
172+
for (const key of ADOPTED_KEYS) expect(specKeys).toContain(key);
173+
174+
// The second, independent route to the same floor: `pageName` on a whole
175+
// ListView document (objectui#8429's fixture, refused before 17.3.0).
176+
const page = SpecListViewSchema.safeParse({
177+
name: 'account_page',
178+
type: 'page',
179+
pageName: 'crm_welcome',
180+
columns: [],
181+
});
182+
expect(page.success, '`pageName` on a `page` view must be authorable').toBe(true);
183+
});
184+
});
185+
});

‎packages/core/src/utils/__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,15 @@
4848
* empties, this pin reddens and gets updated deliberately.
4949
*
5050
* ⚠️ Requires `@objectstack/spec >= 17.3.0` (the release that adopted the
51-
* three). `@object-ui/core` still declares `^17.2.0`; if this file ever reddens
52-
* on a resolved 17.2.x, the reading is that the declared floor is too low, not
53-
* that the fold regressed.
51+
* three). That is now also what `@object-ui/core` DECLARES: objectui#9012
52+
* measured the refusal against every published 17.x and raised the
53+
* `dependencies` floor from `^17.2.0` to `^17.3.0`, so a resolution this
54+
* package admits can no longer land on a spec that refuses the fold. The
55+
* standing instruction that used to live here — "if this file ever reddens on a
56+
* resolved 17.2.x, the reading is that the declared floor is too low, not that
57+
* the fold regressed" — has been DISCHARGED, not deleted: it was read exactly
58+
* that way. The declared floor itself is pinned by
59+
* `normalize-list-view.declaredSpecFloor-9012.test.ts`.
5460
*/
5561

5662
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

‎pnpm-lock.yaml‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)