@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,130 @@
1
+ /**
2
+ * ARV-176: response-schema overlay. `schema-from-runs` (ARV-175) emits a
3
+ * `patch.schema.json` of inferred response schemas keyed by `METHOD /path` →
4
+ * status. `refresh-api --merge-schema` folds that patch into a persistent
5
+ * overlay (`apis/<name>/.api-schema.local.yaml`) and applies it onto the
6
+ * freshly-pulled spec.json.
7
+ *
8
+ * Why a dedicated overlay file (not `.api-resources.local.yaml`): that file
9
+ * is resource-shaped (ResourceYaml[]); response schemas are a different
10
+ * dimension and shoehorning them in would muddy both. Same survives-refresh
11
+ * contract as ARV-111's resource overlay — refresh-api re-applies it on every
12
+ * run, so an upstream re-pull never loses the mined schemas.
13
+ */
14
+
15
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
18
+ import type { JsonSchema } from "./infer-schema.ts";
19
+
20
+ export const SCHEMA_OVERLAY_FILENAME = ".api-schema.local.yaml";
21
+
22
+ /** `METHOD /path` → status code → JSON Schema. */
23
+ export type ResponseSchemaPatch = Record<string, Record<string, JsonSchema>>;
24
+
25
+ interface SchemaOverlayFile {
26
+ version: 1;
27
+ response_schemas: ResponseSchemaPatch;
28
+ }
29
+
30
+ export function overlayPath(baseDir: string): string {
31
+ return join(baseDir, SCHEMA_OVERLAY_FILENAME);
32
+ }
33
+
34
+ /** Load the overlay for an API, or null when absent. */
35
+ export function loadSchemaOverlay(baseDir: string): ResponseSchemaPatch | null {
36
+ const p = overlayPath(baseDir);
37
+ if (!existsSync(p)) return null;
38
+ try {
39
+ const parsed = parseYaml(readFileSync(p, "utf-8")) as Partial<SchemaOverlayFile> | null;
40
+ if (!parsed || typeof parsed !== "object") return null;
41
+ return parsed.response_schemas ?? null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /** Union two patches; entries in `incoming` win on collision. */
48
+ export function mergePatch(base: ResponseSchemaPatch | null, incoming: ResponseSchemaPatch): ResponseSchemaPatch {
49
+ const out: ResponseSchemaPatch = {};
50
+ for (const [ep, byStatus] of Object.entries(base ?? {})) out[ep] = { ...byStatus };
51
+ for (const [ep, byStatus] of Object.entries(incoming)) {
52
+ out[ep] = { ...(out[ep] ?? {}), ...byStatus };
53
+ }
54
+ return out;
55
+ }
56
+
57
+ /** Persist the overlay (sorted for a stable, diff-friendly file). */
58
+ export function saveSchemaOverlay(baseDir: string, patch: ResponseSchemaPatch): void {
59
+ const sorted: ResponseSchemaPatch = {};
60
+ for (const ep of Object.keys(patch).sort()) {
61
+ const byStatus = patch[ep]!;
62
+ sorted[ep] = {};
63
+ for (const st of Object.keys(byStatus).sort()) sorted[ep]![st] = byStatus[st]!;
64
+ }
65
+ const file: SchemaOverlayFile = { version: 1, response_schemas: sorted };
66
+ writeFileSync(overlayPath(baseDir), stringifyYaml(file), "utf-8");
67
+ }
68
+
69
+ export interface ApplyOverlayResult {
70
+ /** `METHOD /path status` labels that got a schema written. */
71
+ applied: string[];
72
+ /** Labels skipped because a schema already existed (no --force). */
73
+ preserved: string[];
74
+ /** Labels skipped because the endpoint no longer exists upstream. */
75
+ conflicts: string[];
76
+ }
77
+
78
+ const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
79
+
80
+ /**
81
+ * Apply a response-schema patch onto a dereferenced OpenAPI doc, in place.
82
+ * Fills `responses.<status>.content['application/json'].schema` where it is
83
+ * absent (or with `force`). An endpoint/method missing from the doc is a
84
+ * conflict (upstream drift) and is skipped, not fabricated.
85
+ */
86
+ export function applySchemaOverlay(
87
+ doc: unknown,
88
+ patch: ResponseSchemaPatch,
89
+ opts: { force?: boolean } = {},
90
+ ): ApplyOverlayResult {
91
+ const result: ApplyOverlayResult = { applied: [], preserved: [], conflicts: [] };
92
+ const paths = (doc as { paths?: Record<string, unknown> }).paths;
93
+ if (!paths) {
94
+ for (const [ep, byStatus] of Object.entries(patch)) {
95
+ for (const st of Object.keys(byStatus)) result.conflicts.push(`${ep} ${st}`);
96
+ }
97
+ return result;
98
+ }
99
+
100
+ for (const [endpoint, byStatus] of Object.entries(patch)) {
101
+ const sp = endpoint.indexOf(" ");
102
+ const method = endpoint.slice(0, sp).toLowerCase();
103
+ const path = endpoint.slice(sp + 1);
104
+ const pathItem = paths[path] as Record<string, unknown> | undefined;
105
+ const op = pathItem && HTTP_METHODS.includes(method)
106
+ ? (pathItem[method] as { responses?: Record<string, unknown> } | undefined)
107
+ : undefined;
108
+
109
+ for (const [status, schema] of Object.entries(byStatus)) {
110
+ const label = `${endpoint} ${status}`;
111
+ if (!op) {
112
+ result.conflicts.push(label);
113
+ continue;
114
+ }
115
+ if (!op.responses) op.responses = {};
116
+ const responses = op.responses as Record<string, { content?: Record<string, { schema?: unknown }> }>;
117
+ if (!responses[status]) responses[status] = { content: {} } as { content: Record<string, { schema?: unknown }> };
118
+ const resp = responses[status]!;
119
+ if (!resp.content) resp.content = {};
120
+ const mt = resp.content["application/json"] ?? (resp.content["application/json"] = {});
121
+ if (mt.schema !== undefined && !opts.force) {
122
+ result.preserved.push(label);
123
+ continue;
124
+ }
125
+ mt.schema = schema;
126
+ result.applied.push(label);
127
+ }
128
+ }
129
+ return result;
130
+ }
@@ -0,0 +1,13 @@
1
+ import Ajv2020 from "ajv/dist/2020.js";
2
+ import Ajv from "ajv";
3
+ import addFormats from "ajv-formats";
4
+
5
+ /** OpenAPI 3.1 → JSON Schema Draft 2020-12 (Ajv2020); 3.0 → Draft 4/7-ish
6
+ * (plain Ajv). Both get the `ajv-formats` format keywords registered. */
7
+ export function makeAjv(isV31: boolean, opts: ConstructorParameters<typeof Ajv>[0] = {}): Ajv {
8
+ const ajv = isV31
9
+ ? new (Ajv2020 as unknown as typeof Ajv)(opts)
10
+ : new Ajv(opts);
11
+ addFormats(ajv);
12
+ return ajv;
13
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * ARV-249: human-readable duration formatter for ETA / progress lines.
3
+ *
4
+ * Distinct from `core/reporter/console.formatDuration`, which is geared to
5
+ * per-step latencies (sub-second resolution, single unit). Here we drop
6
+ * sub-second precision but emit two units once we cross a minute, so a
7
+ * five-minute ETA reads `5m12s` instead of `5m 12s`.
8
+ */
9
+ export function formatEta(seconds: number): string {
10
+ if (!Number.isFinite(seconds) || seconds < 0) return "?";
11
+ const s = Math.round(seconds);
12
+ if (s < 60) return `${s}s`;
13
+ if (s < 3600) {
14
+ const m = Math.floor(s / 60);
15
+ const rem = s % 60;
16
+ return rem > 0 ? `${m}m${rem}s` : `${m}m`;
17
+ }
18
+ const h = Math.floor(s / 3600);
19
+ const m = Math.floor((s % 3600) / 60);
20
+ return m > 0 ? `${h}h${m}m` : `${h}h`;
21
+ }
@@ -0,0 +1,9 @@
1
+ /** Case-insensitive check whether a header is already present in the map.
2
+ * Deliberately not `new Headers(headers).has(name)` — the WHATWG Headers
3
+ * constructor throws on values with invalid characters (e.g. raw CRLF),
4
+ * which real interpolated var values can contain; a plain key scan never
5
+ * throws on the value shape. */
6
+ export function hasHeaderCI(headers: Record<string, string>, name: string): boolean {
7
+ const lower = name.toLowerCase();
8
+ return Object.keys(headers).some((k) => k.toLowerCase() === lower);
9
+ }
@@ -0,0 +1,24 @@
1
+ /** Concatenate baseUrl and path, stripping any trailing slashes from base. */
2
+ export function joinBaseAndPath(baseUrl: string | undefined, path: string): string {
3
+ if (!baseUrl) return path;
4
+ return `${baseUrl.replace(/\/+$/, "")}${path}`;
5
+ }
6
+
7
+ export type QueryValue = string | number | boolean;
8
+
9
+ /** Build a URL from base + path with an optional query record. Values are
10
+ * coerced to strings via String(); URL-encoding is delegated to
11
+ * URLSearchParams. */
12
+ export function buildUrl(
13
+ baseUrl: string | undefined,
14
+ path: string,
15
+ query?: Record<string, QueryValue>,
16
+ ): string {
17
+ let url = joinBaseAndPath(baseUrl, path);
18
+ if (query && Object.keys(query).length > 0) {
19
+ const params = new URLSearchParams();
20
+ for (const [k, v] of Object.entries(query)) params.append(k, String(v));
21
+ url += `?${params.toString()}`;
22
+ }
23
+ return url;
24
+ }
package/src/core/utils.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  export function getByPath(obj: unknown, path: string, defaultVal?: unknown): unknown {
2
- const keys = path.split(".");
2
+ // Normalize JSONPath-like bracket indexing (`data[0].id`) to dotted form
3
+ // (`data.0.id`) so callers can use either spelling. Numeric segments also
4
+ // index arrays correctly because `array["0"]` is equivalent to `array[0]`.
5
+ const normalized = path.replace(/\[(\d+)\]/g, ".$1").replace(/^\./, "");
6
+ const keys = normalized.split(".");
3
7
  let result: unknown = obj;
4
8
  for (const key of keys) {
5
9
  result = (result as Record<string, unknown>)?.[key];
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Workspace `zond.config.yml` loader (TASK-301).
3
+ *
4
+ * Centralises read-only access to workspace-level defaults. Right now only
5
+ * two fields are honoured:
6
+ *
7
+ * defaults:
8
+ * timeout_ms: 30000 # used by cleanup / prepare-fixtures / probe
9
+ * # mass-assignment / probe security / request
10
+ * rate_limit: 5 # used by `zond run` (number, or "auto")
11
+ *
12
+ * Resolution chain across the CLI (highest wins):
13
+ *
14
+ * CLI flag → per-API .env.yaml meta → workspace defaults → hard-coded fallback
15
+ *
16
+ * Per-API overrides live in `apis/<name>/.env.yaml` as `rateLimit:` /
17
+ * `timeoutMs:` (see `loadEnvMeta`); we deliberately don't carve a second
18
+ * channel into this file to avoid two ways of saying the same thing.
19
+ *
20
+ * Read-once-and-cache: the file is parsed at most once per process from
21
+ * the workspace root resolved by `findWorkspaceRoot`. Tests can call
22
+ * `_resetWorkspaceConfigCache()` between runs.
23
+ */
24
+
25
+ import { existsSync, readFileSync } from "node:fs";
26
+ import { join } from "node:path";
27
+ import { findWorkspaceRoot } from "./root.ts";
28
+
29
+ export interface WorkspaceDefaults {
30
+ /** Per-request timeout in ms applied when the CLI flag and `.env.yaml` are silent. */
31
+ timeoutMs?: number;
32
+ /** Run-time rate limit (rps) or `"auto"`. */
33
+ rateLimit?: number | "auto";
34
+ }
35
+
36
+ let cache: { root: string; defaults: WorkspaceDefaults } | null = null;
37
+
38
+ function parseTimeoutMs(v: unknown): number | undefined {
39
+ if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
40
+ if (typeof v === "string") {
41
+ const n = Number.parseInt(v, 10);
42
+ if (Number.isFinite(n) && n > 0) return n;
43
+ }
44
+ return undefined;
45
+ }
46
+
47
+ function parseRateLimit(v: unknown): number | "auto" | undefined {
48
+ if (v === "auto") return "auto";
49
+ if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
50
+ if (typeof v === "string") {
51
+ if (v === "auto") return "auto";
52
+ const n = Number.parseFloat(v);
53
+ if (Number.isFinite(n) && n > 0) return n;
54
+ }
55
+ return undefined;
56
+ }
57
+
58
+ function readDefaults(configPath: string): WorkspaceDefaults {
59
+ if (!existsSync(configPath)) return {};
60
+ let parsed: unknown;
61
+ try {
62
+ const text = readFileSync(configPath, "utf8");
63
+ parsed = Bun.YAML.parse(text);
64
+ } catch {
65
+ return {};
66
+ }
67
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
68
+ const obj = parsed as Record<string, unknown>;
69
+ const defaultsRaw = obj.defaults;
70
+ if (typeof defaultsRaw !== "object" || defaultsRaw === null || Array.isArray(defaultsRaw)) return {};
71
+ const d = defaultsRaw as Record<string, unknown>;
72
+ const out: WorkspaceDefaults = {};
73
+ const t = parseTimeoutMs(d.timeout_ms ?? d.timeoutMs);
74
+ if (t !== undefined) out.timeoutMs = t;
75
+ const r = parseRateLimit(d.rate_limit ?? d.rateLimit);
76
+ if (r !== undefined) out.rateLimit = r;
77
+ return out;
78
+ }
79
+
80
+ /**
81
+ * Returns the workspace defaults block, cached for the lifetime of the
82
+ * process. When no workspace marker is found, returns `{}` (no defaults).
83
+ */
84
+ export function loadWorkspaceDefaults(cwd?: string): WorkspaceDefaults {
85
+ const ws = findWorkspaceRoot(cwd);
86
+ if (ws.fromFallback) return {};
87
+ if (cache && cache.root === ws.root) return cache.defaults;
88
+ const defaults = readDefaults(join(ws.root, "zond.config.yml"));
89
+ cache = { root: ws.root, defaults };
90
+ return defaults;
91
+ }
92
+
93
+ /** Test helper: drop the parse cache so the next call re-reads from disk. */
94
+ export function _resetWorkspaceConfigCache(): void {
95
+ cache = null;
96
+ }
97
+
98
+ export const HARD_DEFAULT_TIMEOUT_MS = 30000;
99
+
100
+ /**
101
+ * Resolve `--timeout` via CLI > per-API `.env.yaml` (`timeoutMs`) > workspace
102
+ * `defaults.timeout_ms` > 30000. Each layer accepts `undefined` to mean
103
+ * "fall through".
104
+ */
105
+ export function resolveTimeoutMs(
106
+ cliFlag: number | undefined,
107
+ envMetaTimeout: number | undefined,
108
+ cwd?: string,
109
+ ): number {
110
+ if (cliFlag !== undefined) return cliFlag;
111
+ if (envMetaTimeout !== undefined) return envMetaTimeout;
112
+ const ws = loadWorkspaceDefaults(cwd);
113
+ return ws.timeoutMs ?? HARD_DEFAULT_TIMEOUT_MS;
114
+ }
115
+
116
+ /**
117
+ * Resolve `--rate-limit` via CLI > per-API `.env.yaml` (`rateLimit`) >
118
+ * workspace `defaults.rate_limit` > undefined (no throttling).
119
+ */
120
+ export function resolveRateLimit(
121
+ cliFlag: number | "auto" | undefined,
122
+ envMetaRateLimit: number | "auto" | undefined,
123
+ cwd?: string,
124
+ ): number | "auto" | undefined {
125
+ if (cliFlag !== undefined) return cliFlag;
126
+ if (envMetaRateLimit !== undefined) return envMetaRateLimit;
127
+ const ws = loadWorkspaceDefaults(cwd);
128
+ return ws.rateLimit;
129
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * ARV-349/350: deterministic fixture-gap report for `prepare-fixtures`.
3
+ *
4
+ * prepare-fixtures is a single-pass discover (ARV-336 removed the autonomous
5
+ * seed engine). It fills FK ids it can resolve from list endpoints, but it
6
+ * used to neither fill NOR flag two other classes of gap, so suites ran with
7
+ * unresolved placeholders and produced noisy 400/404s:
8
+ *
9
+ * - undefinedVars (ARV-349): a suite references {{bank_code}} / {{tax_id}}
10
+ * that nothing produces — not an env value, not a prior-step capture, not
11
+ * a manifest entry. The user / agent must supply it.
12
+ * - unseededRoots (ARV-350): a REQUIRED manifest var (e.g. {{account}}) that
13
+ * is empty in env, referenced by a suite, and captured by no step — the
14
+ * dependency-chain HEAD that gates dependent CRUD suites (persons-crud,
15
+ * external_accounts-crud all skip: "required fixture {{account}} is empty").
16
+ * Source-agnostic on purpose: real manifests model this root as `path`
17
+ * required:true with an empty default, not necessarily `capture-chain`.
18
+ *
19
+ * REPORT ONLY — never invents a value (ARV-349 #2) and never auto-seeds
20
+ * (ARV-350 #2). Deterministic (same suites+env+manifest → same report), so it
21
+ * belongs in zond; supplying the values is the agent's/user's job.
22
+ */
23
+
24
+ import type { TestSuite } from "../parser/types.ts";
25
+ import {
26
+ preflightCheckVars,
27
+ collectCapturesAndSets,
28
+ collectStepRefs,
29
+ } from "../runner/preflight-vars.ts";
30
+
31
+ export interface FixtureGapReport {
32
+ /** Suite {{vars}} with no producer (not env, capture, param, or generator). */
33
+ undefinedVars: { variable: string; refs: number; suites: string[] }[];
34
+ /** Required manifest vars that are empty, suite-referenced, and step-unseeded. */
35
+ unseededRoots: { variable: string }[];
36
+ }
37
+
38
+ export function reportFixtureGaps(
39
+ suites: TestSuite[],
40
+ env: Record<string, string>,
41
+ requiredEmptyVars: Set<string>,
42
+ ): FixtureGapReport {
43
+ // Match runtime capture scoping: only `setup: true` suites share their
44
+ // captures into other suites (TestSuite.setup); a regular suite's captures
45
+ // stay local to itself. So a var is "seeded" for suite S iff env holds it,
46
+ // a setup suite captures it, or S itself captures it.
47
+ const setupProduced = new Set<string>();
48
+ for (const suite of suites) {
49
+ if (!suite.setup) continue;
50
+ for (const step of suite.tests) collectCapturesAndSets(step, setupProduced);
51
+ }
52
+
53
+ // Unseeded root = required + empty in env + SOME referencing suite cannot
54
+ // produce it (not a setup capture, not captured within that suite). This
55
+ // catches cross-suite roots like {{account}}: crud-accounts creates it, but
56
+ // persons-crud references it without a create → that suite skips at runtime.
57
+ const rootSet = new Set<string>();
58
+ for (const suite of suites) {
59
+ const seeded = new Set<string>(setupProduced);
60
+ for (const step of suite.tests) collectCapturesAndSets(step, seeded);
61
+ for (const step of suite.tests) {
62
+ for (const v of collectStepRefs(step)) {
63
+ if (requiredEmptyVars.has(v) && !seeded.has(v)) rootSet.add(v);
64
+ }
65
+ }
66
+ }
67
+ const unseededRoots = [...rootSet].sort().map(variable => ({ variable }));
68
+
69
+ // Undefined vars = preflight hits, minus the roots we already called out
70
+ // (a root absent from env would otherwise land in both buckets).
71
+ const byVar = new Map<string, { refs: number; suites: Set<string> }>();
72
+ for (const h of preflightCheckVars(suites, env)) {
73
+ if (rootSet.has(h.variable)) continue;
74
+ let e = byVar.get(h.variable);
75
+ if (!e) { e = { refs: 0, suites: new Set() }; byVar.set(h.variable, e); }
76
+ e.refs++;
77
+ e.suites.add(h.suite);
78
+ }
79
+ const undefinedVars = [...byVar.entries()]
80
+ .map(([variable, e]) => ({ variable, refs: e.refs, suites: [...e.suites].sort() }))
81
+ .sort((a, b) => a.variable.localeCompare(b.variable));
82
+
83
+ return { undefinedVars, unseededRoots };
84
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * ARV-324: `.fixture-gaps.yaml` — per-API sidecar recording operations
3
+ * that `prepare-fixtures`/`discover` already confirmed return a client
4
+ * error (or an empty list) while hunting for fixture values — e.g. an
5
+ * empty/under-provisioned test account.
6
+ *
7
+ * `checks run` runs as a separate process invocation and, without this
8
+ * file, has no way to tell "known fixture gap" apart from "new backend
9
+ * bug" — both would land on the same operation with
10
+ * `recommended_action: report_backend_bug`. Loading this file lets the
11
+ * classifier downgrade to `fix_fixture` instead (see
12
+ * `core/classifier/recommended-action.ts`).
13
+ *
14
+ * Not the manifest (`.api-fixtures.yaml` stays spec-derived/read-only,
15
+ * regenerated only by `refresh-api`) — this is runtime-observed state,
16
+ * so it gets its own file, rewritten wholesale on every
17
+ * discover/prepare-fixtures run so a since-fixed gap doesn't linger.
18
+ */
19
+
20
+ import { join } from "node:path";
21
+ import { writeFile } from "node:fs/promises";
22
+
23
+ export interface FixtureGap {
24
+ method: string;
25
+ path: string;
26
+ resource: string;
27
+ var: string;
28
+ reason: string;
29
+ }
30
+
31
+ const FILENAME = ".fixture-gaps.yaml";
32
+
33
+ export async function readFixtureGaps(apiDir: string): Promise<FixtureGap[]> {
34
+ const file = Bun.file(join(apiDir, FILENAME));
35
+ if (!(await file.exists())) return [];
36
+ let parsed: unknown;
37
+ try {
38
+ parsed = Bun.YAML.parse(await file.text());
39
+ } catch {
40
+ return [];
41
+ }
42
+ if (!parsed || typeof parsed !== "object" || !Array.isArray((parsed as { gaps?: unknown }).gaps)) {
43
+ return [];
44
+ }
45
+ return (parsed as { gaps: FixtureGap[] }).gaps;
46
+ }
47
+
48
+ export async function writeFixtureGaps(apiDir: string, gaps: FixtureGap[]): Promise<void> {
49
+ const path = join(apiDir, FILENAME);
50
+ const header = `# .fixture-gaps.yaml — auto-written by prepare-fixtures/discover (ARV-324)
51
+ #
52
+ # Operations that returned a client error (or an empty list) while zond
53
+ # hunted for a fixture value — a resource that's empty/inaccessible in
54
+ # the target API. Read by \`checks run\` so it doesn't relabel the same
55
+ # known gap as report_backend_bug. Safe to delete — regenerated on the
56
+ # next prepare-fixtures / discover run.
57
+ `;
58
+ const { stringify } = await import("yaml");
59
+ const body = stringify({ gaps }, { lineWidth: 0, defaultStringType: "PLAIN" });
60
+ await writeFile(path, header + "\n" + body, "utf-8");
61
+ }
62
+
63
+ /** Key an operation the same way on both the write side (discover/bootstrap,
64
+ * always GET list-probes) and the read side (`CheckFinding.operation`). */
65
+ export function gapKey(method: string, path: string): string {
66
+ return `${method.toUpperCase()} ${path}`;
67
+ }
68
+
69
+ export function gapIndex(gaps: FixtureGap[]): Set<string> {
70
+ return new Set(gaps.map((g) => gapKey(g.method, g.path)));
71
+ }