@dikshanty94/webnav 0.2.0

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 (145) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +283 -0
  3. package/bin/webnav +18 -0
  4. package/dist/agent/loop.d.ts +59 -0
  5. package/dist/agent/loop.js +267 -0
  6. package/dist/agent/server.d.ts +60 -0
  7. package/dist/agent/server.js +254 -0
  8. package/dist/cli-help.d.ts +3 -0
  9. package/dist/cli-help.js +111 -0
  10. package/dist/cli-spec.d.ts +24 -0
  11. package/dist/cli-spec.js +460 -0
  12. package/dist/cli.d.ts +208 -0
  13. package/dist/cli.js +2558 -0
  14. package/dist/contract.d.ts +20 -0
  15. package/dist/contract.js +15 -0
  16. package/dist/creds.d.ts +46 -0
  17. package/dist/creds.js +126 -0
  18. package/dist/dashboard/server.d.ts +137 -0
  19. package/dist/dashboard/server.js +395 -0
  20. package/dist/dashboard/shell.d.ts +1 -0
  21. package/dist/dashboard/shell.js +1343 -0
  22. package/dist/explorer/analyse.d.ts +25 -0
  23. package/dist/explorer/analyse.js +39 -0
  24. package/dist/explorer/diff.d.ts +11 -0
  25. package/dist/explorer/diff.js +22 -0
  26. package/dist/explorer/draft.d.ts +81 -0
  27. package/dist/explorer/draft.js +1658 -0
  28. package/dist/explorer/fingerprint-page.d.ts +11 -0
  29. package/dist/explorer/fingerprint-page.js +19 -0
  30. package/dist/explorer/fingerprint.d.ts +14 -0
  31. package/dist/explorer/fingerprint.js +18 -0
  32. package/dist/explorer/infer.d.ts +85 -0
  33. package/dist/explorer/infer.js +370 -0
  34. package/dist/explorer/pattern-propose.d.ts +27 -0
  35. package/dist/explorer/pattern-propose.js +81 -0
  36. package/dist/explorer/patterns.d.ts +49 -0
  37. package/dist/explorer/patterns.js +290 -0
  38. package/dist/explorer/shadow.d.ts +3 -0
  39. package/dist/explorer/shadow.js +95 -0
  40. package/dist/graph/coverage.d.ts +33 -0
  41. package/dist/graph/coverage.js +155 -0
  42. package/dist/graph/edit.d.ts +47 -0
  43. package/dist/graph/edit.js +211 -0
  44. package/dist/graph/frontier.d.ts +24 -0
  45. package/dist/graph/frontier.js +95 -0
  46. package/dist/graph/seed.d.ts +23 -0
  47. package/dist/graph/seed.js +29 -0
  48. package/dist/graph/show.d.ts +8 -0
  49. package/dist/graph/show.js +6 -0
  50. package/dist/graph/teach.d.ts +8 -0
  51. package/dist/graph/teach.js +10 -0
  52. package/dist/hosted.d.ts +36 -0
  53. package/dist/hosted.js +69 -0
  54. package/dist/index.d.ts +2 -0
  55. package/dist/index.js +4 -0
  56. package/dist/mapstore/record.d.ts +128 -0
  57. package/dist/mapstore/record.js +224 -0
  58. package/dist/mapstore/schema.sql +47 -0
  59. package/dist/mapstore/store.d.ts +108 -0
  60. package/dist/mapstore/store.js +399 -0
  61. package/dist/mapstore/types.d.ts +87 -0
  62. package/dist/mapstore/types.js +29 -0
  63. package/dist/mcp/server.d.ts +46 -0
  64. package/dist/mcp/server.js +163 -0
  65. package/dist/paths.d.ts +10 -0
  66. package/dist/paths.js +33 -0
  67. package/dist/playwright/adapter.d.ts +62 -0
  68. package/dist/playwright/adapter.js +145 -0
  69. package/dist/playwright/ax-adapter.d.ts +33 -0
  70. package/dist/playwright/ax-adapter.js +76 -0
  71. package/dist/playwright/capture.d.ts +2 -0
  72. package/dist/playwright/capture.js +10 -0
  73. package/dist/playwright/fingerprint.d.ts +40 -0
  74. package/dist/playwright/fingerprint.js +139 -0
  75. package/dist/playwright/profile-lock.d.ts +15 -0
  76. package/dist/playwright/profile-lock.js +51 -0
  77. package/dist/playwright/sessions.d.ts +72 -0
  78. package/dist/playwright/sessions.js +229 -0
  79. package/dist/playwright/snapshot.d.ts +10 -0
  80. package/dist/playwright/snapshot.js +52 -0
  81. package/dist/playwright/throttle.d.ts +14 -0
  82. package/dist/playwright/throttle.js +76 -0
  83. package/dist/playwright/video.d.ts +13 -0
  84. package/dist/playwright/video.js +32 -0
  85. package/dist/protocol.d.ts +49 -0
  86. package/dist/protocol.js +3 -0
  87. package/dist/recorder/agent-session.d.ts +59 -0
  88. package/dist/recorder/agent-session.js +230 -0
  89. package/dist/recorder/capture-loop.d.ts +19 -0
  90. package/dist/recorder/capture-loop.js +40 -0
  91. package/dist/recorder/coverage.d.ts +22 -0
  92. package/dist/recorder/coverage.js +49 -0
  93. package/dist/recorder/hover-probe.d.ts +35 -0
  94. package/dist/recorder/hover-probe.js +100 -0
  95. package/dist/recorder/ingest.d.ts +39 -0
  96. package/dist/recorder/ingest.js +138 -0
  97. package/dist/recorder/live-record.d.ts +41 -0
  98. package/dist/recorder/live-record.js +255 -0
  99. package/dist/recorder/live.d.ts +53 -0
  100. package/dist/recorder/live.js +400 -0
  101. package/dist/recorder/probe.d.ts +14 -0
  102. package/dist/recorder/probe.js +33 -0
  103. package/dist/recorder/replay.d.ts +60 -0
  104. package/dist/recorder/replay.js +401 -0
  105. package/dist/recorder/review.d.ts +73 -0
  106. package/dist/recorder/review.js +171 -0
  107. package/dist/recorder/snapshot-dom.d.ts +7 -0
  108. package/dist/recorder/snapshot-dom.js +25 -0
  109. package/dist/router/auth-status.d.ts +20 -0
  110. package/dist/router/auth-status.js +55 -0
  111. package/dist/router/browse.d.ts +108 -0
  112. package/dist/router/browse.js +214 -0
  113. package/dist/router/catalog.d.ts +18 -0
  114. package/dist/router/catalog.js +6 -0
  115. package/dist/router/extract-content.d.ts +13 -0
  116. package/dist/router/extract-content.js +87 -0
  117. package/dist/router/live-extension-browser.d.ts +35 -0
  118. package/dist/router/live-extension-browser.js +175 -0
  119. package/dist/router/path.d.ts +7 -0
  120. package/dist/router/path.js +55 -0
  121. package/dist/router/read.d.ts +26 -0
  122. package/dist/router/read.js +30 -0
  123. package/dist/router/readiness.d.ts +9 -0
  124. package/dist/router/readiness.js +55 -0
  125. package/dist/router/replay.d.ts +14 -0
  126. package/dist/router/replay.js +23 -0
  127. package/dist/router/resolve.d.ts +21 -0
  128. package/dist/router/resolve.js +33 -0
  129. package/dist/router/search-live.d.ts +21 -0
  130. package/dist/router/search-live.js +98 -0
  131. package/dist/router/search-providers.d.ts +5 -0
  132. package/dist/router/search-providers.js +12 -0
  133. package/dist/router/search.d.ts +6 -0
  134. package/dist/router/search.js +62 -0
  135. package/dist/router/suite.d.ts +69 -0
  136. package/dist/router/suite.js +226 -0
  137. package/dist/router/tokens.d.ts +13 -0
  138. package/dist/router/tokens.js +26 -0
  139. package/dist/router/walk-live.d.ts +57 -0
  140. package/dist/router/walk-live.js +308 -0
  141. package/dist/router/walk-session.d.ts +49 -0
  142. package/dist/router/walk-session.js +90 -0
  143. package/dist/router/walk.d.ts +48 -0
  144. package/dist/router/walk.js +384 -0
  145. package/package.json +59 -0
