Skip to content

chore(wasm): merge prep β€” main fixes, pinact, per-image error tolerance, test parity#604

Merged
bokuweb merged 64 commits intowasmfrom
chore/wasm-merge-prep
May 3, 2026
Merged

chore(wasm): merge prep β€” main fixes, pinact, per-image error tolerance, test parity#604
bokuweb merged 64 commits intowasmfrom
chore/wasm-merge-prep

Conversation

@bokuweb
Copy link
Copy Markdown
Member

@bokuweb bokuweb commented May 3, 2026

Summary

Brings the wasm branch closer to mergeable state.

  • Merge main into wasm β€” pulls minimatch ReDoS overrides, the -U restore, version bump to 0.18.16, and other security bumps. Conflict resolution kept wasm's structure (root JS code intact, js/ + crates/ additions preserved); main's pnpm overrides + onlyBuiltDependencies merged.
  • Pin GitHub Actions with pinact run β€” every action is now hash-pinned with version comments.
  • Per-image error tolerance in crates/reg_core/src/lib.rs: a corrupt PNG or unreadable file no longer aborts the whole batch with Err(CompareError). Each failure is logged to stderr, fired as a compare-event{type:"fail"} for live progress, and folded into failedItems β€” matching classic reg-cli's tolerance (it forks per image, so individual decode failures never sank the run).
  • Rebuilt js/reg.wasm so CI exercises the new behaviour (Cargo.lock pinned to image 0.25.5 / icu 2.0.0 to stay compatible with the project's nightly-2025-01-01 toolchain).
  • JS test parity with main's test/cli.test.mjs β€” adds boundary tests for -T/-S, identical-dirs, all-deleted, plus corrupt-PNG and non-image-file edge cases.
  • reg-suit drop-in integration test (library.test.mjs) pins every option/event/CompareOutput field that reg-suit/processor.ts consumes β€” if wasm reg-cli stops being a drop-in for reg-suit, this test fires first.

Test plan

  • cargo +stable test -p reg_core --lib β€” 12 passed (3 new in per_image_failure_tests)
  • cd js && pnpm test β€” 38 passed (8 new across cli + library)
  • sh ./scripts/build-wasm.sh succeeds; js/reg.wasm regenerated
  • CI green (test + wasm-test jobs)

πŸ€– Generated with Claude Code

wadackel and others added 30 commits August 18, 2024 20:26
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* 0.18.8

* update
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* docs: update readme

* chore: 0.18.9 published
…ty] (#528)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](moxystudio/node-cross-spawn@v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-version: 7.0.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.2 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](micromatch/micromatch@4.0.2...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-version: 4.0.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* fix: update

* fix: greenkeep

* fix: use latest
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* fix: ws version

* fix: lock
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
renovate Bot and others added 26 commits April 18, 2026 21:07
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.7 to 7.5.13.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](isaacs/node-tar@v7.5.7...v7.5.13)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
…nerability

chore(deps): update dependency brace-expansion to v5 [security]
chore(deps): update dependency puppeteer to v24.42.0
chore(deps): update dependency @babel/preset-env to v7.29.3
chore(deps): update pnpm to v10.33.2
chore(deps): update dependency tar-fs to v3.1.2
chore(deps): update dependency ava to v8
* test: remove mistakenly committed .only from cli test

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: fix __dirname not defined in ESM perf test

Use process.cwd() instead of __dirname so the test file works regardless
of CommonJS/ESM module loading. Other tests in this file already rely on
the default cwd.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: use deleteAsync from del v8 (default export removed)

del v8 dropped the default export and now exposes deleteAsync/deleteSync
as named exports. The babel-compiled CJS was calling _del.default(...),
which is undefined, crashing the CLI whenever -U triggered the cleanup
path. This was masked by a stray test.serial.only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses three minimatch ReDoS advisories:
- GHSA-23c5-xmqv-rm74 (CVE-2026-27904) β€” nested *() extglobs
- GHSA-7r86-cg39-jmmj (CVE-2026-27903) β€” matchOne() backtracking
- GHSA-3ppc-4f35-3m26 (CVE-2026-26996) β€” repeated wildcards

Replace the duplicate/partial minimatch overrides with per-major-line
selectors that pin to the first patched release in each line. After
this, the lockfile resolves minimatch to 3.1.5 and 10.2.5 (both fully
patched) instead of the vulnerable 9.0.5.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…version bump

- Adopts main's pnpm.overrides (minimatch security, tar 7.x, etc.)
- Keeps wasm-test job alongside the legacy puppeteer test job
- Pulls main's src/index.js -U restore (legacy JS still ships from root)
- Top-level version 0.18.6 β†’ 0.18.16
- Keeps wasm-side onlyBuiltDependencies (cli-spinner + puppeteer)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wrap each rayon-thread image into a `ImageOutcome::{Ok,Failed}` rather
than propagating a single Err out of the closure. On read or decode
failure: log to stderr, fire `compare-event{type:"fail"}` for live
progress, and fold into `failedItems` so callers (CLI, EventEmitter
consumers) see a normal end-of-run result.

Matches classic reg-cli's tolerance β€” one corrupt PNG must not abort a
batch of 1000 images.

Adds tempfile dev-dep + 3 unit tests:
- corrupt PNG β†’ failedItems, neighbour passes
- non-image extensions stay silently filtered

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cli.test.mjs (+6):
- -T 0.00 / -T 1.00 thresholdRate boundaries
- -S 0 / -S 10000000 thresholdPixel boundaries
- identical dirs β†’ passedItems all populated
- actual empty β†’ all baselines surface as deletedItems
- corrupt PNG β†’ failedItems entry, sibling still passes, exit 1
- non-image files (.md, .json) silently skipped

library.test.mjs (+2):
- reg-suit drop-in: every event + CompareOutput field processor.ts
  consumes (start/compare/complete/error + failed/new/deleted/passed)
- compare() does NOT fire `error` on single-image decode failure;
  bad.png β†’ failedItems + 'fail' compare-event, run completes

js/reg.wasm rebuilt against the updated reg_core (per-image error
tolerance) so CI exercises the new behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two CI failures on PR #604:
- `test` job: pnpm-lock.yaml didn't list @pyroscope/nodejs (we adopted
  main's lock during the merge), so --frozen-lockfile rejected the
  package.json that still had it. @pyroscope is only used by the wasm
  wrapper (`js/`); it doesn't belong in the root JS reg-cli's deps.
- `wasm-test` job: pnpm/action-setup with `version: 10.26.2` conflicted
  with root package.json's `packageManager: pnpm@10.33.2+...`. Drop the
  explicit version and let action-setup read packageManager.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokuweb bokuweb merged commit 56cc0ee into wasm May 3, 2026
4 checks passed
@bokuweb bokuweb deleted the chore/wasm-merge-prep branch May 3, 2026 04:32
bokuweb added a commit that referenced this pull request May 3, 2026
* init

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* 0.0.0-experimental0

* perf: improve perf

* update

* fix: image diff

