@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.
- package/CHANGELOG.md +176 -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 +53 -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
|
@@ -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/ —
|
|
11
|
-
*
|
|
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", "
|
|
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
|
|
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
|
|
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?: "
|
|
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: {
|
package/src/db/queries/runs.ts
CHANGED
|
@@ -77,10 +77,10 @@ function decodeTags(raw: unknown): string[] | null {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function decodeRunKind(raw: unknown): "
|
|
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.
|
|
13
|
-
*
|
|
14
|
-
* without migration.
|
|
12
|
+
* artifacts out of the project root. (The flat `<workspace>/zond.db` layout
|
|
13
|
+
* was dropped in m-25 — pass 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
|
-
|
|
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 (
|