@kirrosh/zond 0.26.1 → 0.27.1

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 (261) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +39 -22
  3. package/bin/zond.mjs +23 -0
  4. package/package.json +36 -23
  5. package/scripts/npm/postinstall.mjs +76 -0
  6. package/src/CLAUDE.md +0 -112
  7. package/src/bun-types.d.ts +0 -5
  8. package/src/cli/argv.ts +0 -122
  9. package/src/cli/commands/add-api.ts +0 -146
  10. package/src/cli/commands/api/annotate/idempotency.ts +0 -59
  11. package/src/cli/commands/api/annotate/index.ts +0 -880
  12. package/src/cli/commands/api/annotate/lifecycle.ts +0 -74
  13. package/src/cli/commands/api/annotate/overlay.ts +0 -206
  14. package/src/cli/commands/api/annotate/pagination.ts +0 -64
  15. package/src/cli/commands/api/annotate/prompts.ts +0 -220
  16. package/src/cli/commands/api/annotate/readback.ts +0 -58
  17. package/src/cli/commands/api/annotate/resources.ts +0 -91
  18. package/src/cli/commands/api/annotate/seed-bodies.ts +0 -61
  19. package/src/cli/commands/audit.ts +0 -786
  20. package/src/cli/commands/catalog.ts +0 -97
  21. package/src/cli/commands/check.ts +0 -361
  22. package/src/cli/commands/checks.ts +0 -1072
  23. package/src/cli/commands/ci-init.ts +0 -223
  24. package/src/cli/commands/clean.ts +0 -212
  25. package/src/cli/commands/cleanup.ts +0 -236
  26. package/src/cli/commands/completions.ts +0 -192
  27. package/src/cli/commands/coverage.ts +0 -872
  28. package/src/cli/commands/db.ts +0 -611
  29. package/src/cli/commands/describe.ts +0 -120
  30. package/src/cli/commands/discover.ts +0 -1356
  31. package/src/cli/commands/doctor.ts +0 -661
  32. package/src/cli/commands/fixtures.ts +0 -402
  33. package/src/cli/commands/generate.ts +0 -577
  34. package/src/cli/commands/init/agents-md.ts +0 -61
  35. package/src/cli/commands/init/bootstrap.ts +0 -111
  36. package/src/cli/commands/init/index.ts +0 -244
  37. package/src/cli/commands/init/skills.ts +0 -141
  38. package/src/cli/commands/init/templates/agents.md +0 -86
  39. package/src/cli/commands/init/templates/markdown.d.ts +0 -4
  40. package/src/cli/commands/init/templates/skills/warm-up-target.md +0 -122
  41. package/src/cli/commands/init/templates/skills/zond-checks.md +0 -621
  42. package/src/cli/commands/init/templates/skills/zond-seed.md +0 -114
  43. package/src/cli/commands/init/templates/skills/zond-triage.md +0 -272
  44. package/src/cli/commands/init/templates/skills/zond.md +0 -861
  45. package/src/cli/commands/init/templates/zond-config.yml +0 -14
  46. package/src/cli/commands/prepare-fixtures.ts +0 -97
  47. package/src/cli/commands/probe/_seed-bodies.ts +0 -52
  48. package/src/cli/commands/probe/mass-assignment.ts +0 -594
  49. package/src/cli/commands/probe/security.ts +0 -537
  50. package/src/cli/commands/probe/static.ts +0 -255
  51. package/src/cli/commands/probe/webhooks.ts +0 -163
  52. package/src/cli/commands/probe.ts +0 -535
  53. package/src/cli/commands/reference.ts +0 -87
  54. package/src/cli/commands/refresh-api.ts +0 -227
  55. package/src/cli/commands/remove-api.ts +0 -150
  56. package/src/cli/commands/report-bundle.ts +0 -310
  57. package/src/cli/commands/report.ts +0 -241
  58. package/src/cli/commands/request.ts +0 -548
  59. package/src/cli/commands/run.ts +0 -1162
  60. package/src/cli/commands/schema-from-runs.ts +0 -128
  61. package/src/cli/commands/secrets.ts +0 -133
  62. package/src/cli/commands/session.ts +0 -244
  63. package/src/cli/commands/use.ts +0 -74
  64. package/src/cli/index.ts +0 -55
  65. package/src/cli/json-envelope.ts +0 -108
  66. package/src/cli/json-schemas.ts +0 -314
  67. package/src/cli/output.ts +0 -40
  68. package/src/cli/program.ts +0 -219
  69. package/src/cli/resolve.ts +0 -105
  70. package/src/cli/runtime.ts +0 -7
  71. package/src/cli/safe-live.ts +0 -24
  72. package/src/cli/status-filter.ts +0 -114
  73. package/src/cli/util/api-context.ts +0 -85
  74. package/src/cli/version.ts +0 -8
  75. package/src/core/audit/persist.ts +0 -183
  76. package/src/core/checks/budget.ts +0 -59
  77. package/src/core/checks/checks/_crud-helpers.ts +0 -133
  78. package/src/core/checks/checks/_negative_mutator.ts +0 -133
  79. package/src/core/checks/checks/_readback-helpers.ts +0 -133
  80. package/src/core/checks/checks/content_type_conformance.ts +0 -39
  81. package/src/core/checks/checks/cross_call_references.ts +0 -147
  82. package/src/core/checks/checks/cursor_boundary_fuzzing.ts +0 -219
  83. package/src/core/checks/checks/ensure_resource_availability.ts +0 -62
  84. package/src/core/checks/checks/idempotency_replay.ts +0 -242
  85. package/src/core/checks/checks/ignored_auth.ts +0 -254
  86. package/src/core/checks/checks/index.ts +0 -68
  87. package/src/core/checks/checks/lifecycle_transitions.ts +0 -416
  88. package/src/core/checks/checks/missing_required_header.ts +0 -40
  89. package/src/core/checks/checks/negative_data_rejection.ts +0 -148
  90. package/src/core/checks/checks/not_a_server_error.ts +0 -35
  91. package/src/core/checks/checks/open_cors_on_sensitive.ts +0 -160
  92. package/src/core/checks/checks/pagination_invariants.ts +0 -419
  93. package/src/core/checks/checks/positive_data_acceptance.ts +0 -33
  94. package/src/core/checks/checks/rate_limit_headers_absent.ts +0 -77
  95. package/src/core/checks/checks/response_headers_conformance.ts +0 -74
  96. package/src/core/checks/checks/response_schema_conformance.ts +0 -30
  97. package/src/core/checks/checks/status_code_conformance.ts +0 -132
  98. package/src/core/checks/checks/unsupported_method.ts +0 -63
  99. package/src/core/checks/checks/use_after_free.ts +0 -78
  100. package/src/core/checks/index.ts +0 -30
  101. package/src/core/checks/mode.ts +0 -82
  102. package/src/core/checks/recommended-action.ts +0 -68
  103. package/src/core/checks/registry.ts +0 -78
  104. package/src/core/checks/runner.ts +0 -1461
  105. package/src/core/checks/sarif.ts +0 -230
  106. package/src/core/checks/spec-findings.ts +0 -308
  107. package/src/core/checks/stateful.ts +0 -121
  108. package/src/core/checks/types.ts +0 -305
  109. package/src/core/checks/zond-extensions.ts +0 -73
  110. package/src/core/classifier/recommended-action.ts +0 -251
  111. package/src/core/context/current.ts +0 -51
  112. package/src/core/context/session.ts +0 -78
  113. package/src/core/coverage/loader.ts +0 -216
  114. package/src/core/coverage/reasons.ts +0 -300
  115. package/src/core/diagnostics/db-analysis.ts +0 -666
  116. package/src/core/diagnostics/failure-class.ts +0 -140
  117. package/src/core/diagnostics/failure-hints.ts +0 -112
  118. package/src/core/diagnostics/spec-pointer.ts +0 -99
  119. package/src/core/diagnostics/suggested-fixes.ts +0 -155
  120. package/src/core/exporter/case-study/index.ts +0 -270
  121. package/src/core/exporter/curl.ts +0 -40
  122. package/src/core/exporter/exporter.ts +0 -48
  123. package/src/core/exporter/html-report/escape.ts +0 -24
  124. package/src/core/exporter/html-report/index.ts +0 -479
  125. package/src/core/exporter/html-report/script.ts +0 -100
  126. package/src/core/exporter/html-report/styles.ts +0 -408
  127. package/src/core/generator/catalog-builder.ts +0 -179
  128. package/src/core/generator/chunker.ts +0 -78
  129. package/src/core/generator/coverage-phase.ts +0 -0
  130. package/src/core/generator/coverage-scanner.ts +0 -87
  131. package/src/core/generator/data-factory.ts +0 -759
  132. package/src/core/generator/describe.ts +0 -302
  133. package/src/core/generator/endpoint-warnings.ts +0 -52
  134. package/src/core/generator/fixtures-builder.ts +0 -332
  135. package/src/core/generator/index.ts +0 -14
  136. package/src/core/generator/openapi-reader.ts +0 -297
  137. package/src/core/generator/path-param-disambig.ts +0 -140
  138. package/src/core/generator/resources-builder.ts +0 -898
  139. package/src/core/generator/schema-utils.ts +0 -112
  140. package/src/core/generator/serializer.ts +0 -343
  141. package/src/core/generator/suite-generator.ts +0 -1301
  142. package/src/core/generator/types.ts +0 -63
  143. package/src/core/identity/identity-file.ts +0 -0
  144. package/src/core/lint/affects.ts +0 -28
  145. package/src/core/lint/config.ts +0 -96
  146. package/src/core/lint/format.ts +0 -42
  147. package/src/core/lint/index.ts +0 -94
  148. package/src/core/lint/reporter.ts +0 -128
  149. package/src/core/lint/rules/consistency.ts +0 -158
  150. package/src/core/lint/rules/heuristics.ts +0 -97
  151. package/src/core/lint/rules/strictness.ts +0 -109
  152. package/src/core/lint/types.ts +0 -96
  153. package/src/core/lint/walker.ts +0 -248
  154. package/src/core/meta/meta-store.ts +0 -11
  155. package/src/core/output/README.md +0 -73
  156. package/src/core/output/index.ts +0 -13
  157. package/src/core/output/run.ts +0 -91
  158. package/src/core/output/types.ts +0 -122
  159. package/src/core/parser/dynamic-values.ts +0 -160
  160. package/src/core/parser/env-interpolation.ts +0 -104
  161. package/src/core/parser/filter.ts +0 -97
  162. package/src/core/parser/schema.ts +0 -359
  163. package/src/core/parser/types.ts +0 -117
  164. package/src/core/parser/variables.ts +0 -0
  165. package/src/core/parser/yaml-parser.ts +0 -181
  166. package/src/core/probe/bootstrap.ts +0 -34
  167. package/src/core/probe/dry-run-envelope.ts +0 -61
  168. package/src/core/probe/mass-assignment/classify.ts +0 -175
  169. package/src/core/probe/mass-assignment/cleanup.ts +0 -52
  170. package/src/core/probe/mass-assignment/digest.ts +0 -114
  171. package/src/core/probe/mass-assignment/orchestrator.ts +0 -459
  172. package/src/core/probe/mass-assignment/regression.ts +0 -141
  173. package/src/core/probe/mass-assignment/suspects.ts +0 -92
  174. package/src/core/probe/mass-assignment/types.ts +0 -135
  175. package/src/core/probe/mass-assignment-probe-class.ts +0 -198
  176. package/src/core/probe/mass-assignment-probe.ts +0 -27
  177. package/src/core/probe/mass-assignment-template.ts +0 -240
  178. package/src/core/probe/method-probe.ts +0 -164
  179. package/src/core/probe/method-shared.ts +0 -69
  180. package/src/core/probe/negative-probe.ts +0 -691
  181. package/src/core/probe/orphan-tracker.ts +0 -188
  182. package/src/core/probe/path-discovery.ts +0 -439
  183. package/src/core/probe/probe-harness.ts +0 -119
  184. package/src/core/probe/registry.ts +0 -89
  185. package/src/core/probe/runner.ts +0 -136
  186. package/src/core/probe/security/baseline.ts +0 -174
  187. package/src/core/probe/security/classify.ts +0 -341
  188. package/src/core/probe/security/cleanup.ts +0 -125
  189. package/src/core/probe/security/detectors.ts +0 -71
  190. package/src/core/probe/security/digest.ts +0 -104
  191. package/src/core/probe/security/orchestrator.ts +0 -398
  192. package/src/core/probe/security/regression.ts +0 -103
  193. package/src/core/probe/security/types.ts +0 -151
  194. package/src/core/probe/security-probe-class.ts +0 -207
  195. package/src/core/probe/security-probe.ts +0 -32
  196. package/src/core/probe/shared.ts +0 -531
  197. package/src/core/probe/static-probe-class.ts +0 -125
  198. package/src/core/probe/types.ts +0 -165
  199. package/src/core/probe/verdict-aggregator.ts +0 -33
  200. package/src/core/probe/webhooks-probe.ts +0 -282
  201. package/src/core/reporter/console.ts +0 -240
  202. package/src/core/reporter/index.ts +0 -22
  203. package/src/core/reporter/json.ts +0 -22
  204. package/src/core/reporter/junit.ts +0 -93
  205. package/src/core/reporter/ndjson.ts +0 -37
  206. package/src/core/reporter/types.ts +0 -15
  207. package/src/core/runner/assertions.ts +0 -383
  208. package/src/core/runner/async-pool.ts +0 -108
  209. package/src/core/runner/auth-path.ts +0 -8
  210. package/src/core/runner/ci-context.ts +0 -72
  211. package/src/core/runner/executor.ts +0 -652
  212. package/src/core/runner/expr-eval.ts +0 -41
  213. package/src/core/runner/form-encode.ts +0 -41
  214. package/src/core/runner/http-client.ts +0 -224
  215. package/src/core/runner/learn-drift.ts +0 -293
  216. package/src/core/runner/preflight-vars.ts +0 -153
  217. package/src/core/runner/progress-tracker.ts +0 -73
  218. package/src/core/runner/rate-limiter.ts +0 -185
  219. package/src/core/runner/run-kind.ts +0 -45
  220. package/src/core/runner/schema-validator.ts +0 -308
  221. package/src/core/runner/send-request.ts +0 -232
  222. package/src/core/runner/transforms.ts +0 -65
  223. package/src/core/runner/types.ts +0 -94
  224. package/src/core/secrets/registry.ts +0 -164
  225. package/src/core/secrets/secrets-file.ts +0 -115
  226. package/src/core/selectors/operation-filter.ts +0 -144
  227. package/src/core/setup-api.ts +0 -590
  228. package/src/core/severity/category.ts +0 -94
  229. package/src/core/severity/index.ts +0 -58
  230. package/src/core/spec/infer-schema.ts +0 -102
  231. package/src/core/spec/layers.ts +0 -154
  232. package/src/core/spec/merge-specs.ts +0 -156
  233. package/src/core/spec/schema-from-runs.ts +0 -117
  234. package/src/core/spec/schema-overlay.ts +0 -130
  235. package/src/core/util/ajv.ts +0 -13
  236. package/src/core/util/format-eta.ts +0 -21
  237. package/src/core/util/headers.ts +0 -9
  238. package/src/core/util/url.ts +0 -24
  239. package/src/core/utils.ts +0 -13
  240. package/src/core/workspace/config.ts +0 -129
  241. package/src/core/workspace/fixture-gap-report.ts +0 -84
  242. package/src/core/workspace/fixture-gaps.ts +0 -71
  243. package/src/core/workspace/manifest.ts +0 -283
  244. package/src/core/workspace/output-rotation.ts +0 -62
  245. package/src/core/workspace/root.ts +0 -96
  246. package/src/core/workspace/triage-path.ts +0 -87
  247. package/src/db/lint-runs.ts +0 -47
  248. package/src/db/migrate.ts +0 -128
  249. package/src/db/migrations/0001_run_kind.sql +0 -25
  250. package/src/db/migrations/0002_run_kind_request.sql +0 -59
  251. package/src/db/migrations/sql.d.ts +0 -4
  252. package/src/db/queries/collections.ts +0 -133
  253. package/src/db/queries/coverage.ts +0 -9
  254. package/src/db/queries/dashboard.ts +0 -59
  255. package/src/db/queries/results.ts +0 -216
  256. package/src/db/queries/runs.ts +0 -289
  257. package/src/db/queries/sessions.ts +0 -42
  258. package/src/db/queries/settings.ts +0 -28
  259. package/src/db/queries/types.ts +0 -172
  260. package/src/db/queries.ts +0 -75
  261. package/src/db/schema.ts +0 -298
