waygraph 0.15.19 → 0.15.21

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 (47) hide show
  1. package/README.md +72 -1309
  2. package/dist/auto-session.d.ts +10 -1
  3. package/dist/auto-session.js +27 -7
  4. package/dist/cli.js +98 -31
  5. package/dist/engine.d.ts +20 -0
  6. package/dist/engine.js +18 -0
  7. package/dist/highlight-shorthand.d.ts +24 -0
  8. package/dist/highlight-shorthand.js +30 -0
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.js +2 -1
  11. package/dist/mem-stub.d.ts +41 -0
  12. package/dist/mem-stub.js +36 -0
  13. package/examples/saucedemo/.waygraph-auto/11c497cd.log +44 -0
  14. package/examples/saucedemo/.waygraph-auto/29d11a49.log +44 -0
  15. package/examples/saucedemo/.waygraph-auto/2c9a4003.log +44 -0
  16. package/examples/saucedemo/.waygraph-auto/32ef32a8.log +44 -0
  17. package/examples/saucedemo/.waygraph-auto/36ba01f7.log +44 -0
  18. package/examples/saucedemo/.waygraph-auto/537af83d.log +44 -0
  19. package/examples/saucedemo/.waygraph-auto/5adfacd5.log +44 -0
  20. package/examples/saucedemo/.waygraph-auto/76c7d418.log +44 -0
  21. package/examples/saucedemo/.waygraph-auto/78197960.log +44 -0
  22. package/examples/saucedemo/.waygraph-auto/7c2acbc2.log +44 -0
  23. package/examples/saucedemo/.waygraph-auto/844b63ff.log +44 -0
  24. package/examples/saucedemo/.waygraph-auto/88a4200b.log +0 -0
  25. package/examples/saucedemo/.waygraph-auto/b0eed995.log +44 -0
  26. package/examples/saucedemo/.waygraph-auto/bb69dc1a.log +44 -0
  27. package/examples/saucedemo/.waygraph-auto/bc4ae56f.log +0 -0
  28. package/examples/saucedemo/.waygraph-auto/c17253b0.log +44 -0
  29. package/examples/saucedemo/.waygraph-auto/c31a9465.log +0 -0
  30. package/examples/saucedemo/.waygraph-auto/c5ac2393.log +44 -0
  31. package/examples/saucedemo/.waygraph-auto/c701e3c2.log +0 -0
  32. package/examples/saucedemo/.waygraph-auto/d207a000.log +44 -0
  33. package/examples/saucedemo/.waygraph-auto/fa3c5655.log +44 -0
  34. package/examples/saucedemo/package-lock.json +20 -532
  35. package/examples/saucedemo/package.json +0 -1
  36. package/examples/saucedemo/src/flows/mem-stub-demo.flow.ts +23 -0
  37. package/package.json +1 -1
  38. package/templates/scaffold/README.md +21 -19
  39. package/templates/scaffold/STRUCTURE.md +37 -70
  40. package/templates/scaffold/src/map/(app_base)/docs/_nav.block.ts +1 -1
  41. package/templates/scaffold/src/map/(external)/mailpit/_methods/assert-email-content.method.block.ts +1 -1
  42. package/templates/skills/waygraph-convention.skill.md +5 -1
  43. package/templates/skills/waygraph-pilot.skill.md +1 -1
  44. package/examples/saucedemo/.waygraph-auto/4adfbe91.json +0 -8
  45. package/examples/saucedemo/.waygraph-auto/9f0287e6.json +0 -8
  46. /package/examples/saucedemo/.waygraph-auto/{4adfbe91.log → 32e2c26f.log} +0 -0
  47. /package/examples/saucedemo/.waygraph-auto/{9f0287e6.log → 4c6b9e15.log} +0 -0
package/README.md CHANGED
@@ -1,1352 +1,115 @@
1
1
  # Waygraph
2
2
 
3
- A typed graph of reusable Blocks for driving a browser through E2E flows, in place of a
4
- flat pile of ad hoc helper functions.
5
-
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).
10
-
11
- This repo is the `waygraph` npm package itself. The full live **Sauce Demo** example
12
- (Page inventory hub, Effect Add/Remove, MemNav Open details, `waygraph auto`) ships
13
- in-tree at [`examples/saucedemo`](./examples/saucedemo). `waygraph try demo` copies
14
- [`templates/quickstart`](./templates/quickstart) (same Sauce Demo Blocks) into an OS
15
- temp dir.
16
-
17
- ## Docs (GitHub Pages)
18
-
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/) -
36
- https://deviate-dv8.github.io/waygraph/saucedemo/
37
- - **Helpers (consumer copy):** [`examples/saucedemo/docs/HELPERS.md`](./examples/saucedemo/docs/HELPERS.md)
38
-
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
3
+ Write browser tests as a **graph of small, reusable steps** instead of one long script.
4
+ Built on Playwright. Every step says where it starts, where it ends, and how to confirm it
5
+ worked - so a whole app becomes something you (or an AI agent) can walk, check, and demo.
68
6
 
