@kirrosh/zond 0.21.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 +758 -3
  2. package/README.md +78 -15
  3. package/package.json +17 -10
  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 +55 -6
  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 +192 -0
  24. package/src/cli/commands/coverage.ts +605 -132
  25. package/src/cli/commands/db.ts +180 -8
  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 -47
  31. package/src/cli/commands/init/agents-md.ts +61 -0
  32. package/src/cli/commands/init/bootstrap.ts +108 -0
  33. package/src/cli/commands/init/index.ts +244 -0
  34. package/src/cli/commands/init/skills.ts +98 -0
  35. package/src/cli/commands/init/templates/agents.md +77 -0
  36. package/src/cli/commands/init/templates/markdown.d.ts +4 -0
  37. package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
  38. package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
  39. package/src/cli/commands/init/templates/skills/zond.md +651 -0
  40. package/src/cli/commands/init/templates/zond-config.yml +14 -0
  41. package/src/cli/commands/prepare-fixtures.ts +135 -0
  42. package/src/cli/commands/probe/mass-assignment.ts +503 -0
  43. package/src/cli/commands/probe/security.ts +454 -0
  44. package/src/cli/commands/probe/static.ts +255 -0
  45. package/src/cli/commands/probe/webhooks.ts +161 -0
  46. package/src/cli/commands/probe.ts +459 -0
  47. package/src/cli/commands/reference.ts +87 -0
  48. package/src/cli/commands/refresh-api.ts +169 -0
  49. package/src/cli/commands/remove-api.ts +150 -0
  50. package/src/cli/commands/report-bundle.ts +318 -0
  51. package/src/cli/commands/report.ts +241 -0
  52. package/src/cli/commands/request.ts +379 -4
  53. package/src/cli/commands/run.ts +911 -33
  54. package/src/cli/commands/session.ts +244 -0
  55. package/src/cli/commands/use.ts +74 -0
  56. package/src/cli/index.ts +36 -607
  57. package/src/cli/json-envelope.ts +112 -3
  58. package/src/cli/json-schemas.ts +263 -0
  59. package/src/cli/program.ts +218 -0
  60. package/src/cli/resolve.ts +105 -0
  61. package/src/cli/status-filter.ts +124 -0
  62. package/src/cli/util/api-context.ts +85 -0
  63. package/src/cli/version.ts +8 -0
  64. package/src/core/anti-fp/bootstrap.ts +34 -0
  65. package/src/core/anti-fp/index.ts +33 -0
  66. package/src/core/anti-fp/registry.ts +44 -0
  67. package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
  68. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
  69. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
  70. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
  71. package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
  72. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
  73. package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
  74. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
  75. package/src/core/anti-fp/types.ts +68 -0
  76. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  77. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  78. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  79. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  80. package/src/core/checks/checks/cross_call_references.ts +134 -0
  81. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  82. package/src/core/checks/checks/idempotency_replay.ts +246 -0
  83. package/src/core/checks/checks/ignored_auth.ts +211 -0
  84. package/src/core/checks/checks/index.ts +65 -0
  85. package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
  86. package/src/core/checks/checks/missing_required_header.ts +40 -0
  87. package/src/core/checks/checks/negative_data_rejection.ts +45 -0
  88. package/src/core/checks/checks/not_a_server_error.ts +27 -0
  89. package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
  90. package/src/core/checks/checks/pagination_invariants.ts +238 -0
  91. package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
  92. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  93. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  94. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  95. package/src/core/checks/checks/status_code_conformance.ts +61 -0
  96. package/src/core/checks/checks/unsupported_method.ts +63 -0
  97. package/src/core/checks/checks/use_after_free.ts +78 -0
  98. package/src/core/checks/index.ts +30 -0
  99. package/src/core/checks/mode.ts +79 -0
  100. package/src/core/checks/recommended-action.ts +64 -0
  101. package/src/core/checks/registry.ts +78 -0
  102. package/src/core/checks/runner.ts +874 -0
  103. package/src/core/checks/sarif.ts +230 -0
  104. package/src/core/checks/stateful.ts +121 -0
  105. package/src/core/checks/types.ts +189 -0
  106. package/src/core/classifier/recommended-action.ts +222 -0
  107. package/src/core/context/current.ts +51 -0
  108. package/src/core/context/session.ts +78 -0
  109. package/src/core/coverage/loader.ts +185 -0
  110. package/src/core/coverage/reasons.ts +300 -0
  111. package/src/core/diagnostics/db-analysis.ts +161 -12
  112. package/src/core/diagnostics/failure-class.ts +120 -0
  113. package/src/core/diagnostics/failure-hints.ts +212 -9
  114. package/src/core/diagnostics/spec-pointer.ts +99 -0
  115. package/src/core/diagnostics/suggested-fixes.ts +156 -0
  116. package/src/core/exporter/case-study/index.ts +270 -0
  117. package/src/core/exporter/curl.ts +40 -0
  118. package/src/core/exporter/exporter.ts +48 -0
  119. package/src/core/exporter/html-report/escape.ts +24 -0
  120. package/src/core/exporter/html-report/index.ts +479 -0
  121. package/src/core/exporter/html-report/script.ts +100 -0
  122. package/src/core/exporter/html-report/styles.ts +408 -0
  123. package/src/core/generator/chunker.ts +53 -15
  124. package/src/core/generator/coverage-phase.ts +0 -0
  125. package/src/core/generator/create-body.ts +89 -0
  126. package/src/core/generator/data-factory.ts +490 -33
  127. package/src/core/generator/describe.ts +1 -1
  128. package/src/core/generator/fixtures-builder.ts +325 -0
  129. package/src/core/generator/index.ts +7 -5
  130. package/src/core/generator/openapi-reader.ts +55 -3
  131. package/src/core/generator/path-param-disambig.ts +114 -0
  132. package/src/core/generator/resources-builder.ts +648 -0
  133. package/src/core/generator/schema-utils.ts +11 -3
  134. package/src/core/generator/serializer.ts +114 -15
  135. package/src/core/generator/suite-generator.ts +484 -77
  136. package/src/core/generator/types.ts +8 -0
  137. package/src/core/identity/identity-file.ts +129 -0
  138. package/src/core/lint/affects.ts +28 -0
  139. package/src/core/lint/config.ts +96 -0
  140. package/src/core/lint/format.ts +42 -0
  141. package/src/core/lint/index.ts +94 -0
  142. package/src/core/lint/reporter.ts +128 -0
  143. package/src/core/lint/rules/consistency.ts +158 -0
  144. package/src/core/lint/rules/heuristics.ts +97 -0
  145. package/src/core/lint/rules/strictness.ts +109 -0
  146. package/src/core/lint/types.ts +96 -0
  147. package/src/core/lint/walker.ts +248 -0
  148. package/src/core/meta/meta-store.ts +6 -73
  149. package/src/core/output/README.md +91 -0
  150. package/src/core/output/index.ts +13 -0
  151. package/src/core/output/run.ts +126 -0
  152. package/src/core/output/types.ts +129 -0
  153. package/src/core/parser/env-interpolation.ts +104 -0
  154. package/src/core/parser/filter.ts +57 -0
  155. package/src/core/parser/schema.ts +132 -5
  156. package/src/core/parser/types.ts +29 -2
  157. package/src/core/parser/variables.ts +0 -0
  158. package/src/core/parser/yaml-parser.ts +108 -13
  159. package/src/core/probe/bootstrap.ts +34 -0
  160. package/src/core/probe/dry-run-envelope.ts +57 -0
  161. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  162. package/src/core/probe/mass-assignment-probe.ts +1122 -0
  163. package/src/core/probe/mass-assignment-template.ts +212 -0
  164. package/src/core/probe/method-probe.ts +164 -0
  165. package/src/core/probe/method-shared.ts +69 -0
  166. package/src/core/probe/negative-probe.ts +691 -0
  167. package/src/core/probe/orphan-tracker.ts +188 -0
  168. package/src/core/probe/path-discovery.ts +440 -0
  169. package/src/core/probe/probe-harness.ts +120 -0
  170. package/src/core/probe/registry.ts +89 -0
  171. package/src/core/probe/runner.ts +136 -0
  172. package/src/core/probe/security-probe-class.ts +201 -0
  173. package/src/core/probe/security-probe.ts +1453 -0
  174. package/src/core/probe/shared.ts +505 -0
  175. package/src/core/probe/static-probe-class.ts +125 -0
  176. package/src/core/probe/types.ts +165 -0
  177. package/src/core/probe/verdict-aggregator.ts +33 -0
  178. package/src/core/probe/webhooks-probe.ts +284 -0
  179. package/src/core/reporter/console.ts +69 -4
  180. package/src/core/reporter/index.ts +2 -3
  181. package/src/core/reporter/json.ts +15 -2
  182. package/src/core/reporter/junit.ts +27 -12
  183. package/src/core/reporter/ndjson.ts +37 -0
  184. package/src/core/reporter/types.ts +3 -0
  185. package/src/core/runner/assertions.ts +62 -2
  186. package/src/core/runner/async-pool.ts +108 -0
  187. package/src/core/runner/auth-path.ts +8 -0
  188. package/src/core/runner/ci-context.ts +72 -0
  189. package/src/core/runner/executor.ts +391 -52
  190. package/src/core/runner/form-encode.ts +51 -0
  191. package/src/core/runner/http-client.ts +115 -7
  192. package/src/core/runner/learn-drift.ts +293 -0
  193. package/src/core/runner/preflight-vars.ts +149 -0
  194. package/src/core/runner/progress-tracker.ts +73 -0
  195. package/src/core/runner/rate-limiter.ts +203 -0
  196. package/src/core/runner/run-kind.ts +39 -0
  197. package/src/core/runner/schema-validator.ts +312 -0
  198. package/src/core/runner/send-request.ts +153 -20
  199. package/src/core/runner/types.ts +38 -0
  200. package/src/core/secrets/registry.ts +164 -0
  201. package/src/core/secrets/secrets-file.ts +115 -0
  202. package/src/core/selectors/operation-filter.ts +144 -0
  203. package/src/core/setup-api.ts +419 -17
  204. package/src/core/severity/category.ts +94 -0
  205. package/src/core/severity/index.ts +121 -0
  206. package/src/core/spec/layers.ts +154 -0
  207. package/src/core/util/format-eta.ts +21 -0
  208. package/src/core/utils.ts +5 -1
  209. package/src/core/workspace/config.ts +129 -0
  210. package/src/core/workspace/manifest.ts +283 -0
  211. package/src/core/workspace/output-rotation.ts +62 -0
  212. package/src/core/workspace/root.ts +94 -0
  213. package/src/core/workspace/triage-path.ts +87 -0
  214. package/src/db/lint-runs.ts +47 -0
  215. package/src/db/migrate.ts +126 -0
  216. package/src/db/migrations/0001_run_kind.sql +25 -0
  217. package/src/db/migrations/sql.d.ts +4 -0
  218. package/src/db/queries/collections.ts +133 -0
  219. package/src/db/queries/coverage.ts +9 -0
  220. package/src/db/queries/dashboard.ts +59 -0
  221. package/src/db/queries/results.ts +128 -0
  222. package/src/db/queries/runs.ts +235 -0
  223. package/src/db/queries/sessions.ts +42 -0
  224. package/src/db/queries/settings.ts +28 -0
  225. package/src/db/queries/types.ts +172 -0
  226. package/src/db/queries.ts +72 -802
  227. package/src/db/schema.ts +179 -48
  228. package/src/cli/commands/export.ts +0 -144
  229. package/src/cli/commands/guide.ts +0 -127
  230. package/src/cli/commands/init.ts +0 -57
  231. package/src/cli/commands/serve.ts +0 -81
  232. package/src/cli/commands/sync.ts +0 -269
  233. package/src/cli/commands/update.ts +0 -189
  234. package/src/cli/commands/validate.ts +0 -34
  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 -21
  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,408 @@