* fix: wasm

* fix: remove console.log

* fix

* fix

* fix

* fix

* fix (#539)

* fix

* fix: diff ext (#540)

* updatev wazm

* feat: otel

* Fix 2s sleep in tracing shutdown; add JS-bridge spans (#579)

Two related changes surfaced by a trace comparison between the classic JS
reg-cli and this Wasm build.

1. Remove the `setTimeout(resolve, 2000)` after `sdk.shutdown()` in
   `js/tracing.ts`. `sdk.shutdown()` already awaits exporter flush; the extra
   sleep was a paranoia workaround that dominated wall-clock when OTEL is on:

     20 images Γ— 1280Γ—720, OTEL on:
       before: Wasm 2.53s   (JS 0.88s)
       after:  Wasm 0.49s   (JS 0.81s)   ← Wasm is now faster

2. Add JS-bridge spans so the invisible time between `main()` and Rust
   `reg_cli_main` is visible. New spans (worker -> parent via postMessage,
   parent converts to OTel):

     main.init_tracing
     main.new_entry_worker
     main.new_thread_worker (Γ— N)
     main.process_trace_and_spans
     main.run_total
     entry.wasm_compile
     entry.wasm_instantiate
     entry.wasi_start
     entry.init_tracing_rust
     entry.wasm_main
     entry.read_report_string
     entry.collect_rust_traces
     entry.worker_total
     worker.wasm_compile
     worker.wasm_instantiate
     worker.wasi_start
     worker.wasi_thread_start
     worker.thread_total

   Before this, the ~180ms between Node startup and Rust `reg_cli_main`
   looked like a mystery gap in Jaeger. Now each stage is attributable.

3. Fix bare `postMessage(...)` in `js/worker.ts`. `postMessage` is not a
   global inside Node's `worker_threads`; use `parentPort?.postMessage(...)`
   to match `js/entry.ts`. This was silently not working or emitting an
   async error that aborted thread-spawn under some conditions.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add bench/ harness; bump image-diff-rs to 0.1.0 (#580)

* Add bench/ harness; bump image-diff-rs to 0.1.0

## Changes

- **Cargo.toml (workspace)**: add `[profile.release]` (lto / codegen-units=1 /
  opt-level=3 / panic=abort / strip) for smaller + faster wasm binary
- **crates/reg_core/Cargo.toml**: pin `image-diff-rs` to tag `0.1.0`
  (https://github.com/bokuweb/image-diff-rs/releases/tag/0.1.0 β€” adds
  internal tracing spans for decode / compare / encode)
- **bench/**: JS vs Wasm trace-comparison benchmark harness
  - `generate.sh` β€” ImageMagick-based fixture generator (20 pairs Γ— 1280Γ—720,
    5 mutation types: shift / hue / badge / rect / stripe)
  - `run.sh` β€” run both `dist/cli.js` and `js/dist/cli.mjs` against the same
    fixture, capturing OTLP traces
  - `export-traces.sh` β€” pull latest JS and Wasm traces from Jaeger and
    write `out/traces.json`
  - `viz/index.html` β€” single-trace waterfall viewer (per-service)
  - `viz/compare.html` β€” **aligned-timescale stacked view** (JS on top,
    Wasm on bottom, shared x-axis)
  - `README.md` β€” mutation matrix and usage
  - `RESULTS.md` β€” measurement results with honest caveats (PNG vs WebP
    encode, reg.json file write, etc.)

## Measured (macOS Apple Silicon / Node v20 / warm median)

| workload | JS | Wasm | ratio |
|---|---:|---:|---:|
| 20 Γ— 1280Γ—720 | 0.59 s | 0.42 s | 1.40Γ— |
| 1 Γ— 1280Γ—720 | 0.28 s | 0.24 s | 1.18Γ— |
| 1 Γ— 3840Γ—2160 | 1.29 s | 0.69 s | 1.87Γ— |
| identical Γ— 20 | 0.30 s | 0.29 s | ~equal |

Caveats included in `bench/RESULTS.md`:
- JS writes diff images as PNG; Wasm writes as WebP (WebP encode is cheaper)
- JS writes `reg.json` to file; Wasm returns it as a string
- Concurrency default 4 for both; md5 / byte short-circuit for identical both

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix pathdiff panic; add --diffFormat flag; update RESULTS

Three fixes that fell out of "why is the JS vs Wasm benchmark not
apples-to-apples":

## 1. `pathdiff::diff_paths` no longer panics on mixed paths

`resolve_dir(base, target)` previously called `.expect("should resolve
relative path.")` which panicked whenever `base` and `target` were a mix
of absolute and relative paths (e.g. absolute `--report` + relative
fixture directory). Now:

- Both-same-kind β†’ pathdiff as before
- Mixed β†’ canonicalize both to absolute via `current_dir()` and retry
- Still no match β†’ return `target` unchanged instead of panicking

Added 4 unit tests covering both-relative, both-absolute, and both
mixed directions.

## 2. `--diffFormat {webp,png}` CLI flag

Depends on image-diff-rs PR #34 (bokuweb/image-diff-rs#34)
which adds `EncodeFormat`. Plumbed through:

- `reg_core::Options` gets `diff_image_format: Option<DiffImageFormat>`
- `DiffImageFormat::{Webp, Png}` as a user-facing mirror
- File extension of written diff images + HTML report follows the choice
- `reg_cli`'s `main.rs` exposes `--diffFormat {webp,png}` via clap

Pointed `reg_core`'s `image-diff-rs` dep at the PR branch temporarily;
will move back to a tag once image-diff-rs #34 is merged and released.

## 3. bench/RESULTS.md β€” corrected numbers

Replaced the earlier WebP-vs-PNG confounded numbers with apples-to-apples
PNG-vs-PNG measurement:

| workload | JS (PNG) | Wasm `--diffFormat png` | ratio |
|---|---:|---:|---:|
| 20 Γ— 1280Γ—720 | 0.71 s | 0.46 s | **1.54Γ— faster** |
| 1 Γ— 3840Γ—2160 (4K) | 0.92 s | 0.42 s | **2.19Γ— faster** |
| identical Γ— 20 (skip) | 0.30 s | 0.29 s | ~equal |

Also documents all the setup caveats surfaced during the investigation
(WASI preopen sandbox, pathdiff panic, postMessage form, 2s sleep, etc.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Extend benchmark to 100 images β€” gap widens to 1.98Γ— faster

Added 100 Γ— 1280Γ—720 row to the apples-to-apples and default tables.
At 100 images the Wasm advantage grows from 1.54Γ— β†’ 1.98Γ— compared to
JS PNG (and 1.31Γ— β†’ 1.54Γ— in default WebP mode).

Consistent with the hypothesis that startup cost amortizes and per-image
compute stays flat β€” the more work to do, the more the Rust core wins.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Bump image-diff-rs to 0.1.1 (EncodeFormat released)

image-diff-rs#34 merged and tagged as 0.1.1, so we can drop the branch
pin and use a proper tag.

- Before: branch = "add-png-encode-option"
- After:  tag = "0.1.1"

No code changes; still smoke-tested with `--diffFormat png`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(otel): drop stale packageManager field that breaks yarn install

The root package.json carried a `packageManager: "pnpm@10.26.2+..."`
field (introduced via the wasm branch's tooling migration), but CI still
installs the outer project with yarn and the repository still ships a
`yarn.lock`. Corepack/yarn 1.x refuses to install when the declared
packageManager doesn't match the invoked tool:

  error This project's package.json defines "packageManager":
  "yarn@pnpm@10.26.2". However the current global version of Yarn is
  1.22.22.

The simplest fix that restores CI without churn is to drop the field.
The inner `js/` workspace (Wasm bridge) uses its own pnpm + lockfile
and is unaffected by removing the root-level declaration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(otel): migrate root project from yarn to pnpm

Matches the `packageManager: pnpm@10.26.2` declared in package.json.

- `.github/workflows/ci.yml`: use `pnpm/action-setup@v4` + `pnpm install
  --frozen-lockfile`, bump `actions/checkout`/`setup-node` to v4, Node 20
- `package.json`: replace `resolutions` (yarn glob syntax) with
  `pnpm.overrides` (flat, pnpm native). Same 21 pinned versions, no
  behaviour change.
- `pnpm-lock.yaml`: added (generated by `pnpm install`)
- `yarn.lock`: removed (no longer authoritative)
- `decls/deps.js`: add flow declarations for third-party modules that
  .flowconfig's `[ignore] node_modules` hides from flow's resolver. The
  nine "Cannot resolve module" errors that surface after a clean install
  are all third-party imports (`cli-spinner`, `meow`, `lodash`, `chalk`,
  etc.); this restores `pnpm run flow` to pass.

The inner `js/` workspace (Wasm bridge) already uses its own pnpm
install and is unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(otel): restore packageManager field so pnpm/action-setup works

An earlier commit removed the `packageManager` field as a workaround
for yarn, but pnpm/action-setup@v4 reads exactly this field to pick
the pnpm version. Restore it now that CI is on pnpm.

Fixes: "Error: No pnpm version is specified."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(otel): pin pnpm version in action-setup, drop outer packageManager

- Specify `version: 10.26.2` directly on pnpm/action-setup@v4 instead of
  relying on the outer `packageManager` field. The outer field was
  conflicting with the inner `yarn install` inside `report/ui/` because
  yarn 1.22 walks up the directory tree when checking the corepack
  packageManager constraint.
- Remove `packageManager` from the root package.json so yarn inside the
  cloned `report/ui` (which has its own yarn.lock and needs Yarn 1.22)
  doesn't refuse to install.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(otel): allow puppeteer postinstall so test:screenshot finds Chrome

pnpm 10 blocks postinstall scripts by default (via `onlyBuiltDependencies`).
puppeteer 13.7 downloads Chromium in its postinstall; without it
`test/screenshot.js` fails with:

  Error: Could not find expected browser (chrome) locally. Run
  `npm install` to download the correct Chromium revision (982053).

Add `puppeteer` to `pnpm.onlyBuiltDependencies` so its install.js runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* viz: collapsible parent spans in compare.html + index.html

Click the label row of a span that has children to toggle the sub-tree.
Rows with children show β–Ύ (expanded) or β–Έ (collapsed); leaves show no
arrow. New "collapse all" / "expand all" buttons in the toolbar operate
on the union of parent-span IDs across both traces so the same state
applies to every lane.

Useful for the 100-image waterfall where `parallel_image_diff` or
`diff_single_image` otherwise push all the interesting one-liners
(boot / cleanup / threadpool) off-screen.

Implementation notes:
- Shared `collapsed: Set<spanId>` state at the module level
- `flatten()` (compare.html) / `walk()` (index.html) skip descendants of
  collapsed ids, and tag each row with `hasChildren`
- Invisible full-width hit rect per collapsible row so the entire label
  area is clickable, not just the arrow glyph
- `pointer-events: none` on the text/badge inside so the underlying hit
  rect catches the click reliably

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Narrow the WASI sandbox: single-ancestor preopen + env allowlist (#581)

Before this, entry.ts / worker.ts set `preopens: { './': './' }` and
`env: env as Record<string, string>`. That means any Wasm code that
takes control of the pipeline (e.g. via an image-decoder CVE β€” libpng,
libwebp, libjpeg have a long history of RCE-class ones) could read
anything under the invocation cwd (`.npmrc`, `.env`, `node_modules`,
...) and see every host env var (`NPM_TOKEN`, `AWS_*`, ...).

This change introduces `computeWasiSandbox(argv)` in `js/utils.ts` and
plugs it into both `entry.ts` and `worker.ts`:

- **preopen** β€” the narrowest single ancestor directory that contains
  every path the run actually touches: `actualDir`, `expectedDir`,
  `diffDir`, and the parents of `--report` / `--json`. In a typical
  invocation inside a repo the sandbox shrinks from "all of CWD" to
  "just the reg-cli fixture folder", i.e. the Wasm side no longer sees
  sibling files like `.env` or `~/.config/...`
- **env** β€” allowlist only OTel-related variables
  (`OTEL_ENABLED`, `OTEL_DEBUG`, `OTEL_EXPORTER_OTLP_ENDPOINT`,
  `OTEL_SERVICE_NAME`, …). Everything else (AWS_*, NPM_TOKEN, CI
  secrets) stops at the Wasm boundary

## Why only a single ancestor preopen?

Originally this PR tried to register one preopen per directory
(actual / expected / diff / report-parent / json-parent). Writes to
the first preopen worked; the other 4 silently became invisible to
Rust. Instrumenting `@tybys/wasm-util` showed that WASI-side all 3-5
preopens were correctly installed (fd 3, 4, 5, ...) but the Wasm side
only ever issued `fd_prestat_get(3)` and never queried fd 4+, which
smells like an enumeration bug in `wasm32-wasip1-threads` libstd.

Falling back to the common ancestor avoids the bug while still giving
a real defense-in-depth win. When the upstream issue is fixed we can
swap back to per-directory preopens.

## Not in scope (explicit follow-ups)

- Symlinks / FIFOs inside the preopened directory are still resolved
  by the host. An attacker who can plant files under `./diff/` could
  still exfiltrate via an evil symlink.
- `fs: fs as IFs` still hands `@tybys/wasm-util`'s WASI unrestricted
  Node `fs`. We constrain *the paths Wasm is allowed to name*, not
  what the host fs layer underneath would do on Wasm's behalf.
- stdout / stderr inheritance. If the host redirects them to a socket
  (`reg-cli > >(nc ...)`) Wasm output leaves the box through the
  host's pipe, not through a WASI syscall.

## Also bumps `rust-toolchain.toml` nightly-2024-08-24 β†’ 2025-01-01

Build fix: dependencies now require `edition2024` which the old nightly
cannot stabilise. No behavioural change.

## Test plan

- [x] 20-image fixture: 16 diff images produced, report.html written
- [x] 100-image fixture: same (timings within noise of pre-change)
- [x] `SECRET_TOKEN=x NPM_TOKEN=y AWS_ACCESS_KEY_ID=z reg-cli ...` β€”
      none of those are forwarded into Wasm (confirmed via logged
      `sandbox.env` during bring-up)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* CLI compat with classic reg-cli: exit codes, short aliases, -I/-E/-U (#583)

Makes `@bokuweb/reg-cli-wasm`'s CLI a drop-in replacement for
`reg-cli` for the most common CI use-cases. Closes the first tier of
compat gaps between the JS classic CLI and the Wasm build; `--from`,
`--junit`, `-D`, `-X` and the per-file library events remain for a
follow-up PR.

## Before

- `node js/dist/cli.mjs ... ` exited 0 regardless of diffs β†’ CI
  didn't detect failures
- Only long-form flags (`--json`, `--report`, ...) worked; `-J`, `-R`,
  `-M`, `-T`, `-S`, `-C`, `-A` all failed with "unexpected argument"
- No progress / summary lines on stdout
- `-U`, `-I`, `-E` were not recognised at all
- Worker `error` events were re-thrown outside the EventEmitter, so
  library users couldn't `.on('error', ...)` them

Swapping classic reg-cli β†’ the Wasm build was therefore a script
change, not a drop-in replacement.

## After

### Rust / clap (`crates/reg_cli/src/main.rs`)

- Add POSIX short aliases to every existing option:
    `-R/--report`, `-J/--json`, `-M/--matchingThreshold`,
    `-T/--thresholdRate`, `-S/--thresholdPixel`, `-P/--urlPrefix`,
    `-C/--concurrency`, `-A/--enableAntialias`
- `-A` now also accepts a bare form (`num_args = 0..=1` +
  `default_missing_value = "true"`) so `-A` alone means `-A=true`,
  matching meow's boolean semantics.
- `--diffFormat` unchanged (long-only, matching no-short policy for
  wasm-only flag).

### CLI wrapper (`js/cli.ts`)

Rewritten as a thin front-end around `run()` that handles everything
the Wasm side doesn't (yet) know about:

- Proper arg parsing with Node's built-in `util.parseArgs` β€” no extra
  dep; short aliases listed there too as a safety net.
- `--help` / `-h` short-circuit prints the help text (clap help is
  reachable via the Wasm too but is cosmetically different).
- `-U / --update` β€” after complete, copy `actualItems` over
  `expectedDir` with `mkdir(recursive)` + `copyFile`.
- `-I / --ignoreChange` β€” suppresses non-zero exit.
- `-E / --extendedErrors` β€” treats new/deleted counts as failures.
- `-D / --customDiffMessage` β€” trailing message printed on diff,
  defaulting to classic's "Inspect your code changes, re-run with
  `-U` to update them.".
- Per-file `βœ” pass / ✘ change / ✚ append / βˆ’ delete` log lines +
  summary.
- `process.exitCode = 1` on failure (honouring `-I`).

### Library (`js/index.ts`)

The two `worker.on('error', e => { throw new Error(e.message) })` hooks
are now `emitter.emit('error', e)`, matching classic reg-cli's
EventEmitter surface. Library users can now:

    const emitter = compare({ ... });
    emitter.on('error', (err) => { /* handle */ });
    emitter.on('complete', (data) => { /* ... */ });

instead of needing a global `uncaughtException` handler.

### Also: `rust-toolchain.toml` nightly-2024-08-24 β†’ 2025-01-01

Transitive dep now requires `edition2024` which the old nightly
cannot stabilise. No behavioural change.

## Test plan

- [x] 20 images: all short aliases (`-R -J -M -C`) parsed, 16 PNG
      diffs produced, exit 1.
- [x] `-I` flag: exit 0 even with diffs.
- [x] `-E` with no diffs: exit 0 (no escalation triggered).
- [x] `-E` with new images: exit 1.
- [x] `-U`: `expected/` is updated to match `actual/`, exit 0.
- [x] `--help`: prints usage and exits 0.
- [x] Missing positional args: clear error + exit 1.

## Not in this PR (follow-up)

- `-F / --from` (JSON β†’ report without re-running diff)
- `--junit` output
- `-X / --additionalDetection`
- Library `start` / `compare` (per-file) / `update` events
- `reg.json` / `junit.xml` written to disk by default
- Default `--diffFormat png` (to fully match classic's output
  filenames)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* CLI compat (phase B): library events + --junit + update via compare()

Follow-up to #583 (phase A). Library surface now matches classic reg-cli.

- run() / compare() emit 'start', 'compare' (per-file synthesised),
  'update', 'complete', 'error'
- compare(input) handles update / junitReport as side effects after
  the Wasm run, instead of forwarding them to clap (which rejected them)
- Translates threshold -> thresholdRate alias
- New js/junit.ts writes minimal JUnit XML matching classic's schema
- CLI --junit <path> flag wires the writer on complete

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* CLI compat (phase C): default PNG output + always persist reg.json

Follow-up to #583 and #584. Closes the last first-class compat gap
with classic reg-cli: the JSON/HTML report now references PNG diff
images by default and `reg.json` is always written to disk.

## What changes for users

### Default `diffFormat` = `png`

Before: Wasm defaulted to WebP, so `failedItems` / `diffItems` in the
JSON contained `*.webp` entries and the HTML report loaded WebP
images. That's a real behaviour break for downstream consumers of
reg-cli's JSON (reg-suit, reg-notify-*, custom bots).

After: both `cli.ts` and `compare()` default `--diffFormat` to `png`
when the caller hasn't specified it. Users who want WebP can still
opt in with `--diffFormat webp` or `{ diffFormat: 'webp' }`.

### `reg.json` is always persisted

Before: the Wasm side returned the JSON report as a string through the
Worker `complete` message, but nobody wrote it. CIs that read
`./reg.json` after running reg-cli saw nothing.

After: both `cli.ts` and `compare()` write the report to the `--json`
path (defaulting to `./reg.json`) immediately before emitting the
external `complete` event. If the write fails, the library emits
`'error'` instead of `'complete'`.

## Implementation

- `js/cli.ts`: reads `--json` / `-J` (default `./reg.json`), reads
  `--diffFormat` (default `png`). Pushes the effective values to the
  Wasm argv and writes `reg.json` itself after complete.
- `js/index.ts`: same defaults applied inside `compare()` before
  building the Wasm argv; `writeRegJson()` extracted and exported so
  `cli.ts` reuses the exact same persistence code path.
- The previous fast-path in `compare()` that returned the inner
  emitter unchanged when no side effects were needed is gone, since
  writing `reg.json` is now an unconditional side effect. Cheap β€”
  one `writeFile` of ~KB JSON.

## Test plan

- [x] CLI: diff images in diff dir are `*.png`, `reg.json` exists,
      `reg.json` contains `*.png` entries in `diffItems`.
- [x] CLI explicit `--diffFormat webp`: diff images are `*.webp`, JSON
      references `*.webp`.
- [x] Library `compare({ ... })` with no `json`: `./reg.json` written,
      diffItems end in `.png`.
- [x] Library `compare({ json: '/path/out.json' })`: written to
      /path/out.json, parent dir created if missing.
- [x] Writing to an unwritable path fires `error`, not `complete`.

## Still not in scope

- `-F / --from` (JSON β†’ HTML without running diff)
- `-X / --additionalDetection`

Both have limited blast radius (few users rely on them) and can land
in a follow-up without blocking migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* CLI compat (phase D): -F/--from, -X/--additionalDetection, Rust-side junit + reg.json writes

Closes the remaining CLI gaps with classic reg-cli:

- `-F, --from <reg.json>`: new `reg_core::run_from_json()` re-renders the HTML
  report from an existing reg.json without running any image comparison, and
  the CLI makes positional dirs optional in this mode.
- `-X, --additionalDetection <none|client>`: plumbs
  `Options.enable_client_additional_detection` into `ReportInput`, flipping
  the HTML report's `ximgdiffConfig.enabled`. The report template's
  worker_url is finally wired ("./worker.js" instead of the leftover
  "TODO:").
- `--junit <path>`: moved JUnit XML generation into `reg_core`
  (`build_junit_xml`) and the `run()` / `run_from_json()` entry points now
  write it themselves. `js/junit.ts` is deleted; the JS side no longer
  writes reg.json or junit.xml (both artefacts land inside the WASI
  preopened root instead of via host-side fs).

JS side:
- `computeWasiSandbox` now also covers `--junit` and `--from` parents when
  deciding the common-ancestor preopen root.
- `cli.ts`: new `-F/--from`, `-X/--additionalDetection`, `--junit` flags
  forwarded to Wasm; drops the duplicate reg.json / junit writes.
- `compare()` in `index.ts`: remaps `junitReport` β†’ `--junit`, translates
  the historical `enableClientAdditionalDetection: true` to
  `additionalDetection: "client"`, drops the JS json/junit writes, and now
  only keeps the `update` shim (file-copy still needs host fs).

Smoke-tested end to end against `js/sample/` with both diff + `-F`
regeneration; reg.json / report.html / junit.xml now all come from Rust.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* CLI compat (phase E): byte-for-byte junit + Rust unit tests + JS integration tests

## Fix: junit XML is now byte-identical to classic reg-cli

Phase D (#586) shipped a simplified junit shape (`<testsuites>` bare,
`classname="reg-cli"`, `message="changed|new|deleted"`) that diverged
from what classic reg-cli produces via xmlbuilder2 β€” which is exactly
what downstream CI parsers and `test/cli.test.mjs` snapshot-check.

Fixed to the classic schema:

- `<?xml version="1.0"?>` (no `encoding` attr)
- `<testsuites name="reg-cli tests" tests=N failures=M>` (attrs on BOTH
  testsuites and testsuite)
- `<testcase name="...">` (no `classname`)
- `<failure message="failed"/>` for failedItems, `"newItem"`/`"deletedItem"`
  for new/deleted ONLY when `-E/--extendedErrors` is set
- Without `-E`, new/deleted items are counted as passed testcases (classic
  behaviour)

To make that last branch possible, `extended_errors` is now plumbed through
`Options` β†’ `reg_cli` clap (`-E/--extendedErrors`) β†’ `compare()` library
forwarding β†’ `build_junit_xml`. The JS CLI wrapper still owns exit-code
semantics for `-E`, but the flag is now dual-forwarded so Rust can also see
it.

## New tests

Rust (`cargo test -p reg_core --lib`, 6 new tests):
- single failure
- passed + failed mix
- new/deleted with and without extended_errors (both branches)
- XML attribute escaping (&, <, >, ")
- empty report β†’ self-closing `<testsuite/>`

JS (`node --test`, 19 tests across two files, no new deps):
- `js/test/cli.test.mjs` β€” spawns `node dist/cli.mjs`, asserts exit codes
  (`-I`, `-E`), reg.json schema, JUnit XML **byte-for-byte match** to
  classic, `-R`, `-X client`, `-F` (regenerate from reg.json, verifies
  source reg.json is immutable and no diff dir is recreated), stdout
  formatting, `-D` custom trailer.
- `js/test/library.test.mjs` β€” `compare()` EventEmitter lifecycle
  (`start`β†’`compare(xN)`β†’`complete`), JUnit + reg.json via Rust, `update: true`
  file copy + `update` event, `additionalDetection: 'client'` and the
  legacy `enableClientAdditionalDetection: true` alias.

Notable compatibility finding documented in the library test header:
`wasm32-wasip1-threads` libstd's prestat enumeration only honours the
first path segment of a preopen name. `computeWasiSandbox` collapses
every touched dir to a single common ancestor, so when all positional
dirs live under one scratch (update mode), that scratch must itself be
one segment deep at the repo root. The library test scaffolding uses
`.libtest-<pid>-<n>-<rand>` for exactly that reason.

## CI

New `wasm-test` job runs `cargo test -p reg_core --lib`, then builds the
js dist (using the committed `js/reg.wasm`, no wasi-sdk needed) and runs
`pnpm --filter ./js test`. The classic `test` job is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(wasm-test): build report-ui before cargo test

`reg_core` embeds the report UI bundle via include_str!:

    let js = include_str!("../../../report/ui/dist/report.js");
    let css = include_str!("../../../report/ui/dist/style.css");

So `cargo test -p reg_core --lib` fails at macro expansion unless
`report/ui/dist/` has been populated. Run `scripts/build-ui.sh v0.3.0`
(same version the root `build:report` npm script uses) before the cargo
test step, and enable corepack so the yarn that script invokes is
available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* CLI compat (phase F): -X client assets, -U semantics, favicon, --version, small-set concurrency

Closes the remaining classic-reg-cli gaps from the phase-E audit. One
notable item (N9, live per-file `compare` events during the diff loop)
is deferred as a follow-up β€” it requires plumbing a stderr-parsing event
channel between Rust/Wasm and the worker and is intrusive enough to
deserve its own change.

## B1 β€” `-X client` now emits the browser-side assets

`ximgdiffConfig.enabled=true` alone is a no-op without a worker script
and wasm binary next to the HTML report. Matching classic reg-cli
(src/report.js:158-163):

- New `js/ximgdiff.ts` concatenates `worker_pre.js` (mustache-rendered
  with the wasm URL) + the report-ui `worker.js` + the x-img-diff-js
  loader and writes `<report-dir>/worker.js` + `detector.wasm` (the
  x-img-diff-js wasm binary, renamed).
- `js/cli.ts` and the library's `compare()` both invoke it when
  `-X client` is paired with `-R`.
- `x-img-diff-js` is now a runtime dep of `@bokuweb/reg-cli-wasm`.
- unbuild hook in `build.config.ts` stages `worker_pre.js` and the
  report-ui worker into `dist/shared/` at build time; `writeXimgdiffAssets`
  takes an explicit `distDir` from the entry module's `dir()` so chunk
  splitting doesn't break asset lookup.
- `compare()` lazy-loads ximgdiff (dynamic import) to keep the cold path
  for users who never pass `-X client`.

## B2 β€” `-U/--update` now matches classic's prune-then-selective-copy

Classic (`src/index.js:134-146`):
  1. rm from expected: deletedItems βˆͺ failedItems (so stale baselines
     don't linger and overwrites are clean).
  2. copy actualβ†’expected: newItems βˆͺ failedItems.
  3. passedItems untouched (preserves mtime, keeps git status clean).

Previously we copied every `actualItems` entry, which (a) never pruned
deleted baselines (they accumulated in expected/ forever) and
(b) needlessly rewrote unchanged files, breaking reg-suit workflows that
rely on a clean tree. Implemented in both `js/cli.ts` and `js/index.ts`.

## N2 β€” Force concurrency=1 when < 20 images

Classic (`src/index.js:77`) short-circuits the per-image parallelism for
small sets because the ProcessAdaptor spin-up dominates. In our Rust
implementation the equivalent is the rayon thread-pool + cross-thread
span propagation. Apply the same < 20 heuristic in
`crates/reg_core/src/lib.rs`.

## N3 β€” Favicon data URL in HTML report

Classic embeds the PNG as a data URL so the generated HTML is
self-contained. `reg_core` now `include_bytes!`es
`report/assets/favicon_{success,failure}.png`, base64-encodes at render
time, and fills the existing `{{&faviconData}}` template slot. Switches
on the report status (Danger β†’ failure favicon, Success β†’ success).

## N4 β€” `--version` reads from package.json

Was `reg-cli-wasm\n` placeholder. Now uses `createRequire` to read the
adjacent `package.json` so reg-suit's `reg-cli --version` probe works.

## Tests

- `cargo test -p reg_core --lib` β€” 10 passing.
- `pnpm --filter ./js test` β€” 24 passing. New cases:
  - `--version` prints semver-shaped string
  - HTML report embeds a favicon data URL
  - `-X client` writes worker.js (>10KB) + detector.wasm (>100KB)
  - `-U` prunes deleted baselines, preserves passed-file mtime
  - library test: `-U` prunes stale baseline via `compare()`

## Notable deferrals

- **N9** (live `compare` events): reg-suit / spinners currently see all
  per-file events fire synchronously just before `complete`, not as each
  diff completes. Fixing this needs a WASI-stderr event channel
  (`REG_CLI_EVT:type:path` lines captured by `printErr` and forwarded via
  `parentPort.postMessage`). Non-trivial and deserves its own PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* CLI compat (phase G): live compare events + .expect("TODO:") cleanup

## N9 β€” Live per-file `compare` events during the diff loop

Classic reg-cli's `ProcessAdaptor` fires `emitter.emit('compare', ...)`
as each image completes, so spinners/progress bars animate. Phase E's
wasm port synthesised all events in one burst *after* `complete`, with
a comment admitting the limitation. Fixed by streaming them through a
tagged WASI stderr channel:

- `crates/reg_core/src/lib.rs` emits `emit_progress(kind, path)` which
  writes `__REG_CLI_EVT__\t{"type":"…","path":"…"}\n` to stderr. Fired
  from:
  - `find_images` post-detection for `new` / `delete` items.
  - inside `par_iter` immediately after the pixel-diff completes and
    threshold classification runs (classification was moved into the
    closure so events fire on whichever rayon thread did the work, not
    serialised after collect).
- New `js/progress.ts` (`createPrintErrHook`) parses those lines out of
  the WASI stderr stream, forwarding progress events to the caller and
  passing everything else through to `console.error` so real diagnostics
  still reach users. Piggybacks on `@tybys/wasm-util`'s
  `StandardOutput.write` already being line-buffered.
- `js/entry.ts` + `js/worker.ts` each install `printErr` on their WASI
  instance and forward events via `parentPort.postMessage({ cmd:
  'compare-event', event })`.
- `js/index.ts` `run()` handles `compare-event` messages from both the
  main entry worker AND every spawned rayon thread worker, emitting them
  live on the outer EventEmitter.
- The old post-complete batched emission is removed β€” `emitter.on('compare',
  …)` now sees events live, same as classic.

New library test `compare() emits compare events LIVE, before complete`
asserts the ordering so we don't silently regress.

## M1 / M2 β€” Drop the `.expect("TODO:")` and the stale input-validation TODO

`create_dir_for_json_report` previously `.expect`ed on `url::Url::join`
failures, which would crash with the useless panic message `"TODO:"` if
the `--urlPrefix` value ever produced an unjoinable URL (unlikely after
clap parses it as `url::Url`, but still). Swapped for a graceful fallback
to the relative path + a `tracing::warn!` log, matching classic
reg-cli's silent-fallthrough behaviour. The adjacent
`// TODO: please validate input on cli input.` comment is removed as the
edit subsumes it.

## Tests

- `cargo test -p reg_core --lib` β€” 10/10.
- `pnpm --filter ./js test` β€” 25/25 (one new: live-event ordering).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* CLI compat (phase H): replace glob::glob with direct walker to fix silent find_images==[] for multi-segment preopens

## The bug

When `computeWasiSandbox` registered a preopen whose mapped path had more
than one `/`-separated segment, `find_images` returned ZERO results β€”
silent data loss: reg.json has every list empty, exit code 0, HTML says
"success". CI goes green while no images were compared.

Reproduction:

    preopen                    | find_images result
    ---------------------------|-------------------
    ./repro              (1seg) | βœ… images found
    ./repro/inner        (2seg) | ❌ empty set
    ./repro/a/b/c        (3seg) | ❌ empty set

Who hit it:
  - `reg-cli packages/app/screenshots/actual …` (monorepos) πŸ’₯
  - `reg-cli /Users/alice/project/screens/actual …` (absolute paths) πŸ’₯
  - `reg-cli screens/actual …` (flat) βœ…

## Root cause (properly diagnosed this time)

Not wasi-sdk. Not @tybys/wasm-util. Not Rust toolchain. All three were
tested (wasm-util 0.9.0 β†’ 0.10.1, Rust nightly-2025-01-01 β†’
nightly-2026-04-18, Node 20 β†’ 22) with no change.

The actual culprit: **the `glob` crate**. For pattern
`deep/a/b/actual/**/*`, glob walks from cwd opening each intermediate
directory (`.` β†’ `deep` β†’ `deep/a` β†’ `deep/a/b` β†’ `deep/a/b/actual`).
Under our WASI sandbox the preopen IS `./deep/a/b` β€” so `.`, `deep`, and
`deep/a` are OUTSIDE the sandbox. `read_dir(.)` fails with EBADF, glob
silently swallows the error (`.flatten()` in our filter code also
contributed), and the iterator produces zero items.

Verified by instrumenting entry.ts's WASI imports: direct
`std::fs::read_dir("deep/a/b/actual")` SUCCEEDS via `path_open(fd=3,
"actual")`. Only glob's walk-from-cwd strategy fails.

## The fix

Replace `glob::glob` in `find_images` with a direct recursive
`std::fs::read_dir(root)` walker (`walk_images`). Starts AT the
sandboxed directory instead of traversing to it. No intermediate
`read_dir(".")` calls. Multi-segment preopens now Just Work with the
original narrowest-ancestor sandbox β€” no widening, no tradeoffs, no
warnings.

Also drops the unused `glob` and `path-clean` crates from
`crates/reg_core/Cargo.toml`.

## Tests

`js/test/cli.test.mjs`:
  - `multi-segment positional dirs still discover images` β€” deep path
    (`.phase-h-deep/<pid>/nested/level/...`), asserts `actualItems` is
    populated despite the preopen being 4+ segments deep.
  - `multi-segment positional dirs: nested subdirs still discover
    images` β€” image at `actual/sub/a.png` under a multi-segment preopen,
    verifies both recursion AND the actual_dir-relative path in reg.json.

Full JS: 27/27 passing. Rust: 10/10 unchanged.

## Why this PR is better than the earlier "truncate to first segment" attempt

The first revision of this PR shipped a JS-side workaround that
truncated multi-segment preopens to their first segment (widening the
sandbox β€” `./packages/app/screens` β†’ `./packages`, or even
`/Users/alice/…` β†’ `/Users`). That avoided the bug but exposed strictly
more of the host than needed.

The direct walker approach fixes the ACTUAL bug: `find_images` now works
correctly against the narrowest-ancestor preopen, and monorepo / absolute
path users get the same sandbox privileges as flat-layout users.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* CLI compat (phase I): reg-suit drop-in β€” strip ignoreChange + enableCliAdditionalDetection before forwarding

## The bug

reg-viz/reg-suit's `packages/reg-suit-core/src/processor.ts:105-116`
invokes `compare({…})` with two keys the Wasm port's Rust clap layer
does not recognise:

    compare({
      actualDir, expectedDir, diffDir, json, report,
      update: false,
      ignoreChange: true,            // ← not a Rust clap flag
      urlPrefix: '',
      …
      enableCliAdditionalDetection: true,  // ← not a Rust clap flag
      enableClientAdditionalDetection: …,
      …
    })

`compare()` was forwarding unknown keys verbatim as `--foo value` pairs.
When reg-suit calls us, Rust clap aborts with `error: unexpected
argument '--ignoreChange' found` before the diff loop even runs.

## The fix

Add both to `CLI_ONLY_KEYS` in `js/index.ts`. Semantically both are
no-ops at the EventEmitter layer:

- `ignoreChange` only governs classic reg-cli's process exit code; the
  library never needed it.
- `enableCliAdditionalDetection` was classic's flag for running an extra
  CLI-side x-img-diff pass during the diff. Our Wasm pipeline already
  produces the final pass/fail classification, so toggling it changes
  nothing. (The *client*-side variant is handled separately via
  `additionalDetection: 'client'` / the legacy
  `enableClientAdditionalDetection: true` alias.)

Both keys also added to the `CompareInput` TS type so TypeScript users
can pass them without a `// @ts-ignore` dance.

## Tests

New `js/test/library.test.mjs` case `reg-suit compat: compare() accepts
reg-suit-shaped options without aborting` β€” constructs the exact option
bag reg-suit's `processor.ts` passes and verifies `complete` fires
cleanly with the expected reg.json. If someone later removes a key from
`CLI_ONLY_KEYS`, this catches it.

Full JS: 28/28 passing. Rust: 10/10 unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* ci: pin GitHub Actions to SHAs via pinact

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* reg_core: tolerate per-image read/decode failures

Wrap each rayon-thread image into a `ImageOutcome::{Ok,Failed}` rather
than propagating a single Err out of the closure. On read or decode
failure: log to stderr, fire `compare-event{type:"fail"}` for live
progress, and fold into `failedItems` so callers (CLI, EventEmitter
consumers) see a normal end-of-run result.

Matches classic reg-cli's tolerance β€” one corrupt PNG must not abort a
batch of 1000 images.

Adds tempfile dev-dep + 3 unit tests:
- corrupt PNG β†’ failedItems, neighbour passes
- non-image extensions stay silently filtered

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* js tests: backfill JS-branch parity + corrupt-image edge cases

cli.test.mjs (+6):
- -T 0.00 / -T 1.00 thresholdRate boundaries
- -S 0 / -S 10000000 thresholdPixel boundaries
- identical dirs β†’ passedItems all populated
- actual empty β†’ all baselines surface as deletedItems
- corrupt PNG β†’ failedItems entry, sibling still passes, exit 1
- non-image files (.md, .json) silently skipped

library.test.mjs (+2):
- reg-suit drop-in: every event + CompareOutput field processor.ts
  consumes (start/compare/complete/error + failed/new/deleted/passed)
- compare() does NOT fire `error` on single-image decode failure;
  bad.png β†’ failedItems + 'fail' compare-event, run completes

js/reg.wasm rebuilt against the updated reg_core (per-image error
tolerance) so CI exercises the new behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: drop @pyroscope/nodejs from root deps + remove pnpm version conflict

Two CI failures on PR #604:
- `test` job: pnpm-lock.yaml didn't list @pyroscope/nodejs (we adopted
  main's lock during the merge), so --frozen-lockfile rejected the
  package.json that still had it. @pyroscope is only used by the wasm
  wrapper (`js/`); it doesn't belong in the root JS reg-cli's deps.
- `wasm-test` job: pnpm/action-setup with `version: 10.26.2` conflicted
  with root package.json's `packageManager: pnpm@10.33.2+...`. Drop the
  explicit version and let action-setup read packageManager.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove legacy JS implementation, tests, and dependencies

The Wasm-backed reg-cli (under js/ and crates/) is now the single
source of truth β€” its 12 cargo tests + 38 node:test cases cover
classic reg.json/junit schema parity, every reg-suit processor.ts
option/event/CompareOutput field, and per-image read/decode failure
tolerance. Keeping the legacy JS impl alongside it doubled the CI
matrix and the dep surface (puppeteer, babel, ava, flow…) for no
incremental coverage.

Removed
-------
- src/, test/, decls/, sample/, resource/  β€” legacy JS source/tests/fixtures
- .babelrc, .flowconfig                    β€” legacy build/lint config
- .travis.yml, appveyor.yml, docker-compose.yml β€” legacy CI/dev infra
- fixture.html, index.html, reg.json       β€” legacy generated artefacts
- report/{index.html,worker.js,sample/,diff/} β€” legacy demo HTML
- root package.json deps (babel, ava, puppeteer, flow-bin, img-diff-js,
  x-img-diff-js, lodash, etc.) and the pnpm.overrides scaffolding that
  existed solely to neutralise transitive vulns in those legacy deps

Kept (consumed by the Wasm pipeline)
------------------------------------
- template/{template.html,worker_pre.js}   β€” `include_str!` from reg_core
- report/assets/favicon_{success,failure}.png β€” `include_bytes!` from reg_core
- scripts/{build-ui.sh,build-wasm.sh}      β€” wasm + report-ui build entry points
- crates/, js/                             β€” the actual implementation
- bench/, docs/                            β€” benchmarks + user docs

CI
--
- Drop the legacy `test` job (puppeteer screenshot + ava). Rename the
  former `wasm-test` to `test`; it remains the only required check.

Root package.json is now `private: true` and minimal. The publishable
package lives under js/ as `@bokuweb/reg-cli-wasm` (renaming back to
`reg-cli` is a separate decision).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Collapse js/ into the repo root so the published package == the repo

The Wasm wrapper used to live under `js/` while the legacy JS impl
sat at the root. After #605 dropped the legacy impl the dual layout
was no longer load-bearing, just confusing β€” `package.json` at the
root was a stub `private: true` shim while the real package.json that
publishes to npm lived one directory deeper.

Moves (preserving git history via `git mv`)
-------------------------------------------
- js/{build.config.ts, cli.ts, entry.ts, index.ts, progress.ts,
  proxy.{js,ts}, tracing.ts, tsconfig.json, utils.ts, worker.ts,
  ximgdiff.ts, reg.wasm} β†’ root
- js/sample/   β†’ sample/
- js/test/     β†’ test/
- js/package.json β†’ root package.json (overwrites the stub; keeps the
  top-level `packageManager` field and adds a `repository` block)
- js/pnpm-lock.yaml β†’ root pnpm-lock.yaml

Path adjustments
----------------
- build.config.ts: `repoRoot = resolve(here, '..')` β†’ just `here`.
- test/{cli,library}.test.mjs: REPO/SAMPLE_REL/CLI/DIST/TMP_ROOT_*
  drop the leading `js/` segment.
- scripts/build-wasm.sh: copies to `./reg.wasm` (was `js/reg.wasm`).
- .github/workflows/ci.yml: drop `working-directory: js` from install/
  build/test steps; rename them.
- .gitignore: collapse the workspace path.

Verification
------------
- `pnpm install --frozen-lockfile` βœ“
- `pnpm build` βœ“ (dist regenerated; `reg-cli-wasm.*` chunks identical
  byte-for-byte to pre-collapse)
- `pnpm test` β†’ 38 / 38 βœ“
- `cargo +stable test -p reg_core --lib --locked` β†’ 12 / 12 βœ“
- `npm pack --dry-run` β†’ publishable as
  `@bokuweb/reg-cli-wasm@0.0.0-experimental6` (32 files, 926 kB)
- end-to-end: `node ./dist/cli.mjs ./sample/actual ./sample/expected
  /tmp/diff -I` exits 0 with classic per-file output.

The package still publishes as `@bokuweb/reg-cli-wasm`. Renaming back
to `reg-cli` is the next step once a publish dry-run lands cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Move TS sources back into src/ β€” keep root limited to package metadata

Reverts the visual flatness from the previous collapse. Having a dozen
.ts files at the repo root next to package.json / Cargo.toml /
README.md was hard to scan; the npm convention is `src/` for the
TypeScript sources and the root for package metadata + build config.

Moved (history-preserving git mv): cli.ts, entry.ts, index.ts,
progress.ts, proxy.{js,ts}, tracing.ts, utils.ts, worker.ts,
ximgdiff.ts β†’ src/

Build config:
- build.config.ts entries β†’ ./src/{index,cli,worker,entry}.ts
- tsconfig.json `include` β†’ ./src/**/*.ts

Stays at root: package.json, pnpm-lock.yaml, build.config.ts,
tsconfig.json, reg.wasm, sample/, test/, scripts/, crates/, template/,
report/.

Verified: pnpm build β†’ identical tarball (`shasum 0d8db1e5...`,
926.6 kB, 32 files) as the flat layout. pnpm test β†’ 38/38.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* scripts: cross-platform wasm build + one-shot release prep

build-wasm.sh
- Add Linux support (x86_64-linux, arm64-linux). Was macOS-only.
- Switch to OS+arch case statement; fail loudly on unsupported triples
  rather than silently building for the wrong target.
- Auto-install the rustup target (wasm32-wasip1-threads) when rustup
  is present β€” saves a "first build" footgun.
- `set -euo pipefail` (was `set -e`).
- Comments on what wasi-sdk + SYSROOT are actually for.

scripts/release.sh (new)
- One-shot publish prep. Chains: build-ui β†’ build-wasm β†’ pnpm install
  β†’ pnpm build β†’ npm pack [--dry-run].
- `--pack` writes the .tgz; default is dry-run.
- SKIP_UI=1 / SKIP_WASM=1 escape hatches for iterating locally.
- REPORT_UI_TAG env var (defaults to v0.3.0, matching CI).

package.json scripts:
  build:wasm       β†’ bash ./scripts/build-wasm.sh
  release:prep     β†’ bash ./scripts/release.sh           (dry-run pack)
  release:pack     β†’ bash ./scripts/release.sh --pack    (writes .tgz)

Verified locally: `SKIP_UI=1 SKIP_WASM=1 bash scripts/release.sh`
produces a 926.6 kB tarball, 32 files. Full chain (no skips) tested
separately via `bash scripts/build-wasm.sh` β€” wasi-sdk download +
cargo build --release succeeds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Drop the last legacy bits: bench/, empty .npmignore, flow-only prettier opts

bench/
  Was JS-vs-Wasm trace comparison infrastructure (bench/run.sh runs
  `dist/cli.js` AND `js/dist/cli.mjs` against the same fixtures). With
  the legacy JS impl gone there's no second runner to compare against β€”
  the only path the script tests is one that no longer exists. Delete
  the directory rather than leave a broken benchmark.

.npmignore
  0-byte file. The `files: ["dist"]` whitelist in package.json is what
  actually decides what ships, so .npmignore was redundant *and* empty.

.prettierrc
  Drop `parser: "flow"` (no flow files left β€” TS files use the
  TypeScript parser, auto-selected by extension) and the long-deprecated
  `jsxBracketSameLine` (renamed to `bracketSameLine` in prettier 2.4).

Verified end-to-end with the BUILT tarball:
  $ bash scripts/release.sh --pack
    β†’ bokuweb-reg-cli-wasm-0.0.0-experimental6.tgz (926.6 kB, 32 files)
  $ npm install /path/to/.tgz   # in a scratch project
  $ node run.mjs                # mirrors reg-suit/processor.ts:18
    β†’ 'start' fires, 'compare' fires per file, 'complete' arrives with
      failedItems / newItems / deletedItems / passedItems all arrays,
      no 'error' event. Drop-in compat confirmed.
  $ node node_modules/.../dist/cli.mjs ./actual ./expected ./diff -I
    β†’ writes diff/sample0.png, exits 0.

cargo test -p reg_core --lib --locked β†’ 12 / 12 βœ“
pnpm test                              β†’ 38 / 38 βœ“

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants