@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,91 @@
1
+ /**
2
+ * ARV-116 (m-19): runner that turns an `OutputSpec` + CLI flags into
3
+ * a `ResolvedOutput`, then renders and writes the payload.
4
+ *
5
+ * Resolution order:
6
+ * 1. `--json` and `--report` are mutually exclusive — error.
7
+ * 2. If `--report` is set, look it up in `aliases` first, then in
8
+ * `formats`. Unknown → error (consistent with ARV-97; never
9
+ * silently swallow a typo).
10
+ * 3. If `--json` is set, pick the first envelope-wrapping format
11
+ * from the spec. Specs without one fall back to the
12
+ * `defaultFormat` (acceptable for commands like `run` whose
13
+ * `--json` is special-cased — see ARV-117).
14
+ * 4. Otherwise use `defaultFormat`.
15
+ * 5. Channel: `--output` forces file. Without it, take the format's
16
+ * `defaultChannel`. File channel without an explicit `--output`
17
+ * uses `defaultFilename` (relative paths resolved against cwd).
18
+ */
19
+ import { resolve as resolvePath } from "path";
20
+ import {
21
+ OutputSpecError,
22
+ type OutputOptions,
23
+ type OutputSpec,
24
+ type ResolvedOutput,
25
+ } from "./types.ts";
26
+
27
+ export function resolveOutput<P>(
28
+ spec: OutputSpec<P>,
29
+ opts: OutputOptions,
30
+ ): ResolvedOutput {
31
+ if (opts.json && typeof opts.report === "string" && opts.report.length > 0) {
32
+ throw new OutputSpecError(
33
+ "--json and --report are mutually exclusive — pick one output channel",
34
+ );
35
+ }
36
+
37
+ // Step 1: resolve the format.
38
+ let format: string;
39
+ if (typeof opts.report === "string" && opts.report.length > 0) {
40
+ const alias = spec.aliases?.[opts.report];
41
+ format = alias ?? opts.report;
42
+ } else if (opts.json) {
43
+ format = pickEnvelopeFormat(spec) ?? spec.defaultFormat;
44
+ } else {
45
+ format = spec.defaultFormat;
46
+ }
47
+
48
+ const policy = spec.formats[format];
49
+ if (!policy) {
50
+ const known = Object.keys(spec.formats).sort().join(", ");
51
+ throw new OutputSpecError(
52
+ `Unknown --report format: "${format}". Available for ${spec.command}: ${known}`,
53
+ );
54
+ }
55
+
56
+ // Step 2: resolve channel + path.
57
+ const explicitOutput = typeof opts.output === "string" && opts.output.length > 0
58
+ ? opts.output
59
+ : undefined;
60
+ let channel: "stdout" | "file";
61
+ let path: string | undefined;
62
+ if (explicitOutput) {
63
+ channel = "file";
64
+ path = resolvePath(explicitOutput);
65
+ } else if (policy.defaultChannel === "file") {
66
+ channel = "file";
67
+ path = policy.defaultFilename ? resolvePath(policy.defaultFilename) : undefined;
68
+ if (!path) {
69
+ throw new OutputSpecError(
70
+ `Format "${format}" defaults to file but has no defaultFilename and --output was not set`,
71
+ );
72
+ }
73
+ } else {
74
+ channel = "stdout";
75
+ }
76
+
77
+ return {
78
+ format,
79
+ channel,
80
+ path,
81
+ envelopeWrap: policy.envelopeWrap === true,
82
+ };
83
+ }
84
+
85
+ function pickEnvelopeFormat<P>(spec: OutputSpec<P>): string | undefined {
86
+ for (const [name, policy] of Object.entries(spec.formats)) {
87
+ if (policy.envelopeWrap) return name;
88
+ }
89
+ return undefined;
90
+ }
91
+
@@ -0,0 +1,122 @@
1
+ /**
2
+ * ARV-116 (m-19): typed declaration of every `--report` / `--output` /
3
+ * `--json` combination a command supports.
4
+ *
5
+ * Background. Lesson §E of strategy/lessons.md collects seven separate
6
+ * bug reports about output-flag plumbing:
7
+ * - ARV-97 — `--report ndjson --output <path>` silently dropped events;
8
+ * - ARV-50 — probe `--dry-run` JSON shape diverged from `--report json`;
9
+ * - ARV-82 — `db runs --json` envelope `data` field shape disagreed
10
+ * with sibling commands;
11
+ * - …and four more along the same lines.
12
+ *
13
+ * Root cause: each command has its own ad-hoc flag parser. Some treat
14
+ * "ndjson" as an alias for `--ndjson`, some don't. Some honour
15
+ * `--output` for SARIF only, some for every format. Some wrap in a JSON
16
+ * envelope, some emit a raw stream. There is no single place to read
17
+ * "what does `command X` produce when I pass `--report Y --output Z`".
18
+ *
19
+ * This module gives that single place. A command declares an
20
+ * `OutputSpec<Payload>` once — listing the formats it supports, each
21
+ * format's default channel (stdout vs file), each format's default
22
+ * filename, and whether the format wraps in the standard `--json`
23
+ * envelope. A runner helper (`runCommandWithOutput`) consumes the spec
24
+ * plus the CLI flags and resolves them to a single `ResolvedOutput`
25
+ * decision, with consistent mutual-exclusion enforcement.
26
+ *
27
+ * The spec is the source-of-truth for ARV-120's build-time check:
28
+ * every `--json` command must declare an `OutputSpec` with an
29
+ * `envelopeWrap: true` entry, so the published schema and the runtime
30
+ * output cannot drift.
31
+ *
32
+ * This task only ships the infrastructure. Per-command migration
33
+ * (`run`, `checks`, `probe*`) lands in ARV-117/118/119.
34
+ */
35
+
36
+ /** Where a rendered payload lands. */
37
+ export type OutputChannel = "stdout" | "file";
38
+
39
+ /** A format name — `sarif` / `ndjson` / `json` / `junit` etc. The set is
40
+ * open: each command declares its own. */
41
+ export type OutputFormat = string;
42
+
43
+ /** Per-format policy: where the format writes by default, what filename
44
+ * to use when it writes to a file, and whether the payload is wrapped
45
+ * in the standard `{ ok, command, data, ... }` envelope. */
46
+ export interface FormatPolicy {
47
+ /** Default destination when neither `--output` nor channel-overriding
48
+ * flags are present. SARIF defaults to file (`zond-checks.sarif`);
49
+ * NDJSON defaults to stdout (one event per line for piping); JSON
50
+ * envelopes default to stdout. */
51
+ defaultChannel: OutputChannel;
52
+ /** Default filename when `defaultChannel === "file"`. Relative paths
53
+ * are resolved against cwd by the runner. Ignored when
54
+ * `defaultChannel === "stdout"` — the user has to opt in to a file
55
+ * via `--output`. */
56
+ defaultFilename?: string;
57
+ /** True for formats that should be wrapped in the shared envelope
58
+ * (`jsonOk` / `jsonError` from `cli/json-envelope.ts`). Streaming
59
+ * formats (NDJSON) and bespoke serialisations (SARIF, JUnit) are
60
+ * not envelope-wrapped — they have their own contracts. */
61
+ envelopeWrap?: boolean;
62
+ /** ARV-120: for `envelopeWrap` formats — basename of the JSON schema
63
+ * under `docs/json-schema/` that describes the envelope's `data`
64
+ * field. The build-time coverage test asserts the file exists, so
65
+ * a renamed/deleted schema fails CI together with the spec. */
66
+ envelopeSchemaFile?: string;
67
+ /** Optional human description, surfaced by `--help` generators and
68
+ * the README table. */
69
+ description?: string;
70
+ }
71
+
72
+ export interface OutputSpec<Payload = unknown> {
73
+ /** Command name — propagated into the JSON envelope's `command` field
74
+ * and used by error messages. */
75
+ command: string;
76
+ /** Supported formats keyed by name. Unknown formats fall through to
77
+ * an error (no silent acceptance, see ARV-97). */
78
+ formats: Record<OutputFormat, FormatPolicy>;
79
+ /** Default format when neither `--report` nor `--json` is set. */
80
+ defaultFormat: OutputFormat;
81
+ /** Optional alias map: `{ ndjson: 'ndjson' }` lets `--report ndjson`
82
+ * fold into the `--ndjson` flag (ARV-63 alias, retained because
83
+ * skill prompts ship it). Keys are flag values seen on the CLI;
84
+ * values are the resolved format name. */
85
+ aliases?: Record<string, OutputFormat>;
86
+ }
87
+
88
+ /** Decision the runner makes after applying the spec to CLI flags. */
89
+ export interface ResolvedOutput {
90
+ /** Resolved format name (always one of `spec.formats`). */
91
+ format: OutputFormat;
92
+ /** Resolved destination. */
93
+ channel: OutputChannel;
94
+ /** Filesystem path when `channel === "file"`. Absolute path expected
95
+ * by callers — the runner resolves it against cwd. */
96
+ path?: string;
97
+ /** Whether the runner should wrap the payload in the standard
98
+ * envelope before writing. Mirrors `FormatPolicy.envelopeWrap`. */
99
+ envelopeWrap: boolean;
100
+ }
101
+
102
+ /** Inputs the runner reads from the CLI layer. Names mirror commander
103
+ * options so a command can pass `cmd.opts<OutputOptions>()` directly. */
104
+ export interface OutputOptions {
105
+ /** `--report <format>`. */
106
+ report?: string;
107
+ /** `--output <path>`. */
108
+ output?: string;
109
+ /** `--json`. */
110
+ json?: boolean;
111
+ }
112
+
113
+ /** Thrown by `resolveOutput` when the user-supplied flags violate the
114
+ * spec's policy (unknown format, mutually exclusive flags, …). The
115
+ * CLI layer catches this and emits the standard `jsonError` or
116
+ * human `printError`. */
117
+ export class OutputSpecError extends Error {
118
+ constructor(message: string) {
119
+ super(message);
120
+ this.name = "OutputSpecError";
121
+ }
122
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * `#(funcName)` / `#(funcName(args))` dynamic-value substitution for
3
+ * loaded YAML values (ARV-190, m-21).
4
+ *
5
+ * Lets a workspace avoid stale hardcoded data — UUIDs that get reused
6
+ * by the API and pin existing rows, dates that expire days after
7
+ * checkout, idempotency keys that race against past runs. Functions
8
+ * are evaluated at LOAD time (per-run, not per-request), so a single
9
+ * `#(uuid)` reference resolves to the same value across every step
10
+ * inside one run — the idempotency-replay scenarios only work if the
11
+ * key stays stable for the run's lifetime.
12
+ *
13
+ * Supported functions:
14
+ * #(uuid) — fresh UUID v4 (stable within one run via cache)
15
+ * #(uuidStable(seed)) — deterministic UUID derived from seed (sha-256 → v4 shape)
16
+ * #(today) — YYYY-MM-DD now (UTC)
17
+ * #(todayPlus(N)) — today + N days (N may be negative)
18
+ * #(now) — ISO 8601 timestamp
19
+ * #(unix) — seconds since epoch
20
+ * #(alphanumeric(N)) — N random a-z0-9 chars
21
+ * #(env:VAR) — process.env.VAR (alias for ${VAR})
22
+ *
23
+ * Resolution order: env-interpolation (${VAR}) runs first because its
24
+ * default-value syntax can produce strings the dynamic resolver should
25
+ * see literally. Dynamic values run BEFORE @secret/@identity reference
26
+ * resolution so the function tokens never leak into a secret-typed
27
+ * value (and so secret-stored values that happen to look like `#(...)`
28
+ * stay opaque). Strings that have no `#(` substring short-circuit.
29
+ *
30
+ * Nested forms: a single value may carry multiple references mixed
31
+ * with literal text — `"req-#(uuid)-#(today)"` yields `req-<uuid>-2026-05-16`.
32
+ */
33
+
34
+ import { createHash } from "node:crypto";
35
+
36
+ export interface DynamicValueContext {
37
+ /** Per-run cache keyed by raw expression (`#(uuid)`, `#(today)`). */
38
+ cache: Map<string, string>;
39
+ /** Source of env vars; defaults to process.env. Override in tests. */
40
+ env?: Record<string, string | undefined>;
41
+ /** File path the value came from — surfaced in error messages. */
42
+ filePath?: string;
43
+ }
44
+
45
+ /** Build a fresh cache for one resolution pass. Exported so the caller
46
+ * can share a cache across multiple `resolveDynamicValuesDeep` calls
47
+ * that belong to the same run (e.g. workspace + per-API env files). */
48
+ export function newDynamicCache(): Map<string, string> {
49
+ return new Map();
50
+ }
51
+
52
+ // Captures `#(funcName)` or `#(funcName(args))`. `args` may contain any
53
+ // chars except an unescaped closing paren — keep it simple, the funcs we
54
+ // support take string or integer args without nested parens.
55
+ const FUNC_RE = /#\(([A-Za-z_][A-Za-z0-9_]*)(?::([^)]*))?(?:\(([^)]*)\))?\)/g;
56
+
57
+ export function resolveDynamicValues(
58
+ text: string,
59
+ ctx: DynamicValueContext,
60
+ ): string {
61
+ if (typeof text !== "string" || text.indexOf("#(") === -1) return text;
62
+ return text.replace(FUNC_RE, (full, name: string, colonArg: string | undefined, parenArg: string | undefined) => {
63
+ const cached = ctx.cache.get(full);
64
+ if (cached !== undefined) return cached;
65
+ const value = evaluate(name, colonArg ?? parenArg, full, ctx);
66
+ ctx.cache.set(full, value);
67
+ return value;
68
+ });
69
+ }
70
+
71
+ export function resolveDynamicValuesDeep(
72
+ obj: Record<string, unknown>,
73
+ ctx: DynamicValueContext,
74
+ ): Record<string, string> {
75
+ const out: Record<string, string> = {};
76
+ for (const [k, v] of Object.entries(obj)) {
77
+ if (typeof v === "string") {
78
+ out[k] = resolveDynamicValues(v, ctx);
79
+ } else {
80
+ out[k] = String(v);
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+
86
+ function evaluate(
87
+ name: string,
88
+ arg: string | undefined,
89
+ fullExpr: string,
90
+ ctx: DynamicValueContext,
91
+ ): string {
92
+ switch (name) {
93
+ case "uuid":
94
+ return crypto.randomUUID();
95
+ case "uuidStable": {
96
+ if (!arg) throw makeError(`#(uuidStable(seed)) requires a seed argument`, fullExpr, ctx);
97
+ return seedToUuid(arg);
98
+ }
99
+ case "today":
100
+ return new Date().toISOString().slice(0, 10);
101
+ case "todayPlus": {
102
+ if (!arg) throw makeError(`#(todayPlus(N)) requires an integer N`, fullExpr, ctx);
103
+ const n = Number.parseInt(arg, 10);
104
+ if (!Number.isFinite(n)) throw makeError(`#(todayPlus(${arg})) — N must be an integer`, fullExpr, ctx);
105
+ const d = new Date();
106
+ d.setUTCDate(d.getUTCDate() + n);
107
+ return d.toISOString().slice(0, 10);
108
+ }
109
+ case "now":
110
+ return new Date().toISOString();
111
+ case "unix":
112
+ return String(Math.floor(Date.now() / 1000));
113
+ case "alphanumeric": {
114
+ const n = arg ? Number.parseInt(arg, 10) : 8;
115
+ if (!Number.isFinite(n) || n <= 0 || n > 1024) {
116
+ throw makeError(`#(alphanumeric(${arg ?? ""})) — length must be 1..1024`, fullExpr, ctx);
117
+ }
118
+ return randomAlphanumeric(n);
119
+ }
120
+ case "env": {
121
+ if (!arg) throw makeError(`#(env:VAR) requires a variable name`, fullExpr, ctx);
122
+ const env = ctx.env ?? (process.env as Record<string, string | undefined>);
123
+ const v = env[arg];
124
+ if (v === undefined || v === "") {
125
+ throw makeError(`#(env:${arg}) is not set — define it in your shell or CI secret`, fullExpr, ctx);
126
+ }
127
+ return v;
128
+ }
129
+ default:
130
+ throw makeError(`unknown dynamic function "${name}" in ${fullExpr} — supported: uuid, uuidStable, today, todayPlus, now, unix, alphanumeric, env`, fullExpr, ctx);
131
+ }
132
+ }
133
+
134
+ function seedToUuid(seed: string): string {
135
+ // SHA-256 the seed, take first 16 bytes, format as UUID v4 shape so
136
+ // any consumer-side `format: uuid` validator accepts it. Variant +
137
+ // version bits are forced to match v4 — the value is still
138
+ // deterministic because the source bytes are stable per seed.
139
+ const hash = createHash("sha256").update(seed).digest();
140
+ const bytes = Buffer.from(hash.subarray(0, 16));
141
+ bytes[6] = (bytes[6]! & 0x0f) | 0x40; // version 4
142
+ bytes[8] = (bytes[8]! & 0x3f) | 0x80; // RFC 4122 variant
143
+ const hex = bytes.toString("hex");
144
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
145
+ }
146
+
147
+ const ALPHANUM = "abcdefghijklmnopqrstuvwxyz0123456789";
148
+
149
+ function randomAlphanumeric(n: number): string {
150
+ let out = "";
151
+ for (let i = 0; i < n; i++) {
152
+ out += ALPHANUM[Math.floor(Math.random() * ALPHANUM.length)];
153
+ }
154
+ return out;
155
+ }
156
+
157
+ function makeError(msg: string, expr: string, ctx: DynamicValueContext): Error {
158
+ const where = ctx.filePath ? ` (referenced from ${ctx.filePath})` : "";
159
+ return new Error(`Dynamic value ${expr}: ${msg}${where}`);
160
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * `${VAR}` / `${VAR:-default}` substitution for `.env.yaml` (TASK-169, m-10).
3
+ *
4
+ * Lets a workspace commit `.env.yaml` without bare secrets:
5
+ *
6
+ * auth_token: "${MYAPI_AUTH_TOKEN}"
7
+ * base_url: "${MYAPI_BASE_URL:-https://api.example.com}"
8
+ *
9
+ * Rules:
10
+ * - `${VAR}` → process.env.VAR (throws if missing).
11
+ * - `${VAR:-default}` → process.env.VAR ?? default. The default may
12
+ * contain `:` (everything after `:-` up to the closing `}` is the
13
+ * default).
14
+ * - `\${LITERAL}` → literal `${LITERAL}` (the backslash is
15
+ * stripped). Matches the `dotenv-expand` / docker-compose convention.
16
+ * - One level of resolution only — values pulled from env are NOT
17
+ * re-scanned for further `${...}` (cycle-risk).
18
+ * - Variable names matching /TOKEN|SECRET|PASSWORD|KEY|DSN/i are NOT
19
+ * auto-registered with the redaction registry. Auto-registration is
20
+ * opt-in via `@secret:` (TASK-170). We do print a one-line warning
21
+ * suggesting the user mark them as a secret, but only the first time.
22
+ */
23
+
24
+ const ENV_REF_RE = /(\\?)\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
25
+ const SUSPICIOUS_NAME_RE = /TOKEN|SECRET|PASSWORD|API_KEY|^KEY$|_KEY$|DSN/i;
26
+
27
+ export interface EnvInterpolationContext {
28
+ /** Absolute path of the file we're resolving — used for error messages. */
29
+ filePath: string;
30
+ /** YAML key whose value contains the reference — used for error messages. */
31
+ key: string;
32
+ /** Source of variables; defaults to `process.env`. Override in tests. */
33
+ env?: Record<string, string | undefined>;
34
+ /** Sink for human-facing warnings. Default: stderr write. */
35
+ warn?: (msg: string) => void;
36
+ }
37
+
38
+ /** Module-level set of variable names we've already warned about, so the
39
+ * same `${MYAPI_AUTH_TOKEN}` reference doesn't yell once per env file. */
40
+ const warned = new Set<string>();
41
+
42
+ export function _resetEnvInterpolationWarnings(): void {
43
+ warned.clear();
44
+ }
45
+
46
+ /**
47
+ * Substitute every `${VAR}` / `${VAR:-default}` in `text`. Returns the
48
+ * fully-resolved string. Throws `Error` when an unresolved reference has
49
+ * no default.
50
+ */
51
+ export function interpolateEnvRefs(text: string, ctx: EnvInterpolationContext): string {
52
+ if (typeof text !== "string" || text.length === 0) return text;
53
+ if (text.indexOf("${") === -1 && text.indexOf("\\$") === -1) return text;
54
+ const env = ctx.env ?? (process.env as Record<string, string | undefined>);
55
+ const warn = ctx.warn ?? ((m: string) => process.stderr.write(m + "\n"));
56
+
57
+ return text.replace(ENV_REF_RE, (full, escape: string, name: string, def: string | undefined) => {
58
+ if (escape === "\\") {
59
+ // Escaped reference → strip the backslash, keep the literal.
60
+ return full.slice(1);
61
+ }
62
+ const value = env[name];
63
+ if (value === undefined || value === "") {
64
+ if (def !== undefined) {
65
+ return def;
66
+ }
67
+ throw new Error(
68
+ `Environment variable \${${name}} is not set (referenced from "${ctx.filePath}", key "${ctx.key}"). ` +
69
+ `Provide it via your shell, CI secret, or use the \${${name}:-<default>} form to give it a fallback.`,
70
+ );
71
+ }
72
+ if (SUSPICIOUS_NAME_RE.test(name) && !warned.has(name)) {
73
+ warned.add(name);
74
+ warn(
75
+ `[zond] ${ctx.filePath}: variable \${${name}} looks like a secret. ` +
76
+ `Consider mapping it through @secret:${ctx.key} (TASK-170) so it is redacted in artifacts.`,
77
+ );
78
+ }
79
+ return value;
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Apply interpolation to every string value in a flat env object. Other
85
+ * value types (numbers, booleans, nulls coming back from YAML) are
86
+ * stringified untouched, matching the existing loader behaviour.
87
+ */
88
+ export function interpolateEnvObject(
89
+ obj: Record<string, unknown>,
90
+ filePath: string,
91
+ env?: Record<string, string | undefined>,
92
+ ): Record<string, string> {
93
+ const out: Record<string, string> = {};
94
+ for (const [k, v] of Object.entries(obj)) {
95
+ if (typeof v === "string") {
96
+ out[k] = interpolateEnvRefs(v, { filePath, key: k, env });
97
+ } else {
98
+ out[k] = String(v);
99
+ }
100
+ }
101
+ return out;
102
+ }
103
+
104
+ const SUSPICIOUS_ENV_NAME_RE = SUSPICIOUS_NAME_RE;
@@ -1,4 +1,6 @@
1
1
  import type { TestSuite } from "./types.ts";
2
+ import { compileOperationFilter } from "../selectors/operation-filter.ts";
3
+ import type { EndpointInfo } from "../generator/types.ts";
2
4
 
3
5
  /**
4
6
  * Filter suites by tags (OR logic, case-insensitive).
@@ -38,3 +40,58 @@ export function filterSuitesByMethod(suites: TestSuite[], method: string): TestS
38
40
  }));
39
41
  return filtered.filter(s => s.tests.length > 0);
40
42
  }
43
+
44
+ export interface SuiteFilterResult {
45
+ suites: TestSuite[];
46
+ errors: string[];
47
+ }
48
+
49
+ /**
50
+ * ARV-25: parity with `zond generate`/`zond checks run` — apply the unified
51
+ * `--include`/`--exclude` selector grammar (path/method/tag/operation-id)
52
+ * to a list of test suites. Step-level selectors (path, method, operation-id)
53
+ * filter steps within a suite; suite-level selectors (tag) borrow each
54
+ * step's parent suite tags. A suite drops out once it has no steps left.
55
+ *
56
+ * Reuses `compileOperationFilter` so semantics match generate/checks 1:1
57
+ * (multiple --include combine with OR; --exclude evaluated after includes).
58
+ *
59
+ * `operation-id` matches against `step.source?.endpoint` ("METHOD /path"),
60
+ * which is what the generator records; tests authored manually without
61
+ * `source.endpoint` simply never match operation-id selectors.
62
+ */
63
+ export function filterSuitesByOperationFilter(
64
+ suites: TestSuite[],
65
+ includes: string[],
66
+ excludes: string[],
67
+ ): SuiteFilterResult {
68
+ if (includes.length === 0 && excludes.length === 0) {
69
+ return { suites, errors: [] };
70
+ }
71
+ const compiled = compileOperationFilter({ includes, excludes });
72
+ if (compiled.errors.length > 0) {
73
+ return { suites: [], errors: compiled.errors };
74
+ }
75
+ const filtered = suites.map(suite => ({
76
+ ...suite,
77
+ tests: suite.tests.filter(step => compiled.filter(stepToEndpoint(suite, step))),
78
+ }));
79
+ return { suites: filtered.filter(s => s.tests.length > 0), errors: [] };
80
+ }
81
+
82
+ function stepToEndpoint(suite: TestSuite, step: TestSuite["tests"][number]): EndpointInfo {
83
+ const sourceEndpoint = typeof step.source?.endpoint === "string" ? step.source.endpoint : undefined;
84
+ return {
85
+ path: step.path,
86
+ method: step.method,
87
+ operationId: sourceEndpoint,
88
+ summary: undefined,
89
+ tags: suite.tags ?? [],
90
+ parameters: [],
91
+ requestBodySchema: undefined,
92
+ requestBodyContentType: undefined,
93
+ responseContentTypes: [],
94
+ responses: [],
95
+ security: [],
96
+ };
97
+ }