@kirrosh/zond 0.22.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 +648 -0
  2. package/README.md +58 -6
  3. package/package.json +9 -6
  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 +43 -0
  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 +16 -0
  24. package/src/cli/commands/coverage.ts +605 -132
  25. package/src/cli/commands/db.ts +178 -7
  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 -46
  31. package/src/cli/commands/init/bootstrap.ts +30 -1
  32. package/src/cli/commands/{init.ts → init/index.ts} +99 -5
  33. package/src/cli/commands/init/skills.ts +56 -3
  34. package/src/cli/commands/init/templates/agents.md +65 -61
  35. package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
  36. package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
  37. package/src/cli/commands/init/templates/skills/zond.md +592 -125
  38. package/src/cli/commands/init/templates/zond-config.yml +8 -9
  39. package/src/cli/commands/prepare-fixtures.ts +135 -0
  40. package/src/cli/commands/probe/mass-assignment.ts +503 -0
  41. package/src/cli/commands/probe/security.ts +454 -0
  42. package/src/cli/commands/probe/static.ts +255 -0
  43. package/src/cli/commands/probe/webhooks.ts +161 -0
  44. package/src/cli/commands/probe.ts +459 -0
  45. package/src/cli/commands/reference.ts +87 -0
  46. package/src/cli/commands/refresh-api.ts +169 -0
  47. package/src/cli/commands/remove-api.ts +150 -0
  48. package/src/cli/commands/report-bundle.ts +318 -0
  49. package/src/cli/commands/report.ts +241 -0
  50. package/src/cli/commands/request.ts +379 -4
  51. package/src/cli/commands/run.ts +842 -53
  52. package/src/cli/commands/session.ts +244 -0
  53. package/src/cli/commands/use.ts +18 -1
  54. package/src/cli/index.ts +20 -3
  55. package/src/cli/json-envelope.ts +112 -3
  56. package/src/cli/json-schemas.ts +263 -0
  57. package/src/cli/program.ts +198 -635
  58. package/src/cli/resolve.ts +105 -0
  59. package/src/cli/status-filter.ts +124 -0
  60. package/src/cli/util/api-context.ts +85 -0
  61. package/src/cli/version.ts +5 -0
  62. package/src/core/anti-fp/bootstrap.ts +34 -0
  63. package/src/core/anti-fp/index.ts +33 -0
  64. package/src/core/anti-fp/registry.ts +44 -0
  65. package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
  66. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
  67. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
  68. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
  69. package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
  70. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
  71. package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
  72. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
  73. package/src/core/anti-fp/types.ts +68 -0
  74. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  75. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  76. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  77. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  78. package/src/core/checks/checks/cross_call_references.ts +134 -0
  79. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  80. package/src/core/checks/checks/idempotency_replay.ts +246 -0
  81. package/src/core/checks/checks/ignored_auth.ts +211 -0
  82. package/src/core/checks/checks/index.ts +65 -0
  83. package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
  84. package/src/core/checks/checks/missing_required_header.ts +40 -0
  85. package/src/core/checks/checks/negative_data_rejection.ts +45 -0
  86. package/src/core/checks/checks/not_a_server_error.ts +27 -0
  87. package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
  88. package/src/core/checks/checks/pagination_invariants.ts +238 -0
  89. package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
  90. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  91. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  92. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  93. package/src/core/checks/checks/status_code_conformance.ts +61 -0
  94. package/src/core/checks/checks/unsupported_method.ts +63 -0
  95. package/src/core/checks/checks/use_after_free.ts +78 -0
  96. package/src/core/checks/index.ts +30 -0
  97. package/src/core/checks/mode.ts +79 -0
  98. package/src/core/checks/recommended-action.ts +64 -0
  99. package/src/core/checks/registry.ts +78 -0
  100. package/src/core/checks/runner.ts +874 -0
  101. package/src/core/checks/sarif.ts +230 -0
  102. package/src/core/checks/stateful.ts +121 -0
  103. package/src/core/checks/types.ts +189 -0
  104. package/src/core/classifier/recommended-action.ts +222 -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 +185 -0
  108. package/src/core/coverage/reasons.ts +300 -0
  109. package/src/core/diagnostics/db-analysis.ts +151 -11
  110. package/src/core/diagnostics/failure-class.ts +120 -0
  111. package/src/core/diagnostics/failure-hints.ts +212 -9
  112. package/src/core/diagnostics/spec-pointer.ts +99 -0
  113. package/src/core/diagnostics/suggested-fixes.ts +156 -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 +42 -16
  122. package/src/core/generator/coverage-phase.ts +0 -0
  123. package/src/core/generator/create-body.ts +89 -0
  124. package/src/core/generator/data-factory.ts +445 -19
  125. package/src/core/generator/describe.ts +1 -1
  126. package/src/core/generator/fixtures-builder.ts +325 -0
  127. package/src/core/generator/index.ts +7 -5
  128. package/src/core/generator/openapi-reader.ts +37 -3
  129. package/src/core/generator/path-param-disambig.ts +114 -0
  130. package/src/core/generator/resources-builder.ts +648 -0
  131. package/src/core/generator/schema-utils.ts +11 -3
  132. package/src/core/generator/serializer.ts +103 -13
  133. package/src/core/generator/suite-generator.ts +419 -111
  134. package/src/core/generator/types.ts +8 -0
  135. package/src/core/identity/identity-file.ts +129 -0
  136. package/src/core/lint/affects.ts +28 -0
  137. package/src/core/lint/config.ts +96 -0
  138. package/src/core/lint/format.ts +42 -0
  139. package/src/core/lint/index.ts +94 -0
  140. package/src/core/lint/reporter.ts +128 -0
  141. package/src/core/lint/rules/consistency.ts +158 -0
  142. package/src/core/lint/rules/heuristics.ts +97 -0
  143. package/src/core/lint/rules/strictness.ts +109 -0
  144. package/src/core/lint/types.ts +96 -0
  145. package/src/core/lint/walker.ts +248 -0
  146. package/src/core/meta/meta-store.ts +6 -73
  147. package/src/core/output/README.md +91 -0
  148. package/src/core/output/index.ts +13 -0
  149. package/src/core/output/run.ts +126 -0
  150. package/src/core/output/types.ts +129 -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 +57 -0
  159. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  160. package/src/core/probe/mass-assignment-probe.ts +1122 -0
  161. package/src/core/probe/mass-assignment-template.ts +212 -0
  162. package/src/core/probe/method-probe.ts +43 -76
  163. package/src/core/probe/method-shared.ts +69 -0
  164. package/src/core/probe/negative-probe.ts +183 -149
  165. package/src/core/probe/orphan-tracker.ts +188 -0
  166. package/src/core/probe/path-discovery.ts +440 -0
  167. package/src/core/probe/probe-harness.ts +120 -0
  168. package/src/core/probe/registry.ts +89 -0
  169. package/src/core/probe/runner.ts +136 -0
  170. package/src/core/probe/security-probe-class.ts +201 -0
  171. package/src/core/probe/security-probe.ts +1453 -0
  172. package/src/core/probe/shared.ts +505 -0
  173. package/src/core/probe/static-probe-class.ts +125 -0
  174. package/src/core/probe/types.ts +165 -0
  175. package/src/core/probe/verdict-aggregator.ts +33 -0
  176. package/src/core/probe/webhooks-probe.ts +284 -0
  177. package/src/core/reporter/console.ts +41 -2
  178. package/src/core/reporter/index.ts +2 -3
  179. package/src/core/reporter/json.ts +11 -1
  180. package/src/core/reporter/junit.ts +27 -12
  181. package/src/core/reporter/ndjson.ts +37 -0
  182. package/src/core/reporter/types.ts +3 -0
  183. package/src/core/runner/assertions.ts +58 -1
  184. package/src/core/runner/async-pool.ts +108 -0
  185. package/src/core/runner/auth-path.ts +8 -0
  186. package/src/core/runner/ci-context.ts +72 -0
  187. package/src/core/runner/executor.ts +264 -20
  188. package/src/core/runner/form-encode.ts +51 -0
  189. package/src/core/runner/http-client.ts +75 -2
  190. package/src/core/runner/learn-drift.ts +293 -0
  191. package/src/core/runner/preflight-vars.ts +149 -0
  192. package/src/core/runner/progress-tracker.ts +73 -0
  193. package/src/core/runner/rate-limiter.ts +89 -17
  194. package/src/core/runner/run-kind.ts +39 -0
  195. package/src/core/runner/schema-validator.ts +312 -0
  196. package/src/core/runner/send-request.ts +153 -20
  197. package/src/core/runner/types.ts +38 -0
  198. package/src/core/secrets/registry.ts +164 -0
  199. package/src/core/secrets/secrets-file.ts +115 -0
  200. package/src/core/selectors/operation-filter.ts +144 -0
  201. package/src/core/setup-api.ts +415 -16
  202. package/src/core/severity/category.ts +94 -0
  203. package/src/core/severity/index.ts +121 -0
  204. package/src/core/spec/layers.ts +154 -0
  205. package/src/core/util/format-eta.ts +21 -0
  206. package/src/core/utils.ts +5 -1
  207. package/src/core/workspace/config.ts +129 -0
  208. package/src/core/workspace/manifest.ts +283 -0
  209. package/src/core/workspace/output-rotation.ts +62 -0
  210. package/src/core/workspace/triage-path.ts +87 -0
  211. package/src/db/lint-runs.ts +47 -0
  212. package/src/db/migrate.ts +126 -0
  213. package/src/db/migrations/0001_run_kind.sql +25 -0
  214. package/src/db/migrations/sql.d.ts +4 -0
  215. package/src/db/queries/collections.ts +133 -0
  216. package/src/db/queries/coverage.ts +9 -0
  217. package/src/db/queries/dashboard.ts +59 -0
  218. package/src/db/queries/results.ts +128 -0
  219. package/src/db/queries/runs.ts +235 -0
  220. package/src/db/queries/sessions.ts +42 -0
  221. package/src/db/queries/settings.ts +28 -0
  222. package/src/db/queries/types.ts +172 -0
  223. package/src/db/queries.ts +72 -802
  224. package/src/db/schema.ts +178 -50
  225. package/src/cli/commands/export.ts +0 -144
  226. package/src/cli/commands/guide.ts +0 -127
  227. package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
  228. package/src/cli/commands/probe-methods.ts +0 -108
  229. package/src/cli/commands/probe-validation.ts +0 -124
  230. package/src/cli/commands/serve.ts +0 -114
  231. package/src/cli/commands/sync.ts +0 -268
  232. package/src/cli/commands/update.ts +0 -189
  233. package/src/cli/commands/validate.ts +0 -34
  234. package/src/core/diagnostics/render-md.ts +0 -112
  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 -19
  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/src/db/schema.ts CHANGED
