waygraph 0.15.17 → 0.15.18

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 CHANGED
@@ -412,6 +412,36 @@ engine.defineFlow([start, NavHomeBlock, AddItemBlock, AssertItemAddedBlock, Clea
412
412
  Live reference: `templates/scaffold/src/map/(app_base)/home/_methods/assert-item-added.method.block.ts`,
413
413
  wired into `shop.flow.ts`.
414
414
 
415
+ **An assert Block also names a feature *state*, not just a result or static content.** A
416
+ submit button greyed out until required fields are filled, a "Save" button re-enabling
417
+ after a successful edit, a field-level validation error appearing under one specific input
418
+ - these are conditions the UI is *in*, worth a nameable Checkpoint of their own the same way
419
+ `AssertItemAddedBlock` names "did add-item work." There's no built-in `Trait.disabled` -
420
+ write the state check as a custom Trait against the real DOM condition (`isDisabled()`, a
421
+ class, an `aria-disabled` attribute - whatever the app actually uses), the same pattern as
422
+ `removeButtonVisibleForSelectedItem` above:
423
+
424
+ ```typescript
425
+ const submitGatedUntilFieldsComplete: Trait = {
426
+ name: "submit-gated-until-fields-complete",
427
+ async check(page) {
428
+ return page.locator(CheckoutSel.submitButton).isDisabled();
429
+ },
430
+ };
431
+
432
+ export const AssertSubmitGatedBlock = defineAssertBlock<CheckoutInfoPage>({
433
+ name: "assert-submit-gated",
434
+ checkpoint: "CheckoutInfoPage",
435
+ verify: [submitGatedUntilFieldsComplete],
436
+ });
437
+
438
+ // wired right after the fields that gate it, before the ones that clear the gate:
439
+ engine.defineFlow([start, NavCheckoutInfoBlock, AssertSubmitGatedBlock, FillFirstNameBlock, ...])
440
+ ```
441
+
442
+ Same rule as any other assert Block: give it an explicit type argument when it sits
443
+ mid-chain (see above), and keep the selector in a `*Sel` object, never inlined in the Trait.
444
+
415
445
  **`methods/` folder:** on-page work lives next to the route, not free-floating.
416
446
 
417
447
  **`*Sel`:** DOM selectors only (static strings + `(id) => ...` for item-no-1 style). Mem
@@ -748,6 +778,22 @@ actually shows. Doesn't crash or corrupt anything if you never need it - it's a
748
778
  gap (confirmed by reading the exact code path, `src/auto-session.ts`'s `currentMenu()`), fixed
749
779
  outright rather than left as a caveat.
750
780
 
781
+ **Coverage gaps `waygraph check`/`graph` can't see, because there's no Block to scan yet.**
782
+ Static tools only find gaps in Blocks that already exist - a real link or button on the live
783
+ page with *no Block written for it at all* has nothing in source to catch it. Every live
784
+ `auto`/`browser`/`pilot` session watches the real page instead and warns (once per element,
785
+ via `console.warn`) on a same-origin `<a href>` no `defineNavBlock` covers, or a visible
786
+ `button`/`[role=button]`/submit input no Method/NavClick Block selector matches:
787
+
788
+ ```text
789
+ [waygraph] unmapped nav link on this page: /settings - no NavBlock covers this URL; add defineNavBlock
790
+ [waygraph] unmapped button on this page: #save-draft - no Method/NavClick Block selector matches; consider defineMethodBlock or defineNavClickBlock
791
+ ```
792
+
793
+ Read them back with `auto console <sessionId>` - the same channel real console/network errors
794
+ already come through, not a separate command (`src/coverage-gap.ts`, wired into every session
795
+ in `src/auto-session.ts`).
796
+
751
797
  **On-page indicator.** A headful (`--non-headless`) session looks like an ordinary browser
752
798
  tab otherwise - no sign anything is driving it, which matters once a human might be watching
753
799
  or co-driving the same visible session. Every session injects a small badge (bottom-right
@@ -1084,6 +1130,8 @@ Flows are files (0.10.5+):
1084
1130
  | Scope either to one element | `waygraph auto dom <sessionId> --selector ".inventory_list"` |
1085
1131
  | Detached session with a real visible browser | `waygraph auto --cli --detach --non-headless` |
1086
1132
  | Read the session's Checkpoint/Block history | `waygraph auto trace <sessionId>` |
1133
+ | Read real console/network errors + coverage-gap warnings | `waygraph auto console <sessionId>` |
1134
+ | Read localStorage/sessionStorage + service workers | `waygraph auto storage <sessionId>` |
1087
1135
  | Whole-project nav-escape + inline-selector + orphan sweep | `waygraph check [project]` |
1088
1136
  | `tsc --noEmit` + bad-practice warnings | `waygraph typecheck [project]` (`--no-practices` to skip scan) |
1089
1137
  | Waygraph Map enforcement (verbatim url-vs-folder, exits 1 on a violation) | `waygraph map [project]` |
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type { Checkpoint, Instruction, Block, DefinedBlock, Start, WaygraphInstanceOption } from "./types.js";
2
2
  export { connect, checkpoint } from "./types.js";
3
3
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
4
- export { Trait, urlMatches, textEquals, visible, frameVisible, frameTextEquals, frameContainsText } from "./trait.js";
4
+ export { Trait, urlMatches, textEquals, visible, disabled, enabled, frameVisible, frameTextEquals, frameContainsText, } from "./trait.js";
5
5
  export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, chainFlow, defineLayout, map, MapBuilder, } from "./engine.js";
6
6
  export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, DEVICE_PRESETS, resolveTodoDockUi, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, WAYGRAPH_RING_CSS, } from "./highlights.js";
7
7
  export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, WaygraphTodoItem, WaygraphTodoInput, WaygraphTodoGroup, WaygraphTodoGroupInput, WaygraphTodosOpts, TodoDockUiOpts, TodoDockUiResolved, TodoDockState, TodoListStyle, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, StubCtx, StubLifecycleFn, StubPhaseResult, StubPhaseFixtures, DevicePreset, DeviceViewport, DeviceState, DeviceOrientation, } from "./highlights.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { connect, checkpoint } from "./types.js";
2
2
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
3
- export { Trait, urlMatches, textEquals, visible, frameVisible, frameTextEquals, frameContainsText } from "./trait.js";
3
+ export { Trait, urlMatches, textEquals, visible, disabled, enabled, frameVisible, frameTextEquals, frameContainsText, } from "./trait.js";
4
4
  export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, chainFlow, defineLayout, map, MapBuilder, } from "./engine.js";
