waygraph 0.16.2 → 0.16.4

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 (123) hide show
  1. package/README.md +12 -0
  2. package/dist/analysis/practices-check.d.ts +1 -1
  3. package/dist/analysis/practices-check.js +36 -0
  4. package/dist/beta.d.ts +6 -0
  5. package/dist/beta.js +5 -0
  6. package/dist/cli/commands/codemod-flows.js +2 -2
  7. package/dist/cli/commands/codemod-stubs.js +43 -1
  8. package/dist/cli/commands/pilot.js +3 -16
  9. package/dist/cli/commands/session-ops.js +26 -1
  10. package/dist/cli/flags.d.ts +1 -1
  11. package/dist/cli/flags.js +1 -1
  12. package/dist/cli/usage.js +6 -0
  13. package/dist/demo/narration/stub-ctx.js +126 -124
  14. package/dist/demo/overlay/cursor.js +1 -1
  15. package/dist/demo/overlay/inpage/banner.js +5 -0
  16. package/dist/demo/overlay/inpage/core.js +1 -0
  17. package/dist/demo/overlay/inpage/network.js +1 -1
  18. package/dist/demo/overlay/inpage/todos.js +0 -15
  19. package/dist/demo/overlay/network-capture.js +2 -2
  20. package/dist/demo/overlay/overlay-css.js +18 -21
  21. package/dist/demo/overlay/overlay-stage.js +1 -1
  22. package/dist/demo/overlay/phase-fixtures.js +5 -5
  23. package/dist/demo/overlay/pilot/shell.js +7 -5
  24. package/dist/demo/overlay/ui/components/ring.js +2 -2
  25. package/dist/demo/overlay/ui/overlay.css +98 -35
  26. package/dist/demo/overlay/ui/shadow.js +69 -2
  27. package/dist/demo/stepper/demo-log.js +8 -8
  28. package/dist/demo/stepper/slides.d.ts +1 -1
  29. package/dist/demo/stepper/slides.js +1 -1
  30. package/dist/demo/stepper/stepper.js +1 -1
  31. package/dist/engine/block/kind.d.ts +3 -0
  32. package/dist/engine/block/kind.js +11 -0
  33. package/dist/engine/errors.d.ts +12 -0
  34. package/dist/engine/errors.js +12 -0
  35. package/dist/engine/read/read-block.d.ts +61 -0
  36. package/dist/engine/read/read-block.js +94 -0
  37. package/dist/engine/read/read-key.d.ts +32 -0
  38. package/dist/engine/read/read-key.js +34 -0
  39. package/dist/engine/read/read-page.d.ts +19 -0
  40. package/dist/engine/read/read-page.js +50 -0
  41. package/dist/engine/read/readers.d.ts +82 -0
  42. package/dist/engine/read/readers.js +43 -0
  43. package/dist/engine/read/run-read.d.ts +17 -0
  44. package/dist/engine/read/run-read.js +156 -0
  45. package/dist/engine/run/connect.js +3 -2
  46. package/dist/engine/run/events.d.ts +28 -10
  47. package/dist/engine/run/events.js +52 -38
  48. package/dist/engine/run/run.js +25 -13
  49. package/dist/engine/stub/stub.d.ts +26 -21
  50. package/dist/index.d.ts +7 -4
  51. package/dist/index.js +9 -4
  52. package/dist/live/auto/auto.js +2 -3
  53. package/dist/live/auto/menu.js +3 -2
  54. package/dist/live/auto-session/auto-session.d.ts +9 -2
  55. package/dist/live/auto-session/auto-session.js +23 -20
  56. package/dist/live/auto-session/feed.d.ts +16 -0
  57. package/dist/live/auto-session/feed.js +71 -0
  58. package/dist/live/auto-session/helpers.d.ts +7 -0
  59. package/dist/live/auto-session/helpers.js +9 -0
  60. package/dist/live/auto-session/ipc.d.ts +15 -47
  61. package/dist/live/auto-session/ipc.js +34 -4
  62. package/dist/live/auto-session/protocol.d.ts +127 -0
  63. package/dist/live/auto-session/protocol.js +6 -0
  64. package/dist/live/auto-session/read.d.ts +25 -0
  65. package/dist/live/auto-session/read.js +50 -0
  66. package/dist/live/traverse/worker.d.ts +1 -0
  67. package/dist/live/traverse/worker.js +6 -1
  68. package/dist/manifest/manifest.d.ts +22 -2
  69. package/dist/manifest/manifest.js +27 -1
  70. package/dist/manifest/project.d.ts +10 -1
  71. package/dist/manifest/project.js +9 -12
  72. package/dist/manifest/read-blocks.d.ts +2 -0
  73. package/dist/manifest/read-blocks.js +23 -0
  74. package/dist/manifest/scan.d.ts +3 -2
  75. package/dist/manifest/scan.js +8 -4
  76. package/dist/runner/runner.d.ts +0 -1
  77. package/dist/runner/runner.js +6 -6
  78. package/dist/runtime/full-launch.d.ts +2 -1
  79. package/dist/runtime/full-launch.js +29 -27
  80. package/dist/runtime/run-log.d.ts +2 -0
  81. package/dist/runtime/run-log.js +13 -6
  82. package/examples/saucedemo/README.md +2 -2
  83. package/examples/saucedemo/src/blocks/SITE-MAP.md +3 -0
  84. package/examples/saucedemo/src/blocks/saucedemo-web/cart/cart.read.block.ts +26 -0
  85. package/examples/saucedemo/src/blocks/saucedemo-web/header.read.block.ts +25 -0
  86. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.read.block.ts +28 -0
  87. package/examples/saucedemo/src/flows/api-demo.flow.ts +1 -1
  88. package/examples/saucedemo/src/flows/cart-bulk.flow.ts +1 -1
  89. package/examples/saucedemo/src/flows/cart-continue.flow.ts +1 -1
  90. package/examples/saucedemo/src/flows/catalog-logout.flow.ts +1 -1
  91. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +1 -1
  92. package/examples/saucedemo/src/flows/checkout.flow.ts +11 -11
  93. package/examples/saucedemo/src/flows/login.flow.ts +1 -1
  94. package/examples/saucedemo/src/flows/shop.flow.ts +1 -1
  95. package/examples/saucedemo/src/flows/stub-on-error.flow.ts +1 -1
  96. package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +1 -1
  97. package/examples/saucedemo/src/states/checkout.reads.ts +9 -0
  98. package/examples/saucedemo/src/stubs/device-showcase.ts +37 -37
  99. package/examples/saucedemo/src/stubs/finish-order.ts +2 -2
  100. package/examples/saucedemo/src/stubs/login.ts +5 -5
  101. package/package.json +5 -1
  102. package/templates/quickstart/src/flows/api-demo.flow.ts +1 -1
  103. package/templates/quickstart/src/flows/cart-bulk.flow.ts +1 -1
  104. package/templates/quickstart/src/flows/cart-continue.flow.ts +1 -1
  105. package/templates/quickstart/src/flows/catalog-logout.flow.ts +1 -1
  106. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +1 -1
  107. package/templates/quickstart/src/flows/checkout.flow.ts +11 -11
  108. package/templates/quickstart/src/flows/login.flow.ts +1 -1
  109. package/templates/quickstart/src/flows/shop.flow.ts +1 -1
  110. package/templates/quickstart/src/flows/stub-on-error.flow.ts +1 -1
  111. package/templates/quickstart/src/flows/viewer-blocked.flow.ts +1 -1
  112. package/templates/quickstart/src/stubs/device-showcase.ts +37 -37
  113. package/templates/quickstart/src/stubs/finish-order.ts +2 -2
  114. package/templates/quickstart/src/stubs/login.ts +5 -5
  115. package/templates/scaffold/src/flows/(external)/hoppscotch/auth.flow.ts +1 -1
  116. package/templates/scaffold/src/flows/(external)/hoppscotch/get.flow.ts +1 -1
  117. package/templates/scaffold/src/flows/(external)/hoppscotch/post.flow.ts +1 -1
  118. package/templates/scaffold/src/flows/(user-stories)/email-verification.flow.ts +4 -4
  119. package/templates/scaffold/src/flows/README.md +1 -1
  120. package/templates/scaffold/src/flows/home/cart.flow.ts +7 -7
  121. package/templates/scaffold/src/flows/home/hello.flow.ts +8 -8
  122. package/templates/skills/waygraph-convention.skill.md +10 -2
  123. package/templates/skills/waygraph-pilot.skill.md +7 -0
