waygraph 0.13.7 → 0.15.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 (358) hide show
  1. package/README.md +970 -232
  2. package/assets/stubs/stub.mp4 +0 -0
  3. package/assets/stubs/stub.pdf +32 -0
  4. package/assets/stubs/stub.png +0 -0
  5. package/dist/agent-dive.d.ts +9 -0
  6. package/dist/agent-dive.js +32 -0
  7. package/dist/auto-explore-run.d.ts +20 -2
  8. package/dist/auto-explore-run.js +9 -8
  9. package/dist/auto-explore.js +22 -2
  10. package/dist/auto-session-ipc.d.ts +123 -0
  11. package/dist/auto-session-ipc.js +381 -0
  12. package/dist/auto-session.d.ts +342 -0
  13. package/dist/auto-session.js +760 -0
  14. package/dist/cli.js +450 -51
  15. package/dist/engine.d.ts +201 -1
  16. package/dist/engine.js +312 -2
  17. package/dist/graph.d.ts +18 -0
  18. package/dist/graph.js +65 -2
  19. package/dist/highlights.d.ts +20 -1
  20. package/dist/highlights.js +60 -0
  21. package/dist/index.d.ts +8 -4
  22. package/dist/index.js +5 -3
  23. package/dist/map-check.d.ts +18 -0
  24. package/dist/map-check.js +140 -0
  25. package/dist/pilot-overlay.d.ts +88 -0
  26. package/dist/pilot-overlay.js +363 -0
  27. package/dist/pilot.d.ts +49 -0
  28. package/dist/pilot.js +53 -0
  29. package/dist/trait.d.ts +22 -0
  30. package/dist/trait.js +60 -3
  31. package/dist/types.d.ts +21 -1
  32. package/dist/types.js +17 -1
  33. package/examples/routed-demo/.waygraph-auto/4d4902b7.log +0 -0
  34. package/examples/routed-demo/.waygraph-auto/51ef462e.log +0 -0
  35. package/examples/routed-demo/.waygraph-auto/71527ad9.log +0 -0
  36. package/examples/routed-demo/.waygraph-auto/ca16778e.log +0 -0
  37. package/examples/routed-demo/package.json +26 -0
  38. package/examples/routed-demo/playwright.config.ts +20 -0
  39. package/examples/routed-demo/scripts/fixture-server.mjs +37 -0
  40. package/examples/routed-demo/scripts/with-fixture.mjs +70 -0
  41. package/examples/routed-demo/src/flows/routed-demo-map.flow.ts +39 -0
  42. package/examples/routed-demo/src/flows/routed-demo.flow.ts +12 -0
  43. package/examples/routed-demo/src/map/(app_base)/dashboard/_methods/click-widget.block.ts +23 -0
  44. package/examples/routed-demo/src/map/(app_base)/dashboard/_nav.block.ts +18 -0
  45. package/examples/routed-demo/src/map/(app_base)/dashboard/_page.block.ts +22 -0
  46. package/examples/routed-demo/src/map/(app_base)/dashboard/_sel.ts +10 -0
  47. package/examples/routed-demo/src/map/(external)/docs/_nav.block.ts +21 -0
  48. package/examples/routed-demo/src/map/(external)/docs/_page.block.ts +15 -0
  49. package/examples/routed-demo/src/map/(external)/docs/_sel.ts +7 -0
  50. package/examples/routed-demo/src/pages/dashboard.html +10 -0
  51. package/examples/routed-demo/src/pages/docs.html +8 -0
  52. package/examples/routed-demo/src/states/routed-demo.states.ts +10 -0
  53. package/examples/routed-demo/test-results/.last-run.json +4 -0
  54. package/examples/routed-demo/tests/routed-demo-map.spec.ts +17 -0
  55. package/examples/routed-demo/tests/routed-demo.spec.ts +75 -0
  56. package/examples/routed-demo/tsconfig.json +18 -0
  57. package/examples/routed-demo-consumer/package.json +18 -0
  58. package/examples/saucedemo/.waygraph-auto/00d5336e.log +0 -0
  59. package/examples/saucedemo/.waygraph-auto/010dd92b.log +0 -0
  60. package/examples/saucedemo/.waygraph-auto/011ad671.log +0 -0
  61. package/examples/saucedemo/.waygraph-auto/01b45645.log +0 -0
  62. package/examples/saucedemo/.waygraph-auto/03764153.log +0 -0
  63. package/examples/saucedemo/.waygraph-auto/0593587e.log +0 -0
  64. package/examples/saucedemo/.waygraph-auto/06d12823.log +0 -0
  65. package/examples/saucedemo/.waygraph-auto/083cc5b9.log +0 -0
  66. package/examples/saucedemo/.waygraph-auto/0b1f5b26.json +8 -0
  67. package/examples/saucedemo/.waygraph-auto/0c00f25f.log +0 -0
  68. package/examples/saucedemo/.waygraph-auto/0e480591.log +0 -0
  69. package/examples/saucedemo/.waygraph-auto/104751ef.log +0 -0
  70. package/examples/saucedemo/.waygraph-auto/110958b8.log +0 -0
  71. package/examples/saucedemo/.waygraph-auto/138107c1.log +0 -0
  72. package/examples/saucedemo/.waygraph-auto/159d541b.log +0 -0
  73. package/examples/saucedemo/.waygraph-auto/16aa2c01.log +0 -0
  74. package/examples/saucedemo/.waygraph-auto/16c35dd5.log +0 -0
  75. package/examples/saucedemo/.waygraph-auto/1706547d.log +0 -0
  76. package/examples/saucedemo/.waygraph-auto/17537e42.log +0 -0
  77. package/examples/saucedemo/.waygraph-auto/175655f7.log +0 -0
  78. package/examples/saucedemo/.waygraph-auto/17ede493.log +0 -0
  79. package/examples/saucedemo/.waygraph-auto/1fe2414f.log +0 -0
  80. package/examples/saucedemo/.waygraph-auto/1fe2e3b8.log +0 -0
  81. package/examples/saucedemo/.waygraph-auto/2219f00b.log +0 -0
  82. package/examples/saucedemo/.waygraph-auto/23e62571.log +0 -0
  83. package/examples/saucedemo/.waygraph-auto/25f68ada.log +0 -0
  84. package/examples/saucedemo/.waygraph-auto/26ca9cd8.log +0 -0
  85. package/examples/saucedemo/.waygraph-auto/2bb6c4e8.log +0 -0
  86. package/examples/saucedemo/.waygraph-auto/2d425004.log +0 -0
  87. package/examples/saucedemo/.waygraph-auto/2f5b3ea6.log +0 -0
  88. package/examples/saucedemo/.waygraph-auto/31bb303d.log +0 -0
  89. package/examples/saucedemo/.waygraph-auto/31f2042f.log +0 -0
  90. package/examples/saucedemo/.waygraph-auto/36551ec1.log +0 -0
  91. package/examples/saucedemo/.waygraph-auto/36a1c274.log +0 -0
  92. package/examples/saucedemo/.waygraph-auto/381aa338.log +0 -0
  93. package/examples/saucedemo/.waygraph-auto/3882bce1.log +0 -0
  94. package/examples/saucedemo/.waygraph-auto/38acd1dc.log +0 -0
  95. package/examples/saucedemo/.waygraph-auto/3b8b77b9.log +0 -0
  96. package/examples/saucedemo/.waygraph-auto/4028d2a2.log +0 -0
  97. package/examples/saucedemo/.waygraph-auto/4168b208.log +0 -0
  98. package/examples/saucedemo/.waygraph-auto/42cc9113.log +0 -0
  99. package/examples/saucedemo/.waygraph-auto/439d30aa.log +0 -0
  100. package/examples/saucedemo/.waygraph-auto/43ab66ce.log +0 -0
  101. package/examples/saucedemo/.waygraph-auto/43f66838.log +0 -0
  102. package/examples/saucedemo/.waygraph-auto/442171d4.log +0 -0
  103. package/examples/saucedemo/.waygraph-auto/453b1ce1.log +0 -0
  104. package/examples/saucedemo/.waygraph-auto/457322bd.log +0 -0
  105. package/examples/saucedemo/.waygraph-auto/46197027.log +0 -0
  106. package/examples/saucedemo/.waygraph-auto/46889468.log +0 -0
  107. package/examples/saucedemo/.waygraph-auto/4890da1c.log +0 -0
  108. package/examples/saucedemo/.waygraph-auto/4893a63b.log +0 -0
  109. package/examples/saucedemo/.waygraph-auto/4a85f496.log +0 -0
  110. package/examples/saucedemo/.waygraph-auto/4af71440.log +0 -0
  111. package/examples/saucedemo/.waygraph-auto/4f7496d1.log +0 -0
  112. package/examples/saucedemo/.waygraph-auto/50e8baaa.log +0 -0
  113. package/examples/saucedemo/.waygraph-auto/52b3a455.log +0 -0
  114. package/examples/saucedemo/.waygraph-auto/532dd9b8.log +0 -0
  115. package/examples/saucedemo/.waygraph-auto/5391ce40.log +0 -0
  116. package/examples/saucedemo/.waygraph-auto/57dbddb3.log +0 -0
  117. package/examples/saucedemo/.waygraph-auto/5897f501.log +0 -0
  118. package/examples/saucedemo/.waygraph-auto/59c0b57d.log +0 -0
  119. package/examples/saucedemo/.waygraph-auto/5b511794.log +0 -0
  120. package/examples/saucedemo/.waygraph-auto/5c0eef7f.log +0 -0
  121. package/examples/saucedemo/.waygraph-auto/5c47910f.log +0 -0
  122. package/examples/saucedemo/.waygraph-auto/5d60da77.log +0 -0
  123. package/examples/saucedemo/.waygraph-auto/5eda8273.log +0 -0
  124. package/examples/saucedemo/.waygraph-auto/5fc3a33b.log +0 -0
  125. package/examples/saucedemo/.waygraph-auto/61b6546d.log +0 -0
  126. package/examples/saucedemo/.waygraph-auto/621518ad.log +0 -0
  127. package/examples/saucedemo/.waygraph-auto/632b7118.log +0 -0
  128. package/examples/saucedemo/.waygraph-auto/64314c2b.log +0 -0
  129. package/examples/saucedemo/.waygraph-auto/650ed066.log +0 -0
  130. package/examples/saucedemo/.waygraph-auto/66516213.log +0 -0
  131. package/examples/saucedemo/.waygraph-auto/66c17079.log +0 -0
  132. package/examples/saucedemo/.waygraph-auto/69bf0b80.log +0 -0
  133. package/examples/saucedemo/.waygraph-auto/69c2347a.log +0 -0
  134. package/examples/saucedemo/.waygraph-auto/6d22bdb0.log +0 -0
  135. package/examples/saucedemo/.waygraph-auto/6d9aaad3.log +0 -0
  136. package/examples/saucedemo/.waygraph-auto/7103bebb.log +0 -0
  137. package/examples/saucedemo/.waygraph-auto/740c9faa.log +0 -0
  138. package/examples/saucedemo/.waygraph-auto/74872fc5.log +0 -0
  139. package/examples/saucedemo/.waygraph-auto/75846ca9.log +0 -0
  140. package/examples/saucedemo/.waygraph-auto/7626c85b.log +0 -0
  141. package/examples/saucedemo/.waygraph-auto/76308eec.log +0 -0
  142. package/examples/saucedemo/.waygraph-auto/76c1650e.log +0 -0
  143. package/examples/saucedemo/.waygraph-auto/7861f38f.log +0 -0
  144. package/examples/saucedemo/.waygraph-auto/78b21219.log +0 -0
  145. package/examples/saucedemo/.waygraph-auto/7af224ed.log +0 -0
  146. package/examples/saucedemo/.waygraph-auto/7c3be585.log +0 -0
  147. package/examples/saucedemo/.waygraph-auto/7ed55a2c.log +0 -0
  148. package/examples/saucedemo/.waygraph-auto/80086ba2.log +0 -0
  149. package/examples/saucedemo/.waygraph-auto/80206369.log +0 -0
  150. package/examples/saucedemo/.waygraph-auto/847f1092.log +0 -0
  151. package/examples/saucedemo/.waygraph-auto/84d6d293.log +0 -0
  152. package/examples/saucedemo/.waygraph-auto/8581abfa.log +0 -0
  153. package/examples/saucedemo/.waygraph-auto/87b7e0fd.log +0 -0
  154. package/examples/saucedemo/.waygraph-auto/87c79256.log +0 -0
  155. package/examples/saucedemo/.waygraph-auto/88af2495.log +0 -0
  156. package/examples/saucedemo/.waygraph-auto/8a3eb088.log +0 -0
  157. package/examples/saucedemo/.waygraph-auto/8c04a403.log +0 -0
  158. package/examples/saucedemo/.waygraph-auto/8c9ab09e.log +0 -0
  159. package/examples/saucedemo/.waygraph-auto/8cc5db51.log +0 -0
  160. package/examples/saucedemo/.waygraph-auto/8ea3a69c.log +0 -0
  161. package/examples/saucedemo/.waygraph-auto/9150f913.log +0 -0
  162. package/examples/saucedemo/.waygraph-auto/929a4313.log +0 -0
  163. package/examples/saucedemo/.waygraph-auto/95a599e4.log +0 -0
  164. package/examples/saucedemo/.waygraph-auto/96c45350.log +0 -0
  165. package/examples/saucedemo/.waygraph-auto/986030e6.log +0 -0
  166. package/examples/saucedemo/.waygraph-auto/99787abb.log +0 -0
  167. package/examples/saucedemo/.waygraph-auto/99ddb62c.log +0 -0
  168. package/examples/saucedemo/.waygraph-auto/a2dd30dd.log +0 -0
  169. package/examples/saucedemo/.waygraph-auto/a63a2d20.log +0 -0
  170. package/examples/saucedemo/.waygraph-auto/a78befb7.log +0 -0
  171. package/examples/saucedemo/.waygraph-auto/a84d9b79.log +0 -0
  172. package/examples/saucedemo/.waygraph-auto/a87bb3ee.log +0 -0
  173. package/examples/saucedemo/.waygraph-auto/a9270890.log +0 -0
  174. package/examples/saucedemo/.waygraph-auto/aa440153.log +0 -0
  175. package/examples/saucedemo/.waygraph-auto/ab7561e0.log +0 -0
  176. package/examples/saucedemo/.waygraph-auto/ac19ae60.log +0 -0
  177. package/examples/saucedemo/.waygraph-auto/ae21ad8e.log +0 -0
  178. package/examples/saucedemo/.waygraph-auto/af09042f.log +0 -0
  179. package/examples/saucedemo/.waygraph-auto/b1684126.log +0 -0
  180. package/examples/saucedemo/.waygraph-auto/b21e9d12.log +0 -0
  181. package/examples/saucedemo/.waygraph-auto/b5a838be.log +0 -0
  182. package/examples/saucedemo/.waygraph-auto/b603549d.log +0 -0
  183. package/examples/saucedemo/.waygraph-auto/b81e074f.log +0 -0
  184. package/examples/saucedemo/.waygraph-auto/b83e6259.log +0 -0
  185. package/examples/saucedemo/.waygraph-auto/b9cb2796.log +0 -0
  186. package/examples/saucedemo/.waygraph-auto/ba58abb7.log +0 -0
  187. package/examples/saucedemo/.waygraph-auto/bb0a0018.log +0 -0
  188. package/examples/saucedemo/.waygraph-auto/bb9fd384.log +0 -0
  189. package/examples/saucedemo/.waygraph-auto/bc0bdd0e.log +0 -0
  190. package/examples/saucedemo/.waygraph-auto/bc7d4a83.log +0 -0
  191. package/examples/saucedemo/.waygraph-auto/bd8e082a.log +0 -0
  192. package/examples/saucedemo/.waygraph-auto/bea0c93e.log +0 -0
  193. package/examples/saucedemo/.waygraph-auto/bf35201a.log +0 -0
  194. package/examples/saucedemo/.waygraph-auto/bffa3a7a.log +0 -0
  195. package/examples/saucedemo/.waygraph-auto/c02ea583.log +0 -0
  196. package/examples/saucedemo/.waygraph-auto/c2caf0a8.log +0 -0
  197. package/examples/saucedemo/.waygraph-auto/c3bdc714.log +0 -0
  198. package/examples/saucedemo/.waygraph-auto/c4afb8bc.log +0 -0
  199. package/examples/saucedemo/.waygraph-auto/c916a654.log +0 -0
  200. package/examples/saucedemo/.waygraph-auto/c9b8d69e.log +0 -0
  201. package/examples/saucedemo/.waygraph-auto/cd68bbc4.log +0 -0
  202. package/examples/saucedemo/.waygraph-auto/cdc6db50.log +0 -0
  203. package/examples/saucedemo/.waygraph-auto/d01906a7.log +0 -0
  204. package/examples/saucedemo/.waygraph-auto/d02e5e7e.log +0 -0
  205. package/examples/saucedemo/.waygraph-auto/d09aff71.log +0 -0
  206. package/examples/saucedemo/.waygraph-auto/d0e41396.log +0 -0
  207. package/examples/saucedemo/.waygraph-auto/d1370d76.log +0 -0
  208. package/examples/saucedemo/.waygraph-auto/d3714ae4.log +0 -0
  209. package/examples/saucedemo/.waygraph-auto/d9e5f8a4.log +0 -0
  210. package/examples/saucedemo/.waygraph-auto/da0685dc.log +0 -0
  211. package/examples/saucedemo/.waygraph-auto/da894ad8.log +0 -0
  212. package/examples/saucedemo/.waygraph-auto/daa2f922.log +0 -0
  213. package/examples/saucedemo/.waygraph-auto/daefb614.log +0 -0
  214. package/examples/saucedemo/.waygraph-auto/db97e833.log +0 -0
  215. package/examples/saucedemo/.waygraph-auto/dec3076e.log +0 -0
  216. package/examples/saucedemo/.waygraph-auto/e101d19a.log +0 -0
  217. package/examples/saucedemo/.waygraph-auto/e1973991.log +0 -0
  218. package/examples/saucedemo/.waygraph-auto/e207c989.log +0 -0
  219. package/examples/saucedemo/.waygraph-auto/e633e545.log +0 -0
  220. package/examples/saucedemo/.waygraph-auto/e69aec68.log +0 -0
  221. package/examples/saucedemo/.waygraph-auto/e70e2d06.log +0 -0
  222. package/examples/saucedemo/.waygraph-auto/e8306a4b.log +0 -0
  223. package/examples/saucedemo/.waygraph-auto/ebad3410.log +0 -0
  224. package/examples/saucedemo/.waygraph-auto/ec88b3ef.log +0 -0
  225. package/examples/saucedemo/.waygraph-auto/ecbb0966.log +0 -0
  226. package/examples/saucedemo/.waygraph-auto/ecdee753.log +0 -0
  227. package/examples/saucedemo/.waygraph-auto/ed888783.log +0 -0
  228. package/examples/saucedemo/.waygraph-auto/ed931dac.log +0 -0
  229. package/examples/saucedemo/.waygraph-auto/ef655a3f.log +0 -0
  230. package/examples/saucedemo/.waygraph-auto/f34c9a26.log +0 -0
  231. package/examples/saucedemo/.waygraph-auto/f3c29b21.log +0 -0
  232. package/examples/saucedemo/.waygraph-auto/f53f7cfc.log +0 -0
  233. package/examples/saucedemo/.waygraph-auto/f5525d53.log +0 -0
  234. package/examples/saucedemo/.waygraph-auto/f5ad5830.log +0 -0
  235. package/examples/saucedemo/.waygraph-auto/f90035b3.log +0 -0
  236. package/examples/saucedemo/.waygraph-auto/fa687bc2.log +0 -0
  237. package/examples/saucedemo/.waygraph-auto/faed36dd.log +0 -0
  238. package/examples/saucedemo/.waygraph-auto/fb66fba0.log +0 -0
  239. package/examples/saucedemo/.waygraph-auto/fc48fd76.log +0 -0
  240. package/examples/saucedemo/docs/CONVENTION.md +25 -5
  241. package/examples/saucedemo/docs/HELPERS.md +15 -4
  242. package/examples/saucedemo/scripts/capture-wrong-login.mjs +8 -2
  243. package/examples/saucedemo/scripts/prove-wrong-login-runtime.mjs +8 -2
  244. package/examples/saucedemo/src/blocks/SITE-MAP.md +14 -3
  245. package/examples/saucedemo/src/blocks/saucedemo-web/NAV.md +15 -1
  246. package/examples/saucedemo/src/blocks/saucedemo-web/cart/NAV.md +1 -0
  247. package/examples/saucedemo/src/blocks/saucedemo-web/cart/cart.sel.ts +9 -0
  248. package/examples/saucedemo/src/blocks/saucedemo-web/cart/nav-cart.block.ts +3 -2
  249. package/examples/saucedemo/src/blocks/saucedemo-web/cart/nav-checkout-info.block.ts +2 -1
  250. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/NAV.md +6 -1
  251. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/methods/checkout-info.sel.ts +11 -0
  252. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/methods/fill-first-name.method.block.ts +22 -0
  253. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/methods/fill-last-name.method.block.ts +21 -0
  254. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/methods/fill-postal-code.method.block.ts +21 -0
  255. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.ts +6 -7
  256. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/NAV.md +2 -0
  257. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/checkout-overview.sel.ts +9 -0
  258. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +7 -6
  259. package/examples/saucedemo/src/blocks/saucedemo-web/ff-owner-auth.block.ts +7 -2
  260. package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/NAV.md +1 -0
  261. package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/item-detail.sel.ts +9 -0
  262. package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/nav-back-to-inventory.block.ts +2 -1
  263. package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/nav-back-to-products.block.ts +2 -1
  264. package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/nav-continue-shopping.block.ts +2 -1
  265. package/examples/saucedemo/src/blocks/saucedemo-web/methods/fill-password.method.block.ts +51 -0
  266. package/examples/saucedemo/src/blocks/saucedemo-web/methods/fill-username.method.block.ts +69 -0
  267. package/examples/saucedemo/src/blocks/saucedemo-web/methods/login.sel.ts +13 -0
  268. package/examples/saucedemo/src/blocks/saucedemo-web/methods/submit-login-for-flow.ts +4 -4
  269. package/examples/saucedemo/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +13 -51
  270. package/examples/saucedemo/src/blocks/saucedemo-web/methods/submit-logout.method.block.ts +4 -3
  271. package/examples/saucedemo/src/blocks/saucedemo-web/nav-login.block.ts +14 -6
  272. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +15 -0
  273. package/examples/saucedemo/src/flows/checkout.flow.ts +20 -2
  274. package/examples/saucedemo/src/flows/device-showcase.flow.ts +4 -0
  275. package/examples/saucedemo/src/flows/login.flow.ts +3 -1
  276. package/examples/saucedemo/src/flows/shop.flow.ts +6 -0
  277. package/examples/saucedemo/src/flows/smoke.flow.ts +13 -7
  278. package/examples/saucedemo/src/flows/stub-on-error.flow.ts +4 -2
  279. package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +6 -2
  280. package/examples/saucedemo/tests/flow-block-verify.spec.ts +3 -3
  281. package/examples/saucedemo/tests/submit-login-branch.spec.ts +5 -3
  282. package/package.json +2 -1
  283. package/templates/agents/waygraph-author.agent.md +43 -5
  284. package/templates/agents/waygraph-planner.agent.md +33 -2
  285. package/templates/quickstart/src/blocks/saucedemo-web/NAV.md +15 -1
  286. package/templates/quickstart/src/blocks/saucedemo-web/cart/NAV.md +1 -0
  287. package/templates/quickstart/src/blocks/saucedemo-web/cart/cart.sel.ts +9 -0
  288. package/templates/quickstart/src/blocks/saucedemo-web/cart/nav-cart.block.ts +19 -15
  289. package/templates/quickstart/src/blocks/saucedemo-web/cart/nav-checkout-info.block.ts +2 -1
  290. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/NAV.md +6 -1
  291. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/methods/checkout-info.sel.ts +11 -0
  292. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/methods/fill-first-name.method.block.ts +22 -0
  293. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/methods/fill-last-name.method.block.ts +21 -0
  294. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/methods/fill-postal-code.method.block.ts +21 -0
  295. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.ts +6 -7
  296. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/NAV.md +2 -0
  297. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/checkout-overview.sel.ts +9 -0
  298. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +7 -6
  299. package/templates/quickstart/src/blocks/saucedemo-web/ff-owner-auth.block.ts +7 -2
  300. package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +24 -16
  301. package/templates/quickstart/src/blocks/saucedemo-web/inventory/nav-item-detail.block.ts +3 -3
  302. package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/NAV.md +1 -0
  303. package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/item-detail.sel.ts +9 -0
  304. package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/nav-back-to-inventory.block.ts +2 -1
  305. package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/nav-back-to-products.block.ts +2 -1
  306. package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/nav-continue-shopping.block.ts +2 -1
  307. package/templates/quickstart/src/blocks/saucedemo-web/methods/fill-password.method.block.ts +51 -0
  308. package/templates/quickstart/src/blocks/saucedemo-web/methods/fill-username.method.block.ts +69 -0
  309. package/templates/quickstart/src/blocks/saucedemo-web/methods/login.sel.ts +13 -0
  310. package/templates/quickstart/src/blocks/saucedemo-web/methods/submit-login-for-flow.ts +4 -4
  311. package/templates/quickstart/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +35 -63
  312. package/templates/quickstart/src/blocks/saucedemo-web/methods/submit-logout.method.block.ts +4 -3
  313. package/templates/quickstart/src/blocks/saucedemo-web/nav-login.block.ts +14 -6
  314. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +15 -0
  315. package/templates/quickstart/src/flows/checkout.flow.ts +29 -2
  316. package/templates/quickstart/src/flows/device-showcase.flow.ts +4 -0
  317. package/templates/quickstart/src/flows/login.flow.ts +3 -1
  318. package/templates/quickstart/src/flows/shop.flow.ts +6 -0
  319. package/templates/quickstart/src/flows/smoke.flow.ts +13 -7
  320. package/templates/quickstart/src/flows/viewer-blocked.flow.ts +6 -2
  321. package/templates/quickstart/tests/chain-flow.spec.ts +8 -1
  322. package/templates/scaffold/STRUCTURE.md +23 -3
  323. package/templates/scaffold/scripts/fixture-server.mjs +11 -2
  324. package/templates/scaffold/src/flows/example.flow.ts +8 -6
  325. package/templates/scaffold/src/flows/mail-verify.flow.ts +85 -0
  326. package/templates/scaffold/src/flows/shop.flow.ts +23 -6
  327. package/templates/scaffold/src/map/(app_base)/docs/_nav.block.ts +20 -0
  328. package/templates/scaffold/src/map/(app_base)/docs/_page.block.ts +15 -0
  329. package/templates/scaffold/src/map/(app_base)/docs/_sel.ts +7 -0
  330. package/templates/scaffold/src/map/(app_base)/docs/docs.html +8 -0
  331. package/templates/scaffold/src/{blocks/demo-web/methods → map/(app_base)/home/_methods}/add-item.effect.block.ts +3 -3
  332. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-hello.method.block.ts +46 -0
  333. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-item-added.method.block.ts +38 -0
  334. package/templates/scaffold/src/{blocks/demo-web/methods → map/(app_base)/home/_methods}/clear-cart.method.block.ts +9 -3
  335. package/templates/scaffold/src/{blocks/demo-web/methods → map/(app_base)/home/_methods}/remove-item.effect.block.ts +3 -3
  336. package/templates/scaffold/src/{blocks/demo-web/nav-home.block.ts → map/(app_base)/home/_nav.block.ts} +2 -2
  337. package/templates/scaffold/src/{blocks/demo-web/home.page.block.ts → map/(app_base)/home/_page.block.ts} +7 -7
  338. package/templates/scaffold/src/{blocks/demo-web/demo-sel.ts → map/(app_base)/home/_sel.ts} +1 -0
  339. package/templates/scaffold/src/{blocks/demo-web → map/(app_base)/home}/home.html +4 -0
  340. package/templates/scaffold/src/map/(app_base)/home/nav-verification-link.block.ts +34 -0
  341. package/templates/scaffold/src/map/(external)/mailpit/_methods/assert-email-content.method.block.ts +48 -0
  342. package/templates/scaffold/src/map/(external)/mailpit/_methods/assert-email-received.method.block.ts +49 -0
  343. package/templates/scaffold/src/map/(external)/mailpit/_methods/extract-email-link.method.block.ts +41 -0
  344. package/templates/scaffold/src/map/(external)/mailpit/_methods/open-message.method.block.ts +39 -0
  345. package/templates/scaffold/src/map/(external)/mailpit/_nav.block.ts +20 -0
  346. package/templates/scaffold/src/map/(external)/mailpit/_page.block.ts +30 -0
  347. package/templates/scaffold/src/map/(external)/mailpit/_sel.ts +5 -0
  348. package/templates/scaffold/src/states/demo.mem-keys.ts +25 -1
  349. package/templates/scaffold/src/states/demo.states.ts +11 -0
  350. package/templates/scaffold/test-results/.last-run.json +4 -0
  351. package/templates/scaffold/tests/example.spec.ts +2 -2
  352. package/templates/scaffold/tests/mail-verify.spec.ts +191 -0
  353. package/templates/skills/waygraph-blind-pilot.skill.md +61 -0
  354. package/templates/skills/waygraph-pilot.skill.md +108 -0
  355. package/templates/quickstart/README-saucedemo-commands.md +0 -72
  356. package/templates/scaffold/src/blocks/SITE-MAP.md +0 -12
  357. package/templates/scaffold/src/blocks/demo-web/NAV.md +0 -29
  358. package/templates/scaffold/src/blocks/demo-web/methods/assert-hello.method.block.ts +0 -50