1
+ // Single-file inline CSS for the run report.
2
+ // Self-contained: no @import, no external fonts. System UI stack only.
3
+ // Light + dark via prefers-color-scheme. Print-friendly for PDF export.
4
+
5
+ export const STYLES = `
6
+ :root {
7
+ color-scheme: light dark;
8
+ --bg: #fbfbfd;
9
+ --bg-elev: #ffffff;
10
+ --bg-muted: #f3f4f6;
11
+ --bg-code: #f6f8fa;
12
+ --fg: #0b0d12;
13
+ --fg-muted: #5b6472;
14
+ --border: #e5e7eb;
15
+ --border-strong: #d1d5db;
16
+ --accent: #3b82f6;
17
+ --accent-fg: #ffffff;
18
+ --pass: #10b981;
19
+ --pass-bg: #d1fae5;
20
+ --fail: #ef4444;
21
+ --fail-bg: #fee2e2;
22
+ --warn: #f59e0b;
23
+ --warn-bg: #fef3c7;
24
+ --info: #6366f1;
25
+ --info-bg: #e0e7ff;
26
+ --shadow: 0 1px 2px rgba(0,0,0,.04), 0 4px 12px rgba(0,0,0,.06);
27
+ }
28
+ @media (prefers-color-scheme: dark) {
29
+ :root {
30
+ --bg: #0b0d12;
31
+ --bg-elev: #11141b;
32
+ --bg-muted: #181c25;
33
+ --bg-code: #0f1219;
34
+ --fg: #e5e7eb;
35
+ --fg-muted: #9aa3b2;
36
+ --border: #1f242f;
37
+ --border-strong: #2c3340;
38
+ --accent: #60a5fa;
39
+ --accent-fg: #0b0d12;
40
+ --pass: #34d399;
41
+ --pass-bg: #052e22;
42
+ --fail: #f87171;
43
+ --fail-bg: #2a0e10;
44
+ --warn: #fbbf24;
45
+ --warn-bg: #2a1d05;
46
+ --info: #818cf8;
47
+ --info-bg: #1c1c3a;
48
+ --shadow: 0 1px 2px rgba(0,0,0,.4), 0 6px 24px rgba(0,0,0,.4);
49
+ }
50
+ }
51
+
52
+ * { box-sizing: border-box; }
53
+ html, body { margin: 0; padding: 0; }
54
+ body {
55
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
56
+ font-size: 14px;
57
+ line-height: 1.5;
58
+ background: var(--bg);
59
+ color: var(--fg);
60
+ -webkit-font-smoothing: antialiased;
61
+ }
62
+ .container { max-width: 1080px; margin: 0 auto; padding: 32px 24px 64px; }
63
+ .mono { font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace; }
64
+
65
+ /* ── Hero ── */
66
+ .hero {
67
+ background: var(--bg-elev);
68
+ border: 1px solid var(--border);
69
+ border-radius: 16px;
70
+ padding: 28px 32px;
71
+ margin-bottom: 24px;
72
+ box-shadow: var(--shadow);
73
+ display: grid;
74
+ grid-template-columns: 1fr auto;
75
+ gap: 24px;
76
+ align-items: center;
77
+ }
78
+ .hero h1 {
79
+ margin: 0 0 6px;
80
+ font-size: 28px;
81
+ font-weight: 700;
82
+ letter-spacing: -0.02em;
83
+ }
84
+ .hero .sub {
85
+ font-size: 13px;
86
+ color: var(--fg-muted);
87
+ }
88
+ .hero .meta {
89
+ margin-top: 16px;
90
+ display: grid;
91
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
92
+ gap: 10px 18px;
93
+ }
94
+ .hero .meta dt { font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--fg-muted); margin: 0; }
95
+ .hero .meta dd { font-size: 13px; margin: 2px 0 0; word-break: break-all; }
96
+ .hero .meta dd.mono { font-size: 12px; }
97
+
98
+ /* Pass-rate ring */
99
+ .ring { position: relative; width: 140px; height: 140px; }
100
+ .ring svg { transform: rotate(-90deg); width: 100%; height: 100%; }
101
+ .ring .track { stroke: var(--border); }
102
+ .ring .fill { transition: stroke-dashoffset .6s ease; }
103
+ .ring .label {
104
+ position: absolute; inset: 0;
105
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
106
+ text-align: center;
107
+ }
108
+ .ring .label .pct { font-size: 30px; font-weight: 700; letter-spacing: -0.02em; }
109
+ .ring .label .lbl { font-size: 11px; color: var(--fg-muted); text-transform: uppercase; letter-spacing: .08em; }
110
+
111
+ /* ── Status badges ── */
112
+ .badge {
113
+ display: inline-flex; align-items: center; gap: 4px;
114
+ padding: 2px 8px; border-radius: 999px;
115
+ font-size: 11px; font-weight: 600;
116
+ background: var(--bg-muted); color: var(--fg-muted);
117
+ border: 1px solid var(--border);
118
+ white-space: nowrap;
119
+ }
120
+ .badge.pass { background: var(--pass-bg); color: var(--pass); border-color: transparent; }
121
+ .badge.fail { background: var(--fail-bg); color: var(--fail); border-color: transparent; }
122
+ .badge.warn { background: var(--warn-bg); color: var(--warn); border-color: transparent; }
123
+ .badge.info { background: var(--info-bg); color: var(--info); border-color: transparent; }
124
+ .badge.solid-pass { background: var(--pass); color: white; border-color: transparent; }
125
+ .badge.solid-fail { background: var(--fail); color: white; border-color: transparent; }
126
+ .badge.dot::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
127
+
128
+ /* ── KPI strip ── */
129
+ .kpis {
130
+ display: grid;
131
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
132
+ gap: 12px;
133
+ margin-bottom: 24px;
134
+ }
135
+ .kpi {
136
+ background: var(--bg-elev);
137
+ border: 1px solid var(--border);
138
+ border-radius: 12px;
139
+ padding: 14px 16px;
140
+ }
141
+ .kpi .n { font-size: 24px; font-weight: 700; letter-spacing: -0.02em; }
142
+ .kpi .l { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: var(--fg-muted); margin-top: 2px; }
143
+ .kpi.pass .n { color: var(--pass); }
144
+ .kpi.fail .n { color: var(--fail); }
145
+ .kpi.warn .n { color: var(--warn); }
146
+
147
+ /* ── Section ── */
148
+ section { margin-top: 32px; }
149
+ section h2 { font-size: 16px; font-weight: 600; margin: 0 0 12px; display: flex; align-items: center; gap: 10px; }
150
+ section h2 .count { font-size: 12px; color: var(--fg-muted); font-weight: 500; }
151
+
152
+ /* ── Filter bar ── */
153
+ .filters {
154
+ display: flex; flex-wrap: wrap; gap: 6px;
155
+ margin-bottom: 14px;
156
+ }
157
+ .filters button {
158
+ font: inherit;
159
+ font-size: 12px;
160
+ padding: 5px 10px;
161
+ border-radius: 999px;
162
+ border: 1px solid var(--border);
163
+ background: var(--bg-elev);
164
+ color: var(--fg-muted);
165
+ cursor: pointer;
166
+ transition: all .15s;
167
+ }
168
+ .filters button:hover { color: var(--fg); border-color: var(--border-strong); }
169
+ .filters button.active { background: var(--fg); color: var(--bg); border-color: var(--fg); }
170
+
171
+ /* ── Failure card ── */
172
+ .cards { display: flex; flex-direction: column; gap: 8px; }
173
+ .card {
174
+ background: var(--bg-elev);
175
+ border: 1px solid var(--border);
176
+ border-radius: 10px;
177
+ overflow: hidden;
178
+ transition: border-color .15s;
179
+ }
180
+ .card:hover { border-color: var(--border-strong); }
181
+ .card.hidden { display: none; }
182
+ .card > .head {
183
+ display: flex; align-items: center; gap: 10px;
184
+ padding: 10px 14px;
185
+ cursor: pointer;
186
+ user-select: none;
187
+ background: transparent;
188
+ border: 0;
189
+ width: 100%;
190
+ text-align: left;
191
+ font: inherit;
192
+ color: inherit;
193
+ }
194
+ .card > .head:hover { background: var(--bg-muted); }
195
+ .card .chev {
196
+ width: 14px; height: 14px;
197
+ transition: transform .2s;
198
+ flex-shrink: 0;
199
+ color: var(--fg-muted);
200
+ }
201
+ .card.open .chev { transform: rotate(90deg); }
202
+ .card .method {
203
+ font-family: ui-monospace, SFMono-Regular, monospace;
204
+ font-size: 11px;
205
+ font-weight: 700;
206
+ padding: 2px 6px;
207
+ border-radius: 4px;
208
+ background: var(--bg-muted);
209
+ color: var(--fg-muted);
210
+ flex-shrink: 0;
211
+ min-width: 50px;
212
+ text-align: center;
213
+ }
214
+ .card .method.GET { color: #10b981; }
215
+ .card .method.POST { color: #3b82f6; }
216
+ .card .method.PUT { color: #f59e0b; }
217
+ .card .method.PATCH { color: #f59e0b; }
218
+ .card .method.DELETE { color: #ef4444; }
219
+ .card .name {
220
+ flex: 1;
221
+ font-size: 13px;
222
+ overflow: hidden;
223
+ text-overflow: ellipsis;
224
+ white-space: nowrap;
225
+ }
226
+ .card .badges { display: flex; gap: 6px; flex-shrink: 0; align-items: center; }
227
+ .card .body {
228
+ display: none;
229
+ border-top: 1px solid var(--border);
230
+ background: var(--bg-muted);
231
+ }
232
+ .card.open .body { display: block; }
233
+
234
+ .card .actions {
235
+ display: flex; gap: 6px;
236
+ padding: 10px 14px;
237
+ border-bottom: 1px solid var(--border);
238
+ flex-wrap: wrap;
239
+ }
240
+ .btn {
241
+ font: inherit;
242
+ font-size: 12px;
243
+ padding: 5px 10px;
244
+ border: 1px solid var(--border);
245
+ border-radius: 6px;
246
+ background: var(--bg-elev);
247
+ color: var(--fg);
248
+ cursor: pointer;
249
+ display: inline-flex;
250
+ align-items: center;
251
+ gap: 5px;
252
+ transition: all .15s;
253
+ }
254
+ .btn:hover { border-color: var(--border-strong); background: var(--bg); }
255
+ .btn.copied { background: var(--pass-bg); color: var(--pass); border-color: transparent; }
256
+
257
+ /* Tabs */
258
+ .tabs { display: flex; border-bottom: 1px solid var(--border); padding: 0 14px; }
259
+ .tabs button {
260
+ font: inherit;
261
+ font-size: 12px;
262
+ background: transparent;
263
+ border: 0;
264
+ border-bottom: 2px solid transparent;
265
+ padding: 10px 12px;
266
+ margin-bottom: -1px;
267
+ cursor: pointer;
268
+ color: var(--fg-muted);
269
+ transition: all .15s;
270
+ }
271
+ .tabs button:hover { color: var(--fg); }
272
+ .tabs button.active { color: var(--fg); border-bottom-color: var(--fg); font-weight: 600; }
273
+ .panel { padding: 14px; display: none; }
274
+ .panel.active { display: block; }
275
+
276
+ .kv { display: grid; grid-template-columns: 110px 1fr; gap: 8px 14px; font-size: 12px; }
277
+ .kv dt { color: var(--fg-muted); }
278
+ .kv dd { margin: 0; word-break: break-all; }
279
+
280
+ pre.code {
281
+ margin: 6px 0 0;
282
+ padding: 12px 14px;
283
+ background: var(--bg-code);
284
+ border: 1px solid var(--border);
285
+ border-radius: 6px;
286
+ font-family: ui-monospace, SFMono-Regular, monospace;
287
+ font-size: 12px;
288
+ line-height: 1.55;
289
+ overflow-x: auto;
290
+ max-height: 360px;
291
+ overflow-y: auto;
292
+ white-space: pre;
293
+ }
294
+ .code-label {
295
+ font-size: 10px;
296
+ text-transform: uppercase;
297
+ letter-spacing: .08em;
298
+ color: var(--fg-muted);
299
+ margin-top: 12px;
300
+ }
301
+ .code-label:first-child { margin-top: 0; }
302
+
303
+ /* JSON syntax highlighting (regex-driven) */
304
+ .j-key { color: #b45309; }
305
+ .j-str { color: #047857; }
306
+ .j-num { color: #1d4ed8; }
307
+ .j-bool { color: #7c3aed; }
308
+ .j-null { color: #6b7280; }
309
+ @media (prefers-color-scheme: dark) {
310
+ .j-key { color: #fbbf24; }
311
+ .j-str { color: #34d399; }
312
+ .j-num { color: #60a5fa; }
313
+ .j-bool { color: #c084fc; }
314
+ .j-null { color: #9aa3b2; }
315
+ }
316
+
317
+ /* Assertion list */
318
+ .asserts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
319
+ .asserts li {
320
+ border: 1px solid var(--border);
321
+ border-radius: 6px;
322
+ padding: 8px 10px;
323
+ background: var(--bg-elev);
324
+ font-size: 12px;
325
+ }
326
+ .asserts li.failed { border-color: var(--fail); background: var(--fail-bg); }
327
+ .asserts li.passed { border-color: var(--pass); background: var(--pass-bg); }
328
+ .asserts .a-head { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
329
+ .asserts .a-diff { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 6px; font-family: ui-monospace, SFMono-Regular, monospace; font-size: 11px; }
330
+ .asserts .a-diff .lbl { color: var(--fg-muted); }
331
+
332
+ /* Coverage matrix */
333
+ .cov-grid {
334
+ display: grid;
335
+ gap: 1px;
336
+ background: var(--border);
337
+ border: 1px solid var(--border);
338
+ border-radius: 8px;
339
+ overflow: hidden;
340
+ font-size: 11px;
341
+ font-family: ui-monospace, SFMono-Regular, monospace;
342
+ }
343
+ .cov-row { display: grid; grid-template-columns: 1.5fr repeat(5, 60px); }
344
+ .cov-cell {
345
+ background: var(--bg-elev);
346
+ padding: 6px 8px;
347
+ text-align: center;
348
+ display: flex;
349
+ align-items: center;
350
+ justify-content: center;
351
+ }
352
+ .cov-cell.head { font-weight: 600; color: var(--fg-muted); text-transform: uppercase; font-size: 10px; }
353
+ .cov-cell.path { justify-content: flex-start; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
354
+ .cov-cell.s2 { background: var(--pass-bg); color: var(--pass); font-weight: 600; }
355
+ .cov-cell.s4 { background: var(--warn-bg); color: var(--warn); font-weight: 600; }
356
+ .cov-cell.s5 { background: var(--fail-bg); color: var(--fail); font-weight: 600; }
357
+ .cov-cell.serr { background: var(--fail); color: white; font-weight: 600; }
358
+ .cov-cell.empty { color: var(--border-strong); }
359
+ .cov-row.reasons { grid-template-columns: 2fr repeat(3, 1fr); }
360
+ .cov-cell.reasons { flex-wrap: wrap; gap: 3px; padding: 6px; min-height: 32px; }
361
+ .cov-cell.s2.reasons { background: var(--pass-bg); }
362
+ .cov-cell.s4.reasons { background: var(--warn-bg); }
363
+ .cov-cell.su { background: var(--bg-muted); color: var(--fg-muted); }
364
+ .cov-cell .rchip { background: rgba(255,255,255,0.6); padding: 1px 5px; border-radius: 3px; font-size: 10px; font-weight: 500; color: var(--fg); }
365
+ .method-mini { font-weight: 700; color: var(--accent); margin-right: 4px; }
366
+ .cov-cell.path .badge { margin-left: 6px; vertical-align: middle; }
367
+
368
+ /* Footer */
369
+ footer {
370
+ margin-top: 48px;
371
+ padding-top: 20px;
372
+ border-top: 1px solid var(--border);
373
+ font-size: 11px;
374
+ color: var(--fg-muted);
375
+ display: flex;
376
+ justify-content: space-between;
377
+ flex-wrap: wrap;
378
+ gap: 8px;
379
+ }
380
+ footer a { color: var(--accent); text-decoration: none; }
381
+ footer a:hover { text-decoration: underline; }
382
+
383
+ /* Empty state */
384
+ .empty {
385
+ background: var(--bg-elev);
386
+ border: 1px dashed var(--border);
387
+ border-radius: 10px;
388
+ padding: 32px 24px;
389
+ text-align: center;
390
+ color: var(--fg-muted);
391
+ font-size: 13px;
392
+ }
393
+
394
+ /* Print */
395
+ @media print {
396
+ .hero, .kpi, .card, .empty { box-shadow: none !important; }
397
+ .card { break-inside: avoid; }
398
+ .actions, .filters, .tabs button:not(.active) { display: none !important; }
399
+ .panel { display: block !important; padding: 8px 14px; }
400
+ body { background: white; color: black; }
401
+ }
402
+
403
+ @media (max-width: 720px) {
404
+ .hero { grid-template-columns: 1fr; }
405
+ .ring { width: 110px; height: 110px; justify-self: center; }
406
+ .cov-row { grid-template-columns: 1.5fr repeat(5, 1fr); }
407
+ }
408
+ `;
@@ -1,6 +1,6 @@
1
1
  import type { EndpointInfo } from "./types.ts";
