@kirrosh/zond 0.23.0 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +176 -1
  2. package/README.md +8 -7
  3. package/package.json +2 -3
  4. package/src/CLAUDE.md +112 -0
  5. package/src/cli/commands/add-api.ts +19 -7
  6. package/src/cli/commands/api/annotate/index.ts +359 -4
  7. package/src/cli/commands/api/annotate/lifecycle.ts +1 -1
  8. package/src/cli/commands/api/annotate/overlay.ts +1 -1
  9. package/src/cli/commands/api/annotate/pagination.ts +10 -6
  10. package/src/cli/commands/api/annotate/prompts.ts +39 -2
  11. package/src/cli/commands/audit.ts +360 -54
  12. package/src/cli/commands/check.ts +15 -2
  13. package/src/cli/commands/checks.ts +352 -36
  14. package/src/cli/commands/cleanup.ts +4 -30
  15. package/src/cli/commands/coverage.ts +275 -57
  16. package/src/cli/commands/db.ts +311 -8
  17. package/src/cli/commands/discover.ts +281 -161
  18. package/src/cli/commands/doctor.ts +57 -3
  19. package/src/cli/commands/fixtures.ts +1 -1
  20. package/src/cli/commands/generate.ts +24 -7
  21. package/src/cli/commands/init/bootstrap.ts +4 -1
  22. package/src/cli/commands/init/index.ts +2 -2
  23. package/src/cli/commands/init/skills.ts +43 -0
  24. package/src/cli/commands/init/templates/agents.md +12 -3
  25. package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
  26. package/src/cli/commands/init/templates/skills/zond-checks.md +268 -44
  27. package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
  28. package/src/cli/commands/init/templates/skills/zond-triage.md +88 -26
  29. package/src/cli/commands/init/templates/skills/zond.md +274 -64
  30. package/src/cli/commands/init/templates/zond-config.yml +1 -1
  31. package/src/cli/commands/prepare-fixtures.ts +14 -52
  32. package/src/cli/commands/probe/_seed-bodies.ts +52 -0
  33. package/src/cli/commands/probe/mass-assignment.ts +101 -10
  34. package/src/cli/commands/probe/security.ts +95 -12
  35. package/src/cli/commands/probe/webhooks.ts +2 -0
  36. package/src/cli/commands/probe.ts +87 -11
  37. package/src/cli/commands/refresh-api.ts +59 -1
  38. package/src/cli/commands/report-bundle.ts +3 -11
  39. package/src/cli/commands/request.ts +116 -0
  40. package/src/cli/commands/run.ts +53 -5
  41. package/src/cli/commands/schema-from-runs.ts +128 -0
  42. package/src/cli/commands/secrets.ts +133 -0
  43. package/src/cli/json-envelope.ts +0 -20
  44. package/src/cli/json-schemas.ts +51 -0
  45. package/src/cli/output.ts +17 -1
  46. package/src/cli/program.ts +5 -4
  47. package/src/cli/safe-live.ts +24 -0
  48. package/src/cli/status-filter.ts +0 -10
  49. package/src/core/audit/persist.ts +183 -0
  50. package/src/core/checks/budget.ts +59 -0
  51. package/src/core/checks/checks/cross_call_references.ts +17 -4
  52. package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
  53. package/src/core/checks/checks/idempotency_replay.ts +1 -5
  54. package/src/core/checks/checks/ignored_auth.ts +44 -1
  55. package/src/core/checks/checks/index.ts +3 -0
  56. package/src/core/checks/checks/lifecycle_transitions.ts +169 -26
  57. package/src/core/checks/checks/negative_data_rejection.ts +119 -16
  58. package/src/core/checks/checks/not_a_server_error.ts +8 -0
  59. package/src/core/checks/checks/open_cors_on_sensitive.ts +47 -18
  60. package/src/core/checks/checks/pagination_invariants.ts +298 -117
  61. package/src/core/checks/checks/positive_data_acceptance.ts +1 -4
  62. package/src/core/checks/checks/status_code_conformance.ts +78 -7
  63. package/src/core/checks/mode.ts +3 -0
  64. package/src/core/checks/recommended-action.ts +5 -1
  65. package/src/core/checks/runner.ts +614 -27
  66. package/src/core/checks/spec-findings.ts +308 -0
  67. package/src/core/checks/types.ts +117 -1
  68. package/src/core/checks/zond-extensions.ts +73 -0
  69. package/src/core/classifier/recommended-action.ts +35 -6
  70. package/src/core/coverage/loader.ts +31 -0
  71. package/src/core/diagnostics/db-analysis.ts +200 -106
  72. package/src/core/diagnostics/failure-class.ts +21 -1
  73. package/src/core/diagnostics/failure-hints.ts +4 -208
  74. package/src/core/diagnostics/suggested-fixes.ts +2 -3
  75. package/src/core/generator/chunker.ts +1 -8
  76. package/src/core/generator/data-factory.ts +199 -61
  77. package/src/core/generator/fixtures-builder.ts +38 -31
  78. package/src/core/generator/index.ts +0 -2
  79. package/src/core/generator/openapi-reader.ts +98 -4
  80. package/src/core/generator/path-param-disambig.ts +30 -4
  81. package/src/core/generator/resources-builder.ts +276 -26
  82. package/src/core/generator/schema-utils.ts +22 -0
  83. package/src/core/generator/suite-generator.ts +168 -15
  84. package/src/core/generator/types.ts +6 -0
  85. package/src/core/identity/identity-file.ts +0 -0
  86. package/src/core/output/README.md +11 -29
  87. package/src/core/output/index.ts +1 -1
  88. package/src/core/output/run.ts +0 -35
  89. package/src/core/output/types.ts +0 -7
  90. package/src/core/parser/dynamic-values.ts +160 -0
  91. package/src/core/parser/variables.ts +0 -0
  92. package/src/core/probe/dry-run-envelope.ts +4 -0
  93. package/src/core/probe/mass-assignment/classify.ts +175 -0
  94. package/src/core/probe/mass-assignment/cleanup.ts +52 -0
  95. package/src/core/probe/mass-assignment/digest.ts +114 -0
  96. package/src/core/probe/mass-assignment/orchestrator.ts +459 -0
  97. package/src/core/probe/mass-assignment/regression.ts +141 -0
  98. package/src/core/probe/mass-assignment/suspects.ts +92 -0
  99. package/src/core/probe/mass-assignment/types.ts +135 -0
  100. package/src/core/probe/mass-assignment-probe.ts +23 -1118
  101. package/src/core/probe/mass-assignment-template.ts +32 -4
  102. package/src/core/probe/path-discovery.ts +3 -4
  103. package/src/core/probe/probe-harness.ts +21 -22
  104. package/src/core/probe/security/baseline.ts +174 -0
  105. package/src/core/probe/security/classify.ts +341 -0
  106. package/src/core/probe/security/cleanup.ts +125 -0
  107. package/src/core/probe/security/detectors.ts +71 -0
  108. package/src/core/probe/security/digest.ts +104 -0
  109. package/src/core/probe/security/orchestrator.ts +398 -0
  110. package/src/core/probe/security/regression.ts +103 -0
  111. package/src/core/probe/security/types.ts +151 -0
  112. package/src/core/probe/security-probe-class.ts +8 -2
  113. package/src/core/probe/security-probe.ts +28 -1449
  114. package/src/core/probe/shared.ts +26 -0
  115. package/src/core/probe/webhooks-probe.ts +5 -7
  116. package/src/core/runner/assertions.ts +1 -1
  117. package/src/core/runner/executor.ts +3 -18
  118. package/src/core/runner/form-encode.ts +8 -18
  119. package/src/core/runner/http-client.ts +38 -1
  120. package/src/core/runner/preflight-vars.ts +19 -15
  121. package/src/core/runner/rate-limiter.ts +11 -29
  122. package/src/core/runner/run-kind.ts +7 -1
  123. package/src/core/runner/schema-validator.ts +2 -6
  124. package/src/core/runner/send-request.ts +11 -6
  125. package/src/core/runner/types.ts +6 -0
  126. package/src/core/setup-api.ts +53 -15
  127. package/src/core/severity/index.ts +0 -63
  128. package/src/core/spec/infer-schema.ts +102 -0
  129. package/src/core/spec/merge-specs.ts +156 -0
  130. package/src/core/spec/schema-from-runs.ts +117 -0
  131. package/src/core/spec/schema-overlay.ts +130 -0
  132. package/src/core/util/ajv.ts +13 -0
  133. package/src/core/util/headers.ts +9 -0
  134. package/src/core/util/url.ts +24 -0
  135. package/src/core/workspace/fixture-gap-report.ts +84 -0
  136. package/src/core/workspace/fixture-gaps.ts +71 -0
  137. package/src/core/workspace/root.ts +13 -11
  138. package/src/db/migrate.ts +2 -0
  139. package/src/db/migrations/0002_run_kind_request.sql +59 -0
  140. package/src/db/queries/collections.ts +2 -2
  141. package/src/db/queries/results.ts +88 -0
  142. package/src/db/queries/runs.ts +56 -2
  143. package/src/db/queries.ts +3 -0
  144. package/src/db/schema.ts +7 -7
  145. package/src/cli/commands/bootstrap.ts +0 -710
  146. package/src/core/anti-fp/bootstrap.ts +0 -34
  147. package/src/core/anti-fp/index.ts +0 -33
  148. package/src/core/anti-fp/registry.ts +0 -44
  149. package/src/core/anti-fp/rules/baseline-echo.ts +0 -74
  150. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +0 -52
  151. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +0 -38
  152. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +0 -35
  153. package/src/core/anti-fp/rules/schemathesis/index.ts +0 -24
  154. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +0 -53
  155. package/src/core/anti-fp/rules/subscription-gated/index.ts +0 -11
  156. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +0 -75
  157. package/src/core/anti-fp/types.ts +0 -68
  158. package/src/core/generator/create-body.ts +0 -89
