waygraph 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +171 -21
  2. package/dist/cli.js +33 -0
  3. package/dist/engine.d.ts +161 -0
  4. package/dist/engine.js +230 -2
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.js +1 -1
  7. package/dist/map-check.d.ts +18 -0
  8. package/dist/map-check.js +140 -0
  9. package/dist/types.d.ts +21 -1
  10. package/dist/types.js +17 -1
  11. package/examples/routed-demo/.waygraph-auto/4d4902b7.log +0 -0
  12. package/examples/routed-demo/.waygraph-auto/51ef462e.log +0 -0
  13. package/examples/routed-demo/.waygraph-auto/71527ad9.log +0 -0
  14. package/examples/routed-demo/.waygraph-auto/ca16778e.log +0 -0
  15. package/examples/routed-demo/src/flows/routed-demo-map.flow.ts +39 -0
  16. package/examples/routed-demo/src/flows/routed-demo.flow.ts +5 -5
  17. package/examples/routed-demo/src/{routes/(base_app)/dashboard/methods → map/(app_base)/dashboard/_methods}/click-widget.block.ts +2 -2
  18. package/examples/routed-demo/src/{routes/(base_app)/dashboard/nav.block.ts → map/(app_base)/dashboard/_nav.block.ts} +2 -2
  19. package/examples/routed-demo/src/{routes/(base_app)/dashboard/page.block.ts → map/(app_base)/dashboard/_page.block.ts} +4 -4
  20. package/examples/routed-demo/src/{routes/(external)/docs/nav.block.ts → map/(external)/docs/_nav.block.ts} +2 -2
  21. package/examples/routed-demo/src/{routes/(external)/docs/page.block.ts → map/(external)/docs/_page.block.ts} +1 -1
  22. package/examples/routed-demo/test-results/.last-run.json +4 -0
  23. package/examples/routed-demo/tests/routed-demo-map.spec.ts +17 -0
  24. package/examples/saucedemo/.waygraph-auto/00d5336e.log +0 -0
  25. package/examples/saucedemo/.waygraph-auto/083cc5b9.log +0 -0
  26. package/examples/saucedemo/.waygraph-auto/104751ef.log +0 -0
  27. package/examples/saucedemo/.waygraph-auto/175655f7.log +0 -0
  28. package/examples/saucedemo/.waygraph-auto/2219f00b.log +0 -0
  29. package/examples/saucedemo/.waygraph-auto/23e62571.log +0 -0
  30. package/examples/saucedemo/.waygraph-auto/42cc9113.log +0 -0
  31. package/examples/saucedemo/.waygraph-auto/439d30aa.log +0 -0
  32. package/examples/saucedemo/.waygraph-auto/4a85f496.log +0 -0
  33. package/examples/saucedemo/.waygraph-auto/5c0eef7f.log +0 -0
  34. package/examples/saucedemo/.waygraph-auto/5d60da77.log +0 -0
  35. package/examples/saucedemo/.waygraph-auto/5fc3a33b.log +0 -0
  36. package/examples/saucedemo/.waygraph-auto/61b6546d.log +0 -0
  37. package/examples/saucedemo/.waygraph-auto/650ed066.log +0 -0
  38. package/examples/saucedemo/.waygraph-auto/6d9aaad3.log +0 -0
  39. package/examples/saucedemo/.waygraph-auto/78b21219.log +0 -0
  40. package/examples/saucedemo/.waygraph-auto/84d6d293.log +0 -0
  41. package/examples/saucedemo/.waygraph-auto/8a3eb088.log +0 -0
  42. package/examples/saucedemo/.waygraph-auto/a2dd30dd.log +0 -0
  43. package/examples/saucedemo/.waygraph-auto/a87bb3ee.log +0 -0
  44. package/examples/saucedemo/.waygraph-auto/b81e074f.log +0 -0
  45. package/examples/saucedemo/.waygraph-auto/ba58abb7.log +0 -0
  46. package/examples/saucedemo/.waygraph-auto/bb9fd384.log +0 -0
  47. package/examples/saucedemo/.waygraph-auto/c02ea583.log +0 -0
  48. package/examples/saucedemo/.waygraph-auto/c2caf0a8.log +0 -0
  49. package/examples/saucedemo/.waygraph-auto/c3bdc714.log +0 -0
  50. package/examples/saucedemo/.waygraph-auto/cd68bbc4.log +0 -0
  51. package/examples/saucedemo/.waygraph-auto/cdc6db50.log +0 -0
  52. package/examples/saucedemo/.waygraph-auto/d01906a7.log +0 -0
  53. package/examples/saucedemo/.waygraph-auto/d9e5f8a4.log +0 -0
  54. package/examples/saucedemo/.waygraph-auto/dec3076e.log +0 -0
  55. package/examples/saucedemo/.waygraph-auto/e1973991.log +0 -0
  56. package/examples/saucedemo/.waygraph-auto/e8306a4b.log +0 -0
  57. package/examples/saucedemo/.waygraph-auto/ec88b3ef.log +0 -0
  58. package/examples/saucedemo/.waygraph-auto/f34c9a26.log +0 -0
  59. package/examples/saucedemo/.waygraph-auto/f5525d53.log +0 -0
  60. package/examples/saucedemo/.waygraph-auto/f90035b3.log +0 -0
  61. package/examples/saucedemo/.waygraph-auto/fa687bc2.log +0 -0
  62. package/examples/saucedemo/.waygraph-auto/fb66fba0.log +0 -0
  63. package/examples/saucedemo/.waygraph-auto/fc48fd76.log +0 -0
  64. package/package.json +1 -1
  65. package/templates/scaffold/scripts/fixture-server.mjs +11 -2
  66. package/templates/scaffold/src/flows/example.flow.ts +8 -6
  67. package/templates/scaffold/src/flows/mail-verify.flow.ts +8 -8
  68. package/templates/scaffold/src/flows/shop.flow.ts +6 -6
  69. package/templates/scaffold/src/{routes/(external)/docs/nav.block.ts → map/(app_base)/docs/_nav.block.ts} +5 -6
  70. package/templates/scaffold/src/{routes/(external)/docs/page.block.ts → map/(app_base)/docs/_page.block.ts} +2 -2
  71. package/templates/scaffold/src/{blocks/demo-web/methods → map/(app_base)/home/_methods}/add-item.effect.block.ts +3 -3
  72. package/templates/scaffold/src/{blocks/demo-web/methods → map/(app_base)/home/_methods}/assert-hello.method.block.ts +1 -1
  73. package/templates/scaffold/src/{blocks/demo-web/methods → map/(app_base)/home/_methods}/assert-item-added.method.block.ts +3 -3
  74. package/templates/scaffold/src/{blocks/demo-web/methods → map/(app_base)/home/_methods}/clear-cart.method.block.ts +9 -3
  75. package/templates/scaffold/src/{blocks/demo-web/methods → map/(app_base)/home/_methods}/remove-item.effect.block.ts +3 -3
  76. package/templates/scaffold/src/{blocks/demo-web/nav-home.block.ts → map/(app_base)/home/_nav.block.ts} +2 -2
  77. package/templates/scaffold/src/{blocks/demo-web/home.page.block.ts → map/(app_base)/home/_page.block.ts} +7 -7
  78. package/templates/scaffold/src/{blocks/demo-web → map/(app_base)/home}/nav-verification-link.block.ts +4 -4
  79. package/templates/scaffold/src/{blocks/demo-external/mailpit/methods → map/(external)/mailpit/_methods}/assert-email-content.method.block.ts +2 -2
  80. package/templates/scaffold/src/{blocks/demo-external/mailpit/methods → map/(external)/mailpit/_methods}/assert-email-received.method.block.ts +2 -2
  81. package/templates/scaffold/src/{blocks/demo-external/mailpit/methods → map/(external)/mailpit/_methods}/extract-email-link.method.block.ts +2 -2
  82. package/templates/scaffold/src/{blocks/demo-external/mailpit/methods → map/(external)/mailpit/_methods}/open-message.method.block.ts +2 -2
  83. package/templates/scaffold/src/{blocks/demo-external/mailpit/nav-mailpit-inbox.block.ts → map/(external)/mailpit/_nav.block.ts} +2 -2
  84. package/templates/scaffold/src/{blocks/demo-external/mailpit/mailpit-inbox.page.block.ts → map/(external)/mailpit/_page.block.ts} +5 -5
  85. package/templates/scaffold/src/states/demo.states.ts +2 -4
  86. package/templates/scaffold/tests/example.spec.ts +2 -2
  87. package/templates/scaffold/tests/mail-verify.spec.ts +1 -1
  88. package/templates/scaffold/src/blocks/SITE-MAP.md +0 -12
  89. package/templates/scaffold/src/blocks/demo-web/NAV.md +0 -29
  90. package/templates/scaffold/src/flows/routes-demo.flow.ts +0 -14
  91. /package/examples/routed-demo/src/{routes/(base_app)/dashboard/dashboard.sel.ts → map/(app_base)/dashboard/_sel.ts} +0 -0
  92. /package/examples/routed-demo/src/{routes/(external)/docs/docs.sel.ts → map/(external)/docs/_sel.ts} +0 -0
  93. /package/templates/scaffold/src/{routes/(external)/docs/docs.sel.ts → map/(app_base)/docs/_sel.ts} +0 -0
  94. /package/templates/scaffold/src/{blocks/demo-web → map/(app_base)/docs}/docs.html +0 -0
  95. /package/templates/scaffold/src/{blocks/demo-web/demo-sel.ts → map/(app_base)/home/_sel.ts} +0 -0
  96. /package/templates/scaffold/src/{blocks/demo-web → map/(app_base)/home}/home.html +0 -0
  97. /package/templates/scaffold/src/{blocks/demo-external/mailpit/mailpit-sel.ts → map/(external)/mailpit/_sel.ts} +0 -0
