@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
package/README.md CHANGED
@@ -4,35 +4,69 @@ AI-powered API testing for Claude Code, Cursor, and CI/CD.
4
4
 
5
5
  Say "test my API" — get working tests, coverage dashboard, and CI config in minutes.
6
6
 
7
- <!-- TODO: add demo GIF (15 sec: plugin install "cover openapi.json with tests" 42/47 endpoints covered dashboard) -->
8
-
9
- Zond reads your OpenAPI spec and gives your AI agent everything it needs to test your API: structured tools, safety guardrails, coverage tracking, and run history. You don't need to learn anything new — just describe what you want and the agent handles the rest.
7
+ Zond reads your OpenAPI spec and gives your AI agent everything it needs to test your API: a focused CLI, safety guardrails, coverage tracking, and run history. You don't need to learn anything new just describe what you want and the agent runs `zond` commands.
10
8
 
11
9
  ## Quick Start
12
10
 
11
+ Install the binary (no Node.js required):
12
+
13
+ ```bash
14
+ curl -fsSL https://raw.githubusercontent.com/kirrosh/zond/master/install.sh | sh # macOS/Linux
15
+ iwr https://raw.githubusercontent.com/kirrosh/zond/master/install.ps1 | iex # Windows
16
+ ```
17
+
18
+ Bootstrap a workspace, register your first API, then fill its fixtures:
19
+
20
+ ```bash
21
+ zond init # bootstrap workspace (no fixture changes)
22
+ zond add api my-api --spec ./openapi.json # register: copies spec.json + emits manifest
23
+ zond doctor --api my-api --missing-only # gap report: which vars are UNSET
24
+ zond prepare-fixtures --api my-api --apply [--seed] # fill apis/my-api/.env.yaml from live API
13
25
  ```
14
- /plugin marketplace add kirrosh/zond
15
- /plugin install zond@zond-marketplace
26
+
27
+ For path-FK ids that auto-discover/--seed can't reach (vendor-dashboard
28
+ ids like `cus_*`, GitHub PR numbers, Sentry issue ids), use the manual
29
+ helpers (ARV-195):
30
+
31
+ ```bash
32
+ zond fixtures add --api my-api customer_id=cus_123 --validate --apply
33
+ pbpaste | zond fixtures import --api my-api --from-curl --apply # paste a curl from devtools
16
34
  ```
17
35
 
18
- Then say: _"Safely cover the API from openapi.json with tests"_
36
+ `zond init` writes a self-contained [`AGENTS.md`](AGENTS.md) and Claude Code
37
+ skills — agents read it and use the CLI directly (`zond run`,
38
+ `zond probe static`, `zond db diagnose`, …). No daemon, no transport, no
39
+ extra configuration. `init` is workspace-only — it never touches
40
+ `.env.yaml`; the fixture loop above is the canonical path.
19
41
 
20
- You get auto-validation hooks and CLI tools — all in one package.
42
+ Each registered API gets four files in `apis/<name>/`:
21
43
 