@@ -29,16 +29,28 @@ For triage of a failing run — see `zond-triage`.
29
29
  - **NEVER `curl` or `wget`** — use `zond request <method> <url> --api <name>`
30
30
  for ad-hoc HTTP so it lands in the run DB and respects auth. Never
31
31
  shell-substitute the token by hand (`$(yq …)` is blocked by the sandbox).
32
+ `--body '{…}'` always sends `application/json`. For form-encoded APIs
33
+ (Stripe v1, classic webforms) pass `--form` — zond URL-encodes the
34
+ JSON body. Auto-detect from spec: if `requestBody.content` in
35
+ `.api-catalog.yaml` declares only `application/x-www-form-urlencoded`,
36
+ use `--form`; mixed content-types → pick by `Content-Type` header you
37
+ want to test.
32
38
  - **NEVER hardcode tokens** — put them in `apis/<name>/.secrets.yaml`
33
39
  (auto-gitignored), reference from `.env.yaml` as `@secret:auth_token`.
34
40
  Plain `${SHELL_VAR}` references also work. Tests read the resolved
35
41
  value as `{{auth_token}}`.
36
42
  - **NEVER read `.secrets.yaml` directly.** Use `zond doctor --api <name> --json`
37
- — reports `set | unset` and value length only.
43
+ — reports `set | unset` and value length only. To WRITE / rotate a secret
44
+ use `zond secrets set <key> <value> --api <name>` (writes a `.bak`, never
45
+ echoes the value, auto-strips a pasted `Bearer ` prefix — pass `--literal`
46
+ to keep it verbatim).
38
47
  - **NEVER edit `.api-*.yaml`** (catalog / resources / fixtures) by hand —
39
48
  regenerated by `zond refresh-api`. `.api-resources.local.yaml` IS the
40
- hand-editable / annotate-target overlay; safe to edit. `.env.yaml`
41
- values are editable (see ARV-114).
49
+ hand-editable / annotate-target overlay; safe to edit.
50
+ `.api-schema.local.yaml` is the response-schema overlay (ARV-176)
51
+ written by `refresh-api --merge-schema`, also survives refresh; edit by
52
+ re-merging a `patch.schema.json`, not by hand. `.env.yaml` values are
53
+ editable (see ARV-114).
42
54
  - **NEVER invent endpoints.** Only use entries from `.api-catalog.yaml`.
43
55
  - **NEVER run destructive ops on a shared / production org without
44
56
  `--dry-run` first.** Probes, `prepare-fixtures --apply`, `cleanup` hit
@@ -55,12 +67,15 @@ For triage of a failing run — see `zond-triage`.
55
67
  In autonomous / loop / audit-sweep mode (no user-in-the-loop), log to
