@reticlehq/server 3.9.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/SKILL.md +13 -12
  2. package/dist/command/cli/adhoc-suite.js +2 -0
  3. package/dist/command/cli/adhoc-verdict.d.ts +2 -43
  4. package/dist/command/cli/adhoc-verdict.js +50 -187
  5. package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
  6. package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
  7. package/dist/command/cli/cli-doctor.js +1 -1
  8. package/dist/command/cli/cli-flow-commands.js +2 -9
  9. package/dist/command/cli/cli-kill.js +1 -1
  10. package/dist/command/cli/cli-parse-compare.d.ts +22 -0
  11. package/dist/command/cli/cli-parse-compare.js +75 -0
  12. package/dist/command/cli/cli-parse-grade.d.ts +36 -0
  13. package/dist/command/cli/cli-parse-grade.js +75 -0
  14. package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
  15. package/dist/command/cli/cli-parse-grammar.js +18 -20
  16. package/dist/command/cli/cli-parse-verify.d.ts +10 -0
  17. package/dist/command/cli/cli-parse-verify.js +63 -14
  18. package/dist/command/cli/cli-parse.d.ts +17 -15
  19. package/dist/command/cli/cli-parse.js +44 -27
  20. package/dist/command/cli/cli-update-commands.d.ts +1 -1
  21. package/dist/command/cli/cli-update-commands.js +29 -20
  22. package/dist/command/cli/cli-usage.js +58 -29
  23. package/dist/command/cli/cli-verify.d.ts +11 -2
  24. package/dist/command/cli/cli-verify.js +27 -8
  25. package/dist/command/cli/cloud-cli.js +2 -119
  26. package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
  27. package/dist/command/cli/daemon-link/daemon-client.js +205 -0
  28. package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
  29. package/dist/command/cli/doctor-repairs.js +1 -1
  30. package/dist/command/cli/ensure-daemon.d.ts +26 -0
  31. package/dist/command/cli/ensure-daemon.js +57 -0
  32. package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
  33. package/dist/command/cli/finished-run/grade-command.js +180 -0
  34. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
  35. package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
  36. package/dist/command/cli/ports/port-holder.d.ts +9 -0
  37. package/dist/command/cli/ports/port-holder.js +27 -3
  38. package/dist/command/cli/status/status-lines.d.ts +1 -0
  39. package/dist/command/cli/status/status-lines.js +22 -0
  40. package/dist/command/cli/status-fields.d.ts +5 -1
  41. package/dist/command/cli/status-fields.js +7 -3
  42. package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
  43. package/dist/command/cli/two-builds/compare-command.js +314 -0
  44. package/dist/command/cli/verify-against.d.ts +61 -0
  45. package/dist/command/cli/verify-against.js +187 -0
  46. package/dist/command/cli/verify-no-session.d.ts +2 -0
  47. package/dist/command/cli/verify-no-session.js +13 -0
  48. package/dist/command/cli/verify-results.d.ts +35 -2
  49. package/dist/command/cli/verify-results.js +76 -10
  50. package/dist/command/cli.js +46 -41
  51. package/dist/command/daemon/daemon-resolve.d.ts +8 -1
  52. package/dist/command/daemon/daemon-resolve.js +27 -1
  53. package/dist/command/daemon/daemon.d.ts +12 -1
  54. package/dist/command/daemon/daemon.js +35 -4
  55. package/dist/command/daemon/lifetime/death-cause.js +21 -2
  56. package/dist/command/daemon/lifetime/heartbeat.js +7 -2
  57. package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
  58. package/dist/command/daemon/lifetime/startup-failure.js +62 -0
  59. package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
  60. package/dist/command/setup/bringup/dev-server-files.js +17 -0
  61. package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
  62. package/dist/command/setup/bringup/static-page-server.js +43 -9
  63. package/dist/command/setup/init/init-runtime.js +6 -0
  64. package/dist/command/setup/node-effects.d.ts +3 -2
  65. package/dist/command/setup/node-effects.js +9 -3
  66. package/dist/command/setup/remaining-steps.d.ts +8 -0
  67. package/dist/command/setup/remaining-steps.js +10 -0
  68. package/dist/command/setup/run-setup.d.ts +2 -0
  69. package/dist/command/setup/run-setup.js +9 -1
  70. package/dist/command/setup/setup-command.js +6 -3
  71. package/dist/command/update/updater.js +6 -0
  72. package/dist/command/version/version-skew.js +1 -1
  73. package/dist/features/compare/action-plan.d.ts +260 -0
  74. package/dist/features/compare/action-plan.js +543 -0
  75. package/dist/features/compare/behaviour-compare.d.ts +24 -0
  76. package/dist/features/compare/behaviour-compare.js +85 -0
  77. package/dist/features/compare/behaviour-diff.d.ts +97 -0
  78. package/dist/features/compare/behaviour-diff.js +202 -0
  79. package/dist/features/compare/behaviour-report.d.ts +40 -0
  80. package/dist/features/compare/behaviour-report.js +128 -0
  81. package/dist/features/compare/build-readings.d.ts +106 -0
  82. package/dist/features/compare/build-readings.js +250 -0
  83. package/dist/features/compare/compare-diff.d.ts +62 -0
  84. package/dist/features/compare/compare-diff.js +177 -0
  85. package/dist/features/compare/compare-report-html.d.ts +6 -0
  86. package/dist/features/compare/compare-report-html.js +102 -0
  87. package/dist/features/compare/compare-report.d.ts +15 -0
  88. package/dist/features/compare/compare-report.js +73 -0
  89. package/dist/features/compare/compare-run.d.ts +60 -0
  90. package/dist/features/compare/compare-run.js +123 -0
  91. package/dist/features/compare/outside/effect-lines.d.ts +26 -0
  92. package/dist/features/compare/outside/effect-lines.js +103 -0
  93. package/dist/features/compare/outside/state-sources.d.ts +76 -0
  94. package/dist/features/compare/outside/state-sources.js +164 -0
  95. package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
  96. package/dist/features/compare/outside/value-dictionary.js +227 -0
  97. package/dist/features/compare/visible-content.d.ts +20 -0
  98. package/dist/features/compare/visible-content.js +125 -0
  99. package/dist/features/crawl/crawl-empty.js +2 -1
  100. package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
  101. package/dist/features/crawl/crawl-exhaustive.js +138 -0
  102. package/dist/features/crawl/crawl-tools.d.ts +9 -2
  103. package/dist/features/crawl/crawl-tools.js +28 -12
  104. package/dist/features/crawl/crawl.d.ts +8 -9
  105. package/dist/features/crawl/crawl.js +61 -65
  106. package/dist/features/exhaust/app-coverage.d.ts +5 -1
  107. package/dist/features/exhaust/app-coverage.js +3 -1
  108. package/dist/features/exhaust/code-coverage.js +8 -4
  109. package/dist/features/exhaust/edge-cases.d.ts +133 -0
  110. package/dist/features/exhaust/edge-cases.js +310 -0
  111. package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
  112. package/dist/features/exhaust/edges/edge-drive.js +364 -0
  113. package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
  114. package/dist/features/exhaust/edges/edge-run.js +169 -0
  115. package/dist/features/exhaust/explorer.d.ts +87 -14
  116. package/dist/features/exhaust/explorer.js +159 -25
  117. package/dist/features/exhaust/failure-branches.d.ts +5 -0
  118. package/dist/features/exhaust/failure-branches.js +29 -11
  119. package/dist/features/exhaust/invariants.d.ts +112 -0
  120. package/dist/features/exhaust/invariants.js +188 -0
  121. package/dist/features/exhaust/ledger.d.ts +68 -0
  122. package/dist/features/exhaust/ledger.js +121 -13
  123. package/dist/features/exhaust/session-fold.d.ts +10 -0
  124. package/dist/features/exhaust/session-fold.js +46 -3
  125. package/dist/features/exhaust/session-port.d.ts +3 -1
  126. package/dist/features/exhaust/session-port.js +97 -28
  127. package/dist/features/grade/grade-answer.d.ts +99 -0
  128. package/dist/features/grade/grade-answer.js +112 -0
  129. package/dist/features/grade/grade-noise.d.ts +27 -0
  130. package/dist/features/grade/grade-noise.js +55 -0
  131. package/dist/features/grade/grade-renumber.d.ts +12 -0
  132. package/dist/features/grade/grade-renumber.js +86 -0
  133. package/dist/features/grade/grade-state.d.ts +50 -0
  134. package/dist/features/grade/grade-state.js +93 -0
  135. package/dist/features/grade/grade-suggest.d.ts +33 -0
  136. package/dist/features/grade/grade-suggest.js +98 -0
  137. package/dist/features/visual/visual-tools.js +66 -19
  138. package/dist/index.js +7 -4
  139. package/dist/judgement/capsule/causal-summary.js +9 -2
  140. package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
  141. package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
  142. package/dist/judgement/runs/reticle-runner.js +16 -2
  143. package/dist/judgement/runs/runner-port.d.ts +2 -0
  144. package/dist/judgement/runs/runner-port.js +23 -5
  145. package/dist/language/flows/affected-tools.d.ts +1 -1
  146. package/dist/language/flows/affected-tools.js +49 -30
  147. package/dist/language/flows/change/git-changed.d.ts +2 -1
  148. package/dist/language/flows/change/git-changed.js +19 -2
  149. package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
  150. package/dist/language/flows/fields/flow-secret-field.js +77 -21
  151. package/dist/language/flows/flow-anchor.d.ts +60 -2
  152. package/dist/language/flows/flow-anchor.js +125 -20
  153. package/dist/language/flows/flow-classify.d.ts +20 -0
  154. package/dist/language/flows/flow-classify.js +90 -12
  155. package/dist/language/flows/flow-cross-step.d.ts +7 -1
  156. package/dist/language/flows/flow-cross-step.js +25 -1
  157. package/dist/language/flows/flow-journey.d.ts +38 -4
  158. package/dist/language/flows/flow-journey.js +138 -8
  159. package/dist/language/flows/flow-replay-run.d.ts +0 -16
  160. package/dist/language/flows/flow-replay-run.js +76 -14
  161. package/dist/language/flows/flow-replay-types.d.ts +4 -2
  162. package/dist/language/flows/flow-replay.d.ts +19 -1
  163. package/dist/language/flows/flow-replay.js +169 -52
  164. package/dist/language/flows/flow-replayable.js +7 -1
  165. package/dist/language/flows/flow-start-path.d.ts +34 -5
  166. package/dist/language/flows/flow-start-path.js +202 -15
  167. package/dist/language/flows/flow-step-runners.d.ts +14 -13
  168. package/dist/language/flows/flow-step-runners.js +184 -109
  169. package/dist/language/flows/flow-store-for-session.d.ts +26 -25
  170. package/dist/language/flows/flow-store-for-session.js +44 -0
  171. package/dist/language/flows/flow-tools.js +46 -33
  172. package/dist/language/flows/flows.js +176 -76
  173. package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
  174. package/dist/language/flows/recording/tape/recordings.js +71 -5
  175. package/dist/language/flows/replay.d.ts +19 -5
  176. package/dist/language/flows/replay.js +99 -7
  177. package/dist/language/flows/steps/page-steps.d.ts +68 -0
  178. package/dist/language/flows/steps/page-steps.js +198 -0
  179. package/dist/language/flows/suite/sequential-run.d.ts +68 -0
  180. package/dist/language/flows/suite/sequential-run.js +96 -0
  181. package/dist/language/flows/verify-change-tools.js +26 -15
  182. package/dist/memory/cloud/background-sync.d.ts +13 -0
  183. package/dist/memory/cloud/background-sync.js +24 -3
  184. package/dist/memory/cloud/sync-daemon.js +26 -27
  185. package/dist/memory/journal/attach-journal.js +2 -1
  186. package/dist/memory/journal/drive-flow.d.ts +7 -0
  187. package/dist/memory/journal/drive-flow.js +44 -13
  188. package/dist/memory/journal/journal-recorder.d.ts +4 -2
  189. package/dist/memory/journal/session-journal.d.ts +7 -0
  190. package/dist/memory/journal/session-journal.js +58 -10
  191. package/dist/memory/project/root-claims.d.ts +9 -0
  192. package/dist/memory/project/root-claims.js +9 -4
  193. package/dist/portal/input/download-capture.d.ts +7 -0
  194. package/dist/portal/input/download-capture.js +92 -0
  195. package/dist/portal/input/network-detail.d.ts +6 -0
  196. package/dist/portal/input/network-detail.js +12 -1
  197. package/dist/portal/input/network-mock-tools.js +49 -13
  198. package/dist/portal/input/network-mock.d.ts +8 -0
  199. package/dist/portal/input/network-mock.js +28 -9
  200. package/dist/portal/input/page-capture.d.ts +24 -0
  201. package/dist/portal/input/page-capture.js +33 -0
  202. package/dist/portal/input/real-input.d.ts +22 -20
  203. package/dist/portal/input/real-input.js +41 -31
  204. package/dist/portal/input/viewport-tools.d.ts +8 -0
  205. package/dist/portal/input/viewport-tools.js +75 -18
  206. package/dist/portal/pool/browser-pool.d.ts +11 -29
  207. package/dist/portal/pool/browser-pool.js +35 -17
  208. package/dist/portal/pool/context-permissions.d.ts +17 -3
  209. package/dist/portal/pool/context-permissions.js +36 -5
  210. package/dist/portal/pool/determinism.d.ts +52 -0
  211. package/dist/portal/pool/determinism.js +259 -0
  212. package/dist/portal/pool/lease-emulation.d.ts +34 -1
  213. package/dist/portal/pool/lease-emulation.js +18 -0
  214. package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
  215. package/dist/portal/pool/lease-marker-stamp.js +15 -1
  216. package/dist/portal/pool/lease-page-error.d.ts +44 -17
  217. package/dist/portal/pool/lease-page-error.js +147 -5
  218. package/dist/portal/pool/page-error-journal.d.ts +7 -1
  219. package/dist/portal/pool/page-error-journal.js +27 -0
  220. package/dist/portal/pool/playwright-launcher.js +51 -8
  221. package/dist/portal/pool/pool-contract.d.ts +27 -1
  222. package/dist/portal/pool/pool-lease.d.ts +36 -1
  223. package/dist/portal/pool/zero-install.d.ts +8 -0
  224. package/dist/portal/pool/zero-install.js +23 -1
  225. package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
  226. package/dist/portal/session/dev-server/dev-command.js +61 -15
  227. package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
  228. package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
  229. package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
  230. package/dist/portal/session/dev-server/project-ports.js +62 -0
  231. package/dist/portal/session/facts/departed-network.d.ts +66 -0
  232. package/dist/portal/session/facts/departed-network.js +142 -0
  233. package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
  234. package/dist/portal/session/no-session-diagnosis.js +67 -25
  235. package/dist/portal/session/no-session-next-action.d.ts +2 -0
  236. package/dist/portal/session/no-session-next-action.js +12 -3
  237. package/dist/portal/session/no-session-watch.d.ts +6 -0
  238. package/dist/portal/session/no-session-watch.js +38 -3
  239. package/dist/portal/session/serving-directory.d.ts +10 -0
  240. package/dist/portal/session/serving-directory.js +39 -8
  241. package/dist/portal/session/session-manager.d.ts +10 -0
  242. package/dist/portal/session/session-manager.js +174 -63
  243. package/dist/start-wiring.d.ts +2 -1
  244. package/dist/start-wiring.js +2 -1
  245. package/dist/surface/mcp/mcp.js +3 -2
  246. package/dist/surface/mcp/shared-params.d.ts +2 -0
  247. package/dist/surface/mcp/shared-params.js +31 -0
  248. package/dist/surface/tools/act/act-observation.d.ts +40 -2
  249. package/dist/surface/tools/act/act-observation.js +118 -12
  250. package/dist/surface/tools/act/act-preflight.js +1 -1
  251. package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
  252. package/dist/surface/tools/act/act-sequence-retry.js +61 -5
  253. package/dist/surface/tools/act/after-match.d.ts +4 -1
  254. package/dist/surface/tools/act/after-match.js +8 -1
  255. package/dist/surface/tools/act/already-true.js +1 -1
  256. package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
  257. package/dist/surface/tools/act/navigation/reload-result.js +25 -2
  258. package/dist/surface/tools/act/resolve-target.d.ts +15 -1
  259. package/dist/surface/tools/act/resolve-target.js +24 -1
  260. package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
  261. package/dist/surface/tools/act/sequence-grade.js +37 -26
  262. package/dist/surface/tools/act/settle-in-flight.js +27 -15
  263. package/dist/surface/tools/act-command.d.ts +23 -0
  264. package/dist/surface/tools/act-command.js +50 -0
  265. package/dist/surface/tools/act-sequence-tool.js +78 -50
  266. package/dist/surface/tools/act-tools.d.ts +1 -13
  267. package/dist/surface/tools/act-tools.js +74 -86
  268. package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
  269. package/dist/surface/tools/agent/build-staleness.js +1 -1
  270. package/dist/surface/tools/assert/assert-verdict.js +16 -37
  271. package/dist/surface/tools/browser-tools.js +12 -6
  272. package/dist/surface/tools/compare-tools.d.ts +11 -0
  273. package/dist/surface/tools/compare-tools.js +94 -0
  274. package/dist/surface/tools/coverage-tools.js +1 -1
  275. package/dist/surface/tools/dynamic-tools.js +9 -2
  276. package/dist/surface/tools/error-recovery.d.ts +12 -1
  277. package/dist/surface/tools/error-recovery.js +91 -21
  278. package/dist/surface/tools/explore-tools.js +1 -1
  279. package/dist/surface/tools/invoke-tool.js +3 -2
  280. package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
  281. package/dist/surface/tools/lease-coverage-tools.js +128 -0
  282. package/dist/surface/tools/lease-hint.d.ts +35 -1
  283. package/dist/surface/tools/lease-hint.js +42 -0
  284. package/dist/surface/tools/lease-readiness.d.ts +26 -0
  285. package/dist/surface/tools/lease-readiness.js +79 -0
  286. package/dist/surface/tools/lease-seed.d.ts +14 -1
  287. package/dist/surface/tools/lease-seed.js +35 -3
  288. package/dist/surface/tools/lease-tools.d.ts +3 -41
  289. package/dist/surface/tools/lease-tools.js +62 -110
  290. package/dist/surface/tools/lease-url-identity.d.ts +46 -0
  291. package/dist/surface/tools/lease-url-identity.js +97 -0
  292. package/dist/surface/tools/merge-tools.js +99 -2
  293. package/dist/surface/tools/merged-name-redirect.js +5 -1
  294. package/dist/surface/tools/observe-tools.js +15 -9
  295. package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
  296. package/dist/surface/tools/read/console-pre-attach.js +23 -0
  297. package/dist/surface/tools/read-tools.js +33 -30
  298. package/dist/surface/tools/real-input-attempt.d.ts +0 -8
  299. package/dist/surface/tools/real-input-attempt.js +143 -28
  300. package/dist/surface/tools/session-ports.d.ts +14 -0
  301. package/dist/surface/tools/session-ports.js +53 -0
  302. package/dist/surface/tools/tool-kit.d.ts +6 -0
  303. package/dist/surface/tools/tool-kit.js +2 -3
  304. package/dist/surface/tools/tool-surface.js +0 -1
  305. package/dist/surface/tools/tools.js +28 -5
  306. package/dist/surface/tools/type-spelling.d.ts +13 -0
  307. package/dist/surface/tools/type-spelling.js +36 -0
  308. package/dist/telemetry/agent-client.d.ts +6 -0
  309. package/dist/telemetry/agent-client.js +10 -0
  310. package/dist/telemetry/argument-shape.js +1 -0
  311. package/dist/telemetry/tool-refused.d.ts +6 -3
  312. package/dist/telemetry/tool-refused.js +8 -3
  313. package/docs/actions.mdx +3 -3
  314. package/docs/agent-cheatsheet.md +1 -1
  315. package/docs/capabilities.mdx +61 -1
  316. package/docs/cli/affected.mdx +9 -7
  317. package/docs/cli/cloud.mdx +3 -3
  318. package/docs/cli/compare.mdx +94 -0
  319. package/docs/cli/doctor.mdx +1 -1
  320. package/docs/cli/grade.mdx +97 -0
  321. package/docs/cli/license.mdx +9 -7
  322. package/docs/cli/restart.mdx +7 -5
  323. package/docs/cli/rollback.mdx +5 -5
  324. package/docs/cli/stop.mdx +3 -3
  325. package/docs/cli/update.mdx +2 -2
  326. package/docs/cli/verify.mdx +23 -1
  327. package/docs/cli.mdx +33 -23
  328. package/docs/deploy-checks.md +1 -1
  329. package/docs/desktop.mdx +1 -1
  330. package/docs/enterprise.md +2 -2
  331. package/docs/environment.mdx +1 -1
  332. package/docs/flows.md +11 -1
  333. package/docs/frameworks.mdx +9 -9
  334. package/docs/getting-started.md +1 -1
  335. package/docs/install-manual.mdx +1 -1
  336. package/docs/license-activation.mdx +3 -3
  337. package/docs/packages/tauri.mdx +1 -1
  338. package/docs/packages.mdx +1 -1
  339. package/docs/predicates.mdx +1 -1
  340. package/docs/quickstart.mdx +68 -49
  341. package/docs/skill-setup.md +1 -1
  342. package/docs/skill.md +14 -13
  343. package/docs/telemetry.md +2 -2
  344. package/docs/tools/navigate.mdx +1 -1
  345. package/docs/tools/verify.mdx +4 -1
  346. package/docs/troubleshooting.mdx +1 -1
  347. package/docs/usage.md +17 -14
  348. package/package.json +13 -13
  349. package/dist/features/exhaust/exhaust-run.d.ts +0 -33
  350. package/dist/features/exhaust/exhaust-run.js +0 -82