@@ -0,0 +1,156 @@
1
+ import { ignorePageGone, ignoreTimeout } from "../page-gone.js";
2
+ import { Reader } from "./readers.js";
3
+ /** In the page (self-contained, shipped by evaluateAll): reads a field, list rows or a table from the matched elements. */
4
+ function readInPage(matched, ask) {
5
+ const shown = (el) => {
6
+ const box = el.getBoundingClientRect();
7
+ const style = getComputedStyle(el);
8
+ return box.width > 0 && box.height > 0 && style.visibility !== "hidden" && style.display !== "none";
9
+ };
10
+ // textContent, whitespace collapsed: innerText forces layout per element (125ms vs 3ms for 8,000 table cells).
11
+ const textOf = (el) => (el.textContent ?? "").replace(/\s+/g, " ").trim();
12
+ const take = (els, plan) => {
13
+ const first = els[0];
14
+ switch (plan.kind) {
15
+ case "count":
16
+ return els.length;
17
+ case "visible":
18
+ return els.some(shown);
19
+ case "texts":
20
+ return els.map(textOf);
21
+ case "attr":
22
+ return first ? first.getAttribute(plan.attr ?? "") : null;
23
+ case "value":
24
+ return first && "value" in first ? String(first.value) : null;
25
+ default:
26
+ // text, and number (parsed in Node from the text)
27
+ return first ? textOf(first) : null;
28
+ }
29
+ };
30
+ const within = (root, plan) => (plan.selector ? [...root.querySelectorAll(plan.selector)] : [root]);
31
+ if (ask.mode === "field")
32
+ return take(matched, ask.plan);
33
+ if (ask.mode === "list") {
34
+ return matched.map((row) => {
35
+ const out = {};
36
+ for (const [name, plan] of Object.entries(ask.fields))
37
+ out[name] = take(within(row, plan), plan);
38
+ return out;
39
+ });
40
+ }
41
+ const table = matched[0];
42
+ if (!table)
43
+ return null;
44
+ const HEAD = "th, [role=columnheader]";
45
+ const CELL = "td, th, [role=cell], [role=gridcell], [role=rowheader], [role=columnheader]";
46
+ const rows = [...table.querySelectorAll("tr, [role=row]")];
47
+ const headRow = rows.find((r) => r.querySelector(HEAD) && !r.querySelector("td, [role=cell], [role=gridcell]"));
48
+ const headers = headRow ? [...headRow.querySelectorAll(CELL)].map(textOf) : [];
49
+ const index = {};
50
+ const absent = [];
51
+ for (const [name, header] of Object.entries(ask.columns)) {
52
+ const at = headers.findIndex((h) => h.toLowerCase() === header.toLowerCase());
53
+ if (at < 0)
54
+ absent.push(name);
55
+ else
56
+ index[name] = at;
57
+ }
58
+ const body = rows.filter((r) => r !== headRow && r.querySelector("td, [role=cell], [role=gridcell], [role=rowheader]"));
59
+ const out = body.map((r) => {
60
+ const cells = [...r.querySelectorAll(CELL)];
61
+ const row = {};
62
+ for (const name of Object.keys(ask.columns)) {
63
+ const cell = index[name] === undefined ? undefined : cells[index[name]];
64
+ row[name] = cell ? textOf(cell) : null;
65
+ }
66
+ return row;
67
+ });
68
+ const declared = Number(table.getAttribute("aria-rowcount"));
69
+ return { rows: out, absent, rowCount: Number.isFinite(declared) && declared > 0 ? declared - (headRow ? 1 : 0) : out.length };
70
+ }
71
+ /** In the page: resolves once the DOM has had no change for `quietMs` (or after `capMs`, whichever comes first). */
72
+ function domQuiet({ quietMs, capMs }) {
73
+ return new Promise((resolve) => {
74
+ const done = () => {
75
+ observer.disconnect();
76
+ clearTimeout(quiet);
77
+ clearTimeout(cap);
78
+ resolve();
79
+ };
80
+ let quiet = setTimeout(done, quietMs);
81
+ const cap = setTimeout(done, capMs);
82
+ const observer = new MutationObserver(() => {
83
+ clearTimeout(quiet);
84
+ quiet = setTimeout(done, quietMs);
85
+ });
86
+ observer.observe(document, { subtree: true, childList: true, characterData: true, attributes: true });
87
+ });
88
+ }
89
+ /**
90
+ * Waits for the page to stop changing before a read. A step's verify can pass before the app has rendered where it
91
+ * landed (a URL check passes the moment a client-side route changes), so a read right after it can see the old page.
92
+ */
93
+ export async function settlePage(page, quietMs = 150, capMs = 2000) {
94
+ await page.waitForLoadState("domcontentloaded", { timeout: capMs }).catch(ignoreTimeout);
95
+ await page.evaluate(domQuiet, { quietMs, capMs }).catch(ignorePageGone);
96
+ }
97
+ /** The first number in a display text: `"$1,299.00"` -> 1299, `"41 likes"` -> 41, no number -> null. */
98
+ export function firstNumber(text) {
99
+ if (typeof text !== "string")
100
+ return null;
101
+ const m = /-?\d[\d,]*(?:\.\d+)?/.exec(text);
102
+ return m ? Number(m[0].replace(/,/g, "")) : null;
103
+ }
104
+ function finishField(plan, raw) {
105
+ return plan.kind === "number" ? firstNumber(raw) : raw;
106
+ }
107
+ async function readOne(page, mem, plan, path, notes) {
108
+ if (plan.kind === "fn")
109
+ return (await plan.fn(page, mem)) ?? null;
110
+ const matched = page.locator(plan.selector);
111
+ if (plan.kind === "list") {
112
+ const rows = (await matched.evaluateAll(readInPage, { mode: "list", fields: plan.fields }));
113
+ return rows.map((row) => {
114
+ for (const [name, field] of Object.entries(plan.fields))
115
+ row[name] = finishField(field, row[name]);
116
+ return row;
117
+ });
118
+ }
119
+ if (plan.kind === "table") {
120
+ const got = (await matched.evaluateAll(readInPage, { mode: "table", columns: plan.columns }));
121
+ if (!got)
122
+ return null;
123
+ const why = [];
124
+ if (got.absent.length)
125
+ why.push(`no column ${got.absent.map((c) => JSON.stringify(plan.columns[c])).join(", ")}`);
126
+ if (got.rowCount > got.rows.length)
127
+ why.push(`${got.rows.length} of ${got.rowCount} rows rendered (scroll or page to read more)`);
128
+ if (why.length)
129
+ notes.set(path, why.join("; "));
130
+ return got.rows;
131
+ }
132
+ return finishField(plan, await matched.evaluateAll(readInPage, { mode: "field", plan }));
133
+ }
134
+ /** Reads every field of every spec, in parallel, into leaves by path. */
135
+ export async function runReadSpecs(page, mem, specs) {
136
+ const values = new Map();
137
+ const notes = new Map();
138
+ const jobs = [];
139
+ const walk = (fields, prefix) => {
140
+ for (const [name, value] of Object.entries(fields)) {
141
+ if (value === undefined)
142
+ continue;
143
+ const path = `${prefix}.${name}`;
144
+ if (value instanceof Reader) {
145
+ values.set(path, null);
146
+ jobs.push(readOne(page, mem, value.plan, path, notes).then((v) => void values.set(path, v)));
147
+ }
148
+ else
149
+ walk(value, path);
150
+ }
151
+ };
152
+ for (const spec of specs)
153
+ walk(spec.fields, spec.namespace);
154
+ await Promise.all(jobs);
155
+ return { values, notes };
156
+ }
@@ -1,5 +1,6 @@
1
1
  import { runVerify, runPrecondition } from "../trait/trait.js";
