@kirrosh/zond 0.21.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 +758 -3
  2. package/README.md +78 -15
  3. package/package.json +17 -10
  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 +55 -6
  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 +192 -0
  24. package/src/cli/commands/coverage.ts +605 -132
  25. package/src/cli/commands/db.ts +180 -8
  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 -47
  31. package/src/cli/commands/init/agents-md.ts +61 -0
  32. package/src/cli/commands/init/bootstrap.ts +108 -0
  33. package/src/cli/commands/init/index.ts +244 -0
  34. package/src/cli/commands/init/skills.ts +98 -0
  35. package/src/cli/commands/init/templates/agents.md +77 -0
  36. package/src/cli/commands/init/templates/markdown.d.ts +4 -0
  37. package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
  38. package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
  39. package/src/cli/commands/init/templates/skills/zond.md +651 -0
  40. package/src/cli/commands/init/templates/zond-config.yml +14 -0
  41. package/src/cli/commands/prepare-fixtures.ts +135 -0
  42. package/src/cli/commands/probe/mass-assignment.ts +503 -0
  43. package/src/cli/commands/probe/security.ts +454 -0
  44. package/src/cli/commands/probe/static.ts +255 -0
  45. package/src/cli/commands/probe/webhooks.ts +161 -0
  46. package/src/cli/commands/probe.ts +459 -0
  47. package/src/cli/commands/reference.ts +87 -0
  48. package/src/cli/commands/refresh-api.ts +169 -0
  49. package/src/cli/commands/remove-api.ts +150 -0
  50. package/src/cli/commands/report-bundle.ts +318 -0
  51. package/src/cli/commands/report.ts +241 -0
  52. package/src/cli/commands/request.ts +379 -4
  53. package/src/cli/commands/run.ts +911 -33
  54. package/src/cli/commands/session.ts +244 -0
  55. package/src/cli/commands/use.ts +74 -0
  56. package/src/cli/index.ts +36 -607
  57. package/src/cli/json-envelope.ts +112 -3
  58. package/src/cli/json-schemas.ts +263 -0
  59. package/src/cli/program.ts +218 -0
  60. package/src/cli/resolve.ts +105 -0
  61. package/src/cli/status-filter.ts +124 -0
  62. package/src/cli/util/api-context.ts +85 -0
  63. package/src/cli/version.ts +8 -0
  64. package/src/core/anti-fp/bootstrap.ts +34 -0
  65. package/src/core/anti-fp/index.ts +33 -0
  66. package/src/core/anti-fp/registry.ts +44 -0
  67. package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
  68. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
  69. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
  70. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
  71. package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
  72. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
  73. package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
  74. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
  75. package/src/core/anti-fp/types.ts +68 -0
  76. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  77. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  78. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  79. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  80. package/src/core/checks/checks/cross_call_references.ts +134 -0
  81. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  82. package/src/core/checks/checks/idempotency_replay.ts +246 -0
  83. package/src/core/checks/checks/ignored_auth.ts +211 -0
  84. package/src/core/checks/checks/index.ts +65 -0
  85. package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
  86. package/src/core/checks/checks/missing_required_header.ts +40 -0
  87. package/src/core/checks/checks/negative_data_rejection.ts +45 -0
  88. package/src/core/checks/checks/not_a_server_error.ts +27 -0
  89. package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
  90. package/src/core/checks/checks/pagination_invariants.ts +238 -0
  91. package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
  92. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  93. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  94. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  95. package/src/core/checks/checks/status_code_conformance.ts +61 -0
  96. package/src/core/checks/checks/unsupported_method.ts +63 -0
  97. package/src/core/checks/checks/use_after_free.ts +78 -0
  98. package/src/core/checks/index.ts +30 -0
  99. package/src/core/checks/mode.ts +79 -0
  100. package/src/core/checks/recommended-action.ts +64 -0
  101. package/src/core/checks/registry.ts +78 -0
  102. package/src/core/checks/runner.ts +874 -0
  103. package/src/core/checks/sarif.ts +230 -0
  104. package/src/core/checks/stateful.ts +121 -0
  105. package/src/core/checks/types.ts +189 -0
  106. package/src/core/classifier/recommended-action.ts +222 -0
  107. package/src/core/context/current.ts +51 -0
  108. package/src/core/context/session.ts +78 -0
  109. package/src/core/coverage/loader.ts +185 -0
  110. package/src/core/coverage/reasons.ts +300 -0
  111. package/src/core/diagnostics/db-analysis.ts +161 -12
  112. package/src/core/diagnostics/failure-class.ts +120 -0
  113. package/src/core/diagnostics/failure-hints.ts +212 -9
  114. package/src/core/diagnostics/spec-pointer.ts +99 -0
  115. package/src/core/diagnostics/suggested-fixes.ts +156 -0
  116. package/src/core/exporter/case-study/index.ts +270 -0
  117. package/src/core/exporter/curl.ts +40 -0
  118. package/src/core/exporter/exporter.ts +48 -0
  119. package/src/core/exporter/html-report/escape.ts +24 -0
  120. package/src/core/exporter/html-report/index.ts +479 -0
  121. package/src/core/exporter/html-report/script.ts +100 -0
  122. package/src/core/exporter/html-report/styles.ts +408 -0
  123. package/src/core/generator/chunker.ts +53 -15
  124. package/src/core/generator/coverage-phase.ts +0 -0
  125. package/src/core/generator/create-body.ts +89 -0
  126. package/src/core/generator/data-factory.ts +490 -33
  127. package/src/core/generator/describe.ts +1 -1
  128. package/src/core/generator/fixtures-builder.ts +325 -0
  129. package/src/core/generator/index.ts +7 -5
  130. package/src/core/generator/openapi-reader.ts +55 -3
  131. package/src/core/generator/path-param-disambig.ts +114 -0
  132. package/src/core/generator/resources-builder.ts +648 -0
  133. package/src/core/generator/schema-utils.ts +11 -3
  134. package/src/core/generator/serializer.ts +114 -15
  135. package/src/core/generator/suite-generator.ts +484 -77
  136. package/src/core/generator/types.ts +8 -0
  137. package/src/core/identity/identity-file.ts +129 -0
  138. package/src/core/lint/affects.ts +28 -0
  139. package/src/core/lint/config.ts +96 -0
  140. package/src/core/lint/format.ts +42 -0
  141. package/src/core/lint/index.ts +94 -0
  142. package/src/core/lint/reporter.ts +128 -0
  143. package/src/core/lint/rules/consistency.ts +158 -0
  144. package/src/core/lint/rules/heuristics.ts +97 -0
  145. package/src/core/lint/rules/strictness.ts +109 -0
  146. package/src/core/lint/types.ts +96 -0
  147. package/src/core/lint/walker.ts +248 -0
  148. package/src/core/meta/meta-store.ts +6 -73
  149. package/src/core/output/README.md +91 -0
  150. package/src/core/output/index.ts +13 -0
  151. package/src/core/output/run.ts +126 -0
  152. package/src/core/output/types.ts +129 -0
  153. package/src/core/parser/env-interpolation.ts +104 -0
  154. package/src/core/parser/filter.ts +57 -0
  155. package/src/core/parser/schema.ts +132 -5
  156. package/src/core/parser/types.ts +29 -2
  157. package/src/core/parser/variables.ts +0 -0
  158. package/src/core/parser/yaml-parser.ts +108 -13
  159. package/src/core/probe/bootstrap.ts +34 -0
  160. package/src/core/probe/dry-run-envelope.ts +57 -0
  161. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  162. package/src/core/probe/mass-assignment-probe.ts +1122 -0
  163. package/src/core/probe/mass-assignment-template.ts +212 -0
  164. package/src/core/probe/method-probe.ts +164 -0
  165. package/src/core/probe/method-shared.ts +69 -0
  166. package/src/core/probe/negative-probe.ts +691 -0
  167. package/src/core/probe/orphan-tracker.ts +188 -0
  168. package/src/core/probe/path-discovery.ts +440 -0
  169. package/src/core/probe/probe-harness.ts +120 -0
  170. package/src/core/probe/registry.ts +89 -0
  171. package/src/core/probe/runner.ts +136 -0
  172. package/src/core/probe/security-probe-class.ts +201 -0
  173. package/src/core/probe/security-probe.ts +1453 -0
  174. package/src/core/probe/shared.ts +505 -0
  175. package/src/core/probe/static-probe-class.ts +125 -0
  176. package/src/core/probe/types.ts +165 -0
  177. package/src/core/probe/verdict-aggregator.ts +33 -0
  178. package/src/core/probe/webhooks-probe.ts +284 -0
  179. package/src/core/reporter/console.ts +69 -4
  180. package/src/core/reporter/index.ts +2 -3
  181. package/src/core/reporter/json.ts +15 -2
  182. package/src/core/reporter/junit.ts +27 -12
  183. package/src/core/reporter/ndjson.ts +37 -0
  184. package/src/core/reporter/types.ts +3 -0
  185. package/src/core/runner/assertions.ts +62 -2
  186. package/src/core/runner/async-pool.ts +108 -0
  187. package/src/core/runner/auth-path.ts +8 -0
  188. package/src/core/runner/ci-context.ts +72 -0
  189. package/src/core/runner/executor.ts +391 -52
  190. package/src/core/runner/form-encode.ts +51 -0
  191. package/src/core/runner/http-client.ts +115 -7
  192. package/src/core/runner/learn-drift.ts +293 -0
  193. package/src/core/runner/preflight-vars.ts +149 -0
  194. package/src/core/runner/progress-tracker.ts +73 -0
  195. package/src/core/runner/rate-limiter.ts +203 -0
  196. package/src/core/runner/run-kind.ts +39 -0
  197. package/src/core/runner/schema-validator.ts +312 -0
  198. package/src/core/runner/send-request.ts +153 -20
  199. package/src/core/runner/types.ts +38 -0
  200. package/src/core/secrets/registry.ts +164 -0
  201. package/src/core/secrets/secrets-file.ts +115 -0
  202. package/src/core/selectors/operation-filter.ts +144 -0
  203. package/src/core/setup-api.ts +419 -17
  204. package/src/core/severity/category.ts +94 -0
  205. package/src/core/severity/index.ts +121 -0
  206. package/src/core/spec/layers.ts +154 -0
  207. package/src/core/util/format-eta.ts +21 -0
  208. package/src/core/utils.ts +5 -1
  209. package/src/core/workspace/config.ts +129 -0
  210. package/src/core/workspace/manifest.ts +283 -0
  211. package/src/core/workspace/output-rotation.ts +62 -0
  212. package/src/core/workspace/root.ts +94 -0
  213. package/src/core/workspace/triage-path.ts +87 -0
  214. package/src/db/lint-runs.ts +47 -0
  215. package/src/db/migrate.ts +126 -0
  216. package/src/db/migrations/0001_run_kind.sql +25 -0
  217. package/src/db/migrations/sql.d.ts +4 -0
  218. package/src/db/queries/collections.ts +133 -0
  219. package/src/db/queries/coverage.ts +9 -0
  220. package/src/db/queries/dashboard.ts +59 -0
  221. package/src/db/queries/results.ts +128 -0
  222. package/src/db/queries/runs.ts +235 -0
  223. package/src/db/queries/sessions.ts +42 -0
  224. package/src/db/queries/settings.ts +28 -0
  225. package/src/db/queries/types.ts +172 -0
  226. package/src/db/queries.ts +72 -802
  227. package/src/db/schema.ts +179 -48
  228. package/src/cli/commands/export.ts +0 -144
  229. package/src/cli/commands/guide.ts +0 -127
  230. package/src/cli/commands/init.ts +0 -57
  231. package/src/cli/commands/serve.ts +0 -81
  232. package/src/cli/commands/sync.ts +0 -269
  233. package/src/cli/commands/update.ts +0 -189
  234. package/src/cli/commands/validate.ts +0 -34
  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 -21
  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
