@kirrosh/zond 0.22.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.
Files changed (282) hide show
  1. package/CHANGELOG.md +811 -0
  2. package/README.md +59 -6
  3. package/package.json +9 -7
  4. package/src/CLAUDE.md +112 -0
  5. package/src/cli/argv.ts +122 -0
  6. package/src/cli/commands/add-api.ts +146 -0
  7. package/src/cli/commands/api/annotate/idempotency.ts +59 -0
  8. package/src/cli/commands/api/annotate/index.ts +880 -0
  9. package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
  10. package/src/cli/commands/api/annotate/overlay.ts +206 -0
  11. package/src/cli/commands/api/annotate/pagination.ts +64 -0
  12. package/src/cli/commands/api/annotate/prompts.ts +220 -0
  13. package/src/cli/commands/api/annotate/readback.ts +58 -0
  14. package/src/cli/commands/api/annotate/resources.ts +91 -0
  15. package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
  16. package/src/cli/commands/audit.ts +786 -0
  17. package/src/cli/commands/catalog.ts +35 -0
  18. package/src/cli/commands/check.ts +361 -0
  19. package/src/cli/commands/checks.ts +1072 -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 +236 -0
  23. package/src/cli/commands/completions.ts +16 -0
  24. package/src/cli/commands/coverage.ts +823 -132
  25. package/src/cli/commands/db.ts +486 -12
  26. package/src/cli/commands/describe.ts +37 -2
  27. package/src/cli/commands/discover.ts +1356 -0
  28. package/src/cli/commands/doctor.ts +661 -0
  29. package/src/cli/commands/fixtures.ts +402 -0
  30. package/src/cli/commands/generate.ts +438 -47
  31. package/src/cli/commands/init/bootstrap.ts +34 -2
  32. package/src/cli/commands/{init.ts → init/index.ts} +99 -5
  33. package/src/cli/commands/init/skills.ts +99 -3
  34. package/src/cli/commands/init/templates/agents.md +77 -64
  35. package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
  36. package/src/cli/commands/init/templates/skills/zond-checks.md +621 -0
  37. package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
  38. package/src/cli/commands/init/templates/skills/zond-triage.md +272 -0
  39. package/src/cli/commands/init/templates/skills/zond.md +802 -125
  40. package/src/cli/commands/init/templates/zond-config.yml +8 -9
  41. package/src/cli/commands/prepare-fixtures.ts +97 -0
  42. package/src/cli/commands/probe/_seed-bodies.ts +52 -0
  43. package/src/cli/commands/probe/mass-assignment.ts +594 -0
  44. package/src/cli/commands/probe/security.ts +537 -0
  45. package/src/cli/commands/probe/static.ts +255 -0
  46. package/src/cli/commands/probe/webhooks.ts +163 -0
  47. package/src/cli/commands/probe.ts +535 -0
  48. package/src/cli/commands/reference.ts +87 -0
  49. package/src/cli/commands/refresh-api.ts +227 -0
  50. package/src/cli/commands/remove-api.ts +150 -0
  51. package/src/cli/commands/report-bundle.ts +310 -0
  52. package/src/cli/commands/report.ts +241 -0
  53. package/src/cli/commands/request.ts +495 -4
  54. package/src/cli/commands/run.ts +870 -53
  55. package/src/cli/commands/schema-from-runs.ts +128 -0
  56. package/src/cli/commands/secrets.ts +133 -0
  57. package/src/cli/commands/session.ts +244 -0
  58. package/src/cli/commands/use.ts +18 -1
  59. package/src/cli/index.ts +20 -3
  60. package/src/cli/json-envelope.ts +92 -3
  61. package/src/cli/json-schemas.ts +314 -0
  62. package/src/cli/output.ts +17 -1
  63. package/src/cli/program.ts +199 -635
  64. package/src/cli/resolve.ts +105 -0
  65. package/src/cli/safe-live.ts +24 -0
  66. package/src/cli/status-filter.ts +114 -0
  67. package/src/cli/util/api-context.ts +85 -0
  68. package/src/cli/version.ts +5 -0
  69. package/src/core/audit/persist.ts +183 -0
  70. package/src/core/checks/budget.ts +59 -0
  71. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  72. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  73. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  74. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  75. package/src/core/checks/checks/cross_call_references.ts +147 -0
  76. package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
  77. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  78. package/src/core/checks/checks/idempotency_replay.ts +242 -0
  79. package/src/core/checks/checks/ignored_auth.ts +254 -0
  80. package/src/core/checks/checks/index.ts +68 -0
  81. package/src/core/checks/checks/lifecycle_transitions.ts +416 -0
  82. package/src/core/checks/checks/missing_required_header.ts +40 -0
  83. package/src/core/checks/checks/negative_data_rejection.ts +148 -0
  84. package/src/core/checks/checks/not_a_server_error.ts +35 -0
  85. package/src/core/checks/checks/open_cors_on_sensitive.ts +160 -0
  86. package/src/core/checks/checks/pagination_invariants.ts +419 -0
  87. package/src/core/checks/checks/positive_data_acceptance.ts +33 -0
  88. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  89. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  90. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  91. package/src/core/checks/checks/status_code_conformance.ts +132 -0
  92. package/src/core/checks/checks/unsupported_method.ts +63 -0
  93. package/src/core/checks/checks/use_after_free.ts +78 -0
  94. package/src/core/checks/index.ts +30 -0
  95. package/src/core/checks/mode.ts +82 -0
  96. package/src/core/checks/recommended-action.ts +68 -0
  97. package/src/core/checks/registry.ts +78 -0
  98. package/src/core/checks/runner.ts +1461 -0
  99. package/src/core/checks/sarif.ts +230 -0
  100. package/src/core/checks/spec-findings.ts +308 -0
  101. package/src/core/checks/stateful.ts +121 -0
  102. package/src/core/checks/types.ts +305 -0
  103. package/src/core/checks/zond-extensions.ts +73 -0
  104. package/src/core/classifier/recommended-action.ts +251 -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 +216 -0
  108. package/src/core/coverage/reasons.ts +300 -0
  109. package/src/core/diagnostics/db-analysis.ts +293 -59
  110. package/src/core/diagnostics/failure-class.ts +140 -0
  111. package/src/core/diagnostics/failure-hints.ts +88 -89
  112. package/src/core/diagnostics/spec-pointer.ts +99 -0
  113. package/src/core/diagnostics/suggested-fixes.ts +155 -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 +38 -19
  122. package/src/core/generator/coverage-phase.ts +0 -0
  123. package/src/core/generator/data-factory.ts +586 -22
  124. package/src/core/generator/describe.ts +1 -1
  125. package/src/core/generator/fixtures-builder.ts +332 -0
  126. package/src/core/generator/index.ts +5 -5
  127. package/src/core/generator/openapi-reader.ts +135 -7
  128. package/src/core/generator/path-param-disambig.ts +140 -0
  129. package/src/core/generator/resources-builder.ts +898 -0
  130. package/src/core/generator/schema-utils.ts +33 -3
  131. package/src/core/generator/serializer.ts +103 -13
  132. package/src/core/generator/suite-generator.ts +583 -122
  133. package/src/core/generator/types.ts +14 -0
  134. package/src/core/identity/identity-file.ts +0 -0
  135. package/src/core/lint/affects.ts +28 -0
  136. package/src/core/lint/config.ts +96 -0
  137. package/src/core/lint/format.ts +42 -0
  138. package/src/core/lint/index.ts +94 -0
  139. package/src/core/lint/reporter.ts +128 -0
  140. package/src/core/lint/rules/consistency.ts +158 -0
  141. package/src/core/lint/rules/heuristics.ts +97 -0
  142. package/src/core/lint/rules/strictness.ts +109 -0
  143. package/src/core/lint/types.ts +96 -0
  144. package/src/core/lint/walker.ts +248 -0
  145. package/src/core/meta/meta-store.ts +6 -73
  146. package/src/core/output/README.md +73 -0
  147. package/src/core/output/index.ts +13 -0
  148. package/src/core/output/run.ts +91 -0
  149. package/src/core/output/types.ts +122 -0
  150. package/src/core/parser/dynamic-values.ts +160 -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 +61 -0
  159. package/src/core/probe/mass-assignment/classify.ts +175 -0
  160. package/src/core/probe/mass-assignment/cleanup.ts +52 -0
  161. package/src/core/probe/mass-assignment/digest.ts +114 -0
  162. package/src/core/probe/mass-assignment/orchestrator.ts +459 -0
  163. package/src/core/probe/mass-assignment/regression.ts +141 -0
  164. package/src/core/probe/mass-assignment/suspects.ts +92 -0
  165. package/src/core/probe/mass-assignment/types.ts +135 -0
  166. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  167. package/src/core/probe/mass-assignment-probe.ts +27 -0
  168. package/src/core/probe/mass-assignment-template.ts +240 -0
  169. package/src/core/probe/method-probe.ts +43 -76
  170. package/src/core/probe/method-shared.ts +69 -0
  171. package/src/core/probe/negative-probe.ts +183 -149
  172. package/src/core/probe/orphan-tracker.ts +188 -0
  173. package/src/core/probe/path-discovery.ts +439 -0
  174. package/src/core/probe/probe-harness.ts +119 -0
  175. package/src/core/probe/registry.ts +89 -0
  176. package/src/core/probe/runner.ts +136 -0
  177. package/src/core/probe/security/baseline.ts +174 -0
  178. package/src/core/probe/security/classify.ts +341 -0
  179. package/src/core/probe/security/cleanup.ts +125 -0
  180. package/src/core/probe/security/detectors.ts +71 -0
  181. package/src/core/probe/security/digest.ts +104 -0
  182. package/src/core/probe/security/orchestrator.ts +398 -0
  183. package/src/core/probe/security/regression.ts +103 -0
  184. package/src/core/probe/security/types.ts +151 -0
  185. package/src/core/probe/security-probe-class.ts +207 -0
  186. package/src/core/probe/security-probe.ts +32 -0
  187. package/src/core/probe/shared.ts +531 -0
  188. package/src/core/probe/static-probe-class.ts +125 -0
  189. package/src/core/probe/types.ts +165 -0
  190. package/src/core/probe/verdict-aggregator.ts +33 -0
  191. package/src/core/probe/webhooks-probe.ts +282 -0
  192. package/src/core/reporter/console.ts +41 -2
  193. package/src/core/reporter/index.ts +2 -3
  194. package/src/core/reporter/json.ts +11 -1
  195. package/src/core/reporter/junit.ts +27 -12
  196. package/src/core/reporter/ndjson.ts +37 -0
  197. package/src/core/reporter/types.ts +3 -0
  198. package/src/core/runner/assertions.ts +59 -2
  199. package/src/core/runner/async-pool.ts +108 -0
  200. package/src/core/runner/auth-path.ts +8 -0
  201. package/src/core/runner/ci-context.ts +72 -0
  202. package/src/core/runner/executor.ts +265 -36
  203. package/src/core/runner/form-encode.ts +41 -0
  204. package/src/core/runner/http-client.ts +112 -2
  205. package/src/core/runner/learn-drift.ts +293 -0
  206. package/src/core/runner/preflight-vars.ts +153 -0
  207. package/src/core/runner/progress-tracker.ts +73 -0
  208. package/src/core/runner/rate-limiter.ts +87 -33
  209. package/src/core/runner/run-kind.ts +45 -0
  210. package/src/core/runner/schema-validator.ts +308 -0
  211. package/src/core/runner/send-request.ts +158 -20
  212. package/src/core/runner/types.ts +44 -0
  213. package/src/core/secrets/registry.ts +164 -0
  214. package/src/core/secrets/secrets-file.ts +115 -0
  215. package/src/core/selectors/operation-filter.ts +144 -0
  216. package/src/core/setup-api.ts +457 -20
  217. package/src/core/severity/category.ts +94 -0
  218. package/src/core/severity/index.ts +58 -0
  219. package/src/core/spec/infer-schema.ts +102 -0
  220. package/src/core/spec/layers.ts +154 -0
  221. package/src/core/spec/merge-specs.ts +156 -0
  222. package/src/core/spec/schema-from-runs.ts +117 -0
  223. package/src/core/spec/schema-overlay.ts +130 -0
  224. package/src/core/util/ajv.ts +13 -0
  225. package/src/core/util/format-eta.ts +21 -0
  226. package/src/core/util/headers.ts +9 -0
  227. package/src/core/util/url.ts +24 -0
  228. package/src/core/utils.ts +5 -1
  229. package/src/core/workspace/config.ts +129 -0
  230. package/src/core/workspace/fixture-gap-report.ts +84 -0
  231. package/src/core/workspace/fixture-gaps.ts +71 -0
  232. package/src/core/workspace/manifest.ts +283 -0
  233. package/src/core/workspace/output-rotation.ts +62 -0
  234. package/src/core/workspace/root.ts +13 -11
  235. package/src/core/workspace/triage-path.ts +87 -0
  236. package/src/db/lint-runs.ts +47 -0
  237. package/src/db/migrate.ts +128 -0
  238. package/src/db/migrations/0001_run_kind.sql +25 -0
  239. package/src/db/migrations/0002_run_kind_request.sql +59 -0
  240. package/src/db/migrations/sql.d.ts +4 -0
  241. package/src/db/queries/collections.ts +133 -0
  242. package/src/db/queries/coverage.ts +9 -0
  243. package/src/db/queries/dashboard.ts +59 -0
  244. package/src/db/queries/results.ts +216 -0
  245. package/src/db/queries/runs.ts +289 -0
  246. package/src/db/queries/sessions.ts +42 -0
  247. package/src/db/queries/settings.ts +28 -0
  248. package/src/db/queries/types.ts +172 -0
  249. package/src/db/queries.ts +75 -802
  250. package/src/db/schema.ts +178 -50
  251. package/src/cli/commands/export.ts +0 -144
  252. package/src/cli/commands/guide.ts +0 -127
  253. package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
  254. package/src/cli/commands/probe-methods.ts +0 -108
  255. package/src/cli/commands/probe-validation.ts +0 -124
  256. package/src/cli/commands/serve.ts +0 -114
  257. package/src/cli/commands/sync.ts +0 -268
  258. package/src/cli/commands/update.ts +0 -189
  259. package/src/cli/commands/validate.ts +0 -34
  260. package/src/core/diagnostics/render-md.ts +0 -112
  261. package/src/core/exporter/postman.ts +0 -963
  262. package/src/core/generator/guide-builder.ts +0 -253
  263. package/src/core/meta/types.ts +0 -19
  264. package/src/core/parser/index.ts +0 -21
  265. package/src/core/runner/execute-run.ts +0 -132
  266. package/src/core/runner/index.ts +0 -12
  267. package/src/core/sync/spec-differ.ts +0 -38
  268. package/src/web/data/collection-state.ts +0 -362
  269. package/src/web/routes/api.ts +0 -314
  270. package/src/web/routes/dashboard.ts +0 -350
  271. package/src/web/routes/runs.ts +0 -64
  272. package/src/web/schemas.ts +0 -121
  273. package/src/web/server.ts +0 -134
  274. package/src/web/static/htmx.min.cjs +0 -1
  275. package/src/web/static/style.css +0 -1148
  276. package/src/web/views/endpoints-tab.ts +0 -174
  277. package/src/web/views/explorer-tab.ts +0 -402
  278. package/src/web/views/health-strip.ts +0 -92
  279. package/src/web/views/layout.ts +0 -48
  280. package/src/web/views/results.ts +0 -210
  281. package/src/web/views/runs-tab.ts +0 -126
  282. package/src/web/views/suites-tab.ts +0 -181
