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.
- package/README.md +970 -232
- package/assets/stubs/stub.mp4 +0 -0
- package/assets/stubs/stub.pdf +32 -0
- package/assets/stubs/stub.png +0 -0
- package/dist/agent-dive.d.ts +9 -0
- package/dist/agent-dive.js +32 -0
- package/dist/auto-explore-run.d.ts +20 -2
- package/dist/auto-explore-run.js +9 -8
- package/dist/auto-explore.js +22 -2
- package/dist/auto-session-ipc.d.ts +123 -0
- package/dist/auto-session-ipc.js +381 -0
- package/dist/auto-session.d.ts +342 -0
- package/dist/auto-session.js +760 -0
- package/dist/cli.js +450 -51
- package/dist/engine.d.ts +201 -1
- package/dist/engine.js +312 -2
- package/dist/graph.d.ts +18 -0
- package/dist/graph.js +65 -2
- package/dist/highlights.d.ts +20 -1
- package/dist/highlights.js +60 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.js +5 -3
- package/dist/map-check.d.ts +18 -0
- package/dist/map-check.js +140 -0
- package/dist/pilot-overlay.d.ts +88 -0
- package/dist/pilot-overlay.js +363 -0
- package/dist/pilot.d.ts +49 -0
- package/dist/pilot.js +53 -0
- package/dist/trait.d.ts +22 -0
- package/dist/trait.js +60 -3
- package/dist/types.d.ts +21 -1
- package/dist/types.js +17 -1
- package/examples/routed-demo/.waygraph-auto/4d4902b7.log +0 -0
- package/examples/routed-demo/.waygraph-auto/51ef462e.log +0 -0
- package/examples/routed-demo/.waygraph-auto/71527ad9.log +0 -0
- package/examples/routed-demo/.waygraph-auto/ca16778e.log +0 -0
- package/examples/routed-demo/package.json +26 -0
- package/examples/routed-demo/playwright.config.ts +20 -0
- package/examples/routed-demo/scripts/fixture-server.mjs +37 -0
- package/examples/routed-demo/scripts/with-fixture.mjs +70 -0
- package/examples/routed-demo/src/flows/routed-demo-map.flow.ts +39 -0
- package/examples/routed-demo/src/flows/routed-demo.flow.ts +12 -0
- package/examples/routed-demo/src/map/(app_base)/dashboard/_methods/click-widget.block.ts +23 -0
- package/examples/routed-demo/src/map/(app_base)/dashboard/_nav.block.ts +18 -0
- package/examples/routed-demo/src/map/(app_base)/dashboard/_page.block.ts +22 -0
- package/examples/routed-demo/src/map/(app_base)/dashboard/_sel.ts +10 -0
- package/examples/routed-demo/src/map/(external)/docs/_nav.block.ts +21 -0
- package/examples/routed-demo/src/map/(external)/docs/_page.block.ts +15 -0
- package/examples/routed-demo/src/map/(external)/docs/_sel.ts +7 -0
- package/examples/routed-demo/src/pages/dashboard.html +10 -0
- package/examples/routed-demo/src/pages/docs.html +8 -0
- package/examples/routed-demo/src/states/routed-demo.states.ts +10 -0
- package/examples/routed-demo/test-results/.last-run.json +4 -0
- package/examples/routed-demo/tests/routed-demo-map.spec.ts +17 -0
- package/examples/routed-demo/tests/routed-demo.spec.ts +75 -0
- package/examples/routed-demo/tsconfig.json +18 -0
- package/examples/routed-demo-consumer/package.json +18 -0
- package/examples/saucedemo/.waygraph-auto/00d5336e.log +0 -0
- package/examples/saucedemo/.waygraph-auto/010dd92b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/011ad671.log +0 -0
- package/examples/saucedemo/.waygraph-auto/01b45645.log +0 -0
- package/examples/saucedemo/.waygraph-auto/03764153.log +0 -0
- package/examples/saucedemo/.waygraph-auto/0593587e.log +0 -0
- package/examples/saucedemo/.waygraph-auto/06d12823.log +0 -0
- package/examples/saucedemo/.waygraph-auto/083cc5b9.log +0 -0
- package/examples/saucedemo/.waygraph-auto/0b1f5b26.json +8 -0
- package/examples/saucedemo/.waygraph-auto/0c00f25f.log +0 -0
- package/examples/saucedemo/.waygraph-auto/0e480591.log +0 -0
- package/examples/saucedemo/.waygraph-auto/104751ef.log +0 -0
- package/examples/saucedemo/.waygraph-auto/110958b8.log +0 -0
- package/examples/saucedemo/.waygraph-auto/138107c1.log +0 -0
- package/examples/saucedemo/.waygraph-auto/159d541b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/16aa2c01.log +0 -0
- package/examples/saucedemo/.waygraph-auto/16c35dd5.log +0 -0
- package/examples/saucedemo/.waygraph-auto/1706547d.log +0 -0
- package/examples/saucedemo/.waygraph-auto/17537e42.log +0 -0
- package/examples/saucedemo/.waygraph-auto/175655f7.log +0 -0
- package/examples/saucedemo/.waygraph-auto/17ede493.log +0 -0
- package/examples/saucedemo/.waygraph-auto/1fe2414f.log +0 -0
- package/examples/saucedemo/.waygraph-auto/1fe2e3b8.log +0 -0
- package/examples/saucedemo/.waygraph-auto/2219f00b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/23e62571.log +0 -0
- package/examples/saucedemo/.waygraph-auto/25f68ada.log +0 -0
- package/examples/saucedemo/.waygraph-auto/26ca9cd8.log +0 -0
- package/examples/saucedemo/.waygraph-auto/2bb6c4e8.log +0 -0
- package/examples/saucedemo/.waygraph-auto/2d425004.log +0 -0
- package/examples/saucedemo/.waygraph-auto/2f5b3ea6.log +0 -0
- package/examples/saucedemo/.waygraph-auto/31bb303d.log +0 -0
- package/examples/saucedemo/.waygraph-auto/31f2042f.log +0 -0
- package/examples/saucedemo/.waygraph-auto/36551ec1.log +0 -0
- package/examples/saucedemo/.waygraph-auto/36a1c274.log +0 -0
- package/examples/saucedemo/.waygraph-auto/381aa338.log +0 -0
- package/examples/saucedemo/.waygraph-auto/3882bce1.log +0 -0
- package/examples/saucedemo/.waygraph-auto/38acd1dc.log +0 -0
- package/examples/saucedemo/.waygraph-auto/3b8b77b9.log +0 -0
- package/examples/saucedemo/.waygraph-auto/4028d2a2.log +0 -0
- package/examples/saucedemo/.waygraph-auto/4168b208.log +0 -0
- package/examples/saucedemo/.waygraph-auto/42cc9113.log +0 -0
- package/examples/saucedemo/.waygraph-auto/439d30aa.log +0 -0
- package/examples/saucedemo/.waygraph-auto/43ab66ce.log +0 -0
- package/examples/saucedemo/.waygraph-auto/43f66838.log +0 -0
- package/examples/saucedemo/.waygraph-auto/442171d4.log +0 -0
- package/examples/saucedemo/.waygraph-auto/453b1ce1.log +0 -0
- package/examples/saucedemo/.waygraph-auto/457322bd.log +0 -0
- package/examples/saucedemo/.waygraph-auto/46197027.log +0 -0
- package/examples/saucedemo/.waygraph-auto/46889468.log +0 -0
- package/examples/saucedemo/.waygraph-auto/4890da1c.log +0 -0
- package/examples/saucedemo/.waygraph-auto/4893a63b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/4a85f496.log +0 -0
- package/examples/saucedemo/.waygraph-auto/4af71440.log +0 -0
- package/examples/saucedemo/.waygraph-auto/4f7496d1.log +0 -0
- package/examples/saucedemo/.waygraph-auto/50e8baaa.log +0 -0
- package/examples/saucedemo/.waygraph-auto/52b3a455.log +0 -0
- package/examples/saucedemo/.waygraph-auto/532dd9b8.log +0 -0
- package/examples/saucedemo/.waygraph-auto/5391ce40.log +0 -0
- package/examples/saucedemo/.waygraph-auto/57dbddb3.log +0 -0
- package/examples/saucedemo/.waygraph-auto/5897f501.log +0 -0
- package/examples/saucedemo/.waygraph-auto/59c0b57d.log +0 -0
- package/examples/saucedemo/.waygraph-auto/5b511794.log +0 -0
- package/examples/saucedemo/.waygraph-auto/5c0eef7f.log +0 -0
- package/examples/saucedemo/.waygraph-auto/5c47910f.log +0 -0
- package/examples/saucedemo/.waygraph-auto/5d60da77.log +0 -0
- package/examples/saucedemo/.waygraph-auto/5eda8273.log +0 -0
- package/examples/saucedemo/.waygraph-auto/5fc3a33b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/61b6546d.log +0 -0
- package/examples/saucedemo/.waygraph-auto/621518ad.log +0 -0
- package/examples/saucedemo/.waygraph-auto/632b7118.log +0 -0
- package/examples/saucedemo/.waygraph-auto/64314c2b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/650ed066.log +0 -0
- package/examples/saucedemo/.waygraph-auto/66516213.log +0 -0
- package/examples/saucedemo/.waygraph-auto/66c17079.log +0 -0
- package/examples/saucedemo/.waygraph-auto/69bf0b80.log +0 -0
- package/examples/saucedemo/.waygraph-auto/69c2347a.log +0 -0
- package/examples/saucedemo/.waygraph-auto/6d22bdb0.log +0 -0
- package/examples/saucedemo/.waygraph-auto/6d9aaad3.log +0 -0
- package/examples/saucedemo/.waygraph-auto/7103bebb.log +0 -0
- package/examples/saucedemo/.waygraph-auto/740c9faa.log +0 -0
- package/examples/saucedemo/.waygraph-auto/74872fc5.log +0 -0
- package/examples/saucedemo/.waygraph-auto/75846ca9.log +0 -0
- package/examples/saucedemo/.waygraph-auto/7626c85b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/76308eec.log +0 -0
- package/examples/saucedemo/.waygraph-auto/76c1650e.log +0 -0
- package/examples/saucedemo/.waygraph-auto/7861f38f.log +0 -0
- package/examples/saucedemo/.waygraph-auto/78b21219.log +0 -0
- package/examples/saucedemo/.waygraph-auto/7af224ed.log +0 -0
- package/examples/saucedemo/.waygraph-auto/7c3be585.log +0 -0
- package/examples/saucedemo/.waygraph-auto/7ed55a2c.log +0 -0
- package/examples/saucedemo/.waygraph-auto/80086ba2.log +0 -0
- package/examples/saucedemo/.waygraph-auto/80206369.log +0 -0
- package/examples/saucedemo/.waygraph-auto/847f1092.log +0 -0
- package/examples/saucedemo/.waygraph-auto/84d6d293.log +0 -0
- package/examples/saucedemo/.waygraph-auto/8581abfa.log +0 -0
- package/examples/saucedemo/.waygraph-auto/87b7e0fd.log +0 -0
- package/examples/saucedemo/.waygraph-auto/87c79256.log +0 -0
- package/examples/saucedemo/.waygraph-auto/88af2495.log +0 -0
- package/examples/saucedemo/.waygraph-auto/8a3eb088.log +0 -0
- package/examples/saucedemo/.waygraph-auto/8c04a403.log +0 -0
- package/examples/saucedemo/.waygraph-auto/8c9ab09e.log +0 -0
- package/examples/saucedemo/.waygraph-auto/8cc5db51.log +0 -0
- package/examples/saucedemo/.waygraph-auto/8ea3a69c.log +0 -0
- package/examples/saucedemo/.waygraph-auto/9150f913.log +0 -0
- package/examples/saucedemo/.waygraph-auto/929a4313.log +0 -0
- package/examples/saucedemo/.waygraph-auto/95a599e4.log +0 -0
- package/examples/saucedemo/.waygraph-auto/96c45350.log +0 -0
- package/examples/saucedemo/.waygraph-auto/986030e6.log +0 -0
- package/examples/saucedemo/.waygraph-auto/99787abb.log +0 -0
- package/examples/saucedemo/.waygraph-auto/99ddb62c.log +0 -0
- package/examples/saucedemo/.waygraph-auto/a2dd30dd.log +0 -0
- package/examples/saucedemo/.waygraph-auto/a63a2d20.log +0 -0
- package/examples/saucedemo/.waygraph-auto/a78befb7.log +0 -0
- package/examples/saucedemo/.waygraph-auto/a84d9b79.log +0 -0
- package/examples/saucedemo/.waygraph-auto/a87bb3ee.log +0 -0
- package/examples/saucedemo/.waygraph-auto/a9270890.log +0 -0
- package/examples/saucedemo/.waygraph-auto/aa440153.log +0 -0
- package/examples/saucedemo/.waygraph-auto/ab7561e0.log +0 -0
- package/examples/saucedemo/.waygraph-auto/ac19ae60.log +0 -0
- package/examples/saucedemo/.waygraph-auto/ae21ad8e.log +0 -0
- package/examples/saucedemo/.waygraph-auto/af09042f.log +0 -0
- package/examples/saucedemo/.waygraph-auto/b1684126.log +0 -0
- package/examples/saucedemo/.waygraph-auto/b21e9d12.log +0 -0
- package/examples/saucedemo/.waygraph-auto/b5a838be.log +0 -0
- package/examples/saucedemo/.waygraph-auto/b603549d.log +0 -0
- package/examples/saucedemo/.waygraph-auto/b81e074f.log +0 -0
- package/examples/saucedemo/.waygraph-auto/b83e6259.log +0 -0
- package/examples/saucedemo/.waygraph-auto/b9cb2796.log +0 -0
- package/examples/saucedemo/.waygraph-auto/ba58abb7.log +0 -0
- package/examples/saucedemo/.waygraph-auto/bb0a0018.log +0 -0
- package/examples/saucedemo/.waygraph-auto/bb9fd384.log +0 -0
- package/examples/saucedemo/.waygraph-auto/bc0bdd0e.log +0 -0
- package/examples/saucedemo/.waygraph-auto/bc7d4a83.log +0 -0
- package/examples/saucedemo/.waygraph-auto/bd8e082a.log +0 -0
- package/examples/saucedemo/.waygraph-auto/bea0c93e.log +0 -0
- package/examples/saucedemo/.waygraph-auto/bf35201a.log +0 -0
- package/examples/saucedemo/.waygraph-auto/bffa3a7a.log +0 -0
- package/examples/saucedemo/.waygraph-auto/c02ea583.log +0 -0
- package/examples/saucedemo/.waygraph-auto/c2caf0a8.log +0 -0
- package/examples/saucedemo/.waygraph-auto/c3bdc714.log +0 -0
- package/examples/saucedemo/.waygraph-auto/c4afb8bc.log +0 -0
- package/examples/saucedemo/.waygraph-auto/c916a654.log +0 -0
- package/examples/saucedemo/.waygraph-auto/c9b8d69e.log +0 -0
- package/examples/saucedemo/.waygraph-auto/cd68bbc4.log +0 -0
- package/examples/saucedemo/.waygraph-auto/cdc6db50.log +0 -0
- package/examples/saucedemo/.waygraph-auto/d01906a7.log +0 -0
- package/examples/saucedemo/.waygraph-auto/d02e5e7e.log +0 -0
- package/examples/saucedemo/.waygraph-auto/d09aff71.log +0 -0
- package/examples/saucedemo/.waygraph-auto/d0e41396.log +0 -0
- package/examples/saucedemo/.waygraph-auto/d1370d76.log +0 -0
- package/examples/saucedemo/.waygraph-auto/d3714ae4.log +0 -0
- package/examples/saucedemo/.waygraph-auto/d9e5f8a4.log +0 -0
- package/examples/saucedemo/.waygraph-auto/da0685dc.log +0 -0
- package/examples/saucedemo/.waygraph-auto/da894ad8.log +0 -0
- package/examples/saucedemo/.waygraph-auto/daa2f922.log +0 -0
- package/examples/saucedemo/.waygraph-auto/daefb614.log +0 -0
- package/examples/saucedemo/.waygraph-auto/db97e833.log +0 -0
- package/examples/saucedemo/.waygraph-auto/dec3076e.log +0 -0
- package/examples/saucedemo/.waygraph-auto/e101d19a.log +0 -0
- package/examples/saucedemo/.waygraph-auto/e1973991.log +0 -0
- package/examples/saucedemo/.waygraph-auto/e207c989.log +0 -0
- package/examples/saucedemo/.waygraph-auto/e633e545.log +0 -0
- package/examples/saucedemo/.waygraph-auto/e69aec68.log +0 -0
- package/examples/saucedemo/.waygraph-auto/e70e2d06.log +0 -0
- package/examples/saucedemo/.waygraph-auto/e8306a4b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/ebad3410.log +0 -0
- package/examples/saucedemo/.waygraph-auto/ec88b3ef.log +0 -0
- package/examples/saucedemo/.waygraph-auto/ecbb0966.log +0 -0
- package/examples/saucedemo/.waygraph-auto/ecdee753.log +0 -0
- package/examples/saucedemo/.waygraph-auto/ed888783.log +0 -0
- package/examples/saucedemo/.waygraph-auto/ed931dac.log +0 -0
- package/examples/saucedemo/.waygraph-auto/ef655a3f.log +0 -0
- package/examples/saucedemo/.waygraph-auto/f34c9a26.log +0 -0
- package/examples/saucedemo/.waygraph-auto/f3c29b21.log +0 -0
- package/examples/saucedemo/.waygraph-auto/f53f7cfc.log +0 -0
- package/examples/saucedemo/.waygraph-auto/f5525d53.log +0 -0
- package/examples/saucedemo/.waygraph-auto/f5ad5830.log +0 -0
- package/examples/saucedemo/.waygraph-auto/f90035b3.log +0 -0
- package/examples/saucedemo/.waygraph-auto/fa687bc2.log +0 -0
- package/examples/saucedemo/.waygraph-auto/faed36dd.log +0 -0
- package/examples/saucedemo/.waygraph-auto/fb66fba0.log +0 -0
- package/examples/saucedemo/.waygraph-auto/fc48fd76.log +0 -0
- package/examples/saucedemo/docs/CONVENTION.md +25 -5
- package/examples/saucedemo/docs/HELPERS.md +15 -4
- package/examples/saucedemo/scripts/capture-wrong-login.mjs +8 -2
- package/examples/saucedemo/scripts/prove-wrong-login-runtime.mjs +8 -2
- package/examples/saucedemo/src/blocks/SITE-MAP.md +14 -3
- package/examples/saucedemo/src/blocks/saucedemo-web/NAV.md +15 -1
- package/examples/saucedemo/src/blocks/saucedemo-web/cart/NAV.md +1 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/cart/cart.sel.ts +9 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/cart/nav-cart.block.ts +3 -2
- package/examples/saucedemo/src/blocks/saucedemo-web/cart/nav-checkout-info.block.ts +2 -1
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/NAV.md +6 -1
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/methods/checkout-info.sel.ts +11 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/methods/fill-first-name.method.block.ts +22 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/methods/fill-last-name.method.block.ts +21 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/methods/fill-postal-code.method.block.ts +21 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.ts +6 -7
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/NAV.md +2 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/checkout-overview.sel.ts +9 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +7 -6
- package/examples/saucedemo/src/blocks/saucedemo-web/ff-owner-auth.block.ts +7 -2
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/NAV.md +1 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/item-detail.sel.ts +9 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/nav-back-to-inventory.block.ts +2 -1
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/nav-back-to-products.block.ts +2 -1
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/nav-continue-shopping.block.ts +2 -1
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/fill-password.method.block.ts +51 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/fill-username.method.block.ts +69 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/login.sel.ts +13 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/submit-login-for-flow.ts +4 -4
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +13 -51
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/submit-logout.method.block.ts +4 -3
- package/examples/saucedemo/src/blocks/saucedemo-web/nav-login.block.ts +14 -6
- package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +15 -0
- package/examples/saucedemo/src/flows/checkout.flow.ts +20 -2
- package/examples/saucedemo/src/flows/device-showcase.flow.ts +4 -0
- package/examples/saucedemo/src/flows/login.flow.ts +3 -1
- package/examples/saucedemo/src/flows/shop.flow.ts +6 -0
- package/examples/saucedemo/src/flows/smoke.flow.ts +13 -7
- package/examples/saucedemo/src/flows/stub-on-error.flow.ts +4 -2
- package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +6 -2
- package/examples/saucedemo/tests/flow-block-verify.spec.ts +3 -3
- package/examples/saucedemo/tests/submit-login-branch.spec.ts +5 -3
- package/package.json +2 -1
- package/templates/agents/waygraph-author.agent.md +43 -5
- package/templates/agents/waygraph-planner.agent.md +33 -2
- package/templates/quickstart/src/blocks/saucedemo-web/NAV.md +15 -1
- package/templates/quickstart/src/blocks/saucedemo-web/cart/NAV.md +1 -0
- package/templates/quickstart/src/blocks/saucedemo-web/cart/cart.sel.ts +9 -0
- package/templates/quickstart/src/blocks/saucedemo-web/cart/nav-cart.block.ts +19 -15
- package/templates/quickstart/src/blocks/saucedemo-web/cart/nav-checkout-info.block.ts +2 -1
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/NAV.md +6 -1
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/methods/checkout-info.sel.ts +11 -0
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/methods/fill-first-name.method.block.ts +22 -0
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/methods/fill-last-name.method.block.ts +21 -0
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/methods/fill-postal-code.method.block.ts +21 -0
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.ts +6 -7
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/NAV.md +2 -0
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/checkout-overview.sel.ts +9 -0
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +7 -6
- package/templates/quickstart/src/blocks/saucedemo-web/ff-owner-auth.block.ts +7 -2
- package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +24 -16
- package/templates/quickstart/src/blocks/saucedemo-web/inventory/nav-item-detail.block.ts +3 -3
- package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/NAV.md +1 -0
- package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/item-detail.sel.ts +9 -0
- package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/nav-back-to-inventory.block.ts +2 -1
- package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/nav-back-to-products.block.ts +2 -1
- package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/nav-continue-shopping.block.ts +2 -1
- package/templates/quickstart/src/blocks/saucedemo-web/methods/fill-password.method.block.ts +51 -0
- package/templates/quickstart/src/blocks/saucedemo-web/methods/fill-username.method.block.ts +69 -0
- package/templates/quickstart/src/blocks/saucedemo-web/methods/login.sel.ts +13 -0
- package/templates/quickstart/src/blocks/saucedemo-web/methods/submit-login-for-flow.ts +4 -4
- package/templates/quickstart/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +35 -63
- package/templates/quickstart/src/blocks/saucedemo-web/methods/submit-logout.method.block.ts +4 -3
- package/templates/quickstart/src/blocks/saucedemo-web/nav-login.block.ts +14 -6
- package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +15 -0
- package/templates/quickstart/src/flows/checkout.flow.ts +29 -2
- package/templates/quickstart/src/flows/device-showcase.flow.ts +4 -0
- package/templates/quickstart/src/flows/login.flow.ts +3 -1
- package/templates/quickstart/src/flows/shop.flow.ts +6 -0
- package/templates/quickstart/src/flows/smoke.flow.ts +13 -7
- package/templates/quickstart/src/flows/viewer-blocked.flow.ts +6 -2
- package/templates/quickstart/tests/chain-flow.spec.ts +8 -1
- package/templates/scaffold/STRUCTURE.md +23 -3
- package/templates/scaffold/scripts/fixture-server.mjs +11 -2
- package/templates/scaffold/src/flows/example.flow.ts +8 -6
- package/templates/scaffold/src/flows/mail-verify.flow.ts +85 -0
- package/templates/scaffold/src/flows/shop.flow.ts +23 -6
- package/templates/scaffold/src/map/(app_base)/docs/_nav.block.ts +20 -0
- package/templates/scaffold/src/map/(app_base)/docs/_page.block.ts +15 -0
- package/templates/scaffold/src/map/(app_base)/docs/_sel.ts +7 -0
- package/templates/scaffold/src/map/(app_base)/docs/docs.html +8 -0
- package/templates/scaffold/src/{blocks/demo-web/methods → map/(app_base)/home/_methods}/add-item.effect.block.ts +3 -3
- package/templates/scaffold/src/map/(app_base)/home/_methods/assert-hello.method.block.ts +46 -0
- package/templates/scaffold/src/map/(app_base)/home/_methods/assert-item-added.method.block.ts +38 -0
- package/templates/scaffold/src/{blocks/demo-web/methods → map/(app_base)/home/_methods}/clear-cart.method.block.ts +9 -3
- package/templates/scaffold/src/{blocks/demo-web/methods → map/(app_base)/home/_methods}/remove-item.effect.block.ts +3 -3
- package/templates/scaffold/src/{blocks/demo-web/nav-home.block.ts → map/(app_base)/home/_nav.block.ts} +2 -2
- package/templates/scaffold/src/{blocks/demo-web/home.page.block.ts → map/(app_base)/home/_page.block.ts} +7 -7
- package/templates/scaffold/src/{blocks/demo-web/demo-sel.ts → map/(app_base)/home/_sel.ts} +1 -0
- package/templates/scaffold/src/{blocks/demo-web → map/(app_base)/home}/home.html +4 -0
- package/templates/scaffold/src/map/(app_base)/home/nav-verification-link.block.ts +34 -0
- package/templates/scaffold/src/map/(external)/mailpit/_methods/assert-email-content.method.block.ts +48 -0
- package/templates/scaffold/src/map/(external)/mailpit/_methods/assert-email-received.method.block.ts +49 -0
- package/templates/scaffold/src/map/(external)/mailpit/_methods/extract-email-link.method.block.ts +41 -0
- package/templates/scaffold/src/map/(external)/mailpit/_methods/open-message.method.block.ts +39 -0
- package/templates/scaffold/src/map/(external)/mailpit/_nav.block.ts +20 -0
- package/templates/scaffold/src/map/(external)/mailpit/_page.block.ts +30 -0
- package/templates/scaffold/src/map/(external)/mailpit/_sel.ts +5 -0
- package/templates/scaffold/src/states/demo.mem-keys.ts +25 -1
- package/templates/scaffold/src/states/demo.states.ts +11 -0
- package/templates/scaffold/test-results/.last-run.json +4 -0
- package/templates/scaffold/tests/example.spec.ts +2 -2
- package/templates/scaffold/tests/mail-verify.spec.ts +191 -0
- package/templates/skills/waygraph-blind-pilot.skill.md +61 -0
- package/templates/skills/waygraph-pilot.skill.md +108 -0
- package/templates/quickstart/README-saucedemo-commands.md +0 -72
- package/templates/scaffold/src/blocks/SITE-MAP.md +0 -12
- package/templates/scaffold/src/blocks/demo-web/NAV.md +0 -29
- package/templates/scaffold/src/blocks/demo-web/methods/assert-hello.method.block.ts +0 -50
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { Engine, MemPage } from "./index.js";
|
|
4
|
+
import { buildExploreContext, buildExploreMenu, } from "./auto-explore.js";
|
|
5
|
+
import { ensureLivePage, detectHere, seedDefaultMem, ensureMem, runOneBlock, resolveBaseUrl, } from "./auto-explore-run.js";
|
|
6
|
+
import { runStubPhase } from "./highlights.js";
|
|
7
|
+
import { findBlockPathDetailed } from "./graph.js";
|
|
8
|
+
import { updatePilotOverlay, showPilotActivity, showPilotVision } from "./pilot-overlay.js";
|
|
9
|
+
/** True when a stub-phase result actually carries authored content worth keeping. */
|
|
10
|
+
function stubPhaseHasContent(r) {
|
|
11
|
+
return (Object.keys(r.highlights).length > 0 ||
|
|
12
|
+
r.todos.length > 0 ||
|
|
13
|
+
r.device !== undefined ||
|
|
14
|
+
r.todoDock !== undefined);
|
|
15
|
+
}
|
|
16
|
+
/** Plain, JSON-serializable view of a menu - no Block/MemKey/class references. */
|
|
17
|
+
export function buildSessionSnapshot(menu, library, here, lastRunNote) {
|
|
18
|
+
let n = 0;
|
|
19
|
+
const sections = menu.sections.map((section) => ({
|
|
20
|
+
title: section.title,
|
|
21
|
+
edges: section.edges.map((edge) => {
|
|
22
|
+
n++;
|
|
23
|
+
const entry = library.get(edge.block);
|
|
24
|
+
return {
|
|
25
|
+
index: n,
|
|
26
|
+
block: edge.block,
|
|
27
|
+
kind: edge.kind,
|
|
28
|
+
to: edge.to,
|
|
29
|
+
...(edge.label ? { label: edge.label } : {}),
|
|
30
|
+
...(entry?.description ? { description: entry.description } : {}),
|
|
31
|
+
};
|
|
32
|
+
}),
|
|
33
|
+
}));
|
|
34
|
+
return { here, sections, done: menu.flat.length === 0, lastRunNote };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Same number/`q`/range rules `cliPick` already applies, but returning a
|
|
38
|
+
* result instead of looping to re-prompt - one request gets one answer.
|
|
39
|
+
*/
|
|
40
|
+
export function parsePick(raw, menuLength) {
|
|
41
|
+
const ans = raw.trim().toLowerCase();
|
|
42
|
+
if (ans === "q" || ans === "quit")
|
|
43
|
+
return { type: "quit" };
|
|
44
|
+
const n = Number(ans);
|
|
45
|
+
if (Number.isInteger(n) && n >= 1 && n <= menuLength) {
|
|
46
|
+
return { type: "pick", index: n - 1 };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
type: "invalid",
|
|
50
|
+
reason: `"${raw}" is not a valid pick - enter a number from 1 to ${menuLength}, or "q" to quit`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const FULL_MODE_DEFAULT_DEPTH = 12;
|
|
54
|
+
const FULL_MODE_MAX_NODES = 800;
|
|
55
|
+
const FULL_MODE_MAX_TEXT_LENGTH = 300;
|
|
56
|
+
/** Runs entirely inside the browser (page.evaluate) - no closures over outer TS state. */
|
|
57
|
+
function walkFullDom(root, caps) {
|
|
58
|
+
let count = 0;
|
|
59
|
+
let truncated = false;
|
|
60
|
+
function walk(el, depth) {
|
|
61
|
+
if (depth > caps.depth) {
|
|
62
|
+
truncated = true;
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (count >= caps.maxNodes) {
|
|
66
|
+
truncated = true;
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
count++;
|
|
70
|
+
const node = { tag: el.tagName.toLowerCase() };
|
|
71
|
+
const attrs = {};
|
|
72
|
+
for (const a of Array.from(el.attributes))
|
|
73
|
+
attrs[a.name] = a.value;
|
|
74
|
+
if (Object.keys(attrs).length > 0)
|
|
75
|
+
node.attrs = attrs;
|
|
76
|
+
const directText = Array.from(el.childNodes)
|
|
77
|
+
.filter((n) => n.nodeType === 3)
|
|
78
|
+
.map((n) => n.textContent ?? "")
|
|
79
|
+
.join("")
|
|
80
|
+
.trim();
|
|
81
|
+
if (directText) {
|
|
82
|
+
node.text =
|
|
83
|
+
directText.length > caps.maxTextLength
|
|
84
|
+
? directText.slice(0, caps.maxTextLength) + "..."
|
|
85
|
+
: directText;
|
|
86
|
+
}
|
|
87
|
+
const children = [];
|
|
88
|
+
for (const child of Array.from(el.children)) {
|
|
89
|
+
if (count >= caps.maxNodes) {
|
|
90
|
+
truncated = true;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
const c = walk(child, depth + 1);
|
|
94
|
+
if (c)
|
|
95
|
+
children.push(c);
|
|
96
|
+
}
|
|
97
|
+
if (children.length > 0)
|
|
98
|
+
node.children = children;
|
|
99
|
+
return node;
|
|
100
|
+
}
|
|
101
|
+
const tree = walk(root, 0) ?? { tag: root.tagName.toLowerCase() };
|
|
102
|
+
return { tree, truncated };
|
|
103
|
+
}
|
|
104
|
+
const TRACE_MAX_STEPS = 500;
|
|
105
|
+
/**
|
|
106
|
+
* Real, minimal, valid fixture files bundled at `assets/stubs/` - not
|
|
107
|
+
* placeholder/renamed-empty files: `stub.png` is a real 1x1 PNG,
|
|
108
|
+
* `stub.pdf` a real single-page PDF, `stub.mp4` a real ffmpeg-encoded
|
|
109
|
+
* 1s black clip. Real, direct user request: Blind Pilot hitting a real
|
|
110
|
+
* `<input type="file">` (e.g. an avatar upload) had no way to supply
|
|
111
|
+
* anything without a human handing over a real file each time - these are
|
|
112
|
+
* small enough to ship in the package and pass most apps' basic file-type
|
|
113
|
+
* checks (magic bytes, not just extension).
|
|
114
|
+
*/
|
|
115
|
+
/**
|
|
116
|
+
* Public so a Block a consumer writes can reach the same real fixture files
|
|
117
|
+
* `rawUpload` uses - not just the raw primitive. Needed when a real upload
|
|
118
|
+
* flow has no stable `<input type="file">` to hand a selector to at all
|
|
119
|
+
* (e.g. one created on the fly inside a click handler, then immediately
|
|
120
|
+
* `.click()`'d to open the OS picker and possibly torn down right after) -
|
|
121
|
+
* a Block's own `act()` should pair this with `page.waitForEvent("filechooser")`
|
|
122
|
+
* around the triggering click, not go hunting for a selector that may not
|
|
123
|
+
* exist for more than a moment.
|
|
124
|
+
*/
|
|
125
|
+
export function stubFilePath(kind) {
|
|
126
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
127
|
+
const names = {
|
|
128
|
+
image: "stub.png",
|
|
129
|
+
pdf: "stub.pdf",
|
|
130
|
+
video: "stub.mp4",
|
|
131
|
+
};
|
|
132
|
+
return join(packageRoot, "assets", "stubs", names[kind]);
|
|
133
|
+
}
|
|
134
|
+
const CONSOLE_LOG_MAX_ENTRIES = 200;
|
|
135
|
+
/**
|
|
136
|
+
* Non-interactive explore session: same graph/library/mem/browser setup
|
|
137
|
+
* `runAutoExplore` runs in `--cli` mode, driven by `applyPick` instead of a
|
|
138
|
+
* blocking `readline` loop. Headless by default (matching
|
|
139
|
+
* `runAutoExplore`'s own `cli` branch), optionally visible via
|
|
140
|
+
* `AutoSessionInit.headless: false` - this type only ever backs the `--cli`
|
|
141
|
+
* picker, never the headful page-embedded panel, headless or not.
|
|
142
|
+
*/
|
|
143
|
+
export class AutoSession {
|
|
144
|
+
projectDir;
|
|
145
|
+
blocksSelect;
|
|
146
|
+
graph;
|
|
147
|
+
library;
|
|
148
|
+
mem;
|
|
149
|
+
engine;
|
|
150
|
+
browser;
|
|
151
|
+
context;
|
|
152
|
+
page;
|
|
153
|
+
startUrl;
|
|
154
|
+
sessionId;
|
|
155
|
+
here = null;
|
|
156
|
+
lastRunNote = null;
|
|
157
|
+
trace = [];
|
|
158
|
+
consoleLog = [];
|
|
159
|
+
constructor(projectDir, blocksSelect, graph, library, mem, engine, browser, context, page, startUrl, sessionId) {
|
|
160
|
+
this.projectDir = projectDir;
|
|
161
|
+
this.blocksSelect = blocksSelect;
|
|
162
|
+
this.graph = graph;
|
|
163
|
+
this.library = library;
|
|
164
|
+
this.mem = mem;
|
|
165
|
+
this.engine = engine;
|
|
166
|
+
this.browser = browser;
|
|
167
|
+
this.context = context;
|
|
168
|
+
this.page = page;
|
|
169
|
+
this.startUrl = startUrl;
|
|
170
|
+
this.sessionId = sessionId;
|
|
171
|
+
}
|
|
172
|
+
static async start(init) {
|
|
173
|
+
const baseURL = init.baseURL ?? resolveBaseUrl(init.projectDir);
|
|
174
|
+
const startUrl = init.startUrl ?? baseURL;
|
|
175
|
+
const { graph, library } = await buildExploreContext(init.projectDir, init.blocksSelect ? { blocksSelect: init.blocksSelect } : undefined);
|
|
176
|
+
const mem = new MemPage();
|
|
177
|
+
seedDefaultMem(library.byName, mem);
|
|
178
|
+
const headless = init.headless ?? true;
|
|
179
|
+
const engine = new Engine({ headless });
|
|
180
|
+
const { chromium } = await import("@playwright/test");
|
|
181
|
+
const executablePath = process.env.CHROME_PATH || process.env.CHROMIUM_PATH;
|
|
182
|
+
// Real gap found live: a headed (--non-headless) session kept a fixed
|
|
183
|
+
// 1280x720 content area even when the user maximized/fullscreened the
|
|
184
|
+
// actual window - `viewport: null` alone only makes the PAGE track the
|
|
185
|
+
// window; the window itself still launches at Chromium's own small
|
|
186
|
+
// default size unless told to start maximized. Same fix cli.ts's own
|
|
187
|
+
// --step/headed launch already uses (see its own comment there) -
|
|
188
|
+
// headless keeps the fixed viewport since there's no real window to size.
|
|
189
|
+
const launchOpts = {
|
|
190
|
+
headless,
|
|
191
|
+
args: headless ? [] : ["--start-maximized"],
|
|
192
|
+
};
|
|
193
|
+
if (executablePath)
|
|
194
|
+
launchOpts.executablePath = executablePath;
|
|
195
|
+
const browser = await chromium.launch(launchOpts);
|
|
196
|
+
const contextOpts = {
|
|
197
|
+
viewport: headless ? { width: 1280, height: 720 } : null,
|
|
198
|
+
};
|
|
199
|
+
if (baseURL)
|
|
200
|
+
contextOpts.baseURL = baseURL;
|
|
201
|
+
const context = await browser.newContext(contextOpts);
|
|
202
|
+
const page = await context.newPage();
|
|
203
|
+
if (startUrl) {
|
|
204
|
+
await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
|
|
205
|
+
await page.waitForLoadState("load").catch(() => { });
|
|
206
|
+
}
|
|
207
|
+
const session = new AutoSession(init.projectDir, init.blocksSelect, graph, library, mem, engine, browser, context, page, startUrl, init.sessionId);
|
|
208
|
+
// Attached once, on the page created here - `ensureLivePage` only swaps
|
|
209
|
+
// to a fresh Page if the current one closed (a rare case, e.g. the human
|
|
210
|
+
// closing the tab), not on ordinary same-page SPA navigation, which is
|
|
211
|
+
// all this target (and most real sites) actually do.
|
|
212
|
+
page.on("console", (msg) => {
|
|
213
|
+
session.pushConsoleLog({
|
|
214
|
+
type: "console",
|
|
215
|
+
level: msg.type(),
|
|
216
|
+
text: msg.text(),
|
|
217
|
+
timestamp: new Date().toISOString(),
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
page.on("pageerror", (err) => {
|
|
221
|
+
session.pushConsoleLog({
|
|
222
|
+
type: "pageerror",
|
|
223
|
+
text: err instanceof Error ? err.message : String(err),
|
|
224
|
+
timestamp: new Date().toISOString(),
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
page.on("response", (res) => {
|
|
228
|
+
// Every failed response (the original case), PLUS every mutation
|
|
229
|
+
// (non-GET) response regardless of status - a real gap found live:
|
|
230
|
+
// "auto console" only logging failures made a SUCCESSFUL POST that
|
|
231
|
+
// simply didn't navigate anywhere look identical to "nothing
|
|
232
|
+
// happened at all," undiagnosable the same way a real failure was.
|
|
233
|
+
// A mutation's outcome (2xx or not) is always diagnostically
|
|
234
|
+
// relevant; a GET's usually isn't (assets, trackers) unless it failed.
|
|
235
|
+
const method = res.request().method();
|
|
236
|
+
if (res.status() < 400 && method === "GET")
|
|
237
|
+
return;
|
|
238
|
+
session.pushConsoleLog({
|
|
239
|
+
type: "response",
|
|
240
|
+
text: `${method} ${res.statusText()}`,
|
|
241
|
+
url: res.url(),
|
|
242
|
+
status: res.status(),
|
|
243
|
+
timestamp: new Date().toISOString(),
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
return session;
|
|
247
|
+
}
|
|
248
|
+
/** Pure getter - returns a copy, not the live array. */
|
|
249
|
+
getTrace() {
|
|
250
|
+
return [...this.trace];
|
|
251
|
+
}
|
|
252
|
+
pushTrace(step) {
|
|
253
|
+
this.trace.push(step);
|
|
254
|
+
if (this.trace.length > TRACE_MAX_STEPS)
|
|
255
|
+
this.trace.shift();
|
|
256
|
+
}
|
|
257
|
+
/** Pure getter - returns a copy, not the live array. */
|
|
258
|
+
getConsoleLog() {
|
|
259
|
+
return [...this.consoleLog];
|
|
260
|
+
}
|
|
261
|
+
pushConsoleLog(entry) {
|
|
262
|
+
this.consoleLog.push(entry);
|
|
263
|
+
if (this.consoleLog.length > CONSOLE_LOG_MAX_ENTRIES)
|
|
264
|
+
this.consoleLog.shift();
|
|
265
|
+
}
|
|
266
|
+
async currentMenu() {
|
|
267
|
+
this.page = await ensureLivePage(this.context, this.page, this.startUrl);
|
|
268
|
+
if (this.here === null) {
|
|
269
|
+
this.here = await detectHere(this.page, this.library.navBlocks);
|
|
270
|
+
}
|
|
271
|
+
const menu = await buildExploreMenu(this.page, this.graph, this.library, this.here);
|
|
272
|
+
// Every caller of currentMenu() (status reads, applyPick/applyPath,
|
|
273
|
+
// the raw click/type/goto primitives, resync) already routes through
|
|
274
|
+
// here, so hooking the overlay refresh at this one point covers every
|
|
275
|
+
// real state-changing (and reading) path without needing a separate
|
|
276
|
+
// call at each site. Awaited deliberately, not fire-and-forget - the
|
|
277
|
+
// whole point is a human watching the screen sees the SAME state a
|
|
278
|
+
// concurrent API caller just got back, not a stale frame that catches
|
|
279
|
+
// up moments later.
|
|
280
|
+
await updatePilotOverlay(this.page, {
|
|
281
|
+
sessionId: this.sessionId,
|
|
282
|
+
snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
|
|
283
|
+
graph: this.graph,
|
|
284
|
+
});
|
|
285
|
+
await this.warnUnmappedLinks();
|
|
286
|
+
return menu;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Real, direct user request: a Blind Pilot agent had no way to ask "which
|
|
290
|
+
* of the links visible on THIS page are already covered by a NavBlock" -
|
|
291
|
+
* only manual DOM inspection + guessing hrefs, exactly what this session
|
|
292
|
+
* had been doing by hand the whole time this feature was built. Compares
|
|
293
|
+
* every same-origin `<a href>` on the page against every NavBlock's own
|
|
294
|
+
* static url (`__waygraphNavUrl` - see `defineNavBlock`'s own comment for
|
|
295
|
+
* why only plain-string urls qualify), and `console.warn()`s each pathname
|
|
296
|
+
* with no NavBlock at all - surfaced through the same real console capture
|
|
297
|
+
* `auto console` already reads, not a separate channel. Deduped per path
|
|
298
|
+
* per page load (a `Set` living on `window`, so it survives repeated
|
|
299
|
+
* `currentMenu()` calls but resets on a real navigation) - once is enough
|
|
300
|
+
* to flag a gap, repeating it on every status/send call would just be noise.
|
|
301
|
+
*/
|
|
302
|
+
async warnUnmappedLinks() {
|
|
303
|
+
const known = new Set();
|
|
304
|
+
for (const entry of this.library.navBlocks) {
|
|
305
|
+
const url = entry.block.__waygraphNavUrl;
|
|
306
|
+
if (!url)
|
|
307
|
+
continue;
|
|
308
|
+
try {
|
|
309
|
+
known.add(new URL(url, this.page.url()).pathname);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
/* not a resolvable URL (relative to an unset base, etc.) - skip */
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
await this.page
|
|
316
|
+
.evaluate((knownPathnames) => {
|
|
317
|
+
const w = window;
|
|
318
|
+
w.__wgWarnedPaths ??= new Set();
|
|
319
|
+
const seenThisPass = new Set();
|
|
320
|
+
for (const a of Array.from(document.querySelectorAll("a[href]"))) {
|
|
321
|
+
let url;
|
|
322
|
+
try {
|
|
323
|
+
url = new URL(a.getAttribute("href") || "", location.href);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (url.origin !== location.origin)
|
|
329
|
+
continue;
|
|
330
|
+
const path = url.pathname;
|
|
331
|
+
if (seenThisPass.has(path) || knownPathnames.includes(path) || w.__wgWarnedPaths.has(path))
|
|
332
|
+
continue;
|
|
333
|
+
seenThisPass.add(path);
|
|
334
|
+
w.__wgWarnedPaths.add(path);
|
|
335
|
+
console.warn(`[waygraph] unmapped nav link on this page: ${path} - no NavBlock covers this URL`);
|
|
336
|
+
}
|
|
337
|
+
}, Array.from(known))
|
|
338
|
+
.catch(() => { });
|
|
339
|
+
}
|
|
340
|
+
/** Pure getter - no Block runs, no mem/page mutation. */
|
|
341
|
+
async currentSnapshot() {
|
|
342
|
+
const menu = await this.currentMenu();
|
|
343
|
+
return buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Pure getter - reads the live page's structure at the requested fidelity.
|
|
347
|
+
* No Block runs, no mem/page mutation. `selector` scopes either mode to one
|
|
348
|
+
* element's subtree; it is a modifier, not a third mode.
|
|
349
|
+
*/
|
|
350
|
+
async inspectDom(opts) {
|
|
351
|
+
this.page = await ensureLivePage(this.context, this.page, this.startUrl);
|
|
352
|
+
const mode = opts.mode ?? "aria";
|
|
353
|
+
const ariaOptions = opts.depth !== undefined ? { mode: "ai", depth: opts.depth } : { mode: "ai" };
|
|
354
|
+
if (opts.selector) {
|
|
355
|
+
const locator = this.page.locator(opts.selector).first();
|
|
356
|
+
if ((await locator.count()) === 0) {
|
|
357
|
+
return { ok: false, error: `no element matches selector "${opts.selector}"` };
|
|
358
|
+
}
|
|
359
|
+
await showPilotVision(this.page, opts.selector, `dom (${mode}): ${opts.selector}`, "orange");
|
|
360
|
+
if (mode === "full") {
|
|
361
|
+
const caps = {
|
|
362
|
+
depth: opts.depth ?? FULL_MODE_DEFAULT_DEPTH,
|
|
363
|
+
maxNodes: FULL_MODE_MAX_NODES,
|
|
364
|
+
maxTextLength: FULL_MODE_MAX_TEXT_LENGTH,
|
|
365
|
+
};
|
|
366
|
+
const { tree, truncated } = await locator.evaluate(walkFullDom, caps);
|
|
367
|
+
return { ok: true, snapshot: { mode: "full", selector: opts.selector, truncated, tree } };
|
|
368
|
+
}
|
|
369
|
+
const tree = await locator.ariaSnapshotJSON(ariaOptions);
|
|
370
|
+
return { ok: true, snapshot: { mode: "aria", selector: opts.selector, truncated: false, tree } };
|
|
371
|
+
}
|
|
372
|
+
await showPilotActivity(this.page, `Running (Dom): ${mode}, whole page`);
|
|
373
|
+
if (mode === "full") {
|
|
374
|
+
// page.evaluate only ships the one function passed to it - it can't
|
|
375
|
+
// reach another named function by closure. Routing the whole-page case
|
|
376
|
+
// through the same `html` Locator lets both cases share one
|
|
377
|
+
// self-contained walkFullDom instead of two near-duplicate callbacks.
|
|
378
|
+
const caps = {
|
|
379
|
+
depth: opts.depth ?? FULL_MODE_DEFAULT_DEPTH,
|
|
380
|
+
maxNodes: FULL_MODE_MAX_NODES,
|
|
381
|
+
maxTextLength: FULL_MODE_MAX_TEXT_LENGTH,
|
|
382
|
+
};
|
|
383
|
+
const { tree, truncated } = await this.page.locator("html").first().evaluate(walkFullDom, caps);
|
|
384
|
+
return { ok: true, snapshot: { mode: "full", truncated, tree } };
|
|
385
|
+
}
|
|
386
|
+
const tree = await this.page.ariaSnapshotJSON(ariaOptions);
|
|
387
|
+
return { ok: true, snapshot: { mode: "aria", truncated: false, tree } };
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Pure getter - reads the live page's client-side state: localStorage,
|
|
391
|
+
* sessionStorage, and registered service workers (scope/active URL/state).
|
|
392
|
+
* Real, direct follow-up to `auto console`'s own gap: a site doing web
|
|
393
|
+
* push (this target has a real `NOTIFICATION_VAPID` config, confirmed via
|
|
394
|
+
* `auto dom` earlier) keeps push-subscription/auth state in these places,
|
|
395
|
+
* not in the rendered DOM `inspectDom` reads.
|
|
396
|
+
*/
|
|
397
|
+
async inspectStorage() {
|
|
398
|
+
this.page = await ensureLivePage(this.context, this.page, this.startUrl);
|
|
399
|
+
const [localStorageEntries, sessionStorageEntries] = await this.page.evaluate(() => [
|
|
400
|
+
Object.entries(window.localStorage),
|
|
401
|
+
Object.entries(window.sessionStorage),
|
|
402
|
+
]);
|
|
403
|
+
const cookies = await this.context.cookies();
|
|
404
|
+
const workers = this.context.serviceWorkers();
|
|
405
|
+
const registrations = await this.page
|
|
406
|
+
.evaluate(async () => {
|
|
407
|
+
if (!("serviceWorker" in navigator))
|
|
408
|
+
return [];
|
|
409
|
+
const regs = await navigator.serviceWorker.getRegistrations();
|
|
410
|
+
return regs.map((r) => ({
|
|
411
|
+
scope: r.scope,
|
|
412
|
+
...(r.active?.scriptURL ? { activeUrl: r.active.scriptURL } : {}),
|
|
413
|
+
...(r.active?.state ? { state: r.active.state } : {}),
|
|
414
|
+
}));
|
|
415
|
+
})
|
|
416
|
+
.catch(() => []);
|
|
417
|
+
return {
|
|
418
|
+
localStorage: Object.fromEntries(localStorageEntries),
|
|
419
|
+
sessionStorage: Object.fromEntries(sessionStorageEntries),
|
|
420
|
+
cookies: cookies.map((c) => ({
|
|
421
|
+
name: c.name,
|
|
422
|
+
value: c.value,
|
|
423
|
+
domain: c.domain,
|
|
424
|
+
path: c.path,
|
|
425
|
+
httpOnly: c.httpOnly,
|
|
426
|
+
secure: c.secure,
|
|
427
|
+
})),
|
|
428
|
+
serviceWorkers: workers.map((w) => w.url()),
|
|
429
|
+
serviceWorkerRegistrations: registrations,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Runs one already-resolved Block entry for real - the shared core both
|
|
434
|
+
* `applyPick` (resolved via a live-menu index) and `applyPath` (resolved
|
|
435
|
+
* directly by name, bypassing live-menu-visibility entirely - see
|
|
436
|
+
* `applyPath`'s own comment for why) call. `expectedTo` is only used as a
|
|
437
|
+
* fallback when a Block's own `resolve()` doesn't set `__state` - same
|
|
438
|
+
* behavior `applyPick` always had, just factored out, not changed.
|
|
439
|
+
*/
|
|
440
|
+
async runNamedBlock(entry, expectedTo, instanceOption) {
|
|
441
|
+
const from = this.here;
|
|
442
|
+
const stubBeforeResult = await runStubPhase(entry.block, "stubBefore");
|
|
443
|
+
const stepBase = {
|
|
444
|
+
block: entry.block.name,
|
|
445
|
+
from,
|
|
446
|
+
timestamp: new Date().toISOString(),
|
|
447
|
+
...(stubPhaseHasContent(stubBeforeResult) ? { stubBefore: stubBeforeResult } : {}),
|
|
448
|
+
};
|
|
449
|
+
try {
|
|
450
|
+
if (instanceOption) {
|
|
451
|
+
this.mem.set(instanceOption.key, instanceOption.value);
|
|
452
|
+
}
|
|
453
|
+
// Always the non-interactive path (cli: false) - a detached session has
|
|
454
|
+
// no TTY to prompt on; a missing mem key fails loud instead of hanging.
|
|
455
|
+
await ensureMem(entry, this.mem, false);
|
|
456
|
+
const result = await runOneBlock(this.engine, entry, this.context, this.page, this.mem, false);
|
|
457
|
+
this.here = result.__state ?? expectedTo;
|
|
458
|
+
this.lastRunNote = `${entry.block.name} -> ${this.here}`;
|
|
459
|
+
const stubAfterResult = await runStubPhase(entry.block, "stubAfter", { out: result });
|
|
460
|
+
this.pushTrace({
|
|
461
|
+
...stepBase,
|
|
462
|
+
to: this.here,
|
|
463
|
+
...(stubPhaseHasContent(stubAfterResult) ? { stubAfter: stubAfterResult } : {}),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
this.lastRunNote = `${entry.block.name} failed`;
|
|
468
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
469
|
+
const stubOnErrorResult = await runStubPhase(entry.block, "stubOnError", { error: err });
|
|
470
|
+
this.pushTrace({
|
|
471
|
+
...stepBase,
|
|
472
|
+
error: message,
|
|
473
|
+
...(stubPhaseHasContent(stubOnErrorResult) ? { stubOnError: stubOnErrorResult } : {}),
|
|
474
|
+
});
|
|
475
|
+
return { ok: false, error: `${entry.block.name} failed - ${message}` };
|
|
476
|
+
}
|
|
477
|
+
const nextMenu = await this.currentMenu();
|
|
478
|
+
return {
|
|
479
|
+
ok: true,
|
|
480
|
+
snapshot: buildSessionSnapshot(nextMenu, this.library.byName, this.here, this.lastRunNote),
|
|
481
|
+
quit: false,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/** Applies exactly one pick: runs at most one Block, returns the resulting state. */
|
|
485
|
+
async applyPick(raw) {
|
|
486
|
+
const menu = await this.currentMenu();
|
|
487
|
+
const pick = parsePick(raw, menu.flat.length);
|
|
488
|
+
if (pick.type === "invalid")
|
|
489
|
+
return { ok: false, error: pick.reason };
|
|
490
|
+
if (pick.type === "quit") {
|
|
491
|
+
return {
|
|
492
|
+
ok: true,
|
|
493
|
+
snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
|
|
494
|
+
quit: true,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
const edge = menu.flat[pick.index];
|
|
498
|
+
const entry = this.library.byName.get(edge.block);
|
|
499
|
+
if (!entry) {
|
|
500
|
+
return { ok: false, error: `Block "${edge.block}" not loaded - skipped` };
|
|
501
|
+
}
|
|
502
|
+
await showPilotActivity(this.page, `Running (Block): ${edge.block}`);
|
|
503
|
+
return this.runNamedBlock(entry, edge.to, edge.instanceOption);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Re-discovers the project's Block library and graph from disk, using the
|
|
507
|
+
* exact same call `start()` itself makes - not a duplicated read - and
|
|
508
|
+
* replaces `this.library`/`this.graph` in place. Leaves the live page,
|
|
509
|
+
* mem, browser/context, and current Checkpoint (`here`) untouched, so a
|
|
510
|
+
* Block written to disk mid-session becomes pickable via a later
|
|
511
|
+
* `applyPick` without restarting the session. See
|
|
512
|
+
* openspec/changes/waygraph-blind-pilot/design.md.
|
|
513
|
+
*/
|
|
514
|
+
async reloadLibrary() {
|
|
515
|
+
if (this.page)
|
|
516
|
+
await showPilotActivity(this.page, "Running: reload");
|
|
517
|
+
const { graph, library } = await buildExploreContext(this.projectDir, this.blocksSelect ? { blocksSelect: this.blocksSelect } : undefined);
|
|
518
|
+
this.graph = graph;
|
|
519
|
+
this.library = library;
|
|
520
|
+
if (this.page)
|
|
521
|
+
await showPilotActivity(this.page, "Map updated");
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Forces `here` to be re-detected from the real live page, discarding
|
|
525
|
+
* whatever was cached - the fix for a real, confirmed gap: `here` is only
|
|
526
|
+
* ever updated by an action this class itself ran (`applyPick`/`applyPath`
|
|
527
|
+
* set it to a Block's own resolved Checkpoint; `rawClick`/`rawType`/
|
|
528
|
+
* `rawGoto` reset it to `null` because *they* just changed the page). If a
|
|
529
|
+
* human co-driving the same visible session clicks/types/navigates
|
|
530
|
+
* directly - a real scenario for a headful Pilot session someone is
|
|
531
|
+
* watching, not hypothetical - none of that goes through this class at
|
|
532
|
+
* all, so `here` silently goes stale: `currentMenu()`'s own lazy check
|
|
533
|
+
* (`if (this.here === null)`, just above) only re-detects when `here` is
|
|
534
|
+
* already unknown, never to confirm a *known* value is still correct.
|
|
535
|
+
* This does not crash the session (the process/socket/page all stay
|
|
536
|
+
* alive) but subsequent `send`/`reach` calls would act on a menu built
|
|
537
|
+
* from the wrong Checkpoint until something forces re-detection - this
|
|
538
|
+
* method is that: unconditionally re-run the same `detectHere` logic the
|
|
539
|
+
* lazy path already uses, regardless of what `here` currently holds.
|
|
540
|
+
*/
|
|
541
|
+
async resync() {
|
|
542
|
+
this.page = await ensureLivePage(this.context, this.page, this.startUrl);
|
|
543
|
+
await showPilotActivity(this.page, "Running: resync");
|
|
544
|
+
this.here = await detectHere(this.page, this.library.navBlocks);
|
|
545
|
+
const menu = await this.currentMenu();
|
|
546
|
+
this.lastRunNote = `resync -> ${this.here ?? "Unknown"}`;
|
|
547
|
+
return {
|
|
548
|
+
ok: true,
|
|
549
|
+
snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
|
|
550
|
+
quit: false,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Raw interaction primitives for a page no Block covers yet (Blind Pilot -
|
|
555
|
+
* see openspec/changes/waygraph-blind-pilot). Unlike `applyPick`, the
|
|
556
|
+
* resulting Checkpoint isn't known ahead of time from a Block's own
|
|
557
|
+
* `resolve()`, so each of these invalidates the cached `here` (forcing
|
|
558
|
+
* `currentMenu()`'s existing lazy `detectHere` call to run again) rather
|
|
559
|
+
* than assuming the page didn't move anywhere a Block now recognizes.
|
|
560
|
+
* A selector matching nothing is a reported failure, not a silent no-op.
|
|
561
|
+
*/
|
|
562
|
+
async rawClick(selector) {
|
|
563
|
+
this.page = await ensureLivePage(this.context, this.page, this.startUrl);
|
|
564
|
+
await showPilotActivity(this.page, `Running (Dom): click "${selector}"`);
|
|
565
|
+
const locator = this.page.locator(selector).first();
|
|
566
|
+
if ((await locator.count()) === 0) {
|
|
567
|
+
return { ok: false, error: `no element matches selector "${selector}"` };
|
|
568
|
+
}
|
|
569
|
+
await showPilotVision(this.page, selector, `click: ${selector}`);
|
|
570
|
+
await locator.click();
|
|
571
|
+
this.here = null;
|
|
572
|
+
const menu = await this.currentMenu();
|
|
573
|
+
this.lastRunNote = `click "${selector}" -> ${this.here ?? "Unknown"}`;
|
|
574
|
+
return {
|
|
575
|
+
ok: true,
|
|
576
|
+
snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
|
|
577
|
+
quit: false,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
async rawType(selector, text) {
|
|
581
|
+
this.page = await ensureLivePage(this.context, this.page, this.startUrl);
|
|
582
|
+
await showPilotActivity(this.page, `Running (Dom): type into "${selector}"`);
|
|
583
|
+
const locator = this.page.locator(selector).first();
|
|
584
|
+
if ((await locator.count()) === 0) {
|
|
585
|
+
return { ok: false, error: `no element matches selector "${selector}"` };
|
|
586
|
+
}
|
|
587
|
+
await showPilotVision(this.page, selector, `type: ${selector}`);
|
|
588
|
+
await locator.fill(text);
|
|
589
|
+
this.here = null;
|
|
590
|
+
const menu = await this.currentMenu();
|
|
591
|
+
this.lastRunNote = `type into "${selector}" -> ${this.here ?? "Unknown"}`;
|
|
592
|
+
return {
|
|
593
|
+
ok: true,
|
|
594
|
+
snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
|
|
595
|
+
quit: false,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Presses a real key on a real focused element - a real gap found live:
|
|
600
|
+
* many real inline-edit inputs (no visible Save button at all) commit on
|
|
601
|
+
* Enter, not on blur/click-elsewhere. `key` is a Playwright key name
|
|
602
|
+
* (e.g. "Enter", "Escape", "Tab") - same vocabulary as
|
|
603
|
+
* `page.keyboard.press`, which this wraps.
|
|
604
|
+
*/
|
|
605
|
+
async rawPress(selector, key) {
|
|
606
|
+
this.page = await ensureLivePage(this.context, this.page, this.startUrl);
|
|
607
|
+
await showPilotActivity(this.page, `Running (Dom): press "${key}" on "${selector}"`);
|
|
608
|
+
const locator = this.page.locator(selector).first();
|
|
609
|
+
if ((await locator.count()) === 0) {
|
|
610
|
+
return { ok: false, error: `no element matches selector "${selector}"` };
|
|
611
|
+
}
|
|
612
|
+
await showPilotVision(this.page, selector, `press ${key}: ${selector}`);
|
|
613
|
+
await locator.press(key);
|
|
614
|
+
this.here = null;
|
|
615
|
+
const menu = await this.currentMenu();
|
|
616
|
+
this.lastRunNote = `press "${key}" on "${selector}" -> ${this.here ?? "Unknown"}`;
|
|
617
|
+
return {
|
|
618
|
+
ok: true,
|
|
619
|
+
snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
|
|
620
|
+
quit: false,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
async rawGoto(url) {
|
|
624
|
+
this.page = await ensureLivePage(this.context, this.page, this.startUrl);
|
|
625
|
+
await showPilotActivity(this.page, `Running (Dom): goto "${url}"`);
|
|
626
|
+
try {
|
|
627
|
+
await this.page.goto(url, { waitUntil: "domcontentloaded" });
|
|
628
|
+
}
|
|
629
|
+
catch (err) {
|
|
630
|
+
return { ok: false, error: `goto "${url}" failed - ${err instanceof Error ? err.message : String(err)}` };
|
|
631
|
+
}
|
|
632
|
+
this.here = null;
|
|
633
|
+
const menu = await this.currentMenu();
|
|
634
|
+
this.lastRunNote = `goto "${url}" -> ${this.here ?? "Unknown"}`;
|
|
635
|
+
return {
|
|
636
|
+
ok: true,
|
|
637
|
+
snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
|
|
638
|
+
quit: false,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Fills a real `<input type="file">` - a built-in stub kind
|
|
643
|
+
* ("image"/"pdf"/"video", see `stubFilePath`) or a caller-supplied
|
|
644
|
+
* `filePath` for anything else ("whatever", per the real request this
|
|
645
|
+
* responds to). Same raw-primitive shape as click/type/goto: works with
|
|
646
|
+
* zero Blocks, invalidates `here` since the app may react to the upload.
|
|
647
|
+
*/
|
|
648
|
+
async rawUpload(selector, stub) {
|
|
649
|
+
this.page = await ensureLivePage(this.context, this.page, this.startUrl);
|
|
650
|
+
const filePath = typeof stub === "string" ? stubFilePath(stub) : stub.filePath;
|
|
651
|
+
const label = typeof stub === "string" ? stub : filePath;
|
|
652
|
+
await showPilotActivity(this.page, `Running (Dom): upload ${label} into "${selector}"`);
|
|
653
|
+
const locator = this.page.locator(selector).first();
|
|
654
|
+
if ((await locator.count()) === 0) {
|
|
655
|
+
return { ok: false, error: `no element matches selector "${selector}"` };
|
|
656
|
+
}
|
|
657
|
+
await showPilotVision(this.page, selector, `upload ${label}: ${selector}`);
|
|
658
|
+
try {
|
|
659
|
+
await locator.setInputFiles(filePath);
|
|
660
|
+
}
|
|
661
|
+
catch (err) {
|
|
662
|
+
return { ok: false, error: `upload into "${selector}" failed - ${err instanceof Error ? err.message : String(err)}` };
|
|
663
|
+
}
|
|
664
|
+
this.here = null;
|
|
665
|
+
const menu = await this.currentMenu();
|
|
666
|
+
this.lastRunNote = `upload ${label} into "${selector}" -> ${this.here ?? "Unknown"}`;
|
|
667
|
+
return {
|
|
668
|
+
ok: true,
|
|
669
|
+
snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
|
|
670
|
+
quit: false,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Runs a whole multi-step route to `targetCheckpoint` in one call, instead
|
|
675
|
+
* of an agent hand-picking one index at a time across many separate
|
|
676
|
+
* `applyPick` round trips - real, reported pain for any non-trivial task
|
|
677
|
+
* on a rich graph ("the entire prompts of the day" driving one step per
|
|
678
|
+
* call).
|
|
679
|
+
*
|
|
680
|
+
* Two other designs were tried and rejected by direct reproduction against
|
|
681
|
+
* live saucedemo.com before this one, both undone by the same root cause:
|
|
682
|
+
* a `from: "*"` edge (e.g. "Checkout", only clickable once you're actually
|
|
683
|
+
* on the cart page with items) looks globally reachable to
|
|
684
|
+
* `findBlockPath`'s static-graph BFS, which has no live-page access and
|
|
685
|
+
* can't know the edge's real precondition isn't showing yet. Computing the
|
|
686
|
+
* whole route once upfront names a premature wildcard step immediately;
|
|
687
|
+
* scoring each live menu option by its static-graph distance to the
|
|
688
|
+
* target (greedy best-first) doesn't fix it either - a harmless self-loop
|
|
689
|
+
* Method can score just as well as real progress once wildcard "shortcuts"
|
|
690
|
+
* are baked into every distance calculation, and the session gets stuck
|
|
691
|
+
* repeating it.
|
|
692
|
+
*
|
|
693
|
+
* The fix that actually works: stop trying to be clever about live-menu
|
|
694
|
+
* visibility per step, and reuse `auto --blocks <From> <To>`'s own already
|
|
695
|
+
* -proven approach instead - compute the path ONCE (via
|
|
696
|
+
* `findBlockPathDetailed`, the same BFS `findBlockPath`/`runChainAuto`
|
|
697
|
+
* already use, extended to also return each hop's specific edge - see its
|
|
698
|
+
* own doc comment for why that extra detail is load-bearing, not cosmetic),
|
|
699
|
+
* then run each Block in it directly via `runNamedBlock`, trusting the
|
|
700
|
+
* graph the same way `runChain`'s own flow execution already does, not
|
|
701
|
+
* gated on live-menu visibility at all. The one real difference from
|
|
702
|
+
* `runChainAuto`: this runs against the session's own already-live
|
|
703
|
+
* page/context/mem, not a freshly spawned browser - the whole point of
|
|
704
|
+
* staying in one session. A step whose real precondition genuinely isn't
|
|
705
|
+
* met still fails loud, naming that step, exactly the way a mis-ordered
|
|
706
|
+
* `runChain` spec would - and a step that runs without throwing but lands
|
|
707
|
+
* somewhere other than what its own edge promised (a real, observed case,
|
|
708
|
+
* not hypothetical - see this method's own test suite) fails loud too,
|
|
709
|
+
* rather than reporting false success.
|
|
710
|
+
*/
|
|
711
|
+
async applyPath(targetCheckpoint) {
|
|
712
|
+
await this.currentMenu();
|
|
713
|
+
const from = this.here;
|
|
714
|
+
if (from === null) {
|
|
715
|
+
return { ok: false, error: "cannot path-find: the session's current Checkpoint is unknown (here is null)" };
|
|
716
|
+
}
|
|
717
|
+
const path = findBlockPathDetailed(this.graph, from, targetCheckpoint);
|
|
718
|
+
if (!path) {
|
|
719
|
+
return { ok: false, error: `no Block path from "${from}" to "${targetCheckpoint}" in the discovered graph` };
|
|
720
|
+
}
|
|
721
|
+
for (const step of path) {
|
|
722
|
+
const entry = this.library.byName.get(step.block);
|
|
723
|
+
if (!entry) {
|
|
724
|
+
return { ok: false, error: `path step "${step.block}" is in the discovered graph but not loaded in this session's library` };
|
|
725
|
+
}
|
|
726
|
+
if (entry.block.instanceOptions) {
|
|
727
|
+
return {
|
|
728
|
+
ok: false,
|
|
729
|
+
error: `path step "${step.block}" needs a specific live option chosen (an instanceOptions Block, e.g. one per product) - ambiguous for automatic routing, use applyPick directly with the specific index`,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
if (this.page)
|
|
733
|
+
await showPilotActivity(this.page, `Running (Block): ${step.block} (reach -> ${targetCheckpoint})`);
|
|
734
|
+
const result = await this.runNamedBlock(entry, step.to);
|
|
735
|
+
if (!result.ok) {
|
|
736
|
+
return { ok: false, error: `step "${step.block}" failed - ${result.error}` };
|
|
737
|
+
}
|
|
738
|
+
// A Block can resolve to a DIFFERENT real Checkpoint than this exact
|
|
739
|
+
// hop's own edge expected, without throwing at all - e.g.
|
|
740
|
+
// submit-login's own resolve() legitimately "stays on LoginPage" on
|
|
741
|
+
// bad auth instead of erroring. Trusting `result.ok` alone would
|
|
742
|
+
// silently report success while sitting on the wrong page - a real
|
|
743
|
+
// bug caught by this method's own test suite, not theoretical.
|
|
744
|
+
// `step.to` (not a name-based re-lookup - a union-Out Block can have
|
|
745
|
+
// several edges sharing one name with different `to` tags) is exactly
|
|
746
|
+
// the Checkpoint THIS hop's own edge in the computed route promised.
|
|
747
|
+
if (this.here !== step.to) {
|
|
748
|
+
return {
|
|
749
|
+
ok: false,
|
|
750
|
+
error: `step "${step.block}" ran but landed on "${this.here}", not the expected "${step.to}" - it did not throw, but did not make the expected progress either`,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return { ok: true, path: path.map((s) => s.block), snapshot: await this.currentSnapshot() };
|
|
755
|
+
}
|
|
756
|
+
async close() {
|
|
757
|
+
await this.context.close();
|
|
758
|
+
await this.browser.close();
|
|
759
|
+
}
|
|
760
|
+
}
|