56
68
  `api-bugs-<NN>.md`, continue the sweep — bailing on bug #1 forfeits #2..N.
57
69
  - **CRUD-run ≥80% 401/403 / `permission_denied` → `env_issue`, not bug.**
58
- Token scope issue. Confirm via `zond db diagnose <run-id> --env-only`;
59
- do not generate case-studies, do not `expect:`-mask.
70
+ Token scope issue. Confirm via `zond db diagnose <run-id> --json`
71
+ (401/403 dominate `by_recommended_action`); do not generate
72
+ case-studies, do not `expect:`-mask.
60
73
  - **MUST run `zond doctor --api <name> --missing-only` before generating
61
74
  fixtures or touching `.env.yaml`** — identifies unfilled keys early.
62
- - **Cascade cap is 8 passes** (`zond prepare-fixtures --cascade [--seed]`)
63
- never override without a written reason.
75
+ - **`prepare-fixtures` is single-pass, never POST-creates, and never
76
+ harvests values** (ARV-362) it verifies existing fixtures and reports
77
+ unfilled vars as gaps. Which record/field fills a slot is your call:
78
+ fill fixtures by hand (`fixtures add` / editing `.env.yaml`).
64
79
 
65
80
  ## Workspace artifact model
66
81
 
@@ -101,12 +116,13 @@ probes/ ← probe-emitted suites
101
116
  - "Generate should sync `.env.yaml`" is a rejected design (decision-7,
102
117
  m-17).
103
118
 
104
- ### Manual fixture-bootstrap (when `prepare-fixtures --seed` can't help)
119
+ ### Manual fixture-bootstrap (you fill every value)
105
120
 
106
- `prepare-fixtures --seed` requires a discoverable list endpoint or a
107
- postable create. Path-id ids that live only in a vendor dashboard
108
- (Stripe `cus_*`, GitHub PR numbers, Sentry issue ids) need a manual
109
- hand-off:
121
+ ARV-362: `prepare-fixtures` **reports** FK gaps it never harvests a
122
+ value. Every fixture is filled by hand: read the gap (empty list → create
123
+ a record first; non-empty pick a value; 404/403 auth/drift), then
124
+ supply it. Vendor-dashboard ids (Stripe `cus_*`, GitHub PR numbers,
125
+ Sentry issue ids) have no list endpoint at all — same hand-off:
110
126
 
111
127
  | Input you have | Command |
112
128
  |---|---|
@@ -116,7 +132,7 @@ hand-off:
116
132
  `--validate` GETs the spec's read-by-id endpoint and classifies the
117
133
  fixture as `live` / `stale` / `unknown` so dead ids don't silently
118
134
  break later runs (ARV-32). Both commands write to `apis/<name>/.env.yaml`
119
- with a `.env.yaml.bak` backup, mirroring `prepare-fixtures --apply`.
135
+ with a `.env.yaml.bak` backup.
120
136
 
121
137
  ### When to read which file
122
138
 
@@ -153,7 +169,9 @@ accept `--json` — use `--report json`.
153
169
 
154
170
  | User asked… | Start at |
155
171
  |---|---|
156
- | "audit this API", "test the whole API", "raise coverage" | Phase 0 → `zond audit --api <name>` (one-shot) OR walk Phase 0–8 |
172
+ | "smoke this API", "quick first pass", "demo / CI gate" | `zond audit --api <name>` (safe-mode breadth pass no live mutations) |
173
+ | "audit this API", "test the whole API", "raise coverage", "deep pass" | walk Phase 0–9 — audit covers smoke; depth (stateful, security, learn-apply) needs phase-level decisions |
174
+ | "full live audit against sandbox", "include mass-assignment/security probes" | `zond audit --api <name> --live --with-mass-assignment --with-security` (REQUIRES throwaway/sandbox account) |
157
175
  | "find bugs", "test for 5xx", "probe sweep" | Phase 0 → Phase 7 (Probes) |
158
176
  | "security only / SSRF / CRLF" | Phase 7.2 directly with `--dry-run` first |
159
177
  | "deep depth-checks", "SARIF", "stateful invariants" | Hand off → `zond-checks` |
@@ -181,32 +199,85 @@ If doctor reports stale → `zond refresh-api <name>`.
181
199
 
182
200
  ## Phase 1 — Fixture pack
183
201
 
184
- `zond prepare-fixtures` walks `.api-resources.yaml`, hits each owner
185
- list endpoint, fills `.env.yaml`. Always `--apply`; add `--cascade --seed`
186
- for empty workspaces.
202
+ `zond prepare-fixtures` is **single-pass and deterministic**: it walks
203
+ `.api-resources.yaml`, hits each owner list/read endpoint, and **reports**
204
+ which FK vars are unfilled (empty list → create one first; non-empty →
205
+ pick a value; 404/403 → auth/drift). It does **not** POST-create resources
206
+ and (ARV-362) **never harvests a value** — which record/field fills a slot
207
+ is your judgment. Fill every gap by hand (`fixtures add` / `.env.yaml`).
187
208
 
188
209
  ```bash
189
- zond prepare-fixtures --api <name> # dry-run preview
190
- zond prepare-fixtures --api <name> --apply # single-pass
191
- zond prepare-fixtures --api <name> --apply --cascade --seed # cascade + POST-create when list is empty
192
- zond prepare-fixtures --api <name> --refresh # = --verify --apply: drop stale ids, re-resolve
210
+ zond prepare-fixtures --api <name> # report gaps (never writes values)
211
+ zond prepare-fixtures --api <name> --apply # only unsets stale ids under --refresh; never harvests
212
+ zond prepare-fixtures --api <name> --refresh # = --verify --apply: drop (unset) stale 404 ids you refill
193
213
  ```
194
214
 
195
- `--seed` POSTs to create endpoints when a list returns `[]`, using a
196
- schema-derived body. Bracket-notation nested params (Stripe's
197
- `card[number]`, `items[0][price]`) are a known weak spot (ARV-196)
198
- some APIs will 400 here; **don't ask the user to fill missing ids
199
- manually first**, jump to Phase 2 (annotate seed_body) instead.
215
+ It also scans the generated suites and reports two named gap buckets
216
+ (warnings in text, `summary.fixtureGaps` in `--json`) report only, no
217
+ values invented, no auto-seed (ARV-349/350):
218
+ - **`unseededRoots`** required manifest vars that are empty, referenced by
219
+ a suite, and seeded by no step (e.g. `{{account}}` created in `crud-accounts`
220
+ but referenced uncaptured in `persons-crud`). These are chain-heads: one id
221
+ un-skips a whole dependent CRUD suite. Fix these first.
222
+ - **`undefinedVars`** — suite `{{vars}}` no manifest entry, capture, or param
223
+ produces (`{{bank_code}}`, `{{tax_id}}`). Supply by hand.
200
224
 