package/README.md CHANGED
@@ -3,7 +3,10 @@
3
3
  A typed graph of reusable Blocks for driving a browser through E2E flows, in place of a
4
4
  flat pile of ad hoc helper functions.
5
5
 
6
- **Current:** `0.12.44` — floating todo dock (visible under `--mini`, `ctx.todoPos` / click / `--todo-left|right`), live `ctx.title` banner updates, instant highlight tone snaps.
6
+ Not yet `1.0.0` - breaking changes between minor versions are expected. See
7
+ [ROADMAP.md](./ROADMAP.md) for what's shipped, spec'd, and planned, and
8
+ `openspec/specs/` for the current merged spec behind each capability
9
+ (`openspec/changes/archive/` holds the historical planning artifacts once a change ships).
7
10
 
8
11
  This repo is the `waygraph` npm package itself. The full live **Sauce Demo** example
9
12
  (Page inventory hub, Effect Add/Remove, MemNav Open details, `waygraph auto`) ships
@@ -11,48 +14,403 @@ in-tree at [`examples/saucedemo`](./examples/saucedemo). `waygraph try demo` cop
11
14
  [`templates/quickstart`](./templates/quickstart) (same Sauce Demo Blocks) into an OS
12
15
  temp dir.
13
16
 
14
- See [ROADMAP.md](./ROADMAP.md) for what's shipped, spec'd, and planned by version, and
15
- `openspec/changes/` for the actual planning artifacts behind each roadmap item.
16
-
17
17
  ## Docs (GitHub Pages)
