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
179 changes: 171 additions & 8 deletions scripts/git-merge-regen.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,62 @@ import {
import { blankAnchorLineNumbers } from './doc-line-anchors.mjs';
import { workspacePackages } from './workspace-enumerator.mjs';

/**
* The ROOT manifest, in the SUBTREE spelling `scripts/pm/dispatch-gates.mjs`
* can match (#15501).
*
* `reconcileGenerators` and `reconcileScripts` read `gen:` / `check:` rows out
* of the MANIFESTS -- the root one, plus every workspace member's -- and that
* population was declared nowhere. What this family DID declare is the artifact
* paths `scripts/regen-artifacts.mjs` carries, imported one level down as its
* hints: the artifacts ALREADY routed. A card that ADDS a generator touches a
* manifest and a new `scripts/*.mjs`, and neither is in that population until
* the card lands the very row it is being asked to add. So the one class of
* change the "no recorded merge disposition" refusal exists to catch was the
* one class the dispatch derivation could not name, and the gate fired a cycle
* late, in CI, on every card of that shape. Measured on `615fac3a0`, before
* this declaration existed:
*
* node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -- package.json
* -> 0 lines naming check:merge-driver
*
* ⛔ The fix is the POPULATION, never a hand list of generator names: that
* ledger is `scripts/regen-artifacts.mjs`'s and this gate already owns the
* two-way reconciliation over it.
*
* A bare `package.json` carries no path separator and `hintCovers` refuses it
* as too generic; the trailing `/` + `**` suffix collapses back to the literal
* filename and matches it exactly -- `check-workspace-manifest-cycles.mjs`'s
* idiom for `pnpm-workspace.yaml` and `check-turbo-task-graph.mjs`'s for
* `turbo.json`. `reconcileManifestPopulation` pins the exact string, because
* every other signal this gate emits stays green when it is reworded back.
*/
const ROOT_FILE_WATCH_HINTS = ['package.json/**'];

/**
* The MEMBER manifests this gate opens -- one per workspace package, which is
* exactly the list `workspacePackages(REPO_ROOT)` hands `reconcileGenerators`
* -- spelled as full-path glob LITERALS, never built from a `SCAN_ROOT`-shaped
* constant (the bare-top-level-dir species `scripts/pm/bare-root-worklist.mjs`
* records as unjudged). The three roots and their spelling follow
* `check-workspace-manifest-cycles.mjs`, which declares the identical
* population for the identical reason, and they are held against the
* enumerator's LIVE answer in both directions by `reconcileManifestPopulation`
* below -- so a workspace root this repo grows cannot leave the declaration
* behind in silence.
*
* ⚠️ Each of these carries its glob in a NON-FINAL segment, so `hintCovers`
* judges it as a PATTERN: it reaches member manifests and nothing else, never
* the thousands of files under those roots. That is the distinction between
* this declaration and the whole-subtree widening
* `check-pnpm-filter-targets.mjs` refuses for its own population.
*/
const DECLARED_WATCH_HINTS = [
'packages/**/package.json',
'apps/**/package.json',
'examples/**/package.json',
];

/**
* The comparators a row's `mixed` field may name (#14064).
*
Expand Down Expand Up @@ -288,7 +344,7 @@ function drive(argv) {
// ── Why the CALLEE NAME is the battery ──
//
// This file has no `selfTest()` entry function and no named section banners:
// the `--self-test` dispatch at the bottom invokes ELEVEN named callees, each
// the `--self-test` dispatch at the bottom invokes TWELVE named callees, each
// printing its own line and returning a boolean. So the roster's unit is the
// CALLEE, and its label is the one the SOURCE ALREADY CARRIES — the function's
// own name. Nothing is invented and nothing is judged per comment, and a set
Expand All @@ -303,10 +359,10 @@ function drive(argv) {
// (PR #15271, `check-sdui-manifest`) makes a table row a battery. It does so
// for a file whose SELF-TEST *is* the table: one literal table, one driving
// loop over it, and a sink that writes only when a row fails. Here the table is
// a local of ONE callee among eleven, its rows are evaluated eagerly into
// a local of ONE callee among twelve, its rows are evaluated eagerly into
// booleans before anything loops, and the callee already reduces them to a
// single printed verdict of its own. Flooring those rows would floor one
// callee's internals while the other ten stayed at callee granularity — a
// callee's internals while the other eleven stayed at callee granularity — a
// roster whose unit changes per entry. The rule: the battery is the unit the
// DISPATCH names.
//
Expand All @@ -321,6 +377,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
reconcileAttributeSemantics: 1,
reconcileScripts: 1,
reconcileGenerators: 1,
reconcileManifestPopulation: 1,
reconcileUntrackedDispositions: 1,
reconcileOwnership: 1,
hookIsExecutable: 1,
Expand All @@ -336,7 +393,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
// key in the literal above, so the roster falls below this number; the
// roster ↔ dispatch cross-check in the floor block is the other half, and it
// names WHICH callee was listed twice.
const SELF_TEST_BATTERY_FLOOR = 11;
const SELF_TEST_BATTERY_FLOOR = 12;

// The key a registration is filed under when a callee registers no name at all.
// It is not a declared battery, so it reds by the same set difference rather
Expand All @@ -346,7 +403,7 @@ const UNATTRIBUTED_BATTERY = '(no callee named)';
// The battery ledger, read by `batteryFloorFailures()` from the dispatch block
// at the very bottom of this file. It is MODULE-level rather than local to a
// self-test body because this file HAS no self-test body: the registrations
// happen inside eleven separate callees and the floor is read at the dispatch's
// happen inside twelve separate callees and the floor is read at the dispatch's
// verdict site, so the ledger has to outlive every one of those frames.
//
// ⚠️ Named for the roster's role, deliberately NOT with a self-test spelling:
Expand All @@ -358,7 +415,7 @@ const batterySeen = new Map();
/**
* Record that a self-test callee RAN.
*
* Called as the FIRST statement of each of the eleven callees the `--self-test`
* Called as the FIRST statement of each of the twelve callees the `--self-test`
* dispatch invokes — above any early return, so a callee that bails out early
* still reports that it ran, and the floor is never met by a frame that
* returned before doing anything.
Expand All @@ -371,7 +428,7 @@ function registerCase(name) {
/**
* The floor: every declared callee RAN (#13489).
*
* Evaluated at the dispatch's verdict site — after all eleven callees have had
* Evaluated at the dispatch's verdict site — after all twelve callees have had
* their chance and immediately before the success line — and reached only from
* the `--self-test` branch, so a production merge-driver run never reads the
* ledger at all.
Expand Down Expand Up @@ -677,6 +734,111 @@ function reconcileGenerators() {
return ok;
}

/**
* The two watch-hint declarations above, held against the manifest population
* `reconcileGenerators` really reads (#15501).
*
* Nothing else in this repo can redden when they go wrong. They are read by
* ANOTHER tool entirely -- `extractWatchHints` in
* `scripts/pm/dispatch-gates.mjs` scans this file's SOURCE TEXT -- so a wrong,
* stale or re-computed declaration runs green here forever and pays itself out
* as a dev dispatched on a new-generator card with this gate missing from the
* brief, which is the whole failure this declaration was added to end. Same
* reason `check-pnpm-filter-targets.mjs` pins its own, and the same discipline
* `check-workspace-manifest-cycles.mjs` applies to the identical population.
*
* The population is derived through the SAME two functions the gate uses --
* `workspacePackages` and `manifestFor` -- never re-spelled here, so a moved
* read cannot leave this reconciliation agreeing with a list nobody opens.
*/
function reconcileManifestPopulation() {
registerCase('reconcileManifestPopulation');

// `**` crosses separators; every other glob character stays within one
// segment. Local rather than shared for the reason `check-watch-hint-literal`
// gives for not importing the files it judges: a gate that pulled in the
// derivation to borrow one predicate would import 20k lines of another tool's
// module body -- and, here, hand this family every path literal in it.
const patternMatches = (pattern, path) => {
const segs = pattern.split('/');
let rx = '';
for (let i = 0; i < segs.length; i++) {
if (segs[i] === '**') {
rx += '(?:[^/]+/)*';
continue;
}
rx += segs[i].replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*');
if (i < segs.length - 1) rx += '/';
}
return new RegExp(`^${rx}$`).test(path);
};

const problems = [];
const ok = (label, cond) => {
if (!cond) problems.push(label);
};

// ── The ROOT manifest, pinned as BYTES ──
// A reword back to the bare filename is invisible in every other signal:
// production green, CI green, and the only thing lost is that a card adding a
// `gen:` row to the root manifest can name this gate at all.
ok(
'the root manifest is declared in the SUBTREE spelling (hintCovers refuses the bare filename as too generic)',
ROOT_FILE_WATCH_HINTS.join(',') === 'package.json/**',
);
// …and the literal is tied to the real read rather than typed twice: the
// declaration's collapsed form must be the path `reconcileGenerators` opens
// for the root owner.
ok(
'the collapsed root literal is the path this gate actually opens for the root owner',
ROOT_FILE_WATCH_HINTS[0].replace(/\/\*+$/, '') === manifestFor('.'),
);
ok(
'every declared entry carries a path separator',
[...ROOT_FILE_WATCH_HINTS, ...DECLARED_WATCH_HINTS].every((h) => h.includes('/')),
);

// ── The MEMBER manifests, against the enumerator's LIVE answer ──
// Both directions, mirroring `check-workspace-manifest-cycles.mjs`: a pattern
// covering nothing is a fabricated lead on every card, and a manifest no
// pattern covers is the undeclared read this gate would otherwise ship with.
let memberManifests;
try {
memberManifests = workspacePackages(REPO_ROOT).map((p) => manifestFor(p.dir));
} catch (err) {
return fail(`the workspace could not be enumerated, so the declaration could not be reconciled: ${err?.message ?? err}`);
}
ok(`the enumerator finds member manifests to declare (${memberManifests.length})`, memberManifests.length > 0);
const undeclared = memberManifests.filter((m) => !DECLARED_WATCH_HINTS.some((h) => patternMatches(h, m)));
ok(
`every member manifest this gate opens is covered by a declared pattern (uncovered: ${undeclared.join(', ') || 'none'})`,
undeclared.length === 0,
);
const empty = DECLARED_WATCH_HINTS.filter((h) => !memberManifests.some((m) => patternMatches(h, m)));
ok(`and no declared pattern covers zero of them (empty: ${empty.join(', ') || 'none'})`, empty.length === 0);
// The other direction on the ROOT: it is not a workspace member, so a member
// pattern that reached it would mean the two declarations had collapsed into
// one over-wide glob.
ok(
'no member pattern reaches the root manifest — the root is declared separately because it is not a workspace member',
!DECLARED_WATCH_HINTS.some((h) => patternMatches(h, manifestFor('.'))),
);
ok(
'the pattern matcher crosses separators for `**` and does not run past the filename',
patternMatches('packages/**/package.json', 'packages/spec/package.json') &&
patternMatches('packages/**/package.json', 'packages/plugins/plugin-auth/package.json') &&
!patternMatches('packages/**/package.json', 'packages/spec/src/index.ts'),
);

if (problems.length) {
return fail(`watch-hint declaration(s) out of step with the manifest population:\n ${problems.join('\n ')}\n`
+ ' ROOT_FILE_WATCH_HINTS / DECLARED_WATCH_HINTS at the top of this file declare the manifests\n'
+ ' reconcileGenerators reads. Repair the DECLARATION, never this reconciliation.');
}
console.log(`✓ the declared manifest population covers the root manifest and all ${memberManifests.length} member manifest(s)`);
return true;
}

/**
* The `untracked: true` dispositions, held against git rather than against their own
* prose (#13731).
Expand Down Expand Up @@ -1140,7 +1302,7 @@ function endToEndMixed() {

if (process.argv.includes('--self-test')) {
console.log('git-merge-regen --self-test\n');
// The eleven callees as a literal LIST rather than eleven bare calls, so the
// The twelve callees as a literal LIST rather than twelve bare calls, so the
// names this block invokes are data the floor below can cross-check the
// roster against, in both directions. The names are read off the function
// declarations themselves (`fn.name`), so a renamed callee moves this list
Expand All @@ -1150,6 +1312,7 @@ if (process.argv.includes('--self-test')) {
reconcileAttributeSemantics,
reconcileScripts,
reconcileGenerators,
reconcileManifestPopulation,
reconcileUntrackedDispositions,
reconcileOwnership,
hookIsExecutable,
Expand Down
Loading
Loading