@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.
- package/LICENSE +202 -0
- package/README.md +283 -0
- package/bin/webnav +18 -0
- package/dist/agent/loop.d.ts +59 -0
- package/dist/agent/loop.js +267 -0
- package/dist/agent/server.d.ts +60 -0
- package/dist/agent/server.js +254 -0
- package/dist/cli-help.d.ts +3 -0
- package/dist/cli-help.js +111 -0
- package/dist/cli-spec.d.ts +24 -0
- package/dist/cli-spec.js +460 -0
- package/dist/cli.d.ts +208 -0
- package/dist/cli.js +2558 -0
- package/dist/contract.d.ts +20 -0
- package/dist/contract.js +15 -0
- package/dist/creds.d.ts +46 -0
- package/dist/creds.js +126 -0
- package/dist/dashboard/server.d.ts +137 -0
- package/dist/dashboard/server.js +395 -0
- package/dist/dashboard/shell.d.ts +1 -0
- package/dist/dashboard/shell.js +1343 -0
- package/dist/explorer/analyse.d.ts +25 -0
- package/dist/explorer/analyse.js +39 -0
- package/dist/explorer/diff.d.ts +11 -0
- package/dist/explorer/diff.js +22 -0
- package/dist/explorer/draft.d.ts +81 -0
- package/dist/explorer/draft.js +1658 -0
- package/dist/explorer/fingerprint-page.d.ts +11 -0
- package/dist/explorer/fingerprint-page.js +19 -0
- package/dist/explorer/fingerprint.d.ts +14 -0
- package/dist/explorer/fingerprint.js +18 -0
- package/dist/explorer/infer.d.ts +85 -0
- package/dist/explorer/infer.js +370 -0
- package/dist/explorer/pattern-propose.d.ts +27 -0
- package/dist/explorer/pattern-propose.js +81 -0
- package/dist/explorer/patterns.d.ts +49 -0
- package/dist/explorer/patterns.js +290 -0
- package/dist/explorer/shadow.d.ts +3 -0
- package/dist/explorer/shadow.js +95 -0
- package/dist/graph/coverage.d.ts +33 -0
- package/dist/graph/coverage.js +155 -0
- package/dist/graph/edit.d.ts +47 -0
- package/dist/graph/edit.js +211 -0
- package/dist/graph/frontier.d.ts +24 -0
- package/dist/graph/frontier.js +95 -0
- package/dist/graph/seed.d.ts +23 -0
- package/dist/graph/seed.js +29 -0
- package/dist/graph/show.d.ts +8 -0
- package/dist/graph/show.js +6 -0
- package/dist/graph/teach.d.ts +8 -0
- package/dist/graph/teach.js +10 -0
- package/dist/hosted.d.ts +36 -0
- package/dist/hosted.js +69 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/mapstore/record.d.ts +128 -0
- package/dist/mapstore/record.js +224 -0
- package/dist/mapstore/schema.sql +47 -0
- package/dist/mapstore/store.d.ts +108 -0
- package/dist/mapstore/store.js +399 -0
- package/dist/mapstore/types.d.ts +87 -0
- package/dist/mapstore/types.js +29 -0
- package/dist/mcp/server.d.ts +46 -0
- package/dist/mcp/server.js +163 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +33 -0
- package/dist/playwright/adapter.d.ts +62 -0
- package/dist/playwright/adapter.js +145 -0
- package/dist/playwright/ax-adapter.d.ts +33 -0
- package/dist/playwright/ax-adapter.js +76 -0
- package/dist/playwright/capture.d.ts +2 -0
- package/dist/playwright/capture.js +10 -0
- package/dist/playwright/fingerprint.d.ts +40 -0
- package/dist/playwright/fingerprint.js +139 -0
- package/dist/playwright/profile-lock.d.ts +15 -0
- package/dist/playwright/profile-lock.js +51 -0
- package/dist/playwright/sessions.d.ts +72 -0
- package/dist/playwright/sessions.js +229 -0
- package/dist/playwright/snapshot.d.ts +10 -0
- package/dist/playwright/snapshot.js +52 -0
- package/dist/playwright/throttle.d.ts +14 -0
- package/dist/playwright/throttle.js +76 -0
- package/dist/playwright/video.d.ts +13 -0
- package/dist/playwright/video.js +32 -0
- package/dist/protocol.d.ts +49 -0
- package/dist/protocol.js +3 -0
- package/dist/recorder/agent-session.d.ts +59 -0
- package/dist/recorder/agent-session.js +230 -0
- package/dist/recorder/capture-loop.d.ts +19 -0
- package/dist/recorder/capture-loop.js +40 -0
- package/dist/recorder/coverage.d.ts +22 -0
- package/dist/recorder/coverage.js +49 -0
- package/dist/recorder/hover-probe.d.ts +35 -0
- package/dist/recorder/hover-probe.js +100 -0
- package/dist/recorder/ingest.d.ts +39 -0
- package/dist/recorder/ingest.js +138 -0
- package/dist/recorder/live-record.d.ts +41 -0
- package/dist/recorder/live-record.js +255 -0
- package/dist/recorder/live.d.ts +53 -0
- package/dist/recorder/live.js +400 -0
- package/dist/recorder/probe.d.ts +14 -0
- package/dist/recorder/probe.js +33 -0
- package/dist/recorder/replay.d.ts +60 -0
- package/dist/recorder/replay.js +401 -0
- package/dist/recorder/review.d.ts +73 -0
- package/dist/recorder/review.js +171 -0
- package/dist/recorder/snapshot-dom.d.ts +7 -0
- package/dist/recorder/snapshot-dom.js +25 -0
- package/dist/router/auth-status.d.ts +20 -0
- package/dist/router/auth-status.js +55 -0
- package/dist/router/browse.d.ts +108 -0
- package/dist/router/browse.js +214 -0
- package/dist/router/catalog.d.ts +18 -0
- package/dist/router/catalog.js +6 -0
- package/dist/router/extract-content.d.ts +13 -0
- package/dist/router/extract-content.js +87 -0
- package/dist/router/live-extension-browser.d.ts +35 -0
- package/dist/router/live-extension-browser.js +175 -0
- package/dist/router/path.d.ts +7 -0
- package/dist/router/path.js +55 -0
- package/dist/router/read.d.ts +26 -0
- package/dist/router/read.js +30 -0
- package/dist/router/readiness.d.ts +9 -0
- package/dist/router/readiness.js +55 -0
- package/dist/router/replay.d.ts +14 -0
- package/dist/router/replay.js +23 -0
- package/dist/router/resolve.d.ts +21 -0
- package/dist/router/resolve.js +33 -0
- package/dist/router/search-live.d.ts +21 -0
- package/dist/router/search-live.js +98 -0
- package/dist/router/search-providers.d.ts +5 -0
- package/dist/router/search-providers.js +12 -0
- package/dist/router/search.d.ts +6 -0
- package/dist/router/search.js +62 -0
- package/dist/router/suite.d.ts +69 -0
- package/dist/router/suite.js +226 -0
- package/dist/router/tokens.d.ts +13 -0
- package/dist/router/tokens.js +26 -0
- package/dist/router/walk-live.d.ts +57 -0
- package/dist/router/walk-live.js +308 -0
- package/dist/router/walk-session.d.ts +49 -0
- package/dist/router/walk-session.js +90 -0
- package/dist/router/walk.d.ts +48 -0
- package/dist/router/walk.js +384 -0
- package/package.json +59 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { parseSnapshot } from '../playwright/snapshot.js';
|
|
2
|
+
// Host SUFFIXES that are search chrome / mirrors / license boilerplate, not real
|
|
3
|
+
// results. Suffix-matched so the whole Marginalia family (search.marginalia.nu,
|
|
4
|
+
// about.marginalia-search.com), archive mirrors, the engine's footer license
|
|
5
|
+
// link (creativecommons.org), and Wiby's own nav/footer (wiby.me) are all
|
|
6
|
+
// excluded — same shape across all providers we fan out across.
|
|
7
|
+
const EXCLUDED_HOST_SUFFIXES = [
|
|
8
|
+
'marginalia.nu', 'marginalia-search.com', 'web.archive.org', 'archive.org', 'creativecommons.org', 'wiby.me',
|
|
9
|
+
];
|
|
10
|
+
function isExcludedHost(host) {
|
|
11
|
+
const h = host.toLowerCase();
|
|
12
|
+
return EXCLUDED_HOST_SUFFIXES.some((s) => h === s || h.endsWith('.' + s) || h.includes(s));
|
|
13
|
+
}
|
|
14
|
+
// The engine's own footer/header links can point off-site (e.g. its source repo
|
|
15
|
+
// at github.com/MarginaliaSearch). Exclude such chrome URLs by substring.
|
|
16
|
+
const EXCLUDED_URL_SUBSTRINGS = ['github.com/MarginaliaSearch', '/MarginaliaSearch/'];
|
|
17
|
+
/**
|
|
18
|
+
* A real search RESULT has a descriptive multi-word TITLE; search-engine chrome
|
|
19
|
+
* links are short nav labels ("About", "git repository", "CC-BY-SA 4.0"). Require
|
|
20
|
+
* >=3 whitespace-separated words to separate the dozens of real result titles
|
|
21
|
+
* from the handful of chrome links that otherwise pass host filtering.
|
|
22
|
+
* (Live-found bug: those 3 chrome links were leaking in as "results".)
|
|
23
|
+
*/
|
|
24
|
+
function looksLikeResultTitle(name) {
|
|
25
|
+
return name.trim().split(/\s+/).length >= 3;
|
|
26
|
+
}
|
|
27
|
+
/** Parse external result links from a Marginalia results snapshot. Pure. */
|
|
28
|
+
export function parseSearchResults(resultsYaml, limit) {
|
|
29
|
+
const nodes = parseSnapshot(resultsYaml);
|
|
30
|
+
const results = [];
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
for (const n of nodes) {
|
|
33
|
+
if (n.role !== 'link')
|
|
34
|
+
continue;
|
|
35
|
+
const url = n.url;
|
|
36
|
+
if (!url || !url.startsWith('http'))
|
|
37
|
+
continue;
|
|
38
|
+
const name = n.name?.trim();
|
|
39
|
+
if (!name)
|
|
40
|
+
continue;
|
|
41
|
+
if (name === url)
|
|
42
|
+
continue; // bare-url duplicate link
|
|
43
|
+
if (!looksLikeResultTitle(name))
|
|
44
|
+
continue; // chrome nav label, not a result title
|
|
45
|
+
if (EXCLUDED_URL_SUBSTRINGS.some((s) => url.includes(s)))
|
|
46
|
+
continue;
|
|
47
|
+
let host;
|
|
48
|
+
try {
|
|
49
|
+
host = new URL(url).host;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (isExcludedHost(host))
|
|
55
|
+
continue;
|
|
56
|
+
if (seen.has(url))
|
|
57
|
+
continue;
|
|
58
|
+
seen.add(url);
|
|
59
|
+
results.push({ title: name, url });
|
|
60
|
+
}
|
|
61
|
+
return results.slice(0, limit);
|
|
62
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { MapStore } from '../mapstore/store.js';
|
|
2
|
+
import type { State } from '../mapstore/types.js';
|
|
3
|
+
import type { WalkBrowser } from './walk.js';
|
|
4
|
+
export interface CheckpointAssert {
|
|
5
|
+
repertoireContains?: string[];
|
|
6
|
+
kinds?: Record<string, string>;
|
|
7
|
+
snapshotContains?: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface CaseExpect {
|
|
10
|
+
status?: string;
|
|
11
|
+
maxInteractions?: number;
|
|
12
|
+
checkpoint?: Record<string, CheckpointAssert>;
|
|
13
|
+
}
|
|
14
|
+
export interface SuiteCase {
|
|
15
|
+
name: string;
|
|
16
|
+
start: string;
|
|
17
|
+
goal: string;
|
|
18
|
+
observe?: string[];
|
|
19
|
+
expect?: CaseExpect;
|
|
20
|
+
}
|
|
21
|
+
export interface Suite {
|
|
22
|
+
site: string;
|
|
23
|
+
profile?: string;
|
|
24
|
+
cases: SuiteCase[];
|
|
25
|
+
}
|
|
26
|
+
export interface CaseResult {
|
|
27
|
+
name: string;
|
|
28
|
+
verdict: 'pass' | 'fail';
|
|
29
|
+
elapsedMs: number;
|
|
30
|
+
failure?: {
|
|
31
|
+
at: string;
|
|
32
|
+
payload: unknown;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface SuiteResult {
|
|
36
|
+
status: 'ok' | 'failed';
|
|
37
|
+
passed: number;
|
|
38
|
+
failed: number;
|
|
39
|
+
cases: CaseResult[];
|
|
40
|
+
preflight?: {
|
|
41
|
+
auth: string;
|
|
42
|
+
loginUrl?: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** A bad-suite error the CLI maps to exit 2 (config error, with a hint). */
|
|
46
|
+
export declare class SuiteConfigError extends Error {
|
|
47
|
+
}
|
|
48
|
+
/** Validate + normalize a parsed suite JSON. Throws SuiteConfigError with a hint on
|
|
49
|
+
* anything malformed — the CLI turns that into exit 2. */
|
|
50
|
+
export declare function parseSuite(raw: unknown): Suite;
|
|
51
|
+
/** Everything the runner needs to execute ONE case without knowing about live
|
|
52
|
+
* browsers — the CLI wires the live version, tests wire a fake. `openCase` yields
|
|
53
|
+
* a fresh WalkBrowser (the walk drives it) and a `close` to reap it after. */
|
|
54
|
+
export interface SuiteDeps {
|
|
55
|
+
store: MapStore;
|
|
56
|
+
states: State[];
|
|
57
|
+
preflight?: () => Promise<{
|
|
58
|
+
auth: string;
|
|
59
|
+
loginUrl?: string;
|
|
60
|
+
}>;
|
|
61
|
+
openCase: (startUrl: string) => Promise<{
|
|
62
|
+
browser: WalkBrowser;
|
|
63
|
+
close: () => Promise<void>;
|
|
64
|
+
}>;
|
|
65
|
+
onProgress?: (line: string) => void;
|
|
66
|
+
}
|
|
67
|
+
/** Run a whole suite: auth pre-flight ONCE, then every case serially with a fresh
|
|
68
|
+
* session each. Fails fast on a needs-login pre-flight. */
|
|
69
|
+
export declare function runSuite(suite: Suite, deps: SuiteDeps): Promise<SuiteResult>;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { walkRoute } from './walk.js';
|
|
2
|
+
import { findPath } from './path.js';
|
|
3
|
+
/** A bad-suite error the CLI maps to exit 2 (config error, with a hint). */
|
|
4
|
+
export class SuiteConfigError extends Error {
|
|
5
|
+
}
|
|
6
|
+
/** Validate + normalize a parsed suite JSON. Throws SuiteConfigError with a hint on
|
|
7
|
+
* anything malformed — the CLI turns that into exit 2. */
|
|
8
|
+
export function parseSuite(raw) {
|
|
9
|
+
if (typeof raw !== 'object' || raw === null)
|
|
10
|
+
throw new SuiteConfigError('suite must be a JSON object');
|
|
11
|
+
const o = raw;
|
|
12
|
+
if (typeof o.site !== 'string' || !o.site)
|
|
13
|
+
throw new SuiteConfigError('suite.site (the host) is required');
|
|
14
|
+
if (!Array.isArray(o.cases) || o.cases.length === 0)
|
|
15
|
+
throw new SuiteConfigError('suite.cases must be a non-empty array');
|
|
16
|
+
const cases = o.cases.map((c, i) => {
|
|
17
|
+
if (typeof c !== 'object' || c === null)
|
|
18
|
+
throw new SuiteConfigError(`cases[${i}] must be an object`);
|
|
19
|
+
const cc = c;
|
|
20
|
+
if (typeof cc.name !== 'string' || !cc.name)
|
|
21
|
+
throw new SuiteConfigError(`cases[${i}].name is required`);
|
|
22
|
+
if (typeof cc.start !== 'string' || !cc.start)
|
|
23
|
+
throw new SuiteConfigError(`cases[${i}] (${cc.name ?? i}).start is required`);
|
|
24
|
+
if (typeof cc.goal !== 'string' || !cc.goal)
|
|
25
|
+
throw new SuiteConfigError(`cases[${i}] (${cc.name ?? i}).goal is required`);
|
|
26
|
+
const expect = (cc.expect ?? {});
|
|
27
|
+
if (expect.maxInteractions !== undefined && (typeof expect.maxInteractions !== 'number' || expect.maxInteractions < 0))
|
|
28
|
+
throw new SuiteConfigError(`cases[${i}] (${cc.name}).expect.maxInteractions must be a non-negative number`);
|
|
29
|
+
return { name: cc.name, start: cc.start, goal: cc.goal,
|
|
30
|
+
observe: Array.isArray(cc.observe) ? cc.observe : undefined, expect };
|
|
31
|
+
});
|
|
32
|
+
return { site: o.site, profile: typeof o.profile === 'string' ? o.profile : undefined, cases };
|
|
33
|
+
}
|
|
34
|
+
// Resolve a label (state id OR bare semanticName) to a concrete state id in the
|
|
35
|
+
// site's states. Mirrors the CLI's --start/--goal/--observe resolution so a suite
|
|
36
|
+
// author can write a bare state name (e.g. "report-list") instead of the fully
|
|
37
|
+
// qualified id ("<site>:report-list").
|
|
38
|
+
function resolveState(states, label) {
|
|
39
|
+
if (states.some((s) => s.id === label))
|
|
40
|
+
return label;
|
|
41
|
+
const byName = states.find((s) => s.semanticName === label || s.semanticName === label.split(':').pop());
|
|
42
|
+
if (byName)
|
|
43
|
+
return byName.id;
|
|
44
|
+
// suite authors use the bare tail (e.g. "report-list") — match the id suffix.
|
|
45
|
+
const bySuffix = states.find((s) => s.id.endsWith(':' + label));
|
|
46
|
+
return bySuffix ? bySuffix.id : null;
|
|
47
|
+
}
|
|
48
|
+
// Flatten a repertoire (affordances + nested reveal children) to label->kind, so a
|
|
49
|
+
// checkpoint can assert against the FULL stored repertoire the state declares.
|
|
50
|
+
function flatRepertoire(affs) {
|
|
51
|
+
const m = new Map();
|
|
52
|
+
const walk = (list) => {
|
|
53
|
+
for (const a of list) {
|
|
54
|
+
if (!m.has(a.label))
|
|
55
|
+
m.set(a.label, a.kind);
|
|
56
|
+
if (a.children)
|
|
57
|
+
walk(a.children);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
walk(affs);
|
|
61
|
+
return m;
|
|
62
|
+
}
|
|
63
|
+
/** Assert one checkpoint payload against its CheckpointAssert. Returns a failure
|
|
64
|
+
* reason string, or null if all assertions hold. */
|
|
65
|
+
function assertCheckpoint(cp, a) {
|
|
66
|
+
const rep = flatRepertoire(cp.repertoire ?? []);
|
|
67
|
+
for (const label of a.repertoireContains ?? []) {
|
|
68
|
+
if (!rep.has(label))
|
|
69
|
+
return `repertoire missing "${label}" (have: ${[...rep.keys()].join(', ')})`;
|
|
70
|
+
}
|
|
71
|
+
for (const [label, kind] of Object.entries(a.kinds ?? {})) {
|
|
72
|
+
if (rep.get(label) !== kind)
|
|
73
|
+
return `"${label}" expected kind ${kind}, got ${rep.get(label) ?? 'ABSENT'}`;
|
|
74
|
+
}
|
|
75
|
+
for (const sub of a.snapshotContains ?? []) {
|
|
76
|
+
if (!cp.snapshot.includes(sub))
|
|
77
|
+
return `snapshot does not contain "${sub}"`;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
// A checkpoint id in the suite may be written as a bare label; match it to the fired
|
|
82
|
+
// checkpoint state either by exact id or by the id's tail.
|
|
83
|
+
function checkpointAssertFor(expectCp, stateId) {
|
|
84
|
+
if (!expectCp)
|
|
85
|
+
return undefined;
|
|
86
|
+
if (expectCp[stateId])
|
|
87
|
+
return expectCp[stateId];
|
|
88
|
+
for (const [k, v] of Object.entries(expectCp))
|
|
89
|
+
if (stateId.endsWith(':' + k) || stateId === k)
|
|
90
|
+
return v;
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Run one case: pathfind, walk on autopilot, auto-assert + --continue past every
|
|
95
|
+
* checkpoint, and charge every needs-* escalation against maxInteractions. Fails
|
|
96
|
+
* the case (with the pause payload) when the budget is exceeded, when a commit
|
|
97
|
+
* point is hit (needs-classification — never classified safe here), on drift, or
|
|
98
|
+
* when a checkpoint assertion or the terminal status doesn't match expect.
|
|
99
|
+
*/
|
|
100
|
+
async function runCase(c, deps) {
|
|
101
|
+
const t0 = Date.now();
|
|
102
|
+
const done = (verdict, failure) => ({ name: c.name, verdict, elapsedMs: Date.now() - t0, failure });
|
|
103
|
+
const startId = resolveState(deps.states, c.start);
|
|
104
|
+
const goalId = resolveState(deps.states, c.goal);
|
|
105
|
+
if (!startId)
|
|
106
|
+
return done('fail', { at: 'resolve', payload: `unknown start state "${c.start}"` });
|
|
107
|
+
if (!goalId)
|
|
108
|
+
return done('fail', { at: 'resolve', payload: `unknown goal state "${c.goal}"` });
|
|
109
|
+
const observe = [];
|
|
110
|
+
for (const label of c.observe ?? []) {
|
|
111
|
+
const id = resolveState(deps.states, label);
|
|
112
|
+
if (!id)
|
|
113
|
+
return done('fail', { at: 'resolve', payload: `unknown observe state "${label}"` });
|
|
114
|
+
observe.push(id);
|
|
115
|
+
}
|
|
116
|
+
const path = findPath(deps.store, startId, goalId);
|
|
117
|
+
if (!path)
|
|
118
|
+
return done('fail', { at: 'route', payload: `no route from ${startId} to ${goalId}` });
|
|
119
|
+
const expect = c.expect ?? {};
|
|
120
|
+
const wantStatus = expect.status ?? 'done';
|
|
121
|
+
const maxInteractions = expect.maxInteractions ?? 0;
|
|
122
|
+
const startState = deps.store.getState(startId);
|
|
123
|
+
const { browser, close } = await deps.openCase(startState.urlPattern || 'about:blank');
|
|
124
|
+
const observedSoFar = [];
|
|
125
|
+
let interactions = 0;
|
|
126
|
+
try {
|
|
127
|
+
let answer;
|
|
128
|
+
let from = startId;
|
|
129
|
+
// The walk loop: run walkRoute; on a checkpoint, assert then --continue; on a
|
|
130
|
+
// needs-* escalation, charge the budget and either fail or (if there were more
|
|
131
|
+
// states before the halt) there is nothing to continue with — the runner never
|
|
132
|
+
// answers navigation/classification, so any such pause ends the case.
|
|
133
|
+
// Guard against a pathological non-advancing loop with a bounded iteration cap.
|
|
134
|
+
for (let iter = 0; iter <= path.length + observe.length + 2; iter++) {
|
|
135
|
+
const res = await walkRoute({
|
|
136
|
+
goalName: 'suite:' + goalId, startStateId: from, goalStateId: goalId,
|
|
137
|
+
store: deps.store, states: deps.states, browser, path, answer,
|
|
138
|
+
observe, observed: observedSoFar,
|
|
139
|
+
});
|
|
140
|
+
answer = undefined;
|
|
141
|
+
if (res.status === 'checkpoint') {
|
|
142
|
+
observedSoFar.push(res.state);
|
|
143
|
+
const a = checkpointAssertFor(expect.checkpoint, res.state);
|
|
144
|
+
if (a) {
|
|
145
|
+
const fail = assertCheckpoint(res, a);
|
|
146
|
+
if (fail)
|
|
147
|
+
return done('fail', { at: res.state, payload: fail });
|
|
148
|
+
}
|
|
149
|
+
// auto-assert done → --continue from where it paused (relative `at` over path)
|
|
150
|
+
from = path[res.at] ?? from;
|
|
151
|
+
answer = { kind: 'continue' };
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (res.status === 'needs-navigation' || res.status === 'needs-classification') {
|
|
155
|
+
// A needs-* is the walk asking for judgment. The runner answers NONE (a
|
|
156
|
+
// release check has no judge — including the commit-point classification,
|
|
157
|
+
// which therefore ALWAYS fails a case). Charge it against the budget; a case
|
|
158
|
+
// that escalates beyond maxInteractions (default 0) is a regression signal or
|
|
159
|
+
// a bad case. Since the runner can't answer, the pause ends the case either
|
|
160
|
+
// way — but we report the budget in the failure so intent stays visible.
|
|
161
|
+
interactions++;
|
|
162
|
+
deps.onProgress?.(` interaction ${interactions} (budget ${maxInteractions}): ${res.status}`);
|
|
163
|
+
return done('fail', { at: interactions > maxInteractions ? 'interaction-budget' : 'needs-answer',
|
|
164
|
+
payload: { interactions, maxInteractions, pause: res } });
|
|
165
|
+
}
|
|
166
|
+
if (res.status === 'needs-auth') {
|
|
167
|
+
return done('fail', { at: 'needs-auth', payload: res });
|
|
168
|
+
}
|
|
169
|
+
// terminal: done or failed
|
|
170
|
+
if (res.status !== wantStatus) {
|
|
171
|
+
return done('fail', { at: 'terminal', payload: { expected: wantStatus, got: res } });
|
|
172
|
+
}
|
|
173
|
+
// A checkpoint on the GOAL state does NOT fire as a `checkpoint` pause — the
|
|
174
|
+
// walk halts at the goal and returns `done`, attaching the goal's live snapshot
|
|
175
|
+
// + repertoire to evidence (walkRoute's goal-state enrichment). So a start==goal
|
|
176
|
+
// observe case is asserted HERE, against the done evidence, using the same
|
|
177
|
+
// assertCheckpoint machinery (the repertoire is the stored one).
|
|
178
|
+
if (res.status === 'done') {
|
|
179
|
+
for (const obsId of observe) {
|
|
180
|
+
if (observedSoFar.includes(obsId))
|
|
181
|
+
continue;
|
|
182
|
+
const a = checkpointAssertFor(expect.checkpoint, obsId);
|
|
183
|
+
if (!a)
|
|
184
|
+
continue;
|
|
185
|
+
const goalRep = deps.store.getState(obsId)?.affordances ?? res.evidence.repertoire ?? [];
|
|
186
|
+
const synthetic = { status: 'checkpoint', at: 0, state: obsId,
|
|
187
|
+
snapshot: res.evidence.snapshot ?? '', repertoire: goalRep };
|
|
188
|
+
const fail = assertCheckpoint(synthetic, a);
|
|
189
|
+
if (fail)
|
|
190
|
+
return done('fail', { at: obsId, payload: fail });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return done('pass');
|
|
194
|
+
}
|
|
195
|
+
return done('fail', { at: 'loop', payload: 'walk did not terminate within the step budget' });
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
await close().catch(() => { });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Run a whole suite: auth pre-flight ONCE, then every case serially with a fresh
|
|
202
|
+
* session each. Fails fast on a needs-login pre-flight. */
|
|
203
|
+
export async function runSuite(suite, deps) {
|
|
204
|
+
if (deps.preflight) {
|
|
205
|
+
deps.onProgress?.(`pre-flight: checking auth for ${suite.site}...`);
|
|
206
|
+
const pf = await deps.preflight();
|
|
207
|
+
if (pf.auth === 'needs-login') {
|
|
208
|
+
deps.onProgress?.(`pre-flight FAILED: not logged in — ${pf.loginUrl ?? suite.site}`);
|
|
209
|
+
return { status: 'failed', passed: 0, failed: suite.cases.length,
|
|
210
|
+
preflight: pf,
|
|
211
|
+
cases: suite.cases.map((c) => ({ name: c.name, verdict: 'fail', elapsedMs: 0,
|
|
212
|
+
failure: { at: 'preflight', payload: `not logged in for ${suite.site} — log in by hand, then re-run (loginUrl: ${pf.loginUrl ?? 'n/a'})` } })) };
|
|
213
|
+
}
|
|
214
|
+
deps.onProgress?.(`pre-flight ok: auth=${pf.auth}`);
|
|
215
|
+
}
|
|
216
|
+
const cases = [];
|
|
217
|
+
for (const c of suite.cases) {
|
|
218
|
+
deps.onProgress?.(`case: ${c.name}`);
|
|
219
|
+
const r = await runCase(c, deps);
|
|
220
|
+
deps.onProgress?.(` → ${r.verdict}${r.failure ? ' @ ' + r.failure.at : ''} (${r.elapsedMs}ms)`);
|
|
221
|
+
cases.push(r);
|
|
222
|
+
}
|
|
223
|
+
const passed = cases.filter((c) => c.verdict === 'pass').length;
|
|
224
|
+
const failed = cases.length - passed;
|
|
225
|
+
return { status: failed === 0 ? 'ok' : 'failed', passed, failed, cases };
|
|
226
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function estimateTokens(text: string): number;
|
|
2
|
+
export interface TokenSavings {
|
|
3
|
+
/** Estimated tokens the agent would have ingested reading the raw snapshots itself. */
|
|
4
|
+
raw_snapshot_tokens: number;
|
|
5
|
+
/** Estimated tokens of the compact bundle the agent actually receives. */
|
|
6
|
+
bundle_tokens: number;
|
|
7
|
+
/** raw_snapshot_tokens - bundle_tokens (>= 0). The agent-token cost webnav absorbs. */
|
|
8
|
+
tokens_saved: number;
|
|
9
|
+
/** Rough chars-per-token divisor used for the estimate (for transparency). */
|
|
10
|
+
chars_per_token: number;
|
|
11
|
+
}
|
|
12
|
+
/** Compute the savings estimate from total raw snapshot chars vs. the bundle's serialized size. */
|
|
13
|
+
export declare function tokenSavings(rawSnapshotChars: number, bundleSerialized: string): TokenSavings;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token estimation for the cost thesis (criterion #2). The real saving webnav
|
|
3
|
+
* delivers is the calling AGENT's LLM tokens: without webnav, the agent would
|
|
4
|
+
* have to ingest every raw page snapshot into its own context and reason about
|
|
5
|
+
* navigation. webnav parses those snapshots deterministically (zero LLM) and
|
|
6
|
+
* returns a compact evidence bundle instead. The saving = tokens in the raw
|
|
7
|
+
* snapshots the agent DIDN'T read, minus the tokens in the bundle it DID receive.
|
|
8
|
+
*
|
|
9
|
+
* Estimate, not exact: ~4 characters per token is the standard rough heuristic
|
|
10
|
+
* for English/markup. We report it as an estimate, never as a precise count.
|
|
11
|
+
*/
|
|
12
|
+
const CHARS_PER_TOKEN = 4;
|
|
13
|
+
export function estimateTokens(text) {
|
|
14
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
15
|
+
}
|
|
16
|
+
/** Compute the savings estimate from total raw snapshot chars vs. the bundle's serialized size. */
|
|
17
|
+
export function tokenSavings(rawSnapshotChars, bundleSerialized) {
|
|
18
|
+
const raw = Math.ceil(rawSnapshotChars / CHARS_PER_TOKEN);
|
|
19
|
+
const bundle = estimateTokens(bundleSerialized);
|
|
20
|
+
return {
|
|
21
|
+
raw_snapshot_tokens: raw,
|
|
22
|
+
bundle_tokens: bundle,
|
|
23
|
+
tokens_saved: Math.max(0, raw - bundle),
|
|
24
|
+
chars_per_token: CHARS_PER_TOKEN,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { PlaywrightAdapter, type BrowserOpts } from '../playwright/adapter.js';
|
|
2
|
+
import { MapStore } from '../mapstore/store.js';
|
|
3
|
+
import { type WalkBrowser } from './walk.js';
|
|
4
|
+
import type { RecallResponse } from '../protocol.js';
|
|
5
|
+
/**
|
|
6
|
+
* Seed the `www.saucedemo.com` page-states + navigation edges inline (STRUCTURE
|
|
7
|
+
* ONLY, principle #6 — no credentials/names/zips; those are runtime, supplied by
|
|
8
|
+
* the live browser closure). This replaces the old hand-seeded saucedemo skeleton:
|
|
9
|
+
* saucedemo is now an agent-built `www.saucedemo.com` graph, and the multi-step
|
|
10
|
+
* walk seeds its interior inline here for the live wiring + gated e2es.
|
|
11
|
+
*
|
|
12
|
+
* Idempotent: upsertState (ON CONFLICT id) and upsertEdge (UNIQUE from,to,step).
|
|
13
|
+
*/
|
|
14
|
+
export declare function seedSaucedemoForWalk(store: MapStore): void;
|
|
15
|
+
/**
|
|
16
|
+
* Build a live WalkBrowser over a playwright adapter, resolving each edge's input
|
|
17
|
+
* slot from `inputs` at fill time. `inputs` is held only in memory here — never
|
|
18
|
+
* persisted. Shared by runWalkLive and the walk / walk-resume CLI verbs.
|
|
19
|
+
*
|
|
20
|
+
* `browserOpts` (the SAME launch opts — incl. `profile` — the caller opened
|
|
21
|
+
* `adapter` with) enables the fresh-session SSO-wall retry (design item 2):
|
|
22
|
+
* when supplied, the returned WalkBrowser gets `currentUrl`/`reopenFresh` so
|
|
23
|
+
* walkRoute can close this session and open a brand-new one under the SAME
|
|
24
|
+
* profile on a wall. Omit it (as the saucedemo dry-run wiring below does) to
|
|
25
|
+
* leave those capabilities off — the walk simply never attempts the retry.
|
|
26
|
+
*/
|
|
27
|
+
export declare function makeLiveWalkBrowser(adapter: PlaywrightAdapter, inputs: Record<string, string>, browserOpts?: BrowserOpts, initialSessionId?: string): WalkBrowser;
|
|
28
|
+
/**
|
|
29
|
+
* Live wiring for the saucedemo multi-step walk (increment W2).
|
|
30
|
+
*
|
|
31
|
+
* Drives the REAL PlaywrightAdapter against live saucedemo.com through the SAME
|
|
32
|
+
* async `walkRoute` loop the unit test exercises (zero duplicated loop logic).
|
|
33
|
+
*
|
|
34
|
+
* The walk is runtime-value-free: it only passes each edge's `acceptsInput` slot
|
|
35
|
+
* NAME to `act`. THIS closure owns the `inputs` map and resolves slot -> value(s)
|
|
36
|
+
* when filling fields (principle #6: inputs are runtime, never stored as map).
|
|
37
|
+
*
|
|
38
|
+
* Goal is `www.saucedemo.com:checkout-overview` (a PASS-THROUGH state), so the walk
|
|
39
|
+
* HALTS there and returns `done` WITHOUT ever attempting the next edge — the
|
|
40
|
+
* unclassified "Finish" commit point is never fired (principle #2).
|
|
41
|
+
*/
|
|
42
|
+
export declare function runWalkLive(inputs: Record<string, string>, dbPath?: string): Promise<RecallResponse>;
|
|
43
|
+
/**
|
|
44
|
+
* Seed the saucedemo interior WITH the post-commit `checkout-complete` state, so a
|
|
45
|
+
* walk can go all the way through the Finish commit to the "Thank you" page. The
|
|
46
|
+
* Finish affordance is commit:true → the walk still HALTS there (needs-classification)
|
|
47
|
+
* unless the agent classifies it safe (which runWalkLiveComplete does, because
|
|
48
|
+
* saucedemo is a demo with no real payment).
|
|
49
|
+
*/
|
|
50
|
+
export declare function seedSaucedemoComplete(store: MapStore): void;
|
|
51
|
+
/**
|
|
52
|
+
* Live wiring for the FULL order completion (increment R5): walk login → … →
|
|
53
|
+
* checkout-overview → (Finish commit, classified SAFE because saucedemo is a demo)
|
|
54
|
+
* → checkout-complete. Proves the resume loop fires a commit point end-to-end on a
|
|
55
|
+
* real site after an explicit agent "safe" verdict — the only path that fires a commit.
|
|
56
|
+
*/
|
|
57
|
+
export declare function runWalkLiveComplete(inputs: Record<string, string>, dbPath?: string): Promise<RecallResponse>;
|