@kirrosh/zond 0.22.0 → 0.26.0

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 (282) hide show
  1. package/CHANGELOG.md +811 -0
  2. package/README.md +59 -6
  3. package/package.json +9 -7
  4. package/src/CLAUDE.md +112 -0
  5. package/src/cli/argv.ts +122 -0
  6. package/src/cli/commands/add-api.ts +146 -0
  7. package/src/cli/commands/api/annotate/idempotency.ts +59 -0
  8. package/src/cli/commands/api/annotate/index.ts +880 -0
  9. package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
  10. package/src/cli/commands/api/annotate/overlay.ts +206 -0
  11. package/src/cli/commands/api/annotate/pagination.ts +64 -0
  12. package/src/cli/commands/api/annotate/prompts.ts +220 -0
  13. package/src/cli/commands/api/annotate/readback.ts +58 -0
  14. package/src/cli/commands/api/annotate/resources.ts +91 -0
  15. package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
  16. package/src/cli/commands/audit.ts +786 -0
  17. package/src/cli/commands/catalog.ts +35 -0
  18. package/src/cli/commands/check.ts +361 -0
  19. package/src/cli/commands/checks.ts +1072 -0
  20. package/src/cli/commands/ci-init.ts +43 -0
  21. package/src/cli/commands/clean.ts +212 -0
  22. package/src/cli/commands/cleanup.ts +236 -0
  23. package/src/cli/commands/completions.ts +16 -0
  24. package/src/cli/commands/coverage.ts +823 -132
  25. package/src/cli/commands/db.ts +486 -12
  26. package/src/cli/commands/describe.ts +37 -2
  27. package/src/cli/commands/discover.ts +1356 -0
  28. package/src/cli/commands/doctor.ts +661 -0
  29. package/src/cli/commands/fixtures.ts +402 -0
  30. package/src/cli/commands/generate.ts +438 -47
  31. package/src/cli/commands/init/bootstrap.ts +34 -2
  32. package/src/cli/commands/{init.ts → init/index.ts} +99 -5
  33. package/src/cli/commands/init/skills.ts +99 -3
  34. package/src/cli/commands/init/templates/agents.md +77 -64
  35. package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
  36. package/src/cli/commands/init/templates/skills/zond-checks.md +621 -0
  37. package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
  38. package/src/cli/commands/init/templates/skills/zond-triage.md +272 -0
  39. package/src/cli/commands/init/templates/skills/zond.md +802 -125
  40. package/src/cli/commands/init/templates/zond-config.yml +8 -9
  41. package/src/cli/commands/prepare-fixtures.ts +97 -0
  42. package/src/cli/commands/probe/_seed-bodies.ts +52 -0
  43. package/src/cli/commands/probe/mass-assignment.ts +594 -0
  44. package/src/cli/commands/probe/security.ts +537 -0
  45. package/src/cli/commands/probe/static.ts +255 -0
  46. package/src/cli/commands/probe/webhooks.ts +163 -0
  47. package/src/cli/commands/probe.ts +535 -0
  48. package/src/cli/commands/reference.ts +87 -0
  49. package/src/cli/commands/refresh-api.ts +227 -0
  50. package/src/cli/commands/remove-api.ts +150 -0
  51. package/src/cli/commands/report-bundle.ts +310 -0
  52. package/src/cli/commands/report.ts +241 -0
  53. package/src/cli/commands/request.ts +495 -4
  54. package/src/cli/commands/run.ts +870 -53
  55. package/src/cli/commands/schema-from-runs.ts +128 -0
  56. package/src/cli/commands/secrets.ts +133 -0
  57. package/src/cli/commands/session.ts +244 -0
  58. package/src/cli/commands/use.ts +18 -1
  59. package/src/cli/index.ts +20 -3
  60. package/src/cli/json-envelope.ts +92 -3
  61. package/src/cli/json-schemas.ts +314 -0
  62. package/src/cli/output.ts +17 -1
  63. package/src/cli/program.ts +199 -635
  64. package/src/cli/resolve.ts +105 -0
  65. package/src/cli/safe-live.ts +24 -0
  66. package/src/cli/status-filter.ts +114 -0
  67. package/src/cli/util/api-context.ts +85 -0
  68. package/src/cli/version.ts +5 -0
  69. package/src/core/audit/persist.ts +183 -0
  70. package/src/core/checks/budget.ts +59 -0
  71. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  72. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  73. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  74. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  75. package/src/core/checks/checks/cross_call_references.ts +147 -0
  76. package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
  77. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  78. package/src/core/checks/checks/idempotency_replay.ts +242 -0
  79. package/src/core/checks/checks/ignored_auth.ts +254 -0
  80. package/src/core/checks/checks/index.ts +68 -0
  81. package/src/core/checks/checks/lifecycle_transitions.ts +416 -0
  82. package/src/core/checks/checks/missing_required_header.ts +40 -0
  83. package/src/core/checks/checks/negative_data_rejection.ts +148 -0
  84. package/src/core/checks/checks/not_a_server_error.ts +35 -0
  85. package/src/core/checks/checks/open_cors_on_sensitive.ts +160 -0
  86. package/src/core/checks/checks/pagination_invariants.ts +419 -0
  87. package/src/core/checks/checks/positive_data_acceptance.ts +33 -0
  88. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  89. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  90. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  91. package/src/core/checks/checks/status_code_conformance.ts +132 -0
  92. package/src/core/checks/checks/unsupported_method.ts +63 -0
  93. package/src/core/checks/checks/use_after_free.ts +78 -0
  94. package/src/core/checks/index.ts +30 -0
  95. package/src/core/checks/mode.ts +82 -0
  96. package/src/core/checks/recommended-action.ts +68 -0
  97. package/src/core/checks/registry.ts +78 -0
  98. package/src/core/checks/runner.ts +1461 -0
  99. package/src/core/checks/sarif.ts +230 -0
  100. package/src/core/checks/spec-findings.ts +308 -0
  101. package/src/core/checks/stateful.ts +121 -0
  102. package/src/core/checks/types.ts +305 -0
  103. package/src/core/checks/zond-extensions.ts +73 -0
  104. package/src/core/classifier/recommended-action.ts +251 -0
  105. package/src/core/context/current.ts +22 -6
  106. package/src/core/context/session.ts +78 -0
  107. package/src/core/coverage/loader.ts +216 -0
  108. package/src/core/coverage/reasons.ts +300 -0
  109. package/src/core/diagnostics/db-analysis.ts +293 -59
  110. package/src/core/diagnostics/failure-class.ts +140 -0
  111. package/src/core/diagnostics/failure-hints.ts +88 -89
  112. package/src/core/diagnostics/spec-pointer.ts +99 -0
  113. package/src/core/diagnostics/suggested-fixes.ts +155 -0
  114. package/src/core/exporter/case-study/index.ts +270 -0
  115. package/src/core/exporter/curl.ts +40 -0
  116. package/src/core/exporter/exporter.ts +48 -0
  117. package/src/core/exporter/html-report/escape.ts +24 -0
  118. package/src/core/exporter/html-report/index.ts +479 -0
  119. package/src/core/exporter/html-report/script.ts +100 -0
  120. package/src/core/exporter/html-report/styles.ts +408 -0
  121. package/src/core/generator/chunker.ts +38 -19
  122. package/src/core/generator/coverage-phase.ts +0 -0
  123. package/src/core/generator/data-factory.ts +586 -22
  124. package/src/core/generator/describe.ts +1 -1
  125. package/src/core/generator/fixtures-builder.ts +332 -0
  126. package/src/core/generator/index.ts +5 -5
  127. package/src/core/generator/openapi-reader.ts +135 -7
  128. package/src/core/generator/path-param-disambig.ts +140 -0
  129. package/src/core/generator/resources-builder.ts +898 -0
  130. package/src/core/generator/schema-utils.ts +33 -3
  131. package/src/core/generator/serializer.ts +103 -13
  132. package/src/core/generator/suite-generator.ts +583 -122
  133. package/src/core/generator/types.ts +14 -0
  134. package/src/core/identity/identity-file.ts +0 -0
  135. package/src/core/lint/affects.ts +28 -0
  136. package/src/core/lint/config.ts +96 -0
  137. package/src/core/lint/format.ts +42 -0
  138. package/src/core/lint/index.ts +94 -0
  139. package/src/core/lint/reporter.ts +128 -0
  140. package/src/core/lint/rules/consistency.ts +158 -0
  141. package/src/core/lint/rules/heuristics.ts +97 -0
  142. package/src/core/lint/rules/strictness.ts +109 -0
  143. package/src/core/lint/types.ts +96 -0
  144. package/src/core/lint/walker.ts +248 -0
  145. package/src/core/meta/meta-store.ts +6 -73
  146. package/src/core/output/README.md +73 -0
  147. package/src/core/output/index.ts +13 -0
  148. package/src/core/output/run.ts +91 -0
  149. package/src/core/output/types.ts +122 -0
  150. package/src/core/parser/dynamic-values.ts +160 -0
  151. package/src/core/parser/env-interpolation.ts +104 -0
  152. package/src/core/parser/filter.ts +57 -0
  153. package/src/core/parser/schema.ts +129 -4
  154. package/src/core/parser/types.ts +19 -1
  155. package/src/core/parser/variables.ts +0 -0
  156. package/src/core/parser/yaml-parser.ts +58 -12
  157. package/src/core/probe/bootstrap.ts +34 -0
  158. package/src/core/probe/dry-run-envelope.ts +61 -0
  159. package/src/core/probe/mass-assignment/classify.ts +175 -0
  160. package/src/core/probe/mass-assignment/cleanup.ts +52 -0
  161. package/src/core/probe/mass-assignment/digest.ts +114 -0
  162. package/src/core/probe/mass-assignment/orchestrator.ts +459 -0
  163. package/src/core/probe/mass-assignment/regression.ts +141 -0
  164. package/src/core/probe/mass-assignment/suspects.ts +92 -0
  165. package/src/core/probe/mass-assignment/types.ts +135 -0
  166. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  167. package/src/core/probe/mass-assignment-probe.ts +27 -0
  168. package/src/core/probe/mass-assignment-template.ts +240 -0
  169. package/src/core/probe/method-probe.ts +43 -76
  170. package/src/core/probe/method-shared.ts +69 -0
  171. package/src/core/probe/negative-probe.ts +183 -149
  172. package/src/core/probe/orphan-tracker.ts +188 -0
  173. package/src/core/probe/path-discovery.ts +439 -0
  174. package/src/core/probe/probe-harness.ts +119 -0
  175. package/src/core/probe/registry.ts +89 -0
  176. package/src/core/probe/runner.ts +136 -0
  177. package/src/core/probe/security/baseline.ts +174 -0
  178. package/src/core/probe/security/classify.ts +341 -0
  179. package/src/core/probe/security/cleanup.ts +125 -0
  180. package/src/core/probe/security/detectors.ts +71 -0
  181. package/src/core/probe/security/digest.ts +104 -0
  182. package/src/core/probe/security/orchestrator.ts +398 -0
  183. package/src/core/probe/security/regression.ts +103 -0
  184. package/src/core/probe/security/types.ts +151 -0
  185. package/src/core/probe/security-probe-class.ts +207 -0
  186. package/src/core/probe/security-probe.ts +32 -0
  187. package/src/core/probe/shared.ts +531 -0
  188. package/src/core/probe/static-probe-class.ts +125 -0
  189. package/src/core/probe/types.ts +165 -0
  190. package/src/core/probe/verdict-aggregator.ts +33 -0
  191. package/src/core/probe/webhooks-probe.ts +282 -0
  192. package/src/core/reporter/console.ts +41 -2
  193. package/src/core/reporter/index.ts +2 -3
  194. package/src/core/reporter/json.ts +11 -1
  195. package/src/core/reporter/junit.ts +27 -12
  196. package/src/core/reporter/ndjson.ts +37 -0
  197. package/src/core/reporter/types.ts +3 -0
  198. package/src/core/runner/assertions.ts +59 -2
  199. package/src/core/runner/async-pool.ts +108 -0
  200. package/src/core/runner/auth-path.ts +8 -0
  201. package/src/core/runner/ci-context.ts +72 -0
  202. package/src/core/runner/executor.ts +265 -36
  203. package/src/core/runner/form-encode.ts +41 -0
  204. package/src/core/runner/http-client.ts +112 -2
  205. package/src/core/runner/learn-drift.ts +293 -0
  206. package/src/core/runner/preflight-vars.ts +153 -0
  207. package/src/core/runner/progress-tracker.ts +73 -0
  208. package/src/core/runner/rate-limiter.ts +87 -33
  209. package/src/core/runner/run-kind.ts +45 -0
  210. package/src/core/runner/schema-validator.ts +308 -0
  211. package/src/core/runner/send-request.ts +158 -20
  212. package/src/core/runner/types.ts +44 -0
  213. package/src/core/secrets/registry.ts +164 -0
  214. package/src/core/secrets/secrets-file.ts +115 -0
  215. package/src/core/selectors/operation-filter.ts +144 -0
  216. package/src/core/setup-api.ts +457 -20
  217. package/src/core/severity/category.ts +94 -0
  218. package/src/core/severity/index.ts +58 -0
  219. package/src/core/spec/infer-schema.ts +102 -0
  220. package/src/core/spec/layers.ts +154 -0
  221. package/src/core/spec/merge-specs.ts +156 -0
  222. package/src/core/spec/schema-from-runs.ts +117 -0
  223. package/src/core/spec/schema-overlay.ts +130 -0
  224. package/src/core/util/ajv.ts +13 -0
  225. package/src/core/util/format-eta.ts +21 -0
  226. package/src/core/util/headers.ts +9 -0
  227. package/src/core/util/url.ts +24 -0
  228. package/src/core/utils.ts +5 -1
  229. package/src/core/workspace/config.ts +129 -0
  230. package/src/core/workspace/fixture-gap-report.ts +84 -0
  231. package/src/core/workspace/fixture-gaps.ts +71 -0
  232. package/src/core/workspace/manifest.ts +283 -0
  233. package/src/core/workspace/output-rotation.ts +62 -0
  234. package/src/core/workspace/root.ts +13 -11
  235. package/src/core/workspace/triage-path.ts +87 -0
  236. package/src/db/lint-runs.ts +47 -0
  237. package/src/db/migrate.ts +128 -0
  238. package/src/db/migrations/0001_run_kind.sql +25 -0
  239. package/src/db/migrations/0002_run_kind_request.sql +59 -0
  240. package/src/db/migrations/sql.d.ts +4 -0
  241. package/src/db/queries/collections.ts +133 -0
  242. package/src/db/queries/coverage.ts +9 -0
  243. package/src/db/queries/dashboard.ts +59 -0
  244. package/src/db/queries/results.ts +216 -0
  245. package/src/db/queries/runs.ts +289 -0
  246. package/src/db/queries/sessions.ts +42 -0
  247. package/src/db/queries/settings.ts +28 -0
  248. package/src/db/queries/types.ts +172 -0
  249. package/src/db/queries.ts +75 -802
  250. package/src/db/schema.ts +178 -50
  251. package/src/cli/commands/export.ts +0 -144
  252. package/src/cli/commands/guide.ts +0 -127
  253. package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
  254. package/src/cli/commands/probe-methods.ts +0 -108
  255. package/src/cli/commands/probe-validation.ts +0 -124
  256. package/src/cli/commands/serve.ts +0 -114
  257. package/src/cli/commands/sync.ts +0 -268
  258. package/src/cli/commands/update.ts +0 -189
  259. package/src/cli/commands/validate.ts +0 -34
  260. package/src/core/diagnostics/render-md.ts +0 -112
  261. package/src/core/exporter/postman.ts +0 -963
  262. package/src/core/generator/guide-builder.ts +0 -253
  263. package/src/core/meta/types.ts +0 -19
  264. package/src/core/parser/index.ts +0 -21
  265. package/src/core/runner/execute-run.ts +0 -132
  266. package/src/core/runner/index.ts +0 -12
  267. package/src/core/sync/spec-differ.ts +0 -38
  268. package/src/web/data/collection-state.ts +0 -362
  269. package/src/web/routes/api.ts +0 -314
  270. package/src/web/routes/dashboard.ts +0 -350
  271. package/src/web/routes/runs.ts +0 -64
  272. package/src/web/schemas.ts +0 -121
  273. package/src/web/server.ts +0 -134
  274. package/src/web/static/htmx.min.cjs +0 -1
  275. package/src/web/static/style.css +0 -1148
  276. package/src/web/views/endpoints-tab.ts +0 -174
  277. package/src/web/views/explorer-tab.ts +0 -402
  278. package/src/web/views/health-strip.ts +0 -92
  279. package/src/web/views/layout.ts +0 -48
  280. package/src/web/views/results.ts +0 -210
  281. package/src/web/views/runs-tab.ts +0 -126
  282. package/src/web/views/suites-tab.ts +0 -181
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Shared helpers for command-action callbacks: resolving the active API
3
+ * collection, the spec argument that spec-consuming commands accept, and
4
+ * tiny utilities (`globalJson`, deprecation warning). Extracted from
5
+ * program.ts (TASK-190 round 2a) so per-command modules can register
6
+ * themselves without re-importing program.ts.
7
+ */
8
+
9
+ import type { Command } from "commander";
10
+ import { getDb } from "../db/schema.ts";
11
+ import { findCollectionByNameOrId } from "../db/queries.ts";
12
+ import { resolveCollectionSpec } from "../core/setup-api.ts";
13
+ import { readCurrentApi } from "../core/context/current.ts";
14
+
15
+ /**
16
+ * TASK-73: `--json` is a per-command option (not a top-level global) so
17
+ * that `run --json` does not collide with `run --report json`.
18
+ * Subcommands that support an envelope output add `.option("--json", ...)`
19
+ * themselves and we read it from local opts.
20
+ */
21
+ export function globalJson(cmd: Command): boolean {
22
+ return cmd.opts().json === true;
23
+ }
24
+
25
+ /** Resolve API collection → returns { spec?, testPath?, baseDir? } or { error } when not found. */
26
+ export function resolveApiCollection(apiName: string, dbPath: string | undefined):
27
+ | { spec: string | null; testPath: string | null; baseDir: string | null }
28
+ | { error: string } {
29
+ if (typeof apiName !== "string" || apiName.length === 0) {
30
+ return { error: "Internal: --api received non-string value" };
31
+ }
32
+ try {
33
+ getDb(dbPath);
34
+ const col = findCollectionByNameOrId(apiName);
35
+ if (!col) return { error: `API '${apiName}' not found` };
36
+ const spec = col.openapi_spec ? resolveCollectionSpec(col.openapi_spec) : null;
37
+ return { spec, testPath: col.test_path ?? null, baseDir: col.base_dir ?? null };
38
+ } catch (err) {
39
+ return { error: `Failed to resolve --api: ${(err as Error).message}` };
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Resolve `apis/<name>/.env.yaml` for a registered API. TASK-233: probe
45
+ * subcommands required `--env <file>` even when `--api <name>` was given,
46
+ * forcing users to repeat the path. When --api is set we derive the env
47
+ * file from the collection's base_dir; only error if the file is missing.
48
+ *
49
+ * Returns the absolute path to the env file when it exists, otherwise an
50
+ * error object. Callers may also fall back to other strategies on miss.
51
+ */
52
+ export function resolveApiEnv(apiName: string, dbPath: string | undefined):
53
+ | { env: string }
54
+ | { error: string } {
55
+ const col = resolveApiCollection(apiName, dbPath);
56
+ if ("error" in col) return col;
57
+ if (!col.baseDir) {
58
+ return { error: `API '${apiName}' has no base_dir registered — pass --env <file> explicitly.` };
59
+ }
60
+ const envPath = `${col.baseDir.replace(/\/+$/, "")}/.env.yaml`;
61
+ return { env: envPath };
62
+ }
63
+
64
+ /**
65
+ * Resolve a `<spec>` argument used by spec-consuming commands —
66
+ * catalog, sync, generate, probe-validation, probe-methods,
67
+ * probe-mass-assignment, lint-spec, describe, guide.
68
+ *
69
+ * Resolution order:
70
+ * 1. Explicit positional/flag value — used as-is (URL or filesystem path).
71
+ * 2. --api <name> — look up the workspace-local snapshot via
72
+ * `resolveCollectionSpec`.
73
+ * 3. ZOND_API env / .zond/current-api — same lookup using the currently-selected API
74
+ * (TASK-290; resolution chain implemented in core/context/current.ts).
75
+ *
76
+ * Returns `{ spec }` on success, `{ error }` on failure. Centralised here
77
+ * so commands stay thin and skill/CI prompts can rely on either form.
78
+ */
79
+ export function resolveSpecArg(
80
+ positional: string | undefined,
81
+ apiFlag: string | undefined,
82
+ dbPath: string | undefined,
83
+ ): { spec: string } | { error: string } {
84
+ if (typeof positional === "string" && positional.length > 0) {
85
+ return { spec: positional };
86
+ }
87
+ const apiName = apiFlag ?? readCurrentApi() ?? undefined;
88
+ if (!apiName) {
89
+ return {
90
+ error: "Need a spec — pass it positionally, via --api <name>, or set the current API with `zond use <name>`.",
91
+ };
92
+ }
93
+ const resolved = resolveApiCollection(apiName, dbPath);
94
+ if ("error" in resolved) return { error: resolved.error };
95
+ if (!resolved.spec) {
96
+ return {
97
+ error:
98
+ `API '${apiName}' is registered without an OpenAPI spec — this command needs one. ` +
99
+ `Run \`zond refresh-api ${apiName} --spec <path|url>\` to attach a spec, ` +
100
+ `or use \`zond run --api ${apiName} <test.yaml>\` for YAML-based testing.`,
101
+ };
102
+ }
103
+ return { spec: resolved.spec };
104
+ }
105
+
@@ -0,0 +1,24 @@
1
+ /**
2
+ * ARV-299: one safe/live vocabulary across `audit`, `checks run`, and the
3
+ * mutating `probe` subcommands so a developer has a single button —
4
+ * `--safe` (default) means "no destructive traffic", `--live` opts in.
5
+ *
6
+ * The help strings live here so all three commands read identically
7
+ * (ARV-299 AC#3). `audit` predates this module and keeps its own richer
8
+ * --live text (it gates several stages); the shorter pair here is for
9
+ * checks/probe.
10
+ */
11
+
12
+ export const SAFE_HELP =
13
+ "Safe mode (DEFAULT): no destructive/mutating traffic — no create/update/delete " +
14
+ "chains, no live attack payloads. Explicit inverse of --live; safe if neither is given.";
15
+
16
+ export const LIVE_HELP =
17
+ "Opt into destructive/mutating operations (create/update/delete chains, live " +
18
+ "attack payloads) against a real API. Use only against a throwaway/sandbox account.";
19
+
20
+ /** Resolve the safe/live pair to a single boolean. Default is safe;
21
+ * `--live` wins if both are somehow passed. */
22
+ export function resolveLive(opts: { safe?: boolean; live?: boolean }): boolean {
23
+ return opts.live === true;
24
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * TASK-140: parse `--status` filter expressions for `zond db run/runs`.
3
+ *
4
+ * Accepted forms (combinable via comma):
5
+ * 502 — exact code
6
+ * 5xx / 4xx / 3xx / 2xx / 1xx — class wildcard (`5xx` ≡ 500..599)
7
+ * 500-599 — inclusive range
8
+ * >=500 / >500 / <=400 / <400 — open-ended comparison
9
+ * 500,502,504 — list of exacts
10
+ * 5xx,429 — mix of class + exact
11
+ *
12
+ * The parser yields a `StatusMatcher` which the DB layer converts into a
13
+ * single SQL `WHERE` fragment (set + ranges combined with `OR`).
14
+ */
15
+
16
+ export interface StatusMatcher {
17
+ /** Specific codes that should match. */
18
+ exacts: number[];
19
+ /** Inclusive ranges `[min, max]` — including class wildcards (5xx → 500..599). */
20
+ ranges: Array<[number, number]>;
21
+ }
22
+
23
+ const CLASS_RE = /^([1-5])xx$/i;
24
+ const RANGE_RE = /^(\d{3})-(\d{3})$/;
25
+ const CMP_RE = /^(>=|<=|>|<)\s*(\d{3})$/;
26
+ const CODE_RE = /^\d{3}$/;
27
+
28
+ function pushExact(out: StatusMatcher, code: number): void {
29
+ if (code < 100 || code > 599) {
30
+ throw new Error(`status code out of range: ${code}`);
31
+ }
32
+ if (!out.exacts.includes(code)) out.exacts.push(code);
33
+ }
34
+
35
+ function pushRange(out: StatusMatcher, min: number, max: number): void {
36
+ if (min > max) throw new Error(`status range start > end: ${min}-${max}`);
37
+ out.ranges.push([min, max]);
38
+ }
39
+
40
+ /**
41
+ * Parse a `--status` argument. Throws on invalid syntax — caller wraps the
42
+ * thrown message in a CLI error.
43
+ */
44
+ export function parseStatusFilter(raw: string): StatusMatcher {
45
+ const trimmed = raw.trim();
46
+ if (trimmed === "") throw new Error("empty --status value");
47
+ const parts = trimmed.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
48
+ if (parts.length === 0) throw new Error("empty --status value");
49
+
50
+ const out: StatusMatcher = { exacts: [], ranges: [] };
51
+ for (const part of parts) {
52
+ let m: RegExpMatchArray | null;
53
+ if ((m = part.match(CODE_RE))) {
54
+ pushExact(out, Number(part));
55
+ continue;
56
+ }
57
+ if ((m = part.match(CLASS_RE))) {
58
+ const klass = Number(m[1]);
59
+ pushRange(out, klass * 100, klass * 100 + 99);
60
+ continue;
61
+ }
62
+ if ((m = part.match(RANGE_RE))) {
63
+ const lo = Number(m[1]);
64
+ const hi = Number(m[2]);
65
+ if (lo < 100 || lo > 599 || hi < 100 || hi > 599) {
66
+ throw new Error(`status range out of bounds: ${part} (expected 100..599)`);
67
+ }
68
+ pushRange(out, lo, hi);
69
+ continue;
70
+ }
71
+ if ((m = part.match(CMP_RE))) {
72
+ const op = m[1]!;
73
+ const code = Number(m[2]);
74
+ if (code < 100 || code > 599) {
75
+ throw new Error(`status comparison out of bounds: ${part} (expected 100..599)`);
76
+ }
77
+ switch (op) {
78
+ case ">=": pushRange(out, code, 599); break;
79
+ case ">": pushRange(out, code + 1, 599); break;
80
+ case "<=": pushRange(out, 100, code); break;
81
+ case "<": pushRange(out, 100, code - 1); break;
82
+ }
83
+ continue;
84
+ }
85
+ throw new Error(
86
+ `invalid --status part: '${part}' (expected one of: 502, 5xx, 500-599, >=500, <400)`,
87
+ );
88
+ }
89
+ return out;
90
+ }
91
+
92
+ /**
93
+ * Compile a `StatusMatcher` to a SQL `WHERE` fragment + bound parameters.
94
+ * The fragment is wrapped in parentheses; combine with other conditions via
95
+ * `AND`. Returns `null` if the matcher is empty (no constraints).
96
+ */
97
+ export function compileStatusFilterToSql(
98
+ matcher: StatusMatcher,
99
+ column: string,
100
+ ): { sql: string; params: number[] } | null {
101
+ const clauses: string[] = [];
102
+ const params: number[] = [];
103
+ if (matcher.exacts.length > 0) {
104
+ const placeholders = matcher.exacts.map(() => "?").join(",");
105
+ clauses.push(`${column} IN (${placeholders})`);
106
+ params.push(...matcher.exacts);
107
+ }
108
+ for (const [lo, hi] of matcher.ranges) {
109
+ clauses.push(`${column} BETWEEN ? AND ?`);
110
+ params.push(lo, hi);
111
+ }
112
+ if (clauses.length === 0) return null;
113
+ return { sql: `(${clauses.join(" OR ")})`, params };
114
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * ARV-53: single resolver for the active `--api` collection name.
3
+ *
4
+ * Before this module the chain `localOpts.api → cmd.parent?.opts().api →
5
+ * readCurrentApi()` was inlined in nine call-sites (probe / prepare-fixtures
6
+ * / audit / checks / coverage / run / request, plus two intermediate helpers
7
+ * in probe.ts and resolve.ts). Each repeat was a separate commit
8
+ * (TASK-17, TASK-20, ARV-21, ARV-29, ARV-33). Centralising the chain here
9
+ * means new commands consume one import and the fallback rules live in
10
+ * exactly one place.
11
+ *
12
+ * Resolution order (matches the chain users see documented for `zond use`):
13
+ * 1. Per-command `--api <name>` (the local Commander scope).
14
+ * 2. Any ancestor command's `--api`, walking up to the program root
15
+ * (covers the global `zond --api X <subcmd>` form whose value is
16
+ * otherwise stranded on the parent Command).
17
+ * 3. `readCurrentApi()` — which itself folds in
18
+ * `ZOND_API_GLOBAL` (mirrored by program.ts preAction) →
19
+ * `ZOND_API` (user env) → `.zond/current-api` (persisted by `zond use`).
20
+ */
21
+
22
+ import { readCurrentApi } from "../../core/context/current.ts";
23
+
24
+ /**
25
+ * Minimal shape we need from a Commander `Command`. Spelled out as an
26
+ * interface (not `import("commander").Command`) so test doubles can pass a
27
+ * plain `{ opts, parent }` literal and TS still type-checks.
28
+ */
29
+ export interface CommandLike {
30
+ opts(): Record<string, unknown>;
31
+ parent?: CommandLike | null;
32
+ }
33
+
34
+ export type ApiResolution =
35
+ | { ok: true; api: string; source: "local" | "ancestor" | "current" }
36
+ | { ok: false };
37
+
38
+ /** Pull the explicit --api value out of a command's parsed opts, if any. */
39
+ function readApiOpt(cmd: CommandLike): string | undefined {
40
+ const v = cmd.opts().api;
41
+ return typeof v === "string" && v.trim().length > 0 ? v.trim() : undefined;
42
+ }
43
+
44
+ /**
45
+ * Resolve the active API collection name for `cmd`. Pass `localOpts` when
46
+ * the action handler already has the parsed local opts in hand (avoids a
47
+ * second `cmd.opts()` parse and lets callers tunnel through pre-coerced
48
+ * shapes from tests).
49
+ */
50
+ export function resolveApi(
51
+ cmd: CommandLike | undefined,
52
+ localOpts?: Record<string, unknown>,
53
+ ): ApiResolution {
54
+ const localRaw = localOpts?.api;
55
+ const local = typeof localRaw === "string" && localRaw.trim().length > 0
56
+ ? localRaw.trim()
57
+ : (cmd ? readApiOpt(cmd) : undefined);
58
+ if (local) return { ok: true, api: local, source: "local" };
59
+
60
+ let parent: CommandLike | null | undefined = cmd?.parent ?? null;
61
+ while (parent) {
62
+ const fromAncestor = readApiOpt(parent);
63
+ if (fromAncestor) return { ok: true, api: fromAncestor, source: "ancestor" };
64
+ parent = parent.parent ?? null;
65
+ }
66
+
67
+ const fromCurrent = readCurrentApi();
68
+ if (fromCurrent) return { ok: true, api: fromCurrent, source: "current" };
69
+
70
+ return { ok: false };
71
+ }
72
+
73
+ /**
74
+ * Convenience: returns the API name as `string | undefined`. Use this when
75
+ * the caller decides for itself how to react to "missing" (e.g. `coverage`
76
+ * falls back to `--spec`, `run` falls back to a positional path).
77
+ */
78
+ export function getApi(cmd: CommandLike | undefined, localOpts?: Record<string, unknown>): string | undefined {
79
+ const r = resolveApi(cmd, localOpts);
80
+ return r.ok ? r.api : undefined;
81
+ }
82
+
83
+ /** Default error message for commands that strictly require an API. */
84
+ export const MISSING_API_MESSAGE =
85
+ "--api is required (or set ZOND_API / `zond use <name>`).";
@@ -1,3 +1,8 @@
1
1
  import { version } from "../../package.json";
2
2
 
3
3
  export const VERSION = version;
4
+
5
+ /** Canonical GitHub repo (owner/name). Single source of truth for any code
6
+ * that links to releases, install scripts, or generated artefacts. */
7
+ export const REPO = "kirrosh/zond";
8
+ export const REPO_URL = `https://github.com/${REPO}`;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * ARV-265: shared helpers for "any HTTP touch by zond should be visible
3
+ * to `audit-coverage`". `zond run` was the only producer that wrote into
4
+ * `runs` / `results`; `checks run`, `probe`, `request`, and
5
+ * `prepare-fixtures --cascade` all emitted ndjson/stdout and left no
6
+ * persistent trace. This module gives those producers a uniform path:
7
+ *
8
+ * 1. `beginAuditRun(opts)` — INSERT into `runs` with the right
9
+ * `run_kind` and return the row id.
10
+ * 2. accumulate `AuditCaseRecord` entries as the work runs.
11
+ * 3. `finalizeAuditRun(runId, suiteName, cases)` — group cases into a
12
+ * single synthetic `TestRunResult`, call `saveResults`, then
13
+ * `finalizeRun` so the row carries totals.
14
+ *
15
+ * Suite/test naming is *pseudo* (`checks/response`, `probe/security`,
16
+ * `request/ad-hoc`) — these rows aren't meant to drive failure triage,
17
+ * just to keep the audit-coverage matrix honest. The coverage engine
18
+ * matches results to endpoints via (request_method, request_url) regex,
19
+ * so the suite name is purely cosmetic.
20
+ *
21
+ * Error handling: every write is wrapped in try/catch by the CLI caller
22
+ * and degrades to a warning — failing to persist must not break the
23
+ * primary command (a `checks run` that exits 0 with no DB row is still
24
+ * a successful checks run for the user; audit-coverage just won't pick
25
+ * it up).
26
+ */
27
+ import type { HttpRequest, HttpResponse, TestRunResult, StepResult, StepStatus } from "../runner/types.ts";
28
+ import type { RunKind } from "../runner/run-kind.ts";
29
+ import { createRun, saveResults, finalizeRun } from "../../db/queries.ts";
30
+ import { setHttpAuditRecorder, type AuditRecord } from "../runner/http-client.ts";
31
+
32
+ export interface BeginAuditRunOpts {
33
+ runKind: RunKind;
34
+ collectionId?: number;
35
+ sessionId?: string | null;
36
+ environment?: string;
37
+ trigger?: string;
38
+ tags?: string[];
39
+ commitSha?: string;
40
+ branch?: string;
41
+ startedAt?: string;
42
+ }
43
+
44
+ export interface AuditCaseRecord {
45
+ suiteName: string;
46
+ testName: string;
47
+ status: StepStatus;
48
+ request: HttpRequest;
49
+ response?: HttpResponse;
50
+ durationMs: number;
51
+ error?: string;
52
+ /** Optional `suite_file` value — surfaces in the diagnose output and
53
+ * helps the legacy detectRunKind heuristic understand the row's
54
+ * origin even if `run_kind` ever drifts. */
55
+ suiteFile?: string;
56
+ }
57
+
58
+ export function beginAuditRun(opts: BeginAuditRunOpts): number {
59
+ return createRun({
60
+ started_at: opts.startedAt ?? new Date().toISOString(),
61
+ environment: opts.environment,
62
+ trigger: opts.trigger ?? "manual",
63
+ commit_sha: opts.commitSha,
64
+ branch: opts.branch,
65
+ collection_id: opts.collectionId,
66
+ session_id: opts.sessionId ?? undefined,
67
+ tags: opts.tags && opts.tags.length > 0 ? opts.tags : undefined,
68
+ run_kind: opts.runKind,
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Group flat case records into one `TestRunResult` per `suite_file` (or
74
+ * per `suiteName` when no file is set) and write them. Returns the number
75
+ * of `results` rows persisted so the caller can sanity-check the totals.
76
+ */
77
+ export function finalizeAuditRun(
78
+ runId: number,
79
+ cases: AuditCaseRecord[],
80
+ ): { rows: number } {
81
+ if (cases.length === 0) {
82
+ // Nothing executed — still close the run so coverage queries can
83
+ // see an empty audit row instead of an open-ended row dangling
84
+ // without finished_at.
85
+ finalizeRun(runId, []);
86
+ return { rows: 0 };
87
+ }
88
+ const bySuite = new Map<string, AuditCaseRecord[]>();
89
+ for (const c of cases) {
90
+ const key = c.suiteFile ?? c.suiteName;
91
+ const list = bySuite.get(key) ?? [];
92
+ list.push(c);
93
+ bySuite.set(key, list);
94
+ }
95
+ const now = new Date().toISOString();
96
+ const results: TestRunResult[] = [];
97
+ for (const [, group] of bySuite) {
98
+ const first = group[0]!;
99
+ const steps: StepResult[] = group.map((c) => ({
100
+ name: c.testName,
101
+ status: c.status,
102
+ duration_ms: c.durationMs,
103
+ request: c.request,
104
+ response: c.response,
105
+ assertions: [],
106
+ captures: {},
107
+ ...(c.error ? { error: c.error } : {}),
108
+ }));
109
+ let passed = 0, failed = 0, skipped = 0;
110
+ for (const s of steps) {
111
+ if (s.status === "pass") passed += 1;
112
+ else if (s.status === "skip") skipped += 1;
113
+ else failed += 1; // fail | error both bucket as failed for the row totals
114
+ }
115
+ results.push({
116
+ suite_name: first.suiteName,
117
+ ...(first.suiteFile ? { suite_file: first.suiteFile } : {}),
118
+ started_at: now,
119
+ finished_at: now,
120
+ total: steps.length,
121
+ passed,
122
+ failed,
123
+ skipped,
124
+ steps,
125
+ });
126
+ }
127
+ saveResults(runId, results);
128
+ finalizeRun(runId, results);
129
+ return { rows: cases.length };
130
+ }
131
+
132
+ /** ARV-265: ZOND_CHECKS_PERSIST opt-out. Default ON — the task's whole
133
+ * point is that `checks run` contributes to audit-coverage out of the
134
+ * box. Setting the env var to "0" / "false" disables it (for tests, or
135
+ * for users who want the pre-ARV-265 behaviour back). */
136
+ export function checksPersistEnabled(): boolean {
137
+ const v = (process.env.ZOND_CHECKS_PERSIST ?? "").trim().toLowerCase();
138
+ if (v === "0" || v === "false" || v === "off" || v === "no") return false;
139
+ return true;
140
+ }
141
+
142
+ /**
143
+ * ARV-265: run `fn` with the HTTP audit recorder set; return both the
144
+ * function's value and the collected records. Convenience for the live
145
+ * probe commands (probe security / probe mass-assignment) which can't
146
+ * easily hook into their own internals but route every request through
147
+ * `executeRequest`.
148
+ *
149
+ * The recorder is always cleared in `finally`, even if `fn` throws.
150
+ */
151
+ export async function withHttpAudit<T>(
152
+ fn: () => Promise<T>,
153
+ ): Promise<{ value: T; records: AuditRecord[] }> {
154
+ const records: AuditRecord[] = [];
155
+ setHttpAuditRecorder((rec) => records.push(rec));
156
+ try {
157
+ const value = await fn();
158
+ return { value, records };
159
+ } finally {
160
+ setHttpAuditRecorder(null);
161
+ }
162
+ }
163
+
164
+ export function auditRecordToCase(
165
+ rec: AuditRecord,
166
+ meta: { suiteName: string; suiteFile: string; testName: string },
167
+ ): AuditCaseRecord {
168
+ const status: StepStatus = rec.error
169
+ ? "error"
170
+ : rec.response && rec.response.status >= 200 && rec.response.status < 400 ? "pass" : "fail";
171
+ return {
172
+ suiteName: meta.suiteName,
173
+ suiteFile: meta.suiteFile,
174
+ testName: meta.testName,
175
+ status,
176
+ request: rec.request,
177
+ ...(rec.response ? { response: rec.response } : {}),
178
+ durationMs: rec.durationMs,
179
+ ...(rec.error ? { error: rec.error } : {}),
180
+ };
181
+ }
182
+
183
+ export type { AuditRecord } from "../runner/http-client.ts";
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `--budget quick|standard|full` (ARV-292): adaptive cap for `zond audit` and
3
+ * `zond checks run`, designed so small-team CI gets a "60-sec gate" by default
4
+ * without having to remember `--max-requests` for huge specs.
5
+ *
6
+ * Tiers:
7
+ * quick — 50 req cap, skip stateful (CRUD) checks. ~60-sec wall-clock.
8
+ * standard — 500 req cap, all checks (sampling kept as a Phase-B knob).
9
+ * full — uncapped, all checks.
10
+ *
11
+ * Omitted `--budget` keeps the legacy uncapped behaviour so existing CI
12
+ * pipelines don't suddenly trip `max-requests-cap-reached`. `--max-requests`
13
+ * always wins over the tier-seeded cap.
14
+ */
15
+
16
+ export type Budget = "quick" | "standard" | "full";
17
+
18
+ export const BUDGETS: readonly Budget[] = ["quick", "standard", "full"];
19
+
20
+ export interface ResolvedBudget {
21
+ maxRequests?: number;
22
+ skipStateful: boolean;
23
+ }
24
+
25
+ interface TierDefaults {
26
+ maxRequests?: number;
27
+ skipStateful: boolean;
28
+ }
29
+
30
+ const TIERS: Record<Budget, TierDefaults> = {
31
+ quick: { maxRequests: 50, skipStateful: true },
32
+ standard: { maxRequests: 500, skipStateful: false },
33
+ full: { skipStateful: false },
34
+ };
35
+
36
+ export function isBudget(value: unknown): value is Budget {
37
+ return typeof value === "string" && (BUDGETS as readonly string[]).includes(value);
38
+ }
39
+
40
+ /** Resolve final budget given an explicit budget tier and an optional
41
+ * `--max-requests` override. When `--max-requests` is set it always wins,
42
+ * even if it relaxes a tighter tier cap (e.g. `--budget quick --max-requests
43
+ * 200` → cap=200). Stateful-skip is tier-driven only; pass an explicit
44
+ * include list via `forceStatefulIfIncluded` to opt back in (e.g. when the
45
+ * user typed `--check stateful`). */
46
+ export function resolveBudget(
47
+ budget: Budget | undefined,
48
+ maxRequestsOverride: number | undefined,
49
+ opts?: { forceStatefulIfIncluded?: boolean },
50
+ ): ResolvedBudget {
51
+ const tier = budget ? TIERS[budget] : undefined;
52
+ const skipStateful = tier?.skipStateful === true && opts?.forceStatefulIfIncluded !== true;
53
+ const cap = typeof maxRequestsOverride === "number" && maxRequestsOverride > 0
54
+ ? maxRequestsOverride
55
+ : tier?.maxRequests;
56
+ const result: ResolvedBudget = { skipStateful };
57
+ if (cap !== undefined) result.maxRequests = cap;
58
+ return result;
59
+ }