package/README.md CHANGED
@@ -138,6 +138,84 @@ structurally incapable of touching the browser or shared memory. `verify` runs o
138
138
  its own panel row; former FF inners keep blitz pacing so wall-clock stays comparable.
139
139
  `--ff-expand` is the older expand-only flag (same flatten).
140
140
 
141
+ ### Cross-cutting Layouts (`defineLayout`)
142
+
143
+ Real, direct request this responds to: persistent chrome (a sidebar, say) that's supposed
144
+ to stay on screen across most of an app shouldn't need its own `Trait` copy-pasted into
145
+ every single Checkpoint's `verify` - one missed copy and a regression silently ships. A
146
+ **Layout** is a `verify` that's enforced automatically, by Checkpoint tag, wherever it
147
+ applies - not once per Block:
148
+
149
+ ```ts
150
+ import { defineLayout, Engine } from "waygraph";
151
+
152
+ const AppShellLayout = defineLayout({
153
+ name: "AppShell",
154
+ // string[] of exact Checkpoint tags, or a (tag) => boolean predicate for a
155
+ // broader match (e.g. "every Checkpoint except the signed-out ones")
156
+ appliesTo: ["AppHome", "Chats", "Notifications", "Settings"],
157
+ verify: [Trait.visible(AppShellSel.sidebar)],
158
+ });
159
+
160
+ const engine = new Engine({ layouts: [AppShellLayout] });
161
+ ```
162
+
163
+ Every `Flow` this `Engine` defines - **and every intermediate Checkpoint inside it**, not
164
+ just the final one - runs `AppShellLayout.verify` automatically whenever it resolves to a
165
+ tag `appliesTo` matches, alongside that Block's own `verify`. `runGraph`/`Flow.run` also
166
+ take `{ layouts }` directly (`RunGraphOptions.layouts`), for driving a Layout outside an
167
+ `Engine`-owned Flow (e.g. `waygraph auto`'s own runtime pathfinding).
168
+
169
+ Deliberately matched by Checkpoint tag, never by folder path - a Layout works identically
170
+ whether a project uses the [Waygraph Map](#waygraph-map) convention or the older freeform
171
+ folder style, since some real consumers of this package still use the latter.
172
+
173
+ ### `map()` - a kind-checked, no-teleporting flow builder
174
+
175
+ `Engine.defineFlow([start, ...blocks, end])` already typechecks that each Block's `Out`
176
+ matches the next one's `In`. It does **not** stop an agent (or a careless human) from
177
+ handing it a hand-rolled plain object that merely *looks* like a Block - TypeScript's
178
+ structural typing can't tell a real `defineNavBlock(...)` output from a copy-pasted
179
+ object literal with the right shape. That gap is exactly what broke down in real
180
+ consumer projects: agents editing/hand-composing Blocks into ad hoc shapes, a "locks"
181
+ convention tried and still bypassed. `map()` closes it with an actual runtime check, not
182
+ just a convention:
183
+
184
+ ```ts
185
+ import { Engine } from "waygraph";
186
+
187
+ const engine = new Engine();
188
+ const flow = engine
189
+ .map({ homeOrigin: "https://app.example.com" })
190
+ .start()
191
+ .gotoPage(NavHomeBlock) // must be defineNavBlock / defineMemNavBlock / definePageBlock
192
+ .assert(AssertHelloBlock) // must be defineAssertBlock
193
+ .gotoExternal(NavMailpitBlock) // same kind requirement, plus the INVERSE origin check
194
+ .method(ClearCartBlock) // must be defineMethodBlock / defineActionBlock / defineEffectBlock
195
+ .end(); // -> a real Flow<Out> - withBlockVerify/withTitle/withHighlightFixtures/etc. all still apply
196
+ ```
197
+
198
+ Each step method is scoped to exactly the Block kind its name promises, checked against
199
+ the same `__waygraphKind`/`__waygraphSalt` runtime markers `waygraph check`/`graph`/`map`
200
+ already trust - a Block that didn't come from a real `define*Block` factory throws
201
+ immediately, naming the step and the factory it should have used, instead of silently
202
+ entering the chain. Give `homeOrigin` and `.gotoPage()`/`.gotoExternal()` also cross-check
203
+ each Nav/Page Block's own static `url` against it (skipped, honestly, for click-based or
204
+ mem-dependent nav - not statically checkable). The Checkpoint chain itself is still
205
+ typechecked exactly like `defineFlow`'s own tuple overloads - there is no method on this
206
+ builder that can skip from one Checkpoint to an unrelated one without a real, kind-correct
207
+ Block in between, which is what "no teleporting" means here.
208
+
209
+ `map()` is sugar over `defineFlow`, not a new execution engine - the result is a real
210
+ `Flow`, so every existing Flow-level feature (Layouts, `withBlockVerify`, demo narration,
211
+ `chainFlow`) works on it unchanged. `import { map } from "waygraph"` is also available
212
+ standalone (`map(options)` ~= `new Engine(options).map(options)`) for a call site that
213
+ doesn't otherwise need its own `Engine` instance. **Recommended for new projects** over
214
+ hand-assembled `defineFlow([start, ...])` arrays - the freeform array form still works
215
+ and isn't going away (real consumers on the older folder convention depend on it), but
216
+ `map()` is the one that actually stops a broken/hacked Block from entering a flow
217
+ unnoticed.
218
+
141
219
  ### Traits beyond the page
142
220
 
143
221
  A hand-written `Trait`'s `check(page, mem)` receives `mem` too, for the same-Checkpoint
@@ -324,7 +402,7 @@ export const AssertItemAddedBlock = defineAssertBlock<ItemInCart>({
324
402
  engine.defineFlow([start, NavHomeBlock, AddItemBlock, AssertItemAddedBlock, ClearCartBlock, end])
325
403
  ```
326
404
 
327
- Live reference: `templates/scaffold/src/blocks/demo-web/methods/assert-item-added.method.block.ts`,
405
+ Live reference: `templates/scaffold/src/map/(app_base)/home/_methods/assert-item-added.method.block.ts`,
328
406
  wired into `shop.flow.ts`.
329
407
 
330
408
  **`methods/` folder:** on-page work lives next to the route, not free-floating.
@@ -439,7 +517,7 @@ preflight reject the run before it ever gets a chance to produce it - `requires`
439
517
  keys a caller truly must supply from outside the chain (like `ExpectedRecipient` above).
440
518
 
441
519
  Live reference, proven end to end against a real, throwaway Mailpit container:
442
- `templates/scaffold/src/blocks/demo-external/mailpit/` + `src/flows/mail-verify.flow.ts` +
520
+ `templates/scaffold/src/map/(external)/mailpit/` + `src/flows/mail-verify.flow.ts` +
443
521
  `tests/mail-verify.spec.ts`.
444
522
 
445
523
  ### Reusable across scenarios: mem-driven, not one Block per email
@@ -720,27 +798,46 @@ recurse through any folder structure, matching only the `.block.ts` filename pat
720
798
  naming and depth are already 100% cosmetic to every existing command.
721
799
 
722
800
  ```text
723
- src/routes/
724
- (base_app)/ # purely organizational - never part of any Checkpoint tag
725
- dashboard/ # one folder per Checkpoint - "Dashboard"
726
- page.block.ts # arrival hub (definePageBlock) - fixed name, every folder
727
- nav.block.ts # navigation (defineNavBlock) - fixed name, every folder
728
- dashboard.sel.ts
729
- methods/
730
- click-widget.block.ts
731
- (external)/
732
- docs/ # a different group, same shape - "Docs"
733
- page.block.ts
734
- nav.block.ts
735
- docs.sel.ts
801
+ src/map/
802
+ (app_base)/ # the landing/base group - purely organizational, never part of
803
+ dashboard/ # any Checkpoint tag. One folder per Checkpoint - "Dashboard"
804
+ _page.block.ts # arrival hub (definePageBlock) - fixed name, every folder
805
+ _nav.block.ts # navigation (defineNavBlock) - fixed name, every folder
806
+ _sel.ts # selectors for this Checkpoint - fixed name, every folder
807
+ _methods/ # this Checkpoint's own action Blocks - fixed name, every
808
+ click-widget.block.ts # folder. Contents keep their own descriptive names -
809
+ (external)/ # only the folder itself is fixed.
810
+ docs/ # a genuinely cross-origin group, same shape - "Docs"
811
+ _page.block.ts
812
+ _nav.block.ts
813
+ _sel.ts
736
814
  ```
737
815
 
738
- Same Block helpers as today's freeform "manual mode" (`definePageBlock`/`defineNavBlock`/
739
- `defineMethodBlock`/etc.) - this only changes *where files live and what they're named*,
740
- never what kind of Block authors them or how they execute. A project adopts it incrementally,
741
- folder by folder; the two styles compose in the same flow with no special-casing (proven in
742
- `templates/scaffold`'s own `routes-demo.flow.ts`: `nav-home` from `src/blocks/`, manual mode,
743
- into `nav-docs` from `src/routes/`, Map convention, one real navigation).
816
+ **Not just a naming preference - a forced convention.** Folder nesting under `map/(group)/`
817
+ must **verbatim-match** the real site's own URL path segments, `(group)` parens excluded
818
+ (purely organizational, same as a Next.js route group): `/app/chats` lives at
819
+ `map/(app_base)/app/chats/`, never `map/(auth)/chats/` for a fabricated grouping that was
820
+ never a real `/auth/*` URL. `waygraph map [project]` (see [CLI reference](#cli-reference))
821
+ enforces this mechanically and exits 1 on a mismatch - don't rely on review alone. Same Block
822
+ helpers as today's freeform "manual mode" (`definePageBlock`/`defineNavBlock`/
823
+ `defineMethodBlock`/etc.) either way - Map only changes *where files live and what they're
824
+ named*, never what kind of Block authors them or how they execute. A project adopts it
825
+ incrementally, folder by folder. Prefer [`map()`](#map---a-kind-checked-no-teleporting-flow-builder)
826
+ over a hand-assembled `defineFlow([start, ...])` array when wiring these Blocks into a Flow -
827
+ it's the one that can't silently accept a Block that didn't come from a real factory.
828
+
829
+ **Leading underscore = this Checkpoint's own fixed machinery, never a route segment.**
830
+ `_nav.block.ts`/`_page.block.ts`/`_sel.ts`/`_methods/` (same private-folder idea as Next.js's
831
+ own `_folder` convention) mark "this belongs to the endpoint whose folder it's directly in,"
832
+ so a bare (non-underscore, non-`(group)`) folder name always means a real child
833
+ Checkpoint/URL segment - `_methods/` can never be mistaken for a sibling endpoint folder the
834
+ way a bare `methods/` structurally could be, since only ONE of those two readings is
835
+ mechanically distinguishable at a glance. `waygraph map`'s own folder-vs-URL comparison
836
+ excludes underscore segments the same way it excludes `(group)` ones - `_methods/foo.block.ts`
837
+ compares against the URL as if `_methods/` weren't there at all. Individual files inside
838
+ `_methods/` keep their own descriptive names (`join-pool.method.block.ts`, not
839
+ `_join-pool...`) - they're hand-authored per-project content, not fixed convention names; the
840
+ `_methods/` folder boundary alone already marks the whole subtree as non-route.
744
841
 
745
842
  **"waypack" - loading another project's Blocks needs no separate manifest either.** A second
746
843
  project depending on a Map-convention project as a plain `file:` dependency (the same
@@ -763,6 +860,58 @@ Full design history (why "Router" isn't a separate thing to build, two prior wro
763
860
  of this before landing here, and everything above stated as requirements) in
764
861
  `openspec/changes/waygraph-map/`.
765
862
 
863
+ ## Migrating from freeform ("router") to Waygraph Map
864
+
865
+ "Router" isn't a separate mode to turn off - it's this project's own name for today's
866
+ freeform, folder-organized style (`src/blocks/<slug>/`, any nesting, any file names).
867
+ Migrating means moving those files under `src/map/(group)/<page-slug>/` with verbatim
868
+ URL-matching names, nothing more - no engine change, no new Block helpers, no flag to flip.
869
+ Do it incrementally: the two styles coexist in the same project (and even the same Flow) with
870
+ no special-casing, since `discoverGraph`/`loadBlockLibrary` only ever match the `.block.ts`
871
+ filename pattern, never folder depth or naming.
872
+
873
+ 1. **List every real URL your NavBlocks/PageBlocks already navigate to.** For each one with a
874
+ static string `url`, note its real path. For each one that's `click`-based (no `url` field
875
+ at all) or a dynamic `(mem) => string` function, note that too - neither is statically
876
+ checkable by `waygraph map`, so they can go wherever makes sense; only a literal string
877
+ `url` is verified.
878
+ 2. **Pick your group(s).** `(app_base)/` for the landing/base app (organizational only, never
879
+ part of a Checkpoint tag); `(external)/` for genuinely cross-origin Blocks (mail tools,
880
+ third-party embeds) - verify with the real URL, not the folder's own current name, since a
881
+ folder can already be mis-grouped (a real bug this session found: a project's own
882
+ `(external)/docs/` was actually same-origin the whole time).
883
+ 3. **Create `src/map/(group)/<verbatim-url-segments>/` per Checkpoint**, `git mv` each
884
+ Block/Sel file in, and rename the fixed ones: `nav.block.ts` -> `_nav.block.ts`,
885
+ `page.block.ts` -> `_page.block.ts`, your selector file -> `_sel.ts` (drops its slug - now a
886
+ fully fixed name), `methods/` -> `_methods/` (contents keep their own names unchanged).
887
+ **Two Checkpoints sharing one real URL** (a DOM-state distinction, not a URL one - e.g. a
888
+ "joined" vs "not joined" variant of the same page) can't both use the bare fixed names in
889
+ one folder; give each pair a slug prefix instead (`app-home._nav.block.ts` /
890
+ `in-pool._nav.block.ts`, or similar) rather than nesting either under its own subfolder
891
+ (that would add a folder segment the real URL doesn't have, and fail step 5).
892
+ 4. **A dynamic per-instance URL** (a real chat/order/ticket id, not a fixed route) - if it's
893
+ currently a plain string constant, wrap it as `url: () => thatConstant` instead. A bare
894
+ string is always treated as a real, checkable static route; a function is correctly treated
895
+ as dynamic/non-canonical and skipped, matching how you'd actually navigate a real instance of
896
+ it in practice anyway.
897
+ 5. **Fix every import** the moves break (states/mem-key paths change depth; sibling
898
+ nav/page/methods imports need their new `_`-prefixed names) - let the compiler do this for
899
+ you: `tsc --noEmit` if the project has one, otherwise `waygraph check [project]` and
900
+ `waygraph graph [project]` (both do real dynamic imports of every Block file and report
901
+ import failures by name) are just as effective a proof.
902
+ 6. **Run `waygraph map [project]`.** Exit 0 / "0 violations" is the actual finish line - not a
903
+ subjective review. A remaining violation names the exact file, its real folder path, and the
904
+ real URL it doesn't match; fix the folder, not the check.
905
+ 7. **Diff `waygraph graph`'s node/edge/skipped/orphan counts against before the move.** A pure
906
+ migration changes zero Block behavior - if a count changed, something broke in transit
907
+ (a bad import silently dropped a Block from discovery is the usual cause), not something to
908
+ wave off as "close enough."
909
+
910
+ Once the folder structure is settled, prefer building/rebuilding Flows over these Blocks with
911
+ [`map()`](#map---a-kind-checked-no-teleporting-flow-builder) instead of a hand-assembled
912
+ `defineFlow([start, ...])` array - it's the one that can't silently accept a Block that isn't
913
+ really a Nav/Page/Assert/Method from this package's own factories.
914
+
766
915
  ## Getting started (pick one)
767
916
 
768
917
  | Goal | Command |
@@ -881,6 +1030,7 @@ Flows are files (0.10.5+):
881
1030
  | Detached session with a real visible browser | `waygraph auto --cli --detach --non-headless` |
882
1031
  | Read the session's Checkpoint/Block history | `waygraph auto trace <sessionId>` |
883
1032
  | Whole-project nav-escape + inline-selector + orphan sweep | `waygraph check [project]` |
1033
+ | Waygraph Map enforcement (verbatim url-vs-folder, exits 1 on a violation) | `waygraph map [project]` |
884
1034
  | Graph crawl | `waygraph traverse [project] --parallel N --min-edge-coverage 80%` |
885
1035
  | Coding-agent defs | `waygraph agent-dive --loop claude` |
886
1036
  | One-shot temp-dir demo | `waygraph try demo` / `waygraph try auto` / `waygraph try auto:cli` |
package/dist/cli.js CHANGED
@@ -18,6 +18,7 @@ import { tmpdir } from "node:os";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { spawn } from "node:child_process";
20
20
  import { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
21
+ import { checkMap } from "./map-check.js";
21
22
  import { runAutoExplore } from "./auto-explore-run.js";
22
23
  import { pilotStart } from "./pilot.js";
23
24
  import { spawnDetachedSession, requestSession, runAttachLoop, runAutoServeCommand, } from "./auto-session-ipc.js";
@@ -6971,6 +6972,12 @@ Primary (less is more):
6971
6972
 
6972
6973
  Also:
6973
6974
  waygraph list | nav | validate | check | graph | init <name>
6975
+ waygraph map [project] Waygraph Map convention enforcement: every static
6976
+ Nav/Page url must verbatim-match its src/map/ folder
6977
+ path ((group) segments excluded) - exits 1 on a
6978
+ violation, unlike check's warnings-only stance. A
6979
+ no-op (exit 0, informational) if the project has no
6980
+ src/map/ directory at all.
6974
6981
  waygraph traverse [project] Graph crawl (Phase B-E)
6975
6982
  --blocks <glob|/re/|sub> Phase C: filter *.block.ts discovery
6976
6983
  --parallel N Phase D: N clone workers (max 4)
@@ -7801,6 +7808,32 @@ Agents shipped: waygraph-planner, waygraph-author, waygraph-healer.
7801
7808
  // human/agent cleanup; chain auto refuses while any remain.
7802
7809
  break;
7803
7810
  }
7811
+ case "map": {
7812
+ const proj = resolve(args[1] ?? process.cwd());
7813
+ const result = await checkMap(proj);
7814
+ if (!result.hasMapDir) {
7815
+ console.log(`waygraph map: no src/map/ directory under ${proj} - nothing to check ` +
7816
+ "(this project isn't on the Waygraph Map convention, or hasn't been migrated yet)");
7817
+ break;
7818
+ }
7819
+ console.log(`waygraph map: ${result.nodes.length} node(s) found under ${relative(proj, result.mapRoot)}`);
7820
+ if (result.violations.length === 0) {
7821
+ console.log("waygraph map: 0 violations - every static Nav/Page url verbatim-matches its folder path");
7822
+ }
7823
+ else {
7824
+ for (const v of result.violations) {
7825
+ console.error(`waygraph map: ${v.file} (${v.block}) - ${v.reason}`);
7826
+ }
7827
+ console.error(`waygraph map: ${result.violations.length} violation${result.violations.length === 1 ? "" : "s"}`);
7828
+ // Unlike `check`'s warnings-only stance, a Map violation is exactly
7829
+ // the class of bug this command exists to catch (a fabricated
7830
+ // folder grouping that never matched the real site, e.g. the real
7831
+ // (auth)/signin/ mistake this command's own header comment cites) -
7832
+ // fail loud, not just warn.
7833
+ process.exitCode = 1;
7834
+ }
7835
+ break;
7836
+ }
7804
7837
  default:
7805
7838
  console.error(`waygraph: unknown command "${command}"`);
7806
7839
  process.exit(1);
package/dist/engine.d.ts CHANGED
@@ -13,6 +13,52 @@ import { type HighlightStyleDefaults, type DemoPace } from "./highlights.js";
13
13
  * since those depend on a runtime decision that hasn't happened yet.
14
14
  */
15
15
  export declare function preflight(mem: MemPage, block: Block<any, any>): void;
16
+ /**
17
+ * A cross-cutting, automatically-enforced verify - the real engine
18
+ * primitive behind "persistent UI" (a sidebar, a header, anything that's
19
+ * supposed to be present on many/most Checkpoints, not just one page's own
20
+ * Block). Deliberately NOT tied to any folder convention - `appliesTo`
21
+ * matches by Checkpoint tag, explicitly, so this works the same whether a
22
+ * project uses the Waygraph Map folder convention or the older freeform
23
+ * "manual mode" layout real production consumers (pia-waygraph,
24
+ * zsign-atomic-waygraph) already use - registering a Layout with an Engine
25
+ * is the only requirement, folder structure is irrelevant to it.
26
+ *
27
+ * Not a Block: no `act`/`resolve`/Checkpoint of its own, never a graph
28
+ * node - it only ever adds extra `verify` Traits to Checkpoints something
29
+ * else already resolved to, the same way `runVerify` already runs after
30
+ * any Block's own `resolve()` (see `runGraph`'s own verify step, just
31
+ * below) - a Layout is that mechanism made reusable across many
32
+ * Checkpoints instead of hand-repeated per Block.
33
+ */
34
+ export interface Layout {
35
+ name: string;
36
+ description?: string;
37
+ appliesTo: (tag: string) => boolean;
38
+ /** Function form gets the full resolved Checkpoint, same shape `runVerify`/a Block's own `verify` already use - not just the tag. */
39
+ verify: Trait[] | ((out: Checkpoint<string>) => Trait[]);
40
+ }
41
+ export interface LayoutOptions {
42
+ name: string;
43
+ description?: string;
44
+ /** A fixed tag list, or a predicate for open-ended matching (e.g. a naming pattern). */
45
+ appliesTo: readonly string[] | ((tag: string) => boolean);
46
+ verify: Trait[] | ((out: Checkpoint<string>) => Trait[]);
47
+ }
48
+ /**
49
+ * @example
50
+ * const AppShellLayout = defineLayout({
51
+ * name: "app-shell",
52
+ * appliesTo: ["AppHome", "Chats", "Notifications", "Settings"],
53
+ * verify: [Trait.visible(SidebarSel.root)],
54
+ * });
55
+ * const engine = new Engine({ layouts: [AppShellLayout] });
56
+ * // Now every Block resolving to one of those four Checkpoints, in any
57
+ * // Flow this engine defines, also gets AppShellLayout's own verify run
58
+ * // automatically - a page that silently drops the sidebar fails loud,
59
+ * // even though no individual Block's own `verify` mentions it.
60
+ */
61
+ export declare function defineLayout(options: LayoutOptions): Layout;
16
62
  /**
17
63
  * Runs `entry` against a fresh tab and returns the resulting terminal Checkpoint.
18
64
  * The engine owns the tab's lifecycle, not any individual Block: the tab is
@@ -52,6 +98,14 @@ export interface RunGraphOptions {
52
98
  * `page` and `closeOnFinish` together) - it just does what you ask.
53
99
  */
54
100
  closeOnFinish?: boolean;
101
+ /**
102
+ * Layouts (see {@link defineLayout}) whose `verify` runs automatically
103
+ * after any Block in this run resolves to a Checkpoint they apply to -
104
+ * `Flow.run` threads this in from `EngineConfig.layouts` by default; pass
105
+ * it directly here to call `runGraph` standalone (outside a `Flow`) with
106
+ * layouts still enforced.
107
+ */
108
+ layouts?: readonly Layout[];
55
109
  }
56
110
  /**
57
111
  * Drives a genuinely separate second tab through its own Block/Flow, in the
@@ -435,6 +489,14 @@ export interface EngineConfig {
435
489
  height: number;
436
490
  };
437
491
  };
492
+ /**
493
+ * Cross-cutting, automatically-enforced verify (see {@link defineLayout})
494
+ * - applies to every Flow this Engine defines. Works the same regardless
495
+ * of folder convention (Waygraph Map or freeform "manual mode") since
496
+ * matching is by Checkpoint tag, never by file path.
497
+ * @example new Engine({ layouts: [AppShellLayout] })
498
+ */
499
+ layouts?: readonly Layout[];
438
500
  }
439
501
  type S = Checkpoint<"__start__">;
440
502
  /**
@@ -595,7 +657,106 @@ export declare class Engine {
595
657
  Block<N, O>,
596
658
  EndMarker
597
659
  ]): Flow<O>;
660
+ /**
661
+ * Fluent, kind-checked alternative to `defineFlow([start, ...blocks, end])` -
662
+ * see {@link MapBuilder}. Real, direct request this responds to: pia/zsign's
663
+ * own agents kept hand-editing/hand-composing Blocks into ad hoc shapes
664
+ * ("locks" convention tried, still got broken) - `map()` forces every step
665
+ * through this Engine's own `define*Block` factories (checked by the same
666
+ * `__waygraphKind`/`__waygraphSalt` runtime markers `graph.ts`/`map-check.ts`
667
+ * already trust), so a hand-rolled plain-object Block can never silently
668
+ * pass as a real navigation/assertion/method step. `homeOrigin`, if given,
669
+ * also gates `.gotoPage()`/`.gotoExternal()` against each Nav/Page Block's
670
+ * own static `url` (skipped, honestly, for click-based/dynamic nav - not
671
+ * statically checkable, same limitation `map-check.ts` already documents).
672
+ * @example
673
+ * const flow = engine.map({ homeOrigin: "https://app.example.com" })
674
+ * .start()
675
+ * .gotoPage(NavHomeBlock)
676
+ * .assert(AssertHelloBlock)
677
+ * .gotoExternal(NavMailpitBlock)
678
+ * .end();
679
+ */
680
+ map(options?: MapBuilderOptions): MapBuilder<S>;
598
681
  }
