diff --git a/.github/workflows/node-minimum-compatibility.yml b/.github/workflows/node-minimum-compatibility.yml new file mode 100644 index 000000000..35f9ff2b4 --- /dev/null +++ b/.github/workflows/node-minimum-compatibility.yml @@ -0,0 +1,72 @@ +name: node-minimum-compatibility + +on: + pull_request: + push: + branches: + - develop + - main + +permissions: + contents: read + +env: + GIT_CONFIG_COUNT: "1" + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: develop + EXPECTED_NPM_VERSION: "10.9.9" + +jobs: + node-minimum-compatibility: + name: gate / build / node-minimum-compatibility + runs-on: macos-15 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 22.22.2 + package-manager-cache: false + - name: Activate pinned npm runtime + run: corepack enable npm + - name: Verify exact npm lockfile generator and bundled tar + run: | + test "$(npm --version)" = "$EXPECTED_NPM_VERSION" + npm run check:npm-runtime + - name: Install frozen Node dependencies + run: npm ci --ignore-scripts --no-audit --no-fund + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + version: "0.8.6" + enable-cache: false + - name: Install stable Rust toolchain + run: rustup toolchain install stable --profile minimal + - name: Sync frozen Python dependencies + run: uv sync --project services/analysis-engine --group dev --frozen + - name: Build and install Rust numeric extension + shell: bash + run: | + VENV_PY="$PWD/services/analysis-engine/.venv/bin/python" + uvx maturin@1.9.6 build --release \ + --manifest-path services/analysis-engine/rust/Cargo.toml \ + --interpreter "$VENV_PY" \ + --out services/analysis-engine/rust/dist + uv pip install --python "$VENV_PY" services/analysis-engine/rust/dist/*.whl + - name: Lint + run: npm run lint + - name: Typecheck + run: npm run typecheck + - name: Test with measured coverage + run: npm run test + - name: Build production workspaces + run: npm run build + - name: Build Storybook + run: npm run build-storybook --workspace @bandscope/desktop + - name: Check Tauri shell + run: cargo +stable check --manifest-path apps/desktop/src-tauri/Cargo.toml --locked + - name: Test Tauri shell + run: cargo +stable test --manifest-path apps/desktop/src-tauri/Cargo.toml --locked diff --git a/CLAUDE.md b/CLAUDE.md index b5a34c1fa..58303dcf2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ Agent execution and delegation rules live in `docs/agents/README.md`. PR canonic ## Common commands -Setup (Node >=22.13 <23, Python >=3.12 via `uv`, Rust stable only for the Tauri shell): +Setup (Node >=22.22.2 <23, Python >=3.12 via `uv`, Rust stable only for the Tauri shell): ```bash npm install @@ -51,7 +51,7 @@ BandScope is a local-first desktop app for rehearsal prep: it turns a song into Three layers, decoupled through shared contracts: -- `apps/desktop` — Tauri 2 + Vite + React 19 shell (Tailwind 4, Base UI, Storybook). Feature screens live in `src/features/` (home, workspace, chords, ranges, player, settings). The ready workspace names tonight's first playable range and the next instrument check. `src/lib/analysis.ts` and `src/lib/job_runner.ts` call typed Tauri IPC commands, with a browser fallback that serves demo data when not running inside Tauri. +- `apps/desktop` — Tauri 2 + Vite + React 19 shell (Tailwind 4, Base UI, Storybook). Feature screens live in `src/features/` (home, workspace, chords, ranges, player, settings). `src/lib/analysis.ts` and `src/lib/job_runner.ts` call typed Tauri IPC commands, with a browser fallback that serves demo data when not running inside Tauri. - `apps/desktop/src-tauri/src/main.rs` — the Rust orchestration boundary. Tauri commands (`start_analysis_job`, `get_analysis_job_status`, `select_local_audio_source`, `import_youtube_url`) validate untrusted input (project IDs, file paths, URLs) and spawn the Python engine as a subprocess. There is no loopback HTTP listener and no network path for local analysis. - `services/analysis-engine` — Python package `bandscope_analysis` (librosa/numpy). Entry point `cli.py` reads a JSON job request on stdin and prints a structured job-status JSON envelope on stdout (`--progress-jsonl` streams progress lines). `api.py` orchestrates the pipeline across the `separation`, `sections`, `roles`, `chords`, `ranges`, `temporal`, `transcription`, and `youtube` modules. diff --git a/apps/desktop/package.json b/apps/desktop/package.json index e09719b22..49ee6919c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -39,7 +39,7 @@ "@vitejs/plugin-react": "^6.0.2", "@vitest/coverage-v8": "^4.1.10", "eslint": "^10.7.0", - "jsdom": "^29.1.1", + "jsdom": "^30.0.1", "storybook": "^10.4.6", "tailwindcss": "^4.2.4", "typescript": "^6.0.3", diff --git a/docs/doctoring/npm-lockfile-generator-provenance.md b/docs/doctoring/npm-lockfile-generator-provenance.md index 72a31befc..4bead33d7 100644 --- a/docs/doctoring/npm-lockfile-generator-provenance.md +++ b/docs/doctoring/npm-lockfile-generator-provenance.md @@ -2,34 +2,27 @@ ## Decision -BandScope records npm `10.9.9` as the approved generator for root workspace dependency updates. The root manifest records that decision through: +BandScope records npm `10.9.9` as the approved generator for root workspace dependency updates. The root manifest records that decision through `packageManager: npm@10.9.9` and `devEngines.packageManager` with `onFail: error`. npm is intentionally not repeated under runtime `engines`; the package-manager generator and the supported Node runtime are separate contracts. -- `packageManager: npm@10.9.9` as package-manager selection metadata; and -- `devEngines.packageManager` with `onFail: error` as npm's source-tree command gate. +The #779/jsdom 30 compatibility slice raises the supported Node 22 interval to `>=22.22.2 <23`. Primary CI continues to exercise Node `22.22.3`, while `.github/workflows/node-minimum-compatibility.yml` exercises the exact `22.22.2` lower boundary. Both lanes activate the repository-pinned npm runtime with Corepack, verify npm `10.9.9` and its bundled `tar` security floor before dependency extraction, and consume only the committed lock with frozen `npm ci`. -The npm version is intentionally not repeated under `engines`. npm serializes `engines` into the root lock package, so adding an npm-only source-tool constraint there creates lock metadata churn unrelated to dependency resolution. `devEngines` and the explicit CI assertion enforce the approved generator while the published `engines.node` range remains the runtime compatibility contract. +The current #779 branch is intentionally fail-closed until `package-lock.json` is regenerated as one complete artifact with the approved npm `10.9.9` generator after the jsdom 30 manifest change. The lock must not be hand-edited or partially transplanted from predecessor dependency PRs. Until the generated lock carries the Node floor and jsdom 30 graph and passes exact-head frozen consumption, the branch is not mergeable. -Primary CI does **not** regenerate or update `package-lock.json`. It uses Node `22.22.3`, enables the npm shim supplied by the Node-bundled Corepack, resolves the project-pinned npm `10.9.9`, verifies that exact npm runtime and its own bundled `tar` package before dependency consumption, and validates the committed lock with `npm ci --ignore-scripts --no-audit --no-fund`. The gate then rejects any manifest or lockfile working-tree change. The normal verification jobs repeat the same runtime provenance gate before the repository's reviewed `npm ci` installation. +## Why npm 10.9.9 is authoritative -The Node runtime support decision remains separate. This change does not raise the public `>=22.13 <23` Node range; a coordinated Node-floor migration is tracked independently. +The prior approved npm `10.9.8` bundled `tar 7.5.11`. GitHub advisory GHSA-23hp-3jrh-7fpw / CVE-2026-59873 records `tar <=7.5.18` as affected by an unbounded decompression/parse denial-of-service vulnerability and `7.5.19` as the patched floor. npm `10.9.9` bundles `tar 7.5.22`. -## Why the npm runtime was advanced - -The prior approved npm `10.9.8` bundled `tar 7.5.11`. GitHub's reviewed advisory GHSA-23hp-3jrh-7fpw / CVE-2026-59873 marks `tar <=7.5.18` affected by an unbounded decompression/parse denial-of-service vulnerability and records `7.5.19` as the patched floor. npm `10.9.9` updates its bundled `tar` to `7.5.22`. - -The Node 22 distribution line still bundled npm `10.9.8` when this repair was made, so merely advancing the Node 22 patch selector did not remove the vulnerable package-manager runtime. BandScope therefore keeps the supported Node 22 contract and activates the repository-pinned npm `10.9.9` through bundled Corepack before any `npm ci` step. `scripts/checks/verify_npm_runtime.mjs`, executed through that npm runtime, locates the running npm package via `npm_execpath`, verifies npm `10.9.9`, reads npm's own `node_modules/tar/package.json`, and rejects a tar version below `7.5.19` before dependency extraction is allowed. +Node `22.22.2` and `22.22.3` ship an older bundled npm, so merely selecting the Node patch release is not sufficient. BandScope enables the project-pinned npm shim before repository-scoped npm dependency consumption. `scripts/checks/verify_npm_runtime.mjs`, executed by that selected npm runtime, verifies npm `10.9.9` and rejects a bundled `tar` below `7.5.19`. This is a package-manager execution boundary, not an application dependency override. BandScope does not add `tar` to the application graph or suppress the advisory. -## Why generator provenance still matters +## Why generator provenance matters -npm documents `package-lock.json` as the location-keyed description of the exact dependency tree. Lockfile version 3 is intended for npm 9 and newer. npm also notes that package-manager versions and tree-shaping configuration can affect the generated dependency graph and metadata. Dependency updates therefore use the reviewed npm `10.9.9` toolchain, and reviewers examine the complete generated lock diff together with its manifest change. +npm documents `package-lock.json` as the location-keyed description of the exact dependency tree. Package-manager versions and tree-shaping configuration can affect generated graph and metadata. Dependency changes therefore use the reviewed npm `10.9.9` toolchain and reviewers examine the complete generated lock diff together with manifest intent. -That provenance is distinct from CI validation. `npm ci` is the immutable consumption path: it requires a lockfile, rejects manifest/lock dependency disagreement, removes an existing `node_modules`, and never writes the manifest or lock. CI relies on that frozen behavior instead of running `npm install`, `npm update`, or `npx` commands that may perform mutable resolution. +CI validation is deliberately different from generation. `npm ci` requires a lockfile, rejects manifest/lock disagreement, removes an existing `node_modules`, and never rewrites the manifest or lock. Primary CI and the exact-minimum Node lane use this immutable path; they do not run `npm install`, `npm update`, `npx`, or another mutable resolution command to make a stale lock appear green. -The repository additionally requires a Subresource Integrity value for every package-lock entry resolved from the public npm registry. npm documents `integrity` as the SHA-512 or SHA-1 SRI string for the artifact unpacked at that location. - -The root lock also retains `peer: true` on the platform-specific `node_modules/@esbuild/*` records produced by the approved tree. Multiple dependency-update branches generated with a different serialization path were observed removing those markers even when the requested package change was unrelated to esbuild. Because frozen `npm ci` consumes rather than regenerates the lock, ordinary frozen-install validation alone cannot prove that this generator-sensitive metadata was preserved. The repository therefore treats those markers as a regression sentinel: a dependency PR that strips them must be regenerated with the approved npm toolchain rather than normalizing the unrelated churn by hand. +Every package-lock entry resolved from the public npm registry must retain Subresource Integrity evidence. The root lock also retains `peer: true` on platform-specific `node_modules/@esbuild/*` records. Loss of those markers is treated as generator drift and requires regeneration with the approved toolchain rather than manual normalization. ```mermaid flowchart LR @@ -37,63 +30,75 @@ flowchart LR C --> R[verify npm 10.9.9 and bundled tar >= 7.5.19] R --> G[approved npm update toolchain] G --> L[reviewed package-lock.json v3] - L --> V[npm ci frozen validation, lifecycle disabled] + L --> V[npm ci frozen validation] V --> D{manifest or lock drift?} D -->|yes| F[fail closed] D -->|no| S[verify SRI and generator-sensitive metadata] - S --> N[normal npm ci and repository checks] + S --> Q[full product and security gates] ``` +## Exact-minimum Node verification + +jsdom `30.0.1` declares a Node floor compatible with Node `22.22.2`. BandScope intentionally stays on the Node 22 line for this migration, so the repository contract is `>=22.22.2 <23` rather than an implicit expansion to Node 24 or 26. + +The dedicated minimum-runtime workflow must: + +1. check out without persisted credentials; +2. install exact Node `22.22.2` with package-manager cache discovery disabled; +3. run `corepack enable npm` before repository dependency consumption; +4. verify exact npm `10.9.9` and bundled `tar >=7.5.19` through `check:npm-runtime`; +5. run frozen `npm ci --ignore-scripts --no-audit --no-fund`; and +6. run lint, strict typecheck, measured tests, production build, Storybook, and locked Tauri check/test. + +The ordering is security-significant: setup-node must not invoke npm cache discovery through the Node-bundled npm before the reviewed project npm runtime is authoritative. + ## Security and operational boundary - Every primary CI job that consumes npm dependencies activates the project-pinned npm runtime and runs `check:npm-runtime` before its first `npm ci`. -- The runtime check fails closed unless the executing npm is exactly `10.9.9` and its own bundled `tar` is at least `7.5.19`. -- CI lock validation must not run `npm install`, `npm update`, `npx`, or another mutable dependency-resolution command. -- Dependency PRs change manifest intent and the complete lock artifact produced by the approved npm `10.9.9` update toolchain; reviewers reject unexplained lock churn rather than hand-editing records. -- Platform-specific root `@esbuild/*` lock records must retain their expected `peer: true` metadata. Missing markers are treated as generator drift, not as an acceptable side effect of an unrelated dependency update. -- The lock-validation job disables dependency lifecycle scripts. The normal clean install retains the repository's reviewed execution behavior. -- Registry-resolved package records require SRI evidence in the committed lock. -- Install-shaping flags that affect the dependency tree, such as `legacy-peer-deps` or `install-links`, must be committed in project configuration and applied consistently to generation and `npm ci`. -- The root `package-lock.json` remains the sole npm workspace lock. Nested workspace locks are prohibited. - -`packageManager` alone is not the enforcement boundary for npm because Node distributions do not enable Corepack's npm shim by default. Enforcement is provided by explicit `corepack enable npm`, npm `devEngines`, the exact runtime/tar provenance check, the frozen `npm ci` contract, and repository tests that prohibit mutable resolution in the lock gate. +- The runtime check fails closed unless npm is exactly `10.9.9` and its own bundled `tar` is at least `7.5.19`. +- CI lock validation and the exact-minimum lane must not run mutable npm resolution. +- Dependency PRs change manifest intent and the complete lock artifact produced by npm `10.9.9`; unexplained lock churn is rejected rather than hand-edited. +- Registry-resolved lock records require SRI evidence, and root `@esbuild/*` platform records retain expected peer metadata. +- Checkout credentials are not persisted in npm-consuming CI jobs. +- Install-shaping flags that affect the tree must be committed and applied consistently to generation and frozen consumption. +- The root `package-lock.json` remains the sole npm workspace lock; nested workspace locks are prohibited. ## Verification -`services/analysis-engine/tests/test_npm_toolchain_contract.py` verifies: - -1. the manifest's approved npm metadata and Node/runtime separation; -2. the exact Node/npm identity used by primary CI; -3. Corepack activation and npm runtime/tar verification before every primary npm dependency-consumption step; -4. frozen `npm ci` lock validation with lifecycle execution disabled; -5. absence of `npm install`, `npm update`, and `npx` from the lock-validation job; -6. a clean manifest/lock working tree after validation; -7. package-lock version 3; -8. SRI evidence for every public npm-registry artifact in the root lock; and -9. preservation of `peer: true` on every root `node_modules/@esbuild/*` platform record. +`services/analysis-engine/tests/test_npm_toolchain_contract.py` verifies the npm generator metadata, Node/npm identity in primary CI, Corepack/runtime-audit ordering, credential-free checkouts, immutable lock validation, lockfile version 3, SRI evidence, and generator-sensitive esbuild peer metadata. -The exact PDF.js and Undici baseline is covered separately by `test_high_security_dependency_baseline.py` and the desktop PDF loader tests. +`services/analysis-engine/tests/test_node_runtime_contract.py` separately verifies the `>=22.22.2 <23` interval, explicit rejection of Node `22.22.1`, jsdom 30 manifest/lock alignment, the exact-minimum workflow, npm runtime verification before dependency reads, the full compatibility acceptance surface, and removal of the superseded Node floor from canonical runtime/build documentation. -A dependency update is mergeable only after the updated manifest and complete generated lock are reviewed together and the exact current head passes npm runtime provenance, frozen lock validation, normal install, lint, strict typecheck, measured tests, production build, Rust/Tauri checks, security/supply-chain gates, current review, independent approval, and branch protection without bypass. +The PDF.js and Undici baseline remains covered separately by `test_high_security_dependency_baseline.py` and desktop PDF-loader tests. ## Claim boundary -CI proves that the committed manifest and lock can be consumed as a frozen pair by the approved toolchain, that the npm runtime used for dependency extraction is the reviewed version with a non-vulnerable bundled tar floor, that public-registry lock entries carry integrity evidence, and that the known generator-sensitive `@esbuild/*` peer markers remain present. It does **not** claim that resolving mutable manifest ranges again at a later time will reproduce byte-identical lock metadata. When a dependency update is needed, npm `10.9.9` remains the approved generator and its entire resulting lock diff is review evidence. +Passing frozen validation proves only that the committed manifest and lock can be consumed together by the reviewed toolchain and that the package-manager extraction runtime satisfies the pinned security floor. It does not prove that resolving mutable dependency ranges later will reproduce byte-identical lock metadata. + +Likewise, the exact-minimum lane proves only BandScope's selected Node 22 lower boundary. It does not broaden support to other Node major lines because upstream jsdom supports them. + +For the active #779 branch, no success claim is valid until the complete jsdom 30 lock is generated by npm `10.9.9`, reviewed as a whole, and all required exact-head CI, security, supply-chain, coverage, build, release, and independent-review gates pass on an unchanged head. ## Incident response and rollback When an update produces unexpected lock churn or npm runtime provenance fails: 1. preserve the exact head SHA, npm, bundled tar and Node versions, project npm configuration, original lock blob SHA, generated lock, and relevant CI run IDs; -2. determine whether manifest intent, npm, project configuration, registry metadata, transitive dependency resolution, or the package-manager runtime changed; -3. never accept a partial or hand-edited lock or disable the runtime check to satisfy a validator; -4. regenerate the complete lock in a dedicated update branch using the reviewed npm version, then review the full diff before relying on it; and -5. if rollback is necessary, restore the prior manifest and complete lock together, then rerun the entire exact-head gate. Do not roll back to a package-manager runtime with a known unfixed extraction vulnerability without an explicit temporary security exception. +2. determine whether manifest intent, npm, project configuration, registry metadata, transitive resolution, or package-manager runtime changed; +3. never accept a partial/hand-edited lock or disable the runtime check; +4. regenerate the complete lock in the canonical dependency branch using npm `10.9.9`, then review the full diff before relying on it; and +5. if rollback is necessary, restore the prior manifest and complete lock together and rerun the entire exact-head gate. + +For an exact-minimum runtime failure, preserve setup-node details, bundled npm identity, first npm invocation, Corepack activation, and exact workflow job log. Do not weaken `devEngines`; repair ordering so the reviewed npm runtime is authoritative before dependency consumption. ## References GitHub. (2026). *node-tar: Decompression/parse DoS via unlimited input* (GHSA-23hp-3jrh-7fpw; CVE-2026-59873) [Security advisory]. https://github.com/advisories/GHSA-23hp-3jrh-7fpw +jsdom contributors. (2026). *jsdom 30.0.1 package manifest* [Source code]. GitHub. https://github.com/jsdom/jsdom/blob/v30.0.1/package.json + +Node.js contributors. (2026). *Node.js v22.22.2 bundled npm package manifest* [Source code]. GitHub. https://github.com/nodejs/node/blob/v22.22.2/deps/npm/package.json + Node.js contributors. (2026). *Corepack* [Software documentation]. GitHub. https://github.com/nodejs/corepack npm, Inc. (2026). *npm 10.9.9* [Software release]. GitHub. https://github.com/npm/cli/releases/tag/v10.9.9 diff --git a/package.json b/package.json index 8c118c48f..174929213 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "packageManager": "npm@10.9.9", "engines": { - "node": ">=22.13 <23" + "node": ">=22.22.2 <23" }, "devEngines": { "packageManager": { diff --git a/services/analysis-engine/tests/test_node_runtime_contract.py b/services/analysis-engine/tests/test_node_runtime_contract.py new file mode 100644 index 000000000..d67ef7819 --- /dev/null +++ b/services/analysis-engine/tests/test_node_runtime_contract.py @@ -0,0 +1,112 @@ +"""Regression tests for the supported Node.js and jsdom compatibility floor.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +EXPECTED_NODE_ENGINE = ">=22.22.2 <23" +EXPECTED_NODE_FLOOR = (22, 22, 2) +EXPECTED_NPM_VERSION = "10.9.9" +EXPECTED_JSDOM_RANGE = "^30.0.1" + + +def _load_json(path: str) -> dict[str, object]: + """Load one repository JSON file for an exact contract assertion.""" + return json.loads((ROOT / path).read_text(encoding="utf-8")) + + +def _supports_band_node(version: tuple[int, int, int]) -> bool: + """Model the deliberately narrow supported Node 22 patch interval.""" + return EXPECTED_NODE_FLOOR <= version < (23, 0, 0) + + +def test_node_engine_floor_matches_jsdom_30_runtime_contract() -> None: + """Root manifest and lock metadata must publish the same Node compatibility floor.""" + package = _load_json("package.json") + package_lock = _load_json("package-lock.json") + + assert package["engines"] == {"node": EXPECTED_NODE_ENGINE} + assert package["packageManager"] == f"npm@{EXPECTED_NPM_VERSION}" + assert package_lock["packages"][""]["engines"] == {"node": EXPECTED_NODE_ENGINE} + + +def test_node_floor_rejects_pre_floor_patch_and_accepts_exact_minimum() -> None: + """Node 22.22.1 is unsupported while the exact 22.22.2 floor is supported.""" + assert not _supports_band_node((22, 22, 1)) + assert _supports_band_node((22, 22, 2)) + assert _supports_band_node((22, 99, 0)) + assert not _supports_band_node((23, 0, 0)) + + +def test_jsdom_30_is_adopted_in_manifest_and_lock() -> None: + """The coordinated compatibility slice must carry jsdom 30 in both package graphs.""" + desktop = _load_json("apps/desktop/package.json") + package_lock = _load_json("package-lock.json") + + assert desktop["devDependencies"]["jsdom"] == EXPECTED_JSDOM_RANGE + assert package_lock["packages"]["apps/desktop"]["devDependencies"]["jsdom"] == EXPECTED_JSDOM_RANGE + assert package_lock["packages"]["apps/desktop/node_modules/jsdom"]["version"] == "30.0.1" + + +def test_minimum_node_lane_runs_complete_suite_with_pinned_npm() -> None: + """Exercise the exact Node floor after activating the reviewed npm runtime.""" + workflow = (ROOT / ".github/workflows/node-minimum-compatibility.yml").read_text( + encoding="utf-8" + ) + + match = re.search( + r"(?ms)^ node-minimum-compatibility:\n(?P.*?)(?=^ [a-zA-Z0-9_-]+:\n|\Z)", + workflow, + ) + assert match is not None, "minimum-version workflow must define node-minimum-compatibility" + body = match.group("body") + + required_fragments = ( + "node-version: 22.22.2", + "package-manager-cache: false", + f'EXPECTED_NPM_VERSION: "{EXPECTED_NPM_VERSION}"', + "corepack enable npm", + 'test "$(npm --version)" = "$EXPECTED_NPM_VERSION"', + "npm run check:npm-runtime", + "npm ci --ignore-scripts --no-audit --no-fund", + "npm run lint", + "npm run typecheck", + "npm run test", + "npm run build", + "npm run build-storybook --workspace @bandscope/desktop", + "cargo +stable check --manifest-path apps/desktop/src-tauri/Cargo.toml --locked", + "cargo +stable test --manifest-path apps/desktop/src-tauri/Cargo.toml --locked", + ) + for fragment in required_fragments: + assert fragment in body, f"minimum-version job is missing: {fragment}" + + for mutable_command in ("npm install ", "npm update ", "npx "): + assert mutable_command not in body, ( + f"minimum-version workflow must not resolve dependencies mutably: {mutable_command.strip()}" + ) + + setup_node = body.split("- uses: actions/setup-node@", maxsplit=1)[1].split( + "- name: Activate pinned npm runtime", maxsplit=1 + )[0] + assert "cache: npm" not in setup_node + assert "package-manager-cache: false" in setup_node + + +def test_repository_no_longer_advertises_node_22_13_floor() -> None: + """Canonical runtime/build documentation must not retain the superseded 22.13 floor.""" + audited_paths = ( + "package.json", + "package-lock.json", + "README.md", + "CONTRIBUTING.md", + "CLAUDE.md", + "docs/engineering/harness-engineering.md", + "docs/security/cross-platform-build-policy.md", + "docs/operations/deploy-runbook.md", + ) + + stale = [path for path in audited_paths if "22.13" in (ROOT / path).read_text(encoding="utf-8")] + assert stale == [] diff --git a/services/analysis-engine/tests/test_npm_toolchain_contract.py b/services/analysis-engine/tests/test_npm_toolchain_contract.py index f50c9adef..a5adebfce 100644 --- a/services/analysis-engine/tests/test_npm_toolchain_contract.py +++ b/services/analysis-engine/tests/test_npm_toolchain_contract.py @@ -120,7 +120,7 @@ def test_root_manifest_pins_the_lockfile_generator_and_fails_on_drift() -> None: manifest = _root_manifest() assert manifest["packageManager"] == f"npm@{_EXPECTED_NPM_VERSION}" - assert manifest["engines"] == {"node": ">=22.13 <23"} + assert manifest["engines"] == {"node": ">=22.22.2 <23"} assert manifest["devEngines"] == { "packageManager": { "name": "npm", @@ -243,9 +243,7 @@ def test_npm_consuming_workflows_activate_pinned_runtime_before_dependency_reads assert len(setup_node_steps) == 1, f"{workflow_name}:{job_name} setup-node ownership" setup_options = setup_node_steps[0].get("with") assert isinstance(setup_options, dict) - assert "cache" not in setup_options, ( - f"{workflow_name}:{job_name} pre-Corepack npm cache" - ) + assert "cache" not in setup_options, f"{workflow_name}:{job_name} pre-Corepack npm cache" assert setup_options.get("package-manager-cache") is False, ( f"{workflow_name}:{job_name} must disable setup-node package-manager cache" )