@@ -0,0 +1,109 @@
1
+ import type { OpenAPIV3 } from "openapi-types";
2
+ import type { Issue, RuleId, Severity, HeuristicConfig } from "../types.ts";
3
+ import { DEFAULT_SEVERITY } from "../types.ts";
4
+ import type { ParamContext, ResponseContext, RequestBodyContext, SchemaContext } from "../walker.ts";
5
+ import { normalisedTypes } from "../walker.ts";
6
+
7
+ interface RuleSink {
8
+ push(rule: RuleId, severity: Severity, message: string, opts: Partial<Pick<Issue, "path" | "method" | "fix_hint">> & { jsonpointer: string }): void;
9
+ }
10
+
11
+ /**
12
+ * Group B — formal strictness gaps on parameters.
13
+ * B1 (path-param without format/pattern) → high.
14
+ * B3 (integer-param without min/max — pagination names get medium, others low).
15
+ * B4 (cursor-name string-param without minLength: 1) → low.
16
+ */
17
+ export function runParamStrictnessRules(ctx: ParamContext, sink: RuleSink, h: HeuristicConfig): void {
18
+ const p = ctx.param;
19
+ const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject;
20
+ const types = normalisedTypes(schema);
21
+
22
+ if (p.in === "path" && types.includes("string")) {
23
+ // Only flag string path-params: integer/number params have type-level
24
+ // constraints (minimum/maximum, multipleOf), so missing format/pattern
25
+ // is benign there.
26
+ const hasFormat = !!(schema as { format?: string }).format;
27
+ const hasPattern = !!(schema as { pattern?: string }).pattern;
28
+ if (!hasFormat && !hasPattern) {
29
+ sink.push("B1", DEFAULT_SEVERITY.B1, `path-param "${p.name}" missing format/pattern`, {
30
+ jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
31
+ fix_hint: "add format: uuid (or pattern: ^...$) so SDKs reject malformed values client-side",
32
+ });
33
+ }
34
+ }
35
+
36
+ if (types.includes("integer") || types.includes("number")) {
37
+ const hasMin = typeof (schema as { minimum?: unknown }).minimum === "number";
38
+ const hasMax = typeof (schema as { maximum?: unknown }).maximum === "number";
39
+ if (!hasMin || !hasMax) {
40
+ const isPagination = h.pagination_names.some(n => n.toLowerCase() === p.name.toLowerCase());
41
+ const sev: Severity = isPagination ? "medium" : "low";
42
+ const which = !hasMin && !hasMax ? "minimum/maximum" : !hasMin ? "minimum" : "maximum";
43
+ sink.push("B3", sev, `${p.in}-param "${p.name}" (${types.join("|")}) missing ${which}`, {
44
+ jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
45
+ fix_hint: `add ${which} so out-of-range values are rejected before reaching the server`,
46
+ });
47
+ }
48
+ }
49
+
50
+ if (types.includes("string") && h.cursor_names.some(n => n.toLowerCase() === p.name.toLowerCase())) {
51
+ const minLen = (schema as { minLength?: unknown }).minLength;
52
+ if (typeof minLen !== "number" || minLen < 1) {
53
+ sink.push("B4", DEFAULT_SEVERITY.B4, `cursor-param "${p.name}" missing minLength: 1`, {
54
+ jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
55
+ fix_hint: "add minLength: 1 so empty cursor strings are rejected client-side",
56
+ });
57
+ }
58
+ }
59
+ }
60
+
61
+ /**
62
+ * B7 — 2xx response without a JSON schema. `--validate-schema` silently skips
63
+ * such endpoints, masking real type drift.
64
+ */
65
+ export function runResponseStrictnessRules(ctx: ResponseContext, sink: RuleSink): void {
66
+ const status = parseInt(ctx.status, 10);
67
+ if (!Number.isFinite(status) || status < 200 || status >= 300) return;
68
+ // 204 No Content / 205 Reset Content / 304 Not Modified by definition carry no body.
69
+ if (status === 204 || status === 205) return;
70
+ const r = ctx.response;
71
+ const hasJsonSchema = r.content && Object.entries(r.content).some(([ct, mt]) => {
72
+ return ct.includes("json") && (mt.schema !== undefined);
73
+ });
74
+ if (!hasJsonSchema) {
75
+ sink.push("B7", DEFAULT_SEVERITY.B7, `${ctx.status} response missing JSON schema`, {
76
+ jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
77
+ fix_hint: "declare content.application/json.schema so --validate-schema can verify the response",
78
+ });
79
+ }
80
+ }
81
+
82
+ /**
83
+ * B8 — request-body root schema without explicit `additionalProperties`.
84
+ * Informational; tied to mass-assignment risk (T58).
85
+ */
86
+ export function runRequestBodyStrictnessRules(ctx: RequestBodyContext, sink: RuleSink): void {
87
+ if (!ctx.requestBody.content) return;
88
+ for (const [ct, mt] of Object.entries(ctx.requestBody.content)) {
89
+ if (!ct.includes("json")) continue;
90
+ const schema = mt.schema as OpenAPIV3.SchemaObject | undefined;
91
+ if (!schema) continue;
92
+ const ap = (schema as { additionalProperties?: unknown }).additionalProperties;
93
+ if (ap === undefined) {
94
+ sink.push("B8", DEFAULT_SEVERITY.B8, `request body schema does not set additionalProperties`, {
95
+ jsonpointer: `${ctx.jsonpointer}/content/${ct.replace(/~/g, "~0").replace(/\//g, "~1")}/schema`,
96
+ path: ctx.path, method: ctx.method,
97
+ fix_hint: "set additionalProperties: false to make mass-assignment vectors explicit",
98
+ });
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Schema-level B-rules that aren't on parameters: currently empty placeholder
105
+ * for future expansion (e.g. response-body B5 picked up via heuristics).
106
+ */
107
+ export function runSchemaStrictnessRules(_ctx: SchemaContext, _sink: RuleSink): void {
108
+ // intentionally empty — heuristic schema-level checks live in heuristics.ts
109
+ }
@@ -0,0 +1,96 @@
1
+ import type { RecommendedAction } from "../diagnostics/failure-hints.ts";
2
+
3
+ // Severity unified in src/core/severity (ARV-250). Lint historically used a
4
+ // 3-tier ladder (high/medium/low) without 'critical' or 'info'. ARV-255
5
+ // will downgrade most lint findings to info/low; this re-export aligns the
6
+ // type but does not yet change DEFAULT_SEVERITY values per rule.
7
+ import type { Severity } from "../severity/index.ts";
8
+ export type { Severity };
9
+
10
+ export type { RecommendedAction };
11
+
12
+ export type RuleId =
13
+ | "A1" | "A2" | "A3" | "A4" | "A5" | "A6"
14
+ | "B1" | "B2" | "B3" | "B4" | "B5" | "B6" | "B7" | "B8" | "B9";
15
+
16
+ export const ALL_RULES: RuleId[] = [
17
+ "A1", "A2", "A3", "A4", "A5", "A6",
18
+ "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9",
19
+ ];
20
+
21
+ /**
22
+ * ARV-255 (m-21 pivot): all lint findings cap at LOW/INFO. Static spec
23
+ * analysis is hygiene — no runtime evidence, no exploit pathway, no
24
+ * security or contract drift. The old "HIGH on missing additionalProperties"
25
+ * inflation made the audit report unreadable; now lint lives in the
26
+ * hygiene category and surfaces via `zond lint` separately.
27
+ *
28
+ * Tier assignment:
29
+ * - `low`: real spec violations (format mismatch in example, missing
30
+ * path-param format, response without schema). Worth fixing, but not
31
+ * security.
32
+ * - `info`: style and documentation gaps (additionalProperties, naming,
33
+ * missing examples, optional descriptions). Could be intentional.
34
+ */
35
+ export const DEFAULT_SEVERITY: Record<RuleId, Severity> = {
36
+ A1: "low", A2: "low", A3: "info", A4: "info", A5: "info", A6: "info",
37
+ B1: "low", B2: "info", B3: "info", B4: "info", B5: "info",
38
+ B6: "info", B7: "low", B8: "info", B9: "info",
39
+ };
40
+
41
+ export interface Issue {
42
+ rule: RuleId;
43
+ severity: Severity;
44
+ path?: string;
45
+ method?: string;
46
+ jsonpointer: string;
47
+ message: string;
48
+ fix_hint?: string;
49
+ affects?: string[];
50
+ /** TASK-294: agent-routable action — always `fix_spec` for lint issues
51
+ * (the spec is the source of truth and the only thing to edit). */
52
+ recommended_action: RecommendedAction;
53
+ }
54
+
55
+ export type RuleSetting = "off" | Severity;
56
+
57
+ export interface HeuristicConfig {
58
+ id_suffixes: string[];
59
+ timestamp_suffixes: string[];
60
+ url_names: string[];
61
+ cursor_names: string[];
62
+ pagination_names: string[];
63
+ semantic_required: string[];
64
+ }
65
+
66
+ export interface LintConfig {
67
+ rules: Partial<Record<RuleId, RuleSetting>>;
68
+ heuristics: HeuristicConfig;
69
+ ignore_paths: string[];
70
+ include_paths?: string[];
71
+ max_issues?: number;
72
+ }
73
+
74
+ export interface LintStats {
75
+ total: number;
76
+ critical: number;
77
+ high: number;
78
+ medium: number;
79
+ low: number;
80
+ info: number;
81
+ endpoints: number;
82
+ }
83
+
84
+ export interface LintResult {
85
+ issues: Issue[];
86
+ stats: LintStats;
87
+ }
88
+
89
+ export const DEFAULT_HEURISTICS: HeuristicConfig = {
90
+ id_suffixes: ["_id", "Id", "ID"],
91
+ timestamp_suffixes: ["_at", "_date", "_time"],
92
+ url_names: ["url", "website", "homepage", "callback_url", "webhook_url"],
93
+ cursor_names: ["after", "before", "cursor", "token", "page_token", "next_token"],
94
+ pagination_names: ["limit", "offset", "page", "size", "count", "per_page", "page_size"],
95
+ semantic_required: ["name", "email", "title"],
96
+ };
@@ -0,0 +1,248 @@
1
+ import type { OpenAPIV3 } from "openapi-types";
2
+
3
+ const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"] as const;
4
+
5
+ export type SchemaContextKind =
6
+ | "param-schema"
7
+ | "request-body"
8
+ | "response-body"
9
+ | "property";
10
+
11
+ export interface ParamContext {
12
+ kind: "parameter";
13
+ jsonpointer: string;
14
+ path: string;
15
+ method: string;
16
+ param: OpenAPIV3.ParameterObject;
17
+ }
18
+
19
+ export interface ResponseContext {
20
+ kind: "response";
21
+ jsonpointer: string;
22
+ path: string;
23
+ method: string;
24
+ status: string;
25
+ response: OpenAPIV3.ResponseObject;
26
+ }
27
+
28
+ export interface RequestBodyContext {
29
+ kind: "requestBody";
30
+ jsonpointer: string;
31
+ path: string;
32
+ method: string;
33
+ requestBody: OpenAPIV3.RequestBodyObject;
34
+ }
35
+
36
+ export interface SchemaContext {
37
+ kind: "schema";
38
+ jsonpointer: string;
39
+ path?: string;
40
+ method?: string;
41
+ origin: SchemaContextKind;
42
+ /** Property name if this schema is a value of `properties.<name>`. */
43
+ propertyName?: string;
44
+ schema: OpenAPIV3.SchemaObject;
45
+ }
46
+
47
+ export type WalkContext = ParamContext | ResponseContext | RequestBodyContext | SchemaContext;
48
+
49
+ export type Visitor = (ctx: WalkContext) => void;
50
+
51
+ /**
52
+ * RFC6901 segment encoder: `~` → `~0`, `/` → `~1`.
53
+ */
54
+ function escapePointerSegment(s: string | number): string {
55
+ return String(s).replace(/~/g, "~0").replace(/\//g, "~1");
56
+ }
57
+
58
+ /**
59
+ * Walk an OpenAPI 3.x document, invoking `visit` for parameters, request bodies,
60
+ * responses, and every nested schema (recursively through properties / items /
61
+ * combinators). Each call carries a stable RFC6901 jsonpointer so issues can
62
+ * point precisely to the source.
63
+ *
64
+ * Cycles (already-visited schema objects by reference) are short-circuited so
65
+ * `@readme/openapi-parser`-resolved $ref-cycles don't loop.
66
+ */
67
+ export function walk(doc: OpenAPIV3.Document, visit: Visitor): void {
68
+ if (!doc.paths) return;
69
+ for (const [path, pathItem] of Object.entries(doc.paths)) {
70
+ if (!pathItem) continue;
71
+ const pathPtr = `/paths/${escapePointerSegment(path)}`;
72
+
73
+ // Path-level parameters
74
+ if (pathItem.parameters) {
75
+ pathItem.parameters.forEach((p, idx) => {
76
+ const param = p as OpenAPIV3.ParameterObject;
77
+ const ptr = `${pathPtr}/parameters/${idx}`;
78
+ visit({ kind: "parameter", jsonpointer: ptr, path, method: "*", param });
79
+ if (param.schema) {
80
+ walkSchema(
81
+ param.schema as OpenAPIV3.SchemaObject,
82
+ `${ptr}/schema`,
83
+ { origin: "param-schema", path, method: "*" },
84
+ visit,
85
+ new Set(),
86
+ );
87
+ }
88
+ });
89
+ }
90
+
91
+ for (const m of HTTP_METHODS) {
92
+ const op = (pathItem as Record<string, unknown>)[m] as OpenAPIV3.OperationObject | undefined;
93
+ if (!op) continue;
94
+ const opPtr = `${pathPtr}/${m}`;
95
+ const method = m.toUpperCase();
96
+
97
+ // Operation-level parameters
98
+ if (op.parameters) {
99
+ op.parameters.forEach((p, idx) => {
100
+ const param = p as OpenAPIV3.ParameterObject;
101
+ const ptr = `${opPtr}/parameters/${idx}`;
102
+ visit({ kind: "parameter", jsonpointer: ptr, path, method, param });
103
+ if (param.schema) {
104
+ walkSchema(
105
+ param.schema as OpenAPIV3.SchemaObject,
106
+ `${ptr}/schema`,
107
+ { origin: "param-schema", path, method },
108
+ visit,
109
+ new Set(),
110
+ );
111
+ }
112
+ });
113
+ }
114
+
115
+ // Request body
116
+ if (op.requestBody) {
117
+ const rb = op.requestBody as OpenAPIV3.RequestBodyObject;
118
+ const rbPtr = `${opPtr}/requestBody`;
119
+ visit({ kind: "requestBody", jsonpointer: rbPtr, path, method, requestBody: rb });
120
+ if (rb.content) {
121
+ for (const [ct, mt] of Object.entries(rb.content)) {
122
+ if (mt.schema) {
123
+ walkSchema(
124
+ mt.schema as OpenAPIV3.SchemaObject,
125
+ `${rbPtr}/content/${escapePointerSegment(ct)}/schema`,
126
+ { origin: "request-body", path, method },
127
+ visit,
128
+ new Set(),
129
+ );
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ // Responses
136
+ if (op.responses) {
137
+ for (const [status, respObj] of Object.entries(op.responses)) {
138
+ const resp = respObj as OpenAPIV3.ResponseObject;
139
+ const respPtr = `${opPtr}/responses/${escapePointerSegment(status)}`;
140
+ visit({ kind: "response", jsonpointer: respPtr, path, method, status, response: resp });
141
+ if (resp.content) {
142
+ for (const [ct, mt] of Object.entries(resp.content)) {
143
+ if (mt.schema) {
144
+ walkSchema(
145
+ mt.schema as OpenAPIV3.SchemaObject,
146
+ `${respPtr}/content/${escapePointerSegment(ct)}/schema`,
147
+ { origin: "response-body", path, method },
148
+ visit,
149
+ new Set(),
150
+ );
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ interface WalkSchemaCtx {
161
+ origin: SchemaContextKind;
162
+ path?: string;
163
+ method?: string;
164
+ propertyName?: string;
165
+ }
166
+
167
+ function walkSchema(
168
+ schema: OpenAPIV3.SchemaObject,
169
+ pointer: string,
170
+ ctx: WalkSchemaCtx,
171
+ visit: Visitor,
172
+ visited: Set<unknown>,
173
+ ): void {
174
+ if (!schema || typeof schema !== "object") return;
175
+ if (visited.has(schema)) return;
176
+ visited.add(schema);
177
+
178
+ visit({
179
+ kind: "schema",
180
+ jsonpointer: pointer,
181
+ path: ctx.path,
182
+ method: ctx.method,
183
+ origin: ctx.origin,
184
+ propertyName: ctx.propertyName,
185
+ schema,
186
+ });
187
+
188
+ if (schema.properties) {
189
+ for (const [name, sub] of Object.entries(schema.properties)) {
190
+ walkSchema(
191
+ sub as OpenAPIV3.SchemaObject,
192
+ `${pointer}/properties/${escapePointerSegment(name)}`,
193
+ { origin: "property", path: ctx.path, method: ctx.method, propertyName: name },
194
+ visit,
195
+ visited,
196
+ );
197
+ }
198
+ }
199
+
200
+ const arraySchema = schema as OpenAPIV3.ArraySchemaObject;
201
+ if (arraySchema.items) {
202
+ walkSchema(
203
+ arraySchema.items as OpenAPIV3.SchemaObject,
204
+ `${pointer}/items`,
205
+ { ...ctx, propertyName: undefined },
206
+ visit,
207
+ visited,
208
+ );
209
+ }
210
+
211
+ for (const combinator of ["allOf", "anyOf", "oneOf"] as const) {
212
+ const arr = (schema as Record<string, unknown>)[combinator] as OpenAPIV3.SchemaObject[] | undefined;
213
+ if (Array.isArray(arr)) {
214
+ arr.forEach((sub, idx) => {
215
+ walkSchema(
216
+ sub,
217
+ `${pointer}/${combinator}/${idx}`,
218
+ { ...ctx, propertyName: undefined },
219
+ visit,
220
+ visited,
221
+ );
222
+ });
223
+ }
224
+ }
225
+
226
+ if (typeof schema.additionalProperties === "object" && schema.additionalProperties !== null) {
227
+ walkSchema(
228
+ schema.additionalProperties as OpenAPIV3.SchemaObject,
229
+ `${pointer}/additionalProperties`,
230
+ { ...ctx, propertyName: undefined },
231
+ visit,
232
+ visited,
233
+ );
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Normalise OpenAPI 3.0 `nullable: true` so callers can reason about a single
239
+ * type list. Returns the (possibly array) type without mutating the schema.
240
+ */
241
+ export function normalisedTypes(schema: OpenAPIV3.SchemaObject): string[] {
242
+ const t = (schema as { type?: string | string[] }).type;
243
+ const list: string[] = Array.isArray(t) ? [...t] : t ? [t] : [];
244
+ if ((schema as { nullable?: boolean }).nullable === true && !list.includes("null")) {
245
+ list.push("null");
246
+ }
247
+ return list;
248
+ }
@@ -1,78 +1,11 @@
1
- import { join } from "path";
2
1
  import { createHash } from "crypto";
3
- import type { ZondMeta, FileMeta } from "./types.ts";
4
- import type { RawSuite } from "../generator/serializer.ts";
5
- import { normalizePath } from "../generator/coverage-scanner.ts";
6
-
7
- const META_FILENAME = ".zond-meta.json";
8
-
9
- export async function readMeta(testsDir: string): Promise<ZondMeta | null> {
10
- const metaPath = join(testsDir, META_FILENAME);
11
- const file = Bun.file(metaPath);
12
- if (!(await file.exists())) return null;
13
- try {
14
- return JSON.parse(await file.text()) as ZondMeta;
15
- } catch {
16
- return null;
17
- }
18
- }
19
-
20
- export async function writeMeta(testsDir: string, meta: ZondMeta): Promise<void> {
21
- const metaPath = join(testsDir, META_FILENAME);
22
- await Bun.write(metaPath, JSON.stringify(meta, null, 2) + "\n");
23
- }
24
-
25
- export function hashSpec(specContent: string): string {
26
- return createHash("sha256").update(specContent).digest("hex");
27
- }
28
2
 
29
3
  /**
30
- * Derive suite type from tags array or filename.
4
+ * SHA-256 of the canonical (decycled) JSON form of an OpenAPI document.
5
+ * Used as the freshness hash recorded in `.api-catalog.yaml`,
6
+ * `.api-resources.yaml`, and `.api-fixtures.yaml` so `zond doctor` can
7
+ * detect drift between the local snapshot and its derived artifacts.
31
8
  */
32
- function detectSuiteType(suite: RawSuite): FileMeta["suiteType"] {
33
- const tags = suite.tags ?? [];
34
- if (tags.includes("auth")) return "auth";
35
- if (tags.includes("sanity")) return "sanity";
36
- if (tags.includes("crud")) return "crud";
37
- if (tags.includes("unsafe")) return "unsafe";
38
- return "smoke";
39
- }
40
-
41
- /**
42
- * Extract first tag from fileStem or suite folder for grouping.
43
- * e.g. fileStem "smoke-users" → tag "users"
44
- */
45
- function detectTag(suite: RawSuite): string | undefined {
46
- const stem = suite.fileStem ?? suite.name;
47
- const match = stem.match(/^(?:smoke|crud|auth|sanity|unsafe)-(.+?)(?:-unsafe)?$/);
48
- return match?.[1];
49
- }
50
-
51
- /**
52
- * Build normalized endpoint keys from a raw suite's test steps.
53
- * e.g. "GET /users/{*}", "POST /users"
54
- */
55
- function extractEndpointKeys(suite: RawSuite): string[] {
56
- const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
57
- const keys: string[] = [];
58
- for (const step of suite.tests) {
59
- for (const method of HTTP_METHODS) {
60
- const path = step[method] as string | undefined;
61
- if (path) {
62
- keys.push(`${method} ${normalizePath(path)}`);
63
- break;
64
- }
65
- }
66
- }
67
- return [...new Set(keys)];
68
- }
69
-
70
- export function buildFileMeta(suite: RawSuite, zondVersion: string): FileMeta {
71
- return {
72
- generatedAt: new Date().toISOString(),
73
- zondVersion,
74
- suiteType: detectSuiteType(suite),
75
- tag: detectTag(suite),
76
- endpoints: extractEndpointKeys(suite),
77
- };
9
+ export function hashSpec(specContent: string): string {
10
+ return createHash("sha256").update(specContent).digest("hex");
78
11
  }
@@ -0,0 +1,73 @@
1
+ # core/output — typed `--report` / `--output` / `--json` policy
2
+
3
+ `OutputSpec<Payload>` is the single source-of-truth for how a command
4
+ produces output. Closes the seven divergent-output bugs collected in
5
+ `strategy/lessons.md` §E (ARV-50, ARV-82, ARV-97, …) by replacing N
6
+ ad-hoc parsers with one resolver.
7
+
8
+ ## Policy matrix
9
+
10
+ `resolveOutput(spec, opts)` reads `--report`, `--output`, and `--json`
11
+ from the CLI layer and resolves them to a `ResolvedOutput` decision
12
+ according to this matrix:
13
+
14
+ | Input | Format | Channel | Notes |
15
+ | ------------------------------------ | -------------- | ------------------- | ----- |
16
+ | _(nothing)_ | `defaultFormat`| from format policy | bare invocation |
17
+ | `--report <fmt>` | `<fmt>` (or alias) | from format policy | unknown → error |
18
+ | `--json` | first format with `envelopeWrap: true` | from format policy | falls back to `defaultFormat` when no envelope-wrap format exists |
19
+ | `--output <path>` | unchanged | `file` (path = resolved absolute) | `--output` always wins over `defaultChannel` |
20
+ | `--report sarif` (defaults to file) | `sarif` | `file` (`defaultFilename` if no `--output`) | ARV-5 default `zond-checks.sarif` |
21
+ | `--report ndjson` (defaults to stdout) | `ndjson` | `stdout` | event stream — see ARV-10 |
22
+ | `--report ndjson --output <path>` | `ndjson` | `file` | ARV-97 — explicit `--output` redirects the stream |
23
+ | `--json` + `--report <fmt>` | — | — | mutually exclusive (throws `OutputSpecError`) |
24
+
25
+ Aliases (`spec.aliases`) let a single CLI flag value resolve to
26
+ another format — used by `checks run` so `--report ndjson` continues to
27
+ mean "the `--ndjson` streaming flag", matching skill-prompt
28
+ expectations (ARV-63).
29
+
30
+ ## Building a spec
31
+
32
+ ```ts
33
+ import type { OutputSpec } from "@/core/output";
34
+
35
+ interface ChecksPayload { findings: Finding[]; summary: Summary }
36
+
37
+ export const CHECKS_RUN_OUTPUT: OutputSpec<ChecksPayload> = {
38
+ command: "checks run",
39
+ defaultFormat: "json",
40
+ formats: {
41
+ json: { defaultChannel: "stdout", envelopeWrap: true, description: "JSON envelope" },
42
+ sarif: { defaultChannel: "file", defaultFilename: "zond-checks.sarif" },
43
+ ndjson: { defaultChannel: "stdout", description: "event stream — one JSON per line" },
44
+ },
45
+ aliases: {
46
+ // `--report ndjson` is a friendly alias retained from skill prompts.
47
+ ndjson: "ndjson",
48
+ },
49
+ };
50
+ ```
51
+
52
+ The CLI handler calls `resolveOutput()` and renders/writes the payload
53
+ itself — each command's shape (streaming vs single-shot, envelope vs
54
+ raw) differs enough that a shared render/write runner added indirection
55
+ without removing per-command logic. `checks run` and `probe *` open an
56
+ fd from `resolved.path`/`resolved.channel` and feed output into it
57
+ incrementally; `run` renders a single payload and writes it once. See
58
+ any of `src/cli/commands/{run,checks,probe}.ts` for the pattern.
59
+
60
+ ## Why the format set is open
61
+
62
+ Each command owns its own format vocabulary. `run` ships
63
+ `json`/`junit`; `checks run` ships `json`/`sarif`/`ndjson`; `probe *`
64
+ ships `json` only. Declaring formats per-spec instead of in a global
65
+ union avoids a leaky enum and keeps `--help` accurate per command.
66
+
67
+ ## Errors
68
+
69
+ `resolveOutput` throws `OutputSpecError` for policy violations
70
+ (`--json + --report`, unknown format, file-default without filename).
71
+ The CLI handler catches it and produces either `jsonError` or a human
72
+ `printError`, matching the rest of the codebase's input-error
73
+ behaviour (exit code 2).
@@ -0,0 +1,13 @@
1
+ /**
2
+ * ARV-116 (m-19): public surface of the output-spec module.
3
+ */
4
+ export type {
5
+ OutputChannel,
6
+ OutputFormat,
7
+ OutputOptions,
8
+ OutputSpec,
9
+ FormatPolicy,
10
+ ResolvedOutput,
11
+ } from "./types.ts";
12
+ export { OutputSpecError } from "./types.ts";
13
+ export { resolveOutput } from "./run.ts";