@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,158 @@
1
+ import type { OpenAPIV3 } from "openapi-types";
2
+ import type { Issue, RuleId, Severity } from "../types.ts";
3
+ import { DEFAULT_SEVERITY } from "../types.ts";
4
+ import { RULE_AFFECTS } from "../affects.ts";
5
+ import type { SchemaContext } from "../walker.ts";
6
+ import { normalisedTypes } from "../walker.ts";
7
+ import { validateExampleAgainstFormat } from "../format.ts";
8
+
9
+ interface RuleSink {
10
+ push(rule: RuleId, severity: Severity, message: string, opts: Partial<Pick<Issue, "path" | "method" | "fix_hint">> & { jsonpointer: string }): void;
11
+ }
12
+
13
+ export function runConsistencyRules(ctx: SchemaContext, sink: RuleSink): void {
14
+ const s = ctx.schema;
15
+ if (!s || typeof s !== "object") return;
16
+
17
+ const types = normalisedTypes(s);
18
+ const isStringy = types.includes("string");
19
+ const isNumeric = types.includes("number") || types.includes("integer");
20
+
21
+ const examples = collectExamples(s);
22
+ for (const { value, pointer } of examples) {
23
+ checkValueAgainstSchema(value, s, pointer, "example", isStringy, isNumeric, ctx, sink);
24
+ }
25
+
26
+ if (s.default !== undefined) {
27
+ checkValueAgainstSchema(s.default, s, `${ctx.jsonpointer}/default`, "default", isStringy, isNumeric, ctx, sink);
28
+ }
29
+
30
+ // A6: enum values pairwise unique.
31
+ if (Array.isArray(s.enum) && s.enum.length > 0) {
32
+ const seen = new Set<string>();
33
+ let dupAt = -1;
34
+ for (let i = 0; i < s.enum.length; i++) {
35
+ const k = stableKey(s.enum[i]);
36
+ if (seen.has(k)) { dupAt = i; break; }
37
+ seen.add(k);
38
+ }
39
+ if (dupAt >= 0) {
40
+ sink.push("A6", DEFAULT_SEVERITY.A6, `enum has duplicate value at index ${dupAt}`, {
41
+ jsonpointer: `${ctx.jsonpointer}/enum/${dupAt}`,
42
+ path: ctx.path, method: ctx.method,
43
+ fix_hint: "remove the duplicate enum entry",
44
+ });
45
+ }
46
+ }
47
+ }
48
+
49
+ function collectExamples(s: OpenAPIV3.SchemaObject): Array<{ value: unknown; pointer: string }> {
50
+ const out: Array<{ value: unknown; pointer: string }> = [];
51
+ if ((s as { example?: unknown }).example !== undefined) {
52
+ out.push({ value: (s as { example: unknown }).example, pointer: "example" });
53
+ }
54
+ // OpenAPI 3.1 allows examples[]
55
+ const arr = (s as { examples?: unknown }).examples;
56
+ if (Array.isArray(arr)) {
57
+ arr.forEach((v, i) => out.push({ value: v, pointer: `examples/${i}` }));
58
+ }
59
+ return out;
60
+ }
61
+
62
+ function checkValueAgainstSchema(
63
+ value: unknown,
64
+ s: OpenAPIV3.SchemaObject,
65
+ basePointer: string,
66
+ kind: "example" | "default",
67
+ isStringy: boolean,
68
+ isNumeric: boolean,
69
+ ctx: SchemaContext,
70
+ sink: RuleSink,
71
+ ): void {
72
+ // basePointer for example/default is sometimes a relative segment like "example".
73
+ // Prepend ctx.jsonpointer if needed.
74
+ const jsonpointer = basePointer.startsWith("/")
75
+ ? basePointer
76
+ : `${ctx.jsonpointer}/${basePointer}`;
77
+ const ruleA1 = kind === "example" ? "A1" : "A5";
78
+ const ruleA2 = kind === "example" ? "A2" : "A5";
79
+ const ruleA3 = kind === "example" ? "A3" : "A5";
80
+ const ruleA4 = kind === "example" ? "A4" : "A5";
81
+
82
+ // Format check
83
+ const fmt = (s as { format?: string }).format;
84
+ if (fmt && isStringy && typeof value === "string") {
85
+ if (!validateExampleAgainstFormat(value, fmt)) {
86
+ sink.push(ruleA1, DEFAULT_SEVERITY[ruleA1], `${kind} ${JSON.stringify(value)} violates format: ${fmt}`, {
87
+ jsonpointer, path: ctx.path, method: ctx.method,
88
+ fix_hint: `make ${kind} match RFC for format: ${fmt}`,
89
+ });
90
+ }
91
+ }
92
+
93
+ // Enum check
94
+ if (Array.isArray(s.enum) && s.enum.length > 0) {
95
+ const key = stableKey(value);
96
+ if (!s.enum.some(e => stableKey(e) === key)) {
97
+ sink.push(ruleA2, DEFAULT_SEVERITY[ruleA2], `${kind} ${JSON.stringify(value)} is not in enum`, {
98
+ jsonpointer, path: ctx.path, method: ctx.method,
99
+ fix_hint: `pick a value from enum: ${JSON.stringify(s.enum)}`,
100
+ });
101
+ }
102
+ }
103
+
104
+ // Pattern check
105
+ const pattern = (s as { pattern?: string }).pattern;
106
+ if (pattern && typeof value === "string") {
107
+ let re: RegExp | null = null;
108
+ try { re = new RegExp(pattern); } catch { /* invalid regex — skip silently */ }
109
+ if (re && !re.test(value)) {
110
+ sink.push(ruleA3, DEFAULT_SEVERITY[ruleA3], `${kind} ${JSON.stringify(value)} does not match pattern ${pattern}`, {
111
+ jsonpointer, path: ctx.path, method: ctx.method,
112
+ fix_hint: "adjust the example to match the regex",
113
+ });
114
+ }
115
+ }
116
+
117
+ // Length / range
118
+ if (isStringy && typeof value === "string") {
119
+ const min = (s as { minLength?: number }).minLength;
120
+ const max = (s as { maxLength?: number }).maxLength;
121
+ if (typeof min === "number" && value.length < min) {
122
+ sink.push(ruleA4, DEFAULT_SEVERITY[ruleA4], `${kind} length ${value.length} < minLength ${min}`, {
123
+ jsonpointer, path: ctx.path, method: ctx.method,
124
+ });
125
+ }
126
+ if (typeof max === "number" && value.length > max) {
127
+ sink.push(ruleA4, DEFAULT_SEVERITY[ruleA4], `${kind} length ${value.length} > maxLength ${max}`, {
128
+ jsonpointer, path: ctx.path, method: ctx.method,
129
+ });
130
+ }
131
+ }
132
+ if (isNumeric && typeof value === "number") {
133
+ const minimum = (s as { minimum?: number }).minimum;
134
+ const maximum = (s as { maximum?: number }).maximum;
135
+ if (typeof minimum === "number" && value < minimum) {
136
+ sink.push(ruleA4, DEFAULT_SEVERITY[ruleA4], `${kind} ${value} < minimum ${minimum}`, {
137
+ jsonpointer, path: ctx.path, method: ctx.method,
138
+ });
139
+ }
140
+ if (typeof maximum === "number" && value > maximum) {
141
+ sink.push(ruleA4, DEFAULT_SEVERITY[ruleA4], `${kind} ${value} > maximum ${maximum}`, {
142
+ jsonpointer, path: ctx.path, method: ctx.method,
143
+ });
144
+ }
145
+ }
146
+ }
147
+
148
+ function stableKey(v: unknown): string {
149
+ if (v === null || typeof v !== "object") return JSON.stringify(v);
150
+ if (Array.isArray(v)) return "[" + v.map(stableKey).join(",") + "]";
151
+ const obj = v as Record<string, unknown>;
152
+ const keys = Object.keys(obj).sort();
153
+ return "{" + keys.map(k => JSON.stringify(k) + ":" + stableKey(obj[k])).join(",") + "}";
154
+ }
155
+
156
+ // Suppress unused-export warning: RULE_AFFECTS imported here for future inline
157
+ // affects-tagging if rules grow more local context.
158
+ void RULE_AFFECTS;
@@ -0,0 +1,97 @@
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, SchemaContext, RequestBodyContext } 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
+ * B2 — path/query param named like an id (`*_id`, `id`) without `format: uuid`
13
+ * or `pattern`. Heuristic: only on params whose name matches the id-suffix list.
14
+ */
15
+ export function runParamHeuristics(ctx: ParamContext, sink: RuleSink, h: HeuristicConfig): void {
16
+ const p = ctx.param;
17
+ if (p.in !== "path" && p.in !== "query") return;
18
+ const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject;
19
+ const types = normalisedTypes(schema);
20
+ // B2 only applies to string-shaped ids (uuid). Integer ids are constrained
21
+ // by minimum/maximum (B3 territory), not by format: uuid.
22
+ if (types.length > 0 && !types.includes("string")) return;
23
+ const looksLikeId = p.name === "id" || h.id_suffixes.some(s => p.name.endsWith(s));
24
+ if (!looksLikeId) return;
25
+ const fmt = (schema as { format?: string }).format;
26
+ const hasPattern = !!(schema as { pattern?: string }).pattern;
27
+ if (fmt !== "uuid" && !hasPattern) {
28
+ sink.push("B2", DEFAULT_SEVERITY.B2, `id-like param "${p.name}" missing format: uuid or pattern (heuristic)`, {
29
+ jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
30
+ fix_hint: "if the id is a UUID, add format: uuid; otherwise add a pattern",
31
+ });
32
+ }
33
+ }
34
+
35
+ /**
36
+ * B5/B6 — schema property name suggests a known semantic type, but `format`
37
+ * is missing.
38
+ * - `*_at`, `*_date`, `*_time`, `created`, `updated`, `timestamp` → `date-time`
39
+ * - `email`, `url`, `website`, `homepage` → `email` / `uri`
40
+ */
41
+ export function runSchemaHeuristics(ctx: SchemaContext, sink: RuleSink, h: HeuristicConfig): void {
42
+ if (ctx.origin !== "property" || !ctx.propertyName) return;
43
+ const s = ctx.schema;
44
+ const types = normalisedTypes(s);
45
+ if (!types.includes("string")) return;
46
+ const fmt = (s as { format?: string }).format;
47
+ const name = ctx.propertyName;
48
+
49
+ // B5 — timestamp fields
50
+ const looksLikeTimestamp =
51
+ h.timestamp_suffixes.some(suf => name.endsWith(suf)) ||
52
+ ["created", "updated", "timestamp"].includes(name);
53
+ if (looksLikeTimestamp && fmt !== "date-time" && fmt !== "date") {
54
+ sink.push("B5", DEFAULT_SEVERITY.B5, `field "${name}" looks like a timestamp but has no format: date-time (heuristic)`, {
55
+ jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
56
+ fix_hint: "add format: date-time so --validate-schema enforces RFC3339",
57
+ });
58
+ }
59
+
60
+ // B6 — email / url
61
+ if (name === "email" && fmt !== "email") {
62
+ sink.push("B6", DEFAULT_SEVERITY.B6, `field "${name}" missing format: email (heuristic)`, {
63
+ jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
64
+ fix_hint: "add format: email",
65
+ });
66
+ }
67
+ if (h.url_names.includes(name) && fmt !== "uri" && fmt !== "url") {
68
+ sink.push("B6", DEFAULT_SEVERITY.B6, `field "${name}" missing format: uri (heuristic)`, {
69
+ jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
70
+ fix_hint: "add format: uri",
71
+ });
72
+ }
73
+ }
74
+
75
+ /**
76
+ * B9 — request-body schema declares semantically-required-looking properties
77
+ * (`name`, `email`, `title`) but `required: []` is empty / absent. Heuristic.
78
+ */
79
+ export function runRequestBodyHeuristics(ctx: RequestBodyContext, sink: RuleSink, h: HeuristicConfig): void {
80
+ if (!ctx.requestBody.content) return;
81
+ for (const [ct, mt] of Object.entries(ctx.requestBody.content)) {
82
+ if (!ct.includes("json")) continue;
83
+ const schema = mt.schema as OpenAPIV3.SchemaObject | undefined;
84
+ if (!schema || !schema.properties) continue;
85
+ const required = (schema.required ?? []) as string[];
86
+ const propNames = Object.keys(schema.properties);
87
+ const semanticPresent = h.semantic_required.filter(n => propNames.includes(n));
88
+ const semanticMissing = semanticPresent.filter(n => !required.includes(n));
89
+ if (semanticPresent.length > 0 && semanticMissing.length === semanticPresent.length) {
90
+ sink.push("B9", DEFAULT_SEVERITY.B9, `request body has properties [${semanticPresent.join(", ")}] but none are required (heuristic)`, {
91
+ jsonpointer: `${ctx.jsonpointer}/content/${ct.replace(/~/g, "~0").replace(/\//g, "~1")}/schema/required`,
92
+ path: ctx.path, method: ctx.method,
93
+ fix_hint: `consider adding to required: [${semanticMissing.map(n => `"${n}"`).join(", ")}]`,
94
+ });
95
+ }
96
+ }
97
+ }
@@ -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
+ };