@kirrosh/zond 0.21.0 → 0.23.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 (256) hide show
  1. package/CHANGELOG.md +758 -3
  2. package/README.md +78 -15
  3. package/package.json +17 -10
  4. package/src/cli/argv.ts +122 -0
  5. package/src/cli/commands/add-api.ts +134 -0
  6. package/src/cli/commands/api/annotate/idempotency.ts +59 -0
  7. package/src/cli/commands/api/annotate/index.ts +525 -0
  8. package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
  9. package/src/cli/commands/api/annotate/overlay.ts +206 -0
  10. package/src/cli/commands/api/annotate/pagination.ts +60 -0
  11. package/src/cli/commands/api/annotate/prompts.ts +183 -0
  12. package/src/cli/commands/api/annotate/readback.ts +58 -0
  13. package/src/cli/commands/api/annotate/resources.ts +91 -0
  14. package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
  15. package/src/cli/commands/audit.ts +480 -0
  16. package/src/cli/commands/bootstrap.ts +710 -0
  17. package/src/cli/commands/catalog.ts +35 -0
  18. package/src/cli/commands/check.ts +348 -0
  19. package/src/cli/commands/checks.ts +756 -0
  20. package/src/cli/commands/ci-init.ts +55 -6
  21. package/src/cli/commands/clean.ts +212 -0
  22. package/src/cli/commands/cleanup.ts +262 -0
  23. package/src/cli/commands/completions.ts +192 -0
  24. package/src/cli/commands/coverage.ts +605 -132
  25. package/src/cli/commands/db.ts +180 -8
  26. package/src/cli/commands/describe.ts +37 -2
  27. package/src/cli/commands/discover.ts +1236 -0
  28. package/src/cli/commands/doctor.ts +607 -0
  29. package/src/cli/commands/fixtures.ts +402 -0
  30. package/src/cli/commands/generate.ts +420 -47
  31. package/src/cli/commands/init/agents-md.ts +61 -0
  32. package/src/cli/commands/init/bootstrap.ts +108 -0
  33. package/src/cli/commands/init/index.ts +244 -0
  34. package/src/cli/commands/init/skills.ts +98 -0
  35. package/src/cli/commands/init/templates/agents.md +77 -0
  36. package/src/cli/commands/init/templates/markdown.d.ts +4 -0
  37. package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
  38. package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
  39. package/src/cli/commands/init/templates/skills/zond.md +651 -0
  40. package/src/cli/commands/init/templates/zond-config.yml +14 -0
  41. package/src/cli/commands/prepare-fixtures.ts +135 -0
  42. package/src/cli/commands/probe/mass-assignment.ts +503 -0
  43. package/src/cli/commands/probe/security.ts +454 -0
  44. package/src/cli/commands/probe/static.ts +255 -0
  45. package/src/cli/commands/probe/webhooks.ts +161 -0
  46. package/src/cli/commands/probe.ts +459 -0
  47. package/src/cli/commands/reference.ts +87 -0
  48. package/src/cli/commands/refresh-api.ts +169 -0
  49. package/src/cli/commands/remove-api.ts +150 -0
  50. package/src/cli/commands/report-bundle.ts +318 -0
  51. package/src/cli/commands/report.ts +241 -0
  52. package/src/cli/commands/request.ts +379 -4
  53. package/src/cli/commands/run.ts +911 -33
  54. package/src/cli/commands/session.ts +244 -0
  55. package/src/cli/commands/use.ts +74 -0
  56. package/src/cli/index.ts +36 -607
  57. package/src/cli/json-envelope.ts +112 -3
  58. package/src/cli/json-schemas.ts +263 -0
  59. package/src/cli/program.ts +218 -0
  60. package/src/cli/resolve.ts +105 -0
  61. package/src/cli/status-filter.ts +124 -0
  62. package/src/cli/util/api-context.ts +85 -0
  63. package/src/cli/version.ts +8 -0
  64. package/src/core/anti-fp/bootstrap.ts +34 -0
  65. package/src/core/anti-fp/index.ts +33 -0
  66. package/src/core/anti-fp/registry.ts +44 -0
  67. package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
  68. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
  69. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
  70. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
  71. package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
  72. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
  73. package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
  74. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
  75. package/src/core/anti-fp/types.ts +68 -0
  76. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  77. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  78. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  79. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  80. package/src/core/checks/checks/cross_call_references.ts +134 -0
  81. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  82. package/src/core/checks/checks/idempotency_replay.ts +246 -0
  83. package/src/core/checks/checks/ignored_auth.ts +211 -0
  84. package/src/core/checks/checks/index.ts +65 -0
  85. package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
  86. package/src/core/checks/checks/missing_required_header.ts +40 -0
  87. package/src/core/checks/checks/negative_data_rejection.ts +45 -0
  88. package/src/core/checks/checks/not_a_server_error.ts +27 -0
  89. package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
  90. package/src/core/checks/checks/pagination_invariants.ts +238 -0
  91. package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
  92. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  93. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  94. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  95. package/src/core/checks/checks/status_code_conformance.ts +61 -0
  96. package/src/core/checks/checks/unsupported_method.ts +63 -0
  97. package/src/core/checks/checks/use_after_free.ts +78 -0
  98. package/src/core/checks/index.ts +30 -0
  99. package/src/core/checks/mode.ts +79 -0
  100. package/src/core/checks/recommended-action.ts +64 -0
  101. package/src/core/checks/registry.ts +78 -0
  102. package/src/core/checks/runner.ts +874 -0
  103. package/src/core/checks/sarif.ts +230 -0
  104. package/src/core/checks/stateful.ts +121 -0
  105. package/src/core/checks/types.ts +189 -0
  106. package/src/core/classifier/recommended-action.ts +222 -0
  107. package/src/core/context/current.ts +51 -0
  108. package/src/core/context/session.ts +78 -0
  109. package/src/core/coverage/loader.ts +185 -0
  110. package/src/core/coverage/reasons.ts +300 -0
  111. package/src/core/diagnostics/db-analysis.ts +161 -12
  112. package/src/core/diagnostics/failure-class.ts +120 -0
  113. package/src/core/diagnostics/failure-hints.ts +212 -9
  114. package/src/core/diagnostics/spec-pointer.ts +99 -0
  115. package/src/core/diagnostics/suggested-fixes.ts +156 -0
  116. package/src/core/exporter/case-study/index.ts +270 -0
  117. package/src/core/exporter/curl.ts +40 -0
  118. package/src/core/exporter/exporter.ts +48 -0
  119. package/src/core/exporter/html-report/escape.ts +24 -0
  120. package/src/core/exporter/html-report/index.ts +479 -0
  121. package/src/core/exporter/html-report/script.ts +100 -0
  122. package/src/core/exporter/html-report/styles.ts +408 -0
  123. package/src/core/generator/chunker.ts +53 -15
  124. package/src/core/generator/coverage-phase.ts +0 -0
  125. package/src/core/generator/create-body.ts +89 -0
  126. package/src/core/generator/data-factory.ts +490 -33
  127. package/src/core/generator/describe.ts +1 -1
  128. package/src/core/generator/fixtures-builder.ts +325 -0
  129. package/src/core/generator/index.ts +7 -5
  130. package/src/core/generator/openapi-reader.ts +55 -3
  131. package/src/core/generator/path-param-disambig.ts +114 -0
  132. package/src/core/generator/resources-builder.ts +648 -0
  133. package/src/core/generator/schema-utils.ts +11 -3
  134. package/src/core/generator/serializer.ts +114 -15
  135. package/src/core/generator/suite-generator.ts +484 -77
  136. package/src/core/generator/types.ts +8 -0
  137. package/src/core/identity/identity-file.ts +129 -0
  138. package/src/core/lint/affects.ts +28 -0
  139. package/src/core/lint/config.ts +96 -0
  140. package/src/core/lint/format.ts +42 -0
  141. package/src/core/lint/index.ts +94 -0
  142. package/src/core/lint/reporter.ts +128 -0
  143. package/src/core/lint/rules/consistency.ts +158 -0
  144. package/src/core/lint/rules/heuristics.ts +97 -0
  145. package/src/core/lint/rules/strictness.ts +109 -0
  146. package/src/core/lint/types.ts +96 -0
  147. package/src/core/lint/walker.ts +248 -0
  148. package/src/core/meta/meta-store.ts +6 -73
  149. package/src/core/output/README.md +91 -0
  150. package/src/core/output/index.ts +13 -0
  151. package/src/core/output/run.ts +126 -0
  152. package/src/core/output/types.ts +129 -0
  153. package/src/core/parser/env-interpolation.ts +104 -0
  154. package/src/core/parser/filter.ts +57 -0
  155. package/src/core/parser/schema.ts +132 -5
  156. package/src/core/parser/types.ts +29 -2
  157. package/src/core/parser/variables.ts +0 -0
  158. package/src/core/parser/yaml-parser.ts +108 -13
  159. package/src/core/probe/bootstrap.ts +34 -0
  160. package/src/core/probe/dry-run-envelope.ts +57 -0
  161. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  162. package/src/core/probe/mass-assignment-probe.ts +1122 -0
  163. package/src/core/probe/mass-assignment-template.ts +212 -0
  164. package/src/core/probe/method-probe.ts +164 -0
  165. package/src/core/probe/method-shared.ts +69 -0
  166. package/src/core/probe/negative-probe.ts +691 -0
  167. package/src/core/probe/orphan-tracker.ts +188 -0
  168. package/src/core/probe/path-discovery.ts +440 -0
  169. package/src/core/probe/probe-harness.ts +120 -0
  170. package/src/core/probe/registry.ts +89 -0
  171. package/src/core/probe/runner.ts +136 -0
  172. package/src/core/probe/security-probe-class.ts +201 -0
  173. package/src/core/probe/security-probe.ts +1453 -0
  174. package/src/core/probe/shared.ts +505 -0
  175. package/src/core/probe/static-probe-class.ts +125 -0
  176. package/src/core/probe/types.ts +165 -0
  177. package/src/core/probe/verdict-aggregator.ts +33 -0
  178. package/src/core/probe/webhooks-probe.ts +284 -0
  179. package/src/core/reporter/console.ts +69 -4
  180. package/src/core/reporter/index.ts +2 -3
  181. package/src/core/reporter/json.ts +15 -2
  182. package/src/core/reporter/junit.ts +27 -12
  183. package/src/core/reporter/ndjson.ts +37 -0
  184. package/src/core/reporter/types.ts +3 -0
  185. package/src/core/runner/assertions.ts +62 -2
  186. package/src/core/runner/async-pool.ts +108 -0
  187. package/src/core/runner/auth-path.ts +8 -0
  188. package/src/core/runner/ci-context.ts +72 -0
  189. package/src/core/runner/executor.ts +391 -52
  190. package/src/core/runner/form-encode.ts +51 -0
  191. package/src/core/runner/http-client.ts +115 -7
  192. package/src/core/runner/learn-drift.ts +293 -0
  193. package/src/core/runner/preflight-vars.ts +149 -0
  194. package/src/core/runner/progress-tracker.ts +73 -0
  195. package/src/core/runner/rate-limiter.ts +203 -0
  196. package/src/core/runner/run-kind.ts +39 -0
  197. package/src/core/runner/schema-validator.ts +312 -0
  198. package/src/core/runner/send-request.ts +153 -20
  199. package/src/core/runner/types.ts +38 -0
  200. package/src/core/secrets/registry.ts +164 -0
  201. package/src/core/secrets/secrets-file.ts +115 -0
  202. package/src/core/selectors/operation-filter.ts +144 -0
  203. package/src/core/setup-api.ts +419 -17
  204. package/src/core/severity/category.ts +94 -0
  205. package/src/core/severity/index.ts +121 -0
  206. package/src/core/spec/layers.ts +154 -0
  207. package/src/core/util/format-eta.ts +21 -0
  208. package/src/core/utils.ts +5 -1
  209. package/src/core/workspace/config.ts +129 -0
  210. package/src/core/workspace/manifest.ts +283 -0
  211. package/src/core/workspace/output-rotation.ts +62 -0
  212. package/src/core/workspace/root.ts +94 -0
  213. package/src/core/workspace/triage-path.ts +87 -0
  214. package/src/db/lint-runs.ts +47 -0
  215. package/src/db/migrate.ts +126 -0
  216. package/src/db/migrations/0001_run_kind.sql +25 -0
  217. package/src/db/migrations/sql.d.ts +4 -0
  218. package/src/db/queries/collections.ts +133 -0
  219. package/src/db/queries/coverage.ts +9 -0
  220. package/src/db/queries/dashboard.ts +59 -0
  221. package/src/db/queries/results.ts +128 -0
  222. package/src/db/queries/runs.ts +235 -0
  223. package/src/db/queries/sessions.ts +42 -0
  224. package/src/db/queries/settings.ts +28 -0
  225. package/src/db/queries/types.ts +172 -0
  226. package/src/db/queries.ts +72 -802
  227. package/src/db/schema.ts +179 -48
  228. package/src/cli/commands/export.ts +0 -144
  229. package/src/cli/commands/guide.ts +0 -127
  230. package/src/cli/commands/init.ts +0 -57
  231. package/src/cli/commands/serve.ts +0 -81
  232. package/src/cli/commands/sync.ts +0 -269
  233. package/src/cli/commands/update.ts +0 -189
  234. package/src/cli/commands/validate.ts +0 -34
  235. package/src/core/exporter/postman.ts +0 -963
  236. package/src/core/generator/guide-builder.ts +0 -253
  237. package/src/core/meta/types.ts +0 -21
  238. package/src/core/parser/index.ts +0 -21
  239. package/src/core/runner/execute-run.ts +0 -132
  240. package/src/core/runner/index.ts +0 -12
  241. package/src/core/sync/spec-differ.ts +0 -38
  242. package/src/web/data/collection-state.ts +0 -362
  243. package/src/web/routes/api.ts +0 -314
  244. package/src/web/routes/dashboard.ts +0 -350
  245. package/src/web/routes/runs.ts +0 -64
  246. package/src/web/schemas.ts +0 -121
  247. package/src/web/server.ts +0 -134
  248. package/src/web/static/htmx.min.cjs +0 -1
  249. package/src/web/static/style.css +0 -1148
  250. package/src/web/views/endpoints-tab.ts +0 -174
  251. package/src/web/views/explorer-tab.ts +0 -402
  252. package/src/web/views/health-strip.ts +0 -92
  253. package/src/web/views/layout.ts +0 -48
  254. package/src/web/views/results.ts +0 -210
  255. package/src/web/views/runs-tab.ts +0 -126
  256. package/src/web/views/suites-tab.ts +0 -181