69
7
  ```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",
8
+ // One Block = one action. It says: start on LoginPage, end on LoggedIn, and prove it.
9
+ export const SubmitLoginBlock = defineMethodBlock<LoginPage, LoggedIn>({
10
+ name: "submit-login",
79
11
  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
- },
12
+ async act(page) { await page.locator(LoginSel.button).click(); },
85
13
  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.
298
-
299
- **`waygraph typecheck [project]`** runs `tsc --noEmit` then the same **bad-practice** scan
300
- (wildcard `Checkpoint<string>`, assert blocks missing type args, multi-`.fill()` Methods,
301
- fill+click in one Method, empty `verify` on a transition Block or `defineAssertBlock`).
302
- Practices warn only unless `tsc` fails; use `--no-practices` to skip the scan. Opt out per
303
- file: `// waygraph-ignore-practices` or `// waygraph-ignore: multi-input, combined-action,
304
- empty-verify`. Print full agent guidance: `waygraph --skill-convention`.
305
-
306
- **Empty verify is the one that matters most for codegen'd Blocks.** A framework can enforce
307
- that a check *exists* and stays structurally honest (typed, wired, right file) - it cannot
308
- author the semantic content of that check, since only someone who knows what "success" means
309
- for this specific app can write it. That's a correct place to draw the line, but it shifts
310
- all the risk onto whether anything catches a Block left with a placeholder `verify: []` by
311
- the time it ships - which would otherwise pass every static check, the orphan scan, and
312
- `tsc`, and run green in CI forever, because an empty array isn't a wrong check, it's the
313
- *absence* of one. `waygraph check`/`typecheck` now flag exactly that: a `defineMethodBlock`/
314
- `defineEffectBlock` whose `In`/`Out` Checkpoints actually differ (a real transition, not a
315
- self-loop like `fill-username`) with no `verify` key or a literal `verify: []`, and the same
316
- for `defineAssertBlock` (whose entire job is checking). Silence it deliberately, the same as
317
- any other practice warning, with `// waygraph-ignore: empty-verify` - e.g. when a downstream
318
- `defineAssertBlock` right after it in the `.flow.ts` is what actually confirms the transition
319
- (see `templates/scaffold`'s own `open-message.method.block.ts`, confirmed by
320
- `AssertEmailContentBlock` right after it, not by its own `verify`).
321
-
322
- ### Demo narration
323
-
324
- Automation rings are gray; authored stubs/slides can use
325
- `tone: "info" | "warning" | "danger" | "success"` (iconified captions), plus
326
- `size: "sm" | "md" | "lg"` and `weight: "normal" | "bold"`. Flow episode defaults:
327
- `withHighlightStyle(flow, { size, weight, tone? })` (slot/fixture wins). Episode pacing:
328
- `withDemoPace(flow, "fast" | "slow" | "normal" | "blitz" | number)` or
329
- `withBlockPace(block, ...)` (`number <= 20` = scale vs normal, e.g. `0.5` / `2`; `> 20` =
330
- absolute ms, e.g. `4500`). FFCompose stays blitz. Living objectives:
331
- `docs/proposals/OBJECTIVES.md`.
332
-
333
- ### Graph traversal
334
-
335
- `waygraph traverse` walks the Block graph. `--parallel N` (default `--session clone`)
336
- bootstraps one context, forks N workers with `storageState` + mem snapshot, partitions
337
- edges by hash, and claims via in-process edge leases (audit trail under
338
- `.waygraph-traverse/`). `--session inherit` is refused when `parallel > 1`. It writes
339
- `.waygraph-traverse/coverage.json` and prints `[Coverage edges=H/T ratio=R%...]`;
340
- `--min-edge-coverage 80%` fails the suite with exit 2 when the ratio is below the gate
341
- (even if every worker leaf-PASSed). `--coverage-out PATH` / `--no-coverage-report` adjust
342
- the JSON. See `docs/proposals/traverse-ffcompose-rfc.md`.
343
-
344
- ### Not yet implemented
345
-
346
- Tracked on the project board / roadmap, not in this package today:
347
-
348
- - Interstitials/Watchers - background overlay handling (cookie banners, popups).
349
- - Split `requires` (externally-supplied vs producedBy) recovery hints.
350
- - Federated multi-package pool of waygraphs.
351
-
352
- ## Block helpers (Page, Method, Sel)
353
-
354
- Runtime is still one type: `Block`. Helpers are TypeScript salt.
355
-
356
- | Helper | Role | Typical file |
357
- |--------|------|----------------|
358
- | `definePageBlock` | Screen hub (checkpoint + `verify` + registered methods) | `*.page.block.ts` |
359
- | `defineMethodBlock` | One-shot non-nav step, exactly one action (submit, upload, logout) | `methods/*.method.block.ts` |
360
- | `defineAssertBlock` | Self-loop-only assertion (no state change) - `verify` only | `methods/*.method.block.ts` |
361
- | `defineEffectBlock` | Instance mutate + `instanceOptions` auto menu | `methods/*.effect.block.ts` |
362
- | `defineNavBlock` / `defineNavClickBlock` | `goto` / click-nav | `nav-*.block.ts` |
363
- | `defineMemNavBlock` | Nav + per-row `instanceOptions` | `nav-*.block.ts` |
364
- | `defineActionBlock` | Deprecated alias of `defineMethodBlock` | - |
365
-
366
- **Page hub:** one Checkpoint = this screen. Methods hang off the page for readability /
367
- auto grouping. Arrival-only hubs omit `url`/`click` (previous Block already landed here).
368
- Deep-link hubs pass `url` or `click` like a Nav.
369
-
370
- **One distinct action per Block - hard rule.** A Method that fills form fields *and*
371
- submits, or that drives several steps inside one `act()`, is wrong even if it "works" -
372
- split it into atomic Blocks (a login form is `fill-username` + `fill-password` +
373
- `submit-login`, never one Block doing all three; see `examples/saucedemo`'s own
374
- `saucedemo-web/methods/` for the real split). A Block that only asserts something on the
375
- current page, with no state change, is `defineAssertBlock({ name, checkpoint, verify,
376
- waitForHeading?, requires? })` - self-loop and `resolve` are generated for you, so there's no
377
- hand-written `act`/`resolve` to accidentally make do two things. It also accepts
378
- `stubBefore`/`stubAfter`/`stubOnError`/`slides`, the same narration fields every other
379
- Block helper takes, so converting a hand-written assertion Method loses no demo/highlight
380
- fixture data. `requires` (declare it whenever a mem-aware Trait reads a mem key the caller
381
- must supply externally - see the mail example below) lets preflight catch a missing input
382
- before the flow ever runs, the same as every other Block helper:
383
-
384
- ```typescript
385
- export const AssertInventoryHeaderBlock = defineAssertBlock({
386
- name: "assert-inventory-header",
387
- checkpoint: "LoggedIn",
388
- waitForHeading: "Products",
389
- verify: [Trait.visible(InventorySel.list)],
390
- });
391
- ```
392
-
393
- `waygraph check` warns when a `verify` array inlines a literal selector string
394
- (`Trait.visible("#some-id")`) instead of referencing a `*Sel` object - the exact pattern
395
- `defineAssertBlock` exists to make easy to avoid.
396
-
397
- **Give `defineAssertBlock` an explicit type argument when it sits mid-chain.** Without one,
398
- `Out` defaults to wildcard `Checkpoint<string>` on both sides - harmless when the assert is
399
- the last real Block before `end` (nothing downstream needs it narrower), but a `defineFlow`
400
- array's tuple typing breaks once a wildcard Block is sandwiched between two
401
- specifically-typed ones. Write `defineAssertBlock<LoggedIn>({ ..., checkpoint: "LoggedIn",
402
- ... })` any time the assert has a real Block after it, not just before.
403
-
404
- **An assert Block can check the *result* of an action, not just static page state.** Since
405
- `Trait.check(page, mem)` receives mem as well as the page, an assert placed right after a
406
- Method/Effect can confirm what that specific action actually did - not a duplicate of that
407
- Block's own inline `verify`, a genuinely separate, nameable QA checkpoint:
408
-
409
- ```typescript
410
- const removeButtonVisibleForSelectedItem: Trait = {
411
- name: "remove-button-visible-for-selected-item",
412
- async check(page, mem) {
413
- const { id } = mem.get(SelectedItem.key); // what add-item just acted on
414
- return page.locator(RemoveBtnSel(id)).isVisible();
415
- },
416
- };
417
-
418
- export const AssertItemAddedBlock = defineAssertBlock<ItemInCart>({
419
- name: "assert-item-added",
420
- checkpoint: "ItemInCart",
421
- verify: [removeButtonVisibleForSelectedItem, Trait.text(CartCountSel, "1")],
422
- });
423
-
424
- // wired right after the action it checks:
425
- engine.defineFlow([start, NavHomeBlock, AddItemBlock, AssertItemAddedBlock, ClearCartBlock, end])
426
- ```
427
-
428
- Live reference: `templates/scaffold/src/map/(app_base)/home/_methods/assert-item-added.method.block.ts`,
429
- wired into `shop.flow.ts`.
430
-
431
- **An assert Block also names a feature *state*, not just a result or static content.** A
432
- submit button greyed out until required fields are filled, a "Save" button re-enabling
433
- after a successful edit, a field-level validation error appearing under one specific input
434
- - these are conditions the UI is *in*, worth a nameable Checkpoint of their own the same way
435
- `AssertItemAddedBlock` names "did add-item work." There's no built-in `Trait.disabled` -
436
- write the state check as a custom Trait against the real DOM condition (`isDisabled()`, a
437
- class, an `aria-disabled` attribute - whatever the app actually uses), the same pattern as
438
- `removeButtonVisibleForSelectedItem` above:
439
-
440
- ```typescript
441
- const submitGatedUntilFieldsComplete: Trait = {
442
- name: "submit-gated-until-fields-complete",
443
- async check(page) {
444
- return page.locator(CheckoutSel.submitButton).isDisabled();
445
- },
446
- };
447
-
448
- export const AssertSubmitGatedBlock = defineAssertBlock<CheckoutInfoPage>({
449
- name: "assert-submit-gated",
450
- checkpoint: "CheckoutInfoPage",
451
- verify: [submitGatedUntilFieldsComplete],
452
- });
453
-
454
- // wired right after the fields that gate it, before the ones that clear the gate:
455
- engine.defineFlow([start, NavCheckoutInfoBlock, AssertSubmitGatedBlock, FillFirstNameBlock, ...])
456
- ```
457
-
458
- Same rule as any other assert Block: give it an explicit type argument when it sits
459
- mid-chain (see above), and keep the selector in a `*Sel` object, never inlined in the Trait.
460
-
461
- **`methods/` folder:** on-page work lives next to the route, not free-floating.
462
-
463
- **`*Sel`:** DOM selectors only (static strings + `(id) => ...` for item-no-1 style). Mem
464
- keys store values (which item / upload queue), not selectors. `instanceOptions` scrapes the
465
- live DOM into menu rows and `mem.set`s on pick; predefined methods seed mem from the flow
466
- instead.
467
-
468
- ```typescript
469
- import { definePageBlock, Trait } from "waygraph";
470
-
471
- export const InventorySel = {
472
- list: ".inventory_list",
473
- addBtn: (id: string) => `[data-test="add-to-cart-${id}"]`,
474
- };
475
-
476
- export const InventoryPage = definePageBlock({
477
- name: "page-inventory",
478
- checkpoint: "LoggedIn",
479
- verify: [
480
- Trait.url({ pathname: "/inventory.html" }),
481
- Trait.visible(InventorySel.list),
482
- ],
483
- methods: {
484
- addToCart: () => AddToCartBlock,
485
- removeFromCart: () => RemoveFromCartBlock,
486
- addAllToCart: () => AddAllToCartBlock,
487
- removeAllFromCart: () => RemoveAllFromCartBlock,
488
- },
489
- });
490
- ```
491
-
492
- Live reference: `examples/saucedemo/src/blocks/saucedemo-web/inventory/`
493
- (`inventory.page.block.ts` + `methods/` + `InventorySel`). Bulk `add-all-to-cart` /
494
- `remove-all-from-cart` are one menu row each (`from: "*"` - works after leave/return).
495
-
496
- ## Mail adapters (cross-origin, browser-driven)
497
-
498
- For flows driven by an email (signup confirmation, password reset, a magic link), the
499
- convention is **not** a REST/HTTP client to the mail catcher - it's more Blocks, pointed at
500
- the catcher's own web UI (MailHog, MailDev, Mailpit all ship one) as a real, separate,
501
- cross-origin page. This needs zero new engine surface: a `NavBlock` navigates there like any
502
- other page, `MethodBlock`s read the DOM with ordinary `page.locator`/`page.frameLocator`
503
- calls, and a final `NavBlock` carries the extracted link back into the app under test.
504
- Convergent, real-world evidence for this exact shape: independently, more than one real
505
- consumer project settled on it rather than a REST client.
506
-
507
- **Keep it in its own `*-external/<tool>/` folder, never mixed into your app's own Blocks** -
508
- it's a different origin, driving a different site:
509
-
510
- ```typescript
511
- // demo-external/mailpit/nav-mailpit-inbox.block.ts
512
- export const NavMailpitInboxBlock = defineNavBlock<MailpitInbox>({
513
- name: "nav-mailpit-inbox",
514
- checkpoint: "MailpitInbox",
515
- url: () => process.env.WAYGRAPH_MAIL_URL ?? "http://127.0.0.1:8025",
516
- verify: [Trait.url({ pathname: "/" })],
517
- });
518
-
519
- // demo-external/mailpit/methods/open-message.method.block.ts
520
- export const OpenMessageBlock = defineMethodBlock<MailpitInbox, MailpitMessageOpen>({
521
- name: "open-message",
522
- requires: [ExpectedRecipient.key], // externally supplied - avoids picking the wrong inbox row
523
- instruction: {
524
- async act(page, _in, mem) {
525
- const { email } = mem.get(ExpectedRecipient.key);
526
- await page.locator(MailpitSel.messageRow, { hasText: email }).first().click();
527
- await page.waitForURL(/\/view\//);
528
- },
529
- resolve: () => checkpoint("MailpitMessageOpen"),
530
- },
531
- });
532
-
533
- // demo-external/mailpit/methods/extract-email-link.method.block.ts
534
- // Named for what it does, not for one scenario - see "reusable across
535
- // scenarios" below for why.
536
- export const ExtractEmailLinkBlock = defineMethodBlock<MailpitMessageOpen, MailpitMessageOpen>({
537
- name: "extract-email-link",
538
- requires: [ExpectedLinkPattern], // which link in the body to follow - externally supplied
539
- instruction: {
540
- async act(page, _in, mem) {
541
- const pattern = mem.get(ExpectedLinkPattern);
542
- const link = page.frameLocator(MailpitSel.previewIframe).locator(`a[href*="${pattern}"]`).first();
543
- const href = await link.getAttribute("href");
544
- if (!href) throw new Error(`no link matching "${pattern}" found`);
545
- mem.set(EmailLink({ url: href }));
546
- },
547
- resolve: () => checkpoint("MailpitMessageOpen"),
548
- },
549
- });
550
-
551
- // demo-web/nav-verification-link.block.ts - back into the app, no `requires` (see below)
552
- export const NavVerificationLinkBlock = defineNavBlock<HomeVerified>({
553
- name: "nav-verification-link",
554
- checkpoint: "HomeVerified",
555
- url: (mem) => mem.get(EmailLink.key).url,
556
- verify: [Trait.visible(DemoSel.verifiedBanner)],
557
- });
558
- ```
559
-
560
- Four atomic Blocks, each doing exactly one thing - `open-message` and `extract-email-link`
561
- stay separate Methods for the same reason `fill-username`/`submit-login` do in the Sauce
562
- Demo example: reading the link and navigating to it are two distinct actions, so they are
563
- two distinct Blocks (`nav-verification-link` does the actual navigation).
564
-
565
- Note `nav-verification-link` deliberately does **not** declare `requires: [EmailLink.key]` -
566
- `requires` means "the caller must seed this externally before the flow starts" (preflight
567
- checks a whole chain's requires up front), and this key is instead produced by
568
- `extract-email-link` earlier in the very same chain. Declaring it as a requirement would make
569
- preflight reject the run before it ever gets a chance to produce it - `requires` is only for
570
- keys a caller truly must supply from outside the chain (like `ExpectedRecipient` above).
571
-
572
- Live reference, proven end to end against a real, throwaway Mailpit container:
573
- `templates/scaffold/src/map/(external)/mailpit/` + `src/flows/mail-verify.flow.ts` +
574
- `tests/mail-verify.spec.ts`.
575
-
576
- ### Reusable across scenarios: mem-driven, not one Block per email
577
-
578
- The natural instinct once you have a signup-verification flow is to want a matching set of
579
- Blocks for password reset, then another for a magic link, and so on - N scenarios, N Block
580
- sets. That's the wrong axis to scale on. `open-message`, `extract-email-link`, and the two
581
- assert Blocks below are already scenario-agnostic: **only mem varies per run, never the
582
- Blocks**:
583
-
584
- | Mem key | Answers |
585
- |---|---|
586
- | `ExpectedRecipient` | Which inbox row to open |
587
- | `ExpectedLinkPattern` | Which link in the body to follow (a real email often has more than one - "report abuse" and the real link both present) |
588
- | `ExpectedEmailContent` | What the body is supposed to say |
589
-
590
- The same four Blocks (`assert-email-received`, `open-message`, `assert-email-content`,
591
- `extract-email-link`) run a signup-verification check and a password-reset check identically
592
- - just with different mem, in `templates/scaffold/tests/mail-verify.spec.ts`:
593
-
594
- ```typescript
595
- mem.set(ExpectedRecipient({ email: "demo-user@example.com" }));
596
- mem.set(ExpectedLinkPattern, "verified=1");
597
- mem.set(ExpectedEmailContent, "Please confirm your email address...");
598
- await mailVerifyFlow.run(context, mem); // signup verification
599
-
600
- mem.set(ExpectedRecipient({ email: "password-reset-user@example.com" }));
601
- mem.set(ExpectedLinkPattern, "verified=1"); // picks the real link, not the "report abuse" decoy also in the body
602
- mem.set(ExpectedEmailContent, "We received a request to reset your password.");
603
- await mailVerifyFlow.run(context, mem); // same flow, same Blocks, different email entirely
604
- ```
605
-
606
- Naming matters here: a Block named `extract-verification-link` would wrongly imply it only
607
- ever handles verification emails - once a Block's behavior is mem-driven, its name should
608
- describe the mechanism (`extract-email-link`), not one scenario that happens to use it.
609
-
610
- A **mail-provider page hub** ties it together, the same `definePageBlock` pattern used
611
- everywhere else - one page, several reusable methods, each pulling its specifics from mem:
612
-
613
- ```typescript
614
- export const MailpitInboxPageBlock = definePageBlock<MailpitInbox>({
615
- name: "page-mailpit-inbox",
616
- checkpoint: "MailpitInbox",
617
- verify: [Trait.url({ pathname: "/" })],
618
- methods: {
619
- assertEmailReceived: () => AssertEmailReceivedBlock,
620
- openMessage: () => OpenMessageBlock,
621
- assertEmailContent: () => AssertEmailContentBlock,
622
- extractEmailLink: () => ExtractEmailLinkBlock,
623
- },
624
- });
625
- ```
626
-
627
- ### QA-rich: does the email exist, and does it say the right thing
628
-
629
- Two more assert Blocks slot into the same chain, both mem-driven, both proven to fail loud
630
- (not silently pass) on a real negative case - no email arrived, and an email arrived with
631
- the wrong copy:
632
-
633
- ```typescript
634
- // "Did the email even arrive" - doesn't open/consume it, safe to check before deciding to.
635
- // A mem-aware Trait: the selector depends on the expected recipient, so it
636
- // can't be a static Trait.visible(...) string - check(page, mem) already gets mem.
637
- const emailReceived: Trait = {
638
- name: "email-received",
639
- async check(page, mem) {
640
- const { email } = mem.get(ExpectedRecipient.key);
641
- try {
642
- await page.locator(MailpitSel.messageRow, { hasText: email }).first().waitFor({
643
- state: "visible",
644
- timeout: 10_000,
645
- });
646
- return true;
647
- } catch {
648
- return false;
649
- }
650
- },
651
- };
652
- export const AssertEmailReceivedBlock = defineAssertBlock<MailpitInbox>({
653
- name: "assert-email-received",
654
- checkpoint: "MailpitInbox",
655
- requires: [ExpectedRecipient.key],
656
- verify: [emailReceived],
657
- });
658
-
659
- // "Does the email say what it's supposed to" - reads the body content, not
660
- // just whether a link exists. The expected text comes from mem too (same
661
- // reuse reasoning as above) via another bespoke mem-aware Trait, since
662
- // Trait.frameContains's own factory bakes its expected string in at
663
- // Block-definition time, not runtime.
664
- const bodyContainsExpectedText: Trait = {
665
- name: "body-contains-expected-text",
666
- async check(page, mem) {
667
- const expected = mem.get(ExpectedEmailContent);
668
- const text = await page.frameLocator(MailpitSel.previewIframe).locator("body").textContent();
669
- return (text ?? "").includes(expected);
14
+ verify: [Trait.url({ pathname: "/inventory.html" })],
670
15
  },
671
- };
672
- export const AssertEmailContentBlock = defineAssertBlock<MailpitMessageOpen>({
673
- name: "assert-email-content",
674
- checkpoint: "MailpitMessageOpen",
675
- requires: [ExpectedEmailContent],
676
- verify: [bodyContainsExpectedText],
677
16
  });
678
- ```
679
-
680
- `Trait.frameVisible(frameSelector, innerSelector)` / `Trait.frameText(frameSelector,
681
- innerSelector, expected)` (exact match) / `Trait.frameContains(frameSelector, innerSelector,
682
- expected)` (substring match, for a *fixed* known expected string) mirror
683
- `Trait.visible`/`Trait.text` exactly, scoped to a frame - reusable for any iframe content,
684
- not just mail; reach for a bespoke mem-aware Trait instead when the expected value itself
685
- needs to vary per run, as both examples above do. Wired into the chain right where they
686
- belong:
687
17
 
688
- ```typescript
689
- engine.defineFlow([
690
- start, NavMailpitInboxBlock, AssertEmailReceivedBlock, OpenMessageBlock,
691
- AssertEmailContentBlock, ExtractEmailLinkBlock, NavVerificationLinkBlock, end,
692
- ])
18
+ // A Flow = Blocks in order.
19
+ export const loginFlow = engine.defineFlow([start, NavLoginBlock, FillUsernameBlock, FillPasswordBlock, SubmitLoginBlock, end]);
693
20
  ```
694
21
 
695
- ### Where this shows up in `waygraph auto`
696
-
697
- A `NavBlock` always has wildcard `In` (`from: "*"` in the static graph), so it's reasonable to
698
- wonder whether `nav-mailpit-inbox` clutters the live picker on every single screen. It
699
- doesn't - `waygraph auto`'s menu builder already special-cases exactly this
700
- (`src/auto-explore.ts`, `buildExploreMenu`): a URL-based Nav is excluded from the interactive
701
- menu once you're on a known screen, offered only as a "Start here" bootstrap option. The
702
- static graph still keeps the `from: "*"` edge for `waygraph graph`/`findBlockPath`/`traverse`,
703
- so `waygraph auto --blocks "<AppCheckpoint> MailpitInbox"` can still route there deliberately -
704
- it's just never suggested unprompted. The tested, recommended way to actually use this is an
705
- authored flow/chain (`someAppFlow then mailVerifyFlow`), exactly like `mailVerifyFlow` itself.
706
-
707
- ## Waygraph Pilot
708
-
709
- Bootstraps a real, persistent, inspectable browser session for an **agent** to discover and
710
- drive toward a natural-language goal it plans itself - not a tool that resolves one ask to one
711
- Block internally. A real request is a multi-step task ("log in and buy the backpack for me",
712
- "make a signature draft request") that needs an agent reasoning over a sequence of Blocks, not
713
- a single best-matching edge. Comparable in shape to a browser-use-style agent, but the action
714
- space is constrained to this project's own pre-verified Blocks (each with a real resolve/verify
715
- already checked) instead of freeform DOM/pixel guessing - narrower, but far more accurate and
716
- deterministic.
717
-
718
- ### auto vs browser vs pilot
719
-
720
- | Layer | Command | What it is |
721
- |-------|---------|------------|
722
- | **auto** | `waygraph auto` | Interactive explore (picker / `--cli` menu). **Not** a persistent agent browser. |
723
- | **browser** | `waygraph browser start` | New persistent Playwright session — **headful by default**, **about:blank by default**. |
724
- | **pilot** | `waygraph pilot start` | Bootstrap only: `browser start` + whole-project `graph` + starting `snapshot` in one JSON payload. Does not plan or act. |
725
-
726
- Session control (`send`, `status`, `highlight`, `dom`, …) works on **`auto`**, **`browser`**, and
727
- **`pilot`** — same socket, same session id. Prefer the `browser`/`pilot` prefix when driving a
728
- detached session; `auto` remains for the interactive picker and legacy `--cli --detach`.
729
-
730
- **Session lifecycle is explicit** — list what's live before opening another:
731
-
732
- ```bash
733
- waygraph browser # show browser subcommands
734
- waygraph browser sessions [--json] # list live sessions; --json for agents
735
- waygraph browser start # always opens a new session
736
- waygraph browser stop <sessionId> # shut one down
737
- waygraph browser stop --all # shut every session for this project
738
- waygraph browser attach <sessionId> # terminal picker on an existing session
739
- waygraph pilot attach <sessionId> # graph + snapshot, no new browser
740
- ```
22
+ ## Try it in 60 seconds
741
23
 