18
18
 
19
- Browser docs (quick start, **demo / run**, **auto explore**, engine handout, consumer layout, deploy):
20
-
21
- - **Published:** https://deviate-dv8.github.io/waygraph/ (workflow auto-enables Pages via `enablement: true`; manual Settings only if org policy blocks it)
22
- - **Source:** [`docs/`](./docs/) — static HTML, no build step
23
- - **Local preview:** `npm run docs:preview` → http://127.0.0.1:4173/
24
- - **Deploy:** push to `main` touching `docs/**` runs [`.github/workflows/pages.yml`](./.github/workflows/pages.yml) (`workflow_dispatch` also works)
25
- - **Demo / run contract:** [`docs/demo.html`](./docs/demo.html) — `list` / `.flow.ts` path / export / flags
26
- - **Auto explore:** [`docs/auto.html`](./docs/auto.html) — prefer `auto --cli` / `npm run auto:cli` (same menus as headed)
27
- - **In-repo example:** [`examples/saucedemo`](./examples/saucedemo) — full Sauce Demo project
28
- - **Convention showcase (Pages):** [`docs/saucedemo/`](./docs/saucedemo/) —
19
+ Browser docs (quick start, **demo / run**, **auto explore**, engine handout, consumer
20
+ layout, deploy):
21
+
22
+ - **Published:** https://deviate-dv8.github.io/waygraph/ (workflow auto-enables Pages via
23
+ `enablement: true`; manual Settings only if org policy blocks it)
24
+ - **Source:** [`docs/`](./docs/) - static HTML, no build step
25
+ - **Local preview:** `npm run docs:preview` -> http://127.0.0.1:4173/
26
+ - **Deploy:** push to `main` touching `docs/**` runs
27
+ [`.github/workflows/pages.yml`](./.github/workflows/pages.yml) (`workflow_dispatch` also
28
+ works)
29
+ - **Demo / run contract:** [`docs/demo.html`](./docs/demo.html) - `list` / `.flow.ts` path
30
+ / export / flags
31
+ - **Auto explore:** [`docs/auto.html`](./docs/auto.html) - prefer `auto --cli` /
32
+ `npm run auto:cli` (same menus as headed)
33
+ - **In-repo example:** [`examples/saucedemo`](./examples/saucedemo) - full Sauce Demo
34
+ project
35
+ - **Convention showcase (Pages):** [`docs/saucedemo/`](./docs/saucedemo/) -
29
36
  https://deviate-dv8.github.io/waygraph/saucedemo/
30
37
  - **Helpers (consumer copy):** [`examples/saucedemo/docs/HELPERS.md`](./examples/saucedemo/docs/HELPERS.md)
31
38
 