@@ -1,5 +1,5 @@
1
1
  import { AnchorKind, DriftReason, ReticleCommand, QueryBy, asRecord, asString, } from '@reticlehq/core';
2
- import { queryRefs } from './replay.js';
2
+ import { namelessRefs, queryRefs } from './replay.js';
3
3
  /**
4
4
  * Finding a step's anchor again, and saying legibly why it could not be found.
5
5
  *
@@ -20,16 +20,18 @@ export function editDistance(a, b) {
20
20
  const cols = t.length + 1;
21
21
  const prev = new Array(cols);
22
22
  const curr = new Array(cols);
23
- for (let j = 0; j < cols; j++)
23
+ for (let j = 0; j < cols; j++) {
24
24
  prev[j] = j;
25
+ }
25
26
  for (let i = 1; i < rows; i++) {
26
27
  curr[0] = i;
27
28
  for (let j = 1; j < cols; j++) {
28
29
  const cost = s[i - 1] === t[j - 1] ? 0 : 1;
29
30
  curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
30
31
  }
31
- for (let j = 0; j < cols; j++)
32
+ for (let j = 0; j < cols; j++) {
32
33
  prev[j] = curr[j] ?? 0;
34
+ }
33
35
  }
34
36
  return prev[cols - 1] ?? 0;
35
37
  }
@@ -58,8 +60,9 @@ export function nearestTestid(missing, present) {
58
60
  bestDistance = distance;
59
61
  }
60
62
  }
61
- if (null === best)
63
+ if (null === best) {
62
64
  return null;
65
+ }
63
66
  return bestDistance <= Math.max(missing.length, best.length) / 2 ? best : null;
64
67
  }
65
68
  /**
@@ -90,8 +93,9 @@ const ANCHOR_SETTLE_MAX_ATTEMPTS = 40;
90
93
  /** Extract the live element refs + the zero-match near-miss hint from a QUERY command result. */
