@kirrosh/zond 0.22.0 → 0.26.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 (282) hide show
  1. package/CHANGELOG.md +811 -0
  2. package/README.md +59 -6
  3. package/package.json +9 -7
  4. package/src/CLAUDE.md +112 -0
  5. package/src/cli/argv.ts +122 -0
  6. package/src/cli/commands/add-api.ts +146 -0
  7. package/src/cli/commands/api/annotate/idempotency.ts +59 -0
  8. package/src/cli/commands/api/annotate/index.ts +880 -0
  9. package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
  10. package/src/cli/commands/api/annotate/overlay.ts +206 -0
  11. package/src/cli/commands/api/annotate/pagination.ts +64 -0
  12. package/src/cli/commands/api/annotate/prompts.ts +220 -0
  13. package/src/cli/commands/api/annotate/readback.ts +58 -0
  14. package/src/cli/commands/api/annotate/resources.ts +91 -0
  15. package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
  16. package/src/cli/commands/audit.ts +786 -0
  17. package/src/cli/commands/catalog.ts +35 -0
  18. package/src/cli/commands/check.ts +361 -0
  19. package/src/cli/commands/checks.ts +1072 -0
  20. package/src/cli/commands/ci-init.ts +43 -0
  21. package/src/cli/commands/clean.ts +212 -0
  22. package/src/cli/commands/cleanup.ts +236 -0
  23. package/src/cli/commands/completions.ts +16 -0
  24. package/src/cli/commands/coverage.ts +823 -132
  25. package/src/cli/commands/db.ts +486 -12
  26. package/src/cli/commands/describe.ts +37 -2
  27. package/src/cli/commands/discover.ts +1356 -0
  28. package/src/cli/commands/doctor.ts +661 -0
  29. package/src/cli/commands/fixtures.ts +402 -0
  30. package/src/cli/commands/generate.ts +438 -47
  31. package/src/cli/commands/init/bootstrap.ts +34 -2
  32. package/src/cli/commands/{init.ts → init/index.ts} +99 -5
  33. package/src/cli/commands/init/skills.ts +99 -3
  34. package/src/cli/commands/init/templates/agents.md +77 -64
  35. package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
  36. package/src/cli/commands/init/templates/skills/zond-checks.md +621 -0
  37. package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
  38. package/src/cli/commands/init/templates/skills/zond-triage.md +272 -0
  39. package/src/cli/commands/init/templates/skills/zond.md +802 -125
  40. package/src/cli/commands/init/templates/zond-config.yml +8 -9
  41. package/src/cli/commands/prepare-fixtures.ts +97 -0
  42. package/src/cli/commands/probe/_seed-bodies.ts +52 -0
  43. package/src/cli/commands/probe/mass-assignment.ts +594 -0
  44. package/src/cli/commands/probe/security.ts +537 -0
  45. package/src/cli/commands/probe/static.ts +255 -0
  46. package/src/cli/commands/probe/webhooks.ts +163 -0
  47. package/src/cli/commands/probe.ts +535 -0
  48. package/src/cli/commands/reference.ts +87 -0
  49. package/src/cli/commands/refresh-api.ts +227 -0
  50. package/src/cli/commands/remove-api.ts +150 -0
  51. package/src/cli/commands/report-bundle.ts +310 -0
  52. package/src/cli/commands/report.ts +241 -0
  53. package/src/cli/commands/request.ts +495 -4
  54. package/src/cli/commands/run.ts +870 -53
  55. package/src/cli/commands/schema-from-runs.ts +128 -0
  56. package/src/cli/commands/secrets.ts +133 -0
  57. package/src/cli/commands/session.ts +244 -0
  58. package/src/cli/commands/use.ts +18 -1
  59. package/src/cli/index.ts +20 -3
  60. package/src/cli/json-envelope.ts +92 -3
  61. package/src/cli/json-schemas.ts +314 -0
  62. package/src/cli/output.ts +17 -1
  63. package/src/cli/program.ts +199 -635
  64. package/src/cli/resolve.ts +105 -0
  65. package/src/cli/safe-live.ts +24 -0
  66. package/src/cli/status-filter.ts +114 -0
  67. package/src/cli/util/api-context.ts +85 -0
  68. package/src/cli/version.ts +5 -0
  69. package/src/core/audit/persist.ts +183 -0
  70. package/src/core/checks/budget.ts +59 -0
  71. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  72. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  73. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  74. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  75. package/src/core/checks/checks/cross_call_references.ts +147 -0
  76. package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
  77. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  78. package/src/core/checks/checks/idempotency_replay.ts +242 -0
  79. package/src/core/checks/checks/ignored_auth.ts +254 -0
  80. package/src/core/checks/checks/index.ts +68 -0
  81. package/src/core/checks/checks/lifecycle_transitions.ts +416 -0
  82. package/src/core/checks/checks/missing_required_header.ts +40 -0
  83. package/src/core/checks/checks/negative_data_rejection.ts +148 -0
  84. package/src/core/checks/checks/not_a_server_error.ts +35 -0
  85. package/src/core/checks/checks/open_cors_on_sensitive.ts +160 -0
  86. package/src/core/checks/checks/pagination_invariants.ts +419 -0
  87. package/src/core/checks/checks/positive_data_acceptance.ts +33 -0
  88. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  89. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  90. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  91. package/src/core/checks/checks/status_code_conformance.ts +132 -0
  92. package/src/core/checks/checks/unsupported_method.ts +63 -0
  93. package/src/core/checks/checks/use_after_free.ts +78 -0
  94. package/src/core/checks/index.ts +30 -0
  95. package/src/core/checks/mode.ts +82 -0
  96. package/src/core/checks/recommended-action.ts +68 -0
  97. package/src/core/checks/registry.ts +78 -0
  98. package/src/core/checks/runner.ts +1461 -0
  99. package/src/core/checks/sarif.ts +230 -0
  100. package/src/core/checks/spec-findings.ts +308 -0
  101. package/src/core/checks/stateful.ts +121 -0
  102. package/src/core/checks/types.ts +305 -0
  103. package/src/core/checks/zond-extensions.ts +73 -0
  104. package/src/core/classifier/recommended-action.ts +251 -0
  105. package/src/core/context/current.ts +22 -6
  106. package/src/core/context/session.ts +78 -0
  107. package/src/core/coverage/loader.ts +216 -0
  108. package/src/core/coverage/reasons.ts +300 -0
  109. package/src/core/diagnostics/db-analysis.ts +293 -59
  110. package/src/core/diagnostics/failure-class.ts +140 -0
  111. package/src/core/diagnostics/failure-hints.ts +88 -89
  112. package/src/core/diagnostics/spec-pointer.ts +99 -0
  113. package/src/core/diagnostics/suggested-fixes.ts +155 -0
  114. package/src/core/exporter/case-study/index.ts +270 -0
  115. package/src/core/exporter/curl.ts +40 -0
  116. package/src/core/exporter/exporter.ts +48 -0
  117. package/src/core/exporter/html-report/escape.ts +24 -0
  118. package/src/core/exporter/html-report/index.ts +479 -0
  119. package/src/core/exporter/html-report/script.ts +100 -0
  120. package/src/core/exporter/html-report/styles.ts +408 -0
  121. package/src/core/generator/chunker.ts +38 -19
  122. package/src/core/generator/coverage-phase.ts +0 -0
  123. package/src/core/generator/data-factory.ts +586 -22
  124. package/src/core/generator/describe.ts +1 -1
  125. package/src/core/generator/fixtures-builder.ts +332 -0
  126. package/src/core/generator/index.ts +5 -5
  127. package/src/core/generator/openapi-reader.ts +135 -7
  128. package/src/core/generator/path-param-disambig.ts +140 -0
  129. package/src/core/generator/resources-builder.ts +898 -0
  130. package/src/core/generator/schema-utils.ts +33 -3
  131. package/src/core/generator/serializer.ts +103 -13
  132. package/src/core/generator/suite-generator.ts +583 -122
  133. package/src/core/generator/types.ts +14 -0
  134. package/src/core/identity/identity-file.ts +0 -0
  135. package/src/core/lint/affects.ts +28 -0
  136. package/src/core/lint/config.ts +96 -0
  137. package/src/core/lint/format.ts +42 -0
  138. package/src/core/lint/index.ts +94 -0
  139. package/src/core/lint/reporter.ts +128 -0
  140. package/src/core/lint/rules/consistency.ts +158 -0
  141. package/src/core/lint/rules/heuristics.ts +97 -0
  142. package/src/core/lint/rules/strictness.ts +109 -0
  143. package/src/core/lint/types.ts +96 -0
  144. package/src/core/lint/walker.ts +248 -0
  145. package/src/core/meta/meta-store.ts +6 -73
  146. package/src/core/output/README.md +73 -0
  147. package/src/core/output/index.ts +13 -0
  148. package/src/core/output/run.ts +91 -0
  149. package/src/core/output/types.ts +122 -0
  150. package/src/core/parser/dynamic-values.ts +160 -0
  151. package/src/core/parser/env-interpolation.ts +104 -0
  152. package/src/core/parser/filter.ts +57 -0
  153. package/src/core/parser/schema.ts +129 -4
  154. package/src/core/parser/types.ts +19 -1
  155. package/src/core/parser/variables.ts +0 -0
  156. package/src/core/parser/yaml-parser.ts +58 -12
  157. package/src/core/probe/bootstrap.ts +34 -0
  158. package/src/core/probe/dry-run-envelope.ts +61 -0
  159. package/src/core/probe/mass-assignment/classify.ts +175 -0
  160. package/src/core/probe/mass-assignment/cleanup.ts +52 -0
  161. package/src/core/probe/mass-assignment/digest.ts +114 -0
  162. package/src/core/probe/mass-assignment/orchestrator.ts +459 -0
  163. package/src/core/probe/mass-assignment/regression.ts +141 -0
  164. package/src/core/probe/mass-assignment/suspects.ts +92 -0
  165. package/src/core/probe/mass-assignment/types.ts +135 -0
  166. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  167. package/src/core/probe/mass-assignment-probe.ts +27 -0
  168. package/src/core/probe/mass-assignment-template.ts +240 -0
  169. package/src/core/probe/method-probe.ts +43 -76
  170. package/src/core/probe/method-shared.ts +69 -0
  171. package/src/core/probe/negative-probe.ts +183 -149
  172. package/src/core/probe/orphan-tracker.ts +188 -0
  173. package/src/core/probe/path-discovery.ts +439 -0
  174. package/src/core/probe/probe-harness.ts +119 -0
  175. package/src/core/probe/registry.ts +89 -0
  176. package/src/core/probe/runner.ts +136 -0
  177. package/src/core/probe/security/baseline.ts +174 -0
  178. package/src/core/probe/security/classify.ts +341 -0
  179. package/src/core/probe/security/cleanup.ts +125 -0
  180. package/src/core/probe/security/detectors.ts +71 -0
  181. package/src/core/probe/security/digest.ts +104 -0
  182. package/src/core/probe/security/orchestrator.ts +398 -0
  183. package/src/core/probe/security/regression.ts +103 -0
  184. package/src/core/probe/security/types.ts +151 -0
  185. package/src/core/probe/security-probe-class.ts +207 -0
  186. package/src/core/probe/security-probe.ts +32 -0
  187. package/src/core/probe/shared.ts +531 -0
  188. package/src/core/probe/static-probe-class.ts +125 -0
  189. package/src/core/probe/types.ts +165 -0
  190. package/src/core/probe/verdict-aggregator.ts +33 -0
  191. package/src/core/probe/webhooks-probe.ts +282 -0
  192. package/src/core/reporter/console.ts +41 -2
  193. package/src/core/reporter/index.ts +2 -3
  194. package/src/core/reporter/json.ts +11 -1
  195. package/src/core/reporter/junit.ts +27 -12
  196. package/src/core/reporter/ndjson.ts +37 -0
  197. package/src/core/reporter/types.ts +3 -0
  198. package/src/core/runner/assertions.ts +59 -2
  199. package/src/core/runner/async-pool.ts +108 -0
  200. package/src/core/runner/auth-path.ts +8 -0
  201. package/src/core/runner/ci-context.ts +72 -0
  202. package/src/core/runner/executor.ts +265 -36
  203. package/src/core/runner/form-encode.ts +41 -0
  204. package/src/core/runner/http-client.ts +112 -2
  205. package/src/core/runner/learn-drift.ts +293 -0
  206. package/src/core/runner/preflight-vars.ts +153 -0
  207. package/src/core/runner/progress-tracker.ts +73 -0
  208. package/src/core/runner/rate-limiter.ts +87 -33
  209. package/src/core/runner/run-kind.ts +45 -0
  210. package/src/core/runner/schema-validator.ts +308 -0
  211. package/src/core/runner/send-request.ts +158 -20
  212. package/src/core/runner/types.ts +44 -0
  213. package/src/core/secrets/registry.ts +164 -0
  214. package/src/core/secrets/secrets-file.ts +115 -0
  215. package/src/core/selectors/operation-filter.ts +144 -0
  216. package/src/core/setup-api.ts +457 -20
  217. package/src/core/severity/category.ts +94 -0
  218. package/src/core/severity/index.ts +58 -0
  219. package/src/core/spec/infer-schema.ts +102 -0
  220. package/src/core/spec/layers.ts +154 -0
  221. package/src/core/spec/merge-specs.ts +156 -0
  222. package/src/core/spec/schema-from-runs.ts +117 -0
  223. package/src/core/spec/schema-overlay.ts +130 -0
  224. package/src/core/util/ajv.ts +13 -0
  225. package/src/core/util/format-eta.ts +21 -0
  226. package/src/core/util/headers.ts +9 -0
  227. package/src/core/util/url.ts +24 -0
  228. package/src/core/utils.ts +5 -1
  229. package/src/core/workspace/config.ts +129 -0
  230. package/src/core/workspace/fixture-gap-report.ts +84 -0
  231. package/src/core/workspace/fixture-gaps.ts +71 -0
  232. package/src/core/workspace/manifest.ts +283 -0
  233. package/src/core/workspace/output-rotation.ts +62 -0
  234. package/src/core/workspace/root.ts +13 -11
  235. package/src/core/workspace/triage-path.ts +87 -0
  236. package/src/db/lint-runs.ts +47 -0
  237. package/src/db/migrate.ts +128 -0
  238. package/src/db/migrations/0001_run_kind.sql +25 -0
  239. package/src/db/migrations/0002_run_kind_request.sql +59 -0
  240. package/src/db/migrations/sql.d.ts +4 -0
  241. package/src/db/queries/collections.ts +133 -0
  242. package/src/db/queries/coverage.ts +9 -0
  243. package/src/db/queries/dashboard.ts +59 -0
  244. package/src/db/queries/results.ts +216 -0
  245. package/src/db/queries/runs.ts +289 -0
  246. package/src/db/queries/sessions.ts +42 -0
  247. package/src/db/queries/settings.ts +28 -0
  248. package/src/db/queries/types.ts +172 -0
  249. package/src/db/queries.ts +75 -802
  250. package/src/db/schema.ts +178 -50
  251. package/src/cli/commands/export.ts +0 -144
  252. package/src/cli/commands/guide.ts +0 -127
  253. package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
  254. package/src/cli/commands/probe-methods.ts +0 -108
  255. package/src/cli/commands/probe-validation.ts +0 -124
  256. package/src/cli/commands/serve.ts +0 -114
  257. package/src/cli/commands/sync.ts +0 -268
  258. package/src/cli/commands/update.ts +0 -189
  259. package/src/cli/commands/validate.ts +0 -34
  260. package/src/core/diagnostics/render-md.ts +0 -112
  261. package/src/core/exporter/postman.ts +0 -963
  262. package/src/core/generator/guide-builder.ts +0 -253
  263. package/src/core/meta/types.ts +0 -19
  264. package/src/core/parser/index.ts +0 -21
  265. package/src/core/runner/execute-run.ts +0 -132
  266. package/src/core/runner/index.ts +0 -12
  267. package/src/core/sync/spec-differ.ts +0 -38
  268. package/src/web/data/collection-state.ts +0 -362
  269. package/src/web/routes/api.ts +0 -314
  270. package/src/web/routes/dashboard.ts +0 -350
  271. package/src/web/routes/runs.ts +0 -64
  272. package/src/web/schemas.ts +0 -121
  273. package/src/web/server.ts +0 -134
  274. package/src/web/static/htmx.min.cjs +0 -1
  275. package/src/web/static/style.css +0 -1148
  276. package/src/web/views/endpoints-tab.ts +0 -174
  277. package/src/web/views/explorer-tab.ts +0 -402
  278. package/src/web/views/health-strip.ts +0 -92
  279. package/src/web/views/layout.ts +0 -48
  280. package/src/web/views/results.ts +0 -210
  281. package/src/web/views/runs-tab.ts +0 -126
  282. package/src/web/views/suites-tab.ts +0 -181
