@kirrosh/zond 0.22.0 → 0.23.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 (256) hide show
  1. package/CHANGELOG.md +648 -0
  2. package/README.md +58 -6
  3. package/package.json +9 -6
  4. package/src/cli/argv.ts +122 -0
  5. package/src/cli/commands/add-api.ts +134 -0
  6. package/src/cli/commands/api/annotate/idempotency.ts +59 -0
  7. package/src/cli/commands/api/annotate/index.ts +525 -0
  8. package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
  9. package/src/cli/commands/api/annotate/overlay.ts +206 -0
  10. package/src/cli/commands/api/annotate/pagination.ts +60 -0
  11. package/src/cli/commands/api/annotate/prompts.ts +183 -0
  12. package/src/cli/commands/api/annotate/readback.ts +58 -0
  13. package/src/cli/commands/api/annotate/resources.ts +91 -0
  14. package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
  15. package/src/cli/commands/audit.ts +480 -0
  16. package/src/cli/commands/bootstrap.ts +710 -0
  17. package/src/cli/commands/catalog.ts +35 -0
  18. package/src/cli/commands/check.ts +348 -0
  19. package/src/cli/commands/checks.ts +756 -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 +262 -0
  23. package/src/cli/commands/completions.ts +16 -0
  24. package/src/cli/commands/coverage.ts +605 -132
  25. package/src/cli/commands/db.ts +178 -7
  26. package/src/cli/commands/describe.ts +37 -2
  27. package/src/cli/commands/discover.ts +1236 -0
  28. package/src/cli/commands/doctor.ts +607 -0
  29. package/src/cli/commands/fixtures.ts +402 -0
  30. package/src/cli/commands/generate.ts +420 -46
  31. package/src/cli/commands/init/bootstrap.ts +30 -1
  32. package/src/cli/commands/{init.ts → init/index.ts} +99 -5
  33. package/src/cli/commands/init/skills.ts +56 -3
  34. package/src/cli/commands/init/templates/agents.md +65 -61
  35. package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
  36. package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
  37. package/src/cli/commands/init/templates/skills/zond.md +592 -125
  38. package/src/cli/commands/init/templates/zond-config.yml +8 -9
  39. package/src/cli/commands/prepare-fixtures.ts +135 -0
  40. package/src/cli/commands/probe/mass-assignment.ts +503 -0
  41. package/src/cli/commands/probe/security.ts +454 -0
  42. package/src/cli/commands/probe/static.ts +255 -0
  43. package/src/cli/commands/probe/webhooks.ts +161 -0
  44. package/src/cli/commands/probe.ts +459 -0
  45. package/src/cli/commands/reference.ts +87 -0
  46. package/src/cli/commands/refresh-api.ts +169 -0
  47. package/src/cli/commands/remove-api.ts +150 -0
  48. package/src/cli/commands/report-bundle.ts +318 -0
  49. package/src/cli/commands/report.ts +241 -0
  50. package/src/cli/commands/request.ts +379 -4
  51. package/src/cli/commands/run.ts +842 -53
  52. package/src/cli/commands/session.ts +244 -0
  53. package/src/cli/commands/use.ts +18 -1
  54. package/src/cli/index.ts +20 -3
  55. package/src/cli/json-envelope.ts +112 -3
  56. package/src/cli/json-schemas.ts +263 -0
  57. package/src/cli/program.ts +198 -635
  58. package/src/cli/resolve.ts +105 -0
  59. package/src/cli/status-filter.ts +124 -0
  60. package/src/cli/util/api-context.ts +85 -0
  61. package/src/cli/version.ts +5 -0
  62. package/src/core/anti-fp/bootstrap.ts +34 -0
  63. package/src/core/anti-fp/index.ts +33 -0
  64. package/src/core/anti-fp/registry.ts +44 -0
  65. package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
  66. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
  67. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
  68. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
  69. package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
  70. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
  71. package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
  72. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
  73. package/src/core/anti-fp/types.ts +68 -0
  74. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  75. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  76. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  77. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  78. package/src/core/checks/checks/cross_call_references.ts +134 -0
  79. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  80. package/src/core/checks/checks/idempotency_replay.ts +246 -0
  81. package/src/core/checks/checks/ignored_auth.ts +211 -0
  82. package/src/core/checks/checks/index.ts +65 -0
  83. package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
  84. package/src/core/checks/checks/missing_required_header.ts +40 -0
  85. package/src/core/checks/checks/negative_data_rejection.ts +45 -0
  86. package/src/core/checks/checks/not_a_server_error.ts +27 -0
  87. package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
  88. package/src/core/checks/checks/pagination_invariants.ts +238 -0
  89. package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
  90. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  91. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  92. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  93. package/src/core/checks/checks/status_code_conformance.ts +61 -0
  94. package/src/core/checks/checks/unsupported_method.ts +63 -0
  95. package/src/core/checks/checks/use_after_free.ts +78 -0
  96. package/src/core/checks/index.ts +30 -0
  97. package/src/core/checks/mode.ts +79 -0
  98. package/src/core/checks/recommended-action.ts +64 -0
  99. package/src/core/checks/registry.ts +78 -0
  100. package/src/core/checks/runner.ts +874 -0
  101. package/src/core/checks/sarif.ts +230 -0
  102. package/src/core/checks/stateful.ts +121 -0
  103. package/src/core/checks/types.ts +189 -0
  104. package/src/core/classifier/recommended-action.ts +222 -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 +185 -0
  108. package/src/core/coverage/reasons.ts +300 -0
  109. package/src/core/diagnostics/db-analysis.ts +151 -11
  110. package/src/core/diagnostics/failure-class.ts +120 -0
  111. package/src/core/diagnostics/failure-hints.ts +212 -9
  112. package/src/core/diagnostics/spec-pointer.ts +99 -0
  113. package/src/core/diagnostics/suggested-fixes.ts +156 -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 +42 -16
  122. package/src/core/generator/coverage-phase.ts +0 -0
  123. package/src/core/generator/create-body.ts +89 -0
  124. package/src/core/generator/data-factory.ts +445 -19
  125. package/src/core/generator/describe.ts +1 -1
  126. package/src/core/generator/fixtures-builder.ts +325 -0
  127. package/src/core/generator/index.ts +7 -5
  128. package/src/core/generator/openapi-reader.ts +37 -3
  129. package/src/core/generator/path-param-disambig.ts +114 -0
  130. package/src/core/generator/resources-builder.ts +648 -0
  131. package/src/core/generator/schema-utils.ts +11 -3
  132. package/src/core/generator/serializer.ts +103 -13
  133. package/src/core/generator/suite-generator.ts +419 -111
  134. package/src/core/generator/types.ts +8 -0
  135. package/src/core/identity/identity-file.ts +129 -0
  136. package/src/core/lint/affects.ts +28 -0
  137. package/src/core/lint/config.ts +96 -0
  138. package/src/core/lint/format.ts +42 -0
  139. package/src/core/lint/index.ts +94 -0
  140. package/src/core/lint/reporter.ts +128 -0
  141. package/src/core/lint/rules/consistency.ts +158 -0
  142. package/src/core/lint/rules/heuristics.ts +97 -0
  143. package/src/core/lint/rules/strictness.ts +109 -0
  144. package/src/core/lint/types.ts +96 -0
  145. package/src/core/lint/walker.ts +248 -0
  146. package/src/core/meta/meta-store.ts +6 -73
  147. package/src/core/output/README.md +91 -0
  148. package/src/core/output/index.ts +13 -0
  149. package/src/core/output/run.ts +126 -0
  150. package/src/core/output/types.ts +129 -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 +57 -0
  159. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  160. package/src/core/probe/mass-assignment-probe.ts +1122 -0
  161. package/src/core/probe/mass-assignment-template.ts +212 -0
  162. package/src/core/probe/method-probe.ts +43 -76
  163. package/src/core/probe/method-shared.ts +69 -0
  164. package/src/core/probe/negative-probe.ts +183 -149
  165. package/src/core/probe/orphan-tracker.ts +188 -0
  166. package/src/core/probe/path-discovery.ts +440 -0
  167. package/src/core/probe/probe-harness.ts +120 -0
  168. package/src/core/probe/registry.ts +89 -0
  169. package/src/core/probe/runner.ts +136 -0
  170. package/src/core/probe/security-probe-class.ts +201 -0
  171. package/src/core/probe/security-probe.ts +1453 -0
  172. package/src/core/probe/shared.ts +505 -0
  173. package/src/core/probe/static-probe-class.ts +125 -0
  174. package/src/core/probe/types.ts +165 -0
  175. package/src/core/probe/verdict-aggregator.ts +33 -0
  176. package/src/core/probe/webhooks-probe.ts +284 -0
  177. package/src/core/reporter/console.ts +41 -2
  178. package/src/core/reporter/index.ts +2 -3
  179. package/src/core/reporter/json.ts +11 -1
  180. package/src/core/reporter/junit.ts +27 -12
  181. package/src/core/reporter/ndjson.ts +37 -0
  182. package/src/core/reporter/types.ts +3 -0
  183. package/src/core/runner/assertions.ts +58 -1
  184. package/src/core/runner/async-pool.ts +108 -0
  185. package/src/core/runner/auth-path.ts +8 -0
  186. package/src/core/runner/ci-context.ts +72 -0
  187. package/src/core/runner/executor.ts +264 -20
  188. package/src/core/runner/form-encode.ts +51 -0
  189. package/src/core/runner/http-client.ts +75 -2
  190. package/src/core/runner/learn-drift.ts +293 -0
  191. package/src/core/runner/preflight-vars.ts +149 -0
  192. package/src/core/runner/progress-tracker.ts +73 -0
  193. package/src/core/runner/rate-limiter.ts +89 -17
  194. package/src/core/runner/run-kind.ts +39 -0
  195. package/src/core/runner/schema-validator.ts +312 -0
  196. package/src/core/runner/send-request.ts +153 -20
  197. package/src/core/runner/types.ts +38 -0
  198. package/src/core/secrets/registry.ts +164 -0
  199. package/src/core/secrets/secrets-file.ts +115 -0
  200. package/src/core/selectors/operation-filter.ts +144 -0
  201. package/src/core/setup-api.ts +415 -16
  202. package/src/core/severity/category.ts +94 -0
  203. package/src/core/severity/index.ts +121 -0
  204. package/src/core/spec/layers.ts +154 -0
  205. package/src/core/util/format-eta.ts +21 -0
  206. package/src/core/utils.ts +5 -1
  207. package/src/core/workspace/config.ts +129 -0
  208. package/src/core/workspace/manifest.ts +283 -0
  209. package/src/core/workspace/output-rotation.ts +62 -0
  210. package/src/core/workspace/triage-path.ts +87 -0
  211. package/src/db/lint-runs.ts +47 -0
  212. package/src/db/migrate.ts +126 -0
  213. package/src/db/migrations/0001_run_kind.sql +25 -0
  214. package/src/db/migrations/sql.d.ts +4 -0
  215. package/src/db/queries/collections.ts +133 -0
  216. package/src/db/queries/coverage.ts +9 -0
  217. package/src/db/queries/dashboard.ts +59 -0
  218. package/src/db/queries/results.ts +128 -0
  219. package/src/db/queries/runs.ts +235 -0
  220. package/src/db/queries/sessions.ts +42 -0
  221. package/src/db/queries/settings.ts +28 -0
  222. package/src/db/queries/types.ts +172 -0
  223. package/src/db/queries.ts +72 -802
  224. package/src/db/schema.ts +178 -50
  225. package/src/cli/commands/export.ts +0 -144
  226. package/src/cli/commands/guide.ts +0 -127
  227. package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
  228. package/src/cli/commands/probe-methods.ts +0 -108
  229. package/src/cli/commands/probe-validation.ts +0 -124
  230. package/src/cli/commands/serve.ts +0 -114
  231. package/src/cli/commands/sync.ts +0 -268
  232. package/src/cli/commands/update.ts +0 -189
  233. package/src/cli/commands/validate.ts +0 -34
  234. package/src/core/diagnostics/render-md.ts +0 -112
  235. package/src/core/exporter/postman.ts +0 -963
  236. package/src/core/generator/guide-builder.ts +0 -253
  237. package/src/core/meta/types.ts +0 -19
  238. package/src/core/parser/index.ts +0 -21
  239. package/src/core/runner/execute-run.ts +0 -132
  240. package/src/core/runner/index.ts +0 -12
  241. package/src/core/sync/spec-differ.ts +0 -38
  242. package/src/web/data/collection-state.ts +0 -362
  243. package/src/web/routes/api.ts +0 -314
  244. package/src/web/routes/dashboard.ts +0 -350
  245. package/src/web/routes/runs.ts +0 -64
  246. package/src/web/schemas.ts +0 -121
  247. package/src/web/server.ts +0 -134
  248. package/src/web/static/htmx.min.cjs +0 -1
  249. package/src/web/static/style.css +0 -1148
  250. package/src/web/views/endpoints-tab.ts +0 -174
  251. package/src/web/views/explorer-tab.ts +0 -402
  252. package/src/web/views/health-strip.ts +0 -92
  253. package/src/web/views/layout.ts +0 -48
  254. package/src/web/views/results.ts +0 -210
  255. package/src/web/views/runs-tab.ts +0 -126
  256. 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,124 @@
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
+ /** True if `code` matches the parsed filter. Useful for in-memory filtering. */
93
+ export function statusMatches(matcher: StatusMatcher, code: number | null | undefined): boolean {
94
+ if (code == null) return false;
95
+ if (matcher.exacts.includes(code)) return true;
96
+ for (const [lo, hi] of matcher.ranges) {
97
+ if (code >= lo && code <= hi) return true;
98
+ }
99
+ return false;
100
+ }
101
+
102
+ /**
103
+ * Compile a `StatusMatcher` to a SQL `WHERE` fragment + bound parameters.
104
+ * The fragment is wrapped in parentheses; combine with other conditions via
105
+ * `AND`. Returns `null` if the matcher is empty (no constraints).
106
+ */
107
+ export function compileStatusFilterToSql(
108
+ matcher: StatusMatcher,
109
+ column: string,
110
+ ): { sql: string; params: number[] } | null {
111
+ const clauses: string[] = [];
112
+ const params: number[] = [];
113
+ if (matcher.exacts.length > 0) {
114
+ const placeholders = matcher.exacts.map(() => "?").join(",");
115
+ clauses.push(`${column} IN (${placeholders})`);
116
+ params.push(...matcher.exacts);
117
+ }
118
+ for (const [lo, hi] of matcher.ranges) {
119
+ clauses.push(`${column} BETWEEN ? AND ?`);
120
+ params.push(lo, hi);
121
+ }
122
+ if (clauses.length === 0) return null;
123
+ return { sql: `(${clauses.join(" OR ")})`, params };
124
+ }
@@ -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,34 @@
1
+ /**
2
+ * ARV-124: register every shipped anti-FP rule.
3
+ *
4
+ * Mirrors `core/probe/bootstrap.ts` — called once at CLI startup so
5
+ * checks/probes can rely on the registry being populated. Idempotent:
6
+ * repeated calls are no-ops. Tests should pair `resetAntiFpBootstrap()`
7
+ * with `reset()` from the registry to start from a clean slate.
8
+ */
9
+ import { register, reset } from "./registry.ts";
10
+ import { SCHEMATHESIS_RULES } from "./rules/schemathesis/index.ts";
11
+ import { SUBSCRIPTION_GATED_RULES } from "./rules/subscription-gated/index.ts";
12
+ import { BASELINE_ECHO_RULE } from "./rules/baseline-echo.ts";
13
+
14
+ let bootstrapped = false;
15
+
16
+ export function bootstrapAntiFp(): void {
17
+ if (bootstrapped) return;
18
+ for (const rule of SCHEMATHESIS_RULES) register(rule);
19
+ // ARV-125: subscription/scope-gated 403 wontfix tail in mass-assignment
20
+ // baseline summaries.
21
+ for (const rule of SUBSCRIPTION_GATED_RULES) register(rule);
22
+ // ARV-126: probe:security baseline-echo FP guard. The
23
+ // coverage-phase-boundary rule is shared with checks via its
24
+ // canonical re-export and is already covered by SCHEMATHESIS_RULES.
25
+ register(BASELINE_ECHO_RULE);
26
+ bootstrapped = true;
27
+ }
28
+
29
+ /** Test helper — clears the registry and the bootstrap flag so the
30
+ * next call re-registers from scratch. */
31
+ export function resetAntiFpBootstrap(): void {
32
+ reset();
33
+ bootstrapped = false;
34
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * ARV-123 (m-19): public surface of the anti-FP registry.
3
+ *
4
+ * Callers (checks / probes) interact with a single helper —
5
+ * `applyAntiFp(ctx, scope)` — which walks every rule registered for
6
+ * `scope`, returns the first suppression that fires, or null. The
7
+ * suppression object carries the rule id, the resolved scope, a
8
+ * human reason, and the upstream references the rule was attributed
9
+ * to.
10
+ *
11
+ * The registry itself is exported for migration tooling (ARV-124..126)
12
+ * and for tests; production callers should prefer the helper.
13
+ */
14
+ export type { FpRule, FpScope, FpSuppression } from "./types.ts";
15
+ export { register, get, list, reset, matchesScope } from "./registry.ts";
16
+
17
+ import { list } from "./registry.ts";
18
+ import type { FpScope, FpSuppression } from "./types.ts";
19
+
20
+ export function applyAntiFp<Ctx>(ctx: Ctx, scope: FpScope): FpSuppression | null {
21
+ for (const rule of list(scope)) {
22
+ const hit = rule.applies(ctx);
23
+ if (hit) {
24
+ return {
25
+ ruleId: hit.ruleId || rule.id,
26
+ scope,
27
+ reason: hit.reason,
28
+ references: hit.references ?? rule.references,
29
+ };
30
+ }
31
+ }
32
+ return null;
33
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ARV-123 (m-19): in-process registry for anti-FP rules.
3
+ *
4
+ * Module-level mutable state on purpose — rules are registered at
5
+ * bootstrap time (similar to `core/probe/bootstrap.ts`) and read by
6
+ * checks/probes during a run. `reset()` exists for tests; production
7
+ * code never calls it.
8
+ */
9
+ import type { FpRule, FpScope, FpSuppression } from "./types.ts";
10
+
11
+ const rules = new Map<string, FpRule<unknown>>();
12
+
13
+ /** Register a rule. Re-registering with the same `id` replaces the
14
+ * prior entry — this keeps test setups simple (swap in a stub) and
15
+ * matches how the probe-bootstrap pattern handles dedup. */
16
+ export function register<Ctx>(rule: FpRule<Ctx>): void {
17
+ rules.set(rule.id, rule as FpRule<unknown>);
18
+ }
19
+
20
+ /** Lookup by id. Used mostly by tests and the `list` filter. */
21
+ export function get(id: string): FpRule<unknown> | undefined {
22
+ return rules.get(id);
23
+ }
24
+
25
+ /** List rules in registration order. Optional scope filter keeps the
26
+ * hot path (checks/probes) from re-implementing the scope-match
27
+ * predicate. Pass a `scope` like `"check:positive_data_acceptance"`
28
+ * to get only rules that declared that scope. */
29
+ export function list(scope?: FpScope): FpRule<unknown>[] {
30
+ const all = Array.from(rules.values());
31
+ if (!scope) return all;
32
+ return all.filter(r => matchesScope(r, scope));
33
+ }
34
+
35
+ /** Drop every registered rule. Call only from test setup. */
36
+ export function reset(): void {
37
+ rules.clear();
38
+ }
39
+
40
+ export function matchesScope(rule: FpRule<unknown>, scope: FpScope): boolean {
41
+ if (Array.isArray(rule.scope)) return rule.scope.includes(scope);
42
+ return rule.scope === scope;
43
+ }
44
+
@@ -0,0 +1,74 @@
1
+ /**
2
+ * ARV-126: baseline-echo FP guard for `probe:security`.
3
+ *
4
+ * Context: the live security probe sends a mutated body against a
5
+ * 2xx-able baseline. When the response body is byte-for-byte identical
6
+ * to the baseline response — same URL bouncing back unchanged — the
7
+ * server effectively ignored the mutation. classifyInner currently
8
+ * lands such findings at `severity: "low"` with the reason "2xx
9
+ * accepted but no echo observed — verify side-effects manually",
10
+ * which floods the digest with sites that have nothing to verify.
11
+ *
12
+ * This rule consumes a `{responseBody, baselineBody}` context and
13
+ * fires when the two bodies are deeply equal. The probe consults
14
+ * `applyAntiFp(ctx, "probe:security")` after classifyInner returns a
15
+ * low-severity 2xx no-echo finding and, on a hit, downgrades the
16
+ * finding to OK with the rule's reason as the wontfix banner.
17
+ *
18
+ * The deep-equality check is intentionally narrow — referential or
19
+ * shape-only matches would over-suppress (a generic "ok: true" body
20
+ * trivially equals across many endpoints). The probe is responsible
21
+ * for passing the *full* parsed response, not a digest.
22
+ */
23
+ import type { FpRule } from "../types.ts";
24
+
25
+ export interface BaselineEchoCtx {
26
+ /** Parsed response body for the mutated request. */
27
+ responseBody: unknown;
28
+ /** Parsed response body for the pre-mutation baseline. May be
29
+ * `undefined` when the probe didn't retain a baseline (in which
30
+ * case the rule never fires — fail-open). */
31
+ baselineBody: unknown;
32
+ }
33
+
34
+ function deepEqual(a: unknown, b: unknown): boolean {
35
+ if (a === b) return true;
36
+ if (a === null || b === null) return false;
37
+ if (typeof a !== typeof b) return false;
38
+ if (typeof a !== "object") return false;
39
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
40
+ if (Array.isArray(a) && Array.isArray(b)) {
41
+ if (a.length !== b.length) return false;
42
+ for (let i = 0; i < a.length; i++) {
43
+ if (!deepEqual(a[i], b[i])) return false;
44
+ }
45
+ return true;
46
+ }
47
+ const ao = a as Record<string, unknown>;
48
+ const bo = b as Record<string, unknown>;
49
+ const keys = Object.keys(ao);
50
+ if (keys.length !== Object.keys(bo).length) return false;
51
+ for (const k of keys) {
52
+ if (!Object.prototype.hasOwnProperty.call(bo, k)) return false;
53
+ if (!deepEqual(ao[k], bo[k])) return false;
54
+ }
55
+ return true;
56
+ }
57
+
58
+ export const BASELINE_ECHO_RULE: FpRule<BaselineEchoCtx> = {
59
+ id: "baseline-echo",
60
+ scope: "probe:security",
61
+ references: ["ARV-126"],
62
+ applies(ctx) {
63
+ if (ctx.baselineBody === undefined) return null;
64
+ if (!deepEqual(ctx.responseBody, ctx.baselineBody)) return null;
65
+ return {
66
+ ruleId: "baseline-echo",
67
+ scope: "probe:security",
68
+ reason:
69
+ "response body identical to the pre-mutation baseline — server " +
70
+ "ignored the attack payload; no side-effect to verify",
71
+ references: ["ARV-126"],
72
+ };
73
+ },
74
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * ARV-124: migrated from `src/core/checks/checks/_anti_fp.ts` (guard #1).
3
+ *
4
+ * Form-encoded / multipart bodies often *re-validate* after wire
5
+ * serialisation: empty strings round-trip as missing, dropped optional
6
+ * fields default at the server, numeric strings coerce. When the
7
+ * mutation is a drop/empty-string on a form-shaped request, the
8
+ * negative_data_rejection finding would be a false positive — the
9
+ * mutation is a no-op on the wire.
10
+ *
11
+ * Sources: schemathesis #2482, #2726, #3712.
12
+ */
13
+ import type { CheckCase } from "../../../checks/types.ts";
14
+ import type { MutationMeta } from "../../../checks/checks/_negative_mutator.ts";
15
+ import type { FpRule } from "../../types.ts";
16
+
17
+ function getMutation(c: CheckCase): MutationMeta | undefined {
18
+ const m = c.meta as { mutation?: MutationMeta["mutation"] } | undefined;
19
+ if (!m || typeof m.mutation !== "string") return undefined;
20
+ return c.meta as unknown as MutationMeta;
21
+ }
22
+
23
+ function isFormLike(contentType: string | undefined): boolean {
24
+ if (!contentType) return false;
25
+ const ct = contentType.toLowerCase();
26
+ return (
27
+ ct.includes("application/x-www-form-urlencoded") ||
28
+ ct.includes("multipart/form-data")
29
+ );
30
+ }
31
+
32
+ export const bodyNegationBecomesValidRule: FpRule<CheckCase> = {
33
+ id: "_body_negation_becomes_valid_after_serialization",
34
+ scope: "check:negative_data_rejection",
35
+ references: ["#2482", "#2726", "#3712"],
36
+ applies(c) {
37
+ const m = getMutation(c);
38
+ if (!m) return null;
39
+ const ct =
40
+ c.request.headers["Content-Type"] ?? c.request.headers["content-type"];
41
+ if (!isFormLike(ct)) return null;
42
+ if (m.mutation === "drop_required" || m.mutation === "constraint_violation") {
43
+ return {
44
+ ruleId: "_body_negation_becomes_valid_after_serialization",
45
+ scope: "check:negative_data_rejection",
46
+ reason: `mutation "${m.mutation}" on a ${ct} body re-validates after wire serialisation`,
47
+ references: ["#2482", "#2726", "#3712"],
48
+ };
49
+ }
50
+ return null;
51
+ },
52
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * ARV-124: migrated from `src/core/checks/checks/_anti_fp.ts` (guard #4).
3
+ *
4
+ * `--phase coverage` enumerates boundary values across the body schema:
5
+ * - shortest/longest string, min/max int, every enum option, ...
6
+ * Those bodies are JSON-Schema-valid but semantically synthetic — they
7
+ * sit on the contract edge. Real APIs reject them with 422 for reasons
8
+ * that have nothing to do with the contract:
9
+ * - "from" email must be on a verified-sending-domain,
10
+ * - "broadcast.from_audience_id" must exist on this tenant,
11
+ * - rate-limited resource (a plan_limit).
12
+ * Treating each one as `positive_data_acceptance` fail floods the
13
+ * report (171/349 findings on a benchmark run) and drowns real depth
14
+ * signal. Skip when the case is a coverage-phase positive — keep the
15
+ * examples-phase positive (one realistic baseline body) as the strict
16
+ * signal.
17
+ *
18
+ * Source: feedback round-03 F20 / ARV-77.
19
+ */
20
+ import type { CheckCase } from "../../../checks/types.ts";
21
+ import type { FpRule } from "../../types.ts";
22
+
23
+ export const coveragePhaseBoundaryPositiveRule: FpRule<CheckCase> = {
24
+ id: "_coverage_phase_boundary_positive",
25
+ scope: "check:positive_data_acceptance",
26
+ references: ["ARV-77"],
27
+ applies(c) {
28
+ const meta = c.meta as { phase?: string } | undefined;
29
+ if (!meta || meta.phase !== "coverage") return null;
30
+ if (c.kind !== "positive") return null;
31
+ return {
32
+ ruleId: "_coverage_phase_boundary_positive",
33
+ scope: "check:positive_data_acceptance",
34
+ reason:
35
+ "boundary-positive bodies are synthetic — server may reject for semantic reasons unrelated to the contract",
36
+ };
37
+ },
38
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * ARV-124: migrated from `src/core/checks/checks/_anti_fp.ts` (guard #3).
3
+ *
4
+ * Multiple disjoint mutations make accept/reject ambiguous: the server
5
+ * might accept due to one site even while rejecting another. Our
6
+ * single-site mutator emits exactly one mutation, so the guard fires
7
+ * only when callers attach `mutation_count > 1` to `case.meta` — used
8
+ * by future shrinkers / batched probes.
9
+ *
10
+ * Scope covers both data-rejection checks so a multi-site mutation
11
+ * payload that survives into either side gets suppressed consistently.
12
+ *
13
+ * Source: schemathesis #2713.
14
+ */
15
+ import type { CheckCase } from "../../../checks/types.ts";
16
+ import type { FpRule } from "../../types.ts";
17
+
18
+ export const hasUnverifiableMutationsRule: FpRule<CheckCase> = {
19
+ id: "_has_unverifiable_mutations",
20
+ scope: ["check:negative_data_rejection", "check:positive_data_acceptance"],
21
+ references: ["#2713"],
22
+ applies(c) {
23
+ const meta = c.meta as { mutation_count?: number } | undefined;
24
+ if (!meta) return null;
25
+ if (typeof meta.mutation_count === "number" && meta.mutation_count > 1) {
26
+ return {
27
+ ruleId: "_has_unverifiable_mutations",
28
+ scope: "check:negative_data_rejection",
29
+ reason: `${meta.mutation_count} mutations on disjoint sites — finding can't be attributed`,
30
+ references: ["#2713"],
31
+ };
32
+ }
33
+ return null;
34
+ },
35
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * ARV-124: schemathesis-attributed rule bundle. Each export is a
3
+ * standalone FpRule for testing/introspection; the side-effect-free
4
+ * list is re-exported as `SCHEMATHESIS_RULES` so the bootstrap can
5
+ * register them in one batch.
6
+ */
7
+ import { bodyNegationBecomesValidRule } from "./body_negation_becomes_valid.ts";
8
+ import { coveragePhaseBoundaryPositiveRule } from "./coverage_phase_boundary_positive.ts";
9
+ import { hasUnverifiableMutationsRule } from "./has_unverifiable_mutations.ts";
10
+ import { stringTypeMutationBecomesValidRule } from "./string_type_mutation_becomes_valid.ts";
11
+
12
+ export {
13
+ bodyNegationBecomesValidRule,
14
+ coveragePhaseBoundaryPositiveRule,
15
+ hasUnverifiableMutationsRule,
16
+ stringTypeMutationBecomesValidRule,
17
+ };
18
+
19
+ export const SCHEMATHESIS_RULES = [
20
+ bodyNegationBecomesValidRule,
21
+ stringTypeMutationBecomesValidRule,
22
+ hasUnverifiableMutationsRule,
23
+ coveragePhaseBoundaryPositiveRule,
24
+ ] as const;