@kirrosh/zond 0.26.1 → 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 +47 -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 -1162
  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,302 +0,0 @@
1
- import type { OpenAPIV3 } from "openapi-types";
2
- import { readOpenApiSpec } from "./openapi-reader.ts";
3
- import { decycleSchema } from "./schema-utils.ts";
4
-
5
- export interface DescribeEndpointResult {
6
- method: string;
7
- path: string;
8
- operationId?: string;
9
- summary?: string;
10
- description?: string;
11
- tags?: string[];
12
- deprecated: boolean;
13
- security: string[];
14
- parameters: Record<string, object[]>;
15
- requestBody?: object;
16
- responses: Record<string, object>;
17
- testSnippet: string;
18
- }
19
-
20
- export interface CompactEndpoint {
21
- method: string;
22
- path: string;
23
- operationId?: string;
24
- summary?: string;
25
- deprecated: boolean;
26
- }
27
-
28
- function generateTestSnippet(params: {
29
- method: string;
30
- path: string;
31
- operationId?: string;
32
- pathParams: string[];
33
- queryParams: Array<{ name: string; required?: boolean }>;
34
- requestBody?: { required?: boolean; schema?: OpenAPIV3.SchemaObject };
35
- hasSecurity: boolean;
36
- successStatus: string;
37
- }): string {
38
- const { method, path, operationId, queryParams, requestBody, hasSecurity, successStatus } = params;
39
-
40
- const urlPath = path.replace(/\{([^}]+)\}/g, (_, name) => `{{${name}}}`);
41
- const url = `{{base_url}}${urlPath}`;
42
-
43
- const lines: string[] = [];
44
- const testName = operationId ?? `${method} ${path}`;
45
- lines.push(`- name: "${testName}"`);
46
- lines.push(` ${method}: "${url}"`);
47
-
48
- if (hasSecurity) {
49
- lines.push(` headers:`);
50
- lines.push(` Authorization: "Bearer {{auth_token}}"`);
51
- }
52
-
53
- const requiredQuery = queryParams.filter(p => p.required);
54
- if (requiredQuery.length > 0) {
55
- lines.push(` query:`);
56
- for (const p of requiredQuery) {
57
- lines.push(` ${p.name}: "{{${p.name}}}"`);
58
- }
59
- }
60
-
61
- if (requestBody && ["POST", "PUT", "PATCH"].includes(method)) {
62
- const schema = requestBody.schema as OpenAPIV3.SchemaObject | undefined;
63
- const required = Array.isArray(schema?.required) ? schema.required : [];
64
- const properties = schema?.properties as Record<string, OpenAPIV3.SchemaObject> | undefined;
65
- if (properties && Object.keys(properties).length > 0) {
66
- lines.push(` json:`);
67
- for (const [propName, propSchema] of Object.entries(properties)) {
68
- if (!required.includes(propName)) continue;
69
- const type = (propSchema as OpenAPIV3.SchemaObject).type ?? "string";
70
- const placeholder = type === "integer" || type === "number" ? 0 : type === "boolean" ? false : `"{{${propName}}}"`;
71
- lines.push(` ${propName}: ${placeholder}`);
72
- }
73
- }
74
- }
75
-
76
- lines.push(` expect:`);
77
- lines.push(` status: ${successStatus}`);
78
-
79
- return lines.join("\n");
80
- }
81
-
82
- export async function describeEndpoint(
83
- specPath: string,
84
- method: string,
85
- endpointPath: string,
86
- options?: { insecure?: boolean },
87
- ): Promise<DescribeEndpointResult> {
88
- const doc = await readOpenApiSpec(specPath, options) as OpenAPIV3.Document;
89
-
90
- const methodLower = method.toLowerCase() as OpenAPIV3.HttpMethods;
91
- const normalizedPath = endpointPath.replace(/\/+$/, "") || "/";
92
-
93
- let operation: OpenAPIV3.OperationObject | undefined;
94
- let resolvedPath = normalizedPath;
95
- const paths = doc.paths ?? {};
96
-
97
- if (paths[normalizedPath]?.[methodLower]) {
98
- operation = paths[normalizedPath][methodLower] as OpenAPIV3.OperationObject;
99
- } else {
100
- const lowerTarget = normalizedPath.toLowerCase();
101
- for (const [p, pathItem] of Object.entries(paths)) {
102
- if (p.toLowerCase() === lowerTarget && pathItem?.[methodLower]) {
103
- operation = pathItem[methodLower] as OpenAPIV3.OperationObject;
104
- resolvedPath = p;
105
- break;
106
- }
107
- }
108
- }
109
-
110
- if (!operation) {
111
- const available = Object.entries(paths).flatMap(([p, pathItem]) =>
112
- Object.keys(pathItem ?? {})
113
- .filter(k => ["get","post","put","patch","delete","head","options","trace"].includes(k))
114
- .map(k => `${k.toUpperCase()} ${p}`)
115
- ).sort();
116
- throw new Error(`Endpoint ${method.toUpperCase()} ${endpointPath} not found in spec. Available: ${available.join(", ")}`);
117
- }
118
-
119
- const pathItem = paths[resolvedPath] ?? {};
120
-
121
- const pathLevelParams = (pathItem.parameters ?? []) as OpenAPIV3.ParameterObject[];
122
- const opLevelParams = (operation.parameters ?? []) as OpenAPIV3.ParameterObject[];
123
-
124
- const paramMap = new Map<string, OpenAPIV3.ParameterObject>();
125
- for (const p of pathLevelParams) paramMap.set(`${p.in}:${p.name}`, p);
126
- for (const p of opLevelParams) paramMap.set(`${p.in}:${p.name}`, p);
127
-
128
- const grouped: Record<string, object[]> = { path: [], query: [], header: [], cookie: [] };
129
- for (const p of paramMap.values()) {
130
- const loc = p.in in grouped ? p.in : "query";
131
- const schema = p.schema as OpenAPIV3.SchemaObject | undefined;
132
- grouped[loc]!.push({
133
- name: p.name,
134
- required: p.required ?? false,
135
- ...(schema?.type ? { type: schema.type } : {}),
136
- ...(schema?.format ? { format: schema.format } : {}),
137
- ...(schema?.enum ? { enum: schema.enum } : {}),
138
- ...(schema?.default !== undefined ? { default: schema.default } : {}),
139
- ...(p.description ? { description: p.description } : {}),
140
- });
141
- }
142
-
143
- let requestBody: object | undefined;
144
- if (operation.requestBody) {
145
- const rb = operation.requestBody as OpenAPIV3.RequestBodyObject;
146
- const contentTypes = Object.keys(rb.content ?? {});
147
- const preferredCt = contentTypes.find(ct => ct.includes("application/json")) ?? contentTypes[0];
148
- const mediaObj = preferredCt ? rb.content[preferredCt] : undefined;
149
- requestBody = {
150
- required: rb.required ?? false,
151
- ...(preferredCt ? { contentType: preferredCt } : {}),
152
- ...(mediaObj?.schema ? { schema: mediaObj.schema } : {}),
153
- ...(rb.description ? { description: rb.description } : {}),
154
- };
155
- }
156
-
157
- const responses: Record<string, object> = {};
158
- for (const [statusCode, respObj] of Object.entries(operation.responses ?? {})) {
159
- const resp = respObj as OpenAPIV3.ResponseObject;
160
- const contentTypes = Object.keys(resp.content ?? {});
161
- const preferredCt = contentTypes.find(ct => ct.includes("application/json")) ?? contentTypes[0];
162
- const mediaObj = preferredCt ? resp.content?.[preferredCt] : undefined;
163
-
164
- const headers: Record<string, object> = {};
165
- for (const [hName, hObj] of Object.entries(resp.headers ?? {})) {
166
- const h = hObj as OpenAPIV3.HeaderObject;
167
- headers[hName] = {
168
- ...(h.description ? { description: h.description } : {}),
169
- ...(h.schema ? { schema: h.schema } : {}),
170
- };
171
- }
172
-
173
- responses[statusCode] = {
174
- description: resp.description,
175
- headers,
176
- ...(preferredCt ? { contentType: preferredCt } : {}),
177
- ...(mediaObj?.schema ? { schema: mediaObj.schema } : {}),
178
- };
179
- }
180
-
181
- const docSecurity = (doc.security ?? []) as OpenAPIV3.SecurityRequirementObject[];
182
- const opSecurity = (operation.security ?? docSecurity) as OpenAPIV3.SecurityRequirementObject[];
183
- const securityNames = [...new Set(opSecurity.flatMap(req => Object.keys(req)))];
184
-
185
- const responseCodes = Object.keys(operation.responses ?? {});
186
- const successStatus = responseCodes.find(c => c.startsWith("2")) ?? responseCodes[0] ?? "200";
187
-
188
- const pathParamNames = [...paramMap.values()]
189
- .filter(p => p.in === "path")
190
- .map(p => p.name);
191
- const queryParamsList = [...paramMap.values()]
192
- .filter(p => p.in === "query")
193
- .map(p => ({ name: p.name, required: p.required }));
194
- const reqBodyForSnippet = requestBody
195
- ? { required: (operation.requestBody as OpenAPIV3.RequestBodyObject)?.required, schema: (requestBody as any).schema }
196
- : undefined;
197
-
198
- const testSnippet = generateTestSnippet({
199
- method: method.toUpperCase(),
200
- path: resolvedPath,
201
- operationId: operation.operationId,
202
- pathParams: pathParamNames,
203
- queryParams: queryParamsList,
204
- requestBody: reqBodyForSnippet,
205
- hasSecurity: securityNames.length > 0,
206
- successStatus,
207
- });
208
-
209
- const result: DescribeEndpointResult = {
210
- method: method.toUpperCase(),
211
- path: resolvedPath,
212
- ...(operation.operationId ? { operationId: operation.operationId } : {}),
213
- ...(operation.summary ? { summary: operation.summary } : {}),
214
- ...(operation.description ? { description: operation.description } : {}),
215
- ...(operation.tags?.length ? { tags: operation.tags } : {}),
216
- deprecated: operation.deprecated ?? false,
217
- security: securityNames,
218
- parameters: grouped,
219
- ...(requestBody ? { requestBody } : {}),
220
- responses,
221
- testSnippet,
222
- };
223
-
224
- return decycleSchema(result) as DescribeEndpointResult;
225
- }
226
-
227
- export async function describeCompact(
228
- specPath: string,
229
- options?: { insecure?: boolean },
230
- ): Promise<CompactEndpoint[]> {
231
- const doc = await readOpenApiSpec(specPath, options) as OpenAPIV3.Document;
232
- const paths = doc.paths ?? {};
233
- const result: CompactEndpoint[] = [];
234
-
235
- for (const [path, pathItem] of Object.entries(paths)) {
236
- for (const method of ["get","post","put","patch","delete","head","options","trace"]) {
237
- const op = (pathItem as any)?.[method] as OpenAPIV3.OperationObject | undefined;
238
- if (!op) continue;
239
- result.push({
240
- method: method.toUpperCase(),
241
- path,
242
- ...(op.operationId ? { operationId: op.operationId } : {}),
243
- ...(op.summary ? { summary: op.summary } : {}),
244
- deprecated: op.deprecated ?? false,
245
- });
246
- }
247
- }
248
-
249
- return result;
250
- }
251
-
252
- export interface ParamInfo {
253
- name: string;
254
- in: string;
255
- type?: string;
256
- required: boolean;
257
- usedBy: string[];
258
- }
259
-
260
- export async function describeAllParams(
261
- specPath: string,
262
- options?: { insecure?: boolean },
263
- ): Promise<ParamInfo[]> {
264
- const doc = await readOpenApiSpec(specPath, options) as OpenAPIV3.Document;
265
- const paths = doc.paths ?? {};
266
- const paramMap = new Map<string, ParamInfo>();
267
-
268
- for (const [path, pathItem] of Object.entries(paths)) {
269
- const pathParams = ((pathItem as any)?.parameters ?? []) as OpenAPIV3.ParameterObject[];
270
-
271
- for (const method of ["get", "post", "put", "patch", "delete"]) {
272
- const op = (pathItem as any)?.[method] as OpenAPIV3.OperationObject | undefined;
273
- if (!op) continue;
274
-
275
- const allParams = [...pathParams, ...((op.parameters ?? []) as OpenAPIV3.ParameterObject[])];
276
- const endpoint = `${method.toUpperCase()} ${path}`;
277
-
278
- for (const p of allParams) {
279
- const key = `${p.in}:${p.name}`;
280
- const existing = paramMap.get(key);
281
- const schema = p.schema as OpenAPIV3.SchemaObject | undefined;
282
- if (existing) {
283
- existing.usedBy.push(endpoint);
284
- if (p.required) existing.required = true;
285
- } else {
286
- paramMap.set(key, {
287
- name: p.name,
288
- in: p.in,
289
- type: schema?.type,
290
- required: p.required ?? false,
291
- usedBy: [endpoint],
292
- });
293
- }
294
- }
295
- }
296
- }
297
-
298
- return [...paramMap.values()].sort((a, b) => {
299
- if (a.in !== b.in) return a.in.localeCompare(b.in);
300
- return a.name.localeCompare(b.name);
301
- });
302
- }
@@ -1,52 +0,0 @@
1
- import type { EndpointInfo } from "./types.ts";
2
-
3
- export type WarningCode = "deprecated" | "no_response_schema" | "no_responses_defined" | "required_params_no_examples" | "post_body_as_query";
4
-
5
- export interface EndpointWarning {
6
- method: string;
7
- path: string;
8
- warnings: string[];
9
- }
10
-
11
- export function analyzeEndpoints(endpoints: EndpointInfo[]): EndpointWarning[] {
12
- const result: EndpointWarning[] = [];
13
-
14
- for (const ep of endpoints) {
15
- const warnings: string[] = [];
16
-
17
- if (ep.deprecated) {
18
- warnings.push("deprecated");
19
- }
20
-
21
- if (ep.responses.length === 0) {
22
- warnings.push("no_responses_defined");
23
- } else {
24
- const has2xx = ep.responses.filter(r => r.statusCode >= 200 && r.statusCode < 300);
25
- if (has2xx.length > 0 && has2xx.every(r => !r.schema)) {
26
- warnings.push("no_response_schema");
27
- }
28
- }
29
-
30
- const missingExamples = ep.parameters
31
- .filter(p => p.required && !p.example && !(p.schema && (p.schema as any).example) && !(p.schema && (p.schema as any).default))
32
- .map(p => p.name);
33
- if (missingExamples.length > 0) {
34
- warnings.push(`required_params_no_examples: ${missingExamples.join(", ")}`);
35
- }
36
-
37
- // SpringDoc quirk: POST/PUT/PATCH with query param named "body" or single complex object query param
38
- if (["POST", "PUT", "PATCH"].includes(ep.method)) {
39
- const queryParams = ep.parameters.filter(p => p.in === "query");
40
- const hasBodyQuery = queryParams.some(p => p.name.toLowerCase() === "body");
41
- if (hasBodyQuery) {
42
- warnings.push("post_body_as_query: query param 'body' on POST/PUT/PATCH likely means request body (SpringDoc quirk)");
43
- }
44
- }
45
-
46
- if (warnings.length > 0) {
47
- result.push({ method: ep.method, path: ep.path, warnings });
48
- }
49
- }
50
-
51
- return result;
52
- }
@@ -1,332 +0,0 @@
1
- /**
2
- * Build `.api-fixtures.yaml` — manifest of variables this API needs from
3
- * the user's `.env.yaml`.
4
- *
5
- * Purpose: when the skill (or `zond doctor`, future) needs to tell the
6
- * user what to fill in before scenarios will run, it reads this manifest
7
- * instead of inferring fixtures from generated tests. The manifest is
8
- * derived purely from the OpenAPI spec — auth schemes, required path
9
- * params, server URL — so it's stable across re-runs of `generate`.
10
- *
11
- * Manifest is read-only (regenerate via `zond refresh-api`); user edits
12
- * land in `.env.yaml`, not here.
13
- */
14
-
15
- import type { EndpointInfo, SecuritySchemeInfo } from "./types.ts";
16
- import type { OpenAPIV3 } from "openapi-types";
17
- import { schemeVarName, resourceVar, computeAmbiguousPathParams, fixtureVarNameForPathParam, owningCollectionForPathParam } from "./suite-generator.ts";
18
- import type { ApiResourceMap } from "./resources-builder.ts";
19
- import { canonicalVarName, isFkFixtureField, effectiveObjectShape } from "./data-factory.ts";
20
-
21
- // canonicalVarName now lives in data-factory (leaf module, shared with the
22
- // suite generator). Re-exported here for back-compat: create-body.ts and
23
- // existing callers still import it from fixtures-builder.
24
- export { canonicalVarName };
25
-
26
- export type FixtureSource = "auth" | "server" | "path" | "header" | "body-fk" | "capture-chain";
27
-
28
- export interface FixtureRequirement {
29
- /** Variable name as referenced via {{var}} in tests. */
30
- name: string;
31
- /** Where this fixture comes from in the spec. */
32
- source: FixtureSource;
33
- /** Free-text description for the user (one line). */
34
- description: string;
35
- /** Endpoints affected if this fixture is missing (sample, ≤10). */
36
- affectedEndpoints: string[];
37
- /** True when at least one consumer marks this required. */
38
- required: boolean;
39
- /** Suggested placeholder value, used to seed `.env.yaml`. */
40
- defaultValue?: string;
41
- }
42
-
43
- export interface ApiFixtureManifest {
44
- generatedAt: string;
45
- specHash: string;
46
- fixtureCount: number;
47
- fixtures: FixtureRequirement[];
48
- }
49
-
50
- function epLabel(ep: EndpointInfo): string {
51
- return `${ep.method.toUpperCase()} ${ep.path}`;
52
- }
53
-
54
- function pushAffected(req: FixtureRequirement, ep: EndpointInfo): void {
55
- if (req.affectedEndpoints.length >= 10) return;
56
- const label = epLabel(ep);
57
- if (!req.affectedEndpoints.includes(label)) req.affectedEndpoints.push(label);
58
- }
59
-
60
- export interface BuildFixturesParams {
61
- endpoints: EndpointInfo[];
62
- securitySchemes: SecuritySchemeInfo[];
63
- baseUrl?: string;
64
- specHash: string;
65
- /**
66
- * Resource map (CRUD groups + body-FK refs) — when provided, the manifest
67
- * also lists body-FK and capture-chain variables that the test generator
68
- * will reference. Keeps `.api-fixtures.yaml` in sync with what generated
69
- * tests actually consume (per decision-7: manifest = source of truth for
70
- * the *list* of variables).
71
- */
72
- resourceMap?: ApiResourceMap;
73
- }
74
-
75
- export function buildApiFixtureManifest(params: BuildFixturesParams): ApiFixtureManifest {
76
- const fixtures = new Map<string, FixtureRequirement>();
77
-
78
- // 1. Server URL → base_url
79
- fixtures.set("base_url", {
80
- name: "base_url",
81
- source: "server",
82
- description: params.baseUrl
83
- ? `Base URL of the API (from spec: ${params.baseUrl}).`
84
- : `Base URL of the API. Spec did not declare a server — fill in manually.`,
85
- affectedEndpoints: ["*"],
86
- required: true,
87
- defaultValue: params.baseUrl ?? "",
88
- });
89
-
90
- // 2. Auth schemes → auth tokens
91
- // We map each scheme that endpoints actually reference into an env var.
92
- const usedSchemeNames = new Set<string>();
93
- for (const ep of params.endpoints) {
94
- for (const s of ep.security) usedSchemeNames.add(s);
95
- }
96
- for (const scheme of params.securitySchemes) {
97
- if (!usedSchemeNames.has(scheme.name)) continue;
98
- const varName = schemeVarName(scheme, params.securitySchemes);
99
- let description: string;
100
- if (scheme.type === "http" && scheme.scheme === "bearer") {
101
- description = `Bearer token for security scheme "${scheme.name}".`;
102
- } else if (scheme.type === "apiKey") {
103
- description = `API key for "${scheme.name}" (sent as ${scheme.in === "header" ? `header ${scheme.apiKeyName}` : `${scheme.in} param ${scheme.apiKeyName}`}).`;
104
- } else if (scheme.type === "oauth2") {
105
- description = `OAuth2 access token for scheme "${scheme.name}".`;
106
- } else {
107
- description = `Token for security scheme "${scheme.name}" (${scheme.type}).`;
108
- }
109
- const existing = fixtures.get(varName);
110
- if (existing) {
111
- // Multiple schemes might collapse to one var (single-bearer case).
112
- // Keep the most informative description.
113
- if (description.length > existing.description.length) existing.description = description;
114
- } else {
115
- fixtures.set(varName, {
116
- name: varName,
117
- source: "auth",
118
- description,
119
- affectedEndpoints: [],
120
- required: true,
121
- defaultValue: "",
122
- });
123
- }
124
- const req = fixtures.get(varName)!;
125
- for (const ep of params.endpoints) {
126
- if (ep.security.includes(scheme.name)) pushAffected(req, ep);
127
- }
128
- }
129
-
130
- // 3. Required path params → one var per unique name, disambiguated by
131
- // owning resource when the raw name is genuinely reused across
132
- // distinct collections (ARV-369). Without this, a spec where
133
- // `{code}` appears under both `/macros/v30/{code}` and
134
- // `/templates/v30/{code}` collapses to ONE `code` fixture — one
135
- // `.env.yaml` value then silently misapplies to whichever resources
136
- // don't own it, producing false-negative 404s in generated suites.
137
- // Single-owner param names (the common case) keep their raw name,
138
- // unchanged from pre-ARV-369 behavior.
139
- const ambiguousPathParams = computeAmbiguousPathParams(params.endpoints);
140
- for (const ep of params.endpoints) {
141
- for (const p of ep.parameters) {
142
- if (p.in !== "path") continue;
143
- if (p.required === false) continue;
144
- const varName = fixtureVarNameForPathParam(ep.path, p.name, ambiguousPathParams);
145
- let req = fixtures.get(varName);
146
- if (!req) {
147
- const schema = p.schema as { type?: string; format?: string; example?: unknown } | undefined;
148
- let defaultValue = "";
149
- if (schema?.example !== undefined) defaultValue = String(schema.example);
150
- else if (schema?.format === "uuid") defaultValue = "";
151
- else if (schema?.type === "integer" || schema?.type === "number") defaultValue = "";
152
-
153
- const scopeNote = ambiguousPathParams.has(p.name)
154
- ? ` Scoped to "${owningCollectionForPathParam(ep.path, p.name) ?? "?"}" — the raw name "${p.name}" is also used by other resources with unrelated ids.`
155
- : "";
156
- req = {
157
- name: varName,
158
- source: "path",
159
- description: `Path parameter ${p.name}${schema?.format ? ` (${schema.format})` : schema?.type ? ` (${schema.type})` : ""}.${scopeNote} Set to a real id from your account, or leave blank to skip dependent tests.`,
160
- affectedEndpoints: [],
161
- required: true,
162
- defaultValue,
163
- };
164
- fixtures.set(varName, req);
165
- }
166
- pushAffected(req, ep);
167
- }
168
- }
169
-
170
- // 4. Required header params → one var per unique name (skip Authorization
171
- // & Content-Type — those are handled by auth + suite headers).
172
- for (const ep of params.endpoints) {
173
- for (const p of ep.parameters) {
174
- if (p.in !== "header") continue;
175
- if (p.required === false) continue;
176
- const lname = p.name.toLowerCase();
177
- if (lname === "authorization" || lname === "content-type" || lname === "accept") continue;
178
- const varName = lname.replace(/-/g, "_");
179
- let req = fixtures.get(varName);
180
- if (!req) {
181
- req = {
182
- name: varName,
183
- source: "header",
184
- description: `Required header ${p.name}.`,
185
- affectedEndpoints: [],
186
- required: true,
187
- defaultValue: "",
188
- };
189
- fixtures.set(varName, req);
190
- }
191
- pushAffected(req, ep);
192
- }
193
- }
194
-
195
- // 5. Body-FK fields — required parent-id fields in request bodies that
196
- // the generator copies from `.env.yaml` (e.g. `audience_id` in
197
- // POST /contacts). Without these in the manifest, prepare-fixtures
198
- // discovers/seeds nothing for them and `zond audit` 422s on first
199
- // nested resource. Walks ALL mutating endpoints (not only full
200
- // CRUD groups) so POST-only resources still surface their FK deps.
201
- // Source precedence: path > body-fk (path-params more constraining).
202
- for (const ep of params.endpoints) {
203
- const method = ep.method.toUpperCase();
204
- if (method !== "POST" && method !== "PUT" && method !== "PATCH") continue;
205
- const schema = ep.requestBodySchema as OpenAPIV3.SchemaObject | undefined;
206
- if (!schema) continue;
207
- // effectiveObjectShape merges allOf — .NET/Swagger bodies wrap models in
208
- // allOf, so a direct schema.properties read misses every FK field.
209
- const { properties, required } = effectiveObjectShape(schema);
210
- if (Object.keys(properties).length === 0) continue;
211
- for (const [fieldName, propSchema] of Object.entries(properties)) {
212
- if (!required.has(fieldName)) continue;
213
- // ARV-45: catch FK ids AND closed-vocab reference codes
214
- // (`sequenceTypeCode`) the generator can't synthesise — kept in
215
- // lockstep with `wireBodyFkRefs` in suite-generator so every var the
216
- // tests reference appears here (decision-7 manifest contract).
217
- if (!isFkFixtureField(fieldName, propSchema as OpenAPIV3.SchemaObject)) continue;
218
- // ARV-138: canonicalise camelCase to snake_case so `issueId` shares
219
- // a manifest slot with path-param `issue_id`. The raw `fieldName`
220
- // still goes to the server unchanged via `create-body.ts` —
221
- // canonicalisation only affects the manifest var-name namespace.
222
- const varName = canonicalVarName(fieldName);
223
- const existing = fixtures.get(varName);
224
- if (existing) {
225
- // Already covered (likely as path-param). Keep the existing entry
226
- // and just surface the additional affected endpoint.
227
- pushAffected(existing, ep);
228
- continue;
229
- }
230
- fixtures.set(varName, {
231
- name: varName,
232
- source: "body-fk",
233
- description: `Foreign-key / reference field "${fieldName}" consumed by ${epLabel(ep)} request body. Set to a real value from your account, or leave blank to skip dependent tests.`,
234
- affectedEndpoints: [epLabel(ep)],
235
- required: true,
236
- defaultValue: "",
237
- });
238
- }
239
- }
240
-
241
- // 6. CRUD-chain capture vars — the generator emits `capture: <idParam>`
242
- // in POST steps and references {{<idParam>}} downstream. These are
243
- // auto-captured at runtime; surfacing them in the manifest keeps the
244
- // "var in tests but not in manifest" contract intact (per decision-7)
245
- // and lets prepare-fixtures distinguish "captured automatically" from
246
- // "user must fill". required: false — env override is advanced-only.
247
- //
248
- // ARV-137: capture name = `r.idParam` (the spec's path-param name), not
249
- // `resourceVar(r.resource, "id")`. The synthesised form produced phantom
250
- // manifest dupes whenever the spec's path-param didn't equal
251
- // `<resource>_id` (e.g. monitors/monitor_id_or_slug, saved/query_id,
252
- // releases/version). Now the manifest carries one var per resource id
253
- // that matches both the path-source entry and the generated test refs.
254
- if (params.resourceMap) {
255
- for (const r of params.resourceMap.resources) {
256
- if (!r.endpoints.create) continue;
257
- const captureName = r.idParam || resourceVar(r.resource, "id");
258
- if (fixtures.has(captureName)) continue;
259
- const description = `Captured automatically from ${r.endpoints.create} response (field "${r.captureField}") and used in downstream CRUD steps. Set in .env.yaml only to override the captured value.`;
260
- const affectedFromGroup = Object.entries(r.endpoints)
261
- .filter(([k]) => k !== "list" && k !== "create")
262
- .map(([, v]) => v as string);
263
- const req: FixtureRequirement = {
264
- name: captureName,
265
- source: "capture-chain",
266
- description,
267
- affectedEndpoints: affectedFromGroup.slice(0, 10),
268
- required: false,
269
- defaultValue: "",
270
- };
271
- fixtures.set(captureName, req);
272
- }
273
- }
274
-
275
- const ordered = Array.from(fixtures.values()).sort((a, b) => {
276
- const sourceOrder: Record<FixtureSource, number> = {
277
- server: 0,
278
- auth: 1,
279
- header: 2,
280
- path: 3,
281
- "body-fk": 4,
282
- "capture-chain": 5,
283
- };
284
- if (sourceOrder[a.source] !== sourceOrder[b.source]) {
285
- return sourceOrder[a.source] - sourceOrder[b.source];
286
- }
287
- return a.name.localeCompare(b.name);
288
- });
289
-
290
- return {
291
- generatedAt: new Date().toISOString(),
292
- specHash: params.specHash,
293
- fixtureCount: ordered.length,
294
- fixtures: ordered,
295
- };
296
- }
297
-
298
- // ── YAML serialization ──
299
-
300
- function escape(s: string): string {
301
- if (/[:#\[\]{}&*!|>'"@`,%]/.test(s) || s.includes("\n") || s === "") {
302
- return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
303
- }
304
- return s;
305
- }
306
-
307
- export function serializeApiFixtureManifest(m: ApiFixtureManifest): string {
308
- const lines: string[] = [];
309
- lines.push("# Auto-generated by zond. Do not edit by hand.");
310
- lines.push("# Read-only manifest of variables this API needs in .env.yaml.");
311
- lines.push("# Regenerate via: zond refresh-api <name>");
312
- lines.push(`generatedAt: ${escape(m.generatedAt)}`);
313
- lines.push(`specHash: ${escape(m.specHash)}`);
314
- lines.push(`fixtureCount: ${m.fixtureCount}`);
315
- lines.push("fixtures:");
316
- for (const f of m.fixtures) {
317
- lines.push(` - name: ${escape(f.name)}`);
318
- lines.push(` source: ${f.source}`);
319
- lines.push(` required: ${f.required}`);
320
- lines.push(` description: ${escape(f.description)}`);
321
- if (f.defaultValue !== undefined) {
322
- lines.push(` defaultValue: ${escape(f.defaultValue)}`);
323
- }
324
- if (f.affectedEndpoints.length === 0) {
325
- lines.push(` affectedEndpoints: []`);
326
- } else {
327
- lines.push(` affectedEndpoints:`);
328
- for (const e of f.affectedEndpoints) lines.push(` - ${escape(e)}`);
329
- }
330
- }
331
- return lines.join("\n") + "\n";
332
- }