diff --git a/.github/workflows/node-compat-matrix.yml b/.github/workflows/node-compat-matrix.yml index ead4229ca1..288943cf40 100644 --- a/.github/workflows/node-compat-matrix.yml +++ b/.github/workflows/node-compat-matrix.yml @@ -1,6 +1,6 @@ name: Node Compat Matrix Guard -# Gates scripts/node_compat_matrix.mjs --check: a breadth sweep over every +# Gates scripts/node_compat_matrix.mts --check: a breadth sweep over every # require("module").builtinModules entry, both import forms (M and node:M), # against a PINNED, SRI-verified Node oracle (external-tools.json tools.node, # currently 26.5.1). It compares Perry's export-SHAPE fingerprint per module @@ -11,7 +11,7 @@ name: Node Compat Matrix Guard # # Its OWN job on purpose: the runner downloads the pinned Node dist tarball # (~40MB) and verifies it, which must not slow the main cargo-test job. The -# node used by setup-node here only EXECUTES the .mjs; the oracle Node is the +# node used by setup-node here only EXECUTES the .mts; the oracle Node is the # pinned dist the runner fetches + SRI-verifies itself. # # Decoupled from the (not-yet-enabled) merge queue like node-suite-guard.yml: @@ -76,7 +76,7 @@ jobs: uses: actions/setup-node@v7 with: # Single source of truth: .node-version at the repo root. This node - # only RUNS node_compat_matrix.mjs; the compat oracle is the pinned + # only RUNS node_compat_matrix.mts; the compat oracle is the pinned # Node the runner downloads + SRI-verifies from external-tools.json. node-version-file: .node-version @@ -91,6 +91,6 @@ jobs: set -euo pipefail echo '### Node builtin compat matrix guard' >> "$GITHUB_STEP_SUMMARY" echo '```' >> "$GITHUB_STEP_SUMMARY" - node scripts/node_compat_matrix.mjs --check \ + node scripts/node_compat_matrix.mts --check \ | tee -a "$GITHUB_STEP_SUMMARY" echo '```' >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 231ba4b619..7ec1c21c35 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -294,7 +294,7 @@ jobs: # newly-soaked upstream releases as advisories. - name: Binding upstream pins (lock-step) if: ${{ !cancelled() }} - run: node scripts/binding_pins.mjs --check + run: node scripts/binding_pins.mts --check # GC write-barrier store-site inventory: every raw heap-slot store in # perry-codegen / perry-runtime / perry-stdlib must be barriered or diff --git a/.gitignore b/.gitignore index f1a078cbc8..ad96c20fcd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ __pycache__/ *.py[cod] # Pinned Node oracle for the builtin-module compat matrix -# (scripts/node_compat_matrix.mjs downloads + SRI-verifies + caches here). +# (scripts/node_compat_matrix.mts downloads + SRI-verifies + caches here). .cache/node-pin/ # Android Gradle: caches and build outputs are regenerable. Source under @@ -192,7 +192,7 @@ private-* # mdBook output docs/book/ -# scripts/fp_fuzz.mjs failure dumps (regenerable, machine-local) +# scripts/fp_fuzz.mts failure dumps (regenerable, machine-local) fp_fuzz_failures/ test-files/.perry-cache/ diff --git a/CLAUDE.md b/CLAUDE.md index 5e23d062ab..2b4527265d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,22 +29,22 @@ Three workflows are deliberately exempt and say so inline: `node-core-subset.yml - **Test/CI mechanics** — `#794` (per-category parity thresholds), `#796` (gap-suite output truncation + O(n²) `normalize_output`), `#812` (42-module behavioral matrix), `#806/#807/#808` (test harnesses for mixins / async context / ≥300-init scale). - **Skip-list audit** — `#797` covers `test-parity/known_failures.json` provenance (issue # + date per entry). -### Node builtin compatibility matrix (`scripts/node_compat_matrix.mjs`) +### Node builtin compatibility matrix (`scripts/node_compat_matrix.mts`) Breadth sweep over EVERY `require("module").builtinModules` entry, both import forms (`M` and `node:M`), against a **pinned, SRI-verified Node** (the "latest stable" oracle, pinned in `external-tools.json` `tools.node.version` — currently **26.5.1**, independent of the `.node-version` gap-suite oracle). It compares Perry's export-SHAPE fingerprint (sorted `name:typeof` over the module namespace + the default export's typeof) to the oracle's. This is the systematic version of the #812 "42-module behavioral matrix" — shape, not deep behavior (behavioral cases stay in the node-suite). ```bash # FAST LOOP — reach for this first when iterating on ONE builtin: -node scripts/node_compat_matrix.mjs --module fs # one module, both forms -node scripts/node_compat_matrix.mjs --module fs,path,crypto # a few -node scripts/node_compat_matrix.mjs --module fs --method readFileSync,promises # only these exports -node scripts/node_compat_matrix.mjs --only fs.readFileSync,path.join # combined mod.export form +node scripts/node_compat_matrix.mts --module fs # one module, both forms +node scripts/node_compat_matrix.mts --module fs,path,crypto # a few +node scripts/node_compat_matrix.mts --module fs --method readFileSync,promises # only these exports +node scripts/node_compat_matrix.mts --only fs.readFileSync,path.join # combined mod.export form # (the pinned Node download is skipped once cached under .cache/node-pin/) # FULL SWEEP + GATE: -node scripts/node_compat_matrix.mjs # whole matrix + summary table -node scripts/node_compat_matrix.mjs --check # CI gate: exit 1 on regressions vs the baseline -node scripts/node_compat_matrix.mjs --update-baseline # rewrite test-parity/node-compat-matrix.baseline.json +node scripts/node_compat_matrix.mts # whole matrix + summary table +node scripts/node_compat_matrix.mts --check # CI gate: exit 1 on regressions vs the baseline +node scripts/node_compat_matrix.mts --update-baseline # rewrite test-parity/node-compat-matrix.baseline.json ``` A `--module` selector scopes `--check`/`--update-baseline` to just that slice (a single-module refresh never rewrites the whole baseline). A `--method`/`--only` subset is a print-only fast diagnostic (it narrows the fingerprint, so it is refused for `--check`/`--update-baseline`). **Bump the pinned Node** by editing `tools.node.version` in `external-tools.json` (add per-platform sha512 SRI), then `--update-baseline` and review the diff. Needs the release binary (`cargo build --release -p perry`). Full page: `docs/src/testing/node-compat-matrix.md`. diff --git a/changelog.d/8241-scripts-mts.md b/changelog.d/8241-scripts-mts.md new file mode 100644 index 0000000000..60ec260257 --- /dev/null +++ b/changelog.d/8241-scripts-mts.md @@ -0,0 +1 @@ +Converted the three remaining `scripts/**/*.mjs` files (`binding_pins`, `fp_fuzz`, `node_compat_matrix`) to TypeScript `.mts`, standardizing onto the same module + type-stripping convention the soak scripts already use. Declared `engines.node >= 22.18.0` in `package.json` (the floor for native `.mts` type stripping; the repo's `.node-version` pin of 26.5.1 is well above it). All invocation sites — CI workflows, docs, config JSON, in-script help text, Rust comments — updated to reference the new `.mts` paths. diff --git a/crates/perry/src/commands/compile/well_known.rs b/crates/perry/src/commands/compile/well_known.rs index 7f64697861..c5375a8cae 100644 --- a/crates/perry/src/commands/compile/well_known.rs +++ b/crates/perry/src/commands/compile/well_known.rs @@ -71,7 +71,7 @@ pub struct WellKnownBinding { /// Upstream provenance pin — which release of the npm package this /// wrapper ports, and when it was last reviewed against it. See /// `docs/src/native-libraries/upstream-pins.md` and the lock-step - /// gate in `scripts/binding_pins.mjs`. `None` for entries exempt + /// gate in `scripts/binding_pins.mts`. `None` for entries exempt /// from pinning (`node_builtin`, `alias_of`, and perry-owned /// packages). // Provenance metadata parsed from `well_known_bindings.toml` and consulted @@ -133,7 +133,7 @@ fn binding_is_faithful( /// /// The **lock-step rule**: `ported_at` must equal `version`. Re-pinning /// an upstream release without re-reviewing the wrapper against the -/// upstream diff reds the `binding_pins.mjs --check` gate until +/// upstream diff reds the `binding_pins.mts --check` gate until /// `ported_at` advances with the review — an upstream release can never /// go silently stale, and a pin bump can never outrun its port. #[derive(Debug, Clone, PartialEq, Eq)] @@ -317,7 +317,7 @@ fn parse_well_known_toml(raw: &str) -> Result let version = required("version")?; let ported_at = required("ported-at")?; // Parse-time lock-step backstop. The authoritative gate is - // `scripts/binding_pins.mjs --check` (CI); failing here too + // `scripts/binding_pins.mts --check` (CI); failing here too // means a skewed pin can't even ship inside the binary. if ported_at != version { return Err(format!( @@ -741,7 +741,7 @@ mod tests { /// The lock-step rule enforced at parse time: a pin bump /// (`version`) that outruns its review (`ported-at`) must refuse - /// to load — the authoritative CI gate is binding_pins.mjs + /// to load — the authoritative CI gate is binding_pins.mts /// --check, but a skewed pin must not even ship inside the binary. #[test] fn upstream_pin_rejects_lock_step_violation() { @@ -793,7 +793,7 @@ mod tests { assert!( unpinned.is_empty(), "bindings without an [bindings..upstream] pin — provision one with \ - `node scripts/binding_pins.mjs --set `:\n {}", + `node scripts/binding_pins.mts --set `:\n {}", unpinned.join("\n ") ); } diff --git a/docs/po/de.po b/docs/po/de.po index e98470b9b0..5a4deb9808 100644 --- a/docs/po/de.po +++ b/docs/po/de.po @@ -52183,13 +52183,13 @@ msgstr "Korrektheits-Zahlen" #: src/cli/fast-math.md:123 msgid "" -"`scripts/fp_fuzz.mjs` — randomly generates TS programs exercising the six " +"`scripts/fp_fuzz.mts` — randomly generates TS programs exercising the six " "patterns most likely to trip per-instruction FMFs (left-fold, tree-fold, " "right-fold reductions; FMA-shaped chains; algebraic identities like " "`(a/b)*b`; cancellation predicates). Each program is compiled with both Node" " and Perry, and stdout is diffed byte-for-byte." msgstr "" -"`scripts/fp_fuzz.mjs` — generiert zufällig TS-Programme, die die sechs " +"`scripts/fp_fuzz.mts` — generiert zufällig TS-Programme, die die sechs " "Muster ausüben, die am ehesten Per-Instruction-FMFs auslösen (Left-Fold-, " "Tree-Fold-, Right-Fold-Reduktionen; FMA-förmige Ketten; algebraische " "Identitäten wie `(a/b)*b`; Cancellation-Prädikate). Jedes Programm wird " diff --git a/docs/po/es.po b/docs/po/es.po index b6ea157790..f4d643c1a7 100644 --- a/docs/po/es.po +++ b/docs/po/es.po @@ -52566,13 +52566,13 @@ msgstr "Números de corrección" #: src/cli/fast-math.md:123 msgid "" -"`scripts/fp_fuzz.mjs` — randomly generates TS programs exercising the six " +"`scripts/fp_fuzz.mts` — randomly generates TS programs exercising the six " "patterns most likely to trip per-instruction FMFs (left-fold, tree-fold, " "right-fold reductions; FMA-shaped chains; algebraic identities like " "`(a/b)*b`; cancellation predicates). Each program is compiled with both Node" " and Perry, and stdout is diffed byte-for-byte." msgstr "" -"`scripts/fp_fuzz.mjs` — genera aleatoriamente programas TS que ejercitan los" +"`scripts/fp_fuzz.mts` — genera aleatoriamente programas TS que ejercitan los" " seis patrones con más probabilidad de activar FMFs por instrucción " "(reducciones left-fold, tree-fold, right-fold; cadenas con forma FMA; " "identidades algebraicas como `(a/b)*b`; predicados de cancelación). Cada " diff --git a/docs/po/fr.po b/docs/po/fr.po index a558735ce8..07ecc23ac6 100644 --- a/docs/po/fr.po +++ b/docs/po/fr.po @@ -52693,13 +52693,13 @@ msgstr "Chiffres de correction" #: src/cli/fast-math.md:123 msgid "" -"`scripts/fp_fuzz.mjs` — randomly generates TS programs exercising the six " +"`scripts/fp_fuzz.mts` — randomly generates TS programs exercising the six " "patterns most likely to trip per-instruction FMFs (left-fold, tree-fold, " "right-fold reductions; FMA-shaped chains; algebraic identities like " "`(a/b)*b`; cancellation predicates). Each program is compiled with both Node" " and Perry, and stdout is diffed byte-for-byte." msgstr "" -"`scripts/fp_fuzz.mjs` — génère aléatoirement des programmes TS exercant les " +"`scripts/fp_fuzz.mts` — génère aléatoirement des programmes TS exercant les " "six motifs les plus susceptibles de déclencher des FMF par instruction " "(réductions left-fold, tree-fold, right-fold ; chaînes de forme FMA ; " "identités algébriques comme `(a/b)*b` ; prédicats d'annulation). Chaque " diff --git a/docs/po/id.po b/docs/po/id.po index a52dc1a8ea..8fe9bb8384 100644 --- a/docs/po/id.po +++ b/docs/po/id.po @@ -51605,13 +51605,13 @@ msgstr "Angka kebenaran" #: src/cli/fast-math.md:123 msgid "" -"`scripts/fp_fuzz.mjs` — randomly generates TS programs exercising the six " +"`scripts/fp_fuzz.mts` — randomly generates TS programs exercising the six " "patterns most likely to trip per-instruction FMFs (left-fold, tree-fold, " "right-fold reductions; FMA-shaped chains; algebraic identities like " "`(a/b)*b`; cancellation predicates). Each program is compiled with both Node" " and Perry, and stdout is diffed byte-for-byte." msgstr "" -"`scripts/fp_fuzz.mjs` — secara acak menghasilkan program TS yang menjalankan" +"`scripts/fp_fuzz.mts` — secara acak menghasilkan program TS yang menjalankan" " enam pola yang paling mungkin memicu FMF per-instruksi (reduksi left-fold, " "tree-fold, right-fold; rantai berbentuk FMA; identitas aljabar seperti " "`(a/b)*b`; predikat pembatalan). Setiap program dikompilasi dengan Node dan " diff --git a/docs/po/it.po b/docs/po/it.po index f033628e49..0872fdf769 100644 --- a/docs/po/it.po +++ b/docs/po/it.po @@ -52042,13 +52042,13 @@ msgstr "Numeri sulla correttezza" #: src/cli/fast-math.md:123 msgid "" -"`scripts/fp_fuzz.mjs` — randomly generates TS programs exercising the six " +"`scripts/fp_fuzz.mts` — randomly generates TS programs exercising the six " "patterns most likely to trip per-instruction FMFs (left-fold, tree-fold, " "right-fold reductions; FMA-shaped chains; algebraic identities like " "`(a/b)*b`; cancellation predicates). Each program is compiled with both Node" " and Perry, and stdout is diffed byte-for-byte." msgstr "" -"`scripts/fp_fuzz.mjs` — genera casualmente programmi TS che esercitano i sei" +"`scripts/fp_fuzz.mts` — genera casualmente programmi TS che esercitano i sei" " pattern più suscettibili di attivare i FMF per istruzione (riduzioni left-" "fold, tree-fold, right-fold; catene con forma FMA; identità algebriche come " "`(a/b)*b`; predicati di cancellazione). Ogni programma viene compilato sia " diff --git a/docs/po/ja.po b/docs/po/ja.po index 38c3e7ef65..96d2f69a9b 100644 --- a/docs/po/ja.po +++ b/docs/po/ja.po @@ -49531,13 +49531,13 @@ msgstr "正確性の数字" #: src/cli/fast-math.md:123 msgid "" -"`scripts/fp_fuzz.mjs` — randomly generates TS programs exercising the six " +"`scripts/fp_fuzz.mts` — randomly generates TS programs exercising the six " "patterns most likely to trip per-instruction FMFs (left-fold, tree-fold, " "right-fold reductions; FMA-shaped chains; algebraic identities like " "`(a/b)*b`; cancellation predicates). Each program is compiled with both Node" " and Perry, and stdout is diffed byte-for-byte." msgstr "" -"`scripts/fp_fuzz.mjs` — 命令ごとの FMF を発生させやすい 6 " +"`scripts/fp_fuzz.mts` — 命令ごとの FMF を発生させやすい 6 " "つのパターン(左折りたたみ、木折りたたみ、右折りたたみリダクション、FMA 形のチェーン、`(a/b)*b` " "のような代数恒等式、キャンセル述語)を行使する TS プログラムをランダムに生成します。各プログラムは Node と Perry " "の両方でコンパイルされ、stdout はバイト単位で diff されます。" diff --git a/docs/po/ko.po b/docs/po/ko.po index c68bd9b132..3925edf898 100644 --- a/docs/po/ko.po +++ b/docs/po/ko.po @@ -49482,13 +49482,13 @@ msgstr "정확성 수치" #: src/cli/fast-math.md:123 msgid "" -"`scripts/fp_fuzz.mjs` — randomly generates TS programs exercising the six " +"`scripts/fp_fuzz.mts` — randomly generates TS programs exercising the six " "patterns most likely to trip per-instruction FMFs (left-fold, tree-fold, " "right-fold reductions; FMA-shaped chains; algebraic identities like " "`(a/b)*b`; cancellation predicates). Each program is compiled with both Node" " and Perry, and stdout is diffed byte-for-byte." msgstr "" -"`scripts/fp_fuzz.mjs` — 명령어별 FMF를 트리거할 가능성이 가장 높은 여섯 가지 패턴(왼쪽 폴드, 트리 폴드, 오른쪽" +"`scripts/fp_fuzz.mts` — 명령어별 FMF를 트리거할 가능성이 가장 높은 여섯 가지 패턴(왼쪽 폴드, 트리 폴드, 오른쪽" " 폴드 리덕션; FMA 형태 체인; `(a/b)*b`와 같은 대수 항등식; 소거 술어)을 실행하는 TS 프로그램을 무작위로 생성합니다. " "각 프로그램은 Node와 Perry 양쪽에서 컴파일되며 stdout을 바이트 단위로 비교합니다." diff --git a/docs/po/messages.pot b/docs/po/messages.pot index 489b2c4bf1..b43575c82d 100644 --- a/docs/po/messages.pot +++ b/docs/po/messages.pot @@ -46721,7 +46721,7 @@ msgstr "" #: src/cli/fast-math.md:123 msgid "" -"`scripts/fp_fuzz.mjs` — randomly generates TS programs exercising the six " +"`scripts/fp_fuzz.mts` — randomly generates TS programs exercising the six " "patterns most likely to trip per-instruction FMFs (left-fold, tree-fold, " "right-fold reductions; FMA-shaped chains; algebraic identities like " "`(a/b)*b`; cancellation predicates). Each program is compiled with both Node " diff --git a/docs/po/th.po b/docs/po/th.po index 1d0783ba54..91f0220b8b 100644 --- a/docs/po/th.po +++ b/docs/po/th.po @@ -51127,13 +51127,13 @@ msgstr "ตัวเลขความถูกต้อง" #: src/cli/fast-math.md:123 msgid "" -"`scripts/fp_fuzz.mjs` — randomly generates TS programs exercising the six " +"`scripts/fp_fuzz.mts` — randomly generates TS programs exercising the six " "patterns most likely to trip per-instruction FMFs (left-fold, tree-fold, " "right-fold reductions; FMA-shaped chains; algebraic identities like " "`(a/b)*b`; cancellation predicates). Each program is compiled with both Node" " and Perry, and stdout is diffed byte-for-byte." msgstr "" -"`scripts/fp_fuzz.mjs` — สร้างโปรแกรม TS " +"`scripts/fp_fuzz.mts` — สร้างโปรแกรม TS " "แบบสุ่มที่ทดสอบรูปแบบหกแบบที่มีแนวโน้มจะทำให้ per-instruction FMF สะดุด " "(การรวม left-fold, tree-fold, right-fold; chain รูปแบบ FMA; identity " "เชิงพีชคณิตเช่น `(a/b)*b`; predicate การยกเลิก) แต่ละโปรแกรมถูก compile " diff --git a/docs/po/vi.po b/docs/po/vi.po index 9ca76094ac..fab82fdd04 100644 --- a/docs/po/vi.po +++ b/docs/po/vi.po @@ -51541,13 +51541,13 @@ msgstr "Số liệu tính chính xác" #: src/cli/fast-math.md:123 msgid "" -"`scripts/fp_fuzz.mjs` — randomly generates TS programs exercising the six " +"`scripts/fp_fuzz.mts` — randomly generates TS programs exercising the six " "patterns most likely to trip per-instruction FMFs (left-fold, tree-fold, " "right-fold reductions; FMA-shaped chains; algebraic identities like " "`(a/b)*b`; cancellation predicates). Each program is compiled with both Node" " and Perry, and stdout is diffed byte-for-byte." msgstr "" -"`scripts/fp_fuzz.mjs` — tạo ngẫu nhiên các chương trình TS thực thi sáu mẫu " +"`scripts/fp_fuzz.mts` — tạo ngẫu nhiên các chương trình TS thực thi sáu mẫu " "có khả năng kích hoạt FMF từng lệnh nhất (rút gọn left-fold, tree-fold, " "right-fold; chuỗi hình FMA; đẳng thức đại số như `(a/b)*b`; vị từ triệt " "tiêu). Mỗi chương trình được biên dịch bằng cả Node và Perry, và stdout được" diff --git a/docs/po/zh-CN.po b/docs/po/zh-CN.po index 373a9f2128..38aed1be26 100644 --- a/docs/po/zh-CN.po +++ b/docs/po/zh-CN.po @@ -49008,13 +49008,13 @@ msgstr "正确性数据" #: src/cli/fast-math.md:123 msgid "" -"`scripts/fp_fuzz.mjs` — randomly generates TS programs exercising the six " +"`scripts/fp_fuzz.mts` — randomly generates TS programs exercising the six " "patterns most likely to trip per-instruction FMFs (left-fold, tree-fold, " "right-fold reductions; FMA-shaped chains; algebraic identities like " "`(a/b)*b`; cancellation predicates). Each program is compiled with both Node" " and Perry, and stdout is diffed byte-for-byte." msgstr "" -"`scripts/fp_fuzz.mjs` — 随机生成 TS 程序,使用最有可能触发按指令 FMF 的六种模式(左折叠、树折叠、右折叠归约;FMA " +"`scripts/fp_fuzz.mts` — 随机生成 TS 程序,使用最有可能触发按指令 FMF 的六种模式(左折叠、树折叠、右折叠归约;FMA " "形状链;像 `(a/b)*b` 的代数恒等式;抵消谓词)。每个程序都用 Node 和 Perry 编译,并对 stdout 进行逐字节比对。" #: src/cli/fast-math.md:129 diff --git a/docs/src/cli/fast-math.md b/docs/src/cli/fast-math.md index 5f12914ead..e84bd88e5d 100644 --- a/docs/src/cli/fast-math.md +++ b/docs/src/cli/fast-math.md @@ -120,7 +120,7 @@ giving up bit-exact parity. ## Correctness numbers -`scripts/fp_fuzz.mjs` — randomly generates TS programs exercising the +`scripts/fp_fuzz.mts` — randomly generates TS programs exercising the six patterns most likely to trip per-instruction FMFs (left-fold, tree-fold, right-fold reductions; FMA-shaped chains; algebraic identities like `(a/b)*b`; cancellation predicates). Each program is diff --git a/docs/src/native-libraries/upstream-pins.md b/docs/src/native-libraries/upstream-pins.md index 789ebb62e8..0c78cac2f9 100644 --- a/docs/src/native-libraries/upstream-pins.md +++ b/docs/src/native-libraries/upstream-pins.md @@ -26,7 +26,7 @@ date = "2026-07-30" **`ported-at` must equal `version`.** Re-pinning a binding to a newer upstream release without re-reviewing the wrapper against the upstream diff reds the -`binding_pins.mjs --check` gate — and the perry binary itself refuses to load a +`binding_pins.mts --check` gate — and the perry binary itself refuses to load a skewed table. An upstream release can never go silently stale, and a pin bump can never outrun the review it demands: bumping `version` forces you to advance `ported-at`, which forces the review. @@ -46,26 +46,26 @@ Note that a distinct npm package served by a shared wrapper crate is **not** an alias — `redis` and `iovalkey` both use `perry-ext-ioredis` but are separately published and versioned, so each carries its own pin. -## Tooling — `scripts/binding_pins.mjs` +## Tooling — `scripts/binding_pins.mts` ```sh # Provision or bump one pin to a specific version (default: latest stable) -node scripts/binding_pins.mjs --set ioredis 5.11.1 +node scripts/binding_pins.mts --set ioredis 5.11.1 # Provision every currently-unpinned binding at its latest stable -node scripts/binding_pins.mjs --backfill +node scripts/binding_pins.mts --backfill # Offline gate (CI): pins present, lock-stepped, crates exist. Exit 1 on any # violation. No network. -node scripts/binding_pins.mjs --check +node scripts/binding_pins.mts --check # Advisory: additionally flag pins whose upstream has a newer stable release # that has soaked >= N days (default 7). Network. Run in the weekly update. -node scripts/binding_pins.mjs --check --refresh --soak-days 7 +node scripts/binding_pins.mts --check --refresh --soak-days 7 # Materialize the upstream repo at the pinned ref into gitignored upstream/ # for port review (diff the old pin against a candidate new tag) -node scripts/binding_pins.mjs --materialize ioredis +node scripts/binding_pins.mts --materialize ioredis ``` Never hand-edit `version` / `sha256` / `ref` — the tarball hash can't be diff --git a/docs/src/testing/node-compat-matrix.md b/docs/src/testing/node-compat-matrix.md index b50eb70e90..602e688e96 100644 --- a/docs/src/testing/node-compat-matrix.md +++ b/docs/src/testing/node-compat-matrix.md @@ -1,7 +1,7 @@ # Node builtin-module Compatibility Matrix Perry reimplements the `node:*` module surface natively. The **compatibility -matrix** (`scripts/node_compat_matrix.mjs`) measures — against a *pinned, +matrix** (`scripts/node_compat_matrix.mts`) measures — against a *pinned, verified* Node — how faithfully Perry reproduces each builtin's **export shape**, for **both** import forms (`M` and `node:M`). @@ -14,7 +14,7 @@ matrix"). Deep *behavioral* parity lives in the hand-authored node-suite ## To check one module fast ```bash -node scripts/node_compat_matrix.mjs --module fs +node scripts/node_compat_matrix.mts --module fs ``` That is the command to reach for while iterating on a single builtin. The @@ -22,9 +22,9 @@ pinned Node download happens once and is cached under `.cache/node-pin/`, so subsequent runs are just Perry compile + run. Narrow further: ```bash -node scripts/node_compat_matrix.mjs --module fs,path,crypto # a few modules -node scripts/node_compat_matrix.mjs --module fs --method readFileSync,promises # only these exports -node scripts/node_compat_matrix.mjs --only fs.readFileSync,path.join # combined mod.export form +node scripts/node_compat_matrix.mts --module fs,path,crypto # a few modules +node scripts/node_compat_matrix.mts --module fs --method readFileSync,promises # only these exports +node scripts/node_compat_matrix.mts --only fs.readFileSync,path.join # combined mod.export form ``` A `--method`/`--only` subset narrows the fingerprint to those exports for a @@ -35,9 +35,9 @@ for `--check`/`--update-baseline`. ## Full sweep and the CI gate ```bash -node scripts/node_compat_matrix.mjs # whole matrix + summary table -node scripts/node_compat_matrix.mjs --check # exit 1 on regressions vs the baseline -node scripts/node_compat_matrix.mjs --update-baseline # rewrite the committed baseline +node scripts/node_compat_matrix.mts # whole matrix + summary table +node scripts/node_compat_matrix.mts --check # exit 1 on regressions vs the baseline +node scripts/node_compat_matrix.mts --update-baseline # rewrite the committed baseline ``` The harness needs the release binary (`cargo build --release -p perry`). The @@ -127,7 +127,7 @@ as `perry-extra`: a documented leniency, not prefix parity. 1. Edit `tools.node.version` in `external-tools.json` and refresh the per-platform `sha512` SRI (download each dist tarball, verify its sha256 against that version's `SHASUMS256.txt`, then record the recomputed sha512). -2. `node scripts/node_compat_matrix.mjs --update-baseline`. +2. `node scripts/node_compat_matrix.mts --update-baseline`. 3. **Review the diff.** A Node bump legitimately changes fingerprints (new exports, typeof changes); confirm the deltas are Node's, not Perry regressions, before committing. diff --git a/external-tools.json b/external-tools.json index f512326849..e4319b873d 100644 --- a/external-tools.json +++ b/external-tools.json @@ -1,15 +1,15 @@ { "tools": { "node": { - "description": "Node.js — the pinned oracle for the builtin-module compatibility matrix (scripts/node_compat_matrix.mjs).", + "description": "Node.js — the pinned oracle for the builtin-module compatibility matrix (scripts/node_compat_matrix.mts).", "version": "26.5.1", "release": "node-dist", "repository": "github:nodejs/node", "distBaseUrl": "https://nodejs.org/dist", "notes": [ - "Latest CURRENT stable at pin time. Bump by editing tools.node.version here (plus each platform asset name and its sha512 SRI), then run scripts/node_compat_matrix.mjs --update-baseline and review the diff. There is no NODE_PIN constant in that script — it reads this file; an earlier revision of this note said otherwise.", + "Latest CURRENT stable at pin time. Bump by editing tools.node.version here (plus each platform asset name and its sha512 SRI), then run scripts/node_compat_matrix.mts --update-baseline and review the diff. There is no NODE_PIN constant in that script — it reads this file; an earlier revision of this note said otherwise.", "Assets are the official nodejs.org dist tarballs. Integrity is sha512 recomputed from the downloaded bytes (matching this file's sha512 SRI convention); nodejs.org also publishes sha256 in SHASUMS256.txt, cross-checked at pin time against every asset here.", - "Installed by scripts/node_compat_matrix.mjs via its own resolver (download + SRI-verify + cache under .cache/node-pin/), NOT the shared tool rack: nodejs.org is not a github release host, and the tarball is a full node tree rather than a single bin.", + "Installed by scripts/node_compat_matrix.mts via its own resolver (download + SRI-verify + cache under .cache/node-pin/), NOT the shared tool rack: nodejs.org is not a github release host, and the tarball is a full node tree rather than a single bin.", "Official dist ships no musl build (musl lives on unofficial-builds.nodejs.org); alpine/musl runners fall back to a system node.", "LTS alternate: v24.18.1 (Krypton). Pass --node-version to fetch a non-pinned line — it is verified against that version's SHASUMS256.txt (sha256) since no sha512 pin exists here for it." ], diff --git a/gc-handoff/NODE26-NOTES.md b/gc-handoff/NODE26-NOTES.md index e45aa6eac3..8a9fe6a80f 100644 --- a/gc-handoff/NODE26-NOTES.md +++ b/gc-handoff/NODE26-NOTES.md @@ -107,7 +107,7 @@ because a gap-suite oracle bump must not be able to move a publishing toolchain. ### FINDING 4 — stale instruction inside `external-tools.json` `tools.node.notes[0]` says "Bump via the NODE_PIN.version constant in -scripts/node_compat_matrix.mjs". There is **no `NODE_PIN` constant** in that +scripts/node_compat_matrix.mts". There is **no `NODE_PIN` constant** in that file (grep: zero hits); the script reads `external-tools.json` itself and its own `--help` says so ("The pinned Node version lives in external-tools.json"). CLAUDE.md gives the correct instruction. Fixed the note in place. diff --git a/llms.txt b/llms.txt index 7f9ad41505..808e8cb065 100644 --- a/llms.txt +++ b/llms.txt @@ -35,7 +35,7 @@ perry update # Self-update ## Testing / node parity - `./run_parity_tests.sh` — hand-authored node-suite (behavioral parity vs `node --experimental-strip-types`). -- `node scripts/node_compat_matrix.mjs --module fs` — fast per-module export-shape check against a pinned Node oracle (26.5.1); drop `--module` for the full builtin sweep, add `--check` for the CI gate (systematic form of tracker #812). +- `node scripts/node_compat_matrix.mts --module fs` — fast per-module export-shape check against a pinned Node oracle (26.5.1); drop `--module` for the full builtin sweep, add `--check` for the CI gate (systematic form of tracker #812). ## Documentation diff --git a/package.json b/package.json index 654c347cdc..3a4b0d7c36 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,8 @@ { "type": "module", + "engines": { + "node": ">=22.18.0" + }, "scripts": { "soak": "node scripts/soak/soak.mts --check", "soak:fix": "node scripts/soak/soak.mts --fix", diff --git a/scripts/binding_pins.mjs b/scripts/binding_pins.mts similarity index 86% rename from scripts/binding_pins.mjs rename to scripts/binding_pins.mts index 543ae4988b..037a27167a 100644 --- a/scripts/binding_pins.mjs +++ b/scripts/binding_pins.mts @@ -45,6 +45,34 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +interface UpstreamPin { + version: string; + sha256: string; + repo?: string; + ref?: string; + 'ported-at': string; + date: string; +} + +interface Binding { + name: string; + fields: Record; + upstream: Record | null; +} + +interface NpmManifest { + dist: { tarball: string }; + repository?: string | { url?: string }; + gitHead?: string; +} + +interface NpmPackument { + name: string; + 'dist-tags'?: { latest?: string }; + versions?: Record; + time?: Record; +} + const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const TOML_PATH = path.join(ROOT, 'crates', 'perry', 'well_known_bindings.toml'); const REGISTRY = 'https://registry.npmjs.org'; @@ -54,12 +82,13 @@ const TARBALL_TIMEOUT_MS = 60_000; const GIT_TIMEOUT_MS = 60_000; // Perry's own packages have no third-party upstream to pin. -const SELF_OWNED = (name) => name.startsWith('@perryts/') || name.startsWith('perry/'); +const SELF_OWNED = (name: string): boolean => + name.startsWith('@perryts/') || name.startsWith('perry/'); // A binding is exempt from carrying its own npm provenance pin when it is // perry-owned, a Node builtin (upstream is Node core, not an npm dist), or an // alias/subpath of another binding (it shares that binding's pin). -const isExempt = (b) => +const isExempt = (b: Binding): boolean => SELF_OWNED(b.name) || b.fields['node-builtin'] === 'true' || Boolean(b.fields['alias-of']); // --------------------------------------------------------------------------- @@ -69,10 +98,10 @@ const isExempt = (b) => // instead of pulling a toml dependency into the repo's script surface. // --------------------------------------------------------------------------- -function parseBindings(raw) { - const bindings = new Map(); - let current = null; - let section = null; // 'binding' | 'upstream' +function parseBindings(raw: string): Map { + const bindings = new Map(); + let current: Binding | null = null; + let section: 'binding' | 'upstream' | null = null; for (const line of raw.split('\n')) { const header = line.match(/^\[bindings\.(?:"([^"]+)"|([^.\]"]+))(\.upstream)?\]\s*$/); if (header) { @@ -107,7 +136,7 @@ function parseBindings(raw) { return bindings; } -function upstreamBlockText(name, pin) { +function upstreamBlockText(name: string, pin: UpstreamPin): string { const key = /^[A-Za-z0-9_-]+$/.test(name) ? name : `"${name}"`; const lines = [`[bindings.${key}.upstream]`]; lines.push(`version = "${pin.version}"`); @@ -121,7 +150,7 @@ function upstreamBlockText(name, pin) { // Insert or replace the upstream sub-block directly after the binding's // own block, preserving every other byte of the file (comments included). -function writePin(raw, name, pin) { +function writePin(raw: string, name: string, pin: UpstreamPin): string { const esc = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // A binding key may appear bare (`date-fns`) or quoted (`"decimal.js"`, // and quoted-anyway `"date-fns"`); match whichever the file actually uses @@ -152,7 +181,7 @@ function writePin(raw, name, pin) { // npm registry // --------------------------------------------------------------------------- -async function fetchJson(url) { +async function fetchJson(url: string): Promise { let res; try { res = await fetch(url, { @@ -168,17 +197,17 @@ async function fetchJson(url) { return res.json(); } -async function fetchPackument(name) { +async function fetchPackument(name: string): Promise { return fetchJson(`${REGISTRY}/${name.replace('/', '%2f')}`); } -function latestStable(packument) { +function latestStable(packument: NpmPackument): string { const version = packument['dist-tags']?.latest; if (!version) throw new Error(`${packument.name}: no dist-tags.latest`); return version; } -async function sha256OfTarball(url) { +async function sha256OfTarball(url: string): Promise { let res; try { res = await fetch(url, { signal: AbortSignal.timeout(TARBALL_TIMEOUT_MS) }); @@ -192,7 +221,7 @@ async function sha256OfTarball(url) { return createHash('sha256').update(bytes).digest('hex'); } -function normalizeRepo(repository) { +function normalizeRepo(repository: string | { url?: string } | undefined): string | undefined { const url = typeof repository === 'string' ? repository : repository?.url; if (!url) return undefined; return url @@ -202,7 +231,7 @@ function normalizeRepo(repository) { .replace(/\.git$/, ''); } -async function provisionPin(name, requestedVersion) { +async function provisionPin(name: string, requestedVersion?: string): Promise { const packument = await fetchPackument(name); const version = requestedVersion ?? latestStable(packument); const manifest = packument.versions?.[version]; @@ -222,7 +251,7 @@ async function provisionPin(name, requestedVersion) { // modes // --------------------------------------------------------------------------- -async function modeSet(names, requestedVersion) { +async function modeSet(names: string[], requestedVersion?: string): Promise { let raw = fs.readFileSync(TOML_PATH, 'utf8'); const bindings = parseBindings(raw); for (const name of names) { @@ -236,7 +265,7 @@ async function modeSet(names, requestedVersion) { fs.writeFileSync(TOML_PATH, raw); } -async function modeBackfill() { +async function modeBackfill(): Promise { const raw = fs.readFileSync(TOML_PATH, 'utf8'); const bindings = parseBindings(raw); const unpinned = [...bindings.values()].filter((b) => !b.upstream && !isExempt(b)); @@ -249,7 +278,7 @@ async function modeBackfill() { } } -function checkOffline(bindings) { +function checkOffline(bindings: Map): string[] { const failures = []; for (const b of bindings.values()) { // Aliases inherit their target's provenance — verify the target exists @@ -297,16 +326,17 @@ function checkOffline(bindings) { return failures; } -async function checkRefresh(bindings, soakDays) { - const advisories = []; +async function checkRefresh(bindings: Map, soakDays: number): Promise { + const advisories: string[] = []; const now = Date.now(); for (const b of bindings.values()) { if (isExempt(b) || !b.upstream?.version) continue; - let packument; + let packument: NpmPackument; try { packument = await fetchPackument(b.name); } catch (err) { - advisories.push(`${b.name}: registry lookup failed (${err.message}) — skipping`); + const msg = err instanceof Error ? err.message : String(err); + advisories.push(`${b.name}: registry lookup failed (${msg}) — skipping`); continue; } const latest = latestStable(packument); @@ -319,14 +349,14 @@ async function checkRefresh(bindings, soakDays) { advisories.push( `${b.name}: pinned ${b.upstream.version}, latest stable ${latest} ` + `(soaked ${soakedDays}d >= ${soakDays}d) — re-pin, re-review, advance ported-at: ` + - `node scripts/binding_pins.mjs --set ${b.name}`, + `node scripts/binding_pins.mts --set ${b.name}`, ); } } return advisories; } -function modeMaterialize(name) { +function modeMaterialize(name: string): void { const bindings = parseBindings(fs.readFileSync(TOML_PATH, 'utf8')); const b = bindings.get(name); if (!b) throw new Error(`no [bindings.${name}] block`); @@ -362,9 +392,9 @@ function modeMaterialize(name) { // --------------------------------------------------------------------------- -const args = process.argv.slice(2); -const has = (flag) => args.includes(flag); -const valueOf = (flag) => { +const args: string[] = process.argv.slice(2); +const has = (flag: string): boolean => args.includes(flag); +const valueOf = (flag: string): string | undefined => { const i = args.indexOf(flag); return i !== -1 ? args[i + 1] : undefined; }; @@ -387,7 +417,7 @@ try { const bindings = parseBindings(fs.readFileSync(TOML_PATH, 'utf8')); const failures = checkOffline(bindings); for (const f of failures) console.error(`FAIL ${f}`); - let advisories = []; + let advisories: string[] = []; if (has('--refresh')) { const soakDays = Number(valueOf('--soak-days') ?? DEFAULT_SOAK_DAYS); advisories = await checkRefresh(bindings, soakDays); @@ -399,11 +429,12 @@ try { ); } else { console.error( - 'usage: binding_pins.mjs --set [version] | --backfill | --check [--refresh [--soak-days N]] | --materialize ', + 'usage: binding_pins.mts --set [version] | --backfill | --check [--refresh [--soak-days N]] | --materialize ', ); process.exit(2); } } catch (err) { - console.error(`error: ${err.message}`); + const msg = err instanceof Error ? err.message : String(err); + console.error(`error: ${msg}`); process.exit(1); } diff --git a/scripts/fp_fuzz.mjs b/scripts/fp_fuzz.mts similarity index 89% rename from scripts/fp_fuzz.mjs rename to scripts/fp_fuzz.mts index ceed8b5c59..c1f8614046 100755 --- a/scripts/fp_fuzz.mjs +++ b/scripts/fp_fuzz.mts @@ -13,10 +13,10 @@ // - identity round-trips (x + t - t, a*1, a/b*b) // // Usage: -// node scripts/fp_fuzz.mjs # 50 cases, random seed -// node scripts/fp_fuzz.mjs --count 500 --seed 42 # reproducible -// node scripts/fp_fuzz.mjs --verbose # per-case markers -// node scripts/fp_fuzz.mjs --replay # rerun a saved case +// node scripts/fp_fuzz.mts # 50 cases, random seed +// node scripts/fp_fuzz.mts --count 500 --seed 42 # reproducible +// node scripts/fp_fuzz.mts --verbose # per-case markers +// node scripts/fp_fuzz.mts --replay # rerun a saved case // // Failures are dumped under fp_fuzz_failures/ as the .ts source plus a // .report with both stdouts. @@ -30,8 +30,8 @@ import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = join(__dirname, ".."); -function parseArgs(argv) { - const out = {}; +function parseArgs(argv: string[]): Record { + const out: Record = {}; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (!a.startsWith("--")) continue; @@ -56,9 +56,9 @@ const VERBOSE = args.verbose === "true" || args.verbose === "1"; const REPLAY = args.replay; // mulberry32 — small reproducible PRNG. -function mulberry32(seed) { +function mulberry32(seed: number): () => number { let s = seed >>> 0; - return function () { + return function (): number { s = (s + 0x6d2b79f5) >>> 0; let t = s; t = Math.imul(t ^ (t >>> 15), t | 1); @@ -69,7 +69,7 @@ function mulberry32(seed) { // Magnitudes spanning subnormal-adjacent through near-overflow, log-uniform // in the exponent so we hit the precision-loss regime where (a+b)+c ≠ a+(b+c). -function randomFp(rng) { +function randomFp(rng: () => number): number { const r = rng(); if (r < 0.04) { const specials = [ @@ -92,7 +92,7 @@ function randomFp(rng) { // engines' parsers, reproduces the exact same f64 bits. Plain // Number#toString already round-trips for finite values; specials need // keyword form. -function tsLit(x) { +function tsLit(x: number): string { if (Object.is(x, -0)) return "-0"; if (Number.isNaN(x)) return "NaN"; if (x === Infinity) return "Infinity"; @@ -100,7 +100,7 @@ function tsLit(x) { return x.toString(); } -function genProgram(rng, seed, idx) { +function genProgram(rng: () => number, seed: number, idx: number): string { const N = 6; const ops = Array.from({ length: N }, () => randomFp(rng)); const lits = ops.map(tsLit).join(", "); @@ -174,7 +174,17 @@ console.log("dot:", dot.toString()); `; } -function runOne({ idx, src, perryBin }) { +interface RunResult { + ok: boolean; + reason?: string; + src?: string; + nodeStdout?: string; + perryStdout?: string; + nodeStderr?: string; + perryStderr?: string; +} + +function runOne({ idx, src, perryBin }: { idx: number | string; src: string; perryBin: string }): RunResult { const tag = `fpfuzz_${process.pid}_${idx}`; const tsPath = join(tmpdir(), `${tag}.ts`); const binPath = join(tmpdir(), `${tag}.bin`); @@ -213,7 +223,7 @@ function runOne({ idx, src, perryBin }) { return { ok: true }; } -function dumpFailure(failDir, seed, idx, result) { +function dumpFailure(failDir: string, seed: number, idx: number, result: RunResult): string { if (!existsSync(failDir)) mkdirSync(failDir, { recursive: true }); const base = join(failDir, `fail_${seed}_${idx}`); writeFileSync(`${base}.ts`, result.src); @@ -234,7 +244,7 @@ function dumpFailure(failDir, seed, idx, result) { return base; } -function diffPreview(node, perry, maxLines = 12) { +function diffPreview(node: string, perry: string, maxLines = 12): string { const nlines = node.split("\n"); const plines = perry.split("\n"); const out = []; @@ -279,7 +289,7 @@ console.log(`fp_fuzz: count=${COUNT} seed=${SEED} perry=${PERRY_BIN}`); const rng = mulberry32(SEED); let pass = 0; let fail = 0; -const firstFailures = []; +const firstFailures: (RunResult & { idx: number; base: string })[] = []; for (let i = 0; i < COUNT; i++) { const src = genProgram(rng, SEED, i); @@ -305,7 +315,7 @@ console.log(`fp_fuzz: pass=${pass}/${COUNT} fail=${fail} seed=${SEED}`); if (fail > 0) { console.log(`\nFailure cases written to ${FAIL_DIR}/`); - console.log(`Replay any with: node scripts/fp_fuzz.mjs --replay .ts\n`); + console.log(`Replay any with: node scripts/fp_fuzz.mts --replay .ts\n`); for (const f of firstFailures) { console.log(`idx=${f.idx} reason=${f.reason} -> ${f.base}.ts`); if (f.reason === "output-diverged") { diff --git a/scripts/node_compat_matrix.mjs b/scripts/node_compat_matrix.mts similarity index 88% rename from scripts/node_compat_matrix.mjs rename to scripts/node_compat_matrix.mts index e3193a3259..2ab8bb3dda 100644 --- a/scripts/node_compat_matrix.mjs +++ b/scripts/node_compat_matrix.mts @@ -95,19 +95,29 @@ const NODE_TIMEOUT_MS = 15_000 // --- CLI ------------------------------------------------------------------- -function parseArgs(argv) { +function parseArgs(argv: string[]) { // moduleSet: null = all builtins; otherwise the selected base modules. // methodMap: base -> [export names] to fingerprint (subset mode). - const args = { + const args: { + mode: 'run' | 'check' | 'update' | 'help' + moduleSet: Set | null + methodMap: Map + globalMethods: string[] + nodeVersion: string | null + json: boolean + subsetActive: boolean + } = { mode: 'run', moduleSet: null, - methodMap: new Map(), + methodMap: new Map(), globalMethods: [], nodeVersion: null, json: false, + subsetActive: false, } - const modules = new Set() - const splitList = s => (s || '').split(',').map(x => x.trim()).filter(Boolean) + const modules = new Set() + const splitList = (s: string | undefined): string[] => + (s || '').split(',').map(x => x.trim()).filter(Boolean) for (let i = 0; i < argv.length; i++) { const a = argv[i] if (a === '--check') args.mode = 'check' @@ -151,20 +161,20 @@ function parseArgs(argv) { return args } -const HELP = `node_compat_matrix.mjs — Node builtin-module compatibility matrix +const HELP = `node_compat_matrix.mts — Node builtin-module compatibility matrix # FAST LOOP (reach for this while iterating on one builtin): - node scripts/node_compat_matrix.mjs --module fs - node scripts/node_compat_matrix.mjs --module fs,path,crypto - node scripts/node_compat_matrix.mjs --module fs --method readFileSync,promises - node scripts/node_compat_matrix.mjs --only fs.readFileSync,path.join + node scripts/node_compat_matrix.mts --module fs + node scripts/node_compat_matrix.mts --module fs,path,crypto + node scripts/node_compat_matrix.mts --module fs --method readFileSync,promises + node scripts/node_compat_matrix.mts --only fs.readFileSync,path.join # FULL SWEEP + GATE: - node scripts/node_compat_matrix.mjs run + print table - node scripts/node_compat_matrix.mjs --check gate against baseline - node scripts/node_compat_matrix.mjs --update-baseline - node scripts/node_compat_matrix.mjs --node-version 24.18.1 - node scripts/node_compat_matrix.mjs --json + node scripts/node_compat_matrix.mts run + print table + node scripts/node_compat_matrix.mts --check gate against baseline + node scripts/node_compat_matrix.mts --update-baseline + node scripts/node_compat_matrix.mts --node-version 24.18.1 + node scripts/node_compat_matrix.mts --json A --module selector makes --check / --update-baseline touch only that slice. A --method / --only subset is a print-only fast diagnostic (it changes the @@ -175,28 +185,34 @@ Bump it there, run --update-baseline, and review the diff.` // --- pin + platform -------------------------------------------------------- -function loadNodePin() { +interface NodePin { + version: string + distBaseUrl?: string + platforms?: Record +} + +function loadNodePin(): NodePin { const pin = JSON.parse(readFileSync(EXTERNAL_TOOLS_JSON, 'utf8')).tools.node if (!pin) throw new Error('external-tools.json has no "node" pin') return pin } -function platformKey() { +function platformKey(): string { const okey = { darwin: 'darwin', linux: 'linux', win32: 'win' }[process.platform] const akey = { arm64: 'arm64', x64: 'x64' }[process.arch] if (!okey || !akey) throw new Error(`unsupported platform ${process.platform}-${process.arch}`) return `${okey}-${akey}` } -function sriSha512(buf) { +function sriSha512(buf: Buffer): string { return `sha512-${createHash('sha512').update(buf).digest('base64')}` } -function sha256hex(buf) { +function sha256hex(buf: Buffer): string { return createHash('sha256').update(buf).digest('hex') } -async function fetchBuffer(url) { +async function fetchBuffer(url: string): Promise { const res = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(180_000) }) if (!res.ok) throw new Error(`download failed ${res.status} ${url}`) return Buffer.from(await res.arrayBuffer()) @@ -208,7 +224,7 @@ async function fetchBuffer(url) { * Pinned line: verify against the sha512 SRI in external-tools.json. * Non-pinned line: verify against that version's SHASUMS256.txt (sha256). */ -async function resolveNode(pin, versionOverride) { +async function resolveNode(pin: NodePin, versionOverride: string | null): Promise { const version = versionOverride || pin.version const key = platformKey() const [okey, akey] = key.split('-') @@ -285,7 +301,7 @@ async function resolveNode(pin, versionOverride) { const FP_RE = /__FP__([\s\S]*?)__FP__/ -function probeSource(spec, methods) { +function probeSource(spec: string, methods: string[] | undefined): string { // A single sentinel-wrapped line carries the shape fingerprint, so any // node/perry warnings on stdout/stderr are simply ignored by the extractor. // With a method subset, fingerprint ONLY those exports (a fast diagnostic @@ -306,17 +322,17 @@ function probeSource(spec, methods) { ].join('\n') } -function extractFp(output) { +function extractFp(output: string): string | null { const m = FP_RE.exec(output) - return m ? m[1] : null + return m ? m[1]! : null } -function fpHash(fp) { +function fpHash(fp: string | null): string { return fp === null ? '' : createHash('sha256').update(fp).digest('hex').slice(0, 12) } /** Ask the pinned Node (oracle) for a module form's fingerprint. */ -function oracleFingerprint(nodeBin, probeFile) { +function oracleFingerprint(nodeBin: string, probeFile: string): string | null { const res = spawnSync( nodeBin, ['--experimental-strip-types', probeFile], @@ -330,7 +346,7 @@ function oracleFingerprint(nodeBin, probeFile) { } /** Compile with Perry, run, return the fingerprint (or null on any failure). */ -function perryFingerprint(probeFile, outBin) { +function perryFingerprint(probeFile: string, outBin: string): string | null { const compileEnv = { ...process.env, PERRY_ALLOW_UNIMPLEMENTED: '1' } let c = spawnSync(PERRY_BIN, [probeFile, '-o', outBin], { encoding: 'utf8', @@ -361,7 +377,7 @@ function perryFingerprint(probeFile, outBin) { // --- status model ---------------------------------------------------------- // Lower is better. --check flags a cell whose severity increased. -const SEVERITY = { +const SEVERITY: Record = { skip: -1, match: 0, 'perry-extra': 0, // Perry resolves a form Node's oracle didn't — not a regression. @@ -370,7 +386,7 @@ const SEVERITY = { 'perry-unresolved': 3, } -function cellStatus(oracleFp, perryFp, skipped) { +function cellStatus(oracleFp: string | null, perryFp: string | null, skipped: boolean): string { if (skipped) return 'skip' const oracleOk = oracleFp !== null const perryOk = perryFp !== null @@ -382,17 +398,17 @@ function cellStatus(oracleFp, perryFp, skipped) { // --- manifest cross-check -------------------------------------------------- -function extractRustArray(src, name) { +function extractRustArray(src: string, name: string): string[] { const start = src.indexOf(`${name}: &[&str] = &[`) if (start < 0) return [] const end = src.indexOf('];', start) const body = src.slice(start, end) - const out = new Set() + const out = new Set() for (const m of body.matchAll(/"([^"]+)"/g)) out.add(m[1]) return [...out] } -function loadManifestModules() { +function loadManifestModules(): Set { const src = readFileSync(MANIFEST_ENTRIES, 'utf8') const native = extractRustArray(src, 'NATIVE_MODULES') const submodules = extractRustArray(src, 'NODE_SUBMODULES') @@ -401,7 +417,7 @@ function loadManifestModules() { // --- enumerate builtins ---------------------------------------------------- -function enumerateBuiltins(nodeBin) { +function enumerateBuiltins(nodeBin: string): string[] { const res = spawnSync( nodeBin, ['-e', "process.stdout.write(require('module').builtinModules.join('\\n'))"], @@ -420,14 +436,21 @@ function enumerateBuiltins(nodeBin) { return [...bases].sort() } -function loadSkip() { +function loadSkip(): Record { if (!existsSync(SKIP_PATH)) return {} return JSON.parse(readFileSync(SKIP_PATH, 'utf8')).modules || {} } // --- run the matrix -------------------------------------------------------- -async function runMatrix(args) { +type ParsedArgs = ReturnType + +async function runMatrix(args: ParsedArgs): Promise<{ + version: string + platform: string + modules: Record + __partial?: boolean +}> { if (!existsSync(PERRY_BIN)) { throw new Error( `perry release binary missing at ${PERRY_BIN}\n build it: cargo build --release -p perry`, @@ -450,12 +473,12 @@ async function runMatrix(args) { } const tmp = mkdtempSync(path.join(os.tmpdir(), 'perry-node-compat-')) - const results = {} + const results: Record = {} let done = 0 for (const base of bases) { done++ process.stderr.write(`\r[node-compat] ${done}/${bases.length} ${base.padEnd(24)}`) - const entry = {} + const entry: Record = {} const methods = args.methodMap.get(base) for (const [form, spec] of [ ['unprefixed', base], @@ -501,7 +524,7 @@ async function runMatrix(args) { // --- reporting ------------------------------------------------------------- -const GLYPH = { +const GLYPH: Record = { match: 'match', 'shape-diff': 'SHAPE-DIFF', 'perry-unresolved': 'UNRESOLVED', @@ -510,16 +533,25 @@ const GLYPH = { skip: 'skip', } -function summarize(matrix, manifestModules) { +function summarize(matrix: { modules: Record }, manifestModules: Set): { + total: number + bothMatch: number + perryUnresolved: string[] + shapeDiff: string[] + prefixDivergences: string[] + claimedButBroken: string[] + worksButUnclaimed: string[] + skipped: string[] +} { const mods = Object.entries(matrix.modules) const total = mods.length let bothMatch = 0 - const perryUnresolved = [] - const shapeDiff = [] - const prefixDivergences = [] - const claimedButBroken = [] - const worksButUnclaimed = [] - const skipped = [] + const perryUnresolved: string[] = [] + const shapeDiff: string[] = [] + const prefixDivergences: string[] = [] + const claimedButBroken: string[] = [] + const worksButUnclaimed: string[] = [] + const skipped: string[] = [] for (const [base, e] of mods) { if (e.unprefixed.status === 'skip') { @@ -550,7 +582,7 @@ function summarize(matrix, manifestModules) { } } -function printTable(matrix) { +function printTable(matrix: { modules: Record }): void { const rows = Object.entries(matrix.modules) const w = Math.max(...rows.map(([b]) => b.length), 6) console.log('') @@ -564,7 +596,7 @@ function printTable(matrix) { } } -function printSummary(s, matrix) { +function printSummary(s: ReturnType, matrix: { version: string; platform: string }): void { console.log('') console.log(`Node oracle: v${matrix.version} (${matrix.platform})`) console.log(`Builtins probed: ${s.total}`) @@ -581,7 +613,7 @@ function printSummary(s, matrix) { const BASELINE_SCHEMA = { description: - 'Per-module x per-form (unprefixed / node:-prefixed) export-shape status for the Node builtin-module compatibility matrix. Generated by scripts/node_compat_matrix.mjs against the pinned Node oracle (external-tools.json tools.node). --check fails on any cell that got strictly worse or any prefix-parity invariant that broke; improvements are accepted. Regenerate with --update-baseline and review the diff.', + 'Per-module x per-form (unprefixed / node:-prefixed) export-shape status for the Node builtin-module compatibility matrix. Generated by scripts/node_compat_matrix.mts against the pinned Node oracle (external-tools.json tools.node). --check fails on any cell that got strictly worse or any prefix-parity invariant that broke; improvements are accepted. Regenerate with --update-baseline and review the diff.', statuses: { match: 'perry export fingerprint == node oracle fingerprint', 'shape-diff': 'both resolved, fingerprints differ (a real shape gap)', @@ -598,7 +630,7 @@ const BASELINE_SCHEMA = { }, } -function toBaseline(matrix) { +function toBaseline(matrix: { version: string; platform: string; modules: Record }): any { return { _schema: BASELINE_SCHEMA, nodeVersion: matrix.version, @@ -607,7 +639,7 @@ function toBaseline(matrix) { } } -function writeBaseline(matrix, partial) { +function writeBaseline(matrix: { version: string; platform: string; modules: Record }, partial: boolean): void { let doc = toBaseline(matrix) if (partial && existsSync(BASELINE_PATH)) { // Selector-scoped update: overlay ONLY the probed modules onto the @@ -625,7 +657,7 @@ function writeBaseline(matrix, partial) { console.error(`[node-compat] wrote baseline ${path.relative(REPO_ROOT, BASELINE_PATH)}${scope}`) } -function baselineValidationError(base) { +function baselineValidationError(base: any): string | null { if (!base || typeof base !== 'object' || Array.isArray(base)) { return 'baseline root must be an object' } @@ -649,7 +681,7 @@ function baselineValidationError(base) { return null } -function checkAgainstBaseline(matrix) { +function checkAgainstBaseline(matrix: { version: string; platform: string; modules: Record; __partial?: boolean }): number { if (!existsSync(BASELINE_PATH)) { console.error(`[node-compat] no baseline at ${BASELINE_PATH} — run --update-baseline first`) return 1 @@ -707,7 +739,7 @@ function checkAgainstBaseline(matrix) { ) return 1 } - const regressions = [] + const regressions: string[] = [] for (const [name, cur] of Object.entries(matrix.modules)) { const prev = base.modules[name] if (!prev) continue // new module (e.g. after a node bump) — not a regression @@ -743,7 +775,7 @@ function checkAgainstBaseline(matrix) { // --- main ------------------------------------------------------------------ -async function main() { +async function main(): Promise { const args = parseArgs(process.argv.slice(2)) if (args.mode === 'help') { console.log(HELP) diff --git a/scripts/soak/external-tools.mts b/scripts/soak/external-tools.mts index 09c462a36e..88ab76791f 100644 --- a/scripts/soak/external-tools.mts +++ b/scripts/soak/external-tools.mts @@ -519,7 +519,7 @@ export async function installTool(name: string, tools: Record): // bin). Its download+verify+cache resolver lives in the compat runner, // which needs a full node tree rather than a rack bin handle. console.log( - `[external-tools] ${name} is a node-dist pin — installed by scripts/node_compat_matrix.mjs (its own resolver), not the tool rack`, + `[external-tools] ${name} is a node-dist pin — installed by scripts/node_compat_matrix.mts (its own resolver), not the tool rack`, ) return } diff --git a/test-parity/node-compat-matrix.baseline.json b/test-parity/node-compat-matrix.baseline.json index 01bcee8820..9562e8f560 100644 --- a/test-parity/node-compat-matrix.baseline.json +++ b/test-parity/node-compat-matrix.baseline.json @@ -1,6 +1,6 @@ { "_schema": { - "description": "Per-module x per-form (unprefixed / node:-prefixed) export-shape status for the Node builtin-module compatibility matrix. Generated by scripts/node_compat_matrix.mjs against the pinned Node oracle (external-tools.json tools.node). --check fails on any cell that got strictly worse or any prefix-parity invariant that broke; improvements are accepted. Regenerate with --update-baseline and review the diff.", + "description": "Per-module x per-form (unprefixed / node:-prefixed) export-shape status for the Node builtin-module compatibility matrix. Generated by scripts/node_compat_matrix.mts against the pinned Node oracle (external-tools.json tools.node). --check fails on any cell that got strictly worse or any prefix-parity invariant that broke; improvements are accepted. Regenerate with --update-baseline and review the diff.", "statuses": { "match": "perry export fingerprint == node oracle fingerprint", "shape-diff": "both resolved, fingerprints differ (a real shape gap)", diff --git a/test-parity/node-compat-matrix.skip.json b/test-parity/node-compat-matrix.skip.json index bffaacb089..d3bdc033fe 100644 --- a/test-parity/node-compat-matrix.skip.json +++ b/test-parity/node-compat-matrix.skip.json @@ -1,6 +1,6 @@ { "_schema": { - "description": "Curated skip / known-partial list for scripts/node_compat_matrix.mjs. A builtin listed here is NOT probed for export shape and is recorded with status \"skip\" (plus this reason) rather than silently passing. Reserve this for modules that cannot be meaningfully fingerprinted by a bare `import * as m` — side-effectful on import, export shape depends on constructor args, or the import legitimately aborts the process. Behavioral coverage for these lives in the hand-authored node-suite (run_parity_tests.sh).", + "description": "Curated skip / known-partial list for scripts/node_compat_matrix.mts. A builtin listed here is NOT probed for export shape and is recorded with status \"skip\" (plus this reason) rather than silently passing. Reserve this for modules that cannot be meaningfully fingerprinted by a bare `import * as m` — side-effectful on import, export shape depends on constructor args, or the import legitimately aborts the process. Behavioral coverage for these lives in the hand-authored node-suite (run_parity_tests.sh).", "fields": { "reason": "Why a bare-import shape fingerprint is not meaningful for this module.", "added": "ISO date (YYYY-MM-DD) the skip was added."