91
94
  function readQuery(result) {
92
95
  const refs = queryRefs(result);
93
- if (!result.ok)
96
+ if (!result.ok) {
94
97
  return { refs };
98
+ }
95
99
  const payload = asRecord(result.result);
96
100
  const rawHint = payload['hint'];
97
101
  if ('object' === typeof rawHint && rawHint !== null) {
@@ -117,8 +121,9 @@ function readQuery(result) {
117
121
  * Such a drift is surfaced (with a nearest) but never auto-healed.
118
122
  */
119
123
  export function nearestIsAmbiguous(missing, present) {
120
- if (present.length < 2)
124
+ if (present.length < 2) {
121
125
  return false;
126
+ }
122
127
  let min = Number.POSITIVE_INFINITY;
123
128
  let count = 0;
124
129
  for (const candidate of present) {
@@ -142,8 +147,9 @@ export function testidDrift(value, hint) {
142
147
  anchor: value,
143
148
  nearest: nearestTestid(value, present),
144
149
  };
145
- if (nearestIsAmbiguous(value, present))
150
+ if (nearestIsAmbiguous(value, present)) {
146
151
  drift.ambiguous = true;
152
+ }
147
153
  return drift;
148
154
  }
149
155
  /**
@@ -159,6 +165,19 @@ export function testidDrift(value, hint) {
159
165
  * same string twice, so the `anchor` field and the sentence could drift apart with nothing to catch
160
166
  * it, and the next anchor kind would have had to remember the convention on its own.
161
167
  */
168
+ /**
169
+ * How a role anchor reads in a message: `button "View result"`, or for a control with no accessible
170
+ * name, `button (no accessible name, #2 of 3)` — never `button "undefined"`.
171
+ */
172
+ export function roleAnchorLabel(anchor) {
173
+ if (anchor.name !== undefined) {
174
+ return `${anchor.role} "${anchor.name}"`;
175
+ }
176
+ const at = anchor.nth !== undefined && anchor.of !== undefined
177
+ ? `, #${String(anchor.nth + 1)} of ${String(anchor.of)}`
178
+ : '';
179
+ return `${anchor.role} (no accessible name${at})`;
180
+ }
162
181
  function anchorAddress(anchor) {
163
182
  if (anchor.kind === AnchorKind.TESTID) {
164
183
  return { described: `testid "${anchor.value}"`, label: anchor.value };
@@ -166,7 +185,7 @@ function anchorAddress(anchor) {
166
185
  if (anchor.kind === AnchorKind.ROLE) {
167
186
  // Kind named explicitly: `button "View result"` alone reads as a testid that happens to contain
168
187
  // a space, and the reader would go looking for a locator nothing on the step declares.
169
- const label = `${anchor.role} "${String(anchor.name)}"`;
188
+ const label = roleAnchorLabel(anchor);
170
189
  return { described: `role anchor ${label}`, label };
171
190
  }
172
191
  // A component needs no such qualifier: `componentLabel` already answers
@@ -226,8 +245,9 @@ export function ambiguityDrift(anchor, refs) {
226
245
  * while the page still has exactly as many of them as when it was recorded. Otherwise undefined.
227
246
  */
228
247
  export function refFor(anchor, refs) {
229
- if (1 === refs.length)
248
+ if (1 === refs.length) {
230
249
  return refs[0];
250
+ }
231
251
  const { nth, of } = { ...anchor };
232
252
  return 'number' === typeof nth && of === refs.length ? refs[nth] : undefined;
233
253
  }
@@ -246,8 +266,9 @@ export function refFor(anchor, refs) {
246
266
  */
247
267
  export function reresolved(anchor, refs) {
248
268
  const drift = ambiguityDrift(anchor, refs);
249
- if (drift !== null)
269
+ if (drift !== null) {
250
270
  return { drift };
271
+ }
251
272
  const ref = refFor(anchor, refs);
252
273
  return ref === undefined ? undefined : { ref };
253
274
  }
@@ -268,8 +289,9 @@ export function expectElementDrift(value, hint) {
268
289
  anchor: value,
269
290
  nearest: nearestTestid(value, present),
270
291
  };
271
- if (nearestIsAmbiguous(value, present))
292
+ if (nearestIsAmbiguous(value, present)) {
272
293
  drift.ambiguous = true;
294
+ }
273
295
  return drift;
274
296
  }
275
297
  /**
@@ -296,8 +318,9 @@ async function settleTick(session, sleep) {
296
318
  await new Promise((resolve) => {
297
319
  let done = false;
298
320
  const finish = () => {
299
- if (done)
321
+ if (done) {
300
322
  return;
323
+ }
301
324
  done = true;
302
325
  resolve();
303
326
  };
@@ -313,15 +336,34 @@ async function settleTick(session, sleep) {
313
336
  export async function queryOnce(session, queryArgs) {
314
337
  return readQuery(await session.command(ReticleCommand.QUERY, queryArgs));
315
338
  }
316
- export async function resolveQuery(session, queryArgs, sleep, now = Date.now) {
317
- let last = readQuery(await session.command(ReticleCommand.QUERY, queryArgs));
339
+ export async function resolveQuery(session, queryArgs, sleep, now = Date.now, read = readQuery) {
340
+ return await resolveAnyQuery(session, [queryArgs], sleep, now, read);
341
+ }
342
+ /**
343
+ * Several queries for one control, pooled: each attempt runs every query and joins the refs, so
344
+ * the settle budget is spent once, not once for each query. A role anchor with `alsoRoles` uses it.
345
+ */
346
+ export async function resolveAnyQuery(session, queries, sleep, now = Date.now, read = readQuery) {
347
+ const look = async () => {
348
+ const reads = [];
349
+ for (const queryArgs of queries) {
350
+ reads.push(read(await session.command(ReticleCommand.QUERY, queryArgs)));
351
+ }
352
+ const hint = reads.find((read) => read.hint !== undefined)?.hint;
353
+ return { refs: reads.flatMap((read) => read.refs), ...(hint === undefined ? {} : { hint }) };
354
+ };
355
+ let last = await look();
318
356
  const deadline = now() + ANCHOR_SETTLE_BUDGET_MS;
319
357
  for (let attempt = 1; 0 === last.refs.length && now() < deadline && attempt < ANCHOR_SETTLE_MAX_ATTEMPTS; attempt += 1) {
320
358
  await settleTick(session, sleep);
321
- last = readQuery(await session.command(ReticleCommand.QUERY, queryArgs));
359
+ last = await look();
322
360
  }
323
361
  return last;
324
362
  }
363
+ /** `resolveQuery`'s reader for a nameless role anchor: only the matches with no accessible name. */
364
+ export function readNamelessQuery(result) {
365
+ return { refs: namelessRefs(result) };
366
+ }
325
367
  /** Re-resolve a testid anchor. */
326
368
  export function resolveTestid(session, value, sleep) {
327
369
  return resolveQuery(session, { by: QueryBy.TESTID, value }, sleep);
@@ -337,20 +379,83 @@ export function componentLabel(anchor) {
337
379
  }
338
380
  /** The value of a step's primary anchor, for labelling the result row. */
339
381
  export function anchorLabel(anchor) {
340
- if (anchor.kind === AnchorKind.TESTID)
382
+ if (anchor.kind === AnchorKind.TESTID) {
341
383
  return anchor.value;
342
- if (anchor.kind === AnchorKind.SIGNAL)
384
+ }
385
+ if (anchor.kind === AnchorKind.SIGNAL) {
343
386
  return anchor.name;
344
- if (anchor.kind === AnchorKind.COMPONENT)
387
+ }
388
+ if (anchor.kind === AnchorKind.COMPONENT) {
345
389
  return componentLabel(anchor);
390
+ }
346
391
  return anchor.name ?? anchor.role;
347
392
  }
348
393
  /** QUERY args for a component auto-anchor — source (precise) + component name (coarse) as given. */
349
394
  export function componentQueryArgs(anchor) {
350
395
  const args = { by: QueryBy.COMPONENT };
351
- if (anchor.component !== undefined)
396
+ if (anchor.component !== undefined) {
352
397
  args['component'] = anchor.component;
353
- if (anchor.source !== undefined)
398
+ }
399
+ if (anchor.source !== undefined) {
354
400
  args['source'] = anchor.source;
401
+ }
402
+ if (anchor.text !== undefined) {
403
+ args['text'] = anchor.text;
404
+ }
405
+ // An older SDK ignores a role on a component query and answers as before.
406
+ if (anchor.role !== undefined) {
407
+ args['role'] = anchor.role;
408
+ }
355
409
  return args;
356
410
  }
411
+ /** Query args for a NAMED role anchor — the handle that identifies an instance, not a JSX site. */
412
+ export function roleQueryArgs(anchor) {
413
+ return {
414
+ by: QueryBy.ROLE,
415
+ value: anchor.role,
416
+ ...(anchor.name === undefined ? {} : { name: anchor.name }),
417
+ };
418
+ }
419
+ /** One query for each role the anchor accepts: its own role, then `alsoRoles`. */
420
+ function roleQueries(anchor) {
421
+ return [anchor.role, ...(anchor.alsoRoles ?? [])].map((role) => roleQueryArgs({ ...anchor, role }));
422
+ }
423
+ /**
424
+ * Resolve a role anchor. ONE function for the first look and the re-resolve after a stale ref: the
425
+ * re-resolve used to count every control of the role, so a nameless anchor's position came back
426
+ * ambiguous on a working page.
427
+ *
428
+ * A nameless anchor's position counts only the nameless controls of its role (an icon-only edit
429
+ * button is the 2nd nameless button of its row, not the 5th button), so it reads only those.
430
+ */
431
+ export async function resolveRoleAnchor(session, anchor, sleep) {
432
+ // Every role the anchor accepts (`alsoRoles`), pooled in one settle loop; a nameless anchor keeps
433
+ // only the matches with no accessible name.
434
+ const found = await resolveAnyQuery(session, roleQueries(anchor), sleep, Date.now, anchor.name === undefined ? readNamelessQuery : undefined);
435
+ if (found.refs.length > 0 || anchor.name === undefined || anchor.within === undefined) {
436
+ return found;
437
+ }
438
+ const refs = await refsWithin(session, anchor.role, anchor.within);
439
+ return refs.length > 0 ? { refs } : found;
440
+ }
441
+ /**
442
+ * The controls of `role` inside the recorded container chain, for a named control whose name is
443
+ * gone (a correct build may word a button differently). The chain is walked outermost first, and
444
+ * each container must be the only one of its role in the one before. When a container is not
445
+ * unique, its matches come back, so the caller reports the step as ambiguous and does not act.
446
+ */
447
+ async function refsWithin(session, role, within) {
448
+ let scope;
449
+ for (const container of [...within].reverse()) {
450
+ const { refs } = await queryOnce(session, {
451
+ by: QueryBy.ROLE,
452
+ value: container,
453
+ ...(scope === undefined ? {} : { scope }),
454
+ });
455
+ if (1 !== refs.length) {
456
+ return refs;
457
+ }
458
+ scope = refs[0];
459
+ }
460
+ return (await queryOnce(session, { by: QueryBy.ROLE, value: role, scope })).refs;
461
+ }
@@ -19,6 +19,7 @@
19
19
  * Pure: no IO, no clock.
20
20
  */
21
21
  import type { FlowFile, FlowStep } from '@reticlehq/core';
22
+ import { z } from 'zod';
22
23
  export declare const FlowAssertionGrade: {
23
24
  /** At least one step (or the success end-condition) asserts a signal/network consequence. */
24
25
  readonly ASSERTED: "asserted";
@@ -45,9 +46,28 @@ export interface FlowAssertionClassification {
45
46
  * intent but only presence-only checks is the dangerous case — it claims a goal it cannot verify.
46
47
  */
47
48
  intentVerified: boolean;
49
+ /**
50
+ * How many checks of each predicate kind the flow runs, over every step, sub-step and the success
51
+ * state, composites flattened. Absent when the flow checks nothing. A presence-only grade with
52
+ * this list says WHAT is checked, so the grade does not read as "checks nothing" (#1840).
53
+ */
54
+ checks?: Record<string, number>;
48
55
  /** Present for presence-only / assertion-free flows: how to make the flow a real test. */
49
56
  warning?: string;
50
57
  }
58
+ /**
59
+ * The `assertions` block of the `reticle_flow_save` result. Declared here, next to the type it
60
+ * describes, because a validating profile removes a field that the output schema does not declare.
61
+ */
62
+ export declare const FLOW_ASSERTIONS_OUTPUT: z.ZodOptional<z.ZodObject<{
63
+ grade: z.ZodString;
64
+ hasConsequenceAssertion: z.ZodBoolean;
65
+ totalSteps: z.ZodNumber;
66
+ consequenceSteps: z.ZodNumber;
67
+ weakSteps: z.ZodNumber;
68
+ checks: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
69
+ warning: z.ZodOptional<z.ZodString>;
70
+ }, z.core.$strip>>;
51
71
  /**
52
72
  * A step expect counts the same as a flow-level one, because replay now evaluates the same set.
53
73
  *
@@ -18,7 +18,8 @@
18
18
  *
19
19
  * Pure: no IO, no clock.
20
20
  */
21
- import { flowExpectHasConsequence, flowExpectIsPresenceOnly } from '@reticlehq/core';
21
+ import { PredicateKind, flowExpectHasConsequence, flowExpectIsPresenceOnly } from '@reticlehq/core';
22
+ import { z } from 'zod';
22
23
  export const FlowAssertionGrade = {
23
24
  /** At least one step (or the success end-condition) asserts a signal/network consequence. */
24
25
  ASSERTED: 'asserted',
@@ -27,8 +28,32 @@ export const FlowAssertionGrade = {
27
28
  /** Performs actions but asserts nothing observable — passes even if the feature is broken. */
28
29
  ASSERTION_FREE: 'assertion-free',
29
30
  };
31
+ /**
32
+ * The `assertions` block of the `reticle_flow_save` result. Declared here, next to the type it
33
+ * describes, because a validating profile removes a field that the output schema does not declare.
34
+ */
35
+ export const FLOW_ASSERTIONS_OUTPUT = z
36
+ .object({
37
+ grade: z.string().describe('asserted | presence-only | assertion-free'),
38
+ hasConsequenceAssertion: z.boolean(),
39
+ totalSteps: z.number(),
40
+ consequenceSteps: z.number(),
41
+ weakSteps: z.number(),
42
+ checks: z
43
+ .record(z.string(), z.number())
44
+ .optional()
45
+ .describe('Checks of each predicate kind (text, element, route, net, signal, …) the flow runs.'),
46
+ warning: z.string().optional(),
47
+ })
48
+ .optional();
30
49
  const ASSERTION_FREE_WARNING = 'This flow performs actions but asserts no observable consequence, so it will pass even if the feature is broken. Add a consequence assertion with reticle_annotate (assert-signal / assert-net) or a success-state.';
31
- const PRESENCE_ONLY_WARNING = 'This flow only checks element presence, not an observable consequence (signal/network). A locator healed to the wrong element can still pass it. Add a consequence assertion (assert-signal / assert-net / success-state).';
50
+ /** Why a presence check is weaker than a consequence, and the fix. Shared by both presence notes. */
51
+ const PRESENCE_ONLY_ADVICE = 'A locator healed to the wrong element can still pass it. Add a consequence assertion (assert-signal / assert-net / success-state).';
52
+ const PRESENCE_ONLY_WARNING = `This flow only checks element presence, not an observable consequence (signal/network). ${PRESENCE_ONLY_ADVICE}`;
53
+ /** The presence note when the checks are known: it names them, then says why they are weak. */
54
+ const presenceOnlyWarning = (checks) => `This flow checks ${checks} on the page. These are presence checks, not an observable consequence (signal/network). ${PRESENCE_ONLY_ADVICE}`;
55
+ /** The intent note when the flow has presence checks: it names them, then says what is not checked (#1801). */
56
+ const intentWithPresenceWarning = (checks) => `This flow declares a business intent and checks ${checks} on the page, but no step checks a request, a signal or app state. A change that keeps the page as it is but breaks the work behind it still passes. Add a success-state consequence so the flow fails when the goal stops being met.`;
32
57
  const INTENT_WITHOUT_OUTCOME_WARNING = 'This flow declares a business intent but asserts no observable outcome (signal/network) — it claims to verify a goal it cannot actually check. Add a success-state consequence so the flow fails when the goal stops being met.';
33
58
  // Consequence vs presence-only classification lives in @reticlehq/core (one source for all graders).
34
59
  const expectIsConsequence = flowExpectHasConsequence;
@@ -50,8 +75,9 @@ export function flattenSteps(steps) {
50
75
  const out = [];
51
76
  for (const s of steps) {
52
77
  out.push(s);
53
- if (s.steps !== undefined)
78
+ if (s.steps !== undefined) {
54
79
  out.push(...flattenSteps(s.steps));
80
+ }
55
81
  }
56
82
  return out;
57
83
  }
@@ -72,24 +98,65 @@ export function flattenSteps(steps) {
72
98
  * having run — grading is reached from save, which is exactly where a bad composite arrives first.
73
99
  */
74
100
  function invokedAssertsConsequence(flow, invoked, seen) {
75
- if (seen.has(flow.name))
101
+ if (seen.has(flow.name)) {
76
102
  return false;
103
+ }
77
104
  const deeper = new Set([...seen, flow.name]);
78
105
  for (const step of flattenSteps(flow.steps)) {
79
- if (step.invoke === undefined)
106
+ if (step.invoke === undefined) {
80
107
  continue;
108
+ }
81
109
  const sub = invoked.get(step.invoke);
82
- if (sub === undefined)
110
+ if (sub === undefined) {
83
111
  continue;
112
+ }
84
113
  const subSteps = flattenSteps(sub.steps);
85
114
  if (subSteps.some((s) => expectIsConsequence(s.expect)) || expectIsConsequence(sub.success)) {
86
115
  return true;
87
116
  }
88
- if (invokedAssertsConsequence(sub, invoked, deeper))
117
+ if (invokedAssertsConsequence(sub, invoked, deeper)) {
89
118
  return true;
119
+ }
90
120
  }
91
121
  return false;
92
122
  }
123
+ /** Every leaf clause of an expectation, composites flattened. */
124
+ function leafClauses(expect) {
125
+ if (expect === undefined) {
126
+ return [];
127
+ }
128
+ if (PredicateKind.ALL_OF === expect.kind || PredicateKind.ANY_OF === expect.kind) {
129
+ return expect.predicates.flatMap(leafClauses);
130
+ }
131
+ if (PredicateKind.NOT === expect.kind) {
132
+ return leafClauses(expect.predicate);
133
+ }
134
+ return [expect];
135
+ }
136
+ /** Checks of each kind over the given expectations, keys in name order, or undefined for none. */
137
+ function countChecks(expects) {
138
+ const counts = new Map();
139
+ for (const clause of expects.flatMap(leafClauses)) {
140
+ counts.set(clause.kind, (counts.get(clause.kind) ?? 0) + 1);
141
+ }
142
+ if (0 === counts.size) {
143
+ return undefined;
144
+ }
145
+ return Object.fromEntries([...counts.entries()].sort(([a], [b]) => a.localeCompare(b)));
146
+ }
147
+ /** "text (3), element (1) and route (1)": most checks first, ties in name order. */
148
+ function describeChecks(checks) {
149
+ const parts = Object.entries(checks)
150
+ .sort(([a, x], [b, y]) => y - x || a.localeCompare(b))
151
+ .map(([kind, n]) => `${kind} (${String(n)})`);
152
+ const last = parts.pop() ?? '';
153
+ return 0 === parts.length ? last : `${parts.join(', ')} and ${last}`;
154
+ }
155
+ function responseIsDeclared(step) {
156
+ const r = step.response;
157
+ return (r !== undefined &&
158
+ (r.status !== undefined || r.contains !== undefined || r.absent !== undefined));
159
+ }
93
160
  export function classifyFlowAssertions(flow,
94
161
  /**
95
162
  * The flows this one invokes, already loaded.
@@ -103,16 +170,20 @@ invoked = new Map()) {
103
170
  let consequenceSteps = 0;
104
171
  let weakSteps = 0;
105
172
  for (const s of all) {
106
- if (expectIsConsequence(s.expect))
173
+ // A request step's declared response judges what the server answered: a consequence.
174
+ if (expectIsConsequence(s.expect) || responseIsDeclared(s)) {
107
175
  consequenceSteps++;
108
- else if (expectIsWeak(s.expect))
176
+ }
177
+ else if (expectIsWeak(s.expect)) {
109
178
  weakSteps++;
179
+ }
110
180
  }
111
181
  const invokedAsserts = invokedAssertsConsequence(flow, invoked, new Set());
112
182
  const successIsConsequence = expectIsConsequence(flow.success);
113
183
  const successIsWeak = expectIsWeak(flow.success);
114
184
  const hasConsequenceAssertion = consequenceSteps > 0 || successIsConsequence || invokedAsserts;
115
185
  const hasAnyAssertion = hasConsequenceAssertion || weakSteps > 0 || successIsWeak;
186
+ const checks = countChecks([...all.map((s) => s.expect), flow.success]);
116
187
  const intent = flow.intent;
117
188
  const intentVerified = intent !== undefined && hasConsequenceAssertion;
118
189
  let grade;
@@ -122,7 +193,9 @@ invoked = new Map()) {
122
193
  }
123
194
  else if (hasAnyAssertion) {
124
195
  grade = FlowAssertionGrade.PRESENCE_ONLY;
125
- warning = PRESENCE_ONLY_WARNING;
196
+ // What it checks first, so the grade does not read as "checks nothing" (#1840).
197
+ warning =
198
+ checks === undefined ? PRESENCE_ONLY_WARNING : presenceOnlyWarning(describeChecks(checks));
126
199
  }
127
200
  else {
128
201
  grade = FlowAssertionGrade.ASSERTION_FREE;
@@ -130,8 +203,12 @@ invoked = new Map()) {
130
203
  }
131
204
  // A declared-but-unverifiable business goal is the sharper failure: surface it over the generic
132
205
  // assertion warning, since the flow actively claims to check something it cannot.
133
- if (intent !== undefined && !hasConsequenceAssertion)
134
- warning = INTENT_WITHOUT_OUTCOME_WARNING;
206
+ if (intent !== undefined && !hasConsequenceAssertion) {
207
+ warning =
208
+ checks === undefined
209
+ ? INTENT_WITHOUT_OUTCOME_WARNING
210
+ : intentWithPresenceWarning(describeChecks(checks));
211
+ }
135
212
  return {
136
213
  grade,
137
214
  hasConsequenceAssertion,
@@ -139,6 +216,7 @@ invoked = new Map()) {
139
216
  consequenceSteps,
140
217
  weakSteps,
141
218
  successIsConsequence,
219
+ ...(checks === undefined ? {} : { checks }),
142
220
  ...(intent !== undefined ? { intent } : {}),
143
221
  intentVerified,
144
222
  ...(warning !== undefined ? { warning } : {}),
@@ -1,4 +1,10 @@
1
- import type { Contradiction, FlowStep, FlowStepResult, Predicate } from '@reticlehq/core';
1
+ import { type Contradiction, type FlowStep, type FlowStepResult, type Predicate } from '@reticlehq/core';
2
+ /** True for a finding that only means something inside one action's window (see ONE_ACTION_ONLY). */
3
+ export declare function isOneActionOnly(found: {
4
+ kind: string;
5
+ }): boolean;
6
+ /** A step's own findings, without the ones only the end of the replay can decide. */
7
+ export declare function stepContradictions(found: readonly Contradiction[]): Contradiction[];
2
8
  /** Cross-step findings belong to no single step; -1 is the address the suite verdict already uses. */
3
9
  export declare const CROSS_STEP_INDEX = -1;
4
10
  /**
@@ -1,8 +1,32 @@
1
+ import { ContradictionKind, } from '@reticlehq/core';
1
2
  import { flattenSteps } from './flow-classify.js';
2
3
  /** A contradiction's identity for de-duplication: the rule that fired, and the evidence it fired on. */
3
4
  function contradictionId(found) {
4
5
  return `${found.kind}|${found.detail}`;
5
6
  }
7
+ /**
8
+ * Kinds that only mean something inside ONE action's window. A duplicate request claims "one user
9
+ * action was performed"; across a whole flow there are many actions, so a toggle hit once per step
10
+ * (Pause, then Resume) read as a double submit (#2013). Each step already checks its own window.
11
+ */
12
+ const ONE_ACTION_ONLY = new Set([
13
+ ContradictionKind.DUPLICATE_REQUEST,
14
+ ContradictionKind.DUPLICATE_REQUEST_UNRELATED,
15
+ ]);
16
+ /** True for a finding that only means something inside one action's window (see ONE_ACTION_ONLY). */
17
+ export function isOneActionOnly(found) {
18
+ return ONE_ACTION_ONLY.has(found.kind);
19
+ }
20
+ /**
21
+ * Kinds a step window cannot decide. "Never settled" means never, and a step's window closes while
22
+ * the replay still runs: a request a step's predecessor started late can finish a moment after this
23
+ * window ends. The whole-span pass, run when the replay is over, sees whether it ever came back.
24
+ */
25
+ const DECIDED_AT_THE_END = new Set([ContradictionKind.REQUEST_NEVER_SETTLED]);
26
+ /** A step's own findings, without the ones only the end of the replay can decide. */
27
+ export function stepContradictions(found) {
28
+ return found.filter((c) => !DECIDED_AT_THE_END.has(c.kind));
29
+ }
6
30
  /** Cross-step findings belong to no single step; -1 is the address the suite verdict already uses. */
7
31
  export const CROSS_STEP_INDEX = -1;
8
32
  /**
@@ -18,7 +42,7 @@ export const CROSS_STEP_INDEX = -1;
18
42
  */
19
43
  export function crossStepOnly(whole, steps) {
20
44
  const seen = new Set(steps.flatMap((step) => (step.contradictions ?? []).map(contradictionId)));
21
- return whole.filter((found) => !seen.has(contradictionId(found)));
45
+ return whole.filter((found) => !seen.has(contradictionId(found)) && !isOneActionOnly(found));
22
46
  }
23
47
  /**
24
48
  * Every consequence the flow declared, sub-steps included. The whole-span pass judges the same
@@ -36,12 +36,37 @@ export declare function routeClaims(startPath: string | undefined, steps: readon
36
36
  /**
37
37
  * The preconditions only another flow can establish.
38
38
  *
39
- * A route claim on the page the flow starts on is not one of them: replay navigates there itself, so
40
- * it neither opts the flow out of the reset nor gates the run. That is the claim a saved drive writes
41
- * for the map (see routeClaims) — reading it as "starts from leftover state" would stop every
42
- * auto-saved flow from ever being reset.
39
+ * A route claim with a `pathname` is not one of them: replay navigates to a declared route itself
40
+ * (see `declaredRoute`), and checks the claim, its `contains` too, once the tab is there (see
41
+ * `unmetRouteClaim`). So a route claim neither opts the flow out of the reset nor gates the run
42
+ * here. The claim on the page the flow starts on is the one a saved drive writes for the map (see
43
+ * routeClaims) — reading it as "starts from leftover state" would stop every auto-saved flow from
44
+ * ever being reset. A route claim on ANOTHER page was read that way, and a suite then ran the flow
45
+ * on whatever page the previous flow left open (#2034).
43
46
  */
44
47
  export declare function establishedState(flow: FlowFile): readonly Predicate[];
48
+ /**
49
+ * The route a flow declares in `requires` when that says more than its `startPath`: the first
50
+ * such claim, in any order of the claims. Undefined when there is none, and replay then arrives
51
+ * at `startPath` as before. Replay puts the tab on this route before step 1 (#2034).
52
+ */
53
+ export declare function declaredRoute(flow: FlowFile): string | undefined;
54
+ /**
55
+ * The first declared route claim that the page `observed` does not satisfy, said for a reader.
56
+ * Undefined when they all hold. Each claim is checked whole: its `pathname` by `onRoute`, and its
57
+ * `contains` against the whole route, as the `route` predicate checks it.
58
+ */
59
+ export declare function unmetRouteClaim(flow: FlowFile, observed: string): string | undefined;
60
+ /** Whether a declared route names an entity id with a `:name` segment, for example `/cases/:id/notes`. */
61
+ export declare function isRoutePattern(route: string): boolean;
62
+ /**
63
+ * Is the tab on a declared route?
64
+ *
65
+ * A plain route is compared as `samePath` compares a `startPath`. A pattern is compared segment by
66
+ * segment on the router's path, and a `:name` segment matches any one non-empty segment. The query
67
+ * is not compared for a pattern: the pattern says which page, and the id says which record.
68
+ */
69
+ export declare function onRoute(observed: string, route: string): boolean;
45
70
  /**
46
71
  * Is the tab where the flow asked to start? Up to a trailing slash, and up to the query the flow
47
72
  * did not ask about.
@@ -66,3 +91,12 @@ export declare function samePath(observed: string, expected: string): boolean;
66
91
  * consequence can also wait 15 s for its page.
67
92
  */
68
93
  export declare function startPathBudgetMs(flow: FlowFile): number;
94
+ /**
95
+ * A pattern route with its `:name` segments filled from the page `observed`.
96
+ *
97
+ * A segment is taken only when every segment before it in the route agrees with the page: from
98
+ * `/cases/c-42/files`, the route `/cases/:id/notes` becomes `/cases/c-42/notes`. So another section
99
+ * of the same case keeps the case, and the recorded id, which can be a deleted record, is not used.
100
+ * Undefined when the page has no id to give (`/settings`), or the route has no `:name` segment.
101
+ */
102
+ export declare function fillRouteParams(route: string, observed: string): string | undefined;