22
- <details>
23
- <summary>Other installation methods (CLI, binary)</summary>
44
+ - `spec.json` — dereferenced OpenAPI snapshot (canonical machine source).
45
+ - `.api-catalog.yaml` endpoint index for agents (cheap to read).
46
+ - `.api-resources.yaml` — CRUD chains, FK dependencies, ETag/soft-delete flags.
47
+ - `.api-fixtures.yaml` — **manifest** of required `{{vars}}` (read-only, auto-generated).
48
+
49
+ Plus a sibling `.env.yaml` that you (or `zond prepare-fixtures`) fill with
50
+ the **values** for those vars. The manifest/values split is strict — see
51
+ the [workspace contract](AGENTS.md#workspace-contract) for details.
52
+
53
+ Run `zond refresh-api <name> [--spec <new-source>]` to re-snapshot when the
54
+ upstream spec changes.
55
+
56
+ Then say to your agent: _"Safely cover the API from openapi.json with tests."_
24
57
 
25
- ### CLI / Binary
58
+ Want the whole pipeline at once? `zond audit --api my-api` runs
59
+ prepare-fixtures → generate → probes → run → coverage → HTML report in a
60
+ single shot.
61
+
62
+ <details>
63
+ <summary>Other installation methods (npx)</summary>
26
64
 
27
65
  ```bash
28
66
  npx -y @kirrosh/zond --version
29
-
30
- # Standalone binary (no Node.js required)
31
- curl -fsSL https://raw.githubusercontent.com/kirrosh/zond/master/install.sh | sh # macOS/Linux
32
- iwr https://raw.githubusercontent.com/kirrosh/zond/master/install.ps1 | iex # Windows
33
67
  ```
34
68
 
35
- See [ZOND.md](ZOND.md) for full CLI reference.
69
+ See [ZOND.md](ZOND.md) for the full CLI reference.
36
70
 
37
71
  </details>
38
72
 
@@ -56,6 +90,11 @@ Claude Code can write pytest from scratch — but it takes 30-60 minutes per flo
56
90
  | **Spec-Grounded** | Tests are derived from your OpenAPI schema, not invented from scratch. The spec is the source of truth. |
57
91
  | **Full Visibility** | Every run is stored in SQLite. Compare runs, track regressions, see exactly what the server returned. |
58
92
  | **Coverage Tracking** | See which endpoints are tested, which aren't, and what broke since last run. |
93
+ | **Schema Validation** | `--validate-schema` checks every JSON response against the OpenAPI schema (types, required, enum, format, `$ref`) — catches contract drift the YAML expectations miss. |
94
+ | **Spec Linting** | `zond check spec` static-analyses the OpenAPI document for internal-consistency bugs (e.g. example violates `format: date-time`) and strictness gaps (path-params without `format`, integer params without min/max) — surfaces issues before any HTTP request. |
95
+ | **Depth Checks (m-15)** | `zond checks run` runs a schemathesis-style catalog of conformance + security probes (`status_code_conformance`, `negative_data_rejection`, `ignored_auth`, `use_after_free`, …) — boundary-value coverage, broken-auth detection, soft-deleted resource leaks. Every finding ships with a `recommended_action` enum so the agent triages without parsing messages. |
96
+ | **SARIF for Code Scanning** | `--report sarif` emits SARIF v2.1.0 with stable `partialFingerprints` — drop-in for `github/codeql-action/upload-sarif@v3` so depth-checks findings show up in GitHub's Security tab. |
97
+ | **Concurrent Workers** | `--workers auto` parallelizes runs at the operation level (bounded async-pool, no threading) — runs that took minutes finish in seconds. Pair with `--rate-limit` to stay within an API's RPS budget. |
59
98
  | **CI-Ready** | One command generates GitHub Actions or GitLab CI workflow. Tests in YAML, in git, with code review. |
60
99
 
61
100
  ## Try It
@@ -67,11 +106,35 @@ Claude Code can write pytest from scratch — but it takes 30-60 minutes per flo
67
106
  "Set up CI for API tests"
68
107
  ```
69
108
 
109
+ ## Upgrading
110
+
111
+ `zond update` was removed in favour of system package managers:
112
+
113
+ ```bash
114
+ # macOS / Linux — re-run the installer
115
+ curl -fsSL https://raw.githubusercontent.com/kirrosh/zond/master/install.sh | sh
116
+
117
+ # npm
118
+ npm install -g @kirrosh/zond@latest
119
+
120
+ # bun
121
+ bun install -g @kirrosh/zond@latest
122
+ ```
123
+
124
+ ## Shell completions
125
+
126
+ ```bash
127
+ zond completions bash > ~/.local/share/bash-completion/completions/zond
128
+ zond completions zsh > ~/.zsh/completions/_zond # then `compinit`
129
+ zond completions fish > ~/.config/fish/completions/zond.fish
130
+ ```
131
+
70
132
  ## Documentation
71
133
 
72
134
  - [ZOND.md](ZOND.md) — full CLI reference
73
135
  - [docs/quickstart.md](docs/quickstart.md) — step-by-step quickstart (RU)
74
136
  - [docs/ci.md](docs/ci.md) — CI/CD integration
137
+ - [backlog/](backlog/) — project tasks (powered by [Backlog.md](https://backlog.md))
75
138
 
76
139
  ## License
77
140
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kirrosh/zond",
3
- "version": "0.21.0",
4
- "description": "API testing platform — define tests in YAML, run from CLI or WebUI, generate from OpenAPI specs",
3
+ "version": "0.23.0",
4
+ "description": "API testing platform — define tests in YAML, run from CLI, generate from OpenAPI specs",
5
5
  "license": "MIT",
6
6
  "module": "index.ts",
7
7
  "type": "module",
@@ -25,17 +25,23 @@
25
25
  },
26
26
  "scripts": {
27
27
  "zond": "bun run src/cli/index.ts",
28
+ "backlog": "bunx backlog",
29
+ "board": "bunx backlog board",
28
30
  "test": "bun run test:unit && bun run test:mocked",
29
- "test:unit": "bun test tests/db/ tests/parser/ tests/runner/ tests/generator/ tests/core/ tests/diagnostics/ tests/cli/args.test.ts tests/cli/ci-init.test.ts tests/cli/commands.test.ts tests/cli/safe-run.test.ts tests/cli/json-envelope.test.ts tests/cli/describe.test.ts tests/cli/catalog.test.ts tests/cli/db.test.ts tests/cli/request.test.ts tests/cli/init.test.ts tests/cli/guide.test.ts tests/integration/ tests/web/ tests/reporter/ tests/version-sync.test.ts",
31
+ "test:unit": "bun test tests/",
30
32
  "test:mocked": "bun run scripts/run-mocked-tests.ts",
31
33
  "check": "tsc --noEmit --project tsconfig.json",
32
- "build": "bun build --compile src/cli/index.ts --outfile zond",
33
- "version:sync": "bun run scripts/sync-version.ts",
34
- "postversion": "bun run scripts/sync-version.ts && git add .claude-plugin/plugin.json",
34
+ "schemas": "bun run scripts/emit-json-schemas.ts",
35
+ "schemas:check": "bun run scripts/emit-json-schemas.ts --check",
36
+ "lint:dead": "knip --reporter compact",
37
+ "build": "bun build --compile src/cli/index.ts --outfile dist/zond && bun run scripts/codesign-darwin.ts ./dist/zond",
35
38
  "bench:api": "bun benchmarks/api/server.ts"
36
39
  },
37
40
  "devDependencies": {
38
- "@types/bun": "latest"
41
+ "@types/bun": "latest",
42
+ "ajv-draft-04": "^1.0.0",
43
+ "backlog.md": "^1.44.0",
44
+ "knip": "^6.7.0"
39
45
  },
40
46
  "engines": {
41
47
  "bun": ">=1.1.0"
@@ -44,11 +50,12 @@
44
50
  "typescript": "^5"
45
51
  },
46
52
  "dependencies": {
47
- "@humanwhocodes/momoa": "^2.0.3",
48
- "@hono/zod-openapi": "^1.2.2",
49
53
  "@readme/openapi-parser": "^5.5.0",
50
- "hono": "^4.12.2",
54
+ "ajv": "^8.20.0",
55
+ "ajv-formats": "^3.0.1",
56
+ "commander": "^14.0.0",
51
57
  "openapi-types": "^12.1.3",
58
+ "yaml": "^2.8.3",
52
59
  "zod": "^4.3.6"
53
60
  }
54
61
  }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Pre-commander argv handling and shared argument parsers used across the
3
+ * commander tree. Pulled out of program.ts (TASK-190, m-11) so program.ts
4
+ * shrinks toward "just command registration" and these helpers can be
5
+ * unit-tested in isolation.
6
+ */
7
+
8
+ import { InvalidArgumentError } from "commander";
9
+ import type { ReporterName } from "../core/reporter/types.ts";
10
+
11
+ // ── MSYS path preprocessing ──
12
+ //
13
+ // Git Bash on Windows converts API paths like "/users" → "C:/Program Files/Git/users".
14
+ // We reverse that for flags whose values are API paths, not filesystem paths.
15
+
16
+ const MSYS_PREFIX_RE = /^[A-Z]:[\\/](?:Program Files[\\/]Git|msys64|usr)[\\/]/i;
17
+
18
+ const API_PATH_FLAGS = new Set(["--path", "--json-path"]);
19
+
20
+ function stripMsysPath(value: string): string {
21
+ if (!MSYS_PREFIX_RE.test(value)) return value;
22
+ return value.replace(MSYS_PREFIX_RE, "/");
23
+ }
24
+
25
+ /**
26
+ * Pre-process argv before commander sees it: undo Git Bash's MSYS path conversion
27
+ * for `--path` and `--json-path` values (both `--path X` and `--path=X` forms).
28
+ */
29
+ export function preprocessArgv(argv: string[]): string[] {
30
+ const out = [...argv];
31
+ for (let i = 0; i < out.length; i++) {
32
+ const arg = out[i]!;
33
+
34
+ // --flag=value form
35
+ const eqIdx = arg.indexOf("=");
36
+ if (arg.startsWith("--") && eqIdx !== -1) {
37
+ const flag = arg.slice(0, eqIdx);
38
+ if (API_PATH_FLAGS.has(flag)) {
39
+ out[i] = `${flag}=${stripMsysPath(arg.slice(eqIdx + 1))}`;
40
+ }
41
+ continue;
42
+ }
43
+
44
+ // --flag value form
45
+ if (API_PATH_FLAGS.has(arg)) {
46
+ const next = out[i + 1];
47
+ if (next !== undefined && !next.startsWith("-")) {
48
+ out[i + 1] = stripMsysPath(next);
49
+ }
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+
55
+ // ── Argument parsers ──
56
+
57
+ export function parsePositiveInt(name: string): (raw: string) => number {
58
+ return (raw: string) => {
59
+ const n = Number.parseInt(raw, 10);
60
+ if (Number.isNaN(n) || n <= 0) {
61
+ throw new InvalidArgumentError(`Invalid ${name} value: ${raw}`);
62
+ }
63
+ return n;
64
+ };
65
+ }
66
+
67
+ /** `--rate-limit` accepts a positive integer (req/sec cap) or the literal
68
+ * string `auto` (no static cap; throttle adaptively from ratelimit-* headers). */
69
+ export function parseRateLimit(raw: string): number | "auto" {
70
+ if (raw.toLowerCase() === "auto") return "auto";
71
+ const n = Number.parseInt(raw, 10);
72
+ if (Number.isNaN(n) || n <= 0) {
73
+ throw new InvalidArgumentError(`Invalid --rate-limit value: ${raw} (expected a positive integer or "auto")`);
74
+ }
75
+ return n;
76
+ }
77
+
78
+ export function parseNonNegativeInt(name: string): (raw: string) => number {
79
+ return (raw: string) => {
80
+ const n = Number.parseInt(raw, 10);
81
+ if (Number.isNaN(n) || n < 0 || String(n) !== raw.trim()) {
82
+ throw new InvalidArgumentError(`Invalid ${name} value: ${raw} (expected a non-negative integer)`);
83
+ }
84
+ return n;
85
+ };
86
+ }
87
+
88
+ export function parseInteger(name: string): (raw: string) => number {
89
+ return (raw: string) => {
90
+ const n = Number.parseInt(raw, 10);
91
+ if (Number.isNaN(n)) {
92
+ throw new InvalidArgumentError(`Invalid ${name} value: ${raw}`);
93
+ }
94
+ return n;
95
+ };
96
+ }
97
+
98
+ export function parsePercentage(raw: string): number {
99
+ const n = Number.parseInt(raw, 10);
100
+ if (Number.isNaN(n) || n < 0 || n > 100) {
101
+ throw new InvalidArgumentError(`Invalid --fail-on-coverage value: ${raw} (must be 0–100)`);
102
+ }
103
+ return n;
104
+ }
105
+
106
+ export const collect = (val: string, prev: string[]): string[] => [...prev, val];
107
+
108
+ const VALID_REPORTERS = new Set<string>(["console", "json", "junit"]);
109
+
110
+ export function parseReporter(raw: string): ReporterName {
111
+ if (!VALID_REPORTERS.has(raw)) {
112
+ throw new InvalidArgumentError(`Unknown reporter: ${raw}. Available: console, json, junit`);
113
+ }
114
+ return raw as ReporterName;
115
+ }
116
+
117
+ /** Helper: split repeatable values like ["a,b", "c"] → ["a", "b", "c"] */
118
+ export function flatSplit(values: string[] | undefined): string[] | undefined {
119
+ if (!values || values.length === 0) return undefined;
120
+ const out = values.flatMap((v) => v.split(",")).filter(Boolean);
121
+ return out.length > 0 ? out : undefined;
122
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * `zond add api <name> --spec <path|url>` — register a new API in the
3
+ * current workspace.
4
+ *
5
+ * The split from `zond init --spec` exists so the two operations
6
+ * (workspace bootstrap vs. API registration) have separate names and
7
+ * separate skill mentions. `init --spec` still works as a deprecated
8
+ * alias.
9
+ *
10
+ * This command refuses to run when no workspace marker is present,
11
+ * pointing the user at `zond init` first. setupApi handles spec
12
+ * snapshot + artifact generation.
13
+ */
14
+
15
+ import { setupApi, type SetupApiResult } from "../../core/setup-api.ts";
16
+ import { findWorkspaceRoot } from "../../core/workspace/root.ts";
17
+ import { jsonOk, jsonError, printJson, zerr } from "../json-envelope.ts";
18
+ import { printError, printSuccess } from "../output.ts";
19
+
20
+ export interface AddApiOptions {
21
+ name: string;
22
+ spec?: string;
23
+ baseUrl?: string;
24
+ dir?: string;
25
+ force?: boolean;
26
+ insecure?: boolean;
27
+ dbPath?: string;
28
+ json?: boolean;
29
+ }
30
+
31
+ export async function addApiCommand(opts: AddApiOptions): Promise<number> {
32
+ const ws = findWorkspaceRoot();
33
+ if (ws.fromFallback) {
34
+ const m = `No workspace detected (no zond.config.yml / .zond / zond.db / apis marker). Run \`zond init\` first to bootstrap a workspace.`;
35
+ if (opts.json) printJson(jsonError("add-api", [m])); else printError(m);
36
+ return 2;
37
+ }
38
+
39
+ const envVars: Record<string, string> = {};
40
+ if (opts.baseUrl) envVars.base_url = opts.baseUrl;
41
+
42
+ let result: SetupApiResult;
43
+ try {
44
+ result = await setupApi({
45
+ name: opts.name,
46
+ spec: opts.spec,
47
+ dir: opts.dir,
48
+ envVars: Object.keys(envVars).length > 0 ? envVars : undefined,
49
+ dbPath: opts.dbPath,
50
+ force: opts.force,
51
+ insecure: opts.insecure,
52
+ });
53
+ } catch (err) {
54
+ const m = (err as Error).message;
55
+ // Tag known spec-ingest failures with a structured code so downstream
56
+ // tooling (skills, retry logic) can branch on it (ARV-145). Cyclic
57
+ // structures escape decycleSchema only when @readme/openapi-parser
58
+ // builds an unusual graph — surface that as spec_load_failure with a
59
+ // pointer to the underlying serializer message.
60
+ const isCycleError = /cyclic structures|spec_serialize_failed/i.test(m);
61
+ const errInput = isCycleError ? zerr("spec_load_failure", m) : m;
62
+ if (opts.json) printJson(jsonError("add-api", [errInput])); else printError(m);
63
+ return 2;
64
+ }
65
+
66
+ const mode: "spec" | "run-only" = opts.spec ? "spec" : "run-only";
67
+ const artifacts = mode === "spec"
68
+ ? ["spec.json", ".api-catalog.yaml", ".api-resources.yaml", ".api-fixtures.yaml", ".env.yaml"]
69
+ : [".env.yaml"];
70
+
71
+ if (opts.json) {
72
+ printJson(jsonOk("add-api", {
73
+ api: opts.name,
74
+ mode,
75
+ collectionId: result.collectionId,
76
+ baseDir: result.baseDir,
77
+ testPath: result.testPath,
78
+ endpoints: result.specEndpoints,
79
+ artifacts,
80
+ }, result.warnings));
81
+ } else {
82
+ if (mode === "spec") {
83
+ printSuccess(`Registered API '${opts.name}' at ${result.baseDir} (${result.specEndpoints} endpoints)`);
84
+ process.stdout.write(` Artifacts: spec.json + .api-catalog.yaml + .api-resources.yaml + .api-fixtures.yaml\n`);
85
+ if (result.authVars && result.authVars.length > 0) {
86
+ const list = result.authVars.map((v) => `\`${v}\``).join(", ");
87
+ process.stdout.write(` Auth required: fill ${list} in ${result.baseDir}/.secrets.yaml (already wired via @secret in .env.yaml).\n`);
88
+ }
89
+ process.stdout.write(` Next: run \`zond doctor --api ${opts.name}\` to see required fixtures.\n`);
90
+ } else {
91
+ printSuccess(`Registered API '${opts.name}' at ${result.baseDir} (no spec — run-only mode)`);
92
+ process.stdout.write(` Artifacts: .env.yaml (base_url=${opts.baseUrl})\n`);
93
+ process.stdout.write(` Next: write tests in ${result.testPath}/, run \`zond run --api ${opts.name} <test.yaml>\`.\n`);
94
+ process.stdout.write(` To enable generate/probe/validate-schema, attach a spec: \`zond refresh-api ${opts.name} --spec <path|url>\`.\n`);
95
+ }
96
+ if (result.warnings) for (const w of result.warnings) process.stderr.write(`Warning: ${w}\n`);
97
+ }
98
+ return 0;
99
+ }
100
+
101
+ import type { Command } from "commander";
102
+ import { globalJson } from "../resolve.ts";
103
+
104
+ export function registerAdd(program: Command): void {
105
+ const add = program.command("add").description("Register objects in the workspace");
106
+ add
107
+ .command("api <name>")
108
+ .description("Register an API: from an OpenAPI spec (full toolkit) or just --base-url (run-only mode)")
109
+ .option("--spec <path>", "Path or URL to OpenAPI spec — enables generate/probe/validate-schema")
110
+ .option("--base-url <url>", "Base URL recorded in .env.yaml (required if --spec is omitted)")
111
+ .option("--dir <path>", "Target directory (defaults to apis/<name>/)")
112
+ .option("--force", "Overwrite an existing API with the same name")
113
+ .option("--insecure", "Skip TLS verification when fetching the spec from https")
114
+ .option("--db <path>", "Path to SQLite database file")
115
+ .action(async (name: string, opts, cmd: Command) => {
116
+ const json = globalJson(cmd);
117
+ if (!opts.spec && !opts.baseUrl) {
118
+ const m = "Provide --spec <path|url> for a full registration, or --base-url <url> for run-only mode.";
119
+ if (json) printJson(jsonError("add-api", [m])); else printError(m);
120
+ process.exitCode = 2;
121
+ return;
122
+ }
123
+ process.exitCode = await addApiCommand({
124
+ name,
125
+ spec: opts.spec,
126
+ baseUrl: opts.baseUrl,
127
+ dir: opts.dir,
128
+ force: opts.force === true,
129
+ insecure: opts.insecure === true,
130
+ dbPath: typeof opts.db === "string" ? opts.db : undefined,
131
+ json,
132
+ });
133
+ });
134
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * ARV-187 / idempotency: 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 IdempotencySchema = z.object({
10
+ header: z.string().default("Idempotency-Key"),
11
+ scope: z.enum(["endpoint", "global"]).optional(),
12
+ ignore_response_fields: z.array(z.string()).optional(),
13
+ });
14
+
15
+ const ResponseSchema = z.object({
16
+ resource: z.string(),
17
+ idempotency: IdempotencySchema.nullable(),
18
+ rationale: z.string().optional(),
19
+ confidence: z.enum(["low", "medium", "high"]).optional(),
20
+ });
21
+
22
+ export const EXPECTED_OUTPUT_SHAPE = {
23
+ resource: "string (echo input)",
24
+ idempotency: {
25
+ header: "string (header name, e.g. 'Idempotency-Key')",
26
+ scope: "endpoint | global (optional)",
27
+ ignore_response_fields: "string[] (optional — fields that change between replays, e.g. 'created')",
28
+ },
29
+ rationale: "string (optional)",
30
+ confidence: "low | medium | high",
31
+ null_form: "if create endpoint doesn't support idempotency-replay, return { resource, idempotency: null }",
32
+ };
33
+
34
+ export function parseIdempotencyResponse(parsed: unknown, slice: ResourceSlice): { patch: ResourcePatch; audit: Record<string, unknown> } {
35
+ const validated = ResponseSchema.safeParse(parsed);
36
+ if (!validated.success) {
37
+ throw new Error(`idempotency response failed schema for ${slice.resource}: ${validated.error.message}`);
38
+ }
39
+ const v = validated.data;
40
+ if (v.idempotency == null) {
41
+ return {
42
+ patch: { resource: slice.resource },
43
+ audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence, dropped: "no Idempotency-Key support" },
44
+ };
45
+ }
46
+ return {
47
+ patch: {
48
+ resource: slice.resource,
49
+ idempotency: {
50
+ header: v.idempotency.header,
51
+ scope: v.idempotency.scope,
52
+ ignore_response_fields: v.idempotency.ignore_response_fields,
53
+ },
54
+ },
55
+ audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence },
56
+ };
57
+ }
58
+
59
+ export function isApplicable(slice: ResourceSlice): boolean { return Boolean(slice.endpoints.create); }