Module plans (run --module-plan)
A module plan answers one question before any request fires: which attack ids WOULD this target’s run carry, and which of those are not actually armed yet. It attacks nothing, writes nothing, and always exits 0 - it is informational, never a gate.
npx crashtest run --module-plan # human output
npx crashtest run --module-plan --json # machine plan blob (last stdout line)
Plans resolve through the SAME loaders and filter semantics as a real run, so a plan cannot drift from what a run would actually fire.
The three statuses
Per module (and inherited by every attack id in it):
| Status | Meaning | What to do |
|---|---|---|
WILL-FIRE |
Loads, and its config vocabulary is in place | Nothing - this attack is in the campaign |
NEEDS-ARMING |
The module or pack declares armedByKey and your moduleCtx lacks that key |
Set moduleCtx['<key>'] in crashtest.config.js |
SKIPPED-FILTER |
The module name is not in the config modules filter |
Expected if you filtered on purpose |
$ npx crashtest run --module-plan
> wp-money-shapes [pack:public @0.3.0] 3 attack(s) - NEEDS-ARMING: vocabulary rides moduleCtx['wp-money-shapes'] - set it in crashtest.config.js to arm (at run time these attacks REFUSE-skip, never false-positive)
> wp-money-shapes/nonce-less-volley - NEEDS-ARMING
> wp-money-shapes/payment-replay - NEEDS-ARMING
> wp-money-shapes/gate-burst - NEEDS-ARMING
> auth [library] 3 attack(s) - WILL-FIRE
> auth/identity-spoof-headers - WILL-FIRE
> auth/secret-watermark - WILL-FIRE
> auth/privilege-escalation - WILL-FIRE
> plan: 13 attack ids - 10 WILL-FIRE - 3 NEEDS-ARMING - 0 SKIPPED-FILTER
Why NEEDS-ARMING exists
An un-armed pack’s attacks REFUSE-skip at run time (skip-parity: never a
false positive). The trouble is a green tally can hide those skips - six
modules report REFUSED, but two of them never fired a request. The plan
surfaces that BEFORE you trust the gate verdict: NEEDS-ARMING names the
exact moduleCtx key to set, and the pack guides you from there.
The plan blob (--json)
The last stdout line is a machine-plan object:
| Field | Shape |
|---|---|
schemaVersion |
1 |
modules[] |
{ name, origin, source, version, armedKey, status, reason, attacks[] } |
packs[] |
{ name, visibility, version, origin, attacks, armedByKey, package } |
totals |
{ attacks, willFire, needsArming, skippedFilter } |
The dashboard (web/src/lib/ledgerModel.js) renders this shape natively - paste it in like a run blob or a ledger. Plans get NO gate verdict by design: they attack nothing, so they never pass or fail CI.
Where it fits the workflow
- Adopt a target:
crashtest run --module-planfirst - see the coverage and arming gaps before spending a run - Arm what needs arming in
crashtest.config.js(moduleCtx) - Re-plan until every attack you want is
WILL-FIRE - Run for real:
crashtest run(see Quick Start)