Skip to content

Commit 7ffc310

Browse files
committed
test(spec): pin that the in-tree anchor is not older than the newest aged-out deletion (#27015)
Reads only committed files, as an offline build does: the anchor's [RETIRED] lines that the live surface has dropped, in a def still on the surface, must each be dated by RETIRED_KEYS_BY_MAJOR. A line only the @objectstack/spec@<major-1>.0.0 tag can date is one a build with no route to origin/main refuses. Red at the previous anchor 85c6d76 (45 lines, the 38 the offline gate refused plus 7 it proves unreachable), green at 7eb613c. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QUs3Bshfmr8srPY21BVS4u
1 parent 4736469 commit 7ffc310

1 file changed

Lines changed: 100 additions & 0 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// #27015 — the in-tree anchor is not older than the newest aged-out deletion.
4+
//
5+
// A build that cannot reach origin/main (cloud's EE image stage, an air-gapped
6+
// or tagless clone) runs the #4650 deletion gate against the committed
7+
// `authorable-surface.base.json` (#5235). Every `[RETIRED]` line the anchor
8+
// still records and the live surface has since dropped must then be dated by
9+
// proof 1 of that gate. Its registry half (RETIRED_KEYS_BY_MAJOR) is in the
10+
// tree; its tag half (`@objectstack/spec@<major - 1>.0.0`, #25767) is not, and
11+
// such a build can neither read nor fetch it. So a tombstone deletion that only
12+
// the tag dates is one the offline build refuses.
13+
//
14+
// That is what happened: #26599 (dbf7e04adf) aged the ≤16.x kits out and
15+
// deleted 38 tag-dated lines, while the anchor stayed at 85c6d76ec42a, before
16+
// that commit. Every online build passed; `@objectstack/spec#build` failed in
17+
// every offline one, and nothing in this repository noticed.
18+
//
19+
// This pin reads only committed files — no git, no tag, no build — which is
20+
// exactly what the offline build has, and asks the one question that build
21+
// cannot answer: is there a deleted tombstone that only the tag could date?
22+
// It deliberately leaves out deletions the gate proves from the build itself
23+
// (a def no longer on the surface, proof 3; a never-tombstoned line, proof 4),
24+
// and it is stricter than the gate on one shape: a tombstone in a def that is
25+
// still on the surface but no longer reachable from the metadata roots (proof
26+
// 2), which only the generator's Zod graph can tell. That errs toward a
27+
// re-anchor, never toward a broken offline build.
28+
29+
import { describe, expect, it } from 'vitest';
30+
import fs from 'node:fs';
31+
import path from 'node:path';
32+
import { fileURLToPath } from 'node:url';
33+
import { RETIRED_KEYS_BY_MAJOR } from '../src/migrations/registry';
34+
import { carryAuthorableKey } from './lib/renamed-defs';
35+
import {
36+
AUTHORABLE_SURFACE_DIR_NAME,
37+
readShards,
38+
type AuthorableSurfaceShard,
39+
} from './lib/sharded-artifacts';
40+
41+
const SPEC_DIR = fileURLToPath(new URL('..', import.meta.url));
42+
const RETIRED_MARK = ' [RETIRED]';
43+
/** build-schemas.ts `TOMBSTONE_AGE_MAJORS` (#25767). */
44+
const TOMBSTONE_AGE_MAJORS = 1;
45+
const CURRENT_MAJOR = Number.parseInt(
46+
JSON.parse(fs.readFileSync(path.join(SPEC_DIR, 'package.json'), 'utf-8')).version,
47+
10,
48+
);
49+
50+
const anchor = JSON.parse(
51+
fs.readFileSync(path.join(SPEC_DIR, 'authorable-surface.base.json'), 'utf-8'),
52+
) as { baseRev: string; keys: string[] };
53+
54+
const liveKeys = new Set(
55+
readShards<AuthorableSurfaceShard>(path.join(SPEC_DIR, AUTHORABLE_SURFACE_DIR_NAME))
56+
.flatMap((s) => s.doc.keys)
57+
.map((e) => e.replace(RETIRED_MARK, '')),
58+
);
59+
const liveDefs = new Set([...liveKeys].map((k) => k.slice(0, k.indexOf(':'))));
60+
61+
/** The earliest major that registers each key, carried through def renames (build-schemas.ts `registeredRetiredKeys`). */
62+
const registeredAt = new Map<string, number>();
63+
for (const [major, keys] of Object.entries(RETIRED_KEYS_BY_MAJOR)) {
64+
for (const key of keys) {
65+
const carried = carryAuthorableKey(key);
66+
const prev = registeredAt.get(carried);
67+
if (prev === undefined || Number(major) < prev) registeredAt.set(carried, Number(major));
68+
}
69+
}
70+
71+
describe('authorable-surface.base.json is not older than the newest aged-out deletion (#27015)', () => {
72+
it('reads a real anchor and a real surface', () => {
73+
expect(anchor.baseRev).toMatch(/^[0-9a-f]{40}$/);
74+
expect(anchor.keys.length).toBeGreaterThan(0);
75+
expect(liveKeys.size).toBeGreaterThan(0);
76+
});
77+
78+
it('records no deleted tombstone that only the dating release tag can date', () => {
79+
const tagDatedOnly = anchor.keys
80+
.filter((e) => e.endsWith(RETIRED_MARK))
81+
.map((e) => carryAuthorableKey(e.replace(RETIRED_MARK, '')))
82+
.filter((key) => !liveKeys.has(key) && liveDefs.has(key.slice(0, key.indexOf(':'))))
83+
.filter((key) => {
84+
const major = registeredAt.get(key);
85+
return major === undefined || CURRENT_MAJOR - major < TOMBSTONE_AGE_MAJORS;
86+
});
87+
88+
expect(
89+
tagDatedOnly,
90+
`The anchor (baseRev ${anchor.baseRev.slice(0, 12)}) still records ${tagDatedOnly.length} [RETIRED] ` +
91+
`line(s) that the live surface has dropped and that RETIRED_KEYS_BY_MAJOR does not date. Online, ` +
92+
`the release tag @objectstack/spec@${CURRENT_MAJOR - TOMBSTONE_AGE_MAJORS}.0.0 dates them; a build ` +
93+
`with no route to origin/main has no tag, so its spec build fails the #4650 deletion gate on these ` +
94+
`lines (#27015). Once the deletion is on origin/main, re-anchor in a commit of its own from a branch ` +
95+
`cut there: \`pnpm --filter @objectstack/spec gen:authorable-surface-base\`. In the PR that deletes ` +
96+
`the lines, the anchor cannot move yet; date each key there instead with an exact entry under ` +
97+
`src/migrations/entries/retired-keys/ at its true major.`,
98+
).toEqual([]);
99+
});
100+
});

0 commit comments

Comments
 (0)