# Debug regressions

> Investigate snapshot drift, raw vs final webpack4 layers, rule trace bisection, profile export, and symptom-to-cause mapping (unresolved_mark, early-rule cascades).

- Repository: pionxzh/wakaru
- GitHub: https://github.com/pionxzh/wakaru
- Human docs: https://grok-wiki.com/public/docs/pionxzh-wakaru-77a438a6cc6b
- Complete Markdown: https://grok-wiki.com/public/docs/pionxzh-wakaru-77a438a6cc6b/llms-full.txt

## Source Files

- `docs/debugging.md`
- `docs/testing.md`
- `crates/cli/src/main.rs`
- `.cargo/config.toml`

---

---
title: "Debug regressions"
description: "Investigate snapshot drift, raw vs final webpack4 layers, rule trace bisection, profile export, and symptom-to-cause mapping (unresolved_mark, early-rule cascades)."
---

Wakaru regression debugging centers on insta snapshot failures in `crates/core/tests/`, the `debug trace` CLI for per-rule bisection on single files, and the webpack4 raw-vs-final snapshot split that separates unpack extraction from decompile pipeline changes. `.cargo/config.toml` sets `INSTA_UPDATE=new`, so drift fails tests and writes `.snap.new` files instead of silently updating committed snapshots.

## Snapshot drift workflow

When a test output changes, insta fails the test and writes a sibling `.snap.new` under `crates/core/tests/snapshots/`. Review every diff before accepting — a snapshot change is valid only when output is semantically better or the fixture expectation intentionally changed.

<Steps>
<Step title="Reproduce the failure">

Run the failing test binary directly:

```bash
cargo test -p wakaru-core --test webpack4_unpack
cargo test -p wakaru-core --test my_rule_rule -- my_specific_test
```

For faster iteration on the core suite, use nextest:

```bash
cargo nextest run -p wakaru-core --test webpack4_unpack
```

</Step>
<Step title="Inspect the diff">

Insta prints a unified diff between the committed `.snap` and the new output. A `.snap.new` file appears next to the original snapshot. Do not commit `.snap.new` files — accept or reject first.

</Step>
<Step title="Accept or reject">

```bash
cargo insta review    # interactive accept/reject per snapshot
cargo insta accept    # bulk-accept all pending .snap.new files
```

For a one-off bulk accept during a run:

```bash
INSTA_UPDATE=always cargo test -p wakaru-core --test webpack4_unpack
```

<Warning>
`INSTA_UPDATE=always` bypasses the review gate. The project deliberately uses `INSTA_UPDATE=new` so regressions cannot land green without human review.
</Warning>

</Step>
<Step title="Add a focused regression test">

Snapshot updates alone do not satisfy the definition of done for rule changes. Add or update a per-rule test in `crates/core/tests/*_rule.rs` that reproduces the exact bug or behavior change.

</Step>
</Steps>

## Webpack4 snapshot layers

Webpack4 unpack tests pin output at two boundaries. Comparing them localizes whether a regression originates in extraction or in the decompile pipeline.

| Layer | Test binary | Snapshot prefix | What it captures |
|-------|-------------|-----------------|------------------|
| Raw | `webpack4_unpack_raw` | `webpack4_unpack_raw__raw_*` | Module code after webpack extraction and bundler-coupled normalization, **before** decompile rules (`SimplifySequence`, `UnEsm`, etc.) |
| Final | `webpack4_unpack` | `webpack4_unpack__*` | Fully decompiled module output after the normal rule pipeline |

```text
webpack4 bundle (testcases/webpack4/dist/index.js)
        │
        ▼
  unpack_webpack4_raw()          ──► webpack4_unpack_raw__raw_*.snap
        │
        ▼
  full unpack() + decompile      ──► webpack4_unpack__*.snap
```

<Note>
Raw snapshots may still contain webpack markers such as `require.r(exports)` and `require.d(...)` getters. These are semantic inputs for later ESM recovery — they are expected in raw output, not raw-layer failures by themselves.
</Note>

**Decision tree when snapshots drift:**

- Raw unchanged, final changed → inspect the decompile pipeline (rule ordering, individual rules, `RewriteLevel`).
- Raw and final both changed → inspect the unpacker or bundler-coupled normalization first, then trace downstream rules.
- Many final snapshots changed at once → suspect an early pipeline rule cascade (see symptom table below).

Run the layer-specific tests:

```bash
cargo test -p wakaru-core --test webpack4_unpack_raw
cargo test -p wakaru-core --test webpack4_unpack
```

## Rule trace bisection

`debug trace` runs the normal single-file rule pipeline and prints the initial source once, followed by a git-style unified diff for each rule that changes rendered output. Use it before manually bisecting with `apply_rules()` and `RulePipelineOptions::between(...)`.

<CodeGroup>

```bash title="Trace all changed rules"
cargo run -p wakaru-cli -- debug trace path/to/module.js
```