@@ -0,0 +1,206 @@
1
+ /**
2
+ * ARV-187: read / merge / write `.api-resources.local.yaml`.
3
+ *
4
+ * The annotate command writes _only_ into the `patches:` block. That's
5
+ * the ARV-169 field-level overlay (see discover.ts:readResourcePatches)
6
+ * — strictly additive, doesn't touch the upstream `.api-resources.yaml`.
7
+ * The `extensions:` block (ARV-111, full-resource replacement) is left
8
+ * intact when we re-write the file, so users can mix annotate-generated
9
+ * patches with hand-written extensions.
10
+ *
11
+ * Idempotent re-annotation: when a patch field already exists in the
12
+ * overlay, we compare values. Equal → no-op (keep existing). Different
13
+ * → conflict (surface in diff, requires --yes to overwrite, or --keep
14
+ * to skip).
15
+ */
16
+
17
+ import { join } from "node:path";
18
+ import { writeFile } from "node:fs/promises";
19
+ import type { ResourceYaml } from "../../discover.ts";
20
+
21
+ export type ResourcePatch = Partial<ResourceYaml> & { resource: string };
22
+
23
+ export interface LocalOverlayFile {
24
+ extensions?: ResourceYaml[];
25
+ patches?: ResourcePatch[];
26
+ /** Any other top-level keys the user added (preserved on rewrite). */
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ const FILENAME = ".api-resources.local.yaml";
31
+
32
+ export async function readLocalOverlay(apiDir: string): Promise<LocalOverlayFile> {
33
+ const file = Bun.file(join(apiDir, FILENAME));
34
+ if (!(await file.exists())) return {};
35
+ const parsed = Bun.YAML.parse(await file.text());
36
+ if (!parsed || typeof parsed !== "object") return {};
37
+ return parsed as LocalOverlayFile;
38
+ }
39
+
40
+ export async function writeLocalOverlay(apiDir: string, overlay: LocalOverlayFile): Promise<void> {
41
+ const path = join(apiDir, FILENAME);
42
+ const header = `# .api-resources.local.yaml — local overlay (ARV-111 / ARV-169 / ARV-187)
43
+ #
44
+ # extensions: full ResourceYaml entries that REPLACE the upstream entry
45
+ # (by resource name) or ADD a new one.
46
+ # patches: partial overlay — fields here are merged onto the matching
47
+ # upstream entry (idempotency / pagination / lifecycle /
48
+ # readback_diff / seed_body).
49
+ #
50
+ # This file is checked into git. Edit by hand or via \`zond api annotate\`.
51
+ `;
52
+ // Bun.YAML doesn't have a stringify; use the yaml package.
53
+ const { stringify } = await import("yaml");
54
+ const body = stringify(overlay, { lineWidth: 0, defaultStringType: "PLAIN" });
55
+ await writeFile(path, header + "\n" + body, "utf-8");
56
+ }
57
+
58
+ export interface MergeConflict {
59
+ resource: string;
60
+ field: string;
61
+ existing: unknown;
62
+ proposed: unknown;
63
+ }
64
+
65
+ export interface MergeResult {
66
+ /** New patches array, with proposed merged over existing. */
67
+ patches: ResourcePatch[];
68
+ /** Per-resource per-field conflicts (existing value differs from proposed). */
69
+ conflicts: MergeConflict[];
70
+ /** Per-resource per-field accepted changes (new field, or overwrite when force=true). */
71
+ changes: MergeConflict[];
72
+ }
73
+
74
+ /**
75
+ * Merge proposed patches into existing patches.
76
+ *
77
+ * Conflict policy:
78
+ * - Field absent in existing → added (counts as a `change`).
79
+ * - Field present and structurally equal → kept (no-op).
80
+ * - Field present and different → if force=true, overwritten and
81
+ * counted as both `change` and `conflict`; else kept and counted
82
+ * as `conflict` only.
83
+ *
84
+ * "Structural equality" uses JSON stringification with stable key order;
85
+ * good enough for yaml-roundtrip data which is plain JSON-shaped.
86
+ */
87
+ export function mergePatches(
88
+ existing: ResourcePatch[],
89
+ proposed: ResourcePatch[],
90
+ opts: { force?: boolean } = {},
91
+ ): MergeResult {
92
+ const force = opts.force === true;
93
+ const byName = new Map<string, ResourcePatch>();
94
+ for (const p of existing) byName.set(p.resource, deepClone(p));
95
+ const conflicts: MergeConflict[] = [];
96
+ const changes: MergeConflict[] = [];
97
+
98
+ for (const proposedPatch of proposed) {
99
+ const name = proposedPatch.resource;
100
+ const current = byName.get(name) ?? { resource: name };
101
+ for (const [field, proposedVal] of Object.entries(proposedPatch)) {
102
+ if (field === "resource") continue;
103
+ const existingVal = (current as Record<string, unknown>)[field];
104
+ if (existingVal === undefined) {
105
+ (current as Record<string, unknown>)[field] = proposedVal;
106
+ changes.push({ resource: name, field, existing: undefined, proposed: proposedVal });
107
+ continue;
108
+ }
109
+ if (structurallyEqual(existingVal, proposedVal)) continue;
110
+ // Conflict.
111
+ conflicts.push({ resource: name, field, existing: existingVal, proposed: proposedVal });
112
+ if (force) {
113
+ (current as Record<string, unknown>)[field] = proposedVal;
114
+ changes.push({ resource: name, field, existing: existingVal, proposed: proposedVal });
115
+ }
116
+ }
117
+ byName.set(name, current);
118
+ }
119
+
120
+ return { patches: [...byName.values()], conflicts, changes };
121
+ }
122
+
123
+ function structurallyEqual(a: unknown, b: unknown): boolean {
124
+ return stableJson(a) === stableJson(b);
125
+ }
126
+
127
+ function stableJson(v: unknown): string {
128
+ if (v === null || typeof v !== "object") return JSON.stringify(v);
129
+ if (Array.isArray(v)) return "[" + v.map(stableJson).join(",") + "]";
130
+ const obj = v as Record<string, unknown>;
131
+ const keys = Object.keys(obj).sort();
132
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableJson(obj[k])).join(",") + "}";
133
+ }
134
+
135
+ function deepClone<T>(v: T): T {
136
+ return JSON.parse(JSON.stringify(v));
137
+ }
138
+
139
+ /**
140
+ * Render a unified, human-readable diff of the changes a merge produced.
141
+ * Returns "" when there are no changes.
142
+ *
143
+ * Format (kept simple so it works in plain stdout, no chalk dep):
144
+ *
145
+ * resource: customers
146
+ * + seed_body:
147
+ * content_type: application/x-www-form-urlencoded
148
+ * body:
149
+ * description: 'zond probe customer'
150
+ * email: 'probe@example.com'
151
+ * ~ idempotency: (conflict — kept existing)
152
+ * existing: { header: Idempotency-Key }
153
+ * proposed: { header: X-Idempotency }
154
+ */
155
+ export function renderChangesDiff(result: MergeResult): string {
156
+ const lines: string[] = [];
157
+ const byResource = new Map<string, { changes: MergeConflict[]; conflicts: MergeConflict[] }>();
158
+ for (const c of result.changes) {
159
+ if (!byResource.has(c.resource)) byResource.set(c.resource, { changes: [], conflicts: [] });
160
+ byResource.get(c.resource)!.changes.push(c);
161
+ }
162
+ for (const c of result.conflicts) {
163
+ if (!byResource.has(c.resource)) byResource.set(c.resource, { changes: [], conflicts: [] });
164
+ byResource.get(c.resource)!.conflicts.push(c);
165
+ }
166
+ for (const [resource, group] of byResource) {
167
+ lines.push(`resource: ${resource}`);
168
+ for (const ch of group.changes) {
169
+ const op = ch.existing === undefined ? "+" : "~";
170
+ lines.push(` ${op} ${ch.field}:`);
171
+ lines.push(indent(yamlSnippet(ch.proposed), 6));
172
+ }
173
+ for (const cf of group.conflicts) {
174
+ // Skip if already rendered as a change (force=true case).
175
+ if (group.changes.some((c) => c.field === cf.field && c.existing !== undefined)) continue;
176
+ lines.push(` ! ${cf.field}: (conflict — kept existing; pass --yes to overwrite)`);
177
+ lines.push(` existing: ${oneLineYaml(cf.existing)}`);
178
+ lines.push(` proposed: ${oneLineYaml(cf.proposed)}`);
179
+ }
180
+ }
181
+ return lines.join("\n");
182
+ }
183
+
184
+ function yamlSnippet(v: unknown): string {
185
+ // Reuse yaml package for nested rendering.
186
+ try {
187
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
188
+ const { stringify } = require("yaml") as { stringify: (v: unknown, opts?: unknown) => string };
189
+ return stringify(v, { lineWidth: 0 }).trimEnd();
190
+ } catch {
191
+ return JSON.stringify(v, null, 2);
192
+ }
193
+ }
194
+
195
+ function oneLineYaml(v: unknown): string {
196
+ try {
197
+ return JSON.stringify(v);
198
+ } catch {
199
+ return String(v);
200
+ }
201
+ }
202
+
203
+ function indent(text: string, n: number): string {
204
+ const pad = " ".repeat(n);
205
+ return text.split("\n").map((l) => pad + l).join("\n");
206
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * ARV-187 / pagination: parser + expected shape.
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import type { ResourcePatch } from "./overlay.ts";
7
+ import type { ResourceSlice } from "./prompts.ts";
8
+
9
+ const PaginationSchema = z.object({
10
+ type: z.enum(["cursor", "page", "offset", "token"]),
11
+ cursor_param: z.string().optional(),
12
+ cursor_field: z.string().optional(),
13
+ has_more_field: z.string().optional(),
14
+ limit_param: z.string().optional(),
15
+ default_limit: z.number().int().positive().optional(),
16
+ items_field: z.string().optional(),
17
+ });
18
+
19
+ const ResponseSchema = z.object({
20
+ resource: z.string(),
21
+ pagination: PaginationSchema.nullable(),
22
+ rationale: z.string().optional(),
23
+ confidence: z.enum(["low", "medium", "high"]).optional(),
24
+ });
25
+
26
+ export const EXPECTED_OUTPUT_SHAPE = {
27
+ resource: "string (echo input)",
28
+ pagination: {
29
+ type: "cursor | page | offset | token",
30
+ cursor_param: "string (optional — query param carrying cursor value)",
31
+ cursor_field: "string (optional — response field that becomes next cursor)",
32
+ has_more_field: "string (optional — boolean response field signalling more)",
33
+ limit_param: "string (optional)",
34
+ default_limit: "integer (optional)",
35
+ items_field: "string (optional — response field carrying array)",
36
+ },
37
+ rationale: "string (optional)",
38
+ confidence: "low | medium | high",
39
+ null_form: "if list endpoint doesn't paginate, return { resource, pagination: null }",
40
+ };
41
+
42
+ export function parsePaginationResponse(parsed: unknown, slice: ResourceSlice): { patch: ResourcePatch; audit: Record<string, unknown> } {
43
+ const validated = ResponseSchema.safeParse(parsed);
44
+ if (!validated.success) {
45
+ throw new Error(`pagination response failed schema for ${slice.resource}: ${validated.error.message}`);
46
+ }
47
+ const v = validated.data;
48
+ if (v.pagination == null) {
49
+ return {
50
+ patch: { resource: slice.resource },
51
+ audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence, dropped: "endpoint does not paginate" },
52
+ };
53
+ }
54
+ return {
55
+ patch: { resource: slice.resource, pagination: v.pagination },
56
+ audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence },
57
+ };
58
+ }
59
+
60
+ export function isApplicable(slice: ResourceSlice): boolean { return Boolean(slice.endpoints.list); }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * ARV-187: shared prompt utilities — extract per-resource CRUD slices
3
+ * from a parsed OpenAPI document so each subcommand can build a tight,
4
+ * cheap prompt for the LLM.
5
+ *
6
+ * Per-resource slicing (vs. send-the-whole-spec) is the cost-control
7
+ * pattern AutoRestTest / KAT both use: keeps each call <4k tokens and
8
+ * lets us cache by `sha256(slice + prompt_version + model)`.
9
+ */
10
+
11
+ import type { OpenAPIV3 } from "openapi-types";
12
+ import type { ResourceYaml } from "../../discover.ts";
13
+
14
+ export interface ResourceSlice {
15
+ resource: string;
16
+ basePath: string;
17
+ itemPath: string;
18
+ /** Concrete EndpointInfo-like dump per role with the *relevant* spec
19
+ * bits inlined (summary, description, parameters, request schema,
20
+ * example bodies, x-codeSamples). Designed to be JSON.stringify'd
21
+ * straight into the prompt. */
22
+ endpoints: {
23
+ list?: EndpointDump;
24
+ create?: EndpointDump;
25
+ read?: EndpointDump;
26
+ update?: EndpointDump;
27
+ delete?: EndpointDump;
28
+ };
29
+ }
30
+
31
+ export interface EndpointDump {
32
+ method: string;
33
+ path: string;
34
+ operationId?: string;
35
+ summary?: string;
36
+ description?: string;
37
+ parameters?: Array<{
38
+ name: string;
39
+ in: string;
40
+ required?: boolean;
41
+ description?: string;
42
+ schema?: unknown;
43
+ }>;
44
+ requestBody?: {
45
+ contentType?: string;
46
+ description?: string;
47
+ schema?: unknown;
48
+ example?: unknown;
49
+ };
50
+ responses?: Record<string, { description?: string; schema?: unknown }>;
51
+ /** Stripe/Redocly convention — prose curl examples live here. */
52
+ xCodeSamples?: unknown;
53
+ }
54
+
55
+ /**
56
+ * Build per-resource CRUD slices from spec + the upstream resource map.
57
+ * We trust the resource map's endpoint pointers (already computed by
58
+ * the catalog builder) rather than re-doing path-grouping. Each slice
59
+ * carries the spec-fragments the LLM needs to reason about that
60
+ * resource — no more, no less.
61
+ */
62
+ export function buildResourceSlices(
63
+ doc: OpenAPIV3.Document,
64
+ resources: ResourceYaml[],
65
+ ): ResourceSlice[] {
66
+ return resources.map((r) => {
67
+ const out: ResourceSlice = {
68
+ resource: r.resource,
69
+ basePath: r.basePath,
70
+ itemPath: r.itemPath,
71
+ endpoints: {},
72
+ };
73
+ for (const role of ["list", "create", "read", "update", "delete"] as const) {
74
+ const label = r.endpoints[role];
75
+ if (!label) continue;
76
+ const parsed = parseLabel(label);
77
+ if (!parsed) continue;
78
+ const dump = dumpEndpoint(doc, parsed.method, parsed.path);
79
+ if (dump) out.endpoints[role] = dump;
80
+ }
81
+ return out;
82
+ });
83
+ }
84
+
85
+ function parseLabel(label: string): { method: string; path: string } | null {
86
+ const parts = label.trim().split(/\s+/);
87
+ if (parts.length < 2) return null;
88
+ return { method: parts[0]!.toLowerCase(), path: parts[1]! };
89
+ }
90
+
91
+ function dumpEndpoint(doc: OpenAPIV3.Document, method: string, path: string): EndpointDump | null {
92
+ const pathItem = doc.paths?.[path] as OpenAPIV3.PathItemObject | undefined;
93
+ if (!pathItem) return null;
94
+ const op = (pathItem as Record<string, unknown>)[method] as OpenAPIV3.OperationObject | undefined;
95
+ if (!op) return null;
96
+
97
+ const parameters: EndpointDump["parameters"] = [];
98
+ for (const p of [...(pathItem.parameters ?? []), ...(op.parameters ?? [])]) {
99
+ if ("$ref" in p) continue;
100
+ parameters.push({
101
+ name: p.name,
102
+ in: p.in,
103
+ required: p.required,
104
+ description: truncate(p.description, 240),
105
+ schema: simplifySchema(p.schema as OpenAPIV3.SchemaObject | undefined),
106
+ });
107
+ }
108
+
109
+ let requestBody: EndpointDump["requestBody"];
110
+ if (op.requestBody && !("$ref" in op.requestBody)) {
111
+ const rb = op.requestBody as OpenAPIV3.RequestBodyObject;
112
+ const contentEntries = Object.entries(rb.content ?? {});
113
+ const [ct, media] = contentEntries[0] ?? [];
114
+ if (media) {
115
+ requestBody = {
116
+ contentType: ct,
117
+ description: truncate(rb.description, 240),
118
+ schema: simplifySchema(media.schema as OpenAPIV3.SchemaObject | undefined),
119
+ example: media.example,
120
+ };
121
+ }
122
+ }
123
+
124
+ const responses: EndpointDump["responses"] = {};
125
+ for (const [code, resp] of Object.entries(op.responses ?? {})) {
126
+ if (resp && !("$ref" in resp)) {
127
+ const r = resp as OpenAPIV3.ResponseObject;
128
+ const content = Object.values(r.content ?? {})[0];
129
+ responses[code] = {
130
+ description: truncate(r.description, 240),
131
+ schema: simplifySchema(content?.schema as OpenAPIV3.SchemaObject | undefined),
132
+ };
133
+ }
134
+ }
135
+
136
+ return {
137
+ method: method.toUpperCase(),
138
+ path,
139
+ operationId: op.operationId,
140
+ summary: truncate(op.summary, 240),
141
+ description: truncate(op.description, 600),
142
+ parameters: parameters.length > 0 ? parameters : undefined,
143
+ requestBody,
144
+ responses: Object.keys(responses).length > 0 ? responses : undefined,
145
+ xCodeSamples: (op as Record<string, unknown>)["x-codeSamples"],
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Strip refs and nested noise from a schema dump. We don't need full
151
+ * fidelity — the LLM just needs to know the field names + types + any
152
+ * descriptions that hint at example values. Drops `additionalProperties`,
153
+ * collapses `oneOf/anyOf` to the first variant, truncates descriptions.
154
+ */
155
+ function simplifySchema(s: OpenAPIV3.SchemaObject | undefined): unknown {
156
+ if (!s) return undefined;
157
+ if ((s as OpenAPIV3.ReferenceObject).$ref) return { $ref: (s as OpenAPIV3.ReferenceObject).$ref };
158
+ const out: Record<string, unknown> = {};
159
+ if (s.type) out.type = s.type;
160
+ if (s.format) out.format = s.format;
161
+ if (s.enum) out.enum = s.enum;
162
+ if (s.example !== undefined) out.example = s.example;
163
+ if (s.default !== undefined) out.default = s.default;
164
+ if (s.description) out.description = truncate(s.description, 240);
165
+ if (s.required) out.required = s.required;
166
+ if (s.properties) {
167
+ out.properties = Object.fromEntries(
168
+ Object.entries(s.properties).map(([k, v]) => [k, simplifySchema(v as OpenAPIV3.SchemaObject)]),
169
+ );
170
+ }
171
+ const asArr = s as OpenAPIV3.ArraySchemaObject;
172
+ if (asArr.items) out.items = simplifySchema(asArr.items as OpenAPIV3.SchemaObject);
173
+ const o = s as Record<string, unknown>;
174
+ if (Array.isArray(o.oneOf) && o.oneOf.length > 0) out.oneOf_first = simplifySchema(o.oneOf[0] as OpenAPIV3.SchemaObject);
175
+ if (Array.isArray(o.anyOf) && o.anyOf.length > 0) out.anyOf_first = simplifySchema(o.anyOf[0] as OpenAPIV3.SchemaObject);
176
+ return out;
177
+ }
178
+
179
+ function truncate(s: string | undefined, n: number): string | undefined {
180
+ if (!s) return undefined;
181
+ return s.length > n ? s.slice(0, n) + "…" : s;
182
+ }
183
+
@@ -0,0 +1,58 @@
1
+ /**
2
+ * ARV-187 / readback: parser + expected shape.
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import type { ResourcePatch } from "./overlay.ts";
7
+ import type { ResourceSlice } from "./prompts.ts";
8
+
9
+ const ReadbackSchema = z.object({
10
+ ignore_fields: z.array(z.string()).default([]),
11
+ write_to_read_map: z.record(z.string(), z.string()).default({}),
12
+ });
13
+
14
+ const ResponseSchema = z.object({
15
+ resource: z.string(),
16
+ readback_diff: ReadbackSchema.nullable(),
17
+ rationale: z.string().optional(),
18
+ confidence: z.enum(["low", "medium", "high"]).optional(),
19
+ });
20
+
21
+ export const EXPECTED_OUTPUT_SHAPE = {
22
+ resource: "string (echo input)",
23
+ readback_diff: {
24
+ ignore_fields: "string[] (write-only fields cross_call_references should ignore)",
25
+ write_to_read_map: "{ <write_field>: <read_field> } (renames on the read shape)",
26
+ },
27
+ rationale: "string (optional)",
28
+ confidence: "low | medium | high",
29
+ null_form: "if read shape echoes write shape, return { resource, readback_diff: null }",
30
+ };
31
+
32
+ export function parseReadbackResponse(parsed: unknown, slice: ResourceSlice): { patch: ResourcePatch; audit: Record<string, unknown> } {
33
+ const validated = ResponseSchema.safeParse(parsed);
34
+ if (!validated.success) {
35
+ throw new Error(`readback response failed schema for ${slice.resource}: ${validated.error.message}`);
36
+ }
37
+ const v = validated.data;
38
+ if (v.readback_diff == null) {
39
+ return {
40
+ patch: { resource: slice.resource },
41
+ audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence, dropped: "no drift expected" },
42
+ };
43
+ }
44
+ if ((v.readback_diff.ignore_fields?.length ?? 0) === 0 && Object.keys(v.readback_diff.write_to_read_map ?? {}).length === 0) {
45
+ return {
46
+ patch: { resource: slice.resource },
47
+ audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence, dropped: "empty readback hints" },
48
+ };
49
+ }
50
+ return {
51
+ patch: { resource: slice.resource, readback_diff: v.readback_diff },
52
+ audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence },
53
+ };
54
+ }
55
+
56
+ export function isApplicable(slice: ResourceSlice): boolean {
57
+ return Boolean(slice.endpoints.create && slice.endpoints.read);
58
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * ARV-187 / resources: parser + expected shape for orphan-clustering.
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import type { ResourceYaml } from "../../discover.ts";
7
+
8
+ const EndpointMapSchema = z.object({
9
+ list: z.string().optional(),
10
+ create: z.string().optional(),
11
+ read: z.string().optional(),
12
+ update: z.string().optional(),
13
+ delete: z.string().optional(),
14
+ });
15
+
16
+ const FkSchema = z.object({
17
+ var: z.string(),
18
+ param: z.string(),
19
+ in: z.enum(["path", "body"]),
20
+ ownerResource: z.string().nullable(),
21
+ });
22
+
23
+ const ExtensionSchema = z.object({
24
+ resource: z.string(),
25
+ basePath: z.string(),
26
+ itemPath: z.string(),
27
+ idParam: z.string(),
28
+ captureField: z.string().optional(),
29
+ hasFullCrud: z.boolean().optional(),
30
+ endpoints: EndpointMapSchema,
31
+ fkDependencies: z.array(FkSchema).default([]),
32
+ confidence: z.enum(["low", "medium", "high"]),
33
+ rationale: z.string().optional(),
34
+ });
35
+
36
+ const ResponseSchema = z.object({
37
+ extensions: z.array(ExtensionSchema).default([]),
38
+ rationale: z.string().optional(),
39
+ });
40
+
41
+ export const EXPECTED_OUTPUT_SHAPE = {
42
+ extensions: [{
43
+ resource: "slug",
44
+ basePath: "/v1/<collection>",
45
+ itemPath: "/v1/<collection>/{<id_param>}",
46
+ idParam: "string",
47
+ captureField: "string (optional, default 'id')",
48
+ hasFullCrud: "boolean (optional)",
49
+ endpoints: { list: "GET /...", create: "POST /...", read: "GET /...", update: "PATCH /...", delete: "DELETE /..." },
50
+ fkDependencies: "FkRef[] (usually [])",
51
+ confidence: "low | medium | high (only 'high' is accepted by zond)",
52
+ rationale: "string (optional)",
53
+ }],
54
+ rationale: "string (optional)",
55
+ };
56
+
57
+ export interface ResourcesParseResult {
58
+ extensions: ResourceYaml[];
59
+ audit: {
60
+ proposed: number;
61
+ droppedLowConfidence: number;
62
+ rationale?: string;
63
+ };
64
+ }
65
+
66
+ export function parseResourcesResponse(parsed: unknown): ResourcesParseResult {
67
+ const validated = ResponseSchema.safeParse(parsed);
68
+ if (!validated.success) {
69
+ throw new Error(`resources response failed schema: ${validated.error.message}`);
70
+ }
71
+ const v = validated.data;
72
+ let dropped = 0;
73
+ const extensions: ResourceYaml[] = [];
74
+ for (const ext of v.extensions) {
75
+ if (ext.confidence !== "high") { dropped++; continue; }
76
+ extensions.push({
77
+ resource: ext.resource,
78
+ basePath: ext.basePath,
79
+ itemPath: ext.itemPath,
80
+ idParam: ext.idParam,
81
+ captureField: ext.captureField,
82
+ hasFullCrud: ext.hasFullCrud,
83
+ endpoints: ext.endpoints,
84
+ fkDependencies: ext.fkDependencies,
85
+ });
86
+ }
87
+ return {
88
+ extensions,
89
+ audit: { proposed: v.extensions.length, droppedLowConfidence: dropped, rationale: v.rationale },
90
+ };
91
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * ARV-187 / seed-bodies: parser + schema-shape. zond does NOT
3
+ * formulate prompts — that's the agent's job. zond exposes the spec
4
+ * slice and the expected response shape; the agent decides how to ask
5
+ * its LLM.
6
+ */
7
+
8
+ import { z } from "zod";
9
+ import type { ResourcePatch } from "./overlay.ts";
10
+ import type { ResourceSlice } from "./prompts.ts";
11
+
12
+ const SeedBodySchema = z.object({
13
+ content_type: z.string().optional(),
14
+ body: z.record(z.string(), z.unknown()),
15
+ });
16
+
17
+ const ResponseSchema = z.object({
18
+ resource: z.string(),
19
+ seed_body: SeedBodySchema.nullable(),
20
+ rationale: z.string().optional(),
21
+ confidence: z.enum(["low", "medium", "high"]).optional(),
22
+ });
23
+
24
+ export const EXPECTED_OUTPUT_SHAPE = {
25
+ resource: "string (echo input)",
26
+ seed_body: {
27
+ content_type: "string (optional; defaults to spec's requestBodyContentType)",
28
+ body: "object (minimal-required field-set the create endpoint accepts)",
29
+ },
30
+ rationale: "string (optional, one sentence)",
31
+ confidence: "low | medium | high",
32
+ null_form: "if endpoint cannot be seeded statically, return { resource, seed_body: null, rationale }",
33
+ };
34
+
35
+ export function parseSeedBodyResponse(parsedYaml: unknown, slice: ResourceSlice): { patch: ResourcePatch; audit: Record<string, unknown> } {
36
+ const validated = ResponseSchema.safeParse(parsedYaml);
37
+ if (!validated.success) {
38
+ throw new Error(`seed_body response failed schema for ${slice.resource}: ${validated.error.message}`);
39
+ }
40
+ const v = validated.data;
41
+ if (v.seed_body == null) {
42
+ return {
43
+ patch: { resource: slice.resource },
44
+ audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence, dropped: "agent judged endpoint not seedable" },
45
+ };
46
+ }
47
+ return {
48
+ patch: {
49
+ resource: slice.resource,
50
+ seed_body: {
51
+ content_type: v.seed_body.content_type ?? slice.endpoints.create?.requestBody?.contentType,
52
+ body: v.seed_body.body,
53
+ },
54
+ },
55
+ audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence },
56
+ };
57
+ }
58
+
59
+ export function isApplicable(slice: ResourceSlice): boolean {
60
+ return Boolean(slice.endpoints.create);
61
+ }