@@ -1,15 +1,27 @@
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. For back-compat we still recognise a
13
+ * legacy `<workspace>/zond.db` if it exists — old workspaces keep working
14
+ * without migration.
15
+ */
16
+ function defaultDbPath(): string {
17
+ const root = findWorkspaceRoot().root;
18
+ const legacy = resolve(root, "zond.db");
19
+ if (existsSync(legacy)) return legacy;
20
+ return resolve(root, ".zond", "zond.db");
21
+ }
22
+
9
23
  export function getDb(dbPath?: string): Database {
10
- const path = dbPath
11
- ? resolve(dbPath)
12
- : (_dbPath ?? resolve(findWorkspaceRoot().root, "zond.db"));
24
+ const path = dbPath ? resolve(dbPath) : (_dbPath ?? defaultDbPath());
13
25
 
14
26
  // If cached connection exists, verify the file still exists
15
27
  if (_db && _dbPath === path && existsSync(path)) return _db;
@@ -20,19 +32,68 @@ export function getDb(dbPath?: string): Database {
20
32
  _db = null;
21
33
  _dbPath = null;
22
34
  }
35
+ // SQLite won't auto-create parent dirs; ensure `.zond/` (or any custom
36
+ // path's parent) exists before opening the file.
37
+ const parent = dirname(path);
38
+ if (!existsSync(parent)) mkdirSync(parent, { recursive: true });
39
+
23
40
  const db = new Database(path, { create: true });
24
41
 
25
42
  // Performance and integrity settings
26
43
  db.exec("PRAGMA journal_mode = WAL");
27
44
  db.exec("PRAGMA foreign_keys = ON");
45
+ // ARV-163: concurrent zond processes (e.g. `probe security` in one terminal
46
+ // + `checks run` in another) collide on the write-lock and surface as
47
+ // "database is locked". Letting SQLite spin at the C-level for up to 5s
48
+ // resolves the overwhelming majority without any per-call retry logic.
49
+ // `synchronous=NORMAL` is safe under WAL and cuts fsync overhead in the
50
+ // bulk-insert path (saveResults). withDbRetry() in this file remains the
51
+ // belt-and-suspenders for the rare long-running transactions.
52
+ db.exec("PRAGMA busy_timeout = 5000");
53
+ db.exec("PRAGMA synchronous = NORMAL");
28
54
 
29
55
  runMigrations(db);
30
56
 
57
+ // ARV-127: file-based migrations sit on top of the legacy PRAGMA
58
+ // path. On an existing DB the runner pre-seeds `schema_migrations`
59
+ // so the v9→v10 inline migration (now mirrored in
60
+ // src/db/migrations/0001_run_kind.sql) is treated as applied.
61
+ applyMigrations(db);
62
+
31
63
  _db = db;
32
64
  _dbPath = path;
33
65
  return db;
34
66
  }
