Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/6313-quick-reference-node-floor.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
---
---

Tooling-only fix (objectui#6313): `QUICK_REFERENCE.md`'s "Current Release" pin
(`scripts/__tests__/quick-reference-current-release-4143.test.ts`) and the sync
generator (`scripts/sync-quick-reference-release.mjs`) derived the Node/pnpm
version floors with `match(/(\d+)/)?.[1]`, which keeps only the LEADING integer
group. Against root `engines.node: ">=22.11"` (objectui#5306 / PR #6311) that
produced a floor of `22`, silently discarding the `.11` — so a row reading
exactly `≥ 22` passed a pin whose whole premise is disagreeing with its own
cited anchor. Both derivations now strip the comparator and keep the WHOLE
version string; `pnpm quick-reference:sync` regenerated the Node.js row to
`≥ 22.11`. Added regression coverage pinning that a `≥ 22`-shaped row is now
rejected and that a `≥ 220`-shaped row still is (objectui#4913), and corrected
the test file's docblock, which had gone stale on the same anchor one decimal
place up. No published package source changed.
2 changes: 1 addition & 1 deletion QUICK_REFERENCE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,7 +127,7 @@ behind (objectui#5394 — that had happened once per release, three times).
`apps/console/package.json`)
- **Client:** `@objectstack/client` ^17.0.0 (declared by `apps/console/package.json`
and `packages/data-objectstack/package.json`)
- **Node.js:** ≥ 22 (see root `engines.node`)
- **Node.js:** ≥ 22.11 (see root `engines.node`)
- **pnpm:** ≥ 10 (the workspace pins `pnpm@10.31.0` via `packageManager`)
- **React:** 18.x or 19.x (the `peerDependencies.react` range the packages declare)
- **TypeScript:** ≥ 5.0 (strict mode) — the stack floor stated in AGENTS.md §2, not a
Expand Down
96 changes: 87 additions & 9 deletions scripts/__tests__/quick-reference-current-release-4143.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,10 +12,19 @@ import { fileURLToPath } from 'node:url';
*
* The card's premise also said the block's Node/pnpm/React rows were "currently
* accurate". Two of the three were. **Node was not** — the row read `≥ 20` while
* root `engines.node` has been `>=22`, and the row *names that anchor in its own
* text*. A claim that cites its anchor and disagrees with it is the strongest
* possible argument that review alone does not hold this block: the reviewer had
* the pointer and still did not follow it.
* root `engines.node` was `>=22` at the time, and the row *names that anchor in
* its own text*. A claim that cites its anchor and disagrees with it is the
* strongest possible argument that review alone does not hold this block: the
* reviewer had the pointer and still did not follow it.
*
* `engines.node` has since moved again, to `>=22.11` (objectui#5306 / PR #6311) —
* and objectui#6313 is the same shape of drift one decimal place down: the
* forward assertion below derived its expected floor with `match(/(\d+)/)`,
* which keeps only the LEADING integer group and silently discards everything
* after it. `>=22.11` derived a floor of `22`, so a row reading exactly `≥ 22`
* (missing the `.11` its own anchor carries) passed. The derivation below now
* strips the comparator and keeps the WHOLE version string instead, so the row
* and the anchor can no longer disagree at the minor version.
*
* ## Why a per-block pin and not the repo's existing ratchet
*
Expand DownExpand Up@@ -123,6 +132,21 @@ function peerMajors(range: string): string[] {
.filter((major): major is string => major !== undefined);
}

/**
* The floor an `engines`-style range states, as the WHOLE version string — not just
* its leading integer group.
*
* objectui#6313: the previous derivation, `match(/(\d+)/)?.[1]`, kept only the
* leading digits, so `>=22.11` produced a floor of `22` and silently discarded the
* `.11` rather than failing. A row reading exactly `≥ 22` then satisfied an anchor
* that actually says `22.11`. Stripping the comparator and keeping the remainder
* whole means the row and the anchor can no longer disagree at any segment the
* anchor carries.
*/
function versionFloor(range: string): string | undefined {
return range.match(/^[\^~>=<\s]*(.+)$/)?.[1];
}

/**
* The `## Current Release` section, from its heading to the next `## ` heading (or EOF).
* Scoping matters: `## Repository Layout` above it also carries numbers, and asserting
Expand DownExpand Up@@ -314,19 +338,73 @@ describe('QUICK_REFERENCE.md "Current Release" — toolchain rows', () => {
const engine = rootManifest.engines?.node;
expect(engine, 'root package.json must still declare engines.node').toBeDefined();

const floor = (engine as string).match(/(\d+)/)?.[1];
expect(floor, `engines.node must still carry a numeric floor (got "${engine}")`).toBeDefined();
const floor = versionFloor(engine as string);
expect(floor, `engines.node must still carry a version floor (got "${engine}")`).toBeDefined();
expectRowStates(
'Node.js',
[`≥ ${floor}`],
`${docRel} must state the Node floor as exactly "≥ ${floor}" to match engines.node "${engine}"`,
);
});

/**
* objectui#6313: `match(/(\d+)/)?.[1]` on `engines.node` kept only the leading
* integer group, so `>=22.11` derived a floor of `22` and a row reading exactly
* `≥ 22` — missing the `.11` its own anchor states — passed. Two properties, both
* measured against the REAL `engines.node` rather than a hard-coded literal, so
* this cannot itself go stale the way the row it guards did:
*
* - The old (leading-integer) and new (whole-string) derivations must disagree
* whenever the anchor carries a minor — proving the fix changed what floor
* comes OUT, not just how it is computed. If `engines.node` ever loses its
* minor this assertion goes quiet, which is why the next one does not depend
* on that disagreement to make its point.
* - A row stating the coarser, pre-fix floor must be REJECTED by the same
* equality `expectRowStates` uses above. This is the entire point of the card:
* if `≥ 22` were accepted before the fix and still accepted after it, the
* derivation changed without changing what the pin enforces.
*/
it('derives the Node floor as the whole version, not just its leading integer group', () => {
const engine = rootManifest.engines?.node as string;
const staleFloor = engine.match(/(\d+)/)?.[1];
const floor = versionFloor(engine);

expect(
floor,
`engines.node ("${engine}") no longer carries a minor segment for the leading-integer ` +
`derivation ("${staleFloor}") to differ from — objectui#6313's regression case needs one ` +
're-anchored to keep exercising the disagreement',
).not.toBe(staleFloor);

expect(
statedLiterals(`≥ ${staleFloor}`),
`a row reading "≥ ${staleFloor}" must NOT satisfy the derived Node floor "≥ ${floor}" — ` +
'accepting it is exactly the objectui#6313 staleness this pin exists to catch',
).not.toEqual(statedLiterals(`≥ ${floor}`));
});

/**
* The coarseness objectui#4913 already fixed for `toContain` (a numerically-prefixed
* SUPERSET like `≥ 220` passing against a derived `≥ 22`) must not come back now that
* the floor is derived differently. Built off the real anchor's leading integer group
* so this states the exact shape the card's own history section records, rather than
* a made-up number.
*/
it('rejects a `≥ 220`-shaped row as equal to a shorter derived Node floor', () => {
const engine = rootManifest.engines?.node as string;
const staleFloor = engine.match(/(\d+)/)?.[1] as string;

expect(
statedLiterals(`≥ ${staleFloor}0`),
`"≥ ${staleFloor}0" must not equal "≥ ${staleFloor}" — a numeric-prefix superset is not the ` +
'same version literal, the objectui#4913 regression this guards',
).not.toEqual(statedLiterals(`≥ ${staleFloor}`));
});

it('states the pnpm floor and the pinned `packageManager`', () => {
const engine = rootManifest.engines?.pnpm;
expect(engine, 'root package.json must still declare engines.pnpm').toBeDefined();
const floor = (engine as string).match(/(\d+)/)?.[1];
const floor = versionFloor(engine as string);

const pinned = rootManifest.packageManager;
expect(pinned, 'root package.json must still declare packageManager').toBeDefined();
Expand DownExpand Up@@ -420,8 +498,8 @@ describe('QUICK_REFERENCE.md "Current Release" — no un-derived literal', () =>
add(consoleManifest.version);
add(rootManifest.devDependencies?.['@objectstack/spec']);
add(consoleManifest.devDependencies?.['@objectstack/client']);
add(`≥ ${rootManifest.engines?.node?.match(/(\d+)/)?.[1]}`);
add(`≥ ${rootManifest.engines?.pnpm?.match(/(\d+)/)?.[1]}`);
add(`≥ ${versionFloor(rootManifest.engines?.node ?? '')}`);
add(`≥ ${versionFloor(rootManifest.engines?.pnpm ?? '')}`);
add(rootManifest.packageManager?.split('@').pop());
for (const major of peerMajors(reactManifest.peerDependencies?.react ?? '')) {
add(`${major}.x`);
Expand Down
27 changes: 21 additions & 6 deletions scripts/sync-quick-reference-release.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -88,8 +88,8 @@ export const normalize = (literal) => literal.replace(/\s+/g, '');
* @property {string} version the one version the `fixed` group carries
* @property {string} spec the `@objectstack/spec` range
* @property {string} client the `@objectstack/client` range
* @property {string} nodeFloor numeric floor out of root `engines.node`
* @property {string} pnpmFloor numeric floor out of root `engines.pnpm`
* @property {string} nodeFloor version floor out of root `engines.node`
* @property {string} pnpmFloor version floor out of root `engines.pnpm`
* @property {string} pnpmPinned the version half of root `packageManager`
* @property {string[]} reactMajors the majors named by the react peer range
*/
Expand All@@ -114,6 +114,21 @@ export function peerMajors(range) {
.filter((major) => major !== undefined);
}

/**
* The floor an `engines`-style range states, as the WHOLE version string — not
* just its leading integer group.
*
* objectui#6313: the previous derivation, `match(/(\d+)/)?.[1]`, kept only the
* leading digits, so `>=22.11` produced a floor of `22` and this script would
* regenerate `≥ 22`, silently discarding the `.11` the anchor actually states.
* Stripping the comparator and keeping the remainder whole means the row this
* script writes and the anchor it reads can no longer disagree at any segment
* the anchor carries. Same derivation as the gate's copy of this function.
*/
export function versionFloor(range) {
return range.match(/^[\^~>=<\s]*(.+)$/)?.[1];
}

/**
* Reads every anchor the block quotes, from the manifests that own them.
*
Expand DownExpand Up@@ -180,11 +195,11 @@ export function readAnchors(root = repoRoot) {
);
}

const nodeFloor = rootManifest.engines?.node?.match(/(\d+)/)?.[1];
const pnpmFloor = rootManifest.engines?.pnpm?.match(/(\d+)/)?.[1];
const nodeFloor = rootManifest.engines?.node && versionFloor(rootManifest.engines.node);
const pnpmFloor = rootManifest.engines?.pnpm && versionFloor(rootManifest.engines.pnpm);
const pnpmPinned = rootManifest.packageManager?.split('@').pop();
if (!nodeFloor) throw new Error('root package.json must still declare a numeric engines.node floor');
if (!pnpmFloor) throw new Error('root package.json must still declare a numeric engines.pnpm floor');
if (!nodeFloor) throw new Error('root package.json must still declare a version-floor engines.node');
if (!pnpmFloor) throw new Error('root package.json must still declare a version-floor engines.pnpm');
if (!pnpmPinned) throw new Error('root package.json must still declare packageManager');

const peer = reactManifest.peerDependencies?.react;
Expand Down
Loading