```bash title="Include unchanged rules"
cargo run -p wakaru-cli -- debug trace path/to/module.js --all
```

```bash title="Trace a rule range"
cargo run -p wakaru-cli -- debug trace path/to/module.js --from RemoveVoid --until UnEsm
```

```bash title="Write trace to file"
cargo run -p wakaru-cli -- debug trace path/to/module.js -o trace.txt
```

</CodeGroup>

<ParamField body="--from" type="string">
First rule to run. Must match a name from `rule_names()` (e.g. `RemoveVoid`, `UnIife`, `SmartInline`). Second passes use suffixed names like `UnIife2`, `UnParameters2`.
</ParamField>

<ParamField body="--until" type="string">
Last rule to run (inclusive). Unknown rule names produce an error.
</ParamField>

<ParamField body="--all" type="boolean">
Include rules that ran but left rendered output unchanged. These appear as `=== RuleName (unchanged) ===` headers with no diff hunk.
</ParamField>

<ParamField body="--level" type="RewriteLevel" default="standard">
Rewrite aggressiveness passed through to `DecompileOptions`. Affects which rules are enabled.
</ParamField>

<ParamField body="-m / --source-map" type="path">
Optional source map for identifier recovery during trace, same as normal decompile.
</ParamField>

<Warning>
`debug trace` is single-file only. `trace_rules()` rejects bundle inputs because unpack uses a two-phase fact-system pipeline where per-rule tracing would be misleading. For bundle regressions, trace an extracted raw module or reduce the issue to a single-file reproduction.
</Warning>

### Core API equivalent

```rust
use wakaru_core::{trace_rules, format_trace_events, DecompileOptions, RuleTraceOptions};

let events = trace_rules(
    source,
    DecompileOptions { filename: "module.js".into(), ..Default::default() },
    RuleTraceOptions {
        start_from: Some("RemoveVoid".into()),
        stop_after: Some("UnEsm".into()),
        only_changed: true,  // default
    },
)?;
let output = format_trace_events(&events);
```

`RuleTraceEvent` carries `rule`, `changed`, `before`, and `after` strings. `format_trace_events` renders changed events as unified diffs; unchanged events get header-only lines.

### Bisection workflow

<Steps>
<Step title="Find the introducing rule">

Run `debug trace` on a representative module. Scroll to the first diff that matches the regression symptom.

</Step>
<Step title="Confirm input shape">

Use `render_pipeline_until(source, "RuleName")` from `crates/core/tests/common/mod.rs` to capture cumulative output just before the suspect rule. Verify the AST shape is what you expect.

</Step>
<Step title="Isolate the rule">

Use `render_pipeline_between(source, "Start", "Stop")` to run only a narrow rule range without downstream effects.

</Step>
<Step title="Check pipeline ordering">

If the issue is ordering rather than a single rule, consult the rule dependency inventory and the ordered `RuleDescriptor` registry in `pipeline.rs`. Fragile orderings are documented with explicit `requires` chains.

</Step>
</Steps>

Test helpers available in `common/mod.rs`:

| Helper | Purpose |
|--------|---------|
| `render_pipeline_until(source, stop_after)` | Pipeline through named rule (inclusive), then emit |
| `render_pipeline_between(source, start, stop)` | Only rules from `start` through `stop` (inclusive) |
| `trace_pipeline(source, options)` | Collect `RuleTraceEvent`s programmatically |
| `changed_rules(source)` | List rule names that changed output |

## Chrome trace profiling

Global `--profile` writes a Chrome trace-format file suitable for `chrome://tracing`. Use it to measure parse, resolver, rule, and unpack phase timing rather than to inspect AST diffs.

```bash
cargo run -p wakaru-cli -- --profile trace.json input.js -o output.js
cargo run -p wakaru-cli -- --unpack --profile unpack-trace.json bundle.js -o out/
```

<ParamField body="--profile" type="path" required>
Output file for the Chrome trace profile. Creates the file via `tracing_chrome::ChromeLayerBuilder`.
</ParamField>

<ParamField body="--profile-rules" type="boolean">
Requires `--profile`. Sets subscriber level to `DEBUG` so per-rule `debug_span!("rule", name = ...)` spans appear in the trace. Without it, only `INFO`-level spans (parse, resolver, rules aggregate, emit, unpack phases) are recorded.
</ParamField>

Per-rule spans are emitted inside `apply_rules_impl` at `DEBUG` level. Unpack paths add `INFO` spans for detection (`detect_webpack4`, `detect_webpack5`, etc.) and phase boundaries (`phase1_collect_facts`, `phase2_decompile_modules`).

## Symptom-to-cause mapping

