waygraph 0.12.6 → 0.12.8
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/README.md +2 -1
- package/dist/agent-dive.d.ts +19 -0
- package/dist/agent-dive.js +162 -0
- package/dist/auto-explore-run.d.ts +5 -0
- package/dist/auto-explore-run.js +15 -5
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +152 -1
- package/dist/traverse-run.d.ts +24 -0
- package/dist/traverse-run.js +227 -0
- package/package.json +1 -1
- package/templates/agents/waygraph-author.agent.md +36 -0
- package/templates/agents/waygraph-healer.agent.md +33 -0
- package/templates/agents/waygraph-planner.agent.md +35 -0
- package/templates/scaffold/README.md +11 -8
- package/templates/scaffold/STRUCTURE.md +42 -17
- package/templates/scaffold/package.json +13 -8
- package/templates/scaffold/playwright.config.ts +15 -0
- package/templates/scaffold/scripts/fixture-server.mjs +41 -0
- package/templates/scaffold/scripts/with-fixture.mjs +69 -0
- package/templates/scaffold/src/blocks/SITE-MAP.md +5 -6
- package/templates/scaffold/src/blocks/demo-web/NAV.md +17 -8
- package/templates/scaffold/src/blocks/demo-web/demo-sel.ts +14 -0
- package/templates/scaffold/src/blocks/demo-web/home.html +65 -0
- package/templates/scaffold/src/blocks/demo-web/home.page.block.ts +29 -0
- package/templates/scaffold/src/blocks/demo-web/methods/add-item.effect.block.ts +68 -0
- package/templates/scaffold/src/blocks/demo-web/methods/assert-hello.method.block.ts +11 -12
- package/templates/scaffold/src/blocks/demo-web/methods/clear-cart.method.block.ts +26 -0
- package/templates/scaffold/src/blocks/demo-web/methods/remove-item.effect.block.ts +63 -0
- package/templates/scaffold/src/blocks/demo-web/nav-home.block.ts +5 -8
- package/templates/scaffold/src/flows/example.flow.ts +3 -4
- package/templates/scaffold/src/flows/shop.flow.ts +42 -0
- package/templates/scaffold/src/states/demo.mem-keys.ts +7 -0
- package/templates/scaffold/src/states/demo.states.ts +2 -0
- package/templates/scaffold/tests/example.spec.ts +9 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase B: serial graph traverse (RFC traverse-ffcompose).
|
|
3
|
+
* Walk unused legal edges until leaf / budget / break. No parallel yet.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { Engine, start, end, locate } from "./engine.js";
|
|
8
|
+
import { MemPage } from "./mem-page.js";
|
|
9
|
+
import { buildExploreContext, buildExploreMenu, } from "./auto-explore.js";
|
|
10
|
+
import { defaultMemValueForKey } from "./auto-explore-run.js";
|
|
11
|
+
function resolveBaseUrl(projectDir) {
|
|
12
|
+
if (process.env.WAYGRAPH_BASE_URL)
|
|
13
|
+
return process.env.WAYGRAPH_BASE_URL;
|
|
14
|
+
const pkgPath = join(projectDir, "package.json");
|
|
15
|
+
if (!existsSync(pkgPath))
|
|
16
|
+
return undefined;
|
|
17
|
+
try {
|
|
18
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
19
|
+
return pkg.waygraph?.baseUrl;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function seedMem(library, mem, dataJson) {
|
|
26
|
+
const raw = dataJson ?? process.env.WAYGRAPH_DATA ?? process.env.WAYGRAPH_AUTO_MEM;
|
|
27
|
+
let envParsed = {};
|
|
28
|
+
if (raw) {
|
|
29
|
+
try {
|
|
30
|
+
envParsed = JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new Error(`waygraph traverse: --data / WAYGRAPH_DATA is not valid JSON`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const entry of library.values()) {
|
|
37
|
+
for (const k of entry.block.requires ?? []) {
|
|
38
|
+
if (mem.has(k))
|
|
39
|
+
continue;
|
|
40
|
+
if (k.name in envParsed) {
|
|
41
|
+
mem.set(k, envParsed[k.name]);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const def = defaultMemValueForKey(k.name);
|
|
45
|
+
if (def !== undefined)
|
|
46
|
+
mem.set(k, def);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function missingMemKeys(entry, mem) {
|
|
51
|
+
return (entry.block.requires ?? []).filter((k) => !mem.has(k));
|
|
52
|
+
}
|
|
53
|
+
function ensureMem(entry, mem) {
|
|
54
|
+
const missing = missingMemKeys(entry, mem);
|
|
55
|
+
if (missing.length === 0)
|
|
56
|
+
return;
|
|
57
|
+
throw new Error(`Block "${entry.block.name}" needs Mem keys: ${missing.map((k) => k.name).join(", ")} ` +
|
|
58
|
+
`(pass --data '{...}' or WAYGRAPH_DATA)`);
|
|
59
|
+
}
|
|
60
|
+
function edgeKey(edge) {
|
|
61
|
+
const id = edge.instanceOption?.id;
|
|
62
|
+
return id ? `${edge.block}::${id}` : edge.block;
|
|
63
|
+
}
|
|
64
|
+
async function detectHere(page, navBlocks) {
|
|
65
|
+
try {
|
|
66
|
+
const url = page.url();
|
|
67
|
+
if (url.includes("/login"))
|
|
68
|
+
return "PiaLogin";
|
|
69
|
+
if (url.includes("mailpit") || url.includes(":54324") || url.includes("/mailhog")) {
|
|
70
|
+
return "PiaMailpitGui";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* ignore */
|
|
75
|
+
}
|
|
76
|
+
const tags = navBlocks.map((n) => n.block);
|
|
77
|
+
return locate(page, tags, { timeoutMs: 1500 });
|
|
78
|
+
}
|
|
79
|
+
async function runOneBlock(engine, entry, context, page, mem) {
|
|
80
|
+
const flow = engine.defineFlow([start, entry.block, end]);
|
|
81
|
+
const BLOCK_TIMEOUT_MS = 45_000;
|
|
82
|
+
let timer;
|
|
83
|
+
try {
|
|
84
|
+
const ran = await Promise.race([
|
|
85
|
+
flow.run(context, mem, { page, closeOnFinish: false }),
|
|
86
|
+
new Promise((_, reject) => {
|
|
87
|
+
timer = setTimeout(() => reject(new Error(`Block "${entry.block.name}" timed out after ${BLOCK_TIMEOUT_MS / 1000}s`)), BLOCK_TIMEOUT_MS);
|
|
88
|
+
}),
|
|
89
|
+
]);
|
|
90
|
+
return ran.result;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
if (timer)
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Serial graph crawl. Prints greppable PASS/FAIL lines; non-zero exit on break.
|
|
99
|
+
*/
|
|
100
|
+
export async function runTraverse(projectDir, options = {}) {
|
|
101
|
+
const traverseId = options.traverseId ?? "traverse-1";
|
|
102
|
+
const maxSteps = options.maxSteps ?? 50;
|
|
103
|
+
const maxVisitsPerNode = options.maxVisitsPerNode ?? 2;
|
|
104
|
+
const maxVisitsPerEdge = options.maxVisitsPerEdge ?? 1;
|
|
105
|
+
const timeoutMs = options.timeoutMs ?? 15 * 60_000;
|
|
106
|
+
const headed = options.headed === true || process.env.WAYGRAPH_HEADED === "1";
|
|
107
|
+
const baseURL = options.baseURL ?? resolveBaseUrl(projectDir) ?? process.env.WAYGRAPH_BASE_URL;
|
|
108
|
+
const startUrl = options.startUrl ?? baseURL;
|
|
109
|
+
const { graph, library } = await buildExploreContext(projectDir);
|
|
110
|
+
const mem = new MemPage();
|
|
111
|
+
seedMem(library.byName, mem, options.data);
|
|
112
|
+
const engine = new Engine({ headless: !headed, slowMo: headed ? 100 : 0 });
|
|
113
|
+
const { chromium } = await import("@playwright/test");
|
|
114
|
+
const executablePath = process.env.CHROME_PATH || process.env.CHROMIUM_PATH;
|
|
115
|
+
const launchOpts = {
|
|
116
|
+
headless: !headed,
|
|
117
|
+
};
|
|
118
|
+
if (executablePath)
|
|
119
|
+
launchOpts.executablePath = executablePath;
|
|
120
|
+
const browser = await chromium.launch(launchOpts);
|
|
121
|
+
const contextOpts = {
|
|
122
|
+
viewport: { width: 1280, height: 720 },
|
|
123
|
+
};
|
|
124
|
+
if (baseURL)
|
|
125
|
+
contextOpts.baseURL = baseURL;
|
|
126
|
+
const context = await browser.newContext(contextOpts);
|
|
127
|
+
let page = await context.newPage();
|
|
128
|
+
const nodeVisits = new Map();
|
|
129
|
+
const edgeVisits = new Map();
|
|
130
|
+
const edgesHit = new Set();
|
|
131
|
+
let steps = 0;
|
|
132
|
+
const startedAt = Date.now();
|
|
133
|
+
let lastHere = options.from ?? null;
|
|
134
|
+
let exitCode = 0;
|
|
135
|
+
console.error(`waygraph traverse[${traverseId}]: ${graph.nodes.length} node(s), ${graph.edges.length} edge(s)` +
|
|
136
|
+
(baseURL ? ` base=${baseURL}` : "") +
|
|
137
|
+
` maxSteps=${maxSteps} maxVisits/node=${maxVisitsPerNode} maxVisits/edge=${maxVisitsPerEdge}`);
|
|
138
|
+
try {
|
|
139
|
+
if (startUrl) {
|
|
140
|
+
await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
|
|
141
|
+
await page.waitForLoadState("load").catch(() => { });
|
|
142
|
+
}
|
|
143
|
+
for (;;) {
|
|
144
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
145
|
+
console.log(`[Killed timeout[${traverseId}] node=${lastHere ?? "?"} wallMs=${timeoutMs}]`);
|
|
146
|
+
exitCode = 1;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
if (steps >= maxSteps) {
|
|
150
|
+
console.log(`[Killed max-steps[${traverseId}] node=${lastHere ?? "?"} steps=${steps} max=${maxSteps}]`);
|
|
151
|
+
exitCode = 1;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
if (page.isClosed()) {
|
|
155
|
+
page = await context.newPage();
|
|
156
|
+
if (startUrl) {
|
|
157
|
+
await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const detected = await detectHere(page, library.navBlocks);
|
|
161
|
+
const here = lastHere ?? detected ?? options.from ?? null;
|
|
162
|
+
if (here && (nodeVisits.get(here) ?? 0) >= maxVisitsPerNode) {
|
|
163
|
+
console.log(`[Killed loop[${traverseId}] node=${here} visits=${nodeVisits.get(here)} max=${maxVisitsPerNode}]`);
|
|
164
|
+
exitCode = 1;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
const menu = await buildExploreMenu(page, graph, library, here);
|
|
168
|
+
const candidates = menu.flat.filter((edge) => {
|
|
169
|
+
const key = edgeKey(edge);
|
|
170
|
+
return (edgeVisits.get(key) ?? 0) < maxVisitsPerEdge;
|
|
171
|
+
});
|
|
172
|
+
if (candidates.length === 0) {
|
|
173
|
+
const label = here ?? detected ?? "unknown";
|
|
174
|
+
console.log(`[Reached Leaf Node[${traverseId}] at=${label} steps=${steps}]`);
|
|
175
|
+
console.error(`waygraph traverse[${traverseId}]: PASS leaf edgesHit=${edgesHit.size} steps=${steps}`);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
const pick = [...candidates].sort((a, b) => {
|
|
179
|
+
const aWild = a.from === "*" ? 1 : 0;
|
|
180
|
+
const bWild = b.from === "*" ? 1 : 0;
|
|
181
|
+
if (aWild !== bWild)
|
|
182
|
+
return aWild - bWild;
|
|
183
|
+
return edgeKey(a).localeCompare(edgeKey(b));
|
|
184
|
+
})[0];
|
|
185
|
+
const entry = library.byName.get(pick.block);
|
|
186
|
+
if (!entry) {
|
|
187
|
+
console.log(`[Broke at edge[${traverseId}] block=${pick.block} from=${here ?? "?"} to=${pick.to} reason=not-loaded]`);
|
|
188
|
+
exitCode = 1;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
const fromLabel = here ?? pick.from;
|
|
192
|
+
try {
|
|
193
|
+
if (pick.instanceOption) {
|
|
194
|
+
mem.set(pick.instanceOption.key, pick.instanceOption.value);
|
|
195
|
+
}
|
|
196
|
+
ensureMem(entry, mem);
|
|
197
|
+
console.error(`waygraph traverse[${traverseId}]: step ${steps + 1} ${fromLabel} -[${pick.block}]-> ${pick.to}` +
|
|
198
|
+
(pick.label ? ` (${pick.label})` : ""));
|
|
199
|
+
const out = await runOneBlock(engine, entry, context, page, mem);
|
|
200
|
+
const observed = typeof out === "object" && out && "__state" in out
|
|
201
|
+
? String(out.__state)
|
|
202
|
+
: pick.to;
|
|
203
|
+
const key = edgeKey(pick);
|
|
204
|
+
edgeVisits.set(key, (edgeVisits.get(key) ?? 0) + 1);
|
|
205
|
+
edgesHit.add(key);
|
|
206
|
+
if (here)
|
|
207
|
+
nodeVisits.set(here, (nodeVisits.get(here) ?? 0) + 1);
|
|
208
|
+
lastHere = observed;
|
|
209
|
+
steps += 1;
|
|
210
|
+
if (observed !== pick.to && pick.to !== "*") {
|
|
211
|
+
console.error(`waygraph traverse[${traverseId}]: WARN static to=${pick.to} observed=${observed}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
216
|
+
console.log(`[Broke at edge[${traverseId}] block=${pick.block} from=${fromLabel} to=${pick.to}] ${msg}`);
|
|
217
|
+
exitCode = 1;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
await browser.close().catch(() => { });
|
|
224
|
+
}
|
|
225
|
+
console.error(`waygraph traverse[${traverseId}]: done exit=${exitCode} steps=${steps} edgesHit=${edgesHit.size}`);
|
|
226
|
+
return exitCode;
|
|
227
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: waygraph-author
|
|
3
|
+
description: Author waygraph Nav/Page/Method/Effect blocks and flows from an approved SITE-MAP. Use after planning, or when extending coverage for a named route.
|
|
4
|
+
model: sonnet
|
|
5
|
+
color: green
|
|
6
|
+
tools:
|
|
7
|
+
- search
|
|
8
|
+
- edit
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You are a waygraph block author. Implement Blocks and Flows that match the consumer SITE-MAP.
|
|
12
|
+
|
|
13
|
+
## Rules
|
|
14
|
+
|
|
15
|
+
1. One concern per file: `nav-*.block.ts`, `*.page.block.ts`, `*.method.block.ts`, `*.effect.block.ts`, `*.flow.ts`.
|
|
16
|
+
2. Navigation only inside `defineNavBlock` / `defineNavClickBlock`. Regular Method/Effect `act` uses ActionPage (no goto).
|
|
17
|
+
3. Effects that appear in `waygraph auto` menus need `instanceOptions` that scan the live DOM (or mem) for labeled rows.
|
|
18
|
+
4. Stubs: `stubBefore` / `stubAfter` / `stubOnError` for demo narration; YAP slides on Methods when useful; flow fixtures via `withHighlightFixtures` when AC copy is flow-specific.
|
|
19
|
+
5. Mem: `requires` lists keys the block needs; seed via `--data` / `WAYGRAPH_DATA` / project defaults — never assume saucedemo creds for email keys.
|
|
20
|
+
6. External Mailpit/MailHog: nav to inbox URL from env; Methods open message / click verify link; never put mail UI under `*-web/`.
|
|
21
|
+
|
|
22
|
+
## Verify locally
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx waygraph check .
|
|
26
|
+
npx waygraph list
|
|
27
|
+
npm run typecheck
|
|
28
|
+
# offline fixtures or live:
|
|
29
|
+
npx playwright test
|
|
30
|
+
# explore:
|
|
31
|
+
npm run auto # or: npx waygraph auto --cli
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Output
|
|
35
|
+
|
|
36
|
+
Ship compiling TypeScript under `src/blocks/` + `src/flows/` + `src/states/`. Update NAV.md when edges change. Keep STRUCTURE / SITE-MAP honest.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: waygraph-healer
|
|
3
|
+
description: Debug and fix failing waygraph flows, auto menus, Mem seeds, and Mailpit/email verify hops. Use when npm test, waygraph run, or waygraph auto fails.
|
|
4
|
+
model: sonnet
|
|
5
|
+
color: red
|
|
6
|
+
tools:
|
|
7
|
+
- search
|
|
8
|
+
- edit
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You are a waygraph healer. Fix failing Blocks/Flows with evidence — do not rewrite the architecture.
|
|
12
|
+
|
|
13
|
+
## Diagnose order
|
|
14
|
+
|
|
15
|
+
1. Read the error (selector timeout, Trait fail, Mem missing, wrong checkpoint).
|
|
16
|
+
2. Confirm base URL / fixture server / env (`WAYGRAPH_BASE_URL`, Mailpit URL).
|
|
17
|
+
3. Check Mem: wrong type for a key (object vs string) is a common auto bug — `login-email` must be a string address, not `{username,password}`.
|
|
18
|
+
4. Nav vs Method: accidental `page.goto` outside NavBlock → move to `defineNavBlock`.
|
|
19
|
+
5. Effect auto menus: `instanceOptions` must match live DOM; empty menu usually means wrong selector or not on the page yet.
|
|
20
|
+
6. Mailpit: filter by the real recipient email in Mem; ambiguous `.msglist-message` with multiple rows → tighten locator.
|
|
21
|
+
|
|
22
|
+
## Fix loop
|
|
23
|
+
|
|
24
|
+
1. Reproduce with the smallest command (`waygraph run --blocks …` or one Playwright spec).
|
|
25
|
+
2. Patch the owning block (or fixture), re-run the same command.
|
|
26
|
+
3. Keep stubs/fixtures working after selector changes.
|
|
27
|
+
4. Report proof: command + exit 0 / expected checkpoint.
|
|
28
|
+
|
|
29
|
+
## Do not
|
|
30
|
+
|
|
31
|
+
- Invent new route folders that are not app URLs
|
|
32
|
+
- Seed credential objects into email Mem keys
|
|
33
|
+
- Clear shared remote MailHog/Mailpit unless the operator said so
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: waygraph-planner
|
|
3
|
+
description: Plan waygraph SITE-MAP and route coverage for an existing web app. Use when diving into a product codebase to map URLs to Nav/Page/Method/Effect blocks before writing code.
|
|
4
|
+
model: sonnet
|
|
5
|
+
color: blue
|
|
6
|
+
tools:
|
|
7
|
+
- search
|
|
8
|
+
- edit
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You are a waygraph planner. Your job is to dive an existing app and produce a SITE-MAP + NAV.md plan — not dump ad hoc Playwright helpers.
|
|
12
|
+
|
|
13
|
+
## Hard conventions
|
|
14
|
+
|
|
15
|
+
1. **Route folders = app URL folders.** A folder gets `NAV.md` + a nav block iff the app has a real page at that URL. Do not invent `landing/`, `root/`, or shell folders that are not routes.
|
|
16
|
+
2. **Namespaces by origin:** `<app>-web/` for the product FE; `<app>-external/` for Mailpit/MailHog, Studio, probes (never mix).
|
|
17
|
+
3. **Block kinds:**
|
|
18
|
+
- Nav — `defineNavBlock` / `defineNavClickBlock` — only place for `page.goto` / click-nav
|
|
19
|
+
- Page — `definePageBlock` hub + `methods` registry
|
|
20
|
+
- Method — forms, asserts, same-URL actions without instance menus
|
|
21
|
+
- Effect — `defineEffectBlock` + `instanceOptions` for per-row auto menus (add/remove/toggle)
|
|
22
|
+
4. **Mem keys:** typed `key` / `keyGroup`. Never rely on substring `"login"` defaults — email keys like `login-email` stay strings; credential objects use exact names such as `saucedemo.credentials` / `login-credentials`.
|
|
23
|
+
5. Prefer `click` NavBlocks when the product has a real control; use `url` for deep links / email tokens.
|
|
24
|
+
|
|
25
|
+
## Workflow
|
|
26
|
+
|
|
27
|
+
1. Read the app router (Next.js `app/`, etc.) and list real page URLs.
|
|
28
|
+
2. Draft `src/blocks/SITE-MAP.md` with namespaces + route folders.
|
|
29
|
+
3. For each route: list nav-to edges, methods/effects, checkpoints, mem keys in `NAV.md` (plan only).
|
|
30
|
+
4. Call out external surfaces (Mailpit inbox, magic-link verify) under `*-external/`.
|
|
31
|
+
5. Propose 1-3 starter flows (happy path, login/verify, one feature) — do not implement unless asked.
|
|
32
|
+
|
|
33
|
+
## Output
|
|
34
|
+
|
|
35
|
+
Write/update markdown under `src/blocks/` (`SITE-MAP.md`, route `NAV.md` stubs). ASCII only. No localhost ports in shared docs.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# __PROJECT_NAME__
|
|
2
2
|
|
|
3
|
-
Offline waygraph scaffold
|
|
3
|
+
Offline waygraph scaffold - Nav, Page hub, Method, Effect (`instanceOptions` for
|
|
4
|
+
`waygraph auto`), stubs/fixtures/YAP, mem keys.
|
|
4
5
|
|
|
5
6
|
## Layout
|
|
6
7
|
|
|
7
|
-
See [STRUCTURE.md](./STRUCTURE.md).
|
|
8
|
+
See [STRUCTURE.md](./STRUCTURE.md). Docs:
|
|
8
9
|
https://deviate-dv8.github.io/waygraph/scaffold.html
|
|
9
10
|
|
|
10
11
|
## Commands
|
|
@@ -13,13 +14,15 @@ https://deviate-dv8.github.io/waygraph/scaffold.html
|
|
|
13
14
|
npm install
|
|
14
15
|
npx playwright install chromium
|
|
15
16
|
npm test
|
|
16
|
-
npm run list
|
|
17
|
-
npm run
|
|
18
|
-
npm run demo
|
|
19
|
-
npm run
|
|
20
|
-
npm run auto
|
|
17
|
+
npm run list && npm run check && npm run graph
|
|
18
|
+
npm run demo # exampleFlow (hello + YAP)
|
|
19
|
+
npm run demo:shop # Effect path (needs --data selectedItem)
|
|
20
|
+
npm run auto # headed explore - pick Add Alpha / …
|
|
21
21
|
npm run auto:cli
|
|
22
|
+
npx waygraph agent-dive --loop claude # coding-agent defs (optional)
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
Point `package.json` `waygraph.baseUrl` at your real app when you leave the
|
|
25
|
-
`
|
|
26
|
+
fixture (and drop `scripts/with-fixture.mjs` wrappers). Keep route folders =
|
|
27
|
+
app URLs (consumer layout).
|
|
28
|
+
Offline fixture is HTTP `http://127.0.0.1:4177/home.html` (Flatpak-safe).
|
|
@@ -1,36 +1,61 @@
|
|
|
1
1
|
# Offline scaffold layout (`waygraph init` / `create-waygraph`)
|
|
2
2
|
|
|
3
3
|
Shipped under `templates/scaffold/` in the **waygraph** package.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Docs (readable in the package site): [docs/scaffold.html](../../docs/scaffold.html)
|
|
4
|
+
Docs: [docs/scaffold.html](../../docs/scaffold.html)
|
|
7
5
|
|
|
8
6
|
```text
|
|
9
7
|
.
|
|
10
|
-
├── package.json
|
|
11
|
-
├── playwright.config.ts
|
|
12
|
-
├── tsconfig.json
|
|
8
|
+
├── package.json
|
|
13
9
|
├── STRUCTURE.md # this file
|
|
14
10
|
├── README.md
|
|
15
11
|
├── src/
|
|
16
12
|
│ ├── blocks/
|
|
17
|
-
│ │ ├── SITE-MAP.md
|
|
18
|
-
│ │ └── demo-web/ # synthetic
|
|
13
|
+
│ │ ├── SITE-MAP.md
|
|
14
|
+
│ │ └── demo-web/ # synthetic "/" (HTTP fixture :4177)
|
|
19
15
|
│ │ ├── NAV.md
|
|
20
|
-
│ │ ├──
|
|
16
|
+
│ │ ├── home.html # offline fixture (catalog + cart)
|
|
17
|
+
│ │ ├── demo-sel.ts
|
|
18
|
+
│ │ ├── nav-home.block.ts # Nav
|
|
19
|
+
│ │ ├── home.page.block.ts # Page hub (methods registry)
|
|
21
20
|
│ │ └── methods/
|
|
22
|
-
│ │
|
|
21
|
+
│ │ ├── assert-hello.method.block.ts # Method + stubs + YAP
|
|
22
|
+
│ │ ├── add-item.effect.block.ts # Effect + instanceOptions (auto)
|
|
23
|
+
│ │ ├── remove-item.effect.block.ts # Effect + instanceOptions
|
|
24
|
+
│ │ └── clear-cart.method.block.ts # Method
|
|
23
25
|
│ ├── states/
|
|
24
|
-
│ │
|
|
26
|
+
│ │ ├── demo.states.ts
|
|
27
|
+
│ │ └── demo.mem-keys.ts # SelectedItem for Effect / auto
|
|
25
28
|
│ └── flows/
|
|
26
|
-
│
|
|
29
|
+
│ ├── example.flow.ts # nav + assert (smoke)
|
|
30
|
+
│ └── shop.flow.ts # nav + add-item + clear (Effect path)
|
|
31
|
+
├── scripts/
|
|
32
|
+
│ ├── fixture-server.mjs # serves home.html on :4177
|
|
33
|
+
│ └── with-fixture.mjs # start server + run waygraph auto/demo/run
|
|
27
34
|
└── tests/
|
|
28
35
|
└── example.spec.ts
|
|
29
36
|
```
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
## Features covered
|
|
39
|
+
|
|
40
|
+
| Feature | Where |
|
|
41
|
+
|---------|--------|
|
|
42
|
+
| Nav (`defineNavBlock`) | `nav-home.block.ts` |
|
|
43
|
+
| Page hub (`definePageBlock` + `methods`) | `home.page.block.ts` |
|
|
44
|
+
| Method (`defineMethodBlock`) | `assert-hello`, `clear-cart` |
|
|
45
|
+
| Effect + `instanceOptions` (auto menus) | `add-item`, `remove-item` |
|
|
46
|
+
| Mem (`keyGroup`) | `demo.mem-keys.ts` |
|
|
47
|
+
| `stubBefore` / `stubAfter` / `stubOnError` | all instruction blocks |
|
|
48
|
+
| Flow fixtures + title | `example.flow.ts`, `shop.flow.ts` |
|
|
49
|
+
| YAP slides | `assert-hello` |
|
|
50
|
+
| `withSessionReset` | `shop.flow.ts` |
|
|
51
|
+
| `waygraph auto` / `auto --cli` | `npm run auto` |
|
|
52
|
+
| `demo` / `run` / `list` / `check` / `graph` | `package.json` scripts |
|
|
53
|
+
|
|
54
|
+
## Auto
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm run auto # headed explore - pick Add Alpha / Add Beta from live DOM
|
|
58
|
+
npm run auto:cli # same menus in the terminal
|
|
59
|
+
```
|
|
34
60
|
|
|
35
|
-
|
|
36
|
-
and adding one folder per `page.tsx` URL. See package docs **Consumer layout**.
|
|
61
|
+
After `nav-home`, Effect blocks expose one menu row per visible Add/Remove button.
|
|
@@ -6,20 +6,25 @@
|
|
|
6
6
|
"scripts": {
|
|
7
7
|
"typecheck": "tsc --noEmit",
|
|
8
8
|
"test": "playwright test",
|
|
9
|
+
"fixture": "node scripts/fixture-server.mjs",
|
|
9
10
|
"list": "waygraph list",
|
|
10
11
|
"check": "waygraph check",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
12
|
+
"graph": "waygraph graph",
|
|
13
|
+
"graph:mermaid": "waygraph graph --mermaid",
|
|
14
|
+
"auto": "node scripts/with-fixture.mjs waygraph auto",
|
|
15
|
+
"auto:cli": "node scripts/with-fixture.mjs waygraph auto --cli",
|
|
16
|
+
"demo": "node scripts/with-fixture.mjs waygraph demo --blocks exampleFlow",
|
|
17
|
+
"demo:auto-next": "node scripts/with-fixture.mjs waygraph demo --blocks exampleFlow --auto-next --title \"Scaffold hello\"",
|
|
18
|
+
"demo:shop": "node scripts/with-fixture.mjs waygraph demo --blocks shopFlow --data \"{\\\"demo.selectedItem\\\":{\\\"id\\\":\\\"alpha\\\",\\\"name\\\":\\\"Alpha\\\"}}\" --title \"Scaffold shop\"",
|
|
19
|
+
"demo:shop:auto-next": "node scripts/with-fixture.mjs waygraph demo --blocks shopFlow --auto-next --data \"{\\\"demo.selectedItem\\\":{\\\"id\\\":\\\"alpha\\\",\\\"name\\\":\\\"Alpha\\\"}}\"",
|
|
20
|
+
"run": "node scripts/with-fixture.mjs waygraph run --blocks exampleFlow",
|
|
21
|
+
"run:shop": "node scripts/with-fixture.mjs waygraph run --blocks shopFlow --data \"{\\\"demo.selectedItem\\\":{\\\"id\\\":\\\"alpha\\\",\\\"name\\\":\\\"Alpha\\\"}}\""
|
|
17
22
|
},
|
|
18
23
|
"waygraph": {
|
|
19
|
-
"baseUrl": "
|
|
24
|
+
"baseUrl": "http://127.0.0.1:4177"
|
|
20
25
|
},
|
|
21
26
|
"dependencies": {
|
|
22
|
-
"waygraph": "^0.12.
|
|
27
|
+
"waygraph": "^0.12.7"
|
|
23
28
|
},
|
|
24
29
|
"devDependencies": {
|
|
25
30
|
"@playwright/test": "^1.55.0",
|
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
import { defineConfig } from "@playwright/test";
|
|
2
2
|
|
|
3
|
+
const FIXTURE_PORT = process.env.WAYGRAPH_FIXTURE_PORT || "4177";
|
|
4
|
+
const FIXTURE_HOST = process.env.WAYGRAPH_FIXTURE_HOST || "127.0.0.1";
|
|
5
|
+
const FIXTURE_ORIGIN = `http://${FIXTURE_HOST}:${FIXTURE_PORT}`;
|
|
6
|
+
|
|
3
7
|
export default defineConfig({
|
|
4
8
|
testDir: "./tests",
|
|
5
9
|
fullyParallel: true,
|
|
6
10
|
reporter: "list",
|
|
7
11
|
use: {
|
|
12
|
+
baseURL: FIXTURE_ORIGIN,
|
|
8
13
|
launchOptions: {
|
|
9
14
|
executablePath: process.env.CHROME_PATH || process.env.CHROMIUM_PATH,
|
|
10
15
|
},
|
|
11
16
|
},
|
|
17
|
+
webServer: {
|
|
18
|
+
command: "node scripts/fixture-server.mjs",
|
|
19
|
+
url: `${FIXTURE_ORIGIN}/home.html`,
|
|
20
|
+
reuseExistingServer: !process.env.CI,
|
|
21
|
+
env: {
|
|
22
|
+
WAYGRAPH_FIXTURE_PORT: FIXTURE_PORT,
|
|
23
|
+
WAYGRAPH_FIXTURE_HOST: FIXTURE_HOST,
|
|
24
|
+
WAYGRAPH_FIXTURE_QUIET: "1",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
12
27
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Tiny static server for the offline scaffold fixture (home.html).
|
|
4
|
+
* Flatpak Chromium cannot open file:// under /tmp — HTTP localhost works.
|
|
5
|
+
*/
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
8
|
+
import { join, dirname, extname, normalize } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "../src/blocks/demo-web");
|
|
12
|
+
const PORT = Number(process.env.WAYGRAPH_FIXTURE_PORT || 4177);
|
|
13
|
+
const HOST = process.env.WAYGRAPH_FIXTURE_HOST || "127.0.0.1";
|
|
14
|
+
|
|
15
|
+
const TYPES = {
|
|
16
|
+
".html": "text/html; charset=utf-8",
|
|
17
|
+
".css": "text/css; charset=utf-8",
|
|
18
|
+
".js": "text/javascript; charset=utf-8",
|
|
19
|
+
".svg": "image/svg+xml",
|
|
20
|
+
".png": "image/png",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const server = createServer((req, res) => {
|
|
24
|
+
const raw = (req.url || "/").split("?")[0] || "/";
|
|
25
|
+
const rel = raw === "/" ? "home.html" : raw.replace(/^\//, "");
|
|
26
|
+
const full = normalize(join(ROOT, rel));
|
|
27
|
+
if (!full.startsWith(ROOT) || !existsSync(full)) {
|
|
28
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
29
|
+
res.end("not found");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const type = TYPES[extname(full)] || "application/octet-stream";
|
|
33
|
+
res.writeHead(200, { "content-type": type });
|
|
34
|
+
res.end(readFileSync(full));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
server.listen(PORT, HOST, () => {
|
|
38
|
+
if (process.env.WAYGRAPH_FIXTURE_QUIET !== "1") {
|
|
39
|
+
console.error(`waygraph fixture: http://${HOST}:${PORT}/home.html`);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Start fixture-server, run a child command, tear down on exit.
|
|
4
|
+
* Usage: node scripts/with-fixture.mjs [--] <cmd> [args...]
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const PORT = process.env.WAYGRAPH_FIXTURE_PORT || "4177";
|
|
12
|
+
const HOST = process.env.WAYGRAPH_FIXTURE_HOST || "127.0.0.1";
|
|
13
|
+
|
|
14
|
+
const argv = process.argv.slice(2).filter((a) => a !== "--");
|
|
15
|
+
if (argv.length === 0) {
|
|
16
|
+
console.error("usage: node scripts/with-fixture.mjs <cmd> [args...]");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const env = {
|
|
21
|
+
...process.env,
|
|
22
|
+
WAYGRAPH_FIXTURE_PORT: PORT,
|
|
23
|
+
WAYGRAPH_FIXTURE_HOST: HOST,
|
|
24
|
+
WAYGRAPH_FIXTURE_QUIET: "1",
|
|
25
|
+
WAYGRAPH_BASE_URL: process.env.WAYGRAPH_BASE_URL || `http://${HOST}:${PORT}`,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const server = spawn(process.execPath, [join(HERE, "fixture-server.mjs")], {
|
|
29
|
+
env,
|
|
30
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function killServer() {
|
|
34
|
+
try {
|
|
35
|
+
server.kill("SIGTERM");
|
|
36
|
+
} catch {
|
|
37
|
+
/* ignore */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function waitReady(ms = 5000) {
|
|
42
|
+
const url = `http://${HOST}:${PORT}/home.html`;
|
|
43
|
+
const start = Date.now();
|
|
44
|
+
while (Date.now() - start < ms) {
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(url);
|
|
47
|
+
if (res.ok) return;
|
|
48
|
+
} catch {
|
|
49
|
+
/* retry */
|
|
50
|
+
}
|
|
51
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`fixture server not ready at ${url}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let code = 1;
|
|
57
|
+
try {
|
|
58
|
+
await waitReady();
|
|
59
|
+
const child = spawn(argv[0], argv.slice(1), { env, stdio: "inherit", shell: false });
|
|
60
|
+
code = await new Promise((resolve) => {
|
|
61
|
+
child.on("exit", (c, signal) => resolve(signal ? 1 : (c ?? 1)));
|
|
62
|
+
});
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
65
|
+
code = 1;
|
|
66
|
+
} finally {
|
|
67
|
+
killServer();
|
|
68
|
+
}
|
|
69
|
+
process.exit(code);
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# Site map (scaffold)
|
|
2
2
|
|
|
3
|
-
Offline
|
|
4
|
-
so `npm test`
|
|
3
|
+
Offline catalog uses one synthetic namespace (`demo-web/`) and a local
|
|
4
|
+
`home.html` fixture so `npm test` / `waygraph auto` need no network.
|
|
5
5
|
|
|
6
6
|
| Folder | URL | Notes |
|
|
7
7
|
|--------|-----|-------|
|
|
8
|
-
| `demo-web/` | `/` (synthetic) | Nav +
|
|
9
|
-
| `demo-web/methods/` | same URL |
|
|
8
|
+
| `demo-web/` | `/` (synthetic) | Nav + Page hub + methods/effects |
|
|
9
|
+
| `demo-web/methods/` | same URL | assert, add/remove Effects, clear |
|
|
10
10
|
|
|
11
11
|
When you wire a real app, rename `demo-web/` to your FE origin namespace and
|
|
12
|
-
add one folder per real `page.tsx` route.
|
|
13
|
-
`NAV.md`. Full contract: waygraph **Consumer layout** docs.
|
|
12
|
+
add one folder per real `page.tsx` route. See **Consumer layout** docs.
|