@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
@@ -1,22 +1,25 @@
1
1
  import { formatStepAddress } from 'open-verification';
2
2
  import { assertsState, expectLabel, expectedElementTestid, withoutClauses } from '@reticlehq/core';
3
3
  import { span } from '../../trace.js';
4
- import { ambiguityDrift, anchorLabel, expectElementDrift, reresolved, resolveTestid, testidDrift, } from './flow-anchor.js';
4
+ import { ambiguityDrift, anchorLabel, expectElementDrift, refFor, reresolved, resolveTestid, testidDrift, } from './flow-anchor.js';
5
5
  export { nearestIsAmbiguous, nearestTestid, resolveQuery } from './flow-anchor.js';
6
6
  import { routeOfEvent, routeOfUrl } from '@reticlehq/engine/question/predicate/predicate-route.js';
7
7
  import { evalConsole } from '@reticlehq/engine/question/predicate/predicate-console.js';
8
8
  import { appScope, stepEffect } from '@reticlehq/engine/evidence/step-effect.js';
9
+ import { stepContradictions } from './flow-cross-step.js';
9
10
  import { AnchorKind, DriftReason, EventType, FlowStepTool, StepEffect, PredicateKind, ReplayStatus, } from '@reticlehq/core';
10
11
  import { asString, invokedFlowMissing, isConsequenceDrift } from '@reticlehq/core';
11
12
  import { anchorFieldName } from './fields/flow-secret-field.js';
12
13
  import { degradedStepResult, isDocumentPressStep, isDegradedAnchor, runComponentStep, runRoleStep, runSequenceStep, } from './flow-step-runners.js';
13
14
  import { inFlightRequestLabels } from '../../surface/tools/act/settle-in-flight.js';
14
15
  import { namedNetIsInFlight } from '@reticlehq/engine/evidence/unsettled.js';
16
+ import { captureBaselines } from '@reticlehq/engine/evidence/baseline.js';
15
17
  /** What a still-open named request means, in the drift's own words. */
16
18
  const IN_FLIGHT_AT_BUDGET_END = 'the request this step declared had not come back when the budget ended — it is still in flight, ' +
17
19
  'so nothing here says the app failed. Raise the step timeout, or look at the endpoint';
18
20
  import { isDocumentGoneError, SESSION_DISCONNECTED_REASON, } from '../../portal/session/facts/session-replaced.js';
19
21
  import { actOnResolvedRef } from './flow-step-runners.js';
22
+ import { isPageStep, runPageStep } from './steps/page-steps.js';
20
23
  /**
21
24
  * The document this replay was driving went away mid-run.
22
25
  *
@@ -41,6 +44,18 @@ export class DocumentLostDuringReplay extends Error {
41
44
  }
42
45
  }
43
46
  const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
47
+ /*
48
+ * How replay paces itself between steps. A person does not act on a screen that is still changing,
49
+ * and replay did: it acted the moment the last step's consequence held. An htmx row swap puts a new
50
+ * button in the page and binds it a moment later, so a click in between did nothing, and a correct
51
+ * build failed. A busy machine makes that moment longer, so the false fail came under parallel load.
52
+ *
53
+ * Quiet means no network, DOM or animation activity for `STEP_QUIET_MS`, which is shorter than any
54
+ * person takes to act on a changed screen. A page that never goes quiet (a poll, a spinner) gets
55
+ * `STEP_QUIET_BUDGET_MS` and then the step runs anyway: the wait paces replay and decides nothing.
56
+ */
57
+ const STEP_QUIET_MS = 250;
58
+ const STEP_QUIET_BUDGET_MS = 2_000;
44
59
  /**
45
60
  * The route (pathname) currently in effect — the page a step runs on. Reads the latest ROUTE_CHANGE
46
61
  * from the whole event buffer; mirrors the predicate engine's route field order (pathname → to).
@@ -58,8 +73,9 @@ function currentRoute(session) {
58
73
  // pathname is `/` on every page of a hash-routed app — so a whole desktop replay reported `/` for
59
74
  // every step. Sixth place the same reading was wrong; see routeOfEvent.
60
75
  const parts = routeOfEvent(last);
61
- if (parts === undefined || 0 === parts.docPath.length)
76
+ if (parts === undefined || 0 === parts.docPath.length) {
62
77
  return undefined;
78
+ }
63
79
  return parts.routePath.length > 0 ? parts.routePath : undefined;
64
80
  }
65
81
  /** Pathname only (drop origin + query) so a net URL stays terse in the journey. */