32
- This README stays the in-repo API narrative; Pages is the same material for eyeballing and cross-mesh handoff.
39
+ This README stays the in-repo API narrative; Pages is the same material for eyeballing and
40
+ cross-mesh handoff.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ npm install waygraph @playwright/test
46
+ ```
47
+
48
+ `@playwright/test` is a peer dependency, not bundled - you bring your own version
49
+ (`^1.40.0` or later) since it's also what drives the rest of your test suite. Requires
50
+ Node 22+ (uses `URL`/`URLPattern` from `@types/node` for `Trait.url`'s type checking).
51
+
52
+ ## Core concepts
53
+
54
+ - **`Checkpoint<Tag>`** - a state identified solely by its string tag. Carries no data.
55
+ - **`MemPage`** / **`MemKey<T>`** - typed shared memory across a run, keyed by object
56
+ identity (`key<T>("debug-name")`), not by name string.
57
+ - **`Block<In, Out>`** - one step: `act(page, input, mem)` does the work,
58
+ `resolve()` decides the resulting Checkpoint (never touches the page or mem),
59
+ `verify` (optional) confirms the page actually matches what `resolve()` claimed.
60
+ - **`Trait`** - a named, independently-reportable check (`{ name, check(page, mem) }`).
61
+ Built-in factories: `Trait.url` / `Trait.text` / `Trait.visible` (also importable as
62
+ free functions `urlMatches` / `textEquals` / `visible`).
63
+ - **`Flow`** - several Blocks chained with `Engine().defineFlow([start, ...blocks, end])`,
64
+ run with `flow.run(mem)` (Engine launches its own browser) or `flow.run(context, mem)`
65
+ (reuse an existing `@playwright/test` `context` fixture).
66
+
67
+ ## Quick start
68
+
69
+ ```typescript
70
+ import { Engine, start, end, MemPage, key, checkpoint, Trait } from "waygraph";
71
+ import type { Block, Checkpoint, Start } from "waygraph";
72
+
73
+ type LoggedIn = Checkpoint<"LoggedIn">;
74
+
75
+ const Username = key<string>("username");
76
+
77
+ const Login: Block<Start, LoggedIn> = {
78
+ name: "login",
79
+ instruction: {
80
+ async act(page, _input, mem) {
81
+ await page.goto("https://example.com/login");
82
+ await page.getByLabel("Username").fill(mem.get(Username));
83
+ await page.getByRole("button", { name: "Sign in" }).click();
84
+ },
85
+ resolve: () => checkpoint("LoggedIn"),
86
+ verify: [Trait.url({ pathname: "/dashboard" })],
87
+ },
88
+ };
89
+
90
+ const mem = new MemPage();
91
+ mem.set(Username, "alice");
92
+
93
+ const flow = new Engine().defineFlow([start, Login, end]);
94
+ const result = await flow.run(mem); // Engine launches its own browser
95
+ // result === { __state: "LoggedIn" }
96
+ ```
97
+
98
+ Inside an existing `@playwright/test` file, reuse its `context` fixture instead of having
99
+ the Engine launch its own browser: `await flow.run(context, mem)`.
100
+
101
+ See `tests/define-flow.spec.ts` here, and
102
+ `examples/saucedemo/tests/checkout-flow.spec.ts` (a real multi-Block flow against live
103
+ saucedemo.com), for this running for real. The lower-level `connect()`/`runGraph()` still
104
+ exist and are what `defineFlow` builds on - reach for them directly only if you need a
105
+ shape `defineFlow`'s array can't express yet.
106
+
107
+ ## Engine features
108
+
109
+ A Block's output type is the only contract the next Block can rely on. `resolve` is
110
+ structurally incapable of touching the browser or shared memory. `verify` runs only after
111
+ `resolve` has already decided (confirms or fails loud, never redirects).
112
+
113
+ ### Routing and composition
114
+
115
+ - **`branch()`** attaches routing to a Block - including back to itself (self-loops) -
116
+ without ever touching that Block's own phases. A `maxSteps` safety cap keeps a routing
117
+ bug from hanging a run forever.
118
+ - **`withVerify(block, verify)`** decorates any existing Block with different
119
+ confirmation, same `act`/`resolve`, same routing - without that Block needing to have
120
+ been authored as a parameterized factory (`withVerify(LoginBlock, [])` is the "I only
121
+ care that I navigated this far, no DOM check" case).
122
+ - **`flow.withBlockVerify` / `flow.modBlockVerify`** patch one Block's verify from
123
+ outside a finished `Flow` - addressed by the Block reference (preferred), its name, or
124
+ its numeric position - without editing the flow's own file.
125
+ - **`composeBlock(name, steps)`** chains several Blocks into one named unit, the same way
126
+ `connect()` already does - useful for a multi-step form that's naturally "one feature"
127
+ but still wants each step's own real `verify` independently overridable, not one giant
128
+ Block with no per-step confirmation. `composedBlock.withStepVerify` / `modStepVerify`
129
+ patch one step from outside, addressed the same three ways as `withBlockVerify`. A
130
+ `composeBlock` result is a plain Block, so it drops straight into
131
+ `defineFlow([start, ..., composed, ..., end])` like any other.
132
+ - **`fastForwardComposeBlock(name, steps)`** is the same shape marked for wall-clock
133
+ **fast-forward**: one opaque demo step that blitzes (no per-inner gate, no smooth cursor
134
+ theater, Playwright `slowMo` off for that flow unless `WAYGRAPH_SLOWMO` is set) unless
135
+ expanded. Put several FF units in one Flow to race past auth, then a heavy dashboard
136
+ settle, then watch the interesting middle. `waygraph demo --ff-disabled` (alias
137
+ `--no-ff`) expands every FFCompose into its inner Blocks so a broken step is visible as
138
+ its own panel row; former FF inners keep blitz pacing so wall-clock stays comparable.
139
+ `--ff-expand` is the older expand-only flag (same flatten).
140
+
141
+ ### Cross-cutting Layouts (`defineLayout`)
142
+
143
+ Real, direct request this responds to: persistent chrome (a sidebar, say) that's supposed
144
+ to stay on screen across most of an app shouldn't need its own `Trait` copy-pasted into
145
+ every single Checkpoint's `verify` - one missed copy and a regression silently ships. A
146
+ **Layout** is a `verify` that's enforced automatically, by Checkpoint tag, wherever it
147
+ applies - not once per Block:
148
+
149
+ ```ts
150
+ import { defineLayout, Engine } from "waygraph";
151
+
152
+ const AppShellLayout = defineLayout({
153
+ name: "AppShell",
154
+ // string[] of exact Checkpoint tags, or a (tag) => boolean predicate for a
155
+ // broader match (e.g. "every Checkpoint except the signed-out ones")
156
+ appliesTo: ["AppHome", "Chats", "Notifications", "Settings"],
157
+ verify: [Trait.visible(AppShellSel.sidebar)],
158
+ });
159
+
160
+ const engine = new Engine({ layouts: [AppShellLayout] });
161
+ ```
162
+
163
+ Every `Flow` this `Engine` defines - **and every intermediate Checkpoint inside it**, not
164
+ just the final one - runs `AppShellLayout.verify` automatically whenever it resolves to a
165
+ tag `appliesTo` matches, alongside that Block's own `verify`. `runGraph`/`Flow.run` also
166
+ take `{ layouts }` directly (`RunGraphOptions.layouts`), for driving a Layout outside an
167
+ `Engine`-owned Flow (e.g. `waygraph auto`'s own runtime pathfinding).
168
+
169
+ Deliberately matched by Checkpoint tag, never by folder path - a Layout works identically
170
+ whether a project uses the [Waygraph Map](#waygraph-map) convention or the older freeform
171
+ folder style, since some real consumers of this package still use the latter.
172
+
173
+ ### `map()` - a kind-checked, no-teleporting flow builder
174
+
175
+ `Engine.defineFlow([start, ...blocks, end])` already typechecks that each Block's `Out`
176
+ matches the next one's `In`. It does **not** stop an agent (or a careless human) from
177
+ handing it a hand-rolled plain object that merely *looks* like a Block - TypeScript's
178
+ structural typing can't tell a real `defineNavBlock(...)` output from a copy-pasted
179
+ object literal with the right shape. That gap is exactly what broke down in real
180
+ consumer projects: agents editing/hand-composing Blocks into ad hoc shapes, a "locks"
181
+ convention tried and still bypassed. `map()` closes it with an actual runtime check, not
182
+ just a convention:
183
+
184
+ ```ts
185
+ import { Engine } from "waygraph";
186
+
187
+ const engine = new Engine();
188
+ const flow = engine
189
+ .map({ homeOrigin: "https://app.example.com" })
190
+ .start()
191
+ .gotoPage(NavHomeBlock) // must be defineNavBlock / defineMemNavBlock / definePageBlock
192
+ .assert(AssertHelloBlock) // must be defineAssertBlock
193
+ .gotoExternal(NavMailpitBlock) // same kind requirement, plus the INVERSE origin check
194
+ .method(ClearCartBlock) // must be defineMethodBlock / defineActionBlock / defineEffectBlock
195
+ .end(); // -> a real Flow<Out> - withBlockVerify/withTitle/withHighlightFixtures/etc. all still apply
196
+ ```
197
+
198
+ Each step method is scoped to exactly the Block kind its name promises, checked against
199
+ the same `__waygraphKind`/`__waygraphSalt` runtime markers `waygraph check`/`graph`/`map`
200
+ already trust - a Block that didn't come from a real `define*Block` factory throws
201
+ immediately, naming the step and the factory it should have used, instead of silently
202
+ entering the chain. Give `homeOrigin` and `.gotoPage()`/`.gotoExternal()` also cross-check
203
+ each Nav/Page Block's own static `url` against it (skipped, honestly, for click-based or
204
+ mem-dependent nav - not statically checkable). The Checkpoint chain itself is still
205
+ typechecked exactly like `defineFlow`'s own tuple overloads - there is no method on this
206
+ builder that can skip from one Checkpoint to an unrelated one without a real, kind-correct
207
+ Block in between, which is what "no teleporting" means here.
208
+
209
+ `map()` is sugar over `defineFlow`, not a new execution engine - the result is a real
210
+ `Flow`, so every existing Flow-level feature (Layouts, `withBlockVerify`, demo narration,
211
+ `chainFlow`) works on it unchanged. `import { map } from "waygraph"` is also available
212
+ standalone (`map(options)` ~= `new Engine(options).map(options)`) for a call site that
213
+ doesn't otherwise need its own `Engine` instance. **Recommended for new projects** over
214
+ hand-assembled `defineFlow([start, ...])` arrays - the freeform array form still works
215
+ and isn't going away (real consumers on the older folder convention depend on it), but
216
+ `map()` is the one that actually stops a broken/hacked Block from entering a flow
217
+ unnoticed.
218
+
219
+ ### Traits beyond the page
220
+
221
+ A hand-written `Trait`'s `check(page, mem)` receives `mem` too, for the same-Checkpoint
222
+ case: an action that doesn't navigate at all (clicking "+" to bump a cart item's quantity,
223
+ say) has `In === Out`, which `defineBlock`/`defineFlow` already accept with no
224
+ special-casing - `act()` updates `mem` alongside the real click, and a bespoke `verify`
225
+ Trait confirms the page's own displayed value actually matches what `mem` now expects, not
226
+ just that *some* number is showing. `resolve()` itself stays exactly as pure as ever - it
227
+ never sees `mem`, only `verify` does.
228
+
229
+ `Trait.url` takes a structured `URLPatternInit` (`{ pathname, hostname, search, hash, ... }`,
230
+ the same shape the standard `URLPattern` Web API takes), not a hand-written regex -
231
+ whichever components you leave out default to "match anything," so
232
+ `Trait.url({ pathname: "/inventory.html" })` already ignores whatever query params a
233
+ hybrid SPA tacks on, with no separate flag needed for that.
234
+
235
+ ### `precondition` - confirming the page is *still* there
236
+
237
+ `verify` only ever confirms what a Block itself just did - nothing previously checked
238
+ whether the page was still there by the time the next Block's `act()` starts. A
239
+ `precondition` (same `Trait[]` / `(input) => Trait[]` shape as `verify`, checked
240
+ automatically by `connect()`/`runGraph` right before `act()` runs, never called directly)
241
+ closes that gap: a session timeout, a redirect, an interstitial popup - anything that
242
+ changes the page in the moment between one Block finishing and the next one starting -
243
+ fails loud with a clear "trait X failed before Y," instead of a confusing error deep
244
+ inside `act()` trying to click something that's no longer there.
245
+
246
+ ### `requires` and `preflight()`
247
+
248
+ A Block's `requires: MemKey[]` gets checked by `preflight()` (built into `runGraph`)
249
+ before a tab even opens - a missing credential fails in milliseconds, not after several
250
+ real browser actions already ran.
251
+
252
+ ### Engine config and pluggable browsers
253
+
254
+ `new Engine({ headless, browserName, slowMo })` configures a browser the Engine launches
255
+ and owns itself: `flow.run(mem)` (no context) uses it, and
256
+ `flow.run(mem, { ...overrides })` overrides it per call - so one module-level `Flow`,
257
+ imported by many specs, doesn't force every caller into the same settings.
258
+ `flow.run(context, mem)` still works exactly as before and ignores all of this, since that
259
+ context is already launched.
260
+
261
+ `new Engine({ browsers: { chromium, firefox, webkit } })` overrides which `BrowserType`
262
+ actually launches for a `mem`-only run, per browser name - waygraph is deliberately "just
263
+ an opinionated Playwright," so a stealth-patched or otherwise customized launcher (e.g.
264
+ `playwright-extra` plus a stealth plugin) drops in unmodified, since those already expose
265
+ the same `.launch()` shape as Playwright's own `chromium`/`firefox`/`webkit`.
266
+
267
+ ### Driving an existing page / tabs
268
+
269
+ `spawnTab(entry, page, mem)` drives a genuinely separate second tab through its own
270
+ Block/Flow, in the same browser context an existing `page` already belongs to - the
271
+ pattern `observe()` could already reach for (it's the only phase allowed to touch
272
+ `page.context()`), now a named, discoverable, tested primitive.
273
+
274
+ `flow.run(context, mem, options)` also accepts a trailing optional options object -
275
+ additively, so `flow.run(context, mem)` still behaves byte-for-byte as before:
276
+ `options.page` drives that already-open page instead of opening a fresh tab (the run
277
+ won't close a page you handed it unless you say so), and `options.closeOnFinish: false`
278
+ leaves the run's page open and returns `{ result, page }` so the caller can keep driving
279
+ it. See [Recipes](#recipes) for the surviving-tab and popup-capture patterns.
280
+
281
+ ### NavBlock / ActionPage / waygraph check
282
+
283
+ `defineNavBlock({ name, checkpoint, url })` or
284
+ `defineNavClickBlock({ name, checkpoint, click })` builds a Block whose only possible
285
+ action is navigating. Prefer `defineNavClickBlock` for click-nav in app code;
286
+ `defineNavBlock` for `url` / `goto` deep links. Both accept a plain string or
287
+ `(mem) => string` / `(mem) => selector`. Generated `act()` is always exactly
288
+ `page.goto(url)` or `page.locator(click).click()`. For screen hubs that also register
289
+ methods, prefer `definePageBlock` (see [Block helpers](#block-helpers-page-method-sel)
290
+ below).
291
+
292
+ A regular `defineBlock` / `defineMethodBlock`'s `act()` receives `page` typed as
293
+ `ActionPage` - `goto`/`reload`/`goBack`/`goForward` are `@deprecated` there (editor
294
+ strike-through pointing at Nav/Page helpers). This is a soft, TypeScript-only signal:
295
+ nothing is blocked, nothing fails to build. `waygraph check [project]` is the
296
+ complementary whole-project sweep for contexts with no editor watching (CI, generated
297
+ code, an autonomous agent authoring Blocks) - it warns, never fails the process.
33
298
 
34
- ## 0.9.0 — Page, Method, Sel
299
+ ### Demo narration
300
+
301
+ Automation rings are gray; authored stubs/slides can use
302
+ `tone: "info" | "warning" | "danger" | "success"` (iconified captions), plus
303
+ `size: "sm" | "md" | "lg"` and `weight: "normal" | "bold"`. Flow episode defaults:
304
+ `withHighlightStyle(flow, { size, weight, tone? })` (slot/fixture wins). Episode pacing:
305
+ `withDemoPace(flow, "fast" | "slow" | "normal" | "blitz" | number)` or
306
+ `withBlockPace(block, ...)` (`number <= 20` = scale vs normal, e.g. `0.5` / `2`; `> 20` =
307
+ absolute ms, e.g. `4500`). FFCompose stays blitz. Living objectives:
308
+ `docs/proposals/OBJECTIVES.md`.
309
+
310
+ ### Graph traversal
311
+
312
+ `waygraph traverse` walks the Block graph. `--parallel N` (default `--session clone`)
313
+ bootstraps one context, forks N workers with `storageState` + mem snapshot, partitions
314
+ edges by hash, and claims via in-process edge leases (audit trail under
315
+ `.waygraph-traverse/`). `--session inherit` is refused when `parallel > 1`. It writes
316
+ `.waygraph-traverse/coverage.json` and prints `[Coverage edges=H/T ratio=R%...]`;
317
+ `--min-edge-coverage 80%` fails the suite with exit 2 when the ratio is below the gate
318
+ (even if every worker leaf-PASSed). `--coverage-out PATH` / `--no-coverage-report` adjust
319
+ the JSON. See `docs/proposals/traverse-ffcompose-rfc.md`.
320
+
321
+ ### Not yet implemented
322
+
323
+ Tracked on the project board / roadmap, not in this package today:
324
+
325
+ - Interstitials/Watchers - background overlay handling (cookie banners, popups).
326
+ - Split `requires` (externally-supplied vs producedBy) recovery hints.
327
+ - Federated multi-package pool of waygraphs.
328
+
329
+ ## Block helpers (Page, Method, Sel)
35
330
 
36
331
  Runtime is still one type: `Block`. Helpers are TypeScript salt.
37
332
 
38
333
  | Helper | Role | Typical file |
39
334
  |--------|------|----------------|
40
335
  | `definePageBlock` | Screen hub (checkpoint + `verify` + registered methods) | `*.page.block.ts` |
41
- | `defineMethodBlock` | One-shot non-nav step (submit, upload, logout) | `methods/*.method.block.ts` |
336
+ | `defineMethodBlock` | One-shot non-nav step, exactly one action (submit, upload, logout) | `methods/*.method.block.ts` |
337
+ | `defineAssertBlock` | Self-loop-only assertion (no state change) - `verify` only | `methods/*.method.block.ts` |
42
338
  | `defineEffectBlock` | Instance mutate + `instanceOptions` auto menu | `methods/*.effect.block.ts` |
43
339
  | `defineNavBlock` / `defineNavClickBlock` | `goto` / click-nav | `nav-*.block.ts` |
44
340
  | `defineMemNavBlock` | Nav + per-row `instanceOptions` | `nav-*.block.ts` |
45
- | `defineActionBlock` | **Deprecated** alias of `defineMethodBlock` | — |
341
+ | `defineActionBlock` | Deprecated alias of `defineMethodBlock` | - |
46
342
 
47
343
  **Page hub:** one Checkpoint = this screen. Methods hang off the page for readability /
48
344
  auto grouping. Arrival-only hubs omit `url`/`click` (previous Block already landed here).
49
345
  Deep-link hubs pass `url` or `click` like a Nav.
50
346
 
51
- **`methods/` folder** (was `actions/`): on-page work lives next to the route, not free-floating.
347
+ **One distinct action per Block - hard rule.** A Method that fills form fields *and*
348
+ submits, or that drives several steps inside one `act()`, is wrong even if it "works" -
349
+ split it into atomic Blocks (a login form is `fill-username` + `fill-password` +
350
+ `submit-login`, never one Block doing all three; see `examples/saucedemo`'s own
351
+ `saucedemo-web/methods/` for the real split). A Block that only asserts something on the
352
+ current page, with no state change, is `defineAssertBlock({ name, checkpoint, verify,
353
+ waitForHeading?, requires? })` - self-loop and `resolve` are generated for you, so there's no
354
+ hand-written `act`/`resolve` to accidentally make do two things. It also accepts
355
+ `stubBefore`/`stubAfter`/`stubOnError`/`slides`, the same narration fields every other
356
+ Block helper takes, so converting a hand-written assertion Method loses no demo/highlight
357
+ fixture data. `requires` (declare it whenever a mem-aware Trait reads a mem key the caller
358
+ must supply externally - see the mail example below) lets preflight catch a missing input
359
+ before the flow ever runs, the same as every other Block helper:
360
+
361
+ ```typescript
362
+ export const AssertInventoryHeaderBlock = defineAssertBlock({
363
+ name: "assert-inventory-header",
364
+ checkpoint: "LoggedIn",
365
+ waitForHeading: "Products",
366
+ verify: [Trait.visible(InventorySel.list)],
367
+ });
368
+ ```
369
+
370
+ `waygraph check` warns when a `verify` array inlines a literal selector string
371
+ (`Trait.visible("#some-id")`) instead of referencing a `*Sel` object - the exact pattern
372
+ `defineAssertBlock` exists to make easy to avoid.
373
+
374
+ **Give `defineAssertBlock` an explicit type argument when it sits mid-chain.** Without one,
375
+ `Out` defaults to wildcard `Checkpoint<string>` on both sides - harmless when the assert is
376
+ the last real Block before `end` (nothing downstream needs it narrower), but a `defineFlow`
377
+ array's tuple typing breaks once a wildcard Block is sandwiched between two
378
+ specifically-typed ones. Write `defineAssertBlock<LoggedIn>({ ..., checkpoint: "LoggedIn",
379
+ ... })` any time the assert has a real Block after it, not just before.
52
380
 
53
- **`*Sel`:** DOM selectors only (static strings + `(id) => …` for item-no-1 style). Mem keys
54
- store values (which item / upload queue), not selectors. `instanceOptions` scrapes the live
55
- DOM into menu rows and `mem.set`s on pick; predefined methods seed mem from the flow instead.
381
+ **An assert Block can check the *result* of an action, not just static page state.** Since
382
+ `Trait.check(page, mem)` receives mem as well as the page, an assert placed right after a
383
+ Method/Effect can confirm what that specific action actually did - not a duplicate of that
384
+ Block's own inline `verify`, a genuinely separate, nameable QA checkpoint:
385
+
386
+ ```typescript
387
+ const removeButtonVisibleForSelectedItem: Trait = {
388
+ name: "remove-button-visible-for-selected-item",
389
+ async check(page, mem) {
390
+ const { id } = mem.get(SelectedItem.key); // what add-item just acted on
391
+ return page.locator(RemoveBtnSel(id)).isVisible();
392
+ },
393
+ };
394
+
395
+ export const AssertItemAddedBlock = defineAssertBlock<ItemInCart>({
396
+ name: "assert-item-added",
397
+ checkpoint: "ItemInCart",
398
+ verify: [removeButtonVisibleForSelectedItem, Trait.text(CartCountSel, "1")],
399
+ });
400
+
401
+ // wired right after the action it checks:
402
+ engine.defineFlow([start, NavHomeBlock, AddItemBlock, AssertItemAddedBlock, ClearCartBlock, end])
403
+ ```
404
+
405
+ Live reference: `templates/scaffold/src/map/(app_base)/home/_methods/assert-item-added.method.block.ts`,
406
+ wired into `shop.flow.ts`.
407
+
408
+ **`methods/` folder:** on-page work lives next to the route, not free-floating.
409
+
410
+ **`*Sel`:** DOM selectors only (static strings + `(id) => ...` for item-no-1 style). Mem
411
+ keys store values (which item / upload queue), not selectors. `instanceOptions` scrapes the
412
+ live DOM into menu rows and `mem.set`s on pick; predefined methods seed mem from the flow
413
+ instead.
56
414
 
57
415
  ```typescript
58
416
  import { definePageBlock, Trait } from "waygraph";
@@ -80,240 +438,611 @@ export const InventoryPage = definePageBlock({
80
438
 
81
439
  Live reference: `examples/saucedemo/src/blocks/saucedemo-web/inventory/`
82
440
  (`inventory.page.block.ts` + `methods/` + `InventorySel`). Bulk `add-all-to-cart` /
83
- `remove-all-from-cart` are one menu row each (`from: "*"` — works after leave/return).
441
+ `remove-all-from-cart` are one menu row each (`from: "*"` - works after leave/return).
84
442
 
443
+ ## Mail adapters (cross-origin, browser-driven)
85
444
 
86
- ## Getting started (pick one)
445
+ For flows driven by an email (signup confirmation, password reset, a magic link), the
446
+ convention is **not** a REST/HTTP client to the mail catcher - it's more Blocks, pointed at
447
+ the catcher's own web UI (MailHog, MailDev, Mailpit all ship one) as a real, separate,
448
+ cross-origin page. This needs zero new engine surface: a `NavBlock` navigates there like any
449
+ other page, `MethodBlock`s read the DOM with ordinary `page.locator`/`page.frameLocator`
450
+ calls, and a final `NavBlock` carries the extracted link back into the app under test.
451
+ Convergent, real-world evidence for this exact shape: independently, more than one real
452
+ consumer project settled on it rather than a REST client.
87
453
 
88
- | Goal | Command |
89
- |------|---------|
90
- | Watch Sauce Demo step-through (temp dir only) | `npx waygraph try demo` |
91
- | Interactive explore (Add/Remove/Open details from live page) | `cd examples/saucedemo && npm i && npx waygraph auto` |
92
- | Scaffold a new **offline** project (green `npm test` on local fixture `:4177`) | `npx create-waygraph my-app` **or** `npx waygraph init my-app` |
93
- | Install coding-agent defs for diving an app into Blocks | `npx waygraph agent-dive --loop claude` (also `opencode` / `cursor` / `vscode`) |
94
- | Add waygraph to an existing repo | `npm install waygraph @playwright/test` |
454
+ **Keep it in its own `*-external/<tool>/` folder, never mixed into your app's own Blocks** -
455
+ it's a different origin, driving a different site:
95
456
 
96
- **Scaffold is not hidden inside waygraph alone** - the offline starter also ships as
97
- [`create-waygraph`](https://github.com/deviate-dv8/create-waygraph) on npm (same template as
98
- `waygraph init` since 0.7.5). Use whichever entry you already have: `npx create-waygraph my-app`
99
- when reading the GitHub Pages docs, or `npx waygraph init my-app` when the CLI is already
100
- installed. After either:
457
+ ```typescript
458
+ // demo-external/mailpit/nav-mailpit-inbox.block.ts
459
+ export const NavMailpitInboxBlock = defineNavBlock<MailpitInbox>({
460
+ name: "nav-mailpit-inbox",
461
+ checkpoint: "MailpitInbox",
462
+ url: () => process.env.WAYGRAPH_MAIL_URL ?? "http://127.0.0.1:8025",
463
+ verify: [Trait.url({ pathname: "/" })],
464
+ });
101
465
 
102
- ```bash
103
- cd my-app && npm install && npx playwright install chromium && npm test
104
- ```
466
+ // demo-external/mailpit/methods/open-message.method.block.ts
467
+ export const OpenMessageBlock = defineMethodBlock<MailpitInbox, MailpitMessageOpen>({
468
+ name: "open-message",
469
+ requires: [ExpectedRecipient.key], // externally supplied - avoids picking the wrong inbox row
470
+ instruction: {
471
+ async act(page, _in, mem) {
472
+ const { email } = mem.get(ExpectedRecipient.key);
473
+ await page.locator(MailpitSel.messageRow, { hasText: email }).first().click();
474
+ await page.waitForURL(/\/view\//);
475
+ },
476
+ resolve: () => checkpoint("MailpitMessageOpen"),
477
+ },
478
+ });
105
479
 
106
- `try demo` is different: live saucedemo.com, step-through **Sign In → Shop & Checkout →
107
- blocked Viewer login**, then headless tests. Permanent full example:
108
- [`examples/saucedemo`](./examples/saucedemo). Offline empty scaffold: `init` /
109
- `create-waygraph`.
480
+ // demo-external/mailpit/methods/extract-email-link.method.block.ts
481
+ // Named for what it does, not for one scenario - see "reusable across
482
+ // scenarios" below for why.
483
+ export const ExtractEmailLinkBlock = defineMethodBlock<MailpitMessageOpen, MailpitMessageOpen>({
484
+ name: "extract-email-link",
485
+ requires: [ExpectedLinkPattern], // which link in the body to follow - externally supplied
486
+ instruction: {
487
+ async act(page, _in, mem) {
488
+ const pattern = mem.get(ExpectedLinkPattern);
489
+ const link = page.frameLocator(MailpitSel.previewIframe).locator(`a[href*="${pattern}"]`).first();
490
+ const href = await link.getAttribute("href");
491
+ if (!href) throw new Error(`no link matching "${pattern}" found`);
492
+ mem.set(EmailLink({ url: href }));
493
+ },
494
+ resolve: () => checkpoint("MailpitMessageOpen"),
495
+ },
496
+ });
110
497
 
111
- ```bash
112
- # From this checkout, after npm run build:
113
- cd examples/saucedemo && npm install && npx playwright install chromium
114
- npm test # live Playwright suite
115
- npm run auto # headed interactive explore
116
- npm run demo:step # stepper (manual Next)
117
- npm run demo:autoplay # stepper with Auto-advance
498
+ // demo-web/nav-verification-link.block.ts - back into the app, no `requires` (see below)
499
+ export const NavVerificationLinkBlock = defineNavBlock<HomeVerified>({
500
+ name: "nav-verification-link",
501
+ checkpoint: "HomeVerified",
502
+ url: (mem) => mem.get(EmailLink.key).url,
503
+ verify: [Trait.visible(DemoSel.verifiedBanner)],
504
+ });
118
505
  ```
119
506
 
507
+ Four atomic Blocks, each doing exactly one thing - `open-message` and `extract-email-link`
508
+ stay separate Methods for the same reason `fill-username`/`submit-login` do in the Sauce
509
+ Demo example: reading the link and navigating to it are two distinct actions, so they are
510
+ two distinct Blocks (`nav-verification-link` does the actual navigation).
120
511
 
121
- ## Install
512
+ Note `nav-verification-link` deliberately does **not** declare `requires: [EmailLink.key]` -
513
+ `requires` means "the caller must seed this externally before the flow starts" (preflight
514
+ checks a whole chain's requires up front), and this key is instead produced by
515
+ `extract-email-link` earlier in the very same chain. Declaring it as a requirement would make
516
+ preflight reject the run before it ever gets a chance to produce it - `requires` is only for
517
+ keys a caller truly must supply from outside the chain (like `ExpectedRecipient` above).
122
518
 
123
- ```bash
124
- npm install waygraph @playwright/test
125
- ```
519
+ Live reference, proven end to end against a real, throwaway Mailpit container:
520
+ `templates/scaffold/src/map/(external)/mailpit/` + `src/flows/mail-verify.flow.ts` +
521
+ `tests/mail-verify.spec.ts`.
126
522
 
127
- `@playwright/test` is a peer dependency, not bundled - you bring your own version (`^1.40.0`
128
- or later) since it's also what drives the rest of your test suite. Requires Node 22+ (uses
129
- `URL`/`URLPattern` from `@types/node` for `Trait.url`'s type checking).
523
+ ### Reusable across scenarios: mem-driven, not one Block per email
130
524
 
131
- ## Quick start
525
+ The natural instinct once you have a signup-verification flow is to want a matching set of
526
+ Blocks for password reset, then another for a magic link, and so on - N scenarios, N Block
527
+ sets. That's the wrong axis to scale on. `open-message`, `extract-email-link`, and the two
528
+ assert Blocks below are already scenario-agnostic: **only mem varies per run, never the
529
+ Blocks**:
530
+
531
+ | Mem key | Answers |
532
+ |---|---|
533
+ | `ExpectedRecipient` | Which inbox row to open |
534
+ | `ExpectedLinkPattern` | Which link in the body to follow (a real email often has more than one - "report abuse" and the real link both present) |
535
+ | `ExpectedEmailContent` | What the body is supposed to say |
132
536
 
133
- A Block wraps one `act`/`resolve` step (add `observe`/`verify` once you need them); a `Flow`
134
- chains several into a run:
537
+ The same four Blocks (`assert-email-received`, `open-message`, `assert-email-content`,
538
+ `extract-email-link`) run a signup-verification check and a password-reset check identically
539
+ - just with different mem, in `templates/scaffold/tests/mail-verify.spec.ts`:
135
540
 
136
541
  ```typescript
137
- import { Engine, start, end, MemPage, key, checkpoint, Trait } from "waygraph";
138
- import type { Block, Checkpoint, Start } from "waygraph";
542
+ mem.set(ExpectedRecipient({ email: "demo-user@example.com" }));
543
+ mem.set(ExpectedLinkPattern, "verified=1");
544
+ mem.set(ExpectedEmailContent, "Please confirm your email address...");
545
+ await mailVerifyFlow.run(context, mem); // signup verification
546
+
547
+ mem.set(ExpectedRecipient({ email: "password-reset-user@example.com" }));
548
+ mem.set(ExpectedLinkPattern, "verified=1"); // picks the real link, not the "report abuse" decoy also in the body
549
+ mem.set(ExpectedEmailContent, "We received a request to reset your password.");
550
+ await mailVerifyFlow.run(context, mem); // same flow, same Blocks, different email entirely
551
+ ```
139
552
 
140
- type LoggedIn = Checkpoint<"LoggedIn">;
553
+ Naming matters here: a Block named `extract-verification-link` would wrongly imply it only
554
+ ever handles verification emails - once a Block's behavior is mem-driven, its name should
555
+ describe the mechanism (`extract-email-link`), not one scenario that happens to use it.
141
556
 
142
- const Username = key<string>("username");
557
+ A **mail-provider page hub** ties it together, the same `definePageBlock` pattern used
558
+ everywhere else - one page, several reusable methods, each pulling its specifics from mem:
143
559
 
144
- const Login: Block<Start, LoggedIn> = {
145
- name: "login",
146
- instruction: {
147
- async act(page, _input, mem) {
148
- await page.goto("https://example.com/login");
149
- await page.getByLabel("Username").fill(mem.get(Username));
150
- await page.getByRole("button", { name: "Sign in" }).click();
151
- },
152
- resolve: () => checkpoint("LoggedIn"),
153
- verify: [Trait.url({ pathname: "/dashboard" })],
560
+ ```typescript
561
+ export const MailpitInboxPageBlock = definePageBlock<MailpitInbox>({
562
+ name: "page-mailpit-inbox",
563
+ checkpoint: "MailpitInbox",
564
+ verify: [Trait.url({ pathname: "/" })],
565
+ methods: {
566
+ assertEmailReceived: () => AssertEmailReceivedBlock,
567
+ openMessage: () => OpenMessageBlock,
568
+ assertEmailContent: () => AssertEmailContentBlock,
569
+ extractEmailLink: () => ExtractEmailLinkBlock,
570
+ },
571
+ });
572
+ ```
573
+
574
+ ### QA-rich: does the email exist, and does it say the right thing
575
+
576
+ Two more assert Blocks slot into the same chain, both mem-driven, both proven to fail loud
577
+ (not silently pass) on a real negative case - no email arrived, and an email arrived with
578
+ the wrong copy:
579
+
580
+ ```typescript
581
+ // "Did the email even arrive" - doesn't open/consume it, safe to check before deciding to.
582
+ // A mem-aware Trait: the selector depends on the expected recipient, so it
583
+ // can't be a static Trait.visible(...) string - check(page, mem) already gets mem.
584
+ const emailReceived: Trait = {
585
+ name: "email-received",
586
+ async check(page, mem) {
587
+ const { email } = mem.get(ExpectedRecipient.key);
588
+ try {
589
+ await page.locator(MailpitSel.messageRow, { hasText: email }).first().waitFor({
590
+ state: "visible",
591
+ timeout: 10_000,
592
+ });
593
+ return true;
594
+ } catch {
595
+ return false;
596
+ }
154
597
  },
155
598
  };
599
+ export const AssertEmailReceivedBlock = defineAssertBlock<MailpitInbox>({
600
+ name: "assert-email-received",
601
+ checkpoint: "MailpitInbox",
602
+ requires: [ExpectedRecipient.key],
603
+ verify: [emailReceived],
604
+ });
156
605
 
157
- const mem = new MemPage();
158
- mem.set(Username, "alice");
606
+ // "Does the email say what it's supposed to" - reads the body content, not
607
+ // just whether a link exists. The expected text comes from mem too (same
608
+ // reuse reasoning as above) via another bespoke mem-aware Trait, since
609
+ // Trait.frameContains's own factory bakes its expected string in at
610
+ // Block-definition time, not runtime.
611
+ const bodyContainsExpectedText: Trait = {
612
+ name: "body-contains-expected-text",
613
+ async check(page, mem) {
614
+ const expected = mem.get(ExpectedEmailContent);
615
+ const text = await page.frameLocator(MailpitSel.previewIframe).locator("body").textContent();
616
+ return (text ?? "").includes(expected);
617
+ },
618
+ };
619
+ export const AssertEmailContentBlock = defineAssertBlock<MailpitMessageOpen>({
620
+ name: "assert-email-content",
621
+ checkpoint: "MailpitMessageOpen",
622
+ requires: [ExpectedEmailContent],
623
+ verify: [bodyContainsExpectedText],
624
+ });
625
+ ```
159
626
 
160
- const flow = new Engine().defineFlow([start, Login, end]);
161
- const result = await flow.run(mem); // Engine launches its own browser
162
- // result === { __state: "LoggedIn" }
627
+ `Trait.frameVisible(frameSelector, innerSelector)` / `Trait.frameText(frameSelector,
628
+ innerSelector, expected)` (exact match) / `Trait.frameContains(frameSelector, innerSelector,
629
+ expected)` (substring match, for a *fixed* known expected string) mirror
630
+ `Trait.visible`/`Trait.text` exactly, scoped to a frame - reusable for any iframe content,
631
+ not just mail; reach for a bespoke mem-aware Trait instead when the expected value itself
632
+ needs to vary per run, as both examples above do. Wired into the chain right where they
633
+ belong:
634
+
635
+ ```typescript
636
+ engine.defineFlow([
637
+ start, NavMailpitInboxBlock, AssertEmailReceivedBlock, OpenMessageBlock,
638
+ AssertEmailContentBlock, ExtractEmailLinkBlock, NavVerificationLinkBlock, end,
639
+ ])
163
640
  ```
164
641
 
165
- Inside an existing `@playwright/test` file, reuse its `context` fixture instead of having the
166
- Engine launch its own browser: `await flow.run(context, mem)`.
167
-
168
- **Status:** `Checkpoint`, `MemPage`, `Instruction`/`Block` (`act`/`observe`/`resolve`/`verify`),
169
- `connect()`, `Trait`, `Engine.defineFlow([start, ...blocks, end])`, and `branch()`/self-loops
170
- with a `maxSteps` safety cap. A Block's output type is the only contract the next Block can
171
- rely on, `resolve` is structurally incapable of touching the browser or shared memory,
172
- `verify` runs only after `resolve` has already decided (confirms or fails loud, never
173
- redirects), `branch()` attaches routing to a Block - including back to itself - without ever
174
- touching that Block's own phases, a Block's `requires: MemKey[]` gets checked by
175
- `preflight()` (built into `runGraph`) before a tab even opens - a missing credential fails
176
- in milliseconds, not after several real browser actions already ran - and `new Engine({ headless, browserName, slowMo })` configures a browser the Engine
177
- launches and owns itself: `flow.run(mem)` (no context) uses it, and `flow.run(mem, {
178
- ...overrides })` overrides it per call - so one module-level `Flow`, imported by many
179
- specs, doesn't force every caller into the same settings. `flow.run(context, mem)` still
180
- works exactly as before and ignores all of this, since that context is already launched.
181
- `withVerify(block, verify)` decorates any existing Block with different confirmation - same
182
- `act`/`resolve`, same routing - without that Block needing to have been authored as a
183
- parameterized factory (`withVerify(LoginBlock, [])` is the "I only care that I navigated
184
- this far, no DOM check" case). Built-in Trait factories are reachable two ways - as free
185
- functions (`urlMatches`, `textEquals`, `visible`) or, for autocomplete discoverability,
186
- off `Trait.` itself (`Trait.url`, `Trait.text`, `Trait.visible` - the exact same functions,
187
- just findable by typing `Trait.` without needing to already know their names). `Trait.url`
188
- takes a structured `URLPatternInit` (`{ pathname, hostname, search, hash, ... }`, the same
189
- shape the standard `URLPattern` Web API takes), not a hand-written regex - whichever
190
- components you leave out default to "match anything," so `Trait.url({ pathname:
191
- "/inventory.html" })` already ignores whatever query params a hybrid SPA tacks on, with no
192
- separate flag needed for that. A hand-written `Trait` isn't limited to the page, either - its
193
- `check(page, mem)` receives `mem` too, for the same-Checkpoint case: an action that doesn't
194
- navigate anywhere at all (clicking "+" to bump a cart item's quantity, say) has `In === Out`,
195
- which `defineBlock`/`defineFlow` already accept with no special-casing - `act()` updates `mem`
196
- alongside the real click (`mem.set(Quantity, mem.get(Quantity) + 1)`), and a bespoke `verify`
197
- Trait confirms the page's own displayed value actually matches what `mem` now expects
198
- (`shown === String(mem.get(Quantity))`), not just that *some* number is showing. `resolve()`
199
- itself stays exactly as pure as ever - it never sees `mem`, only `verify` does. `verify` only
200
- ever confirms what a Block itself just did, though - nothing previously checked whether the
201
- page was STILL there by the time the NEXT Block's `act()` starts. A `precondition` (same
202
- `Trait[]`/`(input) => Trait[]` shape as `verify`, checked automatically by `connect()`/
203
- `runGraph` right before `act()` runs, never called directly) closes that gap: a session
204
- timeout, a redirect, an interstitial popup - anything that changes the page in the moment
205
- between one Block finishing and the next one starting - fails loud with a clear "trait X
206
- failed before Y," instead of a confusing error deep inside `act()` trying to click something
207
- that's no longer there. Once Blocks are composed into a `Flow`, `flow.withBlockVerify`/
208
- `flow.modBlockVerify` patch one Block's verify from outside - a spec that only imports the
209
- finished flow, addressed by the Block reference (preferred), its name, or its numeric
210
- position in the flow - without editing the flow's own file. `composeBlock(name, steps)`
211
- chains several Blocks into one named unit the same way `connect()` already does - the
212
- multi-step-form case (a government form with several stepper pages, say), which is
213
- naturally "one feature" but still wants each step's own real `verify` independently
214
- overridable, not one giant Block with no per-step confirmation. `composedBlock.withStepVerify`/
215
- `modStepVerify` patch one step from outside, addressed the same three ways
216
- `withBlockVerify`/`modBlockVerify` already are - a `composeBlock` result is a plain Block,
217
- so it drops straight into `defineFlow([start, ..., composed, ..., end])` like any other.
218
- `fastForwardComposeBlock(name, steps)` is the same shape marked for **wall-clock
219
- fast-forward**: one opaque demo step that **blitzes** (no per-inner gate, no smooth
220
- cursor theater, Playwright slowMo off for that flow unless `WAYGRAPH_SLOWMO` is set)
221
- unless expanded. Put **several** FF units in one Flow when you need to race past auth,
222
- then a heavy dashboard settle, then watch the interesting middle.
223
-
224
- **Dispute / disable FF:** `waygraph demo --ff-disabled` (alias `--no-ff`) expands every
225
- FFCompose into its inner Blocks so a broken login step is visible as its own panel row.
226
- Former FF inners keep blitz pacing so wall-clock stays comparable to opaque FF (same
227
- real acts). `--ff-expand` is the older expand-only flag (same flatten). Saucedemo
228
- `checkoutFlow` uses `ff-owner-auth` for the login prefix; `loginFlow` stays expanded for
229
- Sign In narration.
230
-
231
- `waygraph traverse` walks the Block graph (Phase B-E). `--parallel N` (default
232
- `--session clone`) bootstraps one context, forks N workers with `storageState` + mem
233
- snapshot, partitions edges by hash, and claims via in-process edge leases (audit under
234
- `.waygraph-traverse/`). `--session inherit` is refused when `parallel > 1`. Phase E
235
- writes `.waygraph-traverse/coverage.json` and prints `[Coverage edges=H/T ratio=R%...]`;
236
- `--min-edge-coverage 80%` fails the suite with exit 2 when the ratio is below the gate
237
- (even if every worker leaf-PASSed). `--coverage-out PATH` / `--no-coverage-report` adjust
238
- the JSON. See `docs/proposals/traverse-ffcompose-rfc.md`.
642
+ ### Where this shows up in `waygraph auto`
643
+
644
+ A `NavBlock` always has wildcard `In` (`from: "*"` in the static graph), so it's reasonable to
645
+ wonder whether `nav-mailpit-inbox` clutters the live picker on every single screen. It
646
+ doesn't - `waygraph auto`'s menu builder already special-cases exactly this
647
+ (`src/auto-explore.ts`, `buildExploreMenu`): a URL-based Nav is excluded from the interactive
648
+ menu once you're on a known screen, offered only as a "Start here" bootstrap option. The
649
+ static graph still keeps the `from: "*"` edge for `waygraph graph`/`findBlockPath`/`traverse`,
650
+ so `waygraph auto --blocks "<AppCheckpoint> MailpitInbox"` can still route there deliberately -
651
+ it's just never suggested unprompted. The tested, recommended way to actually use this is an
652
+ authored flow/chain (`someAppFlow then mailVerifyFlow`), exactly like `mailVerifyFlow` itself.
653
+
654
+ ## Waygraph Pilot
655
+
656
+ Bootstraps a real, persistent, inspectable browser session for an **agent** to discover and
657
+ drive toward a natural-language goal it plans itself - not a tool that resolves one ask to one
658
+ Block internally. A real request is a multi-step task ("log in and buy the backpack for me",
659
+ "make a signature draft request") that needs an agent reasoning over a sequence of Blocks, not
660
+ a single best-matching edge. Comparable in shape to a browser-use-style agent, but the action
661
+ space is constrained to this project's own pre-verified Blocks (each with a real resolve/verify
662
+ already checked) instead of freeform DOM/pixel guessing - narrower, but far more accurate and
663
+ deterministic.
239
664
 
240
- **Richer demo narration:** automation rings are **gray**; authored stubs/slides can use
241
- `tone: "info" | "warning" | "danger" | "success"` (iconified captions), plus
242
- `size: "sm" | "md" | "lg"` and `weight: "normal" | "bold"`. Flow episode defaults:
243
- `withHighlightStyle(flow, { size, weight, tone? })` (slot/fixture wins). Episode pacing:
244
- `withDemoPace(flow, "fast" | "slow" | "normal" | "blitz" | number)` or `withBlockPace(block, ...)`
245
- (`number` `<=20` = scale vs normal, e.g. `0.5` / `2`; `>20` = absolute ms, e.g. `4500`).
246
- FFCompose stays blitz. Living objectives: `docs/proposals/OBJECTIVES.md`.
247
- `spawnTab(entry, page, mem)` drives a genuinely separate second tab through its own
248
- Block/Flow, in the same browser context an existing `page` already belongs to - the
249
- pattern `observe()` could already reach for (it's the only phase allowed to touch
250
- `page.context()`), now a named, discoverable, tested primitive instead of something
251
- you'd have to already know to hand-roll. `flow.run(context, mem, options)` also accepts a
252
- trailing optional options object - additively, so `flow.run(context, mem)` still behaves
253
- byte-for-byte as before: `options.page` drives that already-open page instead of opening
254
- a fresh tab (the run won't close a page you handed it unless you say so), and
255
- `options.closeOnFinish: false` leaves the run's page open and returns `{ result, page }`
256
- so the caller can keep driving it - hands a run tab back instead of losing it. See
257
- "Recipes" for capture-on-popup, which stays a documented pattern until a second real use
258
- case promotes it to an engine API.
259
- `defineNavBlock({ name, checkpoint, url })` or `defineNavClickBlock({ name, checkpoint, click })`
260
- builds a Block whose only possible action is navigating. Prefer `defineNavClickBlock` for
261
- click-nav in app code; `defineNavBlock` for `url` / `goto` deep links. Both accept a plain
262
- string or `(mem) => string` / `(mem) => selector`. Generated `act()` is always exactly
263
- `page.goto(url)` or `page.locator(click).click()`. For screen hubs that also register
264
- methods, prefer `definePageBlock` (see **0.9.0 — Page, Method, Sel** above). A regular
265
- `defineBlock` / `defineMethodBlock`'s `act()` receives `page` typed as `ActionPage` —
266
- `goto`/`reload`/`goBack`/`goForward` are `@deprecated` there (editor strike-through pointing
267
- at Nav/Page helpers). `waygraph check [project]` is the complementary whole-project sweep
268
- for contexts with no editor watching (CI, generated code).
269
- `new Engine({ browsers: { chromium, firefox, webkit } })` overrides which `BrowserType`
270
- actually launches for a `mem`-only run, per browser name - waygraph is deliberately "just
271
- an opinionated Playwright," so a stealth-patched or otherwise customized launcher (e.g.
272
- `playwright-extra` plus a stealth plugin) drops in unmodified, since those already expose
273
- the same `.launch()` shape as Playwright's own `chromium`/`firefox`/`webkit`.
665
+ ```bash
666
+ waygraph pilot start --non-headless
667
+ # {"sessionId":"bb65d74c","socketPath":"...","headless":false,"graph":{...},"snapshot":{...}}
668
+ ```
274
669
 
275
- See **Getting started** above for `try demo`, `create-waygraph`, and `waygraph init`.
276
- Both scaffold commands emit the same offline tree (route-shaped `demo-web/` +
277
- `methods/`, stubs, fixtures, one YAP slide). Layout:
278
- [`docs/scaffold.html`](./docs/scaffold.html) · `templates/scaffold/STRUCTURE.md`.
670
+ One call gives an agent everything it needs to start: a real, already-running Playwright
671
+ session (the exact mechanism `auto --cli --detach` already provides), and the whole project's
672
+ Block graph (`graph` - every Checkpoint/edge/description, not just what's reachable from the
673
+ current position, so the agent can plan several steps ahead). From there, the agent drives the
674
+ session itself, one pick at a time, using the session-control commands that already existed
675
+ before Pilot did:
279
676
 
280
- **Consumer layout (Next.js App Router):** block folders mirror `app/` page routes (`/`
281
- at the namespace root; no invented `landing/`/`root/`; sidebar = chrome + edges). See
282
- [`docs/consumer.html`](./docs/consumer.html) and mesh handout
283
- `WAYGRAPH-CONSUMER-CONVENTION.md` - separate from this package API doc.
677
+ ```bash
678
+ waygraph auto status <sessionId> # read the current menu (no side effects)
679
+ waygraph auto send <sessionId> "<pick>" # run one pick, get the resulting menu back
680
+ waygraph auto dom <sessionId> # inspect the live DOM/ARIA tree
681
+ waygraph auto trace <sessionId> # read the Checkpoint/Block history so far
682
+ waygraph auto reach <sessionId> <Checkpoint> # run a whole multi-step route in one call
683
+ waygraph auto resync <sessionId> # force here to re-detect from the real page
684
+ ```
284
685
 
285
- Deliberately **not yet implemented** (tracked on the project board):
286
- - Interstitials/Watchers - background overlay handling (cookie banners, popups).
287
- - Split `requires` (externally-supplied vs producedBy) recovery hints.
288
- - Federated multi-package pool of waygraphs.
686
+ Worked example (run for real against `examples/saucedemo`, no new execution code involved): an
687
+ agent gets a plain request - "log me in and buy the backpack" - reads the graph and the starting
688
+ `status`, reasons out login -> add-to-cart -> cart -> checkout info -> submit -> finish as the
689
+ route to `OrderComplete`, then drives it by repeatedly reading each `status`/`send` response and
690
+ picking the next index itself. The browser stays open the whole time; the user can watch it, or
691
+ check the final tab afterward.
692
+
693
+ **`auto reach <sessionId> <Checkpoint>`** runs a whole route in one call instead of one
694
+ `send` per step - real, reported pain for any non-trivial task on a rich graph ("the entire
695
+ prompts of the day" for one manual walkthrough). It path-finds via the same BFS `auto --blocks
696
+ <From> <To>` already uses, then runs each step directly against the session's own live
697
+ page/mem, verifying every step actually lands where its own graph edge promised (not just that
698
+ it ran without throwing - a Block can legitimately resolve elsewhere, e.g. a login submission
699
+ staying put after bad auth). **Real, stated limitation, not silently papered over:** for a
700
+ target only reachable by crossing a wildcard (`from: "*"`) edge whose real DOM precondition
701
+ depends on same-Checkpoint setup (e.g. filling form fields before a submit), neither `reach`
702
+ nor the already-shipped `auto --blocks` can safely route there automatically - both fail loud
703
+ within a bounded time (a real Playwright click/wait timeout) rather than silently guessing
704
+ wrong or hanging. Full design history - three rejected approaches, a real correctness bug this
705
+ change's own tests caught, and this limitation confirmed by direct reproduction - is in
706
+ `openspec/changes/waygraph-pilot/tasks.md`'s own M5 section.
707
+
708
+ **`auto resync <sessionId>`** - `here` is only ever updated by an action the session itself
709
+ ran (`send`/`reach` set it from a Block's own resolved Checkpoint; the raw `click`/`type`/
710
+ `goto` primitives null it because *they* just changed the page). Anything that changes the
711
+ page *outside* the session - a human clicking around in a visible `--non-headless` session
712
+ someone is co-driving, for instance - leaves `here` silently stale, since `send`/`reach` only
713
+ ever re-detect when the position is already unknown, never to double-check a known one.
714
+ `resync` forces that re-detection unconditionally, right now, against whatever the real page
715
+ actually shows. Doesn't crash or corrupt anything if you never need it - it's a real, found
716
+ gap (confirmed by reading the exact code path, `src/auto-session.ts`'s `currentMenu()`), fixed
717
+ outright rather than left as a caveat.
718
+
719
+ **On-page indicator.** A headful (`--non-headless`) session looks like an ordinary browser
720
+ tab otherwise - no sign anything is driving it, which matters once a human might be watching
721
+ or co-driving the same visible session. Every session now injects a small badge (bottom-right
722
+ corner: `Waygraph Pilot - session <id> - <Checkpoint>`) that click-expands into a panel
723
+ listing the current live menu's real edges (block name, kind, target Checkpoint,
724
+ description) - the same data `auto status` returns, rendered in the page itself. Both refresh
725
+ automatically after every `send`/`reach`/`reload`/`resync`/raw action - there's nothing
726
+ separate to call to keep it in sync. Particularly useful for Blind Pilot: watching the panel
727
+ update as newly-authored Blocks get picked up via `reload` shows the map actually growing,
728
+ live, not just a JSON diff in a terminal. Self-contained (`src/pilot-overlay.ts`), not built
729
+ on `waygraph demo`'s own panel/banner system, for the same `cli.ts`-side-effect reason
730
+ `pilot-v1`'s old narrate-mode ring wasn't either.
731
+
732
+ **Pilot itself does no planning, resolving, narrating, or acting** - `pilotStart`'s only job is
733
+ starting the session and handing back its graph in one round trip; see `src/pilot.ts`. An
734
+ earlier shape (`pilot ask "<text>"`, a deterministic ask-to-one-edge text matcher) was built,
735
+ shipped, and then explicitly rejected as not matching this goal - preserved for reference at git
736
+ tag `waygraph-pilot-v1-logs-prettified`, not on any active branch.
737
+
738
+ **Honest scope, not silently redefined:** this capability's own proof runs against an in-repo
739
+ example (`examples/saucedemo`), not a real external consumer application - it demonstrates the
740
+ mechanism works, not the "demoable on one real consumer" bar `ROADMAP.md` sets for `1.0.0`,
741
+ which remains separate, later, unmet work. Live reference: `tests/pilot/pilot.spec.ts` (the
742
+ library function) and `tests/cli/pilot.spec.ts` (the real CLI), both proven against live
743
+ saucedemo.com.
744
+
745
+ ## Blind Pilot (cold start, zero Blocks)
746
+
747
+ Waygraph Pilot above assumes a Block library already exists. Blind Pilot is the opposite
748
+ case: a session on a site nobody has described to this package yet. `AutoSession`/`auto --cli
749
+ --detach` already tolerate a project directory with **zero** `.block.ts` files (no crash,
750
+ `here` just stays `null`), so an agent can open a real session and start exploring cold with
751
+ no new code at all - `auto dom <sessionId>` already gives real DOM/ARIA visibility
752
+ independent of any Block library.
753
+
754
+ What's actually new here is two primitives an agent needs to go from "exploring" to "building
755
+ a Block library incrementally":
756
+
757
+ ```bash
758
+ waygraph auto click <sessionId> "<selector>" # act before any Block covers this
759
+ waygraph auto type <sessionId> "<selector>" "<text>" # same, for a real input
760
+ waygraph auto goto <sessionId> "<url>" # navigate the real live page
761
+ waygraph auto reload <sessionId> # pick up a Block just written to disk,
762
+ # without restarting the session
763
+ ```
289
764
 
290
- **Available now:** `waygraph auto [project]` is the **interactive explore** loop:
291
- `locate()` reads where you are, lists runnable Blocks from the graph, you pick one
292
- (headful panel or `--cli` terminal menu), repeat. Static graph export (old JSON /
293
- Mermaid) is `waygraph graph [project]` (`--mermaid`). Unattended JSON execution is
294
- `chain` + `WAYGRAPH_JSON=1`.
765
+ `click`/`type`/`goto` each re-detect the session's Checkpoint afterward (the same detection
766
+ `send`/`status` already use) and return a fresh snapshot - a selector matching nothing is a
767
+ reported failure naming it, not a silent no-op. `reload` re-discovers the project's Block
768
+ library/graph from disk in place, leaving the live page, mem, and current Checkpoint
769
+ untouched - so a Block an agent just wrote to disk becomes immediately runnable via the
770
+ session's existing `send`, without relaunching the browser.
771
+
772
+ **This package writes no Block content.** The driving agent recognizes a pattern ("this is a
773
+ login form") and writes the `.block.ts`/`*Sel`/mem-keys files itself, using the conventions
774
+ documented above - the same way it would write any other source file. Worked example (proven
775
+ end to end in `tests/cli/auto-session-blind-pilot.spec.ts` against an in-repo synthetic
776
+ fixture, `tests/fixtures/blind-pilot-site/`): start a session with zero Blocks, explore via
777
+ `dom`/`click`/`goto`, write one real `defineNavBlock` to disk, `reload`, then `send` it -
778
+ reaching the real Checkpoint it declares, proving it's genuinely driveable, not just visible.
779
+
780
+ **Two real gotchas found while proving this, worth knowing before debugging a silent
781
+ failure:** a `.block.ts` file that throws on import (e.g. a malformed Trait call) vanishes
782
+ from the graph with no error printed anywhere - `discoverGraph`'s per-file import errors are
783
+ mostly swallowed silently, so "reload did nothing" can mean a real mistake in the file just
784
+ written, not a package bug. And a Block not wired into any `.flow.ts` (`waygraph graph`'s own
785
+ "orphan" warning) is still fully visible and runnable in a live session's menu - orphan status
786
+ only affects `auto --blocks <From> <To>` path-finding, not `auto`'s own menu.
787
+
788
+ **Honest scope:** Blind Pilot does not currently author into the Waygraph Map convention
789
+ below by default - it still writes plain files wherever it's pointed. Proof stays against an
790
+ in-repo synthetic fixture, not a real external site.
791
+
792
+ ## Waygraph Map
793
+
794
+ An opinionated, Next.js/Nuxt-style folder convention where a project's own directory
795
+ structure mechanically *is* its map - not a separate schema file to generate, maintain, or
796
+ let drift out of sync. Needs zero new engine code: `discoverGraph`/`loadBlockLibrary` already
797
+ recurse through any folder structure, matching only the `.block.ts` filename pattern - folder
798
+ naming and depth are already 100% cosmetic to every existing command.
799
+
800
+ ```text
801
+ src/map/
802
+ (app_base)/ # the landing/base group - purely organizational, never part of
803
+ dashboard/ # any Checkpoint tag. One folder per Checkpoint - "Dashboard"
804
+ _page.block.ts # arrival hub (definePageBlock) - fixed name, every folder
805
+ _nav.block.ts # navigation (defineNavBlock) - fixed name, every folder
806
+ _sel.ts # selectors for this Checkpoint - fixed name, every folder
807
+ _methods/ # this Checkpoint's own action Blocks - fixed name, every
808
+ click-widget.block.ts # folder. Contents keep their own descriptive names -
809
+ (external)/ # only the folder itself is fixed.
810
+ docs/ # a genuinely cross-origin group, same shape - "Docs"
811
+ _page.block.ts
812
+ _nav.block.ts
813
+ _sel.ts
814
+ ```
295
815
 
296
- **Friendly demo / run / auto (0.10.5 — flows are files):**
816
+ **Not just a naming preference - a forced convention.** Folder nesting under `map/(group)/`
817
+ must **verbatim-match** the real site's own URL path segments, `(group)` parens excluded
818
+ (purely organizational, same as a Next.js route group): `/app/chats` lives at
819
+ `map/(app_base)/app/chats/`, never `map/(auth)/chats/` for a fabricated grouping that was
820
+ never a real `/auth/*` URL. `waygraph map [project]` (see [CLI reference](#cli-reference))
821
+ enforces this mechanically and exits 1 on a mismatch - don't rely on review alone. Same Block
822
+ helpers as today's freeform "manual mode" (`definePageBlock`/`defineNavBlock`/
823
+ `defineMethodBlock`/etc.) either way - Map only changes *where files live and what they're
824
+ named*, never what kind of Block authors them or how they execute. A project adopts it
825
+ incrementally, folder by folder. Prefer [`map()`](#map---a-kind-checked-no-teleporting-flow-builder)
826
+ over a hand-assembled `defineFlow([start, ...])` array when wiring these Blocks into a Flow -
827
+ it's the one that can't silently accept a Block that didn't come from a real factory.
828
+
829
+ **Leading underscore = this Checkpoint's own fixed machinery, never a route segment.**
830
+ `_nav.block.ts`/`_page.block.ts`/`_sel.ts`/`_methods/` (same private-folder idea as Next.js's
831
+ own `_folder` convention) mark "this belongs to the endpoint whose folder it's directly in,"
832
+ so a bare (non-underscore, non-`(group)`) folder name always means a real child
833
+ Checkpoint/URL segment - `_methods/` can never be mistaken for a sibling endpoint folder the
834
+ way a bare `methods/` structurally could be, since only ONE of those two readings is
835
+ mechanically distinguishable at a glance. `waygraph map`'s own folder-vs-URL comparison
836
+ excludes underscore segments the same way it excludes `(group)` ones - `_methods/foo.block.ts`
837
+ compares against the URL as if `_methods/` weren't there at all. Individual files inside
838
+ `_methods/` keep their own descriptive names (`join-pool.method.block.ts`, not
839
+ `_join-pool...`) - they're hand-authored per-project content, not fixed convention names; the
840
+ `_methods/` folder boundary alone already marks the whole subtree as non-route.
841
+
842
+ **"waypack" - loading another project's Blocks needs no separate manifest either.** A second
843
+ project depending on a Map-convention project as a plain `file:` dependency (the same
844
+ mechanism `examples/saucedemo`'s own `"waygraph": "file:../.."` already uses) is immediately
845
+ understandable: `waygraph graph node_modules/<loaded-package>` correctly discovers its full
846
+ Checkpoint/edge structure, with nothing generated or maintained separately. Proven end to end
847
+ in `examples/routed-demo-consumer/` against `examples/routed-demo/`.
848
+
849
+ **A real bug this proof surfaced, fixed, not left as a caveat:** a *live* `--detach` session
850
+ against a package loaded through a consumer project's own deeply nested `node_modules/<pkg>`
851
+ path used to fail with `listen EINVAL` - the session's Unix socket lived under the project
852
+ directory, and that absolute path could exceed the OS's AF_UNIX limit (~108 bytes on Linux).
853
+ Fixed: session sockets now live in a short, fixed location (`os.tmpdir()/waygraph-auto/`),
854
+ independent of how deeply the project itself is nested - confirmed against the exact
855
+ previously-failing path (`auto --cli --detach`, `auto reach`, and quit all working normally).
856
+ Session metadata (`.waygraph-auto/<id>.json`, discoverable per-project) is unaffected - only
857
+ the socket moved.
858
+
859
+ Full design history (why "Router" isn't a separate thing to build, two prior wrong readings
860
+ of this before landing here, and everything above stated as requirements) in
861
+ `openspec/changes/waygraph-map/`.
862
+
863
+ ## Migrating from freeform ("router") to Waygraph Map
864
+
865
+ "Router" isn't a separate mode to turn off - it's this project's own name for today's
866
+ freeform, folder-organized style (`src/blocks/<slug>/`, any nesting, any file names).
867
+ Migrating means moving those files under `src/map/(group)/<page-slug>/` with verbatim
868
+ URL-matching names, nothing more - no engine change, no new Block helpers, no flag to flip.
869
+ Do it incrementally: the two styles coexist in the same project (and even the same Flow) with
870
+ no special-casing, since `discoverGraph`/`loadBlockLibrary` only ever match the `.block.ts`
871
+ filename pattern, never folder depth or naming.
872
+
873
+ 1. **List every real URL your NavBlocks/PageBlocks already navigate to.** For each one with a
874
+ static string `url`, note its real path. For each one that's `click`-based (no `url` field
875
+ at all) or a dynamic `(mem) => string` function, note that too - neither is statically
876
+ checkable by `waygraph map`, so they can go wherever makes sense; only a literal string
877
+ `url` is verified.
878
+ 2. **Pick your group(s).** `(app_base)/` for the landing/base app (organizational only, never
879
+ part of a Checkpoint tag); `(external)/` for genuinely cross-origin Blocks (mail tools,
880
+ third-party embeds) - verify with the real URL, not the folder's own current name, since a
881
+ folder can already be mis-grouped (a real bug this session found: a project's own
882
+ `(external)/docs/` was actually same-origin the whole time).
883
+ 3. **Create `src/map/(group)/<verbatim-url-segments>/` per Checkpoint**, `git mv` each
884
+ Block/Sel file in, and rename the fixed ones: `nav.block.ts` -> `_nav.block.ts`,
885
+ `page.block.ts` -> `_page.block.ts`, your selector file -> `_sel.ts` (drops its slug - now a
886
+ fully fixed name), `methods/` -> `_methods/` (contents keep their own names unchanged).
887
+ **Two Checkpoints sharing one real URL** (a DOM-state distinction, not a URL one - e.g. a
888
+ "joined" vs "not joined" variant of the same page) can't both use the bare fixed names in
889
+ one folder; give each pair a slug prefix instead (`app-home._nav.block.ts` /
890
+ `in-pool._nav.block.ts`, or similar) rather than nesting either under its own subfolder
891
+ (that would add a folder segment the real URL doesn't have, and fail step 5).
892
+ 4. **A dynamic per-instance URL** (a real chat/order/ticket id, not a fixed route) - if it's
893
+ currently a plain string constant, wrap it as `url: () => thatConstant` instead. A bare
894
+ string is always treated as a real, checkable static route; a function is correctly treated
895
+ as dynamic/non-canonical and skipped, matching how you'd actually navigate a real instance of
896
+ it in practice anyway.
897
+ 5. **Fix every import** the moves break (states/mem-key paths change depth; sibling
898
+ nav/page/methods imports need their new `_`-prefixed names) - let the compiler do this for
899
+ you: `tsc --noEmit` if the project has one, otherwise `waygraph check [project]` and
900
+ `waygraph graph [project]` (both do real dynamic imports of every Block file and report
901
+ import failures by name) are just as effective a proof.
902
+ 6. **Run `waygraph map [project]`.** Exit 0 / "0 violations" is the actual finish line - not a
903
+ subjective review. A remaining violation names the exact file, its real folder path, and the
904
+ real URL it doesn't match; fix the folder, not the check.
905
+ 7. **Diff `waygraph graph`'s node/edge/skipped/orphan counts against before the move.** A pure
906
+ migration changes zero Block behavior - if a count changed, something broke in transit
907
+ (a bad import silently dropped a Block from discovery is the usual cause), not something to
908
+ wave off as "close enough."
909
+
910
+ Once the folder structure is settled, prefer building/rebuilding Flows over these Blocks with
911
+ [`map()`](#map---a-kind-checked-no-teleporting-flow-builder) instead of a hand-assembled
912
+ `defineFlow([start, ...])` array - it's the one that can't silently accept a Block that isn't
913
+ really a Nav/Page/Assert/Method from this package's own factories.
914
+
915
+ ## Getting started (pick one)
916
+
917
+ | Goal | Command |
918
+ |------|---------|
919
+ | Watch Sauce Demo step-through (temp dir only) | `npx waygraph try demo` |
920
+ | Interactive explore (Add/Remove/Open details from live page) | `cd examples/saucedemo && npm i && npx waygraph auto` |
921
+ | Scaffold a new **offline** project (green `npm test` on local fixture `:4177`) | `npx create-waygraph my-app` **or** `npx waygraph init my-app` |
922
+ | Install coding-agent defs for diving an app into Blocks | `npx waygraph agent-dive --loop claude` (also `opencode` / `cursor` / `vscode`) |
923
+ | Add waygraph to an existing repo | `npm install waygraph @playwright/test` |
924
+
925
+ **Scaffold is not hidden inside waygraph alone** - the offline starter also ships as
926
+ [`create-waygraph`](https://github.com/deviate-dv8/create-waygraph) on npm (same template
927
+ as `waygraph init` since 0.7.5). Use whichever entry you already have:
928
+ `npx create-waygraph my-app` when reading the GitHub Pages docs, or `npx waygraph init my-app`
929
+ when the CLI is already installed. After either:
930
+
931
+ ```bash
932
+ cd my-app && npm install && npx playwright install chromium && npm test
933
+ ```
934
+
935
+ `try demo` is different: live saucedemo.com, step-through **Sign In -> Shop & Checkout ->
936
+ blocked Viewer login**, then headless tests. Permanent full example:
937
+ [`examples/saucedemo`](./examples/saucedemo). Offline empty scaffold: `init` /
938
+ `create-waygraph`.
939
+
940
+ ```bash
941
+ # From this checkout, after npm run build:
942
+ cd examples/saucedemo && npm install && npx playwright install chromium
943
+ npm test # live Playwright suite
944
+ npm run auto # headed interactive explore
945
+ npm run demo:step # stepper (manual Next)
946
+ npm run demo:autoplay # stepper with Auto-advance
947
+ ```
948
+
949
+ Both scaffold commands emit the same offline tree (route-shaped `demo-web/` + `methods/`,
950
+ stubs, fixtures, one YAP slide). Layout: [`docs/scaffold.html`](./docs/scaffold.html) ·
951
+ `templates/scaffold/STRUCTURE.md`.
952
+
953
+ **Consumer layout (Next.js App Router):** block folders mirror `app/` page routes (`/` at
954
+ the namespace root; no invented `landing/`/`root/`; sidebar = chrome + edges). See
955
+ [`docs/consumer.html`](./docs/consumer.html). A separate mesh handout
956
+ (`WAYGRAPH-CONSUMER-CONVENTION.md`) covers the same convention outside this repo - not
957
+ part of this package's own tree.
958
+
959
+ ## CLI reference
960
+
961
+ `waygraph auto [project]` is the **interactive explore** loop: `locate()` reads where you
962
+ are, lists runnable Blocks from the graph, you pick one (headful panel or `--cli` terminal
963
+ menu), repeat. Static graph export (old JSON / Mermaid) is `waygraph graph [project]`
964
+ (`--mermaid`). Unattended JSON execution is `run` + `WAYGRAPH_JSON=1`.
965
+
966
+ **`--cli` session control:** `waygraph auto --cli` on its own is a blocking terminal loop -
967
+ fine for a person, awkward for an agent that needs to inspect state between picks or send
968
+ one command at a time without guessing a whole input sequence upfront. `--detach` runs that
969
+ same session as a background socket server instead (session identity under
970
+ `.waygraph-auto/` in the target project, mirroring `.waygraph-traverse/`'s convention);
971
+ `auto send <sessionId> "<pick>"` and `auto status <sessionId>` are non-interactive
972
+ request/response calls against it (no TTY needed), and `auto attach <sessionId>` reopens an
973
+ interactive terminal against an already-running session. `send q`/`send quit` ends the
974
+ session and cleans up its socket/metadata. `--detach` defaults to headless;
975
+ `--non-headless` (the same flag `run`/`demo` already use) launches a real visible browser
976
+ window while the session stays driven entirely through `send`/`status`/`attach`/`dom` - not
977
+ a new picker UI, and the existing page-embedded headful panel (bare `waygraph auto`, no
978
+ `--cli`) is untouched either way.
979
+
980
+ **Session history:** `auto trace <sessionId>` returns a Checkpoint/Block-level record of
981
+ what the session actually did - Block name, Checkpoint before/after, and (when the Block
982
+ authors them) its resolved `stubBefore`/`stubAfter`/`stubOnError` demo-narration fixtures
983
+ (the same highlight/todo/device data `waygraph demo`'s own lifecycle logging computes via
984
+ `runStubPhase`, reused here since it's pure data with no browser side effects). Deliberately
985
+ not a raw action recorder - no clicks/fills, only the same Block-level resolution the rest of
986
+ the engine already reasons about, capped at the last 500 steps.
987
+
988
+ **Reading the live page:** `auto dom <sessionId>` gives an agent driving a session read-only
989
+ visibility into the actual page, without reading the target project's frontend source.
990
+ Default `--mode aria` returns Playwright's AI-oriented accessibility snapshot
991
+ (`ariaSnapshotJSON({ mode: "ai" })`) - small and usually enough to find "the Username
992
+ textbox" or "the Login button." `--mode full` returns a bounded raw DOM subtree
993
+ (tag/attributes/text/children - useful when you need actual CSS classes or data-attributes
994
+ `aria` can't see), hard-capped on depth/node count/text length and marked `truncated: true`
995
+ whenever a cap is hit, since this is read by an LLM, not a human. `--selector <sel>` scopes
996
+ either mode to one element's subtree instead of the whole page - a modifier on the mode, not
997
+ a third mode.
998
+
999
+ **Agent bootstrap:** `waygraph pilot start` starts a `--detach` session and reads back the
1000
+ whole project graph in one call, so an agent can begin planning a multi-step request
1001
+ immediately - see "Waygraph Pilot" above for the full picture.
1002
+
1003
+ **Cold start (zero Blocks):** `auto click/type/goto <sessionId>` act on the live page even
1004
+ when no Block covers that action yet, and `auto reload <sessionId>` picks up a Block just
1005
+ written to disk without restarting the session - see "Blind Pilot" above.
1006
+
1007
+ Flows are files (0.10.5+):
297
1008
 
298
1009
  | Want | Command |
299
1010
  |------|---------|
300
- | List flows | `waygraph list` → `src/flows/shop.flow.ts shopFlow` |
301
- | Run by file | `waygraph run src/flows/shop.flow.ts --data '{…}'` |
302
- | Same via `auto` | `waygraph auto src/flows/shop.flow.ts --data '{…}'` |
1011
+ | List flows | `waygraph list` -> `src/flows/shop.flow.ts shopFlow` |
1012
+ | Run by file | `waygraph run src/flows/shop.flow.ts --data '{...}'` |
1013
+ | Same via `auto` | `waygraph auto src/flows/shop.flow.ts --data '{...}'` |
303
1014
  | Run by export | `waygraph run --blocks shopFlow` |
304
1015
  | Manual watch | `waygraph demo src/flows/shop.flow.ts` |
305
1016
  | Auto-advance | `waygraph demo --blocks shopFlow --auto-next` (nav auto-hides strip) |
306
- | Fast / full strip | `waygraph demo … --fast` (shorter gates; keeps cursor) · `--full` (classic chips; default = carousel) |
1017
+ | Fast / full strip | `waygraph demo ... --fast` (shorter gates; keeps cursor) · `--full` (classic chips; default = carousel) |
307
1018
  | QA watch + record | `waygraph demo --blocks shopFlow --auto-play-video` |
308
- | Ad-hoc Blocks | `waygraph run --blocks "login then nav-cart" --data '{…}'` |
1019
+ | Ad-hoc Blocks | `waygraph run --blocks "login then nav-cart" --data '{...}'` |
309
1020
  | Headed execute | `waygraph run --blocks shopFlow --non-headless --video` |
310
1021
  | Explore | `waygraph auto` / `waygraph auto --cli` |
311
1022
  | Path-find | `waygraph auto --blocks LoginPage OrderComplete` |
1023
+ | Run `--cli` as a background session | `waygraph auto --cli --detach` -> `{sessionId, socketPath}` |
1024
+ | Drive a detached session (no TTY) | `waygraph auto send <sessionId> "<pick>"` -> JSON state |
1025
+ | Read a detached session's state | `waygraph auto status <sessionId>` (no side effects) |
1026
+ | Reattach a terminal to a detached session | `waygraph auto attach <sessionId>` |
1027
+ | Read the live page (aria, small/default) | `waygraph auto dom <sessionId>` |
1028
+ | Read the live page (raw DOM, bounded) | `waygraph auto dom <sessionId> --mode full` |
1029
+ | Scope either to one element | `waygraph auto dom <sessionId> --selector ".inventory_list"` |
1030
+ | Detached session with a real visible browser | `waygraph auto --cli --detach --non-headless` |
1031
+ | Read the session's Checkpoint/Block history | `waygraph auto trace <sessionId>` |
1032
+ | Whole-project nav-escape + inline-selector + orphan sweep | `waygraph check [project]` |
1033
+ | Waygraph Map enforcement (verbatim url-vs-folder, exits 1 on a violation) | `waygraph map [project]` |
1034
+ | Graph crawl | `waygraph traverse [project] --parallel N --min-edge-coverage 80%` |
1035
+ | Coding-agent defs | `waygraph agent-dive --loop claude` |
1036
+ | One-shot temp-dir demo | `waygraph try demo` / `waygraph try auto` / `waygraph try auto:cli` |
312
1037
 
313
1038
  `--blocks` / positional accepts a Flow export, a `.flow.ts` path, or `"a then b"`.
314
1039
  `auto <file.flow.ts>` **runs** that flow (same as `run`); bare `auto` still explores.
315
- `--auto-next` (alias `--autoplay`) = panel Auto-advance. `--auto-play-video` is **demo only**.
316
- `chain` remains a compat alias for `run --blocks` / `auto --blocks`.
1040
+ `--auto-next` (alias `--autoplay`) = panel Auto-advance. `--auto-play-video` is **demo
1041
+ only**. `chain` remains a compat alias for `run --blocks` / `auto --blocks`.
1042
+
1043
+ Run `waygraph --help` (or any subcommand with no args) for the full flag reference kept
1044
+ in `src/cli.ts`'s own `usage()` - that's the source of truth for flags, this table is the
1045
+ scannable summary.
317
1046
 
318
1047
  ## Example
319
1048
 
@@ -372,18 +1101,13 @@ const result = await flow.run(context, mem);
372
1101
  // result === { __state: "Submitted" }
373
1102
  ```
374
1103
 
375
- See `tests/define-flow.spec.ts` here, and `examples/saucedemo/tests/checkout-flow.spec.ts`
376
- (a real multi-Block flow against live saucedemo.com), for this running for real. The
377
- lower-level `connect()`/`runGraph()` still exist and are what `defineFlow` builds on -
378
- reach for them directly only if you need a shape `defineFlow`'s array can't express yet.
379
-
380
1104
  ## Recipes
381
1105
 
382
1106
  ### Detect / drive the surviving tab after a run
383
1107
 
384
1108
  The default run closes the page it opened, and a page you hand in via `options.page`
385
- survives. For a flow that must BE the surviving tab after it returns (an interactive demo,
386
- a hand-back to a caller), ask for the page back explicitly:
1109
+ survives. For a flow that must BE the surviving tab after it returns (an interactive
1110
+ demo, a hand-back to a caller), ask for the page back explicitly:
387
1111
 
388
1112
  ```typescript
389
1113
  const { result, page } = await registerFlow.run(context, mem, { closeOnFinish: false });
@@ -402,7 +1126,7 @@ event armed before the click that is expected to open exactly one popup:
402
1126
  let popup: Page | undefined;
403
1127
  context.once("page", (p) => { popup = p; });
404
1128
  await linkHandle.click(); // the block that causes the popup (e.g. the
405
- // MailHog email's real verify link)
1129
+ // MailHog email's real verify link)
406
1130
  if (popup) {
407
1131
  await popup.waitForLoadState(); // fully loaded before calling into the engine
408
1132
  await verifyLinkFlow.run(context, mem, { page: popup, closeOnFinish: false });
@@ -431,9 +1155,9 @@ npm run build # tsc -p tsconfig.build.json, emits dist/
431
1155
 
432
1156
  ### Blind-agent overlay gate (anti-blank)
433
1157
 
434
- Every live modal (`#wg-panel`, `#wg-banner`, `#wg-auto-panel`) stamps
435
- `data-wg-ui` / `data-wg-modal` / `data-wg-ready="1"`. Do **not** claim the
436
- stepper works unless one of these passes:
1158
+ Every live modal (`#wg-panel`, `#wg-banner`, `#wg-auto-panel`, `#wg-todo-dock`) stamps
1159
+ `data-wg-ui` / `data-wg-modal` / `data-wg-ready="1"`. Do **not** claim the stepper works
1160
+ unless one of these passes:
437
1161
 
438
1162
  ```bash
439
1163
  # Playwright helper (exported from waygraph)
@@ -446,32 +1170,46 @@ WAYGRAPH_PROVE_EXIT=1 WAYGRAPH_PROVE_SHOT=/tmp/wg.png \
446
1170
  # stderr: WAYGRAPH_PROVE {"ok":true,"beacons":[...],"readyAttr":"1"}
447
1171
  ```
448
1172
 
449
- Tests: `tests/cli/overlay-beacon.spec.ts`. Helpers: `src/overlay-beacon.ts`.
1173
+ Tests: `tests/cli/overlay-beacon.spec.ts`. Helpers: `src/overlay-beacon.ts`. Rules for
1174
+ touching the stepper/overlay chrome itself: `src/CLI-STOMP-GUARD.md`.
450
1175
 
451
1176
  ## Layout
452
1177
 
453
1178
  ```
454
- package.json the "waygraph" npm package itself (0.9.0+)
1179
+ package.json the "waygraph" npm package itself
455
1180
  src/
456
1181
  types.ts Checkpoint, Instruction, Block, connect()
457
1182
  mem-page.ts MemKey, key(), MemPage
458
1183
  trait.ts Trait (type + discoverable Trait.url/.text/.visible), runVerify()
459
- engine.ts runGraph(), definePageBlock, defineMethodBlock, Engine, …
1184
+ engine.ts runGraph(), definePageBlock, defineMethodBlock, Engine, ...
460
1185
  graph.ts discoverGraph, orphans, paths
461
- auto-explore.ts waygraph auto menu
462
- index.ts public barrel
1186
+ auto-explore.ts waygraph auto menu building
1187
+ auto-explore-run.ts waygraph auto interactive loop (headful + --cli)
1188
+ highlights.ts stub/fixture/slide/todo-dock/device narration types + helpers
1189
+ step-overlay.ts demo step overlay chrome (panel, ring, cursor)
1190
+ overlay-beacon.ts blind-agent overlay readiness beacons
1191
+ traverse-run.ts waygraph traverse crawl (serial/parallel)
1192
+ traverse-coverage.ts traverse coverage report + CI gate
1193
+ traverse-lease.ts traverse edge lease coordinator
1194
+ blocks-select.ts shared --blocks glob/regex/bare grammar
1195
+ agent-dive.ts waygraph agent-dive coding-agent scaffolding
1196
+ cli.ts waygraph CLI entrypoint (all subcommands)
1197
+ index.ts public barrel
463
1198
  examples/saucedemo/ live Sauce Demo (Page + methods/ + Sel)
464
1199
  templates/quickstart/ try demo / init template (mirrors sauce)
465
- docs/ GitHub Pages static HTML
466
- typecheck/ compile-time-only fixtures
1200
+ templates/scaffold/ offline init/create-waygraph scaffold template
1201
+ templates/agents/ waygraph agent-dive coding-agent definitions
1202
+ docs/ GitHub Pages static HTML + docs/proposals/ RFCs
1203
+ openspec/ spec-driven planning: specs/ (current), changes/ (in-flight),
1204
+ changes/archive/ (shipped)
467
1205
  tests/ runtime unit + integration tests (@playwright/test)
468
1206
  tsconfig.json base config
469
1207
  tsconfig.build.json emits to dist/
470
1208
  ```
471
1209
 
472
1210
  In-package Sauce Demo: [`examples/saucedemo`](./examples/saucedemo). Other consumer
473
- experiments (e.g. zsign-app integration) may still live in a sibling workspace folder and
474
- depend on this package via `"waygraph": "file:../waygraph"` during local dev (see that
1211
+ experiments may still live in a sibling workspace folder and depend on this package via
1212
+ `"waygraph": "file:../waygraph"` during local dev (see that
475
1213
  project's own `.npmrc` - `install-links=true` is required there, or npm will symlink
476
1214
  instead of copy and pull this package's own `node_modules` in through the symlink, causing
477
1215
  a duplicate-Playwright-installation error).