@kirrosh/zond 0.23.0 → 0.26.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 (158) hide show
  1. package/CHANGELOG.md +176 -1
  2. package/README.md +8 -7
  3. package/package.json +2 -3
  4. package/src/CLAUDE.md +112 -0
  5. package/src/cli/commands/add-api.ts +19 -7
  6. package/src/cli/commands/api/annotate/index.ts +359 -4
  7. package/src/cli/commands/api/annotate/lifecycle.ts +1 -1
  8. package/src/cli/commands/api/annotate/overlay.ts +1 -1
  9. package/src/cli/commands/api/annotate/pagination.ts +10 -6
  10. package/src/cli/commands/api/annotate/prompts.ts +39 -2
  11. package/src/cli/commands/audit.ts +360 -54
  12. package/src/cli/commands/check.ts +15 -2
  13. package/src/cli/commands/checks.ts +352 -36
  14. package/src/cli/commands/cleanup.ts +4 -30
  15. package/src/cli/commands/coverage.ts +275 -57
  16. package/src/cli/commands/db.ts +311 -8
  17. package/src/cli/commands/discover.ts +281 -161
  18. package/src/cli/commands/doctor.ts +57 -3
  19. package/src/cli/commands/fixtures.ts +1 -1
  20. package/src/cli/commands/generate.ts +24 -7
  21. package/src/cli/commands/init/bootstrap.ts +4 -1
  22. package/src/cli/commands/init/index.ts +2 -2
  23. package/src/cli/commands/init/skills.ts +43 -0
  24. package/src/cli/commands/init/templates/agents.md +12 -3
  25. package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
  26. package/src/cli/commands/init/templates/skills/zond-checks.md +268 -44
  27. package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
  28. package/src/cli/commands/init/templates/skills/zond-triage.md +88 -26
  29. package/src/cli/commands/init/templates/skills/zond.md +274 -64
  30. package/src/cli/commands/init/templates/zond-config.yml +1 -1
  31. package/src/cli/commands/prepare-fixtures.ts +14 -52
  32. package/src/cli/commands/probe/_seed-bodies.ts +52 -0
  33. package/src/cli/commands/probe/mass-assignment.ts +101 -10
  34. package/src/cli/commands/probe/security.ts +95 -12
  35. package/src/cli/commands/probe/webhooks.ts +2 -0
  36. package/src/cli/commands/probe.ts +87 -11
  37. package/src/cli/commands/refresh-api.ts +59 -1
  38. package/src/cli/commands/report-bundle.ts +3 -11
  39. package/src/cli/commands/request.ts +116 -0
  40. package/src/cli/commands/run.ts +53 -5
  41. package/src/cli/commands/schema-from-runs.ts +128 -0
  42. package/src/cli/commands/secrets.ts +133 -0
  43. package/src/cli/json-envelope.ts +0 -20
  44. package/src/cli/json-schemas.ts +51 -0
  45. package/src/cli/output.ts +17 -1
  46. package/src/cli/program.ts +5 -4
  47. package/src/cli/safe-live.ts +24 -0
  48. package/src/cli/status-filter.ts +0 -10
  49. package/src/core/audit/persist.ts +183 -0
  50. package/src/core/checks/budget.ts +59 -0
  51. package/src/core/checks/checks/cross_call_references.ts +17 -4
  52. package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
  53. package/src/core/checks/checks/idempotency_replay.ts +1 -5
  54. package/src/core/checks/checks/ignored_auth.ts +44 -1
  55. package/src/core/checks/checks/index.ts +3 -0
  56. package/src/core/checks/checks/lifecycle_transitions.ts +169 -26
  57. package/src/core/checks/checks/negative_data_rejection.ts +119 -16
  58. package/src/core/checks/checks/not_a_server_error.ts +8 -0
  59. package/src/core/checks/checks/open_cors_on_sensitive.ts +47 -18
  60. package/src/core/checks/checks/pagination_invariants.ts +298 -117
  61. package/src/core/checks/checks/positive_data_acceptance.ts +1 -4
  62. package/src/core/checks/checks/status_code_conformance.ts +78 -7
  63. package/src/core/checks/mode.ts +3 -0
  64. package/src/core/checks/recommended-action.ts +5 -1
  65. package/src/core/checks/runner.ts +614 -27
  66. package/src/core/checks/spec-findings.ts +308 -0
  67. package/src/core/checks/types.ts +117 -1
  68. package/src/core/checks/zond-extensions.ts +73 -0
  69. package/src/core/classifier/recommended-action.ts +35 -6
  70. package/src/core/coverage/loader.ts +31 -0
  71. package/src/core/diagnostics/db-analysis.ts +200 -106
  72. package/src/core/diagnostics/failure-class.ts +21 -1
  73. package/src/core/diagnostics/failure-hints.ts +4 -208
  74. package/src/core/diagnostics/suggested-fixes.ts +2 -3
  75. package/src/core/generator/chunker.ts +1 -8
  76. package/src/core/generator/data-factory.ts +199 -61
  77. package/src/core/generator/fixtures-builder.ts +38 -31
  78. package/src/core/generator/index.ts +0 -2
  79. package/src/core/generator/openapi-reader.ts +98 -4
  80. package/src/core/generator/path-param-disambig.ts +30 -4
  81. package/src/core/generator/resources-builder.ts +276 -26
  82. package/src/core/generator/schema-utils.ts +22 -0
  83. package/src/core/generator/suite-generator.ts +168 -15
  84. package/src/core/generator/types.ts +6 -0
  85. package/src/core/identity/identity-file.ts +0 -0
  86. package/src/core/output/README.md +11 -29
  87. package/src/core/output/index.ts +1 -1
  88. package/src/core/output/run.ts +0 -35
  89. package/src/core/output/types.ts +0 -7
  90. package/src/core/parser/dynamic-values.ts +160 -0
  91. package/src/core/parser/variables.ts +0 -0
  92. package/src/core/probe/dry-run-envelope.ts +4 -0
  93. package/src/core/probe/mass-assignment/classify.ts +175 -0
  94. package/src/core/probe/mass-assignment/cleanup.ts +52 -0
  95. package/src/core/probe/mass-assignment/digest.ts +114 -0
  96. package/src/core/probe/mass-assignment/orchestrator.ts +459 -0
  97. package/src/core/probe/mass-assignment/regression.ts +141 -0
  98. package/src/core/probe/mass-assignment/suspects.ts +92 -0
  99. package/src/core/probe/mass-assignment/types.ts +135 -0
  100. package/src/core/probe/mass-assignment-probe.ts +23 -1118
  101. package/src/core/probe/mass-assignment-template.ts +32 -4
  102. package/src/core/probe/path-discovery.ts +3 -4
  103. package/src/core/probe/probe-harness.ts +21 -22
  104. package/src/core/probe/security/baseline.ts +174 -0
  105. package/src/core/probe/security/classify.ts +341 -0
  106. package/src/core/probe/security/cleanup.ts +125 -0
  107. package/src/core/probe/security/detectors.ts +71 -0
  108. package/src/core/probe/security/digest.ts +104 -0
  109. package/src/core/probe/security/orchestrator.ts +398 -0
  110. package/src/core/probe/security/regression.ts +103 -0
  111. package/src/core/probe/security/types.ts +151 -0
  112. package/src/core/probe/security-probe-class.ts +8 -2
  113. package/src/core/probe/security-probe.ts +28 -1449
  114. package/src/core/probe/shared.ts +26 -0
  115. package/src/core/probe/webhooks-probe.ts +5 -7
  116. package/src/core/runner/assertions.ts +1 -1
  117. package/src/core/runner/executor.ts +3 -18
  118. package/src/core/runner/form-encode.ts +8 -18
  119. package/src/core/runner/http-client.ts +38 -1
  120. package/src/core/runner/preflight-vars.ts +19 -15
  121. package/src/core/runner/rate-limiter.ts +11 -29
  122. package/src/core/runner/run-kind.ts +7 -1
  123. package/src/core/runner/schema-validator.ts +2 -6
  124. package/src/core/runner/send-request.ts +11 -6
  125. package/src/core/runner/types.ts +6 -0
  126. package/src/core/setup-api.ts +53 -15
  127. package/src/core/severity/index.ts +0 -63
  128. package/src/core/spec/infer-schema.ts +102 -0
  129. package/src/core/spec/merge-specs.ts +156 -0
  130. package/src/core/spec/schema-from-runs.ts +117 -0
  131. package/src/core/spec/schema-overlay.ts +130 -0
  132. package/src/core/util/ajv.ts +13 -0
  133. package/src/core/util/headers.ts +9 -0
  134. package/src/core/util/url.ts +24 -0
  135. package/src/core/workspace/fixture-gap-report.ts +84 -0
  136. package/src/core/workspace/fixture-gaps.ts +71 -0
  137. package/src/core/workspace/root.ts +13 -11
  138. package/src/db/migrate.ts +2 -0
  139. package/src/db/migrations/0002_run_kind_request.sql +59 -0
  140. package/src/db/queries/collections.ts +2 -2
  141. package/src/db/queries/results.ts +88 -0
  142. package/src/db/queries/runs.ts +56 -2
  143. package/src/db/queries.ts +3 -0
  144. package/src/db/schema.ts +7 -7
  145. package/src/cli/commands/bootstrap.ts +0 -710
  146. package/src/core/anti-fp/bootstrap.ts +0 -34
  147. package/src/core/anti-fp/index.ts +0 -33
  148. package/src/core/anti-fp/registry.ts +0 -44
  149. package/src/core/anti-fp/rules/baseline-echo.ts +0 -74
  150. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +0 -52
  151. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +0 -38
  152. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +0 -35
  153. package/src/core/anti-fp/rules/schemathesis/index.ts +0 -24
  154. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +0 -53
  155. package/src/core/anti-fp/rules/subscription-gated/index.ts +0 -11
  156. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +0 -75
  157. package/src/core/anti-fp/types.ts +0 -68
  158. package/src/core/generator/create-body.ts +0 -89