5
5
  export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, DEVICE_PRESETS, resolveTodoDockUi, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, WAYGRAPH_RING_CSS, } from "./highlights.js";
6
6
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
package/dist/trait.d.ts CHANGED
@@ -40,6 +40,20 @@ export declare function textEquals(selector: string, expected: string): Trait;
40
40
  * "something matching is visible", not "the selector is unambiguous".
41
41
  */
42
42
  export declare function visible(selector: string): Trait;
43
+ /**
44
+ * Passes when at least one element matching `selector` is currently disabled -
45
+ * a submit button greyed out until required fields are filled, a "Save" not
46
+ * yet re-enabled after a pending edit. A real, nameable feature *state*, not
47
+ * just static content or an action's result. Uses `.first()` for the same
48
+ * reason `visible` does. A point-in-time read via Playwright's own
49
+ * `isDisabled()` - no wait built in, so pair it with `Trait.visible` (or rely
50
+ * on `verify`'s own natural timing right after `resolve`) if the element
51
+ * itself might not be on the page yet.
52
+ * @example Trait.disabled(CheckoutSel.submitButton)
53
+ */
54
+ export declare function disabled(selector: string): Trait;
55
+ /** The complement of {@link disabled} - passes once `selector` is enabled. */
56
+ export declare function enabled(selector: string): Trait;
43
57
  /**
44
58
  * `visible`, scoped to a frame (e.g. a mail catcher's message-preview iframe)
45
59
  * instead of the top-level page. `page.locator(selector)` alone never reaches
@@ -73,6 +87,8 @@ export declare const Trait: {
73
87
  url: typeof urlMatches;
74
88
  text: typeof textEquals;
75
89
  visible: typeof visible;
90
+ disabled: typeof disabled;
91
+ enabled: typeof enabled;
76
92
  frameVisible: typeof frameVisible;
77
93
  frameText: typeof frameTextEquals;
78
94
  frameContains: typeof frameContainsText;
package/dist/trait.js CHANGED
@@ -65,6 +65,34 @@ export function visible(selector) {
65
65
  },
66
66
  };
67
67
  }
68
+ /**
69
+ * Passes when at least one element matching `selector` is currently disabled -
70
+ * a submit button greyed out until required fields are filled, a "Save" not
71
+ * yet re-enabled after a pending edit. A real, nameable feature *state*, not
72
+ * just static content or an action's result. Uses `.first()` for the same
73
+ * reason `visible` does. A point-in-time read via Playwright's own
74
+ * `isDisabled()` - no wait built in, so pair it with `Trait.visible` (or rely
75
+ * on `verify`'s own natural timing right after `resolve`) if the element
76
+ * itself might not be on the page yet.
77
+ * @example Trait.disabled(CheckoutSel.submitButton)
78
+ */
79
+ export function disabled(selector) {
80
+ return {
81
+ name: `disabled(${selector})`,
82
+ async check(page) {
83
+ return page.locator(selector).first().isDisabled();
84
+ },
85
+ };
86
+ }
87
+ /** The complement of {@link disabled} - passes once `selector` is enabled. */
88
+ export function enabled(selector) {
89
+ return {
90
+ name: `enabled(${selector})`,
91
+ async check(page) {
92
+ return page.locator(selector).first().isEnabled();
93
+ },
94
+ };
95
+ }
68
96
  /**
69
97
  * `visible`, scoped to a frame (e.g. a mail catcher's message-preview iframe)
70
98
  * instead of the top-level page. `page.locator(selector)` alone never reaches
@@ -121,6 +149,8 @@ export const Trait = {
121
149
  url: urlMatches,
122
150
  text: textEquals,
123
151
  visible,
152
+ disabled,
153
+ enabled,
124
154
  frameVisible,
125
155
  frameText: frameTextEquals,
126
156
  frameContains: frameContainsText,
@@ -16,29 +16,424 @@
16
16
  "typescript": "^5.6.0"
17
17
  }
18
18
  },
19
- "../..": {
20
- "version": "0.12.47",
19
+ "node_modules/@esbuild/aix-ppc64": {
20
+ "version": "0.28.2",
21
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
22
+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
23
+ "cpu": [
24
+ "ppc64"
25
+ ],
21
26
  "license": "MIT",
22
- "dependencies": {
23
- "tsx": "^4.0.0"
24
- },
25
- "bin": {
26
- "waygraph": "bin/waygraph"
27
- },
28
- "devDependencies": {
29
- "@playwright/test": "^1.63.0",
30
- "@types/node": "^26.0.0",
31
- "typescript": "^5.6.0"
32
- },
33
- "peerDependencies": {
34
- "@playwright/test": "^1.40.0"
27
+ "optional": true,
28
+ "os": [
29
+ "aix"
30
+ ],
31
+ "engines": {
32
+ "node": ">=18"
33
+ }
34
+ },
35
+ "node_modules/@esbuild/android-arm": {
36
+ "version": "0.28.2",
37
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
38
+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
39
+ "cpu": [
40
+ "arm"
41
+ ],
42
+ "license": "MIT",
43
+ "optional": true,
44
+ "os": [
45
+ "android"
46
+ ],
47
+ "engines": {
48
+ "node": ">=18"
49
+ }
50
+ },
51
+ "node_modules/@esbuild/android-arm64": {
52
+ "version": "0.28.2",
53
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
54
+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
55
+ "cpu": [
56
+ "arm64"
57
+ ],
58
+ "license": "MIT",
59
+ "optional": true,
60
+ "os": [
61
+ "android"
62
+ ],
63
+ "engines": {
64
+ "node": ">=18"
65
+ }
66
+ },
67
+ "node_modules/@esbuild/android-x64": {
68
+ "version": "0.28.2",
69
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
70
+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
71
+ "cpu": [
72
+ "x64"
73
+ ],
74
+ "license": "MIT",
75
+ "optional": true,
76
+ "os": [
77
+ "android"
78
+ ],
79
+ "engines": {
80
+ "node": ">=18"
81
+ }
82
+ },
83
+ "node_modules/@esbuild/darwin-arm64": {
84
+ "version": "0.28.2",
85
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
86
+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
87
+ "cpu": [
88
+ "arm64"
89
+ ],
90
+ "license": "MIT",
91
+ "optional": true,
92
+ "os": [
93
+ "darwin"
94
+ ],
95
+ "engines": {
96
+ "node": ">=18"
97
+ }
98
+ },
99
+ "node_modules/@esbuild/darwin-x64": {
100
+ "version": "0.28.2",
101
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
102
+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
103
+ "cpu": [
104
+ "x64"
105
+ ],
106
+ "license": "MIT",
107
+ "optional": true,
108
+ "os": [
109
+ "darwin"
110
+ ],
111
+ "engines": {
112
+ "node": ">=18"
113
+ }
114
+ },
115
+ "node_modules/@esbuild/freebsd-arm64": {
116
+ "version": "0.28.2",
117
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
118
+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
119
+ "cpu": [
120
+ "arm64"
121
+ ],
122
+ "license": "MIT",
123
+ "optional": true,
124
+ "os": [
125
+ "freebsd"
126
+ ],
127
+ "engines": {
128
+ "node": ">=18"
129
+ }
130
+ },
131
+ "node_modules/@esbuild/freebsd-x64": {
132
+ "version": "0.28.2",
133
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
134
+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
135
+ "cpu": [
136
+ "x64"
137
+ ],
138
+ "license": "MIT",
139
+ "optional": true,
140
+ "os": [
141
+ "freebsd"
142
+ ],
143
+ "engines": {
144
+ "node": ">=18"
145
+ }
146
+ },
147
+ "node_modules/@esbuild/linux-arm": {
148
+ "version": "0.28.2",
149
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
150
+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
151
+ "cpu": [
152
+ "arm"
153
+ ],
154
+ "license": "MIT",
155
+ "optional": true,
156
+ "os": [
157
+ "linux"
158
+ ],
159
+ "engines": {
160
+ "node": ">=18"
161
+ }
162
+ },
163
+ "node_modules/@esbuild/linux-arm64": {
164
+ "version": "0.28.2",
165
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
166
+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
167
+ "cpu": [
168
+ "arm64"
169
+ ],
170
+ "license": "MIT",
171
+ "optional": true,
172
+ "os": [
173
+ "linux"
174
+ ],
175
+ "engines": {
176
+ "node": ">=18"
177
+ }
178
+ },
179
+ "node_modules/@esbuild/linux-ia32": {
180
+ "version": "0.28.2",
181
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
182
+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
183
+ "cpu": [
184
+ "ia32"
185
+ ],
186
+ "license": "MIT",
187
+ "optional": true,
188
+ "os": [
189
+ "linux"
190
+ ],
191
+ "engines": {
192
+ "node": ">=18"
193
+ }
194
+ },
195
+ "node_modules/@esbuild/linux-loong64": {
196
+ "version": "0.28.2",
197
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
198
+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
199
+ "cpu": [
200
+ "loong64"
201
+ ],
202
+ "license": "MIT",
203
+ "optional": true,
204
+ "os": [
205
+ "linux"
206
+ ],
207
+ "engines": {
208
+ "node": ">=18"
209
+ }
210
+ },
211
+ "node_modules/@esbuild/linux-mips64el": {
212
+ "version": "0.28.2",
213
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
214
+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
215
+ "cpu": [
216
+ "mips64el"
217
+ ],
218
+ "license": "MIT",
219
+ "optional": true,
220
+ "os": [
221
+ "linux"
222
+ ],
223
+ "engines": {
224
+ "node": ">=18"
225
+ }
226
+ },
227
+ "node_modules/@esbuild/linux-ppc64": {
228
+ "version": "0.28.2",
229
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
230
+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
231
+ "cpu": [
232
+ "ppc64"
233
+ ],
234
+ "license": "MIT",
235
+ "optional": true,
236
+ "os": [
237
+ "linux"
238
+ ],
239
+ "engines": {
240
+ "node": ">=18"
241
+ }
242
+ },
243
+ "node_modules/@esbuild/linux-riscv64": {
244
+ "version": "0.28.2",
245
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
246
+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
247
+ "cpu": [
248
+ "riscv64"
249
+ ],
250
+ "license": "MIT",
251
+ "optional": true,
252
+ "os": [
253
+ "linux"
254
+ ],
255
+ "engines": {
256
+ "node": ">=18"
257
+ }
258
+ },
259
+ "node_modules/@esbuild/linux-s390x": {
260
+ "version": "0.28.2",
261
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
262
+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
263
+ "cpu": [
264
+ "s390x"
265
+ ],
266
+ "license": "MIT",
267
+ "optional": true,
268
+ "os": [
269
+ "linux"
270
+ ],
271
+ "engines": {
272
+ "node": ">=18"
273
+ }
274
+ },
275
+ "node_modules/@esbuild/linux-x64": {
276
+ "version": "0.28.2",
277
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
278
+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
279
+ "cpu": [
280
+ "x64"
281
+ ],
282
+ "license": "MIT",
283
+ "optional": true,
284
+ "os": [
285
+ "linux"
286
+ ],
287
+ "engines": {
288
+ "node": ">=18"
289
+ }
290
+ },
291
+ "node_modules/@esbuild/netbsd-arm64": {
292
+ "version": "0.28.2",
293
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
294
+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
295
+ "cpu": [
296
+ "arm64"
297
+ ],
298
+ "license": "MIT",
299
+ "optional": true,
300
+ "os": [
301
+ "netbsd"
302
+ ],
303
+ "engines": {
304
+ "node": ">=18"
305
+ }
306
+ },
307
+ "node_modules/@esbuild/netbsd-x64": {
308
+ "version": "0.28.2",
309
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
310
+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
311
+ "cpu": [
312
+ "x64"
313
+ ],
314
+ "license": "MIT",
315
+ "optional": true,
316
+ "os": [
317
+ "netbsd"
318
+ ],
319
+ "engines": {
320
+ "node": ">=18"
321
+ }
322
+ },
323
+ "node_modules/@esbuild/openbsd-arm64": {
324
+ "version": "0.28.2",
325
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
326
+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
327
+ "cpu": [
328
+ "arm64"
329
+ ],
330
+ "license": "MIT",
331
+ "optional": true,
332
+ "os": [
333
+ "openbsd"
334
+ ],
335
+ "engines": {
336
+ "node": ">=18"
337
+ }
338
+ },
339
+ "node_modules/@esbuild/openbsd-x64": {
340
+ "version": "0.28.2",
341
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
342
+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
343
+ "cpu": [
344
+ "x64"
345
+ ],
346
+ "license": "MIT",
347
+ "optional": true,
348
+ "os": [
349
+ "openbsd"
350
+ ],
351
+ "engines": {
352
+ "node": ">=18"
353
+ }
354
+ },
355
+ "node_modules/@esbuild/openharmony-arm64": {
356
+ "version": "0.28.2",
357
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
358
+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
359
+ "cpu": [
360
+ "arm64"
361
+ ],
362
+ "license": "MIT",
363
+ "optional": true,
364
+ "os": [
365
+ "openharmony"
366
+ ],
367
+ "engines": {
368
+ "node": ">=18"
369
+ }
370
+ },
371
+ "node_modules/@esbuild/sunos-x64": {
372
+ "version": "0.28.2",
373
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
374
+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
375
+ "cpu": [
376
+ "x64"
377
+ ],
378
+ "license": "MIT",
379
+ "optional": true,
380
+ "os": [
381
+ "sunos"
382
+ ],
383
+ "engines": {
384
+ "node": ">=18"
385
+ }
386
+ },
387
+ "node_modules/@esbuild/win32-arm64": {
388
+ "version": "0.28.2",
389
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
390
+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
391
+ "cpu": [
392
+ "arm64"
393
+ ],
394
+ "license": "MIT",
395
+ "optional": true,
396
+ "os": [
397
+ "win32"
398
+ ],
399
+ "engines": {
400
+ "node": ">=18"
401
+ }
402
+ },
403
+ "node_modules/@esbuild/win32-ia32": {
404
+ "version": "0.28.2",
405
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
406
+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
407
+ "cpu": [
408
+ "ia32"
409
+ ],
410
+ "license": "MIT",
411
+ "optional": true,
412
+ "os": [
413
+ "win32"
414
+ ],
415
+ "engines": {
416
+ "node": ">=18"
417
+ }
418
+ },
419
+ "node_modules/@esbuild/win32-x64": {
420
+ "version": "0.28.2",
421
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
422
+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
423
+ "cpu": [
424
+ "x64"
425
+ ],
426
+ "license": "MIT",
427
+ "optional": true,
428
+ "os": [
429
+ "win32"
430
+ ],
431
+ "engines": {
432
+ "node": ">=18"
35
433
  }
36
434
  },
37
435
  "node_modules/@playwright/test": {
38
436
  "version": "1.63.0",
39
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz",
40
- "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==",
41
- "dev": true,
42
437
  "license": "Apache-2.0",
43
438
  "dependencies": {
44
439
  "playwright": "1.63.0"
@@ -52,19 +447,69 @@
52
447
  },
53
448
  "node_modules/@types/node": {
54
449
  "version": "26.6.1",
55
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.1.tgz",
56
- "integrity": "sha512-VqGJBMCtdhqkBUCcBLvywI0NJ+KLuVzgNnlBUNFOQjqVxzo2lxLUNg1DSey8+u2u6ktswSAxg+s68QLzWHNOuA==",
57
450
  "dev": true,
58
451
  "license": "MIT",
59
452
  "dependencies": {
60
453
  "undici-types": "~8.9.0"
61
454
  }
62
455
  },
456
+ "node_modules/esbuild": {
457
+ "version": "0.28.2",
458
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
459
+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
460
+ "hasInstallScript": true,
461
+ "license": "MIT",
462
+ "bin": {
463
+ "esbuild": "bin/esbuild"
464
+ },
465
+ "engines": {
466
+ "node": ">=18"
467
+ },
468
+ "optionalDependencies": {
469
+ "@esbuild/aix-ppc64": "0.28.2",
470
+ "@esbuild/android-arm": "0.28.2",
471
+ "@esbuild/android-arm64": "0.28.2",
472
+ "@esbuild/android-x64": "0.28.2",
473
+ "@esbuild/darwin-arm64": "0.28.2",
474
+ "@esbuild/darwin-x64": "0.28.2",
475
+ "@esbuild/freebsd-arm64": "0.28.2",
476
+ "@esbuild/freebsd-x64": "0.28.2",
477
+ "@esbuild/linux-arm": "0.28.2",
478
+ "@esbuild/linux-arm64": "0.28.2",
479
+ "@esbuild/linux-ia32": "0.28.2",
480
+ "@esbuild/linux-loong64": "0.28.2",
481
+ "@esbuild/linux-mips64el": "0.28.2",
482
+ "@esbuild/linux-ppc64": "0.28.2",
483
+ "@esbuild/linux-riscv64": "0.28.2",
484
+ "@esbuild/linux-s390x": "0.28.2",
485
+ "@esbuild/linux-x64": "0.28.2",
486
+ "@esbuild/netbsd-arm64": "0.28.2",
487
+ "@esbuild/netbsd-x64": "0.28.2",
488
+ "@esbuild/openbsd-arm64": "0.28.2",
489
+ "@esbuild/openbsd-x64": "0.28.2",
490
+ "@esbuild/openharmony-arm64": "0.28.2",
491
+ "@esbuild/sunos-x64": "0.28.2",
492
+ "@esbuild/win32-arm64": "0.28.2",
493
+ "@esbuild/win32-ia32": "0.28.2",
494
+ "@esbuild/win32-x64": "0.28.2"
495
+ }
496
+ },
497
+ "node_modules/fsevents": {
498
+ "version": "2.3.3",
499
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
500
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
501
+ "hasInstallScript": true,
502
+ "license": "MIT",
503
+ "optional": true,
504
+ "os": [
505
+ "darwin"
506
+ ],
507
+ "engines": {
508
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
509
+ }
510
+ },
63
511
  "node_modules/playwright": {
64
512
  "version": "1.63.0",
65
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
66
- "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
67
- "dev": true,
68
513
  "license": "Apache-2.0",
69
514
  "dependencies": {
70
515
  "playwright-core": "1.63.0"
@@ -78,9 +523,6 @@
78
523
  },
79
524
  "node_modules/playwright-core": {
80
525
  "version": "1.63.0",
81
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
82
- "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
83
- "dev": true,
84
526
  "license": "Apache-2.0",
85
527
  "bin": {
86
528
  "playwright-core": "cli.js"
@@ -89,10 +531,26 @@
89
531
  "node": ">=20"
90
532
  }
91
533
  },
534
+ "node_modules/tsx": {
535
+ "version": "4.23.15",
536
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz",
537
+ "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==",
538
+ "license": "MIT",
539
+ "dependencies": {
540
+ "esbuild": "~0.28.0"
541
+ },
542
+ "bin": {
543
+ "tsx": "dist/cli.mjs"
544
+ },
545
+ "engines": {
546
+ "node": ">=18.0.0"
547
+ },
548
+ "optionalDependencies": {
549
+ "fsevents": "~2.3.3"
550
+ }
551
+ },
92
552
  "node_modules/typescript": {
93
553
  "version": "5.9.3",
94
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
95
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
96
554
  "dev": true,
97
555
  "license": "Apache-2.0",
98
556
  "bin": {
@@ -105,14 +563,22 @@
105
563
  },
106
564
  "node_modules/undici-types": {
107
565
  "version": "8.9.0",
108
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
109
- "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
110
566
  "dev": true,
111
567
  "license": "MIT"
112
568
  },
113
569
  "node_modules/waygraph": {
114
- "resolved": "../..",
115
- "link": true
570
+ "version": "0.15.17",
571
+ "resolved": "file:../..",
572
+ "license": "MIT",
573
+ "dependencies": {
574
+ "tsx": "^4.0.0"
575
+ },
576
+ "bin": {
577
+ "waygraph": "bin/waygraph"
578
+ },
579
+ "peerDependencies": {
580
+ "@playwright/test": "^1.40.0"
581
+ }
116
582
  }
117
583
  }
118
584
  }
@@ -5,7 +5,7 @@ import { dirname, join } from "node:path";
5
5
 
6
6
  const projectDir = join(dirname(fileURLToPath(import.meta.url)), "..");
7
7
 
8
- test("waygraph auto discovers the decomposed saucedemo graph with zero skipped Blocks", async () => {
8
+ test("waygraph auto discovers the decomposed saucedemo graph, skipping only the FFCompose auth step", async () => {
9
9
  const graph = await discoverGraph(projectDir);
10
10
 
11
11
  expect(graph.nodes.map((n) => n.checkpoint).sort()).toEqual(
@@ -20,8 +20,12 @@ test("waygraph auto discovers the decomposed saucedemo graph with zero skipped B
20
20
  "OrderComplete",
21
21
  ].sort(),
22
22
  );
23
- // +2 vs pre-remove: remove-from-cart branches * → ItemInCart | LoggedIn
24
- expect(graph.edges.length).toBe(15);
23
+ // 24 vs the earlier 15: checkout-info split into fill-first-name/
24
+ // fill-last-name/fill-postal-code (+2 net) plus the atomic login Blocks
25
+ // (fill-username/fill-password/submit-login's 2 branches) that static
26
+ // discovery still sees even though checkoutFlow itself now drives them
27
+ // through the ff-owner-auth composition below.
28
+ expect(graph.edges.length).toBe(24);
25
29
  const removeEdges = graph.edges.filter((e) => e.block === "remove-from-cart");
26
30
  expect(removeEdges).toEqual(
27
31
  expect.arrayContaining([
@@ -32,7 +36,15 @@ test("waygraph auto discovers the decomposed saucedemo graph with zero skipped B
32
36
 
33
37
  const loginFail = graph.edges.find((e) => e.block === "submit-login" && e.to === "LoginPage");
34
38
  expect(loginFail).toMatchObject({ from: "LoginPage", to: "LoginPage", kind: "action" });
35
- expect(graph.skipped.length).toBe(0);
39
+
40
+ // ff-owner-auth (fastForwardComposeBlock) collapses nav-login + fill-username
41
+ // + fill-password + submit-login into one opaque demo/run step - discoverGraph
42
+ // only understands plain defineMethodBlock/defineEffectBlock<In, Out> generic
43
+ // calls, so this one Block is expected to be skipped by static discovery even
44
+ // though it's a real, used step (see src/blocks/saucedemo-web/ff-owner-auth.block.ts).
45
+ expect(graph.skipped).toEqual([
46
+ expect.objectContaining({ block: "ff-owner-auth" }),
47
+ ]);
36
48
 
37
49
  const logout = graph.edges.find((e) => e.block === "submit-logout");
38
50
  expect(logout).toMatchObject({ from: "LoggedIn", to: "LoginPage", kind: "action" });
@@ -29,10 +29,15 @@ test("each episode's title survives chaining, and blocks() reports the flattened
29
29
 
30
30
  expect(blocks.map((b) => b.name)).toEqual([
31
31
  "nav-login",
32
+ "fill-username",
33
+ "fill-password",
32
34
  "submit-login",
33
35
  "add-to-cart",
34
36
  "nav-cart",
35
37
  "nav-checkout-info",
38
+ "fill-first-name",
39
+ "fill-last-name",
40
+ "fill-postal-code",
36
41
  "submit-checkout-info",
37
42
  "finish-order",
38
43
  ]);
@@ -1,7 +1,7 @@
1
1
  import { test, expect } from "@playwright/test";
2
2
  import { MemPage, checkpoint, Trait } from "waygraph";
3
3
  import { checkoutFlow } from "../src/flows/checkout.flow.js";
4
- import { SubmitLoginBlock } from "../src/blocks/saucedemo-web/methods/submit-login.method.block.js";
4
+ import { FfOwnerAuthBlock } from "../src/blocks/saucedemo-web/ff-owner-auth.block.js";
5
5
  import { LoginCreds, SelectedItem } from "../src/states/checkout.mem-keys.js";
6
6
 
7
7
  // checkoutFlow.withBlockVerify/modBlockVerify - a spec that only imports the
@@ -31,9 +31,12 @@ test("withBlockVerify: replacing login's verify with an extra real check still p
31
31
  mem.set(LoginCreds({ username: "standard_user", password: "secret_sauce" }));
32
32
  mem.set(SelectedItem({ id: "sauce-labs-backpack", name: "Sauce Labs Backpack" }));
33
33
 
34
- // Addressed by the actual SubmitLoginBlock reference - the preferred
35
- // form: no typo risk, refactor-safe, jump-to-definition works.
36
- const patched = checkoutFlow.withBlockVerify(SubmitLoginBlock, [
34
+ // Addressed by the actual FfOwnerAuthBlock reference - the preferred
35
+ // form: no typo risk, refactor-safe, jump-to-definition works. checkoutFlow
36
+ // drives login through the ff-owner-auth FFCompose step (collapsed
37
+ // nav-login + fill-username + fill-password + submit-login), not a bare
38
+ // SubmitLoginBlock reference, so that's the Block whose verify this patches.
39
+ const patched = checkoutFlow.withBlockVerify(FfOwnerAuthBlock, [
37
40
  Trait.url({ pathname: "/inventory.html" }),
38
41
  Trait.visible(".inventory_list"),
39
42
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.15.17",
3
+ "version": "0.15.18",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -22,6 +22,11 @@ against a live site, use `--skill-pilot-blind`.
22
22
  inline selector literals in `Trait.visible` / `Trait.text`.
23
23
  4. **Assert-only = `defineAssertBlock`.** Self-loop, no hand-written `act`/`resolve`. Pass
24
24
  an explicit type argument when it sits between two specifically-typed Blocks in a Flow.
25
+ Not just static content or an action's result - it also names a *feature state* worth a
26
+ Checkpoint of its own (a submit button greyed out until required fields are filled, a
27
+ field error under one input, a stale "Save" re-enabling). No built-in `Trait.disabled` -
28
+ write the state condition as a custom Trait (`isDisabled()`, a class, `aria-disabled`),
29
+ same as any other Trait: real DOM check, no inline selector.
25
30
  5. **`requires` = externally supplied.** A key an earlier Block in the same chain produces
26
31
  is never listed under `requires` - preflight checks the whole chain up front.
27
32
  6. **Keep Checkpoints typed — never `Checkpoint<string>`.** Block helpers take concrete
@@ -105,6 +110,22 @@ npm run typecheck # when the consumer has it
105
110
  are still runnable in Pilot's live menu - orphan status only blocks
106
111
  `auto --blocks <From> <To>` pathfinding.
107
112
 
113
+ **Static tools above only find gaps in Blocks that already exist.** A real link/button on
114
+ the live page with *no Block written for it at all* has nothing in source for `check`/
115
+ `graph` to scan - the only way to find that is to actually run the app. Every live
116
+ `auto`/`browser`/`pilot` session watches the real page and warns (once per element) on
117
+ anything with no matching Block:
118
+
119
+ ```text
120
+ [waygraph] unmapped nav link on this page: /settings - no NavBlock covers this URL; add defineNavBlock
121
+ [waygraph] unmapped button on this page: #save-draft - no Method/NavClick Block selector matches; consider defineMethodBlock or defineNavClickBlock
122
+ ```
123
+
124
+ Read them back with `auto console <sessionId>` (same channel as real console/network
125
+ errors) - see `--skill-pilot`'s "Coverage gaps on the live page" section for the full
126
+ detection rules (same-origin `<a href>` only, visible clickables only, one warning per
127
+ element per session).
128
+
108
129
  ## Related skills (print via CLI)
109
130
 
110
131
  ```bash