@reticlehq/server 3.9.0 → 3.10.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 (350) hide show
  1. package/SKILL.md +13 -12
  2. package/dist/command/cli/adhoc-suite.js +2 -0
  3. package/dist/command/cli/adhoc-verdict.d.ts +2 -43
  4. package/dist/command/cli/adhoc-verdict.js +50 -187
  5. package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
  6. package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
  7. package/dist/command/cli/cli-doctor.js +1 -1
  8. package/dist/command/cli/cli-flow-commands.js +2 -9
  9. package/dist/command/cli/cli-kill.js +1 -1
  10. package/dist/command/cli/cli-parse-compare.d.ts +22 -0
  11. package/dist/command/cli/cli-parse-compare.js +75 -0
  12. package/dist/command/cli/cli-parse-grade.d.ts +36 -0
  13. package/dist/command/cli/cli-parse-grade.js +75 -0
  14. package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
  15. package/dist/command/cli/cli-parse-grammar.js +18 -20
  16. package/dist/command/cli/cli-parse-verify.d.ts +10 -0
  17. package/dist/command/cli/cli-parse-verify.js +63 -14
  18. package/dist/command/cli/cli-parse.d.ts +17 -15
  19. package/dist/command/cli/cli-parse.js +44 -27
  20. package/dist/command/cli/cli-update-commands.d.ts +1 -1
  21. package/dist/command/cli/cli-update-commands.js +29 -20
  22. package/dist/command/cli/cli-usage.js +58 -29
  23. package/dist/command/cli/cli-verify.d.ts +11 -2
  24. package/dist/command/cli/cli-verify.js +27 -8
  25. package/dist/command/cli/cloud-cli.js +2 -119
  26. package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
  27. package/dist/command/cli/daemon-link/daemon-client.js +205 -0
  28. package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
  29. package/dist/command/cli/doctor-repairs.js +1 -1
  30. package/dist/command/cli/ensure-daemon.d.ts +26 -0
  31. package/dist/command/cli/ensure-daemon.js +57 -0
  32. package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
  33. package/dist/command/cli/finished-run/grade-command.js +180 -0
  34. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
  35. package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
  36. package/dist/command/cli/ports/port-holder.d.ts +9 -0
  37. package/dist/command/cli/ports/port-holder.js +27 -3
  38. package/dist/command/cli/status/status-lines.d.ts +1 -0
  39. package/dist/command/cli/status/status-lines.js +22 -0
  40. package/dist/command/cli/status-fields.d.ts +5 -1
  41. package/dist/command/cli/status-fields.js +7 -3
  42. package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
  43. package/dist/command/cli/two-builds/compare-command.js +314 -0
  44. package/dist/command/cli/verify-against.d.ts +61 -0
  45. package/dist/command/cli/verify-against.js +187 -0
  46. package/dist/command/cli/verify-no-session.d.ts +2 -0
  47. package/dist/command/cli/verify-no-session.js +13 -0
  48. package/dist/command/cli/verify-results.d.ts +35 -2
  49. package/dist/command/cli/verify-results.js +76 -10
  50. package/dist/command/cli.js +46 -41
  51. package/dist/command/daemon/daemon-resolve.d.ts +8 -1
  52. package/dist/command/daemon/daemon-resolve.js +27 -1
  53. package/dist/command/daemon/daemon.d.ts +12 -1
  54. package/dist/command/daemon/daemon.js +35 -4
  55. package/dist/command/daemon/lifetime/death-cause.js +21 -2
  56. package/dist/command/daemon/lifetime/heartbeat.js +7 -2
  57. package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
  58. package/dist/command/daemon/lifetime/startup-failure.js +62 -0
  59. package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
  60. package/dist/command/setup/bringup/dev-server-files.js +17 -0
  61. package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
  62. package/dist/command/setup/bringup/static-page-server.js +43 -9
  63. package/dist/command/setup/init/init-runtime.js +6 -0
  64. package/dist/command/setup/node-effects.d.ts +3 -2
  65. package/dist/command/setup/node-effects.js +9 -3
  66. package/dist/command/setup/remaining-steps.d.ts +8 -0
  67. package/dist/command/setup/remaining-steps.js +10 -0
  68. package/dist/command/setup/run-setup.d.ts +2 -0
  69. package/dist/command/setup/run-setup.js +9 -1
  70. package/dist/command/setup/setup-command.js +6 -3
  71. package/dist/command/update/updater.js +6 -0
  72. package/dist/command/version/version-skew.js +1 -1
  73. package/dist/features/compare/action-plan.d.ts +260 -0
  74. package/dist/features/compare/action-plan.js +543 -0
  75. package/dist/features/compare/behaviour-compare.d.ts +24 -0
  76. package/dist/features/compare/behaviour-compare.js +85 -0
  77. package/dist/features/compare/behaviour-diff.d.ts +97 -0
  78. package/dist/features/compare/behaviour-diff.js +202 -0
  79. package/dist/features/compare/behaviour-report.d.ts +40 -0
  80. package/dist/features/compare/behaviour-report.js +128 -0
  81. package/dist/features/compare/build-readings.d.ts +106 -0
  82. package/dist/features/compare/build-readings.js +250 -0
  83. package/dist/features/compare/compare-diff.d.ts +62 -0
  84. package/dist/features/compare/compare-diff.js +177 -0
  85. package/dist/features/compare/compare-report-html.d.ts +6 -0
  86. package/dist/features/compare/compare-report-html.js +102 -0
  87. package/dist/features/compare/compare-report.d.ts +15 -0
  88. package/dist/features/compare/compare-report.js +73 -0
  89. package/dist/features/compare/compare-run.d.ts +60 -0
  90. package/dist/features/compare/compare-run.js +123 -0
  91. package/dist/features/compare/outside/effect-lines.d.ts +26 -0
  92. package/dist/features/compare/outside/effect-lines.js +103 -0
  93. package/dist/features/compare/outside/state-sources.d.ts +76 -0
  94. package/dist/features/compare/outside/state-sources.js +164 -0
  95. package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
  96. package/dist/features/compare/outside/value-dictionary.js +227 -0
  97. package/dist/features/compare/visible-content.d.ts +20 -0
  98. package/dist/features/compare/visible-content.js +125 -0
  99. package/dist/features/crawl/crawl-empty.js +2 -1
  100. package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
  101. package/dist/features/crawl/crawl-exhaustive.js +138 -0
  102. package/dist/features/crawl/crawl-tools.d.ts +9 -2
  103. package/dist/features/crawl/crawl-tools.js +28 -12
  104. package/dist/features/crawl/crawl.d.ts +8 -9
  105. package/dist/features/crawl/crawl.js +61 -65
  106. package/dist/features/exhaust/app-coverage.d.ts +5 -1
  107. package/dist/features/exhaust/app-coverage.js +3 -1
  108. package/dist/features/exhaust/code-coverage.js +8 -4
  109. package/dist/features/exhaust/edge-cases.d.ts +133 -0
  110. package/dist/features/exhaust/edge-cases.js +310 -0
  111. package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
  112. package/dist/features/exhaust/edges/edge-drive.js +364 -0
  113. package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
  114. package/dist/features/exhaust/edges/edge-run.js +169 -0
  115. package/dist/features/exhaust/explorer.d.ts +87 -14
  116. package/dist/features/exhaust/explorer.js +159 -25
  117. package/dist/features/exhaust/failure-branches.d.ts +5 -0
  118. package/dist/features/exhaust/failure-branches.js +29 -11
  119. package/dist/features/exhaust/invariants.d.ts +112 -0
  120. package/dist/features/exhaust/invariants.js +188 -0
  121. package/dist/features/exhaust/ledger.d.ts +68 -0
  122. package/dist/features/exhaust/ledger.js +121 -13
  123. package/dist/features/exhaust/session-fold.d.ts +10 -0
  124. package/dist/features/exhaust/session-fold.js +46 -3
  125. package/dist/features/exhaust/session-port.d.ts +3 -1
  126. package/dist/features/exhaust/session-port.js +97 -28
  127. package/dist/features/grade/grade-answer.d.ts +99 -0
  128. package/dist/features/grade/grade-answer.js +112 -0
  129. package/dist/features/grade/grade-noise.d.ts +27 -0
  130. package/dist/features/grade/grade-noise.js +55 -0
  131. package/dist/features/grade/grade-renumber.d.ts +12 -0
  132. package/dist/features/grade/grade-renumber.js +86 -0
  133. package/dist/features/grade/grade-state.d.ts +50 -0
  134. package/dist/features/grade/grade-state.js +93 -0
  135. package/dist/features/grade/grade-suggest.d.ts +33 -0
  136. package/dist/features/grade/grade-suggest.js +98 -0
  137. package/dist/features/visual/visual-tools.js +66 -19
  138. package/dist/index.js +7 -4
  139. package/dist/judgement/capsule/causal-summary.js +9 -2
  140. package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
  141. package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
  142. package/dist/judgement/runs/reticle-runner.js +16 -2
  143. package/dist/judgement/runs/runner-port.d.ts +2 -0
  144. package/dist/judgement/runs/runner-port.js +23 -5
  145. package/dist/language/flows/affected-tools.d.ts +1 -1
  146. package/dist/language/flows/affected-tools.js +49 -30
  147. package/dist/language/flows/change/git-changed.d.ts +2 -1
  148. package/dist/language/flows/change/git-changed.js +19 -2
  149. package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
  150. package/dist/language/flows/fields/flow-secret-field.js +77 -21
  151. package/dist/language/flows/flow-anchor.d.ts +60 -2
  152. package/dist/language/flows/flow-anchor.js +125 -20
  153. package/dist/language/flows/flow-classify.d.ts +20 -0
  154. package/dist/language/flows/flow-classify.js +90 -12
  155. package/dist/language/flows/flow-cross-step.d.ts +7 -1
  156. package/dist/language/flows/flow-cross-step.js +25 -1
  157. package/dist/language/flows/flow-journey.d.ts +38 -4
  158. package/dist/language/flows/flow-journey.js +138 -8
  159. package/dist/language/flows/flow-replay-run.d.ts +0 -16
  160. package/dist/language/flows/flow-replay-run.js +76 -14
  161. package/dist/language/flows/flow-replay-types.d.ts +4 -2
  162. package/dist/language/flows/flow-replay.d.ts +19 -1
  163. package/dist/language/flows/flow-replay.js +169 -52
  164. package/dist/language/flows/flow-replayable.js +7 -1
  165. package/dist/language/flows/flow-start-path.d.ts +34 -5
  166. package/dist/language/flows/flow-start-path.js +202 -15
  167. package/dist/language/flows/flow-step-runners.d.ts +14 -13
  168. package/dist/language/flows/flow-step-runners.js +184 -109
  169. package/dist/language/flows/flow-store-for-session.d.ts +26 -25
  170. package/dist/language/flows/flow-store-for-session.js +44 -0
  171. package/dist/language/flows/flow-tools.js +46 -33
  172. package/dist/language/flows/flows.js +176 -76
  173. package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
  174. package/dist/language/flows/recording/tape/recordings.js +71 -5
  175. package/dist/language/flows/replay.d.ts +19 -5
  176. package/dist/language/flows/replay.js +99 -7
  177. package/dist/language/flows/steps/page-steps.d.ts +68 -0
  178. package/dist/language/flows/steps/page-steps.js +198 -0
  179. package/dist/language/flows/suite/sequential-run.d.ts +68 -0
  180. package/dist/language/flows/suite/sequential-run.js +96 -0
  181. package/dist/language/flows/verify-change-tools.js +26 -15
  182. package/dist/memory/cloud/background-sync.d.ts +13 -0
  183. package/dist/memory/cloud/background-sync.js +24 -3
  184. package/dist/memory/cloud/sync-daemon.js +26 -27
  185. package/dist/memory/journal/attach-journal.js +2 -1
  186. package/dist/memory/journal/drive-flow.d.ts +7 -0
  187. package/dist/memory/journal/drive-flow.js +44 -13
  188. package/dist/memory/journal/journal-recorder.d.ts +4 -2
  189. package/dist/memory/journal/session-journal.d.ts +7 -0
  190. package/dist/memory/journal/session-journal.js +58 -10
  191. package/dist/memory/project/root-claims.d.ts +9 -0
  192. package/dist/memory/project/root-claims.js +9 -4
  193. package/dist/portal/input/download-capture.d.ts +7 -0
  194. package/dist/portal/input/download-capture.js +92 -0
  195. package/dist/portal/input/network-detail.d.ts +6 -0
  196. package/dist/portal/input/network-detail.js +12 -1
  197. package/dist/portal/input/network-mock-tools.js +49 -13
  198. package/dist/portal/input/network-mock.d.ts +8 -0
  199. package/dist/portal/input/network-mock.js +28 -9
  200. package/dist/portal/input/page-capture.d.ts +24 -0
  201. package/dist/portal/input/page-capture.js +33 -0
  202. package/dist/portal/input/real-input.d.ts +22 -20
  203. package/dist/portal/input/real-input.js +41 -31
  204. package/dist/portal/input/viewport-tools.d.ts +8 -0
  205. package/dist/portal/input/viewport-tools.js +75 -18
  206. package/dist/portal/pool/browser-pool.d.ts +11 -29
  207. package/dist/portal/pool/browser-pool.js +35 -17
  208. package/dist/portal/pool/context-permissions.d.ts +17 -3
  209. package/dist/portal/pool/context-permissions.js +36 -5
  210. package/dist/portal/pool/determinism.d.ts +52 -0
  211. package/dist/portal/pool/determinism.js +259 -0
  212. package/dist/portal/pool/lease-emulation.d.ts +34 -1
  213. package/dist/portal/pool/lease-emulation.js +18 -0
  214. package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
  215. package/dist/portal/pool/lease-marker-stamp.js +15 -1
  216. package/dist/portal/pool/lease-page-error.d.ts +44 -17
  217. package/dist/portal/pool/lease-page-error.js +147 -5
  218. package/dist/portal/pool/page-error-journal.d.ts +7 -1
  219. package/dist/portal/pool/page-error-journal.js +27 -0
  220. package/dist/portal/pool/playwright-launcher.js +51 -8
  221. package/dist/portal/pool/pool-contract.d.ts +27 -1
  222. package/dist/portal/pool/pool-lease.d.ts +36 -1
  223. package/dist/portal/pool/zero-install.d.ts +8 -0
  224. package/dist/portal/pool/zero-install.js +23 -1
  225. package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
  226. package/dist/portal/session/dev-server/dev-command.js +61 -15
  227. package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
  228. package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
  229. package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
  230. package/dist/portal/session/dev-server/project-ports.js +62 -0
  231. package/dist/portal/session/facts/departed-network.d.ts +66 -0
  232. package/dist/portal/session/facts/departed-network.js +142 -0
  233. package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
  234. package/dist/portal/session/no-session-diagnosis.js +67 -25
  235. package/dist/portal/session/no-session-next-action.d.ts +2 -0
  236. package/dist/portal/session/no-session-next-action.js +12 -3
  237. package/dist/portal/session/no-session-watch.d.ts +6 -0
  238. package/dist/portal/session/no-session-watch.js +38 -3
  239. package/dist/portal/session/serving-directory.d.ts +10 -0
  240. package/dist/portal/session/serving-directory.js +39 -8
  241. package/dist/portal/session/session-manager.d.ts +10 -0
  242. package/dist/portal/session/session-manager.js +174 -63
  243. package/dist/start-wiring.d.ts +2 -1
  244. package/dist/start-wiring.js +2 -1
  245. package/dist/surface/mcp/mcp.js +3 -2
  246. package/dist/surface/mcp/shared-params.d.ts +2 -0
  247. package/dist/surface/mcp/shared-params.js +31 -0
  248. package/dist/surface/tools/act/act-observation.d.ts +40 -2
  249. package/dist/surface/tools/act/act-observation.js +118 -12
  250. package/dist/surface/tools/act/act-preflight.js +1 -1
  251. package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
  252. package/dist/surface/tools/act/act-sequence-retry.js +61 -5
  253. package/dist/surface/tools/act/after-match.d.ts +4 -1
  254. package/dist/surface/tools/act/after-match.js +8 -1
  255. package/dist/surface/tools/act/already-true.js +1 -1
  256. package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
  257. package/dist/surface/tools/act/navigation/reload-result.js +25 -2
  258. package/dist/surface/tools/act/resolve-target.d.ts +15 -1
  259. package/dist/surface/tools/act/resolve-target.js +24 -1
  260. package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
  261. package/dist/surface/tools/act/sequence-grade.js +37 -26
  262. package/dist/surface/tools/act/settle-in-flight.js +27 -15
  263. package/dist/surface/tools/act-command.d.ts +23 -0
  264. package/dist/surface/tools/act-command.js +50 -0
  265. package/dist/surface/tools/act-sequence-tool.js +78 -50
  266. package/dist/surface/tools/act-tools.d.ts +1 -13
  267. package/dist/surface/tools/act-tools.js +74 -86
  268. package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
  269. package/dist/surface/tools/agent/build-staleness.js +1 -1
  270. package/dist/surface/tools/assert/assert-verdict.js +16 -37
  271. package/dist/surface/tools/browser-tools.js +12 -6
  272. package/dist/surface/tools/compare-tools.d.ts +11 -0
  273. package/dist/surface/tools/compare-tools.js +94 -0
  274. package/dist/surface/tools/coverage-tools.js +1 -1
  275. package/dist/surface/tools/dynamic-tools.js +9 -2
  276. package/dist/surface/tools/error-recovery.d.ts +12 -1
  277. package/dist/surface/tools/error-recovery.js +91 -21
  278. package/dist/surface/tools/explore-tools.js +1 -1
  279. package/dist/surface/tools/invoke-tool.js +3 -2
  280. package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
  281. package/dist/surface/tools/lease-coverage-tools.js +128 -0
  282. package/dist/surface/tools/lease-hint.d.ts +35 -1
  283. package/dist/surface/tools/lease-hint.js +42 -0
  284. package/dist/surface/tools/lease-readiness.d.ts +26 -0
  285. package/dist/surface/tools/lease-readiness.js +79 -0
  286. package/dist/surface/tools/lease-seed.d.ts +14 -1
  287. package/dist/surface/tools/lease-seed.js +35 -3
  288. package/dist/surface/tools/lease-tools.d.ts +3 -41
  289. package/dist/surface/tools/lease-tools.js +62 -110
  290. package/dist/surface/tools/lease-url-identity.d.ts +46 -0
  291. package/dist/surface/tools/lease-url-identity.js +97 -0
  292. package/dist/surface/tools/merge-tools.js +99 -2
  293. package/dist/surface/tools/merged-name-redirect.js +5 -1
  294. package/dist/surface/tools/observe-tools.js +15 -9
  295. package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
  296. package/dist/surface/tools/read/console-pre-attach.js +23 -0
  297. package/dist/surface/tools/read-tools.js +33 -30
  298. package/dist/surface/tools/real-input-attempt.d.ts +0 -8
  299. package/dist/surface/tools/real-input-attempt.js +143 -28
  300. package/dist/surface/tools/session-ports.d.ts +14 -0
  301. package/dist/surface/tools/session-ports.js +53 -0
  302. package/dist/surface/tools/tool-kit.d.ts +6 -0
  303. package/dist/surface/tools/tool-kit.js +2 -3
  304. package/dist/surface/tools/tool-surface.js +0 -1
  305. package/dist/surface/tools/tools.js +28 -5
  306. package/dist/surface/tools/type-spelling.d.ts +13 -0
  307. package/dist/surface/tools/type-spelling.js +36 -0
  308. package/dist/telemetry/agent-client.d.ts +6 -0
  309. package/dist/telemetry/agent-client.js +10 -0
  310. package/dist/telemetry/argument-shape.js +1 -0
  311. package/dist/telemetry/tool-refused.d.ts +6 -3
  312. package/dist/telemetry/tool-refused.js +8 -3
  313. package/docs/actions.mdx +3 -3
  314. package/docs/agent-cheatsheet.md +1 -1
  315. package/docs/capabilities.mdx +61 -1
  316. package/docs/cli/affected.mdx +9 -7
  317. package/docs/cli/cloud.mdx +3 -3
  318. package/docs/cli/compare.mdx +94 -0
  319. package/docs/cli/doctor.mdx +1 -1
  320. package/docs/cli/grade.mdx +97 -0
  321. package/docs/cli/license.mdx +9 -7
  322. package/docs/cli/restart.mdx +7 -5
  323. package/docs/cli/rollback.mdx +5 -5
  324. package/docs/cli/stop.mdx +3 -3
  325. package/docs/cli/update.mdx +2 -2
  326. package/docs/cli/verify.mdx +23 -1
  327. package/docs/cli.mdx +33 -23
  328. package/docs/deploy-checks.md +1 -1
  329. package/docs/desktop.mdx +1 -1
  330. package/docs/enterprise.md +2 -2
  331. package/docs/environment.mdx +1 -1
  332. package/docs/flows.md +11 -1
  333. package/docs/frameworks.mdx +9 -9
  334. package/docs/getting-started.md +1 -1
  335. package/docs/install-manual.mdx +1 -1
  336. package/docs/license-activation.mdx +3 -3
  337. package/docs/packages/tauri.mdx +1 -1
  338. package/docs/packages.mdx +1 -1
  339. package/docs/predicates.mdx +1 -1
  340. package/docs/quickstart.mdx +68 -49
  341. package/docs/skill-setup.md +1 -1
  342. package/docs/skill.md +14 -13
  343. package/docs/telemetry.md +2 -2
  344. package/docs/tools/navigate.mdx +1 -1
  345. package/docs/tools/verify.mdx +4 -1
  346. package/docs/troubleshooting.mdx +1 -1
  347. package/docs/usage.md +17 -14
  348. package/package.json +13 -13
  349. package/dist/features/exhaust/exhaust-run.d.ts +0 -33
  350. package/dist/features/exhaust/exhaust-run.js +0 -82