2
2
  import { isConnected, lastLeaf, stepEnded, stepStarted } from "./events.js";
3
+ import { blockKindOf } from "../block/kind.js";
3
4
  /**
4
5
  * Composes two Blocks into one, typechecked so this only compiles when A's output
5
6
  * tag equals B's input tag. The composed Block's act runs A's full instruction
@@ -25,7 +26,7 @@ export function connect(a, b, layouts) {
25
26
  instruction: {
26
27
  async act(page, input, mem) {
27
28
  if (!isConnected(a))
28
- stepStarted(a.name, input.__state);
29
+ stepStarted(a.name, blockKindOf(a), input.__state);
29
30
  await runPrecondition(a.instruction.precondition, input, page, mem, a.name);
30
31
  await a.instruction.act(page, input, mem);
31
32
  const aObserved = a.instruction.observe
@@ -42,7 +43,7 @@ export function connect(a, b, layouts) {
42
43
  }
43
44
  stepEnded(lastLeaf(a), mid.__state);
44
45
  if (!isConnected(b))
45
- stepStarted(b.name, mid.__state);
46
+ stepStarted(b.name, blockKindOf(b), mid.__state);
46
47
  await runPrecondition(b.instruction.precondition, mid, page, mem, b.name);
47
48
  await b.instruction.act(page, mid, mem);
48
49
  },
@@ -1,3 +1,4 @@
1
+ import type { BlockKind } from "../block/kind.js";
1
2
  export type RunEvent = {
2
3
  type: "run:start";
3
4
  runId: string;
@@ -9,6 +10,7 @@ export type RunEvent = {
9
10
  at: number;
10
11
  index: number;
11
12
  block: string;
13
+ kind: BlockKind;
12
14
  from: string;
13
15
  } | {
14
16
  type: "step:end";
@@ -16,6 +18,7 @@ export type RunEvent = {
16
18
  at: number;
17
19
  index: number;
18
20
  block: string;
21
+ kind: BlockKind;
19
22
  to: string;
20
23
  ms: number;
21
24
  } | {
@@ -24,9 +27,18 @@ export type RunEvent = {
24
27
  at: number;
25
28
  index: number;
26
29
  block: string;
30
+ kind: BlockKind;
27
31
  error: string;
28
32
  ms: number;
29
- } | {
33
+ } | ({
34
+ type: "step:read";
35
+ runId: string;
36
+ at: number;
37
+ index: number;
38
+ block: string;
39
+ kind: BlockKind;
40
+ checkpoint: string;
41
+ } & StepRead) | {
30
42
  type: "run:end";
31
43
  runId: string;
32
44
  at: number;
@@ -41,22 +53,28 @@ export type RunEvent = {
41
53
  kind: "video" | "trace" | "screenshot";
42
54
  path: string;
43
55
  };
56
+ /** What the page showed after a step, from the Read Blocks for the Checkpoint it landed on (beta). */
57
+ export interface StepRead {
58
+ read: Record<string, unknown>;
59
+ missing: string[];
60
+ notes: Record<string, string>;
61
+ readBlocks: string[];
62
+ /** A Read Block that failed (two reading one key, say): the step still ran; the read is empty. */
63
+ error?: string;
64
+ }
44
65
  /** What {@link onRunEvent} calls with each event. */
