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
@@ -1,52 +1,8 @@
1
1
  import type { PilotHighlightFixtures } from "../../demo/overlay/pilot/fixtures.js";
2
2
  import { AutoSession } from "./auto-session.js";
3
3
  import { SessionMeta } from "./files.js";
4
- import { type ApplyPathResult, type AutoSessionInit, type ConsoleLogEntry, type DomSnapshot, type InspectDomOptions, type SessionSnapshot, type StorageSnapshot, type StubFileKind, type TraceStep } from "./helpers.js";
5
- export type StatusOrSendResponse = {
6
- ok: true;
7
- snapshot: SessionSnapshot;
8
- quit: boolean;
9
- } | {
10
- ok: false;
11
- error: string;
12
- };
13
- export type DomResponse = {
14
- ok: true;
15
- snapshot: DomSnapshot;
16
- } | {
17
- ok: false;
18
- error: string;
19
- };
20
- export type TraceResponse = {
21
- ok: true;
22
- trace: TraceStep[];
23
- } | {
24
- ok: false;
25
- error: string;
26
- };
27
- export type ConsoleResponse = {
28
- ok: true;
29
- consoleLog: ConsoleLogEntry[];
30
- } | {
31
- ok: false;
32
- error: string;
33
- };
34
- export type StorageResponse = ({
35
- ok: true;
36
- } & StorageSnapshot) | {
37
- ok: false;
38
- error: string;
39
- };
40
- export type ReachResponse = ApplyPathResult;
41
- export type HighlightResponse = {
42
- ok: true;
43
- painted: number;
44
- missing: string[];
45
- snapshot: SessionSnapshot;
46
- } | {
47
- ok: false;
48
- error: string;
49
- };
4
+ import { type AutoSessionInit, type InspectDomOptions, type StubFileKind } from "./helpers.js";
5
+ import type { HelloResponse, ConsoleResponse, DomResponse, HighlightResponse, ReachResponse, ReadResponse, StatusOrSendResponse, StorageResponse, TraceResponse } from "./protocol.js";
50
6
  /**
51
7
  * Client-side: sends one request to a running session and returns its
52
8
  * response. Reports a stale/unreachable session as an error within
@@ -94,6 +50,16 @@ export declare function requestSession(projectDir: string, sessionId: string, re
94
50
  export declare function requestSession(projectDir: string, sessionId: string, request: {
95
51
  op: "storage";
96
52
  }, timeoutMs?: number): Promise<StorageResponse>;
53
+ export declare function requestSession(projectDir: string, sessionId: string, request: {
54
+ op: "hello";
55
+ }, timeoutMs?: number): Promise<HelloResponse | {
56
+ ok: false;
57
+ error: string;
58
+ }>;
59
+ export declare function requestSession(projectDir: string, sessionId: string, request: {
60
+ op: "read";
61
+ path?: string;
62
+ }, timeoutMs?: number): Promise<ReadResponse>;
97
63
  export declare function requestSession(projectDir: string, sessionId: string, request: {
98
64
  op: "reach";
99
65
  checkpoint: string;
@@ -110,7 +76,9 @@ export declare function requestSession(projectDir: string, sessionId: string, re
110
76
  * already prevents true parallelism, but two overlapping `await`-heavy
111
77
  * handlers could still interleave against the same page/mem.
112
78
  */
113
- export declare function serveSession(session: AutoSession, projectDir: string, sessionId: string, headless?: boolean): Promise<void>;
79
+ export declare function serveSession(session: AutoSession, projectDir: string, sessionId: string, headless?: boolean,
80
+ /** After a quit closed the session and removed its files: the session process exits here. */
81
+ onShutdown?: () => void): Promise<void>;
114
82
  /** Entry point for the hidden `__auto-serve` CLI command (runs in the detached child). */
115
83
  export declare function runAutoServeCommand(init: AutoSessionInit, sessionId: string): Promise<void>;
116
84
  /**
@@ -5,7 +5,7 @@
5
5
  * See openspec/changes/waygraph-auto-cli-session-control.
6
6
  */