@@ -5,7 +5,7 @@ import { ReticleTool } from '@reticlehq/core';
5
5
  import { asNumber, asString } from '@reticlehq/core';
6
6
  import { stepCountSchema, timeoutMsSchema } from '../../surface/tools/args/numeric-bounds.js';
7
7
  import { crawl } from './crawl.js';
8
- import { EXHAUST_DEFAULT_ACTIONS, runExhaustive } from '../exhaust/exhaust-run.js';
8
+ import { runExhaustive } from './crawl-exhaustive.js';
9
9
  import { heuristicFillValue } from '../harness/fill-values.js';
10
10
  import { secretEnvKey } from '../../language/flows/flows.js';
11
11
  import { CRAWL_DEFAULTS } from '@reticlehq/core';
@@ -16,6 +16,24 @@ const nodeSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16
16
  * from whatever page is on screen, so without the check a page on another origin could name an
17
17
  * empty textbox after a secret and have the crawl type it in.
18
18
  */
19
+ /**
20
+ * How the exhaustive crawl breaks a request: the driven browser first, then the leased page the
21
+ * session runs in. `reticle mcp --headless` drives a lease, and the mock tool already reaches it the
22
+ * same way. Undefined when neither exists, so the crawl says why its failure paths were skipped.
23
+ */
24
+ export function exhaustiveMocks(deps, sessionId) {
25
+ const driven = deps.realInput?.setMocks?.bind(deps.realInput);
26
+ const pool = deps.pool;
27
+ if (driven === undefined && pool === undefined) {
28
+ return undefined;
29
+ }
30
+ return async (url, rules) => {
31
+ if (driven !== undefined && (await driven(url, rules))) {
32
+ return true;
33
+ }
34
+ return true === (await pool?.setMocksLease(sessionId, rules));
35
+ };
36
+ }
19
37
  export function crawlSecret(startUrl, env) {
20
38
  const originOf = (url) => {
21
39
  try {
@@ -29,14 +47,14 @@ export function crawlSecret(startUrl, env) {
29
47
  return (name, pageUrl) => home !== undefined && home === originOf(pageUrl) ? env[secretEnvKey(name)] : undefined;
30
48
  }
31
49
  /**
32
- * The autonomous "smart monkey" tool. Builds on reticle_explore (which only LISTS) by
50
+ * The autonomous "smart monkey" tool. Builds on the interactive snapshot (which only LISTS) by
33
51
  * actually clicking each reachable control and classifying the reaction. DESTRUCTIVE by nature —
34
52
  * it drives the app — so it's an explicit, bounded tool, never part of a passive read.
35
53
  */
36
54
  export const CRAWL_TOOLS = [
37
55
  {
38
56
  name: ReticleTool.CRAWL,
39
- description: 'Autonomously click every reachable interactive control (bounded by maxSteps, default 25) and report anomalies WITHOUT a script. Two classes: single-channel faults (console errors, failed requests ≥400, DEAD controls that dispatched but did nothing) and CONTRADICTIONS — two channels disagreeing about the same click, e.g. the UI advanced while its write failed, a success signal fired over a failed request, a write succeeded and nothing changed, the same write fired twice, or the UI moved on over an in-flight request. Contradictions are the false greens a human cannot see, because a human watches one channel (the screen) and it looks correct. DESTRUCTIVE — it really clicks (may navigate/mutate state); use reticle_explore first for a non-destructive list. Returns { interactiveFound, stepsRun, anomalies[{kind,ref,desc,detail}], counts, visited, truncated }.',
57
+ description: 'Autonomously click every reachable interactive control (bounded by maxSteps, default 25) and report anomalies WITHOUT a script. Two classes: single-channel faults (console errors, failed requests ≥400, DEAD controls that dispatched but did nothing) and CONTRADICTIONS — two channels disagreeing about the same click, e.g. the UI advanced while its write failed, a success signal fired over a failed request, a write succeeded and nothing changed, the same write fired twice, or the UI moved on over an in-flight request. Contradictions are the false greens a human cannot see, because a human watches one channel (the screen) and it looks correct. DESTRUCTIVE — it really clicks (may navigate/mutate state); use reticle_snapshot { mode: "interactive" } first for a non-destructive list. Returns { interactiveFound, stepsRun, anomalies[{kind,ref,desc,detail}], counts, visited, truncated }.',
40
58
  inputSchema: {
41
59
  maxSteps: stepCountSchema
42
60
  .optional()
@@ -51,7 +69,7 @@ export const CRAWL_TOOLS = [
51
69
  exhaustive: z
52
70
  .boolean()
53
71
  .optional()
54
- .describe("Walk every state it can tell apart, drive each write's failure path, fold into the coverage ledger."),
72
+ .describe("Walk every state it can tell apart, drive each write's failure path and edge cases, fold into the coverage ledger."),
55
73
  confirmDangerous: z
56
74
  .boolean()
57
75
  .optional()
@@ -91,7 +109,7 @@ export const CRAWL_TOOLS = [
91
109
  .array(z.object({ ref: z.string(), desc: z.string(), reason: z.string() }))
92
110
  .optional()
93
111
  .describe('Controls clicked whose silence could not be judged, e.g. on a background tab. They are NOT reported as dead, and they are not known to work either.'),
94
- // Only on `exhaustive` — see features/exhaust/exhaust-run.ts. Loose: it carries the levels.
112
+ // Only on `exhaustive` — see crawl-exhaustive.ts. Loose: it carries the levels.
95
113
  exhaustive: z.record(z.string(), z.unknown()).optional(),
96
114
  },
97
115
  handler: async (deps, args) => {
@@ -111,11 +129,12 @@ export const CRAWL_TOOLS = [
111
129
  // Starts code-coverage collection, so the drive below is measured. See takeCodeCoverage.
112
130
  await deps.realInput?.takeCodeCoverage?.(session.url);
113
131
  const secret = crawlSecret(session.url, process.env);
132
+ const mocks = exhaustiveMocks(deps, session.id);
114
133
  return runExhaustive({
115
134
  sessions: deps.sessions,
116
135
  session,
117
136
  startUrl: session.url,
118
- maxActions: maxSteps ?? EXHAUST_DEFAULT_ACTIONS,
137
+ ...(maxSteps === undefined ? {} : { maxActions: maxSteps }),
119
138
  settleMs: settleMs ?? CRAWL_DEFAULTS.SETTLE_MS,
120
139
  fillValue: (label, pageUrl) => {
121
140
  const name = /"([^"]*)"/.exec(label)?.[1] ?? label;
@@ -123,11 +142,7 @@ export const CRAWL_TOOLS = [
123
142
  },
124
143
  now: deps.now,
125
144
  sleep: nodeSleep,
126
- ...(deps.realInput?.setMocks === undefined
127
- ? {}
128
- : {
129
- setMocks: (url, rules) => deps.realInput?.setMocks?.(url, rules) ?? Promise.resolve(false),
130
- }),
145
+ ...(mocks === undefined ? {} : { setMocks: mocks }),
131
146
  ...(deps.realInput?.takeCodeCoverage === undefined
132
147
  ? {}
133
148
  : {
@@ -142,8 +157,9 @@ export const CRAWL_TOOLS = [
142
157
  ...(initialRoute === undefined ? [] : [initialRoute]),
143
158
  ...routesFromEvents(session.eventsSince(since)),
144
159
  ];
145
- if (routes.length > 0)
160
+ if (routes.length > 0) {
146
161
  await projectForRoot(deps, sessionRoot(deps, session.id)).recordRoutes(routes);
162
+ }
147
163
  return report;
148
164
  },
149
165
  },
@@ -1,3 +1,10 @@
1
+ /**
2
+ * The autonomous "smart monkey": discover every reachable interactive control once, click each
3
+ * (bounded by `maxSteps`) and classify the reaction into anomalies — console errors, failed
4
+ * requests, and DEAD controls (dispatched but the app did nothing). Pure orchestration over a
5
+ * `CrawlSession`, so no browser or Node imports. Single-pass by design, so it always terminates.
6
+ */
7
+ import { legitimatelyInert } from '../exhaust/session-fold.js';
1
8
  import { type CommandResult, type ReticleEvent } from '@reticlehq/core';
2
9
  /** The slice of Session the crawler needs — so tests inject a fake without a live browser. */
3
10
  export interface CrawlSession {
@@ -94,13 +101,5 @@ export interface CrawlOptions {
94
101
  scope?: string;
95
102
  confirmDangerous?: boolean;
96
103
  }
97
- /**
98
- * Controls whose correct behaviour IS to do nothing when clicked.
99
- *
100
- * Read off the same descriptor the crawler already has, so no extra round trip: `[disabled]` is inert
101
- * by definition, an already `[checked]` radio/checkbox is idempotent, and clicking a text field
102
- * focuses it rather than changing anything.
103
- */
104
- export declare function legitimatelyInert(desc: string): boolean;
105
104
  export declare function crawl(session: CrawlSession, opts: CrawlOptions, sleep: CrawlSleep): Promise<CrawlReport>;
106
- export {};
105
+ export { legitimatelyInert };
@@ -4,8 +4,9 @@
4
4
  * requests, and DEAD controls (dispatched but the app did nothing). Pure orchestration over a
5
5
  * `CrawlSession`, so no browser or Node imports. Single-pass by design, so it always terminates.
6
6
  */
7
+ import { isActivity, legitimatelyInert } from '../exhaust/session-fold.js';
7
8
  import { span } from '../../trace.js';
8
- import { ActionType, CRAWL_DEFAULTS, CrawlAnomalyKind, DANGEROUS_ACTION_CONFIRM_ARG, EventType, ReticleCommand, } from '@reticlehq/core';
9
+ import { ActionType, CRAWL_DEFAULTS, CrawlAnomalyKind, DANGEROUS_ACTION_CONFIRM_ARG, ReticleCommand, } from '@reticlehq/core';
9
10
  import { crawlEmptyNote } from './crawl-empty.js';
10
11
  import { failedRequests, isConsoleError } from '../exhaust/session-fold.js';
11
12
  import { asNumber, asRecord, asString } from '@reticlehq/core';
@@ -40,20 +41,12 @@ function revealedControlsNote(count) {
40
41
  */
41
42
  const ENDS_SESSION = /\b(?:sign ?out|log ?out|sign ?off|log ?off)\b/i;
42
43
  const ENDS_SESSION_NOT_PRESSED = 'not pressed: it ends the signed-in session the crawl is driving';
44
+ const NO_ACCESSIBLE_NAME_NOT_PRESSED = 'not pressed: no accessible name, so the destructive-action guard cannot classify it (set confirmDangerous to include it)';
45
+ // A snapshot line carries the accessible name, a value or an own text as a quoted string. A line
46
+ // with no quote has no text at all, for example an icon-only button.
47
+ const HAS_TEXT = /"/;
43
48
  /** Why a silent control on a throttled page is not reported as dead. */
44
49
  const BACKGROUND_TAB_NOT_JUDGED = 'not judged: the page is throttled (its tab is in the background, or it has not been heard from recently), so a reaction may not show inside the sample window; bring the tab to the front and crawl again';
45
- /** Any buffer event that proves the app reacted to a click (vs a dead/no-op control). */
46
- function isActivity(e) {
47
- return (e.type === EventType.NET_REQUEST ||
48
- e.type === EventType.DOM_ADDED ||
49
- e.type === EventType.DOM_REMOVED ||
50
- e.type === EventType.DOM_ATTR ||
51
- e.type === EventType.DOM_TEXT ||
52
- e.type === EventType.ROUTE_CHANGE ||
53
- e.type === EventType.SIGNAL ||
54
- e.type === EventType.ANIM_START ||
55
- e.type === EventType.ANIM_END);
56
- }
57
50
  const SOURCE_FRAME = /^(.*):(\d+):(\d+)$/;
58
51
  const COMPONENT_STACK_FIELDS = ['componentStack', 'message'];
59
52
  const URL_SOURCE_PROTOCOLS = new Set([
@@ -66,44 +59,54 @@ const URL_SOURCE_PROTOCOLS = new Set([
66
59
  /** Turn a browser/dev-server frame URL into the file-shaped pointer the rest of Reticle emits. */
67
60
  function sourceFile(raw) {
68
61
  let file = raw.trim();
69
- if (0 === file.length)
62
+ if (0 === file.length) {
70
63
  return undefined;
64
+ }
71
65
  try {
72
66
  const parsed = new URL(file);
73
- if (URL_SOURCE_PROTOCOLS.has(parsed.protocol))
67
+ if (URL_SOURCE_PROTOCOLS.has(parsed.protocol)) {
74
68
  file = decodeURIComponent(parsed.pathname);
69
+ }
75
70
  }
76
71
  catch {
77
72
  // A repo-relative or Windows path is already the more useful form; keep it as-is.
78
73
  }
79
74
  file = file.replace(/[?#].*$/, '').replace(/\\/g, '/');
80
75
  const bundledPrefix = file.lastIndexOf('/./');
81
- if (-1 !== bundledPrefix)
76
+ if (-1 !== bundledPrefix) {
82
77
  file = file.slice(bundledPrefix + 3);
83
- if (file.startsWith('/@fs/'))
78
+ }
79
+ if (file.startsWith('/@fs/')) {
84
80
  file = file.slice('/@fs/'.length);
85
- if (/^\/[A-Za-z]:\//.test(file))
81
+ }
82
+ if (/^\/[A-Za-z]:\//.test(file)) {
86
83
  file = file.slice(1);
87
- else
84
+ }
85
+ else {
88
86
  file = file.replace(/^\/+/, '');
89
- if (file.startsWith('./'))
87
+ }
88
+ if (file.startsWith('./')) {
90
89
  file = file.slice(2);
90
+ }
91
91
  return 0 === file.length ? undefined : file;
92
92
  }
93
93
  /** The location part of a Chrome/React or Firefox stack frame. */
94
94
  function frameLocation(line) {
95
95
  let frame = line.trim();
96
- if (frame.startsWith('at '))
96
+ if (frame.startsWith('at ')) {
97
97
  frame = frame.slice(3).trim();
98
+ }
98
99
  else {
99
100
  const firefoxSeparator = frame.lastIndexOf('@');
100
- if (-1 === firefoxSeparator)
101
+ if (-1 === firefoxSeparator) {
101
102
  return undefined;
103
+ }
102
104
  frame = frame.slice(firefoxSeparator + 1);
103
105
  }
104
106
  const opening = frame.lastIndexOf('(');
105
- if (-1 !== opening && frame.endsWith(')'))
107
+ if (-1 !== opening && frame.endsWith(')')) {
106
108
  return frame.slice(opening + 1, -1);
109
+ }
107
110
  return frame;
108
111
  }
109
112
  function isFrameworkSource(file) {
@@ -114,14 +117,17 @@ function isFrameworkSource(file) {
114
117
  function sourceFromStack(stack) {
115
118
  for (const line of stack.split('\n')) {
116
119
  const location = frameLocation(line);
117
- if (location === undefined)
120
+ if (location === undefined) {
118
121
  continue;
122
+ }
119
123
  const match = SOURCE_FRAME.exec(location);
120
- if (null === match)
124
+ if (null === match) {
121
125
  continue;
126
+ }
122
127
  const [, rawFile, rawLine] = match;
123
- if (rawFile === undefined || rawLine === undefined)
128
+ if (rawFile === undefined || rawLine === undefined) {
124
129
  continue;
130
+ }
125
131
  const file = sourceFile(rawFile);
126
132
  const lineNumber = Number(rawLine);
127
133
  if (file === undefined ||
@@ -140,55 +146,33 @@ function sourceFromConsoleError(event) {
140
146
  // observer carries it inside `message`; richer producers can send `componentStack` directly.
141
147
  for (const field of COMPONENT_STACK_FIELDS) {
142
148
  const componentStack = asString(event.data[field]);
143
- if (componentStack === undefined)
149
+ if (componentStack === undefined) {
144
150
  continue;
151
+ }
145
152
  const componentSource = sourceFromStack(componentStack);
146
- if (componentSource !== undefined)
153
+ if (componentSource !== undefined) {
147
154
  return componentSource;
155
+ }
148
156
  }
149
157
  // The ordinary error stack can also identify a component better than ErrorEvent.filename (for
150
158
  // example, a bundle or click-handler frame), so try app frames before the structured fallback.
151
159
  const stack = asString(event.data['stack']);
152
160
  if (stack !== undefined) {
153
161
  const stackSource = sourceFromStack(stack);
154
- if (stackSource !== undefined)
162
+ if (stackSource !== undefined) {
155
163
  return stackSource;
164
+ }
156
165
  }
157
166
  const directFile = asString(event.data['source']);
158
167
  const directLine = asNumber(event.data['line']);
159
- if (directFile === undefined || directLine === undefined || directLine <= 0)
168
+ if (directFile === undefined || directLine === undefined || directLine <= 0) {
160
169
  return undefined;
170
+ }
161
171
  const file = sourceFile(directFile);
162
172
  return file === undefined || isFrameworkSource(file)
163
173
  ? undefined
164
174
  : `${file}:${String(directLine)}`;
165
175
  }
166
- /**
167
- * Controls whose correct behaviour IS to do nothing when clicked.
168
- *
169
- * Read off the same descriptor the crawler already has, so no extra round trip: `[disabled]` is inert
170
- * by definition, an already `[checked]` radio/checkbox is idempotent, and clicking a text field
171
- * focuses it rather than changing anything.
172
- */
173
- export function legitimatelyInert(desc) {
174
- if (desc.includes('[disabled]'))
175
- return true;
176
- if (desc.includes('[checked]'))
177
- return true;
178
- // Inputs, and the ARIA LIVE-REGION roles.
179
- //
180
- // `alert`, `status`, `log`, `timer` and `marquee` exist to be READ — a screen reader announces
181
- // them and nothing is meant to happen when one is clicked. Clicking twice and seeing nothing is
182
- // therefore correct behaviour, and reporting it as `dead-control` accuses a working app.
183
- //
184
- // Found by driving a real app: the demo reported `dead-control — alert "Invalid email or
185
- // password"`, which is a login form correctly telling somebody their password was wrong. That is
186
- // the most expensive kind of false positive there is — the strongest claim the crawler makes,
187
- // about an app that was working, and on the path a new user meets first.
188
- //
189
- // `alertdialog` is deliberately NOT here: it is interactive, and a word boundary keeps it out.
190
- return /\b(textbox|searchbox|combobox|spinbutton|alert|status|log|timer|marquee)\b/.test(desc);
191
- }
192
176
  /**
193
177
  * How many interactive controls exist now that did not exist when the crawl enumerated.
194
178
  *
@@ -202,8 +186,9 @@ async function revealedItems(session, opts, before) {
202
186
  mode: 'interactive',
203
187
  ...(opts.scope !== undefined ? { scope: opts.scope } : {}),
204
188
  });
205
- if (!snap.ok)
189
+ if (!snap.ok) {
206
190
  return [];
191
+ }
207
192
  const tree = (snap.result ?? {}).tree ?? '';
208
193
  const known = new Set(before.map((i) => (i.ref !== '' ? i.ref : i.desc)));
209
194
  return parseInteractive(tree).filter((i) => !known.has(i.ref !== '' ? i.ref : i.desc));
@@ -237,8 +222,9 @@ async function stillSilent(session, item, settleMs, confirm, sleep) {
237
222
  });
238
223
  await sleep(settleMs);
239
224
  // A control that has GONE (the first click removed it) is not a dead control — it did something.
240
- if (!again.ok || false === asRecord(again.result)['dispatched'])
225
+ if (!again.ok || false === asRecord(again.result)['dispatched']) {
241
226
  return false;
227
+ }
242
228
  return !session.eventsSince(since).some(isActivity);
243
229
  }
244
230
  catch {
@@ -277,18 +263,24 @@ export async function crawl(session, opts, sleep) {
277
263
  // so one call finished a deletion nobody authorized the second half of.
278
264
  const confirm = 0 === round && true === opts.confirmDangerous;
279
265
  for (const item of frontier) {
280
- if (stepsRun >= maxSteps)
266
+ if (stepsRun >= maxSteps) {
281
267
  break;
268
+ }
282
269
  // Dedupe by ref (the unique element), not by label — two "Delete"/"Edit" controls share a desc
283
270
  // but are different controls; collapsing them by label under-covers list/table UIs.
284
271
  const dedupeKey = item.ref !== '' ? item.ref : item.desc;
285
- if (seen.has(dedupeKey))
286
- continue; // don't re-click the same control
272
+ if (seen.has(dedupeKey)) {
273
+ continue;
274
+ } // don't re-click the same control
287
275
  seen.add(dedupeKey);
288
276
  if (ENDS_SESSION.test(item.desc)) {
289
277
  notJudged.push({ ref: item.ref, desc: item.desc, reason: ENDS_SESSION_NOT_PRESSED });
290
278
  continue;
291
279
  }
280
+ if (!confirm && !HAS_TEXT.test(item.desc)) {
281
+ notJudged.push({ ref: item.ref, desc: item.desc, reason: NO_ACCESSIBLE_NAME_NOT_PRESSED });
282
+ continue;
283
+ }
292
284
  stepsRun += 1;
293
285
  visited.push(item.desc);
294
286
  const since = session.elapsed();
@@ -321,8 +313,9 @@ export async function crawl(session, opts, sleep) {
321
313
  });
322
314
  }
323
315
  catch (err) {
324
- if (!isSessionReplacedError(err))
316
+ if (!isSessionReplacedError(err)) {
325
317
  throw err;
318
+ }
326
319
  navigatedAway = true;
327
320
  }
328
321
  finally {
@@ -433,11 +426,13 @@ export async function crawl(session, opts, sleep) {
433
426
  }
434
427
  }
435
428
  }
436
- if (coverageCapped || stepsRun >= maxSteps || REVEAL_ROUNDS <= round)
429
+ if (coverageCapped || stepsRun >= maxSteps || REVEAL_ROUNDS <= round) {
437
430
  break;
431
+ }
438
432
  const next = await revealedItems(session, opts, found);
439
- if (0 === next.length)
433
+ if (0 === next.length) {
440
434
  break;
435
+ }
441
436
  found.push(...next);
442
437
  frontier = next;
443
438
  }
@@ -450,7 +445,7 @@ export async function crawl(session, opts, sleep) {
450
445
  // one would only muddy it.
451
446
  const revealed = coverageCapped ? 0 : (await revealedItems(session, opts, found)).length;
452
447
  // Left alone on purpose, so not a step the budget ran out before.
453
- const leftAlone = notJudged.filter((n) => ENDS_SESSION_NOT_PRESSED === n.reason).length;
448
+ const leftAlone = notJudged.filter((n) => ENDS_SESSION_NOT_PRESSED === n.reason || NO_ACCESSIBLE_NAME_NOT_PRESSED === n.reason).length;
454
449
  return {
455
450
  interactiveFound: found.length,
456
451
  stepsRun,
@@ -480,3 +475,4 @@ export async function crawl(session, opts, sleep) {
480
475
  })(),
481
476
  };
482
477
  }
478
+ export { legitimatelyInert };
@@ -5,7 +5,7 @@
5
5
  import type { ReticleEvent } from '@reticlehq/core';
6
6
  import type { FileSystemPort } from '../../memory/project/fs/fs-port.js';
7
7
  import { type RawScriptCoverage } from './code-coverage.js';
8
- import { type LevelReport } from './ledger.js';
8
+ import { type BehaviourFinding, type HolesReport, type LevelReport } from './ledger.js';
9
9
  export interface AppCoverageReport {
10
10
  levels: LevelReport[];
11
11
  /** Writes whose failure path was driven and the app claimed success anyway. Findings, not gaps. */
@@ -16,6 +16,10 @@ export interface AppCoverageReport {
16
16
  was: number;
17
17
  now: number;
18
18
  }[];
19
+ /** Every route, control, edge case, write, function and component never reached, by name. */
20
+ holes: HolesReport;
21
+ /** Behaviour faults the explorer found, one copy each. Absent when there are none. */
22
+ findings?: BehaviourFinding[];
19
23
  note?: string;
20
24
  }
21
25
  export declare function foldAppCoverage(input: {
@@ -3,7 +3,7 @@
3
3
  * stands, level by level.
4
4
  */
5
5
  import { emptyCodeCoverage, foldCodeCoverage } from './code-coverage.js';
6
- import { LedgerStore, levelsOf, regressions } from './ledger.js';
6
+ import { holesOf, LedgerStore, levelsOf, regressions, } from './ledger.js';
7
7
  import { sessionDelta } from './session-fold.js';
8
8
  const NO_CODE_NOTE = 'The `executed` level is unmeasured here: code coverage needs a browser Reticle drives (`reticle drive`, or RETICLE_CDP_URL), and collection starts at the first coverage request, so drive and ask again. The other levels are real.';
9
9
  export async function foldAppCoverage(input) {
@@ -26,6 +26,8 @@ export async function foldAppCoverage(input) {
26
26
  levels: levelsOf(ledger),
27
27
  unhandledWrites: ledger.writes.unhandled,
28
28
  regressed: regressions(ledger),
29
+ holes: holesOf(ledger),
30
+ ...(ledger.findings.length > 0 ? { findings: ledger.findings } : {}),
29
31
  ...(take === undefined && 0 === Object.keys(ledger.code).length ? { note: NO_CODE_NOTE } : {}),
30
32
  };
31
33
  }
@@ -22,8 +22,9 @@ export function appFileOf(url) {
22
22
  catch {
23
23
  return undefined;
24
24
  }
25
- if (NOT_APP.test(path) || !SOURCE_FILE.test(path))
25
+ if (NOT_APP.test(path) || !SOURCE_FILE.test(path)) {
26
26
  return undefined;
27
+ }
27
28
  return path.replace(/^\/+/, '');
28
29
  }
29
30
  export function emptyCodeCoverage() {
@@ -37,17 +38,20 @@ export function foldCodeCoverage(into, take) {
37
38
  const out = { ...into };
38
39
  for (const script of take) {
39
40
  const file = appFileOf(script.url);
40
- if (file === undefined)
41
+ if (file === undefined) {
41
42
  continue;
43
+ }
42
44
  const fns = { ...(out[file] ?? {}) };
43
45
  for (const f of script.functions) {
44
46
  const first = f.ranges[0];
45
- if (first === undefined)
47
+ if (first === undefined) {
46
48
  continue;
49
+ }
47
50
  // The module's own top-level body is reported as an anonymous function starting at 0. It
48
51
  // always runs when the file loads, so counting it would inflate every file by one.
49
- if ('' === f.functionName && 0 === first.startOffset)
52
+ if ('' === f.functionName && 0 === first.startOffset) {
50
53
  continue;
54
+ }
51
55
  const key = `${f.functionName}@${String(first.startOffset)}`;
52
56
  const ran = first.count > 0 || true === fns[key]?.executed;
53
57
  fns[key] = { name: '' === f.functionName ? '(anonymous)' : f.functionName, executed: ran };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Edge cases for the explorer to drive: the inputs and the interaction orders where behavioural
3
+ * bugs live.
4
+ *
5
+ * The explorer types one ordinary value into each field and activates each control once, in the
6
+ * order the page lists them. That proves the happy path and nothing else. A form that accepts
7
+ * " " as a name, keeps 11 in a field whose maximum is 10, or creates two orders on a double click
8
+ * fails on a value or an order nobody tried. This module lists those values and sequences.
9
+ *
10
+ * Pure and deterministic: the same field, control and clock give the same list in the same order.
11
+ * Every list is bounded, and the most telling items come first, so a small budget still tries them.
12
+ * A sequence says nothing about what a correct result is; `invariants.ts` judges the pages it leaves.
13
+ */
14
+ import type { Step } from './explorer.js';
15
+ /** What a field takes, as far as its type, input mode, role and name say. */
16
+ export declare const FieldKind: {
17
+ readonly TEXT: "text";
18
+ readonly NUMBER: "number";
19
+ readonly EMAIL: "email";
20
+ readonly DATE: "date";
21
+ readonly TIME: "time";
22
+ readonly URL: "url";
23
+ readonly TEL: "tel";
24
+ };
25
+ export type FieldKind = (typeof FieldKind)[keyof typeof FieldKind];
26
+ /** Why a value is an edge case. The report shows this beside the value. */
27
+ export declare const EdgeLabel: {
28
+ readonly EMPTY: "empty";
29
+ readonly EMPTY_REQUIRED: "empty in a required field";
30
+ readonly WHITESPACE: "whitespace only";
31
+ readonly PADDED: "leading and trailing spaces";
32
+ readonly OVER_MAX_LENGTH: "one character over maxlength";
33
+ readonly UNDER_MIN_LENGTH: "one character under minlength";
34
+ readonly LONG_TEXT: "long text, no maxlength set";
35
+ readonly UNICODE: "emoji and non-Latin script";
36
+ readonly RTL: "right-to-left text";
37
+ readonly MARKUP: "markup characters";
38
+ readonly BREAKS_PATTERN: "does not match the pattern";
39
+ readonly DUPLICATE: "duplicate of a value the app already holds";
40
+ readonly ZERO: "zero";
41
+ readonly NEGATIVE: "negative number";
42
+ readonly BELOW_MIN: "one below min";
43
+ readonly ABOVE_MAX: "one above max";
44
+ readonly VERY_LARGE: "very large number";
45
+ readonly DECIMAL: "decimal number";
46
+ readonly WRONG_FORMAT: "wrong format for the field type";
47
+ readonly PAST_DATE: "date in the past";
48
+ readonly FUTURE_DATE: "date far in the future";
49
+ readonly IMPOSSIBLE_DATE: "date that does not exist";
50
+ readonly MIDNIGHT: "midnight";
51
+ readonly IMPOSSIBLE_TIME: "time that does not exist";
52
+ };
53
+ export type EdgeLabel = (typeof EdgeLabel)[keyof typeof EdgeLabel];
54
+ /** One field, with what the snapshot or the DOM gives about it. Every attribute is optional. */
55
+ export interface FieldSpec {
56
+ role: string;
57
+ name?: string;
58
+ type?: string;
59
+ inputMode?: string;
60
+ value?: string;
61
+ min?: string;
62
+ max?: string;
63
+ maxLength?: number;
64
+ minLength?: number;
65
+ pattern?: string;
66
+ required?: boolean;
67
+ /** Values the app already holds for this field, such as the rows of a list. */
68
+ existing?: readonly string[];
69
+ }
70
+ export interface EdgeValue {
71
+ value: string;
72
+ label: EdgeLabel;
73
+ }
74
+ /** The default bound. A field gets at most this many values. */
75
+ export declare const MAX_EDGE_VALUES = 16;
76
+ export declare function fieldKindOf(spec: Pick<FieldSpec, 'role' | 'type' | 'inputMode' | 'name'>): FieldKind;
77
+ /**
78
+ * The edge-case values for a field, most telling first, with no value twice, at most `max`.
79
+ * `now` is the injected clock: past and future dates are relative to it.
80
+ */
81
+ export declare function edgeInputs(spec: FieldSpec, opts: {
82
+ now: () => number;
83
+ max?: number;
84
+ }): EdgeValue[];
85
+ /** The actions a sequence uses. `click` and `fill` are the explorer's own; the rest are new. */
86
+ export declare const EdgeAction: {
87
+ readonly CLICK: "click";
88
+ readonly FILL: "fill";
89
+ /** The browser's back. */
90
+ readonly BACK: "back";
91
+ /** A full reload of the current page. */
92
+ readonly RELOAD: "reload";
93
+ /** Wait until the page settles. Two steps with no settle between them run back to back. */
94
+ readonly SETTLE: "settle";
95
+ };
96
+ export type EdgeAction = (typeof EdgeAction)[keyof typeof EdgeAction];
97
+ export type EdgeStep = Step | {
98
+ action: typeof EdgeAction.BACK | typeof EdgeAction.RELOAD | typeof EdgeAction.SETTLE;
99
+ };
100
+ export declare const EdgeInteraction: {
101
+ readonly DOUBLE_ACTIVATION: "double-activation";
102
+ readonly RAPID_REPEAT: "rapid-repeat";
103
+ readonly ACT_THEN_BACK: "act-then-back";
104
+ readonly ACT_THEN_RELOAD: "act-then-reload";
105
+ readonly SUBMIT_TWICE: "submit-twice";
106
+ readonly SUBMIT_BEFORE_FILL: "submit-before-fill";
107
+ readonly OUT_OF_ORDER: "out-of-order";
108
+ };
109
+ export type EdgeInteraction = (typeof EdgeInteraction)[keyof typeof EdgeInteraction];
110
+ export interface EdgeSequence {
111
+ kind: EdgeInteraction;
112
+ /** The control the sequence is about. */
113
+ target: string;
114
+ steps: EdgeStep[];
115
+ }
116
+ /** How many activations a rapid repeat sends back to back. */
117
+ export declare const RAPID_REPEAT = 5;
118
+ /** The default bound on sequences for one control. */
119
+ export declare const MAX_EDGE_SEQUENCES = 8;
120
+ export declare function looksLikeSubmit(control: string): boolean;
121
+ /** A step in words, for the action sequence on a finding. */
122
+ export declare function describeStep(step: EdgeStep): string;
123
+ /**
124
+ * The edge sequences for `control`. `order` is the page's controls in document order; `fills` are
125
+ * the steps that fill the form the control belongs to, when it has one.
126
+ */
127
+ export declare function edgeInteractions(input: {
128
+ control: string;
129
+ order: readonly string[];
130
+ fills?: readonly Step[];
131
+ }, opts?: {
132
+ max?: number;
133
+ }): EdgeSequence[];