| Symptom | Likely cause | Investigation |
|---------|--------------|---------------|
| Unexpected variable names or wrong binding matches | Missing `unresolved_mark` guard, or matching by `sym` alone instead of `(sym, SyntaxContext)` | Check `id.ctxt.outer() != self.unresolved_mark` gate in the rule; use `BindingRenamer` for renames |
| Many snapshots changed at once | Early pipeline rule cascade | Trace a representative module; inspect `SimplifySequence`, `FlipComparisons`, `RemoveVoid` and other Stage 1 normalization rules |
| Rule not firing in isolated test | Input shape differs from real pipeline | Check raw snapshot or `debug trace` to see AST when the rule receives it; use `render()` instead of `render_rule()` if earlier normalization is required |
| `render_rule` passes but `render` fails | Rule depends on earlier normalization (e.g. helper body-shape matching after `SimplifySequence`) | Use `render_pipeline_until` or full `render()` in the test |
| `cargo test` hangs | Likely infinite recursion in a visitor | `RUST_BACKTRACE=1 cargo test -- --nocapture` |
| Raw snapshot changed unexpectedly | Unpacker or bundler-coupled normalization regression | Compare against `webpack4_unpack_raw`; inspect webpack extraction helpers |
| Final-only snapshot drift | Decompile rule or ordering change | Compare raw (stable) vs final; bisect with `debug trace` on extracted module code |
| Bundle regression, trace fails | Bundle input rejected by design | Extract raw module with `--unpack --raw`, then trace that single file |

### `unresolved_mark` in practice

After `resolver()` runs, free variables (globals like `Object`, `require`) carry `unresolved_mark` as their outer `SyntaxContext`. Rules that match identifiers by name must gate on this mark to avoid rewriting locals, parameters, or imports with the same symbol name. Every new visitor that matches identifiers by name should take `unresolved_mark: Mark` and check `id.ctxt.outer() == self.unresolved_mark` (or the inverse guard to skip bound identifiers).

## Bundle and cross-module regressions

Full bundle unpack runs Phase 1 fact collection after `UnEsm`, then Phase 2 decompilation with cross-module rules (`namespace_decomposition`, cross-module helper refs). Per-rule trace does not cross this barrier.

For bundle-level regressions:

1. Run the relevant pipeline test (`bundle_unpack`, `esbuild_unpack`, `webpack4_unpack`).
2. If webpack4, compare raw vs final layers first.
3. Extract the affected module with `--unpack --raw` and save it to a scratch file.
4. Run `debug trace` on that single module.
5. If the regression involves cross-module facts, inspect `facts_rule` tests and the two-phase barrier described in cross-module facts documentation.

## External validation

### Fixture repository

A sibling `wakaru-fixtures` repository (private) contains real-world bundles. After significant rule changes:

```bash
cd ../wakaru-my-worktree
../wakaru-fixtures/run.sh --check       # diff vs committed reference
../wakaru-fixtures/run.sh --update      # accept reviewed improvements
```

The script builds `wakaru-cli` with the `dev-release` profile from the checkout you launch it from, avoiding stale binaries.

### Reproduction matrices

Matrices under `scripts/repro/` test recovery across transpiler versions and minification levels. Current rates live in `scripts/repro/stats.json`.

```bash
cargo build --profile dev-release -p wakaru-cli
export WAKARU="$PWD/target/dev-release/wakaru"
node scripts/repro/array-spread-rest-matrix/matrix.mjs --details
node scripts/repro/collect-stats.mjs --check
```

Build the binary from the same worktree you are validating — a stale `main` binary can produce false pass/fail results.

## Required verification before commit

After fixing a regression, run the full relevant checklist:

```bash
cargo test -p wakaru-core --test my_rule_rule          # focused rule test
cargo test -p wakaru-core --test noop_pipeline
cargo test -p wakaru-core --test webpack4_unpack
cargo test -p wakaru-core --test webpack4_unpack_raw
cargo test -p wakaru-core --test bundle_unpack
cargo test -p wakaru-core --test esbuild_unpack
cargo fmt --check
cargo clippy -p wakaru-core --all-targets -- -D warnings
git status --short    # no stale .snap.new files
```

<Check>
A regression fix is complete when the focused rule test passes, pipeline snapshots are reviewed and accepted (or unchanged), formatting and clippy are clean, and no `.snap.new` files remain in the working tree.
</Check>

## Related pages

<CardGroup>
<Card title="Trace the rule pipeline" href="/trace-rule-pipeline">
CLI and API details for `debug trace`, `--from`/`--until` ranges, and trace output format.
</Card>
<Card title="Testing and snapshots" href="/testing-and-snapshots">
Insta workflow, nextest usage, test helpers, and the pre-commit verification matrix.
</Card>
<Card title="Develop transformation rules" href="/develop-rules">
Test-first rule development, pipeline placement, and `unresolved_mark` requirements.
</Card>
<Card title="Decompile pipeline" href="/decompile-pipeline">
Single-file parse → resolver → rules → fixer → emit flow and `unresolved_mark` scope gating.
</Card>
<Card title="Webpack bundle recipe" href="/webpack-bundle-recipe">
End-to-end webpack4/webpack5 fixture workflow and reference output comparison.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Common failure modes, warning kinds, and bug report fields.
</Card>
</CardGroup>
