harnery 0.36.0 → 0.37.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 (311) hide show
  1. package/dist/commander.js +4 -0
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +87 -46
  4. package/dist/commands/artifacts.d.ts.map +1 -1
  5. package/dist/commands/artifacts.js +26 -1
  6. package/dist/commands/browse.d.ts.map +1 -1
  7. package/dist/commands/browse.js +11 -0
  8. package/dist/commands/diagnostics.d.ts +4 -0
  9. package/dist/commands/diagnostics.d.ts.map +1 -0
  10. package/dist/commands/diagnostics.js +229 -0
  11. package/dist/commands/files.d.ts +12 -0
  12. package/dist/commands/files.d.ts.map +1 -1
  13. package/dist/commands/files.js +54 -1
  14. package/dist/commands/governor.d.ts.map +1 -1
  15. package/dist/commands/governor.js +4 -0
  16. package/dist/commands/ledger-v3.d.ts.map +1 -1
  17. package/dist/commands/ledger-v3.js +71 -2
  18. package/dist/commands/qa-run.d.ts +4 -0
  19. package/dist/commands/qa-run.d.ts.map +1 -0
  20. package/dist/commands/qa-run.js +141 -0
  21. package/dist/commands/resources.d.ts +4 -0
  22. package/dist/commands/resources.d.ts.map +1 -0
  23. package/dist/commands/resources.js +29 -0
  24. package/dist/commands/supervisor.d.ts +4 -0
  25. package/dist/commands/supervisor.d.ts.map +1 -0
  26. package/dist/commands/supervisor.js +101 -0
  27. package/dist/commands/web.d.ts.map +1 -1
  28. package/dist/commands/web.js +17 -0
  29. package/dist/commands/workflow.d.ts.map +1 -1
  30. package/dist/commands/workflow.js +4 -0
  31. package/dist/core/agents/cli.js +7 -3
  32. package/dist/core/agents/health.d.ts +17 -0
  33. package/dist/core/agents/health.d.ts.map +1 -0
  34. package/dist/core/agents/health.js +109 -0
  35. package/dist/core/agents/mailbox.d.ts +92 -0
  36. package/dist/core/agents/mailbox.d.ts.map +1 -0
  37. package/dist/core/agents/mailbox.js +325 -0
  38. package/dist/core/agents/reconcile-coordination-v3.d.ts +44 -0
  39. package/dist/core/agents/reconcile-coordination-v3.d.ts.map +1 -0
  40. package/dist/core/agents/reconcile-coordination-v3.js +58 -0
  41. package/dist/core/agents/render/prompt-context.d.ts +5 -0
  42. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  43. package/dist/core/agents/render/prompt-context.js +34 -13
  44. package/dist/core/agents/render/session-context.d.ts +8 -0
  45. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  46. package/dist/core/agents/render/session-context.js +34 -3
  47. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  48. package/dist/core/agents/rules/commit-conflict.js +50 -1
  49. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  50. package/dist/core/agents/rules/stop-hook.js +6 -10
  51. package/dist/core/agents/session-finalizer-v3.js +4 -1
  52. package/dist/core/artifacts/index.d.ts +26 -1
  53. package/dist/core/artifacts/index.d.ts.map +1 -1
  54. package/dist/core/artifacts/index.js +146 -6
  55. package/dist/core/config.d.ts +12 -0
  56. package/dist/core/config.d.ts.map +1 -1
  57. package/dist/core/config.js +43 -0
  58. package/dist/core/diagnostics/advice.d.ts +9 -0
  59. package/dist/core/diagnostics/advice.d.ts.map +1 -0
  60. package/dist/core/diagnostics/advice.js +112 -0
  61. package/dist/core/diagnostics/bundle.d.ts +35 -0
  62. package/dist/core/diagnostics/bundle.d.ts.map +1 -0
  63. package/dist/core/diagnostics/bundle.js +874 -0
  64. package/dist/core/diagnostics/comparison.d.ts +5 -0
  65. package/dist/core/diagnostics/comparison.d.ts.map +1 -0
  66. package/dist/core/diagnostics/comparison.js +444 -0
  67. package/dist/core/diagnostics/contract.d.ts +245 -0
  68. package/dist/core/diagnostics/contract.d.ts.map +1 -0
  69. package/dist/core/diagnostics/contract.js +18 -0
  70. package/dist/core/diagnostics/identity.d.ts +3 -0
  71. package/dist/core/diagnostics/identity.d.ts.map +1 -0
  72. package/dist/core/diagnostics/identity.js +10 -0
  73. package/dist/core/diagnostics/index.d.ts +8 -0
  74. package/dist/core/diagnostics/index.d.ts.map +1 -0
  75. package/dist/core/diagnostics/index.js +7 -0
  76. package/dist/core/diagnostics/replay.d.ts +8 -0
  77. package/dist/core/diagnostics/replay.d.ts.map +1 -0
  78. package/dist/core/diagnostics/replay.js +213 -0
  79. package/dist/core/diagnostics/sanitize.d.ts +9 -0
  80. package/dist/core/diagnostics/sanitize.d.ts.map +1 -0
  81. package/dist/core/diagnostics/sanitize.js +84 -0
  82. package/dist/core/events/legacy-storage/compression.d.ts +13 -0
  83. package/dist/core/events/legacy-storage/compression.d.ts.map +1 -0
  84. package/dist/core/events/legacy-storage/compression.js +107 -0
  85. package/dist/core/events/legacy-storage/index.d.ts +1 -0
  86. package/dist/core/events/legacy-storage/index.d.ts.map +1 -1
  87. package/dist/core/events/legacy-storage/index.js +1 -0
  88. package/dist/core/events/v3/archive-retention.d.ts +33 -0
  89. package/dist/core/events/v3/archive-retention.d.ts.map +1 -0
  90. package/dist/core/events/v3/archive-retention.js +195 -0
  91. package/dist/core/events/v3/capabilities.d.ts +1 -1
  92. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  93. package/dist/core/events/v3/capabilities.js +1 -0
  94. package/dist/core/events/v3/index.d.ts +1 -0
  95. package/dist/core/events/v3/index.d.ts.map +1 -1
  96. package/dist/core/events/v3/index.js +1 -0
  97. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  98. package/dist/core/events/v3/live-routing.js +1 -0
  99. package/dist/core/events/v3/producers/hook-base.d.ts +1 -1
  100. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  101. package/dist/core/events/v3/producers/hook-base.js +13 -2
  102. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  103. package/dist/core/events/v3/producers/intake.js +1 -0
  104. package/dist/core/events/v3/producers/recorder.d.ts +16 -0
  105. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  106. package/dist/core/events/v3/producers/recorder.js +88 -1
  107. package/dist/core/hooks/adapter/output.d.ts +8 -0
  108. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  109. package/dist/core/hooks/adapter/output.js +11 -1
  110. package/dist/core/hooks/cli.js +183 -192
  111. package/dist/core/hooks/health.d.ts +59 -0
  112. package/dist/core/hooks/health.d.ts.map +1 -0
  113. package/dist/core/hooks/health.js +84 -0
  114. package/dist/core/resources/contract.d.ts +90 -0
  115. package/dist/core/resources/contract.d.ts.map +1 -0
  116. package/dist/core/resources/contract.js +2 -0
  117. package/dist/core/resources/index.d.ts +6 -0
  118. package/dist/core/resources/index.d.ts.map +1 -0
  119. package/dist/core/resources/index.js +5 -0
  120. package/dist/core/resources/sampler.d.ts +27 -0
  121. package/dist/core/resources/sampler.d.ts.map +1 -0
  122. package/dist/core/resources/sampler.js +357 -0
  123. package/dist/core/resources/service-status.d.ts +3 -0
  124. package/dist/core/resources/service-status.d.ts.map +1 -0
  125. package/dist/core/resources/service-status.js +48 -0
  126. package/dist/core/resources/service.d.ts +26 -0
  127. package/dist/core/resources/service.d.ts.map +1 -0
  128. package/dist/core/resources/service.js +292 -0
  129. package/dist/core/resources/storage.d.ts +13 -0
  130. package/dist/core/resources/storage.d.ts.map +1 -0
  131. package/dist/core/resources/storage.js +30 -0
  132. package/dist/core/storage/atomic-json.d.ts +3 -0
  133. package/dist/core/storage/atomic-json.d.ts.map +1 -0
  134. package/dist/core/storage/atomic-json.js +15 -0
  135. package/dist/core/storage/builtins.d.ts.map +1 -1
  136. package/dist/core/storage/builtins.js +45 -6
  137. package/dist/core/storage/logger.d.ts +2 -0
  138. package/dist/core/storage/logger.d.ts.map +1 -1
  139. package/dist/core/storage/logger.js +2 -0
  140. package/dist/core/storage/query.d.ts +14 -0
  141. package/dist/core/storage/query.d.ts.map +1 -1
  142. package/dist/core/storage/query.js +51 -0
  143. package/dist/core/supervisor/activity.d.ts +4 -0
  144. package/dist/core/supervisor/activity.d.ts.map +1 -0
  145. package/dist/core/supervisor/activity.js +141 -0
  146. package/dist/core/supervisor/contract.d.ts +268 -0
  147. package/dist/core/supervisor/contract.d.ts.map +1 -0
  148. package/dist/core/supervisor/contract.js +35 -0
  149. package/dist/core/supervisor/explanations.d.ts +4 -0
  150. package/dist/core/supervisor/explanations.d.ts.map +1 -0
  151. package/dist/core/supervisor/explanations.js +71 -0
  152. package/dist/core/supervisor/findings.d.ts +20 -0
  153. package/dist/core/supervisor/findings.d.ts.map +1 -0
  154. package/dist/core/supervisor/findings.js +399 -0
  155. package/dist/core/supervisor/history.d.ts +9 -0
  156. package/dist/core/supervisor/history.d.ts.map +1 -0
  157. package/dist/core/supervisor/history.js +46 -0
  158. package/dist/core/supervisor/hook-health-alerts.d.ts +14 -0
  159. package/dist/core/supervisor/hook-health-alerts.d.ts.map +1 -0
  160. package/dist/core/supervisor/hook-health-alerts.js +70 -0
  161. package/dist/core/supervisor/hook-health-storage.d.ts +5 -0
  162. package/dist/core/supervisor/hook-health-storage.d.ts.map +1 -0
  163. package/dist/core/supervisor/hook-health-storage.js +24 -0
  164. package/dist/core/supervisor/hook-health.d.ts +72 -0
  165. package/dist/core/supervisor/hook-health.d.ts.map +1 -0
  166. package/dist/core/supervisor/hook-health.js +203 -0
  167. package/dist/core/supervisor/hooks.d.ts +10 -0
  168. package/dist/core/supervisor/hooks.d.ts.map +1 -0
  169. package/dist/core/supervisor/hooks.js +28 -0
  170. package/dist/core/supervisor/index.d.ts +15 -0
  171. package/dist/core/supervisor/index.d.ts.map +1 -0
  172. package/dist/core/supervisor/index.js +14 -0
  173. package/dist/core/supervisor/log-feed.d.ts +9 -0
  174. package/dist/core/supervisor/log-feed.d.ts.map +1 -0
  175. package/dist/core/supervisor/log-feed.js +103 -0
  176. package/dist/core/supervisor/service.d.ts +29 -0
  177. package/dist/core/supervisor/service.d.ts.map +1 -0
  178. package/dist/core/supervisor/service.js +450 -0
  179. package/dist/core/supervisor/services.d.ts +11 -0
  180. package/dist/core/supervisor/services.d.ts.map +1 -0
  181. package/dist/core/supervisor/services.js +155 -0
  182. package/dist/core/supervisor/status.d.ts +3 -0
  183. package/dist/core/supervisor/status.d.ts.map +1 -0
  184. package/dist/core/supervisor/status.js +48 -0
  185. package/dist/core/supervisor/storage.d.ts +30 -0
  186. package/dist/core/supervisor/storage.d.ts.map +1 -0
  187. package/dist/core/supervisor/storage.js +73 -0
  188. package/dist/core/supervisor/timeline.d.ts +3 -0
  189. package/dist/core/supervisor/timeline.d.ts.map +1 -0
  190. package/dist/core/supervisor/timeline.js +87 -0
  191. package/dist/core/workflow/admission.d.ts +23 -0
  192. package/dist/core/workflow/admission.d.ts.map +1 -0
  193. package/dist/core/workflow/admission.js +136 -0
  194. package/dist/core/workflow/engine.d.ts.map +1 -1
  195. package/dist/core/workflow/engine.js +77 -2
  196. package/dist/core/workflow/index.d.ts +3 -2
  197. package/dist/core/workflow/index.d.ts.map +1 -1
  198. package/dist/core/workflow/index.js +2 -1
  199. package/dist/core/workflow/proof.d.ts +2 -1
  200. package/dist/core/workflow/proof.d.ts.map +1 -1
  201. package/dist/core/workflow/proof.js +39 -1
  202. package/dist/core/workflow/run-state.d.ts +3 -2
  203. package/dist/core/workflow/run-state.d.ts.map +1 -1
  204. package/dist/core/workflow/run-state.js +8 -1
  205. package/dist/core/workflow/types.d.ts +33 -1
  206. package/dist/core/workflow/types.d.ts.map +1 -1
  207. package/dist/core/workflow/types.js +2 -1
  208. package/dist/lib/browser/client.d.ts +5 -0
  209. package/dist/lib/browser/client.d.ts.map +1 -1
  210. package/dist/lib/browser/client.js +1 -0
  211. package/dist/lib/browser/index.d.ts +2 -0
  212. package/dist/lib/browser/index.d.ts.map +1 -1
  213. package/dist/lib/browser/index.js +2 -0
  214. package/dist/lib/browser/qa-run-contracts.d.ts +173 -0
  215. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -0
  216. package/dist/lib/browser/qa-run-contracts.js +226 -0
  217. package/dist/lib/browser/qa-run.d.ts +51 -0
  218. package/dist/lib/browser/qa-run.d.ts.map +1 -0
  219. package/dist/lib/browser/qa-run.js +684 -0
  220. package/dist/lib/coord-root-id.d.ts +9 -0
  221. package/dist/lib/coord-root-id.d.ts.map +1 -0
  222. package/dist/lib/coord-root-id.js +17 -0
  223. package/dist/lib/instructions/apply.d.ts +3 -1
  224. package/dist/lib/instructions/apply.d.ts.map +1 -1
  225. package/dist/lib/instructions/apply.js +74 -3
  226. package/dist/lib/instructions/templates.d.ts.map +1 -1
  227. package/dist/lib/instructions/templates.js +6 -0
  228. package/package.json +12 -1
  229. package/src/commander.ts +26 -0
  230. package/src/commands/agents.ts +102 -49
  231. package/src/commands/artifacts.ts +40 -2
  232. package/src/commands/browse.ts +15 -0
  233. package/src/commands/diagnostics.ts +271 -0
  234. package/src/commands/files.ts +102 -2
  235. package/src/commands/governor.ts +8 -0
  236. package/src/commands/ledger-v3.ts +82 -0
  237. package/src/commands/qa-run.ts +184 -0
  238. package/src/commands/resources.ts +38 -0
  239. package/src/commands/supervisor.ts +141 -0
  240. package/src/commands/web.ts +19 -0
  241. package/src/commands/workflow.ts +8 -0
  242. package/src/core/agents/cli.ts +7 -3
  243. package/src/core/agents/health.ts +142 -0
  244. package/src/core/agents/mailbox.ts +373 -0
  245. package/src/core/agents/reconcile-coordination-v3.ts +77 -0
  246. package/src/core/agents/render/prompt-context.ts +42 -12
  247. package/src/core/agents/render/session-context.ts +37 -3
  248. package/src/core/agents/rules/commit-conflict.ts +58 -1
  249. package/src/core/agents/rules/stop-hook.ts +6 -12
  250. package/src/core/agents/session-finalizer-v3.ts +4 -1
  251. package/src/core/artifacts/index.ts +196 -10
  252. package/src/core/config.ts +111 -2
  253. package/src/core/diagnostics/advice.ts +147 -0
  254. package/src/core/diagnostics/bundle.ts +1142 -0
  255. package/src/core/diagnostics/comparison.ts +565 -0
  256. package/src/core/diagnostics/contract.ts +311 -0
  257. package/src/core/diagnostics/identity.ts +11 -0
  258. package/src/core/diagnostics/index.ts +7 -0
  259. package/src/core/diagnostics/replay.ts +314 -0
  260. package/src/core/diagnostics/sanitize.ts +97 -0
  261. package/src/core/events/legacy-storage/compression.ts +130 -0
  262. package/src/core/events/legacy-storage/index.ts +1 -0
  263. package/src/core/events/v3/archive-retention.ts +251 -0
  264. package/src/core/events/v3/capabilities.ts +2 -0
  265. package/src/core/events/v3/index.ts +9 -0
  266. package/src/core/events/v3/live-routing.ts +1 -0
  267. package/src/core/events/v3/producers/hook-base.ts +14 -2
  268. package/src/core/events/v3/producers/intake.ts +1 -0
  269. package/src/core/events/v3/producers/recorder.ts +110 -1
  270. package/src/core/hooks/adapter/output.ts +12 -1
  271. package/src/core/hooks/cli.ts +192 -215
  272. package/src/core/hooks/health.ts +146 -0
  273. package/src/core/resources/contract.ts +98 -0
  274. package/src/core/resources/index.ts +5 -0
  275. package/src/core/resources/sampler.ts +458 -0
  276. package/src/core/resources/service-status.ts +62 -0
  277. package/src/core/resources/service.ts +337 -0
  278. package/src/core/resources/storage.ts +44 -0
  279. package/src/core/storage/atomic-json.ts +15 -0
  280. package/src/core/storage/builtins.ts +62 -6
  281. package/src/core/storage/logger.ts +2 -0
  282. package/src/core/storage/query.ts +58 -0
  283. package/src/core/supervisor/activity.ts +193 -0
  284. package/src/core/supervisor/contract.ts +312 -0
  285. package/src/core/supervisor/explanations.ts +104 -0
  286. package/src/core/supervisor/findings.ts +788 -0
  287. package/src/core/supervisor/history.ts +61 -0
  288. package/src/core/supervisor/hook-health-alerts.ts +108 -0
  289. package/src/core/supervisor/hook-health-storage.ts +25 -0
  290. package/src/core/supervisor/hook-health.ts +301 -0
  291. package/src/core/supervisor/hooks.ts +35 -0
  292. package/src/core/supervisor/index.ts +14 -0
  293. package/src/core/supervisor/log-feed.ts +131 -0
  294. package/src/core/supervisor/service.ts +511 -0
  295. package/src/core/supervisor/services.ts +203 -0
  296. package/src/core/supervisor/status.ts +59 -0
  297. package/src/core/supervisor/storage.ts +126 -0
  298. package/src/core/supervisor/timeline.ts +133 -0
  299. package/src/core/workflow/admission.ts +215 -0
  300. package/src/core/workflow/engine.ts +93 -1
  301. package/src/core/workflow/index.ts +10 -0
  302. package/src/core/workflow/proof.ts +54 -1
  303. package/src/core/workflow/run-state.ts +13 -1
  304. package/src/core/workflow/types.ts +39 -1
  305. package/src/lib/browser/client.ts +6 -0
  306. package/src/lib/browser/index.ts +29 -0
  307. package/src/lib/browser/qa-run-contracts.ts +380 -0
  308. package/src/lib/browser/qa-run.ts +784 -0
  309. package/src/lib/coord-root-id.ts +23 -0
  310. package/src/lib/instructions/apply.ts +81 -2
  311. package/src/lib/instructions/templates.ts +6 -0
