@kirrosh/zond 0.21.0 → 0.23.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 +758 -3
- package/README.md +78 -15
- package/package.json +17 -10
- package/src/cli/argv.ts +122 -0
- package/src/cli/commands/add-api.ts +134 -0
- package/src/cli/commands/api/annotate/idempotency.ts +59 -0
- package/src/cli/commands/api/annotate/index.ts +525 -0
- package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
- package/src/cli/commands/api/annotate/overlay.ts +206 -0
- package/src/cli/commands/api/annotate/pagination.ts +60 -0
- package/src/cli/commands/api/annotate/prompts.ts +183 -0
- package/src/cli/commands/api/annotate/readback.ts +58 -0
- package/src/cli/commands/api/annotate/resources.ts +91 -0
- package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
- package/src/cli/commands/audit.ts +480 -0
- package/src/cli/commands/bootstrap.ts +710 -0
- package/src/cli/commands/catalog.ts +35 -0
- package/src/cli/commands/check.ts +348 -0
- package/src/cli/commands/checks.ts +756 -0
- package/src/cli/commands/ci-init.ts +55 -6
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +262 -0
- package/src/cli/commands/completions.ts +192 -0
- package/src/cli/commands/coverage.ts +605 -132
- package/src/cli/commands/db.ts +180 -8
- package/src/cli/commands/describe.ts +37 -2
- package/src/cli/commands/discover.ts +1236 -0
- package/src/cli/commands/doctor.ts +607 -0
- package/src/cli/commands/fixtures.ts +402 -0
- package/src/cli/commands/generate.ts +420 -47
- package/src/cli/commands/init/agents-md.ts +61 -0
- package/src/cli/commands/init/bootstrap.ts +108 -0
- package/src/cli/commands/init/index.ts +244 -0
- package/src/cli/commands/init/skills.ts +98 -0
- package/src/cli/commands/init/templates/agents.md +77 -0
- package/src/cli/commands/init/templates/markdown.d.ts +4 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
- package/src/cli/commands/init/templates/skills/zond.md +651 -0
- package/src/cli/commands/init/templates/zond-config.yml +14 -0
- package/src/cli/commands/prepare-fixtures.ts +135 -0
- package/src/cli/commands/probe/mass-assignment.ts +503 -0
- package/src/cli/commands/probe/security.ts +454 -0
- package/src/cli/commands/probe/static.ts +255 -0
- package/src/cli/commands/probe/webhooks.ts +161 -0
- package/src/cli/commands/probe.ts +459 -0
- package/src/cli/commands/reference.ts +87 -0
- package/src/cli/commands/refresh-api.ts +169 -0
- package/src/cli/commands/remove-api.ts +150 -0
- package/src/cli/commands/report-bundle.ts +318 -0
- package/src/cli/commands/report.ts +241 -0
- package/src/cli/commands/request.ts +379 -4
- package/src/cli/commands/run.ts +911 -33
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +74 -0
- package/src/cli/index.ts +36 -607
- package/src/cli/json-envelope.ts +112 -3
- package/src/cli/json-schemas.ts +263 -0
- package/src/cli/program.ts +218 -0
- package/src/cli/resolve.ts +105 -0
- package/src/cli/status-filter.ts +124 -0
- package/src/cli/util/api-context.ts +85 -0
- package/src/cli/version.ts +8 -0
- package/src/core/anti-fp/bootstrap.ts +34 -0
- package/src/core/anti-fp/index.ts +33 -0
- package/src/core/anti-fp/registry.ts +44 -0
- package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
- package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
- package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
- package/src/core/anti-fp/types.ts +68 -0
- package/src/core/checks/checks/_crud-helpers.ts +133 -0
- package/src/core/checks/checks/_negative_mutator.ts +133 -0
- package/src/core/checks/checks/_readback-helpers.ts +133 -0
- package/src/core/checks/checks/content_type_conformance.ts +39 -0
- package/src/core/checks/checks/cross_call_references.ts +134 -0
- package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
- package/src/core/checks/checks/idempotency_replay.ts +246 -0
- package/src/core/checks/checks/ignored_auth.ts +211 -0
- package/src/core/checks/checks/index.ts +65 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
- package/src/core/checks/checks/missing_required_header.ts +40 -0
- package/src/core/checks/checks/negative_data_rejection.ts +45 -0
- package/src/core/checks/checks/not_a_server_error.ts +27 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
- package/src/core/checks/checks/pagination_invariants.ts +238 -0
- package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
- package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
- package/src/core/checks/checks/response_headers_conformance.ts +74 -0
- package/src/core/checks/checks/response_schema_conformance.ts +30 -0
- package/src/core/checks/checks/status_code_conformance.ts +61 -0
- package/src/core/checks/checks/unsupported_method.ts +63 -0
- package/src/core/checks/checks/use_after_free.ts +78 -0
- package/src/core/checks/index.ts +30 -0
- package/src/core/checks/mode.ts +79 -0
- package/src/core/checks/recommended-action.ts +64 -0
- package/src/core/checks/registry.ts +78 -0
- package/src/core/checks/runner.ts +874 -0
- package/src/core/checks/sarif.ts +230 -0
- package/src/core/checks/stateful.ts +121 -0
- package/src/core/checks/types.ts +189 -0
- package/src/core/classifier/recommended-action.ts +222 -0
- package/src/core/context/current.ts +51 -0
- package/src/core/context/session.ts +78 -0
- package/src/core/coverage/loader.ts +185 -0
- package/src/core/coverage/reasons.ts +300 -0
- package/src/core/diagnostics/db-analysis.ts +161 -12
- package/src/core/diagnostics/failure-class.ts +120 -0
- package/src/core/diagnostics/failure-hints.ts +212 -9
- package/src/core/diagnostics/spec-pointer.ts +99 -0
- package/src/core/diagnostics/suggested-fixes.ts +156 -0
- package/src/core/exporter/case-study/index.ts +270 -0
- package/src/core/exporter/curl.ts +40 -0
- package/src/core/exporter/exporter.ts +48 -0
- package/src/core/exporter/html-report/escape.ts +24 -0
- package/src/core/exporter/html-report/index.ts +479 -0
- package/src/core/exporter/html-report/script.ts +100 -0
- package/src/core/exporter/html-report/styles.ts +408 -0
- package/src/core/generator/chunker.ts +53 -15
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/create-body.ts +89 -0
- package/src/core/generator/data-factory.ts +490 -33
- package/src/core/generator/describe.ts +1 -1
- package/src/core/generator/fixtures-builder.ts +325 -0
- package/src/core/generator/index.ts +7 -5
- package/src/core/generator/openapi-reader.ts +55 -3
- package/src/core/generator/path-param-disambig.ts +114 -0
- package/src/core/generator/resources-builder.ts +648 -0
- package/src/core/generator/schema-utils.ts +11 -3
- package/src/core/generator/serializer.ts +114 -15
- package/src/core/generator/suite-generator.ts +484 -77
- package/src/core/generator/types.ts +8 -0
- package/src/core/identity/identity-file.ts +129 -0
- package/src/core/lint/affects.ts +28 -0
- package/src/core/lint/config.ts +96 -0
- package/src/core/lint/format.ts +42 -0
- package/src/core/lint/index.ts +94 -0
- package/src/core/lint/reporter.ts +128 -0
- package/src/core/lint/rules/consistency.ts +158 -0
- package/src/core/lint/rules/heuristics.ts +97 -0
- package/src/core/lint/rules/strictness.ts +109 -0
- package/src/core/lint/types.ts +96 -0
- package/src/core/lint/walker.ts +248 -0
- package/src/core/meta/meta-store.ts +6 -73
- package/src/core/output/README.md +91 -0
- package/src/core/output/index.ts +13 -0
- package/src/core/output/run.ts +126 -0
- package/src/core/output/types.ts +129 -0
- package/src/core/parser/env-interpolation.ts +104 -0
- package/src/core/parser/filter.ts +57 -0
- package/src/core/parser/schema.ts +132 -5
- package/src/core/parser/types.ts +29 -2
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +108 -13
- package/src/core/probe/bootstrap.ts +34 -0
- package/src/core/probe/dry-run-envelope.ts +57 -0
- package/src/core/probe/mass-assignment-probe-class.ts +198 -0
- package/src/core/probe/mass-assignment-probe.ts +1122 -0
- package/src/core/probe/mass-assignment-template.ts +212 -0
- package/src/core/probe/method-probe.ts +164 -0
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +691 -0
- package/src/core/probe/orphan-tracker.ts +188 -0
- package/src/core/probe/path-discovery.ts +440 -0
- package/src/core/probe/probe-harness.ts +120 -0
- package/src/core/probe/registry.ts +89 -0
- package/src/core/probe/runner.ts +136 -0
- package/src/core/probe/security-probe-class.ts +201 -0
- package/src/core/probe/security-probe.ts +1453 -0
- package/src/core/probe/shared.ts +505 -0
- package/src/core/probe/static-probe-class.ts +125 -0
- package/src/core/probe/types.ts +165 -0
- package/src/core/probe/verdict-aggregator.ts +33 -0
- package/src/core/probe/webhooks-probe.ts +284 -0
- package/src/core/reporter/console.ts +69 -4
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +15 -2
- package/src/core/reporter/junit.ts +27 -12
- package/src/core/reporter/ndjson.ts +37 -0
- package/src/core/reporter/types.ts +3 -0
- package/src/core/runner/assertions.ts +62 -2
- package/src/core/runner/async-pool.ts +108 -0
- package/src/core/runner/auth-path.ts +8 -0
- package/src/core/runner/ci-context.ts +72 -0
- package/src/core/runner/executor.ts +391 -52
- package/src/core/runner/form-encode.ts +51 -0
- package/src/core/runner/http-client.ts +115 -7
- package/src/core/runner/learn-drift.ts +293 -0
- package/src/core/runner/preflight-vars.ts +149 -0
- package/src/core/runner/progress-tracker.ts +73 -0
- package/src/core/runner/rate-limiter.ts +203 -0
- package/src/core/runner/run-kind.ts +39 -0
- package/src/core/runner/schema-validator.ts +312 -0
- package/src/core/runner/send-request.ts +153 -20
- package/src/core/runner/types.ts +38 -0
- package/src/core/secrets/registry.ts +164 -0
- package/src/core/secrets/secrets-file.ts +115 -0
- package/src/core/selectors/operation-filter.ts +144 -0
- package/src/core/setup-api.ts +419 -17
- package/src/core/severity/category.ts +94 -0
- package/src/core/severity/index.ts +121 -0
- package/src/core/spec/layers.ts +154 -0
- package/src/core/util/format-eta.ts +21 -0
- package/src/core/utils.ts +5 -1
- package/src/core/workspace/config.ts +129 -0
- package/src/core/workspace/manifest.ts +283 -0
- package/src/core/workspace/output-rotation.ts +62 -0
- package/src/core/workspace/root.ts +94 -0
- package/src/core/workspace/triage-path.ts +87 -0
- package/src/db/lint-runs.ts +47 -0
- package/src/db/migrate.ts +126 -0
- package/src/db/migrations/0001_run_kind.sql +25 -0
- package/src/db/migrations/sql.d.ts +4 -0
- package/src/db/queries/collections.ts +133 -0
- package/src/db/queries/coverage.ts +9 -0
- package/src/db/queries/dashboard.ts +59 -0
- package/src/db/queries/results.ts +128 -0
- package/src/db/queries/runs.ts +235 -0
- package/src/db/queries/sessions.ts +42 -0
- package/src/db/queries/settings.ts +28 -0
- package/src/db/queries/types.ts +172 -0
- package/src/db/queries.ts +72 -802
- package/src/db/schema.ts +179 -48
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init.ts +0 -57
- package/src/cli/commands/serve.ts +0 -81
- package/src/cli/commands/sync.ts +0 -269
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -21
- package/src/core/parser/index.ts +0 -21
- package/src/core/runner/execute-run.ts +0 -132
- package/src/core/runner/index.ts +0 -12
- package/src/core/sync/spec-differ.ts +0 -38
- package/src/web/data/collection-state.ts +0 -362
- package/src/web/routes/api.ts +0 -314
- package/src/web/routes/dashboard.ts +0 -350
- package/src/web/routes/runs.ts +0 -64
- package/src/web/schemas.ts +0 -121
- package/src/web/server.ts +0 -134
- package/src/web/static/htmx.min.cjs +0 -1
- package/src/web/static/style.css +0 -1148
- package/src/web/views/endpoints-tab.ts +0 -174
- package/src/web/views/explorer-tab.ts +0 -402
- package/src/web/views/health-strip.ts +0 -92
- package/src/web/views/layout.ts +0 -48
- package/src/web/views/results.ts +0 -210
- package/src/web/views/runs-tab.ts +0 -126
- package/src/web/views/suites-tab.ts +0 -181
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-56: single producer of `recommended_action` across every source
|
|
3
|
+
* that emits findings — run results (`db diagnose`), spec lint
|
|
4
|
+
* (`lint-spec.Issue`), probe-security findings, probe-mass-assignment
|
|
5
|
+
* verdicts, and conformance checks (`zond checks run`).
|
|
6
|
+
*
|
|
7
|
+
* Before this module, "what action does an agent take?" was answered in
|
|
8
|
+
* five different places:
|
|
9
|
+
* - `recommendedAction` / `recommendedActionForGenerated` (db-analysis)
|
|
10
|
+
* - `recommendForCheck` (checks runner)
|
|
11
|
+
* - `stampRecommendedAction` (mass-assignment) + per-severity policy
|
|
12
|
+
* - `stampAction` (security) + per-severity policy
|
|
13
|
+
* - inline `"fix_spec"` literal (lint)
|
|
14
|
+
*
|
|
15
|
+
* Each accumulated branches independently — ARV-11 added per-check
|
|
16
|
+
* actions, ARV-42 added a generator-aware override, TASK-294 layered
|
|
17
|
+
* probe severity mappings, the env-issue detector then overrode the
|
|
18
|
+
* result of all of them. By the time ARV-56 landed, the same logical
|
|
19
|
+
* question — "given this finding, what should the agent do?" — had four
|
|
20
|
+
* different entry points with subtle drift.
|
|
21
|
+
*
|
|
22
|
+
* This file owns that question. Every producer hands the classifier a
|
|
23
|
+
* `ClassifierContext` (a frozen description of the finding) and gets
|
|
24
|
+
* back a `RecommendedAction`. The classifier is **pure** — no I/O, no
|
|
25
|
+
* config reads, no side effects — so it can be table-tested.
|
|
26
|
+
*
|
|
27
|
+
* The thin wrappers (`recommendedAction`, `recommendedActionForGenerated`,
|
|
28
|
+
* `recommendForCheck`, `stampRecommendedAction`, `stampAction`) now
|
|
29
|
+
* delegate here instead of carrying their own switches. Removing a
|
|
30
|
+
* branch means editing one switch in this file.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import type { RecommendedAction } from "../diagnostics/failure-hints.ts";
|
|
34
|
+
import type { RunKind } from "../runner/run-kind.ts";
|
|
35
|
+
|
|
36
|
+
export type FindingClass =
|
|
37
|
+
// db-analysis run-result rows ────────────────────────────────────
|
|
38
|
+
| "test:network_error"
|
|
39
|
+
| "test:api_error"
|
|
40
|
+
| "test:assertion_failed"
|
|
41
|
+
|
|
42
|
+
// checks/<id> ────────────────────────────────────────────────────
|
|
43
|
+
| "check:status_code_conformance"
|
|
44
|
+
| "check:content_type_conformance"
|
|
45
|
+
| "check:response_headers_conformance"
|
|
46
|
+
| "check:response_schema_conformance"
|
|
47
|
+
| "check:not_a_server_error"
|
|
48
|
+
| "check:unsupported_method"
|
|
49
|
+
| "check:positive_data_acceptance"
|
|
50
|
+
| "check:use_after_free"
|
|
51
|
+
| "check:ensure_resource_availability"
|
|
52
|
+
| "check:negative_data_rejection"
|
|
53
|
+
| "check:missing_required_header"
|
|
54
|
+
| "check:ignored_auth"
|
|
55
|
+
| "check:cross_call_references"
|
|
56
|
+
| "check:idempotency_replay"
|
|
57
|
+
| "check:pagination_invariants"
|
|
58
|
+
| "check:lifecycle_transitions"
|
|
59
|
+
| "check:open_cors_on_sensitive"
|
|
60
|
+
| "check:rate_limit_headers_absent"
|
|
61
|
+
| "check:network_error"
|
|
62
|
+
|
|
63
|
+
// probe verdicts (severity already classified upstream) ───────────
|
|
64
|
+
| "probe:mass_assignment"
|
|
65
|
+
| "probe:security"
|
|
66
|
+
|
|
67
|
+
// lint-spec ───────────────────────────────────────────────────────
|
|
68
|
+
| "lint:issue";
|
|
69
|
+
|
|
70
|
+
/** Optional severity hint — probe families surface a 5-level enum;
|
|
71
|
+
* here we only care about the buckets the action mapping branches on. */
|
|
72
|
+
export type FindingSeverity =
|
|
73
|
+
| "high"
|
|
74
|
+
| "medium"
|
|
75
|
+
| "inconclusive-5xx"
|
|
76
|
+
| "inconclusive-baseline"
|
|
77
|
+
| "low"
|
|
78
|
+
| "ok"
|
|
79
|
+
| "skipped";
|
|
80
|
+
|
|
81
|
+
export interface ClassifierContext {
|
|
82
|
+
finding_class: FindingClass;
|
|
83
|
+
/** HTTP status code observed for the finding, when applicable.
|
|
84
|
+
* null/undefined means "unknown / not relevant". */
|
|
85
|
+
status?: number | null;
|
|
86
|
+
/** Severity already assigned by the probe layer (mass-assignment /
|
|
87
|
+
* security). The classifier reads it instead of re-deriving from
|
|
88
|
+
* status — severity captures multi-step reasoning (baseline + attack
|
|
89
|
+
* + follow-up GET) that status alone can't recover. */
|
|
90
|
+
severity?: FindingSeverity;
|
|
91
|
+
/** Run kind for the finding's parent run — currently informational; the
|
|
92
|
+
* classifier may consult it in the future to e.g. downgrade probe-run
|
|
93
|
+
* signal. Captured now so the contract is forward-compatible. */
|
|
94
|
+
run_kind?: RunKind;
|
|
95
|
+
/** Provenance of the failing test (only relevant for test:* classes). */
|
|
96
|
+
provenance?: { type?: string; generator?: string } | null;
|
|
97
|
+
/** Suite path used to detect generator-emitted tests. */
|
|
98
|
+
suite_path?: string | null;
|
|
99
|
+
/** When the env-issue detector flagged the suite, it overrides the
|
|
100
|
+
* classifier's default. Producers set this *after* clustering. */
|
|
101
|
+
baseline_status?: number | null;
|
|
102
|
+
/** ARV-103 (F8): true when at least one assertion on the failing step
|
|
103
|
+
* has `kind: "schema"`. Schema violations are real contract bugs — per
|
|
104
|
+
* zond/SKILL.md L376-377 they should route to report_backend_bug, not
|
|
105
|
+
* fix_test_logic. Producers (db-analysis) set this after walking the
|
|
106
|
+
* step's assertions array. */
|
|
107
|
+
schema_violation?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Decide the action for a finding. Returns `undefined` only for finding
|
|
112
|
+
* classes that intentionally don't carry an action (e.g. severity:low
|
|
113
|
+
* security findings — the producer should leave the field unset rather
|
|
114
|
+
* than coerce a value).
|
|
115
|
+
*/
|
|
116
|
+
export function classify(ctx: ClassifierContext): RecommendedAction | undefined {
|
|
117
|
+
switch (ctx.finding_class) {
|
|
118
|
+
// ── Run-result rows (db diagnose) ───────────────────────────────
|
|
119
|
+
case "test:api_error":
|
|
120
|
+
return "report_backend_bug";
|
|
121
|
+
|
|
122
|
+
case "test:network_error":
|
|
123
|
+
if (ctx.status === 401 || ctx.status === 403) return "fix_auth_config";
|
|
124
|
+
return "fix_network_config";
|
|
125
|
+
|
|
126
|
+
case "test:assertion_failed": {
|
|
127
|
+
// 401/403 → auth always wins.
|
|
128
|
+
if (ctx.status === 401 || ctx.status === 403) return "fix_auth_config";
|
|
129
|
+
// ARV-103 (F8): schema-kind assertions are real contract bugs (the
|
|
130
|
+
// server returned a body that violates its own spec). Route to
|
|
131
|
+
// report_backend_bug — same bucket as 5xx. Skill (zond/SKILL.md
|
|
132
|
+
// L376-377) explicitly says "treat them like 5xx, do not edit the
|
|
133
|
+
// expectation away". Wins over the generator-aware override below
|
|
134
|
+
// because regenerate_suite would silently re-emit the same broken
|
|
135
|
+
// assertion against the same broken response.
|
|
136
|
+
if (ctx.schema_violation) return "report_backend_bug";
|
|
137
|
+
// ARV-42: generator-emitted suites get a different default — editing
|
|
138
|
+
// the YAML gets clobbered on the next `zond audit`.
|
|
139
|
+
const generated = isGeneratedSource(ctx.provenance, ctx.suite_path);
|
|
140
|
+
if (generated) {
|
|
141
|
+
if (ctx.status === 404) return "fix_fixture";
|
|
142
|
+
if (ctx.status === 400 || ctx.status === 422) return "regenerate_suite";
|
|
143
|
+
}
|
|
144
|
+
return "fix_test_logic";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── checks/<id> ────────────────────────────────────────────────
|
|
148
|
+
case "check:status_code_conformance":
|
|
149
|
+
case "check:content_type_conformance":
|
|
150
|
+
case "check:response_headers_conformance":
|
|
151
|
+
case "check:response_schema_conformance":
|
|
152
|
+
return "fix_spec";
|
|
153
|
+
|
|
154
|
+
case "check:not_a_server_error":
|
|
155
|
+
case "check:unsupported_method":
|
|
156
|
+
case "check:positive_data_acceptance":
|
|
157
|
+
case "check:use_after_free":
|
|
158
|
+
case "check:ensure_resource_availability":
|
|
159
|
+
case "check:cross_call_references":
|
|
160
|
+
case "check:idempotency_replay":
|
|
161
|
+
case "check:pagination_invariants":
|
|
162
|
+
case "check:lifecycle_transitions":
|
|
163
|
+
return "report_backend_bug";
|
|
164
|
+
|
|
165
|
+
case "check:negative_data_rejection":
|
|
166
|
+
return "tighten_validation";
|
|
167
|
+
|
|
168
|
+
case "check:missing_required_header":
|
|
169
|
+
return "add_required_header";
|
|
170
|
+
|
|
171
|
+
case "check:ignored_auth":
|
|
172
|
+
case "check:open_cors_on_sensitive":
|
|
173
|
+
return "fix_auth_config";
|
|
174
|
+
|
|
175
|
+
case "check:rate_limit_headers_absent":
|
|
176
|
+
// ARV-256: missing rate-limit on write endpoints is an
|
|
177
|
+
// infrastructure-config gap — closest existing action is
|
|
178
|
+
// "fix_auth_config" (server-side hardening) rather than report-
|
|
179
|
+
// backend-bug (the spec doesn't say rate-limit must exist).
|
|
180
|
+
return "fix_auth_config";
|
|
181
|
+
|
|
182
|
+
case "check:network_error":
|
|
183
|
+
if (ctx.status === 401 || ctx.status === 403) return "fix_auth_config";
|
|
184
|
+
return "fix_network_config";
|
|
185
|
+
|
|
186
|
+
// ── Probe verdicts ─────────────────────────────────────────────
|
|
187
|
+
case "probe:mass_assignment":
|
|
188
|
+
switch (ctx.severity) {
|
|
189
|
+
case "high":
|
|
190
|
+
case "medium":
|
|
191
|
+
case "inconclusive-5xx":
|
|
192
|
+
return "report_backend_bug";
|
|
193
|
+
case "inconclusive-baseline":
|
|
194
|
+
return "fix_fixture";
|
|
195
|
+
default:
|
|
196
|
+
return undefined; // low / ok / skipped: no action
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case "probe:security":
|
|
200
|
+
// TASK-294 policy: high/low both routed to backend-bug. Low is
|
|
201
|
+
// "server returned 2xx without echoing the payload" — still a
|
|
202
|
+
// surprising acceptance the backend should review.
|
|
203
|
+
if (ctx.severity === "high" || ctx.severity === "low") {
|
|
204
|
+
return "report_backend_bug";
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
|
|
208
|
+
// ── Lint findings ─────────────────────────────────────────────
|
|
209
|
+
case "lint:issue":
|
|
210
|
+
return "fix_spec";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isGeneratedSource(
|
|
215
|
+
provenance: ClassifierContext["provenance"],
|
|
216
|
+
suite_path: ClassifierContext["suite_path"],
|
|
217
|
+
): boolean {
|
|
218
|
+
if (provenance?.type === "openapi-generated") return true;
|
|
219
|
+
if (provenance?.generator && provenance.generator.toLowerCase().includes("zond")) return true;
|
|
220
|
+
if (typeof suite_path === "string" && /(^|\/)apis\/[^/]+\/tests\//.test(suite_path)) return true;
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { findWorkspaceRoot } from "../workspace/root.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TASK-290: current-API resolution chain.
|
|
7
|
+
* - Global --api flag is mirrored into ZOND_API_GLOBAL by program.ts preAction.
|
|
8
|
+
* - Users can also export ZOND_API in their shell.
|
|
9
|
+
* - Persisted choice lives in `.zond/current-api` (was `.zond-current`).
|
|
10
|
+
*/
|
|
11
|
+
const FILENAME = ".zond/current-api";
|
|
12
|
+
|
|
13
|
+
export function currentApiPath(cwd?: string): string {
|
|
14
|
+
const base = cwd ?? findWorkspaceRoot().root;
|
|
15
|
+
return join(base, FILENAME);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns the active API name. Resolution order:
|
|
20
|
+
* 1. ZOND_API_GLOBAL (mirrored from `zond --api <name> ...`)
|
|
21
|
+
* 2. ZOND_API (user env)
|
|
22
|
+
* 3. `.zond/current-api` file (set by `zond use <name>`)
|
|
23
|
+
*/
|
|
24
|
+
export function readCurrentApi(cwd?: string): string | null {
|
|
25
|
+
const fromGlobalFlag = process.env.ZOND_API_GLOBAL?.trim();
|
|
26
|
+
if (fromGlobalFlag) return fromGlobalFlag;
|
|
27
|
+
const fromEnv = process.env.ZOND_API?.trim();
|
|
28
|
+
if (fromEnv) return fromEnv;
|
|
29
|
+
const path = currentApiPath(cwd);
|
|
30
|
+
if (!existsSync(path)) return null;
|
|
31
|
+
const raw = readFileSync(path, "utf-8").trim();
|
|
32
|
+
return raw.length > 0 ? raw : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Writes the API collection name to `.zond/current-api`. Creates the dir if needed. */
|
|
36
|
+
export function writeCurrentApi(name: string, cwd?: string): string {
|
|
37
|
+
const trimmed = name.trim();
|
|
38
|
+
if (!trimmed) throw new Error("API name cannot be empty");
|
|
39
|
+
const path = currentApiPath(cwd);
|
|
40
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
41
|
+
writeFileSync(path, trimmed + "\n", "utf-8");
|
|
42
|
+
return path;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Deletes `.zond/current-api`. Returns true when a file was removed, false when it did not exist. */
|
|
46
|
+
export function clearCurrentApi(cwd?: string): boolean {
|
|
47
|
+
const path = currentApiPath(cwd);
|
|
48
|
+
if (!existsSync(path)) return false;
|
|
49
|
+
unlinkSync(path);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { findWorkspaceRoot } from "../workspace/root.ts";
|
|
4
|
+
|
|
5
|
+
const SESSION_DIR = ".zond";
|
|
6
|
+
const SESSION_FILE = "current-session";
|
|
7
|
+
|
|
8
|
+
export interface SessionRecord {
|
|
9
|
+
id: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
started_at: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function sessionFilePath(cwd?: string): string {
|
|
15
|
+
const base = cwd ?? findWorkspaceRoot().root;
|
|
16
|
+
return join(base, SESSION_DIR, SESSION_FILE);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readCurrentSession(cwd?: string): SessionRecord | null {
|
|
20
|
+
const path = sessionFilePath(cwd);
|
|
21
|
+
if (!existsSync(path)) return null;
|
|
22
|
+
const raw = readFileSync(path, "utf-8").trim();
|
|
23
|
+
if (!raw) return null;
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(raw) as Partial<SessionRecord>;
|
|
26
|
+
if (typeof parsed.id !== "string" || parsed.id.length === 0) return null;
|
|
27
|
+
return {
|
|
28
|
+
id: parsed.id,
|
|
29
|
+
label: typeof parsed.label === "string" ? parsed.label : undefined,
|
|
30
|
+
started_at: typeof parsed.started_at === "string" ? parsed.started_at : new Date().toISOString(),
|
|
31
|
+
};
|
|
32
|
+
} catch {
|
|
33
|
+
// legacy / hand-edited single-line UUID
|
|
34
|
+
return { id: raw, started_at: new Date().toISOString() };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function writeCurrentSession(record: SessionRecord, cwd?: string): string {
|
|
39
|
+
const path = sessionFilePath(cwd);
|
|
40
|
+
const dir = path.slice(0, path.lastIndexOf("/"));
|
|
41
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
42
|
+
else {
|
|
43
|
+
const st = statSync(dir);
|
|
44
|
+
if (!st.isDirectory()) {
|
|
45
|
+
throw new Error(`${dir} exists and is not a directory; remove it before running 'zond session start'`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
writeFileSync(path, JSON.stringify(record) + "\n", "utf-8");
|
|
49
|
+
return path;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function clearCurrentSession(cwd?: string): boolean {
|
|
53
|
+
const path = sessionFilePath(cwd);
|
|
54
|
+
if (!existsSync(path)) return false;
|
|
55
|
+
unlinkSync(path);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolution order for the session_id used by `zond run`:
|
|
61
|
+
* 1. explicit --session-id flag
|
|
62
|
+
* 2. ZOND_SESSION_ID env var
|
|
63
|
+
* 3. .zond/current-session file (written by `zond session start`)
|
|
64
|
+
*
|
|
65
|
+
* Returns null when none of the three is set.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveSessionId(opts: {
|
|
68
|
+
flag?: string | null;
|
|
69
|
+
env?: string | null;
|
|
70
|
+
cwd?: string;
|
|
71
|
+
}): string | null {
|
|
72
|
+
const flag = opts.flag?.trim();
|
|
73
|
+
if (flag) return flag;
|
|
74
|
+
const env = opts.env?.trim();
|
|
75
|
+
if (env) return env;
|
|
76
|
+
const record = readCurrentSession(opts.cwd);
|
|
77
|
+
return record?.id ?? null;
|
|
78
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* I/O wrapper around the pure `buildCoverageMatrix` engine — loads the
|
|
3
|
+
* registered API's spec snapshot, parses suites in the workspace to find
|
|
4
|
+
* ephemeral-tagged endpoints, reads `.api-fixtures.yaml` and `.env.yaml`,
|
|
5
|
+
* pulls run results from SQLite, and feeds it all into the engine.
|
|
6
|
+
*
|
|
7
|
+
* Server `/api/coverage`, the HTML exporter, and any future CLI command
|
|
8
|
+
* call this loader so they stay in sync.
|
|
9
|
+
*/
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { findCollectionByNameOrId, getLatestRunByCollection, getResultsByRunId, getRunById, listRunsBySession, listRunsByCollectionFiltered } from "../../db/queries.ts";
|
|
13
|
+
import type { RunRecord } from "../../db/queries.ts";
|
|
14
|
+
import { assertLocalSpec } from "../setup-api.ts";
|
|
15
|
+
import { readOpenApiSpec, extractEndpoints } from "../generator/openapi-reader.ts";
|
|
16
|
+
import { parseDirectorySafe } from "../parser/yaml-parser.ts";
|
|
17
|
+
import { loadEnvironment } from "../parser/variables.ts";
|
|
18
|
+
import { findWorkspaceRoot } from "../workspace/root.ts";
|
|
19
|
+
import {
|
|
20
|
+
buildCoverageMatrix,
|
|
21
|
+
type CoverageMatrix,
|
|
22
|
+
type BuildMatrixInput,
|
|
23
|
+
} from "./reasons.ts";
|
|
24
|
+
|
|
25
|
+
export interface CoverageLoadOptions {
|
|
26
|
+
apiName: string;
|
|
27
|
+
runId?: number;
|
|
28
|
+
/** TASK-255: union across multiple runs (e.g. tests-run + probes-run from
|
|
29
|
+
* the same session). Loader concatenates results from each run before
|
|
30
|
+
* feeding the matrix engine — buildCoverageMatrix is order-agnostic. When
|
|
31
|
+
* set, takes precedence over `runId`. */
|
|
32
|
+
runIds?: number[];
|
|
33
|
+
/** TASK-255: when set, expanded to all runs in the session (filtered to
|
|
34
|
+
* this collection). Wins over `runIds` and `runId`. */
|
|
35
|
+
sessionId?: string;
|
|
36
|
+
/** TASK-274: ISO timestamp lower bound (inclusive) — fold every run of
|
|
37
|
+
* this collection started at-or-after this point. Wins over `runIds`
|
|
38
|
+
* and `runId`, loses to `sessionId`. */
|
|
39
|
+
sinceIso?: string;
|
|
40
|
+
/** TASK-274: tag membership filter — fold every run of this collection
|
|
41
|
+
* whose stored `tags` JSON contains this name. Wins over `runIds`
|
|
42
|
+
* and `runId`, loses to `sessionId` / `sinceIso`. */
|
|
43
|
+
tag?: string;
|
|
44
|
+
profile?: "safe" | "full";
|
|
45
|
+
tagFilter?: string[];
|
|
46
|
+
/** Override workspace root (defaults to findWorkspaceRoot). */
|
|
47
|
+
workspaceRoot?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CoverageLoadResult {
|
|
51
|
+
apiName: string;
|
|
52
|
+
baseDir: string;
|
|
53
|
+
specPath: string;
|
|
54
|
+
matrix: CoverageMatrix;
|
|
55
|
+
/** Latest of the runs included in the coverage. Null if no runs found.
|
|
56
|
+
* Kept singular for back-compat; for the full list use `runs`. */
|
|
57
|
+
run: RunRecord | null;
|
|
58
|
+
/** TASK-255: every run that contributed results to this coverage matrix,
|
|
59
|
+
* ordered by started_at ascending. Single-element when no union flags
|
|
60
|
+
* were used. */
|
|
61
|
+
runs: RunRecord[];
|
|
62
|
+
/** TASK-274: which selector resolved the run set, for diagnostics and
|
|
63
|
+
* the JSON envelope (`union_mode`). null when only a single run
|
|
64
|
+
* contributed and no union selector was used. */
|
|
65
|
+
unionMode: "session" | "since" | "tag" | "runs" | null;
|
|
66
|
+
profile: "safe" | "full";
|
|
67
|
+
tagFilter: string[];
|
|
68
|
+
ephemeralCount: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function readFixturesAffected(baseDir: string): Promise<BuildMatrixInput["fixturesAffected"]> {
|
|
72
|
+
const path = join(baseDir, ".api-fixtures.yaml");
|
|
73
|
+
if (!existsSync(path)) return new Map();
|
|
74
|
+
const text = await Bun.file(path).text();
|
|
75
|
+
const parsed = Bun.YAML.parse(text) as { fixtures?: Array<{ name: string; required: boolean; source: string; affectedEndpoints?: string[] }> };
|
|
76
|
+
const out = new Map<string, { name: string; required: boolean; source: string }[]>();
|
|
77
|
+
for (const f of parsed.fixtures ?? []) {
|
|
78
|
+
for (const ep of f.affectedEndpoints ?? []) {
|
|
79
|
+
if (ep === "*") continue;
|
|
80
|
+
const list = out.get(ep) ?? [];
|
|
81
|
+
list.push({ name: f.name, required: f.required, source: f.source });
|
|
82
|
+
out.set(ep, list);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function readEphemeralEndpoints(workspaceRoot: string): Promise<Set<string>> {
|
|
89
|
+
const out = new Set<string>();
|
|
90
|
+
const { suites } = await parseDirectorySafe(workspaceRoot);
|
|
91
|
+
for (const suite of suites) {
|
|
92
|
+
if (!suite.tags?.includes("ephemeral")) continue;
|
|
93
|
+
for (const t of suite.tests) out.add(`${t.method.toUpperCase()} ${t.path}`);
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function loadCoverage(options: CoverageLoadOptions): Promise<CoverageLoadResult> {
|
|
99
|
+
const root = options.workspaceRoot ?? findWorkspaceRoot().root;
|
|
100
|
+
const collection = findCollectionByNameOrId(options.apiName);
|
|
101
|
+
if (!collection) throw new Error(`API '${options.apiName}' is not registered. Run \`zond add api --spec <path>\`.`);
|
|
102
|
+
|
|
103
|
+
const baseDir = collection.base_dir ?? join(root, "apis", collection.name);
|
|
104
|
+
const specPath = collection.openapi_spec
|
|
105
|
+
? assertLocalSpec(collection.openapi_spec, collection.name)
|
|
106
|
+
: (() => { throw new Error(`Collection '${collection.name}' has no spec recorded.`); })();
|
|
107
|
+
|
|
108
|
+
const doc = await readOpenApiSpec(specPath);
|
|
109
|
+
const endpoints = extractEndpoints(doc);
|
|
110
|
+
|
|
111
|
+
// Resolve which runs contribute results. Precedence:
|
|
112
|
+
// sessionId > sinceIso > tag > runIds > runId > latest. since:/tag: are
|
|
113
|
+
// filtered to this collection only — the user has named an API, so they
|
|
114
|
+
// want runs of that API, not every collection that happens to share a tag.
|
|
115
|
+
let runs: RunRecord[] = [];
|
|
116
|
+
let unionMode: "session" | "since" | "tag" | "runs" | null = null;
|
|
117
|
+
if (options.sessionId) {
|
|
118
|
+
const sessRuns = listRunsBySession(options.sessionId);
|
|
119
|
+
// Include runs whose collection matches AND runs with NULL collection_id —
|
|
120
|
+
// the latter covers probe-suites and ad-hoc runs that didn't tag the API
|
|
121
|
+
// explicitly but still produced results against this session's workdir.
|
|
122
|
+
// Filtering them out makes `coverage --union session` silently show only
|
|
123
|
+
// the test-suite run (the original feedback-12 #F1 symptom).
|
|
124
|
+
runs = sessRuns
|
|
125
|
+
.filter((r) => r.collection_id === collection.id || r.collection_id == null)
|
|
126
|
+
.map((r) => getRunById(r.id))
|
|
127
|
+
.filter((r): r is RunRecord => r !== null);
|
|
128
|
+
unionMode = "session";
|
|
129
|
+
} else if (options.sinceIso) {
|
|
130
|
+
runs = listRunsByCollectionFiltered(collection.id, { since: options.sinceIso });
|
|
131
|
+
unionMode = "since";
|
|
132
|
+
} else if (options.tag) {
|
|
133
|
+
runs = listRunsByCollectionFiltered(collection.id, { tag: options.tag });
|
|
134
|
+
unionMode = "tag";
|
|
135
|
+
} else if (options.runIds && options.runIds.length > 0) {
|
|
136
|
+
runs = options.runIds
|
|
137
|
+
.map((id) => getRunById(id))
|
|
138
|
+
.filter((r): r is RunRecord => r !== null);
|
|
139
|
+
unionMode = "runs";
|
|
140
|
+
} else if (options.runId != null) {
|
|
141
|
+
const r = getRunById(options.runId);
|
|
142
|
+
if (r) runs = [r];
|
|
143
|
+
} else {
|
|
144
|
+
const latest = getLatestRunByCollection(collection.id);
|
|
145
|
+
if (latest) runs = [latest];
|
|
146
|
+
}
|
|
147
|
+
const results = runs.flatMap((r) => getResultsByRunId(r.id));
|
|
148
|
+
// `run` (singular) reflects the latest contributing run for back-compat
|
|
149
|
+
// with consumers that only care about a single run label.
|
|
150
|
+
const run = runs.length > 0 ? runs[runs.length - 1]! : null;
|
|
151
|
+
|
|
152
|
+
const fixturesAffected = await readFixturesAffected(baseDir);
|
|
153
|
+
const ephemeralEndpoints = await readEphemeralEndpoints(root);
|
|
154
|
+
const envVarsObj = await loadEnvironment(undefined, baseDir);
|
|
155
|
+
const envVars = new Set(Object.keys(envVarsObj).filter((k) => {
|
|
156
|
+
const v = envVarsObj[k];
|
|
157
|
+
return typeof v === "string" ? v.length > 0 : v != null;
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
const profile = options.profile ?? "full";
|
|
161
|
+
const tagFilter = options.tagFilter ?? [];
|
|
162
|
+
|
|
163
|
+
const matrix = buildCoverageMatrix({
|
|
164
|
+
endpoints, results, fixturesAffected, envVars, ephemeralEndpoints,
|
|
165
|
+
tagFilter, profile,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
apiName: collection.name,
|
|
170
|
+
baseDir,
|
|
171
|
+
specPath,
|
|
172
|
+
matrix,
|
|
173
|
+
run,
|
|
174
|
+
runs,
|
|
175
|
+
unionMode,
|
|
176
|
+
profile,
|
|
177
|
+
tagFilter,
|
|
178
|
+
ephemeralCount: ephemeralEndpoints.size,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function listRegisteredApiNames(): Promise<string[]> {
|
|
183
|
+
const { listCollections } = await import("../../db/queries.ts");
|
|
184
|
+
return listCollections().map((c) => c.name);
|
|
185
|
+
}
|