@@ -1,4 +1,5 @@
1
1
  import type { TestRunResult, StepResult } from "../runner/types.ts";
2
+ import { type Exporter, runExporter } from "../exporter/exporter.ts";
2
3
  import type { Reporter, ReporterOptions } from "./types.ts";
3
4
 
4
5
  function escapeXml(str: string): string {
@@ -55,20 +56,34 @@ function renderTestsuite(result: TestRunResult): string {
55
56
  return ` <testsuite name="${name}" tests="${tests}" failures="${failures}" errors="${errors}" skipped="${skipped}" time="${time}">\n${testcases}\n </testsuite>`;
56
57
  }
57
58
 
58
- export function generateJunitXml(results: TestRunResult[]): string {
59
- const totalTests = results.reduce((s, r) => s + r.total, 0);
60
- const totalFailures = results.reduce((s, r) => s + r.failed, 0);
61
- const totalErrors = results.reduce((s, r) => s + r.steps.filter((s) => s.status === "error").length, 0);
62
- const totalTime = formatTime(results.reduce((s, r) => s + r.steps.reduce((ss, step) => ss + step.duration_ms, 0), 0));
59
+ const junitExporter: Exporter<TestRunResult[]> = {
60
+ name: "junit",
61
+ mime: "application/xml",
62
+ render(results: TestRunResult[]): string {
63
+ const totalTests = results.reduce((s, r) => s + r.total, 0);
64
+ const totalFailures = results.reduce((s, r) => s + r.failed, 0);
65
+ const totalErrors = results.reduce(
66
+ (s, r) => s + r.steps.filter((s) => s.status === "error").length,
67
+ 0,
68
+ );
69
+ const totalTime = formatTime(
70
+ results.reduce((s, r) => s + r.steps.reduce((ss, step) => ss + step.duration_ms, 0), 0),
71
+ );
63
72
 
64
- const suites = results.map(renderTestsuite).join("\n");
73
+ const suites = results.map(renderTestsuite).join("\n");
65
74
 
66
- return [
67
- `<?xml version="1.0" encoding="UTF-8"?>`,
68
- `<testsuites tests="${totalTests}" failures="${totalFailures}" errors="${totalErrors}" time="${totalTime}">`,
69
- suites,
70
- `</testsuites>`,
71
- ].join("\n");
75
+ return [
76
+ `<?xml version="1.0" encoding="UTF-8"?>`,
77
+ `<testsuites tests="${totalTests}" failures="${totalFailures}" errors="${totalErrors}" time="${totalTime}">`,
78
+ suites,
79
+ `</testsuites>`,
80
+ ].join("\n");
81
+ },
82
+ };
83
+
84
+ /** TASK-186: pure render → sanitizer pipeline; redaction lives in runExporter. */
85
+ export function generateJunitXml(results: TestRunResult[]): string {
86
+ return runExporter(junitExporter, results);
72
87
  }
73
88
 
74
89
  export const junitReporter: Reporter = {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * ARV-10 (m-15): NDJSON streaming reporter for `zond checks run`.
3
+ *
4
+ * Each event is a single JSON line on stdout — agents pipe the stream
5
+ * into `jq` / ajv / their own consumer and act on findings *as they
6
+ * happen* instead of waiting for the run to wrap up. The discriminated
7
+ * union of event shapes lives in `src/cli/json-schemas.ts` (zod source
8
+ * of truth) and ships as `docs/json-schema/ndjson-events.schema.json`.
9
+ *
10
+ * The CLI passes `emitToStdout` as the `onEvent` callback to runChecks;
11
+ * tests can pass an in-memory accumulator instead.
12
+ */
13
+ import type { z } from "zod";
14
+ import type {
15
+ NdjsonCheckStartEventSchema,
16
+ NdjsonCheckResultEventSchema,
17
+ NdjsonFindingEventSchema,
18
+ NdjsonSummaryEventSchema,
19
+ NdjsonEventSchema,
20
+ } from "../../cli/json-schemas.ts";
21
+
22
+ export type NdjsonCheckStartEvent = z.infer<typeof NdjsonCheckStartEventSchema>;
23
+ export type NdjsonCheckResultEvent = z.infer<typeof NdjsonCheckResultEventSchema>;
24
+ export type NdjsonFindingEvent = z.infer<typeof NdjsonFindingEventSchema>;
25
+ export type NdjsonSummaryEvent = z.infer<typeof NdjsonSummaryEventSchema>;
26
+ export type NdjsonEvent = z.infer<typeof NdjsonEventSchema>;
27
+
28
+ /** Write one event to stdout as a single line. AC #5 — when the CLI is
29
+ * in `--ndjson` mode, *every* user-readable message goes to stderr, so
30
+ * stdout stays a clean NDJSON stream that pipes into `jq` / ajv. */
31
+ export function emitToStdout(ev: NdjsonEvent): void {
32
+ process.stdout.write(`${JSON.stringify(ev)}\n`);
33
+ }
34
+
35
+ export function nowIso(): string {
36
+ return new Date().toISOString();
37
+ }
@@ -3,6 +3,9 @@ import type { TestRunResult } from "../runner/types.ts";
3
3
  export interface ReporterOptions {
4
4
  /** Whether to use ANSI colors. Default: auto-detect via isTTY. */
5
5
  color?: boolean;
6
+ /** TASK-265: suppress per-suite/per-test detail and emit only the
7
+ * grand-total summary line. Exit code still carries pass/fail. */
8
+ quiet?: boolean;
6
9
  }
7
10
 
8
11
  export type ReporterName = "console" | "json" | "junit";
@@ -10,6 +10,7 @@ function checkType(value: unknown, expectedType: string): boolean {
10
10
  case "boolean": return typeof value === "boolean";
11
11
  case "array": return Array.isArray(value);
12
12
  case "object": return typeof value === "object" && value !== null && !Array.isArray(value);
13
+ case "null": return value === null;
13
14
  default: return false;
14
15
  }
15
16
  }
@@ -37,7 +38,9 @@ function checkRule(path: string, rule: AssertionRule, actual: unknown): Assertio
37
38
  const field = `body.${path}`;
38
39
 
39
40
  if (rule.exists !== undefined) {
40
- const doesExist = actual !== undefined && actual !== null;
41
+ // Key-presence semantics: null counts as "exists" (key present in response).
42
+ // Use `not_equals: null` or `type: "null"` to assert non-null specifically.
43
+ const doesExist = actual !== undefined;
41
44
  results.push({
42
45
  field, rule: `exists ${rule.exists}`,
43
46
  passed: doesExist === rule.exists, actual: doesExist, expected: rule.exists,
@@ -230,6 +233,7 @@ export function checkAssertions(expect: TestStepExpect, response: HttpResponse):
230
233
  passed: allowed.includes(response.status),
231
234
  actual: response.status,
232
235
  expected: expect.status,
236
+ kind: "primary",
233
237
  });
234
238
  }
235
239
 
@@ -240,6 +244,7 @@ export function checkAssertions(expect: TestStepExpect, response: HttpResponse):
240
244
  passed: response.duration_ms <= expect.duration,
241
245
  actual: response.duration_ms,
242
246
  expected: expect.duration,
247
+ kind: "auxiliary",
243
248
  });
