waygraph 0.15.11 → 0.15.14

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 (52) hide show
  1. package/README.md +10 -8
  2. package/dist/auto-explore.d.ts +7 -6
  3. package/dist/auto-explore.js +43 -32
  4. package/dist/auto-session-ipc.d.ts +13 -0
  5. package/dist/auto-session-ipc.js +119 -7
  6. package/dist/auto-session.d.ts +16 -13
  7. package/dist/auto-session.js +72 -53
  8. package/dist/block-inject.d.ts +15 -0
  9. package/dist/block-inject.js +91 -0
  10. package/dist/browser.d.ts +15 -0
  11. package/dist/browser.js +29 -0
  12. package/dist/cli.js +322 -47
  13. package/dist/coverage-gap.d.ts +11 -0
  14. package/dist/coverage-gap.js +119 -0
  15. package/dist/graph.d.ts +1 -0
  16. package/dist/graph.js +99 -91
  17. package/dist/pilot-overlay.d.ts +7 -1
  18. package/dist/pilot-overlay.js +59 -41
  19. package/dist/pilot.d.ts +7 -2
  20. package/dist/pilot.js +34 -7
  21. package/dist/practices-check.d.ts +14 -0
  22. package/dist/practices-check.js +115 -0
  23. package/examples/saucedemo/.waygraph-auto/0b937f26.log +4 -0
  24. package/examples/saucedemo/.waygraph-auto/2b608ed3.log +0 -0
  25. package/examples/saucedemo/.waygraph-auto/2c13567b.log +0 -0
  26. package/examples/saucedemo/.waygraph-auto/2d9622a5.log +4 -0
  27. package/examples/saucedemo/.waygraph-auto/305f6e5b.log +4 -0
  28. package/examples/saucedemo/.waygraph-auto/418befbf.log +4 -0
  29. package/examples/saucedemo/.waygraph-auto/483bd763.log +4 -0
  30. package/examples/saucedemo/.waygraph-auto/48e3596a.log +0 -0
  31. package/examples/saucedemo/.waygraph-auto/4adfbe91.json +8 -0
  32. package/examples/saucedemo/.waygraph-auto/4adfbe91.log +0 -0
  33. package/examples/saucedemo/.waygraph-auto/4f0e33e7.log +4 -0
  34. package/examples/saucedemo/.waygraph-auto/5bd8fedf.log +4 -0
  35. package/examples/saucedemo/.waygraph-auto/5dde8331.log +4 -0
  36. package/examples/saucedemo/.waygraph-auto/68903903.log +0 -0
  37. package/examples/saucedemo/.waygraph-auto/78bbf43c.log +0 -0
  38. package/examples/saucedemo/.waygraph-auto/7efa646f.log +4 -0
  39. package/examples/saucedemo/.waygraph-auto/9cecb0c8.log +4 -0
  40. package/examples/saucedemo/.waygraph-auto/aae57d51.log +0 -0
  41. package/examples/saucedemo/.waygraph-auto/b61a08d8.log +0 -0
  42. package/examples/saucedemo/.waygraph-auto/c946f66c.log +0 -0
  43. package/examples/saucedemo/.waygraph-auto/cd0d5b50.log +4 -0
  44. package/examples/saucedemo/.waygraph-auto/cd5b203c.log +0 -0
  45. package/examples/saucedemo/.waygraph-auto/ea2df316.log +4 -0
  46. package/examples/saucedemo/.waygraph-auto/ee60ed24.log +0 -0
  47. package/examples/saucedemo/.waygraph-auto/f2b6cb1b.log +0 -0
  48. package/examples/saucedemo/.waygraph-auto/f392cdc8.log +0 -0
  49. package/package.json +2 -2
  50. package/templates/scaffold/package.json +1 -1
  51. package/templates/skills/waygraph-convention.skill.md +11 -1
  52. package/templates/skills/waygraph-pilot.skill.md +71 -4
package/README.md CHANGED
@@ -718,16 +718,18 @@ outright rather than left as a caveat.
718
718
 
719
719
  **On-page indicator.** A headful (`--non-headless`) session looks like an ordinary browser
720
720
  tab otherwise - no sign anything is driving it, which matters once a human might be watching