201
- If `--seed` converges but vars stay UNSET, the reason is outside the
202
- API path (email verify / paid plan / SCIM / TOS limits). Document the
203
- gap; don't loop further.
225
+ To fill `unseededRoots` autonomously author create-bodies, POST them, and
226
+ capture the returned ids hand off to **`zond-seed`** (agent reasons, zond
227
+ executes via `request … --capture`). It seeds only what the API self-serves
228
+ and reports external-input gaps honestly.
229
+
230
+ When a var stays UNSET after `--apply`, read the per-target reason in the
231
+ output, then fill the gap directly:
232
+
233
+ | Input you have | Command |
234
+ |---|---|
235
+ | Concrete id values | `zond fixtures add <var>=<id> [--validate] --apply` |
236
+ | Curl from devtools / dashboard | `pbpaste \| zond fixtures import --from-curl --apply` |
237
+ | Nothing yet (empty list) | Create the resource in the product UI / via API, then harvest its id with `fixtures add` |
238
+
239
+ Common reasons a var stays UNSET (all fixed outside prepare-fixtures):
240
+ - empty list on the target account — no record exists to harvest;
241
+ - the id lives only in a vendor dashboard (Stripe `cus_*`, GitHub PR
242
+ numbers, Sentry issue ids) with no discoverable list endpoint;
243
+ - the value is gated outside the API path (email verify / paid plan /
244
+ SCIM / TOS limits). Document the gap; don't loop.
204
245
 
205
246
  Editing `.env.yaml` by hand is the sanctioned fallback when the CLI