244
249
  }
245
250
 
@@ -253,12 +258,14 @@ export function checkAssertions(expect: TestStepExpect, response: HttpResponse):
253
258
  passed: actual === rule,
254
259
  actual,
255
260
  expected: rule,
261
+ kind: "auxiliary",
256
262
  });
257
263
  } else {
258
264
  // AssertionRule in header — supports capture and other checks
259
265
  const ruleResults = checkRule(key, rule, actual).map(r => ({
260
266
  ...r,
261
267
  field: r.field.replace(/^body\./, "headers."),
268
+ kind: "auxiliary" as const,
262
269
  }));
263
270
  results.push(...ruleResults);
264
271
  }
@@ -275,7 +282,11 @@ export function checkAssertions(expect: TestStepExpect, response: HttpResponse):
275
282
  } else {
276
283
  actual = getByPath(response.body_parsed, path);
277
284
  }
278
- results.push(...checkRule(path, rule, actual));
285
+ const bodyResults = checkRule(path, rule, actual).map((r) => ({
286
+ ...r,
287
+ kind: "primary" as const,
288
+ }));
289
+ results.push(...bodyResults);
279
290
  }
280
291
  }
281
292
 
@@ -321,3 +332,52 @@ export function extractCaptures(
321
332
 
322
333
  return captures;
323
334
  }
