From 0edc00502469eff77017988a8d81ea9e630faa62 Mon Sep 17 00:00:00 2001 From: Chris Kehayias Date: Fri, 21 Aug 2026 07:23:31 -0400 Subject: [PATCH] fix(deps): restore nested ajv and bundled emnapi entries to the lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI has been red on main since 64f18f0 ("Package Update Cleanup"): `npm ci` failed in ~8s on a lockfile mismatch, before any test ran, on every branch. npm error Invalid: lock file's ajv@6.15.0 does not satisfy ajv@8.20.0 npm error Missing: fast-uri@3.1.5 from lock file npm error Invalid: lock file's json-schema-traverse@0.4.1 does not satisfy json-schema-traverse@1.0.0 Cause `ajv` is an *optional* peer dep of @hookform/resolvers (peerDependenciesMeta.ajv.optional = true), and we do not use the ajv resolver, so it should not be installed at all. The lockfile instead carried a single hoisted top-level ajv@6.15.0 marked `devOptional` — a dedupe of eslint's own ajv@6 — while npm's ideal tree wanted ajv@8.20.0 there to satisfy the optional peer. Lock and ideal tree disagreed, and `npm ci` refuses to guess. Fix Regenerated with: npm install --package-lock-only --os=linux --cpu=x64 Net effect (142 lines, +116/-26): - drops the bogus hoisted top-level ajv@6.15.0 and json-schema-traverse@0.4.1 - gives eslint and @eslint/eslintrc their own nested ajv@6.15.0 subtrees - restores the bundled @tailwindcss/oxide-wasm32-wasi -> @emnapi/* subtree (@emnapi/core, runtime, wasi-threads, @napi-rs/wasm-runtime, @tybys/wasm-util, tslib) that a previous Windows install had pruned That last part also clears the drift tracked in .claude/TODO/investigate-emnapi-lockfile-drift.md, so the manual re-add workaround that TODO describes is no longer needed. package.json is untouched. No dependency versions changed — this is purely a lockfile tree-shape repair. Verification - `npm ci` on Linux (container): exit 0, 587 packages - resulting tree: no top-level ajv; eslint/node_modules/ajv at 6.15.0 - platform coverage unchanged, so the lockfile stays cross-platform: win32 76, darwin 75, linux-x64 46, android 40 — identical before and after. `--os`/`--cpu` steer resolution without narrowing the lockfile, and `--package-lock-only` leaves node_modules alone. The full suite could not run in the container — the only image that would pull is node 20, and jsdom 30's undici needs node >= 22 (`webidl.util.markAsUncloneable is not a function`). CI runs node 22, so the run on this PR is the real test check. TODO housekeeping - removed ci-broken-ajv-lockfile-drift.md — resolved by this commit - updated investigate-emnapi-lockfile-drift.md: marks option 2 confirmed working with the exact flags and the evidence, and narrows the remaining work to the one thing still missing — a CI guard running `npm ci --dry-run` on any lockfile change. Both incidents reached main and were caught a merge later; without that guard there will be a third. Co-Authored-By: Claude Opus 5 (1M context) --- .claude/TODO/ci-broken-ajv-lockfile-drift.md | 81 ---------- .../TODO/investigate-emnapi-lockfile-drift.md | 34 ++++- package-lock.json | 142 ++++++++++++++---- 3 files changed, 148 insertions(+), 109 deletions(-) delete mode 100644 .claude/TODO/ci-broken-ajv-lockfile-drift.md diff --git a/.claude/TODO/ci-broken-ajv-lockfile-drift.md b/.claude/TODO/ci-broken-ajv-lockfile-drift.md deleted file mode 100644 index fbd8737..0000000 --- a/.claude/TODO/ci-broken-ajv-lockfile-drift.md +++ /dev/null @@ -1,81 +0,0 @@ -# TODO: CI is red on `main` — `npm ci` fails on `ajv` lockfile drift - -**Created:** 2026-08-21 -**Severity:** High — **every** CI run on `main` and on every branch fails at the install step. No PR can be verified by CI until this is fixed. -**Status:** Open. Pre-existing; discovered while pushing unit-test coverage (PR #71), unrelated to that work. - -## Symptom - -`npm ci` fails in ~8s on GitHub Actions, before any test runs: - -``` -npm error `npm ci` can only install packages when your package.json and -npm error package-lock.json or npm-shrinkwrap.json are in sync. -npm error Invalid: lock file's ajv@6.15.0 does not satisfy ajv@8.20.0 -npm error Missing: ajv@6.15.0 from lock file -npm error Missing: fast-uri@3.1.5 from lock file -npm error Invalid: lock file's json-schema-traverse@0.4.1 does not satisfy json-schema-traverse@1.0.0 -npm error Missing: json-schema-traverse@0.4.1 from lock file -``` - -## When it started - -Introduced by `64f18f0` ("Package Update Cleanup"). The run immediately before it -(`32470332071`, merge of PR #70) was green; `32472743169` on `64f18f0` is red with this -error, and every run since has failed identically. - -Verified byte-identical between the `main` run and PR #71's run, and PR #71 touches neither -`package.json` nor `package-lock.json` — so this is not branch-specific. - -## Cause - -Two packages want different `ajv` majors: - -| Package | Requires | -|---|---| -| `eslint` | `ajv@^6.14.0` | -| `@hookform/resolvers` | `ajv@^8` | - -`package-lock.json` contains exactly **one** `node_modules/ajv` entry, pinned to `6.15.0` -(line ~5242). The nested `ajv@8.x` entry that `@hookform/resolvers` needs is absent, along -with its `fast-uri@3.1.5` and `json-schema-traverse@1.0.0` subtree. - -This is the same class of failure as `.claude/TODO/investigate-emnapi-lockfile-drift.md`: a -Windows `npm install` / `npm dedupe` pruned nested entries out of the lockfile, and `npm ci` -on Linux then refuses to proceed. `ajv` is a different victim, same mechanism. - -## Fix - -Regenerate the lockfile without touching `node_modules`, then confirm both `ajv` trees survive: - -```bash -npm install --package-lock-only -git diff package-lock.json # expect a nested ajv@8.x under @hookform/resolvers -``` - -**Do this in WSL, a Linux container, or with `--os=linux --cpu=x64`.** A bare -`npm install` on Windows is what caused this, and the emnapi TODO documents it re-pruning -Linux-only optional entries — fixing `ajv` on Windows risks reintroducing that drift in the -same commit. - -Then verify the way CI does, on Linux: - -```bash -rm -rf node_modules && npm ci && npm run test:run -``` - -## Worth doing alongside - -Both incidents share one root cause: lockfiles are generated on Windows and consumed on -Linux. Options in `.claude/TODO/investigate-emnapi-lockfile-drift.md` §"Things to try" apply -verbatim here — in particular a CI guard or pre-commit hook that runs `npm ci --dry-run` -before a lockfile change can reach `main`. That would have caught both incidents at the -commit that introduced them rather than one merge later. - -The `/audit-deps` skill is the natural home for this check. - -## Related - -- `.claude/TODO/investigate-emnapi-lockfile-drift.md` — same mechanism, different packages -- `64f18f0` — the commit that introduced it -- Failing run on `main`: https://github.com/MinistryPlatform-Community/MPNext/actions/runs/32472743169 diff --git a/.claude/TODO/investigate-emnapi-lockfile-drift.md b/.claude/TODO/investigate-emnapi-lockfile-drift.md index 73f4db2..8f3171b 100644 --- a/.claude/TODO/investigate-emnapi-lockfile-drift.md +++ b/.claude/TODO/investigate-emnapi-lockfile-drift.md @@ -2,8 +2,31 @@ **Created:** 2026-05-17 **Severity:** Annoying — CI breaks every time someone bumps a dep on Windows. + **Workaround in place:** Manually re-add the four `@emnapi/*` lockfile entries by hand after every Windows `npm install`. Tracked by commits `dc5e439` and the follow-up patch after `ecaa009`. +> **2026-08-21 update — the regeneration recipe is now verified, the systemic guard is not.** +> +> A second incident (`ajv`, introduced by `64f18f0`) broke `npm ci` on `main` for every branch. +> Fixing it confirmed **option 2 below works and is safe**: +> +> ```bash +> npm install --package-lock-only --os=linux --cpu=x64 +> ``` +> +> Run from Windows, this restored every missing nested/bundled entry — including the +> `@tailwindcss/oxide-wasm32-wasi` → `@emnapi/*` subtree this TODO is about — and pruned +> **nothing**. Platform-specific entry counts were byte-identical before and after +> (76 win32, 75 darwin, 46 linux-x64, 40 android), so `--os`/`--cpu` steer resolution +> without narrowing the lockfile to one platform. `--package-lock-only` never touches +> `node_modules`, so it is safe to run mid-session. +> +> Verified by `npm ci` on Linux (exit 0, 587 packages) before pushing. +> +> **What is still missing is item 4: the guard.** Both incidents reached `main` and were +> found one merge later. Until `npm ci --dry-run` runs on a lockfile change, there will be +> a third incident. That is the remaining work in this TODO — the manual fix is solved. + ## Symptom CI `npm ci` on Ubuntu fails with: @@ -25,9 +48,16 @@ The `@emnapi/core` and `@emnapi/runtime` packages are pulled in as **optional, p ## Things to try 1. **`npm install --include=optional`** on Windows — does this preserve the Linux-only optional graph? If yes, document it as the required install command and add to CLAUDE.md / contributing guide. -2. **`npm install --os=linux --cpu=x64`** — force npm to resolve the lockfile as if it were Linux. This was reportedly used in `e6da5c5` (referenced by `dc5e439`'s commit message) and produced a passing main commit. +2. ~~**`npm install --os=linux --cpu=x64`**~~ — **CONFIRMED WORKING**, see the update at the top. + Use `npm install --package-lock-only --os=linux --cpu=x64`; the `--package-lock-only` part + matters, since it keeps `node_modules` untouched. This is now the documented fix for this + class of drift. 3. **Move all dep-bump work to a Linux container or WSL** so lockfiles are always generated against the CI platform. -4. **Pre-commit hook or CI guard** that detects the four missing `@emnapi/*` lockfile entries and fails fast (or auto-restores them) before they reach `main`. Cheap insurance even if we pick option 1 or 2. +4. **Pre-commit hook or CI guard** — **this is the remaining work.** Rather than detecting the + four `@emnapi/*` entries specifically (the `ajv` incident had nothing to do with emnapi), run + the generic check: on any commit touching `package-lock.json`, run `npm ci --dry-run` on Linux + and fail fast. That catches every variant of this drift at the commit that introduces it + instead of one merge later. The `/audit-deps` skill is the natural home. 5. **Investigate whether `@tailwindcss/oxide-wasm32-wasi` and `@rolldown/binding-wasm32-wasi` are actually needed** — if neither is being used at build/runtime, removing them eliminates the source of the optional/peer entanglement. ## How to verify a fix diff --git a/package-lock.json b/package-lock.json index 67235dc..403a96c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1236,6 +1236,30 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/js": { "version": "9.39.5", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", @@ -4145,6 +4169,72 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", @@ -5239,23 +5329,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -6946,6 +7019,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -6979,6 +7069,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -7104,7 +7201,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -8304,13 +8401,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "devOptional": true, - "license": "MIT" - }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -10820,7 +10910,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0"