@reticlehq/server 3.9.0 → 3.10.1

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 (351) 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 +43 -2
  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/init.mdx +2 -2
  322. package/docs/cli/license.mdx +9 -7
  323. package/docs/cli/restart.mdx +7 -5
  324. package/docs/cli/rollback.mdx +5 -5
  325. package/docs/cli/stop.mdx +3 -3
  326. package/docs/cli/update.mdx +2 -2
  327. package/docs/cli/verify.mdx +23 -1
  328. package/docs/cli.mdx +33 -23
  329. package/docs/deploy-checks.md +1 -1
  330. package/docs/desktop.mdx +1 -1
  331. package/docs/enterprise.md +2 -2
  332. package/docs/environment.mdx +1 -1
  333. package/docs/flows.md +11 -1
  334. package/docs/frameworks.mdx +9 -9
  335. package/docs/getting-started.md +1 -1
  336. package/docs/install-manual.mdx +1 -1
  337. package/docs/license-activation.mdx +3 -3
  338. package/docs/packages/tauri.mdx +1 -1
  339. package/docs/packages.mdx +1 -1
  340. package/docs/predicates.mdx +1 -1
  341. package/docs/quickstart.mdx +68 -49
  342. package/docs/skill-setup.md +1 -1
  343. package/docs/skill.md +14 -13
  344. package/docs/telemetry.md +2 -2
  345. package/docs/tools/navigate.mdx +1 -1
  346. package/docs/tools/verify.mdx +4 -1
  347. package/docs/troubleshooting.mdx +30 -1
  348. package/docs/usage.md +17 -14
  349. package/package.json +11 -14
  350. package/dist/features/exhaust/exhaust-run.d.ts +0 -33
  351. package/dist/features/exhaust/exhaust-run.js +0 -82
@@ -13,27 +13,55 @@
13
13
  * - Controls that differ only by a number — the rows of a table — are one control. It is covered
14
14
  * once, which is what keeps a 500-row page from eating the whole budget.
15
15
  * - Text fields on a state are filled before its buttons are tried, so a submit is driven with