335
+
336
+ /**
337
+ * Find capture rules whose path didn't resolve in the response, returning
338
+ * `{ var, source, path }` per miss. Surfaced as auxiliary assertion failures
339
+ * so the user sees "captures: {}" with a reason instead of silent emptiness —
340
+ * the empty-captures pitfall is the #1 footgun in CRUD chains because the
341
+ * downstream step is silently skipped or runs with `undefined`. TASK-256.
342
+ */
343
+ export function findMissedCaptures(
344
+ bodyRules: Record<string, AssertionRule> | undefined,
345
+ responseBody: unknown,
346
+ headerRules?: Record<string, string | AssertionRule>,
347
+ responseHeaders?: Record<string, string>,
348
+ ): Array<{ var: string; source: "body" | "header"; path: string }> {
349
+ const misses: Array<{ var: string; source: "body" | "header"; path: string }> = [];
350
+
351
+ if (bodyRules) {
352
+ for (const [path, rule] of Object.entries(bodyRules)) {
353
+ if (!rule.capture) continue;
354
+ if (responseBody === undefined) {
355
+ misses.push({ var: rule.capture, source: "body", path });
356
+ continue;
357
+ }
358
+ let value: unknown;
359
+ if (path === "_body") {
360
+ value = responseBody;
361
+ } else if (path.startsWith("_body.")) {
362
+ value = getByPath(responseBody, path.slice(6));
363
+ } else {
364
+ value = getByPath(responseBody, path);
365
+ }
366
+ if (value === undefined) {
367
+ misses.push({ var: rule.capture, source: "body", path });
368
+ }
369
+ }
370
+ }
371
+
372
+ if (headerRules) {
373
+ for (const [key, rule] of Object.entries(headerRules)) {
374
+ if (typeof rule === "string" || !rule.capture) continue;
375
+ const value = responseHeaders ? responseHeaders[key.toLowerCase()] : undefined;
376
+ if (value === undefined) {
377
+ misses.push({ var: rule.capture, source: "header", path: key });
378
+ }
379
+ }
380
+ }
381
+
382
+ return misses;
383
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * ARV-8 (m-15): bounded async-pool for `zond checks run --workers N`.
3
+ *
4
+ * Cooperative concurrency on a single Bun event loop — no threading,
5
+ * no Workers, just N coroutines that pull from a shared cursor. The
6
+ * design choice is deliberate:
7
+ *
8
+ * * Threads/Workers would need request-context plumbing (auth headers,
9
+ * rate-limiter state, per-run schema validators are not transferable
10
+ * without serialization), and Bun's HTTP client is already
11
+ * non-blocking — so cooperative concurrency is faster *and* simpler.
12
+ * * `Promise.all(items.map(fn))` would saturate at items.length, which
13
+ * defeats the rate-limiter and tends to drown small mock servers.
14
+ *
15
+ * `runPool` preserves input order in the result array — callers (the
16
+ * checks runner, mainly) want to merge per-op findings deterministically
17
+ * regardless of which worker finished first.
18
+ */
19
+ import os from "node:os";
20
+
21
+ const WORKERS_MIN = 1;
22
+ const WORKERS_MAX = 64;
23
+ /** `--workers auto` ceiling — beyond ~8 the gains on a typical mock
24
+ * server are dominated by network/IO contention, not parallelism. */
25
+ const WORKERS_AUTO_CEILING = 8;
26
+
27
+ /**
28
+ * Run `fn` over `items` with at most `workers` in-flight at once.
29
+ *
30
+ * Results are returned in input order, *not* completion order — keep
31
+ * call-sites that compose findings/snapshots deterministic.
32
+ *
33
+ * Errors propagate: the first rejection cancels remaining workers'
34
+ * dispatch (they finish their in-flight task and exit). Caller should
35
+ * `try/catch` if it wants partial results — none of zond's callers do
36
+ * today (a runner crash is fatal anyway).
37
+ */
38
+ export async function runPool<T, R>(
39
+ items: readonly T[],
40
+ workers: number,
41
+ fn: (item: T, index: number) => Promise<R>,
42
+ ): Promise<R[]> {
43
+ if (items.length === 0) return [];
44
+ // Effective worker count is clamped both ways: never more than items
45
+ // (idle workers waste a closure), never below 1 (else nothing runs).
46
+ const effective = Math.max(1, Math.min(workers, items.length));
47
+ // Sequential fast-path — preserves the *exact* old behaviour
48
+ // (microtask ordering, error timing) for AC #4 backward-compat.
49
+ if (effective === 1) {
50
+ const out: R[] = new Array(items.length);
51
+ for (let i = 0; i < items.length; i++) {
52
+ out[i] = await fn(items[i]!, i);
53
+ }
54
+ return out;
55
+ }
56
+ const out: R[] = new Array(items.length);
57
+ let cursor = 0;
58
+ let aborted: unknown = null;
59
+ async function worker(): Promise<void> {
60
+ while (true) {
61
+ if (aborted !== null) return;
62
+ const i = cursor++;
63
+ if (i >= items.length) return;
64
+ try {
65
+ out[i] = await fn(items[i]!, i);
66
+ } catch (err) {
67
+ // First rejection wins — store it, drain in-flight tasks, then
68
+ // re-throw at the join point.
69
+ if (aborted === null) aborted = err;
70
+ return;
71
+ }
72
+ }
73
+ }
74
+ const swarm: Promise<void>[] = [];
75
+ for (let w = 0; w < effective; w++) swarm.push(worker());
76
+ await Promise.all(swarm);
77
+ if (aborted !== null) throw aborted;
78
+ return out;
79
+ }
80
+
81
+ /**
82
+ * Parse a `--workers` flag value:
83
+ *
84
+ * undefined → 1 (backward-compat default — AC #4)
85
+ * "auto" / "AUTO"→ min(cpus, WORKERS_AUTO_CEILING) (AC #5)
86
+ * numeric → clamp [1, 64] (AC #5)
87
+ * anything else → throws (caller maps to a friendly CLI error)
88
+ */
89
+ export function parseWorkers(value: string | number | undefined): number {
90
+ if (value === undefined || value === null || value === "") return 1;
91
+ if (typeof value === "string") {
92
+ const trimmed = value.trim().toLowerCase();
93
+ if (trimmed === "auto") {
94
+ return Math.max(WORKERS_MIN, Math.min(os.cpus().length, WORKERS_AUTO_CEILING));
95
+ }
96
+ const n = Number.parseInt(trimmed, 10);
97
+ if (!Number.isFinite(n)) throw new Error(`Invalid --workers value: "${value}"`);
98
+ return Math.max(WORKERS_MIN, Math.min(n, WORKERS_MAX));
99
+ }
100
+ if (!Number.isFinite(value)) throw new Error(`Invalid --workers value: ${value}`);
101
+ return Math.max(WORKERS_MIN, Math.min(Math.trunc(value), WORKERS_MAX));
102
+ }
103
+
104
+ export const WORKERS_LIMITS = {
105
+ min: WORKERS_MIN,
106
+ max: WORKERS_MAX,
107
+ autoCeiling: WORKERS_AUTO_CEILING,
108
+ } as const;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Heuristic for "auth-shaped" endpoint paths used by `--safe` mode and
3
+ * the env-symptom diagnostics in `db-analysis.ts`. Anything matching is
4
+ * treated as a login/refresh route — `--safe` whitelists it for live
5
+ * runs, and the diagnostics flag concentrated POST failures here as
6
+ * `auth_required` rather than per-endpoint bugs.
7
+ */
8
+ export const AUTH_PATH_RE = /\/(auth|login|signin|token|oauth)\b/i;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * TASK-116: detect CI context (commit sha, branch, trigger) from common
3
+ * environment variables. Returns `null` for fields when nothing is present
4
+ * — caller decides whether to default `trigger` to `"manual"`.
5
+ *
6
+ * Supported providers (autodetected — no opt-in required):
7
+ * GitHub Actions GITHUB_ACTIONS=true, GITHUB_SHA, GITHUB_REF_NAME
8
+ * GitLab CI GITLAB_CI=true, CI_COMMIT_SHA, CI_COMMIT_REF_NAME
9
+ * CircleCI CIRCLECI=true, CIRCLE_SHA1, CIRCLE_BRANCH
10
+ * Buildkite BUILDKITE=true, BUILDKITE_COMMIT, BUILDKITE_BRANCH
11
+ * Jenkins JENKINS_URL set, GIT_COMMIT, BRANCH_NAME / GIT_BRANCH
12
+ * Generic CI=true triggers `trigger=ci` even when no provider
13
+ * matches — caller can still pass nullable commit/branch.
14
+ *
15
+ * Manual override is via the explicit `--commit-sha` / `--branch` /
16
+ * `--trigger` flags or the env vars `ZOND_COMMIT_SHA` / `ZOND_BRANCH` /
17
+ * `ZOND_TRIGGER`. These win over autodetection.
18
+ */
19
+ export interface CiContext {
20
+ trigger: "ci" | "manual";
21
+ commit_sha: string | null;
22
+ branch: string | null;
23
+ /** Provider tag (github-actions / gitlab-ci / circleci / …) for diagnostics. */
24
+ provider: string | null;
25
+ }
26
+
27
+ export function detectCiContext(env: NodeJS.ProcessEnv = process.env): CiContext {
28
+ const overrideCommit = env.ZOND_COMMIT_SHA?.trim() || null;
29
+ const overrideBranch = env.ZOND_BRANCH?.trim() || null;
30
+ const overrideTrigger = env.ZOND_TRIGGER?.trim() || null;
31
+
32
+ let provider: string | null = null;
33
+ let commit: string | null = null;
34
+ let branch: string | null = null;
35
+
36
+ if (env.GITHUB_ACTIONS === "true") {
37
+ provider = "github-actions";
38
+ commit = env.GITHUB_SHA?.trim() || null;
39
+ branch = env.GITHUB_REF_NAME?.trim() || env.GITHUB_HEAD_REF?.trim() || null;
40
+ } else if (env.GITLAB_CI === "true") {
41
+ provider = "gitlab-ci";
42
+ commit = env.CI_COMMIT_SHA?.trim() || null;
43
+ branch = env.CI_COMMIT_REF_NAME?.trim() || null;
44
+ } else if (env.CIRCLECI === "true") {
45
+ provider = "circleci";
46
+ commit = env.CIRCLE_SHA1?.trim() || null;
47
+ branch = env.CIRCLE_BRANCH?.trim() || null;
48
+ } else if (env.BUILDKITE === "true") {
49
+ provider = "buildkite";
50
+ commit = env.BUILDKITE_COMMIT?.trim() || null;
51
+ branch = env.BUILDKITE_BRANCH?.trim() || null;
52
+ } else if (env.JENKINS_URL) {
53
+ provider = "jenkins";
54
+ commit = env.GIT_COMMIT?.trim() || null;
55
+ branch = env.BRANCH_NAME?.trim() || env.GIT_BRANCH?.trim() || null;
56
+ }
57
+
58
+ const inferredCi = !!provider || env.CI === "true" || env.CI === "1";
59
+ const trigger: "ci" | "manual" =
60
+ overrideTrigger === "ci" || overrideTrigger === "manual"
61
+ ? overrideTrigger
62
+ : inferredCi
63
+ ? "ci"
64
+ : "manual";
65
+
66
+ return {
67
+ trigger,
68
+ commit_sha: overrideCommit ?? commit,
69
+ branch: overrideBranch ?? branch,
70
+ provider: provider ?? (inferredCi ? "generic" : null),
71
+ };
72
+ }