Skip to content

Commit 0d43004

Browse files
fix(spec): the default-change ratchet authorises from the baseline major, so the version branch major roll stops refusing rows declared under 17 (#23123)
Fixes #23113 Clause-②: no Unblocks #21988, the Version Packages PR for `18.0.0-next.0`. The release branch is not edited, and neither the smoke nor any workflow is touched. ## The failing line I read it from the job log of run `38323903662`, job `115058075855` (`Packed-tarball smoke (release candidate)`, step 10 `Build`). The tree is `changeset-release/main` at `9a73a93f1`, with `@objectstack/spec@18.0.0-next.0`: ```text ❌ 10 authorable key(s) changed the DEFAULT they apply when the author omits them: - api/InstallPackageRequest:enableOnInstall: true → (none) (removed) - api/PackageInstallRequest:enableOnInstall: true → (none) (removed) - data/Cube:public: false → true (changed) - kernel/InstallPackageRequest:enableOnInstall: true → (none) (removed) - marketplace/MarketplaceInstallRequest:enableOnInstall: true → (none) (removed) - marketplace/Package:visibility: "private" → "org" (changed) - system/MetricsConfig:slis: (none) → [] (added) - system/TracingConfig:sampling: (none) → {"rules":[],"type":"always_on"} (added) - ui/PaginationConfig:pageSize: 25 → 50 (changed) - ui/UserActionsConfig:editInline: false → true (changed) Baseline: upstream 85c6d76 ``` - The exit site is `packages/spec/scripts/build-schemas.ts:3352` on `9a73a93f1`: the default-change refusal, with its message at `:3317`. - The exit is not silent. The message was printed, but it sits at log lines 2371–2466, after turbo's own `ERROR … exited (1)` lines at 1639–1641. The rest of the task's output is flushed after turbo reports the failure, so reading only from `[System]` to turbo's error line shows no error. ## Root cause - `authoriseDefaultChanges` (`packages/spec/scripts/lib/authorable-defaults.ts:392`) admitted `DEFAULT_CHANGES_BY_MAJOR` rows only at `CURRENT_MAJOR`, which is `Number.parseInt` of `package.json`'s version (`build-schemas.ts:767`). - The stated premise was: "By the time a major rolls, the baseline already carries the new value." - That premise fails on the version branch: - The smoke checks out at depth 1, so `merge-base HEAD origin/main` cannot answer. The gate re-anchors on `baseRev` `85c6d76ec42a`, at spec `17.4.0`. That anchor is allowed to lag. - The version pass rolls the branch to major 18. - The ten keys main changed after `85c6d76ec42a` are all declared under `17:` in `scripts/lib/default-changes.ts`. - A window holding only major 18 has no rows, so all ten read as undeclared. - **Control: same anchor, same mechanism, one major earlier.** - The last green smoke, run `38247570907` (12:12Z), built `17.8.0` (`HEAD is now at d00f5f2 chore: version packages`). It re-anchored on the same `85c6d76ec42a` and printed `📌 9 declared default change(s) accepted (#4666)`. - The first red, `38248923046` (12:25Z), built `18.0.0-next.0` (`d85ff7c chore: version packages (next)`), on the same `main` sha `4267484fe5`. - The tenth key, `ui/UserActionsConfig:editInline`, landed on main in between. - **Not the prerelease label.** `Number.parseInt('18.0.0-next.0', 10)` is `18`, which is correct. The input that changed is the major itself. ## The fix The authorising window now runs from the **baseline tree's own protocol major** up to this build's major (`authorisingMajors`): - **Where the baseline major comes from.** `readProtocolMajorAtRev` reads it from `package.json` at the baseline rev, the same rev the baseline defaults are read from. The in-tree baseline is this tree, so it uses this build's major. - **Chaining.** Each major's rows chain in ascending order, so contiguity and the chain-tip rule still apply across the roll. Rows older than the baseline's major still authorise nothing. - **Fallbacks.** If the baseline major is unreadable, the window falls back to this build's major alone, which is the old and narrowest window, and the build says so. If the baseline is at a newer major than the build, the window is this build's major alone. - **Visibility.** Whenever the window spans more than one major, the build prints it. - **On `main`** (baseline 17, build 17) the window is `[17]`, the same as before this change. Where the change lands: `packages/spec/scripts/lib/authorable-defaults.ts`, `packages/spec/scripts/build-schemas.ts`, `packages/spec/scripts/lib/default-changes.ts` (header comment only) and `packages/spec/scripts/authorable-defaults.test.ts`. - The producer is the `gen:schema` script's own library under `scripts/lib/`, which falls inside the declared surface ("the spec build's gen:* scripts"). - No `packages/spec/src/**` file is touched. - `build-openapi.ts` needed no change: the spec build passes through it in proof (b). ## Proofs All runs went through `scripts/pm/os-verify-lock.sh`. **(a) The failure reproduces on `9a73a93f1`, unmodified.** - Setup: a fresh `git clone --depth=1 --branch changeset-release/main`, the same shape as the smoke's checkout (HEAD `9a73a93f1c56`, shallow). - Command: `pnpm --filter @objectstack/spec run build`. - Result: `VERDICT command-exit 1`. The run re-anchors (`shallow history … anchors on 85c6d76`), then prints `❌ 10 authorable key(s) changed the DEFAULT…` with the same ten keys and `Baseline: upstream 85c6d76`. **(b) The same tree passes with this fix applied locally, uncommitted.** - Setup: the patch `git diff 21c8644 7f04259` applied with `git apply` to the same clone. The tree shows four ` M` paths and HEAD is still `9a73a93f1`. - Spec build: `pnpm --filter @objectstack/spec run build` gives `VERDICT command-exit 0`. It prints: - `ℹ️ authorable-defaults/ baseline (upstream 85c6d76) was read at protocol major 17, and this build is major 18, so DEFAULT_CHANGES_BY_MAJOR rows at majors 17–18 authorise` - `📌 10 declared default change(s) accepted` - then `gen:openapi`, tsup and DTS, `check-dts-emitted` (38/38), `check-dts-references`, and the dev-prereqs stamp, all passing. - Whole-repo build: the smoke's `Build` step (`turbo run build --filter=!@objectstack/docs`, run here at `--concurrency=2`) gives `Tasks: 73 successful, 73 total`, `VERDICT command-exit 0`. - After the build, the tree carries no tracked change beyond the four patched files. **(c) `main` still builds `@objectstack/spec` green**, on this branch (`7f04259ce`, off `main` `21c86444e`). - Command: `pnpm --filter @objectstack/spec run build` gives `VERDICT command-exit 0`, with `🔒 authorable-defaults/ verified against upstream 21c8644 — 1121 default(s) unchanged`. The window is a single major, so the notice is not printed. - `git status` is clean afterwards. **Ablation** (the fix was committed first; done with `scripts/ablation-replace.mjs`): - Mutation: `for (const major of authorisingMajors(majors))` replaced by `for (const major of [majors.current])`. The mutation landed: anchor 1 → 0, blob `68f3f7ea8795` → `b2f5507f9afb`. - Result: `authorable-defaults.test.ts` gives `3 failed | 33 passed (36)`. The three failures are the rolled-major acceptance, the cross-roll chain and the cross-roll chain-tip tests. The red direction was expected. - Restore: `blob == HEAD (68f3f7e)`, `git diff HEAD` empty. ## Tests and gates (all at `7f04259ce`) - `pnpm --filter @objectstack/spec exec vitest run --project local scripts/authorable-defaults.test.ts`: 36 passed: the 27 existing tests (their 10 `authoriseDefaultChanges` calls now pass the majors object), plus 9 new tests covering the window, the roll and `readProtocolMajorAtRev`. - The spec `local` suite: `Test Files 659 passed (659)`, `Tests 19996 passed | 1 todo`. - `pnpm --filter @objectstack/spec run typecheck`: exit 0. That covers `tsc --noEmit`, `check:scripts-typecheck` and `check:test-typecheck`. `tsc -p tsconfig.scripts.json --listFiles` lists all four changed files. - Gate commands: 80 in total. That is the union of the 74 derived at dispatch and the 62 derived on this branch, with 56 in both. Every final exit is 0. - `node scripts/pm/dispatch-gates.mjs --ran` answers `✓ 62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED`. - Three gates first exited 3 (PREREQUISITE NOT MET). Each was re-measured green after its prerequisite was supplied: - `check-plugin-teardown-shape --self-test`: its pinned fixture commit was absent from the shallow clone. That one commit was fetched. - `check:dual-build-cjs-loads` and `check:lean-entry-closure`: no package `dist/`. The repo was built first (73 tasks). - Lint, narrowed to the changed files: eslint `--no-inline-config --format json` on the four files reports 4 results, 0 errors, 0 warnings, and no file is ignored. `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`), so this diff cannot move a verdict on any untouched file. The repo-wide `pnpm lint` is CI's to run. ## Changeset: `skip-changeset` The diff is confined to `packages/spec/scripts/**`, which is not in `packages/spec`'s `files[]`. After the build, the shipped paths `dist`, `json-schema`, `liveness`, `prompts`, `llms.txt`, `README.md`, `CHANGELOG.md`, `api-surface` and `src/**/*.zod.ts` were grepped: - `authorisingMajors`, `readProtocolMajorAtRev`, `DefaultChangeMajors` and `authoriseDefaultChanges` have 0 hits. - The positive control, `defineStack`, hits 86 files in `dist`. The change decides the gate's verdict and its console output. It changes no emitted artifact, and the tracked artifacts are unchanged after the build. ## Pin **The next Publish Smoke after the fix lands is green on the release branch.** The release refresh carries this fix onto `changeset-release/main`, and the seat reads that run after landing. Locally, the smoke steps after `Build` (pack, fresh install, auth and CRUD) were not run, so they are NOT MEASURED here. ## Acceptance notes - Inferred, not measured: once #21988 lands, `main` itself would be at major 18 while `baseRev` still names `85c6d76ec42a` (17.4.0). Every depth-1 build of `packages/spec` would then re-anchor and reach the same refusal. The fix covers that case by the same mechanism. - Environment observation, not filed: under a checkout path deep enough that tsx's IPC socket path passes the 108-byte Unix limit, `tsx` fails with `EADDRINUSE`. A repro clone under the agent scratchpad hit this during `pnpm install`'s `prepare`. A shorter path cleared it. No public door is affected. --- _Generated by [Claude Code](https://claude.ai/code/session_012zwsLMxQBwHUYJgPyrpEvx)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent e5f9f01 commit 0d43004

4 files changed

Lines changed: 252 additions & 29 deletions

File tree

‎packages/spec/scripts/authorable-defaults.test.ts‎

Lines changed: 123 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { describe, expect, it } from 'vitest';
3030
import {
3131
NO_DEFAULT,
3232
authorableDefaultsShardTexts,
33+
authorisingMajors,
3334
authoriseDefaultChanges,
3435
carryDefaultsThroughRenames,
3536
collectAuthorableDefaults,
@@ -38,9 +39,11 @@ import {
3839
formatDefaultEntry,
3940
parseDefaultEntries,
4041
parseDefaultEntry,
42+
readProtocolMajorAtRev,
4143
type DeclaredDefaultChange,
4244
} from './lib/authorable-defaults';
4345
import { DEFAULT_CHANGES_BY_MAJOR } from './lib/default-changes';
46+
import type { GitRun } from './lib/sharded-artifacts';
4447

4548
/** Authorable keys as the gate models them: `key -> isRetired`. */
4649
const live = (...keys: string[]) => new Map(keys.map((k) => [k, false] as const));
@@ -270,6 +273,8 @@ describe('carryDefaultsThroughRenames — a declared def rename moves no default
270273
});
271274

272275
describe('authoriseDefaultChanges — the acknowledged-change exit stays honest', () => {
276+
/** A baseline read at the build's own major — the window is that major alone. */
277+
const AT_17 = { baseline: 17, current: 17 } as const;
273278
const change = { key: RETRY, kind: 'changed', from: '0', to: '3' } as const;
274279
const declare = (over: Partial<DeclaredDefaultChange> = {}): DeclaredDefaultChange => ({
275280
key: RETRY,
@@ -280,7 +285,7 @@ describe('authoriseDefaultChanges — the acknowledged-change exit stays honest'
280285
});
281286

282287
it('refuses a change nobody declared', () => {
283-
const out = authoriseDefaultChanges([change], {}, 17, new Map([[RETRY, '3']]), live(RETRY));
288+
const out = authoriseDefaultChanges([change], {}, AT_17, new Map([[RETRY, '3']]), live(RETRY));
284289
expect(out.unauthorised).toEqual([change]);
285290
expect(out.authorised).toEqual([]);
286291
});
@@ -289,7 +294,7 @@ describe('authoriseDefaultChanges — the acknowledged-change exit stays honest'
289294
const out = authoriseDefaultChanges(
290295
[change],
291296
{ 17: [declare()] },
292-
17,
297+
AT_17,
293298
new Map([[RETRY, '3']]),
294299
live(RETRY),
295300
);
@@ -304,7 +309,7 @@ describe('authoriseDefaultChanges — the acknowledged-change exit stays honest'
304309
const out = authoriseDefaultChanges(
305310
[change],
306311
{ 17: [declare({ from: '1', to: '3' })] },
307-
17,
312+
AT_17,
308313
new Map([[RETRY, '3']]),
309314
live(RETRY),
310315
);
@@ -315,7 +320,7 @@ describe('authoriseDefaultChanges — the acknowledged-change exit stays honest'
315320
const out = authoriseDefaultChanges(
316321
[change],
317322
{ 16: [declare()] },
318-
17,
323+
AT_17,
319324
new Map([[RETRY, '3']]),
320325
live(RETRY),
321326
);
@@ -327,12 +332,12 @@ describe('authoriseDefaultChanges — the acknowledged-change exit stays honest'
327332
// The property that separates this from an allowlist. The default was moved
328333
// again (or reverted) and the row was left behind — it now pre-approves a
329334
// value nobody wrote down, so it goes red instead of covering it.
330-
const reverted = authoriseDefaultChanges([], { 17: [declare()] }, 17, new Map([[RETRY, '0']]), live(RETRY));
335+
const reverted = authoriseDefaultChanges([], { 17: [declare()] }, AT_17, new Map([[RETRY, '0']]), live(RETRY));
331336
expect(reverted.stale).toEqual([
332337
{ key: RETRY, claims: '3', emits: '0', why: 'chain-tip-mismatch' },
333338
]);
334339

335-
const movedAgain = authoriseDefaultChanges([], { 17: [declare()] }, 17, new Map([[RETRY, '5']]), live(RETRY));
340+
const movedAgain = authoriseDefaultChanges([], { 17: [declare()] }, AT_17, new Map([[RETRY, '5']]), live(RETRY));
336341
expect(movedAgain.stale[0]).toMatchObject({ claims: '3', emits: '5' });
337342
});
338343

@@ -344,7 +349,7 @@ describe('authoriseDefaultChanges — the acknowledged-change exit stays honest'
344349
const fromStart = authoriseDefaultChanges(
345350
[{ key: RETRY, kind: 'changed', from: '0', to: '5' }],
346351
{ 17: chain },
347-
17,
352+
AT_17,
348353
live5,
349354
live(RETRY),
350355
);
@@ -355,7 +360,7 @@ describe('authoriseDefaultChanges — the acknowledged-change exit stays honest'
355360
const fromMiddle = authoriseDefaultChanges(
356361
[{ key: RETRY, kind: 'changed', from: '3', to: '5' }],
357362
{ 17: chain },
358-
17,
363+
AT_17,
359364
live5,
360365
live(RETRY),
361366
);
@@ -365,7 +370,7 @@ describe('authoriseDefaultChanges — the acknowledged-change exit stays honest'
365370
const gapped = authoriseDefaultChanges(
366371
[],
367372
{ 17: [declare({ from: '0', to: '3' }), declare({ from: '4', to: '5' })] },
368-
17,
373+
AT_17,
369374
live5,
370375
live(RETRY),
371376
);
@@ -375,11 +380,119 @@ describe('authoriseDefaultChanges — the acknowledged-change exit stays honest'
375380
it('leaves a declaration about a key this build no longer emits alone', () => {
376381
// The expected end state once the key is retired — the surface gate owns
377382
// that verdict, exactly as check (b2) does for RETIRED_KEYS_BY_MAJOR.
378-
const out = authoriseDefaultChanges([], { 17: [declare()] }, 17, new Map(), new Map());
383+
const out = authoriseDefaultChanges([], { 17: [declare()] }, AT_17, new Map(), new Map());
379384
expect(out.stale).toEqual([]);
380385
});
381386
});
382387

388+
describe('the authorising window spans a major roll — anchored on the BASELINE major', () => {
389+
// The version pass rolls `package.json` to the next major inside the branch
390+
// under test, and a shallow checkout's re-anchored baseline lags by design.
391+
// Both put the baseline at an OLDER major than the build: the shape of the
392+
// `18.0.0-next.0` version branch, where every change main had declared under
393+
// 17 was refused as undeclared by a window of major 18 alone.
394+
const change = { key: RETRY, kind: 'changed', from: '0', to: '3' } as const;
395+
const declare = (over: Partial<DeclaredDefaultChange> = {}): DeclaredDefaultChange => ({
396+
key: RETRY,
397+
from: '0',
398+
to: '3',
399+
reason: 'retry became opt-out; write `maxRetries: 0` to keep the old behaviour',
400+
...over,
401+
});
402+
const ROLLED = { baseline: 17, current: 18 } as const;
403+
404+
it('runs from the baseline major up to the build major, and never below the build major alone', () => {
405+
expect(authorisingMajors({ baseline: 17, current: 17 })).toEqual([17]);
406+
expect(authorisingMajors({ baseline: 17, current: 18 })).toEqual([17, 18]);
407+
expect(authorisingMajors({ baseline: 15, current: 18 })).toEqual([15, 16, 17, 18]);
408+
// A branch that has not merged a roll main already made: this build's major only.
409+
expect(authorisingMajors({ baseline: 18, current: 17 })).toEqual([17]);
410+
});
411+
412+
it('refuses a major it cannot read rather than guessing a window', () => {
413+
expect(() => authorisingMajors({ baseline: Number.NaN, current: 18 })).toThrow(/integers/);
414+
expect(() => authorisingMajors({ baseline: 17, current: 17.5 })).toThrow(/integers/);
415+
});
416+
417+
it('accepts a row from the baseline major when the build has rolled past it', () => {
418+
const out = authoriseDefaultChanges([change], { 17: [declare()] }, ROLLED, new Map([[RETRY, '3']]), live(RETRY));
419+
expect(out.unauthorised).toEqual([]);
420+
expect(out.stale).toEqual([]);
421+
expect(out.authorised[0].declared[0].reason).toContain('opt-out');
422+
});
423+
424+
it('still refuses the same row once the baseline itself has rolled — history stays history', () => {
425+
const out = authoriseDefaultChanges(
426+
[change],
427+
{ 17: [declare()] },
428+
{ baseline: 18, current: 18 },
429+
new Map([[RETRY, '3']]),
430+
live(RETRY),
431+
);
432+
expect(out.unauthorised).toEqual([change]);
433+
});
434+
435+
it('authorises nothing from a major OLDER than the baseline', () => {
436+
const out = authoriseDefaultChanges([change], { 16: [declare()] }, ROLLED, new Map([[RETRY, '3']]), live(RETRY));
437+
expect(out.unauthorised).toEqual([change]);
438+
expect(out.stale).toEqual([]);
439+
});
440+
441+
it('chains hops across the roll in major order, and they must still meet', () => {
442+
const live5 = new Map([[RETRY, '5']]);
443+
const spanning = { 17: [declare({ from: '0', to: '3' })], 18: [declare({ from: '3', to: '5' })] };
444+
const fromStart = authoriseDefaultChanges(
445+
[{ key: RETRY, kind: 'changed', from: '0', to: '5' }],
446+
spanning,
447+
ROLLED,
448+
live5,
449+
live(RETRY),
450+
);
451+
expect(fromStart.unauthorised).toEqual([]);
452+
expect(fromStart.authorised[0].declared.map((hop) => hop.to)).toEqual(['3', '5']);
453+
454+
const gapped = authoriseDefaultChanges(
455+
[],
456+
{ 17: [declare({ from: '0', to: '3' })], 18: [declare({ from: '4', to: '5' })] },
457+
ROLLED,
458+
live5,
459+
live(RETRY),
460+
);
461+
expect(gapped.stale.map((s) => s.why)).toContain('chain-not-contiguous');
462+
});
463+
464+
it('holds a carried row to the chain-tip rule too — it dies when it stops being true', () => {
465+
const reverted = authoriseDefaultChanges([], { 17: [declare()] }, ROLLED, new Map([[RETRY, '0']]), live(RETRY));
466+
expect(reverted.stale).toEqual([{ key: RETRY, claims: '3', emits: '0', why: 'chain-tip-mismatch' }]);
467+
});
468+
});
469+
470+
describe('readProtocolMajorAtRev — the baseline major is read out of git at the baseline rev', () => {
471+
const REV = '85c6d76ec42a879f219436eaa58bda06c68298b2';
472+
const gitAnswering = (status: number, stdout: string) => {
473+
const calls: string[][] = [];
474+
const git: GitRun = (...args) => {
475+
calls.push(args);
476+
return { status, stdout, stderr: '' };
477+
};
478+
return { git, calls };
479+
};
480+
481+
it('reads package.json at that rev, prerelease label included', () => {
482+
const { git, calls } = gitAnswering(0, JSON.stringify({ name: '@objectstack/spec', version: '18.0.0-next.0' }));
483+
expect(readProtocolMajorAtRev(git, REV)).toBe(18);
484+
expect(calls).toEqual([['show', `${REV}:./package.json`]]);
485+
expect(readProtocolMajorAtRev(gitAnswering(0, '{"version":"17.4.0"}').git, REV)).toBe(17);
486+
});
487+
488+
it('answers null — never a guessed major — when the file or its version is unreadable', () => {
489+
expect(readProtocolMajorAtRev(gitAnswering(128, '').git, REV)).toBeNull();
490+
expect(readProtocolMajorAtRev(gitAnswering(0, 'not json').git, REV)).toBeNull();
491+
expect(readProtocolMajorAtRev(gitAnswering(0, '{"name":"@objectstack/spec"}').git, REV)).toBeNull();
492+
expect(readProtocolMajorAtRev(gitAnswering(0, '{"version":"next"}').git, REV)).toBeNull();
493+
});
494+
});
495+
383496
describe('the recorded artifact round-trips', () => {
384497
it('serialises and re-parses to the same fingerprints', () => {
385498
const defaults = new Map([

‎packages/spec/scripts/build-schemas.ts‎

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,13 @@ import {
9595
import {
9696
AUTHORABLE_DEFAULTS_DIR_NAME,
9797
authorableDefaultsShardTexts,
98+
authorisingMajors,
9899
authoriseDefaultChanges,
99100
carryDefaultsThroughRenames,
100101
collectAuthorableDefaults,
101102
diffAuthorableDefaults,
102103
parseDefaultEntries,
104+
readProtocolMajorAtRev,
103105
} from './lib/authorable-defaults';
104106
import { DEFAULT_CHANGES_BY_MAJOR } from './lib/default-changes';
105107
// The nested half of check (b2)'s question (#17969). Its own module because the
@@ -3215,8 +3217,18 @@ try {
32153217
// staleness check below runs unconditionally, so a tree with no
32163218
// authorable-defaults/ fails there in `--check` mode whatever this block
32173219
// decided. Which anchor was used is PRINTED, every run.
3218-
let defaultsBaseline: { defaults: Map<string, string>; keys: Map<string, boolean>; label: string } | null =
3219-
null;
3220+
//
3221+
// Each baseline also carries the protocol MAJOR of the tree it was read from:
3222+
// that major, not this build's, is where the authorising window of
3223+
// DEFAULT_CHANGES_BY_MAJOR starts (`authorisingMajors`, #23113). The upstream
3224+
// major is read out of git at the same rev as its defaults; the in-tree
3225+
// baseline IS this tree, so its major is this build's.
3226+
let defaultsBaseline: {
3227+
defaults: Map<string, string>;
3228+
keys: Map<string, boolean>;
3229+
label: string;
3230+
major: number;
3231+
} | null = null;
32203232
if (resolvedSurfaceBase) {
32213233
const upstream = readSurfaceKeysAtRev(
32223234
gitInPackage,
@@ -3234,10 +3246,19 @@ if (resolvedSurfaceBase) {
32343246
for (const entry of resolvedSurfaceBase.doc.keys ?? []) {
32353247
keys.set(carryAuthorableKey(entry.replace(RETIRED_MARK, '')), entry.endsWith(RETIRED_MARK));
32363248
}
3249+
const upstreamMajor = readProtocolMajorAtRev(gitInPackage, resolvedSurfaceBase.rev);
3250+
if (upstreamMajor === null) {
3251+
console.log(
3252+
`ℹ️ ${DEFAULTS_DIR_LABEL}: no protocol major is readable from package.json at ${resolvedSurfaceBase.rev.slice(0, 12)},\n` +
3253+
` so only DEFAULT_CHANGES_BY_MAJOR rows at this build's major ${CURRENT_MAJOR} authorise this run —\n` +
3254+
` the narrowest window; it can refuse a declared change, never bless an undeclared one.`,
3255+
);
3256+
}
32373257
defaultsBaseline = {
32383258
defaults: carryDefaultsThroughRenames(parseDefaultEntries(upstream.entries)),
32393259
keys,
32403260
label: `upstream ${resolvedSurfaceBase.rev.slice(0, 12)}`,
3261+
major: upstreamMajor ?? CURRENT_MAJOR,
32413262
};
32423263
}
32433264
}
@@ -3250,6 +3271,7 @@ if (!defaultsBaseline && committedDefaults && surfaceDoc) {
32503271
defaults: carryDefaultsThroughRenames(committedDefaults),
32513272
keys,
32523273
label: 'in-tree (this commit owns these bytes — no upstream baseline was reachable)',
3274+
major: CURRENT_MAJOR,
32533275
};
32543276
}
32553277

@@ -3261,6 +3283,22 @@ if (!defaultsBaseline) {
32613283
` \`check:authorable-surface\` regardless of what this block decided.`,
32623284
);
32633285
} else {
3286+
const majors = { baseline: defaultsBaseline.major, current: CURRENT_MAJOR };
3287+
const authorising = authorisingMajors(majors);
3288+
const windowLabel =
3289+
authorising.length === 1
3290+
? `major ${authorising[0]}`
3291+
: `majors ${authorising[0]}–${authorising[authorising.length - 1]}`;
3292+
if (authorising.length > 1) {
3293+
// Printed whenever the window is wider than this build's own major, so a
3294+
// row from an earlier major that authorises below never does so unannounced.
3295+
console.log(
3296+
`\nℹ️ ${DEFAULTS_DIR_LABEL} baseline (${defaultsBaseline.label}) was read at protocol major ${defaultsBaseline.major},\n` +
3297+
` and this build is major ${CURRENT_MAJOR}, so DEFAULT_CHANGES_BY_MAJOR rows at ${windowLabel} authorise:\n` +
3298+
` a change that landed after the baseline was declared under the major it landed in, and\n` +
3299+
` this baseline does not carry it yet. Rows older than major ${authorising[0]} stay the historical record.`,
3300+
);
3301+
}
32643302
const changes = diffAuthorableDefaults({
32653303
baseline: defaultsBaseline.defaults,
32663304
current: currentDefaults,
@@ -3270,7 +3308,7 @@ if (!defaultsBaseline) {
32703308
const { authorised, unauthorised, stale } = authoriseDefaultChanges(
32713309
changes,
32723310
DEFAULT_CHANGES_BY_MAJOR,
3273-
CURRENT_MAJOR,
3311+
majors,
32743312
currentDefaults,
32753313
currentKeys,
32763314
);
@@ -3281,7 +3319,7 @@ if (!defaultsBaseline) {
32813319
// change below looks unauthorised.
32823320
if (stale.length > 0) {
32833321
console.error(
3284-
`\n❌ ${stale.length} DEFAULT_CHANGES_BY_MAJOR declaration(s) at major ${CURRENT_MAJOR} no longer describe reality:`,
3322+
`\n❌ ${stale.length} DEFAULT_CHANGES_BY_MAJOR declaration(s) at ${windowLabel} no longer describe reality:`,
32853323
);
32863324
for (const s of stale) {
32873325
console.error(

0 commit comments

Comments
 (0)