@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
@@ -1,8 +1,26 @@
1
1
  import type { OpenAPIV3 } from "openapi-types";
2
2
 
3
3
  /**
4
- * Deep-clone an object, replacing circular references with `{ "$ref": "[Circular]" }`.
5
- * Uses WeakSet to track visited objects.
4
+ * Deep-clone an object, replacing circular references with the vendor-extension
5
+ * sentinel `{ "x-circular": true }`. Uses WeakSet to track visited objects.
6
+ *
7
+ * Trade-off (intentional): we mark the SECOND visit of any shared object
8
+ * (post-`dereference()` $ref-target reused under multiple parents) as
9
+ * circular, not just true cycles. The alternative — a DFS path stack —
10
+ * preserves every duplicate fresh, but blows the GitHub spec from 14 MB
11
+ * to 106 MB on disk because shared `per_page`/`page`/`owner` params and
12
+ * response schemas are re-cloned for every endpoint that references them.
13
+ * Downstream tools that need param/schema visibility on every endpoint
14
+ * (e.g. `zond api annotate auto`, ARV-262) handle this by re-reading the
15
+ * source spec themselves rather than relying on `apis/<name>/spec.json`.
16
+ *
17
+ * Why a vendor extension and not `$ref`: the decycled doc is now written to
18
+ * disk (apis/<name>/spec.json) and re-read by `@readme/openapi-parser` in
19
+ * downstream commands (check spec, describe, generate). If the sentinel
20
+ * carried a `$ref` field, the parser would try to resolve its value as a
21
+ * JSON pointer / file path — e.g. `apis/stripe/[Circular]` — and fail
22
+ * (ARV-146). `x-*` keys are explicitly reserved for vendor extensions in
23
+ * OpenAPI 3.x and pass through every parser untouched.
6
24
  */
