@kirrosh/zond 0.26.0 → 0.27.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 (261) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +22 -21
  3. package/bin/zond.mjs +23 -0
  4. package/package.json +13 -16
  5. package/scripts/npm/postinstall.mjs +76 -0
  6. package/src/CLAUDE.md +0 -112
  7. package/src/bun-types.d.ts +0 -5
  8. package/src/cli/argv.ts +0 -122
  9. package/src/cli/commands/add-api.ts +0 -146
  10. package/src/cli/commands/api/annotate/idempotency.ts +0 -59
  11. package/src/cli/commands/api/annotate/index.ts +0 -880
  12. package/src/cli/commands/api/annotate/lifecycle.ts +0 -74
  13. package/src/cli/commands/api/annotate/overlay.ts +0 -206
  14. package/src/cli/commands/api/annotate/pagination.ts +0 -64
  15. package/src/cli/commands/api/annotate/prompts.ts +0 -220
  16. package/src/cli/commands/api/annotate/readback.ts +0 -58
  17. package/src/cli/commands/api/annotate/resources.ts +0 -91
  18. package/src/cli/commands/api/annotate/seed-bodies.ts +0 -61
  19. package/src/cli/commands/audit.ts +0 -786
  20. package/src/cli/commands/catalog.ts +0 -97
  21. package/src/cli/commands/check.ts +0 -361
  22. package/src/cli/commands/checks.ts +0 -1072
  23. package/src/cli/commands/ci-init.ts +0 -223
  24. package/src/cli/commands/clean.ts +0 -212
  25. package/src/cli/commands/cleanup.ts +0 -236
  26. package/src/cli/commands/completions.ts +0 -192
  27. package/src/cli/commands/coverage.ts +0 -872
  28. package/src/cli/commands/db.ts +0 -611
  29. package/src/cli/commands/describe.ts +0 -120
  30. package/src/cli/commands/discover.ts +0 -1356
  31. package/src/cli/commands/doctor.ts +0 -661
  32. package/src/cli/commands/fixtures.ts +0 -402
  33. package/src/cli/commands/generate.ts +0 -577
  34. package/src/cli/commands/init/agents-md.ts +0 -61
  35. package/src/cli/commands/init/bootstrap.ts +0 -111
  36. package/src/cli/commands/init/index.ts +0 -244
  37. package/src/cli/commands/init/skills.ts +0 -141
  38. package/src/cli/commands/init/templates/agents.md +0 -86
  39. package/src/cli/commands/init/templates/markdown.d.ts +0 -4
  40. package/src/cli/commands/init/templates/skills/warm-up-target.md +0 -122
  41. package/src/cli/commands/init/templates/skills/zond-checks.md +0 -621
  42. package/src/cli/commands/init/templates/skills/zond-seed.md +0 -114
  43. package/src/cli/commands/init/templates/skills/zond-triage.md +0 -272
  44. package/src/cli/commands/init/templates/skills/zond.md +0 -861
  45. package/src/cli/commands/init/templates/zond-config.yml +0 -14
  46. package/src/cli/commands/prepare-fixtures.ts +0 -97
  47. package/src/cli/commands/probe/_seed-bodies.ts +0 -52
  48. package/src/cli/commands/probe/mass-assignment.ts +0 -594
  49. package/src/cli/commands/probe/security.ts +0 -537
  50. package/src/cli/commands/probe/static.ts +0 -255
  51. package/src/cli/commands/probe/webhooks.ts +0 -163
  52. package/src/cli/commands/probe.ts +0 -535
  53. package/src/cli/commands/reference.ts +0 -87
  54. package/src/cli/commands/refresh-api.ts +0 -227
  55. package/src/cli/commands/remove-api.ts +0 -150
  56. package/src/cli/commands/report-bundle.ts +0 -310
  57. package/src/cli/commands/report.ts +0 -241
  58. package/src/cli/commands/request.ts +0 -548
  59. package/src/cli/commands/run.ts +0 -1142
  60. package/src/cli/commands/schema-from-runs.ts +0 -128
  61. package/src/cli/commands/secrets.ts +0 -133
  62. package/src/cli/commands/session.ts +0 -244
  63. package/src/cli/commands/use.ts +0 -74
  64. package/src/cli/index.ts +0 -55
  65. package/src/cli/json-envelope.ts +0 -108
  66. package/src/cli/json-schemas.ts +0 -314
  67. package/src/cli/output.ts +0 -40
  68. package/src/cli/program.ts +0 -219
  69. package/src/cli/resolve.ts +0 -105
  70. package/src/cli/runtime.ts +0 -7
  71. package/src/cli/safe-live.ts +0 -24
  72. package/src/cli/status-filter.ts +0 -114
  73. package/src/cli/util/api-context.ts +0 -85
  74. package/src/cli/version.ts +0 -8
  75. package/src/core/audit/persist.ts +0 -183
  76. package/src/core/checks/budget.ts +0 -59
  77. package/src/core/checks/checks/_crud-helpers.ts +0 -133
  78. package/src/core/checks/checks/_negative_mutator.ts +0 -133
  79. package/src/core/checks/checks/_readback-helpers.ts +0 -133
  80. package/src/core/checks/checks/content_type_conformance.ts +0 -39
  81. package/src/core/checks/checks/cross_call_references.ts +0 -147
  82. package/src/core/checks/checks/cursor_boundary_fuzzing.ts +0 -219
  83. package/src/core/checks/checks/ensure_resource_availability.ts +0 -62
  84. package/src/core/checks/checks/idempotency_replay.ts +0 -242
  85. package/src/core/checks/checks/ignored_auth.ts +0 -254
  86. package/src/core/checks/checks/index.ts +0 -68
  87. package/src/core/checks/checks/lifecycle_transitions.ts +0 -416
  88. package/src/core/checks/checks/missing_required_header.ts +0 -40
  89. package/src/core/checks/checks/negative_data_rejection.ts +0 -148
  90. package/src/core/checks/checks/not_a_server_error.ts +0 -35
  91. package/src/core/checks/checks/open_cors_on_sensitive.ts +0 -160
  92. package/src/core/checks/checks/pagination_invariants.ts +0 -419
  93. package/src/core/checks/checks/positive_data_acceptance.ts +0 -33
  94. package/src/core/checks/checks/rate_limit_headers_absent.ts +0 -77
  95. package/src/core/checks/checks/response_headers_conformance.ts +0 -74
  96. package/src/core/checks/checks/response_schema_conformance.ts +0 -30
  97. package/src/core/checks/checks/status_code_conformance.ts +0 -132
  98. package/src/core/checks/checks/unsupported_method.ts +0 -63
  99. package/src/core/checks/checks/use_after_free.ts +0 -78
  100. package/src/core/checks/index.ts +0 -30
  101. package/src/core/checks/mode.ts +0 -82
  102. package/src/core/checks/recommended-action.ts +0 -68
  103. package/src/core/checks/registry.ts +0 -78
  104. package/src/core/checks/runner.ts +0 -1461
  105. package/src/core/checks/sarif.ts +0 -230
  106. package/src/core/checks/spec-findings.ts +0 -308
  107. package/src/core/checks/stateful.ts +0 -121
  108. package/src/core/checks/types.ts +0 -305
  109. package/src/core/checks/zond-extensions.ts +0 -73
  110. package/src/core/classifier/recommended-action.ts +0 -251
  111. package/src/core/context/current.ts +0 -51
  112. package/src/core/context/session.ts +0 -78
  113. package/src/core/coverage/loader.ts +0 -216
  114. package/src/core/coverage/reasons.ts +0 -300
  115. package/src/core/diagnostics/db-analysis.ts +0 -666
  116. package/src/core/diagnostics/failure-class.ts +0 -140
  117. package/src/core/diagnostics/failure-hints.ts +0 -112
  118. package/src/core/diagnostics/spec-pointer.ts +0 -99
  119. package/src/core/diagnostics/suggested-fixes.ts +0 -155
  120. package/src/core/exporter/case-study/index.ts +0 -270
  121. package/src/core/exporter/curl.ts +0 -40
  122. package/src/core/exporter/exporter.ts +0 -48
  123. package/src/core/exporter/html-report/escape.ts +0 -24
  124. package/src/core/exporter/html-report/index.ts +0 -479
  125. package/src/core/exporter/html-report/script.ts +0 -100
  126. package/src/core/exporter/html-report/styles.ts +0 -408
  127. package/src/core/generator/catalog-builder.ts +0 -179
  128. package/src/core/generator/chunker.ts +0 -78
  129. package/src/core/generator/coverage-phase.ts +0 -0
  130. package/src/core/generator/coverage-scanner.ts +0 -87
  131. package/src/core/generator/data-factory.ts +0 -759
  132. package/src/core/generator/describe.ts +0 -302
  133. package/src/core/generator/endpoint-warnings.ts +0 -52
  134. package/src/core/generator/fixtures-builder.ts +0 -332
  135. package/src/core/generator/index.ts +0 -14
  136. package/src/core/generator/openapi-reader.ts +0 -297
  137. package/src/core/generator/path-param-disambig.ts +0 -140
  138. package/src/core/generator/resources-builder.ts +0 -898
  139. package/src/core/generator/schema-utils.ts +0 -112
  140. package/src/core/generator/serializer.ts +0 -343
  141. package/src/core/generator/suite-generator.ts +0 -1301
  142. package/src/core/generator/types.ts +0 -63
  143. package/src/core/identity/identity-file.ts +0 -0
  144. package/src/core/lint/affects.ts +0 -28
  145. package/src/core/lint/config.ts +0 -96
  146. package/src/core/lint/format.ts +0 -42
  147. package/src/core/lint/index.ts +0 -94
  148. package/src/core/lint/reporter.ts +0 -128
  149. package/src/core/lint/rules/consistency.ts +0 -158
  150. package/src/core/lint/rules/heuristics.ts +0 -97
  151. package/src/core/lint/rules/strictness.ts +0 -109
  152. package/src/core/lint/types.ts +0 -96
  153. package/src/core/lint/walker.ts +0 -248
  154. package/src/core/meta/meta-store.ts +0 -11
  155. package/src/core/output/README.md +0 -73
  156. package/src/core/output/index.ts +0 -13
  157. package/src/core/output/run.ts +0 -91
  158. package/src/core/output/types.ts +0 -122
  159. package/src/core/parser/dynamic-values.ts +0 -160
  160. package/src/core/parser/env-interpolation.ts +0 -104
  161. package/src/core/parser/filter.ts +0 -97
  162. package/src/core/parser/schema.ts +0 -359
  163. package/src/core/parser/types.ts +0 -117
  164. package/src/core/parser/variables.ts +0 -0
  165. package/src/core/parser/yaml-parser.ts +0 -181
  166. package/src/core/probe/bootstrap.ts +0 -34
  167. package/src/core/probe/dry-run-envelope.ts +0 -61
  168. package/src/core/probe/mass-assignment/classify.ts +0 -175
  169. package/src/core/probe/mass-assignment/cleanup.ts +0 -52
  170. package/src/core/probe/mass-assignment/digest.ts +0 -114
  171. package/src/core/probe/mass-assignment/orchestrator.ts +0 -459
  172. package/src/core/probe/mass-assignment/regression.ts +0 -141
  173. package/src/core/probe/mass-assignment/suspects.ts +0 -92
  174. package/src/core/probe/mass-assignment/types.ts +0 -135
  175. package/src/core/probe/mass-assignment-probe-class.ts +0 -198
  176. package/src/core/probe/mass-assignment-probe.ts +0 -27
  177. package/src/core/probe/mass-assignment-template.ts +0 -240
  178. package/src/core/probe/method-probe.ts +0 -164
  179. package/src/core/probe/method-shared.ts +0 -69
  180. package/src/core/probe/negative-probe.ts +0 -691
  181. package/src/core/probe/orphan-tracker.ts +0 -188
  182. package/src/core/probe/path-discovery.ts +0 -439
  183. package/src/core/probe/probe-harness.ts +0 -119
  184. package/src/core/probe/registry.ts +0 -89
  185. package/src/core/probe/runner.ts +0 -136
  186. package/src/core/probe/security/baseline.ts +0 -174
  187. package/src/core/probe/security/classify.ts +0 -341
  188. package/src/core/probe/security/cleanup.ts +0 -125
  189. package/src/core/probe/security/detectors.ts +0 -71
  190. package/src/core/probe/security/digest.ts +0 -104
  191. package/src/core/probe/security/orchestrator.ts +0 -398
  192. package/src/core/probe/security/regression.ts +0 -103
  193. package/src/core/probe/security/types.ts +0 -151
  194. package/src/core/probe/security-probe-class.ts +0 -207
  195. package/src/core/probe/security-probe.ts +0 -32
  196. package/src/core/probe/shared.ts +0 -531
  197. package/src/core/probe/static-probe-class.ts +0 -125
  198. package/src/core/probe/types.ts +0 -165
  199. package/src/core/probe/verdict-aggregator.ts +0 -33
  200. package/src/core/probe/webhooks-probe.ts +0 -282
  201. package/src/core/reporter/console.ts +0 -240
  202. package/src/core/reporter/index.ts +0 -22
  203. package/src/core/reporter/json.ts +0 -22
  204. package/src/core/reporter/junit.ts +0 -93
  205. package/src/core/reporter/ndjson.ts +0 -37
  206. package/src/core/reporter/types.ts +0 -15
  207. package/src/core/runner/assertions.ts +0 -383
  208. package/src/core/runner/async-pool.ts +0 -108
  209. package/src/core/runner/auth-path.ts +0 -8
  210. package/src/core/runner/ci-context.ts +0 -72
  211. package/src/core/runner/executor.ts +0 -652
  212. package/src/core/runner/expr-eval.ts +0 -41
  213. package/src/core/runner/form-encode.ts +0 -41
  214. package/src/core/runner/http-client.ts +0 -224
  215. package/src/core/runner/learn-drift.ts +0 -293
  216. package/src/core/runner/preflight-vars.ts +0 -153
  217. package/src/core/runner/progress-tracker.ts +0 -73
  218. package/src/core/runner/rate-limiter.ts +0 -185
  219. package/src/core/runner/run-kind.ts +0 -45
  220. package/src/core/runner/schema-validator.ts +0 -308
  221. package/src/core/runner/send-request.ts +0 -232
  222. package/src/core/runner/transforms.ts +0 -65
  223. package/src/core/runner/types.ts +0 -94
  224. package/src/core/secrets/registry.ts +0 -164
  225. package/src/core/secrets/secrets-file.ts +0 -115
  226. package/src/core/selectors/operation-filter.ts +0 -144
  227. package/src/core/setup-api.ts +0 -590
  228. package/src/core/severity/category.ts +0 -94
  229. package/src/core/severity/index.ts +0 -58
  230. package/src/core/spec/infer-schema.ts +0 -102
  231. package/src/core/spec/layers.ts +0 -154
  232. package/src/core/spec/merge-specs.ts +0 -156
  233. package/src/core/spec/schema-from-runs.ts +0 -117
  234. package/src/core/spec/schema-overlay.ts +0 -130
  235. package/src/core/util/ajv.ts +0 -13
  236. package/src/core/util/format-eta.ts +0 -21
  237. package/src/core/util/headers.ts +0 -9
  238. package/src/core/util/url.ts +0 -24
  239. package/src/core/utils.ts +0 -13
  240. package/src/core/workspace/config.ts +0 -129
  241. package/src/core/workspace/fixture-gap-report.ts +0 -84
  242. package/src/core/workspace/fixture-gaps.ts +0 -71
  243. package/src/core/workspace/manifest.ts +0 -283
  244. package/src/core/workspace/output-rotation.ts +0 -62
  245. package/src/core/workspace/root.ts +0 -96
  246. package/src/core/workspace/triage-path.ts +0 -87
  247. package/src/db/lint-runs.ts +0 -47
  248. package/src/db/migrate.ts +0 -128
  249. package/src/db/migrations/0001_run_kind.sql +0 -25
  250. package/src/db/migrations/0002_run_kind_request.sql +0 -59
  251. package/src/db/migrations/sql.d.ts +0 -4
  252. package/src/db/queries/collections.ts +0 -133
  253. package/src/db/queries/coverage.ts +0 -9
  254. package/src/db/queries/dashboard.ts +0 -59
  255. package/src/db/queries/results.ts +0 -216
  256. package/src/db/queries/runs.ts +0 -289
  257. package/src/db/queries/sessions.ts +0 -42
  258. package/src/db/queries/settings.ts +0 -28
  259. package/src/db/queries/types.ts +0 -172
  260. package/src/db/queries.ts +0 -75
  261. package/src/db/schema.ts +0 -298
