@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/src/db/schema.ts CHANGED
@@ -1,12 +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
+ import { findWorkspaceRoot } from "../core/workspace/root.ts";
5
+ import { applyMigrations } from "./migrate.ts";
4
6
 
5
7
  let _db: Database | null = null;
6
8
  let _dbPath: string | null = null;
7
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
+
8
23
  export function getDb(dbPath?: string): Database {
9
- const path = dbPath ? resolve(dbPath) : (_dbPath ?? resolve(process.cwd(), "zond.db"));
24
+ const path = dbPath ? resolve(dbPath) : (_dbPath ?? defaultDbPath());
10
25
 
11
26
  // If cached connection exists, verify the file still exists
12
27
  if (_db && _dbPath === path && existsSync(path)) return _db;
@@ -17,19 +32,68 @@ export function getDb(dbPath?: string): Database {
17
32
  _db = null;
18
33
  _dbPath = null;
19
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
+
20
40
  const db = new Database(path, { create: true });
21
41
 
22
42
  // Performance and integrity settings
23
43
  db.exec("PRAGMA journal_mode = WAL");
24
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");
25
54
 
26
55
  runMigrations(db);
27
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
+
28
63
  _db = db;
29
64
  _dbPath = path;
30
65
  return db;
31
66
  }
32
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
+
33
97
  export function closeDb(): void {
34
98
  if (_db) {
35
99
  try { _db.close(); } catch {}
@@ -38,7 +102,7 @@ export function closeDb(): void {
38
102
  }
39
103
  }
40
104
 
41
- export function resetDb(): void {
105
+ function resetDb(): void {
42
106
  if (_db) { try { _db.close(); } catch {} }
43
107
  _db = null;
44
108
  _dbPath = null;
@@ -48,7 +112,7 @@ export function resetDb(): void {
48
112
  // Schema
49
113
  // ──────────────────────────────────────────────
50
114
 
51
- const SCHEMA_VERSION = 2;
115
+ const SCHEMA_VERSION = 10;
52
116
 
53
117
  const SCHEMA = `
54
118
  CREATE TABLE IF NOT EXISTS runs (
@@ -64,7 +128,13 @@ const SCHEMA = `
64
128
  branch TEXT,
65
129
  environment TEXT,
66
130
  duration_ms INTEGER,
67
- 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'))
68
138
  );
69
139
 
70
140
  CREATE TABLE IF NOT EXISTS results (
@@ -83,7 +153,12 @@ const SCHEMA = `
83
153
  assertions TEXT,
84
154
  captures TEXT,
85
155
  response_headers TEXT,
86
- 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
87
162
  );
88
163
 
89
164
  CREATE TABLE IF NOT EXISTS collections (
@@ -95,44 +170,6 @@ const SCHEMA = `
95
170
  base_dir TEXT
96
171
  );
97
172
 
98
- CREATE TABLE IF NOT EXISTS ai_generations (
99
- id INTEGER PRIMARY KEY AUTOINCREMENT,
100
- collection_id INTEGER REFERENCES collections(id),
101
- prompt TEXT NOT NULL,
102
- model TEXT NOT NULL,
103
- provider TEXT NOT NULL,
104
- generated_yaml TEXT,
105
- output_path TEXT,
106
- status TEXT NOT NULL DEFAULT 'pending',
107
- error_message TEXT,
108
- prompt_tokens INTEGER,
109
- completion_tokens INTEGER,
110
- duration_ms INTEGER,
111
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
112
- );
113
-
114
- CREATE TABLE IF NOT EXISTS chat_sessions (
115
- id INTEGER PRIMARY KEY AUTOINCREMENT,
116
- title TEXT,
117
- provider TEXT NOT NULL,
118
- model TEXT NOT NULL,
119
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
120
- last_active TEXT NOT NULL DEFAULT (datetime('now'))
121
- );
122
-
123
- CREATE TABLE IF NOT EXISTS chat_messages (
124
- id INTEGER PRIMARY KEY AUTOINCREMENT,
125
- session_id INTEGER NOT NULL REFERENCES chat_sessions(id),
126
- role TEXT NOT NULL,
127
- content TEXT NOT NULL,
128
- tool_name TEXT,
129
- tool_args TEXT,
130
- tool_result TEXT,
131
- input_tokens INTEGER,
132
- output_tokens INTEGER,
133
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
134
- );
135
-
136
173
  CREATE TABLE IF NOT EXISTS settings (
137
174
  key TEXT PRIMARY KEY,
138
175
  value TEXT NOT NULL
@@ -140,13 +177,27 @@ const SCHEMA = `
140
177
 
141
178
  CREATE INDEX IF NOT EXISTS idx_runs_started ON runs(started_at DESC);
142
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);
143
181
  CREATE INDEX IF NOT EXISTS idx_results_run ON results(run_id);
144
182
  CREATE INDEX IF NOT EXISTS idx_results_status ON results(status);
145
183
  CREATE INDEX IF NOT EXISTS idx_results_name ON results(suite_name, test_name);
146
184
  CREATE INDEX IF NOT EXISTS idx_collections_name ON collections(name);
147
- CREATE INDEX IF NOT EXISTS idx_ai_gen_collection ON ai_generations(collection_id);
148
- CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id);
149
- 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);
150
201
  `;
151
202
 
152
203
  function runMigrations(db: Database): void {
@@ -162,6 +213,86 @@ function runMigrations(db: Database): void {
162
213
  // Migration v1→v2: add suite_file column to results
163
214
  db.exec("ALTER TABLE results ADD COLUMN suite_file TEXT");
164
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
+ }
165
296
  db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
166
297
  })();
167
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,57 +0,0 @@
1
- import { setupApi } from "../../core/setup-api.ts";
2
- import { printError, printSuccess } from "../output.ts";
3
- import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
4
-
5
- export interface InitOptions {
6
- name?: string;
7
- spec?: string;
8
- baseUrl?: string;
9
- dir?: string;
10
- force?: boolean;
11
- insecure?: boolean;
12
- dbPath?: string;
13
- json?: boolean;
14
- }
15
-
16
- export async function initCommand(options: InitOptions): Promise<number> {
17
- try {
18
- const envVars: Record<string, string> = {};
19
- if (options.baseUrl) envVars.base_url = options.baseUrl;
20
-
21
- const result = await setupApi({
22
- name: options.name,
23
- spec: options.spec,
24
- dir: options.dir,
25
- envVars: Object.keys(envVars).length > 0 ? envVars : undefined,
26
- dbPath: options.dbPath,
27
- force: options.force,
28
- insecure: options.insecure,
29
- });
30
-
31
- if (options.json) {
32
- printJson(jsonOk("init", {
33
- collectionId: result.collectionId,
34
- baseDir: result.baseDir,
35
- testPath: result.testPath,
36
- endpoints: result.specEndpoints,
37
- warnings: result.warnings ?? [],
38
- }, result.warnings));
39
- } else {
40
- printSuccess(`Created API '${options.name ?? "api"}' at ${result.baseDir} (${result.specEndpoints} endpoints)`);
41
- if (result.warnings) {
42
- for (const w of result.warnings) {
43
- process.stderr.write(`Warning: ${w}\n`);
44
- }
45
- }
46
- }
47
- return 0;
48
- } catch (err) {
49
- const message = err instanceof Error ? err.message : String(err);
50
- if (options.json) {
51
- printJson(jsonError("init", [message]));
52
- } else {
53
- printError(message);
54
- }
55
- return 2;
56
- }
57
- }