@@ -108,6 +108,35 @@ export {
108
108
  type QaSignature,
109
109
  type QaStylesheetSignature,
110
110
  } from "./qa-plan.js";
111
+ export {
112
+ defaultQaRunExec,
113
+ QA_RUN_HEADLESS_ONLY_ENV,
114
+ QA_RUN_RESULT_FILENAME,
115
+ type QaRunExec,
116
+ type QaRunExecOptions,
117
+ type QaRunExecResult,
118
+ type QaRunMatrixOptions,
119
+ runQaMatrix,
120
+ } from "./qa-run.js";
121
+ export {
122
+ computeVerdict,
123
+ contextIdFor,
124
+ mergeCoverage,
125
+ QA_RUN_JOB_SCHEMA_VERSION,
126
+ QA_RUN_RESULT_SCHEMA_VERSION,
127
+ type QaRunBlocker,
128
+ type QaRunCheck,
129
+ type QaRunCommandOutcome,
130
+ type QaRunContext,
131
+ type QaRunCritiqueOutcome,
132
+ type QaRunInteraction,
133
+ type QaRunJob,
134
+ type QaRunJobValidation,
135
+ type QaRunPolicy,
136
+ type QaRunResult,
137
+ type QaRunVerdict,
138
+ validateQaRunJob,
139
+ } from "./qa-run-contracts.js";
111
140
  export {
112
141
  listQaSnapshotTargets,
113
142
  loadQaSnapshot,
@@ -0,0 +1,380 @@
1
+ // Contracts for the qa-run matrix runner: one JSON job in, one JSON result
2
+ // out, with the coverage-merge and verdict rules as pure functions.
3
+ //
4
+ // The runner executes a whole page-QA matrix (planner, deterministic gates,
5
+ // interactions, critique, snapshot) in one process so the driving agent stops
6
+ // paying a model turn per browser command. These contracts are frozen first so
7
+ // the orchestrator, its tests, and downstream consumers build against the same
8
+ // shape.
9
+ //
10
+ // Two rules are load-bearing and enforced here, not in prose:
11
+ // - A job may WIDEN coverage beyond the planner's manifest but can never
12
+ // narrow it below the manifest (mergeCoverage is a union that keeps
13
+ // manifest order first).
14
+ // - A job must not carry credentials. Authentication is referenced through
15
+ // existing browser profiles and cookie stores; validation refuses
16
+ // secret-shaped fields before any browser or model work starts.
17
+ //
18
+ // Toolkit tier: this module must not import src/core (layering check).
19
+
20
+ import type { QaContext, QaManifest } from "./qa-plan.js";
21
+
22
+ export const QA_RUN_JOB_SCHEMA_VERSION = 1 as const;
23
+ export const QA_RUN_RESULT_SCHEMA_VERSION = 1 as const;
24
+
25
+ /** One rendering context the runner will capture and check. */
26
+ export interface QaRunContext {
27
+ /** Stable ID, unique within the job; also the artifact prefix component.
28
+ * Derived contexts use `<viewport>-<theme>-<state>`. */
29
+ id: string;
30
+ /** Viewport preset name (`desktop`, `mobile`, …) or `WxH`. */
31
+ viewport: string;
32
+ theme: "light" | "dark";
33
+ /** Named UI state; `default` for the plain page. */
34
+ state: string;
35
+ /** Extra browse arguments that render this context (theme forcing beyond
36
+ * `--color-scheme`, state setup, waits). Argument array by contract. */
37
+ args?: string[];
38
+ }
39
+
40
+ /** One deterministic gate: extra browse arguments appended to the context's
41
+ * base capture command. Arguments are an array by contract — the runner never
42
+ * builds a shell string. */
43
+ export interface QaRunCheck {
44
+ /** Stable ID, unique within the job (e.g. `overflow`, `contrast-hero`). */
45
+ id: string;
46
+ /** Browse flags for this gate, e.g. ["--check-overflow", "--check-overflow-fail"]. */
47
+ args: string[];
48
+ /** Context IDs this gate applies to. Absent = every context. */
49
+ contexts?: string[];
50
+ }
51
+
52
+ /** One named interaction state: setup actions plus outcome assertions that
53
+ * prove the state changed, not merely that something was clicked. */
54
+ export interface QaRunInteraction {
55
+ /** State name; must match a planner/state name when the planner declared one. */
56
+ name: string;
57
+ /** Browse flags that produce the state (e.g. ["--batch", "click #tab-2; wait 500"]). */
58
+ setup: string[];
59
+ /** Assertion specs passed as repeated --assert values. At least one is
60
+ * required — a click without a proven outcome is not an interaction gate. */
61
+ assertions: string[];
62
+ }
63
+
64
+ export interface QaRunPolicy {
65
+ /** Permit the critique provider's metered-API fallback. Default false: an
66
+ * exhausted headless-harness list becomes an `incomplete` blocker. */
67
+ allow_metered_critique?: boolean;
68
+ /** Concurrent deterministic captures (default 2). Interactions always run
69
+ * serially regardless of this value. */
70
+ command_concurrency?: number;
71
+ /** Per-command timeout in milliseconds (default 120000). */
72
+ command_timeout_ms?: number;
73
+ }
74
+
75
+ export interface QaRunJob {
76
+ schema_version: typeof QA_RUN_JOB_SCHEMA_VERSION;
77
+ /** URL, local file path, or framework route the runner will render. */
78
+ target: string;
79
+ /** Git SHA or content identifier of what is being tested. */
80
+ tested_revision?: string;
81
+ /** signoff persists a QA snapshot on a passing run; review never does. */
82
+ mode: "signoff" | "review";
83
+ /** Extra coverage beyond the planner manifest (union, never narrowing). */
84
+ contexts?: QaRunContext[];
85
+ /** Extra deterministic gates. The runner always executes every planner-
86
+ * required deterministic check; a job can add gates but cannot remove them. */
87
+ checks?: QaRunCheck[];
88
+ interaction_states?: QaRunInteraction[];
89
+ /** Forwarded to the planner as --qa-scope / --qa-states inputs. */
90
+ qa_hints?: { scopes?: string[]; states?: string[] };
91
+ policy?: QaRunPolicy;
92
+ }
93
+
94
+ /** Outcome of one executed browse command. */
95
+ export interface QaRunCommandOutcome {
96
+ context_id: string;
97
+ /** Gate ID, `capture` for the base capture, `plan`, `critique`, or an
98
+ * interaction state name prefixed with `interaction:`. */
99
+ check_id: string;
100
+ argv: string[];
101
+ exit_code: number | null;
102
+ outcome: "passed" | "failed" | "unknown";
103
+ /** Human-readable failure details parsed from the JSON artifact. */
104
+ failures: string[];
105
+ artifacts: { png?: string; html?: string; json?: string };
106
+ wall_time_ms: number;
107
+ }
108
+
109
+ export interface QaRunCritiqueOutcome {
110
+ context_id: string;
111
+ provider: string;
112
+ tiles_total: number;
113
+ tiles_reviewed: number;
114
+ tiles_reused: number;
115
+ outcome: "passed" | "failed" | "unknown";
116
+ findings: Array<{ severity: string; summary: string; selector?: string }>;
117
+ }
118
+
119
+ export interface QaRunBlocker {
120
+ stage: "validate" | "plan" | "gates" | "interactions" | "critique" | "snapshot" | "result";
121
+ context_id?: string;
122
+ reason: string;
123
+ }
124
+
125
+ export type QaRunVerdict = "passed" | "failed" | "incomplete";
126
+
127
+ export interface QaRunResult {
128
+ schema_version: typeof QA_RUN_RESULT_SCHEMA_VERSION;
129
+ target: string;
130
+ tested_revision?: string;
131
+ mode: "signoff" | "review";
132
+ /** The authoritative planner manifest, null when planning itself failed. */
133
+ qa_plan: QaManifest | null;
134
+ /** Merged coverage in manifest order (manifest contexts first). */
135
+ contexts: QaRunContext[];
136
+ commands: QaRunCommandOutcome[];
137
+ critique: QaRunCritiqueOutcome[];
138
+ snapshot: { saved: boolean; path?: string };
139
+ wall_time_ms: {
140
+ plan: number;
141
+ gates: number;
142
+ interactions: number;
143
+ critique: number;
144
+ snapshot: number;
145
+ total: number;
146
+ };
147
+ blockers: QaRunBlocker[];
148
+ verdict: QaRunVerdict;
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Job validation
153
+ // ---------------------------------------------------------------------------
154
+
155
+ /** Field names that indicate an embedded credential. Matched on any object
156
+ * key anywhere in the job, case-insensitively. */
157
+ const SECRET_KEY_PATTERN =
158
+ /(password|passwd|secret|token|api[-_]?key|authorization|cookie|credential|bearer)/i;
159
+
160
+ /** Value shapes that indicate an embedded credential even under an innocent
161
+ * key: explicit auth headers and long unbroken high-entropy-looking blobs. */
162
+ const SECRET_VALUE_PATTERNS = [/\bBearer\s+[\w.~+/=-]{16,}/i, /\bBasic\s+[A-Za-z0-9+/=]{16,}/];
163
+
164
+ export type QaRunJobValidation = { ok: true; job: QaRunJob } | { ok: false; errors: string[] };
165
+
166
+ function scanForSecrets(value: unknown, path: string, errors: string[]): void {
167
+ if (typeof value === "string") {
168
+ for (const pattern of SECRET_VALUE_PATTERNS) {
169
+ if (pattern.test(value)) {
170
+ errors.push(
171
+ `${path}: value looks like a credential — reference a browser profile or cookie store instead`,
172
+ );
173
+ return;
174
+ }
175
+ }
176
+ return;
177
+ }
178
+ if (Array.isArray(value)) {
179
+ value.forEach((item, i) => {
180
+ scanForSecrets(item, `${path}[${i}]`, errors);
181
+ });
182
+ return;
183
+ }
184
+ if (value && typeof value === "object") {
185
+ for (const [key, child] of Object.entries(value)) {
186
+ if (SECRET_KEY_PATTERN.test(key)) {
187
+ errors.push(
188
+ `${path}.${key}: secret-bearing field names are refused — reference a browser profile or cookie store instead`,
189
+ );
190
+ continue;
191
+ }
192
+ scanForSecrets(child, `${path}.${key}`, errors);
193
+ }
194
+ }
195
+ }
196
+
197
+ function isStringArray(value: unknown): value is string[] {
198
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
199
+ }
200
+
201
+ /**
202
+ * Validate an untrusted job document. Structural errors and secret-bearing
203
+ * fields are all reported at once so a caller can fix the job in one pass.
204
+ * Validation is deliberately strict: an underspecified job fails here, before
205
+ * any browser or model process starts.
206
+ */
207
+ export function validateQaRunJob(value: unknown): QaRunJobValidation {
208
+ const errors: string[] = [];
209
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
210
+ return { ok: false, errors: ["job must be a JSON object"] };
211
+ }
212
+ const job = value as Record<string, unknown>;
213
+
214
+ if (job.schema_version !== QA_RUN_JOB_SCHEMA_VERSION) {
215
+ errors.push(`schema_version must be ${QA_RUN_JOB_SCHEMA_VERSION}`);
216
+ }
217
+ if (typeof job.target !== "string" || job.target.length === 0) {
218
+ errors.push("target is required (URL, file path, or route)");
219
+ }
220
+ if (job.mode !== "signoff" && job.mode !== "review") {
221
+ errors.push('mode must be "signoff" or "review"');
222
+ }
223
+
224
+ const ids = new Set<string>();
225
+ if (job.contexts !== undefined) {
226
+ if (!Array.isArray(job.contexts)) {
227
+ errors.push("contexts must be an array");
228
+ } else {
229
+ job.contexts.forEach((ctx, i) => {
230
+ const c = ctx as Record<string, unknown>;
231
+ if (typeof c?.id !== "string" || c.id.length === 0)
232
+ errors.push(`contexts[${i}].id is required`);
233
+ else if (ids.has(c.id)) errors.push(`contexts[${i}].id duplicates "${c.id}"`);
234
+ else ids.add(c.id);
235
+ if (typeof c?.viewport !== "string" || c.viewport.length === 0) {
236
+ errors.push(`contexts[${i}].viewport is required`);
237
+ }
238
+ if (c?.theme !== "light" && c?.theme !== "dark") {
239
+ errors.push(`contexts[${i}].theme must be "light" or "dark"`);
240
+ }
241
+ if (typeof c?.state !== "string" || c.state.length === 0) {
242
+ errors.push(`contexts[${i}].state is required ("default" for the plain page)`);
243
+ }
244
+ if (c?.args !== undefined && !isStringArray(c.args)) {
245
+ errors.push(`contexts[${i}].args must be an argument array (never a shell string)`);
246
+ }
247
+ });
248
+ }
249
+ }
250
+
251
+ if (job.checks !== undefined) {
252
+ if (!Array.isArray(job.checks)) {
253
+ errors.push("checks must be an array");
254
+ } else {
255
+ const checkIds = new Set<string>();
256
+ job.checks.forEach((check, i) => {
257
+ const c = check as Record<string, unknown>;
258
+ if (typeof c?.id !== "string" || c.id.length === 0)
259
+ errors.push(`checks[${i}].id is required`);
260
+ else if (checkIds.has(c.id)) errors.push(`checks[${i}].id duplicates "${c.id}"`);
261
+ else checkIds.add(c.id);
262
+ if (!isStringArray(c?.args) || (c.args as string[]).length === 0) {
263
+ errors.push(
264
+ `checks[${i}].args must be a non-empty argument array (never a shell string)`,
265
+ );
266
+ }
267
+ if (c?.contexts !== undefined && !isStringArray(c.contexts)) {
268
+ errors.push(`checks[${i}].contexts must be an array of context IDs`);
269
+ }
270
+ });
271
+ }
272
+ }
273
+
274
+ if (job.interaction_states !== undefined) {
275
+ if (!Array.isArray(job.interaction_states)) {
276
+ errors.push("interaction_states must be an array");
277
+ } else {
278
+ job.interaction_states.forEach((state, i) => {
279
+ const s = state as Record<string, unknown>;
280
+ if (typeof s?.name !== "string" || s.name.length === 0) {
281
+ errors.push(`interaction_states[${i}].name is required`);
282
+ }
283
+ if (!isStringArray(s?.setup) || (s.setup as string[]).length === 0) {
284
+ errors.push(`interaction_states[${i}].setup must be a non-empty argument array`);
285
+ }
286
+ if (!isStringArray(s?.assertions) || (s.assertions as string[]).length === 0) {
287
+ errors.push(
288
+ `interaction_states[${i}].assertions must name at least one outcome assertion — a click without a proven outcome is not a gate`,
289
+ );
290
+ }
291
+ });
292
+ }
293
+ }
294
+
295
+ if (job.policy !== undefined) {
296
+ const p = job.policy as Record<string, unknown>;
297
+ if (p?.command_concurrency !== undefined) {
298
+ const n = p.command_concurrency;
299
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > 8) {
300
+ errors.push("policy.command_concurrency must be an integer between 1 and 8");
301
+ }
302
+ }
303
+ if (p?.command_timeout_ms !== undefined) {
304
+ const n = p.command_timeout_ms;
305
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1000) {
306
+ errors.push("policy.command_timeout_ms must be an integer ≥ 1000");
307
+ }
308
+ }
309
+ }
310
+
311
+ scanForSecrets(job, "job", errors);
312
+
313
+ if (errors.length > 0) return { ok: false, errors };
314
+ return { ok: true, job: value as QaRunJob };
315
+ }
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Coverage merge — widen-only
319
+ // ---------------------------------------------------------------------------
320
+
321
+ export function contextIdFor(context: QaContext): string {
322
+ return `${context.viewport}-${context.theme}-${context.state}`;
323
+ }
324
+
325
+ /**
326
+ * Merge planner-manifest coverage with the job's extra contexts. The manifest
327
+ * is the floor: its contexts always run, in manifest order, and a job can only
328
+ * append. The union construction makes narrowing impossible by design; the
329
+ * returned list is what the runner executes and what the result reports.
330
+ */
331
+ export function mergeCoverage(manifest: QaManifest, job: QaRunJob): QaRunContext[] {
332
+ const merged: QaRunContext[] = manifest.contexts.map((context) => ({
333
+ id: contextIdFor(context),
334
+ viewport: context.viewport,
335
+ theme: context.theme,
336
+ state: context.state,
337
+ }));
338
+ const seen = new Set(merged.map((context) => context.id));
339
+ for (const context of job.contexts ?? []) {
340
+ const canonical = contextIdFor(context);
341
+ if (seen.has(canonical) || seen.has(context.id)) continue;
342
+ seen.add(canonical);
343
+ seen.add(context.id);
344
+ merged.push(context);
345
+ }
346
+ return merged;
347
+ }
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // Verdict — fail-closed
351
+ // ---------------------------------------------------------------------------
352
+
353
+ /**
354
+ * Compute the terminal verdict from completed evidence. The rules are the
355
+ * existing page-QA signoff contract, fail-closed:
356
+ * - any blocker → incomplete (a blocker is a fact the run could not
357
+ * establish, so a defect cannot be ruled out);
358
+ * - any failed or unknown command/critique outcome → failed when the check
359
+ * completed and found a defect, incomplete when the outcome is unknown;
360
+ * - signoff mode additionally requires the snapshot to have been saved.
361
+ * `passed` is only reachable when every input proves out.
362
+ */
363
+ export function computeVerdict(input: {
364
+ mode: QaRunJob["mode"];
365
+ blockers: QaRunBlocker[];
366
+ commands: QaRunCommandOutcome[];
367
+ critique: QaRunCritiqueOutcome[];
368
+ snapshotSaved: boolean;
369
+ }): QaRunVerdict {
370
+ const failed =
371
+ input.commands.some((command) => command.outcome === "failed") ||
372
+ input.critique.some((entry) => entry.outcome === "failed");
373
+ if (failed) return "failed";
374
+ const unknown =
375
+ input.commands.some((command) => command.outcome === "unknown") ||
376
+ input.critique.some((entry) => entry.outcome === "unknown");
377
+ if (input.blockers.length > 0 || unknown) return "incomplete";
378
+ if (input.mode === "signoff" && !input.snapshotSaved) return "incomplete";
379
+ return "passed";
380
+ }