Regenerate the compatibility files offline, and after merges

pnpm node-compat fetched Node's docs on every run, even though only the
classification changes between pins. pnpm node-compat --local now
reclassifies the committed ledger rows from the current sources and
rewrites the ledger and the public artifact, the same files a fetching
run writes while the pin is unchanged. scripts/regen-generated.mjs uses
it after a merge: for each generated file still in conflict it takes the
merged-in branch's version, regenerates the surface manifest and the
compatibility files from the merged sources, and stages them, so these
files never need a hand merge.
This commit is contained in:
Chris Tate
2026-10-20 20:50:04 -05:00
parent 9a802fe635
commit 9c24e64ee2
5 changed files with 97 additions and 4 deletions
+3 -1
View File
@@ -142,7 +142,9 @@ Never hand-edit these files:
- `internal/compatibility/generated/node-v24-internal.json`
- `docs/src/generated/node-v24-compatibility.json`
Change compiler decision sources or the implementation-owned compatibility manifests, then regenerate. Use `pnpm manifest` when compiler decision tables change and `pnpm node-compat` whenever the Node pin, compatibility manifests, compiler surface manifest, changes.
Change compiler decision sources or the implementation-owned compatibility manifests, then regenerate. Use `pnpm manifest` when compiler decision tables change and `pnpm node-compat` whenever the Node pin, compatibility manifests, compiler surface manifest, changes. `pnpm node-compat --local` reclassifies the committed ledger without fetching Node's docs; it writes the same files as `pnpm node-compat` while the Node pin is unchanged.
Never resolve a merge conflict in these files by hand. Resolve the source conflicts, then run `node scripts/regen-generated.mjs`: it takes the merged-in branch's version of each conflicted generated file, regenerates all of them from the merged sources, and stages the ones that conflicted.
The public docs artifact must not expose repository test paths, internal source identifiers, engineering evidence bookkeeping, documentation/metadata headings, command-line or configuration-only entries, or rows that are N/A in the native tier. Those remain in the complete internal census under `internal/compatibility/generated/`.
+1 -1
View File
@@ -12,7 +12,7 @@ Statuses are evidence-aware: `supported` and `partial` require test evidence; `r
Run `pnpm node-compat:backlog` from the repository root for a summary. The filters `--tier`, `--action`, `--priority`, `--chapter`, and `--status` compose; use `--format=json` or `--format=tsv` for tooling.
Run `pnpm node-compat` from the repository root after changing the Node pin or the support manifest. `pnpm node-compat:check` is the offline drift/evidence gate; `pnpm --filter @internal/compatibility check:upstream` additionally verifies the pinned upstream inputs over the network.
Run `pnpm node-compat` from the repository root after changing the Node pin or the support manifest. `pnpm node-compat --local` does the same without network access while the pin is unchanged, by reclassifying the committed ledger rows; after a merge, `node scripts/regen-generated.mjs` regenerates every generated file instead of a hand merge. `pnpm node-compat:check` is the offline drift/evidence gate; `pnpm --filter @internal/compatibility check:upstream` additionally verifies the pinned upstream inputs over the network.
## npm packages that compile as builtins
+39
View File
@@ -13,6 +13,8 @@
*
* pnpm node-compat fetch latest-v24.x and rewrite the snapshot
* pnpm node-compat --check fetch and fail if the snapshot would change
* pnpm node-compat --local rewrite the snapshot without fetching: reclassify
* the committed rows from the current sources
* pnpm node-compat:check offline implementation/profile drift check
*/
@@ -1013,6 +1015,34 @@ function publicSnapshot(snapshot) {
};
}
/** The committed ledger with every row's classification recomputed from
* the current sources. The rows themselves come from the pinned Node docs,
* so while the pin is unchanged this is what a fetching run writes. */
function reclassify(snapshot) {
if (snapshot.nodeVersion !== pin.version || snapshot.nodeCommit !== pin.commit) {
throw new Error(
`the ledger targets Node ${snapshot.nodeVersion}, the pin ${pin.version}; run 'pnpm node-compat' to fetch the new docs`,
);
}
const ctx = classificationContext();
const ancestors = [];
for (const row of snapshot.rows) {
ancestors.length = row.depth;
const parentSignature = ancestors[row.depth - 1]?.signature;
row.static = classifyStatic(row, null, ctx, parentSignature);
ancestors[row.depth] = row;
}
for (const chapter of snapshot.chapters) {
const chapterRows = snapshot.rows.filter((row) => row.chapter === chapter.slug);
chapterRows[0].static = summarizeTier(
chapterRows,
"static",
ctx.compilerCompat.chapterPolicies[chapter.slug],
);
}
return snapshot;
}
function checkLocal() {
for (const path of retiredOutputPaths) {
if (existsSync(path))
@@ -1109,6 +1139,15 @@ async function fetchText(url) {
async function main() {
const args = new Set(process.argv.slice(2));
if (args.has("--check-local")) return checkLocal();
if (args.has("--local")) {
const snapshot = reclassify(JSON.parse(readFileSync(internalOutputPath, "utf8")));
writeFileSync(internalOutputPath, render(snapshot));
writeFileSync(publicOutputPath, render(publicSnapshot(snapshot)));
console.log(
`reclassified the compatibility ledger and public artifact (${snapshot.rows.length} source rows, ${projectPublicMatrix(snapshot).rows.length} public matrix rows, ${pin.tag})`,
);
return;
}
const [allText, indexMarkdown] = await Promise.all([
fetchText(pin.allJsonUrl),
+1 -2
View File
@@ -2,8 +2,7 @@
import { spawnSync } from "node:child_process";
const check = process.argv.slice(2).includes("--check");
const args = check ? ["--check"] : [];
const args = process.argv.slice(2).filter((arg) => arg === "--check" || arg === "--local");
for (const script of ["generate.mjs"]) {
const result = spawnSync(process.execPath, [`src/${script}`, ...args], { stdio: "inherit" });
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env node
/* Regenerates the committed generated files from their sources, so a merge
* never resolves one by hand:
*
* packages/compiler/surface-manifest.json pnpm manifest
* internal/compatibility/generated/node-v24-internal.json
* docs/src/generated/node-v24-compatibility.json pnpm node-compat --local
*
* During a merge, resolve the conflicts in source files first. Each
* generated file git still reports as conflicted is then replaced by the
* merged-in branch's version (MERGE_HEAD; HEAD during a rebase or
* cherry-pick, where HEAD is the branch being built on), every file is
* regenerated from the merged sources, and the conflicted ones are staged.
*
* node scripts/regen-generated.mjs
*/
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const root = fileURLToPath(new URL("..", import.meta.url));
const GENERATED = [
"packages/compiler/surface-manifest.json",
"internal/compatibility/generated/node-v24-internal.json",
"docs/src/generated/node-v24-compatibility.json",
];
function run(command, args) {
const result = spawnSync(command, args, { cwd: root, stdio: ["ignore", "pipe", "inherit"] });
if (result.error) throw result.error;
if (result.status !== 0) {
process.stdout.write(result.stdout);
throw new Error(`${command} ${args.join(" ")} exited with ${result.status}`);
}
return result.stdout.toString();
}
const git = (...args) => run("git", args);
const hasRef = (ref) =>
spawnSync("git", ["rev-parse", "-q", "--verify", ref], { cwd: root }).status === 0;
const unmerged = new Set(git("diff", "--name-only", "--diff-filter=U").split("\n"));
const conflicted = GENERATED.filter((path) => unmerged.has(path));
if (conflicted.length > 0) {
const from = hasRef("MERGE_HEAD") ? "MERGE_HEAD" : "HEAD";
git("checkout", from, "--", ...conflicted);
console.log(`took ${from}'s ${conflicted.join(", ")}`);
}
process.stdout.write(run("pnpm", ["--silent", "manifest"]));
process.stdout.write(run(process.execPath, ["internal/compatibility/src/generate.mjs", "--local"]));
if (conflicted.length > 0) {
git("add", "--", ...conflicted);
console.log(`staged ${conflicted.join(", ")}`);
}