@@ -81,19 +97,23 @@ function summarizeConsequence(events) {
81
97
  const lastRoute = events.filter((e) => e.type === EventType.ROUTE_CHANGE).at(-1);
82
98
  if (lastRoute !== undefined) {
83
99
  const routed = routeOfEvent(lastRoute);
84
- if (routed !== undefined && routed.routePath.length > 0)
100
+ if (routed !== undefined && routed.routePath.length > 0) {
85
101
  parts.push(`→ ${routed.routePath}`);
102
+ }
86
103
  }
87
104
  const signals = new Set();
88
105
  for (const e of events) {
89
- if (e.type !== EventType.SIGNAL)
106
+ if (e.type !== EventType.SIGNAL) {
90
107
  continue;
108
+ }
91
109
  const name = asString((e.data ?? {})['name']);
92
- if (name !== undefined)
110
+ if (name !== undefined) {
93
111
  signals.add(name);
112
+ }
94
113
  }
95
- for (const name of [...signals].slice(0, 2))
114
+ for (const name of [...signals].slice(0, 2)) {
96
115
  parts.push(`signal ${name}`);
116
+ }
97
117
  for (const n of events.filter((e) => e.type === EventType.NET_REQUEST).slice(0, 2)) {
98
118
  const data = n.data ?? {};
99
119
  const method = asString(data['method']) ?? 'GET';
@@ -102,8 +122,9 @@ function summarizeConsequence(events) {
102
122
  parts.push(`${method} ${path}${status}`.trim());
103
123
  }
104
124
  const errors = events.filter(isConsoleError).length;
105
- if (errors > 0)
125
+ if (errors > 0) {
106
126
  parts.push(`${errors} console error${errors > 1 ? 's' : ''}`);
127
+ }
107
128
  return parts.length > 0 ? parts.join('; ') : undefined;
108
129
  }
109
130
  const isConsoleError = (e) => e.type === EventType.CONSOLE_ERROR || e.type === EventType.ERROR_UNCAUGHT;
@@ -119,8 +140,9 @@ function consoleErrorsIn(events) {
119
140
  }
120
141
  /** The console-absent clauses an expect cannot pass without: itself, or any `allOf` member. */
121
142
  function requiredConsoleAbsences(expect) {
122
- if (PredicateKind.ALL_OF === expect.kind)
143
+ if (PredicateKind.ALL_OF === expect.kind) {
123
144
  return expect.predicates.flatMap(requiredConsoleAbsences);
145
+ }
124
146
  return PredicateKind.CONSOLE === expect.kind && true === expect.absent ? [expect] : [];
125
147
  }
126
148
  /**
@@ -132,8 +154,9 @@ function requiredConsoleAbsences(expect) {
132
154
  function lateConsoleDrift(expect, events) {
133
155
  for (const clause of requiredConsoleAbsences(expect)) {
134
156
  const read = evalConsole(events, clause);
135
- if (read.pass)
157
+ if (read.pass) {
136
158
  continue;
159
+ }
137
160
  return {
138
161
  reasonKind: DriftReason.SIGNAL_NOT_OBSERVED,
139
162
  reason: read.failureReason ?? "the step's declared consequence did not hold",
@@ -143,6 +166,13 @@ function lateConsoleDrift(expect, events) {
143
166
  }
144
167
  return undefined;
145
168
  }
169
+ /** A console-absent expect read over the start-page load, said as such. See `loadSince`. */
170
+ function loadDrift(expect, loadEvents) {
171
+ const drift = 0 === loadEvents.length ? undefined : lateConsoleDrift(expect, loadEvents);
172
+ return drift === undefined
173
+ ? undefined
174
+ : { ...drift, reason: `${drift.reason}, logged while replay loaded the start page` };
175
+ }
146
176
  /** Run one testid-anchored step: re-resolve via QUERY, then ACT on the live ref, else drift. */
147
177
  export async function runTestidStep(session, step, index, value, dynamic, confirmDangerous, sleep) {
148
178
  const { refs, hint } = await resolveTestid(session, value, sleep);
@@ -170,7 +200,8 @@ export async function runTestidStep(session, step, index, value, dynamic, confir
170
200
  if (ambiguous !== null) {
171
201
  return { step: index, tool: step.tool, anchor: value, ok: false, drift: ambiguous };
172
202
  }
173
- const ref = refs[0] ?? '';
203
+ // The only match, or the recorded position among a shared test id (#2135).
204
+ const ref = refFor(step.anchor, refs) ?? '';
174
205
  /*
175
206
  * The same dispatch its siblings use, including the one re-resolve on a stale ref (2.3).
176
207
  *
@@ -186,8 +217,9 @@ export async function runTestidStep(session, step, index, value, dynamic, confir
186
217
  // The field this step types into — from the anchor, so a redacted fill can be supplied from
187
218
  // RETICLE_SECRET_<FIELD> without the flow carrying the secret.
188
219
  anchorFieldName(step.anchor), async () => reresolved(step.anchor, (await resolveTestid(session, value, sleep)).refs));
189
- if (!result.ok)
220
+ if (!result.ok) {
190
221
  return result;
222
+ }
191
223
  // assert the step's expect.element testid is present AFTER the action —
192
224
  // unless that testid was marked DYNAMIC (the LLM-output case), in which case its presence/content
193
225
  // is NOT asserted (only the action ran). The skip is scoped strictly to the dynamic set.
@@ -230,7 +262,7 @@ export async function runTestidStep(session, step, index, value, dynamic, confir
230
262
  * quietly stop agreeing, and the disagreement would land as a flow that replays green against a
231
263
  * state it was never meant to run in.
232
264
  */
233
- export async function assertStepExpect(session, expect, waitForSignal, timeoutMs, since) {
265
+ export async function assertStepExpect(session, expect, waitForSignal, timeoutMs, since, baselines) {
234
266
  // A testid is already asserted against the live DOM by the step runner. A role/name locator is
235
267
  // not that path — stripping every element made a recorded `until` by button name a no-op, so a
236
268
  // flow that proved the control at capture time could not go red when it was gone.
@@ -254,16 +286,19 @@ export async function assertStepExpect(session, expect, waitForSignal, timeoutMs
254
286
  (clause) => PredicateKind.ELEMENT === clause.kind &&
255
287
  clause.query.testid !== undefined &&
256
288
  true !== clause.absent);
257
- if (predicate === undefined)
289
+ if (predicate === undefined) {
258
290
  return undefined;
259
- const verdict = await waitForSignal(session, predicate, timeoutMs, since);
260
- if (verdict.pass)
291
+ }
292
+ const verdict = await waitForSignal(session, predicate, timeoutMs, since, baselines);
293
+ if (verdict.pass) {
261
294
  return undefined;
295
+ }
262
296
  // The wait ended because the page stopped being observable, not because the consequence failed:
263
297
  // the same fact as a command the dying socket rejected, so it takes the same path (the replay loop
264
298
  // reports a lost document). As a drift, a socket that blinked for 26ms read as the app's defect.
265
- if (true === verdict.observationLost)
299
+ if (true === verdict.observationLost) {
266
300
  throw new Error(SESSION_DISCONNECTED_REASON);
301
+ }
267
302
  // A request the step NAMED that had not come back yet is a blind spot, not an assertion the app
268
303
  // failed. The live verdict path already draws this line (namedNetIsInFlight); replay read the
269
304
  // same window and never asked, so a slow endpoint was reported as a consequence that never fired.
@@ -272,14 +307,17 @@ export async function assertStepExpect(session, expect, waitForSignal, timeoutMs
272
307
  ? []
273
308
  : inFlightRequestLabels(session.eventsSince(since), session.url, session.background);
274
309
  const namedInFlight = namedNetIsInFlight(predicate, openRequests);
310
+ // The store case keeps its own kind because heal and the run report branch on it; everything
311
+ // else is a consequence that did not hold, and the reason carries observed-vs-expected.
312
+ let reasonKind = DriftReason.SIGNAL_NOT_OBSERVED;
313
+ if (assertsState(expect)) {
314
+ reasonKind = DriftReason.STATE_MISMATCH;
315
+ }
316
+ else if (namedInFlight) {
317
+ reasonKind = DriftReason.NET_STILL_IN_FLIGHT;
318
+ }
275
319
  return {
276
- // The store case keeps its own kind because heal and the run report branch on it; everything
277
- // else is a consequence that did not hold, and the reason carries observed-vs-expected.
278
- reasonKind: assertsState(expect)
279
- ? DriftReason.STATE_MISMATCH
280
- : namedInFlight
281
- ? DriftReason.NET_STILL_IN_FLIGHT
282
- : DriftReason.SIGNAL_NOT_OBSERVED,
320
+ reasonKind,
283
321
  reason: namedInFlight
284
322
  ? `${IN_FLIGHT_AT_BUDGET_END} (${openRequests.join(', ')})`
285
323
  : withCaseOnly(verdict.failureReason ?? "the step's declared consequence did not hold", verdict),
@@ -293,8 +331,9 @@ export async function assertStepExpect(session, expect, waitForSignal, timeoutMs
293
331
  * reading its replay needs the fix (`ignoreCase: true`, or re-record), not a bare "did not hold".
294
332
  */
295
333
  export function withCaseOnly(reason, verdict) {
296
- if (true !== verdict.caseOnlyMiss)
334
+ if (true !== verdict.caseOnlyMiss) {
297
335
  return reason;
336
+ }
298
337
  const shown = verdict.wouldHold === undefined ? '' : ` ("${verdict.wouldHold}")`;
299
338
  return `${reason} — the page shows it in another case${shown}. Text checks match case exactly: if any case is meant, add ignoreCase: true to this check, or re-record the flow.`;
300
339
  }
@@ -310,8 +349,9 @@ async function runSignalStep(session, step, index, name, waitForSignal, signalTi
310
349
  // cross-flow false green on the exact suite-verify path the regression-cost claim rests on. The
311
350
  // replay-start floor excludes prior flows/runs while still seeing this run's adjacent-step signal.
312
351
  const verdict = await waitForSignal(session, { kind: PredicateKind.SIGNAL, name }, signalTimeoutMs, since);
313
- if (verdict.pass)
352
+ if (verdict.pass) {
314
353
  return { step: index, tool: step.tool, anchor: name, ok: true };
354
+ }
315
355
  return {
316
356
  step: index,
317
357
  tool: step.tool,
@@ -332,11 +372,13 @@ async function runSignalStep(session, step, index, name, waitForSignal, signalTi
332
372
  * is not setup, it is a second charge, a second email, a second record.
333
373
  */
334
374
  export function resumeIndex(from, steps) {
335
- if (from === undefined)
375
+ if (from === undefined) {
336
376
  return 0;
377
+ }
337
378
  const at = 'number' === typeof from ? from : steps.findIndex((step) => step.id === from);
338
- if (at < 0 || at >= steps.length || !Number.isInteger(at))
379
+ if (at < 0 || at >= steps.length || !Number.isInteger(at)) {
339
380
  return undefined;
381
+ }
340
382
  return steps.slice(0, at).some((step) => StepEffect.COMMITS === step.effect) ? 0 : at;
341
383
  }
342
384
  /**
@@ -351,8 +393,9 @@ export function replayWindow(args) {
351
393
  }
352
394
  /** A part-way replay's status, from the steps it drove. */
353
395
  export function partialStatus(steps) {
354
- if (steps.some((step) => step.drift !== undefined))
396
+ if (steps.some((step) => step.drift !== undefined)) {
355
397
  return ReplayStatus.DRIFT;
398
+ }
356
399
  return steps.every((step) => step.ok) ? ReplayStatus.OK : ReplayStatus.ERROR;
357
400
  }
358
401
  /** The `from` argument, narrowed: a step index or a step id. */
@@ -361,8 +404,9 @@ export function resumeArg(raw) {
361
404
  }
362
405
  /** Why `from` cannot be resumed at, or undefined when it can (or was not given). */
363
406
  export function unresolvedResume(from, steps) {
364
- if (resumeIndex(from, steps) !== undefined)
407
+ if (resumeIndex(from, steps) !== undefined) {
365
408
  return undefined;
409
+ }
366
410
  return `cannot resume at ${JSON.stringify(from)}: this flow has ${String(steps.length)} step(s) and no step by that id`;
367
411
  }
368
412
  /**
@@ -446,31 +490,70 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
446
490
  };
447
491
  try {
448
492
  for (const step of flow.steps) {
449
- if (options.to !== undefined && index >= options.to)
493
+ if (options.to !== undefined && index >= options.to) {
450
494
  break;
495
+ }
451
496
  if (options.at !== undefined && index < options.at) {
452
497
  index += 1;
453
498
  continue;
454
499
  }
500
+ if (index > (options.at ?? 0)) {
501
+ await waitForSignal(session, { kind: PredicateKind.SETTLED, quietMs: STEP_QUIET_MS }, STEP_QUIET_BUDGET_MS);
502
+ }
455
503
  if (step.invoke !== undefined) {
456
504
  results.push(await runInvokeStep(session, flow, step, index, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, options, index < from));
457
505
  const last = results[results.length - 1];
458
- if (last !== undefined && index < from && last.ok)
506
+ if (last !== undefined && index < from && last.ok) {
459
507
  results.pop();
508
+ }
460
509
  tell(last);
461
- if (last !== undefined && false === last.ok)
510
+ if (last !== undefined && false === last.ok) {
511
+ break;
512
+ }
513
+ index += 1;
514
+ continue;
515
+ }
516
+ if (isPageStep(step)) {
517
+ const ran = await runPageStep(step, index, {
518
+ open: options.navigate,
519
+ send: options.request,
520
+ checked: index >= from,
521
+ confirmDangerous,
522
+ // A navigated page's clock starts at its own connect, so its whole life is the window.
523
+ judge: (on, expectation) => assertStepExpect(on, expectation, waitForSignal, waitFor(step), 0),
524
+ route: currentRoute,
525
+ });
526
+ const result = ran.result;
527
+ if (ran.session !== undefined) {
528
+ // The page that asked for the load is gone; every later step runs on the one that loaded.
529
+ session = ran.session;
530
+ }
531
+ if (index < from && result.ok && result.drift === undefined) {
532
+ index += 1;
533
+ continue;
534
+ }
535
+ if (index < from) {
536
+ result.note = `setup step for resuming at step ${String(from)} failed`;
537
+ }
538
+ await options.onStepResult?.(result, flow.name).catch(() => undefined);
539
+ results.push(result);
540
+ tell(result);
541
+ if (!result.ok || result.drift !== undefined) {
462
542
  break;
543
+ }
463
544
  index += 1;
464
545
  continue;
465
546
  }
466
547
  const label = anchorLabel(step.anchor);
467
548
  /** One sub-step's expect, over the window that sub-step opened. See runSequenceStep. */
468
- const judgeSubStep = async (expectation, since) => (await assertStepExpect(session, expectation, waitForSignal, waitFor(step), since)) ??
549
+ const budget = waitFor(step);
550
+ const judgeSubStep = async (expectation, since, before) => (await assertStepExpect(session, expectation, waitForSignal, budget, since, before)) ??
469
551
  lateConsoleDrift(expectation, session.eventsSince(since).filter((e) => e.t >= since));
470
552
  // The page this step runs on (the journey's "which page") — captured before the action.
471
553
  const page = currentRoute(session);
472
- // Event-time floor so the consequence reflects only THIS step's aftermath, not prior steps'.
554
+ // This step's event-time floor, and (as act_and_wait) what `unchanged` and kin compare with.
473
555
  const cursorBefore = session.elapsed();
556
+ const baselines = index < from ? undefined : await captureBaselines(session, step.expect);
474
557
  const subSteps = step.steps;
475
558
  // Traced per step, so a slow replay says WHICH step and which anchor kind spent the time. The
476
559
  // per-step `durationMs` below is what the agent gets back; this is what a developer profiling
@@ -493,9 +576,12 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
493
576
  if (step.anchor.kind === AnchorKind.COMPONENT) {
494
577
  return runComponentStep(session, step, index, step.anchor, confirmDangerous, sleep);
495
578
  }
496
- if (step.anchor.kind === AnchorKind.ROLE && step.anchor.name !== undefined) {
497
- // A NAMED role anchor addresses one element. The nameless one is the degraded placeholder
498
- // and keeps its old path, where it fails legibly rather than querying a role as a testid.
579
+ if (step.anchor.kind === AnchorKind.ROLE) {
580
+ // Named or nameless, a role anchor resolves by role. The degraded placeholder and the
581
+ // document-key press were handled above, so what reaches here addresses a real control:
582
+ // a nameless one (an icon-only button) by its position among nameless controls of its
583
+ // role. It used to fall through to the testid path, which queried the ROLE as a testid
584
+ // and answered `testid "button" not found` on a working page.
499
585
  return runRoleStep(session, step, index, step.anchor, confirmDangerous, sleep);
500
586
  }
501
587
  return runTestidStep(session, step, index, label, dynamic, confirmDangerous, sleep);
@@ -516,30 +602,40 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
516
602
  // A setup step's consequence is not checked: it is re-driven to get somewhere, not to prove it.
517
603
  const checked = index < from ? [] : own;
518
604
  for (const expectation of checked) {
519
- if (!result.ok || result.drift !== undefined)
605
+ if (!result.ok || result.drift !== undefined) {
520
606
  break;
521
- const expectDrift = await assertStepExpect(session, expectation, waitForSignal, waitFor(step), cursorBefore);
607
+ }
608
+ const expectDrift = await assertStepExpect(session, expectation, waitForSignal, waitFor(step), cursorBefore, baselines);
522
609
  if (expectDrift !== undefined) {
523
610
  result.ok = false;
524
611
  result.drift = expectDrift;
525
612
  }
526
613
  }
527
- if (page !== undefined)
614
+ if (page !== undefined) {
528
615
  result.page = page;
616
+ }
529
617
  const endPage = currentRoute(session);
530
- if (endPage !== undefined && endPage !== page)
618
+ if (endPage !== undefined && endPage !== page) {
531
619
  result.endPage = endPage;
620
+ }
532
621
  const windowEvents = session.eventsSince(cursorBefore).filter((e) => e.t >= cursorBefore);
622
+ const loadSince = 0 === index ? options.loadSince : undefined;
623
+ const loadEvents = loadSince === undefined
624
+ ? []
625
+ : session.eventsSince(loadSince).filter((e) => e.t < cursorBefore);
533
626
  const consequence = summarizeConsequence(windowEvents);
534
- if (consequence !== undefined)
627
+ if (consequence !== undefined) {
535
628
  result.consequence = consequence;
629
+ }
536
630
  const consoleErrors = consoleErrorsIn(windowEvents);
537
- if (consoleErrors !== undefined)
631
+ if (consoleErrors !== undefined) {
538
632
  result.consoleErrors = consoleErrors;
633
+ }
539
634
  for (const expectation of checked) {
540
- if (!result.ok || result.drift !== undefined)
635
+ if (!result.ok || result.drift !== undefined) {
541
636
  break;
542
- const late = lateConsoleDrift(expectation, windowEvents);
637
+ }
638
+ const late = lateConsoleDrift(expectation, windowEvents) ?? loadDrift(expectation, loadEvents);
543
639
  if (late !== undefined) {
544
640
  result.ok = false;
545
641
  result.drift = late;
@@ -551,9 +647,21 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
551
647
  const cursorAfter = session.elapsed();
552
648
  // ONE builder, shared with the live act path, so a driven step and a replayed one describe what
553
649
  // happened in identical words rather than in two vocabularies that agree by coincidence.
554
- Object.assign(result, stepEffect(windowEvents, { since: cursorBefore, until: cursorAfter },
555
- // A failure the step declared is its expected result, as it is on the live path.
556
- { ...appScope(session), declared }));
650
+ const effect = stepEffect(windowEvents, { since: cursorBefore, until: cursorAfter },
651
+ // A failure the step declared is its expected result, as it is on the live path. The reads
652
+ // that finished before the step let a background read that fails on every tick be left
653
+ // alone, instead of being blamed on each step it happens to fall inside.
654
+ {
655
+ ...appScope(session),
656
+ declared,
657
+ before: session
658
+ .eventsSince(0)
659
+ .filter((e) => e.t < cursorBefore && e.type === EventType.NET_REQUEST),
660
+ });
661
+ // "Never settled" is decided once the replay is over, by the whole-span pass (flow-cross-step).
662
+ const stepOwn = stepContradictions(effect.contradictions ?? []);
663
+ delete effect.contradictions;
664
+ Object.assign(result, effect, 0 === stepOwn.length ? {} : { contradictions: stepOwn });
557
665
  /*
558
666
  * A prefix step is setup and is not reported -- UNLESS it failed.
559
667
  *
@@ -563,14 +671,16 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
563
671
  */
564
672
  // `tool` is dropped HERE rather than at the ten places that set it, so a new step runner cannot
565
673
  // forget the rule and quietly re-introduce the cost. Spelled out only when it is NOT the default.
566
- if (FlowStepTool.ACT === result.tool)
674
+ if (FlowStepTool.ACT === result.tool) {
567
675
  delete result.tool;
676
+ }
568
677
  if (index < from && result.ok && result.drift === undefined) {
569
678
  index += 1;
570
679
  continue;
571
680
  }
572
- if (index < from)
681
+ if (index < from) {
573
682
  result.note = `setup step for resuming at step ${String(from)} failed`;
683
+ }
574
684
  await options.onStepResult?.(result, flow.name).catch(() => undefined);
575
685
  results.push(result);
576
686
  tell(result);
@@ -579,14 +689,21 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
579
689
  const sweepPast = true === options.sweep &&
580
690
  result.drift !== undefined &&
581
691
  isConsequenceDrift(result.drift.reasonKind);
582
- if (!sweepPast && (result.drift !== undefined || !result.ok))
692
+ if (!sweepPast && (result.drift !== undefined || !result.ok)) {
583
693
  break;
694
+ }
584
695
  index += 1;
585
696
  }
697
+ // And once after the last step: a page can satisfy the last expect at once and finish its own
698
+ // work after (a sort header marks itself, then fetches the rows). The journey is judged next.
699
+ if (results.length > 0) {
700
+ await waitForSignal(session, { kind: PredicateKind.SETTLED, quietMs: STEP_QUIET_MS }, STEP_QUIET_BUDGET_MS);
701
+ }
586
702
  }
587
703
  catch (error) {
588
- if (!isDocumentGoneError(error))
704
+ if (!isDocumentGoneError(error)) {
589
705
  throw error;
706
+ }
590
707
  throw new DocumentLostDuringReplay(results, index, error);
591
708
  }
592
709
  return results;
@@ -53,6 +53,9 @@ function relax(predicate, money) {
53
53
  }
54
54
  if (PredicateKind.TEXT === predicate.kind &&
55
55
  predicate.satisfies === undefined &&
56
+ // An absent count stays exact: made word-level, it says no count is on the page, and a page
57
+ // that shows another count fails it.
58
+ true !== predicate.absent &&
56
59
  predicate.contains !== undefined &&
57
60
  HAS_NUMBER.test(predicate.contains)) {
58
61
  const relaxed = wordLevel(predicate.contains, money);
@@ -143,7 +146,10 @@ export function replayability(flow) {
143
146
  (AnchorKind.ROLE === anchor?.kind && DEGRADED_ANCHOR_ROLE === anchor.role))) {
144
147
  blocked.push(`step ${at} has no anchor a replay can find`);
145
148
  }
146
- else if (own && AnchorKind.TESTID === anchor?.kind && anchor.of !== undefined) {
149
+ else if (own &&
150
+ AnchorKind.TESTID === anchor?.kind &&
151
+ anchor.of !== undefined &&
152
+ anchor.nth === undefined) {
147
153
  blocked.push(`step ${at} anchors on test id '${anchor.value}', which ${String(anchor.of)} elements share`);
148
154
  }
149
155
  if (own &&
@@ -33,9 +33,15 @@ export interface StartPathArrival {
33
33
  redirected?: string;
34
34
  /** Set when the page's loading kept replay waiting past the step budget. */
35
35
  wait?: StartPageWait;
36
+ /** Set when the flow declares a route and replay could not put the tab on it: nothing may run (#2034). */
37
+ unreached?: string;
36
38
  }
37
39
  /** The replay result, with how long replay waited for its start page when that went past the budget. */
38
40
  export declare function withStartPageWait<R extends FlowReplayResult>(result: R, arrival: StartPathArrival): R;
41
+ /** Record where a replay left the tab, so the next reset does not keep that flow's query. */
42
+ export declare function noteReplayEnded(session: StartPathSession & {
43
+ id: string;
44
+ }): void;
39
45
  /**
40
46
  * Replay's half of the FlowFile contract: put the tab on the flow's `startPath` before step 1, and
41
47
  * RELOAD it if it is already there.
@@ -50,6 +56,10 @@ export declare function withStartPageWait<R extends FlowReplayResult>(result: R,
50
56
  * or from the wrong page. Silence is not an opt-out: a flow that depends on leftovers without
51
57
  * saying so is a flow whose green means nothing on a fresh machine, and CI is a fresh machine.
52
58
  *
59
+ * A route in `requires` is not an opt-out. It is the page the flow needs, and replay goes there:
60
+ * see `arriveAtDeclaredRoute`. A suite ran such a flow on the page the previous flow left open,
61
+ * and its anchors then failed, or matched ambiguously, on an unrelated page (#2034).
62
+ *
53
63
  * A full-page load tears down the session socket, so the navigation must happen here — before any
54
64
  * step runs — and the replay continues on the session the SDK reconnects as (found via the same
55
65
  * tombstone rebind that lets `resolve(oldId)` answer after any navigation). Best-effort by design:
@@ -63,6 +73,11 @@ export declare function arriveAtStartPath(sessions: SessionManager, session: Sta
63
73
  }, flow: FlowFile, timeoutMs?: number, clock?: ArrivalClock,
64
74
  /** Whether the tab is still loading. Replay passes one for a pool lease, the only tab it can ask. */
65
75
  stillLoading?: () => Promise<boolean>, loadingCapMs?: number): Promise<StartPathArrival>;
76
+ /**
77
+ * The arrival for a replay that continues at a step and so does not move the tab. A declared route
78
+ * claim must still hold where the tab is, as it did when it was a precondition the run checked.
79
+ */
80
+ export declare function routeClaimsWhereTheTabIs(flow: FlowFile, session: StartPathSession): StartPathArrival;
66
81
  /**
67
82
  * Send a tab somewhere and wait for the session that comes back.
68
83
  *
@@ -95,14 +110,28 @@ export type NavigateOutcome = {
95
110
  stillLoading: boolean;
96
111
  wait?: StartPageWait;
97
112
  };
113
+ /**
114
+ * Whether assigning `to` from `from` is a jump within the same document. That is the case when the
115
+ * URLs differ at most in the fragment and `to` has a fragment; a browser then loads no page. Without
116
+ * a fragment, the same URL reloads by itself. Undefined or unparsable URLs are not a jump.
117
+ */
118
+ export declare function isFragmentJump(from: string | undefined, to: string): boolean;
98
119
  export declare function navigateAndAwait(sessions: SessionManager, session: {
99
120
  id: string;
100
121
  /** Optional because a session's URL is, and `carryReticleIdentity` already handles its absence. */
101
122
  url?: string;
102
123
  command(name: string, args?: Record<string, unknown>): Promise<CommandResult>;
103
- }, destination: string, expectedPath: string, timeoutMs: number, clock?: ArrivalClock,
104
- /** True when the destination is the page we are already on — see `arrivedSuccessor`. */
105
- isReload?: boolean,
106
- /** Whether the navigated page is still loading; absent when it cannot be asked. */
107
- stillLoading?: () => Promise<boolean>, loadingCapMs?: number): Promise<NavigateOutcome>;
124
+ }, destination: string, expectedPath: string, timeoutMs: number, clock?: ArrivalClock, options?: {
125
+ /** True when the destination is the page we are already on — see `arrivedSuccessor`. */
126
+ isReload?: boolean;
127
+ /** Whether the navigated page is still loading; absent when it cannot be asked. */
128
+ stillLoading?: () => Promise<boolean>;
129
+ loadingCapMs?: number;
130
+ /**
131
+ * Accept the first NEW page at once, on any path. A flow's `navigate` step opens a URL the
132
+ * server may redirect (a paused short link goes to `/404`), and the page that connects is the
133
+ * destination. Start-page arrival leaves this off: it waits for its own path first.
134
+ */
135
+ acceptRedirect?: boolean;
136
+ }): Promise<NavigateOutcome>;
108
137
  export {};