waygraph 0.15.24 → 0.15.26

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/dist/engine/blocks/effect.d.ts +41 -0
  2. package/dist/engine/blocks/effect.js +23 -0
  3. package/dist/engine/blocks/method.d.ts +90 -0
  4. package/dist/engine/blocks/method.js +70 -0
  5. package/dist/engine/blocks/nav.d.ts +138 -0
  6. package/dist/engine/blocks/nav.js +110 -0
  7. package/dist/engine/blocks/page.d.ts +59 -0
  8. package/dist/engine/blocks/page.js +91 -0
  9. package/dist/engine/compose.d.ts +75 -0
  10. package/dist/engine/compose.js +76 -0
  11. package/dist/engine/config.d.ts +55 -0
  12. package/dist/engine/config.js +17 -0
  13. package/dist/engine/core.d.ts +92 -0
  14. package/dist/engine/core.js +256 -0
  15. package/dist/engine/engine-class.d.ts +289 -0
  16. package/dist/engine/engine-class.js +257 -0
  17. package/dist/engine/flow.d.ts +266 -0
  18. package/dist/engine/flow.js +387 -0
  19. package/dist/engine/locate.d.ts +26 -0
  20. package/dist/engine/locate.js +62 -0
  21. package/dist/engine/run-graph.d.ts +147 -0
  22. package/dist/engine/run-graph.js +223 -0
  23. package/dist/engine.d.ts +11 -1224
  24. package/dist/engine.js +12 -1550
  25. package/dist/highlights/css.d.ts +14 -0
  26. package/dist/highlights/css.js +58 -0
  27. package/dist/highlights/demo-pace.d.ts +56 -0
  28. package/dist/highlights/demo-pace.js +184 -0
  29. package/dist/highlights/device.d.ts +29 -0
  30. package/dist/highlights/device.js +171 -0
  31. package/dist/highlights/merge.d.ts +8 -0
  32. package/dist/highlights/merge.js +173 -0
  33. package/dist/highlights/run-phase.d.ts +43 -0
  34. package/dist/highlights/run-phase.js +211 -0
  35. package/dist/highlights/stub-ctx.d.ts +10 -0
  36. package/dist/highlights/stub-ctx.js +281 -0
  37. package/dist/highlights/style.d.ts +27 -0
  38. package/dist/highlights/style.js +84 -0
  39. package/dist/highlights/todo-dock.d.ts +52 -0
  40. package/dist/highlights/todo-dock.js +248 -0
  41. package/dist/highlights/types.d.ts +536 -0
  42. package/dist/highlights/types.js +1 -0
  43. package/dist/highlights.d.ts +9 -734
  44. package/dist/highlights.js +10 -1416
  45. package/examples/saucedemo/.waygraph-auto/008f4e6e.log +0 -0
  46. package/examples/saucedemo/.waygraph-auto/22fefa13.log +0 -0
  47. package/package.json +1 -1
