@kirrosh/zond 0.22.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.
Files changed (256) hide show
  1. package/CHANGELOG.md +648 -0
  2. package/README.md +58 -6
  3. package/package.json +9 -6
  4. package/src/cli/argv.ts +122 -0
  5. package/src/cli/commands/add-api.ts +134 -0
  6. package/src/cli/commands/api/annotate/idempotency.ts +59 -0
  7. package/src/cli/commands/api/annotate/index.ts +525 -0
  8. package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
  9. package/src/cli/commands/api/annotate/overlay.ts +206 -0
  10. package/src/cli/commands/api/annotate/pagination.ts +60 -0
  11. package/src/cli/commands/api/annotate/prompts.ts +183 -0
  12. package/src/cli/commands/api/annotate/readback.ts +58 -0
  13. package/src/cli/commands/api/annotate/resources.ts +91 -0
  14. package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
  15. package/src/cli/commands/audit.ts +480 -0
  16. package/src/cli/commands/bootstrap.ts +710 -0
  17. package/src/cli/commands/catalog.ts +35 -0
  18. package/src/cli/commands/check.ts +348 -0
  19. package/src/cli/commands/checks.ts +756 -0
  20. package/src/cli/commands/ci-init.ts +43 -0
  21. package/src/cli/commands/clean.ts +212 -0
  22. package/src/cli/commands/cleanup.ts +262 -0
  23. package/src/cli/commands/completions.ts +16 -0
  24. package/src/cli/commands/coverage.ts +605 -132
  25. package/src/cli/commands/db.ts +178 -7
  26. package/src/cli/commands/describe.ts +37 -2
  27. package/src/cli/commands/discover.ts +1236 -0
  28. package/src/cli/commands/doctor.ts +607 -0
  29. package/src/cli/commands/fixtures.ts +402 -0
  30. package/src/cli/commands/generate.ts +420 -46
  31. package/src/cli/commands/init/bootstrap.ts +30 -1
  32. package/src/cli/commands/{init.ts → init/index.ts} +99 -5
  33. package/src/cli/commands/init/skills.ts +56 -3
  34. package/src/cli/commands/init/templates/agents.md +65 -61
  35. package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
  36. package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
  37. package/src/cli/commands/init/templates/skills/zond.md +592 -125
  38. package/src/cli/commands/init/templates/zond-config.yml +8 -9
  39. package/src/cli/commands/prepare-fixtures.ts +135 -0
  40. package/src/cli/commands/probe/mass-assignment.ts +503 -0
  41. package/src/cli/commands/probe/security.ts +454 -0
  42. package/src/cli/commands/probe/static.ts +255 -0
  43. package/src/cli/commands/probe/webhooks.ts +161 -0
  44. package/src/cli/commands/probe.ts +459 -0
  45. package/src/cli/commands/reference.ts +87 -0
  46. package/src/cli/commands/refresh-api.ts +169 -0
  47. package/src/cli/commands/remove-api.ts +150 -0
  48. package/src/cli/commands/report-bundle.ts +318 -0
  49. package/src/cli/commands/report.ts +241 -0
  50. package/src/cli/commands/request.ts +379 -4
  51. package/src/cli/commands/run.ts +842 -53
  52. package/src/cli/commands/session.ts +244 -0
  53. package/src/cli/commands/use.ts +18 -1
  54. package/src/cli/index.ts +20 -3
  55. package/src/cli/json-envelope.ts +112 -3
  56. package/src/cli/json-schemas.ts +263 -0
  57. package/src/cli/program.ts +198 -635
  58. package/src/cli/resolve.ts +105 -0
  59. package/src/cli/status-filter.ts +124 -0
  60. package/src/cli/util/api-context.ts +85 -0
  61. package/src/cli/version.ts +5 -0
  62. package/src/core/anti-fp/bootstrap.ts +34 -0
  63. package/src/core/anti-fp/index.ts +33 -0
  64. package/src/core/anti-fp/registry.ts +44 -0
  65. package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
  66. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
  67. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
  68. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
  69. package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
  70. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
  71. package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
  72. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
  73. package/src/core/anti-fp/types.ts +68 -0
  74. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  75. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  76. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  77. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  78. package/src/core/checks/checks/cross_call_references.ts +134 -0
  79. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  80. package/src/core/checks/checks/idempotency_replay.ts +246 -0
  81. package/src/core/checks/checks/ignored_auth.ts +211 -0
  82. package/src/core/checks/checks/index.ts +65 -0
  83. package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
  84. package/src/core/checks/checks/missing_required_header.ts +40 -0
  85. package/src/core/checks/checks/negative_data_rejection.ts +45 -0
  86. package/src/core/checks/checks/not_a_server_error.ts +27 -0
  87. package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
  88. package/src/core/checks/checks/pagination_invariants.ts +238 -0
  89. package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
  90. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  91. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  92. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  93. package/src/core/checks/checks/status_code_conformance.ts +61 -0
  94. package/src/core/checks/checks/unsupported_method.ts +63 -0
  95. package/src/core/checks/checks/use_after_free.ts +78 -0
  96. package/src/core/checks/index.ts +30 -0
  97. package/src/core/checks/mode.ts +79 -0
  98. package/src/core/checks/recommended-action.ts +64 -0
  99. package/src/core/checks/registry.ts +78 -0
  100. package/src/core/checks/runner.ts +874 -0
  101. package/src/core/checks/sarif.ts +230 -0
  102. package/src/core/checks/stateful.ts +121 -0
  103. package/src/core/checks/types.ts +189 -0
  104. package/src/core/classifier/recommended-action.ts +222 -0
  105. package/src/core/context/current.ts +22 -6
  106. package/src/core/context/session.ts +78 -0
  107. package/src/core/coverage/loader.ts +185 -0
  108. package/src/core/coverage/reasons.ts +300 -0
  109. package/src/core/diagnostics/db-analysis.ts +151 -11
  110. package/src/core/diagnostics/failure-class.ts +120 -0
  111. package/src/core/diagnostics/failure-hints.ts +212 -9
  112. package/src/core/diagnostics/spec-pointer.ts +99 -0
  113. package/src/core/diagnostics/suggested-fixes.ts +156 -0
  114. package/src/core/exporter/case-study/index.ts +270 -0
  115. package/src/core/exporter/curl.ts +40 -0
  116. package/src/core/exporter/exporter.ts +48 -0
  117. package/src/core/exporter/html-report/escape.ts +24 -0
  118. package/src/core/exporter/html-report/index.ts +479 -0
  119. package/src/core/exporter/html-report/script.ts +100 -0
  120. package/src/core/exporter/html-report/styles.ts +408 -0
  121. package/src/core/generator/chunker.ts +42 -16
  122. package/src/core/generator/coverage-phase.ts +0 -0
  123. package/src/core/generator/create-body.ts +89 -0
  124. package/src/core/generator/data-factory.ts +445 -19
  125. package/src/core/generator/describe.ts +1 -1
  126. package/src/core/generator/fixtures-builder.ts +325 -0
  127. package/src/core/generator/index.ts +7 -5
  128. package/src/core/generator/openapi-reader.ts +37 -3
  129. package/src/core/generator/path-param-disambig.ts +114 -0
  130. package/src/core/generator/resources-builder.ts +648 -0
  131. package/src/core/generator/schema-utils.ts +11 -3
  132. package/src/core/generator/serializer.ts +103 -13
  133. package/src/core/generator/suite-generator.ts +419 -111
  134. package/src/core/generator/types.ts +8 -0
  135. package/src/core/identity/identity-file.ts +129 -0
  136. package/src/core/lint/affects.ts +28 -0
  137. package/src/core/lint/config.ts +96 -0
  138. package/src/core/lint/format.ts +42 -0
  139. package/src/core/lint/index.ts +94 -0
  140. package/src/core/lint/reporter.ts +128 -0
  141. package/src/core/lint/rules/consistency.ts +158 -0
  142. package/src/core/lint/rules/heuristics.ts +97 -0
  143. package/src/core/lint/rules/strictness.ts +109 -0
  144. package/src/core/lint/types.ts +96 -0
  145. package/src/core/lint/walker.ts +248 -0
  146. package/src/core/meta/meta-store.ts +6 -73
  147. package/src/core/output/README.md +91 -0
  148. package/src/core/output/index.ts +13 -0
  149. package/src/core/output/run.ts +126 -0
  150. package/src/core/output/types.ts +129 -0
  151. package/src/core/parser/env-interpolation.ts +104 -0
  152. package/src/core/parser/filter.ts +57 -0
  153. package/src/core/parser/schema.ts +129 -4
  154. package/src/core/parser/types.ts +19 -1
  155. package/src/core/parser/variables.ts +0 -0
  156. package/src/core/parser/yaml-parser.ts +58 -12
  157. package/src/core/probe/bootstrap.ts +34 -0
  158. package/src/core/probe/dry-run-envelope.ts +57 -0
  159. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  160. package/src/core/probe/mass-assignment-probe.ts +1122 -0
  161. package/src/core/probe/mass-assignment-template.ts +212 -0
  162. package/src/core/probe/method-probe.ts +43 -76
  163. package/src/core/probe/method-shared.ts +69 -0
  164. package/src/core/probe/negative-probe.ts +183 -149
  165. package/src/core/probe/orphan-tracker.ts +188 -0
  166. package/src/core/probe/path-discovery.ts +440 -0
  167. package/src/core/probe/probe-harness.ts +120 -0
  168. package/src/core/probe/registry.ts +89 -0
  169. package/src/core/probe/runner.ts +136 -0
  170. package/src/core/probe/security-probe-class.ts +201 -0
  171. package/src/core/probe/security-probe.ts +1453 -0
  172. package/src/core/probe/shared.ts +505 -0
  173. package/src/core/probe/static-probe-class.ts +125 -0
  174. package/src/core/probe/types.ts +165 -0
  175. package/src/core/probe/verdict-aggregator.ts +33 -0
  176. package/src/core/probe/webhooks-probe.ts +284 -0
  177. package/src/core/reporter/console.ts +41 -2
  178. package/src/core/reporter/index.ts +2 -3
  179. package/src/core/reporter/json.ts +11 -1
  180. package/src/core/reporter/junit.ts +27 -12
  181. package/src/core/reporter/ndjson.ts +37 -0
  182. package/src/core/reporter/types.ts +3 -0
  183. package/src/core/runner/assertions.ts +58 -1
  184. package/src/core/runner/async-pool.ts +108 -0
  185. package/src/core/runner/auth-path.ts +8 -0
  186. package/src/core/runner/ci-context.ts +72 -0
  187. package/src/core/runner/executor.ts +264 -20
  188. package/src/core/runner/form-encode.ts +51 -0
  189. package/src/core/runner/http-client.ts +75 -2
  190. package/src/core/runner/learn-drift.ts +293 -0
  191. package/src/core/runner/preflight-vars.ts +149 -0
  192. package/src/core/runner/progress-tracker.ts +73 -0
  193. package/src/core/runner/rate-limiter.ts +89 -17
  194. package/src/core/runner/run-kind.ts +39 -0
  195. package/src/core/runner/schema-validator.ts +312 -0
  196. package/src/core/runner/send-request.ts +153 -20
  197. package/src/core/runner/types.ts +38 -0
  198. package/src/core/secrets/registry.ts +164 -0
  199. package/src/core/secrets/secrets-file.ts +115 -0
  200. package/src/core/selectors/operation-filter.ts +144 -0
  201. package/src/core/setup-api.ts +415 -16
  202. package/src/core/severity/category.ts +94 -0
  203. package/src/core/severity/index.ts +121 -0
  204. package/src/core/spec/layers.ts +154 -0
  205. package/src/core/util/format-eta.ts +21 -0
  206. package/src/core/utils.ts +5 -1
  207. package/src/core/workspace/config.ts +129 -0
  208. package/src/core/workspace/manifest.ts +283 -0
  209. package/src/core/workspace/output-rotation.ts +62 -0
  210. package/src/core/workspace/triage-path.ts +87 -0
  211. package/src/db/lint-runs.ts +47 -0
  212. package/src/db/migrate.ts +126 -0
  213. package/src/db/migrations/0001_run_kind.sql +25 -0
  214. package/src/db/migrations/sql.d.ts +4 -0
  215. package/src/db/queries/collections.ts +133 -0
  216. package/src/db/queries/coverage.ts +9 -0
  217. package/src/db/queries/dashboard.ts +59 -0
  218. package/src/db/queries/results.ts +128 -0
  219. package/src/db/queries/runs.ts +235 -0
  220. package/src/db/queries/sessions.ts +42 -0
  221. package/src/db/queries/settings.ts +28 -0
  222. package/src/db/queries/types.ts +172 -0
  223. package/src/db/queries.ts +72 -802
  224. package/src/db/schema.ts +178 -50
  225. package/src/cli/commands/export.ts +0 -144
  226. package/src/cli/commands/guide.ts +0 -127
  227. package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
  228. package/src/cli/commands/probe-methods.ts +0 -108
  229. package/src/cli/commands/probe-validation.ts +0 -124
  230. package/src/cli/commands/serve.ts +0 -114
  231. package/src/cli/commands/sync.ts +0 -268
  232. package/src/cli/commands/update.ts +0 -189
  233. package/src/cli/commands/validate.ts +0 -34
  234. package/src/core/diagnostics/render-md.ts +0 -112
  235. package/src/core/exporter/postman.ts +0 -963
  236. package/src/core/generator/guide-builder.ts +0 -253
  237. package/src/core/meta/types.ts +0 -19
  238. package/src/core/parser/index.ts +0 -21
  239. package/src/core/runner/execute-run.ts +0 -132
  240. package/src/core/runner/index.ts +0 -12
  241. package/src/core/sync/spec-differ.ts +0 -38
  242. package/src/web/data/collection-state.ts +0 -362
  243. package/src/web/routes/api.ts +0 -314
  244. package/src/web/routes/dashboard.ts +0 -350
  245. package/src/web/routes/runs.ts +0 -64
  246. package/src/web/schemas.ts +0 -121
  247. package/src/web/server.ts +0 -134
  248. package/src/web/static/htmx.min.cjs +0 -1
  249. package/src/web/static/style.css +0 -1148
  250. package/src/web/views/endpoints-tab.ts +0 -174
  251. package/src/web/views/explorer-tab.ts +0 -402
  252. package/src/web/views/health-strip.ts +0 -92
  253. package/src/web/views/layout.ts +0 -48
  254. package/src/web/views/results.ts +0 -210
  255. package/src/web/views/runs-tab.ts +0 -126
  256. 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
