@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,58 +0,0 @@
1
- /**
2
- * Unified severity matrix (ARV-250, m-21 pivot).
3
- *
4
- * Single source of truth for severity classification across all
5
- * finding-producing subsystems (lint, checks, probes). Replaces three
6
- * divergent ladders (lint 3-tier, checks 4-tier, probes 4-tier).
7
- *
8
- * Principle: **no evidence — no high severity**. Severity reflects
9
- * proven impact, not anomaly presence. CRITICAL exists in the type
10
- * but is reserved for end-to-end exploit chains; producers without
11
- * such chains must NOT emit it.
12
- */
13
-
14
- export type Severity = "critical" | "high" | "medium" | "low" | "info";
15
-
16
- const SEVERITY_RANK: Record<Severity, number> = {
17
- critical: 0,
18
- high: 1,
19
- medium: 2,
20
- low: 3,
21
- info: 4,
22
- };
23
-
24
- export function rankSeverity(s: Severity): number {
25
- return SEVERITY_RANK[s];
26
- }
27
-
28
- /**
29
- * Empty severity-bucket map. Use as starting tally; downstream code
30
- * increments per finding.
31
- */
32
- export function emptySeverityBuckets(): Record<Severity, number> {
33
- return { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
34
- }
35
-
36
- /**
37
- * SARIF level mapping (sarif.ts previously hardcoded 4-tier; this
38
- * keeps the same external semantics + adds 'info' → 'note').
39
- */
40
- export function severityToSarifLevel(s: Severity): "error" | "warning" | "note" {
41
- if (s === "critical" || s === "high") return "error";
42
- if (s === "medium") return "warning";
43
- return "note"; // low + info
44
- }
45
-
46
- /**
47
- * Console glyph for severity. Stable per-glyph keeps fb-loop diff
48
- * compares clean.
49
- */
50
- export function severityGlyph(s: Severity): string {
51
- switch (s) {
52
- case "critical": return "🚨";
53
- case "high": return "🔴";
54
- case "medium": return "⚠️";
55
- case "low": return "ℹ️";
56
- case "info": return "·";
57
- }
58
- }
@@ -1,102 +0,0 @@
1
- /**
2
- * ARV-175: infer a JSON Schema (draft-07 subset) from a set of sample
3
- * response bodies. Built-in, zero-dependency — quicktype/genson would each
4
- * drag in a large dependency tree, which contradicts zond's dumb-tool /
5
- * minimal-deps charter (see src/CLAUDE.md). The `--engine` flag keeps the
6
- * seam open if a heavier engine is ever wanted, but `builtin` is the default
7
- * and the only one wired.
8
- *
9
- * Strategy — structural union over the samples:
10
- * - primitives → { type }
11
- * - arrays → { type: "array", items: <merge of every element> }
12
- * - objects → { type: "object", properties, required }
13
- * `required` = keys present in EVERY object sample (an
14
- * intersection — a field missing from one sample is optional).
15
- * - mixed types across samples → { type: [sorted, unique] } (or a bare
16
- * type when they all agree). null folds into the type list so a
17
- * sometimes-null field reads as `["null","string"]`.
18
- *
19
- * Not a full inference engine: no format detection, no enum mining, no
20
- * anyOf for heterogeneous array items beyond a type union. Good enough to
21
- * seed `response_schema_conformance` on specs that declare no response
22
- * schema, which is the whole point (ARV-175 goal).
23
- */
24
-
25
- export type JsonSchema = Record<string, unknown>;
26
-
27
- type JsonType = "null" | "boolean" | "integer" | "number" | "string" | "array" | "object";
28
-
29
- function typeOf(v: unknown): JsonType {
30
- if (v === null) return "null";
31
- if (Array.isArray(v)) return "array";
32
- const t = typeof v;
33
- if (t === "boolean") return "boolean";
34
- if (t === "number") return Number.isInteger(v) ? "integer" : "number";
35
- if (t === "string") return "string";
36
- return "object";
37
- }
38
-
39
- /** Infer a schema from one or more samples of the same logical value. */
40
- export function inferSchema(samples: unknown[]): JsonSchema {
41
- const nonEmpty = samples.filter((s) => s !== undefined);
42
- if (nonEmpty.length === 0) return {};
43
-
44
- const types = new Set<JsonType>();
45
- for (const s of nonEmpty) types.add(typeOf(s));
46
-
47
- // Object: merge properties across every object sample.
48
- if (types.has("object")) {
49
- const objSamples = nonEmpty.filter((s) => typeOf(s) === "object") as Array<Record<string, unknown>>;
50
- const propSamples = new Map<string, unknown[]>();
51
- for (const obj of objSamples) {
52
- for (const [k, v] of Object.entries(obj)) {
53
- if (!propSamples.has(k)) propSamples.set(k, []);
54
- propSamples.get(k)!.push(v);
55
- }
56
- }
57
- // required = keys present in ALL object samples (intersection).
58
- const required = [...propSamples.keys()].filter(
59
- (k) => objSamples.every((obj) => Object.prototype.hasOwnProperty.call(obj, k)),
60
- );
61
- const properties: JsonSchema = {};
62
- for (const [k, vs] of propSamples) properties[k] = inferSchema(vs);
63
-
64
- const schema: JsonSchema = { type: mergeType(types, "object") };
65
- if (Object.keys(properties).length > 0) schema.properties = sortKeys(properties);
66
- if (required.length > 0) schema.required = required.sort();
67
- return schema;
68
- }
69
-
70
- // Array: items schema is the union of every element across every sample.
71
- if (types.has("array")) {
72
- const elements: unknown[] = [];
73
- for (const s of nonEmpty) if (Array.isArray(s)) elements.push(...s);
74
- const schema: JsonSchema = { type: mergeType(types, "array") };
75
- if (elements.length > 0) schema.items = inferSchema(elements);
76
- return schema;
77
- }
78
-
79
- // Primitives only.
80
- return { type: mergeType(types) };
81
- }
82
-
83
- /** Collapse the observed type set into a single `type` value: a string when
84
- * they agree (preferring `primary` if given, e.g. object/array), otherwise a
85
- * sorted unique list. `integer`+`number` collapses to `number`. */
86
- function mergeType(types: Set<JsonType>, primary?: JsonType): string | string[] {
87
- const t = new Set(types);
88
- if (t.has("number") && t.has("integer")) t.delete("integer");
89
- if (primary && t.size > 1) {
90
- // Object/array with a stray null etc. — keep the structural type plus null.
91
- const rest = [...t].filter((x) => x !== primary);
92
- if (rest.length === 1 && rest[0] === "null") return [primary, "null"].sort();
93
- }
94
- const arr = [...t].sort();
95
- return arr.length === 1 ? arr[0]! : arr;
96
- }
97
-
98
- function sortKeys(obj: JsonSchema): JsonSchema {
99
- const out: JsonSchema = {};
100
- for (const k of Object.keys(obj).sort()) out[k] = obj[k];
101
- return out;
102
- }
@@ -1,154 +0,0 @@
1
- /**
2
- * ARV-122 (m-19, blocker-m-18): layered spec model.
3
- *
4
- * Today two artifact sources contribute to the resource map under
5
- * `apis/<name>/`:
6
- * 1. `.api-resources.yaml` — sha-tracked, regenerated by
7
- * `refresh-api` from `spec.json` (upstream OpenAPI).
8
- * 2. `.api-resources.local.yaml` — ARV-111 user extension that
9
- * survives `refresh-api` (write-only endpoints, common SaaS-style
10
- * ingest, …).
11
- *
12
- * `readResourceMap` in `cli/commands/discover.ts` merges them by hand:
13
- * extensions win on `resource` name collision. m-18 (quicktype-derived
14
- * and mitmproxy-derived schema layers) will add two more sources with
15
- * different precedence and merge semantics — doing that ad-hoc would
16
- * end the way ad-hoc output-format parsing did (ARV-97 family,
17
- * resolved by m-19/ARV-116).
18
- *
19
- * This module is the same move for spec sources: declare each source
20
- * once as a `SpecLayer`, with precedence and merge policy, and feed
21
- * them through `composeSpec()`. The result is a `ComposedSpec` plus a
22
- * `ProvenanceMap` (`resource → layer.id`) that downstream code can
23
- * consult to explain *where* a field came from (m-18 follow-ups will
24
- * surface this in `doctor` / `catalog --provenance`; this task only
25
- * ships the internal API).
26
- *
27
- * Scope of this task: ONLY the resource-map dimension (the actual
28
- * `ResourceYaml` shape used by every other module). Other dimensions
29
- * (raw OpenAPI document, fixture manifest) keep their own loaders —
30
- * generalising further before there's a second consumer would be
31
- * premature.
32
- */
33
-
34
- /** How a layer's entries combine with already-resolved entries. */
35
- export type MergePolicy =
36
- /** Layer entries replace lower-precedence entries on key collision. */
37
- | "override"
38
- /** Layer entries are dropped if a lower-precedence entry already
39
- * owns the key. (Useful for "default" layers that fill gaps but
40
- * shouldn't shadow user input.) */
41
- | "preserve"
42
- /** Layer entries are appended even when a key already exists,
43
- * producing duplicate keys. Reserved for future fixture layers;
44
- * not used by the current two-layer resource composition. */
45
- | "append";
46
-
47
- /** Domain a layer contributes to. Today only `resources` is wired —
48
- * m-18 will likely add `fixtures` (manifest merge) and `spec`
49
- * (raw OpenAPI augmentation). Listed up front so layer factories
50
- * carry the tag at construction. */
51
- export type LayerScope = "resources" | "fixtures" | "spec";
52
-
53
- /** A single typed source of spec data. */
54
- export interface SpecLayer<T> {
55
- /** Stable identifier surfaced in the provenance map (e.g.,
56
- * `"upstream"`, `"extension"`, `"quicktype"`). Must be unique
57
- * inside one `composeSpec` call. */
58
- id: string;
59
- /** Source path/URL for diagnostics. Optional — synthetic layers
60
- * (in-memory test fixtures) can omit. */
61
- path?: string;
62
- /** Higher precedence wins on key collisions when `mergePolicy` is
63
- * `"override"`. Ties: layer ordering in the input array. */
64
- precedence: number;
65
- /** Which composition this layer participates in. `composeSpec` does
66
- * not currently route on scope — it expects callers to filter — but
67
- * the field makes layer arrays self-describing. */
68
- scope: LayerScope;
69
- /** How this layer's entries combine with lower-precedence ones. */
70
- mergePolicy: MergePolicy;
71
- /** Load the layer's entries. Async to accommodate filesystem and
72
- * network sources (an HTTP-fetched OpenAPI snapshot in the future
73
- * could implement this directly). */
74
- load: () => Promise<T[]> | T[];
75
- }
76
-
77
- /** Function deriving the merge key for an entry. Pulled out so callers
78
- * can compose resource lists (key = resource name) and, later, fixture
79
- * lists (key = var name) with the same engine. */
80
- export type KeyFn<T> = (entry: T) => string;
81
-
82
- /** Output of `composeSpec`. `entries` is the merged list in stable
83
- * order (lowest precedence first, with higher-precedence overrides
84
- * applied in-place); `provenance` maps the merged key back to the
85
- * layer id that ultimately owns it. */
86
- export interface ComposedSpec<T> {
87
- entries: T[];
88
- provenance: Map<string, string>;
89
- }
90
-
91
- /**
92
- * Compose layers into a single entry list + provenance map.
93
- *
94
- * Order of operations:
95
- * 1. Layers are sorted by ascending `precedence` (low → high). A
96
- * stable sort is used so ties keep input order.
97
- * 2. Each layer's entries are loaded in sequence (the loader is
98
- * async-safe so a slow loader can't block parallel ones — but
99
- * we keep ordering deterministic by awaiting in turn).
100
- * 3. Entries are folded into the result Map keyed by `keyFn(entry)`:
101
- * - `mergePolicy === "override"` → replace; provenance updates.
102
- * - `mergePolicy === "preserve"` → only set if the key is new.
103
- * - `mergePolicy === "append"` → key suffixed with `#<n>` to
104
- * avoid collision; provenance still recorded.
105
- *
106
- * The result preserves insertion order of the underlying Map, which
107
- * is the natural "lowest-precedence-first" view: callers that need a
108
- * different ordering re-sort downstream.
109
- */
110
- export async function composeSpec<T>(
111
- layers: SpecLayer<T>[],
112
- keyFn: KeyFn<T>,
113
- ): Promise<ComposedSpec<T>> {
114
- const ordered = [...layers].sort((a, b) => a.precedence - b.precedence);
115
-
116
- const seenIds = new Set<string>();
117
- for (const l of ordered) {
118
- if (seenIds.has(l.id)) {
119
- throw new Error(`composeSpec: duplicate layer id "${l.id}"`);
120
- }
121
- seenIds.add(l.id);
122
- }
123
-
124
- const merged = new Map<string, T>();
125
- const provenance = new Map<string, string>();
126
- let appendCounter = 0;
127
-
128
- for (const layer of ordered) {
129
- const entries = await layer.load();
130
- for (const entry of entries) {
131
- const key = keyFn(entry);
132
- switch (layer.mergePolicy) {
133
- case "override":
134
- merged.set(key, entry);
135
- provenance.set(key, layer.id);
136
- break;
137
- case "preserve":
138
- if (!merged.has(key)) {
139
- merged.set(key, entry);
140
- provenance.set(key, layer.id);
141
- }
142
- break;
143
- case "append": {
144
- const tagged = `${key}#${appendCounter++}`;
145
- merged.set(tagged, entry);
146
- provenance.set(tagged, layer.id);
147
- break;
148
- }
149
- }
150
- }
151
- }
152
-
153
- return { entries: Array.from(merged.values()), provenance };
154
- }
@@ -1,156 +0,0 @@
1
- /**
2
- * Deterministic union of two or more dereferenced OpenAPI documents into
3
- * one (ARV-375). Motivation: an org running multiple API versions
4
- * side-by-side (v1/v2, deprecated-but-live + current) wants combined
5
- * coverage instead of re-scanning each version and reconciling reports by
6
- * hand. Before this, that merge was a one-off python script per session.
7
- *
8
- * Policy — pure, deterministic, no judgment (belongs in zond core per the
9
- * litmus test):
10
- * - `paths`: union; on a path-key collision the LATER spec wins, and the
11
- * collision is recorded so the caller can warn (silent shadowing of a
12
- * real endpoint is a correctness trap).
13
- * - `components.*`: union per sub-bucket (schemas, securitySchemes,
14
- * parameters, responses, …); later wins. A component-name collision
15
- * whose two definitions DIFFER (deep-unequal) is reported separately —
16
- * that is the dangerous case (same name, different shape).
17
- * - `servers`, `security`, `tags`: unioned & de-duped by value/name.
18
- * - `info`: taken from the first spec; `version` becomes the unique
19
- * source versions joined with `+` so the merged target is self-labelling.
20
- *
21
- * Inputs are assumed already dereferenced (readOpenApiSpec output), so path
22
- * operations are self-contained — no cross-spec $ref resolution needed.
23
- */
24
-
25
- import type { OpenAPIV3 } from "openapi-types";
26
-
27
- export interface MergeInput {
28
- /** Source label (path or URL) for the merge summary. */
29
- source: string;
30
- doc: OpenAPIV3.Document;
31
- }
32
-
33
- export interface MergeSummary {
34
- sources: { source: string; paths: number }[];
35
- totalPaths: number;
36
- /** Path keys declared by more than one spec (later-wins applied). */
37
- pathCollisions: string[];
38
- /** `components.<bucket>.<name>` whose definition differs across specs. */
39
- schemaConflicts: string[];
40
- }
41
-
42
- export interface MergeResult {
43
- merged: OpenAPIV3.Document;
44
- summary: MergeSummary;
45
- }
46
-
47
- const COMPONENT_BUCKETS = [
48
- "schemas",
49
- "responses",
50
- "parameters",
51
- "examples",
52
- "requestBodies",
53
- "headers",
54
- "securitySchemes",
55
- "links",
56
- "callbacks",
57
- ] as const;
58
-
59
- function deepEqual(a: unknown, b: unknown): boolean {
60
- // Cheap structural compare — specs are plain JSON after dereference.
61
- return JSON.stringify(a) === JSON.stringify(b);
62
- }
63
-
64
- export function mergeOpenApiDocs(inputs: MergeInput[]): MergeResult {
65
- if (inputs.length === 0) throw new Error("mergeOpenApiDocs: no specs to merge");
66
- const first = inputs[0]!.doc;
67
-
68
- const paths: NonNullable<OpenAPIV3.Document["paths"]> = {};
69
- const pathCollisions: string[] = [];
70
- const sources: MergeSummary["sources"] = [];
71
-
72
- for (const { source, doc } of inputs) {
73
- const specPaths = doc.paths ?? {};
74
- let count = 0;
75
- for (const [p, item] of Object.entries(specPaths)) {
76
- if (p in paths) pathCollisions.push(p);
77
- paths[p] = item as OpenAPIV3.PathItemObject;
78
- count++;
79
- }
80
- sources.push({ source, paths: count });
81
- }
82
-
83
- // Merge components bucket-by-bucket; flag same-name-different-shape.
84
- const schemaConflicts: string[] = [];
85
- const components: Record<string, Record<string, unknown>> = {};
86
- for (const { doc } of inputs) {
87
- const comps = (doc.components ?? {}) as Record<string, Record<string, unknown> | undefined>;
88
- for (const bucket of COMPONENT_BUCKETS) {
89
- const incoming = comps[bucket];
90
- if (!incoming) continue;
91
- const target = (components[bucket] ??= {});
92
- for (const [name, def] of Object.entries(incoming)) {
93
- if (name in target && !deepEqual(target[name], def)) {
94
- schemaConflicts.push(`${bucket}.${name}`);
95
- }
96
- target[name] = def;
97
- }
98
- }
99
- }
100
-
101
- // Union servers by url, security by value, tags by name.
102
- const servers = dedupeBy(
103
- inputs.flatMap((i) => i.doc.servers ?? []),
104
- (s) => s.url,
105
- );
106
- const security = dedupeBy(
107
- inputs.flatMap((i) => i.doc.security ?? []),
108
- (s) => JSON.stringify(s),
109
- );
110
- const tags = dedupeBy(
111
- inputs.flatMap((i) => i.doc.tags ?? []),
112
- (t) => t.name,
113
- );
114
-
115
- const versions = dedupe(inputs.map((i) => i.doc.info?.version).filter(Boolean) as string[]);
116
-
117
- const merged: OpenAPIV3.Document = {
118
- ...first,
119
- openapi: first.openapi ?? "3.0.0",
120
- info: {
121
- ...first.info,
122
- version: versions.join("+") || first.info?.version || "merged",
123
- },
124
- paths,
125
- ...(Object.keys(components).length > 0 ? { components: components as OpenAPIV3.ComponentsObject } : {}),
126
- ...(servers.length > 0 ? { servers } : {}),
127
- ...(security.length > 0 ? { security } : {}),
128
- ...(tags.length > 0 ? { tags } : {}),
129
- };
130
-
131
- return {
132
- merged,
133
- summary: {
134
- sources,
135
- totalPaths: Object.keys(paths).length,
136
- pathCollisions: dedupe(pathCollisions),
137
- schemaConflicts: dedupe(schemaConflicts),
138
- },
139
- };
140
- }
141
-
142
- function dedupe<T>(arr: T[]): T[] {
143
- return [...new Set(arr)];
144
- }
145
-
146
- function dedupeBy<T>(arr: T[], key: (x: T) => string): T[] {
147
- const seen = new Set<string>();
148
- const out: T[] = [];
149
- for (const x of arr) {
150
- const k = key(x);
151
- if (seen.has(k)) continue;
152
- seen.add(k);
153
- out.push(x);
154
- }
155
- return out;
156
- }
@@ -1,117 +0,0 @@
1
- /**
2
- * ARV-175: extract 2xx response bodies from a persisted run and infer a
3
- * JSON Schema per (endpoint, status). The output `patch.schema.json` is the
4
- * input to `refresh-api --merge-schema` (ARV-176), which folds it into the
5
- * spec overlay so `response_schema_conformance` has something to check on
6
- * APIs whose upstream spec declares no response schemas.
7
- */
8
-
9
- import { inferSchema, type JsonSchema } from "./infer-schema.ts";
10
- import { specPathToRegex, normalizePath } from "../generator/coverage-scanner.ts";
11
- import type { EndpointInfo } from "../generator/types.ts";
12
-
13
- export interface SchemaFromRunsResult {
14
- /** endpoint label (`METHOD /path/{tpl}`) → status code → inferred schema. */
15
- patch: Record<string, Record<string, JsonSchema>>;
16
- /** Per-group accounting for the CLI to report. */
17
- groups: Array<{
18
- endpoint: string;
19
- status: string;
20
- samples: number;
21
- emitted: boolean;
22
- reason?: string;
23
- }>;
24
- }
25
-
26
- export interface ResultRow {
27
- request_method: string | null;
28
- request_url: string | null;
29
- response_status: number | null;
30
- response_body: string | null;
31
- }
32
-
33
- /** Strip base URL, query, and trailing slash from a concrete request URL,
34
- * leaving just the path for spec-template matching. */
35
- function pathOf(rawUrl: string): string {
36
- let p = rawUrl;
37
- // Drop scheme+host if present.
38
- const schemeIdx = p.indexOf("://");
39
- if (schemeIdx !== -1) {
40
- const afterScheme = p.slice(schemeIdx + 3);
41
- const slash = afterScheme.indexOf("/");
42
- p = slash === -1 ? "/" : afterScheme.slice(slash);
43
- }
44
- const q = p.indexOf("?");
45
- if (q !== -1) p = p.slice(0, q);
46
- return p.replace(/\/+$/, "") || "/";
47
- }
48
-
49
- /** Match a concrete path to the most specific spec endpoint (fewest params
50
- * wins, so `/users/me` beats `/users/{id}`). Returns the endpoint label. */
51
- function matchEndpoint(
52
- method: string,
53
- concretePath: string,
54
- endpoints: EndpointInfo[],
55
- ): string | null {
56
- const norm = normalizePath(concretePath);
57
- const candidates = endpoints
58
- .filter((e) => e.method.toUpperCase() === method.toUpperCase())
59
- .filter((e) => specPathToRegex(e.path).test(norm))
60
- .sort((a, b) => paramCount(a.path) - paramCount(b.path));
61
- const best = candidates[0];
62
- return best ? `${best.method.toUpperCase()} ${best.path}` : null;
63
- }
64
-
65
- function paramCount(path: string): number {
66
- return (path.match(/\{[^}]+\}/g) ?? []).length;
67
- }
68
-
69
- export interface SchemaFromRunsOptions {
70
- results: ResultRow[];
71
- endpoints: EndpointInfo[];
72
- /** Minimum 2xx samples per (endpoint, status) group to emit a schema. */
73
- minSamples: number;
74
- }
75
-
76
- export function schemaFromRuns(opts: SchemaFromRunsOptions): SchemaFromRunsResult {
77
- const { results, endpoints, minSamples } = opts;
78
- // (endpoint → status → parsed bodies)
79
- const buckets = new Map<string, Map<string, unknown[]>>();
80
- // Preserve unmatched URLs so the CLI can hint at spec/base-url drift.
81
- for (const r of results) {
82
- if (r.response_status == null || r.response_status < 200 || r.response_status >= 300) continue;
83
- if (!r.response_body || !r.request_url || !r.request_method) continue;
84
- let body: unknown;
85
- try {
86
- body = JSON.parse(r.response_body);
87
- } catch {
88
- continue; // non-JSON 2xx body — nothing to infer for application/json
89
- }
90
- const endpoint = matchEndpoint(r.request_method, pathOf(r.request_url), endpoints);
91
- if (!endpoint) continue;
92
- const status = String(r.response_status);
93
- if (!buckets.has(endpoint)) buckets.set(endpoint, new Map());
94
- const byStatus = buckets.get(endpoint)!;
95
- if (!byStatus.has(status)) byStatus.set(status, []);
96
- byStatus.get(status)!.push(body);
97
- }
98
-
99
- const patch: SchemaFromRunsResult["patch"] = {};
100
- const groups: SchemaFromRunsResult["groups"] = [];
101
- // Deterministic order: sort by endpoint then status.
102
- for (const endpoint of [...buckets.keys()].sort()) {
103
- const byStatus = buckets.get(endpoint)!;
104
- for (const status of [...byStatus.keys()].sort()) {
105
- const samples = byStatus.get(status)!;
106
- if (samples.length < minSamples) {
107
- groups.push({ endpoint, status, samples: samples.length, emitted: false, reason: `<${minSamples} samples` });
108
- continue;
109
- }
110
- const schema = inferSchema(samples);
111
- if (!patch[endpoint]) patch[endpoint] = {};
112
- patch[endpoint]![status] = schema;
113
- groups.push({ endpoint, status, samples: samples.length, emitted: true });
114
- }
115
- }
116
- return { patch, groups };
117
- }