721
- or co-driving the same visible session. Every session now injects a small badge (bottom-right
721
+ or co-driving the same visible session. Every session injects a small badge (bottom-right
722
722
  corner: `Waygraph Pilot - session <id> - <Checkpoint>`) that click-expands into a panel
723
723
  listing the current live menu's real edges (block name, kind, target Checkpoint,
724
- description) - the same data `auto status` returns, rendered in the page itself. Both refresh
725
- automatically after every `send`/`reach`/`reload`/`resync`/raw action - there's nothing
726
- separate to call to keep it in sync. Particularly useful for Blind Pilot: watching the panel
727
- update as newly-authored Blocks get picked up via `reload` shows the map actually growing,
728
- live, not just a JSON diff in a terminal. Self-contained (`src/pilot-overlay.ts`), not built
729
- on `waygraph demo`'s own panel/banner system, for the same `cli.ts`-side-effect reason
730
- `pilot-v1`'s old narrate-mode ring wasn't either.
724
+ description) - the same data `auto status` returns, rendered in the page itself. The overlay
725
+ shell is installed via `context.addInitScript`, so it **persists on `about:blank`** and
726
+ survives `goto` navigations (browser start defaults to blank; pilot navigates when a base URL
727
+ exists). Headful sessions paint the badge immediately on start — no need to call `status`
728
+ first. Bottom-left activity toasts label raw primitives as **`Running (no Block): …`**
729
+ (click/type/goto/upload outside the Block library); Block runs stay **`Running (Block): …`**.
730
+ Both refresh automatically after every `send`/`reach`/`reload`/`resync`/raw action.
731
+ Self-contained (`src/pilot-overlay.ts`), not built on `waygraph demo`'s own panel/banner
732
+ system.
731
733
 
732
734
  **Pilot itself does no planning, resolving, narrating, or acting** - `pilotStart`'s only job is
733
735
  starting the session and handing back its graph in one round trip; see `src/pilot.ts`. An
@@ -1,6 +1,11 @@
1
1
  import type { Page } from "@playwright/test";
2
2
  import type { Block, Checkpoint, WaygraphInstanceOption } from "./types.js";
3
3
  import { type WaygraphGraph, type WaygraphEdge } from "./graph.js";