@@ -0,0 +1,71 @@
1
+ /**
2
+ * ARV-324: `.fixture-gaps.yaml` — per-API sidecar recording operations
3
+ * that `prepare-fixtures`/`discover` already confirmed return a client
4
+ * error (or an empty list) while hunting for fixture values — e.g. an
5
+ * empty/under-provisioned test account.
6
+ *
7
+ * `checks run` runs as a separate process invocation and, without this
8
+ * file, has no way to tell "known fixture gap" apart from "new backend
9
+ * bug" — both would land on the same operation with
10
+ * `recommended_action: report_backend_bug`. Loading this file lets the
11
+ * classifier downgrade to `fix_fixture` instead (see
12
+ * `core/classifier/recommended-action.ts`).
13
+ *
14
+ * Not the manifest (`.api-fixtures.yaml` stays spec-derived/read-only,
15
+ * regenerated only by `refresh-api`) — this is runtime-observed state,
16
+ * so it gets its own file, rewritten wholesale on every
17
+ * discover/prepare-fixtures run so a since-fixed gap doesn't linger.
18
+ */
19
+
20
+ import { join } from "node:path";
21
+ import { writeFile } from "node:fs/promises";
22
+
23
+ export interface FixtureGap {
24
+ method: string;
25
+ path: string;
26
+ resource: string;
27
+ var: string;
28
+ reason: string;
29
+ }
30
+
31
+ const FILENAME = ".fixture-gaps.yaml";
32
+
33
+ export async function readFixtureGaps(apiDir: string): Promise<FixtureGap[]> {
34
+ const file = Bun.file(join(apiDir, FILENAME));
35
+ if (!(await file.exists())) return [];
36
+ let parsed: unknown;
37
+ try {
38
+ parsed = Bun.YAML.parse(await file.text());
39
+ } catch {
40
+ return [];
41
+ }
42
+ if (!parsed || typeof parsed !== "object" || !Array.isArray((parsed as { gaps?: unknown }).gaps)) {
43
+ return [];
44
+ }
45
+ return (parsed as { gaps: FixtureGap[] }).gaps;
46
+ }
47
+
48
+ export async function writeFixtureGaps(apiDir: string, gaps: FixtureGap[]): Promise<void> {
49
+ const path = join(apiDir, FILENAME);
50
+ const header = `# .fixture-gaps.yaml — auto-written by prepare-fixtures/discover (ARV-324)
51
+ #
52
+ # Operations that returned a client error (or an empty list) while zond
53
+ # hunted for a fixture value — a resource that's empty/inaccessible in
54
+ # the target API. Read by \`checks run\` so it doesn't relabel the same
55
+ # known gap as report_backend_bug. Safe to delete — regenerated on the
56
+ # next prepare-fixtures / discover run.
57
+ `;
58
+ const { stringify } = await import("yaml");
59
+ const body = stringify({ gaps }, { lineWidth: 0, defaultStringType: "PLAIN" });
60
+ await writeFile(path, header + "\n" + body, "utf-8");
61
+ }
62
+
63
+ /** Key an operation the same way on both the write side (discover/bootstrap,
64
+ * always GET list-probes) and the read side (`CheckFinding.operation`). */
65
+ export function gapKey(method: string, path: string): string {
66
+ return `${method.toUpperCase()} ${path}`;
67
+ }
68
+
69
+ export function gapIndex(gaps: FixtureGap[]): Set<string> {
70
+ return new Set(gaps.map((g) => gapKey(g.method, g.path)));
71
+ }
@@ -7,11 +7,10 @@ import { homedir } from "node:os";
7
7
  * markers win when more than one is present in the same directory.
