diff --git a/AGENTS.md b/AGENTS.md index 859c6e4d4..5e4ae5036 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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/`. diff --git a/internal/compatibility/README.md b/internal/compatibility/README.md index e880388eb..86bb4ce2c 100644 --- a/internal/compatibility/README.md +++ b/internal/compatibility/README.md @@ -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 diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs index f380279aa..9bd3927b8 100644 --- a/internal/compatibility/src/generate.mjs +++ b/internal/compatibility/src/generate.mjs @@ -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), diff --git a/internal/compatibility/src/update.mjs b/internal/compatibility/src/update.mjs index 553e2698c..f52a122d2 100644 --- a/internal/compatibility/src/update.mjs +++ b/internal/compatibility/src/update.mjs @@ -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" }); diff --git a/scripts/regen-generated.mjs b/scripts/regen-generated.mjs new file mode 100644 index 000000000..7cff0d258 --- /dev/null +++ b/scripts/regen-generated.mjs @@ -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(", ")}`); +}