4
+ export interface ExploreContextOpts {
5
+ blocksSelect?: import("./blocks-select.js").BlocksSelect;
6
+ /** Absolute paths of extra Block trees to merge (host projectDir is always first). */
7
+ inject?: string[];
8
+ }
4
9
  export interface BlockEntry {
5
10
  block: Block<Checkpoint<string>, Checkpoint<string>>;
6
11
  exportName: string;
@@ -32,9 +37,7 @@ export interface ExploreMenu {
32
37
  /**
33
38
  * Import every `*.block.ts` export keyed by runtime Block `.name`.
34
39
  */
35
- export declare function loadBlockLibrary(projectDir: string, opts?: {
36
- blocksSelect?: import("./blocks-select.js").BlocksSelect;
37
- }): Promise<{
40
+ export declare function loadBlockLibrary(projectDir: string, opts?: ExploreContextOpts): Promise<{
38
41
  byName: Map<string, BlockEntry>;
39
42
  navBlocks: BlockEntry[];
40
43
  }>;
@@ -48,9 +51,7 @@ export declare function exploreChoices(graph: WaygraphGraph, here: string | null
48
51
  export declare function buildExploreMenu(page: Page, graph: WaygraphGraph, library: {
49
52
  byName: Map<string, BlockEntry>;
50
53
  }, here: string | null): Promise<ExploreMenu>;
51
- export declare function buildExploreContext(projectDir: string, opts?: {
52
- blocksSelect?: import("./blocks-select.js").BlocksSelect;
53
- }): Promise<{
54
+ export declare function buildExploreContext(projectDir: string, opts?: ExploreContextOpts): Promise<{
54
55
  graph: WaygraphGraph;
55
56
  library: Awaited<ReturnType<typeof loadBlockLibrary>>;
56
57
  }>;
@@ -1,6 +1,7 @@
1
1
  import { readdirSync, statSync } from "node:fs";
2
2
  import { join, relative, resolve } from "node:path";
3
3
  import { discoverGraph } from "./graph.js";
4
+ import { exploreRoots, injectTaggedFile } from "./block-inject.js";
4
5
  function walkDir(dir, pattern) {
5
6
  const results = [];
6
7
  let entries;
@@ -105,42 +106,52 @@ export async function loadBlockLibrary(projectDir, opts) {
105
106
  const { matchBlocksSelect } = await import("./blocks-select.js");
106
107
  const byName = new Map();
107
108
  const navBlocks = [];
109
+ const navSeen = new Set();
108
110
  const select = opts?.blocksSelect;
109
- for (const file of walkDir(projectDir, /\.block\.ts$/)) {
110
- const relFile = relative(projectDir, file).replace(/\\/g, "/");
111
- // Glob/bare: path filter before import. Regex: need block name — filter after.
112
- if (select && select.kind !== "regex" && !matchBlocksSelect(select, { relFile })) {
113
- continue;
114
- }
115
- let mod;
116
- try {
117
- mod = await importModule(file);
118
- }
119
- catch (err) {
120
- const msg = err instanceof Error ? err.message : String(err);
121
- if (/second time|playwright/i.test(msg)) {
122
- console.error(`waygraph auto: skip ${relFile}: ${msg.split("\n")[0]}`);
123
- }
124
- continue;
125
- }
126
- for (const [exportName, exported] of Object.entries(mod)) {
127
- if (!isBlockLike(exported))
111
+ for (const root of exploreRoots(projectDir, opts?.inject)) {
112
+ for (const file of walkDir(root, /\.block\.ts$/)) {
113
+ const relFile = relative(root, file).replace(/\\/g, "/");
114
+ const taggedFile = injectTaggedFile(projectDir, root, relFile);
115
+ // Glob/bare: path filter before import. Regex: need block name — filter after.
116
+ if (select && select.kind !== "regex" && !matchBlocksSelect(select, { relFile })) {
128
117
  continue;
129
- if (select &&
130
- !matchBlocksSelect(select, { relFile, blockName: exported.name })) {
118
+ }
119
+ let mod;
120
+ try {
121
+ mod = await importModule(file);
122
+ }
123
+ catch (err) {
124
+ const msg = err instanceof Error ? err.message : String(err);
125
+ if (/second time|playwright/i.test(msg)) {
126
+ console.error(`waygraph auto: skip ${relFile}: ${msg.split("\n")[0]}`);
127
+ }
131
128
  continue;
132
129
  }
133
- const kind = isNavBlockMarked(exported) ? "nav" : "action";
134
- const entry = {
135
- block: exported,
136
- exportName,
137
- file: relFile,
138
- kind,
139
- description: typeof exported.description === "string" ? exported.description : "",
140
- };
141
- byName.set(exported.name, entry);
142
- if (kind === "nav")
143
- navBlocks.push(entry);
130
+ for (const [exportName, exported] of Object.entries(mod)) {
131
+ if (!isBlockLike(exported))
132
+ continue;
133
+ if (select &&
134
+ !matchBlocksSelect(select, { relFile, blockName: exported.name })) {
135
+ continue;
136
+ }
137
+ const kind = isNavBlockMarked(exported) ? "nav" : "action";
138
+ const entry = {
139
+ block: exported,
140
+ exportName,
141
+ file: taggedFile,
142
+ kind,
143
+ description: typeof exported.description === "string" ? exported.description : "",
144
+ };
145
+ if (byName.has(exported.name)) {
146
+ console.error(`waygraph: inject skipped duplicate Block name "${exported.name}" (${taggedFile})`);
147
+ continue;
148
+ }
149
+ byName.set(exported.name, entry);
150
+ if (kind === "nav" && !navSeen.has(exported.name)) {
151
+ navSeen.add(exported.name);
152
+ navBlocks.push(entry);
153
+ }
154
+ }
144
155
  }
145
156
  }
146
157
  navBlocks.sort((a, b) => {
@@ -54,6 +54,13 @@ export type HighlightResponse = {
54
54
  ok: false;
55
55
  error: string;
56
56
  };
57
+ export declare function readSessionMeta(projectDir: string, sessionId: string): SessionMeta | null;
58
+ /**
59
+ * Resolve session metadata from a project hint (cwd) or the global registry.
60
+ * Fixes "session unreachable" when the agent calls status/send from a different
61
+ * directory or pane than the one that ran `pilot start`.
62
+ */
63
+ export declare function resolveSessionMeta(sessionId: string, projectDirHint?: string): SessionMeta | null;
57
64
  /**
58
65
  * Client-side: sends one request to a running session and returns its
59
66
  * response. Reports a stale/unreachable session as an error within
@@ -129,6 +136,12 @@ export declare function runAutoServeCommand(init: AutoSessionInit, sessionId: st
129
136
  * than re-implementing that resolution here.
130
137
  */
131
138
  export declare function spawnDetachedSession(init: AutoSessionInit, readyTimeoutMs?: number): Promise<SessionMeta>;
139
+ /** Stop one detached session (SIGTERM + cleanup metadata/socket). */
140
+ export declare function stopBrowserSession(projectDir: string, sessionId: string): boolean;
141
+ /** Stop every live session registered for a project. */
142
+ export declare function stopAllBrowserSessions(projectDir: string): number;
143
+ /** Live browser sessions for a project (metadata + socket + pid still present). */
144
+ export declare function listBrowserSessions(projectDir: string): SessionMeta[];
132
145
  /**
133
146
  * Client-only interactive loop against a running detached session - reopens
134
147
  * the same rendering/parsing the foreground `--cli` loop uses, driven purely
@@ -6,7 +6,8 @@
6
6
  */
7
7
  import { createServer, connect } from "node:net";
8
8
  import { spawn } from "node:child_process";
9
- import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, openSync, } from "node:fs";
9
+ import { createRequire } from "node:module";
10
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, rmSync, openSync, } from "node:fs";
10
11
  import { join, dirname } from "node:path";
11
12
  import { tmpdir } from "node:os";
12
13
  import { fileURLToPath } from "node:url";
@@ -20,6 +21,19 @@ function sessionDir(projectDir) {
20
21
  function metaPath(projectDir, sessionId) {
21
22
  return join(sessionDir(projectDir), `${sessionId}.json`);
22
23
  }
24
+ /** Global index so session control works even when cwd ≠ the project that spawned the session. */
25
+ function globalMetaPath(sessionId) {
26
+ return join(socketDir(), `${sessionId}.meta.json`);
27
+ }
28
+ function isPidAlive(pid) {
29
+ try {
30
+ process.kill(pid, 0);
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
23
37
  /**
24
38
  * Deliberately NOT under the project directory (unlike meta/log files) -
25
39
  * a real bug found while proving out openspec/changes/waygraph-map's own
@@ -44,7 +58,7 @@ function logPathFor(projectDir, sessionId) {
44
58
  function generateSessionId() {
45
59
  return randomBytes(4).toString("hex");
46
60
  }
47
- function readSessionMeta(projectDir, sessionId) {
61
+ export function readSessionMeta(projectDir, sessionId) {
48
62
  const p = metaPath(projectDir, sessionId);
49
63
  if (!existsSync(p))
50
64
  return null;
@@ -55,9 +69,36 @@ function readSessionMeta(projectDir, sessionId) {
55
69
  return null;
56
70
  }
57
71
  }
72
+ /**
73
+ * Resolve session metadata from a project hint (cwd) or the global registry.
74
+ * Fixes "session unreachable" when the agent calls status/send from a different
75
+ * directory or pane than the one that ran `pilot start`.
76
+ */
77
+ export function resolveSessionMeta(sessionId, projectDirHint) {
78
+ if (projectDirHint) {
79
+ const local = readSessionMeta(projectDirHint, sessionId);
80
+ if (local)
81
+ return local;
82
+ }
83
+ const globalPath = globalMetaPath(sessionId);
84
+ if (existsSync(globalPath)) {
85
+ try {
86
+ return JSON.parse(readFileSync(globalPath, "utf-8"));
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+ function writeGlobalSessionMeta(meta) {
95
+ mkdirSync(socketDir(), { recursive: true });
96
+ writeFileSync(globalMetaPath(meta.sessionId), JSON.stringify(meta, null, 2));
97
+ }
58
98
  function removeSessionFiles(projectDir, sessionId) {
59
99
  for (const p of [
60
100
  metaPath(projectDir, sessionId),
101
+ globalMetaPath(sessionId),
61
102
  socketPathFor(sessionId),
62
103
  ]) {
63
104
  try {
@@ -110,9 +151,20 @@ function readOneMessage(sock, timeoutMs) {
110
151
  });
111
152
  }
112
153
  export async function requestSession(projectDir, sessionId, request, timeoutMs = 15_000) {
113
- const meta = readSessionMeta(projectDir, sessionId);
154
+ const meta = resolveSessionMeta(sessionId, projectDir);
114
155
  if (!meta) {
115
- return { ok: false, error: `no such session "${sessionId}" (no metadata under .waygraph-auto/)` };
156
+ return {
157
+ ok: false,
158
+ error: `no such session "${sessionId}" (no metadata under ${projectDir}/.waygraph-auto/ or global registry) — ` +
159
+ "run from the project that started the session, or `waygraph pilot sessions` to list live ids",
160
+ };
161
+ }
162
+ if (!isPidAlive(meta.pid)) {
163
+ removeSessionFiles(meta.projectDir, sessionId);
164
+ return {
165
+ ok: false,
166
+ error: `session "${sessionId}" is not running (pid ${meta.pid} exited) — start a new session with pilot/browser start`,
167
+ };
116
168
  }
117
169
  return new Promise((resolvePromise) => {
118
170
  const sock = connect(meta.socketPath);
@@ -283,6 +335,7 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
283
335
  headless,
284
336
  };
285
337
  writeFileSync(metaPath(projectDir, sessionId), JSON.stringify(meta, null, 2));
338
+ writeGlobalSessionMeta(meta);
286
339
  process.on("SIGTERM", () => void shutdown());
287
340
  process.on("SIGINT", () => void shutdown());
288
341
  }
@@ -305,17 +358,31 @@ export async function runAutoServeCommand(init, sessionId) {
305
358
  export async function spawnDetachedSession(init, readyTimeoutMs = 20_000) {
306
359
  const sessionId = generateSessionId();
307
360
  mkdirSync(sessionDir(init.projectDir), { recursive: true });
308
- const launcher = join(dirname(fileURLToPath(import.meta.url)), "..", "bin", "waygraph");
361
+ const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
362
+ const require = createRequire(join(pkgRoot, "package.json"));
363
+ const tsxEsm = require.resolve("tsx/esm");
364
+ const cli = join(pkgRoot, "dist", "cli.js");
309
365
  const logFd = openSync(logPathFor(init.projectDir, sessionId), "a");
310
- const args = ["__auto-serve", init.projectDir, "--session-id", sessionId];
366
+ const args = ["--import", tsxEsm, cli, "__auto-serve", init.projectDir, "--session-id", sessionId];
311
367
  if (init.baseURL)
312
368
  args.push("--base-url", init.baseURL);
369
+ if (init.startUrl)
370
+ args.push("--goto", init.startUrl);
371
+ if (init.skipInitialNavigation)
372
+ args.push("--blank");
313
373
  if (init.headless === false)
314
374
  args.push("--non-headless");
315
- const child = spawn(process.execPath, [launcher, ...args], {
375
+ if (init.headless === true)
376
+ args.push("--headless");
377
+ for (const p of init.inject ?? []) {
378
+ args.push("--inject", p);
379
+ }
380
+ // Spawn cli.js directly — not bin/waygraph (which would fork a second Node).
381
+ const child = spawn(process.execPath, args, {
316
382
  detached: true,
317
383
  stdio: ["ignore", logFd, logFd],
318
384
  windowsHide: true,
385
+ env: process.env,
319
386
  });
320
387
  child.unref();
321
388
  const deadline = Date.now() + readyTimeoutMs;
@@ -328,6 +395,51 @@ export async function spawnDetachedSession(init, readyTimeoutMs = 20_000) {
328
395
  throw new Error(`session "${sessionId}" did not become ready within ${readyTimeoutMs}ms - ` +
329
396
  `check ${logPathFor(init.projectDir, sessionId)} for errors`);
330
397
  }
398
+ /** Stop one detached session (SIGTERM + cleanup metadata/socket). */
399
+ export function stopBrowserSession(projectDir, sessionId) {
400
+ const meta = resolveSessionMeta(sessionId, projectDir);
401
+ if (!meta)
402
+ return false;
403
+ try {
404
+ process.kill(meta.pid, "SIGTERM");
405
+ }
406
+ catch {
407
+ /* already gone */
408
+ }
409
+ removeSessionFiles(meta.projectDir, sessionId);
410
+ return true;
411
+ }
412
+ /** Stop every live session registered for a project. */
413
+ export function stopAllBrowserSessions(projectDir) {
414
+ const sessions = listBrowserSessions(projectDir);
415
+ for (const meta of sessions) {
416
+ stopBrowserSession(meta.projectDir, meta.sessionId);
417
+ }
418
+ return sessions.length;
419
+ }
420
+ /** Live browser sessions for a project (metadata + socket + pid still present). */
421
+ export function listBrowserSessions(projectDir) {
422
+ const dir = sessionDir(projectDir);
423
+ if (!existsSync(dir))
424
+ return [];
425
+ const out = [];
426
+ for (const f of readdirSync(dir)) {
427
+ if (!f.endsWith(".json"))
428
+ continue;
429
+ const id = f.slice(0, -".json".length);
430
+ const meta = readSessionMeta(projectDir, id);
431
+ if (!meta)
432
+ continue;
433
+ if (!existsSync(socketPathFor(id)))
434
+ continue;
435
+ if (!isPidAlive(meta.pid)) {
436
+ removeSessionFiles(projectDir, id);
437
+ continue;
438
+ }
439
+ out.push(meta);
440
+ }
441
+ return out.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
442
+ }
331
443
  function printSnapshotMenu(snapshot) {
332
444
  console.log("");
333
445
  console.log(`You are here: ${snapshot.here ?? "Unknown (pick Start here)"}`);
@@ -58,6 +58,10 @@ export interface AutoSessionInit {
58
58
  baseURL?: string;
59
59
  startUrl?: string;
60
60
  blocksSelect?: import("./blocks-select.js").BlocksSelect;
61
+ /** Absolute paths — merged Block libraries (see `waygraph browser --inject`). */
62
+ inject?: string[];
63
+ /** When true, open about:blank instead of baseURL/startUrl (browser default). */
64
+ skipInitialNavigation?: boolean;
61
65
  /** Default true. false launches a real visible browser window. */
62
66
  headless?: boolean;
63
67
  /**
@@ -175,6 +179,7 @@ export interface StorageSnapshot {
175
179
  */
176
180
  export declare class AutoSession {
177
181
  private readonly projectDir;
182
+ private readonly inject;
178
183
  private readonly blocksSelect;
179
184
  private graph;
180
185
  private library;
@@ -189,6 +194,9 @@ export declare class AutoSession {
189
194
  private lastRunNote;
190
195
  private readonly trace;
191
196
  private readonly consoleLog;
197
+ private readonly knownInteractionSelectors;
198
+ private readonly headless;
199
+ private lastOverlayInfo;
192
200
  private constructor();
193
201
  static start(init: AutoSessionInit): Promise<AutoSession>;
194
202
  /** Pure getter - returns a copy, not the live array. */
@@ -198,21 +206,16 @@ export declare class AutoSession {
198
206
  getConsoleLog(): ConsoleLogEntry[];
199
207
  private pushConsoleLog;
200
208
  private currentMenu;
209
+ /** Paint the overlay on about:blank / immediately after session start (headful browser). */
210
+ private bootstrapOverlay;
211
+ /** Re-apply cached overlay state after navigation (init script restores shell only). */
212
+ private repaintOverlayAfterNavigation;
201
213
  /**
202
- * Real, direct user request: a Blind Pilot agent had no way to ask "which
203
- * of the links visible on THIS page are already covered by a NavBlock" -
204
- * only manual DOM inspection + guessing hrefs, exactly what this session
205
- * had been doing by hand the whole time this feature was built. Compares
206
- * every same-origin `<a href>` on the page against every NavBlock's own
207
- * static url (`__waygraphNavUrl` - see `defineNavBlock`'s own comment for
208
- * why only plain-string urls qualify), and `console.warn()`s each pathname
209
- * with no NavBlock at all - surfaced through the same real console capture
210
- * `auto console` already reads, not a separate channel. Deduped per path
211
- * per page load (a `Set` living on `window`, so it survives repeated
212
- * `currentMenu()` calls but resets on a real navigation) - once is enough
213
- * to flag a gap, repeating it on every status/send call would just be noise.
214
+ * Flags same-origin `<a href>` paths and visible buttons with no NavBlock
215
+ * URL / Block selector in the loaded library — read via `auto console`.
216
+ * Deduped per page load so repeated status/send calls stay quiet.
214
217
  */
215
- private warnUnmappedLinks;
218
+ private warnUnmappedInteractions;
216
219
  /** Pure getter - no Block runs, no mem/page mutation. */
217
220
  currentSnapshot(): Promise<SessionSnapshot>;
218
221
  /**