742
24
  ```bash
743
- waygraph pilot start
744
- # {"sessionId":"bb65d74c","socketPath":"...","headless":false,"graph":{...},"snapshot":{...}}
25
+ npx waygraph try demo # watch a full Sauce Demo checkout, step by step (nothing written to your folder)
745
26
  ```
746
27
 
747
- `pilot start` is one round trip for agents that need the whole graph immediately. For browser-only
748
- work (no graph payload), use `browser start`. `auto --cli --detach` still works but is the older
749
- explore entry point — prefer `browser start` for agent-driven sessions.
750
-
751
- From a live session, drive one pick at a time:
28
+ ## Start a project
752
29
 
753
30
  ```bash
754
- waygraph browser status <sessionId> # read the current menu (no side effects)
755
- waygraph browser send <sessionId> "<pick>" # run one pick, get the resulting menu back
756
- waygraph browser dom <sessionId> # inspect the live DOM/ARIA tree
757
- waygraph browser trace <sessionId> # read the Checkpoint/Block history so far
758
- waygraph browser reach <sessionId> <Checkpoint> # run a whole multi-step route in one call
759
- waygraph browser resync <sessionId> # force here to re-detect from the real page
760
- ```
761
-
762
- (`auto …` and `pilot …` accept the same subcommands with the same `<sessionId>`.)
763
-
764
- Worked example (run for real against `examples/saucedemo`, no new execution code involved): an
765
- agent gets a plain request - "log me in and buy the backpack" - reads the graph and the starting
766
- `status`, reasons out login -> add-to-cart -> cart -> checkout info -> submit -> finish as the
767
- route to `OrderComplete`, then drives it by repeatedly reading each `status`/`send` response and
768
- picking the next index itself. The browser stays open the whole time; the user can watch it, or
769
- check the final tab afterward.
770
-
771
- **`auto reach <sessionId> <Checkpoint>`** runs a whole route in one call instead of one
772
- `send` per step - real, reported pain for any non-trivial task on a rich graph ("the entire
773
- prompts of the day" for one manual walkthrough). It path-finds via the same BFS `auto --blocks
774
- <From> <To>` already uses, then runs each step directly against the session's own live
775
- page/mem, verifying every step actually lands where its own graph edge promised (not just that
776
- it ran without throwing - a Block can legitimately resolve elsewhere, e.g. a login submission
777
- staying put after bad auth). **Real, stated limitation, not silently papered over:** for a
778
- target only reachable by crossing a wildcard (`from: "*"`) edge whose real DOM precondition
779
- depends on same-Checkpoint setup (e.g. filling form fields before a submit), neither `reach`
780
- nor the already-shipped `auto --blocks` can safely route there automatically - both fail loud
781
- within a bounded time (a real Playwright click/wait timeout) rather than silently guessing
782
- wrong or hanging. Full design history - three rejected approaches, a real correctness bug this
783
- change's own tests caught, and this limitation confirmed by direct reproduction - is in
784
- `openspec/changes/waygraph-pilot/tasks.md`'s own M5 section.
785
-
786
- **`auto resync <sessionId>`** - `here` is only ever updated by an action the session itself
787
- ran (`send`/`reach` set it from a Block's own resolved Checkpoint; the raw `click`/`type`/
788
- `goto` primitives null it because *they* just changed the page). Anything that changes the
789
- page *outside* the session - a human clicking around in a visible `--non-headless` session
790
- someone is co-driving, for instance - leaves `here` silently stale, since `send`/`reach` only
791
- ever re-detect when the position is already unknown, never to double-check a known one.
792
- `resync` forces that re-detection unconditionally, right now, against whatever the real page
793
- actually shows. Doesn't crash or corrupt anything if you never need it - it's a real, found
794
- gap (confirmed by reading the exact code path, `src/auto-session.ts`'s `currentMenu()`), fixed
795
- outright rather than left as a caveat.
796
-
797
- **Coverage gaps `waygraph check`/`graph` can't see, because there's no Block to scan yet.**
798
- Static tools only find gaps in Blocks that already exist - a real link or button on the live
799
- page with *no Block written for it at all* has nothing in source to catch it. Every live
800
- `auto`/`browser`/`pilot` session watches the real page instead and warns (once per element,
801
- via `console.warn`) on a same-origin `<a href>` no `defineNavBlock` covers, or a visible
802
- `button`/`[role=button]`/submit input no Method/NavClick Block selector matches:
803
-
804
- ```text
805
- [waygraph] unmapped nav link on this page: /settings - no NavBlock covers this URL; add defineNavBlock
806
- [waygraph] unmapped button on this page: #save-draft - no Method/NavClick Block selector matches; consider defineMethodBlock or defineNavClickBlock
31
+ npx waygraph init my-app
32
+ cd my-app
33
+ npm install && npx playwright install chromium
34
+ npm test # green immediately, fully offline
35
+ npm run test:ui # same tests in Playwright's interactive UI
36
+ npm run auto # explore the app by picking Blocks in a browser
807
37
  ```
808
38
 
809
- Read them back with `auto console <sessionId>` - the same channel real console/network errors
810
- already come through, not a separate command (`src/coverage-gap.ts`, wired into every session
811
- in `src/auto-session.ts`).
812
-
813
- **On-page indicator.** A headful (`--non-headless`) session looks like an ordinary browser
814
- tab otherwise - no sign anything is driving it, which matters once a human might be watching
815
- or co-driving the same visible session. Every session injects a small badge (bottom-right
816
- corner: `Waygraph Pilot - session <id> - <Checkpoint>`) that click-expands into a panel
817
- listing the current live menu's real edges (block name, kind, target Checkpoint,
818
- description) - the same data `auto status` returns, rendered in the page itself. The overlay
819
- shell is installed via `context.addInitScript`, so it **persists on `about:blank`** and
820
- survives `goto` navigations (browser start defaults to blank; pilot navigates when a base URL
821
- exists). Headful sessions paint the badge immediately on start — no need to call `status`
822
- first. Bottom-left activity toasts label raw primitives as **`Running (no Block): …`**
823
- (click/type/goto/upload outside the Block library); Block runs stay **`Running (Block): …`**.
824
- Both refresh automatically after every `send`/`reach`/`reload`/`resync`/raw action.
825
- Self-contained (`src/pilot-overlay.ts`), not built on `waygraph demo`'s own panel/banner
826
- system.
39
+ Already have a repo? `npm install waygraph @playwright/test`.
827
40
 
828
- **Pilot itself does no planning, resolving, narrating, or acting** - `pilotStart`'s only job is
829
- starting the session and handing back its graph in one round trip; see `src/pilot.ts`. An
830
- earlier shape (`pilot ask "<text>"`, a deterministic ask-to-one-edge text matcher) was built,
831
- shipped, and then explicitly rejected as not matching this goal - preserved for reference at git
832
- tag `waygraph-pilot-v1-logs-prettified`, not on any active branch.
41
+ ## Run the Sauce Demo example
833
42
 
834
- **Honest scope, not silently redefined:** this capability's own proof runs against an in-repo
835
- example (`examples/saucedemo`), not a real external consumer application - it demonstrates the
836
- mechanism works, not the "demoable on one real consumer" bar `ROADMAP.md` sets for `1.0.0`,
837
- which remains separate, later, unmet work. Live reference: `tests/pilot/pilot.spec.ts` (the
838
- library function) and `tests/cli/pilot.spec.ts` (the real CLI), both proven against live
839
- saucedemo.com.
840
-
841
- ## Blind Pilot (cold start, zero Blocks)
842
-
843
- Waygraph Pilot above assumes a Block library already exists. Blind Pilot is the opposite
844
- case: a session on a site nobody has described to this package yet. `AutoSession`/`auto --cli
845
- --detach` already tolerate a project directory with **zero** `.block.ts` files (no crash,
846
- `here` just stays `null`), so an agent can open a real session and start exploring cold with
847
- no new code at all - `auto dom <sessionId>` already gives real DOM/ARIA visibility
848
- independent of any Block library.
849
-
850
- What's actually new here is two primitives an agent needs to go from "exploring" to "building
851
- a Block library incrementally":
43
+ The full example (`examples/saucedemo`) is a real project against saucedemo.com.
852
44
 
853
45
  ```bash
854
- waygraph auto click <sessionId> "<selector>" # act before any Block covers this
855
- waygraph auto type <sessionId> "<selector>" "<text>" # same, for a real input
856
- waygraph auto goto <sessionId> "<url>" # navigate the real live page
857
- waygraph auto reload <sessionId> # pick up a Block just written to disk,
858
- # without restarting the session
46
+ git clone https://github.com/deviate-dv8/waygraph.git
47
+ cd waygraph
48
+ npm install && npm run build # the example uses this checkout of waygraph
49
+ npx playwright install chromium
50
+ cd examples/saucedemo && npm install
51
+ npm test # 15 tests against the live site
52
+ npm run demo # watch the checkout flow with the step overlay
53
+ npm run auto # explore it by hand
859
54
  ```
860
55
 
861
- `click`/`type`/`goto` each re-detect the session's Checkpoint afterward (the same detection
862
- `send`/`status` already use) and return a fresh snapshot - a selector matching nothing is a
863
- reported failure naming it, not a silent no-op. `reload` re-discovers the project's Block
864
- library/graph from disk in place, leaving the live page, mem, and current Checkpoint
865
- untouched - so a Block an agent just wrote to disk becomes immediately runnable via the
866
- session's existing `send`, without relaunching the browser.
56
+ ## The four ideas
867
57
 
868
- **This package writes no Block content.** The driving agent recognizes a pattern ("this is a
869
- login form") and writes the `.block.ts`/`*Sel`/mem-keys files itself, using the conventions
870
- documented above - the same way it would write any other source file. Worked example (proven
871
- end to end in `tests/cli/auto-session-blind-pilot.spec.ts` against an in-repo synthetic
872
- fixture, `tests/fixtures/blind-pilot-site/`): start a session with zero Blocks, explore via
873
- `dom`/`click`/`goto`, write one real `defineNavBlock` to disk, `reload`, then `send` it -
874
- reaching the real Checkpoint it declares, proving it's genuinely driveable, not just visible.
875
-
876
- **Two real gotchas found while proving this, worth knowing before debugging a silent
877
- failure:** a `.block.ts` file that throws on import (e.g. a malformed Trait call) vanishes
878
- from the graph with no error printed anywhere - `discoverGraph`'s per-file import errors are
879
- mostly swallowed silently, so "reload did nothing" can mean a real mistake in the file just
880
- written, not a package bug. And a Block not wired into any `.flow.ts` (`waygraph graph`'s own
881
- "orphan" warning) is still fully visible and runnable in a live session's menu - orphan status
882
- only affects `auto --blocks <From> <To>` path-finding, not `auto`'s own menu.
58
+ | Word | Meaning |
59
+ |---|---|
60
+ | **Checkpoint** | A named place the app can be: `LoginPage`, `LoggedIn`, `OrderComplete`. |
61
+ | **Block** | One action from one Checkpoint to another, with a `verify` that confirms it. |
62
+ | **Flow** | Blocks in order. Run it, demo it, or let `auto` pick the route. |
63
+ | **Mem** | Typed shared data for a run (`LoginCreds`, `SelectedItem`). |
883
64
 
884
- **Honest scope:** Blind Pilot does not currently author into the Waygraph Map convention
885
- below by default - it still writes plain files wherever it's pointed. Proof stays against an
886
- in-repo synthetic fixture, not a real external site.
65
+ Block kinds: **Nav** (go to a page), **Method** (one action), **Effect** (act on one row of a
66
+ list), **Assert** (confirm a state, e.g. a button is disabled), **Page** (a page's hub).
887
67
 
888
- ## Waygraph Map
68
+ ## Where files go: the Map
889
69
 
890
- An opinionated, Next.js/Nuxt-style folder convention where a project's own directory
891
- structure mechanically *is* its map - not a separate schema file to generate, maintain, or
892
- let drift out of sync. Needs zero new engine code: `discoverGraph`/`loadBlockLibrary` already
893
- recurse through any folder structure, matching only the `.block.ts` filename pattern - folder
894
- naming and depth are already 100% cosmetic to every existing command.
70
+ Use the **Waygraph Map**. Folders under `src/map/` mirror your app's URLs, one folder per page:
895
71
 
896
72
  ```text
897
- src/map/
898
- (app_base)/ # the landing/base group - purely organizational, never part of
899
- dashboard/ # any Checkpoint tag. One folder per Checkpoint - "Dashboard"
900
- _page.block.ts # arrival hub (definePageBlock) - fixed name, every folder
901
- _nav.block.ts # navigation (defineNavBlock) - fixed name, every folder
902
- _sel.ts # selectors for this Checkpoint - fixed name, every folder
903
- _methods/ # this Checkpoint's own action Blocks - fixed name, every
904
- click-widget.block.ts # folder. Contents keep their own descriptive names -
905
- (external)/ # only the folder itself is fixed.
906
- docs/ # a genuinely cross-origin group, same shape - "Docs"
907
- _page.block.ts
908
- _nav.block.ts
909
- _sel.ts
910
- ```
911
-
912
- **Not just a naming preference - a forced convention.** Folder nesting under `map/(group)/`
913
- must **verbatim-match** the real site's own URL path segments, `(group)` parens excluded
914
- (purely organizational, same as a Next.js route group): `/app/chats` lives at
915
- `map/(app_base)/app/chats/`, never `map/(auth)/chats/` for a fabricated grouping that was
916
- never a real `/auth/*` URL. `waygraph map [project]` (see [CLI reference](#cli-reference))
917
- enforces this mechanically and exits 1 on a mismatch - don't rely on review alone. Same Block
918
- helpers as today's freeform "manual mode" (`definePageBlock`/`defineNavBlock`/
919
- `defineMethodBlock`/etc.) either way - Map only changes *where files live and what they're
920
- named*, never what kind of Block authors them or how they execute. A project adopts it
921
- incrementally, folder by folder. Prefer [`map()`](#map---a-kind-checked-no-teleporting-flow-builder)
922
- over a hand-assembled `defineFlow([start, ...])` array when wiring these Blocks into a Flow -
923
- it's the one that can't silently accept a Block that didn't come from a real factory.
924
-
925
- **Leading underscore = this Checkpoint's own fixed machinery, never a route segment.**
926
- `_nav.block.ts`/`_page.block.ts`/`_sel.ts`/`_methods/` (same private-folder idea as Next.js's
927
- own `_folder` convention) mark "this belongs to the endpoint whose folder it's directly in,"
928
- so a bare (non-underscore, non-`(group)`) folder name always means a real child
929
- Checkpoint/URL segment - `_methods/` can never be mistaken for a sibling endpoint folder the
930
- way a bare `methods/` structurally could be, since only ONE of those two readings is
931
- mechanically distinguishable at a glance. `waygraph map`'s own folder-vs-URL comparison
932
- excludes underscore segments the same way it excludes `(group)` ones - `_methods/foo.block.ts`
933
- compares against the URL as if `_methods/` weren't there at all. Individual files inside
934
- `_methods/` keep their own descriptive names (`join-pool.method.block.ts`, not
935
- `_join-pool...`) - they're hand-authored per-project content, not fixed convention names; the
936
- `_methods/` folder boundary alone already marks the whole subtree as non-route.
937
-
938
- **"waypack" - loading another project's Blocks needs no separate manifest either.** A second
939
- project depending on a Map-convention project as a plain `file:` dependency (the same
940
- mechanism `examples/saucedemo`'s own `"waygraph": "file:../.."` already uses) is immediately
941
- understandable: `waygraph graph node_modules/<loaded-package>` correctly discovers its full
942
- Checkpoint/edge structure, with nothing generated or maintained separately. Proven end to end
943
- in `examples/routed-demo-consumer/` against `examples/routed-demo/`.
944
-
945
- **A real bug this proof surfaced, fixed, not left as a caveat:** a *live* `--detach` session
946
- against a package loaded through a consumer project's own deeply nested `node_modules/<pkg>`
947
- path used to fail with `listen EINVAL` - the session's Unix socket lived under the project
948
- directory, and that absolute path could exceed the OS's AF_UNIX limit (~108 bytes on Linux).
949
- Fixed: session sockets now live in a short, fixed location (`os.tmpdir()/waygraph-auto/`),
950
- independent of how deeply the project itself is nested - confirmed against the exact
951
- previously-failing path (`auto --cli --detach`, `auto reach`, and quit all working normally).
952
- Session metadata (`.waygraph-auto/<id>.json`, discoverable per-project) is unaffected - only
953
- the socket moved.
954
-
955
- Full design history (why "Router" isn't a separate thing to build, two prior wrong readings
956
- of this before landing here, and everything above stated as requirements) in
957
- `openspec/changes/waygraph-map/`.
958
-
959
- ## Migrating from freeform ("router") to Waygraph Map
960
-
961
- "Router" isn't a separate mode to turn off - it's this project's own name for today's
962
- freeform, folder-organized style (`src/blocks/<slug>/`, any nesting, any file names).
963
- Migrating means moving those files under `src/map/(group)/<page-slug>/` with verbatim
964
- URL-matching names, nothing more - no engine change, no new Block helpers, no flag to flip.
965
- Do it incrementally: the two styles coexist in the same project (and even the same Flow) with
966
- no special-casing, since `discoverGraph`/`loadBlockLibrary` only ever match the `.block.ts`
967
- filename pattern, never folder depth or naming.
968
-
969
- 1. **List every real URL your NavBlocks/PageBlocks already navigate to.** For each one with a
970
- static string `url`, note its real path. For each one that's `click`-based (no `url` field
971
- at all) or a dynamic `(mem) => string` function, note that too - neither is statically
972
- checkable by `waygraph map`, so they can go wherever makes sense; only a literal string
973
- `url` is verified.
974
- 2. **Pick your group(s).** `(app_base)/` for the landing/base app (organizational only, never
975
- part of a Checkpoint tag); `(external)/` for genuinely cross-origin Blocks (mail tools,
976
- third-party embeds) - verify with the real URL, not the folder's own current name, since a
977
- folder can already be mis-grouped (a real bug this session found: a project's own
978
- `(external)/docs/` was actually same-origin the whole time).
979
- 3. **Create `src/map/(group)/<verbatim-url-segments>/` per Checkpoint**, `git mv` each
980
- Block/Sel file in, and rename the fixed ones: `nav.block.ts` -> `_nav.block.ts`,
981
- `page.block.ts` -> `_page.block.ts`, your selector file -> `_sel.ts` (drops its slug - now a
982
- fully fixed name), `methods/` -> `_methods/` (contents keep their own names unchanged).
983
- **Two Checkpoints sharing one real URL** (a DOM-state distinction, not a URL one - e.g. a
984
- "joined" vs "not joined" variant of the same page) can't both use the bare fixed names in
985
- one folder; give each pair a slug prefix instead (`app-home._nav.block.ts` /
986
- `in-pool._nav.block.ts`, or similar) rather than nesting either under its own subfolder
987
- (that would add a folder segment the real URL doesn't have, and fail step 5).
988
- 4. **A dynamic per-instance URL** (a real chat/order/ticket id, not a fixed route) - if it's
989
- currently a plain string constant, wrap it as `url: () => thatConstant` instead. A bare
990
- string is always treated as a real, checkable static route; a function is correctly treated
991
- as dynamic/non-canonical and skipped, matching how you'd actually navigate a real instance of
992
- it in practice anyway.
993
- 5. **Fix every import** the moves break (states/mem-key paths change depth; sibling
994
- nav/page/methods imports need their new `_`-prefixed names) - let the compiler do this for
995
- you: `tsc --noEmit` if the project has one, otherwise `waygraph check [project]` and
996
- `waygraph graph [project]` (both do real dynamic imports of every Block file and report
997
- import failures by name) are just as effective a proof.
998
- 6. **Run `waygraph map [project]`.** Exit 0 / "0 violations" is the actual finish line - not a
999
- subjective review. A remaining violation names the exact file, its real folder path, and the
1000
- real URL it doesn't match; fix the folder, not the check.
1001
- 7. **Diff `waygraph graph`'s node/edge/skipped/orphan counts against before the move.** A pure
1002
- migration changes zero Block behavior - if a count changed, something broke in transit
1003
- (a bad import silently dropped a Block from discovery is the usual cause), not something to
1004
- wave off as "close enough."
1005
-
1006
- Once the folder structure is settled, prefer building/rebuilding Flows over these Blocks with
1007
- [`map()`](#map---a-kind-checked-no-teleporting-flow-builder) instead of a hand-assembled
1008
- `defineFlow([start, ...])` array - it's the one that can't silently accept a Block that isn't
1009
- really a Nav/Page/Assert/Method from this package's own factories.
1010
-
1011
- ## Getting started (pick one)
1012
-
1013
- | Goal | Command |
1014
- |------|---------|
1015
- | Watch Sauce Demo step-through (temp dir only) | `npx waygraph try demo` |
1016
- | Interactive explore (Add/Remove/Open details from live page) | `cd examples/saucedemo && npm i && npx waygraph auto` |
1017
- | Scaffold a new **offline** project (green `npm test` on local fixture `:4177`) | `npx waygraph init my-app` |
1018
- | Install coding-agent defs for diving an app into Blocks | `npx waygraph agent-dive --loop claude` (also `opencode` / `cursor` / `vscode`) |
1019
- | Print a packaged agent skill to stdout | `npx waygraph --skill` (list) / `--skill-pilot` / `--skill-pilot-blind` / `--skill-convention` |
1020
- | Add waygraph to an existing repo | `npm install waygraph @playwright/test` |
1021
-
1022
- ```bash
1023
- npx waygraph init my-app
1024
- cd my-app && npm install && npx playwright install chromium && npm test
1025
- ```
1026
-
1027
- `try demo` is different: live saucedemo.com, step-through **Sign In -> Shop & Checkout ->
1028
- blocked Viewer login**, then headless tests. Permanent full example:
1029
- [`examples/saucedemo`](./examples/saucedemo). Offline empty scaffold: `init`.
1030
-
1031
- **`create-waygraph` (npm) is deprecated** - it was the identical `init` template shipped as
1032
- a second package for no real benefit. Use `npx waygraph init my-app` instead.
1033
-
1034
- ```bash
1035
- # From this checkout, after npm run build:
1036
- cd examples/saucedemo && npm install && npx playwright install chromium
1037
- npm test # live Playwright suite
1038
- npm run auto # headed interactive explore
1039
- npm run demo:step # stepper (manual Next)
1040
- npm run demo:autoplay # stepper with Auto-advance
73
+ src/map/(app)/checkout/
74
+ _nav.block.ts how to get here
75
+ _page.block.ts "you've arrived"
76
+ _sel.ts every selector for this page
77
+ _methods/ one action per file
1041
78
  ```
1042
79
 
1043
- Both scaffold commands emit the same offline tree (route-shaped `demo-web/` + `methods/`,
1044
- stubs, fixtures, one YAP slide). Layout: [`docs/scaffold.html`](./docs/scaffold.html) ·
1045
- `templates/scaffold/STRUCTURE.md`.
1046
-
1047
- **Consumer layout (Next.js App Router):** block folders mirror `app/` page routes (`/` at
1048
- the namespace root; no invented `landing/`/`root/`; sidebar = chrome + edges). See
1049
- [`docs/consumer.html`](./docs/consumer.html). A separate mesh handout
1050
- (`WAYGRAPH-CONSUMER-CONVENTION.md`) covers the same convention outside this repo - not
1051
- part of this package's own tree.
1052
-
1053
- ## CLI reference
1054
-
1055
- `waygraph auto [project]` is the **interactive explore** loop: `locate()` reads where you
1056
- are, lists runnable Blocks from the graph, you pick one (headful panel or `--cli` terminal
1057
- menu), repeat. Static graph export (old JSON / Mermaid) is `waygraph graph [project]`
1058
- (`--mermaid`). Unattended JSON execution is `run` + `WAYGRAPH_JSON=1`.
1059
-
1060
- **`--cli` session control:** `waygraph auto --cli` on its own is a blocking terminal loop -
1061
- fine for a person, awkward for an agent that needs to inspect state between picks or send
1062
- one command at a time without guessing a whole input sequence upfront. `--detach` runs that
1063
- same session as a background socket server instead (session identity under
1064
- `.waygraph-auto/` in the target project, mirroring `.waygraph-traverse/`'s convention);
1065
- `auto send <sessionId> "<pick>"` and `auto status <sessionId>` are non-interactive
1066
- request/response calls against it (no TTY needed), and `auto attach <sessionId>` reopens an
1067
- interactive terminal against an already-running session. `send q`/`send quit` ends the
1068
- session and cleans up its socket/metadata. `--detach` defaults to headless;
1069
- `--non-headless` (the same flag `run`/`demo` already use) launches a real visible browser
1070
- window while the session stays driven entirely through `send`/`status`/`attach`/`dom` - not
1071
- a new picker UI, and the existing page-embedded headful panel (bare `waygraph auto`, no
1072
- `--cli`) is untouched either way.
1073
-
1074
- **Session history:** `auto trace <sessionId>` returns a Checkpoint/Block-level record of
1075
- what the session actually did - Block name, Checkpoint before/after, and (when the Block
1076
- authors them) its resolved `stubBefore`/`stubAfter`/`stubOnError` demo-narration fixtures
1077
- (the same highlight/todo/device data `waygraph demo`'s own lifecycle logging computes via
1078
- `runStubPhase`, reused here since it's pure data with no browser side effects). Deliberately
1079
- not a raw action recorder - no clicks/fills, only the same Block-level resolution the rest of
1080
- the engine already reasons about, capped at the last 500 steps.
1081
-
1082
- **Reading the live page:** `auto dom <sessionId>` gives an agent driving a session read-only
1083
- visibility into the actual page, without reading the target project's frontend source.
1084
- Default `--mode aria` returns Playwright's AI-oriented accessibility snapshot
1085
- (`ariaSnapshotJSON({ mode: "ai" })`) - small and usually enough to find "the Username
1086
- textbox" or "the Login button." `--mode full` returns a bounded raw DOM subtree
1087
- (tag/attributes/text/children - useful when you need actual CSS classes or data-attributes
1088
- `aria` can't see), hard-capped on depth/node count/text length and marked `truncated: true`
1089
- whenever a cap is hit, since this is read by an LLM, not a human. `--selector <sel>` scopes
1090
- either mode to one element's subtree instead of the whole page - a modifier on the mode, not
1091
- a third mode.
1092
-
1093
- **Agent bootstrap:** `waygraph pilot start` starts a `--detach` session and reads back the
1094
- whole project graph in one call, so an agent can begin planning a multi-step request
1095
- immediately - see "Waygraph Pilot" above for the full picture.
1096
-
1097
- **Cold start (zero Blocks):** `auto click/type/goto <sessionId>` act on the live page even
1098
- when no Block covers that action yet, and `auto reload <sessionId>` picks up a Block just
1099
- written to disk without restarting the session - see "Blind Pilot" above.
1100
-
1101
- **`waygraph test [...args]`** is a thin wrapper around the project's own `@playwright/test`
1102
- suite - it forwards to the local `node_modules/.bin/playwright` (falling back to
1103
- `npx playwright` if the project hasn't installed it locally yet), so `waygraph test` behaves
1104
- exactly like `playwright test`, and anything after it (`--grep`, a spec path, `--headed`, …)
1105
- passes straight through. `waygraph test ui` (or `waygraph test --ui`) launches Playwright's
1106
- interactive UI Mode the same way. Scaffolded projects (`waygraph init`) wire this up as
1107
- `npm test` / `npm run test:ui`, and ship `trace: "retain-on-failure"` plus
1108
- the `html` reporter in `playwright.config.ts`, so a failing `waygraph test` already has a
1109
- trace waiting - `waygraph test report` opens the HTML report, and
1110
- `waygraph test show-trace <trace.zip>` opens one trace directly (both forward to Playwright's
1111
- own `show-report`/`show-trace`, which live outside `playwright test`, so they're handled
1112
- before the `test` forward rather than passed through it).
1113
-
1114
- Flows are files (0.10.5+):
1115
-
1116
- | Want | Command |
1117
- |------|---------|
1118
- | List flows | `waygraph list` -> `src/flows/shop.flow.ts shopFlow` |
1119
- | Run by file | `waygraph run src/flows/shop.flow.ts --data '{...}'` |
1120
- | Same via `auto` | `waygraph auto src/flows/shop.flow.ts --data '{...}'` |
1121
- | Run by export | `waygraph run --blocks shopFlow` |
1122
- | Manual watch | `waygraph demo src/flows/shop.flow.ts` |
1123
- | Auto-advance | `waygraph demo --blocks shopFlow --auto-next` (nav auto-hides strip) |
1124
- | Fast / full strip | `waygraph demo ... --fast` (shorter gates; keeps cursor) · `--full` (classic chips; default = carousel) |
1125
- | Todo dock UX | `waygraph demo ... --todo-smart` (default) · `--todo-full` (no compact/collision/behind) |
1126
- | QA watch + record | `waygraph demo --blocks shopFlow --auto-play-video` |
1127
- | Ad-hoc Blocks | `waygraph run --blocks "login then nav-cart" --data '{...}'` |
1128
- | Headed execute | `waygraph run --blocks shopFlow --non-headless --video` |
1129
- | Explore | `waygraph auto` / `waygraph auto --cli` |
1130
- | Browser command reference | `waygraph browser` |
1131
- | List live browser sessions | `waygraph browser sessions [--json]` |
1132
- | Open a new persistent browser | `waygraph browser start` (headful, about:blank by default) |
1133
- | Stop a browser session | `waygraph browser stop <sessionId>` / `stop --all` |
1134
- | Pilot bootstrap (browser + graph + snapshot) | `waygraph pilot start` |
1135
- | Attach to existing session (graph only) | `waygraph pilot attach <sessionId>` |
1136
- | Path-find | `waygraph auto --blocks LoginPage OrderComplete` |
1137
- | Run `--cli` as a background session (legacy) | `waygraph auto --cli --detach` -> `{sessionId, socketPath}` |
1138
- | Drive a detached session (no TTY) | `waygraph browser send <sessionId> "<pick>"` -> JSON state |
1139
- | Read a detached session's state | `waygraph browser status <sessionId>` (no side effects) |
1140
- | Reattach a terminal to a detached session | `waygraph browser attach <sessionId>` |
1141
- | Read the live page (aria, small/default) | `waygraph auto dom <sessionId>` |
1142
- | Read the live page (raw DOM, bounded) | `waygraph auto dom <sessionId> --mode full` |
1143
- | Scope either to one element | `waygraph auto dom <sessionId> --selector ".inventory_list"` |
1144
- | Detached session with a real visible browser | `waygraph auto --cli --detach --non-headless` |
1145
- | Read the session's Checkpoint/Block history | `waygraph auto trace <sessionId>` |
1146
- | Read real console/network errors + coverage-gap warnings | `waygraph auto console <sessionId>` |
1147
- | Read localStorage/sessionStorage + service workers | `waygraph auto storage <sessionId>` |
1148
- | Whole-project nav-escape + inline-selector + orphan sweep | `waygraph check [project]` |
1149
- | `tsc --noEmit` + bad-practice warnings | `waygraph typecheck [project]` (`--no-practices` to skip scan) |
1150
- | Waygraph Map enforcement (verbatim url-vs-folder, exits 1 on a violation) | `waygraph map [project]` |
1151
- | Graph crawl | `waygraph traverse [project] --parallel N --min-edge-coverage 80%` |
1152
- | Coding-agent defs | `waygraph agent-dive --loop claude` |
1153
- | Print packaged skills | `waygraph --skill` / `--skill-pilot` / `--skill-pilot-blind` / `--skill-convention` |
1154
- | One-shot temp-dir demo | `waygraph try demo` / `waygraph try auto` / `waygraph try auto:cli` |
1155
- | Run the project's `@playwright/test` suite | `waygraph test` |
1156
- | Same suite, Playwright's interactive UI Mode | `waygraph test ui` (or `--ui`) |
1157
- | Open the last HTML report | `waygraph test report` |
1158
- | Open one trace in the trace viewer | `waygraph test show-trace <trace.zip>` |
1159
-
1160
- `--blocks` / positional accepts a Flow export, a `.flow.ts` path, or `"a then b"`.
1161
- `auto <file.flow.ts>` **runs** that flow (same as `run`); bare `auto` still explores.
1162
- `--auto-next` (alias `--autoplay`) = panel Auto-advance. `--auto-play-video` is **demo
1163
- only**. `chain` remains a compat alias for `run --blocks` / `auto --blocks`.
1164
-
1165
- ### Todo dock UI (demo, 0.15.8+)
1166
-
1167
- Long FR/AC checklists used to bury highlight rings. **Smart defaults are on:**
1168
-
1169
- | Behavior | Default | Opt out |
1170
- |----------|---------|---------|
1171
- | Compact fold (~5 rows around current; hover expands; `+N more`) | on | `ctx.todoDockFull()` / `ctx.todoDockUi({ compact: false })` / `--todo-full` / `WAYGRAPH_TODO_UI=full` |
1172
- | Collision flip (dock L/R when a ring overlaps) | on | `ctx.todoDockUi({ collision: false })` / `WAYGRAPH_TODO_COLLISION=0` |
1173
- | Behind ring (dim + lower z-index while a ring is up) | on | `ctx.todoDockUi({ behindRing: false })` / `WAYGRAPH_TODO_BEHIND=0` |
1174
-
1175
- Per-knob env also: `WAYGRAPH_TODO_COMPACT`, `WAYGRAPH_TODO_CAP` (default 5),
1176
- `WAYGRAPH_TODO_EXPAND_CAP` (default 14). Author API: `ctx.todoDockUi({ ... })`.
1177
- Pilot `auto highlight` accepts the same knobs as `todoUi` on the JSON body.
1178
-
1179
- Run `waygraph --help` (or any subcommand with no args) for the full flag reference kept
1180
- in `src/cli.ts`'s own `usage()` - that's the source of truth for flags, this table is the
1181
- scannable summary.
1182
-
1183
- ## Example
1184
-
1185
- Two Tier-0 Blocks (only `act` + `resolve`, no branching), each confirmed with `verify`
1186
- after the fact rather than asserting mid-`observe`, defined as a flow and run against a
1187
- real page:
1188
-
1189
- ```typescript
1190
- import { Engine, start, end, MemPage, key, checkpoint } from "waygraph";
1191
- import type { Block, Checkpoint, Start } from "waygraph";
80
+ `waygraph map` fails if a folder stops matching its real URL, so the graph can't quietly go
81
+ stale. `waygraph init` scaffolds this layout. A plainer, freeform layout (any folders, any
82
+ names) still works and is fine for small projects, but the Map is where the tooling is going.
1192
83
 
1193
- type FormLoaded = Checkpoint<"FormLoaded">;
1194
- type Submitted = Checkpoint<"Submitted">;
84
+ ## Commands you'll use
1195
85
 
1196
- const EmailKey = key<string>("form.email");
1197
- const PasswordKey = key<string>("form.password");
1198
-
1199
- const GotoForm: Block<Start, FormLoaded> = {
1200
- name: "goto-form",
1201
- instruction: {
1202
- async act(page) {
1203
- await page.goto("https://example.com/login");
1204
- },
1205
- resolve: () => checkpoint("FormLoaded"),
1206
- },
1207
- };
1208
-
1209
- const FillAndSubmit: Block<FormLoaded, Submitted> = {
1210
- name: "fill-and-submit",
1211
- instruction: {
1212
- async act(page, _input, mem) {
1213
- await page.getByPlaceholder("Email").fill(mem.get(EmailKey));
1214
- await page.getByPlaceholder("Password").fill(mem.get(PasswordKey));
1215
- await page.getByRole("button", { name: "Submit" }).click();
1216
- },
1217
- resolve: () => checkpoint("Submitted"),
1218
- verify: [
1219
- {
1220
- name: "submitted-heading-shown",
1221
- check: async (page) => {
1222
- await page.getByRole("heading", { name: "Submitted" }).waitFor();
1223
- return true;
1224
- },
1225
- },
1226
- ],
1227
- },
1228
- };
1229
-
1230
- const mem = new MemPage();
1231
- mem.set(EmailKey, "alice@test.com");
1232
- mem.set(PasswordKey, "hunter2");
1233
-
1234
- const engine = new Engine();
1235
- const flow = engine.defineFlow([start, GotoForm, FillAndSubmit, end]);
1236
- const result = await flow.run(context, mem);
1237
- // result === { __state: "Submitted" }
1238
- ```
1239
-
1240
- ## Recipes
1241
-
1242
- ### Detect / drive the surviving tab after a run
1243
-
1244
- The default run closes the page it opened, and a page you hand in via `options.page`
1245
- survives. For a flow that must BE the surviving tab after it returns (an interactive
1246
- demo, a hand-back to a caller), ask for the page back explicitly:
1247
-
1248
- ```typescript
1249
- const { result, page } = await registerFlow.run(context, mem, { closeOnFinish: false });
1250
- // `result.__state` reached the terminal Checkpoint; `page` is still open where the
1251
- // flow left it - the caller keeps driving it, and closes it whenever IT is done.
1252
- ```
1253
-
1254
- ### Capture a popup (`target=_blank` click)
86
+ | Do this | Command |
87
+ |---|---|
88
+ | Run tests / UI mode / open last report | `waygraph test` / `waygraph test ui` / `waygraph test report` |
89
+ | Run a flow | `waygraph run shopFlow --data '{...}'` (or `--mem-stub`) |
90
+ | Watch a flow with narration | `waygraph demo shopFlow` |
91
+ | Explore by picking Blocks | `waygraph auto` |
92
+ | See the whole graph | `waygraph graph` |
93
+ | Lint your Blocks and folders | `waygraph check` / `waygraph typecheck` / `waygraph map` |
94
+ | Open a persistent browser for an agent | `waygraph browser start` |
95
+ | Highlight things on the live page | `waygraph browser highlight <id> "#login\|Login button\|warning"` |
96
+ | Set up coding-agent skills | `waygraph agent-dive --loop claude` |
1255
97
 
1256
- Frameworks and email UIs routinely rewrite links to `target="_blank"`, so a click that
1257
- "should navigate" actually opens a second, engine-invisible tab. Until a second real use
1258
- case promotes this to an engine API, capture it in userland with a context-level page
1259
- event armed before the click that is expected to open exactly one popup:
98
+ `waygraph --help` lists everything.
1260
99
 
1261
- ```typescript
1262
- let popup: Page | undefined;
1263
- context.once("page", (p) => { popup = p; });
1264
- await linkHandle.click(); // the block that causes the popup (e.g. the
1265
- // MailHog email's real verify link)
1266
- if (popup) {
1267
- await popup.waitForLoadState(); // fully loaded before calling into the engine
1268
- await verifyLinkFlow.run(context, mem, { page: popup, closeOnFinish: false });
1269
- // popup is now the caller's page - close it when done, or leave it as the
1270
- // surviving tab of a demo.
1271
- }
1272
- ```
100
+ ## Learn more
1273
101
 
1274
- `context.once` (not `.on`) guarantees a second stray popup won't re-fire the handler and
1275
- clobber your handle; if a page truly can open more than one popup, this recipe needs to
1276
- collect them instead. The popup is a normal Playwright `Page` from the same context, so
1277
- anything `Flow.run(context, mem, { page })` can drive is available to keep driving here,
1278
- and `closeOnFinish: false` hands it back into your ownership instead of the run closing
1279
- it out from under you.
102
+ - **[Docs site](https://deviate-dv8.github.io/waygraph/)** - guides and walkthroughs
103
+ - **[Full reference](./docs/REFERENCE.md)** - every feature in detail (long)
104
+ - **[Sauce Demo walkthrough](https://deviate-dv8.github.io/waygraph/saucedemo/)** - a worked example
105
+ - Agent skills: `waygraph --skill` prints them (pilot, blind-pilot, convention)
1280
106
 
1281
- ## Developing this package
107
+ ## Develop this package
1282
108
 
1283
109
  ```bash
1284
- git clone git@github.com:deviate-dv8/waygraph.git
1285
- cd waygraph
1286
- npm install
1287
- npm run typecheck # waygraph typecheck . (tsc --noEmit + practice warnings)
1288
- npm run test # node --test tests/unit + playwright test
1289
- npm run test:ui # playwright test --ui
1290
- npm run build # tsc -p tsconfig.build.json, emits dist/
1291
- ```
1292
-
1293
- ### Blind-agent overlay gate (anti-blank)
1294
-
1295
- Every live modal (`#wg-panel`, `#wg-banner`, `#wg-auto-panel`, `#wg-todo-dock`) stamps
1296
- `data-wg-ui` / `data-wg-modal` / `data-wg-ready="1"`. Do **not** claim the stepper works
1297
- unless one of these passes:
1298
-
1299
- ```bash
1300
- # Playwright helper (exported from waygraph)
1301
- import { assertWgOverlayReady } from "waygraph";
1302
- await assertWgOverlayReady(page); // throws on blank / opacity-0 / missing stamp
1303
-
1304
- # Live demo prove (exit 0 = painted, exit 2 = blank)
1305
- WAYGRAPH_PROVE_EXIT=1 WAYGRAPH_PROVE_SHOT=/tmp/wg.png \
1306
- waygraph demo --blocks loginFlow --data '...'
1307
- # stderr: WAYGRAPH_PROVE {"ok":true,"beacons":[...],"readyAttr":"1"}
1308
- ```
1309
-
1310
- Tests: `tests/cli/overlay-beacon.spec.ts`. Helpers: `src/overlay-beacon.ts`. Rules for
1311
- touching the stepper/overlay chrome itself: `src/CLI-STOMP-GUARD.md`.
1312
-
1313
- ## Layout
1314
-
1315
- ```
1316
- package.json the "waygraph" npm package itself
1317
- src/
1318
- types.ts Checkpoint, Instruction, Block, connect()
1319
- mem-page.ts MemKey, key(), MemPage
1320
- trait.ts Trait (type + discoverable Trait.url/.text/.visible), runVerify()
1321
- engine.ts runGraph(), definePageBlock, defineMethodBlock, Engine, ...
1322
- graph.ts discoverGraph, orphans, paths
1323
- auto-explore.ts waygraph auto menu building
1324
- auto-explore-run.ts waygraph auto interactive loop (headful + --cli)
1325
- highlights.ts stub/fixture/slide/todo-dock/device narration types + helpers
1326
- step-overlay.ts demo step overlay chrome (panel, ring, cursor)
1327
- overlay-beacon.ts blind-agent overlay readiness beacons
1328
- traverse-run.ts waygraph traverse crawl (serial/parallel)
1329
- traverse-coverage.ts traverse coverage report + CI gate
1330
- traverse-lease.ts traverse edge lease coordinator
1331
- blocks-select.ts shared --blocks glob/regex/bare grammar
1332
- agent-dive.ts waygraph agent-dive coding-agent scaffolding
1333
- cli.ts waygraph CLI entrypoint (all subcommands)
1334
- index.ts public barrel
1335
- examples/saucedemo/ live Sauce Demo (Page + methods/ + Sel)
1336
- templates/quickstart/ try demo / init template (mirrors sauce)
1337
- templates/scaffold/ offline waygraph init scaffold template
1338
- templates/agents/ waygraph agent-dive coding-agent definitions
1339
- docs/ GitHub Pages static HTML + docs/proposals/ RFCs
1340
- openspec/ spec-driven planning: specs/ (current), changes/ (in-flight),
1341
- changes/archive/ (shipped)
1342
- tests/ runtime unit + integration tests (@playwright/test)
1343
- tsconfig.json base config
1344
- tsconfig.build.json emits to dist/
110
+ git clone https://github.com/deviate-dv8/waygraph.git && cd waygraph
111
+ npm install && npm run build
112
+ npm run typecheck && npm test # unit + Playwright suites
1345
113
  ```
1346
114
 
1347
- In-package Sauce Demo: [`examples/saucedemo`](./examples/saucedemo). Other consumer
1348
- experiments may still live in a sibling workspace folder and depend on this package via
1349
- `"waygraph": "file:../waygraph"` during local dev (see that
1350
- project's own `.npmrc` - `install-links=true` is required there, or npm will symlink
1351
- instead of copy and pull this package's own `node_modules` in through the symlink, causing
1352
- a duplicate-Playwright-installation error).
115
+ MIT