35
67
 
68
+ /**
69
+ * ARV-163: retry wrapper for SQLite write paths that may collide with
70
+ * concurrent zond processes. `PRAGMA busy_timeout` already absorbs short
71
+ * contention at the C level — this wrapper only catches the residual cases
72
+ * where the lock holder runs longer than the timeout (e.g. a big
73
+ * `saveResults` bulk insert during a parallel `probe security` cleanup).
74
+ *
75
+ * Detects bun:sqlite's "database is locked" / "SQLITE_BUSY" message shapes;
76
+ * other errors propagate immediately. Backoff: 100, 200, 400, 800ms capped at
77
+ * 4 attempts (≈1.5s total) so we never silently stall a CLI command.
78
+ */
79
+ export function withDbRetry<T>(label: string, fn: () => T): T {
80
+ const delaysMs = [100, 200, 400, 800];
81
+ let lastError: unknown;
82
+ for (let attempt = 0; attempt <= delaysMs.length; attempt++) {
83
+ try {
84
+ return fn();
85
+ } catch (err) {
86
+ const msg = err instanceof Error ? err.message : String(err);
87
+ if (!/database is locked|SQLITE_BUSY/i.test(msg)) throw err;
88
+ lastError = err;
89
+ if (attempt === delaysMs.length) break;
90
+ Bun.sleepSync(delaysMs[attempt]!);
91
+ }
92
+ }
93
+ const msg = lastError instanceof Error ? lastError.message : String(lastError);
94
+ throw new Error(`SQLite still locked after ${delaysMs.length + 1} attempts (${label}): ${msg}`);
95
+ }
96
+
36
97
  export function closeDb(): void {
37
98
  if (_db) {
38
99
  try { _db.close(); } catch {}
@@ -41,7 +102,7 @@ export function closeDb(): void {
41
102
  }
42
103
  }
43
104
 
44
- export function resetDb(): void {
105
+ function resetDb(): void {
45
106
  if (_db) { try { _db.close(); } catch {} }
46
107
  _db = null;
47
108
  _dbPath = null;
@@ -51,7 +112,7 @@ export function resetDb(): void {
51
112
  // Schema
52
113
  // ──────────────────────────────────────────────
53
114
 
54
- const SCHEMA_VERSION = 2;
115
+ const SCHEMA_VERSION = 10;
55
116
 
56
117
  const SCHEMA = `
57
118
  CREATE TABLE IF NOT EXISTS runs (
@@ -67,7 +128,13 @@ const SCHEMA = `
67
128
  branch TEXT,
68
129
  environment TEXT,
69
130
  duration_ms INTEGER,
70
- collection_id INTEGER REFERENCES collections(id)
131
+ collection_id INTEGER REFERENCES collections(id),
132
+ session_id TEXT,
133
+ tags TEXT,
134
+ -- ARV-55: classify a run once at INSERT time so coverage / diagnose
135
+ -- queries don't have to re-derive "is this a probe-only run?" from
136
+ -- the results' suite_file paths.
137
+ run_kind TEXT NOT NULL DEFAULT 'regular' CHECK (run_kind IN ('regular','probe','check'))
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.