45
66
  export type RunEventListener = (event: RunEvent) => void;
46
67
  /** Subscribes to every run event in this process; returns the unsubscribe function. */
47
68
  export declare function onRunEvent(listener: RunEventListener): () => void;
48
69
  /**
49
- * Opens a run, or joins the one already open (a flow nested in another, or a runner driving a chain step by step). Returns
50
- * whether this call opened it, so only the opener closes it.
70
+ * Runs `body` as a run: opens one (`run:start` ... `run:end`), or joins the run its caller is already in (a flow nested in
71
+ * another, a runner driving a chain step by step). `resultOf` names the outcome for `run:end`.
51
72
  */
52
- export declare function beginRun(label?: string): boolean;
53
- /** Closes the run when the matching `beginRun` opened it. */
54
- export declare function endRun(opened: boolean, outcome: {
55
- result?: string;
56
- error?: unknown;
57
- }): void;
58
- export declare function stepStarted(block: string, from: string): void;
73
+ export declare function inRun<T>(label: string | undefined, body: () => Promise<T>, resultOf?: (value: T) => string | undefined): Promise<T>;
74
+ export declare function stepStarted(block: string, kind: BlockKind, from: string): void;
59
75
  export declare function stepEnded(block: string, to: string): void;
76
+ /** What the page showed after the current step (emitted between its verify and its `step:end`). */
77
+ export declare function stepRead(checkpoint: string, read: StepRead): void;
60
78
  /** The step that started last failed. */
61
79
  export declare function stepFailed(error: unknown): void;
62
80
  /** The id of the run open right now, if any. */
@@ -1,5 +1,6 @@
1
1
  // Run events: what happened, step by step, during a run. Core emits them; subscribers (the run log, the demo overlay,
2
2
  // reports, Waygraph UI) listen. Emitting never throws into the run.
3
+ import { AsyncLocalStorage } from "node:async_hooks";
3
4
  const listeners = new Set();
4
5
  /** Subscribes to every run event in this process; returns the unsubscribe function. */
5
6
  export function onRunEvent(listener) {
@@ -16,68 +17,81 @@ function emit(event) {
16
17
  }
17
18
  }
18
19
  }
19
- let open;
20
20
  /**
21
- * Opens a run, or joins the one already open (a flow nested in another, or a runner driving a chain step by step). Returns
22
- * whether this call opened it, so only the opener closes it.
21
+ * The run each async call chain is in. Per chain, not per process: two runs at once (parallel traverse workers, a UI
22
+ * hosting several) each get their own id and step counter; a flow nested in another joins its parent's run.
23
23
  */
