@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
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-60: spec-level rollup of systemic gaps.
|
|
3
|
+
*
|
|
4
|
+
* Many findings are a single spec-level fact (no 401 declared anywhere,
|
|
5
|
+
* no response schemas, no DELETE+GET pair detectable) smeared across N
|
|
6
|
+
* operations. The flat finding list reads as "83 problems" when it's
|
|
7
|
+
* really "1 problem × 83 sites". Small teams reading the report can't
|
|
8
|
+
* tell whether to act on the spec or on the server — and the actionable
|
|
9
|
+
* line tends to be the same for every row.
|
|
10
|
+
*
|
|
11
|
+
* This module computes `SpecFinding[]` from the runner's primary outputs:
|
|
12
|
+
*
|
|
13
|
+
* 1. **status_drift** — group existing findings by (check, status); if
|
|
14
|
+
* a group covers ≥80% of the check's applicable operations, emit
|
|
15
|
+
* one rollup row. Per-op findings stay in `data.findings` (so SARIF
|
|
16
|
+
* and `--verbose` keep the long form).
|
|
17
|
+
* 2. **missing_declaration** — a single skipped_outcome reason covers
|
|
18
|
+
* ≥80% of the check's applicable cases. Typical: response schema /
|
|
19
|
+
* header schema not declared on this API.
|
|
20
|
+
* 3. **no_detector** — check is applicable to ≥5 operations but ran
|
|
21
|
+
* zero cases. Typical: `use_after_free` without DELETE+GET pair.
|
|
22
|
+
* Different from skip — the check itself produced no cases.
|
|
23
|
+
*
|
|
24
|
+
* Threshold is hard-coded at 0.8 (AC #1). Lower would create false
|
|
25
|
+
* rollups on small (<10-op) APIs where a 3/4 incidental cluster doesn't
|
|
26
|
+
* indicate a systemic gap.
|
|
27
|
+
*/
|
|
28
|
+
import type {
|
|
29
|
+
CheckFinding,
|
|
30
|
+
SpecFinding,
|
|
31
|
+
} from "./types.ts";
|
|
32
|
+
import { categoryFor } from "../severity/category.ts";
|
|
33
|
+
|
|
34
|
+
export interface PerCheckObservations {
|
|
35
|
+
/** Distinct operations where `check.applies(op) === true`. */
|
|
36
|
+
applicable: number;
|
|
37
|
+
/** Count of cases the check actually ran (passed + failed + skipped). */
|
|
38
|
+
cases: number;
|
|
39
|
+
/** ARV-26-style "check: reason" → count, restricted to this check. */
|
|
40
|
+
skipped: Record<string, number>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SPEC_CLUSTER_RATIO = 0.8;
|
|
44
|
+
const NO_DETECTOR_FLOOR = 5;
|
|
45
|
+
|
|
46
|
+
/** Mapping table: check id + response status → human reason + actionable
|
|
47
|
+
* fix hint. Centralised so a future check that opts into rollup can just
|
|
48
|
+
* register its hint here. */
|
|
49
|
+
function explainStatusDrift(checkId: string, status: number): { reason: string; fix: string } {
|
|
50
|
+
if (checkId === "status_code_conformance") {
|
|
51
|
+
return {
|
|
52
|
+
reason: `Status ${status} not declared in spec`,
|
|
53
|
+
fix: `Add ${status} to the response declarations for the affected operations, or pass --tolerate-undeclared ${status}.`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (checkId === "ignored_auth") {
|
|
57
|
+
return {
|
|
58
|
+
reason: `Auth probes did not produce ${status >= 400 ? "the expected rejection" : "a 4xx"} (got ${status})`,
|
|
59
|
+
fix: `Verify the security scheme is enforced server-side, or relax with --strict-401=false.`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (checkId === "negative_data_rejection") {
|
|
63
|
+
return {
|
|
64
|
+
reason: `Negative payloads accepted with ${status} on most operations`,
|
|
65
|
+
fix: `Server is not validating inputs — fix request-body validation, or downgrade by adjusting tolerated statuses in your gate.`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (checkId === "unsupported_method") {
|
|
69
|
+
return {
|
|
70
|
+
reason: `Undeclared methods returned ${status} instead of 405`,
|
|
71
|
+
fix: `Configure the gateway to emit 405 for undeclared verbs, or pass --strict-405=false.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (checkId === "missing_required_header") {
|
|
75
|
+
return {
|
|
76
|
+
reason: `Required-header omission returned ${status}`,
|
|
77
|
+
fix: `Server should reject with 400/415 when required headers are missing.`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Generic fallback — better than "(unknown reason)".
|
|
81
|
+
return {
|
|
82
|
+
reason: `Response status ${status} clustered across most operations for ${checkId}`,
|
|
83
|
+
fix: `Inspect a sample finding for context, or run with --verbose for per-op detail.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function explainSkipCluster(checkId: string, reason: string): { reason: string; fix: string } | null {
|
|
88
|
+
if (checkId === "response_schema_conformance") {
|
|
89
|
+
return {
|
|
90
|
+
reason: `Response schemas not declared on this API (${reason})`,
|
|
91
|
+
fix: `Add response schemas to spec.json, or run \`zond api annotate dump readback\` to capture them from live runs.`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (checkId === "response_headers_conformance") {
|
|
95
|
+
return {
|
|
96
|
+
reason: `Response headers not declared on this API (${reason})`,
|
|
97
|
+
fix: `Add response header declarations to spec.json — without them this check is a no-op.`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (checkId === "not_a_server_error" && /skipped|max_requests/.test(reason)) {
|
|
101
|
+
return null; // budget-skip, not a spec gap
|
|
102
|
+
}
|
|
103
|
+
if (/max_requests|max-requests/.test(reason)) {
|
|
104
|
+
return null; // ARV-227 budget cap is not a spec finding
|
|
105
|
+
}
|
|
106
|
+
// Other skip clusters fall through — surfaced as `other` kind.
|
|
107
|
+
return {
|
|
108
|
+
reason: `Most cases for ${checkId} skipped (${reason})`,
|
|
109
|
+
fix: `Inspect one sample (zond db diagnose --run-id <id>) to confirm the gap is intentional.`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function explainNoDetector(checkId: string): { reason: string; fix: string } {
|
|
114
|
+
if (checkId === "use_after_free") {
|
|
115
|
+
return {
|
|
116
|
+
reason: `No DELETE+GET pair detectable from spec — check ran 0 cases`,
|
|
117
|
+
fix: `Annotate resources (\`zond api annotate dump readback\`) or add explicit lifecycle declarations.`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (checkId === "ensure_resource_availability") {
|
|
121
|
+
return {
|
|
122
|
+
reason: `No CRUD pair detected — check ran 0 cases`,
|
|
123
|
+
fix: `Run \`zond api annotate dump readback\` to capture resource boundaries.`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (checkId === "cross_call_references") {
|
|
127
|
+
return {
|
|
128
|
+
reason: `No POST→GET follow-up pair detected — check ran 0 cases`,
|
|
129
|
+
fix: `Annotate resources with readback_diff in .api-resources.yaml.`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
reason: `${checkId} applicable but produced 0 cases on this API`,
|
|
134
|
+
fix: `Inspect the case-generator for this check or add resource annotations.`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* ARV-307: broken-baseline guard for conformance checks.
|
|
140
|
+
*
|
|
141
|
+
* On a degenerate baseline (e.g. a fully auth-rejected scan where every
|
|
142
|
+
* response is 401/404, zero 2xx), `status_code_conformance` and
|
|
143
|
+
* `content_type_conformance` fire on every undeclared status/content-type
|
|
144
|
+
* and emit thousands of findings that are pure baseline artifacts — the
|
|
145
|
+
* status_drift rollup in computeSpecFindings can't collapse them because the
|
|
146
|
+
* undeclared statuses are diverse (401 here, 404 there), so no single
|
|
147
|
+
* (check,status) group crosses the 80% threshold.
|
|
148
|
+
*
|
|
149
|
+
* Stateful checks already skip on a per-case broken-baseline guard; this is
|
|
150
|
+
* the run-level equivalent for the conformance family: if the positive
|
|
151
|
+
* (expected-success) probes overwhelmingly failed, the whole conformance
|
|
152
|
+
* signal is untrustworthy, so we roll it up into one `broken_baseline`
|
|
153
|
+
* spec_finding and drop the per-op conformance findings.
|
|
154
|
+
*
|
|
155
|
+
* We gate on the POSITIVE-probe baseline, not all responses — negative /
|
|
156
|
+
* boundary cases legitimately return 4xx on a healthy API, so counting them
|
|
157
|
+
* would false-trip the guard. `positiveTwoxx / positiveTotal` is the success
|
|
158
|
+
* rate of the probes that are supposed to succeed.
|
|
159
|
+
*
|
|
160
|
+
* Threshold: >90% of positive probes non-2xx, with ≥10 positive probes so a
|
|
161
|
+
* tiny run doesn't trip on a single failure. When fewer than 10 positive
|
|
162
|
+
* probes ran (e.g. negative-only mode) the baseline can't be judged and the
|
|
163
|
+
* guard is a no-op.
|
|
164
|
+
*/
|
|
165
|
+
export const BASELINE_GATED_CHECKS: ReadonlySet<string> = new Set([
|
|
166
|
+
"status_code_conformance",
|
|
167
|
+
"content_type_conformance",
|
|
168
|
+
]);
|
|
169
|
+
export const BROKEN_BASELINE_NON2XX_RATIO = 0.9;
|
|
170
|
+
export const BROKEN_BASELINE_MIN_POSITIVE = 10;
|
|
171
|
+
|
|
172
|
+
export function applyBrokenBaselineGuard(input: {
|
|
173
|
+
findings: CheckFinding[];
|
|
174
|
+
positiveTotal: number;
|
|
175
|
+
positiveTwoxx: number;
|
|
176
|
+
}): { kept: CheckFinding[]; removed: CheckFinding[]; specFinding: SpecFinding | null } {
|
|
177
|
+
const { findings, positiveTotal, positiveTwoxx } = input;
|
|
178
|
+
const noop = { kept: findings, removed: [] as CheckFinding[], specFinding: null };
|
|
179
|
+
if (positiveTotal < BROKEN_BASELINE_MIN_POSITIVE) return noop;
|
|
180
|
+
const nonTwoxxRatio = (positiveTotal - positiveTwoxx) / positiveTotal;
|
|
181
|
+
if (nonTwoxxRatio < BROKEN_BASELINE_NON2XX_RATIO) return noop;
|
|
182
|
+
|
|
183
|
+
const kept: CheckFinding[] = [];
|
|
184
|
+
const removed: CheckFinding[] = [];
|
|
185
|
+
for (const f of findings) {
|
|
186
|
+
// Suppressed findings are already out of the CI-gating tallies; leave
|
|
187
|
+
// them in the audit trail untouched.
|
|
188
|
+
if (!f.suppressed_by && BASELINE_GATED_CHECKS.has(f.check)) removed.push(f);
|
|
189
|
+
else kept.push(f);
|
|
190
|
+
}
|
|
191
|
+
if (removed.length === 0) return noop;
|
|
192
|
+
|
|
193
|
+
const affected = new Map<string, { path: string; method: string; operationId?: string }>();
|
|
194
|
+
for (const f of removed) {
|
|
195
|
+
affected.set(`${f.operation.method} ${f.operation.path}`, f.operation);
|
|
196
|
+
}
|
|
197
|
+
const pct = Math.round(nonTwoxxRatio * 100);
|
|
198
|
+
const specFinding: SpecFinding = {
|
|
199
|
+
check: "status_code_conformance",
|
|
200
|
+
kind: "broken_baseline",
|
|
201
|
+
severity: "info",
|
|
202
|
+
category: categoryFor("status_code_conformance"),
|
|
203
|
+
reason:
|
|
204
|
+
`Degenerate baseline: ${pct}% of ${positiveTotal} positive probes returned non-2xx ` +
|
|
205
|
+
`(only ${positiveTwoxx} succeeded). ${removed.length} conformance finding(s) suppressed as ` +
|
|
206
|
+
`baseline artifacts.`,
|
|
207
|
+
fix_hint:
|
|
208
|
+
`Fix the baseline first — supply valid auth (--auth-header / apis/<name>/.env.yaml) and ` +
|
|
209
|
+
`seed path-param fixtures (\`zond prepare-fixtures\`) so positive probes reach 2xx, then re-run ` +
|
|
210
|
+
`conformance. Undeclared statuses on an all-4xx scan are not real spec drift.`,
|
|
211
|
+
affected_operations: [...affected.values()],
|
|
212
|
+
count: removed.length,
|
|
213
|
+
applicable: positiveTotal,
|
|
214
|
+
};
|
|
215
|
+
return { kept, removed, specFinding };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function computeSpecFindings(
|
|
219
|
+
findings: CheckFinding[],
|
|
220
|
+
perCheck: Map<string, PerCheckObservations>,
|
|
221
|
+
): SpecFinding[] {
|
|
222
|
+
const out: SpecFinding[] = [];
|
|
223
|
+
|
|
224
|
+
// --- 1. status_drift: cluster findings by (check, status). -----------
|
|
225
|
+
type Group = {
|
|
226
|
+
severity: CheckFinding["severity"];
|
|
227
|
+
check: string;
|
|
228
|
+
status: number;
|
|
229
|
+
ops: Map<string, CheckFinding["operation"]>;
|
|
230
|
+
};
|
|
231
|
+
const groups = new Map<string, Group>();
|
|
232
|
+
for (const f of findings) {
|
|
233
|
+
const status = f.response_summary?.status ?? 0;
|
|
234
|
+
if (status <= 0) continue; // network errors etc — not a status drift
|
|
235
|
+
const key = `${f.check}|${status}`;
|
|
236
|
+
let g = groups.get(key);
|
|
237
|
+
if (!g) {
|
|
238
|
+
g = { severity: f.severity, check: f.check, status, ops: new Map() };
|
|
239
|
+
groups.set(key, g);
|
|
240
|
+
}
|
|
241
|
+
const opKey = `${f.operation.method} ${f.operation.path}`;
|
|
242
|
+
if (!g.ops.has(opKey)) g.ops.set(opKey, f.operation);
|
|
243
|
+
}
|
|
244
|
+
for (const g of groups.values()) {
|
|
245
|
+
const obs = perCheck.get(g.check);
|
|
246
|
+
const applicable = obs?.applicable ?? g.ops.size;
|
|
247
|
+
if (g.ops.size < 2) continue; // single-op rows aren't a rollup
|
|
248
|
+
if (g.ops.size / Math.max(applicable, 1) < SPEC_CLUSTER_RATIO) continue;
|
|
249
|
+
const { reason, fix } = explainStatusDrift(g.check, g.status);
|
|
250
|
+
out.push({
|
|
251
|
+
check: g.check,
|
|
252
|
+
kind: "status_drift",
|
|
253
|
+
severity: g.severity,
|
|
254
|
+
category: categoryFor(g.check),
|
|
255
|
+
reason,
|
|
256
|
+
fix_hint: fix,
|
|
257
|
+
affected_operations: [...g.ops.values()],
|
|
258
|
+
count: g.ops.size,
|
|
259
|
+
applicable,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// --- 2. missing_declaration: skip cluster. ---------------------------
|
|
264
|
+
for (const [checkId, obs] of perCheck) {
|
|
265
|
+
if (obs.cases <= 0) continue;
|
|
266
|
+
for (const [rawReason, count] of Object.entries(obs.skipped)) {
|
|
267
|
+
if (count / obs.cases < SPEC_CLUSTER_RATIO) continue;
|
|
268
|
+
// skipped key is "<checkId>: <reason>" — strip the prefix when it
|
|
269
|
+
// matches; otherwise treat the whole thing as the reason.
|
|
270
|
+
const reason = rawReason.startsWith(`${checkId}: `) ? rawReason.slice(checkId.length + 2) : rawReason;
|
|
271
|
+
const expl = explainSkipCluster(checkId, reason);
|
|
272
|
+
if (!expl) continue;
|
|
273
|
+
out.push({
|
|
274
|
+
check: checkId,
|
|
275
|
+
kind: /not declared|not declar/i.test(expl.reason) || /\bschema\b|\bheaders?\b/i.test(reason)
|
|
276
|
+
? "missing_declaration"
|
|
277
|
+
: "other",
|
|
278
|
+
severity: "info",
|
|
279
|
+
category: categoryFor(checkId),
|
|
280
|
+
reason: expl.reason,
|
|
281
|
+
fix_hint: expl.fix,
|
|
282
|
+
affected_operations: [],
|
|
283
|
+
count,
|
|
284
|
+
applicable: obs.applicable,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// --- 3. no_detector: applicable ≥5 but 0 cases. ----------------------
|
|
290
|
+
for (const [checkId, obs] of perCheck) {
|
|
291
|
+
if (obs.cases > 0) continue;
|
|
292
|
+
if (obs.applicable < NO_DETECTOR_FLOOR) continue;
|
|
293
|
+
const expl = explainNoDetector(checkId);
|
|
294
|
+
out.push({
|
|
295
|
+
check: checkId,
|
|
296
|
+
kind: "no_detector",
|
|
297
|
+
severity: "info",
|
|
298
|
+
category: categoryFor(checkId),
|
|
299
|
+
reason: expl.reason,
|
|
300
|
+
fix_hint: expl.fix,
|
|
301
|
+
affected_operations: [],
|
|
302
|
+
count: 0,
|
|
303
|
+
applicable: obs.applicable,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return out;
|
|
308
|
+
}
|
package/src/core/checks/types.ts
CHANGED
|
@@ -111,7 +111,31 @@ export interface CheckContext {
|
|
|
111
111
|
export type CheckOutcome =
|
|
112
112
|
| { kind: "pass" }
|
|
113
113
|
| { kind: "skip"; reason: string }
|
|
114
|
-
| {
|
|
114
|
+
| {
|
|
115
|
+
kind: "fail";
|
|
116
|
+
message: string;
|
|
117
|
+
evidence?: Record<string, unknown>;
|
|
118
|
+
/** ARV-284: per-finding severity override. When set, runner uses
|
|
119
|
+
* this in place of `Check.severity` — lets a check emit different
|
|
120
|
+
* severities based on context (e.g. `negative_data_rejection`
|
|
121
|
+
* with `additionalProperties-violation` evidence → LOW, with
|
|
122
|
+
* `pattern-violation` → MEDIUM, with 5xx response → HIGH). The
|
|
123
|
+
* declared `Check.severity` stays as the natural fallback /
|
|
124
|
+
* documentation tier. The agent re-severitizes from the raw
|
|
125
|
+
* evidence downstream. */
|
|
126
|
+
severity?: Severity;
|
|
127
|
+
/** ARV-310: attribute the finding to a specific operation instead of
|
|
128
|
+
* the CRUD group's canonical create/read op. cursor_boundary_fuzzing
|
|
129
|
+
* probes the GET list endpoint — without this the finding lands on the
|
|
130
|
+
* POST create and reads as "a create endpoint that doesn't paginate". */
|
|
131
|
+
operation?: { path: string; method: string; operationId?: string };
|
|
132
|
+
/** ARV-312: observed HTTP status of the response the check acted on.
|
|
133
|
+
* Auth/stateful checks send their own requests, so the runner has no
|
|
134
|
+
* response to summarize and otherwise records `status: 0` — a phantom
|
|
135
|
+
* that reads as "no response captured". Set this so the finding carries
|
|
136
|
+
* the real status (and severity gating can key off it). */
|
|
137
|
+
responseStatus?: number;
|
|
138
|
+
};
|
|
115
139
|
|
|
116
140
|
export interface Check {
|
|
117
141
|
/** Stable identifier — must match schemathesis name where possible. */
|
|
@@ -151,6 +175,16 @@ export interface CheckFinding {
|
|
|
151
175
|
* check id (and response status for `network_error`). Optional
|
|
152
176
|
* because synthetic findings (e.g. unit-test fakes) may skip it. */
|
|
153
177
|
recommended_action?: RecommendedAction;
|
|
178
|
+
/** Suppression trace — present when a finding was removed from the
|
|
179
|
+
* gate counts (today only the deterministic broken-baseline guard,
|
|
180
|
+
* ARV-307, marks findings this way). CI summary excludes such findings
|
|
181
|
+
* from gate counts via this field's presence; presence of
|
|
182
|
+
* `suppressed_by` is the canonical "suppressed" signal. */
|
|
183
|
+
suppressed_by?: {
|
|
184
|
+
source: string;
|
|
185
|
+
rule_index: number;
|
|
186
|
+
reason: string;
|
|
187
|
+
};
|
|
154
188
|
}
|
|
155
189
|
|
|
156
190
|
export interface CheckRunSummary {
|
|
@@ -169,11 +203,71 @@ export interface CheckRunSummary {
|
|
|
169
203
|
* response branch ×2` tells the user why "0 findings" doesn't mean "all
|
|
170
204
|
* green" (probe got 4xx, response schema only declared on 2xx). */
|
|
171
205
|
skipped_outcomes: Record<string, number>;
|
|
206
|
+
/** ARV-83: same data as `skipped_outcomes`, but split into `{check, reason,
|
|
207
|
+
* count}` so consumers don't have to colon-tokenise a key whose reason
|
|
208
|
+
* text may itself contain colons. Sorted by count descending. The legacy
|
|
209
|
+
* `skipped_outcomes` field is kept for back-compat with existing parsers
|
|
210
|
+
* / NDJSON readers. */
|
|
211
|
+
skipped_outcomes_grouped: Array<{ check: string; reason: string; count: number }>;
|
|
212
|
+
/** Count of findings suppressed by the deterministic broken-baseline
|
|
213
|
+
* guard (ARV-307). Excluded from `findings`/`by_severity` so CI gates
|
|
214
|
+
* ignore them, but surfaced here for audit-trail reconciliation. */
|
|
215
|
+
suppressed?: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** ARV-60: spec-level rollup of a systemic gap that manifests on N
|
|
219
|
+
* operations. When ≥80% of a check's applicable operations share the same
|
|
220
|
+
* root cause (same response status undeclared, same missing-schema skip
|
|
221
|
+
* reason, or zero cases when a detector finds no pair), the runner emits
|
|
222
|
+
* a single `SpecFinding` instead of (or in addition to) the N per-op rows.
|
|
223
|
+
*
|
|
224
|
+
* Consumed by the CLI to print one summary line; surfaced verbatim in the
|
|
225
|
+
* JSON envelope and as a dedicated `spec_finding` NDJSON event. Per-op
|
|
226
|
+
* findings are NOT removed from `findings[]` — agents that prefer per-op
|
|
227
|
+
* triage keep their existing surface; agents that triage by spec hit just
|
|
228
|
+
* one row per drift. */
|
|
229
|
+
export interface SpecFinding {
|
|
230
|
+
check: string;
|
|
231
|
+
/** Classifier so consumers can branch:
|
|
232
|
+
* - `status_drift`: response status code clustered across operations
|
|
233
|
+
* (status_code_conformance / negative_data_rejection / ignored_auth).
|
|
234
|
+
* - `missing_declaration`: every applicable case skipped for the same
|
|
235
|
+
* "spec didn't declare X" reason (response_schema_conformance,
|
|
236
|
+
* response_headers_conformance).
|
|
237
|
+
* - `no_detector`: check is applicable to ≥5 operations but ran 0 cases
|
|
238
|
+
* (use_after_free without DELETE+GET pair, cross_call_references
|
|
239
|
+
* without followups in scope).
|
|
240
|
+
* - `broken_baseline`: ARV-307 — the positive/success baseline was
|
|
241
|
+
* degenerate (>90% of positive probes returned non-2xx, e.g. a
|
|
242
|
+
* fully auth-rejected scan). The conformance checks' per-op findings
|
|
243
|
+
* are baseline artifacts, so they're rolled up into this one row and
|
|
244
|
+
* removed from `findings[]` (mirrors the stateful broken-baseline skip).
|
|
245
|
+
* - `other`: skip-cluster that doesn't fit the above. */
|
|
246
|
+
kind: "status_drift" | "missing_declaration" | "no_detector" | "broken_baseline" | "other";
|
|
247
|
+
/** Severity inherited from the underlying findings (status_drift) or
|
|
248
|
+
* fixed to "info" for missing_declaration / no_detector — those signal
|
|
249
|
+
* "spec gap, not server bug" so the team knows where to act. */
|
|
250
|
+
severity: Severity;
|
|
251
|
+
category?: Category;
|
|
252
|
+
/** One-line root cause statement — surfaces what zond observed. */
|
|
253
|
+
reason: string;
|
|
254
|
+
/** Actionable next step. References a spec edit, a tolerate flag, or
|
|
255
|
+
* another zond command. Empty string when no automatic suggestion. */
|
|
256
|
+
fix_hint: string;
|
|
257
|
+
/** Operations the rollup covers (path + method). count = length. */
|
|
258
|
+
affected_operations: Array<{ path: string; method: string; operationId?: string }>;
|
|
259
|
+
count: number;
|
|
260
|
+
/** Applicable population the cluster was measured against. ratio =
|
|
261
|
+
* count / applicable. Lets consumers re-threshold without re-running. */
|
|
262
|
+
applicable: number;
|
|
172
263
|
}
|
|
173
264
|
|
|
174
265
|
export interface CheckRunData {
|
|
175
266
|
findings: CheckFinding[];
|
|
176
267
|
summary: CheckRunSummary;
|
|
268
|
+
/** ARV-60: spec-level rollup, see SpecFinding. Always present (empty
|
|
269
|
+
* array when no clusters cross the 80% threshold). */
|
|
270
|
+
spec_findings: SpecFinding[];
|
|
177
271
|
}
|
|
178
272
|
|
|
179
273
|
export function emptySummary(): CheckRunSummary {
|
|
@@ -185,5 +279,27 @@ export function emptySummary(): CheckRunSummary {
|
|
|
185
279
|
by_severity: emptySeverityBuckets(),
|
|
186
280
|
by_category: emptyCategoryBuckets(),
|
|
187
281
|
skipped_outcomes: {},
|
|
282
|
+
skipped_outcomes_grouped: [],
|
|
283
|
+
suppressed: 0,
|
|
188
284
|
};
|
|
189
285
|
}
|
|
286
|
+
|
|
287
|
+
/** ARV-83: turn the legacy `<check>: <reason>` keys into a structured
|
|
288
|
+
* array. The split parses the colon-separator at the first occurrence; if
|
|
289
|
+
* the reason itself contains colons, only the LEADING `check_id:` is
|
|
290
|
+
* stripped, preserving the rest verbatim. */
|
|
291
|
+
export function groupSkippedOutcomes(
|
|
292
|
+
outcomes: Record<string, number>,
|
|
293
|
+
): Array<{ check: string; reason: string; count: number }> {
|
|
294
|
+
const out: Array<{ check: string; reason: string; count: number }> = [];
|
|
295
|
+
for (const [key, count] of Object.entries(outcomes)) {
|
|
296
|
+
const idx = key.indexOf(": ");
|
|
297
|
+
if (idx > 0) {
|
|
298
|
+
out.push({ check: key.slice(0, idx), reason: key.slice(idx + 2), count });
|
|
299
|
+
} else {
|
|
300
|
+
out.push({ check: key, reason: "", count });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
out.sort((a, b) => b.count - a.count);
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `x-zond-*` OpenAPI vendor-extension policy (ARV-189, m-21).
|
|
3
|
+
*
|
|
4
|
+
* Lets spec authors declare per-operation rules directly in the spec
|
|
5
|
+
* without a sidecar yaml file. This is the low-friction alternative
|
|
6
|
+
* to `.api-resources.local.yaml` for one-off endpoints — typical
|
|
7
|
+
* cases:
|
|
8
|
+
* - public route that shouldn't be flagged for missing auth headers
|
|
9
|
+
* - debugging endpoint whose 500s aren't real bugs
|
|
10
|
+
* - one-off skip during incident triage
|
|
11
|
+
*
|
|
12
|
+
* Resolution priority (highest first):
|
|
13
|
+
* 1. `.api-resources.local.yaml` overlay (explicit operator override)
|
|
14
|
+
* 2. `x-zond-*` extensions (in-spec policy — this module)
|
|
15
|
+
* 3. `.api-resources.yaml` (auto-generated baseline)
|
|
16
|
+
* 4. built-in defaults
|
|
17
|
+
*
|
|
18
|
+
* Supported extensions in this milestone:
|
|
19
|
+
* x-zond-skip: string | string[] — check ids to skip for this op
|
|
20
|
+
* x-zond-public: boolean — shortcut: skip auth-class checks
|
|
21
|
+
*
|
|
22
|
+
* Deliberately NOT implemented yet (deferred to a follow-up task):
|
|
23
|
+
* x-zond-resource / x-zond-idempotent / x-zond-lifecycle-field —
|
|
24
|
+
* these require deeper m-20 overlay wiring (proper integration with
|
|
25
|
+
* resource-config maps), tracked separately to keep this MVP focused
|
|
26
|
+
* on the universally-useful skip rules.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { EndpointInfo } from "../generator/types.ts";
|
|
30
|
+
|
|
31
|
+
/** Auth-related check ids that `x-zond-public: true` should suppress.
|
|
32
|
+
* Kept in sync with the auth-class check registry in mode.ts. */
|
|
33
|
+
const AUTH_CHECK_IDS = new Set<string>([
|
|
34
|
+
"ignored_auth",
|
|
35
|
+
"missing_required_header",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/** True when the endpoint declares the check id should be skipped via
|
|
39
|
+
* any `x-zond-*` extension. The check id is matched case-sensitively
|
|
40
|
+
* against `x-zond-skip` entries; `x-zond-public: true` is expanded to
|
|
41
|
+
* the auth-id set before comparison. */
|
|
42
|
+
export function endpointSkipsCheck(op: EndpointInfo, checkId: string): boolean {
|
|
43
|
+
const ext = op.extensions;
|
|
44
|
+
if (!ext) return false;
|
|
45
|
+
|
|
46
|
+
// x-zond-public: true → implicit skip for every auth-class check.
|
|
47
|
+
if (ext["x-zond-public"] === true && AUTH_CHECK_IDS.has(checkId)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// x-zond-skip: <string> | <string[]>
|
|
52
|
+
const skip = ext["x-zond-skip"];
|
|
53
|
+
if (typeof skip === "string") {
|
|
54
|
+
return skip === checkId;
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(skip)) {
|
|
57
|
+
for (const entry of skip) {
|
|
58
|
+
if (typeof entry === "string" && entry === checkId) return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Reason string surfaced in the skipped-outcomes summary. Mirrors the
|
|
65
|
+
* `<check>: <reason>` convention used elsewhere in the runner. */
|
|
66
|
+
export function reasonForSkip(op: EndpointInfo, checkId: string): string {
|
|
67
|
+
const ext = op.extensions;
|
|
68
|
+
if (!ext) return "x-zond extension"; // shouldn't happen — caller gates on endpointSkipsCheck
|
|
69
|
+
if (ext["x-zond-public"] === true && AUTH_CHECK_IDS.has(checkId)) {
|
|
70
|
+
return "x-zond-public: true (auth check suppressed at the spec level)";
|
|
71
|
+
}
|
|
72
|
+
return `x-zond-skip listed "${checkId}" at the spec level`;
|
|
73
|
+
}
|
|
@@ -57,6 +57,7 @@ export type FindingClass =
|
|
|
57
57
|
| "check:pagination_invariants"
|
|
58
58
|
| "check:lifecycle_transitions"
|
|
59
59
|
| "check:open_cors_on_sensitive"
|
|
60
|
+
| "check:cursor_boundary_fuzzing"
|
|
60
61
|
| "check:rate_limit_headers_absent"
|
|
61
62
|
| "check:network_error"
|
|
62
63
|
|
|
@@ -105,6 +106,13 @@ export interface ClassifierContext {
|
|
|
105
106
|
* fix_test_logic. Producers (db-analysis) set this after walking the
|
|
106
107
|
* step's assertions array. */
|
|
107
108
|
schema_violation?: boolean;
|
|
109
|
+
/** ARV-324: true when `prepare-fixtures`/`discover` already confirmed
|
|
110
|
+
* this exact operation returns a client error / empty list while
|
|
111
|
+
* hunting for fixture values (`.fixture-gaps.yaml`) — e.g. an empty
|
|
112
|
+
* test account. Same shape of problem `probe:mass_assignment`'s
|
|
113
|
+
* `inconclusive-baseline` branch already solves below: a finding
|
|
114
|
+
* caused by our own missing test data isn't a backend bug. */
|
|
115
|
+
unresolved_fixture?: boolean;
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
/**
|
|
@@ -152,14 +160,35 @@ export function classify(ctx: ClassifierContext): RecommendedAction | undefined
|
|
|
152
160
|
return "fix_spec";
|
|
153
161
|
|
|
154
162
|
case "check:not_a_server_error":
|
|
155
|
-
|
|
163
|
+
// A 5xx on this operation is worth flagging regardless of fixture
|
|
164
|
+
// state — even a synthetic/garbage id shouldn't crash the server.
|
|
165
|
+
return "report_backend_bug";
|
|
166
|
+
|
|
156
167
|
case "check:positive_data_acceptance":
|
|
168
|
+
// ARV-324: a known unresolved fixture gap (garbage path-id → 400)
|
|
169
|
+
// is our own missing test data, not a backend or spec defect.
|
|
170
|
+
if (ctx.unresolved_fixture) return "fix_fixture";
|
|
171
|
+
// ARV-341: otherwise this check fires ONLY when the server rejects a
|
|
172
|
+
// body *we* generated as valid (400/422). The server is
|
|
173
|
+
// authoritative — our generated body or the spec was wrong, not the
|
|
174
|
+
// backend. Route to fix_spec, not report_backend_bug, so 100s of
|
|
175
|
+
// generator/spec-drift cases don't spam the API owner with false
|
|
176
|
+
// bug reports.
|
|
177
|
+
return "fix_spec";
|
|
178
|
+
|
|
179
|
+
case "check:unsupported_method":
|
|
157
180
|
case "check:use_after_free":
|
|
158
181
|
case "check:ensure_resource_availability":
|
|
159
182
|
case "check:cross_call_references":
|
|
160
183
|
case "check:idempotency_replay":
|
|
161
184
|
case "check:pagination_invariants":
|
|
162
185
|
case "check:lifecycle_transitions":
|
|
186
|
+
case "check:cursor_boundary_fuzzing":
|
|
187
|
+
// ARV-324: same treatment as probe:mass_assignment's
|
|
188
|
+
// inconclusive-baseline branch below — a finding on an operation
|
|
189
|
+
// we already know is an unresolved fixture gap isn't new evidence
|
|
190
|
+
// of a backend defect.
|
|
191
|
+
if (ctx.unresolved_fixture) return "fix_fixture";
|
|
163
192
|
return "report_backend_bug";
|
|
164
193
|
|
|
165
194
|
case "check:negative_data_rejection":
|
|
@@ -173,11 +202,11 @@ export function classify(ctx: ClassifierContext): RecommendedAction | undefined
|
|
|
173
202
|
return "fix_auth_config";
|
|
174
203
|
|
|
175
204
|
case "check:rate_limit_headers_absent":
|
|
176
|
-
// ARV-
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
return "
|
|
205
|
+
// ARV-304: this is a server-side hygiene gap (RFC-9239 /
|
|
206
|
+
// OWASP-API-04 expect rate-limit metadata on writes), not a
|
|
207
|
+
// caller-side auth problem. Agents triaging on `fix_auth_config`
|
|
208
|
+
// would chase the wrong fix; surface it as a backend-team task.
|
|
209
|
+
return "report_backend_bug";
|
|
181
210
|
|
|
182
211
|
case "check:network_error":
|
|
183
212
|
if (ctx.status === 401 || ctx.status === 403) return "fix_auth_config";
|
|
@@ -24,6 +24,14 @@ import {
|
|
|
24
24
|
|
|
25
25
|
export interface CoverageLoadOptions {
|
|
26
26
|
apiName: string;
|
|
27
|
+
/** ARV-265: which `run_kind`s contribute to the matrix.
|
|
28
|
+
* - 'test' (default, back-compat) — runs whose `run_kind` is
|
|
29
|
+
* 'regular' OR 'probe' (i.e. anything `zond run` produced). This
|
|
30
|
+
* is what `pass-coverage` / `hit-coverage` have always measured.
|
|
31
|
+
* - 'audit' — every kind (regular | probe | check | request |
|
|
32
|
+
* fixture). Used by `audit-coverage` to answer "did this scan
|
|
33
|
+
* reach the API?" regardless of producer. */
|
|
34
|
+
scope?: "test" | "audit";
|
|
27
35
|
runId?: number;
|
|
28
36
|
/** TASK-255: union across multiple runs (e.g. tests-run + probes-run from
|
|
29
37
|
* the same session). Loader concatenates results from each run before
|
|
@@ -66,6 +74,9 @@ export interface CoverageLoadResult {
|
|
|
66
74
|
profile: "safe" | "full";
|
|
67
75
|
tagFilter: string[];
|
|
68
76
|
ephemeralCount: number;
|
|
77
|
+
/** ARV-265: scope this matrix was built under. Echoes `options.scope`
|
|
78
|
+
* (default 'test') so dual-metric callers can sanity-check the result. */
|
|
79
|
+
scope: "test" | "audit";
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
async function readFixturesAffected(baseDir: string): Promise<BuildMatrixInput["fixturesAffected"]> {
|
|
@@ -112,6 +123,14 @@ export async function loadCoverage(options: CoverageLoadOptions): Promise<Covera
|
|
|
112
123
|
// sessionId > sinceIso > tag > runIds > runId > latest. since:/tag: are
|
|
113
124
|
// filtered to this collection only — the user has named an API, so they
|
|
114
125
|
// want runs of that API, not every collection that happens to share a tag.
|
|
126
|
+
// ARV-265: `scope` toggles which run_kinds survive the post-resolution
|
|
127
|
+
// filter (see CoverageLoadOptions.scope). Default 'test' keeps the
|
|
128
|
+
// pre-ARV-265 semantics (regular + probe rows contribute) so existing
|
|
129
|
+
// CI consumers see no surprise.
|
|
130
|
+
const scope = options.scope ?? "test";
|
|
131
|
+
const allowedKinds: ReadonlySet<string> = scope === "audit"
|
|
132
|
+
? new Set(["regular", "probe", "check", "request", "fixture"])
|
|
133
|
+
: new Set(["regular", "probe"]);
|
|
115
134
|
let runs: RunRecord[] = [];
|
|
116
135
|
let unionMode: "session" | "since" | "tag" | "runs" | null = null;
|
|
117
136
|
if (options.sessionId) {
|
|
@@ -140,10 +159,21 @@ export async function loadCoverage(options: CoverageLoadOptions): Promise<Covera
|
|
|
140
159
|
} else if (options.runId != null) {
|
|
141
160
|
const r = getRunById(options.runId);
|
|
142
161
|
if (r) runs = [r];
|
|
162
|
+
} else if (scope === "audit") {
|
|
163
|
+
// ARV-265 audit mode latest-fallback: pick the most recent run of any
|
|
164
|
+
// kind so a scan that only ran `checks run` (no `zond run`) still
|
|
165
|
+
// produces a meaningful audit metric.
|
|
166
|
+
const latest = getLatestRunByCollection(collection.id, { runKind: "any" });
|
|
167
|
+
if (latest) runs = [latest];
|
|
143
168
|
} else {
|
|
144
169
|
const latest = getLatestRunByCollection(collection.id);
|
|
145
170
|
if (latest) runs = [latest];
|
|
146
171
|
}
|
|
172
|
+
// ARV-265: post-resolution kind filter. Session/since/tag may have
|
|
173
|
+
// pulled in audit-only kinds (check/request/fixture); the test-scope
|
|
174
|
+
// filter strips them so `pass-coverage` keeps the "curated suites
|
|
175
|
+
// passed" semantics. Audit scope keeps everything.
|
|
176
|
+
runs = runs.filter((r) => allowedKinds.has(r.run_kind));
|
|
147
177
|
const results = runs.flatMap((r) => getResultsByRunId(r.id));
|
|
148
178
|
// `run` (singular) reflects the latest contributing run for back-compat
|
|
149
179
|
// with consumers that only care about a single run label.
|
|
@@ -176,6 +206,7 @@ export async function loadCoverage(options: CoverageLoadOptions): Promise<Covera
|
|
|
176
206
|
profile,
|
|
177
207
|
tagFilter,
|
|
178
208
|
ephemeralCount: ephemeralEndpoints.size,
|
|
209
|
+
scope,
|
|
179
210
|
};
|
|
180
211
|
}
|
|
181
212
|
|