waygraph 0.16.1 → 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 (154) 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 +132 -2
  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/run-chain.js +2 -2
  13. package/dist/cli/usage.js +6 -0
  14. package/dist/cli/util.js +1 -1
  15. package/dist/demo/narration/demo-pace.d.ts +1 -1
  16. package/dist/demo/narration/demo-pace.js +1 -1
  17. package/dist/demo/narration/merge.d.ts +1 -1
  18. package/dist/demo/narration/merge.js +1 -1
  19. package/dist/demo/narration/narration.d.ts +2 -6
  20. package/dist/demo/narration/narration.js +5 -17
  21. package/dist/demo/narration/stub-ctx.js +126 -124
  22. package/dist/demo/overlay/cursor.js +1 -1
  23. package/dist/demo/overlay/inpage/banner.js +5 -0
  24. package/dist/demo/overlay/inpage/core.js +1 -6
  25. package/dist/demo/overlay/inpage/network.js +1 -1
  26. package/dist/demo/overlay/inpage/todos.js +0 -15
  27. package/dist/demo/overlay/network-capture.js +2 -2
  28. package/dist/demo/overlay/overlay-css.js +19 -76
  29. package/dist/demo/overlay/overlay-stage.js +1 -1
  30. package/dist/demo/overlay/overlay.js +3 -2
  31. package/dist/demo/overlay/phase-fixtures.js +5 -5
  32. package/dist/demo/overlay/pilot/shell.js +7 -5
  33. package/dist/demo/overlay/rings.d.ts +0 -8
  34. package/dist/demo/overlay/rings.js +0 -15
  35. package/dist/demo/overlay/ui/components/ring.js +2 -2
  36. package/dist/demo/overlay/ui/overlay.css +136 -33
  37. package/dist/demo/overlay/ui/shadow.js +69 -2
  38. package/dist/demo/stepper/after.js +4 -17
  39. package/dist/demo/stepper/before.js +3 -1
  40. package/dist/demo/stepper/demo-log.js +8 -8
  41. package/dist/demo/stepper/interaction-patch.js +3 -15
  42. package/dist/demo/stepper/panel-after.js +1 -1
  43. package/dist/demo/stepper/slides.d.ts +3 -0
  44. package/dist/demo/stepper/slides.js +18 -2
  45. package/dist/demo/stepper/stepper.js +1 -1
  46. package/dist/demo/stepper/teardown.js +0 -1
  47. package/dist/demo/verbose-log.js +1 -1
  48. package/dist/engine/block/block.d.ts +1 -12
  49. package/dist/engine/block/kind.d.ts +3 -0
  50. package/dist/engine/block/kind.js +11 -0
  51. package/dist/engine/block/kinds/method.d.ts +0 -2
  52. package/dist/engine/block/kinds/method.js +0 -1
  53. package/dist/engine/block/kinds/nav.js +1 -1
  54. package/dist/engine/errors.d.ts +12 -0
  55. package/dist/engine/errors.js +12 -0
  56. package/dist/engine/read/read-block.d.ts +61 -0
  57. package/dist/engine/read/read-block.js +94 -0
  58. package/dist/engine/read/read-key.d.ts +32 -0
  59. package/dist/engine/read/read-key.js +34 -0
  60. package/dist/engine/read/read-page.d.ts +19 -0
  61. package/dist/engine/read/read-page.js +50 -0
  62. package/dist/engine/read/readers.d.ts +82 -0
  63. package/dist/engine/read/readers.js +43 -0
  64. package/dist/engine/read/run-read.d.ts +17 -0
  65. package/dist/engine/read/run-read.js +156 -0
  66. package/dist/engine/run/connect.js +3 -2
  67. package/dist/engine/run/events.d.ts +28 -10
  68. package/dist/engine/run/events.js +52 -38
  69. package/dist/engine/run/run.d.ts +1 -19
  70. package/dist/engine/run/run.js +21 -97
  71. package/dist/engine/stub/ring.d.ts +2 -4
  72. package/dist/engine/stub/ring.js +1 -0
  73. package/dist/engine/stub/stub.d.ts +38 -25
  74. package/dist/index.d.ts +9 -7
  75. package/dist/index.js +11 -6
  76. package/dist/live/auto/auto.js +2 -3
  77. package/dist/live/auto/menu.js +3 -2
  78. package/dist/live/auto-session/auto-session.d.ts +9 -2
  79. package/dist/live/auto-session/auto-session.js +23 -20
  80. package/dist/live/auto-session/feed.d.ts +16 -0
  81. package/dist/live/auto-session/feed.js +71 -0
  82. package/dist/live/auto-session/helpers.d.ts +7 -0
  83. package/dist/live/auto-session/helpers.js +9 -0
  84. package/dist/live/auto-session/ipc.d.ts +15 -47
  85. package/dist/live/auto-session/ipc.js +34 -4
  86. package/dist/live/auto-session/protocol.d.ts +127 -0
  87. package/dist/live/auto-session/protocol.js +6 -0
  88. package/dist/live/auto-session/read.d.ts +25 -0
  89. package/dist/live/auto-session/read.js +50 -0
  90. package/dist/live/traverse/worker.d.ts +1 -0
  91. package/dist/live/traverse/worker.js +6 -1
  92. package/dist/manifest/manifest.d.ts +22 -2
  93. package/dist/manifest/manifest.js +27 -1
  94. package/dist/manifest/project.d.ts +10 -1
  95. package/dist/manifest/project.js +9 -12
  96. package/dist/manifest/read-blocks.d.ts +2 -0
  97. package/dist/manifest/read-blocks.js +23 -0
  98. package/dist/manifest/scan.d.ts +3 -2
  99. package/dist/manifest/scan.js +8 -4
  100. package/dist/runner/plan/find.js +2 -2
  101. package/dist/runner/plan/plan.js +1 -1
  102. package/dist/runner/plan/seed-mem.js +5 -5
  103. package/dist/runner/plan/spec.js +1 -1
  104. package/dist/runner/runner.d.ts +0 -1
  105. package/dist/runner/runner.js +6 -6
  106. package/dist/runtime/full-launch.d.ts +2 -1
  107. package/dist/runtime/full-launch.js +29 -27
  108. package/dist/runtime/run-log.d.ts +2 -0
  109. package/dist/runtime/run-log.js +13 -6
  110. package/examples/saucedemo/README.md +2 -2
  111. package/examples/saucedemo/src/blocks/SITE-MAP.md +3 -0
  112. package/examples/saucedemo/src/blocks/saucedemo-web/cart/cart.read.block.ts +26 -0
  113. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
  114. package/examples/saucedemo/src/blocks/saucedemo-web/header.read.block.ts +25 -0
  115. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.read.block.ts +28 -0
  116. package/examples/saucedemo/src/flows/api-demo.flow.ts +1 -1
  117. package/examples/saucedemo/src/flows/cart-bulk.flow.ts +1 -1
  118. package/examples/saucedemo/src/flows/cart-continue.flow.ts +1 -1
  119. package/examples/saucedemo/src/flows/catalog-logout.flow.ts +1 -1
  120. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +3 -2
  121. package/examples/saucedemo/src/flows/checkout.flow.ts +13 -12
  122. package/examples/saucedemo/src/flows/login.flow.ts +1 -1
  123. package/examples/saucedemo/src/flows/shop.flow.ts +3 -2
  124. package/examples/saucedemo/src/flows/stub-on-error.flow.ts +1 -1
  125. package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +1 -1
  126. package/examples/saucedemo/src/states/checkout.reads.ts +9 -0
  127. package/examples/saucedemo/src/stubs/device-showcase.ts +70 -76
  128. package/examples/saucedemo/src/stubs/finish-order.ts +38 -0
  129. package/examples/saucedemo/src/stubs/login.ts +5 -5
  130. package/package.json +5 -1
  131. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
  132. package/templates/quickstart/src/flows/api-demo.flow.ts +1 -1
  133. package/templates/quickstart/src/flows/cart-bulk.flow.ts +1 -1
  134. package/templates/quickstart/src/flows/cart-continue.flow.ts +1 -1
  135. package/templates/quickstart/src/flows/catalog-logout.flow.ts +1 -1
  136. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +3 -2
  137. package/templates/quickstart/src/flows/checkout.flow.ts +13 -12
  138. package/templates/quickstart/src/flows/login.flow.ts +1 -1
  139. package/templates/quickstart/src/flows/shop.flow.ts +3 -2
  140. package/templates/quickstart/src/flows/stub-on-error.flow.ts +1 -1
  141. package/templates/quickstart/src/flows/viewer-blocked.flow.ts +1 -1
  142. package/templates/quickstart/src/stubs/device-showcase.ts +70 -76
  143. package/templates/quickstart/src/stubs/finish-order.ts +38 -0
  144. package/templates/quickstart/src/stubs/login.ts +5 -5
  145. package/templates/scaffold/src/flows/(external)/hoppscotch/auth.flow.ts +1 -1
  146. package/templates/scaffold/src/flows/(external)/hoppscotch/get.flow.ts +1 -1
  147. package/templates/scaffold/src/flows/(external)/hoppscotch/post.flow.ts +1 -1
  148. package/templates/scaffold/src/flows/(user-stories)/email-verification.flow.ts +4 -4
  149. package/templates/scaffold/src/flows/README.md +1 -1
  150. package/templates/scaffold/src/flows/home/cart.flow.ts +7 -7
  151. package/templates/scaffold/src/flows/home/hello.flow.ts +10 -7
  152. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-hello.method.block.ts +1 -9
  153. package/templates/skills/waygraph-convention.skill.md +10 -2
  154. package/templates/skills/waygraph-pilot.skill.md +7 -0