+ }
@@ -1,32 +1,48 @@
1
- import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
3
  import { findWorkspaceRoot } from "../workspace/root.ts";
4
4
 
5
- const FILENAME = ".zond-current";
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";
6
12
 
7
13
  export function currentApiPath(cwd?: string): string {
8
14
  const base = cwd ?? findWorkspaceRoot().root;
9
15
  return join(base, FILENAME);
10
16
  }
11
17
 
12
- /** Returns the API collection name stored in `.zond-current`, or null when the file is absent or empty. */
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
+ */
13
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;
14
29
  const path = currentApiPath(cwd);
15
30
  if (!existsSync(path)) return null;
16
31
  const raw = readFileSync(path, "utf-8").trim();
17
32
  return raw.length > 0 ? raw : null;
18
33
  }
19
34
 
20
- /** Writes the API collection name to `.zond-current`. The file is single-line plain text. */
35
+ /** Writes the API collection name to `.zond/current-api`. Creates the dir if needed. */
21
36
  export function writeCurrentApi(name: string, cwd?: string): string {
22
37
  const trimmed = name.trim();
23
38
  if (!trimmed) throw new Error("API name cannot be empty");
24
39
  const path = currentApiPath(cwd);
40
+ mkdirSync(dirname(path), { recursive: true });
25
41
  writeFileSync(path, trimmed + "\n", "utf-8");
26
42
  return path;
27
43
  }
28
44
 
29
- /** Deletes `.zond-current`. Returns true when a file was removed, false when it did not exist. */
45
+ /** Deletes `.zond/current-api`. Returns true when a file was removed, false when it did not exist. */
30
46
  export function clearCurrentApi(cwd?: string): boolean {
31
47
  const path = currentApiPath(cwd);
32
48
  if (!existsSync(path)) return false;
@@ -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
+ }