@@ -0,0 +1,20 @@
1
+ import type { State } from '../mapstore/types.js';
2
+ export type AuthStatus = 'valid' | 'needs-login' | 'unknown';
3
+ export interface AuthClassification {
4
+ auth: AuthStatus;
5
+ loginUrl?: string;
6
+ }
7
+ /**
8
+ * Classify a settled landing page as evidence of whether a profile is still
9
+ * logged in for `site` (a site-node id / host). Judgment-free: no site-specific
10
+ * rules, just the three honest signals from the design doc. Reused by the verb
11
+ * (Task A) and by the walk/use-navigate wall-handling escalation (Task 2).
12
+ *
13
+ * valid — the snapshot matches a known map content state (⇒ logged in and on a
14
+ * real page — this is the STRONGEST signal, so it wins over the URL-host
15
+ * heuristic below), OR the landing is on `site`'s host with no wall signal.
16
+ * needs-login — foreign-host landing, OR an interstitial/bot-wall, OR a password
17
+ * field is present. loginUrl = the landed url.
18
+ * unknown — everything else (no map yet / ambiguous landing on the right host).
19
+ */
20
+ export declare function classifyAuthLanding(landedUrl: string, snapshotYaml: string, site: string, states: State[]): AuthClassification;
@@ -0,0 +1,55 @@
1
+ import { parseSnapshot } from '../playwright/snapshot.js';
2
+ import { matchState } from '../explorer/fingerprint.js';
3
+ import { classifyReadiness } from './readiness.js';
4
+ /** Host of a URL, or null if unparseable (never throws — same posture as
5
+ * playwright/throttle.ts's hostOf). */
6
+ function hostOf(url) {
7
+ try {
8
+ return new URL(url).host;
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ /** A password field, going only by what parseSnapshot actually exposes: role
15
+ * `textbox` with an accessible name matching /password/i. playwright-cli's a11y
16
+ * snapshot carries no HTML `type=` attribute, so this is the honest signal
17
+ * available (per the design doc) — not a substitute for a real type=password read. */
18
+ function hasPasswordField(nodes) {
19
+ return nodes.some((n) => n.role === 'textbox' && /password/i.test(n.name ?? ''));
20
+ }
21
+ /**
22
+ * Classify a settled landing page as evidence of whether a profile is still
23
+ * logged in for `site` (a site-node id / host). Judgment-free: no site-specific
24
+ * rules, just the three honest signals from the design doc. Reused by the verb
25
+ * (Task A) and by the walk/use-navigate wall-handling escalation (Task 2).
26
+ *
27
+ * valid — the snapshot matches a known map content state (⇒ logged in and on a
28
+ * real page — this is the STRONGEST signal, so it wins over the URL-host
29
+ * heuristic below), OR the landing is on `site`'s host with no wall signal.
30
+ * needs-login — foreign-host landing, OR an interstitial/bot-wall, OR a password
31
+ * field is present. loginUrl = the landed url.
32
+ * unknown — everything else (no map yet / ambiguous landing on the right host).
33
+ */
34
+ export function classifyAuthLanding(landedUrl, snapshotYaml, site, states) {
35
+ const nodes = parseSnapshot(snapshotYaml);
36
+ // MATCH FIRST — a snapshot that matches a known content state (non-empty fingerprint,
37
+ // every token present; matchState never matches a bare login/shell page) is direct
38
+ // proof the profile is logged in and on a real page. It OVERRIDES the URL-host
39
+ // heuristic below: `landedUrl` can be a stale/withheld reading from the driver (e.g.
40
+ // the extension's chrome.tabs.get returns '' or a mid-navigation host), which used to
41
+ // trip `foreignHost` and fire a false `needs-login` on an authed SPA. The page itself
42
+ // is the honest evidence; trust it over the address bar.
43
+ if (matchState(nodes, states).status === 'matched')
44
+ return { auth: 'valid' };
45
+ const landedHost = hostOf(landedUrl);
46
+ const foreignHost = landedHost !== null && landedHost !== site;
47
+ if (foreignHost)
48
+ return { auth: 'needs-login', loginUrl: landedUrl };
49
+ if (classifyReadiness(snapshotYaml) === 'interstitial') {
50
+ return { auth: 'needs-login', loginUrl: landedUrl };
51
+ }
52
+ if (hasPasswordField(nodes))
53
+ return { auth: 'needs-login', loginUrl: landedUrl };
54
+ return { auth: 'unknown' };
55
+ }
@@ -0,0 +1,108 @@
1
+ import type { RecordStore } from '../mapstore/record.js';
2
+ import type { ActionRef } from '../mapstore/record.js';
3
+ import type { State } from '../mapstore/types.js';
4
+ export interface BrowseAdapter {
5
+ open(url: string): Promise<string>;
6
+ evalJs?(func: string, ref?: string): Promise<string>;
7
+ network?(): Promise<string>;
8
+ goBack?(): Promise<string>;
9
+ reload?(): Promise<string>;
10
+ snapshot?(): Promise<string>;
11
+ act?(ref: string): Promise<void>;
12
+ fill?(ref: string, text: string): Promise<string | void>;
13
+ currentUrl?(): Promise<string>;
14
+ close(): Promise<string>;
15
+ }
16
+ export type EvalResponse = {
17
+ status: 'done';
18
+ url: string;
19
+ value: string;
20
+ } | {
21
+ status: 'failed';
22
+ url: string;
23
+ reason: string;
24
+ };
25
+ export type NetworkResponse = {
26
+ status: 'done';
27
+ url: string;
28
+ requests: string;
29
+ } | {
30
+ status: 'failed';
31
+ url: string;
32
+ reason: string;
33
+ };
34
+ /** Open url, run a `() => value` JS expression in the page, return the value. */
35
+ export declare function runEval(url: string, func: string, adapter?: BrowseAdapter): Promise<EvalResponse>;
36
+ /**
37
+ * playwright-cli's `eval` prints the value inside a `### Result` block followed
38
+ * by `### Ran Playwright code` / `### Page` chrome. Extract just the value (and
39
+ * JSON-decode it if it's a quoted scalar) so the agent gets the answer, not the
40
+ * wrapper. Falls back to the trimmed raw output if no Result block is present
41
+ * (e.g. a fake/bare value in tests).
42
+ */
43
+ export declare function parseEvalResult(raw: string): string;
44
+ /** Open url, return the network requests the page issued (the API calls behind the DOM). */
45
+ export declare function runNetwork(url: string, adapter?: BrowseAdapter): Promise<NetworkResponse>;
46
+ /** Bounded settle: retry while the snapshot classifies as 'loading' (3x700ms) so a
47
+ * client-side redirect/late render is never captured as the page (the ghost-state
48
+ * class of bugs). Pass `first` when the caller already took the initial snapshot.
49
+ * Never infinite: a permanently-loading page proceeds after 3 retries. */
50
+ export declare function settleSnapshot(snap: () => Promise<string>, first?: string): Promise<string>;
51
+ /** X6 landing name-probe: read tooltip/aria labels off the NAMELESS icon controls of a settled
52
+ * landing. Returns undefined when the page is fully named (so nameHints stays absent — zero eval
53
+ * cost on well-named pages). Best-effort: adapter with no evalJs (a bare fake) probes nothing. */
54
+ export declare function probeLanding(adapter: {
55
+ evalJs?(js: string, ref?: string): Promise<string>;
56
+ }, toSnapshot: string): Promise<Record<string, string> | undefined>;
57
+ /** Capture + record the effect of a standalone `use navigate` (cli.ts routes here
58
+ * AFTER opening `url` on the adapter — caller owns the browser lifecycle and the
59
+ * session auto-start). Settles before reading, records requestedUrl = the url the
60
+ * agent ASKED for (toUrl may differ on a redirect — the draft's alias evidence).
61
+ * Returns toSnapshot too so the caller can classify the landing (e.g. an SSO-wall
62
+ * check, design item 2) without an extra playwright call. */
63
+ export declare function recordNavigateEffect(url: string, sessionId: string, recordStore: RecordStore, adapter: BrowseAdapter): Promise<{
64
+ toUrl: string;
65
+ toSnapshot: string;
66
+ }>;
67
+ export interface NavigateWallCheck {
68
+ authWall: boolean;
69
+ loginUrl?: string;
70
+ }
71
+ /** Design item 2 (`use navigate` half): classify a settled navigate landing as an
72
+ * SSO/login wall — SAME classifyAuthLanding oracle as `walk` and `profile-status`,
73
+ * applied to the TARGET url's own host + map states — with NO retry (a recording
74
+ * captures what actually happened, judgment-free; that's `walk`'s job). Pure
75
+ * composition, so it's unit-testable without a live browser. `requestedUrl` is the
76
+ * url the agent ASKED to navigate to (its host names the site whose map states to
77
+ * check against); an unparseable url honestly reports no wall (nothing to check). */
78
+ export declare function classifyNavigateWall(requestedUrl: string, toUrl: string, toSnapshot: string, states: State[]): NavigateWallCheck;
79
+ export interface SnapshotRecordedResult {
80
+ status: 'done' | 'failed';
81
+ url: string;
82
+ recorded: boolean;
83
+ reason?: string;
84
+ }
85
+ /** Open `url`, snapshot it, and (if `sessionId` is recording) append an
86
+ * observation. The seam that makes a webnav browse contribute to the map. */
87
+ export declare function runSnapshotRecorded(url: string, sessionId: string, recordStore: RecordStore, adapter?: BrowseAdapter): Promise<SnapshotRecordedResult>;
88
+ export interface RunActionArgs {
89
+ sessionId: string;
90
+ recordStore: RecordStore;
91
+ fromUrl: string;
92
+ fromSnapshot: string;
93
+ action: ActionRef;
94
+ text?: string;
95
+ adapter?: BrowseAdapter;
96
+ }
97
+ export interface ActionRecordedResult {
98
+ status: 'done' | 'failed';
99
+ recorded: boolean;
100
+ navigated?: boolean;
101
+ reason?: string;
102
+ stepSeq?: number | null;
103
+ }
104
+ /** Perform the agent's action, capture the after-page, record an ActionEffect.
105
+ * webnav does NOT decide what to fire — the agent supplies `action`; we record
106
+ * what changed (full before/after + diff + navigated). The caller manages the
107
+ * browser lifecycle (an action sequence reuses the session) — we do NOT close. */
108
+ export declare function runActionRecorded(args: RunActionArgs): Promise<ActionRecordedResult>;
@@ -0,0 +1,214 @@
1
+ import { PlaywrightAdapter } from '../playwright/adapter.js';
2
+ import { parseSnapshot } from '../playwright/snapshot.js';
3
+ import { fingerprintPage, declaredLinks } from '../explorer/fingerprint-page.js';
4
+ import { diffSnapshots, didNavigate } from '../explorer/diff.js';
5
+ import { classifyReadiness } from './readiness.js';
6
+ import { classifyAuthLanding } from './auth-status.js';
7
+ import { namelessInteractive, probeNames } from '../recorder/probe.js';
8
+ function newAdapter() {
9
+ return new PlaywrightAdapter(`browse-${Date.now()}`);
10
+ }
11
+ /** Open url, run a `() => value` JS expression in the page, return the value. */
12
+ export async function runEval(url, func, adapter = newAdapter()) {
13
+ try {
14
+ await adapter.open(url);
15
+ const raw = await adapter.evalJs(func);
16
+ return { status: 'done', url, value: parseEvalResult(raw) };
17
+ }
18
+ catch (e) {
19
+ return { status: 'failed', url, reason: String(e) };
20
+ }
21
+ finally {
22
+ await adapter.close().catch(() => { });
23
+ }
24
+ }
25
+ /**
26
+ * playwright-cli's `eval` prints the value inside a `### Result` block followed
27
+ * by `### Ran Playwright code` / `### Page` chrome. Extract just the value (and
28
+ * JSON-decode it if it's a quoted scalar) so the agent gets the answer, not the
29
+ * wrapper. Falls back to the trimmed raw output if no Result block is present
30
+ * (e.g. a fake/bare value in tests).
31
+ */
32
+ export function parseEvalResult(raw) {
33
+ const m = raw.match(/###\s*Result\s*\n([\s\S]*?)(?:\n###|\s*$)/);
34
+ const body = (m ? m[1] : raw).trim();
35
+ try {
36
+ const parsed = JSON.parse(body);
37
+ return typeof parsed === 'string' ? parsed : body;
38
+ }
39
+ catch {
40
+ return body;
41
+ }
42
+ }
43
+ /** Open url, return the network requests the page issued (the API calls behind the DOM). */
44
+ export async function runNetwork(url, adapter = newAdapter()) {
45
+ try {
46
+ await adapter.open(url);
47
+ const requests = (await adapter.network()).trim();
48
+ return { status: 'done', url, requests };
49
+ }
50
+ catch (e) {
51
+ return { status: 'failed', url, reason: String(e) };
52
+ }
53
+ finally {
54
+ await adapter.close().catch(() => { });
55
+ }
56
+ }
57
+ /** Bounded settle: retry while the snapshot classifies as 'loading' (3x700ms) so a
58
+ * client-side redirect/late render is never captured as the page (the ghost-state
59
+ * class of bugs). Pass `first` when the caller already took the initial snapshot.
60
+ * Never infinite: a permanently-loading page proceeds after 3 retries. */
61
+ export async function settleSnapshot(snap, first) {
62
+ let s = first ?? await snap();
63
+ for (let i = 0; i < 3 && classifyReadiness(s) === 'loading'; i++) {
64
+ await new Promise((r) => setTimeout(r, 700));
65
+ s = await snap();
66
+ }
67
+ return s;
68
+ }
69
+ /** X6 landing name-probe: read tooltip/aria labels off the NAMELESS icon controls of a settled
70
+ * landing. Returns undefined when the page is fully named (so nameHints stays absent — zero eval
71
+ * cost on well-named pages). Best-effort: adapter with no evalJs (a bare fake) probes nothing. */
72
+ export async function probeLanding(adapter, toSnapshot) {
73
+ if (!adapter.evalJs)
74
+ return undefined;
75
+ const nameless = namelessInteractive(parseSnapshot(toSnapshot));
76
+ if (nameless.length === 0)
77
+ return undefined;
78
+ const hints = await probeNames({ evalJs: adapter.evalJs.bind(adapter) }, nameless);
79
+ return Object.keys(hints).length ? hints : undefined;
80
+ }
81
+ /** Capture + record the effect of a standalone `use navigate` (cli.ts routes here
82
+ * AFTER opening `url` on the adapter — caller owns the browser lifecycle and the
83
+ * session auto-start). Settles before reading, records requestedUrl = the url the
84
+ * agent ASKED for (toUrl may differ on a redirect — the draft's alias evidence).
85
+ * Returns toSnapshot too so the caller can classify the landing (e.g. an SSO-wall
86
+ * check, design item 2) without an extra playwright call. */
87
+ export async function recordNavigateEffect(url, sessionId, recordStore, adapter) {
88
+ // Ledger the intent BEFORE settling (spec 2026-07-16): an un-stamped row honestly
89
+ // reads as dropped:unprocessed in coverage if this function has no failure branch
90
+ // of its own — the caller's try owns errors.
91
+ const led = recordStore.appendEvent(sessionId, {
92
+ source: 'agent', kind: 'navigate', descriptor: { cmd: 'navigate', url, fromUrl: url },
93
+ });
94
+ const toSnapshot = await settleSnapshot(() => adapter.snapshot());
95
+ const toUrl = adapter.currentUrl ? await adapter.currentUrl() : url;
96
+ const nameHints = await probeLanding(adapter, toSnapshot);
97
+ const stepSeq = recordStore.appendActionEffect(sessionId, {
98
+ fromUrl: url, fromSnapshot: '', action: null,
99
+ toUrl, toSnapshot, navigated: true,
100
+ diff: diffSnapshots([], parseSnapshot(toSnapshot)),
101
+ requestedUrl: url, nameHints,
102
+ });
103
+ if (led != null)
104
+ recordStore.stampEvent(sessionId, led, stepSeq != null ? 'step:' + stepSeq : 'dropped:not-recorded');
105
+ return { toUrl, toSnapshot };
106
+ }
107
+ /** Design item 2 (`use navigate` half): classify a settled navigate landing as an
108
+ * SSO/login wall — SAME classifyAuthLanding oracle as `walk` and `profile-status`,
109
+ * applied to the TARGET url's own host + map states — with NO retry (a recording
110
+ * captures what actually happened, judgment-free; that's `walk`'s job). Pure
111
+ * composition, so it's unit-testable without a live browser. `requestedUrl` is the
112
+ * url the agent ASKED to navigate to (its host names the site whose map states to
113
+ * check against); an unparseable url honestly reports no wall (nothing to check). */
114
+ export function classifyNavigateWall(requestedUrl, toUrl, toSnapshot, states) {
115
+ let site;
116
+ try {
117
+ site = new URL(requestedUrl).host;
118
+ }
119
+ catch {
120
+ return { authWall: false };
121
+ }
122
+ const { auth, loginUrl } = classifyAuthLanding(toUrl, toSnapshot, site, states);
123
+ return auth === 'needs-login' ? { authWall: true, loginUrl } : { authWall: false };
124
+ }
125
+ /** Open `url`, snapshot it, and (if `sessionId` is recording) append an
126
+ * observation. The seam that makes a webnav browse contribute to the map. */
127
+ export async function runSnapshotRecorded(url, sessionId, recordStore, adapter = newAdapter()) {
128
+ try {
129
+ await adapter.open(url);
130
+ const yml = await adapter.snapshot();
131
+ const nodes = parseSnapshot(yml);
132
+ let recorded = false;
133
+ if (recordStore.isActive(sessionId)) {
134
+ recordStore.append(sessionId, {
135
+ url, fingerprint: fingerprintPage(nodes), declaredLinks: declaredLinks(nodes),
136
+ });
137
+ recorded = true;
138
+ }
139
+ return { status: 'done', url, recorded };
140
+ }
141
+ catch (e) {
142
+ return { status: 'failed', url, recorded: false, reason: String(e) };
143
+ }
144
+ finally {
145
+ await adapter.close().catch(() => { });
146
+ }
147
+ }
148
+ /** Perform the agent's action, capture the after-page, record an ActionEffect.
149
+ * webnav does NOT decide what to fire — the agent supplies `action`; we record
150
+ * what changed (full before/after + diff + navigated). The caller manages the
151
+ * browser lifecycle (an action sequence reuses the session) — we do NOT close. */
152
+ export async function runActionRecorded(args) {
153
+ const adapter = args.adapter ?? newAdapter();
154
+ // Ledger the intent BEFORE acting (spec 2026-07-16): a failed action must still be
155
+ // on the record. NO typed text in the descriptor — this path has no secret oracle.
156
+ const led = args.recordStore.appendEvent(args.sessionId, {
157
+ source: 'agent', kind: args.text != null ? 'type' : 'click',
158
+ descriptor: { cmd: args.text != null ? 'type' : 'click', ref: args.action.ref,
159
+ role: args.action.role, name: args.action.name, url: args.fromUrl },
160
+ });
161
+ try {
162
+ if (args.action.ref) {
163
+ if (args.text != null)
164
+ await adapter.fill(args.action.ref, args.text);
165
+ else
166
+ await adapter.act(args.action.ref);
167
+ }
168
+ let toSnapshot = await adapter.snapshot();
169
+ const toUrl = adapter.currentUrl ? await adapter.currentUrl() : args.fromUrl;
170
+ const navigated = didNavigate(args.fromUrl, toUrl);
171
+ // SETTLE before using the snapshot, but ONLY when the action navigated: a
172
+ // client-side redirect/late render on the NEW page otherwise records a transient
173
+ // shell as the page (the ghost-state class of bugs). An in-page mutate/reveal has
174
+ // no such settledness concern — its snapshot IS the (possibly sparse) diff.
175
+ if (navigated)
176
+ toSnapshot = await settleSnapshot(() => adapter.snapshot(), toSnapshot);
177
+ // The clicked node's declared href = the URL the click ASKED for (observed
178
+ // evidence, judgment-free); the settled toUrl may differ when the server
179
+ // redirects the declared destination. Recording it lets the draft alias
180
+ // requestedKey→settledKey. Only a real cross-page http(s) destination
181
+ // counts: a '#'/javascript: href declares no destination, and recording one
182
+ // would alias the FROM page onto the TO page (state-merge poison).
183
+ let requestedUrl;
184
+ if (navigated && args.action.ref) {
185
+ const href = parseSnapshot(args.fromSnapshot).find((n) => n.ref === args.action.ref)?.url;
186
+ if (href) {
187
+ try {
188
+ const abs = new URL(href, args.fromUrl);
189
+ if (/^https?:$/.test(abs.protocol) && didNavigate(args.fromUrl, abs.href))
190
+ requestedUrl = abs.href;
191
+ }
192
+ catch { /* unparseable href → no requested url */ }
193
+ }
194
+ }
195
+ let recorded = false;
196
+ let stepSeq = null;
197
+ if (args.recordStore.isActive(args.sessionId)) {
198
+ stepSeq = args.recordStore.appendActionEffect(args.sessionId, {
199
+ fromUrl: args.fromUrl, fromSnapshot: args.fromSnapshot, action: args.action,
200
+ toUrl, toSnapshot, navigated, requestedUrl,
201
+ diff: diffSnapshots(parseSnapshot(args.fromSnapshot), parseSnapshot(toSnapshot)),
202
+ });
203
+ recorded = stepSeq != null;
204
+ }
205
+ if (led != null)
206
+ args.recordStore.stampEvent(args.sessionId, led, stepSeq != null ? 'step:' + stepSeq : 'dropped:not-recorded');
207
+ return { status: 'done', recorded, navigated, stepSeq };
208
+ }
209
+ catch (e) {
210
+ if (led != null)
211
+ args.recordStore.stampEvent(args.sessionId, led, 'dropped:failed:' + String(e.message ?? e).slice(0, 120));
212
+ return { status: 'failed', recorded: false, reason: String(e) };
213
+ }
214
+ }
@@ -0,0 +1,18 @@
1
+ import type { MapStore } from '../mapstore/store.js';
2
+ /**
3
+ * `list` — the table of contents. "What's on this map?" Reads the local store
4
+ * and returns every site webnav has a map for, with its state count. Pure read,
5
+ * no browser, no LLM. The agent reads this to learn which sites it can walk.
6
+ *
7
+ * (Richer per-site inspection — states, affordances, edges — is `dev graph-show`
8
+ * / `dev outline` / `dev mermaid`. `list` is just the index.)
9
+ */
10
+ export interface SiteListing {
11
+ site: string;
12
+ homeUrl: string;
13
+ states: number;
14
+ }
15
+ export interface Coverage {
16
+ sites: SiteListing[];
17
+ }
18
+ export declare function listCoverage(store: MapStore): Coverage;
@@ -0,0 +1,6 @@
1
+ export function listCoverage(store) {
2
+ const sites = store.allNodes()
3
+ .map((n) => ({ site: n.id, homeUrl: n.homeUrl, states: store.statesForNode(n.id).length }))
4
+ .sort((a, b) => a.site.localeCompare(b.site));
5
+ return { sites };
6
+ }
@@ -0,0 +1,13 @@
1
+ export interface ContentEvidence {
2
+ url: string;
3
+ text: string;
4
+ relevant: string[];
5
+ }
6
+ /**
7
+ * Extract answer-relevant readable content from a page snapshot. Zero LLM:
8
+ * pulls human-readable node names (headings, paragraphs, list items, cells,
9
+ * links), drops chrome/noise, and (if query terms given) surfaces the lines
10
+ * that contain those terms as `relevant`. The agent reasons over this compact
11
+ * text instead of the full raw snapshot.
12
+ */
13
+ export declare function extractContent(snapshotYaml: string, url: string, queryTerms?: string[]): ContentEvidence;
@@ -0,0 +1,87 @@
1
+ import { parseSnapshot } from '../playwright/snapshot.js';
2
+ // Roles whose `name` carries human-readable page content (vs. structural chrome).
3
+ // `generic`/`text` are included on purpose: real pages stash visible prose in
4
+ // generic/text nodes (confirmed against our captured snapshots).
5
+ const CONTENT_ROLES = new Set([
6
+ 'heading',
7
+ 'paragraph',
8
+ 'listitem',
9
+ 'cell',
10
+ 'link',
11
+ 'generic',
12
+ 'text',
13
+ 'button',
14
+ 'term',
15
+ 'definition',
16
+ ]);
17
+ // Pure nav boilerplate to drop. Matched case-insensitively against the full
18
+ // node name. These add no answer signal and only inflate the bundle.
19
+ const CHROME = new Set([
20
+ 'skip to content',
21
+ 'skip to main content',
22
+ 'open menu',
23
+ 'close menu',
24
+ 'menu',
25
+ 'search',
26
+ 'toggle navigation',
27
+ ]);
28
+ // Cap the compacted text so the bundle stays small — this is the whole point:
29
+ // the agent ingests this instead of the full raw snapshot. ~4000 chars with a
30
+ // little slack.
31
+ const TEXT_CAP = 4000;
32
+ // Most-relevant snippets are capped so `relevant` stays a tight shortlist.
33
+ const RELEVANT_CAP = 20;
34
+ function isChrome(name) {
35
+ const lower = name.toLowerCase().trim();
36
+ // Drop empty and single-char names (icon labels, bullets, etc.).
37
+ if (lower.length <= 1)
38
+ return true;
39
+ return CHROME.has(lower);
40
+ }
41
+ /**
42
+ * Extract answer-relevant readable content from a page snapshot. Zero LLM:
43
+ * pulls human-readable node names (headings, paragraphs, list items, cells,
44
+ * links), drops chrome/noise, and (if query terms given) surfaces the lines
45
+ * that contain those terms as `relevant`. The agent reasons over this compact
46
+ * text instead of the full raw snapshot.
47
+ */
48
+ export function extractContent(snapshotYaml, url, queryTerms) {
49
+ const nodes = parseSnapshot(snapshotYaml);
50
+ const kept = [];
51
+ for (const node of nodes) {
52
+ if (!CONTENT_ROLES.has(node.role.toLowerCase()))
53
+ continue;
54
+ const name = node.name?.trim();
55
+ if (!name)
56
+ continue;
57
+ if (isChrome(name))
58
+ continue;
59
+ // Dedupe consecutive identical lines (repeated labels, etc.). Keep order.
60
+ if (kept.length > 0 && kept[kept.length - 1] === name)
61
+ continue;
62
+ kept.push(name);
63
+ }
64
+ // Compact text, capped for bundle size.
65
+ const text = kept.join('\n').slice(0, TEXT_CAP);
66
+ // Query-relevant lines: any line containing ANY term (case-insensitive).
67
+ // NOTE (v1): terms are matched as plain substrings, so a short term like
68
+ // 'am' can match inside an unrelated word. Accepted simplification — no
69
+ // word-boundary logic for v1.
70
+ let relevant = [];
71
+ if (queryTerms && queryTerms.length > 0) {
72
+ const terms = queryTerms
73
+ .map((t) => t.toLowerCase().trim())
74
+ .filter((t) => t.length > 0);
75
+ if (terms.length > 0) {
76
+ for (const line of kept) {
77
+ const lower = line.toLowerCase();
78
+ if (terms.some((t) => lower.includes(t))) {
79
+ relevant.push(line);
80
+ if (relevant.length >= RELEVANT_CAP)
81
+ break;
82
+ }
83
+ }
84
+ }
85
+ }
86
+ return { url, text, relevant };
87
+ }
@@ -0,0 +1,35 @@
1
+ import { type AXNode } from '../playwright/ax-adapter.js';
2
+ import type { RawAXStep } from '../recorder/ingest.js';
3
+ import type { WalkBrowser } from './walk.js';
4
+ export interface AgentChannel {
5
+ getAX(): Promise<AXNode[]>;
6
+ dispatch(cmd: {
7
+ kind: 'click' | 'type';
8
+ nodeId: string;
9
+ text?: string;
10
+ }): Promise<void>;
11
+ currentUrl?(): Promise<string>;
12
+ goto?(url: string): Promise<void>;
13
+ scroll?(dy: number): Promise<void>;
14
+ }
15
+ export interface RecordingBrowser extends WalkBrowser {
16
+ /** All completed steps (fromAX/toAX raw, clickedRef = bN in fromAX). Finalizes any
17
+ * step still awaiting its post-action snapshot using the last observed AX. */
18
+ getRecordedSteps(): RawAXStep[];
19
+ }
20
+ /**
21
+ * WalkBrowser over an AgentChannel — the extension-driven counterpart to
22
+ * makeLiveWalkBrowser (walk-live.ts), so walkRoute runs UNMODIFIED against a live
23
+ * tab instead of a playwright-cli session.
24
+ *
25
+ * REF FORMAT NOTE: adaptAXTreeWithRefs assigns each node a synthetic `bN` ref
26
+ * (settled format, tests/playwright/ax-adapter.test.ts asserts /^b\d+$/). But
27
+ * parseSnapshot's REF_RE only matches `e\d+` (playwright-cli's own format) — a
28
+ * literal `bN` token would round-trip as SnapNode.ref === null, and every walk-side
29
+ * consumer (resolveStep, resolveByFingerprint) filters on `n.ref` truthy, so the
30
+ * walk could never resolve anything. Fix: re-key each node's ref to a sequential
31
+ * `eN` "walk ref" when serializing, and cache walkRef -> real {nodeId,
32
+ * backendDOMNodeId} for act() to dispatch against. This is entirely internal to
33
+ * this file — ax-adapter.ts / snapshot.ts are untouched.
34
+ */
35
+ export declare function makeLiveExtensionBrowser(channel: AgentChannel, inputs: Record<string, string>): RecordingBrowser;