package/README.md CHANGED
@@ -100,6 +100,7 @@ names) still works and is fine for small projects, but the Map is where the tool
100
100
  | See the whole graph | `waygraph graph` |
101
101
  | Lint your Blocks and folders | `waygraph check` / `waygraph typecheck` / `waygraph map` |
102
102
  | Open a persistent browser for an agent | `waygraph browser start` |
103
+ | Read what the page shows, as JSON (beta: Read Blocks) | `waygraph pilot read <id> [path]` |
103
104
  | Highlight things on the live page | `waygraph browser highlight <id> "#login\|Login button\|warning"` |
104
105
  | Set up coding-agent skills | `waygraph agent-dive --loop claude` |
105
106
 
@@ -124,4 +125,15 @@ npm run typecheck && npm test # unit + Playwright suites
124
125
  Working on waygraph: [`AGENTS.md`](./AGENTS.md) (the rules, how to verify) and
125
126
  [`src/ARCHITECTURE.md`](./src/ARCHITECTURE.md) (where the code starts).
126
127
 
128
+ ## Status: alpha
129
+
130
+ > [!WARNING]
131
+ > Waygraph is an alpha build. Its syntax may still change in major ways between releases, and a release can remove
132
+ > API (each removal is listed in [what was removed](./docs/deprecated.md); `waygraph upgrade --codemod` converts most
133
+ > of it). For now we recommend staying on 0.16.0:
134
+ >
135
+ > ```bash
136
+ > npm install --save-exact waygraph@0.16.0
137
+ > ```
138
+
127
139
  MIT