package/src/db/schema.ts CHANGED
@@ -1,15 +1,24 @@
1
1
  import { Database } from "bun:sqlite";
2
- import { resolve } from "path";
3
- import { existsSync } from "fs";
2
+ import { dirname, resolve } from "path";
3
+ import { existsSync, mkdirSync } from "fs";
4
4
  import { findWorkspaceRoot } from "../core/workspace/root.ts";
5
+ import { applyMigrations } from "./migrate.ts";
5
6
 
6
7
  let _db: Database | null = null;
7
8
  let _dbPath: string | null = null;
8
9
 
10
+ /**
11
+ * Default DB path lives under `<workspace>/.zond/zond.db` to keep runtime
12
+ * artifacts out of the project root. (The flat `<workspace>/zond.db` layout
13
+ * was dropped in m-25 — pass an explicit `--db` to target a legacy file.)
14
+ */
15
+ function defaultDbPath(): string {
16
+ const root = findWorkspaceRoot().root;
17
+ return resolve(root, ".zond", "zond.db");
18
+ }
19
+
9
20
  export function getDb(dbPath?: string): Database {
10
- const path = dbPath
11
- ? resolve(dbPath)
12
- : (_dbPath ?? resolve(findWorkspaceRoot().root, "zond.db"));
21
+ const path = dbPath ? resolve(dbPath) : (_dbPath ?? defaultDbPath());
13
22
 
14
23
  // If cached connection exists, verify the file still exists
15
24
  if (_db && _dbPath === path && existsSync(path)) return _db;
@@ -20,19 +29,68 @@ export function getDb(dbPath?: string): Database {
20
29
  _db = null;
21
30
  _dbPath = null;
22
31
  }
32
+ // SQLite won't auto-create parent dirs; ensure `.zond/` (or any custom
33
+ // path's parent) exists before opening the file.
34
+ const parent = dirname(path);
35
+ if (!existsSync(parent)) mkdirSync(parent, { recursive: true });
36
+
23
37
  const db = new Database(path, { create: true });
24
38
 
25
39
  // Performance and integrity settings
26
40
  db.exec("PRAGMA journal_mode = WAL");
27
41
  db.exec("PRAGMA foreign_keys = ON");
42
+ // ARV-163: concurrent zond processes (e.g. `probe security` in one terminal
43
+ // + `checks run` in another) collide on the write-lock and surface as
44
+ // "database is locked". Letting SQLite spin at the C-level for up to 5s
45
+ // resolves the overwhelming majority without any per-call retry logic.
46
+ // `synchronous=NORMAL` is safe under WAL and cuts fsync overhead in the
47
+ // bulk-insert path (saveResults). withDbRetry() in this file remains the
48
+ // belt-and-suspenders for the rare long-running transactions.
49
+ db.exec("PRAGMA busy_timeout = 5000");
50
+ db.exec("PRAGMA synchronous = NORMAL");
28
51
 
29
52
  runMigrations(db);
30
53
 
54
+ // ARV-127: file-based migrations sit on top of the legacy PRAGMA
55
+ // path. On an existing DB the runner pre-seeds `schema_migrations`
56
+ // so the v9→v10 inline migration (now mirrored in
57
+ // src/db/migrations/0001_run_kind.sql) is treated as applied.
58
+ applyMigrations(db);
59
+
31
60
  _db = db;
32
61
  _dbPath = path;
33
62
  return db;
34
63
  }