@@ -1,611 +0,0 @@
1
- import { stringify as stringifyYaml } from "yaml";
2
- import { getCollections, getRuns, getRunDetail, diagnoseRun, compareRuns } from "../../core/diagnostics/db-analysis.ts";
3
- import { getFilteredResults, getLatestFailingRunId, getLatestRunId, runKindStats, deleteRunsOlderThan, listRunsBySession, listRunsByCollectionFiltered, findCollectionByNameOrId } from "../../db/queries.ts";
4
- import { getDb } from "../../db/schema.ts";
5
- import { printError } from "../output.ts";
6
- import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
7
- import { parseStatusFilter, compileStatusFilterToSql, type StatusMatcher } from "../status-filter.ts";
8
- import { parseUnion, type UnionSpec } from "./coverage.ts";
9
- import { readCurrentSession } from "../../core/context/session.ts";
10
- import { readCurrentApi } from "../../core/context/current.ts";
11
-
12
- export interface DbOptions {
13
- subcommand: string;
14
- positional: string[];
15
- limit?: number;
16
- verbose?: boolean;
17
- dbPath?: string;
18
- json?: boolean;
19
- method?: string;
20
- /** Raw `--status` argument (TASK-140). Parsed lazily so help text can show
21
- * the literal user input in error messages. */
22
- status?: string;
23
- /** TASK-266: `db diagnose --latest` — pick the most recent run regardless
24
- * of failure status (default is most recent failing run). */
25
- latest?: boolean;
26
- /** TASK-266: explicit run id override (`--run-id N`). Same effect as
27
- * passing the id positionally; kept as a flag because the `zond-triage`
28
- * skill and other agents prefer self-documenting flags. */
29
- runId?: number;
30
- /** ARV-266: `db prune --older-than <spec>` — cutoff for retention.
31
- * Accepts `30d`, `12h`, or a bare integer (days). Omitted ⇒ per-kind
32
- * documented defaults are applied. */
33
- olderThan?: string;
34
- /** ARV-266: restrict prune to one run_kind. */
35
- kind?: string;
36
- /** ARV-266: show what would be deleted without deleting. */
37
- dryRun?: boolean;
38
- /** ARV-338: `--report yaml` — emit the same payload as YAML instead of
39
- * JSON, so agents can drop it next to suite YAMLs and diff runs as text. */
40
- report?: string;
41
- /** ARV-380: `db diagnose --union <spec>` — aggregate `by_recommended_action`
42
- * across a run set using coverage's `--union` vocabulary
43
- * (session | since:<dur> | tag:<name> | runs:<id1,id2,…>). */
44
- union?: string;
45
- /** ARV-380: API name for scoping since:/tag: unions to a collection. */
46
- api?: string;
47
- }
48
-
49
- /** ARV-338: shared stdout emitter for run/diagnose/compare payloads. */
50
- function emitPayload(payload: unknown, format?: string): void {
51
- console.log(format === "yaml" ? stringifyYaml(payload).trimEnd() : JSON.stringify(payload, null, 2));
52
- }
53
-
54
- /** ARV-266: per-run_kind default retention. Noise kinds (check/probe/
55
- * request/fixture runs) are cheap to regenerate and dominate DB growth —
56
- * drop after a week. `regular` runs are the signal a user compares against
57
- * over time — kept forever unless an explicit --older-than says otherwise. */
58
- const RETENTION_DEFAULT_DAYS: Record<string, number | null> = {
59
- regular: null,
60
- check: 7,
61
- probe: 7,
62
- request: 7,
63
- fixture: 7,
64
- };
65
-
66
- /** ARV-266: parse `30d` / `12h` / `30` into whole days. Returns null on a
67
- * malformed spec so the caller can surface a clear error. */
68
- export function parseRetentionDays(spec: string): number | null {
69
- const m = spec.trim().match(/^(\d+)\s*([dh]?)$/i);
70
- if (!m) return null;
71
- const n = Number.parseInt(m[1]!, 10);
72
- if (!Number.isFinite(n)) return null;
73
- if (m[2]?.toLowerCase() === "h") return n / 24;
74
- return n; // bare number or `d` ⇒ days
75
- }
76
-
77
- /** ARV-266: cutoff ISO timestamp `days` before now. */
78
- function cutoffIso(days: number, now: Date): string {
79
- return new Date(now.getTime() - days * 86_400_000).toISOString();
80
- }
81
-
82
- /** ARV-380: resolve a coverage-style `--union` spec to the concrete run-ids
83
- * it selects. session/since/tag need context (active session / collection);
84
- * `runs:` is self-contained. */
85
- function resolveUnionRunIds(spec: UnionSpec, apiName?: string): { ids: number[] } | { error: string } {
86
- switch (spec.kind) {
87
- case "runIds":
88
- return { ids: spec.ids };
89
- case "session": {
90
- const cur = readCurrentSession();
91
- if (!cur) return { error: "--union session: no active session (start one with `zond session start`, or use --union runs:<ids>)." };
92
- return { ids: listRunsBySession(cur.id).map((r) => (r as { id: number }).id) };
93
- }
94
- case "since":
95
- case "tag": {
96
- // Mirror the standard --api chain: explicit flag > ZOND_API(_GLOBAL) > `zond use`.
97
- const resolvedApi = apiName ?? readCurrentApi();
98
- if (!resolvedApi) return { error: `--union ${spec.kind}:<...> needs --api <name> (or ZOND_API / \`zond use\`) to scope runs to a collection.` };
99
- const col = findCollectionByNameOrId(resolvedApi);
100
- if (!col) return { error: `API '${apiName}' not found. Register it with \`zond add api\` or check the name.` };
101
- const filter = spec.kind === "since"
102
- ? { since: new Date(Date.now() - spec.durationMs).toISOString() }
103
- : { tag: spec.name };
104
- return { ids: listRunsByCollectionFiltered((col as { id: number }).id, filter).map((r) => r.id) };
105
- }
106
- }
107
- }
108
-
109
- /** ARV-380: fold `by_recommended_action` (+ summary counts) across a run set.
110
- * Examples are deduped by method+path+status and capped at 5 per action,
111
- * mirroring the single-run cap in `diagnoseRun`. */
112
- function aggregateDiagnose(ids: number[], options: DbOptions) {
113
- type Example = NonNullable<ReturnType<typeof diagnoseRun>["by_recommended_action"]>[string]["examples"][number];
114
- const byAction: Record<string, { count: number; examples: Example[]; seen: Set<string> }> = {};
115
- const summary = { total: 0, passed: 0, failed: 0, api_errors: 0, assertion_failures: 0, network_errors: 0 };
116
- for (const id of ids) {
117
- const r = diagnoseRun(id, options.verbose, options.dbPath, options.limit);
118
- summary.total += r.summary.total;
119
- summary.passed += r.summary.passed;
120
- summary.failed += r.summary.failed;
121
- summary.api_errors += r.summary.api_errors;
122
- summary.assertion_failures += r.summary.assertion_failures;
123
- summary.network_errors += r.summary.network_errors;
124
- for (const [action, bucket] of Object.entries(r.by_recommended_action ?? {})) {
125
- const agg = byAction[action] ??= { count: 0, examples: [], seen: new Set<string>() };
126
- agg.count += bucket.count;
127
- for (const ex of bucket.examples) {
128
- const key = `${ex.method} ${ex.path} ${ex.status}`;
129
- if (agg.seen.has(key) || agg.examples.length >= 5) continue;
130
- agg.seen.add(key);
131
- agg.examples.push(ex);
132
- }
133
- }
134
- }
135
- const by_recommended_action: Record<string, { count: number; examples: Example[] }> = {};
136
- for (const [action, agg] of Object.entries(byAction)) {
137
- by_recommended_action[action] = { count: agg.count, examples: agg.examples };
138
- }
139
- return { runs_diagnosed: ids.length, run_ids: ids, summary, by_recommended_action };
140
- }
141
-
142
- export async function dbCommand(options: DbOptions): Promise<number> {
143
- const { subcommand, positional, json } = options;
144
-
145
- if (options.report !== undefined && options.report !== "yaml" && options.report !== "json") {
146
- const msg = `Invalid --report format: ${options.report}. Supported: json, yaml`;
147
- if (json) printJson(jsonError(`db ${subcommand}`, [msg]));
148
- else printError(msg);
149
- return 2;
150
- }
151
- if (options.report === "yaml" && json) {
152
- const msg = "--report yaml and --json are mutually exclusive — pick one output format";
153
- printJson(jsonError(`db ${subcommand}`, [msg]));
154
- return 2;
155
- }
156
-
157
- try {
158
- switch (subcommand) {
159
- case "collections": {
160
- const collections = getCollections(options.dbPath);
161
- if (json) {
162
- printJson(jsonOk("db collections", { collections }));
163
- } else {
164
- if (collections.length === 0) {
165
- console.log("No collections found.");
166
- } else {
167
- for (const c of collections) {
168
- console.log(`[${(c as any).id}] ${(c as any).name} — ${(c as any).test_path ?? "no test path"}`);
169
- }
170
- }
171
- }
172
- return 0;
173
- }
174
-
175
- case "runs": {
176
- const runs = getRuns(options.limit ?? 10, options.dbPath);
177
- if (json) {
178
- printJson(jsonOk("db runs", { runs }));
179
- } else {
180
- if (runs.length === 0) {
181
- console.log("No runs found.");
182
- } else {
183
- for (const r of runs) {
184
- const run = r as any;
185
- const isFail = run.failed > 0 || (run.total > 0 && run.passed === 0);
186
- const status = isFail ? "FAIL" : "PASS";
187
- console.log(`#${run.id} ${status} ${run.passed}/${run.total} passed (${run.started_at})`);
188
- }
189
- }
190
- }
191
- return 0;
192
- }
193
-
194
- case "run": {
195
- const id = parseInt(positional[0] ?? "", 10);
196
- if (isNaN(id)) {
197
- const msg = "Missing run ID. Usage: zond db run <id>";
198
- if (json) printJson(jsonError("db run", [msg]));
199
- else printError(msg);
200
- return 2;
201
- }
202
- // If filtering by method/status, show filtered results instead of full detail
203
- if (options.method || options.status !== undefined) {
204
- getDb(options.dbPath);
205
- let statusSql: { sql: string; params: number[] } | undefined;
206
- if (options.status !== undefined) {
207
- let matcher: StatusMatcher;
208
- try {
209
- matcher = parseStatusFilter(options.status);
210
- } catch (err) {
211
- const msg = `Invalid --status: ${(err as Error).message}`;
212
- if (json) printJson(jsonError("db run", [msg]));
213
- else printError(msg);
214
- return 2;
215
- }
216
- const compiled = compileStatusFilterToSql(matcher, "response_status");
217
- if (compiled) statusSql = compiled;
218
- }
219
- const results = getFilteredResults(id, { method: options.method, statusSql });
220
- if (json) {
221
- printJson(jsonOk("db run", { run_id: id, count: results.length, results }));
222
- } else {
223
- emitPayload({ run_id: id, count: results.length, results }, options.report);
224
- }
225
- } else {
226
- const detail = getRunDetail(id, options.verbose, options.dbPath);
227
- if (json) {
228
- printJson(jsonOk("db run", detail));
229
- } else {
230
- emitPayload(detail, options.report);
231
- }
232
- }
233
- return 0;
234
- }
235
-
236
- case "diagnose": {
237
- getDb(options.dbPath);
238
-
239
- // ARV-380: `--union <spec>` aggregates by_recommended_action across a
240
- // run set (same vocabulary as `zond coverage --union`) instead of the
241
- // single-run default, so an agent stops looping `db diagnose $id` per
242
- // run and merging client-side.
243
- if (options.union !== undefined) {
244
- let spec: UnionSpec;
245
- try {
246
- spec = parseUnion(options.union);
247
- } catch (err) {
248
- const msg = (err as Error).message;
249
- if (json) printJson(jsonError("db diagnose", [msg])); else printError(msg);
250
- return 2;
251
- }
252
- const resolved = resolveUnionRunIds(spec, options.api);
253
- if ("error" in resolved) {
254
- if (json) printJson(jsonError("db diagnose", [resolved.error])); else printError(resolved.error);
255
- return 2;
256
- }
257
- if (resolved.ids.length === 0) {
258
- const msg = `No runs match --union ${options.union}.`;
259
- if (json) printJson(jsonError("db diagnose", [msg])); else printError(msg);
260
- return 1;
261
- }
262
- const agg = aggregateDiagnose(resolved.ids, options);
263
- const payload = { ...agg, union: options.union };
264
- if (json) printJson(jsonOk("db diagnose", payload));
265
- else emitPayload(payload, options.report);
266
- return 0;
267
- }
268
-
269
- // TASK-266: resolve run id with the priority
270
- // explicit positional > --run-id > --latest > last failing run.
271
- // The bare `zond db diagnose` is the agent-friendly default the
272
- // `zond-triage` skill relies on; `--latest` exists for "show me the
273
- // last run, even if it passed".
274
- let id: number | null = null;
275
- let resolution: "explicit" | "run-id-flag" | "latest" | "latest-failing" = "explicit";
276
- const positionalRaw = positional[0];
277
- if (positionalRaw !== undefined && positionalRaw !== "") {
278
- const n = parseInt(positionalRaw, 10);
279
- if (isNaN(n)) {
280
- const msg = `Invalid run id: ${positionalRaw}. Expected a positive integer.`;
281
- if (json) printJson(jsonError("db diagnose", [msg]));
282
- else printError(msg);
283
- return 2;
284
- }
285
- id = n;
286
- } else if (options.runId !== undefined) {
287
- id = options.runId;
288
- resolution = "run-id-flag";
289
- } else if (options.latest) {
290
- id = getLatestRunId();
291
- resolution = "latest";
292
- } else {
293
- id = getLatestFailingRunId();
294
- resolution = "latest-failing";
295
- if (id == null) {
296
- // No failures — fall back to the latest run so the user still
297
- // gets a useful payload, with a "no failures" hint.
298
- const fallback = getLatestRunId();
299
- if (fallback == null) {
300
- const msg = "No runs in the database yet. Try `zond run <suite>` first.";
301
- if (json) printJson(jsonError("db diagnose", [msg]));
302
- else printError(msg);
303
- return 1;
304
- }
305
- const result = diagnoseRun(fallback, options.verbose, options.dbPath, options.limit);
306
- const warning = `No failing runs — diagnosing latest run #${fallback} (all passed).`;
307
- if (json) {
308
- printJson(jsonOk("db diagnose", { ...result, resolution: "latest-no-failures", run_id: fallback }, [warning]));
309
- } else {
310
- process.stderr.write(`zond: ${warning}\n`);
311
- emitPayload({ ...result, resolution: "latest-no-failures", run_id: fallback }, options.report);
312
- }
313
- return 0;
314
- }
315
- }
316
- if (id == null) {
317
- const msg = "Missing run ID. Usage: zond db diagnose [id] (default: last failing run)";
318
- if (json) printJson(jsonError("db diagnose", [msg]));
319
- else printError(msg);
320
- return 2;
321
- }
322
- const result = diagnoseRun(id, options.verbose, options.dbPath, options.limit);
323
- if (json) {
324
- printJson(jsonOk("db diagnose", { ...result, resolution, run_id: id }));
325
- } else {
326
- // ARV-208: in human mode, embed resolution + run_id in the stdout
327
- // JSON so users (and the zond-triage skill) don't need --json just
328
- // to discover which run was auto-selected by `zond db diagnose --api`.
329
- // The stderr hint stays as a quick visual cue when running interactively.
330
- if (resolution !== "explicit") {
331
- const label = resolution === "latest-failing"
332
- ? `last failing run #${id}`
333
- : resolution === "latest"
334
- ? `latest run #${id}`
335
- : `run #${id}`;
336
- process.stderr.write(`zond: diagnosing ${label}\n`);
337
- }
338
- emitPayload({ ...result, resolution, run_id: id }, options.report);
339
- }
340
- return 0;
341
- }
342
-
343
- case "compare": {
344
- const idA = parseInt(positional[0] ?? "", 10);
345
- const idB = parseInt(positional[1] ?? "", 10);
346
- if (isNaN(idA) || isNaN(idB)) {
347
- const msg = "Missing run IDs. Usage: zond db compare <idA> <idB>";
348
- if (json) printJson(jsonError("db compare", [msg]));
349
- else printError(msg);
350
- return 2;
351
- }
352
- // ARV-360: a run compared against itself is always a degenerate green
353
- // (0 regressions / 0 body_changes) and signals nothing. This bit an
354
- // audit pipeline that resolved "latest run id" to the same stale run
355
- // for both A and B — the meaningless diff read as "contract stable".
356
- // Fail loud instead of emitting a fake-passing comparison.
357
- if (idA === idB) {
358
- const msg = `Cannot compare run #${idA} against itself — pass two distinct run ids (a self-compare always reports 0 changes and hides that the ids were resolved wrong).`;
359
- if (json) printJson(jsonError("db compare", [msg]));
360
- else printError(msg);
361
- return 2;
362
- }
363
- const result = compareRuns(idA, idB, options.dbPath);
364
- if (json) {
365
- printJson(jsonOk("db compare", result));
366
- } else {
367
- emitPayload(result, options.report);
368
- }
369
- return 0;
370
- }
371
-
372
- case "stats": {
373
- // ARV-266: per-kind row counts so users see DB growth before it bites.
374
- getDb(options.dbPath);
375
- const stats = runKindStats();
376
- const totalRuns = stats.reduce((a, s) => a + s.runs, 0);
377
- const totalResults = stats.reduce((a, s) => a + s.results, 0);
378
- if (json) {
379
- printJson(jsonOk("db stats", { by_kind: stats, totals: { runs: totalRuns, results: totalResults } }));
380
- } else if (stats.length === 0) {
381
- console.log("No runs in the database yet.");
382
- } else {
383
- console.log(`Row counts by run_kind (${totalRuns} runs, ${totalResults} result rows):`);
384
- for (const s of stats) {
385
- const retention = RETENTION_DEFAULT_DAYS[s.run_kind];
386
- const ret = retention == null ? "kept forever" : `default prune ${retention}d`;
387
- console.log(` ${s.run_kind.padEnd(9)} ${String(s.runs).padStart(6)} runs ${String(s.results).padStart(8)} results (${ret})`);
388
- }
389
- console.log("\nPrune noise kinds with `zond db prune` (per-kind defaults) or `zond db prune --older-than 30d`.");
390
- }
391
- return 0;
392
- }
393
-
394
- case "prune": {
395
- // ARV-266: explicit, opt-in retention — never runs unless the user
396
- // asks. Default (no --older-than) applies the documented per-kind
397
- // cutoffs; `regular` runs are never dropped by the defaults.
398
- getDb(options.dbPath);
399
- const now = new Date();
400
- // Build the (kind, cutoffIso) work-list.
401
- let jobs: Array<{ kind?: string; cutoff: string; label: string }> = [];
402
- if (options.olderThan) {
403
- const days = parseRetentionDays(options.olderThan);
404
- if (days == null) {
405
- const msg = `Invalid --older-than "${options.olderThan}" (expected e.g. 30d, 12h, or a day count).`;
406
- if (json) printJson(jsonError("db prune", [msg])); else printError(msg);
407
- return 2;
408
- }
409
- jobs = [{ kind: options.kind, cutoff: cutoffIso(days, now), label: `${options.kind ?? "all"} < ${options.olderThan}` }];
410
- } else {
411
- // Per-kind defaults. A --kind filter narrows to that one kind.
412
- for (const [kind, retDays] of Object.entries(RETENTION_DEFAULT_DAYS)) {
413
- if (retDays == null) continue; // regular ⇒ forever
414
- if (options.kind && options.kind !== kind) continue;
415
- jobs.push({ kind, cutoff: cutoffIso(retDays, now), label: `${kind} < ${retDays}d` });
416
- }
417
- if (options.kind && RETENTION_DEFAULT_DAYS[options.kind] == null) {
418
- const msg = `run_kind "${options.kind}" is retained forever by default. Pass --older-than to prune it explicitly.`;
419
- if (json) printJson(jsonOk("db prune", { deleted: [], dry_run: options.dryRun === true, note: msg }, [msg]));
420
- else console.log(msg);
421
- return 0;
422
- }
423
- }
424
-
425
- const deleted: Array<{ scope: string; runs: number; results: number }> = [];
426
- for (const jobItem of jobs) {
427
- if (options.dryRun) {
428
- // Count-only: reuse the delete query's SELECT by inspecting stats
429
- // is imprecise, so do a lightweight targeted count.
430
- const { getDb: gd } = await import("../../db/schema.ts");
431
- const dbh = gd();
432
- const kindClause = jobItem.kind ? "AND run_kind = ?" : "";
433
- const params = jobItem.kind ? [jobItem.cutoff, jobItem.kind] : [jobItem.cutoff];
434
- const row = dbh.query(`SELECT COUNT(*) AS runs FROM runs WHERE started_at < ? ${kindClause}`).get(...params) as { runs: number };
435
- deleted.push({ scope: jobItem.label, runs: row.runs, results: 0 });
436
- } else {
437
- const r = deleteRunsOlderThan(jobItem.cutoff, jobItem.kind);
438
- deleted.push({ scope: jobItem.label, runs: r.runs, results: r.results });
439
- }
440
- }
441
- const totalRuns = deleted.reduce((a, d) => a + d.runs, 0);
442
- if (!options.dryRun && totalRuns > 0) {
443
- // Reclaim freed pages after a large delete.
444
- const { getDb: gd } = await import("../../db/schema.ts");
445
- gd().exec("VACUUM");
446
- }
447
- if (json) {
448
- printJson(jsonOk("db prune", { deleted, total_runs: totalRuns, dry_run: options.dryRun === true }));
449
- } else if (totalRuns === 0) {
450
- console.log(options.dryRun ? "Nothing would be pruned." : "Nothing to prune.");
451
- } else {
452
- console.log(`${options.dryRun ? "Would prune" : "Pruned"} ${totalRuns} run(s):`);
453
- for (const d of deleted) {
454
- if (d.runs > 0) console.log(` ${d.scope.padEnd(20)} ${d.runs} run(s)${options.dryRun ? "" : `, ${d.results} result row(s)`}`);
455
- }
456
- if (!options.dryRun) console.log("VACUUM reclaimed freed pages.");
457
- }
458
- return 0;
459
- }
460
-
461
- default: {
462
- const msg = `Unknown db subcommand: ${subcommand}. Available: collections, runs, run, diagnose, compare, stats, prune`;
463
- if (json) printJson(jsonError("db", [msg]));
464
- else printError(msg);
465
- return 2;
466
- }
467
- }
468
- } catch (err) {
469
- const message = err instanceof Error ? err.message : String(err);
470
- if (json) {
471
- printJson(jsonError(`db ${subcommand}`, [message]));
472
- } else {
473
- printError(message);
474
- }
475
- return 2;
476
- }
477
- }
478
-
479
- import type { Command } from "commander";
480
- import { globalJson } from "../resolve.ts";
481
- import { parsePositiveInt } from "../argv.ts";
482
-
483
- export function registerDb(program: Command): void {
484
- const db = program.command("db").description("Query the test database");
485
-
486
- db
487
- .command("collections")
488
- .description("List all API collections")
489
- .option("--db <path>", "Path to SQLite database file")
490
- .action(async (opts, cmd: Command) => {
491
- process.exitCode = await dbCommand({
492
- subcommand: "collections",
493
- positional: [],
494
- dbPath: opts.db,
495
- json: globalJson(cmd),
496
- });
497
- });
498
-
499
- db
500
- .command("runs")
501
- .description("List recent test runs")
502
- .option("--limit <N>", "Maximum number of runs to display", parsePositiveInt("--limit"))
503
- .option("--db <path>", "Path to SQLite database file")
504
- .action(async (opts, cmd: Command) => {
505
- process.exitCode = await dbCommand({
506
- subcommand: "runs",
507
- positional: [],
508
- limit: opts.limit,
509
- dbPath: opts.db,
510
- json: globalJson(cmd),
511
- });
512
- });
513
-
514
- db
515
- .command("run <id>")
516
- .description("Show run details")
517
- .option("--verbose", "Show all results")
518
- .option("--method <method>", "Filter by HTTP method")
519
- .option(
520
- "--status <expr>",
521
- "Filter by HTTP status. Accepts: exact code (502), class (5xx), range (500-599), comparison (>=500, <400), or comma-separated mix (5xx,429).",
522
- )
523
- .option("--report <format>", "Output format: json (default) or yaml (ARV-338: agent-friendly run snapshot)")
524
- .option("--db <path>", "Path to SQLite database file")
525
- .action(async (id: string, opts, cmd: Command) => {
526
- process.exitCode = await dbCommand({
527
- subcommand: "run",
528
- positional: [id],
529
- verbose: opts.verbose === true,
530
- method: opts.method,
531
- status: opts.status,
532
- report: opts.report,
533
- dbPath: opts.db,
534
- json: globalJson(cmd),
535
- });
536
- });
537
-
538
- db
539
- .command("diagnose [id]")
540
- .description("Diagnose run failures. Without [id]: defaults to the most recent failing run (TASK-266); falls back to latest run with a 'no failures' note when nothing has failed.")
541
- .option("--latest", "Diagnose the most recent run regardless of status (TASK-266)")
542
- .option("--run-id <N>", "Explicit run id override (same as positional [id]; preferred form for agents)", parsePositiveInt("--run-id"))
543
- .option("--union <spec>", "ARV-380: aggregate by_recommended_action across a run set instead of one run. Vocabulary matches `zond coverage --union`: session | since:<dur> | tag:<name> | runs:<id1,id2,…>. since:/tag: need --api to scope the collection.")
544
- .option("--api <name>", "API/collection to scope --union since:/tag: against. Falls back to ZOND_API / .zond/current-api.")
545
- .option("--limit <N>", "Examples per failure group", parsePositiveInt("--limit"))
546
- .option("--verbose", "Show all examples (not grouped)")
547
- .option("--report <format>", "Output format: json (default) or yaml (ARV-338: agent-friendly summary)")
548
- .option("--db <path>", "Path to SQLite database file")
549
- .action(async (id: string | undefined, opts, cmd: Command) => {
550
- process.exitCode = await dbCommand({
551
- subcommand: "diagnose",
552
- positional: id !== undefined ? [id] : [],
553
- limit: opts.limit,
554
- verbose: opts.verbose === true,
555
- latest: opts.latest === true,
556
- runId: opts.runId,
557
- union: opts.union,
558
- api: opts.api,
559
- report: opts.report,
560
- dbPath: opts.db,
561
- json: globalJson(cmd),
562
- });
563
- });
564
-
565
- db
566
- .command("compare <idA> <idB>")
567
- .description("Compare two runs")
568
- .option("--report <format>", "Output format: json (default) or yaml (ARV-338: agent-friendly diff)")
569
- .option("--db <path>", "Path to SQLite database file")
570
- .action(async (idA: string, idB: string, opts, cmd: Command) => {
571
- process.exitCode = await dbCommand({
572
- subcommand: "compare",
573
- positional: [idA, idB],
574
- report: opts.report,
575
- dbPath: opts.db,
576
- json: globalJson(cmd),
577
- });
578
- });
579
-
580
- db
581
- .command("stats")
582
- .description("ARV-266: row counts per run_kind + default retention, so DB growth is visible before it bites")
583
- .option("--db <path>", "Path to SQLite database file")
584
- .action(async (opts, cmd: Command) => {
585
- process.exitCode = await dbCommand({
586
- subcommand: "stats",
587
- positional: [],
588
- dbPath: opts.db,
589
- json: globalJson(cmd),
590
- });
591
- });
592
-
593
- db
594
- .command("prune")
595
- .description("ARV-266: delete old runs to bound DB growth. Default (no --older-than) applies per-kind retention: check/probe/request/fixture runs older than 7d are dropped; 'regular' runs are kept forever. VACUUMs after a delete. Opt-in — never runs unless invoked.")
596
- .option("--older-than <spec>", "Uniform cutoff for all kinds (e.g. 30d, 12h, or a day count). Overrides per-kind defaults; prunes 'regular' too.")
597
- .option("--kind <run_kind>", "Restrict to one run_kind: regular | check | probe | request | fixture")
598
- .option("--dry-run", "Report what would be deleted without deleting")
599
- .option("--db <path>", "Path to SQLite database file")
600
- .action(async (opts, cmd: Command) => {
601
- process.exitCode = await dbCommand({
602
- subcommand: "prune",
603
- positional: [],
604
- olderThan: opts.olderThan,
605
- kind: opts.kind,
606
- dryRun: opts.dryRun === true,
607
- dbPath: opts.db,
608
- json: globalJson(cmd),
609
- });
610
- });
611
- }