@@ -1,4 +1,4 @@
1
- export type PracticeKind = "multi-input" | "combined-action" | "empty-verify" | "overcomplex-selector" | "inline-selector-should-use-sel-file";
1
+ export type PracticeKind = "multi-input" | "combined-action" | "empty-verify" | "overcomplex-selector" | "inline-selector-should-use-sel-file" | "hand-written-ui";
2
2
  export interface PracticeWarning {
3
3
  kind: PracticeKind;
4
4
  file: string;
@@ -46,6 +46,15 @@ const XPATH_LOCATOR = /\.locator\(\s*["'`](?:xpath=)?\/\//;
46
46
  * scoped to simple, single-token selectors (id/class/attr/tag) so this doesn't fire on the
47
47
  * legitimate multi-part selectors `.sel.ts` files themselves are made of. */
48
48
  const INLINE_SIMPLE_SELECTOR = /(?:\.locator|\bclick|\burl)\s*[:(]\s*["'`](#|\.)[\w-]+["'`]/;
49
+ /**
50
+ * Hand-written UI in a test project - real, recurring pattern: an LLM wanting a caption, a highlight or a banner builds
51
+ * its own DOM in the page or restyles / reaches into waygraph's overlay, instead of using `ctx.ui`. It changes the page
52
+ * under test and breaks whenever the overlay changes.
53
+ */
54
+ const OVERLAY_INTERNALS = /\b__wg[A-Za-z]|["'`][^"'`\n]*(?:#wg-|\.wg-|wg-root)/;
55
+ const DOM_BUILD = /\bdocument\.createElement\(|\.innerHTML\s*=|\binsertAdjacentHTML\(|\baddStyleTag\(|\.style\.cssText|\.style\.[a-zA-Z]+\s*=(?!=)/;
56
+ /** Shared narration (`src/stubs/*.ts`) is checked for hand-written UI too, next to Block and flow files. */
57
+ const STUB_FILE = /[\\/]stubs[\\/][^\\/]+\.ts$/;
49
58
  const METHOD_OR_EFFECT_FILE = /\.(method|effect)\.block\.ts$/;
50
59
  const METHOD_OR_EFFECT_DEFINE = /\bdefine(?:Method|Effect)Block\b/;
51
60
  /** Whole-file opt-out: `// waygraph-ignore-practices` */
@@ -55,6 +64,13 @@ const IGNORE_KINDS = /waygraph-ignore:\s*([^\n*]+)/;
55
64
  function walkBlockAndFlowFiles(projectDir, walkDir) {
56
65
  return walkDir(projectDir, SCAN_GLOB);
57
66
  }
67
+ function handWrittenUi(src) {
68
+ if (OVERLAY_INTERNALS.test(src))
69
+ return "reaches into waygraph's overlay (#wg-*, .wg-*, window.__wg*)";
70
+ if (DOM_BUILD.test(src))
71
+ return "builds or restyles page DOM (createElement / innerHTML / addStyleTag / .style)";
72
+ return undefined;
73
+ }
58
74
  function ignoredKindsForFile(src) {
59
75
  if (IGNORE_ALL.test(src))
60
76
  return "all";
@@ -143,6 +159,9 @@ export function collectPracticeWarnings(projectDir, walkDir, opts) {
143
159
  });
144
160
  }
145
161
  }
162
+ const ui = isIgnored("hand-written-ui", ignore) ? undefined : handWrittenUi(src);
163
+ if (ui)
164
+ warnings.push(handWrittenUiWarning(rel, ui));
146
165
  if (!isIgnored("empty-verify", ignore) && IS_ASSERT_BLOCK.test(src) && EMPTY_VERIFY_ARRAY.test(src)) {
147
166
  warnings.push({
148
167
  kind: "empty-verify",
@@ -152,8 +171,23 @@ export function collectPracticeWarnings(projectDir, walkDir, opts) {
152
171
  });
153
172
  }
154
173
  }
174
+ for (const file of walkDir(projectDir, /\.ts$/).filter((f) => STUB_FILE.test(f) && !SCAN_GLOB.test(f))) {
175
+ const src = readFileSync(file, "utf-8");
176
+ const ui = isIgnored("hand-written-ui", ignoredKindsForFile(src)) ? undefined : handWrittenUi(src);
177
+ if (ui)
178
+ warnings.push(handWrittenUiWarning(relative(projectDir, file).replace(/\\/g, "/"), ui));
179
+ }
155
180
  return warnings;
156
181
  }
182
+ function handWrittenUiWarning(file, what) {
183
+ return {
184
+ kind: "hand-written-ui",
185
+ file,
186
+ detail: `${what} - narration goes through ctx.ui (ring, caption, title, todos, slides, device, zoom, network tab), ` +
187
+ "and a test never changes the page it tests. Missing something in ctx.ui? Ask for it in waygraph instead of " +
188
+ "overriding; opt out with // waygraph-ignore: hand-written-ui",
189
+ };
190
+ }
157
191
  export function practiceKindLabel(kind) {
158
192
  switch (kind) {
159
193
  case "multi-input":
@@ -166,6 +200,8 @@ export function practiceKindLabel(kind) {
166
200
  return "regex/XPath selector where a plain one would do";
167
201
  case "inline-selector-should-use-sel-file":
168
202
  return "inline selector — a _sel.ts/*.sel.ts file already exists here";
203
+ case "hand-written-ui":
204
+ return "hand-written UI — use ctx.ui, don't build or override the overlay";
169
205
  default: {
170
206
  const _exhaustive = kind;
171
207
  return _exhaustive;
package/dist/beta.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { reads, ReadKey } from "./engine/read/read-key.js";
2
+ export type { ReadNamespace, ReadSpec, ReadFieldsOf } from "./engine/read/read-key.js";
3
+ export type { Reader, ReadCtx, RowCtx } from "./engine/read/readers.js";
4
+ export { ReadPage } from "./engine/read/read-page.js";
5
+ export { defineReadBlock, readCheckpoint } from "./engine/read/read-block.js";
6
+ export type { ReadBlock, ReadBlockOptions, CheckpointRead } from "./engine/read/read-block.js";
package/dist/beta.js ADDED
@@ -0,0 +1,5 @@
1
+ // The `waygraph/beta` entry: features whose shape may still change before they move into `waygraph`. Import them from
2
+ // "waygraph/beta"; a beta release can rename or reshape them.
3
+ export { reads, ReadKey } from "./engine/read/read-key.js";
4
+ export { ReadPage } from "./engine/read/read-page.js";
5
+ export { defineReadBlock, readCheckpoint } from "./engine/read/read-block.js";
@@ -103,7 +103,7 @@ function indentOf(node) {
103
103
  return " ".repeat(statement.getIndentationLevel() * 2);
104
104
  }
105
105
  function titleStub(block, title, indent) {
106
- return `${block}.stubBefore((ctx) => {\n${indent} ctx.title(${title});\n${indent} })`;
106
+ return `${block}.stubBefore((ctx) => {\n${indent} ctx.ui.title(${title});\n${indent} })`;
107
107
  }
108
108
  function chain(head, steps, indent) {
109
109
  return [head, ...steps, ".end()"].join(`\n${indent} `);
@@ -172,7 +172,7 @@ function rewrite(call, kindOf, needsMap) {
172
172
  if (name === "withTitle") {
173
173
  const first = mapCall && firstStepOf(mapCall);
174
174
  if (!first)
175
- return `withTitle(${flow.getText().split("\n")[0]}, …): name the flow with ctx.title(…) in a stub on its first step`;
175
+ return `withTitle(${flow.getText().split("\n")[0]}, …): name the flow with ctx.ui.title(…) in a stub on its first step`;
176
176
  addTitle(first, args[1].getText());
177
177
  }
178
178
  else if (mapCall) {
@@ -4,7 +4,10 @@
4
4
  // aliases (`banner`, `bannerPos`, `clearDevice`, `clearTodos`, `landscape`, `portrait`, `todoDockFull`) become the
5
5
  // names they stood for. A stub written as a slot map (`{ email: { selector, label } }`) becomes a function of
6
6
  // `ctx.ring` calls, and a stub set in a `define*Block({ ... })` (options or instruction) moves out to a chained
7
- // `.stubBefore(...)` on the Block. Works on the TypeScript AST (ts-morph).
7
+ // `.stubBefore(...)` on the Block; a Block's `slides` become `ctx.slides(...)` in its chained `.stubAfter(...)`;
8
+ // `ctx.highlights({ a: ring })` becomes one `ctx.ring("a", ring)` per ring; `narrate(locator, caption, action)` in an
9
+ // `act` becomes the action, reported so the caption can move to a stubBefore ring. Last, everything a stub paints moves
10
+ // under `ctx.ui` (`ctx.ring(...)` -> `ctx.ui.ring(...)`). Works on the TypeScript AST (ts-morph).
8
11
  import { Node, SyntaxKind } from "ts-morph";
9
12
  const RENAMED = {
10
13
  banner: (a) => `title(${a.join(", ")})`,
@@ -127,7 +130,7 @@ function convertSlotMaps(sf) {
127
130
  }
128
131
  return changed;
129
132
  }
130
- /** `defineXBlock({ ..., stubBefore: fn })` (or in its instruction) -> `defineXBlock({ ... }).stubBefore(fn)`. */
133
+ /** `defineXBlock({ ..., stubBefore: fn, slides })` (or in its instruction) -> `defineXBlock({ ... }).stubBefore(fn).stubAfter(slides)`. */
131
134
  function hoistDefinitionStubs(sf) {
132
135
  let changed = false;
133
136
  for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression).reverse()) {
@@ -141,9 +144,17 @@ function hoistDefinitionStubs(sf) {
141
144
  if (Node.isPropertyAssignment(instruction) && Node.isObjectLiteralExpression(instruction.getInitializer()))
142
145
  holders.push(instruction.getInitializer());
143
146
  const chained = [];
147
+ let slides;
144
148
  for (const holder of holders) {
145
149
  for (const prop of holder.getProperties()) {
146
150
  const name = Node.isPropertyAssignment(prop) || Node.isMethodDeclaration(prop) ? prop.getName() : "";
151
+ if (name === "slides" && Node.isPropertyAssignment(prop)) {
152
+ const init = prop.getInitializerOrThrow();
153
+ // `slides: (out) => [...]` read the Checkpoint the step reached; in stubAfter that is ctx.out.
154
+ slides = Node.isArrowFunction(init) || Node.isFunctionExpression(init) ? `(${init.getText()})(ctx.out!)` : init.getText();
155
+ prop.remove();
156
+ continue;
157
+ }
147
158
  if (!PHASES.has(name))
148
159
  continue;
149
160
  let fn;
@@ -157,6 +168,15 @@ function hoistDefinitionStubs(sf) {
157
168
  prop.remove();
158
169
  }
159
170
  }
171
+ if (slides !== undefined) {
172
+ const after = chained.findIndex((c) => c.startsWith(".stubAfter("));
173
+ if (after < 0)
174
+ chained.push(`.stubAfter((ctx) => ctx.slides(${slides}))`);
175
+ else {
176
+ const fn = chained[after].slice(".stubAfter(".length, -1);
177
+ chained[after] = `.stubAfter(async (ctx) => {\n await (${fn})(ctx);\n ctx.slides(${slides});\n})`;
178
+ }
179
+ }
160
180
  if (chained.length) {
161
181
  call.replaceWithText(call.getText() + chained.join(""));
162
182
  changed = true;
@@ -164,6 +184,110 @@ function hoistDefinitionStubs(sf) {
164
184
  }
165
185
  return changed;
166
186
  }
187
+ /** `ctx.highlights({ a: ring, b: ring })` -> `ctx.ring("a", ring); ctx.ring("b", ring);`, one call at a time. */
188
+ function convertHighlights(sf, problems) {
189
+ let changed = false;
190
+ const skipped = new Set();
191
+ for (;;) {
192
+ // Raw text replacement forgets the file's nodes, so look the next call up again each time.
193
+ const statement = sf.getDescendantsOfKind(SyntaxKind.ExpressionStatement).find((st) => {
194
+ const c = ctxCall(st);
195
+ return c && c.method === "highlights" && !skipped.has(st.getStart()) && isStubCtx(c.call, c.receiver);
196
+ });
197
+ if (!statement)
198
+ return changed;
199
+ const c = ctxCall(statement);
200
+ const map = c.call.getArguments()[0];
201
+ if (!Node.isObjectLiteralExpression(map)) {
202
+ problems.push({ line: c.call.getStartLineNumber(), reason: "ctx.highlights(...) was removed and its argument is not an object literal: call ctx.ring(id, ring) for each ring" });
203
+ skipped.add(statement.getStart());
204
+ continue;
205
+ }
206
+ // Each ring keeps its own lines, moved to the statement's indent (replaceWithText would re-indent them).
207
+ const column = (n) => n.getStart() - n.getStartLinePos();
208
+ const indent = " ".repeat(column(statement));
209
+ const rings = map.getProperties().flatMap((p) => {
210
+ if (!Node.isPropertyAssignment(p))
211
+ return [];
212
+ const from = column(p);
213
+ const value = p.getInitializerOrThrow().getText().split("\n").map((l, n) => (n === 0 ? l : indent + l.slice(Math.min(from, l.search(/\S|$/))))).join("\n");
214
+ return [`${c.receiver}.ring(${JSON.stringify(p.getName().replace(/^["']|["']$/g, ""))}, ${value});`];
215
+ });
216
+ sf.replaceText([statement.getStart(), statement.getEnd()], rings.join("\n" + indent));
217
+ changed = true;
218
+ }
219
+ }
220
+ /**
221
+ * `narrate(locator, caption, () => action)` in an `act` -> just the action. The caption was narration inside the
222
+ * Block; it belongs in a `stubBefore` ring where the flow uses the Block, which the codemod can't place, so it is
223
+ * reported.
224
+ */
225
+ function unwrapNarrate(sf, problems) {
226
+ let changed = false;
227
+ for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression).reverse()) {
228
+ if (call.wasForgotten() || call.getExpression().getText() !== "narrate" || call.getArguments().length !== 3)
229
+ continue;
230
+ const [, captionNode, action] = call.getArguments();
231
+ const caption = captionNode.getText();
232
+ const line = call.getStartLineNumber();
233
+ let body = `(${action.getText()})()`;
234
+ if (Node.isArrowFunction(action) && !Node.isBlock(action.getBody()))
235
+ body = action.getBody().getText();
236
+ call.replaceWithText(body);
237
+ problems.push({ line, reason: `narrate(...) was removed; its caption ${caption} is gone: ring the element in a stubBefore where the flow uses this Block` });
238
+ changed = true;
239
+ }
240
+ if (changed) {
241
+ for (const decl of sf.getImportDeclarations()) {
242
+ const spec = decl.getNamedImports().find((n) => n.getName() === "narrate");
243
+ if (!spec)
244
+ continue;
245
+ spec.remove();
246
+ if (decl.getNamedImports().length === 0 && !decl.getDefaultImport() && !decl.getNamespaceImport())
247
+ decl.remove();
248
+ }
249
+ }
250
+ return changed;
251
+ }
252
+ /** Everything a stub paints, which 0.16.3 moved under `ctx.ui`. */
253
+ const UI_METHODS = new Set(["ring", "todos", "todoIndex", "todoGroups", "hideTodos", "device", "viewport", "touch", "orientation", "hideDevice", "zoom", "zoomOut", "title", "titlePos", "bannerUi", "caption", "hideCaption", "slides", "hideBanner", "showNetworkTab", "hideNetworkTab", "todoDockUi"]);
254
+ /** Whether `name` at `node` is a stub's ctx: the nearest enclosing function with a parameter of that name is a stub. */
255
+ function isStubCtxName(node, name) {
256
+ for (const fn of node.getAncestors()) {
257
+ if (!(Node.isArrowFunction(fn) || Node.isFunctionExpression(fn) || Node.isFunctionDeclaration(fn) || Node.isMethodDeclaration(fn)))
258
+ continue;
259
+ const param = fn.getParameters().find((p) => p.getName() === name);
260
+ if (!param)
261
+ continue;
262
+ if (/StubCtx/.test(param.getType().getText(param)))
263
+ return true;
264
+ const parent = fn.getParent();
265
+ const callee = Node.isCallExpression(parent) ? parent.getExpression() : undefined;
266
+ if (Node.isPropertyAccessExpression(callee) && /^stub(Before|After|OnError)$/.test(callee.getName()))
267
+ return true;
268
+ if (Node.isPropertyAssignment(parent) && /^stub(Before|After|OnError)$/.test(parent.getName()))
269
+ return true;
270
+ // `((ctx) => ...)(ctx)`, as the slides merge writes it: called at once with an outer stub's ctx.
271
+ const call = Node.isParenthesizedExpression(parent) ? parent.getParent() : undefined;
272
+ const arg = Node.isCallExpression(call) ? call.getArguments()[0] : undefined;
273
+ return arg !== undefined && Node.isIdentifier(arg) && isStubCtxName(call, arg.getText());
274
+ }
275
+ return false;
276
+ }
277
+ /** `ctx.ring(...)` -> `ctx.ui.ring(...)`, everywhere a stub's ctx paints (idempotent: `ctx.ui.ring` is left alone). */
278
+ function moveToUi(sf) {
279
+ let changed = false;
280
+ for (const access of sf.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression).reverse()) {
281
+ if (access.wasForgotten() || !UI_METHODS.has(access.getName()))
282
+ continue;
283
+ const receiver = access.getExpression();
284
+ if (!Node.isIdentifier(receiver) || !isStubCtxName(access, receiver.getText()))
285
+ continue;
286
+ access.replaceWithText(`${receiver.getText()}.ui.${access.getName()}`);
287
+ changed = true;
288
+ }
289
+ return changed;
290
+ }
167
291
  /** Rewrites the old stub forms in every file; returns the files it changed and the calls left for a person. */
168
292
  export function convertStubs(project) {
169
293
  const changed = [];
@@ -171,6 +295,10 @@ export function convertStubs(project) {
171
295
  for (const sf of project.getSourceFiles()) {
172
296
  const left = [];
173
297
  let touched = hoistDefinitionStubs(sf);
298
+ if (unwrapNarrate(sf, left))
299
+ touched = true;
300
+ if (convertHighlights(sf, left))
301
+ touched = true;
174
302
  if (convertSlotMaps(sf))
175
303
  touched = true;
176
304
  for (const block of sf.getDescendantsOfKind(SyntaxKind.Block).reverse()) {
@@ -179,6 +307,8 @@ export function convertStubs(project) {
179
307
  if (convertStatements(block.getStatements(), left))
180
308
  touched = true;
181
309
  }
310
+ if (moveToUi(sf))
311
+ touched = true;
182
312
  if (touched)
183
313
  changed.push(sf);
184
314
  problems.push(...left.map((p) => ({ file: sf.getFilePath(), ...p })));
@@ -9,6 +9,7 @@ import { resolveBaseUrl } from "../../runtime/waygraph-config.js";
9
9
  import { parseCommand } from "../args.js";
10
10
  import { applyRunFlags, parseRunFlags } from "../flags.js";
11
11
  import { printBrowserSessionsList } from "../usage.js";
12
+ import { isSessionOp } from "./session-ops.js";
12
13
  /** Runs a `pilot` subcommand; false when it is a session op for session-ops.ts. */
13
14
  export async function pilotCase(args) {
14
15
  if (args[1] === "sessions") {
@@ -76,26 +77,12 @@ export async function pilotCase(args) {
76
77
  }
77
78
  return true;
78
79
  }
79
- if (args[1] !== "send" &&
80
- args[1] !== "status" &&
81
- args[1] !== "dom" &&
82
- args[1] !== "trace" &&
83
- args[1] !== "console" &&
84
- args[1] !== "storage" &&
85
- args[1] !== "upload" &&
86
- args[1] !== "click" &&
87
- args[1] !== "type" &&
88
- args[1] !== "press" &&
89
- args[1] !== "goto" &&
90
- args[1] !== "reload" &&
91
- args[1] !== "reach" &&
92
- args[1] !== "resync" &&
93
- args[1] !== "highlight") {
80
+ if (!isSessionOp("pilot", args[1])) {
94
81
  console.error("waygraph pilot: usage:\n" +
95
82
  " waygraph pilot start [--inject …] [--goto <url>] [--headless] [--cli] [--base-url <url>] [project]\n" +
96
83
  " waygraph pilot sessions [project] # list live browser sessions\n" +
97
84
  " waygraph pilot attach <sessionId> # graph + snapshot for an existing session\n" +
98
- " waygraph pilot send|status|attach|highlight|… <sessionId> … (controls browser — same as browser/auto)");
85
+ " waygraph pilot send|status|read|attach|highlight|… <sessionId> … (controls browser — same as browser/auto)");
99
86
  process.exit(1);
100
87
  }
101
88
  return false;
@@ -5,6 +5,8 @@ import { parseHighlightShorthand } from "../../demo/narration/shorthand.js";
5
5
  import { resolveSessionMeta } from "../../live/auto-session/files.js";
6
6
  import { requestSession, runAttachLoop } from "../../live/auto-session/ipc.js";
7
7
  import { parseCommand } from "../args.js";
8
+ import { subscribeSession } from "../../live/auto-session/feed.js";
9
+ import { SESSION_OPS } from "../../live/auto-session/protocol.js";
8
10
  const SEND_OPTIONS = {
9
11
  timeout: { type: "string", value: "<ms>", help: "How long one Block may run (default 15000)" },
10
12
  };
@@ -13,7 +15,8 @@ const DOM_OPTIONS = {
13
15
  selector: { type: "string", value: "<css>", help: "Only this element's subtree" },
14
16
  depth: { type: "string", value: "N", help: "Levels to read" },
15
17
  };
16
- const OPS = new Set(["send", "status", "attach", "dom", "trace", "console", "storage", "upload", "click", "type", "press", "goto", "reload", "reach", "resync", "highlight"]);
18
+ /** The protocol's ops, as CLI subcommands: `subscribe` is `watch`; `attach` is the CLI's own interactive loop. */
19
+ const OPS = new Set([...SESSION_OPS.filter((op) => op !== "subscribe"), "watch", "attach"]);
17
20
  /** Whether `sub` is a session op (`attach` is pilot's own subcommand, not an op, under `pilot`). */
18
21
  export function isSessionOp(command, sub) {
19
22
  return sub !== undefined && OPS.has(sub) && !(sub === "attach" && command === "pilot");
@@ -113,6 +116,28 @@ export async function sessionOpCase(args, command) {
113
116
  process.exitCode = 1;
114
117
  return;
115
118
  }
119
+ if (sub === "hello") {
120
+ const res = await requestSession(proj, sessionId, { op: "hello" });
121
+ console.log(JSON.stringify(res));
122
+ if (!res.ok)
123
+ process.exitCode = 1;
124
+ return;
125
+ }
126
+ if (sub === "watch") {
127
+ // One JSON line per run event (steps, reads) and per new snapshot, until Ctrl-C or the session ends.
128
+ const feed = await subscribeSession(proj, sessionId, (message) => console.log(JSON.stringify(message)));
129
+ process.once("SIGINT", () => feed.close());
130
+ await feed.closed;
131
+ return;
132
+ }
133
+ if (sub === "read") {
134
+ const path = args[3];
135
+ const res = await requestSession(proj, sessionId, path ? { op: "read", path } : { op: "read" });
136
+ console.log(JSON.stringify(res));
137
+ if (!res.ok)
138
+ process.exitCode = 1;
139
+ return;
140
+ }
116
141
  if (sub === "storage") {
117
142
  const res = await requestSession(proj, sessionId, { op: "storage" });
118
143
  console.log(JSON.stringify(res));
@@ -110,7 +110,7 @@ export declare const RUN_OPTIONS: {
110
110
  };
111
111
  readonly "todo-left": {
112
112
  readonly type: "boolean";
113
- readonly help: "Floating checklist dock on the left (also ctx.todos(id, items, { pos }))";
113
+ readonly help: "Floating checklist dock on the left (also ctx.ui.todos(id, items, { pos }))";
114
114
  readonly for: readonly ["demo"];
115
115
  };
116
116
  readonly "todo-right": {
package/dist/cli/flags.js CHANGED
@@ -18,7 +18,7 @@ export const RUN_OPTIONS = {
18
18
  "fast": { type: "boolean", help: "Shorter auto-next / Next gates (keeps the smooth cursor)", for: ["demo"] },
19
19
  "full": { type: "boolean", help: "Classic wrap-all block chips (default: carousel)", for: ["demo"] },
20
20
  "mini": { type: "boolean", help: "Force the compact mini panel (Hide pill + Next)", for: ["demo"] },
21
- "todo-left": { type: "boolean", help: "Floating checklist dock on the left (also ctx.todos(id, items, { pos }))", for: ["demo"] },
21
+ "todo-left": { type: "boolean", help: "Floating checklist dock on the left (also ctx.ui.todos(id, items, { pos }))", for: ["demo"] },
22
22
  "todo-right": { type: "boolean", help: "Floating checklist dock on the right", for: ["demo"] },
23
23
  "todo-full": { type: "boolean", help: "Full checklist: no compact / collision / behind-ring", for: ["demo"] },
24
24
  "todo-smart": { type: "boolean", help: "Compact + collision + behind-ring (the default)", for: ["demo"] },
@@ -92,12 +92,12 @@ export async function runChainAuto(projectDir, fromTag, toTag) {
92
92
  const graph = await discoverGraph(projectDir);
93
93
  const path = findBlockPath(graph, fromTag, toTag);
94
94
  if (!path) {
95
- console.error(`waygraph chain auto: no Block path from "${fromTag}" to "${toTag}" in the discovered graph`);
95
+ console.error(`waygraph auto --blocks: no Block path from "${fromTag}" to "${toTag}" in the discovered graph`);
96
96
  process.exitCode = 1;
97
97
  return;
98
98
  }
99
99
  const spec = path.join(" then ");
100
- console.error(`waygraph chain auto: ${fromTag} -> ${toTag} via ${spec}`);
100
+ console.error(`waygraph auto --blocks: ${fromTag} -> ${toTag} via ${spec}`);
101
101
  if (!envString("WAYGRAPH_BASE_URL")) {
102
102
  const resolved = await resolveBaseUrl(projectDir);
103
103
  if (resolved)
package/dist/cli/usage.js CHANGED
@@ -66,6 +66,12 @@ Primary (less is more):
66
66
  does nothing observable in the DOM) that inspectDom
67
67
  alone can't explain, since it only reads what actually
68
68
  rendered, not what the page/network actually said
69
+ waygraph auto watch <sessionId> Live feed, one JSON line each: every run event (steps,
70
+ step:read) and the snapshot after each change. Ctrl-C ends
71
+ waygraph auto hello <sessionId> The session protocol version, waygraph version, where it is
72
+ waygraph auto read <sessionId> [path] (beta) What the page shows, as JSON, from the project's
73
+ Read Blocks (*.read.block.ts); [path] narrows it, e.g.
74
+ post.comments. Read it before reaching for dom
69
75
  waygraph auto storage <sessionId> Read localStorage/sessionStorage plus registered
70
76
  service workers (scope/active URL/state) - client-
71
77
  side state (push-subscription/auth tokens etc.) that
package/dist/cli/util.js CHANGED
@@ -4,7 +4,7 @@ import { spawn } from "node:child_process";
4
4
  // ---------------------------------------------------------------------------
5
5
  // `try` - a bundled, self-contained showcase. No .flow.ts to write, no
6
6
  // scenario to design - copies a small real Flow (nav/action Blocks, Trait
7
- // verify, narrate()) into the target project and runs it in step mode
7
+ // verify, ctx stubs) into the target project and runs it in step mode
8
8
  // against a public demo site, so a curious dev sees the real overlay/panel
9
9
  // experience in under a minute, then gets pointed at the plain waygraph
10
10
  // source that produced it.
@@ -29,7 +29,7 @@ export declare function formatDemoPaceBadge(pace: DemoPace, baseAutoplayMs?: num
29
29
  */
30
30
  export declare function formatDemoPaceLabel(pace: DemoPace, baseAutoplayMs?: number): string;
31
31
  /**
32
- * Resolve min dwell ms for any fixture (stub / flow fixture / yap slide),
32
+ * Resolve min dwell ms for any fixture (a stub ring or a yap slide),
33
33
  * or `null` when duration is unset (caller keeps its default timing).
34
34
  *
35
35
  * Authoring wins: an explicit slow/numeric pace is not crushed by CLI `--fast`
@@ -150,7 +150,7 @@ export function formatDemoPaceLabel(pace, baseAutoplayMs = 1800) {
150
150
  return `pace normal (1x, ~${gate}ms gates)`;
151
151
  }
152
152
  /**
153
- * Resolve min dwell ms for any fixture (stub / flow fixture / yap slide),
153
+ * Resolve min dwell ms for any fixture (a stub ring or a yap slide),
154
154
  * or `null` when duration is unset (caller keeps its default timing).
155
155
  *
156
156
  * Authoring wins: an explicit slow/numeric pace is not crushed by CLI `--fast`
@@ -4,7 +4,7 @@ import type { SomeBlock } from "../../engine/block/block.js";
4
4
  import type { Checkpoint } from "../../engine/checkpoint/checkpoint.js";
5
5
  export declare function applyDefaultZoom(slots: ResolvedHighlight[], defaultZoom?: number): ResolvedHighlight[];
6
6
  export declare function applyDefaultZoomOut(slots: ResolvedHighlight[], defaultZoomOut?: boolean): ResolvedHighlight[];
7
- /** A stub phase's slot map as an ordered list of resolved highlights. */
7
+ /** A stub phase's rings (by id, as ctx.ring set them) as an ordered list of resolved highlights. */
8
8
  export declare function resolvePhaseMap(phaseMap: HighlightStubPhase): ResolvedHighlight[];
9
9
  /** The stub a Block's flow chained for this phase, if any. */
10
10
  export declare function readStubRaw(block: SomeBlock, phase: HighlightStubPhaseName): StubLifecycleFn<Checkpoint<string>> | undefined;
@@ -40,7 +40,7 @@ export function applyDefaultZoomOut(slots, defaultZoomOut) {
40
40
  return slots;
41
41
  return slots.map((h) => h.zoomOut !== undefined ? h : { ...h, zoomOut: defaultZoomOut });
42
42
  }
43
- /** A stub phase's slot map as an ordered list of resolved highlights. */
43
+ /** A stub phase's rings (by id, as ctx.ring set them) as an ordered list of resolved highlights. */
44
44
  export function resolvePhaseMap(phaseMap) {
45
45
  const phaseKeys = Object.keys(phaseMap);
46
46
  const allNumeric = phaseKeys.length > 0 && phaseKeys.every((k) => Number.isFinite(Number(k)) && String(Number(k)) === k);
@@ -1,10 +1,10 @@
1
1
  import type { HighlightStubPhaseName, StubPhaseResult } from "../../engine/stub/stub.js";
2
- import type { ResolvedHighlight, WaygraphSlide } from "../../engine/stub/ring.js";
2
+ import type { ResolvedHighlight } from "../../engine/stub/ring.js";
3
3
  import type { SomeBlock } from "../../engine/block/block.js";
4
4
  import type { Checkpoint } from "../../engine/checkpoint/checkpoint.js";
5
5
  import type { MemPage } from "../../engine/mem/mem.js";
6
6
  /**
7
- * Run an open stub lifecycle (preferred) or resolve a static slot map.
7
+ * Runs a Block's stub for one phase and returns what it painted.
8
8
  * Returns rings + episode fixtures (todos / todoIndex / zoom).
9
9
  *
10
10
  * @example
@@ -23,10 +23,6 @@ export declare function runStubPhase(block: SomeBlock, phase: HighlightStubPhase
23
23
  export declare function resolveHighlightSlots(block: SomeBlock, phase: HighlightStubPhaseName, opts?: {
24
24
  out?: Checkpoint<string>;
25
25
  }): ResolvedHighlight[];
26
- /** Resolve multi-step yap slides from `instruction.slides` (function form allowed). */
27
- export declare function resolveSlides(block: SomeBlock, opts?: {
28
- out?: Checkpoint<string>;
29
- }): WaygraphSlide[];
30
26
  /**
31
27
  * True when the block authored stubAfter (lifecycle fn or slots).
32
28
  * When true, demo should not use verify-trait fallback.
@@ -3,7 +3,7 @@ import { applyDefaultZoom, applyDefaultZoomOut, readStubRaw, resolvePhaseMap } f
3
3
  import { buildTodoDock, normalizeTodoStyle, normalizeTodos } from "./todo-dock.js";
4
4
  import { resolveDeviceState } from "./device.js";
5
5
  /**
6
- * Run an open stub lifecycle (preferred) or resolve a static slot map.
6
+ * Runs a Block's stub for one phase and returns what it painted.
7
7
  * Returns rings + episode fixtures (todos / todoIndex / zoom).
8
8
  *
9
9
  * @example
@@ -27,6 +27,9 @@ export async function runStubPhase(block, phase, opts) {
27
27
  throw new Error(`${block.name}: its ${phase} stub threw: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
28
28
  }
29
29
  }
30
+ if (phase === "stubOnError" && bag.slides?.length) {
31
+ throw new Error(`${block.name}: its stubOnError stub set slides; slides play before or after a step (stubBefore / stubAfter), not on its failure`);
32
+ }
30
33
  let highlights = applyDefaultZoom(resolvePhaseMap(bag.highlights), bag.zoom);
31
34
  highlights = applyDefaultZoomOut(highlights, bag.zoomOut);
32
35
  const todoDock = bag.hideTodos
@@ -95,6 +98,7 @@ export async function runStubPhase(block, phase, opts) {
95
98
  // Boolean, not string - "" isn't a state here, so a plain undefined-check (like zoom/title)
96
99
  // is enough; false is real ("author called hideNetworkTab()") and must still be forwarded.
97
100
  ...(bag.networkTabVisible !== undefined ? { networkTabVisible: bag.networkTabVisible } : {}),
101
+ slides: [...(bag.slides ?? [])],
98
102
  ...(bag.todoPos !== undefined ? { todoPos: bag.todoPos } : {}),
99
103
  ...(bag.todoDockUi !== undefined ? { todoDockUi: bag.todoDockUi } : {}),
100
104
  ...(bag.todoParallel === true ? { todoParallel: true } : {}),
@@ -118,22 +122,6 @@ export function resolveHighlightSlots(block, phase, opts) {
118
122
  }
119
123
  return applyDefaultZoom(resolvePhaseMap(bag.highlights), bag.zoom);
120
124
  }
121
- /** Resolve multi-step yap slides from `instruction.slides` (function form allowed). */
122
- export function resolveSlides(block, opts) {
123
- const instr = block.instruction;
124
- let raw = instr.slides;
125
- if (typeof raw === "function") {
126
- try {
127
- raw = raw(opts?.out ?? { __state: "" });
128
- }
129
- catch {
130
- raw = [];
131
- }
132
- }
133
- if (!Array.isArray(raw))
134
- return [];
135
- return raw.filter((s) => s && typeof s.caption === "string" && s.caption.length > 0);
136
- }
137
125
  function stubPhaseLooksAuthored(block, phase) {
138
126
  return readStubRaw(block, phase) !== undefined;
139
127
  }