682
+ export interface MapBuilderOptions {
683
+ /**
684
+ * This project's own origin (e.g. `"https://app.example.com"`) - enables
685
+ * `.gotoPage()`/`.gotoExternal()`'s origin check against a Block's static
686
+ * `url`. Omit to skip that check entirely (still fully kind-checked either
687
+ * way; only the internal/external origin distinction is opt-in).
688
+ */
689
+ homeOrigin?: string;
690
+ }
691
+ /**
692
+ * Fluent builder over {@link Engine.defineFlow} - see `map()`'s own doc
693
+ * comment for why this exists. Each step method is scoped to exactly the
694
+ * Block kind its name promises, checked at RUNTIME against the
695
+ * `__waygraphKind`/`__waygraphSalt` markers `defineNavBlock`/`definePageBlock`/
696
+ * `defineAssertBlock`/`defineMethodBlock`/`defineEffectBlock` already set
697
+ * (the same markers `graph.ts` and `map-check.ts` trust) - not just a type
698
+ * hint, since a hand-rolled object can satisfy the TypeScript `Block<In,Out>`
699
+ * shape without ever going through a real factory. Each call also
700
+ * typechecks the accumulated Checkpoint chain exactly like `defineFlow`'s
701
+ * own tuple overloads do (a step's `In` must equal the previous step's
702
+ * `Out`) - this is what "no teleporting" means: there is no method on this
703
+ * builder that can skip from one Checkpoint to an unrelated one without a
704
+ * real, kind-correct Block in between.
705
+ */
706
+ export declare class MapBuilder<Out extends Checkpoint<string>> {
707
+ private readonly engine;
708
+ private readonly steps;
709
+ private readonly homeOrigin;
710
+ private constructor();
711
+ /** @internal - use `engine.map()` or the standalone `map()` export. */
712
+ static begin(engine: Engine, homeOrigin: string | undefined): MapBuilder<S>;
713
+ /** Readable chain-opener - mirrors `defineFlow`'s leading `start` sentinel. Returns `this` unchanged; entirely optional. */
714
+ start(): MapBuilder<Out>;
715
+ /**
716
+ * Appends an internal navigation/page-arrival step - requires a Block from
717
+ * `defineNavBlock`/`defineMemNavBlock`/`definePageBlock`. Throws if given
718
+ * anything else, including a `homeOrigin`-mismatched static `url`.
719
+ */
720
+ gotoPage<NextOut extends Checkpoint<string>>(block: Block<Out, NextOut> & {
721
+ name: string;
722
+ }): MapBuilder<NextOut>;
723
+ /**
724
+ * Appends a genuinely cross-origin navigation step (e.g. `(external)/`
725
+ * Blocks in the Waygraph Map convention: mailpit, maildrop.cc) - same
726
+ * kind requirement as {@link gotoPage}, plus the inverse `homeOrigin` check.
727
+ */
728
+ gotoExternal<NextOut extends Checkpoint<string>>(block: Block<Out, NextOut> & {
729
+ name: string;
730
+ }): MapBuilder<NextOut>;
731
+ /** Appends a self-loop verification step - requires a Block from `defineAssertBlock`. */
732
+ assert<NextOut extends Checkpoint<string>>(block: Block<Out, NextOut> & {
733
+ name: string;
734
+ }): MapBuilder<NextOut>;
735
+ /**
736
+ * Appends a non-navigating page action (submit, upload, add/remove an
737
+ * instance) - requires a Block from `defineMethodBlock`/`defineActionBlock`/
738
+ * `defineEffectBlock`/`defineMemEffectBlock`.
739
+ */
740
+ method<NextOut extends Checkpoint<string>>(block: Block<Out, NextOut> & {
741
+ name: string;
742
+ }): MapBuilder<NextOut>;
743
+ /**
744
+ * Finalizes this chain into a real, runnable {@link Flow} - same object
745
+ * `defineFlow` returns, so `withBlockVerify`/`modBlockVerify` and every
746
+ * `withTitle`/`withHighlightFixtures`/etc. decorator still apply exactly
747
+ * as they do today; this builder only changes how the chain is assembled,
748
+ * never what it produces. Throws if no step was ever added - an empty map
749
+ * isn't a flow.
750
+ */
751
+ end(): Flow<Out>;
752
+ }
753
+ /**
754
+ * Standalone convenience for `new Engine(config).map(options)` - use this
755
+ * when a call site doesn't otherwise need its own `Engine` instance (no
756
+ * shared `headless`/`slowMo`/`layouts` across several flows).
757
+ * @example const flow = map({ homeOrigin: "https://app.example.com" }).gotoPage(NavHomeBlock).end();
758
+ */
759
+ export declare function map(options?: MapBuilderOptions & EngineConfig): MapBuilder<S>;
599
760
  /**
600
761
  * Attaches routing to `block` without touching its act/observe/resolve/verify -
601
762
  * `routes` must cover every tag of `block`'s output (a missing route is a compile