206
247
  cannot harvest a value (write-only ingest endpoints, SDK-only ids,
207
248
  slugs that live in user's project config). Touch values only — never
208
249
  add keys not in `.api-fixtures.yaml`.
209
250
 
251
+ ### Dynamic value functions in `.env.yaml` (ARV-190, m-21)
252
+
253
+ Stale hardcoded UUIDs / dates / idempotency keys pin existing rows or
254
+ expire mid-run. Use `#(funcName)` / `#(funcName(args))` references —
255
+ evaluated at load time, stable within one run:
256
+
257
+ | Token | Resolves to | Use for |
258
+ |---|---|---|
259
+ | `#(uuid)` | fresh UUID v4 (same value across all references in this run) | Idempotency-Key, request id |
260
+ | `#(uuidStable(<seed>))` | deterministic UUID from seed (sha-256 shaped as v4) | reproducible tests |
261
+ | `#(today)` | `YYYY-MM-DD` (UTC) | expiry-from dates |
262
+ | `#(todayPlus(N))` | today + N days (N may be negative) | expires_at, valid_until |
263
+ | `#(now)` | ISO 8601 timestamp | created_at hints |
264
+ | `#(unix)` | seconds since epoch | epoch timestamps |
265
+ | `#(alphanumeric(N))` | N random `[a-z0-9]` chars (default 8) | one-shot tokens |
266
+ | `#(env:VAR)` | `process.env.VAR` (alias for `${VAR}`) | secrets passed through |
267
+
268
+ ```yaml
269
+ # apis/foo/.env.yaml
270
+ idempotency_key: "#(uuid)" # same uuid across every step in this run
271
+ trial_expires: "#(todayPlus(30))" # 30 days from now
272
+ request_id: "req-#(uuid)" # embedded; shares uuid with above
273
+ ```
274
+
275
+ Resolution order: `${ENV}` → `#(...)` → `@secret:` → `@identity:`. Dynamic
276
+ functions run BEFORE secret/identity refs so a secret value that happens to
277
+ contain `#(...)` stays opaque (no double-expansion). Per-run cache means
278
+ `#(uuid)` referenced twice in the same `.env.yaml` returns the same value
279
+ — critical for idempotency-replay flows.
280
+
210
281
  ## Phase 2 — Annotate (NEW, m-20 / ARV-187)
211
282
 
212
283
  **Critical**: zond itself does NOT call any LLM. The flow is
@@ -218,7 +289,7 @@ your downstream check needs:
218
289
 
219
290
  | Aspect | Dump produces | Feeds into |
220
291
  |---|---|---|
221
- | `--seed-bodies` | request-body schema per resource | `prepare-fixtures --seed`, all stateful checks |
292
+ | `--seed-bodies` | request-body schema per resource | all stateful checks (create-body overlay) |
222
293
  | `--readback` | create+read pair (write-shape vs read-shape) | `cross_call_references` ignore_fields, write_to_read_map |
223
294
  | `--idempotency` | header candidates | `idempotency_replay` (header, scope, body_hash_fields) |
224
295
  | `--pagination` | list-endpoint cursor/page detection | `pagination_invariants` (type, cursor_param) |
@@ -260,7 +331,36 @@ to defaults and miss API-specific quirks (custom pagination params,
260
331
  non-standard lifecycle field names, write-only fields in create body).
261
332
 
262
333
  **For Stripe-style form-encoded APIs**: `--seed-bodies` is the single
263
- biggest win — it fixes `--seed` 400s for the bulk of resources.
334
+ biggest win — the create-body overlay it produces lets stateful checks
335
+ POST valid resources instead of 400-ing on schema-derived random bodies.
336
+
337
+ ### Agent-in-the-loop annotate (dump → write → apply, ARV-187 / 277 / 278-282)
338
+
339
+ zond does NOT infer annotations. YOU (the agent) read the spec slice and
340
+ write the overlay; zond only dumps raw material and validates/merges what
341
+ you produce.
342
+
343
+ ```bash
344
+ # 1. Dump the raw material for a resource — spec slice + expected YAML
345
+ # shape + (for seed-bodies) the last N seed-POST attempts so you see
346
+ # the error progression, not just the current 400 (ARV-277/278).
347
+ zond api annotate dump --api <name> --seed-bodies --only <res> \
348
+ --with-last-attempt --history 5
349
+
350
+ # 2. YOU write the overlay: read the dumped slice + attempt history,
351
+ # reason about the required create body / pagination / lifecycle /
352
+ # idempotency, and produce one YAML doc per resource. Edit
353
+ # .api-resources.local.yaml directly OR emit a file for apply.
354
+
355
+ # 3. Apply — zond validates your YAML (zod), renders a diff, and (with
356
+ # --yes) merges. With --gap-fill-only (ARV-281) your response can ONLY
357
+ # add missing fields — already-set blocks are protected from overwrite.
358
+ zond api annotate apply --api <name> --seed-bodies --input agent-out.yaml --gap-fill-only --yes
359
+
360
+ # 4. Refresh fixtures — drop (unset) stale (404) ids so they resurface as
361
+ # gaps; refill them by hand / `fixtures add` (discover never harvests).
362
+ zond prepare-fixtures --api <name> --refresh
363
+ ```
264
364
 
265
365
  ## Phase 3 — Generate tests
266
366
 
@@ -269,9 +369,15 @@ zond generate apis/<name>/spec.json --output apis/<name>/tests [--tag <spec-tag>
269
369
  zond check tests apis/<name>/tests
270
370
  ```
271
371
 
272
- `generate` fills bodies with `{{$randomString}}`. Format-strict APIs
273
- reject many — that's a **test-fix**, not a backend bug. Pair with
274
- Phase 1 fixtures + Phase 2 annotation.
372
+ `generate` fills bodies with typed generators (`{{$randomString}}` etc.).
373
+ Format-strict APIs reject many — that's a **test-fix**, not a backend bug.
374
+ Pair with Phase 1 fixtures + Phase 2 annotation.
375
+
376
+ Required FK / reference body fields (`*_id`, unresolved `*Code` like
377
+ `sequenceTypeCode`) are wired to `{{fixture}}` refs (ARV-45) — canonicalised
378
+ (`sequence_type_code`) and listed in `.api-fixtures.yaml` as `source: body-fk`.
379
+ Fill their real values once in `.env.yaml`; no more hand-editing generated
380
+ tests. Blank fixture → the var is unbound and dependent tests skip.
275
381
 
276
382
  If a CRUD chain you expected is missing, run
277
383
  `zond generate <spec> --explain` — diagnostic table shows every POST
@@ -318,6 +424,13 @@ zond session end
318
424
  invisible without it. `schema_violation` failures are real backend bugs;
319
425
  treat them like 5xx.
320
426
 
427
+ **After any `zond run` with failures → delegate to `zond-triage`.** Do
428
+ NOT parse `runs/run-NN.json` with `jq` by hand. The triage skill reads
429
+ `zond db diagnose --json` (which already buckets by `recommended_action`
430
+ in `data.by_recommended_action`, ARV-228) and routes by closed enum —
431
+ re-implementing that logic with prose heuristics drifts. Only fall back
432
+ to manual CLI queries when the failure is outside the example slice.
433
+
321
434
  Rate limit: `zond run` defaults to an adaptive limiter (no-op until
322
435
  `RateLimit-*` headers appear). Pass `--rate-limit <N>` for a hard cap;
323
436
  `--sequential` for old binaries.
@@ -363,29 +476,47 @@ After Phase 2 annotation is applied, run the cross-call invariants.
363
476
  See `zond-checks` for per-check semantics.
364
477
 
365
478
  ```bash
366
- zond checks run --api <name> --check stateful --report ndjson
479
+ zond checks run --api <name> --check stateful --report ndjson --live # CRUD create-chains need --live
367
480
  ```
368
481
 
369
- 5 m-20 stateful checks:
482
+ **Safe by default (ARV-299)**: without `--live`, `checks run` skips the
483
+ stateful CRUD create-chains (`ensure_resource_availability`,
484
+ `use_after_free`) — they'd POST real resources. Read-only stateful checks
485
+ (pagination, observation-mode lifecycle, cross-call GET diff) still run.
486
+ Add `--live` **only against a throwaway/sandbox account** to exercise the
487
+ create-chains.
488
+
489
+ `--check stateful` expands to the state-machine set (ARV-325):
370
490
  - `cross_call_references` — POST→GET drift (uses readback_diff overlay)
371
491
  - `idempotency_replay` — bit-identical replay on `Idempotency-Key`
372
492
  - `pagination_invariants` — `?limit=N` + `?after=last_id` non-overlap
373
493
  - `lifecycle_transitions` — declared state-machine validity
494
+ - `use_after_free`, `ensure_resource_availability` — CRUD lifecycle
495
+ - `cursor_boundary_fuzzing` — list-cursor edge values
374
496
  - `webhooks` (recipe-based, see `docs/recipes/webhook-receiver.md`)
375
497
 
498
+ `ignored_auth` / `open_cors_on_sensitive` are **not** part of the alias —
499
+ they're auth/security checks that roughly triple run time on wide APIs.
500
+ Run them by explicit id: `--check ignored_auth,open_cors_on_sensitive`.
501
+
376
502
  **Iron rule**: don't run `--check stateful` without prior `api annotate`
377
503
  review. Defaults catch the obvious; quirks need declared config.
378
504
 
379
505
  ## Phase 7 — Proactive bug hunting (probes)
380
506
 
381
507
  ```bash
382
- zond probe static --api <name> # validation + methods (defaults)
383
- zond probe mass-assignment --api <name> --emit-tests apis/<name>/probes/mass-assignment
384
- zond probe security ssrf,crlf,open-redirect,prompt-injection --api <name> \
508
+ zond probe static --api <name> # validation + methods (defaults; generator, always safe)
509
+ zond probe mass-assignment --api <name> --live --emit-tests apis/<name>/probes/mass-assignment
510
+ zond probe security ssrf,crlf,open-redirect --api <name> --live \
385
511
  --emit-tests apis/<name>/probes/security
386
512
  zond run apis/<name>/probes --report json
387
513
  ```
388
514
 
515
+ **Safe by default (ARV-299)**: `probe mass-assignment` / `probe security`
516
+ without `--live` plan only (no live attack payloads) — same as `--dry-run`.
517
+ Add `--live` **only against a throwaway/sandbox account** to send probes.
518
+ `probe static` just generates suites, so it's always safe.
519
+
389
520
  Flag: 5xx on null/empty/wrong-type body → missing validation. 2xx on
390
521
  undeclared method → contract drift. `is_admin: true` echoed in response
391
522
  → HIGH from mass-assignment.
@@ -416,13 +547,27 @@ round-trip GET → **HIGH**, file via `report bundle --include case-study`.
416
547
  ### 7.2 — Security probes
417
548
 
418
549
  ```bash
419
- zond probe security ssrf,crlf --api <name> --dry-run # first run: which (endpoint, field)
420
- zond probe security ssrf,crlf,open-redirect,prompt-injection --api <name>
550
+ zond probe security ssrf,crlf --api <name> --dry-run # first run: which (endpoint, field) — same as default --safe
551
+ zond probe security ssrf,crlf,open-redirect --api <name> --live # send, sandbox only
421
552
  ```
422
553
 
554
+ **Targeting** (two filters; an endpoint must pass BOTH to be planned):
555
+
556
+ 1. **Has a JSON request body** — GET-only routes are skipped with
557
+ `no-body`. The probes work by mutating a body field, so read-only
558
+ endpoints have nothing to attack.
559
+ 2. **Has ≥1 field name matching the requested class detectors** — if
560
+ no field matches, the endpoint is skipped with `no-matched-field`.
561
+
423
562
  Field autodetection: SSRF (`*_url`/`webhook`/`callback`/`format: uri`),
424
563
  CRLF (`subject`/`*_prefix`/`name`), open-redirect (`redirect`/`next`).
425
564
 
565
+ If `--dry-run` reports `0 planned / 0 skipped / 0 total` with a narrow
566
+ `--include`, the scope likely captured only GET routes — broaden the
567
+ filter or drop `--include` to see the per-endpoint skip reasons. With
568
+ no `--include`, the dry-run shows `no-body` / `no-matched-field` next
569
+ to each skipped endpoint so the gap is obvious.
570
+
426
571
  Baseline-OK request first; if baseline ≠ 2xx → `INCONCLUSIVE-BASELINE`,
427
572
  attacks skipped (kills 5×404 noise on scope-locked endpoints).
428
573
 
@@ -448,16 +593,34 @@ seeded-fixture endpoints).
448
593
  ## Phase 8 — Coverage + spec drift
449
594
 
450
595
  ```bash
451
- zond coverage --api <name> --union session # default for an audit window
596
+ zond coverage --api <name> --union session # default: dual-metric (test + audit)
452
597
  zond coverage --api <name> --union since:1h # last hour
453
598
  zond coverage --api <name> --union tag:smoke # tag-filtered
454
- zond coverage --api <name> --fail-on-coverage 50 # CI gate (use hit-coverage)
599
+ zond coverage --api <name> --scope audit --union session # only "did zond touch the API"
600
+ zond coverage --api <name> --scope test --union session # legacy single block
601
+ zond coverage --api <name> --fail-on-coverage 50 # CI gate (gates pass-coverage)
602
+ ```
603
+
604
+ **Dual-metric output (ARV-265).** `zond coverage` prints two blocks side-by-side by default:
605
+
606
+ - **test-coverage** — runs produced by `zond run` only (`run_kind` ∈ {regular, probe}). Has two sub-metrics:
607
+ - `pass-coverage`: endpoint had a passing 2xx (strict; CI gate via `--fail-on-coverage`)
608
+ - `hit-coverage`: endpoint received any response (loose; breadth proxy)
609
+ Three-bucket JSON: `covered2xx`, `coveredButNon2xx`, `unhit`.
610
+ - **audit-coverage** — any HTTP touch from any producer (`zond run` + `checks run` + `probe` + `request` + `prepare-fixtures`). Use this to answer "did the scan reach the API at all?" — typical safe-mode runs without `zond run` still report >50% on a 1k-endpoint spec from `checks run` alone.
611
+
612
+ Source breakdown (`--scope audit` text or `data.audit_coverage.by_source` JSON):
613
+
614
+ ```
615
+ audit-coverage: 619/1184 endpoints (52%, 1500 HTTP touches)
616
+ by source:
617
+ check 619 endpoints, 1500 events
618
+ regular 0 endpoints, 0 events
619
+ probe 0 endpoints, 0 events
620
+ request 3 endpoints, 3 events
455
621
  ```
456
622
 
457
- **Two metrics**: `pass-coverage` (covered2xx — strict, CI gate) vs
458
- `hit-coverage` (any stored result, breadth proxy). Three-bucket JSON:
459
- `covered2xx`, `coveredButNon2xx` (hit but failed — usually fixture gap,
460
- not API bug), `unhit`.
623
+ Producers opt out via `ZOND_CHECKS_PERSIST=0`.
461
624
 
462
625
  **Quality signals to gate CI on** (not raw pass-coverage):
463
626
 
@@ -474,8 +637,8 @@ not API bug), `unhit`.
474
637
 
475
638
  ```bash
476
639
  zond report export <run-id> # default: triage/<api>/run-<id>/
477
- zond report bundle 135..142 -o triage/sweep/ # all artefacts (default): case-study + html + diagnose + index.md
478
- zond report bundle <run-id> --include case-study,diagnose # subset only (drop html)
640
+ zond report bundle 135..142 -o triage/sweep/ # all artefacts (default): case-study + export + diagnose + index.md
641
+ zond report bundle <run-id> --include case-study,diagnose # subset only (drop export)
479
642
  zond report bundle --session <id> -o triage/session/ # group by session
480
643
  ```
481
644
 
@@ -489,29 +652,69 @@ Offer this proactively after a run surfaces `definitely_bug`
489
652
  (5xx / schema violation / mass-assignment 2xx). Skip for `env_issue` /
490
653
  `quirk`.
491
654
 
492
- ## One-shot full audit
655
+ ## One-shot smoke audit — `zond audit`
493
656
 
494
- `zond audit --api <name>` runs the whole pipeline in one shot:
495
- prepare-fixtures → generate → probe static → session-wrapped run on
496
- tests+probes → coverage → `audit-report.html`. Each stage prints
497
- `==> Stage N/M: <name>`; stage failure doesn't stop the rest; final
498
- exit 1 if any stage failed.
657
+ **`zond audit` is the breadth pass, not "full audit".** It covers the
658
+ *smoke + coverage* slice — prepare-fixtures → generate → probe static
659
+ → session-wrapped run on tests+probes → coverage → `audit-report.html`.
660
+ Depth (stateful checks, security probes against R18 evidence-gates,
661
+ m-20 annotate, `--learn-apply` iteration) is the **skill's job, not
662
+ audit's**: those stages need decisions audit can't make
663
+ (annotate-before-stateful, scoping after auth-cluster, evidence
664
+ preconditions). Use audit for: CI smoke, demo, "first 5 minutes on a
665
+ new API". For a real depth campaign, walk Phase 0–9 from this skill.
499
666
 
500
667
  ```bash
501
- zond audit --api <name> --dry-run # plan
502
- zond audit --api <name> # minimal pipeline
503
- zond audit --api <name> --seed # add prepare-fixtures --cascade --seed
504
- zond audit --api <name> --with-mass-assignment --with-security
668
+ zond audit --api <name> --dry-run # print stage plan
669
+ zond audit --api <name> # safe mode (default) — no live probes
670
+ zond audit --api <name> --live --with-mass-assignment --with-security # full live pipeline (throwaway/sandbox only)
505
671
  zond audit --api <name> --out reports/audit-<name>.html
506
672
  ```
507
673
 
674
+ **ARV-264 safe vs live decision matrix.** Default is `--safe` (implicit;
675
+ no flag needed). `--live` is the opt-in for traffic that mutates the
676
+ target API.
677
+
678
+ | Concern | `--safe` (default) | `--live` |
679
+ |---|---|---|
680
+ | `prepare-fixtures` mode | single-pass path-FK discovery (GET) | same — single-pass, never POST-creates |
681
+ | `probe mass-assignment` | skipped even if `--with-mass-assignment` set, with a warning | runs when `--with-mass-assignment` set |
682
+ | `probe security` (SSRF/CRLF/redirect) | skipped even if `--with-security` set, with a warning | runs when `--with-security` set |
683
+ | `probe static` (validation+methods) | always runs — pure spec walk, no live traffic | same |
684
+ | `run tests / probes` | runs against the live API but only generated GET-shape suites by default | same |
685
+
686
+ **Pre-flight warnings (printed before any subprocess fires):** missing
687
+ `auth_token` when the spec declares securitySchemes (probes will skip
688
+ with 401 and the report will read misleadingly green); no
689
+ securitySchemes declared in the spec (open API or incomplete spec —
690
+ auth-related checks will skip).
691
+
692
+ **Rule of thumb.** Unknown-scope PAT against a production API → `--safe`
693
+ (default). Throwaway account / sandbox tenant → `--live` is fine. Never
694
+ run `--live` against shared/production data on first contact.
695
+
696
+ Each stage prints `==> Stage N/M: <name>` and a completion line
697
+ `└─ OK · Ns` / `FAIL (exit K) · Ns` / `SKIPPED (reason)`. Exit 1 if
698
+ any non-coverage stage failed.
699
+
508
700
  `generate` skipped via mtime when `tests/` is fresher than `spec.json`
509
701
  (pass `--force` to override).
510
702
 
511
- **Gotchas**:
512
- - Wraps its own session closes any prior `session start` silently.
513
- - Can exit 0 on failed stages (parse stdout `Warning: N failed`).
514
- - `audit-report.html` path not echoed by default pass `--out`.
703
+ **Session reuse (ARV-65)**: if `.zond/current-session` is already set
704
+ when audit starts (i.e. user ran `zond session start` first), audit
705
+ reuses that session `session-start` and `session-end` stages skip
706
+ with reason `reusing active session <id>`. The user's session stays
707
+ active after audit returns; runs inside audit are stamped with the
708
+ outer `session_id` and roll up under the user's `coverage --union
709
+ session` later.
710
+
711
+ ARV-158: when any run inside the audit session has failures, the
712
+ generated `audit-report.html` embeds a "Failures by run" section with
713
+ collapsible `<details>` per run — `by_recommended_action` buckets +
714
+ first example (method, path, status, reason) + concrete drill-down
715
+ commands (`zond db diagnose --run-id N --json`, `zond report export
716
+ N`). Triage starts from the rendered HTML; you only fall back to CLI
717
+ queries when you need fields outside the example slice.
515
718
 
516
719
  When NOT to use audit: narrow asks ("fix run X", "why this endpoint
517
720
  500s") — walk phases.
@@ -582,9 +785,16 @@ verifying a fix** — small focused work. For breadth, use Phase 3
582
785
  zond db runs --limit 5 --json
583
786
  zond db diagnose <run-id> --json # grouped by root_cause
584
787
  zond db run <id> --status 500 --json
585
- zond db compare <idA> <idB> --json # regression diff
788
+ zond db compare <idA> <idB> --json # regression diff + field-level body diff (.data.body_changes[], ARV-339)
789
+ zond db run <id> --report yaml # run snapshot as YAML (ARV-338) — keep/diff as text
586
790
  ```
587
791
 
792
+ > **db --json envelope shape** (consistent across siblings): the array
793
+ > always lives under `data.<plural>`, not directly under `data`.
794
+ > `db runs` → `.data.runs[]`, `db collections` → `.data.collections[]`,
795
+ > `db run <id> --status …` → `.data.results[]`. Top-level totals on runs
796
+ > are at `runs[].total/passed/failed`, not under a `.summary` wrapper.
797
+
588
798
  `recommended_action` enum (closed):
589
799
  - `report_backend_bug` — STOP, surface to user
590
800
  - `fix_test_logic` — edit the YAML
@@ -1,7 +1,7 @@
1
1
  # zond workspace config
2
2
  #
3
3
  # The presence of this file marks the workspace root for `zond` walk-up
4
- # resolution (zond.db, apis/<name>/, .zond/current-api are anchored here).
4
+ # resolution (.zond/zond.db, apis/<name>/, .zond/current-api are anchored here).
5
5
 
6
6
  version: 1
7
7
 
@@ -1,19 +1,18 @@
1
1
  /**
2
- * `zond prepare-fixtures` — unified fixture-pack command.
2
+ * `zond prepare-fixtures` — single-pass fixture-pack command.
3
3
  *
4
- * Consolidates the former `discover` and `bootstrap` (TASK-299, m-13 D):
4
+ * Deterministically fills `apis/<name>/.env.yaml` with FK ids that can be
5
+ * resolved from list/read endpoints in a single discover pass, and reports
6
+ * which fixtures are still missing so an agent (or the user) can fill the
7
+ * gaps by hand (`fixtures add` / editing `.env.yaml`).
5
8
  *
6
9
  * - default → single-pass discover (auto-fill FK ids
7
10
  * from list endpoints).
8
- * - --cascade multi-pass cascade (former bootstrap).
9
- * - --seed → cascade + POST-create when discover misses
10
- * (implies --cascade).
11
- * - --verify / --refresh → revalidate fixtures via read-by-id
12
- * (former `discover --verify/--refresh`).
11
+ * - --verify / --refresh revalidate fixtures via read-by-id.
13
12
  *
14
- * The imperative cores (`discoverCommand`, `bootstrapCommand`) live in
15
- * the original modules and are still consumed directly by tests. This
16
- * module only owns the CLI surface.
13
+ * The imperative core (`discoverCommand`) lives in the original module and
14
+ * is still consumed directly by tests. This module only owns the CLI
15
+ * surface.
17
16
  */
18
17
 
19
18
  import type { Command } from "commander";
@@ -23,7 +22,6 @@ import { getDb } from "../../db/schema.ts";
23
22
  import { findCollectionByNameOrId } from "../../db/queries.ts";
24
23
  import { printError } from "../output.ts";
25
24
  import { discoverCommand } from "./discover.ts";
26
- import { bootstrapCommand } from "./bootstrap.ts";
27
25
  import { loadEnvMeta } from "../../core/parser/variables.ts";
28
26
  import { resolveTimeoutMs } from "../../core/workspace/config.ts";
29
27
  import { getApi, MISSING_API_MESSAGE } from "../util/api-context.ts";
@@ -32,9 +30,9 @@ export function registerPrepareFixtures(program: Command): void {
32
30
  program
33
31
  .command("prepare-fixtures")
34
32
  .description(
35
- "Auto-fill apis/<name>/.env.yaml single-pass discover by default, " +
36
- "or `--cascade` for the multi-pass discover+seed flow (replaces the legacy " +
37
- "`discover` and `bootstrap` commands; TASK-299).",
33
+ "Auto-fill apis/<name>/.env.yaml in a single discover pass: resolve FK " +
34
+ "ids from list/read endpoints and report which fixtures are still " +
35
+ "missing (fill those by hand via `fixtures add` / editing .env.yaml).",
38
36
  )
39
37
  // Not `requiredOption` — the value can also come from the program-level
40
38
  // --api flag (parsed by program.ts and mirrored into ZOND_API_GLOBAL),
@@ -46,13 +44,9 @@ export function registerPrepareFixtures(program: Command): void {
46
44
  .option("--api-dir <path>", "Override apis/<name>/ root (defaults to the collection's base_dir)")
47
45
  .option("--env <path>", "Override .env.yaml path (defaults to <api-dir>/.env.yaml)")
48
46
  .option("--apply", "Write discovered values to .env.yaml (with .env.yaml.bak backup). Default: dry-run.")
49
- .option("--cascade", "Multi-pass cascade discover (former `bootstrap`). Required for --seed / --force / --max-passes.")
50
- .option("--seed", "POST-create resources when discover can't find an existing record (implies --cascade)")
51
- .option("--force", "Re-discover/re-seed even if a fixture is already filled (cascade only)")
52
- .option("--verify", "GET each fixture's read-by-id endpoint and classify live/stale/unknown (single-pass only). Combine with --apply (or use --refresh) to drop stale fixtures and re-resolve them. (TASK-281)")
53
- .option("--refresh", "Shortcut for --verify --apply (single-pass only). (TASK-281)")
47
+ .option("--verify", "GET each fixture's read-by-id endpoint and classify live/stale/unknown. Combine with --apply (or use --refresh) to drop stale fixtures and re-resolve them. (TASK-281)")
48
+ .option("--refresh", "Shortcut for --verify --apply. (TASK-281)")
54
49
  .option("--timeout <ms>", "Per-request timeout in ms (overrides apis/<name>/.env.yaml `timeoutMs` and zond.config.yml `defaults.timeout_ms`; default 30000)", parsePositiveInt("--timeout"))
55
- .option("--max-passes <n>", "Cap on cascade passes (default 8; cascade only)", parsePositiveInt("--max-passes"))
56
50
  .action(async (opts, cmd: Command) => {
57
51
  // ARV-53: --api resolution lives in cli/util/api-context.ts —
58
52
  // local opt > ancestor opt > ZOND_API_GLOBAL/ZOND_API/.zond/current-api.
@@ -64,22 +58,9 @@ export function registerPrepareFixtures(program: Command): void {
64
58
  }
65
59
  opts.api = apiName;
66
60
 
67
- const cascade = opts.cascade === true || opts.seed === true;
68
61
  const refresh = opts.refresh === true;
69
62
  const verify = opts.verify === true || refresh;
70
63
 
71
- // Flag combos that don't make sense — fail fast with a clear hint.
72
- if (cascade && verify) {
73
- printError("--verify / --refresh are single-pass options; drop --cascade/--seed or drop --verify.");
74
- process.exitCode = 2;
75
- return;
76
- }
77
- if (!cascade && (opts.force === true || typeof opts.maxPasses === "number")) {
78
- printError("--force / --max-passes only apply with --cascade (or --seed).");
79
- process.exitCode = 2;
80
- return;
81
- }
82
-
83
64
  const resolved = resolveSpecArg(undefined, opts.api, opts.db);
84
65
  if ("error" in resolved) { printError(resolved.error); process.exitCode = 2; return; }
85
66
 
@@ -100,25 +81,6 @@ export function registerPrepareFixtures(program: Command): void {
100
81
  } catch { /* meta is best-effort */ }
101
82
  const timeoutMs = resolveTimeoutMs(opts.timeout, envTimeout);
102
83
 
103
- if (cascade) {
104
- process.exitCode = await bootstrapCommand({
105
- specPath: resolved.spec,
106
- apiDir,
107
- envPath: opts.env,
108
- apply: opts.apply === true,
109
- seed: opts.seed === true,
110
- force: opts.force === true,
111
- timeoutMs,
112
- maxPasses: opts.maxPasses,
113
- json: globalJson(cmd),
114
- // ARV-205 (R10/F6, R13/F19): surface the user-facing command name
115
- // in the JSON envelope. Without this the user sees command="bootstrap"
116
- // even though they typed `zond prepare-fixtures …`.
117
- commandName: "prepare-fixtures",
118
- });
119
- return;
120
- }
121
-
122
84
  process.exitCode = await discoverCommand({
123
85
  specPath: resolved.spec,
124
86
  apiDir,