2
2
 
3
- export const CHUNK_THRESHOLD = 30;
3
+ const CHUNK_THRESHOLD = 30;
4
4
 
5
5
  export interface ChunkPlan {
6
6
  totalEndpoints: number;
@@ -8,23 +8,38 @@ export interface ChunkPlan {
8
8
  chunks: Array<{ tag: string; count: number }>;
9
9
  }
10
10
 
11
- /** Group endpoints by their first tag, or "untagged" if none */
11
+ /**
12
+ * Group endpoints by their first tag. TASK-36: untagged endpoints fall
13
+ * back to per-resource grouping (first path segment), so `/audiences` and
14
+ * `/audiences/{id}` land in the same `audiences` group instead of being
15
+ * piled into a single `untagged` bucket. Endpoints whose path has no
16
+ * usable first segment (e.g. `/`) keep the legacy `untagged` key.
17
+ */
12
18
  export function groupEndpointsByTag(endpoints: EndpointInfo[]): Map<string, EndpointInfo[]> {
13
19
  const groups = new Map<string, EndpointInfo[]>();
14
20
  for (const ep of endpoints) {
15
- const tag = ep.tags[0] ?? "untagged";
16
- const list = groups.get(tag);
17
- if (list) {
18
- list.push(ep);
19
- } else {
20
- groups.set(tag, [ep]);
21
- }
21
+ const key = ep.tags[0] ?? resourceKeyFromPath(ep.path);
22
+ const list = groups.get(key);
23
+ if (list) list.push(ep);
24
+ else groups.set(key, [ep]);
22
25
  }
23
26
  return groups;
24
27
  }
25
28
 
29
+ /** Extract the first non-templated path segment for tagless fallback. */
30
+ function resourceKeyFromPath(path: string): string {
31
+ const segments = path.split("/").filter(Boolean);
32
+ for (const seg of segments) {
33
+ // Skip templated segments like {id} — they aren't resource names.
34
+ if (seg.startsWith("{") && seg.endsWith("}")) continue;
35
+ if (seg.length === 0) continue;
36
+ return seg;
37
+ }
38
+ return "untagged";
39
+ }
40
+
26
41
  /** Decide whether to chunk, and return the tag breakdown */
27
- export function planChunks(endpoints: EndpointInfo[]): ChunkPlan {
42
+ function planChunks(endpoints: EndpointInfo[]): ChunkPlan {
28
43
  const groups = groupEndpointsByTag(endpoints);
29
44
  const chunks = Array.from(groups.entries())
30
45
  .map(([tag, eps]) => ({ tag, count: eps.length }))
@@ -37,11 +52,34 @@ export function planChunks(endpoints: EndpointInfo[]): ChunkPlan {
37
52
  };
38
53
  }
39
54
 
40
- /** Filter endpoints that have the given tag (case-insensitive) */
55
+ /**
56
+ * Filter endpoints by tag (case-insensitive). Accepts a single tag or a
57
+ * comma-separated list (TASK-239) so callers can run one generate pass for
58
+ * multiple tags instead of looping in the shell — looping prints
59
+ * "Next steps" N times and drowns real warnings.
60
+ */
41
61
  export function filterByTag(endpoints: EndpointInfo[], tag: string): EndpointInfo[] {
42
- const lower = tag.toLowerCase();
43
- if (lower === "untagged") {
44
- return endpoints.filter(ep => ep.tags.length === 0);
62
+ const wanted = tag
63
+ .split(",")
64
+ .map(t => t.trim().toLowerCase())
65
+ .filter(t => t.length > 0);
66
+ if (wanted.length === 0) return [];
67
+ const includeUntagged = wanted.includes("untagged");
68
+ const explicit = wanted.filter(t => t !== "untagged");
69
+ return endpoints.filter(ep => {
70
+ if (includeUntagged && ep.tags.length === 0) return true;
71
+ return ep.tags.some(t => explicit.includes(t.trim().toLowerCase()));
72
+ });
73
+ }
74
+
75
+ /** Collect the unique set of tags across all endpoints (sorted, original casing). */
76
+ export function collectTags(endpoints: EndpointInfo[]): string[] {
77
+ const seen = new Map<string, string>();
78
+ for (const ep of endpoints) {
79
+ for (const t of ep.tags) {
80
+ const key = t.trim().toLowerCase();
81
+ if (!seen.has(key)) seen.set(key, t.trim());
82
+ }
45
83
  }
46
- return endpoints.filter(ep => ep.tags.some(t => t.toLowerCase() === lower));
84
+ return Array.from(seen.values()).sort((a, b) => a.localeCompare(b));
47
85
  }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Build a request body for a POST/create endpoint that can actually pass
3
+ * the API's validators on a live target.
4
+ *
5
+ * `generateFromSchema` is purely schema-driven: it emits `{{$uuid}}` for
6
+ * `audience_id`-shaped fields, so when `prepare-fixtures --seed` POSTs a
7
+ * generated body the API replies 422 ("audience aud_xyz not found") and
8
+ * the seed loop quits with no progress (F1-14 in feedback-round 14).
9
+ *
10
+ * `buildCreateRequestBody` wraps `generateFromSchema` and walks the
11
+ * resulting object: any property name that looks like a foreign-key id
12
+ * (`*_id` / `*Id` / `*_uuid`) AND has a real value in `knownFixtures`
13
+ * (typically loaded from `.env.yaml` plus values captured earlier in the
14
+ * same cascade pass) gets that real value substituted in. Random
15
+ * placeholders only survive when no env value exists.
16
+ *
17
+ * Used by `prepare-fixtures --seed --apply` (former `bootstrap`) so each
18
+ * cascade step that POSTs a child resource pulls its parent ids from
19
+ * what discover already filled. Without this, every nested resource is
20
+ * a guaranteed 422.
21
+ */
22
+
23
+ import type { OpenAPIV3 } from "openapi-types";
24
+ import { generateFromSchema } from "./data-factory.ts";
25
+ import { canonicalVarName } from "./fixtures-builder.ts";
26
+
27
+ const FK_FIELD_RE = /(?:_id|Id|_uuid)$/;
28
+
29
+ function substituteFkFields(value: unknown, knownFixtures: Record<string, string>): unknown {
30
+ if (Array.isArray(value)) {
31
+ return value.map(v => substituteFkFields(v, knownFixtures));
32
+ }
33
+ if (value && typeof value === "object") {
34
+ const obj = value as Record<string, unknown>;
35
+ const out: Record<string, unknown> = {};
36
+ for (const [k, v] of Object.entries(obj)) {
37
+ // Recurse first — nested objects may carry their own FKs.
38
+ const recursed = substituteFkFields(v, knownFixtures);
39
+ if (FK_FIELD_RE.test(k)) {
40
+ // ARV-138: look up by raw field name first (back-compat with envs
41
+ // that still key off `issueId`), then by canonical snake_case form
42
+ // (`issue_id`) — which is the only form the manifest emits since
43
+ // ARV-138. This keeps both old `.env.yaml`s and new ones working
44
+ // during the rollout window.
45
+ const fixture = knownFixtures[k] ?? knownFixtures[canonicalVarName(k)];
46
+ if (typeof fixture === "string" && fixture.length > 0) {
47
+ out[k] = fixture;
48
+ continue;
49
+ }
50
+ }
51
+ out[k] = recursed;
52
+ }
53
+ return out;
54
+ }
55
+ return value;
56
+ }
57
+
58
+ export interface BuildCreateRequestBodyOptions {
59
+ /**
60
+ * Real values from `.env.yaml` (and prior cascade captures) keyed by
61
+ * variable name. When a body field's name matches a key here AND the
62
+ * field looks FK-shaped, that value replaces the schema-derived random
63
+ * placeholder.
64
+ */
65
+ knownFixtures?: Record<string, string>;
66
+ }
67
+
68
+ /**
69
+ * Spec-aware body builder for live-API seed POSTs. Returns a JSON-shaped
70
+ * object ready to `JSON.stringify` and send.
71
+ *
72
+ * - Schema → object via `generateFromSchema(forRequest: true)`. This
73
+ * already strips `readOnly` and bare `id` fields the server assigns.
74
+ * - Walks the result and swaps FK-shaped fields for `knownFixtures`
75
+ * values when present.
76
+ *
77
+ * Tokens like `{{$randomEmail}}` / `{{$uuid}}` produced by the schema
78
+ * layer are intentionally left in place — the live runner resolves them
79
+ * via `substituteDeep` right before the request is sent.
80
+ */
81
+ export function buildCreateRequestBody(
82
+ schema: OpenAPIV3.SchemaObject,
83
+ options: BuildCreateRequestBodyOptions = {},
84
+ ): unknown {
85
+ const generated = generateFromSchema(schema, undefined, { forRequest: true });
86
+ const known = options.knownFixtures ?? {};
87
+ if (Object.keys(known).length === 0) return generated;
88
+ return substituteFkFields(generated, known);
89
+ }