@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,74 +0,0 @@
1
- /**
2
- * ARV-187 / lifecycle: parser + expected shape. No prompts inside zond.
3
- */
4
-
5
- import { z } from "zod";
6
- import type { ResourcePatch } from "./overlay.ts";
7
- import type { ResourceSlice } from "./prompts.ts";
8
-
9
- const ActionSchema = z.object({
10
- endpoint: z.string(),
11
- expected_state: z.string(),
12
- body: z.record(z.string(), z.unknown()).optional(),
13
- });
14
-
15
- const LifecycleSchema = z.object({
16
- field: z.string(),
17
- states: z.array(z.string()).min(2),
18
- transitions: z.array(z.object({ from: z.string(), to: z.array(z.string()) })),
19
- actions: z.record(z.string(), ActionSchema).default({}),
20
- });
21
-
22
- const ResponseSchema = z.object({
23
- resource: z.string(),
24
- lifecycle: LifecycleSchema.nullable(),
25
- rationale: z.string().optional(),
26
- confidence: z.enum(["low", "medium", "high"]).optional(),
27
- });
28
-
29
- export const EXPECTED_OUTPUT_SHAPE = {
30
- resource: "string (echo input)",
31
- lifecycle: {
32
- field: "string (response field holding state, e.g. 'status')",
33
- states: "string[] (≥2 enum values)",
34
- transitions: "[{from: state, to: state[]}]",
35
- actions: "{ <verb>: { endpoint: 'METHOD /path', expected_state: state, body?: object } } — return {} for read-only state machines (observation mode walks the list endpoint and asserts observed ⊆ states; cannot verify transitions in this mode)",
36
- },
37
- rationale: "string (optional)",
38
- confidence: "low | medium | high",
39
- null_form: "if no observable state machine, return { resource, lifecycle: null }",
40
- };
41
-
42
- export function parseLifecycleResponse(parsed: unknown, slice: ResourceSlice): { patch: ResourcePatch; audit: Record<string, unknown> } {
43
- const validated = ResponseSchema.safeParse(parsed);
44
- if (!validated.success) {
45
- throw new Error(`lifecycle response failed schema for ${slice.resource}: ${validated.error.message}`);
46
- }
47
- const v = validated.data;
48
- if (v.lifecycle == null) {
49
- return {
50
- patch: { resource: slice.resource },
51
- audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence, dropped: "no state machine" },
52
- };
53
- }
54
- return {
55
- patch: {
56
- resource: slice.resource,
57
- lifecycle: {
58
- field: v.lifecycle.field,
59
- states: v.lifecycle.states,
60
- transitions: v.lifecycle.transitions,
61
- actions: Object.fromEntries(
62
- Object.entries(v.lifecycle.actions).map(([name, a]) => [name, {
63
- endpoint: a.endpoint,
64
- expected_state: a.expected_state,
65
- body: a.body,
66
- }]),
67
- ),
68
- },
69
- },
70
- audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence },
71
- };
72
- }
73
-
74
- export function isApplicable(_slice: ResourceSlice): boolean { return true; }
@@ -1,206 +0,0 @@
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 structuredClone(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
- }
@@ -1,64 +0,0 @@
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
- page_param: z.string().optional(),
18
- start_page: z.number().int().nonnegative().optional(),
19
- });
20
-
21
- const ResponseSchema = z.object({
22
- resource: z.string(),
23
- pagination: PaginationSchema.nullable(),
24
- rationale: z.string().optional(),
25
- confidence: z.enum(["low", "medium", "high"]).optional(),
26
- });
27
-
28
- export const EXPECTED_OUTPUT_SHAPE = {
29
- resource: "string (echo input)",
30
- pagination: {
31
- type: "cursor | page | offset | token (cursor and page are implemented; offset/token short-circuit)",
32
- cursor_param: "string (optional — only for type=cursor; query param carrying cursor value)",
33
- cursor_field: "string (optional — for cursor: next-cursor field; for page: dedupe key across pages; default `id`)",
34
- has_more_field: "string (optional — only for type=cursor; boolean response field signalling more)",
35
- limit_param: "string (optional — page size param; default `limit` for cursor, `per_page` for page)",
36
- default_limit: "integer (optional — probe page size; default 2)",
37
- items_field: "string (optional — response field carrying array)",
38
- page_param: "string (optional — only for type=page; default `page`)",
39
- start_page: "integer (optional — only for type=page; first page number, default 1; some APIs use 0)",
40
- },
41
- rationale: "string (optional)",
42
- confidence: "low | medium | high",
43
- null_form: "if list endpoint doesn't paginate, return { resource, pagination: null }",
44
- };
45
-
46
- export function parsePaginationResponse(parsed: unknown, slice: ResourceSlice): { patch: ResourcePatch; audit: Record<string, unknown> } {
47
- const validated = ResponseSchema.safeParse(parsed);
48
- if (!validated.success) {
49
- throw new Error(`pagination response failed schema for ${slice.resource}: ${validated.error.message}`);
50
- }
51
- const v = validated.data;
52
- if (v.pagination == null) {
53
- return {
54
- patch: { resource: slice.resource },
55
- audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence, dropped: "endpoint does not paginate" },
56
- };
57
- }
58
- return {
59
- patch: { resource: slice.resource, pagination: v.pagination },
60
- audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence },
61
- };
62
- }
63
-
64
- export function isApplicable(slice: ResourceSlice): boolean { return Boolean(slice.endpoints.list); }
@@ -1,220 +0,0 @@
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 raw of [...(pathItem.parameters ?? []), ...(op.parameters ?? [])]) {
99
- const p = resolveParameter(doc, raw);
100
- if (!p) continue;
101
- parameters.push({
102
- name: p.name,
103
- in: p.in,
104
- required: p.required,
105
- description: truncate(p.description, 240),
106
- schema: simplifySchema(p.schema as OpenAPIV3.SchemaObject | undefined),
107
- });
108
- }
109
-
110
- let requestBody: EndpointDump["requestBody"];
111
- if (op.requestBody && !("$ref" in op.requestBody)) {
112
- const rb = op.requestBody as OpenAPIV3.RequestBodyObject;
113
- const contentEntries = Object.entries(rb.content ?? {});
114
- const [ct, media] = contentEntries[0] ?? [];
115
- if (media) {
116
- requestBody = {
117
- contentType: ct,
118
- description: truncate(rb.description, 240),
119
- schema: simplifySchema(media.schema as OpenAPIV3.SchemaObject | undefined),
120
- example: media.example,
121
- };
122
- }
123
- }
124
-
125
- const responses: EndpointDump["responses"] = {};
126
- for (const [code, resp] of Object.entries(op.responses ?? {})) {
127
- if (resp && !("$ref" in resp)) {
128
- const r = resp as OpenAPIV3.ResponseObject;
129
- const content = Object.values(r.content ?? {})[0];
130
- responses[code] = {
131
- description: truncate(r.description, 240),
132
- schema: simplifySchema(content?.schema as OpenAPIV3.SchemaObject | undefined),
133
- };
134
- }
135
- }
136
-
137
- return {
138
- method: method.toUpperCase(),
139
- path,
140
- operationId: op.operationId,
141
- summary: truncate(op.summary, 240),
142
- description: truncate(op.description, 600),
143
- parameters: parameters.length > 0 ? parameters : undefined,
144
- requestBody,
145
- responses: Object.keys(responses).length > 0 ? responses : undefined,
146
- xCodeSamples: (op as Record<string, unknown>)["x-codeSamples"],
147
- };
148
- }
149
-
150
- /**
151
- * Strip refs and nested noise from a schema dump. We don't need full
152
- * fidelity — the LLM just needs to know the field names + types + any
153
- * descriptions that hint at example values. Drops `additionalProperties`,
154
- * collapses `oneOf/anyOf` to the first variant, truncates descriptions.
155
- */
156
- function simplifySchema(s: OpenAPIV3.SchemaObject | undefined): unknown {
157
- if (!s) return undefined;
158
- if ((s as OpenAPIV3.ReferenceObject).$ref) return { $ref: (s as OpenAPIV3.ReferenceObject).$ref };
159
- const out: Record<string, unknown> = {};
160
- if (s.type) out.type = s.type;
161
- if (s.format) out.format = s.format;
162
- if (s.enum) out.enum = s.enum;
163
- if (s.example !== undefined) out.example = s.example;
164
- if (s.default !== undefined) out.default = s.default;
165
- if (s.description) out.description = truncate(s.description, 240);
166
- if (s.required) out.required = s.required;
167
- if (s.properties) {
168
- out.properties = Object.fromEntries(
169
- Object.entries(s.properties).map(([k, v]) => [k, simplifySchema(v as OpenAPIV3.SchemaObject)]),
170
- );
171
- }
172
- const asArr = s as OpenAPIV3.ArraySchemaObject;
173
- if (asArr.items) out.items = simplifySchema(asArr.items as OpenAPIV3.SchemaObject);
174
- const o = s as Record<string, unknown>;
175
- if (Array.isArray(o.oneOf) && o.oneOf.length > 0) out.oneOf_first = simplifySchema(o.oneOf[0] as OpenAPIV3.SchemaObject);
176
- if (Array.isArray(o.anyOf) && o.anyOf.length > 0) out.anyOf_first = simplifySchema(o.anyOf[0] as OpenAPIV3.SchemaObject);
177
- return out;
178
- }
179
-
180
- function truncate(s: string | undefined, n: number): string | undefined {
181
- if (!s) return undefined;
182
- return s.length > n ? s.slice(0, n) + "…" : s;
183
- }
184
-
185
- /**
186
- * Resolve a parameter that may be inline or a $ref. Handles
187
- * `#/components/parameters/X` (canonical) and, leniently,
188
- * `#/components/headers/X` — some specs (Stripe) keep their
189
- * `Idempotency-Key` declaration under `components.headers` and reuse
190
- * it as a parameter $ref. Returns null on unresolvable refs.
191
- */
192
- function resolveParameter(
193
- doc: OpenAPIV3.Document,
194
- raw: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject,
195
- ): OpenAPIV3.ParameterObject | null {
196
- if (!("$ref" in raw)) return raw;
197
- const ref = raw.$ref;
198
- const paramMatch = ref.match(/^#\/components\/parameters\/(.+)$/);
199
- if (paramMatch) {
200
- const target = doc.components?.parameters?.[paramMatch[1]!];
201
- if (target && !("$ref" in target)) return target as OpenAPIV3.ParameterObject;
202
- }
203
- const headerMatch = ref.match(/^#\/components\/headers\/(.+)$/);
204
- if (headerMatch) {
205
- const name = headerMatch[1]!;
206
- const target = doc.components?.headers?.[name];
207
- if (target && !("$ref" in target)) {
208
- const h = target as OpenAPIV3.HeaderObject;
209
- return {
210
- name,
211
- in: "header",
212
- required: h.required,
213
- description: h.description,
214
- schema: h.schema,
215
- } as OpenAPIV3.ParameterObject;
216
- }
217
- }
218
- return null;
219
- }
220
-
@@ -1,58 +0,0 @@
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
- }