7
25
  export function decycleSchema(obj: unknown): unknown {
8
26
  const seen = new WeakSet<object>();
@@ -16,7 +34,19 @@ export function decycleSchema(obj: unknown): unknown {
16
34
 
17
35
  const obj = value as Record<string, unknown>;
18
36
  if (seen.has(obj)) {
19
- return { $ref: "[Circular]" };
37
+ // ARV-262: parameter-shaped objects (have both `name` and `in`)
38
+ // appear post-`dereference()` as shared identities across every
39
+ // endpoint that $ref'd them — `per_page`, `page`, `owner`, etc.
40
+ // Past the first visit, the full `{"x-circular": true}` stub
41
+ // erases name+in and downstream tools (annotate auto, generator)
42
+ // can no longer tell what query/header that slot represented.
43
+ // Preserve name+in on revisit; schema/required/description still
44
+ // collapse so the bulk-size trade-off is unchanged (revisits
45
+ // grow by ~30 B each, vs. ~7×-blowup if we cloned everything).
46
+ if (typeof obj.name === "string" && typeof obj.in === "string") {
47
+ return { name: obj.name, in: obj.in };
48
+ }
49
+ return { "x-circular": true };
20
50
  }
21
51
  seen.add(obj);
22
52
 
@@ -1,21 +1,22 @@
1
1
  import { resolve } from "path";
2
+ import type { SourceMetadata } from "../parser/types.ts";
2
3
 
3
4
  // ──────────────────────────────────────────────
4
5
  // Utility functions (moved from skeleton.ts)
5
6
  // ──────────────────────────────────────────────
6
7
 
7
- export function isRelativeUrl(url: string): boolean {
8
+ function isRelativeUrl(url: string): boolean {
8
9
  return url.startsWith("/") && !url.includes("://");
9
10
  }
10
11
 
11
- export function resolveSpecPath(specPath: string): string {
12
+ function resolveSpecPath(specPath: string): string {
12
13
  if (specPath.startsWith("http://") || specPath.startsWith("https://")) {
13
14
  return specPath;
14
15
  }
15
16
  return resolve(specPath);
16
17
  }
17
18
 
18
- export function sanitizeEnvName(name: string): string {
19
+ function sanitizeEnvName(name: string): string {
19
20
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 30);
20
21
  }
21
22
 
@@ -25,6 +26,7 @@ export function sanitizeEnvName(name: string): string {
25
26
 
26
27
  export interface RawStep {
27
28
  name: string;
29
+ source?: SourceMetadata;
28
30
  [methodKey: string]: unknown;
29
31
  expect: {
30
32
  status?: number | number[];
@@ -37,6 +39,7 @@ export interface RawSuite {
37
39
  name: string;
38
40
  setup?: boolean;
39
41
  tags?: string[];
42
+ source?: SourceMetadata;
40
43
  folder?: string;
41
44
  fileStem?: string;
42
45
  base_url?: string;
@@ -66,11 +69,20 @@ export function serializeSuite(suite: RawSuite): string {
66
69
  lines.push(` ${hk}: ${yamlScalar(String(hv))}`);
67
70
  }
68
71
  }
72
+ if (suite.source && Object.keys(suite.source).length > 0) {
73
+ lines.push("source:");
74
+ serializeValue(suite.source, 1, lines);
75
+ }
69
76
  lines.push("tests:");
70
77
 
71
78
  for (const test of suite.tests) {
72
79
  lines.push(` - name: ${yamlScalar(test.name)}`);
73
80
 
81
+ if (test.source && Object.keys(test.source).length > 0) {
82
+ lines.push(" source:");
83
+ serializeValue(test.source, 3, lines);
84
+ }
85
+
74
86
  // Write method-as-key (the shorthand)
75
87
  for (const method of ["GET", "POST", "PUT", "PATCH", "DELETE"]) {
76
88
  if (method in test) {
@@ -92,6 +104,27 @@ export function serializeSuite(suite: RawSuite): string {
92
104
  serializeValue(test.json, 3, lines);
93
105
  }
94
106
 
107
+ // ARV-149: form body (application/x-www-form-urlencoded). The runner
108
+ // serialises this via URLSearchParams; values are flat strings with
109
+ // bracket notation for nested fields (e.g. `address[line1]`).
110
+ //
111
+ // ARV-162 (round-08 F19): form values are ALWAYS strings on the wire —
112
+ // x-www-form-urlencoded has no native numbers/bools/nulls. YAML parsing
113
+ // `phone: +1234567890` or `width: 12.5` as int/float makes `zond check
114
+ // tests` reject the suite ("expected string, received number"), and
115
+ // `zond run` silently skipped 21/68 generated Stripe suites this way.
116
+ // Force-quote every value regardless of shape; key still uses yamlScalar
117
+ // because bracket keys (`address[line1]`) need quoting too.
118
+ if (test.form !== undefined && typeof test.form === "object" && test.form !== null) {
119
+ const formEntries = Object.entries(test.form as Record<string, unknown>);
120
+ if (formEntries.length > 0) {
121
+ lines.push(" form:");
122
+ for (const [fk, fv] of formEntries) {
123
+ lines.push(` ${yamlScalar(fk)}: "${escapeYamlDoubleQuoted(String(fv))}"`);
124
+ }
125
+ }
126
+ }
127
+
95
128
  // query
96
129
  if (test.query) {
97
130
  lines.push(" query:");
@@ -151,7 +184,10 @@ export function serializeSuite(suite: RawSuite): string {
151
184
  lines.push(` ${rk}:`);
152
185
  serializeValue(rv, 6, lines);
153
186
  } else {
154
- lines.push(` ${rk}: ${yamlScalar(String(rv))}`);
187
+ // Preserve scalar types — `not_equals: true` (boolean) must not
188
+ // become `not_equals: "true"` (string), or assertions silently
189
+ // mistype against real boolean fields.
190
+ lines.push(` ${rk}: ${formatInlineValue(rv)}`);
155
191
  }
156
192
  }
157
193
  }
@@ -182,16 +218,24 @@ function serializeValue(value: unknown, indent: number, lines: string[]): void {
182
218
  if (entries.length > 0) {
183
219
  const [firstKey, firstVal] = entries[0]!;
184
220
  if (typeof firstVal === "object" && firstVal !== null) {
185
- lines.push(`${prefix}- ${firstKey}:`);
186
- serializeValue(firstVal, indent + 1, lines);
221
+ if (isEmptyContainer(firstVal)) {
222
+ lines.push(`${prefix}- ${firstKey}: ${Array.isArray(firstVal) ? "[]" : "{}"}`);
223
+ } else {
224
+ lines.push(`${prefix}- ${firstKey}:`);
225
+ serializeValue(firstVal, indent + 1, lines);
226
+ }
187
227
  } else {
188
228
  lines.push(`${prefix}- ${firstKey}: ${formatInlineValue(firstVal)}`);
189
229
  }
190
230
  for (let i = 1; i < entries.length; i++) {
191
231
  const [k, v] = entries[i]!;
192
232
  if (typeof v === "object" && v !== null) {
193
- lines.push(`${prefix} ${k}:`);
194
- serializeValue(v, indent + 1, lines);
233
+ if (isEmptyContainer(v)) {
234
+ lines.push(`${prefix} ${k}: ${Array.isArray(v) ? "[]" : "{}"}`);
235
+ } else {
236
+ lines.push(`${prefix} ${k}:`);
237
+ serializeValue(v, indent + 1, lines);
238
+ }
195
239
  } else {
196
240
  lines.push(`${prefix} ${k}: ${formatInlineValue(v)}`);
197
241
  }
@@ -209,8 +253,12 @@ function serializeValue(value: unknown, indent: number, lines: string[]): void {
209
253
  if (typeof value === "object") {
210
254
  for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
211
255
  if (typeof val === "object" && val !== null) {
212
- lines.push(`${prefix}${key}:`);
213
- serializeValue(val, indent + 1, lines);
256
+ if (isEmptyContainer(val)) {
257
+ lines.push(`${prefix}${key}: ${Array.isArray(val) ? "[]" : "{}"}`);
258
+ } else {
259
+ lines.push(`${prefix}${key}:`);
260
+ serializeValue(val, indent + 1, lines);
261
+ }
214
262
  } else {
215
263
  lines.push(`${prefix}${key}: ${formatInlineValue(val)}`);
216
264
  }
@@ -218,12 +266,32 @@ function serializeValue(value: unknown, indent: number, lines: string[]): void {
218
266
  }
219
267
  }
220
268
 
269
+ /** Empty `{}` / `[]` written as a bare `key:` (no value, no children) is
270
+ * re-parsed by YAML as `null` — sending `null` for an `object`-typed field
271
+ * guarantees 422 against strict APIs. Emit inline flow form to preserve type. */
272
+ function isEmptyContainer(val: unknown): boolean {
273
+ if (Array.isArray(val)) return val.length === 0;
274
+ if (typeof val === "object" && val !== null) return Object.keys(val).length === 0;
275
+ return false;
276
+ }
277
+
221
278
  function formatInlineValue(val: unknown): string {
222
279
  if (val === null || val === undefined) return "null";
223
280
  if (typeof val === "string") return yamlScalar(val);
224
281
  return String(val);
225
282
  }
226
283
 
284
+ /** ARV-62 (feedback round-01 / F3): security probes emit attack payloads
285
+ * with raw CRLF (`crlf:`, header-injection) and other control bytes.
286
+ * When written into a double-quoted YAML scalar these *must* be escaped
287
+ * (`\r` / `\n` / `\t` / `\xNN`) — emitting the raw byte produces YAML
288
+ * that the parser rejects with "bad indentation of a mapping entry"
289
+ * (`zond run` then fails the whole suite at load-time before sending a
290
+ * single request). The check also covers `\r`, `\t`, `\x00–\x1f`, and
291
+ * `\x7f` so any other control byte that sneaks into a payload survives
292
+ * the YAML round-trip. */
293
+ // eslint-disable-next-line no-control-regex
294
+ const CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
227
295
  function yamlScalar(value: string): string {
228
296
  if (
229
297
  value === "" ||
@@ -232,7 +300,6 @@ function yamlScalar(value: string): string {
232
300
  value === "null" ||
233
301
  value.includes(":") ||
234
302
  value.includes("#") ||
235
- value.includes("\n") ||
236
303
  value.includes("'") ||
237
304
  value.includes('"') ||
238
305
  value.includes("{") ||
@@ -245,9 +312,32 @@ function yamlScalar(value: string): string {
245
312
  value.startsWith("%") ||
246
313
  value.startsWith("@") ||
247
314
  value.startsWith("`") ||
248
- /^\d+$/.test(value)
315
+ /^\d+$/.test(value) ||
316
+ CONTROL_BYTE_RE.test(value)
249
317
  ) {
250
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
318
+ return `"${escapeYamlDoubleQuoted(value)}"`;
251
319
  }
252
320
  return value;
253
321
  }
322
+
323
+ function escapeYamlDoubleQuoted(value: string): string {
324
+ let out = "";
325
+ for (let i = 0; i < value.length; i++) {
326
+ const ch = value[i]!;
327
+ const code = ch.charCodeAt(0);
328
+ if (ch === "\\") { out += "\\\\"; continue; }
329
+ if (ch === '"') { out += '\\"'; continue; }
330
+ if (ch === "\n") { out += "\\n"; continue; }
331
+ if (ch === "\r") { out += "\\r"; continue; }
332
+ if (ch === "\t") { out += "\\t"; continue; }
333
+ if (code < 0x20 || code === 0x7f) {
334
+ // YAML 1.2 allows \xNN for any byte in (0..0xff); use this for any
335
+ // control character that doesn't have a dedicated short escape
336
+ // (covers \x00–\x1f minus the three handled above, plus DEL).
337
+ out += "\\x" + code.toString(16).padStart(2, "0");
338
+ continue;
339
+ }
340
+ out += ch;
341
+ }
342
+ return out;
343
+ }