@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
@@ -0,0 +1,126 @@
1
+ /**
2
+ * ARV-127 (m-19): file-based SQLite migration runner.
3
+ *
4
+ * Why a new runner. The legacy migration path in `schema.ts`
5
+ * (`runMigrations` + `PRAGMA user_version`) is fine for the additive
6
+ * column changes shipped through v10, but the knowledge-base work
7
+ * planned past m-19 will need richer migrations (multi-statement,
8
+ * data backfills, optional rollback notes). Inlining those as `if
9
+ * (ver >= N && ver < N+1)` blocks in TypeScript stops scaling once
10
+ * each migration becomes a small project of its own.
11
+ *
12
+ * This module sits on top of the legacy path:
13
+ * - `runMigrations()` is untouched — it owns the PRAGMA-version era
14
+ * and keeps fresh DBs / older snapshots correct.
15
+ * - `applyMigrations()` runs *after* `runMigrations()`, walks the
16
+ * registered migration list, and applies anything not yet recorded
17
+ * in `schema_migrations`. New work (v11+) lands as files; the
18
+ * 0001_run_kind.sql file mirrors the most recent legacy migration
19
+ * so the two systems agree on the post-v10 schema for fresh DBs.
20
+ *
21
+ * Existing-DB compatibility (AC#5). On a `.zond/zond.db` that already
22
+ * survived the legacy `runMigrations` path (user_version >= 10), the
23
+ * `run_kind` column already exists — re-running `0001_run_kind.sql`
24
+ * would throw a `duplicate column` error. We seed the legacy ids into
25
+ * `schema_migrations` once, on first contact with the new runner, so
26
+ * those rows are treated as "already applied" without executing.
27
+ *
28
+ * Distribution. The SQL bodies are imported as embedded text so
29
+ * `bun build --compile` packs them into the binary (no on-disk
30
+ * lookup at runtime — same pattern as the init/templates skills).
31
+ */
32
+ import type { Database } from "bun:sqlite";
33
+
34
+ import migration_0001_run_kind from "./migrations/0001_run_kind.sql" with { type: "text" };
35
+
36
+ /** Migration manifest. Each entry is a `{ id, sql }` pair; order in
37
+ * this array is the apply order, matching the lexical sort that the
38
+ * Django / Rails-style `<id>_<slug>.sql` convention would produce on
39
+ * disk. Adding a new migration = add a text-import + push to this
40
+ * list. The runner reads this constant, not the filesystem. */
41
+ const MIGRATIONS: ReadonlyArray<{ id: string; sql: string }> = [
42
+ { id: "0001_run_kind", sql: migration_0001_run_kind },
43
+ ];
44
+
45
+ /** Pre-existing migration ids that were already applied by the legacy
46
+ * PRAGMA-version path. When the new runner first encounters a DB
47
+ * whose `user_version >= 10`, we record these as applied without
48
+ * running them — the inline `runMigrations` already did. */
49
+ const LEGACY_SEED_IDS: ReadonlyArray<{ id: string; minUserVersion: number }> = [
50
+ { id: "0001_run_kind", minUserVersion: 10 },
51
+ ];
52
+
53
+ function currentUserVersion(db: Database): number {
54
+ const row = db.query("PRAGMA user_version").get() as
55
+ | { user_version: number }
56
+ | undefined;
57
+ return row?.user_version ?? 0;
58
+ }
59
+
60
+ /**
61
+ * Idempotently apply every pending migration. Safe to call on every
62
+ * DB open — the registry table makes the no-op case cheap.
63
+ *
64
+ * Failure semantics: each migration runs in its own transaction. A
65
+ * script that throws (bad SQL, constraint violation) rolls its own
66
+ * statements back and re-raises; later migrations don't run. The
67
+ * caller (DB open path) treats this as fatal — there is no partial
68
+ * upgrade.
69
+ *
70
+ * The optional `overrides` parameter lets tests inject a synthetic
71
+ * migration list (e.g. to exercise a migration order or a failing
72
+ * script) without touching the shipped manifest.
73
+ */
74
+ export function applyMigrations(
75
+ db: Database,
76
+ overrides?: { migrations?: ReadonlyArray<{ id: string; sql: string }>; legacySeed?: ReadonlyArray<{ id: string; minUserVersion: number }> },
77
+ ): { applied: string[]; skipped: string[] } {
78
+ const migrations = overrides?.migrations ?? MIGRATIONS;
79
+ const legacySeed = overrides?.legacySeed ?? LEGACY_SEED_IDS;
80
+
81
+ db.exec(`
82
+ CREATE TABLE IF NOT EXISTS schema_migrations (
83
+ id TEXT PRIMARY KEY,
84
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
85
+ )
86
+ `);
87
+
88
+ // Legacy seed: mark already-applied-by-the-PRAGMA-runner ids as done.
89
+ const userVersion = currentUserVersion(db);
90
+ const insertSeed = db.prepare(
91
+ "INSERT OR IGNORE INTO schema_migrations (id) VALUES (?)",
92
+ );
93
+ for (const seed of legacySeed) {
94
+ if (userVersion >= seed.minUserVersion) {
95
+ insertSeed.run(seed.id);
96
+ }
97
+ }
98
+
99
+ const appliedRows = db
100
+ .query("SELECT id FROM schema_migrations")
101
+ .all() as Array<{ id: string }>;
102
+ const alreadyApplied = new Set(appliedRows.map((r) => r.id));
103
+
104
+ const applied: string[] = [];
105
+ const skipped: string[] = [];
106
+
107
+ for (const migration of migrations) {
108
+ if (alreadyApplied.has(migration.id)) {
109
+ skipped.push(migration.id);
110
+ continue;
111
+ }
112
+ db.transaction(() => {
113
+ db.exec(migration.sql);
114
+ db.prepare("INSERT INTO schema_migrations (id) VALUES (?)").run(migration.id);
115
+ })();
116
+ applied.push(migration.id);
117
+ }
118
+
119
+ return { applied, skipped };
120
+ }
121
+
122
+ /** Exported for tests + downstream tooling that wants to know which
123
+ * migration ids ship with the binary. */
124
+ export function listShippedMigrations(): string[] {
125
+ return MIGRATIONS.map((m) => m.id);
126
+ }
@@ -0,0 +1,25 @@
1
+ -- ARV-127 (m-19): captures the legacy v9→v10 inline migration as the
2
+ -- first file-based migration of the new runner. Mirrors the SQL block
3
+ -- previously written in src/db/schema.ts `runMigrations()`. Existing
4
+ -- `.zond/zond.db` files that already ran the inline migration are
5
+ -- pre-seeded as "applied" by `applyMigrations`, so this script never
6
+ -- re-executes the ALTER on a DB where `run_kind` already exists.
7
+ --
8
+ -- Source: ARV-55 — classify each historical run by suite kind so the
9
+ -- coverage default query becomes a column compare.
10
+
11
+ ALTER TABLE runs ADD COLUMN run_kind TEXT NOT NULL DEFAULT 'regular';
12
+
13
+ UPDATE runs SET run_kind = 'probe'
14
+ WHERE id IN (
15
+ SELECT r.id FROM runs r
16
+ WHERE EXISTS (SELECT 1 FROM results WHERE run_id = r.id AND suite_file IS NOT NULL AND suite_file LIKE '%probes/%')
17
+ AND NOT EXISTS (SELECT 1 FROM results WHERE run_id = r.id AND suite_file IS NOT NULL AND suite_file NOT LIKE '%probes/%')
18
+ );
19
+
20
+ UPDATE runs SET run_kind = 'check'
21
+ WHERE id IN (
22
+ SELECT r.id FROM runs r
23
+ WHERE EXISTS (SELECT 1 FROM results WHERE run_id = r.id AND suite_file IS NOT NULL AND suite_file LIKE '%checks/%')
24
+ AND NOT EXISTS (SELECT 1 FROM results WHERE run_id = r.id AND suite_file IS NOT NULL AND suite_file NOT LIKE '%checks/%')
25
+ );
@@ -0,0 +1,4 @@
1
+ declare module "*.sql" {
2
+ const content: string;
3
+ export default content;
4
+ }
@@ -0,0 +1,133 @@
1
+ import { getDb } from "../schema.ts";
2
+ import {
3
+ normalizePath,
4
+ type CollectionRecord,
5
+ type CollectionSummary,
6
+ type CreateCollectionOpts,
7
+ type RunRecord,
8
+ } from "./types.ts";
9
+
10
+ export function createCollection(opts: CreateCollectionOpts): number {
11
+ const db = getDb();
12
+ const stmt = db.prepare(`
13
+ INSERT INTO collections (name, base_dir, test_path, openapi_spec)
14
+ VALUES ($name, $base_dir, $test_path, $openapi_spec)
15
+ `);
16
+ const result = stmt.run({
17
+ $name: opts.name,
18
+ $base_dir: opts.base_dir ?? null,
19
+ $test_path: opts.test_path,
20
+ $openapi_spec: opts.openapi_spec ?? null,
21
+ });
22
+ return Number(result.lastInsertRowid);
23
+ }
24
+
25
+ export function getCollectionById(id: number): CollectionRecord | null {
26
+ const db = getDb();
27
+ return db.query("SELECT * FROM collections WHERE id = ?").get(id) as CollectionRecord | null;
28
+ }
29
+
30
+ export function getLatestRunByCollection(
31
+ collectionId: number,
32
+ opts: { runKind?: "regular" | "probe" | "check" | "any" } = {},
33
+ ): RunRecord | null {
34
+ const db = getDb();
35
+ // ARV-55: 'regular' is the default so coverage skips probe-only runs
36
+ // without an explicit predicate. 'any' opts back into the legacy
37
+ // behaviour (used by `coverage`'s probe-run hint logic).
38
+ const kind = opts.runKind ?? "regular";
39
+ const kindClause = kind === "any" ? "" : "AND run_kind = ?";
40
+ const params: (string | number)[] = [collectionId];
41
+ if (kind !== "any") params.push(kind);
42
+ const row = db.query(`
43
+ SELECT * FROM runs
44
+ WHERE collection_id = ? AND finished_at IS NOT NULL ${kindClause}
45
+ ORDER BY started_at DESC
46
+ LIMIT 1
47
+ `).get(...params) as (Record<string, unknown> & { tags?: unknown }) | null;
48
+ if (!row) return null;
49
+ let tags: string[] | null = null;
50
+ if (typeof row.tags === "string") {
51
+ try {
52
+ const v = JSON.parse(row.tags);
53
+ if (Array.isArray(v) && v.every((x) => typeof x === "string")) tags = v;
54
+ } catch {
55
+ // legacy/corrupt — leave null
56
+ }
57
+ }
58
+ // ARV-55: normalise run_kind alongside tags so RunRecord stays consistent.
59
+ const rk = row.run_kind;
60
+ const run_kind: import("../../core/runner/run-kind.ts").RunKind =
61
+ rk === "probe" || rk === "check" ? rk : "regular";
62
+ return { ...(row as unknown as RunRecord), tags, run_kind };
63
+ }
64
+
65
+ export function listCollections(): CollectionSummary[] {
66
+ const db = getDb();
67
+ return db.query(`
68
+ SELECT
69
+ c.id, c.name, c.base_dir, c.test_path, c.openapi_spec, c.created_at,
70
+ COUNT(r.id) AS total_runs,
71
+ CASE WHEN SUM(r.total) > 0
72
+ THEN ROUND(SUM(r.passed) * 100.0 / SUM(r.total), 1)
73
+ ELSE 0 END AS pass_rate,
74
+ MAX(r.started_at) AS last_run_at,
75
+ COALESCE((SELECT passed FROM runs WHERE collection_id = c.id ORDER BY started_at DESC LIMIT 1), 0) AS last_run_passed,
76
+ COALESCE((SELECT failed FROM runs WHERE collection_id = c.id ORDER BY started_at DESC LIMIT 1), 0) AS last_run_failed,
77
+ COALESCE((SELECT total FROM runs WHERE collection_id = c.id ORDER BY started_at DESC LIMIT 1), 0) AS last_run_total
78
+ FROM collections c
79
+ LEFT JOIN runs r ON r.collection_id = c.id AND r.finished_at IS NOT NULL
80
+ GROUP BY c.id
81
+ ORDER BY c.name
82
+ `).all() as CollectionSummary[];
83
+ }
84
+
85
+ export function updateCollection(id: number, opts: Partial<CreateCollectionOpts>): boolean {
86
+ const db = getDb();
87
+ const sets: string[] = [];
88
+ const params: Record<string, any> = { $id: id };
89
+
90
+ if (opts.name !== undefined) { sets.push("name = $name"); params.$name = opts.name; }
91
+ if (opts.base_dir !== undefined) { sets.push("base_dir = $base_dir"); params.$base_dir = opts.base_dir; }
92
+ if (opts.test_path !== undefined) { sets.push("test_path = $test_path"); params.$test_path = opts.test_path; }
93
+ if (opts.openapi_spec !== undefined) { sets.push("openapi_spec = $openapi_spec"); params.$openapi_spec = opts.openapi_spec; }
94
+
95
+ if (sets.length === 0) return false;
96
+
97
+ const result = db.prepare(`UPDATE collections SET ${sets.join(", ")} WHERE id = $id`).run(params);
98
+ return result.changes > 0;
99
+ }
100
+
101
+ export function deleteCollection(id: number, deleteRuns = false): boolean {
102
+ const db = getDb();
103
+ if (deleteRuns) {
104
+ const runIds = db.query("SELECT id FROM runs WHERE collection_id = ?").all(id) as { id: number }[];
105
+ for (const row of runIds) {
106
+ db.prepare("DELETE FROM results WHERE run_id = ?").run(row.id);
107
+ }
108
+ db.prepare("DELETE FROM runs WHERE collection_id = ?").run(id);
109
+ } else {
110
+ db.prepare("UPDATE runs SET collection_id = NULL WHERE collection_id = ?").run(id);
111
+ }
112
+ const result = db.prepare("DELETE FROM collections WHERE id = ?").run(id);
113
+ return result.changes > 0;
114
+ }
115
+
116
+ export function findCollectionByTestPath(path: string): CollectionRecord | null {
117
+ const db = getDb();
118
+ const normalized = normalizePath(path);
119
+ return db.query("SELECT * FROM collections WHERE test_path = ?").get(normalized) as CollectionRecord | null;
120
+ }
121
+
122
+ export function findCollectionByNameOrId(nameOrId: string): CollectionRecord | null {
123
+ const db = getDb();
124
+ // Try as numeric ID first
125
+ const id = parseInt(nameOrId, 10);
126
+ if (!isNaN(id)) {
127
+ const byId = db.query("SELECT * FROM collections WHERE id = ?").get(id) as CollectionRecord | null;
128
+ if (byId) return byId;
129
+ }
130
+ // Then by name (case-insensitive)
131
+ return db.query("SELECT * FROM collections WHERE lower(name) = lower(?)").get(nameOrId) as CollectionRecord | null;
132
+ }
133
+
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Reserved for coverage-domain DB queries. As of TASK-187, coverage
3
+ * tables (`coverage_runs`, etc.) are still managed entirely from
4
+ * `src/core/coverage/`; nothing in `queries.ts` was coverage-specific to
5
+ * move here. The file exists so the per-domain layout is future-proof
6
+ * — when a coverage table-backed feature lands, queries land here and
7
+ * the cli/UI imports stay stable.
8
+ */
9
+ export {};
@@ -0,0 +1,59 @@
1
+ import { getDb } from "../schema.ts";
2
+ import type {
3
+ DashboardStats,
4
+ PassRateTrendPoint,
5
+ SlowestTest,
6
+ FlakyTest,
7
+ } from "./types.ts";
8
+
9
+ export function getDashboardStats(): DashboardStats {
10
+ const db = getDb();
11
+ const row = db.query(`
12
+ SELECT
13
+ COUNT(*) AS totalRuns,
14
+ COALESCE(SUM(total), 0) AS totalTests,
15
+ CASE WHEN SUM(total) > 0
16
+ THEN ROUND(SUM(passed) * 100.0 / SUM(total), 1)
17
+ ELSE 0 END AS overallPassRate,
18
+ COALESCE(ROUND(AVG(duration_ms), 0), 0) AS avgDuration
19
+ FROM runs
20
+ WHERE finished_at IS NOT NULL
21
+ `).get() as { totalRuns: number; totalTests: number; overallPassRate: number; avgDuration: number };
22
+ return row;
23
+ }
24
+
25
+ export function getPassRateTrend(limit = 30): PassRateTrendPoint[] {
26
+ const db = getDb();
27
+ return db.query(`
28
+ SELECT id AS run_id, started_at,
29
+ CASE WHEN total > 0 THEN ROUND(passed * 100.0 / total, 1) ELSE 0 END AS pass_rate
30
+ FROM runs
31
+ WHERE finished_at IS NOT NULL
32
+ ORDER BY started_at DESC
33
+ LIMIT ?
34
+ `).all(limit) as PassRateTrendPoint[];
35
+ }
36
+
37
+ export function getSlowestTests(limit = 5): SlowestTest[] {
38
+ const db = getDb();
39
+ return db.query(`
40
+ SELECT suite_name, test_name, ROUND(AVG(duration_ms), 0) AS avg_duration
41
+ FROM results
42
+ GROUP BY suite_name, test_name
43
+ ORDER BY avg_duration DESC
44
+ LIMIT ?
45
+ `).all(limit) as SlowestTest[];
46
+ }
47
+
48
+ export function getFlakyTests(runsBack = 20, limit = 5): FlakyTest[] {
49
+ const db = getDb();
50
+ return db.query(`
51
+ SELECT r.suite_name, r.test_name, COUNT(DISTINCT r.status) AS distinct_statuses
52
+ FROM results r
53
+ INNER JOIN (SELECT id FROM runs ORDER BY started_at DESC LIMIT ?) recent ON r.run_id = recent.id
54
+ GROUP BY r.suite_name, r.test_name
55
+ HAVING COUNT(DISTINCT r.status) > 1
56
+ ORDER BY distinct_statuses DESC
57
+ LIMIT ?
58
+ `).all(runsBack, limit) as FlakyTest[];
59
+ }
@@ -0,0 +1,128 @@
1
+ import { getDb, withDbRetry } from "../schema.ts";
2
+ import type { TestRunResult } from "../../core/runner/types.ts";
3
+ import { getSecretRegistry } from "../../core/secrets/registry.ts";
4
+ import type { StoredStepResult } from "./types.ts";
5
+
6
+ function parseProvenance(raw: unknown): import("../../core/parser/types.ts").SourceMetadata | null {
7
+ if (typeof raw !== "string" || raw.length === 0) return null;
8
+ try {
9
+ const parsed = JSON.parse(raw);
10
+ return parsed && typeof parsed === "object" ? parsed : null;
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ export function saveResults(runId: number, suiteResults: TestRunResult[]): void {
17
+ const db = getDb();
18
+
19
+ const stmt = db.prepare(`
20
+ INSERT INTO results
21
+ (run_id, suite_name, test_name, status, duration_ms,
22
+ request_method, request_url, request_body,
23
+ response_status, response_body, response_headers, error_message, assertions, captures, suite_file, provenance, failure_class, failure_class_reason, spec_pointer, spec_excerpt)
24
+ VALUES
25
+ ($run_id, $suite_name, $test_name, $status, $duration_ms,
26
+ $request_method, $request_url, $request_body,
27
+ $response_status, $response_body, $response_headers, $error_message, $assertions, $captures, $suite_file, $provenance, $failure_class, $failure_class_reason, $spec_pointer, $spec_excerpt)
28
+ `);
29
+
30
+ // TASK-167 (m-10): every string field that can carry a leaked secret
31
+ // (URL with token in query, body echo on 401, Set-Cookie header, etc.)
32
+ // goes through the registry sanitizer before INSERT.
33
+ const reg = getSecretRegistry();
34
+ const redactString = (s: string | null | undefined): string | null =>
35
+ s == null ? null : reg.redact(s);
36
+ const redactJson = (v: unknown): string | null => {
37
+ if (v == null) return null;
38
+ if (typeof v === "string") return reg.redact(v);
39
+ return reg.redact(JSON.stringify(v));
40
+ };
41
+
42
+ withDbRetry("saveResults", () => db.transaction(() => {
43
+ for (const suite of suiteResults) {
44
+ for (const step of suite.steps) {
45
+ const maxBodySize = 50_000;
46
+ const truncBody = (s: string | null | undefined) =>
47
+ s && s.length > maxBodySize ? s.slice(0, maxBodySize) + "\n...[truncated]" : (s ?? null);
48
+ stmt.run({
49
+ $run_id: runId,
50
+ $suite_name: suite.suite_name,
51
+ $test_name: step.name,
52
+ $status: step.status,
53
+ $duration_ms: step.duration_ms,
54
+ $request_method: step.request.method,
55
+ $request_url: redactString(step.request.url),
56
+ $request_body: redactString(truncBody(step.request.body)),
57
+ $response_status: step.response?.status ?? null,
58
+ $response_body: redactString(truncBody(step.response?.body)),
59
+ $response_headers: step.response?.headers
60
+ ? redactJson(step.response.headers)
61
+ : null,
62
+ $error_message: redactString(step.error ?? null),
63
+ $assertions: step.assertions.length > 0 ? redactJson(step.assertions) : null,
64
+ $captures: Object.keys(step.captures).length > 0 ? redactJson(step.captures) : null,
65
+ $suite_file: suite.suite_file ?? null,
66
+ $provenance: step.provenance ? JSON.stringify(step.provenance) : null,
67
+ $failure_class: step.failure_class ?? null,
68
+ $failure_class_reason: step.failure_class_reason ?? null,
69
+ $spec_pointer: step.spec_pointer ?? null,
70
+ $spec_excerpt: redactString(step.spec_excerpt ?? null),
71
+ });
72
+ }
73
+ }
74
+ })());
75
+ }
76
+
77
+ export function getResultsByRunId(runId: number): StoredStepResult[] {
78
+ const db = getDb();
79
+ const rows = db.query("SELECT * FROM results WHERE run_id = ? ORDER BY id").all(runId) as Array<
80
+ Omit<StoredStepResult, "assertions" | "captures" | "provenance"> & {
81
+ assertions: string | null;
82
+ captures: string | null;
83
+ provenance: string | null;
84
+ }
85
+ >;
86
+ return rows.map((row) => ({
87
+ ...row,
88
+ assertions: row.assertions ? JSON.parse(row.assertions) : [],
89
+ captures: row.captures ? JSON.parse(row.captures) : {},
90
+ provenance: parseProvenance(row.provenance),
91
+ }));
92
+ }
93
+
94
+ export function getFilteredResults(
95
+ runId: number,
96
+ filters: {
97
+ method?: string;
98
+ /** Compiled SQL fragment for the `--status` filter (TASK-140). */
99
+ statusSql?: { sql: string; params: number[] };
100
+ },
101
+ ): StoredStepResult[] {
102
+ const db = getDb();
103
+ const conditions = ["run_id = ?"];
104
+ const params: (string | number)[] = [runId];
105
+
106
+ if (filters.method) {
107
+ conditions.push("request_method = ?");
108
+ params.push(filters.method.toUpperCase());
109
+ }
110
+ if (filters.statusSql) {
111
+ conditions.push(filters.statusSql.sql);
112
+ params.push(...filters.statusSql.params);
113
+ }
114
+
115
+ const rows = db.query(`SELECT * FROM results WHERE ${conditions.join(" AND ")} ORDER BY id`).all(...params) as Array<
116
+ Omit<StoredStepResult, "assertions" | "captures" | "provenance"> & {
117
+ assertions: string | null;
118
+ captures: string | null;
119
+ provenance: string | null;
120
+ }
121
+ >;
122
+ return rows.map((row) => ({
123
+ ...row,
124
+ assertions: row.assertions ? JSON.parse(row.assertions) : [],
125
+ captures: row.captures ? JSON.parse(row.captures) : {},
126
+ provenance: parseProvenance(row.provenance),
127
+ }));
128
+ }