@kirrosh/zond 0.23.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +164 -1
- package/README.md +8 -7
- package/package.json +2 -3
- package/src/CLAUDE.md +112 -0
- package/src/cli/commands/add-api.ts +19 -7
- package/src/cli/commands/api/annotate/index.ts +359 -4
- package/src/cli/commands/api/annotate/lifecycle.ts +1 -1
- package/src/cli/commands/api/annotate/overlay.ts +1 -1
- package/src/cli/commands/api/annotate/pagination.ts +10 -6
- package/src/cli/commands/api/annotate/prompts.ts +39 -2
- package/src/cli/commands/audit.ts +360 -54
- package/src/cli/commands/check.ts +15 -2
- package/src/cli/commands/checks.ts +352 -36
- package/src/cli/commands/cleanup.ts +4 -30
- package/src/cli/commands/coverage.ts +275 -57
- package/src/cli/commands/db.ts +311 -8
- package/src/cli/commands/discover.ts +281 -161
- package/src/cli/commands/doctor.ts +57 -3
- package/src/cli/commands/fixtures.ts +1 -1
- package/src/cli/commands/generate.ts +24 -7
- package/src/cli/commands/init/bootstrap.ts +4 -1
- package/src/cli/commands/init/index.ts +2 -2
- package/src/cli/commands/init/skills.ts +43 -0
- package/src/cli/commands/init/templates/agents.md +12 -3
- package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +268 -44
- package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +88 -26
- package/src/cli/commands/init/templates/skills/zond.md +274 -64
- package/src/cli/commands/init/templates/zond-config.yml +1 -1
- package/src/cli/commands/prepare-fixtures.ts +14 -52
- package/src/cli/commands/probe/_seed-bodies.ts +52 -0
- package/src/cli/commands/probe/mass-assignment.ts +101 -10
- package/src/cli/commands/probe/security.ts +95 -12
- package/src/cli/commands/probe/webhooks.ts +2 -0
- package/src/cli/commands/probe.ts +87 -11
- package/src/cli/commands/refresh-api.ts +59 -1
- package/src/cli/commands/report-bundle.ts +3 -11
- package/src/cli/commands/request.ts +116 -0
- package/src/cli/commands/run.ts +33 -5
- package/src/cli/commands/schema-from-runs.ts +128 -0
- package/src/cli/commands/secrets.ts +133 -0
- package/src/cli/json-envelope.ts +0 -20
- package/src/cli/json-schemas.ts +51 -0
- package/src/cli/output.ts +17 -1
- package/src/cli/program.ts +5 -4
- package/src/cli/safe-live.ts +24 -0
- package/src/cli/status-filter.ts +0 -10
- package/src/core/audit/persist.ts +183 -0
- package/src/core/checks/budget.ts +59 -0
- package/src/core/checks/checks/cross_call_references.ts +17 -4
- package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
- package/src/core/checks/checks/idempotency_replay.ts +1 -5
- package/src/core/checks/checks/ignored_auth.ts +44 -1
- package/src/core/checks/checks/index.ts +3 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +169 -26
- package/src/core/checks/checks/negative_data_rejection.ts +119 -16
- package/src/core/checks/checks/not_a_server_error.ts +8 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +47 -18
- package/src/core/checks/checks/pagination_invariants.ts +298 -117
- package/src/core/checks/checks/positive_data_acceptance.ts +1 -4
- package/src/core/checks/checks/status_code_conformance.ts +78 -7
- package/src/core/checks/mode.ts +3 -0
- package/src/core/checks/recommended-action.ts +5 -1
- package/src/core/checks/runner.ts +614 -27
- package/src/core/checks/spec-findings.ts +308 -0
- package/src/core/checks/types.ts +117 -1
- package/src/core/checks/zond-extensions.ts +73 -0
- package/src/core/classifier/recommended-action.ts +35 -6
- package/src/core/coverage/loader.ts +31 -0
- package/src/core/diagnostics/db-analysis.ts +200 -106
- package/src/core/diagnostics/failure-class.ts +21 -1
- package/src/core/diagnostics/failure-hints.ts +4 -208
- package/src/core/diagnostics/suggested-fixes.ts +2 -3
- package/src/core/generator/chunker.ts +1 -8
- package/src/core/generator/data-factory.ts +199 -61
- package/src/core/generator/fixtures-builder.ts +38 -31
- package/src/core/generator/index.ts +0 -2
- package/src/core/generator/openapi-reader.ts +98 -4
- package/src/core/generator/path-param-disambig.ts +30 -4
- package/src/core/generator/resources-builder.ts +276 -26
- package/src/core/generator/schema-utils.ts +22 -0
- package/src/core/generator/suite-generator.ts +168 -15
- package/src/core/generator/types.ts +6 -0
- package/src/core/identity/identity-file.ts +0 -0
- package/src/core/output/README.md +11 -29
- package/src/core/output/index.ts +1 -1
- package/src/core/output/run.ts +0 -35
- package/src/core/output/types.ts +0 -7
- package/src/core/parser/dynamic-values.ts +160 -0
- package/src/core/parser/variables.ts +0 -0
- package/src/core/probe/dry-run-envelope.ts +4 -0
- package/src/core/probe/mass-assignment/classify.ts +175 -0
- package/src/core/probe/mass-assignment/cleanup.ts +52 -0
- package/src/core/probe/mass-assignment/digest.ts +114 -0
- package/src/core/probe/mass-assignment/orchestrator.ts +459 -0
- package/src/core/probe/mass-assignment/regression.ts +141 -0
- package/src/core/probe/mass-assignment/suspects.ts +92 -0
- package/src/core/probe/mass-assignment/types.ts +135 -0
- package/src/core/probe/mass-assignment-probe.ts +23 -1118
- package/src/core/probe/mass-assignment-template.ts +32 -4
- package/src/core/probe/path-discovery.ts +3 -4
- package/src/core/probe/probe-harness.ts +21 -22
- package/src/core/probe/security/baseline.ts +174 -0
- package/src/core/probe/security/classify.ts +341 -0
- package/src/core/probe/security/cleanup.ts +125 -0
- package/src/core/probe/security/detectors.ts +71 -0
- package/src/core/probe/security/digest.ts +104 -0
- package/src/core/probe/security/orchestrator.ts +398 -0
- package/src/core/probe/security/regression.ts +103 -0
- package/src/core/probe/security/types.ts +151 -0
- package/src/core/probe/security-probe-class.ts +8 -2
- package/src/core/probe/security-probe.ts +28 -1449
- package/src/core/probe/shared.ts +26 -0
- package/src/core/probe/webhooks-probe.ts +5 -7
- package/src/core/runner/assertions.ts +1 -1
- package/src/core/runner/executor.ts +3 -18
- package/src/core/runner/form-encode.ts +8 -18
- package/src/core/runner/http-client.ts +38 -1
- package/src/core/runner/preflight-vars.ts +19 -15
- package/src/core/runner/rate-limiter.ts +11 -29
- package/src/core/runner/run-kind.ts +7 -1
- package/src/core/runner/schema-validator.ts +2 -6
- package/src/core/runner/send-request.ts +11 -6
- package/src/core/runner/types.ts +6 -0
- package/src/core/setup-api.ts +53 -15
- package/src/core/severity/index.ts +0 -63
- package/src/core/spec/infer-schema.ts +102 -0
- package/src/core/spec/merge-specs.ts +156 -0
- package/src/core/spec/schema-from-runs.ts +117 -0
- package/src/core/spec/schema-overlay.ts +130 -0
- package/src/core/util/ajv.ts +13 -0
- package/src/core/util/headers.ts +9 -0
- package/src/core/util/url.ts +24 -0
- package/src/core/workspace/fixture-gap-report.ts +84 -0
- package/src/core/workspace/fixture-gaps.ts +71 -0
- package/src/core/workspace/root.ts +13 -11
- package/src/db/migrate.ts +2 -0
- package/src/db/migrations/0002_run_kind_request.sql +59 -0
- package/src/db/queries/collections.ts +2 -2
- package/src/db/queries/results.ts +88 -0
- package/src/db/queries/runs.ts +56 -2
- package/src/db/queries.ts +3 -0
- package/src/db/schema.ts +7 -7
- package/src/cli/commands/bootstrap.ts +0 -710
- package/src/core/anti-fp/bootstrap.ts +0 -34
- package/src/core/anti-fp/index.ts +0 -33
- package/src/core/anti-fp/registry.ts +0 -44
- package/src/core/anti-fp/rules/baseline-echo.ts +0 -74
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +0 -52
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +0 -38
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +0 -35
- package/src/core/anti-fp/rules/schemathesis/index.ts +0 -24
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +0 -53
- package/src/core/anti-fp/rules/subscription-gated/index.ts +0 -11
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +0 -75
- package/src/core/anti-fp/types.ts +0 -68
- package/src/core/generator/create-body.ts +0 -89
package/src/cli/commands/db.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
1
2
|
import { getCollections, getRuns, getRunDetail, diagnoseRun, compareRuns } from "../../core/diagnostics/db-analysis.ts";
|
|
2
|
-
import { getFilteredResults, getLatestFailingRunId, getLatestRunId } from "../../db/queries.ts";
|
|
3
|
+
import { getFilteredResults, getLatestFailingRunId, getLatestRunId, runKindStats, deleteRunsOlderThan, listRunsBySession, listRunsByCollectionFiltered, findCollectionByNameOrId } from "../../db/queries.ts";
|
|
3
4
|
import { getDb } from "../../db/schema.ts";
|
|
4
5
|
import { printError } from "../output.ts";
|
|
5
6
|
import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
|
|
6
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";
|
|
7
11
|
|
|
8
12
|
export interface DbOptions {
|
|
9
13
|
subcommand: string;
|
|
@@ -23,11 +27,133 @@ export interface DbOptions {
|
|
|
23
27
|
* passing the id positionally; kept as a flag because the `zond-triage`
|
|
24
28
|
* skill and other agents prefer self-documenting flags. */
|
|
25
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 };
|
|
26
140
|
}
|
|
27
141
|
|
|
28
142
|
export async function dbCommand(options: DbOptions): Promise<number> {
|
|
29
143
|
const { subcommand, positional, json } = options;
|
|
30
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
|
+
|
|
31
157
|
try {
|
|
32
158
|
switch (subcommand) {
|
|
33
159
|
case "collections": {
|
|
@@ -94,26 +220,57 @@ export async function dbCommand(options: DbOptions): Promise<number> {
|
|
|
94
220
|
if (json) {
|
|
95
221
|
printJson(jsonOk("db run", { run_id: id, count: results.length, results }));
|
|
96
222
|
} else {
|
|
97
|
-
|
|
223
|
+
emitPayload({ run_id: id, count: results.length, results }, options.report);
|
|
98
224
|
}
|
|
99
225
|
} else {
|
|
100
226
|
const detail = getRunDetail(id, options.verbose, options.dbPath);
|
|
101
227
|
if (json) {
|
|
102
228
|
printJson(jsonOk("db run", detail));
|
|
103
229
|
} else {
|
|
104
|
-
|
|
230
|
+
emitPayload(detail, options.report);
|
|
105
231
|
}
|
|
106
232
|
}
|
|
107
233
|
return 0;
|
|
108
234
|
}
|
|
109
235
|
|
|
110
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
|
+
|
|
111
269
|
// TASK-266: resolve run id with the priority
|
|
112
270
|
// explicit positional > --run-id > --latest > last failing run.
|
|
113
271
|
// The bare `zond db diagnose` is the agent-friendly default the
|
|
114
272
|
// `zond-triage` skill relies on; `--latest` exists for "show me the
|
|
115
273
|
// last run, even if it passed".
|
|
116
|
-
getDb(options.dbPath);
|
|
117
274
|
let id: number | null = null;
|
|
118
275
|
let resolution: "explicit" | "run-id-flag" | "latest" | "latest-failing" = "explicit";
|
|
119
276
|
const positionalRaw = positional[0];
|
|
@@ -151,7 +308,7 @@ export async function dbCommand(options: DbOptions): Promise<number> {
|
|
|
151
308
|
printJson(jsonOk("db diagnose", { ...result, resolution: "latest-no-failures", run_id: fallback }, [warning]));
|
|
152
309
|
} else {
|
|
153
310
|
process.stderr.write(`zond: ${warning}\n`);
|
|
154
|
-
|
|
311
|
+
emitPayload({ ...result, resolution: "latest-no-failures", run_id: fallback }, options.report);
|
|
155
312
|
}
|
|
156
313
|
return 0;
|
|
157
314
|
}
|
|
@@ -166,6 +323,10 @@ export async function dbCommand(options: DbOptions): Promise<number> {
|
|
|
166
323
|
if (json) {
|
|
167
324
|
printJson(jsonOk("db diagnose", { ...result, resolution, run_id: id }));
|
|
168
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.
|
|
169
330
|
if (resolution !== "explicit") {
|
|
170
331
|
const label = resolution === "latest-failing"
|
|
171
332
|
? `last failing run #${id}`
|
|
@@ -174,7 +335,7 @@ export async function dbCommand(options: DbOptions): Promise<number> {
|
|
|
174
335
|
: `run #${id}`;
|
|
175
336
|
process.stderr.write(`zond: diagnosing ${label}\n`);
|
|
176
337
|
}
|
|
177
|
-
|
|
338
|
+
emitPayload({ ...result, resolution, run_id: id }, options.report);
|
|
178
339
|
}
|
|
179
340
|
return 0;
|
|
180
341
|
}
|
|
@@ -188,17 +349,117 @@ export async function dbCommand(options: DbOptions): Promise<number> {
|
|
|
188
349
|
else printError(msg);
|
|
189
350
|
return 2;
|
|
190
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
|
+
}
|
|
191
363
|
const result = compareRuns(idA, idB, options.dbPath);
|
|
192
364
|
if (json) {
|
|
193
365
|
printJson(jsonOk("db compare", result));
|
|
194
366
|
} else {
|
|
195
|
-
|
|
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.");
|
|
196
457
|
}
|
|
197
458
|
return 0;
|
|
198
459
|
}
|
|
199
460
|
|
|
200
461
|
default: {
|
|
201
|
-
const msg = `Unknown db subcommand: ${subcommand}. Available: collections, runs, run, diagnose, compare`;
|
|
462
|
+
const msg = `Unknown db subcommand: ${subcommand}. Available: collections, runs, run, diagnose, compare, stats, prune`;
|
|
202
463
|
if (json) printJson(jsonError("db", [msg]));
|
|
203
464
|
else printError(msg);
|
|
204
465
|
return 2;
|
|
@@ -259,6 +520,7 @@ export function registerDb(program: Command): void {
|
|
|
259
520
|
"--status <expr>",
|
|
260
521
|
"Filter by HTTP status. Accepts: exact code (502), class (5xx), range (500-599), comparison (>=500, <400), or comma-separated mix (5xx,429).",
|
|
261
522
|
)
|
|
523
|
+
.option("--report <format>", "Output format: json (default) or yaml (ARV-338: agent-friendly run snapshot)")
|
|
262
524
|
.option("--db <path>", "Path to SQLite database file")
|
|
263
525
|
.action(async (id: string, opts, cmd: Command) => {
|
|
264
526
|
process.exitCode = await dbCommand({
|
|
@@ -267,6 +529,7 @@ export function registerDb(program: Command): void {
|
|
|
267
529
|
verbose: opts.verbose === true,
|
|
268
530
|
method: opts.method,
|
|
269
531
|
status: opts.status,
|
|
532
|
+
report: opts.report,
|
|
270
533
|
dbPath: opts.db,
|
|
271
534
|
json: globalJson(cmd),
|
|
272
535
|
});
|
|
@@ -277,8 +540,11 @@ export function registerDb(program: Command): void {
|
|
|
277
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.")
|
|
278
541
|
.option("--latest", "Diagnose the most recent run regardless of status (TASK-266)")
|
|
279
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.")
|
|
280
545
|
.option("--limit <N>", "Examples per failure group", parsePositiveInt("--limit"))
|
|
281
546
|
.option("--verbose", "Show all examples (not grouped)")
|
|
547
|
+
.option("--report <format>", "Output format: json (default) or yaml (ARV-338: agent-friendly summary)")
|
|
282
548
|
.option("--db <path>", "Path to SQLite database file")
|
|
283
549
|
.action(async (id: string | undefined, opts, cmd: Command) => {
|
|
284
550
|
process.exitCode = await dbCommand({
|
|
@@ -288,6 +554,9 @@ export function registerDb(program: Command): void {
|
|
|
288
554
|
verbose: opts.verbose === true,
|
|
289
555
|
latest: opts.latest === true,
|
|
290
556
|
runId: opts.runId,
|
|
557
|
+
union: opts.union,
|
|
558
|
+
api: opts.api,
|
|
559
|
+
report: opts.report,
|
|
291
560
|
dbPath: opts.db,
|
|
292
561
|
json: globalJson(cmd),
|
|
293
562
|
});
|
|
@@ -296,11 +565,45 @@ export function registerDb(program: Command): void {
|
|
|
296
565
|
db
|
|
297
566
|
.command("compare <idA> <idB>")
|
|
298
567
|
.description("Compare two runs")
|
|
568
|
+
.option("--report <format>", "Output format: json (default) or yaml (ARV-338: agent-friendly diff)")
|
|
299
569
|
.option("--db <path>", "Path to SQLite database file")
|
|
300
570
|
.action(async (idA: string, idB: string, opts, cmd: Command) => {
|
|
301
571
|
process.exitCode = await dbCommand({
|
|
302
572
|
subcommand: "compare",
|
|
303
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,
|
|
304
607
|
dbPath: opts.db,
|
|
305
608
|
json: globalJson(cmd),
|
|
306
609
|
});
|