@@ -0,0 +1,26 @@
1
+ import type { NavBlock } from "./blocks/nav.js";
2
+ import type { Page } from "@playwright/test";
3
+ /**
4
+ * "Where am I" - reverse-matches a live `page` against every NavBlock /
5
+ * PageBlock in `library`, in order, returning the first one whose own `verify`
6
+ * Traits all pass (its `checkpoint`), or `null` if none match. Deliberately
7
+ * Nav/Page hubs only: each has exactly one fixed `checkpoint` and a `verify`
8
+ * list whose whole job is already "confirm arrival here," so it's a reliable
9
+ * fingerprint - a regular `defineBlock` can branch to different `Out` tags
10
+ * depending on runtime evidence, so its `verify` alone doesn't identify a
11
+ * single state the way a hub's does.
12
+ *
13
+ * The primitive `waygraph auto`'s discovered graph is *for* - the graph says
14
+ * what pages exist and how to reach them; `locate()` is how an autonomous
15
+ * run figures out which one it's actually looking at right now, without a
16
+ * human telling it.
17
+ *
18
+ * More than one NavBlock matching is a real possibility (two Checkpoints
19
+ * whose `verify` Traits are both satisfied by the same page) - `locate()`
20
+ * returns the first match rather than treating that as an error; resolving
21
+ * genuine ambiguity is a follow-up, not solved here.
22
+ * @example const here = await locate(page, [NavLoginBlock, NavDashboardBlock]);
23
+ */
24
+ export declare function locate(page: Page, library: readonly NavBlock<any>[], options?: {
25
+ timeoutMs?: number;
26
+ }): Promise<string | null>;
@@ -0,0 +1,62 @@
1
+ import { MemPage } from "../mem-page.js";
2
+ /**
3
+ * "Where am I" - reverse-matches a live `page` against every NavBlock /
4
+ * PageBlock in `library`, in order, returning the first one whose own `verify`
5
+ * Traits all pass (its `checkpoint`), or `null` if none match. Deliberately
6
+ * Nav/Page hubs only: each has exactly one fixed `checkpoint` and a `verify`
7
+ * list whose whole job is already "confirm arrival here," so it's a reliable
8
+ * fingerprint - a regular `defineBlock` can branch to different `Out` tags
9
+ * depending on runtime evidence, so its `verify` alone doesn't identify a
10
+ * single state the way a hub's does.
11
+ *
12
+ * The primitive `waygraph auto`'s discovered graph is *for* - the graph says
13
+ * what pages exist and how to reach them; `locate()` is how an autonomous
14
+ * run figures out which one it's actually looking at right now, without a
15
+ * human telling it.
16
+ *
17
+ * More than one NavBlock matching is a real possibility (two Checkpoints
18
+ * whose `verify` Traits are both satisfied by the same page) - `locate()`
19
+ * returns the first match rather than treating that as an error; resolving
20
+ * genuine ambiguity is a follow-up, not solved here.
21
+ * @example const here = await locate(page, [NavLoginBlock, NavDashboardBlock]);
22
+ */
23
+ export async function locate(page, library, options) {
24
+ const mem = new MemPage();
25
+ const timeoutMs = options?.timeoutMs;
26
+ if (timeoutMs !== undefined) {
27
+ page.setDefaultTimeout(timeoutMs);
28
+ }
29
+ try {
30
+ for (const block of library) {
31
+ try {
32
+ // NavBlock's own resolve() ignores whatever it's called with - always
33
+ // exactly `checkpoint(options.checkpoint)` - so this is safe to call
34
+ // before verify, unlike a regular Block where resolve depends on
35
+ // observed evidence.
36
+ const resolved = await block.instruction.resolve(undefined);
37
+ const verify = block.instruction.verify;
38
+ const traits = typeof verify === "function" ? verify(resolved) : (verify ?? []);
39
+ let allPass = true;
40
+ for (const trait of traits) {
41
+ // eslint-disable-next-line no-await-in-loop
42
+ if (!(await trait.check(page, mem))) {
43
+ allPass = false;
44
+ break;
45
+ }
46
+ }
47
+ if (allPass)
48
+ return resolved.__state;
49
+ }
50
+ catch {
51
+ // This NavBlock's own verify threw (e.g. a waitFor timing out) -
52
+ // treat that the same as "didn't match," try the next candidate.
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ finally {
58
+ if (timeoutMs !== undefined) {
59
+ page.setDefaultTimeout(30_000);
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,147 @@
1
+ import { MemPage } from "../mem-page.js";
2
+ import type { Block, Checkpoint } from "../types.js";
3
+ import type { Trait } from "../trait.js";
4
+ import type { BrowserContext, Locator, Page } from "@playwright/test";
5
+ /**
6
+ * Checks `block.requires` against `mem` and throws, naming every missing key at
7
+ * once, if any are unset. `connect()` unions each Block's `requires` into the
8
+ * composed Block, so calling this on a `defineFlow`-built Flow's entry - which
9
+ * `runGraph` does automatically, before opening a tab - covers every Block in
10
+ * that flow. It does not see Blocks only reachable through `branch()` routing,
11
+ * since those depend on a runtime decision that hasn't happened yet.
12
+ */
13
+ export declare function preflight(mem: MemPage, block: Block<any, any>): void;
14
+ /**
15
+ * A cross-cutting, automatically-enforced verify - the real engine
16
+ * primitive behind "persistent UI" (a sidebar, a header, anything that's
17
+ * supposed to be present on many/most Checkpoints, not just one page's own
18
+ * Block). Deliberately NOT tied to any folder convention - `appliesTo`
19
+ * matches by Checkpoint tag, explicitly, so this works the same whether a
20
+ * project uses the Waygraph Map folder convention or the older freeform
21
+ * "manual mode" layout real production consumers (pia-waygraph,
22
+ * zsign-atomic-waygraph) already use - registering a Layout with an Engine
23
+ * is the only requirement, folder structure is irrelevant to it.
24
+ *
25
+ * Not a Block: no `act`/`resolve`/Checkpoint of its own, never a graph
26
+ * node - it only ever adds extra `verify` Traits to Checkpoints something
27
+ * else already resolved to, the same way `runVerify` already runs after
28
+ * any Block's own `resolve()` (see `runGraph`'s own verify step, just
29
+ * below) - a Layout is that mechanism made reusable across many
30
+ * Checkpoints instead of hand-repeated per Block.
31
+ */
32
+ export interface Layout {
33
+ name: string;
34
+ description?: string;
35
+ appliesTo: (tag: string) => boolean;
36
+ /** Function form gets the full resolved Checkpoint, same shape `runVerify`/a Block's own `verify` already use - not just the tag. */
37
+ verify: Trait[] | ((out: Checkpoint<string>) => Trait[]);
38
+ }
39
+ export interface LayoutOptions {
40
+ name: string;
41
+ description?: string;
42
+ /** A fixed tag list, or a predicate for open-ended matching (e.g. a naming pattern). */
43
+ appliesTo: readonly string[] | ((tag: string) => boolean);
44
+ verify: Trait[] | ((out: Checkpoint<string>) => Trait[]);
45
+ }
46
+ /**
47
+ * @example
48
+ * const AppShellLayout = defineLayout({
49
+ * name: "app-shell",
50
+ * appliesTo: ["AppHome", "Chats", "Notifications", "Settings"],
51
+ * verify: [Trait.visible(SidebarSel.root)],
52
+ * });
53
+ * const engine = new Engine({ layouts: [AppShellLayout] });
54
+ * // Now every Block resolving to one of those four Checkpoints, in any
55
+ * // Flow this engine defines, also gets AppShellLayout's own verify run
56
+ * // automatically - a page that silently drops the sidebar fails loud,
57
+ * // even though no individual Block's own `verify` mentions it.
58
+ */
59
+ export declare function defineLayout(options: LayoutOptions): Layout;
60
+ /**
61
+ * Runs `entry` against a fresh tab and returns the resulting terminal Checkpoint.
62
+ * The engine owns the tab's lifecycle, not any individual Block: the tab is
63
+ * created before the first `act` runs and closed in `finally`, so it closes even
64
+ * if a step throws.
65
+ *
66
+ * After each Block resolves, its `next` (if any, from `branch()`) is followed to
67
+ * the following Block, in the same tab - a Block with no `next` runs exactly once,
68
+ * unchanged from before `branch()` existed. `next` returning the same Block is a
69
+ * self-loop; `maxSteps` bounds the loop so a routing bug fails loud instead of
70
+ * hanging.
71
+ *
72
+ * `terminals`, if given, is a sanity check: the Checkpoint the run stops on must be
73
+ * one of the tags the caller actually expects, or the run fails loud instead of
74
+ * silently returning an unexpected result. Omit it when there is only ever one
75
+ * reachable outcome by construction (as with a `defineFlow`-built Flow, which has
76
+ * no routing at all) - there is nothing to validate against.
77
+ */
78
+ export declare function runGraph<TOut extends Checkpoint<string>>(entry: Block<Checkpoint<"__start__">, TOut>, terminals: ReadonlySet<string> | undefined, context: BrowserContext, mem: MemPage, maxSteps?: number, options?: RunGraphOptions): Promise<TOut>;
79
+ /** Options for `runGraph`'s trailing `options` arg and for {@link Flow.run}'s `options` overload. */
80
+ export interface RunGraphOptions {
81
+ /**
82
+ * Drive this already-open page instead of opening a new one via
83
+ * `context.newPage()`. Lets a run continue on a tab the caller already
84
+ * has open (e.g. one that's already logged in), instead of every run
85
+ * being forced to start its own fresh tab and lose access to whatever was
86
+ * already there.
87
+ */
88
+ page?: Page;
89
+ /**
90
+ * `Flow.run` defaults this to `true` only when it opened the page itself
91
+ * (no `page` given) - matching every call from before this option
92
+ * existed. If you pass your own `page`, it defaults to `false` instead:
93
+ * a page you opened is yours to close, not this run's to close out from
94
+ * under you. Either way an explicit value here always wins. `runGraph`
95
+ * itself has no such page-aware default (its caller already decided
96
+ * `page` and `closeOnFinish` together) - it just does what you ask.
97
+ */
98
+ closeOnFinish?: boolean;
99
+ /**
100
+ * Layouts (see {@link defineLayout}) whose `verify` runs automatically
101
+ * after any Block in this run resolves to a Checkpoint they apply to -
102
+ * `Flow.run` threads this in from `EngineConfig.layouts` by default; pass
103
+ * it directly here to call `runGraph` standalone (outside a `Flow`) with
104
+ * layouts still enforced.
105
+ */
106
+ layouts?: readonly Layout[];
107
+ }
108
+ /**
109
+ * Drives a genuinely separate second tab through its own Block/Flow, in the
110
+ * same browser context an existing `page` already belongs to - the primitive
111
+ * `observe()` is specifically allowed to reach for (see {@link Instruction.observe}),
112
+ * since `act`/`resolve` can't touch `page.context()` at all. A thin, named
113
+ * wrapper around `runGraph` (which already opens its own page from whatever
114
+ * context it's given) rather than a new mechanism - the point is making the
115
+ * pattern discoverable and giving it a real name, not inventing new tab-lifecycle
116
+ * logic. The spawned tab is closed (by `runGraph`'s own `finally`) before this
117
+ * resolves; the original `page` is never touched.
118
+ * @example
119
+ * async observe(page, mem) {
120
+ * const receipt = await spawnTab(EmailReceiptFlow, page, mem);
121
+ * mem.set(ReceiptCode, receipt.__state);
122
+ * }
123
+ */
124
+ export declare function spawnTab<TOut extends Checkpoint<string>>(entry: Block<Checkpoint<"__start__">, TOut>, page: Page, mem: MemPage, maxSteps?: number): Promise<TOut>;
125
+ /**
126
+ * Wraps one `act()` interaction with a caption and highlight, for a Block
127
+ * author who wants to explicitly narrate a specific step rather than rely
128
+ * on the generic per-fill/per-click narration `waygraph chain
129
+ * WAYGRAPH_STEP=1` already does automatically -
130
+ * `await narrate(submitButton, "confirms the order", () => submitButton.click())`
131
+ * reads at the call site instead of being inferred.
132
+ *
133
+ * A true no-op everywhere except inside a `WAYGRAPH_STEP=1` run: it checks
134
+ * for a `window.__wgPositionRing` hook the step-mode overlay installs on
135
+ * the page before doing anything, so a headless/CI/automated run (nobody
136
+ * watching, per-character delays and highlight pauses would just be wasted
137
+ * time) pays zero extra cost - not even the boundingBox() lookup runs. The
138
+ * `action` itself always runs regardless; only the narration is
139
+ * conditional.
140
+ * @example await narrate(submitButton, "confirms the order", () => submitButton.click())
141
+ */
142
+ export declare function narrate<T>(locator: Locator, caption: string, action: () => Promise<T>): Promise<T>;
143
+ /** Reserved markers bookending a `defineFlow([start, ...blocks, end])` call. */
144
+ export declare const start: unique symbol;
145
+ export declare const end: unique symbol;
146
+ export type StartMarker = typeof start;
147
+ export type EndMarker = typeof end;
@@ -0,0 +1,223 @@
1
+ import { checkpoint } from "../types.js";
2
+ import { runPrecondition, runVerify } from "../trait.js";
3
+ /**
4
+ * Checks `block.requires` against `mem` and throws, naming every missing key at
5
+ * once, if any are unset. `connect()` unions each Block's `requires` into the
6
+ * composed Block, so calling this on a `defineFlow`-built Flow's entry - which
7
+ * `runGraph` does automatically, before opening a tab - covers every Block in
8
+ * that flow. It does not see Blocks only reachable through `branch()` routing,
9
+ * since those depend on a runtime decision that hasn't happened yet.
10
+ */
11
+ export function preflight(mem, block) {
12
+ const missing = (block.requires ?? []).filter((k) => !mem.has(k));
13
+ if (missing.length > 0) {
14
+ throw new Error(`preflight: MemPage is missing required key(s) before "${block.name}" can run: ${missing
15
+ .map((k) => `"${k.name}"`)
16
+ .join(", ")}`);
17
+ }
18
+ }
19
+ /**
20
+ * @example
21
+ * const AppShellLayout = defineLayout({
22
+ * name: "app-shell",
23
+ * appliesTo: ["AppHome", "Chats", "Notifications", "Settings"],
24
+ * verify: [Trait.visible(SidebarSel.root)],
25
+ * });
26
+ * const engine = new Engine({ layouts: [AppShellLayout] });
27
+ * // Now every Block resolving to one of those four Checkpoints, in any
28
+ * // Flow this engine defines, also gets AppShellLayout's own verify run
29
+ * // automatically - a page that silently drops the sidebar fails loud,
30
+ * // even though no individual Block's own `verify` mentions it.
31
+ */
32
+ export function defineLayout(options) {
33
+ const appliesTo = typeof options.appliesTo === "function"
34
+ ? options.appliesTo
35
+ : (tag) => options.appliesTo.includes(tag);
36
+ return {
37
+ name: options.name,
38
+ ...(options.description ? { description: options.description } : {}),
39
+ appliesTo,
40
+ verify: options.verify,
41
+ };
42
+ }
43
+ /**
44
+ * Runs every registered layout whose `appliesTo(tag)` matches the just-
45
+ * resolved Checkpoint, in registration order, right after the Block's own
46
+ * `verify` already ran. A layout's own Trait failing throws the same way
47
+ * any other `runVerify` failure does, naming the layout so it's clearly
48
+ * distinguishable from the Block's own verify in the error message.
49
+ */
50
+ async function runLayouts(layouts, tag, out, page, mem, blockName) {
51
+ if (!layouts)
52
+ return;
53
+ for (const layout of layouts) {
54
+ if (!layout.appliesTo(tag))
55
+ continue;
56
+ await runVerify(layout.verify, out, page, mem, `${blockName} (layout: "${layout.name}")`);
57
+ }
58
+ }
59
+ /**
60
+ * Runs `entry` against a fresh tab and returns the resulting terminal Checkpoint.
61
+ * The engine owns the tab's lifecycle, not any individual Block: the tab is
62
+ * created before the first `act` runs and closed in `finally`, so it closes even
63
+ * if a step throws.
64
+ *
65
+ * After each Block resolves, its `next` (if any, from `branch()`) is followed to
66
+ * the following Block, in the same tab - a Block with no `next` runs exactly once,
67
+ * unchanged from before `branch()` existed. `next` returning the same Block is a
68
+ * self-loop; `maxSteps` bounds the loop so a routing bug fails loud instead of
69
+ * hanging.
70
+ *
71
+ * `terminals`, if given, is a sanity check: the Checkpoint the run stops on must be
72
+ * one of the tags the caller actually expects, or the run fails loud instead of
73
+ * silently returning an unexpected result. Omit it when there is only ever one
74
+ * reachable outcome by construction (as with a `defineFlow`-built Flow, which has
75
+ * no routing at all) - there is nothing to validate against.
76
+ */
77
+ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000, options) {
78
+ preflight(mem, entry);
79
+ const page = options?.page ?? (await context.newPage());
80
+ const closeOnFinish = options?.closeOnFinish ?? true;
81
+ try {
82
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
+ let current = entry;
84
+ let input = checkpoint("__start__");
85
+ let steps = 0;
86
+ for (;;) {
87
+ if (++steps > maxSteps) {
88
+ throw new Error(`runGraph: exceeded ${maxSteps} steps - check for an unintended self-loop`);
89
+ }
90
+ await runPrecondition(current.instruction.precondition, input, page, mem, current.name);
91
+ await current.instruction.act(page, input, mem);
92
+ const observed = current.instruction.observe
93
+ ? await current.instruction.observe(page, mem)
94
+ : undefined;
95
+ const checkpoint = await current.instruction.resolve(observed);
96
+ await runVerify(current.instruction.verify, checkpoint, page, mem, current.name);
97
+ await runLayouts(options?.layouts, checkpoint.__state, checkpoint, page, mem, current.name);
98
+ const next = current.next?.(checkpoint);
99
+ if (!next) {
100
+ if (terminals && !terminals.has(checkpoint.__state)) {
101
+ throw new Error(`runGraph: "${current.name}" resolved to "${checkpoint.__state}", which is not a registered terminal (expected one of: ${[...terminals].join(", ")})`);
102
+ }
103
+ return checkpoint;
104
+ }
105
+ current = next;
106
+ input = checkpoint;
107
+ }
108
+ }
109
+ finally {
110
+ if (closeOnFinish) {
111
+ await page.close();
112
+ }
113
+ }
114
+ }
115
+ /**
116
+ * Drives a genuinely separate second tab through its own Block/Flow, in the
117
+ * same browser context an existing `page` already belongs to - the primitive
118
+ * `observe()` is specifically allowed to reach for (see {@link Instruction.observe}),
119
+ * since `act`/`resolve` can't touch `page.context()` at all. A thin, named
120
+ * wrapper around `runGraph` (which already opens its own page from whatever
121
+ * context it's given) rather than a new mechanism - the point is making the
122
+ * pattern discoverable and giving it a real name, not inventing new tab-lifecycle
123
+ * logic. The spawned tab is closed (by `runGraph`'s own `finally`) before this
124
+ * resolves; the original `page` is never touched.
125
+ * @example
126
+ * async observe(page, mem) {
127
+ * const receipt = await spawnTab(EmailReceiptFlow, page, mem);
128
+ * mem.set(ReceiptCode, receipt.__state);
129
+ * }
130
+ */
131
+ export async function spawnTab(entry, page, mem, maxSteps = 5000) {
132
+ return runGraph(entry, undefined, page.context(), mem, maxSteps);
133
+ }
134
+ /**
135
+ * Wraps one `act()` interaction with a caption and highlight, for a Block
136
+ * author who wants to explicitly narrate a specific step rather than rely
137
+ * on the generic per-fill/per-click narration `waygraph chain
138
+ * WAYGRAPH_STEP=1` already does automatically -
139
+ * `await narrate(submitButton, "confirms the order", () => submitButton.click())`
140
+ * reads at the call site instead of being inferred.
141
+ *
142
+ * A true no-op everywhere except inside a `WAYGRAPH_STEP=1` run: it checks
143
+ * for a `window.__wgPositionRing` hook the step-mode overlay installs on
144
+ * the page before doing anything, so a headless/CI/automated run (nobody
145
+ * watching, per-character delays and highlight pauses would just be wasted
146
+ * time) pays zero extra cost - not even the boundingBox() lookup runs. The
147
+ * `action` itself always runs regardless; only the narration is
148
+ * conditional.
149
+ * @example await narrate(submitButton, "confirms the order", () => submitButton.click())
150
+ */
151
+ export async function narrate(locator, caption, action) {
152
+ const page = locator.page();
153
+ const active = await page
154
+ .evaluate(() => typeof globalThis.__wgPositionRing === "function")
155
+ .catch(() => false);
156
+ if (!active)
157
+ return action();
158
+ let owning = false;
159
+ try {
160
+ const box = await locator.boundingBox();
161
+ if (box) {
162
+ await page
163
+ .evaluate(({ box, caption }) => {
164
+ const w = globalThis;
165
+ if (w.__wgPositionRing)
166
+ w.__wgPositionRing(box, caption, "planned");
167
+ // The CLI's own automatic per-fill/per-click narration checks
168
+ // this before showing (and overwriting) its own guess - an
169
+ // explicitly authored caption should win, not get immediately
170
+ // replaced a moment later by the generic one.
171
+ w.__wgLastNarrate = Date.now();
172
+ // Also claim the ring itself: the auto-highlight click/fill
173
+ // patches each hide the ring right after THEIR own step
174
+ // finishes, which - if action() below does more than one thing
175
+ // (e.g. a click then a wait for a toast) - would wipe this
176
+ // caption after just the first sub-step instead of the whole
177
+ // narrated action. Their hideRing() calls no-op while this is
178
+ // true; narrate() below is the one that actually clears it.
179
+ w.__wgNarrateOwnsRing = true;
180
+ }, { box, caption })
181
+ .catch(() => { });
182
+ owning = true;
183
+ // The ring alone is pointless if action() fires on the very next
184
+ // tick - a click that navigates (e.g. a form submit) wipes the ring
185
+ // before a human can read the caption at all. Give it the same
186
+ // "pop for a few seconds" dwell as the CLI's own auto-highlight
187
+ // clicks before the real action runs.
188
+ await new Promise((resolve) => setTimeout(resolve, 650));
189
+ // Re-stamp __wgLastNarrate right before handing off to action() - the
190
+ // CLI's auto-highlight patches only treat a narrate() as "just
191
+ // handled" within a short window (500ms), which the dwell above
192
+ // already burned through. Without this, action()'s own click/fill
193
+ // (same patched Locator prototype) would stomp this caption with its
194
+ // own generic one a moment after it finally became visible.
195
+ await page
196
+ .evaluate(() => {
197
+ globalThis.__wgLastNarrate = Date.now();
198
+ })
199
+ .catch(() => { });
200
+ }
201
+ }
202
+ catch {
203
+ // best-effort - the real action below still runs either way
204
+ }
205
+ try {
206
+ return await action();
207
+ }
208
+ finally {
209
+ if (owning) {
210
+ await page
211
+ .evaluate(() => {
212
+ const w = globalThis;
213
+ w.__wgNarrateOwnsRing = false;
214
+ if (w.__wgHideRing)
215
+ w.__wgHideRing();
216
+ })
217
+ .catch(() => { });
218
+ }
219
+ }
220
+ }
221
+ /** Reserved markers bookending a `defineFlow([start, ...blocks, end])` call. */
222
+ export const start = Symbol("waygraph.start");
223
+ export const end = Symbol("waygraph.end");