@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,14 +0,0 @@
1
- export { readOpenApiSpec, extractEndpoints, extractSecuritySchemes } from "./openapi-reader.ts";
2
- export { serializeSuite } from "./serializer.ts";
3
- export type { RawSuite, RawStep } from "./serializer.ts";
4
- export { scanCoveredEndpoints, filterUncoveredEndpoints, normalizePath, specPathToRegex } from "./coverage-scanner.ts";
5
- export type { CoveredEndpoint } from "./coverage-scanner.ts";
6
- export { analyzeEndpoints } from "./endpoint-warnings.ts";
7
- export type { EndpointWarning, WarningCode } from "./endpoint-warnings.ts";
8
- export type { EndpointInfo, ResponseInfo, GenerateOptions, SecuritySchemeInfo, CrudGroup } from "./types.ts";
9
- export { buildCatalog, serializeCatalog } from "./catalog-builder.ts";
10
- export type { ApiCatalog, CatalogEndpoint } from "./catalog-builder.ts";
11
- export { buildApiResourceMap, serializeApiResourceMap } from "./resources-builder.ts";
12
- export type { ApiResourceMap, ApiResourceEntry, ResourceFkRef } from "./resources-builder.ts";
13
- export { buildApiFixtureManifest, serializeApiFixtureManifest } from "./fixtures-builder.ts";
14
- export type { ApiFixtureManifest, FixtureRequirement, FixtureSource } from "./fixtures-builder.ts";
@@ -1,297 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { rootCertificates } from "node:tls";
3
- import { dereference } from "@readme/openapi-parser";
4
- import type { OpenAPIV3 } from "openapi-types";
5
- import type { EndpointInfo, ResponseInfo, SecuritySchemeInfo } from "./types.ts";
6
- import { disambiguateGenericPathParams } from "./path-param-disambig.ts";
7
-
8
- const HTTP_METHODS = ["get", "post", "put", "patch", "delete"] as const;
9
-
10
- export interface SpecFetchTlsOptions {
11
- /** Disable TLS verification entirely (bun `--insecure`). Last resort. */
12
- insecure?: boolean;
13
- /** Path to a PEM CA bundle to trust in addition to the public roots.
14
- * Falls back to the `NODE_EXTRA_CA_CERTS` env var. */
15
- caPath?: string;
16
- }
17
-
18
- /** MF1 (ARV-367): resolve the bun `fetch` `tls` option for a spec fetch so a
19
- * self-signed / internal corporate CA validates *without* disabling TLS.
20
- *
21
- * Precedence: `insecure` (verification off) > explicit `caPath` /
22
- * `NODE_EXTRA_CA_CERTS` (extra CA APPENDED to the public roots — never
23
- * replacing them, so public specs keep validating) > default (undefined).
24
- * Returns undefined when nothing special is needed. Throws if a CA path is
25
- * set but unreadable — a misconfigured CA should surface, not fall through
26
- * to a confusing "self signed certificate" error. */
27
- export function resolveSpecFetchTls(
28
- options?: SpecFetchTlsOptions,
29
- ): { rejectUnauthorized: false } | { ca: string[] } | undefined {
30
- if (options?.insecure) return { rejectUnauthorized: false };
31
- const caPath = options?.caPath ?? process.env.NODE_EXTRA_CA_CERTS;
32
- if (caPath) {
33
- let extra: string;
34
- try {
35
- extra = readFileSync(caPath, "utf8");
36
- } catch (e) {
37
- throw new Error(
38
- `CA bundle not readable: ${caPath} (${(e as Error).message}). ` +
39
- `Set --ca / NODE_EXTRA_CA_CERTS to a valid PEM file, or use --insecure.`,
40
- );
41
- }
42
- if (extra.trim()) return { ca: [extra, ...rootCertificates] };
43
- }
44
- return undefined;
45
- }
46
-
47
- export async function readOpenApiSpec(specPath: string, options?: SpecFetchTlsOptions): Promise<OpenAPIV3.Document> {
48
- // For HTTP URLs, fetch the spec first then dereference the parsed object
49
- if (specPath.startsWith("http://") || specPath.startsWith("https://")) {
50
- const tls = resolveSpecFetchTls(options);
51
- const resp = await fetch(specPath, { ...(tls ? { tls } : {}) });
52
- if (!resp.ok) throw new Error(`Failed to fetch spec: ${resp.status} ${resp.statusText}`);
53
- const spec = await resp.json();
54
- const api = await dereference(spec as string);
55
- return api as OpenAPIV3.Document;
56
- }
57
- const api = await dereference(specPath);
58
- return api as OpenAPIV3.Document;
59
- }
60
-
61
- /** ARV-376: align declared path-param names with the path template when they
62
- * diverge (spec quirk: path `/byid/{id}` but param declared `byid_id`). The
63
- * template is authoritative for substitution, so unmatched params are renamed
64
- * to the unmatched template segment names, in positional order. No-op when
65
- * names already agree (the common, well-formed case). Mutates in place. */
66
- export function reconcilePathParamNames(
67
- path: string,
68
- parameters: OpenAPIV3.ParameterObject[],
69
- ): void {
70
- const tplNames = [...path.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]!);
71
- if (tplNames.length === 0) return;
72
- const pathParams = parameters.filter((p) => p.in === "path");
73
- const declared = new Set(pathParams.map((p) => p.name));
74
- const unmatchedTpl = tplNames.filter((n) => !declared.has(n));
75
- const unmatchedParams = pathParams.filter((p) => !tplNames.includes(p.name));
76
- // Only reconcile a clean 1:1 correspondence — if the counts differ we can't
77
- // safely guess the mapping, so leave the (already odd) spec untouched.
78
- if (unmatchedTpl.length === 0 || unmatchedTpl.length !== unmatchedParams.length) return;
79
- unmatchedParams.forEach((p, i) => {
80
- (p as { name: string }).name = unmatchedTpl[i]!;
81
- });
82
- }
83
-
84
- export function extractSecuritySchemes(doc: OpenAPIV3.Document): SecuritySchemeInfo[] {
85
- const schemes: SecuritySchemeInfo[] = [];
86
- const securitySchemes = doc.components?.securitySchemes;
87
- if (!securitySchemes) return schemes;
88
-
89
- for (const [name, schemeObj] of Object.entries(securitySchemes)) {
90
- const scheme = schemeObj as OpenAPIV3.SecuritySchemeObject;
91
- const info: SecuritySchemeInfo = {
92
- name,
93
- type: scheme.type as SecuritySchemeInfo["type"],
94
- };
95
- if (scheme.type === "http") {
96
- info.scheme = scheme.scheme;
97
- info.bearerFormat = scheme.bearerFormat;
98
- }
99
- if (scheme.type === "apiKey") {
100
- info.in = scheme.in;
101
- info.apiKeyName = scheme.name;
102
- }
103
- schemes.push(info);
104
- }
105
- return schemes;
106
- }
107
-
108
- export function extractEndpoints(doc: OpenAPIV3.Document): EndpointInfo[] {
109
- const endpoints: EndpointInfo[] = [];
110
-
111
- if (!doc.paths) return endpoints;
112
-
113
- for (const [path, pathItem] of Object.entries(doc.paths)) {
114
- if (!pathItem) continue;
115
-
116
- for (const method of HTTP_METHODS) {
117
- const operation = pathItem[method] as OpenAPIV3.OperationObject | undefined;
118
- if (!operation) continue;
119
-
120
- const parameters: OpenAPIV3.ParameterObject[] = [];
121
-
122
- // Skip circular-ref sentinel stubs emitted by decycleSchema —
123
- // they look like `{ "x-circular": true }` (no .name, no .in) and
124
- // crash downstream code that expects p.name/p.in. ARV-200 (R10/F1).
125
- const isUsableParam = (p: any): p is OpenAPIV3.ParameterObject =>
126
- p != null && typeof p === "object" && typeof p.name === "string" && typeof p.in === "string";
127
-
128
- // Path-level parameters
129
- if (pathItem.parameters) {
130
- for (const p of pathItem.parameters) {
131
- if (!isUsableParam(p)) continue;
132
- parameters.push(p as OpenAPIV3.ParameterObject);
133
- }
134
- }
135
-
136
- // Operation-level parameters (override path-level)
137
- if (operation.parameters) {
138
- for (const p of operation.parameters) {
139
- if (!isUsableParam(p)) continue;
140
- const param = p as OpenAPIV3.ParameterObject;
141
- const existingIdx = parameters.findIndex(
142
- (existing) => existing.name === param.name && existing.in === param.in,
143
- );
144
- if (existingIdx >= 0) {
145
- parameters[existingIdx] = param;
146
- } else {
147
- parameters.push(param);
148
- }
149
- }
150
- }
151
-
152
- // ARV-376: reconcile a malformed spec where a path *template* segment
153
- // (`/byid/{id}`) disagrees with the declared path *parameter* name
154
- // (`byid_id`) — a docgen-style quirk. The path template is what the
155
- // runner substitutes, so it wins: rename the diverging param(s) to the
156
- // template segment name(s), positionally. Without this the fixture var
157
- // (from the param) never matches the `{...}` in the path, and every
158
- // downstream layer (disambig, manifest, resource-graph) diverges.
159
- reconcilePathParamNames(path, parameters);
160
-
161
- // Request body schema + content type
162
- let requestBodySchema: OpenAPIV3.SchemaObject | undefined;
163
- let requestBodyContentType: string | undefined;
164
- if (operation.requestBody) {
165
- const rb = operation.requestBody as OpenAPIV3.RequestBodyObject;
166
- if (rb.content) {
167
- // Prefer application/json, fall back to first available
168
- const contentTypes = Object.keys(rb.content);
169
- requestBodyContentType = contentTypes.includes("application/json")
170
- ? "application/json"
171
- : contentTypes[0];
172
- const chosen = rb.content[requestBodyContentType!];
173
- if (chosen?.schema) {
174
- requestBodySchema = chosen.schema as OpenAPIV3.SchemaObject;
175
- // OpenAPI allows examples at the media-type level (sibling to schema).
176
- // Lift them onto the schema so the generator sees a single signal.
177
- if (requestBodySchema.example === undefined) {
178
- if ((chosen as OpenAPIV3.MediaTypeObject).example !== undefined) {
179
- requestBodySchema = {
180
- ...requestBodySchema,
181
- example: (chosen as OpenAPIV3.MediaTypeObject).example,
182
- };
183
- } else if (chosen.examples) {
184
- const firstNamed = Object.values(chosen.examples)[0];
185
- if (firstNamed && typeof firstNamed === "object" && "value" in firstNamed) {
186
- requestBodySchema = {
187
- ...requestBodySchema,
188
- example: (firstNamed as OpenAPIV3.ExampleObject).value,
189
- };
190
- }
191
- }
192
- }
193
- }
194
- }
195
- }
196
-
197
- // Responses
198
- const responses: ResponseInfo[] = [];
199
- const responseContentTypesSet = new Set<string>();
200
- if (operation.responses) {
201
- for (const [statusCode, responseObj] of Object.entries(operation.responses)) {
202
- const parsedStatus = parseInt(statusCode, 10);
203
- // Skip non-numeric keys like "default" — they have no asserting status code.
204
- if (!Number.isFinite(parsedStatus)) continue;
205
- const resp = responseObj as OpenAPIV3.ResponseObject;
206
- const info: ResponseInfo = {
207
- statusCode: parsedStatus,
208
- description: resp.description || "",
209
- };
210
- if (resp.content) {
211
- for (const ct of Object.keys(resp.content)) {
212
- responseContentTypesSet.add(ct);
213
- }
214
- const jsonContent = resp.content["application/json"];
215
- if (jsonContent?.schema) {
216
- info.schema = jsonContent.schema as OpenAPIV3.SchemaObject;
217
- }
218
- }
219
- responses.push(info);
220
- }
221
- }
222
-
223
- // Security: operation-level overrides doc-level
224
- const securityReqs = operation.security ?? doc.security ?? [];
225
- const security = securityReqs.flatMap((req) => Object.keys(req));
226
-
227
- // ETag optimistic locking: detect if endpoint requires If-Match header
228
- const requiresEtag =
229
- responses.some(r => r.statusCode === 412) ||
230
- parameters.some(p => p.name.toLowerCase() === "if-match" && p.in === "header");
231
-
232
- // ARV-189: harvest `x-*` vendor extensions. Operation-level wins on
233
- // key collision over path-level so spec authors can default at the
234
- // path and override per-operation. Empty result = undefined (avoids
235
- // a churn-y `extensions: {}` field in serialised endpoint snapshots).
236
- const extensions = collectExtensions(pathItem, operation);
237
-
238
- endpoints.push({
239
- path,
240
- method: method.toUpperCase(),
241
- operationId: operation.operationId,
242
- summary: operation.summary,
243
- tags: operation.tags ?? [],
244
- parameters,
245
- requestBodySchema,
246
- requestBodyContentType,
247
- responseContentTypes: [...responseContentTypesSet],
248
- responses,
249
- security,
250
- deprecated: (operation.deprecated ?? false) || isMarkedDeprecatedInText(operation.summary, operation.description, operation.operationId),
251
- requiresEtag,
252
- ...(extensions ? { extensions } : {}),
253
- });
254
- }
255
- }
256
-
257
- // ARV-40: when generic path-param names (`{id}`, `{slug}`, ...) collide
258
- // across multiple resources, rewrite each to `<parent_singular>_<param>`
259
- // so the manifest derives per-resource vars and tests stop sharing one
260
- // global `id`. In-memory only; on-disk spec stays untouched.
261
- return disambiguateGenericPathParams(endpoints);
262
- }
263
-
264
- /** Collect `x-*` vendor extensions from a path item and operation,
265
- * with operation values winning on key collision. Returns undefined
266
- * when neither has any so callers can omit the field cleanly. */
267
- function collectExtensions(
268
- pathItem: OpenAPIV3.PathItemObject,
269
- operation: OpenAPIV3.OperationObject,
270
- ): Record<string, unknown> | undefined {
271
- const out: Record<string, unknown> = {};
272
- for (const [k, v] of Object.entries(pathItem)) {
273
- if (k.startsWith("x-")) out[k] = v;
274
- }
275
- for (const [k, v] of Object.entries(operation)) {
276
- if (k.startsWith("x-")) out[k] = v;
277
- }
278
- return Object.keys(out).length > 0 ? out : undefined;
279
- }
280
-
281
- /** Spec authors often mark endpoints as deprecated in the summary or
282
- * description text instead of (or in addition to) the `deprecated: true`
283
- * flag — common across many SaaS and legacy specs. Without this
284
- * fallback, generator emits CRUD suites whose POST returns 404 from a dead
285
- * endpoint. (TASK-245) */
286
- /** Matches `(DEPRECATED) ...`, `[DEPRECATED] ...`, `DEPRECATED: ...` at the
287
- * start of a string. Also matches markdown `## Deprecated` headings, which
288
- * some spec authors use in operation `description` to flag end-of-life
289
- * endpoints. */
290
- const DEPRECATED_PREFIX_RE = /^\s*[\(\[]?\s*DEPRECATED\s*[\)\]:\-—\s]/i;
291
- const DEPRECATED_HEADING_RE = /^\s*#+\s*Deprecated\b/im;
292
- function isMarkedDeprecatedInText(summary?: string, description?: string, operationId?: string): boolean {
293
- if (summary && DEPRECATED_PREFIX_RE.test(summary)) return true;
294
- if (operationId && DEPRECATED_PREFIX_RE.test(operationId)) return true;
295
- if (description && (DEPRECATED_PREFIX_RE.test(description) || DEPRECATED_HEADING_RE.test(description))) return true;
296
- return false;
297
- }
@@ -1,140 +0,0 @@
1
- /**
2
- * Disambiguate generic path-parameter names that collide across resources.
3
- *
4
- * Some OpenAPI specs declare item paths as `/<resource>/{id}` instead of
5
- * `/<resource>/{<resource>_id}` — fine
6
- * within one resource, catastrophic across many: the manifest derives one
7
- * global `id` var, `.env.yaml` stores one value, and N>1 CRUD suites end up
8
- * pointing at the same uuid (false 404 at best; false 200 against a stranger's
9
- * object at worst — the GET returns a real object so the test "passes" while
10
- * having tested nothing of the target resource).
11
- *
12
- * ARV-40 fix: when a generic param name (`id`, `slug`, `uuid`, `key`,
13
- * `identifier`) appears under more than one distinct parent collection in the
14
- * spec, rewrite each occurrence to `<parent_singular>_<param>` in
15
- * EndpointInfo.path AND the matching parameter entry. All downstream code
16
- * (resource map, fixture manifest, suite generator, mass-assignment probe)
17
- * then sees per-resource var names without any extra plumbing.
18
- *
19
- * The on-disk OpenAPI spec.json is untouched — this is an in-memory
20
- * normalisation. Coverage matches by structural shape (`{x}` is `{x}`), and
21
- * the runner substitutes `{{var}}` after path templating, so renaming
22
- * `{id}` → `{template_id}` only affects how zond *names* the variable.
23
- */
24
-
25
- import type { EndpointInfo } from "./types.ts";
26
-
27
- const GENERIC_PARAM_NAMES = new Set(["id", "slug", "uuid", "key", "name", "identifier", "code"]);
28
-
29
- // ARV-376: read-by-id accessor markers. `/business-segment20/byid/{id}` names
30
- // the resource `business-segment20`, not `byid` — the `byid` (or `by-id`)
31
- // segment is a "get by id" verb, not the owning collection. Without skipping
32
- // it, every `/*/byid/{id}` across the spec collapses to one global `byid_id`
33
- // var with no owner resource, so prepare-fixtures reports `miss-no-list`.
34
- const ACCESSOR_MARKER_SEGS = new Set(["byid", "by-id", "by_id"]);
35
-
36
- // ARV-381: version segments (`v30`, `v2`) sit between the collection and its
37
- // id/code param (`/api/macros/v30/{code}`). owningCollectionForPathParam +
38
- // CRUD resource-name derivation ALREADY strip these via
39
- // stripTrailingVersionSegments; the disambig parent-walk must skip them too,
40
- // or `macros_v30_code` (scoped past the version) has no matching resource in
41
- // the graph → miss-no-list. Same regex as stripTrailingVersionSegments.
42
- function isVersionSeg(seg: string): boolean {
43
- return /^v(ersion)?\d+$/i.test(seg);
44
- }
45
-
46
- function isParamSeg(seg: string | undefined): boolean {
47
- return !!seg && /^\{[^}]+\}$/.test(seg);
48
- }
49
-
50
- /** Segments the parent-walk steps over to reach the owning collection:
51
- * read-by-id accessor markers and version segments. */
52
- function isSkippableSeg(seg: string): boolean {
53
- return ACCESSOR_MARKER_SEGS.has(seg.toLowerCase()) || isVersionSeg(seg);
54
- }
55
-
56
- /** English singularization sufficient for resource-collection nouns. */
57
- function singularize(word: string): string {
58
- if (word.length > 3 && /ies$/i.test(word)) return word.slice(0, -3) + "y";
59
- if (word.length > 3 && /(ch|sh|x|ss|z)es$/i.test(word)) return word.slice(0, -2);
60
- if (word.length > 1 && /[^s]s$/i.test(word)) return word.slice(0, -1);
61
- return word;
62
- }
63
-
64
- /** Turn a path segment (`contact-properties`) into an identifier stem (`contact_property`). */
65
- function segToStem(seg: string): string {
66
- return singularize(seg).replace(/[^a-zA-Z0-9]+/g, "_").toLowerCase();
67
- }
68
-
69
- interface Occurrence {
70
- ep: EndpointInfo;
71
- /** Index of the {param} segment in the path. */
72
- segIdx: number;
73
- }
74
-
75
- /** Mutates endpoints in place; returns the same array for chaining. */
76
- export function disambiguateGenericPathParams(endpoints: EndpointInfo[]): EndpointInfo[] {
77
- // param-name → parent-seg → occurrences
78
- const byParamParent = new Map<string, Map<string, Occurrence[]>>();
79
-
80
- for (const ep of endpoints) {
81
- const segs = ep.path.split("/");
82
- for (let i = 0; i < segs.length; i++) {
83
- const m = /^\{([^}]+)\}$/.exec(segs[i]!);
84
- if (!m) continue;
85
- const paramName = m[1]!;
86
- if (!GENERIC_PARAM_NAMES.has(paramName.toLowerCase())) continue;
87
- // Walk back to nearest non-param non-empty segment as "parent",
88
- // skipping read-by-id accessor markers (`byid`, ARV-376) and version
89
- // segments (`v30`, ARV-381) so the owning collection — not an accessor
90
- // verb or version marker — names the param.
91
- let parent: string | undefined;
92
- for (let j = i - 1; j >= 0; j--) {
93
- const s = segs[j]!;
94
- if (s && !isParamSeg(s) && !isSkippableSeg(s)) {
95
- parent = s;
96
- break;
97
- }
98
- }
99
- if (!parent) continue;
100
- let perParent = byParamParent.get(paramName);
101
- if (!perParent) {
102
- perParent = new Map();
103
- byParamParent.set(paramName, perParent);
104
- }
105
- const arr = perParent.get(parent) ?? [];
106
- arr.push({ ep, segIdx: i });
107
- perParent.set(parent, arr);
108
- }
109
- }
110
-
111
- for (const [paramName, perParent] of byParamParent) {
112
- // Only rename when the param collides across ≥2 parents — single-resource
113
- // use of `{id}` stays as-is to avoid churning user .env.yaml entries on
114
- // APIs where the convention isn't a problem.
115
- if (perParent.size < 2) continue;
116
- const lowerParam = paramName.toLowerCase();
117
- for (const [parent, occs] of perParent) {
118
- const stem = segToStem(parent);
119
- if (!stem) continue;
120
- const newName = lowerParam === "id" ? `${stem}_id` : `${stem}_${lowerParam}`;
121
- // Skip rename if newName collides with an unrelated existing name
122
- // for the same endpoint (would corrupt parameters[]). Cheap guard.
123
- for (const occ of occs) {
124
- // ARV-183: preserve the original spec path before mutating, so
125
- // downstream checks (status_code_conformance, response_headers_conformance)
126
- // can still look up `doc.paths[...]` by string equality. Set only
127
- // on first rename — subsequent renames of the same endpoint keep
128
- // the truly original path.
129
- if (occ.ep.originalPath === undefined) occ.ep.originalPath = occ.ep.path;
130
- const segs = occ.ep.path.split("/");
131
- segs[occ.segIdx] = `{${newName}}`;
132
- occ.ep.path = segs.join("/");
133
- const param = occ.ep.parameters.find(p => p.name === paramName && p.in === "path");
134
- if (param) (param as { name: string }).name = newName;
135
- }
136
- }
137
- }
138
-
139
- return endpoints;
140
- }