8
8
  *
9
9
  * zond.config.yml — explicit project config (T12)
10
- * .zond/ — `zond init --here` subdir convention (T19)
11
- * zond.db flat layout from `zond init`
12
- * apis/ — flat layout (collections directory)
10
+ * .zond/ — runtime state subdir (holds zond.db, current-api, …)
11
+ * apis/ collections directory
13
12
  */
14
- export const WORKSPACE_MARKERS = ["zond.config.yml", ".zond", "zond.db", "apis"] as const;
13
+ export const WORKSPACE_MARKERS = ["zond.config.yml", ".zond", "apis"] as const;
15
14
  export type WorkspaceMarker = (typeof WORKSPACE_MARKERS)[number];
16
15
 
17
16
  export interface WorkspaceInfo {
@@ -29,7 +28,7 @@ function hasMarker(dir: string): WorkspaceMarker | null {
29
28
  for (const m of WORKSPACE_MARKERS) {
30
29
  const p = join(dir, m);
31
30
  if (!existsSync(p)) continue;
32
- // .zond and apis must be directories; zond.config.yml and zond.db must be files
31
+ // .zond and apis must be directories; zond.config.yml must be a file
33
32
  try {
34
33
  const st = statSync(p);
35
34
  if (m === ".zond" || m === "apis") {
@@ -47,13 +46,21 @@ function hasMarker(dir: string): WorkspaceMarker | null {
47
46
  /**
48
47
  * Walk-up from `cwd` (default `process.cwd()`) to the nearest workspace
49
48
  * marker. The walk stops at `os.homedir()` to avoid accidentally picking up
50
- * `~/apis` or `~/zond.db` when the user runs zond from somewhere unrelated.
49
+ * `~/apis` or `~/.zond` when the user runs zond from somewhere unrelated.
51
50
  *
52
51
  * When no marker is found, returns `{ root: cwd, fromFallback: true }` and
53
52
  * prints a one-time stderr warning so the user knows zond is operating in
54
53
  * cwd-mode.
55
54
  */
56
55
  export function findWorkspaceRoot(cwd?: string): WorkspaceInfo {
56
+ // Explicit override for headless/CI/workflow runs: skip walk-up entirely and
57
+ // anchor the workspace at ZOND_WORKSPACE. Lets each run own an isolated root
58
+ // (its own .zond/zond.db, apis/) without depending on cwd or a parent marker.
59
+ const override = process.env.ZOND_WORKSPACE?.trim();
60
+ if (override) {
61
+ return { root: resolve(override), marker: "", fromFallback: false };
62
+ }
63
+
57
64
  const start = resolve(cwd ?? process.cwd());
58
65
  const stop = resolve(homedir());
59
66
 
@@ -83,11 +90,6 @@ export function findWorkspaceRoot(cwd?: string): WorkspaceInfo {
83
90
  return { root: start, marker: "", fromFallback: true };
84
91
  }
85
92
 
86
- /** Resolve `relative` against the workspace root (auto-detected from `cwd`). */
87
- export function resolveWorkspacePath(relative: string, cwd?: string): string {
88
- return resolve(findWorkspaceRoot(cwd).root, relative);
89
- }
90
-
91
93
  /** Test helper: reset the one-shot warning latch. */
92
94
  export function _resetWorkspaceWarning(): void {
93
95
  warned = false;
package/src/db/migrate.ts CHANGED
@@ -32,6 +32,7 @@
32
32
  import type { Database } from "bun:sqlite";
33
33
 
34
34
  import migration_0001_run_kind from "./migrations/0001_run_kind.sql" with { type: "text" };
35
+ import migration_0002_run_kind_request from "./migrations/0002_run_kind_request.sql" with { type: "text" };
35
36
 
36
37
  /** Migration manifest. Each entry is a `{ id, sql }` pair; order in
37
38
  * this array is the apply order, matching the lexical sort that the
@@ -40,6 +41,7 @@ import migration_0001_run_kind from "./migrations/0001_run_kind.sql" with { type
40
41
  * list. The runner reads this constant, not the filesystem. */
41
42
  const MIGRATIONS: ReadonlyArray<{ id: string; sql: string }> = [
42
43
  { id: "0001_run_kind", sql: migration_0001_run_kind },
44
+ { id: "0002_run_kind_request", sql: migration_0002_run_kind_request },
43
45
  ];
44
46
 
45
47
  /** Pre-existing migration ids that were already applied by the legacy
@@ -0,0 +1,59 @@
1
+ -- ARV-265 (m-22): extend `runs.run_kind` CHECK constraint to include
2
+ -- 'request' (zond request in-session) and 'fixture' (prepare-fixtures
3
+ -- cascade list-calls), so audit-coverage can attribute HTTP touches
4
+ -- back to those producers.
5
+ --
6
+ -- The CHECK constraint can't be added with ALTER TABLE in SQLite. Bun's
7
+ -- bun:sqlite also locks `sqlite_master` against direct UPDATEs, so the
8
+ -- usual `PRAGMA writable_schema=1` rewrite is rejected. We use the
9
+ -- canonical SQLite 12-step rebuild: create a parallel table with the
10
+ -- new constraint, copy rows over, drop the old, rename.
11
+ --
12
+ -- The migration runner wraps this in a transaction. FK references from
13
+ -- `results.run_id` to `runs(id)` survive the DROP+RENAME because SQLite
14
+ -- resolves FK targets by name at row-modification time, not at table
15
+ -- definition time — when the new `runs` ends up with the same name the
16
+ -- soft reference is reattached transparently. Indexes are explicitly
17
+ -- recreated post-rename (they don't survive a DROP).
18
+
19
+ CREATE TABLE runs_new (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ started_at TEXT NOT NULL,
22
+ finished_at TEXT,
23
+ total INTEGER NOT NULL DEFAULT 0,
24
+ passed INTEGER NOT NULL DEFAULT 0,
25
+ failed INTEGER NOT NULL DEFAULT 0,
26
+ skipped INTEGER NOT NULL DEFAULT 0,
27
+ trigger TEXT DEFAULT 'manual',
28
+ commit_sha TEXT,
29
+ branch TEXT,
30
+ environment TEXT,
31
+ duration_ms INTEGER,
32
+ collection_id INTEGER REFERENCES collections(id),
33
+ session_id TEXT,
34
+ tags TEXT,
35
+ run_kind TEXT NOT NULL DEFAULT 'regular'
36
+ CHECK (run_kind IN ('regular','probe','check','request','fixture'))
37
+ );
38
+
39
+ INSERT INTO runs_new (
40
+ id, started_at, finished_at, total, passed, failed, skipped, trigger,
41
+ commit_sha, branch, environment, duration_ms, collection_id, session_id,
42
+ tags, run_kind
43
+ )
44
+ SELECT
45
+ id, started_at, finished_at, total, passed, failed, skipped, trigger,
46
+ commit_sha, branch, environment, duration_ms, collection_id, session_id,
47
+ tags,
48
+ CASE
49
+ WHEN run_kind IN ('regular','probe','check','request','fixture') THEN run_kind
50
+ ELSE 'regular'
51
+ END
52
+ FROM runs;
53
+
54
+ DROP TABLE runs;
55
+ ALTER TABLE runs_new RENAME TO runs;
56
+
57
+ CREATE INDEX IF NOT EXISTS idx_runs_started ON runs(started_at DESC);
58
+ CREATE INDEX IF NOT EXISTS idx_runs_collection ON runs(collection_id);
59
+ CREATE INDEX IF NOT EXISTS idx_runs_session ON runs(session_id, started_at DESC);
@@ -29,7 +29,7 @@ export function getCollectionById(id: number): CollectionRecord | null {
29
29
 
30
30
  export function getLatestRunByCollection(
31
31
  collectionId: number,
32
- opts: { runKind?: "regular" | "probe" | "check" | "any" } = {},
32
+ opts: { runKind?: import("../../core/runner/run-kind.ts").RunKind | "any" } = {},
33
33
  ): RunRecord | null {
34
34
  const db = getDb();
35
35
  // ARV-55: 'regular' is the default so coverage skips probe-only runs
@@ -58,7 +58,7 @@ export function getLatestRunByCollection(
58
58
  // ARV-55: normalise run_kind alongside tags so RunRecord stays consistent.
59
59
  const rk = row.run_kind;
60
60
  const run_kind: import("../../core/runner/run-kind.ts").RunKind =
61
- rk === "probe" || rk === "check" ? rk : "regular";
61
+ rk === "probe" || rk === "check" || rk === "request" || rk === "fixture" ? rk : "regular";
62
62
  return { ...(row as unknown as RunRecord), tags, run_kind };
63
63
  }
64
64
 
@@ -91,6 +91,94 @@ export function getResultsByRunId(runId: number): StoredStepResult[] {
91
91
  }));
92
92
  }
93
93
 
94
+ /**
95
+ * Row shape for the fixture-kind POST history matched by
96
+ * `getRecentFixturePosts`'s SQL LIKE pattern (typically the
97
+ * create-endpoint URL with `{var}` path params replaced by `%`).
98
+ */
99
+ export interface LastFixtureAttempt {
100
+ request_method: string;
101
+ request_url: string;
102
+ request_body: string | null;
103
+ response_status: number | null;
104
+ response_body: string | null;
105
+ attempted_at: string;
106
+ }
107
+
108
+ /**
109
+ * ARV-278: return the most recent N fixture-POST attempts (most recent
110
+ * first). Powers `dump --with-last-attempt --history N` so the agent
111
+ * sees the progression of errors as the overlay was iterated — e.g.
112
+ * "first 400 said missing X, after fixing the body the next 400 said
113
+ * resource_missing customer" surfaces the cascade-staleness issue (see
114
+ * ARV-282) one level earlier than a single-snapshot view.
115
+ */
116
+ export function getRecentFixturePosts(
117
+ urlLikePattern: string,
118
+ limit: number,
119
+ ): LastFixtureAttempt[] {
120
+ if (!Number.isFinite(limit) || limit <= 0) return [];
121
+ const db = getDb();
122
+ const rows = db.query(`
123
+ SELECT
124
+ r.request_method AS request_method,
125
+ r.request_url AS request_url,
126
+ r.request_body AS request_body,
127
+ r.response_status AS response_status,
128
+ r.response_body AS response_body,
129
+ ru.started_at AS attempted_at
130
+ FROM results r
131
+ JOIN runs ru ON r.run_id = ru.id
132
+ WHERE ru.run_kind = 'fixture'
133
+ AND r.request_method = 'POST'
134
+ AND r.request_url LIKE ?
135
+ ORDER BY ru.started_at DESC, r.id DESC
136
+ LIMIT ?
137
+ `).all(urlLikePattern, Math.floor(limit)) as LastFixtureAttempt[];
138
+ return rows;
139
+ }
140
+
141
+ /**
142
+ * ARV-330: like `getRecentFixturePosts` but across ALL run kinds
143
+ * (`check`/`probe`/`run`/`request`/`fixture`). A root resource with no
144
+ * seed_body is `skip-no-create` by prepare-fixtures, so it never records
145
+ * a fixture-kind POST — yet a depth-check or probe may have POSTed the
146
+ * same create-path and captured the account-level capability error. The
147
+ * hard-blocked classifier reads this wider source so it can still
148
+ * diagnose the gate. Caller is responsible for filtering auth-probe
149
+ * noise (deliberately-broken-cred 401/403s).
150
+ */
151
+ export function getRecentCreatePosts(
152
+ urlLikePattern: string,
153
+ limit: number,
154
+ excludeLikePattern?: string,
155
+ ): LastFixtureAttempt[] {
156
+ if (!Number.isFinite(limit) || limit <= 0) return [];
157
+ const db = getDb();
158
+ // `excludeLikePattern` filters out child sub-resource POSTs in SQL (e.g.
159
+ // `%/v1/accounts/%` drops `/v1/accounts/{id}/reject`); without it the
160
+ // loose trailing wildcard lets a burst of probe sub-calls monopolize the
161
+ // LIMIT window and starve the real create-path attempts (ARV-330).
162
+ const rows = excludeLikePattern
163
+ ? db.query(`
164
+ SELECT r.request_method AS request_method, r.request_url AS request_url,
165
+ r.request_body AS request_body, r.response_status AS response_status,
166
+ r.response_body AS response_body, ru.started_at AS attempted_at
167
+ FROM results r JOIN runs ru ON r.run_id = ru.id
168
+ WHERE r.request_method = 'POST' AND r.request_url LIKE ? AND r.request_url NOT LIKE ?
169
+ ORDER BY ru.started_at DESC, r.id DESC LIMIT ?
170
+ `).all(urlLikePattern, excludeLikePattern, Math.floor(limit))
171
+ : db.query(`
172
+ SELECT r.request_method AS request_method, r.request_url AS request_url,
173
+ r.request_body AS request_body, r.response_status AS response_status,
174
+ r.response_body AS response_body, ru.started_at AS attempted_at
175
+ FROM results r JOIN runs ru ON r.run_id = ru.id
176
+ WHERE r.request_method = 'POST' AND r.request_url LIKE ?
177
+ ORDER BY ru.started_at DESC, r.id DESC LIMIT ?
178
+ `).all(urlLikePattern, Math.floor(limit));
179
+ return rows as LastFixtureAttempt[];
180
+ }
181
+
94
182
  export function getFilteredResults(
95
183
  runId: number,
96
184
  filters: {
@@ -77,10 +77,10 @@ function decodeTags(raw: unknown): string[] | null {
77
77
  }
78
78
  }
79
79
 
80
- function decodeRunKind(raw: unknown): "regular" | "probe" | "check" {
80
+ function decodeRunKind(raw: unknown): import("../../core/runner/run-kind.ts").RunKind {
81
81
  // Migration v10 backfills legacy rows; this is a belt-and-suspenders
82
82
  // normaliser for any value SQLite returns from `run_kind`.
83
- if (raw === "probe" || raw === "check") return raw;
83
+ if (raw === "probe" || raw === "check" || raw === "request" || raw === "fixture") return raw;
84
84
  return "regular";
85
85
  }
86
86
 
@@ -223,6 +223,60 @@ export function deleteRun(runId: number): boolean {
223
223
  });
224
224
  }
225
225
 
226
+ /** ARV-266: per-run_kind row counts so `zond db stats` can surface DB
227
+ * growth. `results` counts the child rows that would be reclaimed. */
228
+ export interface RunKindStat {
229
+ run_kind: string;
230
+ runs: number;
231
+ results: number;
232
+ oldest?: string;
233
+ newest?: string;
234
+ }
235
+
236
+ export function runKindStats(): RunKindStat[] {
237
+ const db = getDb();
238
+ const rows = db.query(`
239
+ SELECT r.run_kind AS run_kind,
240
+ COUNT(DISTINCT r.id) AS runs,
241
+ COUNT(res.id) AS results,
242
+ MIN(r.started_at) AS oldest,
243
+ MAX(r.started_at) AS newest
244
+ FROM runs r
245
+ LEFT JOIN results res ON res.run_id = r.id
246
+ GROUP BY r.run_kind
247
+ ORDER BY runs DESC
248
+ `).all() as Array<{ run_kind: string; runs: number; results: number; oldest: string | null; newest: string | null }>;
249
+ return rows.map((r) => ({
250
+ run_kind: r.run_kind,
251
+ runs: r.runs,
252
+ results: r.results,
253
+ ...(r.oldest ? { oldest: r.oldest } : {}),
254
+ ...(r.newest ? { newest: r.newest } : {}),
255
+ }));
256
+ }
257
+
258
+ /** ARV-266: delete runs (and their results) started strictly before
259
+ * `cutoffIso`. When `kind` is given, only that run_kind is pruned.
260
+ * Returns the number of runs + result rows removed. Caller VACUUMs. */
261
+ export function deleteRunsOlderThan(
262
+ cutoffIso: string,
263
+ kind?: string,
264
+ ): { runs: number; results: number } {
265
+ const db = getDb();
266
+ return withDbRetry("deleteRunsOlderThan", () => {
267
+ const kindClause = kind ? "AND run_kind = ?" : "";
268
+ const selParams = kind ? [cutoffIso, kind] : [cutoffIso];
269
+ const ids = (db.query(
270
+ `SELECT id FROM runs WHERE started_at < ? ${kindClause}`,
271
+ ).all(...selParams) as Array<{ id: number }>).map((r) => r.id);
272
+ if (ids.length === 0) return { runs: 0, results: 0 };
273
+ const placeholders = ids.map(() => "?").join(",");
274
+ const res = db.prepare(`DELETE FROM results WHERE run_id IN (${placeholders})`).run(...ids);
275
+ db.prepare(`DELETE FROM runs WHERE id IN (${placeholders})`).run(...ids);
276
+ return { runs: ids.length, results: res.changes };
277
+ });
278
+ }
279
+
226
280
  export function countRuns(filters?: RunFilters): number {
227
281
  const db = getDb();
228
282
  if (filters && Object.values(filters).some(Boolean)) {
package/src/db/queries.ts CHANGED
@@ -43,6 +43,9 @@ export {
43
43
  listRunsByCollectionFiltered,
44
44
  getLatestFailingRunId,
45
45
  getLatestRunId,
46
+ runKindStats,
47
+ deleteRunsOlderThan,
48
+ type RunKindStat,
46
49
  } from "./queries/runs.ts";
47
50
 
48
51
  export { listSessions, countSessions, listRunsBySession } from "./queries/sessions.ts";
package/src/db/schema.ts CHANGED
@@ -9,14 +9,11 @@ let _dbPath: string | null = null;
9
9
 
10
10
  /**
11
11
  * Default DB path lives under `<workspace>/.zond/zond.db` to keep runtime
12
- * artifacts out of the project root. For back-compat we still recognise a
13
- * legacy `<workspace>/zond.db` if it exists old workspaces keep working
14
- * without migration.
12
+ * artifacts out of the project root. (The flat `<workspace>/zond.db` layout
13
+ * was dropped in m-25pass an explicit `--db` to target a legacy file.)
15
14
  */
16
15
  function defaultDbPath(): string {
17
16
  const root = findWorkspaceRoot().root;
18
- const legacy = resolve(root, "zond.db");
19
- if (existsSync(legacy)) return legacy;
20
17
  return resolve(root, ".zond", "zond.db");
21
18
  }
22
19
 
@@ -133,8 +130,11 @@ const SCHEMA = `
133
130
  tags TEXT,
134
131
  -- ARV-55: classify a run once at INSERT time so coverage / diagnose
135
132
  -- queries don't have to re-derive "is this a probe-only run?" from
136
- -- the results' suite_file paths.
137
- run_kind TEXT NOT NULL DEFAULT 'regular' CHECK (run_kind IN ('regular','probe','check'))
133
+ -- the results' suite_file paths. ARV-265 extends the CHECK list with
134
+ -- 'request' (zond request in-session) and 'fixture' (prepare-fixtures
135
+ -- cascade list-calls) so audit-coverage can attribute HTTP touches
136
+ -- back to the producer.
137
+ run_kind TEXT NOT NULL DEFAULT 'regular' CHECK (run_kind IN ('regular','probe','check','request','fixture'))
138
138
  );
139
139
 
140
140
  CREATE TABLE IF NOT EXISTS results (