16
- * input rather than dismissed as dead because it refused an empty form.
16
+ * input rather than dismissed as dead because it refused an empty form. Each field gets its own
17
+ * value (`fillValue` is told the field's position), so two fields a build swaps do not read alike.
18
+ * - `paths` keeps every click it tried, also a click that kept the same state (a count went up).
19
+ * A caller that reads the page after each path needs those clicks too.
17
20
  *
18
21
  * ponytail: breadth-first with a flat action budget, and every edge replays its path from a fresh
19
22
  * load. That is O(states × path length), correct and slow; add prefix sharing if it proves too slow.
20
23
  * A control that needs a choice (select, combobox) is not driven — it needs a value model.
21
24
  */
22
25
  import { type ReticleEvent } from '@reticlehq/core';
26
+ /** What one action gives back. */
27
+ export interface ActResult {
28
+ ok: boolean;
29
+ /** The events this action caused. */
30
+ events: readonly ReticleEvent[];
31
+ /** The element's source, when the app gives source attribution. */
32
+ source?: {
33
+ file: string;
34
+ line: number;
35
+ };
36
+ }
37
+ /** A page as the explorer reads it: the snapshot tree and the route. */
38
+ export interface PageRead {
39
+ tree: string;
40
+ route?: string;
41
+ }
42
+ /** The attributes `fieldAttrs` reads: what a field takes, for its edge-case values. */
43
+ export declare const FIELD_ATTRS: readonly ["type", "min", "max", "maxlength", "minlength", "pattern", "required", "inputmode"];
23
44
  /** What the explorer needs from a driven page. The tool layer adapts a session to it. */
24
45
  export interface ExplorePort {
25
46
  /** Load the start page fresh. False when it did not come back. */
26
47
  reset(): Promise<boolean>;
27
48
  /** The interactive tree and route of the page now. */
28
- look(): Promise<{
29
- tree: string;
30
- route?: string;
31
- }>;
32
- /** Act on a control; the events are the ones this action caused. */
33
- act(ref: string, action: 'click' | 'fill', value?: string): Promise<{
34
- ok: boolean;
35
- events: readonly ReticleEvent[];
36
- }>;
49
+ look(): Promise<PageRead>;
50
+ /**
51
+ * Act on a control. `settle: false` returns as soon as the action is sent, so the next action
52
+ * runs back to back with it; its late events then arrive with the next action's.
53
+ */
54
+ act(ref: string, action: 'click' | 'fill', value?: string, opts?: {
55
+ settle?: boolean;
56
+ }): Promise<ActResult>;
57
+ /** The whole page, text included. Absent when the port reads only the interactive tree. */
58
+ lookFull?: () => Promise<PageRead>;
59
+ /** Go back to the page before the last action. False when it did not come back. */
60
+ back?: () => Promise<boolean>;
61
+ /** Reload the page now. False when it did not come back. */
62
+ reload?: () => Promise<boolean>;
63
+ /** The `FIELD_ATTRS` the field `control` (a control key) carries, for its edge-case values. */
64
+ fieldAttrs?: (control: string) => Promise<Record<string, string> | undefined>;
37
65
  /** Install network mocks ([] clears). Absent, or false, when no driven browser can. */
38
66
  mock?: (rules: {
39
67
  urlContains: string;
@@ -57,6 +85,33 @@ export interface ExploredWrite {
57
85
  beforeState: string;
58
86
  /** The state the app reached when this write SUCCEEDED — what a failure must not look like. */
59
87
  successState: string;
88
+ /**
89
+ * True when the write answered an error or never completed while exploring. `successState` is
90
+ * then where a failure lands, not a success, and nothing can be judged by landing there.
91
+ */
92
+ failed?: boolean;
93
+ }
94
+ /** One explored state, as the edge-case drive needs it. */
95
+ export interface ExploredSurface {
96
+ /** How to reach the state from a fresh start page, before its fields are filled. */
97
+ path: Step[];
98
+ /** What the explorer typed into the state's empty fields, in page order. */
99
+ fills: Step[];
100
+ /** Every text field on the state, in page order: the empty ones and the pre-filled ones. */
101
+ fields: string[];
102
+ /** Each field's `FIELD_ATTRS`, by control key. */
103
+ attrs: Record<string, Record<string, string>>;
104
+ /** The controls the explorer clicked on this state, in page order. */
105
+ order: string[];
106
+ /** The controls whose click sent a request. */
107
+ requests: string[];
108
+ /** The controls whose click sent a write. */
109
+ writes: string[];
110
+ /**
111
+ * The controls whose click showed no activity at all: no request, no DOM change, no route change,
112
+ * no signal. One sample; the edge drive presses each again before it calls one dead.
113
+ */
114
+ inert: string[];
60
115
  }
61
116
  export interface ExploreReport {
62
117
  states: number;
@@ -80,19 +135,29 @@ export interface ExploreReport {
80
135
  /** States whose recorded path no longer replays — the app is not deterministic from its start. */
81
136
  unreachable: number;
82
137
  budgetExhausted: boolean;
138
+ /** Every click tried, as its path from a fresh start page, in the order the explorer tried them. */
139
+ paths: Step[][];
140
+ /** Every explored state, with its fields, controls and the controls that sent a request. */
141
+ surfaces: ExploredSurface[];
142
+ }
143
+ export interface ExploreOptions {
144
+ maxActions: number;
145
+ /** What to type into an empty field: its label, and its position among the fields on the state. */
146
+ fillValue: (label: string, index: number) => string;
147
+ /** Called after each click, before the page is read, so behaviour driven by a timer can show. */
148
+ wait?: () => Promise<void>;
83
149
  }
84
150
  interface Control {
85
151
  ref: string;
86
152
  key: string;
87
153
  /** The field already holds a value — pre-filled credentials, a default. Typing over it breaks it. */
88
154
  holdsValue: boolean;
155
+ /** The snapshot line, with the states (`[disabled]`) the key leaves out. */
156
+ desc: string;
89
157
  }
90
158
  export declare function controlsOf(tree: string): Control[];
91
159
  export declare function stateKey(route: string | undefined, controls: readonly Control[]): string;
92
- export declare function explore(port: ExplorePort, opts: {
93
- maxActions: number;
94
- fillValue: (label: string) => string;
95
- }): Promise<ExploreReport>;
160
+ export declare function explore(port: ExplorePort, opts: ExploreOptions): Promise<ExploreReport>;
96
161
  /**
97
162
  * A fresh start page, then the path. False when a step's control is gone or refused. Every state
98
163
  * is reached this way, which is what makes returning to one a replay rather than a hope.
@@ -100,4 +165,12 @@ export declare function explore(port: ExplorePort, opts: {
100
165
  export declare function replayPath(port: ExplorePort, path: readonly Step[], act: (ref: string, action: 'click' | 'fill', value?: string) => Promise<{
101
166
  ok: boolean;
102
167
  }>, spent: () => boolean): Promise<boolean>;
168
+ /**
169
+ * One action that sent the same write two or more times, whatever the server answered. A second
170
+ * copy sent after the server refused the credentials of the first is the client's retry, not a
171
+ * duplicate: the first did no work.
172
+ */
173
+ export declare function noteDuplicateWrites(events: readonly ReticleEvent[], control: string, anomalies: ExploreReport['anomalies']): void;
174
+ /** The faults in an action's events: page errors, requests at or above `floor`, contradictions. */
175
+ export declare function noteAnomalies(events: readonly ReticleEvent[], control: string, anomalies: ExploreReport['anomalies'], floor?: number): void;
103
176
  export {};
@@ -13,20 +13,36 @@
13
13
  * - Controls that differ only by a number — the rows of a table — are one control. It is covered
14
14
  * once, which is what keeps a 500-row page from eating the whole budget.
15
15
  * - Text fields on a state are filled before its buttons are tried, so a submit is driven with
16
- * input rather than dismissed as dead because it refused an empty form.
16
+ * input rather than dismissed as dead because it refused an empty form. Each field gets its own
17
+ * value (`fillValue` is told the field's position), so two fields a build swaps do not read alike.
18
+ * - `paths` keeps every click it tried, also a click that kept the same state (a count went up).
19
+ * A caller that reads the page after each path needs those clicks too.
17
20
  *
18
21
  * ponytail: breadth-first with a flat action budget, and every edge replays its path from a fresh
19
22
  * load. That is O(states × path length), correct and slow; add prefix sharing if it proves too slow.
20
23
  * A control that needs a choice (select, combobox) is not driven — it needs a value model.
21
24
  */
22
- import { asNumber, asString, isAbsenceDerived, isAdvisory, parseInteractive, } from '@reticlehq/core';
25
+ import { EventType, asNumber, asString, isAbsenceDerived, isAdvisory, parseInteractive, } from '@reticlehq/core';
23
26
  import { findContradictions } from '@reticlehq/engine/disagreement/contradictions.js';
24
- import { controlKey, failedRequests, isConsoleError, writeKeyOf } from './session-fold.js';
27
+ import { controlKey, failedRequests, isActivity, isConsoleError, legitimatelyInert, writeKeyOf, } from './session-fold.js';
28
+ /** The attributes `fieldAttrs` reads: what a field takes, for its edge-case values. */
29
+ export const FIELD_ATTRS = [
30
+ 'type',
31
+ 'min',
32
+ 'max',
33
+ 'maxlength',
34
+ 'minlength',
35
+ 'pattern',
36
+ 'required',
37
+ 'inputmode',
38
+ ];
25
39
  /** Roles that take typed input. */
26
40
  const TYPED = /^(?:textbox|searchbox|spinbutton)\b/;
27
41
  /** Roles that need a value model this explorer does not have, and are left alone. */
28
42
  const CHOOSER = /^(?:combobox|listbox|option|slider)\b/;
29
43
  const FAILED_STATUS = 400;
44
+ /** The lowest status of an answer that is not an error. 0 is a request that never completed. */
45
+ const OK_MIN = 200;
30
46
  /** A route without its query: `?__reticle_opened=1` and friends are not a different page. */
31
47
  const pathOf = (route) => route?.split('?')[0];
32
48
  export function controlsOf(tree) {
@@ -36,6 +52,7 @@ export function controlsOf(tree) {
36
52
  ref: item.ref,
37
53
  key: controlKey(item.desc),
38
54
  holdsValue: /\[value="[^"]+"/.test(item.desc),
55
+ desc: item.desc,
39
56
  }));
40
57
  }
41
58
  export function stateKey(route, controls) {
@@ -52,6 +69,8 @@ export async function explore(port, opts) {
52
69
  const explored = new Set();
53
70
  const queued = new Set();
54
71
  const queue = [{ path: [] }];
72
+ const paths = [];
73
+ const surfaces = [];
55
74
  const spent = () => actions >= opts.maxActions;
56
75
  const act = async (ref, action, value) => {
57
76
  actions += 1;
@@ -60,32 +79,47 @@ export async function explore(port, opts) {
60
79
  const goTo = (path) => replayPath(port, path, act, spent);
61
80
  while (queue.length > 0 && !spent()) {
62
81
  const node = queue.shift();
63
- if (node === undefined)
82
+ if (node === undefined) {
64
83
  break;
84
+ }
65
85
  if (!(await goTo(node.path))) {
66
- if (!spent())
86
+ if (!spent()) {
67
87
  unreachable += 1;
88
+ }
68
89
  continue;
69
90
  }
70
91
  const here = await port.look();
71
92
  const controls = controlsOf(here.tree);
72
93
  const key = stateKey(here.route, controls);
73
- if (explored.has(key))
94
+ if (explored.has(key)) {
74
95
  continue;
96
+ }
75
97
  explored.add(key);
76
98
  const where = pathOf(here.route);
77
- if (where !== undefined)
99
+ if (where !== undefined) {
78
100
  routes.add(where);
79
- for (const c of controls)
101
+ }
102
+ for (const c of controls) {
80
103
  seen.add(c.key);
104
+ }
81
105
  // Only EMPTY fields. The first drive of the fixture typed placeholders over the login form's
82
106
  // correct pre-filled credentials, got a 401, and never left the first page.
107
+ const fields = [...new Set(controls.filter((c) => TYPED.test(c.key)).map((c) => c.key))];
83
108
  const typed = controls.filter((c) => TYPED.test(c.key) && !c.holdsValue);
84
- const fills = typed.map((c) => ({
109
+ const fills = typed.map((c, index) => ({
85
110
  key: c.key,
86
111
  action: 'fill',
87
- value: opts.fillValue(c.key),
112
+ value: opts.fillValue(c.key, index),
88
113
  }));
114
+ const attrs = {};
115
+ // Every field, the pre-filled ones too: the explorer leaves a pre-filled field alone, but its
116
+ // edge values are still worth typing. A login form whose fields came filled got none.
117
+ for (const field of fields) {
118
+ const read = await port.fieldAttrs?.(field);
119
+ if (read !== undefined) {
120
+ attrs[field] = read;
121
+ }
122
+ }
89
123
  // Fills first, then the targets: valid input can reveal the button that moves on (a Continue,
90
124
  // a Submit), and a list drawn before the fill would never contain it.
91
125
  const fillHere = async () => {
@@ -100,31 +134,69 @@ export async function explore(port, opts) {
100
134
  return 0 < fills.length ? controlsOf((await port.look()).tree) : before;
101
135
  };
102
136
  const ready = await fillHere();
103
- for (const c of ready)
137
+ for (const c of ready) {
104
138
  seen.add(c.key);
139
+ }
105
140
  // Every control of THIS state, once. Not filtered by what other states touched: a "Save" on
106
141
  // another screen is another control, and skipping it left reachable writes undriven.
107
142
  const untried = [
108
143
  ...new Set(ready.filter((c) => !TYPED.test(c.key) && !CHOOSER.test(c.key)).map((c) => c.key)),
109
144
  ];
110
- for (const [index, target] of untried.entries()) {
111
- if (spent())
145
+ const surface = {
146
+ path: node.path,
147
+ fills,
148
+ fields,
149
+ attrs,
150
+ order: untried,
151
+ requests: [],
152
+ writes: [],
153
+ inert: [],
154
+ };
155
+ surfaces.push(surface);
156
+ // Controls no state has tried yet go first. Every page of an app repeats its nav, and trying the
157
+ // nav first on every state spent the budget before a page's own controls were pressed. Each
158
+ // control of the state is still tried; only the order changes.
159
+ const tryOrder = [
160
+ ...untried.filter((c) => !touched.has(c)),
161
+ ...untried.filter((c) => touched.has(c)),
162
+ ];
163
+ for (const [index, target] of tryOrder.entries()) {
164
+ if (spent()) {
112
165
  break;
166
+ }
113
167
  // The first control is tried on the page already loaded and filled; every other one starts
114
168
  // over from the recorded path, because the one before it may have changed or left this state.
115
- if (index > 0 && !(await goTo(node.path)))
169
+ if (index > 0 && !(await goTo(node.path))) {
116
170
  break;
171
+ }
117
172
  const current = 0 === index ? ready : await fillHere();
118
173
  const control = current.find((c) => c.key === target);
119
- if (control === undefined || spent())
174
+ if (control === undefined || spent()) {
120
175
  continue;
176
+ }
121
177
  const result = await act(control.ref, 'click');
122
178
  touched.add(target);
123
179
  const path = [...node.path, ...fills, { key: target, action: 'click' }];
180
+ paths.push(path);
181
+ await opts.wait?.();
124
182
  const after = await port.look();
125
183
  const next = stateKey(after.route, controlsOf(after.tree));
126
184
  noteWrites(result.events, path, key, next, writes);
185
+ if (result.events.some((e) => EventType.NET_REQUEST === e.type)) {
186
+ surface.requests.push(target);
187
+ }
188
+ if (result.events.some((e) => writeKeyOf(e) !== undefined)) {
189
+ surface.writes.push(target);
190
+ }
127
191
  noteAnomalies(result.events, target, anomalies);
192
+ noteDuplicateWrites(result.events, target, anomalies);
193
+ if (result.ok &&
194
+ next === key &&
195
+ pathOf(after.route) === where &&
196
+ !result.events.some(isActivity) &&
197
+ !legitimatelyInert(control.desc)) {
198
+ surface.inert.push(target);
199
+ }
128
200
  if (!explored.has(next) && !queued.has(next)) {
129
201
  queued.add(next);
130
202
  queue.push({ path, key: next });
@@ -142,6 +214,8 @@ export async function explore(port, opts) {
142
214
  frontier: queue.filter((n) => n.key === undefined || !explored.has(n.key)).length,
143
215
  unreachable,
144
216
  budgetExhausted: spent(),
217
+ paths,
218
+ surfaces,
145
219
  };
146
220
  }
147
221
  /**
@@ -149,47 +223,107 @@ export async function explore(port, opts) {
149
223
  * is reached this way, which is what makes returning to one a replay rather than a hope.
150
224
  */
151
225
  export async function replayPath(port, path, act, spent) {
152
- if (!(await port.reset()))
226
+ if (!(await port.reset())) {
153
227
  return false;
228
+ }
154
229
  for (const step of path) {
155
- if (spent())
230
+ if (spent()) {
156
231
  return false;
232
+ }
157
233
  const target = controlsOf((await port.look()).tree).find((c) => c.key === step.key);
158
- if (target === undefined)
234
+ if (target === undefined) {
159
235
  return false;
160
- if (!(await act(target.ref, step.action, step.value)).ok)
236
+ }
237
+ if (!(await act(target.ref, step.action, step.value)).ok) {
161
238
  return false;
239
+ }
162
240
  }
163
241
  return true;
164
242
  }
165
243
  function noteWrites(events, path, beforeState, successState, writes) {
166
244
  for (const event of events) {
167
245
  const key = writeKeyOf(event);
168
- if (key === undefined || writes.has(key))
246
+ if (key === undefined || writes.has(key)) {
169
247
  continue;
248
+ }
170
249
  const [method = '', urlPath = ''] = key.split(' ');
171
250
  const url = event.data['url'];
172
251
  const concrete = 'string' === typeof url ? new URL(url, 'http://localhost').pathname : urlPath;
173
- writes.set(key, { key, method, urlPath: concrete, path, beforeState, successState });
252
+ const status = asNumber(event.data['status']);
253
+ const failed = status === undefined || status < OK_MIN || status >= FAILED_STATUS;
254
+ writes.set(key, {
255
+ key,
256
+ method,
257
+ urlPath: concrete,
258
+ path,
259
+ beforeState,
260
+ successState,
261
+ ...(failed ? { failed } : {}),
262
+ });
263
+ }
264
+ }
265
+ /** HTTP answers that refuse the credentials. A client sends the write again after one by design. */
266
+ const AUTH_REFUSED = new Set([401, 403]);
267
+ const DUPLICATE_REQUEST = 'duplicate-request';
268
+ /**
269
+ * One action that sent the same write two or more times, whatever the server answered. A second
270
+ * copy sent after the server refused the credentials of the first is the client's retry, not a
271
+ * duplicate: the first did no work.
272
+ */
273
+ export function noteDuplicateWrites(events, control, anomalies) {
274
+ // Each request once, by its id: a write still in flight has only its `net.pending` here, and one
275
+ // that answered has both. A slow write sent twice is still two sends.
276
+ const sent = new Map();
277
+ const retried = new Set();
278
+ for (const [index, e] of events.entries()) {
279
+ const key = sentWriteKeyOf(e);
280
+ if (key === undefined) {
281
+ continue;
282
+ }
283
+ if (AUTH_REFUSED.has(asNumber(e.data['status']) ?? 0)) {
284
+ retried.add(key);
285
+ }
286
+ const id = asString(e.data['id']) ?? `#${String(index)}`;
287
+ sent.set(key, (sent.get(key) ?? new Set()).add(id));
288
+ }
289
+ for (const [key, ids] of sent) {
290
+ if (ids.size > 1 && !retried.has(key)) {
291
+ anomalies.push({
292
+ kind: DUPLICATE_REQUEST,
293
+ control,
294
+ detail: `one press sent ${key} ${String(ids.size)} times`,
295
+ });
296
+ }
174
297
  }
175
298
  }
176
- function noteAnomalies(events, control, anomalies) {
299
+ /** The write a request or a request still in flight sends, as `writeKeyOf` spells it. */
300
+ function sentWriteKeyOf(e) {
301
+ if (EventType.NET_PENDING === e.type) {
302
+ return writeKeyOf({ ...e, type: EventType.NET_REQUEST });
303
+ }
304
+ return writeKeyOf(e);
305
+ }
306
+ /** The faults in an action's events: page errors, requests at or above `floor`, contradictions. */
307
+ export function noteAnomalies(events, control, anomalies, floor = FAILED_STATUS) {
177
308
  const list = [...events];
178
- for (const e of list.filter(isConsoleError))
309
+ for (const e of list.filter(isConsoleError)) {
179
310
  anomalies.push({
180
311
  kind: 'console-error',
181
312
  control,
182
313
  detail: asString(e.data['message']) ?? e.type,
183
314
  });
184
- for (const e of failedRequests(list, FAILED_STATUS))
315
+ }
316
+ for (const e of failedRequests(list, floor)) {
185
317
  anomalies.push({
186
318
  kind: 'failed-request',
187
319
  control,
188
320
  detail: `${asString(e.data['method']) ?? ''} ${asString(e.data['url']) ?? ''} → ${String(asNumber(e.data['status']) ?? '')}`,
189
321
  });
322
+ }
190
323
  for (const c of findContradictions(list, { actionSince: 0 })) {
191
- if (isAdvisory(c.kind) || isAbsenceDerived(c.kind))
324
+ if (isAdvisory(c.kind) || isAbsenceDerived(c.kind)) {
192
325
  continue;
326
+ }
193
327
  anomalies.push({ kind: c.kind, control, detail: `${c.claim}, but ${c.counter}` });
194
328
  }
195
329
  }
@@ -25,6 +25,11 @@ export interface FailureBranches {
25
25
  * by hand, or to give the app a visible outcome.
26
26
  */
27
27
  indistinguishable: string[];
28
+ /**
29
+ * Writes that answered an error while exploring too, so there is no success to compare with. Not a
30
+ * finding. A login with its API down lands on its error message both times.
31
+ */
32
+ notSucceeded: string[];
28
33
  /** Why nothing was driven, when nothing could be. */
29
34
  skipped?: string;
30
35
  }
@@ -24,10 +24,16 @@ function aborted(e, key) {
24
24
  (0 === asNumber(e.data['status']) || e.data['error'] !== undefined));
25
25
  }
26
26
  export async function driveFailureBranches(port, writes, opts) {
27
- const out = { branched: [], unhandled: [], indistinguishable: [] };
27
+ const out = {
28
+ branched: [],
29
+ unhandled: [],
30
+ indistinguishable: [],
31
+ notSucceeded: [],
32
+ };
28
33
  const mock = port.mock;
29
- if (mock === undefined)
34
+ if (mock === undefined) {
30
35
  return { ...out, ...(0 < writes.length ? { skipped: NO_MOCKS } : {}) };
36
+ }
31
37
  let actions = 0;
32
38
  const spent = () => actions >= opts.maxActions;
33
39
  const act = (ref, action, value) => {
@@ -35,28 +41,34 @@ export async function driveFailureBranches(port, writes, opts) {
35
41
  return port.act(ref, action, value);
36
42
  };
37
43
  for (const write of writes) {
38
- if (spent())
44
+ if (spent()) {
39
45
  break;
46
+ }
40
47
  const last = write.path.at(-1);
41
- if (last === undefined)
48
+ if (last === undefined) {
42
49
  continue;
50
+ }
43
51
  try {
44
52
  // Everything up to the step that fires the write, THEN the break. Installed before the replay,
45
53
  // the abort also broke any setup step sending the same request, and the replay died short of
46
54
  // the action it existed to reach.
47
- if (!(await replayPath(port, write.path.slice(0, -1), act, spent)))
55
+ if (!(await replayPath(port, write.path.slice(0, -1), act, spent))) {
48
56
  continue;
49
- if (!(await mock([{ urlContains: write.urlPath, method: write.method, abort: true }])))
57
+ }
58
+ if (!(await mock([{ urlContains: write.urlPath, method: write.method, abort: true }]))) {
50
59
  return { ...out, skipped: NO_MOCKS };
60
+ }
51
61
  const control = controlsOf((await port.look()).tree).find((c) => c.key === last.key);
52
- if (control === undefined)
62
+ if (control === undefined) {
53
63
  continue;
64
+ }
54
65
  const result = await act(control.ref, last.action, last.value);
55
66
  // Driven means the write was SENT and did not complete. An action that never dispatched, or a
56
67
  // request the rule did not match, drove nothing — and judging where the app landed after a
57
68
  // write that succeeded would accuse it of claiming success over a failure that never happened.
58
- if (!result.ok || !result.events.some((e) => aborted(e, write.key)))
69
+ if (!result.ok || !result.events.some((e) => aborted(e, write.key))) {
59
70
  continue;
71
+ }
60
72
  out.branched.push(write.key);
61
73
  const after = await port.look();
62
74
  const reached = stateKey(after.route, controlsOf(after.tree));
@@ -68,21 +80,27 @@ export async function driveFailureBranches(port, writes, opts) {
68
80
  actionSince: 0,
69
81
  expectedFailures: [{ method: write.method, urlContains: write.urlPath }],
70
82
  }).find((c) => !isAdvisory(c.kind) && !isAbsenceDerived(c.kind));
71
- if (contradiction !== undefined)
83
+ if (contradiction !== undefined) {
72
84
  out.unhandled.push({
73
85
  key: write.key,
74
86
  detail: `${contradiction.claim}, but ${contradiction.counter}`,
75
87
  });
88
+ }
76
89
  // Landing where success lands only ACCUSES when success visibly moved the app. A write whose
77
90
  // success shows nothing looks identical failing, and the first version of this called that
78
91
  // "claimed success" — found by driving the fixture, whose add-item panel claims nothing at all.
79
- else if (write.successState === write.beforeState)
92
+ else if (true === write.failed) {
93
+ out.notSucceeded.push(write.key);
94
+ }
95
+ else if (write.successState === write.beforeState) {
80
96
  out.indistinguishable.push(write.key);
81
- else if (reached === write.successState)
97
+ }
98
+ else if (reached === write.successState) {
82
99
  out.unhandled.push({
83
100
  key: write.key,
84
101
  detail: `with ${write.key} failing, the app arrived exactly where it goes when the write succeeds (${after.route ?? 'same page'}) — it claimed success over a failure`,
85
102
  });
103
+ }
86
104
  }
87
105
  finally {
88
106
  await mock([]);
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Behaviour checks that need no reference answer.
3
+ *
4
+ * A test needs someone to write down what the app should do. These do not: each one compares the app
5
+ * with itself — the page before an action with the page after its undo, the first press with the
6
+ * second, the value sent with the value shown, one run with an identical run. Each is a pure
7
+ * function over observations the explorer already takes, and each returns findings with the steps
8
+ * that show the fault and, when the app gives it, the source line of the element.
9
+ *
10
+ * A check that cannot decide returns nothing. A false finding costs more than a missed one, so every
11
+ * check below names the case it lets pass on purpose.
12
+ */
13
+ import { type EdgeStep } from './edge-cases.js';
14
+ import type { ExploredWrite } from './explorer.js';
15
+ import type { FailureBranches } from './failure-branches.js';
16
+ import type { BehaviourFinding } from './ledger.js';
17
+ /** The rule each check holds the app to, in words, for the report. */
18
+ export declare const Invariant: {
19
+ readonly UNDO_RESTORES: "undo or cancel returns the previous page";
20
+ readonly REPEAT_IS_STABLE: "a repeated action does not change state again";
21
+ readonly RELOAD_KEEPS: "a saved value is still shown after reload";
22
+ readonly SHOWN_IS_SENT: "the value shown after a write is the value the write sent";
23
+ readonly DETERMINISTIC: "two identical drives give the same result";
24
+ readonly FAILURE_HANDLED: "no success over a failed write";
25
+ readonly NO_FAULT: "an edge case gets an answer, not an error";
26
+ readonly RESPONDS: "a control that is pressed does something";
27
+ };
28
+ export type Invariant = (typeof Invariant)[keyof typeof Invariant];
29
+ /** A page as the explorer looks at it: the route and the interactive snapshot tree. */
30
+ export interface PageObservation {
31
+ route?: string;
32
+ tree: string;
33
+ }
34
+ /** A write an action sent, with the server's answer. */
35
+ export interface WriteObservation {
36
+ /** `METHOD /path`, as `writeKeyOf` spells it. */
37
+ key: string;
38
+ status?: number;
39
+ /** The response body, as text. */
40
+ response?: string;
41
+ }
42
+ /** What every check takes besides its observations. */
43
+ interface Context {
44
+ steps: readonly EdgeStep[];
45
+ /** The element's source. Read it from an action result with `sourceOf` (surface/tools). */
46
+ source?: {
47
+ file: string;
48
+ line: number;
49
+ };
50
+ }
51
+ /** The two pages show the same thing: refs and focus aside, every line and the route are equal. */
52
+ export declare const samePage: (a: PageObservation, b: PageObservation) => boolean;
53
+ /**
54
+ * (a) After an action and its undo or cancel, the page shows what it showed before the action.
55
+ * `after` is the page after the undo. Refs and focus are ignored; every other line counts.
56
+ */
57
+ export declare function checkUndo(input: {
58
+ before: PageObservation;
59
+ after: PageObservation;
60
+ } & Context): BehaviourFinding[];
61
+ /**
62
+ * (b) An action pressed twice. Two faults:
63
+ * - the page after the second press is the page after the first, but the second press sent a POST
64
+ * the server accepted and answered differently: it created a second thing the screen hides;
65
+ * - the first press changed nothing on the page and the second changed it.
66
+ * Lets pass: a toggle or a counter (each press shows its own change), and a POST the server answers
67
+ * the same way twice (an upsert).
68
+ */
69
+ export declare function checkRepeat(input: {
70
+ before: PageObservation;
71
+ afterFirst: PageObservation;
72
+ afterSecond: PageObservation;
73
+ firstWrites?: readonly WriteObservation[];
74
+ secondWrites?: readonly WriteObservation[];
75
+ } & Context): BehaviourFinding[];
76
+ /**
77
+ * (c) A value the app said it saved is still shown after a reload. Lets pass: an empty value (there
78
+ * is nothing to look for), and an app that said the value was not saved.
79
+ */
80
+ export declare function checkReloadKeeps(input: {
81
+ value: string;
82
+ afterReload: PageObservation;
83
+ appSaidNotSaved?: boolean;
84
+ } & Context): BehaviourFinding[];
85
+ /**
86
+ * (d) The value a field shows after a write is the value the write request sent for it. Lets pass:
87
+ * a field that shows nothing (it was cleared or closed), and differences of padding, case and digit
88
+ * grouping.
89
+ */
90
+ export declare function checkShownMatchesSent(input: {
91
+ field: string;
92
+ sent: string;
93
+ shown: string | undefined;
94
+ } & Context): BehaviourFinding[];
95
+ /**
96
+ * (e) Two identical drives from a fresh page show the same page. A difference is reported as
97
+ * intermittent: one run does not say which result is the fault.
98
+ *
99
+ * ponytail: every digit is masked before the compare, so a clock, an id or a relative time does not
100
+ * read as a different result. The ceiling: two runs that differ only in a number ("3 items" against
101
+ * "4 items") pass. Mask only time-like and id-like tokens if that proves too loose.
102
+ */
103
+ export declare function checkDeterministic(input: {
104
+ first: PageObservation;
105
+ second: PageObservation;
106
+ } & Context): BehaviourFinding[];
107
+ /**
108
+ * (f) No success over a failed write. The judgement is `driveFailureBranches`'s own; this only gives
109
+ * each unhandled write the steps that fire it, so it reads like every other finding.
110
+ */
111
+ export declare function failureFindings(branches: FailureBranches, writes: readonly ExploredWrite[]): BehaviourFinding[];
112
+ export {};