35
64
 
65
+ /**
66
+ * ARV-163: retry wrapper for SQLite write paths that may collide with
67
+ * concurrent zond processes. `PRAGMA busy_timeout` already absorbs short
68
+ * contention at the C level — this wrapper only catches the residual cases
69
+ * where the lock holder runs longer than the timeout (e.g. a big
70
+ * `saveResults` bulk insert during a parallel `probe security` cleanup).
71
+ *
72
+ * Detects bun:sqlite's "database is locked" / "SQLITE_BUSY" message shapes;
73
+ * other errors propagate immediately. Backoff: 100, 200, 400, 800ms capped at
74
+ * 4 attempts (≈1.5s total) so we never silently stall a CLI command.
75
+ */
76
+ export function withDbRetry<T>(label: string, fn: () => T): T {
77
+ const delaysMs = [100, 200, 400, 800];
78
+ let lastError: unknown;
79
+ for (let attempt = 0; attempt <= delaysMs.length; attempt++) {
80
+ try {
81
+ return fn();
82
+ } catch (err) {
83
+ const msg = err instanceof Error ? err.message : String(err);
84
+ if (!/database is locked|SQLITE_BUSY/i.test(msg)) throw err;
85
+ lastError = err;
86
+ if (attempt === delaysMs.length) break;
87
+ Bun.sleepSync(delaysMs[attempt]!);
88
+ }
89
+ }
90
+ const msg = lastError instanceof Error ? lastError.message : String(lastError);
91
+ throw new Error(`SQLite still locked after ${delaysMs.length + 1} attempts (${label}): ${msg}`);
92
+ }
93
+
36
94
  export function closeDb(): void {
37
95
  if (_db) {
38
96
  try { _db.close(); } catch {}
@@ -41,7 +99,7 @@ export function closeDb(): void {
41
99
  }
42
100
  }
43
101
 
44
- export function resetDb(): void {
102
+ function resetDb(): void {
45
103
  if (_db) { try { _db.close(); } catch {} }
46
104
  _db = null;
47
105
  _dbPath = null;
@@ -51,7 +109,7 @@ export function resetDb(): void {
51
109
  // Schema
52
110
  // ──────────────────────────────────────────────
53
111
 
54
- const SCHEMA_VERSION = 2;
112
+ const SCHEMA_VERSION = 10;
55
113
 
56
114
  const SCHEMA = `
57
115
  CREATE TABLE IF NOT EXISTS runs (
@@ -67,7 +125,16 @@ const SCHEMA = `
67
125
  branch TEXT,
68
126
  environment TEXT,
69
127
  duration_ms INTEGER,
70
- collection_id INTEGER REFERENCES collections(id)
128
+ collection_id INTEGER REFERENCES collections(id),
129
+ session_id TEXT,
130
+ tags TEXT,
131
+ -- ARV-55: classify a run once at INSERT time so coverage / diagnose
132
+ -- queries don't have to re-derive "is this a probe-only run?" from
133
+ -- the results' suite_file paths. ARV-265 extends the CHECK list with
134
+ -- 'request' (zond request in-session) and 'fixture' (prepare-fixtures
135
+ -- cascade list-calls) so audit-coverage can attribute HTTP touches
136
+ -- back to the producer.
137
+ run_kind TEXT NOT NULL DEFAULT 'regular' CHECK (run_kind IN ('regular','probe','check','request','fixture'))
71
138
  );
72
139
 
73
140
  CREATE TABLE IF NOT EXISTS results (
@@ -86,7 +153,12 @@ const SCHEMA = `
86
153
  assertions TEXT,
87
154
  captures TEXT,
88
155
  response_headers TEXT,
89
- suite_file TEXT
156
+ suite_file TEXT,
157
+ provenance TEXT,
158
+ failure_class TEXT,
159
+ failure_class_reason TEXT,
160
+ spec_pointer TEXT,
161
+ spec_excerpt TEXT
90
162
  );
91
163
 
92
164
  CREATE TABLE IF NOT EXISTS collections (
@@ -98,44 +170,6 @@ const SCHEMA = `
98
170
  base_dir TEXT
99
171
  );
100
172
 
101
- CREATE TABLE IF NOT EXISTS ai_generations (
102
- id INTEGER PRIMARY KEY AUTOINCREMENT,
103
- collection_id INTEGER REFERENCES collections(id),
104
- prompt TEXT NOT NULL,
105
- model TEXT NOT NULL,
106
- provider TEXT NOT NULL,
107
- generated_yaml TEXT,
108
- output_path TEXT,
109
- status TEXT NOT NULL DEFAULT 'pending',
110
- error_message TEXT,
111
- prompt_tokens INTEGER,
112
- completion_tokens INTEGER,
113
- duration_ms INTEGER,
114
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
115
- );
116
-
117
- CREATE TABLE IF NOT EXISTS chat_sessions (
118
- id INTEGER PRIMARY KEY AUTOINCREMENT,
119
- title TEXT,
120
- provider TEXT NOT NULL,
121
- model TEXT NOT NULL,
122
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
123
- last_active TEXT NOT NULL DEFAULT (datetime('now'))
124
- );
125
-
126
- CREATE TABLE IF NOT EXISTS chat_messages (
127
- id INTEGER PRIMARY KEY AUTOINCREMENT,
128
- session_id INTEGER NOT NULL REFERENCES chat_sessions(id),
129
- role TEXT NOT NULL,
130
- content TEXT NOT NULL,
131
- tool_name TEXT,
132
- tool_args TEXT,
133
- tool_result TEXT,
134
- input_tokens INTEGER,
135
- output_tokens INTEGER,
136
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
137
- );
138
-
139
173
  CREATE TABLE IF NOT EXISTS settings (
140
174
  key TEXT PRIMARY KEY,
141
175
  value TEXT NOT NULL
@@ -143,13 +177,27 @@ const SCHEMA = `
143
177
 
144
178
  CREATE INDEX IF NOT EXISTS idx_runs_started ON runs(started_at DESC);
145
179
  CREATE INDEX IF NOT EXISTS idx_runs_collection ON runs(collection_id);
180
+ CREATE INDEX IF NOT EXISTS idx_runs_session ON runs(session_id, started_at DESC);
146
181
  CREATE INDEX IF NOT EXISTS idx_results_run ON results(run_id);
147
182
  CREATE INDEX IF NOT EXISTS idx_results_status ON results(status);
148
183
  CREATE INDEX IF NOT EXISTS idx_results_name ON results(suite_name, test_name);
149
184
  CREATE INDEX IF NOT EXISTS idx_collections_name ON collections(name);
150
- CREATE INDEX IF NOT EXISTS idx_ai_gen_collection ON ai_generations(collection_id);
151
- CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id);
152
- CREATE INDEX IF NOT EXISTS idx_chat_sessions_active ON chat_sessions(last_active DESC);
185
+
186
+ CREATE TABLE IF NOT EXISTS lint_runs (
187
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
188
+ spec_path TEXT NOT NULL,
189
+ started_at TEXT NOT NULL,
190
+ finished_at TEXT,
191
+ total INTEGER NOT NULL DEFAULT 0,
192
+ high_count INTEGER NOT NULL DEFAULT 0,
193
+ medium_count INTEGER NOT NULL DEFAULT 0,
194
+ low_count INTEGER NOT NULL DEFAULT 0,
195
+ endpoint_count INTEGER NOT NULL DEFAULT 0,
196
+ config_json TEXT,
197
+ issues_json TEXT
198
+ );
199
+
200
+ CREATE INDEX IF NOT EXISTS idx_lint_runs_spec ON lint_runs(spec_path, started_at DESC);
153
201
  `;
154
202
 
155
203
  function runMigrations(db: Database): void {
@@ -165,6 +213,86 @@ function runMigrations(db: Database): void {
165
213
  // Migration v1→v2: add suite_file column to results
166
214
  db.exec("ALTER TABLE results ADD COLUMN suite_file TEXT");
167
215
  }
216
+ if (ver >= 2 && ver < 3) {
217
+ // Migration v2→v3: add provenance column (test source metadata)
218
+ db.exec("ALTER TABLE results ADD COLUMN provenance TEXT");
219
+ }
220
+ if (ver >= 3 && ver < 4) {
221
+ // Migration v3→v4: add failure classification columns
222
+ db.exec("ALTER TABLE results ADD COLUMN failure_class TEXT");
223
+ db.exec("ALTER TABLE results ADD COLUMN failure_class_reason TEXT");
224
+ }
225
+ if (ver >= 4 && ver < 5) {
226
+ // Migration v4→v5: add spec_pointer + spec_excerpt (frozen OpenAPI evidence)
227
+ db.exec("ALTER TABLE results ADD COLUMN spec_pointer TEXT");
228
+ db.exec("ALTER TABLE results ADD COLUMN spec_excerpt TEXT");
229
+ }
230
+ if (ver >= 5 && ver < 6) {
231
+ // Migration v5→v6: add lint_runs table for `zond lint-spec` history.
232
+ db.exec(`
233
+ CREATE TABLE IF NOT EXISTS lint_runs (
234
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
235
+ spec_path TEXT NOT NULL,
236
+ started_at TEXT NOT NULL,
237
+ finished_at TEXT,
238
+ total INTEGER NOT NULL DEFAULT 0,
239
+ high_count INTEGER NOT NULL DEFAULT 0,
240
+ medium_count INTEGER NOT NULL DEFAULT 0,
241
+ low_count INTEGER NOT NULL DEFAULT 0,
242
+ endpoint_count INTEGER NOT NULL DEFAULT 0,
243
+ config_json TEXT,
244
+ issues_json TEXT
245
+ );
246
+ CREATE INDEX IF NOT EXISTS idx_lint_runs_spec ON lint_runs(spec_path, started_at DESC);
247
+ `);
248
+ }
249
+ if (ver >= 6 && ver < 7) {
250
+ // Migration v6→v7: add session_id column to runs for grouping CLI invocations
251
+ // (e.g. `zond hunt`, scripted post-init runs) into one campaign.
252
+ db.exec("ALTER TABLE runs ADD COLUMN session_id TEXT");
253
+ db.exec("CREATE INDEX IF NOT EXISTS idx_runs_session ON runs(session_id, started_at DESC)");
254
+ }
255
+ if (ver >= 7 && ver < 8) {
256
+ // Migration v7→v8: drop the unused AI/chat tables. They were a legacy
257
+ // experiment (in-app chat-driven YAML generation) that never shipped a
258
+ // user-facing surface and have no consumers in the codebase.
259
+ db.exec("DROP TABLE IF EXISTS chat_messages");
260
+ db.exec("DROP TABLE IF EXISTS chat_sessions");
261
+ db.exec("DROP TABLE IF EXISTS ai_generations");
262
+ }
263
+ if (ver >= 8 && ver < 9) {
264
+ // Migration v8→v9: tags column on runs (JSON array of strings — union
265
+ // of suite-level tags actually executed in the run, plus any explicit
266
+ // --tag filters). Powers `coverage --union tag:<name>` (TASK-274).
267
+ db.exec("ALTER TABLE runs ADD COLUMN tags TEXT");
268
+ }
269
+ if (ver >= 9 && ver < 10) {
270
+ // Migration v9→v10 (ARV-55): classify each historical run by suite
271
+ // kind so coverage's default query becomes a column compare. The
272
+ // CHECK constraint can't be added retroactively without a table
273
+ // rebuild — accept the looser column for legacy rows; new INSERTs
274
+ // go through `createRun()` which only emits known kinds.
275
+ db.exec("ALTER TABLE runs ADD COLUMN run_kind TEXT NOT NULL DEFAULT 'regular'");
276
+ // Backfill: derive kind per existing run from its stored results.
277
+ // `every` semantics mirror the runtime `detectRunKind` helper —
278
+ // pure-probe / pure-check vs anything else.
279
+ db.exec(`
280
+ UPDATE runs SET run_kind = 'probe'
281
+ WHERE id IN (
282
+ SELECT r.id FROM runs r
283
+ WHERE EXISTS (SELECT 1 FROM results WHERE run_id = r.id AND suite_file IS NOT NULL AND suite_file LIKE '%probes/%')
284
+ AND NOT EXISTS (SELECT 1 FROM results WHERE run_id = r.id AND suite_file IS NOT NULL AND suite_file NOT LIKE '%probes/%')
285
+ )
286
+ `);
287
+ db.exec(`
288
+ UPDATE runs SET run_kind = 'check'
289
+ WHERE id IN (
290
+ SELECT r.id FROM runs r
291
+ WHERE EXISTS (SELECT 1 FROM results WHERE run_id = r.id AND suite_file IS NOT NULL AND suite_file LIKE '%checks/%')
292
+ AND NOT EXISTS (SELECT 1 FROM results WHERE run_id = r.id AND suite_file IS NOT NULL AND suite_file NOT LIKE '%checks/%')
293
+ )
294
+ `);
295
+ }
168
296
  db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
169
297
  })();
170
298
  }
@@ -1,144 +0,0 @@
1
- import { dirname, basename, join } from "path";
2
- import { parse } from "../../core/parser/yaml-parser.ts";
3
- import {
4
- buildCollection,
5
- buildEnvironment,
6
- deriveCollectionName,
7
- } from "../../core/exporter/postman.ts";
8
- import { printError, printSuccess, printWarning } from "../output.ts";
9
- import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
10
-
11
- export interface ExportOptions {
12
- testsPath: string;
13
- output: string;
14
- env?: string;
15
- collectionName?: string;
16
- json?: boolean;
17
- }
18
-
19
- export async function exportCommand(options: ExportOptions): Promise<number> {
20
- // 1. Parse test suites
21
- let suites;
22
- try {
23
- suites = await parse(options.testsPath);
24
- } catch (err) {
25
- const msg = `Failed to parse tests: ${(err as Error).message}`;
26
- if (options.json) {
27
- printJson(jsonError("export postman", [msg]));
28
- } else {
29
- printError(msg);
30
- }
31
- return 2;
32
- }
33
-
34
- if (suites.length === 0) {
35
- const msg = "No test suites found";
36
- if (options.json) {
37
- printJson(jsonError("export postman", [msg]));
38
- } else {
39
- printError(msg);
40
- }
41
- return 1;
42
- }
43
-
44
- // 2. Derive collection name
45
- const collectionName =
46
- options.collectionName ?? deriveCollectionName(options.testsPath);
47
-
48
- // 3. Build collection
49
- const { collection, warnings } = buildCollection(suites, collectionName);
50
-
51
- // Count total items across all folders
52
- const totalItems = collection.item.reduce((sum, folder) => sum + folder.item.length, 0);
53
-
54
- // 4. Write collection file
55
- try {
56
- await Bun.write(options.output, JSON.stringify(collection, null, 2));
57
- } catch (err) {
58
- const msg = `Failed to write collection file: ${(err as Error).message}`;
59
- if (options.json) {
60
- printJson(jsonError("export postman", [msg], warnings));
61
- } else {
62
- printError(msg);
63
- }
64
- return 2;
65
- }
66
-
67
- // 5. Optional env export
68
- let envOutput: string | undefined;
69
- if (options.env) {
70
- let envVars: Record<string, string>;
71
- try {
72
- const text = await Bun.file(options.env).text();
73
- const parsed = Bun.YAML.parse(text);
74
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
75
- throw new Error("Environment file must be a YAML object");
76
- }
77
- // Convert all values to strings
78
- envVars = {};
79
- for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
80
- envVars[k] = String(v);
81
- }
82
- } catch (err) {
83
- const msg = `Failed to read env file: ${(err as Error).message}`;
84
- if (options.json) {
85
- printJson(jsonError("export postman", [msg], warnings));
86
- } else {
87
- printError(msg);
88
- }
89
- return 2;
90
- }
91
-
92
- // Derive env name: e.g. ".env.staging.yaml" → "staging", ".env.yaml" → collectionName
93
- const envBasename = basename(options.env);
94
- const envNameMatch = envBasename.match(/^\.?env\.(.+?)\.ya?ml$/);
95
- const envName = envNameMatch ? envNameMatch[1]! : collectionName;
96
-
97
- const environment = buildEnvironment(envVars, envName);
98
-
99
- // Output path: same directory as output, same base name with .postman_environment.json
100
- const outBase = basename(options.output).replace(/\.postman\.json$/, "").replace(/\.json$/, "");
101
- const outDir = dirname(options.output);
102
- envOutput = join(outDir, `${outBase}.postman_environment.json`);
103
-
104
- try {
105
- await Bun.write(envOutput, JSON.stringify(environment, null, 2));
106
- } catch (err) {
107
- const msg = `Failed to write environment file: ${(err as Error).message}`;
108
- if (options.json) {
109
- printJson(jsonError("export postman", [msg], warnings));
110
- } else {
111
- printError(msg);
112
- }
113
- return 2;
114
- }
115
- }
116
-
117
- // 6. Output result
118
- if (options.json) {
119
- printJson(
120
- jsonOk(
121
- "export postman",
122
- {
123
- output: options.output,
124
- suites: suites.length,
125
- items: totalItems,
126
- ...(envOutput !== undefined ? { envOutput } : {}),
127
- },
128
- warnings
129
- )
130
- );
131
- } else {
132
- for (const w of warnings) {
133
- printWarning(w);
134
- }
135
- printSuccess(
136
- `Exported ${suites.length} suite(s) / ${totalItems} request(s) → ${options.output}`
137
- );
138
- if (envOutput) {
139
- printSuccess(`Environment exported → ${envOutput}`);
140
- }
141
- }
142
-
143
- return 0;
144
- }
@@ -1,127 +0,0 @@
1
- import {
2
- readOpenApiSpec,
3
- extractEndpoints,
4
- extractSecuritySchemes,
5
- scanCoveredEndpoints,
6
- filterUncoveredEndpoints,
7
- } from "../../core/generator/index.ts";
8
- import { compressEndpointsWithSchemas, buildGenerationGuide } from "../../core/generator/guide-builder.ts";
9
- import { planChunks, filterByTag } from "../../core/generator/chunker.ts";
10
- import { findCollectionBySpec } from "../../db/queries.ts";
11
- import { printError } from "../output.ts";
12
- import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
13
-
14
- export interface GuideOptions {
15
- specPath: string;
16
- testsDir?: string;
17
- tag?: string;
18
- json?: boolean;
19
- }
20
-
21
- export async function guideCommand(options: GuideOptions): Promise<number> {
22
- try {
23
- const doc = await readOpenApiSpec(options.specPath);
24
- let endpoints = extractEndpoints(doc);
25
- const securitySchemes = extractSecuritySchemes(doc);
26
- const baseUrl = ((doc as any).servers?.[0]?.url) as string | undefined;
27
- const title = (doc as any).info?.title as string | undefined;
28
-
29
- let outputDir = options.testsDir;
30
- if (!outputDir) {
31
- try {
32
- const collection = findCollectionBySpec(options.specPath);
33
- outputDir = collection?.test_path ?? "./tests/";
34
- } catch {
35
- outputDir = "./tests/";
36
- }
37
- }
38
-
39
- let coverageInfo: { covered: number; total: number; percentage: number } | undefined;
40
- if (options.testsDir) {
41
- const totalBefore = endpoints.length;
42
- const covered = await scanCoveredEndpoints(options.testsDir);
43
- const uncovered = filterUncoveredEndpoints(endpoints, covered);
44
- const coveredCount = totalBefore - uncovered.length;
45
- const percentage = totalBefore > 0 ? Math.round((coveredCount / totalBefore) * 100) : 100;
46
- coverageInfo = { covered: coveredCount, total: totalBefore, percentage };
47
- endpoints = uncovered;
48
- }
49
-
50
- if (endpoints.length === 0) {
51
- if (options.json) {
52
- printJson(jsonOk("guide", { fullyCovered: true, ...coverageInfo }));
53
- } else {
54
- console.log("All endpoints are covered.");
55
- }
56
- return 0;
57
- }
58
-
59
- if (options.tag) {
60
- endpoints = filterByTag(endpoints, options.tag);
61
- if (endpoints.length === 0) {
62
- const msg = `No endpoints found for tag "${options.tag}"`;
63
- if (options.json) printJson(jsonError("guide", [msg]));
64
- else printError(msg);
65
- return 1;
66
- }
67
- }
68
-
69
- const plan = planChunks(endpoints);
70
-
71
- if (plan.needsChunking && !options.tag) {
72
- if (options.json) {
73
- printJson(jsonOk("guide", {
74
- mode: "plan",
75
- title: title ?? "API",
76
- totalEndpoints: plan.totalEndpoints,
77
- chunks: plan.chunks,
78
- ...(coverageInfo ? { coverage: coverageInfo } : {}),
79
- }));
80
- } else {
81
- console.log(`API has ${plan.totalEndpoints} endpoints across ${plan.chunks.length} tags.`);
82
- console.log("Generate per-tag with --tag <name>:\n");
83
- for (const chunk of plan.chunks) {
84
- console.log(` --tag ${chunk.tag} (${chunk.count} endpoints)`);
85
- }
86
- }
87
- return 0;
88
- }
89
-
90
- const coverageHeader = coverageInfo
91
- ? `## Coverage: ${coverageInfo.covered}/${coverageInfo.total} endpoints covered (${coverageInfo.percentage}%). Generating tests for ${endpoints.length} uncovered endpoints:`
92
- : undefined;
93
-
94
- const apiContext = compressEndpointsWithSchemas(endpoints, securitySchemes);
95
- const guide = buildGenerationGuide({
96
- title: options.tag ? `${title ?? "API"} — tag: ${options.tag}` : (title ?? "API"),
97
- baseUrl,
98
- apiContext,
99
- outputDir,
100
- securitySchemes,
101
- endpointCount: endpoints.length,
102
- coverageHeader,
103
- includeFormat: true,
104
- });
105
-
106
- if (options.json) {
107
- printJson(jsonOk("guide", {
108
- title: title ?? "API",
109
- endpointCount: endpoints.length,
110
- outputDir,
111
- guide,
112
- ...(coverageInfo ? { coverage: coverageInfo } : {}),
113
- }));
114
- } else {
115
- console.log(guide);
116
- }
117
- return 0;
118
- } catch (err) {
119
- const message = err instanceof Error ? err.message : String(err);
120
- if (options.json) {
121
- printJson(jsonError("guide", [message]));
122
- } else {
123
- printError(message);
124
- }
125
- return 2;
126
- }
127
- }
@@ -1,97 +0,0 @@
1
- ---
2
- name: zond-scenarios
3
- description: |
4
- Author multi-step API scenario tests (user journeys) with zond. Use when asked to:
5
- write a scenario, model a user flow, replay a UI flow via API, chain requests with
6
- captures, set up test data via API, test a workflow end-to-end. Activates on:
7
- "user scenario", "API workflow", "multi-step test", "login then ...", "create then ...".
8
- allowed-tools: [Read, Write, Bash(zond *), Bash(bunx zond *)]
9
- ---
10
-
11
- # zond — API Scenario Tests
12
-
13
- CLI-only skill. Scenarios are **hand-written** YAML chaining multiple requests
14
- with captures (no `zond generate` for scenarios — `generate` only emits per-endpoint suites).
15
-
16
- ## Critical rules
17
- - **NEVER** run `zond generate` to produce scenarios — write them manually from `.api-catalog.yaml`.
18
- - **NEVER** read OpenAPI/Swagger specs directly — use `zond catalog` or `zond describe`.
19
- - **NEVER** invent endpoints — only use entries present in `.api-catalog.yaml`.
20
- - **Captures are file-scoped** — variables defined in one file do not leak into others
21
- unless the producing suite is marked `setup: true`.
22
- - Tag every scenario `[scenario, <flow-name>]`. Run by `--tag scenario` or `--tag <flow>,setup`.
23
- - Keep one user journey per file; chain steps within that file.
24
-
25
- ## Workflow
26
- ```bash
27
- # 1. Make sure the catalog is current
28
- zond catalog <spec> --output apis/<name>/tests
29
-
30
- # 2. Read the catalog to pick endpoints (NOT the raw spec)
31
- cat apis/<name>/tests/.api-catalog.yaml
32
-
33
- # 3. Author the scenario YAML (see structure below)
34
-
35
- # 4. Validate + run
36
- zond validate apis/<name>/tests/scenarios/<flow>.yaml
37
- zond run apis/<name>/tests/scenarios/<flow>.yaml --json
38
-
39
- # 5. Diagnose failures
40
- zond db diagnose <run-id> --json
41
- ```
42
-
43
- ## Scenario YAML — minimal structure
44
- ```yaml
45
- name: user_signup_to_first_purchase
46
- tags: [scenario, signup_purchase]
47
- steps:
48
- - name: register
49
- request:
50
- method: POST
51
- url: "{{base_url}}/auth/register"
52
- body: { email: "{{generate.email}}", password: "{{generate.password}}" }
53
- expect: { status: 201 }
54
- capture:
55
- user_id: "$.id"
56
- auth_token: "$.token"
57
-
58
- - name: create_cart
59
- request:
60
- method: POST
61
- url: "{{base_url}}/carts"
62
- headers: { Authorization: "Bearer {{auth_token}}" }
63
- expect: { status: 201 }
64
- capture: { cart_id: "$.id" }
65
-
66
- - name: checkout
67
- request:
68
- method: POST
69
- url: "{{base_url}}/carts/{{cart_id}}/checkout"
70
- headers: { Authorization: "Bearer {{auth_token}}" }
71
- expect: { status: 200 }
72
-
73
- - name: cleanup
74
- always: true # runs even if earlier steps failed
75
- request:
76
- method: DELETE
77
- url: "{{base_url}}/users/{{user_id}}"
78
- headers: { Authorization: "Bearer {{auth_token}}" }
79
- ```
80
-
81
- Key building blocks (full reference in `ZOND.md`):
82
- - `capture: { var: "$.json.path" }` — JSONPath extraction into scenario-local vars.
83
- - `expect.status` / `expect.json` / `expect.headers` — assertions.
84
- - `{{generate.email}}`, `{{generate.uuid}}`, `{{generate.int(1,100)}}` — value generators.
85
- - `always: true` on a step — guaranteed cleanup (runs on prior failure).
86
- - `setup: true` at the suite level — captures propagate to other suites in the run.
87
-
88
- ## Sharing auth across scenarios
89
- Put login in `apis/<name>/tests/setup.yaml` with `setup: true`; scenarios reference
90
- `{{auth_token}}` directly. Run with `--tag <flow>,setup`.
91
-
92
- ## When to hand off
93
- - Need broad endpoint coverage, bug hunting, or run diagnosis → `zond`.
94
- - This skill is **only** for hand-written multi-step flows / fixture creation.
95
-
96
- For full YAML structure (assertions, flow control, generators, conditional steps),
97
- see the YAML format section of `ZOND.md` at the repo root.