@@ -1,577 +0,0 @@
1
- import { join, resolve as resolvePath } from "path";
2
- import { existsSync } from "fs";
3
- import { mkdir } from "fs/promises";
4
- import {
5
- readOpenApiSpec,
6
- extractEndpoints,
7
- extractSecuritySchemes,
8
- scanCoveredEndpoints,
9
- filterUncoveredEndpoints,
10
- serializeSuite,
11
- } from "../../core/generator/index.ts";
12
- import {
13
- generateSuites,
14
- findUnresolvedVars,
15
- detectCrudGroupsWithDiagnostics,
16
- } from "../../core/generator/suite-generator.ts";
17
- import { generateFromSchema, classifyFieldSource } from "../../core/generator/data-factory.ts";
18
- import { filterByTag, collectTags } from "../../core/generator/chunker.ts";
19
- import { compileOperationFilter } from "../../core/selectors/operation-filter.ts";
20
- import { parse } from "../../core/parser/yaml-parser.ts";
21
- import { loadEnvironment } from "../../core/parser/variables.ts";
22
- import { printError, printSuccess } from "../output.ts";
23
- import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
24
- import { getDb } from "../../db/schema.ts";
25
- import { findCollectionByTestPath, updateCollection } from "../../db/queries.ts";
26
- import { findWorkspaceRoot } from "../../core/workspace/root.ts";
27
- import { recordGeneratedFiles, inferApiName, autoGenHeader, type RecordInput } from "../../core/workspace/manifest.ts";
28
-
29
- /**
30
- * Walk up from outputDir looking for the API root — the first ancestor
31
- * that already contains `.api-catalog.yaml` (= a directory `zond add api`
32
- * has owned). Falls back to undefined when called from a non-conventional
33
- * layout, in which case the caller writes `.env.yaml` next to outputDir.
34
- *
35
- * The walk stops at filesystem root (or HOME). The optional baseUrl is
36
- * unused at the moment but kept on the signature so callers don't have
37
- * to recompute the conditions for "should we even bother" — when no
38
- * env vars are needed, the caller skips this entirely.
39
- */
40
- function resolveApiRoot(outputDir: string, _baseUrl: string | undefined): string | undefined {
41
- const abs = resolvePath(outputDir);
42
- // 1) Walk up looking for an existing `.api-catalog.yaml` — strongest signal.
43
- let dir = abs;
44
- for (let i = 0; i < 8; i++) {
45
- if (existsSync(join(dir, ".api-catalog.yaml"))) return dir;
46
- const parent = resolvePath(dir, "..");
47
- if (parent === dir) break;
48
- dir = parent;
49
- }
50
- // 2) Fall back to the conventional layout: …/apis/<name>/[anything]/. The
51
- // API root is the directory immediately under `apis/`. Picks up the
52
- // case where the user runs `zond generate` before `zond add api`.
53
- const norm = abs.replace(/\\/g, "/");
54
- const m = norm.match(/^(.*?\/apis\/[^/]+)(?:\/|$)/);
55
- return m?.[1];
56
- }
57
-
58
- export interface GenerateOptions {
59
- specPath: string;
60
- output: string;
61
- tag?: string;
62
- uncoveredOnly?: boolean;
63
- /** When true, deprecated endpoints are included in generation. Default
64
- * (false) filters them out and surfaces the count as a warning so users
65
- * can distinguish "deprecated by design" from "accidentally dropped". */
66
- includeDeprecated?: boolean;
67
- /** TASK-139: dry-run that prints per-resource CRUD detection verdict and
68
- * exits — no files written. Use to debug "why didn't generate emit a
69
- * CRUD chain for resource X?" on real specs. */
70
- explain?: boolean;
71
- /** TASK-219/ARV-361: overwrite hand-edited suite files (those whose
72
- * auto-gen header was removed). Default false = preserve them; files that
73
- * still carry the header are regenerated regardless. */
74
- force?: boolean;
75
- json?: boolean;
76
- /** ARV-9 unified filter: path:<regex> / method:<csv> / tag:<csv> /
77
- * operation-id:<regex>. Multiple flags combine with OR; --exclude
78
- * always removes. Stacks with --tag for back-compat. */
79
- include?: string[];
80
- exclude?: string[];
81
- /** ARV-212 (R13/F16, R14): the explicit --api name. Lets generate read
82
- * apis/<name>/.env.yaml directly even when --output points outside the
83
- * apis/<name>/ tree (e.g. /tmp/<scratch>), where resolveApiRoot /
84
- * inferApiName cannot recover the name from the path. */
85
- apiName?: string;
86
- /** ARV-212: explicit override for apis/<name>/ root. Caller pre-resolved
87
- * it through the DB (base_dir column) for the case where the API was
88
- * registered in a non-standard layout. */
89
- apiDir?: string;
90
- }
91
-
92
- export async function generateCommand(options: GenerateOptions): Promise<number> {
93
- try {
94
- const doc = await readOpenApiSpec(options.specPath);
95
- const allEndpoints = extractEndpoints(doc);
96
- let endpoints = allEndpoints;
97
- const securitySchemes = extractSecuritySchemes(doc);
98
-
99
- // --explain short-circuits: print the CRUD detection table and exit.
100
- if (options.explain) {
101
- let scope = endpoints;
102
- if (options.tag) scope = filterByTag(scope, options.tag);
103
- const { groups, diagnostics } = detectCrudGroupsWithDiagnostics(scope);
104
- // Per-field body sources (TASK-269) — same scope as the table below.
105
- const bodyFieldSources = scope
106
- .filter(ep => ep.requestBodySchema && (ep.requestBodySchema as any).type === "object" &&
107
- (ep.requestBodySchema as any).properties)
108
- .map(ep => {
109
- const props = (ep.requestBodySchema as any).properties as Record<string, any>;
110
- const fields = Object.entries(props)
111
- .filter(([k, s]) => !(s.readOnly === true) && k !== "id")
112
- .map(([key, s]) => ({
113
- field: key,
114
- type: Array.isArray(s.type)
115
- ? (s.type as string[]).find(x => x !== "null") ?? "any"
116
- : (s.type ?? "any"),
117
- value: generateFromSchema(s, key),
118
- source: classifyFieldSource(s, key),
119
- }));
120
- return { method: ep.method.toUpperCase(), path: ep.path, fields };
121
- })
122
- .filter(e => e.fields.length > 0);
123
-
124
- if (options.json) {
125
- printJson(jsonOk("generate", {
126
- mode: "explain",
127
- totalCandidates: diagnostics.length,
128
- chains: groups.length,
129
- diagnostics,
130
- bodyFieldSources,
131
- }));
132
- } else {
133
- if (diagnostics.length === 0) {
134
- console.log("No POST endpoints in scope — nothing to evaluate.");
135
- } else {
136
- const chains = diagnostics.filter(d => d.verdict === "chain").length;
137
- console.log(`CRUD detection: ${chains}/${diagnostics.length} POST endpoints became chain candidates.\n`);
138
- const headers = ["resource", "post", "get/{id}", "put/patch", "delete", "list", "verdict", "reason"];
139
- const rows = diagnostics.map(d => [
140
- d.resource,
141
- d.postPath,
142
- d.hasGetById ? "✓" : "—",
143
- d.hasUpdate ? "✓" : "—",
144
- d.hasDelete ? "✓" : "—",
145
- d.hasList ? "✓" : "—",
146
- d.verdict,
147
- d.reason,
148
- ]);
149
- const widths = headers.map((h, i) =>
150
- Math.max(h.length, ...rows.map(r => r[i]!.length)),
151
- );
152
- const fmt = (cells: string[]) =>
153
- cells.map((c, i) => c.padEnd(widths[i]!)).join(" ");
154
- console.log(fmt(headers));
155
- console.log(widths.map(w => "─".repeat(w)).join(" "));
156
- for (const row of rows) console.log(fmt(row));
157
- }
158
-
159
- // TASK-269: per-field provenance for endpoints carrying a request
160
- // body. Helps debug "why did the API 400 on field X?" without
161
- // re-running with --json and inspecting the generated suite.
162
- if (bodyFieldSources.length > 0) {
163
- console.log("");
164
- console.log("Body field sources:");
165
- for (const ep of bodyFieldSources) {
166
- console.log(` ${ep.method} ${ep.path}`);
167
- const fHeaders = ["field", "type", "value", "source"];
168
- const rows2 = ep.fields.map(f => [
169
- f.field,
170
- String(f.type),
171
- typeof f.value === "string" ? f.value : JSON.stringify(f.value),
172
- `[${f.source}]`,
173
- ]);
174
- const fAll = [fHeaders, ...rows2];
175
- const fWidths = fHeaders.map((h, i) =>
176
- Math.max(h.length, ...fAll.map(r => r[i]!.length)),
177
- );
178
- const ffmt = (cells: string[]) =>
179
- cells.map((c, i) => c.padEnd(fWidths[i]!)).join(" ");
180
- console.log(" " + ffmt(fHeaders));
181
- console.log(" " + fWidths.map(w => "─".repeat(w)).join(" "));
182
- for (const r of rows2) console.log(" " + ffmt(r));
183
- }
184
- }
185
- }
186
- return 0;
187
- }
188
- const baseUrl = ((doc as any).servers?.[0]?.url) as string | undefined;
189
- const warnings: string[] = [];
190
-
191
- // Filter to uncovered only
192
- if (options.uncoveredOnly) {
193
- const covered = await scanCoveredEndpoints(options.output);
194
- const before = endpoints.length;
195
- endpoints = filterUncoveredEndpoints(endpoints, covered);
196
- const coveredCount = before - endpoints.length;
197
- if (coveredCount > 0) {
198
- warnings.push(`Skipped ${coveredCount} already-covered endpoints`);
199
- }
200
- }
201
-
202
- // ARV-9: unified --include/--exclude filter (applied before --tag so
203
- // --tag stays a thin alias when both are passed; usually only one is).
204
- if (options.include?.length || options.exclude?.length) {
205
- const compiled = compileOperationFilter({ includes: options.include, excludes: options.exclude });
206
- if (compiled.errors.length > 0) {
207
- const message = compiled.errors.join("\n");
208
- if (options.json) printJson(jsonOk("generate", { files: [], message }, compiled.errors));
209
- else printError(message);
210
- return 2;
211
- }
212
- endpoints = endpoints.filter(compiled.filter);
213
- }
214
-
215
- // Filter by tag
216
- let tagDiagnostic: string | undefined;
217
- if (options.tag) {
218
- const beforeTag = endpoints;
219
- endpoints = filterByTag(endpoints, options.tag);
220
- // TASK-232: when --tag matches nothing, show the available tags so the
221
- // user can tell "typo" apart from "spec really has no endpoints here".
222
- // Cheap nearest-match: pick the first tag containing or contained-by the
223
- // requested string (case-insensitive); covers "Members" → "Member".
224
- if (endpoints.length === 0 && beforeTag.length > 0) {
225
- const available = collectTags(beforeTag);
226
- const wanted = options.tag.trim().toLowerCase();
227
- const closest = available.find(t => {
228
- const tl = t.toLowerCase();
229
- return tl.includes(wanted) || wanted.includes(tl);
230
- });
231
- const hint = closest ? ` (did you mean ${closest}?)` : "";
232
- const list = available.length > 0 ? available.join(", ") : "(none)";
233
- tagDiagnostic = `No endpoints with tag '${options.tag}'${hint}. Available tags: ${list}`;
234
- }
235
- }
236
-
237
- if (endpoints.length === 0) {
238
- const message = tagDiagnostic ?? "No endpoints to generate tests for";
239
- if (options.json) {
240
- printJson(jsonOk("generate", { files: [], message }, warnings));
241
- } else {
242
- console.log(`${message}.`);
243
- }
244
- return 0;
245
- }
246
-
247
- // Count deprecated endpoints before generateSuites filters them — we
248
- // surface the count as a warning so deprecated-by-design and
249
- // accidentally-dropped look different in stdout.
250
- const deprecatedSkipped = options.includeDeprecated
251
- ? []
252
- : endpoints.filter(ep => ep.deprecated).map(ep => `${ep.method} ${ep.path}`);
253
-
254
- // ARV-212 (R13/F16, R14): peek at .env.yaml *before* generating suites so
255
- // we can pass `defaultAuthVar` into generateSuites when the spec has no
256
- // securitySchemes but the workspace is wired for Bearer auth (the
257
- // ARV-201 seed in setup-api.ts). Without this, GitHub-style suites go
258
- // unauth and brick on the first rate-limited 60 requests.
259
- //
260
- // R14 fix: do NOT rely on resolveApiRoot(options.output) — when the
261
- // user passes --output to a scratch directory outside apis/<name>/
262
- // (e.g. /tmp/foo), the resolver returns undefined and we fall back to
263
- // the output dir, which has no .env.yaml. Prefer the explicit --api
264
- // name to construct apis/<name>/ inside the workspace.
265
- const envForWarnings: Record<string, unknown> = {};
266
- try {
267
- let envDir: string | undefined = options.apiDir;
268
- if (!envDir && options.apiName) {
269
- const ws = findWorkspaceRoot();
270
- envDir = resolvePath(ws.root, "apis", options.apiName);
271
- }
272
- if (!envDir) envDir = resolveApiRoot(options.output, baseUrl) ?? options.output;
273
- Object.assign(envForWarnings, await loadEnvironment(undefined, envDir));
274
- } catch { /* env load failures stay silent — original behaviour for missing files */ }
275
-
276
- let defaultAuthVar: string | undefined;
277
- if (securitySchemes.length === 0 && "auth_token" in envForWarnings) {
278
- // Presence-not-value: an empty .secrets.yaml.auth_token resolves to ""
279
- // here, but the .env.yaml wiring is what matters. Once the user fills
280
- // .secrets.yaml the generated suite picks up the Bearer header without
281
- // a regenerate.
282
- defaultAuthVar = "auth_token";
283
- }
284
-
285
- // Generate suites
286
- const suites = generateSuites({
287
- endpoints,
288
- securitySchemes,
289
- specPath: options.specPath,
290
- includeDeprecated: options.includeDeprecated,
291
- defaultAuthVar,
292
- });
293
-
294
- const missingPathParams = new Set<string>();
295
- let endpointsMissingPathExamples = 0;
296
- for (const ep of endpoints) {
297
- let epHadMiss = false;
298
- for (const p of ep.parameters) {
299
- if (p.in !== "path" || !p.required) continue;
300
- const hasExample =
301
- p.example !== undefined ||
302
- (p.schema && (p.schema as any).example !== undefined) ||
303
- (p.schema && (p.schema as any).default !== undefined);
304
- const filledInEnv = (() => {
305
- const v = envForWarnings[p.name];
306
- return typeof v === "string" && v.length > 0 && !v.startsWith("{{");
307
- })();
308
- if (!hasExample && !filledInEnv) {
309
- missingPathParams.add(p.name);
310
- epHadMiss = true;
311
- }
312
- }
313
- if (epHadMiss) endpointsMissingPathExamples++;
314
- }
315
- if (missingPathParams.size > 0) {
316
- const sample = [...missingPathParams].sort().slice(0, 3).join(", ");
317
- const more = missingPathParams.size > 3 ? `, +${missingPathParams.size - 3} more` : "";
318
- warnings.push(
319
- `${missingPathParams.size} path param(s) have no examples (${sample}${more}) on ${endpointsMissingPathExamples} endpoint(s) — fill apis/<name>/.env.yaml to enable positive/smoke-positive suites`,
320
- );
321
- }
322
-
323
- if (deprecatedSkipped.length > 0) {
324
- const head = deprecatedSkipped.slice(0, 3).join(", ");
325
- const more = deprecatedSkipped.length > 3 ? `, +${deprecatedSkipped.length - 3} more` : "";
326
- warnings.push(
327
- `Skipped ${deprecatedSkipped.length} deprecated endpoint(s): ${head}${more} — pass --include-deprecated to include`,
328
- );
329
- }
330
-
331
- // ARV-15: warn when in-scope endpoints will create/modify real resources.
332
- // POST/PUT/PATCH/DELETE on a live API send real traffic — e.g. an
333
- // email API's `POST /emails` literally sends mail; deleting a record
334
- // is irreversible.
335
- // Generation is harmless (just YAML), but `zond run` against the suite
336
- // is not, so the warning fires here so the user sees it before they grep
337
- // the output for what to run next.
338
- const unsafeOps = endpoints.filter(
339
- ep => ep.method !== "GET" && ep.method !== "HEAD" && ep.method !== "OPTIONS",
340
- );
341
- if (unsafeOps.length > 0) {
342
- const byMethod = new Map<string, number>();
343
- for (const ep of unsafeOps) {
344
- byMethod.set(ep.method, (byMethod.get(ep.method) ?? 0) + 1);
345
- }
346
- const breakdown = [...byMethod.entries()]
347
- .sort(([a], [b]) => a.localeCompare(b))
348
- .map(([m, n]) => `${n} ${m}`)
349
- .join(", ");
350
- warnings.push(
351
- `${unsafeOps.length} write endpoint(s) in scope (${breakdown}) — \`zond run\` on the resulting *-unsafe.yaml / crud-*.yaml suites will hit the real API. Use --include 'method:GET' for read-only smoke first.`,
352
- );
353
- }
354
-
355
- // Ensure output directory exists
356
- await mkdir(options.output, { recursive: true });
357
-
358
- // Write suite files
359
- // ARV-15: tag each created file as safe/unsafe based on suite tags so the
360
- // stdout summary can group them and the user can tell at a glance which
361
- // suites send writes/deletes vs. read-only smoke.
362
- const UNSAFE_TAGS = new Set(["unsafe", "crud", "system", "reset", "cleanup"]);
363
- const isUnsafeSuite = (s: typeof suites[number]) =>
364
- (s.tags ?? []).some(t => UNSAFE_TAGS.has(t));
365
- const createdFiles: Array<{ file: string; suite: string; tests: number; safety: "safe" | "unsafe" }> = [];
366
- const manifestEntries: RecordInput[] = [];
367
- const inferredApi = inferApiName(options.output);
368
-
369
- for (const suite of suites) {
370
- const yaml = serializeSuite(suite);
371
- const fileName = `${suite.fileStem ?? suite.name}.yaml`;
372
- const filePath = join(options.output, fileName);
373
- // m-24 (ARV-361): don't silently clobber agent-edited suites. The
374
- // auto-gen header already promises "drop the header (or rename) to
375
- // keep changes" — honour it deterministically: a file whose first line
376
- // is no longer the auto-gen marker is agent-owned, so preserve it
377
- // unless --force. Files that still carry the header get overwritten,
378
- // exactly as the header warns. Makes the previously no-op --force real.
379
- if (!options.force && existsSync(filePath)) {
380
- // Small yaml — whole-file read is fine; we only need the first line.
381
- const firstLine = (await Bun.file(filePath).text()).split("\n", 1)[0] ?? "";
382
- if (!firstLine.startsWith("# Auto-generated by")) {
383
- warnings.push(
384
- `Preserved ${fileName} — no auto-gen header, treated as hand-edited. Pass --force to overwrite.`,
385
- );
386
- continue;
387
- }
388
- }
389
- const header = autoGenHeader("zond generate", `zond generate --api <name> --output ${options.output}`);
390
- await Bun.write(filePath, header + yaml);
391
- createdFiles.push({
392
- file: filePath,
393
- suite: suite.name,
394
- tests: suite.tests.length,
395
- safety: isUnsafeSuite(suite) ? "unsafe" : "safe",
396
- });
397
- manifestEntries.push({
398
- path: filePath,
399
- by: "zond generate",
400
- api: inferredApi,
401
- category: "tests",
402
- });
403
- }
404
-
405
- // TASK-157 (m-9 P1): generate no longer writes `.api-catalog.yaml` into
406
- // options.output. The API-level catalog at `apis/<name>/.api-catalog.yaml`
407
- // is the single source of truth — `zond add api` / `zond refresh-api`
408
- // emit it.
409
-
410
- // Sync DB collection spec reference if one is registered for this output directory
411
- try {
412
- getDb();
413
- const collection = findCollectionByTestPath(options.output);
414
- if (collection && collection.openapi_spec !== options.specPath) {
415
- updateCollection(collection.id, { openapi_spec: options.specPath });
416
- warnings.push(`Updated collection '${collection.name}' spec reference → ${options.specPath}`);
417
- }
418
- } catch {
419
- // DB unavailable — not fatal
420
- }
421
-
422
- // TASK-158 (m-9 P2): the API-level `apis/<name>/.env.yaml` is the only
423
- // source of truth for runtime variables. We never write a duplicate
424
- // `tests/.env.yaml` — it would silently override the API-level file via
425
- // deeper-scope precedence, wiping the user's auth_token / FK ids on
426
- // every `zond generate`. If the API-level file is missing, we create it
427
- // there; if it already exists, we leave it alone (re-running generate
428
- // never clobbers values the user filled in).
429
- const envTargetDir = resolveApiRoot(options.output, baseUrl) ?? options.output;
430
- const envPath = join(envTargetDir, ".env.yaml");
431
- const envFile = Bun.file(envPath);
432
- if (!(await envFile.exists())) {
433
- const unresolvedVars = new Set<string>();
434
- for (const suite of suites) {
435
- for (const v of findUnresolvedVars(suite)) unresolvedVars.add(v);
436
- }
437
- const lines: string[] = [];
438
- if (baseUrl) lines.push(`base_url: ${baseUrl}`);
439
- for (const v of [...unresolvedVars].sort()) {
440
- lines.push(`${v}: "" # TODO: fill in`);
441
- }
442
- if (lines.length > 0) {
443
- await mkdir(envTargetDir, { recursive: true });
444
- await Bun.write(envPath, lines.join("\n") + "\n");
445
- warnings.push(`Created ${envPath} with ${unresolvedVars.size} placeholder variable(s)`);
446
- manifestEntries.push({
447
- path: envPath,
448
- by: "zond generate",
449
- api: inferredApi,
450
- category: "env",
451
- });
452
- }
453
- }
454
-
455
- // Record everything we wrote into .zond/manifest.json (TASK-156).
456
- try {
457
- const ws = findWorkspaceRoot();
458
- if (!ws.fromFallback && manifestEntries.length > 0) {
459
- recordGeneratedFiles(ws.root, manifestEntries);
460
- }
461
- } catch {
462
- // Manifest is best-effort; never fail the generate command on it.
463
- }
464
-
465
- // Validate generated files
466
- const validationErrors: string[] = [];
467
- try {
468
- await parse(options.output);
469
- } catch (err) {
470
- validationErrors.push(err instanceof Error ? err.message : String(err));
471
- }
472
-
473
- if (validationErrors.length > 0) {
474
- warnings.push(`Validation warnings: ${validationErrors.join("; ")}`);
475
- }
476
-
477
- // Output
478
- const totalTests = createdFiles.reduce((sum, f) => sum + f.tests, 0);
479
-
480
- if (options.json) {
481
- printJson(jsonOk("generate", {
482
- files: createdFiles,
483
- totalSuites: suites.length,
484
- totalTests,
485
- outputDir: options.output,
486
- }, warnings));
487
- } else {
488
- printSuccess(`Generated ${suites.length} suite(s) with ${totalTests} test(s) in ${options.output}`);
489
- // ARV-15: split safe vs unsafe so the user can see at a glance which
490
- // suites are read-only smoke and which will send writes/deletes.
491
- const safeFiles = createdFiles.filter(f => f.safety === "safe");
492
- const unsafeFiles = createdFiles.filter(f => f.safety === "unsafe");
493
- if (safeFiles.length > 0 && unsafeFiles.length > 0) {
494
- console.log(` Safe (read-only) — ${safeFiles.length} suite(s):`);
495
- for (const f of safeFiles) console.log(` ${f.file} (${f.tests} tests)`);
496
- console.log(` Unsafe (writes/deletes — hit live API) — ${unsafeFiles.length} suite(s):`);
497
- for (const f of unsafeFiles) console.log(` ${f.file} (${f.tests} tests)`);
498
- } else {
499
- for (const f of createdFiles) {
500
- console.log(` ${f.file} (${f.tests} tests)`);
501
- }
502
- }
503
- if (warnings.length > 0) {
504
- for (const w of warnings) {
505
- console.log(` ⚠ ${w}`);
506
- }
507
- }
508
- console.log("");
509
- console.log("Next steps:");
510
- console.log(" 1. Fill apis/<name>/.env.yaml with auth_token, real FK ids, verified emails, valid enums");
511
- console.log(" (the fixture pack — without it, {{$randomString}} loses 5+ iterations to format-validation)");
512
- console.log(" 2. zond run <output> --safe --report json # smoke (GET-only)");
513
- console.log(` 3. zond run <output> --tag crud,setup --validate-schema --spec ${options.specPath} --report json`);
514
- console.log(" (--validate-schema catches contract drift; recommended for every CRUD run)");
515
- }
516
-
517
- return 0;
518
- } catch (err) {
519
- const message = err instanceof Error ? err.message : String(err);
520
- if (options.json) {
521
- printJson(jsonError("generate", [message]));
522
- } else {
523
- // ARV-203: pass err so ZOND_DEBUG=1 dumps the stack and the user
524
- // gets a hint about the env var when running without it.
525
- printError(message, err);
526
- }
527
- return 2;
528
- }
529
- }
530
-
531
- import type { Command } from "commander";
532
- import { globalJson, resolveSpecArg } from "../resolve.ts";
533
- import { getApi } from "../util/api-context.ts";
534
-
535
- export function registerGenerate(program: Command): void {
536
- program
537
- .command("generate [spec]")
538
- .description("Generate test suites from OpenAPI spec. Re-run regenerates files that still carry the auto-gen header; hand-edited suites (header removed) are preserved unless --force (ARV-361). Body fields are filled with `{{$random*}}` helpers (slug/email/url/uuid/…) — see `zond reference random-helpers` or docs/random-helpers.md for the full list (TASK-267).")
539
- .option("--api <name>", "Use the registered API's spec (apis/<name>/spec.json)")
540
- .option("--db <path>", "Path to SQLite database file")
541
- .option("--output <dir>", "Output directory for generated test files (required unless --explain)")
542
- .option("--tag <tag>", "Generate only for endpoints with this tag (accepts comma-separated list, e.g. --tag Releases,Events,Alerts — TASK-239)")
543
- .option(
544
- "--include <spec...>",
545
- "ARV-9: keep only operations matching <selector>:<value>. Selectors: path:<regex>, method:<csv>, tag:<csv>, operation-id:<regex>. Repeat the flag for OR semantics.",
546
- )
547
- .option(
548
- "--exclude <spec...>",
549
- "ARV-9: drop operations matching <selector>:<value>. Same grammar as --include.",
550
- )
551
- .option("--uncovered-only", "Skip endpoints already covered by existing tests")
552
- .option("--include-deprecated", "Generate suites for deprecated endpoints too (filtered out by default)")
553
- .option("--explain", "Print the CRUD detection table (which resources became chain candidates and why) without writing files (TASK-139)")
554
- .option("--force, --overwrite", "ARV-361: overwrite even hand-edited suite files (those whose auto-gen header was removed). Default: preserve them and warn — files that still carry the header are always regenerated.")
555
- .action(async (specPos: string | undefined, opts, cmd: Command) => {
556
- const resolved = resolveSpecArg(specPos, opts.api, opts.db);
557
- if ("error" in resolved) { printError(resolved.error); process.exitCode = 2; return; }
558
- if (!opts.explain && !opts.output) {
559
- printError("--output <dir> is required (omit only when running with --explain).");
560
- process.exitCode = 2;
561
- return;
562
- }
563
- process.exitCode = await generateCommand({
564
- specPath: resolved.spec,
565
- output: opts.output ?? "",
566
- tag: opts.tag,
567
- uncoveredOnly: opts.uncoveredOnly === true,
568
- includeDeprecated: opts.includeDeprecated === true,
569
- explain: opts.explain === true,
570
- force: opts.force === true || opts.overwrite === true,
571
- json: globalJson(cmd),
572
- include: Array.isArray(opts.include) ? opts.include : undefined,
573
- exclude: Array.isArray(opts.exclude) ? opts.exclude : undefined,
574
- apiName: getApi(cmd, opts),
575
- });
576
- });
577
- }
@@ -1,61 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
-
4
- import agentsTemplate from "./templates/agents.md" with { type: "text" };
5
-
6
- export const START_MARKER = "<!-- zond:start -->";
7
- export const END_MARKER = "<!-- zond:end -->";
8
-
9
- export interface AgentsBlockResult {
10
- path: string;
11
- action: "created" | "updated" | "noop";
12
- }
13
-
14
- function blockBody(): string {
15
- return agentsTemplate.trim();
16
- }
17
-
18
- function wrap(body: string): string {
19
- return `${START_MARKER}\n${body}\n${END_MARKER}`;
20
- }
21
-
22
- const BLOCK_RE = new RegExp(
23
- `${escapeRe(START_MARKER)}[\\s\\S]*?${escapeRe(END_MARKER)}`,
24
- );
25
-
26
- function escapeRe(s: string): string {
27
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28
- }
29
-
30
- /**
31
- * Idempotently inserts (or updates) the zond instruction block in `<cwd>/AGENTS.md`.
32
- *
33
- * - Missing file → create with just the block.
34
- * - File without markers → append block at the end (preceded by `\n\n---\n\n`).
35
- * - File with existing markers → replace the body between them.
36
- * - File whose existing block already matches → noop.
37
- */
38
- export function upsertAgentsBlock(cwd: string): AgentsBlockResult {
39
- const path = join(cwd, "AGENTS.md");
40
- const next = wrap(blockBody());
41
-
42
- if (!existsSync(path)) {
43
- writeFileSync(path, next + "\n", "utf-8");
44
- return { path, action: "created" };
45
- }
46
-
47
- const current = readFileSync(path, "utf-8");
48
-
49
- if (BLOCK_RE.test(current)) {
50
- const updated = current.replace(BLOCK_RE, next);
51
- if (updated === current) return { path, action: "noop" };
52
- writeFileSync(path, updated, "utf-8");
53
- return { path, action: "updated" };
54
- }
55
-
56
- // Append with separator
57
- const sep = current.endsWith("\n") ? "\n" : "\n\n";
58
- const updated = current + sep + "---\n\n" + next + "\n";
59
- writeFileSync(path, updated, "utf-8");
60
- return { path, action: "updated" };
61
- }