24
- export function beginRun(label) {
25
- if (open) {
26
- open.depth += 1;
27
- return false;
28
- }
29
- const at = Date.now();
30
- open = { runId: `${new Date(at).toISOString().replace(/[:.]/g, "-")}-${crypto.randomUUID().slice(0, 8)}`, startedAt: at, depth: 1, index: 0 };
31
- emit({ type: "run:start", runId: open.runId, at, ...(label ? { label } : {}) });
32
- return true;
33
- }
34
- /** Closes the run when the matching `beginRun` opened it. */
35
- export function endRun(opened, outcome) {
36
- if (!open)
37
- return;
38
- open.depth -= 1;
39
- if (!opened)
40
- return;
24
+ const scope = new AsyncLocalStorage();
25
+ /**
26
+ * Runs `body` as a run: opens one (`run:start` ... `run:end`), or joins the run its caller is already in (a flow nested in
27
+ * another, a runner driving a chain step by step). `resultOf` names the outcome for `run:end`.
28
+ */
29
+ export async function inRun(label, body, resultOf) {
30
+ if (scope.getStore())
31
+ return body();
41
32
  const at = Date.now();
42
- emit({
43
- type: "run:end",
44
- runId: open.runId,
45
- at,
46
- ok: outcome.error === undefined,
47
- ...(outcome.result !== undefined ? { result: outcome.result } : {}),
48
- ...(outcome.error !== undefined ? { error: outcome.error instanceof Error ? outcome.error.message : String(outcome.error) } : {}),
49
- ms: at - open.startedAt,
50
- });
51
- open = undefined;
33
+ const run = { runId: `${new Date(at).toISOString().replace(/[:.]/g, "-")}-${crypto.randomUUID().slice(0, 8)}`, startedAt: at, index: 0 };
34
+ emit({ type: "run:start", runId: run.runId, at, ...(label ? { label } : {}) });
35
+ const end = (outcome) => {
36
+ const done = Date.now();
37
+ emit({
38
+ type: "run:end",
39
+ runId: run.runId,
40
+ at: done,
41
+ ok: !("error" in outcome),
42
+ ...(outcome.result !== undefined ? { result: outcome.result } : {}),
43
+ ...("error" in outcome ? { error: outcome.error instanceof Error ? outcome.error.message : String(outcome.error) } : {}),
44
+ ms: done - run.startedAt,
45
+ });
46
+ };
47
+ try {
48
+ const value = await scope.run(run, body);
49
+ end({ result: resultOf?.(value) });
50
+ return value;
51
+ }
52
+ catch (err) {
53
+ end({ error: err });
54
+ throw err;
55
+ }
52
56
  }
53
- export function stepStarted(block, from) {
57
+ export function stepStarted(block, kind, from) {
58
+ const open = scope.getStore();
54
59
  if (!open)
55
60
  return;
56
61
  const at = Date.now();
57
- open.step = { index: open.index++, block, startedAt: at };
58
- emit({ type: "step:start", runId: open.runId, at, index: open.step.index, block, from });
62
+ open.step = { index: open.index++, block, kind, startedAt: at };
63
+ emit({ type: "step:start", runId: open.runId, at, index: open.step.index, block, kind, from });
59
64
  }
60
65
  export function stepEnded(block, to) {
66
+ const open = scope.getStore();
61
67
  if (!open?.step)
62
68
  return;
63
69
  const at = Date.now();
64
- emit({ type: "step:end", runId: open.runId, at, index: open.step.index, block, to, ms: at - open.step.startedAt });
70
+ emit({ type: "step:end", runId: open.runId, at, index: open.step.index, block, kind: open.step.kind, to, ms: at - open.step.startedAt });
65
71
  open.step = undefined;
66
72
  }
73
+ /** What the page showed after the current step (emitted between its verify and its `step:end`). */
74
+ export function stepRead(checkpoint, read) {
75
+ const open = scope.getStore();
76
+ if (!open?.step)
77
+ return;
78
+ emit({ type: "step:read", runId: open.runId, at: Date.now(), index: open.step.index, block: open.step.block, kind: open.step.kind, checkpoint, ...read });
79
+ }
67
80
  /** The step that started last failed. */
68
81
  export function stepFailed(error) {
82
+ const open = scope.getStore();
69
83
  if (!open?.step)
70
84
  return;
71
85
  const at = Date.now();
72
- const { index, block, startedAt } = open.step;
73
- emit({ type: "step:error", runId: open.runId, at, index, block, error: error instanceof Error ? error.message : String(error), ms: at - startedAt });
86
+ const { index, block, kind, startedAt } = open.step;
87
+ emit({ type: "step:error", runId: open.runId, at, index, block, kind, error: error instanceof Error ? error.message : String(error), ms: at - startedAt });
74
88
  open.step = undefined;
75
89
  }
76
90
  /** The id of the run open right now, if any. */
77
91
  export function currentRunId() {
78
- return open?.runId;
92
+ return scope.getStore()?.runId;
79
93
  }
80
- export function recordArtifact(kind, path, runId = open?.runId) {
94
+ export function recordArtifact(kind, path, runId = scope.getStore()?.runId) {
81
95
  if (runId)
82
96
  emit({ type: "run:artifact", runId, at: Date.now(), kind, path });
83
97
  }
@@ -1,6 +1,8 @@
1
1
  import { WaygraphError, MSG_PREFLIGHT_MISSING_KEY, MSG_RUNGRAPH_MAX_STEPS, MSG_RUNGRAPH_NOT_TERMINAL } from "../errors.js";
2
2
  import { checkpoint } from "../checkpoint/checkpoint.js";
3
- import { beginRun, endRun, isConnected, lastLeaf, stepEnded, stepFailed, stepStarted } from "./events.js";
3
+ import { inRun, isConnected, lastLeaf, stepEnded, stepFailed, stepRead, stepStarted } from "./events.js";
4
+ import { readBlocksFor, readCheckpoint } from "../read/read-block.js";
5
+ import { blockKindOf } from "../block/kind.js";
4
6
  import { runPrecondition, runVerify } from "../trait/trait.js";
5
7
  /**
6
8
  * Checks `block.requires` against `mem` and throws, naming every missing key at
@@ -78,9 +80,13 @@ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000,
78
80
  preflight(mem, entry);
79
81
  const page = options?.page ?? (await context.newPage());
80
82
  const closeOnFinish = options?.closeOnFinish ?? true;
81
- const opened = beginRun();
82
- let result;
83
- let failure;
83
+ return inRun(undefined, () => playGraph(entry, terminals, page, mem, maxSteps, options), (out) => out.__state).finally(async () => {
84
+ if (closeOnFinish)
85
+ await page.close();
86
+ });
87
+ }
88
+ /** The step loop of {@link runGraph}, inside its run: act, observe, resolve, verify, then the next Block. */
89
+ async function playGraph(entry, terminals, page, mem, maxSteps, options) {
84
90
  try {
85
91
  let current = entry;
86
92
  let input = checkpoint("__start__");
@@ -90,7 +96,7 @@ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000,
90
96
  throw new WaygraphError("WG_RUNGRAPH_MAX_STEPS", MSG_RUNGRAPH_MAX_STEPS(maxSteps));
91
97
  }
92
98
  if (!isConnected(current))
93
- stepStarted(current.name, input.__state);
99
+ stepStarted(current.name, blockKindOf(current), input.__state);
94
100
  await runPrecondition(current.instruction.precondition, input, page, mem, current.name);
95
101
  await current.instruction.act(page, input, mem);
96
102
  const observed = current.instruction.observe
@@ -99,13 +105,13 @@ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000,
99
105
  const checkpoint = await current.instruction.resolve(observed);
100
106
  await runVerify(current.instruction.verify, checkpoint, page, mem, current.name);
101
107
  await runLayouts(options?.layouts, checkpoint.__state, checkpoint, page, mem, current.name);
108
+ await readLanding(page, mem, checkpoint.__state);
102
109
  stepEnded(lastLeaf(current), checkpoint.__state);
103
110
  const next = current.next?.(checkpoint);
104
111
  if (!next) {
105
112
  if (terminals && !terminals.has(checkpoint.__state)) {
106
113
  throw new WaygraphError("WG_RUNGRAPH_NOT_TERMINAL", MSG_RUNGRAPH_NOT_TERMINAL(current.name, checkpoint.__state, [...terminals].join(", ")));
107
114
  }
108
- result = checkpoint.__state;
109
115
  return checkpoint;
110
116
  }
111
117
  current = next;
@@ -113,16 +119,9 @@ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000,
113
119
  }
114
120
  }
115
121
  catch (err) {
116
- failure = err;
117
122
  stepFailed(err);
118
123
  throw err;
119
124
  }
120
- finally {
121
- endRun(opened, failure === undefined ? { ...(result !== undefined ? { result } : {}) } : { error: failure });
122
- if (closeOnFinish) {
123
- await page.close();
124
- }
125
- }
126
125
  }
127
126
  /**
128
127
  * Drives a genuinely separate second tab through its own Block/Flow, in the
@@ -143,3 +142,16 @@ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000,
143
142
  export async function spawnTab(entry, page, mem, maxSteps = 5000) {
144
143
  return runGraph(entry, undefined, page.context(), mem, maxSteps);
145
144
  }
145
+ /** After a step's verify: what the page shows, from the Read Blocks for where it landed (none loaded: nothing to do). */
146
+ async function readLanding(page, mem, checkpoint) {
147
+ if (readBlocksFor(checkpoint).length === 0)
148
+ return;
149
+ try {
150
+ const { read, blocks } = await readCheckpoint(page, mem, checkpoint);
151
+ stepRead(checkpoint, { read: read.toJSON(), missing: [...read.missing], notes: { ...read.notes }, readBlocks: blocks });
152
+ }
153
+ catch (err) {
154
+ // A broken Read Block is reported on the step, never fails the run it observes.
155
+ stepRead(checkpoint, { read: {}, missing: [], notes: {}, readBlocks: [], error: err instanceof Error ? err.message : String(err) });
156
+ }
157
+ }
@@ -22,7 +22,7 @@ export type StubPhaseFixtures = {
22
22
  todos?: readonly WaygraphTodoInput[];
23
23
  /** Which todo is current (0-based). Rows before = done, after = pending. */
24
24
  todoIndex?: number;
25
- /** Heading above a single {@link StubCtx.todos} list. */
25
+ /** Heading above a single {@link StubUi.todos} list. */
26
26
  todoTitle?: string;
27
27
  /**
28
28
  * Stable dock id (`data-wg-todo-id`) for external redirect asserts
@@ -85,15 +85,15 @@ export type StubPhaseFixtures = {
85
85
  *
86
86
  * @example
87
87
  * LoginBlock.stubBefore((ctx) => {
88
- * ctx.todos("sign-in", ["Enter email", "Enter password", "Click Sign in"], { index: 0 });
89
- * ctx.zoom(1.35);
90
- * ctx.ring("email", { selector: "#email", label: "Email" });
91
- * ctx.ring("submit", { selector: "#login-button", label: "Sign in" });
88
+ * ctx.ui.todos("sign-in", ["Enter email", "Enter password", "Click Sign in"], { index: 0 });
89
+ * ctx.ui.zoom(1.35);
90
+ * ctx.ui.ring("email", { selector: "#email", label: "Email" });
91
+ * ctx.ui.ring("submit", { selector: "#login-button", label: "Sign in" });
92
92
  * })
93
93
  * // Sequential plan: bump todoIndex in a later phase / a later Block's stubBefore
94
94
  * .stubAfter((ctx) => {
95
- * ctx.todoIndex(2);
96
- * ctx.ring("inventory", { selector: ".inventory_list", label: "Landed" });
95
+ * ctx.ui.todoIndex(2);
96
+ * ctx.ui.ring("inventory", { selector: ".inventory_list", label: "Landed" });
97
97
  * });
98
98
  */
99
99
  export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
@@ -107,17 +107,22 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
107
107
  readonly out: Out | undefined;
108
108
  /** Present on stubOnError when the step threw. */
109
109
  readonly error: unknown | undefined;
110
+ /** Everything a stub paints in the demo: rings, todos, title, caption, device, zoom, network tab, slides. */
111
+ readonly ui: StubUi;
112
+ };
113
+ /** The demo UI a stub paints through: `ctx.ui.ring(...)`, `ctx.ui.todos(...)`, ... */
114
+ export type StubUi = {
110
115
  /** Paints one named ring this phase; the same id again replaces it. */
111
116
  ring(id: string, stub: WaygraphHighlightStub): void;
112
117
  /**
113
- * Floating checklist: `ctx.todos("login", ["Enter email", "Enter password"], { title: "Sign in", index: 0 })`.
118
+ * Floating checklist: `ctx.ui.todos("login", ["Enter email", "Enter password"], { title: "Sign in", index: 0 })`.
114
119
  * The id is the dock's `data-wg-todo-id` (stable through redirects). One dock at a time (setting todos replaces it);
115
120
  * pass `{ parallel: true }` to keep docks with other ids. An empty list hides the dock.
116
121
  */
117
122
  todos(id: string, items: readonly WaygraphTodoInput[], opts?: WaygraphTodosOpts): void;
118
123
  /** Moves the current dock's current item (sequential plans: a later step bumps it). */
119
124
  todoIndex(n: number): void;
120
- /** Several titled lists in one dock (e.g. Functional Requirements / Scenarios / ACs), with the same id and options as {@link StubCtx.todos}. */
125
+ /** Several titled lists in one dock (e.g. Functional Requirements / Scenarios / ACs), with the same id and options as {@link StubUi.todos}. */
121
126
  todoGroups(id: string, groups: readonly WaygraphTodoGroupInput[], opts?: Omit<WaygraphTodosOpts, "index">): void;
122
127
  /**
123
128
  * Hide the floating todo dock. Omit todos on a later block = **keep** the
@@ -127,7 +132,7 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
127
132
  /**
128
133
  * Viewport / device fixture (0.13+). `mobile` | `tablet` | `desktop`.
129
134
  * Omit on later blocks = **keep** (same persist as todos). Use
130
- * {@link StubCtx.hideDevice} or `device("desktop")` to return to desktop.
135
+ * {@link StubUi.hideDevice} or `device("desktop")` to return to desktop.
131
136
  */
132
137
  device(preset: DevicePreset | DeviceState): void;
133
138
  /** Explicit CSS-pixel viewport (implies mobile-ish unless desktop-sized). */
@@ -158,14 +163,14 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
158
163
  titlePos(side: BannerPos): void;
159
164
  /**
160
165
  * Banner UX: `{ collision: false }` keeps it put even over a ring; `{ hidden: true }` hides it.
161
- * @example ctx.bannerUi({ collision: false });
166
+ * @example ctx.ui.bannerUi({ collision: false });
162
167
  */
163
168
  bannerUi(opts: BannerUiOpts): void;
164
169
  /**
165
170
  * Bottom narration caption - a white rounded card (black text, shadow), bottom-centered above
166
- * the step pill; distinct from {@link StubCtx.title}, which is the top banner card. Fades in/out
167
- * on change. Pass `""` (or call {@link StubCtx.hideCaption}) to clear.
168
- * @example ctx.caption("Click Add to cart to continue");
171
+ * the step pill; distinct from {@link StubUi.title}, which is the top banner card. Fades in/out
172
+ * on change. Pass `""` (or call {@link StubUi.hideCaption}) to clear.
173
+ * @example ctx.ui.caption("Click Add to cart to continue");
169
174
  */
170
175
  caption(text: string): void;
171
176
  /** Clears the caption (fades it out). */
@@ -173,7 +178,7 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
173
178
  /**
174
179
  * Multi-step "yap" captions with Next between each, for explaining a longer process. In `stubBefore` they play
175
180
  * before the step runs, in `stubAfter` after it lands; a slide with a `selector` rings it while it is up.
176
- * @example ctx.slides([
181
+ * @example ctx.ui.slides([
177
182
  * { caption: "Checkout is three screens", tag: "YAP" },
178
183
  * { caption: "Overview confirms totals", selector: ".summary_info", duration: true },
179
184
  * ]);
@@ -184,7 +189,7 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
184
189
  /**
185
190
  * Shows the network-tab overlay (Postman-style inspector of real page traffic + defineApiBlock
186
191
  * calls) - capture always runs in the background; this just reveals the panel. Hidden by default.
187
- * @example ctx.showNetworkTab();
192
+ * @example ctx.ui.showNetworkTab();
188
193
  */
189
194
  showNetworkTab(): void;
190
195
  /** Hides the network-tab overlay (capture keeps running - only the panel disappears). */
@@ -193,7 +198,7 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
193
198
  * Todo-dock UX: compact fold, collision flip, dim-behind-ring.
194
199
  * Defaults are smart-on; pass false fields to opt out for this phase.
195
200
  * @example
196
- * ctx.todoDockUi({ compact: false }); // always show the full list
201
+ * ctx.ui.todoDockUi({ compact: false }); // always show the full list
197
202
  */
198
203
  todoDockUi(opts: TodoDockUiOpts): void;
199
204
  };
@@ -213,7 +218,7 @@ export type StubPhaseResult = {
213
218
  * - `keep` - author omitted todos; demo must leave previous dock alone
214
219
  */
215
220
  todoSync?: "set" | "clear" | "keep";
216
- /** Todo-dock UX for this phase (when author called {@link StubCtx.todoDockUi}). */
221
+ /** Todo-dock UX for this phase (when author called {@link StubUi.todoDockUi}). */
217
222
  todoDockUi?: TodoDockUiOpts;
218
223
  /** Resolved device after this phase (undefined when keep with no prior). */
219
224
  device?: DeviceState;
@@ -231,7 +236,7 @@ export type StubPhaseResult = {
231
236
  bannerUi?: BannerUiOpts;
232
237
  caption?: string;
233
238
  networkTabVisible?: boolean;
234
- /** Slides to play in this phase ({@link StubCtx.slides}); empty when the stub set none. */
239
+ /** Slides to play in this phase ({@link StubUi.slides}); empty when the stub set none. */
235
240
  slides: WaygraphSlide[];
236
241
  todoPos?: "left" | "right";
237
242
  /** When true, keep other docks (multi-todo). Default false = replace. */
@@ -256,9 +261,9 @@ export type StubBagState = {
256
261
  /** Author asked to reset to desktop. */
257
262
  hideDevice?: boolean | undefined;
258
263
  device?: DeviceState | undefined;
259
- /** Touch override when set via ctx.touch() alone or with device. */
264
+ /** Touch override when set via ctx.ui.touch() alone or with device. */
260
265
  touchOverride?: boolean | undefined;
261
- /** Orientation override when set via ctx.orientation / landscape / portrait. */
266
+ /** Orientation override when set via ctx.ui.orientation / landscape / portrait. */
262
267
  orientationOverride?: DeviceOrientation | undefined;
263
268
  zoom?: number | undefined;
264
269
  zoomOut?: boolean | undefined;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- import "./runtime/full-launch.js";
2
- import "./runtime/run-log.js";
3
1
  export type { Checkpoint, Start } from "./engine/checkpoint/checkpoint.js";
4
2
  export type { Instruction, Block, DefinedBlock, SomeBlock, SomeDefinedBlock } from "./engine/block/block.js";
5
3
  export type { WaygraphInstanceOption } from "./engine/block/action-page.js";
@@ -55,7 +53,7 @@ export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./ana
55
53
  export { buildManifest } from "./manifest/manifest.js";
56
54
  export { onRunEvent } from "./engine/run/events.js";
57
55
  export type { RunEvent } from "./engine/run/events.js";
58
- export type { Manifest, ManifestBlock, ManifestFlow } from "./manifest/manifest.js";
56
+ export type { Manifest, ManifestBlock, ManifestFlow, ManifestReadBlock } from "./manifest/manifest.js";
59
57
  export type { WaygraphGraph, WaygraphNode, WaygraphEdge, SkippedBlock, OrphanBlock } from "./analysis/graph.js";
60
58
  export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp } from "./manifest/select.js";
61
59
  export type { BlocksSelect, BlocksSelectKind } from "./manifest/select.js";
@@ -67,6 +65,10 @@ export type { TraverseCoverageReport } from "./live/traverse/coverage.js";
67
65
  export { WG_UI_SEL, WG_READY_PANEL_SEL, WG_READY_AUTO_SEL, WG_READY_BANNER_SEL, readWgOverlayBeacon, readWgOverlayBeaconInPage, assertWgOverlayReady } from "./demo/overlay/beacon.js";
68
66
  export type { WgModalKind, WgBeaconRow } from "./demo/overlay/beacon.js";
69
67
  export { AutoSession } from "./live/auto-session/auto-session.js";
68
+ export { SESSION_OPS, SESSION_PROTOCOL_VERSION } from "./live/auto-session/protocol.js";
69
+ export type { SessionRequest, SessionResponse, SessionStreamMessage, HelloResponse } from "./live/auto-session/protocol.js";
70
+ export { subscribeSession } from "./live/auto-session/feed.js";
71
+ export { requestSession } from "./live/auto-session/ipc.js";
70
72
  export { stubFilePath } from "./live/auto-session/helpers.js";
71
73
  export type { AutoSessionInit, SessionSnapshot, SessionSnapshotEdge, SessionSnapshotSection, ApplyPickResult, ApplyPathResult, TraceStep, ConsoleLogEntry, StorageSnapshot, ServiceWorkerRegistrationInfo, CookieInfo, StubFileKind } from "./live/auto-session/helpers.js";
72
74
  export { pilotStart } from "./live/pilot/pilot.js";
@@ -83,7 +85,8 @@ export type { ConnectLayout } from "./engine/run/connect.js";
83
85
  export type { EffectExtras } from "./engine/block/kinds/effect.js";
84
86
  export type { BlockBase, RoutedBlockBase } from "./engine/checkpoint/route.js";
85
87
  export type { RunEventListener } from "./engine/run/events.js";
86
- export type { BlockKind, ScanOptions } from "./manifest/project.js";
88
+ export type { ScanOptions } from "./manifest/project.js";
89
+ export type { BlockKind } from "./engine/block/kind.js";
87
90
  export type { DomSnapshot, InspectDomOptions, InspectDomResult } from "./live/auto-session/helpers.js";
88
91
  export type { BrowserStartInit } from "./live/auto-session/start.js";
89
92
  export type { PilotFixtureRing, PilotHighlightFixtures } from "./demo/overlay/pilot/fixtures.js";