7
7
  import { spawn } from "node:child_process";
8
- import { mkdirSync, openSync, rmSync, writeFileSync, } from "node:fs";
8
+ import { mkdirSync, openSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
9
9
  import { createRequire } from "node:module";
10
10
  import { connect, createServer } from "node:net";
11
11
  import { dirname, join } from "node:path";
@@ -15,6 +15,8 @@ import { fileURLToPath } from "node:url";
15
15
  import { ignorePageGone } from "../../engine/page-gone.js";
16
16
  import { AutoSession } from "./auto-session.js";
17
17
  import { generateSessionId, isPidAlive, logPathFor, metaPath, readSessionMeta, removeSessionFiles, resolveSessionMeta, sessionDir, socketDir, socketPathFor, writeGlobalSessionMeta } from "./files.js";
18
+ import { createFeed } from "./feed.js";
19
+ import { SESSION_PROTOCOL_VERSION } from "./protocol.js";
18
20
  /** Reads newline-delimited JSON off a socket, one message at a time. */
19
21
  function readOneMessage(sock, timeoutMs) {
20
22
  return new Promise((resolvePromise, reject) => {
@@ -109,7 +111,9 @@ export async function requestSession(projectDir, sessionId, request, timeoutMs =
109
111
  * already prevents true parallelism, but two overlapping `await`-heavy
110
112
  * handlers could still interleave against the same page/mem.
111
113
  */
112
- export async function serveSession(session, projectDir, sessionId, headless = true) {
114
+ export async function serveSession(session, projectDir, sessionId, headless = true,
115
+ /** After a quit closed the session and removed its files: the session process exits here. */
116
+ onShutdown = () => { }) {
113
117
  mkdirSync(sessionDir(projectDir), { recursive: true });
114
118
  mkdirSync(socketDir(), { recursive: true });
115
119
  const socketPath = socketPathFor(sessionId);
@@ -120,6 +124,7 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
120
124
  queue = next.catch(() => { });
121
125
  return next;
122
126
  };
127
+ const feed = createFeed();
123
128
  const server = createServer((sock) => {
124
129
  let buf = "";
125
130
  sock.on("data", (chunk) => {
@@ -141,6 +146,15 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
141
146
  sock.write(JSON.stringify({ ok: false, error: "malformed request (invalid JSON)" }) + "\n");
142
147
  return;
143
148
  }
149
+ // Neither waits behind a running request: hello names the protocol, subscribe turns this socket into a live feed.
150
+ if (request.op === "hello") {
151
+ sock.write(JSON.stringify({ ok: true, protocol: SESSION_PROTOCOL_VERSION, waygraph: waygraphVersion(), sessionId, here: session.where }) + "\n");
152
+ return;
153
+ }
154
+ if (request.op === "subscribe") {
155
+ feed.add(sock);
156
+ return;
157
+ }
144
158
  await enqueue(async () => {
145
159
  let response;
146
160
  let shouldQuit = false;
@@ -165,6 +179,9 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
165
179
  else if (request.op === "console") {
166
180
  response = { ok: true, consoleLog: session.getConsoleLog() };
167
181
  }
182
+ else if (request.op === "read") {
183
+ response = { ok: true, ...(await session.read(request.path)) };
184
+ }
168
185
  else if (request.op === "storage") {
169
186
  response = { ok: true, ...(await session.inspectStorage()) };
170
187
  }
@@ -213,6 +230,9 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
213
230
  response = { ok: false, error: err instanceof Error ? err.message : String(err) };
214
231
  }
215
232
  sock.write(JSON.stringify(response) + "\n");
233
+ // Whatever changed (a step ran, a reload, a raw action): subscribers get the new snapshot.
234
+ if (response.ok && "snapshot" in response && "sections" in response.snapshot)
235
+ feed.send({ type: "snapshot", snapshot: response.snapshot });
216
236
  if (shouldQuit) {
217
237
  setImmediate(() => void shutdown());
218
238
  }
@@ -224,9 +244,10 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
224
244
  return;
225
245
  shuttingDown = true;
226
246
  server.close();
247
+ feed.close();
227
248
  await session.close().catch(ignorePageGone);
228
249
  removeSessionFiles(projectDir, sessionId);
229
- process.exit(0);
250
+ onShutdown();
230
251
  }
231
252
  await new Promise((resolvePromise, reject) => {
232
253
  server.on("error", reject);
@@ -248,7 +269,7 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
248
269
  /** Entry point for the hidden `__auto-serve` CLI command (runs in the detached child). */
249
270
  export async function runAutoServeCommand(init, sessionId) {
250
271
  const session = await AutoSession.start({ ...init, sessionId });
251
- await serveSession(session, init.projectDir, sessionId, init.headless ?? true);
272
+ await serveSession(session, init.projectDir, sessionId, init.headless ?? true, () => process.exit(0));
252
273
  // Tell the parent that spawned us (spawnDetachedSession) we are serving, then let it go.
253
274
  process.send?.({ ready: true });
254
275
  process.disconnect?.();
@@ -372,3 +393,12 @@ export async function runAttachLoop(projectDir, sessionId) {
372
393
  rl.close();
373
394
  }
374
395
  }
396
+ /** This package's version, for `hello` (the file sits three levels below the package root in src/ and dist/). */
397
+ function waygraphVersion() {
398
+ try {
399
+ return JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "package.json"), "utf8")).version;
400
+ }
401
+ catch {
402
+ return "unknown";
403
+ }
404
+ }
@@ -0,0 +1,127 @@
1
+ import type { PilotHighlightFixtures } from "../../demo/overlay/pilot/fixtures.js";
2
+ import type { RunEvent } from "../../engine/run/events.js";
3
+ import type { ApplyPathResult, ConsoleLogEntry, DomSnapshot, InspectDomOptions, SessionSnapshot, StorageSnapshot, StubFileKind, TraceStep } from "./helpers.js";
4
+ import type { SessionRead } from "./read.js";
5
+ /** Bumped when a request or response changes shape (adding an op or an optional field does not). */
6
+ export declare const SESSION_PROTOCOL_VERSION = 1;
7
+ /** Every request a session answers. */
8
+ export type SessionRequest = {
9
+ op: "hello";
10
+ } | {
11
+ op: "subscribe";
12
+ } | {
13
+ op: "status";
14
+ } | {
15
+ op: "send";
16
+ pick: string;
17
+ } | ({
18
+ op: "dom";
19
+ } & InspectDomOptions) | {
20
+ op: "trace";
21
+ } | {
22
+ op: "click";
23
+ selector: string;
24
+ } | {
25
+ op: "type";
26
+ selector: string;
27
+ text: string;
28
+ } | {
29
+ op: "press";
30
+ selector: string;
31
+ key: string;
32
+ } | {
33
+ op: "goto";
34
+ url: string;
35
+ } | {
36
+ op: "reload";
37
+ } | {
38
+ op: "reach";
39
+ checkpoint: string;
40
+ } | {
41
+ op: "resync";
42
+ } | {
43
+ op: "console";
44
+ } | {
45
+ op: "storage";
46
+ } | {
47
+ op: "read";
48
+ path?: string;
49
+ } | {
50
+ op: "upload";
51
+ selector: string;
52
+ stub: StubFileKind | {
53
+ filePath: string;
54
+ };
55
+ } | ({
56
+ op: "highlight";
57
+ } & PilotHighlightFixtures);
58
+ export type StatusOrSendResponse = {
59
+ ok: true;
60
+ snapshot: SessionSnapshot;
61
+ quit: boolean;
62
+ } | {
63
+ ok: false;
64
+ error: string;
65
+ };
66
+ export type DomResponse = {
67
+ ok: true;
68
+ snapshot: DomSnapshot;
69
+ } | {
70
+ ok: false;
71
+ error: string;
72
+ };
73
+ export type TraceResponse = {
74
+ ok: true;
75
+ trace: TraceStep[];
76
+ } | {
77
+ ok: false;
78
+ error: string;
79
+ };
80
+ export type ConsoleResponse = {
81
+ ok: true;
82
+ consoleLog: ConsoleLogEntry[];
83
+ } | {
84
+ ok: false;
85
+ error: string;
86
+ };
87
+ export type StorageResponse = ({
88
+ ok: true;
89
+ } & StorageSnapshot) | {
90
+ ok: false;
91
+ error: string;
92
+ };
93
+ export type ReachResponse = ApplyPathResult;
94
+ export type ReadResponse = ({
95
+ ok: true;
96
+ } & SessionRead) | {
97
+ ok: false;
98
+ error: string;
99
+ };
100
+ export type HighlightResponse = {
101
+ ok: true;
102
+ painted: number;
103
+ missing: string[];
104
+ snapshot: SessionSnapshot;
105
+ } | {
106
+ ok: false;
107
+ error: string;
108
+ };
109
+ /** What `hello` answers: the protocol a client is talking to, and where the session is (`null` until a status or step has located it). */
110
+ export type HelloResponse = {
111
+ ok: true;
112
+ protocol: number;
113
+ waygraph: string;
114
+ sessionId: string;
115
+ here: string | null;
116
+ };
117
+ /** One line of a `subscribe` feed: every run event in the session (steps, reads), and the snapshot after each change. */
118
+ export type SessionStreamMessage = {
119
+ type: "event";
120
+ event: RunEvent;
121
+ } | {
122
+ type: "snapshot";
123
+ snapshot: SessionSnapshot;
124
+ };
125
+ export type SessionResponse = HelloResponse | StatusOrSendResponse | DomResponse | TraceResponse | ConsoleResponse | StorageResponse | ReachResponse | ReadResponse | HighlightResponse;
126
+ /** Every op, once: the server, the CLI and `pilot` all check against this list. */
127
+ export declare const SESSION_OPS: readonly ["hello", "subscribe", "status", "send", "dom", "trace", "console", "storage", "read", "upload", "click", "type", "press", "goto", "reload", "reach", "resync", "highlight"];
@@ -0,0 +1,6 @@
1
+ /** Bumped when a request or response changes shape (adding an op or an optional field does not). */
2
+ export const SESSION_PROTOCOL_VERSION = 1;
3
+ /** Every op, once: the server, the CLI and `pilot` all check against this list. */
4
+ export const SESSION_OPS = ["hello", "subscribe", "status", "send", "dom", "trace", "console", "storage", "read", "upload", "click", "type", "press", "goto", "reload", "reach", "resync", "highlight"];
5
+ const complete = true;
6
+ void complete;
@@ -0,0 +1,25 @@
1
+ import type { Page } from "@playwright/test";
2
+ import type { MemPage } from "../../engine/mem/mem.js";
3
+ /** The answer to `waygraph pilot read <id> [path]`. */
4
+ export interface SessionRead {
5
+ page: string | null;
6
+ url: string;
7
+ read: Record<string, unknown>;
8
+ missing: string[];
9
+ notes: Record<string, string>;
10
+ readBlocks: string[];
11
+ skipped: {
12
+ block: string;
13
+ reason: string;
14
+ }[];
15
+ /** Set when no Read Block covers this page. */
16
+ hint?: string;
17
+ }
18
+ /** Reads the page the session is on (`here`); with `path`, only that part (`post.comments`). */
19
+ export declare function readSession(page: Page, mem: MemPage, here: string | null, path?: string): Promise<SessionRead>;
20
+ /** The read a status / send / reach snapshot carries: lists cut to a few items, nothing at all without Read Blocks. */
21
+ export declare function snapshotRead(page: Page, mem: MemPage, here: string | null): Promise<{
22
+ read?: Record<string, unknown>;
23
+ missing?: string[];
24
+ readNotes?: Record<string, string>;
25
+ }>;
@@ -0,0 +1,50 @@
1
+ import { readBlocksFor, readCheckpoint } from "../../engine/read/read-block.js";
2
+ /** A snapshot shows at most this many items of a list; `read <path>` returns the rest. */
3
+ const SNAPSHOT_LIST_CAP = 20;
4
+ function pick(tree, path) {
5
+ const parts = path.split(".");
6
+ let node = tree;
7
+ for (const part of parts)
8
+ node = node !== null && typeof node === "object" ? node[part] : undefined;
9
+ const out = {};
10
+ let at = out;
11
+ for (const part of parts.slice(0, -1))
12
+ at = at[part] = {};
13
+ at[parts[parts.length - 1]] = node ?? null;
14
+ return out;
15
+ }
16
+ /** Reads the page the session is on (`here`); with `path`, only that part (`post.comments`). */
17
+ export async function readSession(page, mem, here, path) {
18
+ const base = { page: here, url: page.url() };
19
+ if (readBlocksFor(here).length === 0) {
20
+ return { ...base, read: {}, missing: [], notes: {}, readBlocks: [], skipped: [], hint: "no Read Block for this page: use `dom` to look at it" };
21
+ }
22
+ const { read, blocks, skipped } = await readCheckpoint(page, mem, here);
23
+ const inPath = (p) => !path || p === path || p.startsWith(path + ".");
24
+ return {
25
+ ...base,
26
+ read: path ? pick(read.toJSON(), path) : read.toJSON(),
27
+ missing: read.missing.filter(inPath),
28
+ notes: Object.fromEntries(Object.entries(read.notes).filter(([p]) => inPath(p))),
29
+ readBlocks: blocks,
30
+ skipped,
31
+ };
32
+ }
33
+ /** The read a status / send / reach snapshot carries: lists cut to a few items, nothing at all without Read Blocks. */
34
+ export async function snapshotRead(page, mem, here) {
35
+ if (readBlocksFor(here).length === 0)
36
+ return {};
37
+ const got = await readSession(page, mem, here);
38
+ const notes = { ...got.notes };
39
+ const cap = (node, path) => {
40
+ if (Array.isArray(node)) {
41
+ if (node.length > SNAPSHOT_LIST_CAP)
42
+ notes[path] = `${SNAPSHOT_LIST_CAP} of ${node.length} shown: \`read <id> ${path}\` for all`;
43
+ return node.slice(0, SNAPSHOT_LIST_CAP);
44
+ }
45
+ if (node === null || typeof node !== "object")
46
+ return node;
47
+ return Object.fromEntries(Object.entries(node).map(([k, v]) => [k, cap(v, path ? `${path}.${k}` : k)]));
48
+ };
49
+ return { read: cap(got.read, ""), missing: got.missing, ...(Object.keys(notes).length ? { readNotes: notes } : {}) };
50
+ }
@@ -34,4 +34,5 @@ export interface WorkerOpts {
34
34
  mem: MemPage;
35
35
  leases: EdgeLeaseCoordinator;
36
36
  }
37
+ /** One worker's walk, as one run of its own (parallel workers never share a run id or step counter). */
37
38
  export declare function runTraverseWorker(opts: WorkerOpts): Promise<TraverseWorkerResult>;
@@ -1,6 +1,7 @@
1
1
  import { envString } from "../../engine/env.js";
2
2
  import { buildFlow } from "../../engine/flow/flow.js";
3
3
  import { ignorePageGone } from "../../engine/page-gone.js";
4
+ import { inRun } from "../../engine/run/events.js";
4
5
  import { detectHere } from "../auto/auto.js";
5
6
  import { buildExploreMenu } from "../auto/explore.js";
6
7
  import { memStubValue } from "../auto/mem.js";
@@ -83,7 +84,11 @@ export async function runOneBlock(engine, entry, context, page, mem) {
83
84
  clearTimeout(timer);
84
85
  }
85
86
  }
86
- export async function runTraverseWorker(opts) {
87
+ /** One worker's walk, as one run of its own (parallel workers never share a run id or step counter). */
88
+ export function runTraverseWorker(opts) {
89
+ return inRun(`traverse ${opts.traverseId} worker ${opts.workerIndex}`, () => walk(opts), (r) => r.leaf);
90
+ }
91
+ async function walk(opts) {
87
92
  const { traverseId, workerIndex, parallel, maxSteps, maxVisitsPerNode, maxVisitsPerEdge, timeoutMs, graph, library, engine, context, mem, leases, } = opts;
88
93
  let page = opts.page;
89
94
  const startUrl = opts.startUrl;
@@ -1,5 +1,7 @@
1
- import type { BlockKind, ProjectScan, ScanOptions } from "./project.js";
2
- export declare const MANIFEST_VERSION = 1;
1
+ import type { BlockKind } from "../engine/block/kind.js";
2
+ import type { ProjectScan, ScanOptions } from "./project.js";
3
+ import { type ReadShape } from "../engine/read/read-block.js";
4
+ export declare const MANIFEST_VERSION = 2;
3
5
  export interface ManifestBlock {
4
6
  /** `file#exportName`, unique across the project. */
5
7
  id: string;
@@ -33,6 +35,22 @@ export interface ManifestFlow {
33
35
  demoPace?: unknown;
34
36
  };
35
37
  }
38
+ /** A Read Block (beta): which Checkpoints it reads and the shape of what it returns. */
39
+ export interface ManifestReadBlock {
40
+ id: string;
41
+ name: string;
42
+ exportName: string;
43
+ description: string;
44
+ /** Checkpoint tags it reads on (`"*"` = every page). */
45
+ from: string[];
46
+ requires: string[];
47
+ /** Field path -> shape (`"catalog.items": [{ name: "string", price: "number" }]`); absent when `read` needs Mem to build. */
48
+ fields?: Record<string, ReadShape>;
49
+ /** Why `fields` is absent. */
50
+ fieldsError?: string;
51
+ file: string;
52
+ map?: string;
53
+ }
36
54
  export interface Manifest {
37
55
  version: number;
38
56
  project: {
@@ -43,6 +61,8 @@ export interface Manifest {
43
61
  checkpoints: string[];
44
62
  maps: string[];
45
63
  blocks: ManifestBlock[];
64
+ /** What each Checkpoint's page shows, from its Read Blocks (version 2+). */
65
+ readBlocks: ManifestReadBlock[];
46
66
  flows: ManifestFlow[];
47
67
  skipped: ProjectScan["skipped"];
48
68
  }
@@ -3,7 +3,9 @@
3
3
  import { readFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { scanProject } from "./project.js";
6
- export const MANIFEST_VERSION = 1;
6
+ import { MemPage } from "../engine/mem/mem.js";
7
+ import { readBlockShape } from "../engine/read/read-block.js";
8
+ export const MANIFEST_VERSION = 2;
7
9
  function packageVersion() {
8
10
  try {
9
11
  return JSON.parse(readFileSync(join(dirname(new URL(import.meta.url).pathname), "..", "..", "package.json"), "utf8")).version;
@@ -77,12 +79,36 @@ export function manifestFromScan(scan) {
77
79
  },
78
80
  };
79
81
  });
82
+ const readBlocks = scan.readBlocks.map((r) => {
83
+ let shape;
84
+ try {
85
+ shape = { fields: readBlockShape(r.block, new MemPage()) };
86
+ }
87
+ catch (err) {
88
+ shape = { fieldsError: err instanceof Error ? err.message : String(err) };
89
+ }
90
+ for (const t of r.block.from)
91
+ if (t !== "*")
92
+ checkpoints.add(t);
93
+ return {
94
+ id: `${r.file}#${r.exportName}`,
95
+ name: r.block.name,
96
+ exportName: r.exportName,
97
+ description: r.block.description,
98
+ from: [...r.block.from],
99
+ requires: r.block.requires.map((k) => k.name),
100
+ ...shape,
101
+ file: r.file,
102
+ ...(r.map ? { map: r.map } : {}),
103
+ };
104
+ });
80
105
  return {
81
106
  version: MANIFEST_VERSION,
82
107
  project: { name: projectName(scan.projectDir), dir: scan.projectDir, waygraph: packageVersion() },
83
108
  checkpoints: [...checkpoints].sort(),
84
109
  maps: [...maps].sort(),
85
110
  blocks,
111
+ readBlocks,
86
112
  flows,
87
113
  skipped: scan.skipped,
88
114
  };
@@ -1,6 +1,7 @@
1
1
  import type { BlocksSelect } from "./select.js";
2
+ import type { ReadBlock } from "../engine/read/read-block.js";
2
3
  import type { AnyBlock } from "./scan.js";
3
- export type BlockKind = "nav" | "page" | "assert" | "method" | "effect" | "api" | "block";
4
+ import { type BlockKind } from "../engine/block/kind.js";
4
5
  export interface ScannedBlock {
5
6
  block: AnyBlock;
6
7
  name: string;
@@ -34,9 +35,17 @@ export interface BlockEntry {
34
35
  kind: "nav" | "action";
35
36
  description: string;
36
37
  }
38
+ /** A Read Block found in a scanned file (beta). */
39
+ export interface ScannedReadBlock {
40
+ block: ReadBlock;
41
+ exportName: string;
42
+ file: string;
43
+ map?: string;
44
+ }
37
45
  export interface ProjectScan {
38
46
  projectDir: string;
39
47
  blocks: ScannedBlock[];
48
+ readBlocks: ScannedReadBlock[];
40
49
  flows: ScannedFlow[];
41
50
  skipped: {
42
51
  block: string;
@@ -2,17 +2,9 @@
2
2
  import { relative } from "node:path";
3
3
  import { matchBlocksSelect } from "./select.js";
4
4
  import { exploreRoots, injectTaggedFile } from "./roots.js";
5
- import { blockKind, importFresh, isBlockLike, isFlowLike, walkDir } from "./scan.js";
5
+ import { importFresh, isBlockLike, isFlowLike, isReadBlockLike, walkDir } from "./scan.js";
6
6
  import { installedMapPackages, mapPackageBlockFiles } from "./maps.js";
7
- function kindOf(block) {
8
- const kind = blockKind(block);
9
- if (kind === "nav" || kind === "page" || kind === "assert" || kind === "api")
10
- return kind;
11
- const salt = block.__waygraphSalt;
12
- if (salt === "effect" || salt === "method" || salt === "api")
13
- return salt;
14
- return "block";
15
- }
7
+ import { blockKindOf } from "../engine/block/kind.js";
16
8
  /** A file that fails to import is left out of the scan, and said so: a silent skip reads as "nothing there". */
17
9
  export function reportImportError(relFile, err) {
18
10
  const msg = err instanceof Error ? err.message : String(err);
@@ -23,6 +15,7 @@ export async function scanProject(projectDir, opts = {}) {
23
15
  const select = opts.blocksSelect;
24
16
  const onError = opts.onImportError ?? reportImportError;
25
17
  const blocks = [];
18
+ const readBlocks = [];
26
19
  const flows = [];
27
20
  const skipped = [];
28
21
  const scanBlockFile = async (absFile, rel, file, map) => {
@@ -35,6 +28,10 @@ export async function scanProject(projectDir, opts = {}) {
35
28
  return;
36
29
  }
37
30
  for (const [exportName, exported] of Object.entries(mod)) {
31
+ if (isReadBlockLike(exported)) {
32
+ readBlocks.push({ block: exported, exportName, file, ...(map ? { map } : {}) });
33
+ continue;
34
+ }
38
35
  if (!isBlockLike(exported))
39
36
  continue;
40
37
  if (select && !matchBlocksSelect(select, { relFile: file, blockName: exported.name }))
@@ -49,7 +46,7 @@ export async function scanProject(projectDir, opts = {}) {
49
46
  block: exported,
50
47
  name: exported.name,
51
48
  exportName,
52
- kind: kindOf(exported),
49
+ kind: blockKindOf(exported),
53
50
  description: typeof exported.description === "string" ? exported.description : "",
54
51
  from: [...from],
55
52
  to: [...to],
@@ -97,5 +94,5 @@ export async function scanProject(projectDir, opts = {}) {
97
94
  }
98
95
  }
99
96
  }
100
- return { projectDir, blocks, flows, skipped };
97
+ return { projectDir, blocks, readBlocks, flows, skipped };
101
98
  }
@@ -0,0 +1,2 @@
1
+ /** Imports every Read Block file of the project; returns how many loaded. */
2
+ export declare function loadReadBlocks(projectDir: string): Promise<number>;
@@ -0,0 +1,23 @@
1
+ // Loads a project's Read Blocks (`*.read.block.ts`, its own and its installed map packages'): importing one registers it,
2
+ // so every run can read the pages it lands on. Sessions get them from the full project scan; a run by flow name does not.
3
+ import { relative } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { installedMapPackages, mapPackageBlockFiles } from "./maps.js";
6
+ import { reportImportError } from "./project.js";
7
+ import { walkDir } from "./scan.js";
8
+ const READ_BLOCK = /\.read\.block\.ts$/;
9
+ /** Imports every Read Block file of the project; returns how many loaded. */
10
+ export async function loadReadBlocks(projectDir) {
11
+ const files = [...walkDir(projectDir, READ_BLOCK), ...installedMapPackages(projectDir).flatMap(mapPackageBlockFiles).filter((f) => READ_BLOCK.test(f))];
12
+ let loaded = 0;
13
+ for (const file of files) {
14
+ try {
15
+ await import(pathToFileURL(file).href);
16
+ loaded += 1;
17
+ }
18
+ catch (err) {
19
+ reportImportError(relative(projectDir, file), err);
20
+ }
21
+ }
22
+ return loaded;
23
+ }
@@ -1,5 +1,6 @@
1
1
  import type { Block } from "../engine/block/block.js";
2
2
  import type { Checkpoint } from "../engine/checkpoint/checkpoint.js";
3
+ import type { ReadBlock } from "../engine/read/read-block.js";
3
4
  /** Every file under `dir` whose name matches, skipping `node_modules`, `dist` and `.git`. */
4
5
  export declare function walkDir(dir: string, pattern: RegExp): string[];
5
6
  /**
@@ -11,9 +12,9 @@ export type AnyBlock = Block<Checkpoint<string>, Checkpoint<string>> & {
11
12
  description?: string;
12
13
  };
13
14
  export declare function isBlockLike(val: unknown): val is AnyBlock;
15
+ /** A Read Block (`defineReadBlock`): a name, the Checkpoints it reads, and an instruction with only `read`. */
16
+ export declare function isReadBlockLike(val: unknown): val is ReadBlock;
14
17
  export declare function isFlowLike(val: unknown): val is {
15
18
  run: (...args: unknown[]) => Promise<unknown>;
16
19
  blocks: () => unknown[];
17
20
  };
18
- /** The factory kind a Block was built with (`nav`, `page`, `assert`), or undefined for Methods and Effects. */
19
- export declare function blockKind(block: object): string | undefined;
@@ -45,13 +45,17 @@ export function isBlockLike(val) {
45
45
  const instruction = obj.instruction;
46
46
  return instruction !== null && typeof instruction === "object" && typeof instruction?.act === "function";
47
47
  }
48
+ /** A Read Block (`defineReadBlock`): a name, the Checkpoints it reads, and an instruction with only `read`. */
49
+ export function isReadBlockLike(val) {
50
+ if (val === null || typeof val !== "object")
51
+ return false;
52
+ const obj = val;
53
+ const instruction = obj.instruction;
54
+ return typeof obj.name === "string" && Array.isArray(obj.from) && typeof instruction?.read === "function" && instruction.act === undefined;
55
+ }
48
56
  export function isFlowLike(val) {
49
57
  return (val !== null &&
50
58
  typeof val === "object" &&
51
59
  typeof val.run === "function" &&
52
60
  typeof val.blocks === "function");
53
61
  }
54
- /** The factory kind a Block was built with (`nav`, `page`, `assert`), or undefined for Methods and Effects. */
55
- export function blockKind(block) {
56
- return block.__waygraphKind;
57
- }