crashtest docs

Scenario packs (the extension surface)

A pack is a bundle you author once and reuse across run roots: scenario modules (same contract as the built-ins), and - going forward - recipes and target declarations. Zero new concepts to learn: a pack’s scenario modules satisfy the exact same contract the seed libraries do.

The schema (pack.json, v1)

{
  "schemaVersion": 1,
  "name": "my-money-pack",
  "description": "one line - what it attacks and how honest its oracles are",
  "version": "0.1.0",
  "entry": "index.js"
}

entry points at an ESM file under the pack directory whose default export is a scenario module ({ name, description, attacks: [{ name, run }] }). A pack.json that exists but is broken fails the run loudly - a config error, never a silent gap.

Where packs live

packs/
├── public/          ← tracked: shipped examples + shared packs
│   └── my-pack/
└── local/           ← gitignored: machine-private packs
    └── my-private-pack/

crashtest run discovers BOTH visibility dirs under packs/ at the run root - automatically, with no config changes. Visibility is a privacy convention, not a semantic difference: code in local/ runs exactly like code in public/; only the file location (and the gitignore) changes.

The naming decision, so it doesn’t come as a surprise: crashtest deliberately does not use vant’s models/public + models/private layout. “Models” carries an AI/ML meaning in this ecosystem’s vocabulary; “pack” is already crashtest’s own word (scenario pack), and public/local names the git reality directly - tracked vs machine-private.

Natural extension points

Naming convention

Pack names are kebab-case, and the prefix names the surface the pack speaks, not the product that inspired it:

What “money” means here: the dollar-shaped abuse family. The same family the built-in volume library already covers (credential-stuffing, storage-fill, cost-amplification) - attacks whose payoff is value: an anonymous write with payment intent must not stick (write-volley), one paid receipt must not buy a second registration (txn-replay), a metered wall must trip under rapid floods (gate-burst). “Shapes” = engines, not instances: every name (CPTs, meta keys, actions) is injected from the target’s moduleCtx[<pack-name>], so one pack serves every plugin and the khf/fm-specific lore stays in their private instantiations.

Public vs local - the decision rule

So: no packs/public/vant-* branded by target. Vant’s generic surface is already covered by the built-in libraries (the raw-node run-root recipe landed 13/13 REFUSED with zero custom code), and anything vant-specific

Config knobs

// crashtest.config.js
export default {
  packs: true,       // default - discover packs/public/ + packs/local/
  // packs: false,   // opt out entirely (the dogfood does: shipped packs
  //                 //   which need their own worker must not join the demo)
  // packs: ['/abs/path', 'node_modules/@you/crashtest-pack'],
};

Name collisions between a pack and the built-in libraries (or another pack) fail the run loudly - one name, one attack, no ambiguity.

Inventory any run root with:

npx crashtest packs
# local @0.0.1 - vant-core: 1 attacks from local/vant-core · armedBy moduleCtx['vant-core']
#         TODO: one line on what vant-core attacks

npx crashtest packs add vant-core          # → packs/local/vant-core/ (gitignored)
npx crashtest packs add my-shapes --public # → packs/public/my-shapes/ (tracked)
npx crashtest packs --json                 # machine inventory (LAST stdout line) - pair with `npm view <pkg> version` in CI for version joins