waygraph 0.14.0 → 0.15.1

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 +101 -5
  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/dist/engine.js CHANGED
@@ -34,6 +34,46 @@ export function preflight(mem, block) {
34
34
  .join(", ")}`);
35
35
  }
36
36
  }
37
+ /**
38
+ * @example
39
+ * const AppShellLayout = defineLayout({
40
+ * name: "app-shell",
41
+ * appliesTo: ["AppHome", "Chats", "Notifications", "Settings"],
42
+ * verify: [Trait.visible(SidebarSel.root)],
43
+ * });
44
+ * const engine = new Engine({ layouts: [AppShellLayout] });
45
+ * // Now every Block resolving to one of those four Checkpoints, in any
46
+ * // Flow this engine defines, also gets AppShellLayout's own verify run
47
+ * // automatically - a page that silently drops the sidebar fails loud,
48
+ * // even though no individual Block's own `verify` mentions it.
49
+ */
50
+ export function defineLayout(options) {
51
+ const appliesTo = typeof options.appliesTo === "function"
52
+ ? options.appliesTo
53
+ : (tag) => options.appliesTo.includes(tag);
54
+ return {
55
+ name: options.name,
56
+ ...(options.description ? { description: options.description } : {}),
57
+ appliesTo,
58
+ verify: options.verify,
59
+ };
60
+ }
61
+ /**
62
+ * Runs every registered layout whose `appliesTo(tag)` matches the just-
63
+ * resolved Checkpoint, in registration order, right after the Block's own
64
+ * `verify` already ran. A layout's own Trait failing throws the same way
65
+ * any other `runVerify` failure does, naming the layout so it's clearly
66
+ * distinguishable from the Block's own verify in the error message.
67
+ */
68
+ async function runLayouts(layouts, tag, out, page, mem, blockName) {
69
+ if (!layouts)
70
+ return;
71
+ for (const layout of layouts) {
72
+ if (!layout.appliesTo(tag))
73
+ continue;
74
+ await runVerify(layout.verify, out, page, mem, `${blockName} (layout: "${layout.name}")`);
75
+ }
76
+ }
37
77
  /**
38
78
  * Runs `entry` against a fresh tab and returns the resulting terminal Checkpoint.
39
79
  * The engine owns the tab's lifecycle, not any individual Block: the tab is
@@ -72,6 +112,7 @@ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000,
72
112
  : undefined;
73
113
  const checkpoint = await current.instruction.resolve(observed);
74
114
  await runVerify(current.instruction.verify, checkpoint, page, mem, current.name);
115
+ await runLayouts(options?.layouts, checkpoint.__state, checkpoint, page, mem, current.name);
75
116
  const next = current.next?.(checkpoint);
76
117
  if (!next) {
77
118
  if (terminals && !terminals.has(checkpoint.__state)) {
@@ -218,7 +259,11 @@ function buildFlow(middle, engineConfig) {
218
259
  // withSessionReset()/withTitle() instead (non-destructive decorators,
219
260
  // same pattern as withVerify), which also re-apply themselves after any
220
261
  // further withBlockVerify/modBlockVerify patch so the flag survives.
221
- const chain = middle.reduce((a, b) => connect(a, b));
262
+ // Layouts have to be threaded into every pairwise connect(), not just the
263
+ // trailing runGraph call - see connect()'s own doc comment in types.ts:
264
+ // buildFlow's reduce IS the only place a defineFlow-built Flow's
265
+ // intermediate (non-final) Checkpoints are ever verified at all.
266
+ const chain = middle.reduce((a, b) => connect(a, b, engineConfig.layouts));
222
267
  // Declared separately (not as the object-literal method below) so the
223
268
  // three-overload `Flow.run` signature can be checked against the actual
224
269
  // per-call-shape behavior, then attached with a cast - object-literal
@@ -248,7 +293,9 @@ function buildFlow(middle, engineConfig) {
248
293
  try {
249
294
  const context = await browser.newContext(config.recordVideo ? { recordVideo: config.recordVideo } : {});
250
295
  try {
251
- return await runGraph(chain, undefined, context, mem);
296
+ return await runGraph(chain, undefined, context, mem, 5000, {
297
+ ...(config.layouts !== undefined ? { layouts: config.layouts } : {}),
298
+ });
252
299
  }
253
300
  finally {
254
301
  // Playwright finalizes recordVideo on context.close(); skip when
@@ -277,9 +324,11 @@ function buildFlow(middle, engineConfig) {
277
324
  // call from before this option existed. An explicit `closeOnFinish`
278
325
  // always wins either way.
279
326
  const closeOnFinish = options?.closeOnFinish ?? gotOwnPage;
327
+ const resolvedLayouts = options?.layouts ?? engineConfig.layouts;
280
328
  const result = await runGraph(chain, undefined, context, mem, 5000, {
281
329
  page,
282
330
  closeOnFinish: false,
331
+ ...(resolvedLayouts !== undefined ? { layouts: resolvedLayouts } : {}),
283
332
  });
284
333
  if (closeOnFinish) {
285
334
  await page.close();
@@ -655,6 +704,185 @@ export class Engine {
655
704
  defineFlow(blocks) {
656
705
  return buildFlow(blocks.slice(1, -1), this.config);
657
706
  }
707
+ /**
708
+ * Fluent, kind-checked alternative to `defineFlow([start, ...blocks, end])` -
709
+ * see {@link MapBuilder}. Real, direct request this responds to: pia/zsign's
710
+ * own agents kept hand-editing/hand-composing Blocks into ad hoc shapes
711
+ * ("locks" convention tried, still got broken) - `map()` forces every step
712
+ * through this Engine's own `define*Block` factories (checked by the same
713
+ * `__waygraphKind`/`__waygraphSalt` runtime markers `graph.ts`/`map-check.ts`
714
+ * already trust), so a hand-rolled plain-object Block can never silently
715
+ * pass as a real navigation/assertion/method step. `homeOrigin`, if given,
716
+ * also gates `.gotoPage()`/`.gotoExternal()` against each Nav/Page Block's
717
+ * own static `url` (skipped, honestly, for click-based/dynamic nav - not
718
+ * statically checkable, same limitation `map-check.ts` already documents).
719
+ * @example
720
+ * const flow = engine.map({ homeOrigin: "https://app.example.com" })
721
+ * .start()
722
+ * .gotoPage(NavHomeBlock)
723
+ * .assert(AssertHelloBlock)
724
+ * .gotoExternal(NavMailpitBlock)
725
+ * .end();
726
+ */
727
+ map(options) {
728
+ return MapBuilder.begin(this, options?.homeOrigin);
729
+ }
730
+ }
731
+ function mapKindOf(block) {
732
+ return block?.__waygraphKind;
733
+ }
734
+ function mapSaltOf(block) {
735
+ return block?.__waygraphSalt;
736
+ }
737
+ function mapNavUrlOf(block) {
738
+ const url = block?.__waygraphNavUrl;
739
+ return typeof url === "string" ? url : undefined;
740
+ }
741
+ const MAP_KIND_FACTORY_HINT = {
742
+ nav: "defineNavBlock/defineMemNavBlock",
743
+ page: "definePageBlock",
744
+ assert: "defineAssertBlock",
745
+ };
746
+ const MAP_SALT_FACTORY_HINT = {
747
+ method: "defineMethodBlock",
748
+ effect: "defineEffectBlock",
749
+ };
750
+ function assertMapKind(block, allowed, method) {
751
+ const kind = mapKindOf(block);
752
+ if (kind !== undefined && allowed.includes(kind))
753
+ return;
754
+ const wanted = allowed.map((k) => MAP_KIND_FACTORY_HINT[k] ?? k).join(" or ");
755
+ const found = kind ? `a Block of kind "${kind}"` : "an object with no waygraph kind marker at all";
756
+ throw new Error(`Waygraph map: .${method}("${block?.name ?? "?"}") requires a Block built with ${wanted} ` +
757
+ `(found ${found} - a hand-built plain object doesn't count). This check is the whole point ` +
758
+ "of the map() builder: only real Blocks from waygraph's own factories can enter a chain.");
759
+ }
760
+ function assertMapSalt(block, allowed, method) {
761
+ const salt = mapSaltOf(block);
762
+ if (salt !== undefined && allowed.includes(salt))
763
+ return;
764
+ const wanted = allowed.map((s) => MAP_SALT_FACTORY_HINT[s] ?? s).join(" or ");
765
+ const found = salt ? `a Block salted "${salt}"` : "an object with no waygraph salt marker at all";
766
+ throw new Error(`Waygraph map: .${method}("${block?.name ?? "?"}") requires a Block built with ${wanted} ` +
767
+ `(found ${found} - a hand-built plain object doesn't count). This check is the whole point ` +
768
+ "of the map() builder: only real Blocks from waygraph's own factories can enter a chain.");
769
+ }
770
+ function assertMapOrigin(block, homeOrigin, expect, method) {
771
+ if (!homeOrigin)
772
+ return; // not configured - can't validate, honest no-op
773
+ const navUrl = mapNavUrlOf(block);
774
+ if (navUrl === undefined)
775
+ return; // click-based/dynamic nav - not statically checkable
776
+ let targetOrigin;
777
+ let wantOrigin;
778
+ try {
779
+ targetOrigin = new URL(navUrl, homeOrigin).origin;
780
+ wantOrigin = new URL(homeOrigin).origin;
781
+ }
782
+ catch {
783
+ return; // malformed URL - not this check's job to validate that
784
+ }
785
+ const isExternal = targetOrigin !== wantOrigin;
786
+ if (expect === "internal" && isExternal) {
787
+ throw new Error(`Waygraph map: .gotoPage("${block.name}") targets ${targetOrigin}, which is NOT this map's ` +
788
+ `home origin (${wantOrigin}) - use .gotoExternal() for a genuinely cross-origin destination.`);
789
+ }
790
+ if (expect === "external" && !isExternal) {
791
+ throw new Error(`Waygraph map: .gotoExternal("${block.name}") targets ${targetOrigin}, which IS this map's ` +
792
+ `home origin (${wantOrigin}) - use .gotoPage() for an internal destination.`);
793
+ }
794
+ }
795
+ /**
796
+ * Fluent builder over {@link Engine.defineFlow} - see `map()`'s own doc
797
+ * comment for why this exists. Each step method is scoped to exactly the
798
+ * Block kind its name promises, checked at RUNTIME against the
799
+ * `__waygraphKind`/`__waygraphSalt` markers `defineNavBlock`/`definePageBlock`/
800
+ * `defineAssertBlock`/`defineMethodBlock`/`defineEffectBlock` already set
801
+ * (the same markers `graph.ts` and `map-check.ts` trust) - not just a type
802
+ * hint, since a hand-rolled object can satisfy the TypeScript `Block<In,Out>`
803
+ * shape without ever going through a real factory. Each call also
804
+ * typechecks the accumulated Checkpoint chain exactly like `defineFlow`'s
805
+ * own tuple overloads do (a step's `In` must equal the previous step's
806
+ * `Out`) - this is what "no teleporting" means: there is no method on this
807
+ * builder that can skip from one Checkpoint to an unrelated one without a
808
+ * real, kind-correct Block in between.
809
+ */
810
+ export class MapBuilder {
811
+ engine;
812
+ steps;
813
+ homeOrigin;
814
+ constructor(engine, steps, homeOrigin) {
815
+ this.engine = engine;
816
+ this.steps = steps;
817
+ this.homeOrigin = homeOrigin;
818
+ }
819
+ /** @internal - use `engine.map()` or the standalone `map()` export. */
820
+ static begin(engine, homeOrigin) {
821
+ return new MapBuilder(engine, [], homeOrigin);
822
+ }
823
+ /** Readable chain-opener - mirrors `defineFlow`'s leading `start` sentinel. Returns `this` unchanged; entirely optional. */
824
+ start() {
825
+ return this;
826
+ }
827
+ /**
828
+ * Appends an internal navigation/page-arrival step - requires a Block from
829
+ * `defineNavBlock`/`defineMemNavBlock`/`definePageBlock`. Throws if given
830
+ * anything else, including a `homeOrigin`-mismatched static `url`.
831
+ */
832
+ gotoPage(block) {
833
+ assertMapKind(block, ["nav", "page"], "gotoPage");
834
+ assertMapOrigin(block, this.homeOrigin, "internal", "gotoPage");
835
+ return new MapBuilder(this.engine, [...this.steps, block], this.homeOrigin);
836
+ }
837
+ /**
838
+ * Appends a genuinely cross-origin navigation step (e.g. `(external)/`
839
+ * Blocks in the Waygraph Map convention: mailpit, maildrop.cc) - same
840
+ * kind requirement as {@link gotoPage}, plus the inverse `homeOrigin` check.
841
+ */
842
+ gotoExternal(block) {
843
+ assertMapKind(block, ["nav", "page"], "gotoExternal");
844
+ assertMapOrigin(block, this.homeOrigin, "external", "gotoExternal");
845
+ return new MapBuilder(this.engine, [...this.steps, block], this.homeOrigin);
846
+ }
847
+ /** Appends a self-loop verification step - requires a Block from `defineAssertBlock`. */
848
+ assert(block) {
849
+ assertMapKind(block, ["assert"], "assert");
850
+ return new MapBuilder(this.engine, [...this.steps, block], this.homeOrigin);
851
+ }
852
+ /**
853
+ * Appends a non-navigating page action (submit, upload, add/remove an
854
+ * instance) - requires a Block from `defineMethodBlock`/`defineActionBlock`/
855
+ * `defineEffectBlock`/`defineMemEffectBlock`.
856
+ */
857
+ method(block) {
858
+ assertMapSalt(block, ["method", "effect"], "method");
859
+ return new MapBuilder(this.engine, [...this.steps, block], this.homeOrigin);
860
+ }
861
+ /**
862
+ * Finalizes this chain into a real, runnable {@link Flow} - same object
863
+ * `defineFlow` returns, so `withBlockVerify`/`modBlockVerify` and every
864
+ * `withTitle`/`withHighlightFixtures`/etc. decorator still apply exactly
865
+ * as they do today; this builder only changes how the chain is assembled,
866
+ * never what it produces. Throws if no step was ever added - an empty map
867
+ * isn't a flow.
868
+ */
869
+ end() {
870
+ if (this.steps.length === 0) {
871
+ throw new Error("Waygraph map: .end() called with zero steps - add at least one .gotoPage()/.gotoExternal()/" +
872
+ ".assert()/.method() before .end()");
873
+ }
874
+ const chain = [start, ...this.steps, end];
875
+ return this.engine.defineFlow(chain);
876
+ }
877
+ }
878
+ /**
879
+ * Standalone convenience for `new Engine(config).map(options)` - use this
880
+ * when a call site doesn't otherwise need its own `Engine` instance (no
881
+ * shared `headless`/`slowMo`/`layouts` across several flows).
882
+ * @example const flow = map({ homeOrigin: "https://app.example.com" }).gotoPage(NavHomeBlock).end();
883
+ */
884
+ export function map(options) {
885
+ return new Engine(options).map(options);
658
886
  }
659
887
  /**
660
888
  * Attaches routing to `block` without touching its act/observe/resolve/verify -
package/dist/index.d.ts CHANGED
@@ -2,10 +2,10 @@ export type { Checkpoint, Instruction, Block, DefinedBlock, Start, WaygraphInsta
2
2
  export { connect, checkpoint } from "./types.js";
3
3
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
4
4
  export { Trait, urlMatches, textEquals, visible, frameVisible, frameTextEquals, frameContainsText } from "./trait.js";
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, chainFlow, } from "./engine.js";
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, 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, 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, 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";
8
- export type { Flow, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, AssertBlockOptions, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, } from "./engine.js";
8
+ export type { Flow, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, AssertBlockOptions, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, Layout, LayoutOptions, MapBuilderOptions, } from "./engine.js";
9
9
  export type { ActionPage } from "./types.js";
10
10
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
11
11
  export type { WaygraphGraph, WaygraphNode, WaygraphEdge, SkippedBlock, OrphanBlock } from "./graph.js";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export { connect, checkpoint } from "./types.js";
2
2
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
3
3
  export { Trait, urlMatches, textEquals, visible, frameVisible, frameTextEquals, frameContainsText } from "./trait.js";
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, chainFlow, } from "./engine.js";
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, 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, 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";
7
7
  export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp, } from "./blocks-select.js";
@@ -0,0 +1,18 @@
1
+ export interface MapViolation {
2
+ file: string;
3
+ block: string;
4
+ reason: string;
5
+ }
6
+ export interface MapNode {
7
+ block: string;
8
+ checkpoint: string | undefined;
9
+ file: string;
10
+ kind: "nav" | "page" | "other";
11
+ }
12
+ export interface MapCheckResult {
13
+ mapRoot: string;
14
+ hasMapDir: boolean;
15
+ nodes: MapNode[];
16
+ violations: MapViolation[];
17
+ }
18
+ export declare function checkMap(projectDir: string): Promise<MapCheckResult>;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * `waygraph map` - enforcement for the Waygraph Map folder convention.
3
+ * Real, direct user correction: this is not just a naming preference, it's
4
+ * a forced structure. Requires `src/map/` to exist, and for every Nav/Page
5
+ * Block whose `url` is a plain string, the folder path (relative to
6
+ * `src/map/`, with `(group)` segments excluded - those are intentionally
7
+ * invisible in the real URL, same as Next.js route groups) must verbatim
8
+ * match the URL's own real pathname segments, in order. Catches exactly
9
+ * the class of bug a live session found: `(auth)/signin/` grouping a page
10
+ * whose real URL is `/signin`, not `/auth/signin` - a fabricated grouping
11
+ * that never matched the real site.
12
+ *
13
+ * Self-contained (no import from cli.ts), same reasoning as graph.ts's own
14
+ * header comment: cli.ts runs main() unconditionally at module load.
15
+ */
16
+ import { existsSync, readdirSync, statSync } from "node:fs";
17
+ import { join, dirname, relative, sep } from "node:path";
18
+ import { pathToFileURL } from "node:url";
19
+ function walkBlockFiles(dir) {
20
+ const results = [];
21
+ let entries;
22
+ try {
23
+ entries = readdirSync(dir, { withFileTypes: true });
24
+ }
25
+ catch {
26
+ return results;
27
+ }
28
+ for (const entry of entries) {
29
+ const full = join(dir, entry.name);
30
+ if (entry.isDirectory()) {
31
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git")
32
+ continue;
33
+ results.push(...walkBlockFiles(full));
34
+ }
35
+ else if (/\.block\.ts$/.test(entry.name)) {
36
+ results.push(full);
37
+ }
38
+ }
39
+ return results;
40
+ }
41
+ function isBlockLike(val) {
42
+ if (val === null || typeof val !== "object")
43
+ return false;
44
+ const obj = val;
45
+ if (typeof obj.name !== "string")
46
+ return false;
47
+ const instruction = obj.instruction;
48
+ return instruction !== null && typeof instruction === "object" && typeof instruction?.act === "function";
49
+ }
50
+ async function importModule(filePath) {
51
+ const url = pathToFileURL(filePath);
52
+ const mtimeMs = statSync(filePath).mtimeMs;
53
+ url.searchParams.set("t", String(mtimeMs));
54
+ return (await import(url.href));
55
+ }
56
+ /**
57
+ * `(group)` segments never appear in the real URL (Next.js route-group
58
+ * convention this whole system is explicitly modeled on) - excluded from
59
+ * the comparison, not just cosmetically ignored by discoverGraph.
60
+ *
61
+ * A leading-underscore segment (`_methods/`, same as Next.js's own private-
62
+ * folder convention) is the fixed machinery belonging to ONE endpoint - not
63
+ * a route segment of its own - excluded from the comparison for the same
64
+ * reason `(group)` is: real direct request answering "so i have clear
65
+ * distinction of endpoints vs block folders" - a bare (non-underscore,
66
+ * non-parenthesized) folder name always means a real child Checkpoint/URL
67
+ * segment, no exceptions, so `_methods/foo.block.ts` never gets mistaken
68
+ * for a sibling endpoint the way a bare `methods/` folder structurally
69
+ * could be.
70
+ */
71
+ function realSegments(relPath) {
72
+ return dirname(relPath)
73
+ .split(sep)
74
+ .filter((s) => s !== "." && s !== "" && !/^\(.*\)$/.test(s) && !s.startsWith("_"));
75
+ }
76
+ export async function checkMap(projectDir) {
77
+ const mapRoot = join(projectDir, "src", "map");
78
+ if (!existsSync(mapRoot)) {
79
+ return { mapRoot, hasMapDir: false, nodes: [], violations: [] };
80
+ }
81
+ const files = walkBlockFiles(mapRoot);
82
+ const nodes = [];
83
+ const violations = [];
84
+ for (const file of files) {
85
+ const relFile = relative(projectDir, file);
86
+ let mod;
87
+ try {
88
+ mod = await importModule(file);
89
+ }
90
+ catch (err) {
91
+ violations.push({
92
+ file: relFile,
93
+ block: "(import failed)",
94
+ reason: err instanceof Error ? err.message.split("\n")[0] : String(err),
95
+ });
96
+ continue;
97
+ }
98
+ for (const exported of Object.values(mod)) {
99
+ if (!isBlockLike(exported))
100
+ continue;
101
+ const waygraphKind = exported.__waygraphKind;
102
+ const kind = waygraphKind === "nav" ? "nav" : waygraphKind === "page" ? "page" : "other";
103
+ const navUrl = exported.__waygraphNavUrl;
104
+ let checkpoint;
105
+ const instruction = exported.instruction;
106
+ if (instruction?.resolve && typeof instruction.resolve === "function") {
107
+ try {
108
+ const out = (await instruction.resolve(undefined));
109
+ checkpoint = out?.__state;
110
+ }
111
+ catch {
112
+ /* resolve() needing real input (a Method Block) - not position-defining, skip */
113
+ }
114
+ }
115
+ nodes.push({ block: exported.name, checkpoint, file: relFile, kind });
116
+ if (typeof navUrl !== "string")
117
+ continue; // only statically-urled Nav/Page Blocks are checkable
118
+ let pathname;
119
+ try {
120
+ pathname = new URL(navUrl).pathname;
121
+ }
122
+ catch {
123
+ continue; // a relative/templated URL - not verbatim-checkable
124
+ }
125
+ const urlSegments = pathname.split("/").filter(Boolean);
126
+ const folderSegments = realSegments(relative(mapRoot, file));
127
+ const matches = urlSegments.length === folderSegments.length && urlSegments.every((s, i) => s === folderSegments[i]);
128
+ if (!matches) {
129
+ violations.push({
130
+ file: relFile,
131
+ block: exported.name,
132
+ reason: `folder path "${folderSegments.join("/")}" doesn't verbatim-match the real URL path ` +
133
+ `"${urlSegments.join("/")}" (${navUrl}) - (group) segments are excluded from this comparison, ` +
134
+ "everything else must match exactly, same as Next.js file-based routing",
135
+ });
136
+ }
137
+ }
138
+ }
139
+ return { mapRoot, hasMapDir: true, nodes, violations };
140
+ }
package/dist/types.d.ts CHANGED
@@ -277,10 +277,30 @@ export type DefinedBlock<In extends Checkpoint<string>, Out extends Checkpoint<s
277
277
  modVerify(nameOrIndex: string | number, newCheck: Trait["check"] | Trait): DefinedBlock<In, Out>;
278
278
  modVerifyAll(patches: Record<string, Trait["check"] | Trait>): DefinedBlock<In, Out>;
279
279
  };
280
+ /**
281
+ * Structural (not imported) twin of `engine.ts`'s `Layout` - `types.ts` sits
282
+ * below `engine.ts` in the module graph, so it can't import `Layout` without
283
+ * a cycle. Same shape, matched by TypeScript's structural typing: any real
284
+ * `Layout` satisfies this with no cast needed at the `connect()` call site.
285
+ */
286
+ export interface ConnectLayout {
287
+ name: string;
288
+ appliesTo: (tag: string) => boolean;
289
+ verify: Trait[] | ((out: any) => Trait[]);
290
+ }
280
291
  /**
281
292
  * Composes two Blocks into one, typechecked so this only compiles when A's output
282
293
  * tag equals B's input tag. The composed Block's act runs A's full instruction
283
294
  * (act -> observe? -> resolve) then hands the resulting Checkpoint into B's act;
284
295
  * its own observe/resolve are simply B's, since B is what decides the final Out.
296
+ *
297
+ * `layouts`, if given, also runs against the intermediate Checkpoint (A's
298
+ * resolved output, right before B's act) - the same enforcement `runGraph`'s
299
+ * own loop already applies to a chain's FINAL Checkpoint, but a
300
+ * `defineFlow`-built Flow never reaches that loop per-hop: `buildFlow`
301
+ * reduces the whole Block array into one `connect()`-composed super-Block, so
302
+ * every intermediate hop's verify (and now layout) has to run HERE, inside
303
+ * this recursive `act`, not in `runGraph`'s loop, which only ever sees the
304
+ * one composed Block run start to finish.
285
305
  */
286
- export declare function connect<A extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>>(a: Block<A, B>, b: Block<B, C>): Block<A, C>;
306
+ export declare function connect<A extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>>(a: Block<A, B>, b: Block<B, C>, layouts?: readonly ConnectLayout[]): Block<A, C>;
package/dist/types.js CHANGED
@@ -11,8 +11,17 @@ export function checkpoint(tag) {
11
11
  * tag equals B's input tag. The composed Block's act runs A's full instruction
12
12
  * (act -> observe? -> resolve) then hands the resulting Checkpoint into B's act;
13
13
  * its own observe/resolve are simply B's, since B is what decides the final Out.
14
+ *
15
+ * `layouts`, if given, also runs against the intermediate Checkpoint (A's
16
+ * resolved output, right before B's act) - the same enforcement `runGraph`'s
17
+ * own loop already applies to a chain's FINAL Checkpoint, but a
18
+ * `defineFlow`-built Flow never reaches that loop per-hop: `buildFlow`
19
+ * reduces the whole Block array into one `connect()`-composed super-Block, so
20
+ * every intermediate hop's verify (and now layout) has to run HERE, inside
21
+ * this recursive `act`, not in `runGraph`'s loop, which only ever sees the
22
+ * one composed Block run start to finish.
14
23
  */
15
- export function connect(a, b) {
24
+ export function connect(a, b, layouts) {
16
25
  const bObserve = b.instruction.observe;
17
26
  const bVerify = b.instruction.verify;
18
27
  const requires = [...(a.requires ?? []), ...(b.requires ?? [])];
@@ -28,6 +37,13 @@ export function connect(a, b) {
28
37
  : undefined;
29
38
  const mid = await a.instruction.resolve(aObserved);
30
39
  await runVerify(a.instruction.verify, mid, page, mem, a.name);
40
+ if (layouts) {
41
+ for (const layout of layouts) {
42
+ if (!layout.appliesTo(mid.__state))
43
+ continue;
44
+ await runVerify(layout.verify, mid, page, mem, `${a.name} (layout: "${layout.name}")`);
45
+ }
46
+ }
31
47
  await runPrecondition(b.instruction.precondition, mid, page, mem, b.name);
32
48
  await b.instruction.act(page, mid, mem);
33
49
  },
@@ -0,0 +1,39 @@
1
+ import { Engine } from "waygraph";
2
+ import { NavDashboardBlock } from "../map/(app_base)/dashboard/_nav.block.js";
3
+ import { ClickWidgetBlock } from "../map/(app_base)/dashboard/_methods/click-widget.block.js";
4
+ import { NavDocsBlock } from "../map/(external)/docs/_nav.block.js";
5
+ // Page hubs registered for auto/docs grouping (arrival-only after nav).
6
+ import "../map/(app_base)/dashboard/_page.block.js";
7
+ import "../map/(external)/docs/_page.block.js";
8
+
9
+ const engine = new Engine();
10
+
11
+ const homeOrigin = process.env.WAYGRAPH_BASE_URL || "http://127.0.0.1:4277";
12
+
13
+ /**
14
+ * Same real Blocks, same real Checkpoints, same real fixture server as
15
+ * routed-demo.flow.ts - built with `engine.map()` instead of
16
+ * `engine.defineFlow([start, ..., end])` to prove the fluent builder
17
+ * against a real browser/DOM, not just the in-memory fakes
18
+ * tests/nav/map-builder.spec.ts already covers in the main package.
19
+ *
20
+ * Both nav steps use `.gotoPage()`, not `.gotoExternal()` - even though
21
+ * NavDocsBlock's file lives under the `(external)/` group. That group name
22
+ * is organizational intent (docs.sel.ts's own `WAYGRAPH_DOCS_URL` env
23
+ * override exists for genuinely pointing it at a different origin), but
24
+ * this fixture server happens to serve docs.html from the exact same
25
+ * origin as dashboard.html - the same real inconsistency this session
26
+ * found and fixed in templates/scaffold's own (external)/docs -> (app_base)
27
+ * rename, left as-is here since routed-demo is the one place a reader
28
+ * benefits from seeing it: passing a real `homeOrigin` below means
29
+ * `.gotoExternal(NavDocsBlock)` would actually THROW (same-origin, not
30
+ * cross-origin) - `.gotoPage()` is the one that's honest about what this
31
+ * fixture really does, regardless of the folder it's grouped under.
32
+ */
33
+ export const routedDemoMapFlow = engine
34
+ .map({ homeOrigin })
35
+ .start()
36
+ .gotoPage(NavDashboardBlock)
37
+ .method(ClickWidgetBlock)
38
+ .gotoPage(NavDocsBlock)
39
+ .end();
@@ -1,10 +1,10 @@
1
1
  import { Engine, start, end } from "waygraph";
2
- import { NavDashboardBlock } from "../routes/(base_app)/dashboard/nav.block.js";
3
- import { ClickWidgetBlock } from "../routes/(base_app)/dashboard/methods/click-widget.block.js";
4
- import { NavDocsBlock } from "../routes/(external)/docs/nav.block.js";
2
+ import { NavDashboardBlock } from "../map/(app_base)/dashboard/_nav.block.js";
3
+ import { ClickWidgetBlock } from "../map/(app_base)/dashboard/_methods/click-widget.block.js";
4
+ import { NavDocsBlock } from "../map/(external)/docs/_nav.block.js";
5
5
  // Page hubs registered for auto/docs grouping (arrival-only after nav).
6
- import "../routes/(base_app)/dashboard/page.block.js";
7
- import "../routes/(external)/docs/page.block.js";
6
+ import "../map/(app_base)/dashboard/_page.block.js";
7
+ import "../map/(external)/docs/_page.block.js";
8
8
 
9
9
  const engine = new Engine();
10
10
 
@@ -1,10 +1,10 @@
1
1
  import { defineMethodBlock, checkpoint, Trait } from "waygraph";
2
2
  import type { Dashboard } from "../../../../states/routed-demo.states.js";
3
- import { DashboardSel } from "../dashboard.sel.js";
3
+ import { DashboardSel } from "../_sel.js";
4
4
 
5
5
  /**
6
6
  * Kind: Method
7
- * Route: (base_app)/dashboard/methods/
7
+ * Route: (app_base)/dashboard/_methods/
8
8
  *
9
9
  * Fixed folder name under the Waygraph Map convention - every page-slug
10
10
  * folder's own actions live under methods/, same as today's freeform
@@ -1,10 +1,10 @@
1
1
  import { defineNavBlock, Trait } from "waygraph";
2
2
  import type { Dashboard } from "../../../states/routed-demo.states.js";
3
- import { DashboardSel, DASHBOARD_URL } from "./dashboard.sel.js";
3
+ import { DashboardSel, DASHBOARD_URL } from "./_sel.js";
4
4
 
5
5
  /**
6
6
  * Kind: Nav
7
- * Route: (base_app)/dashboard/
7
+ * Route: (app_base)/dashboard/
8
8
  *
9
9
  * Fixed file name under the Waygraph Map convention - every page-slug
10
10
  * folder's own navigation Block is always nav.block.ts.
@@ -1,11 +1,11 @@
1
1
  import { definePageBlock, Trait } from "waygraph";
2
2
  import type { Dashboard } from "../../../states/routed-demo.states.js";
3
- import { DashboardSel } from "./dashboard.sel.js";
4
- import { ClickWidgetBlock } from "./methods/click-widget.block.js";
3
+ import { DashboardSel } from "./_sel.js";
4
+ import { ClickWidgetBlock } from "./_methods/click-widget.block.js";
5
5
 
6
6
  /**
7
7
  * Kind: Page
8
- * Route: (base_app)/dashboard/
8
+ * Route: (app_base)/dashboard/
9
9
  *
10
10
  * Fixed file name under the Waygraph Map convention - every page-slug
11
11
  * folder's own arrival hub is always page.block.ts. Arrival-only
@@ -13,7 +13,7 @@ import { ClickWidgetBlock } from "./methods/click-widget.block.js";
13
13
  */
14
14
  export const DashboardPageBlock = definePageBlock<Dashboard>({
15
15
  name: "page-dashboard",
16
- description: "Dashboard hub - the routed-demo example's (base_app) group.",
16
+ description: "Dashboard hub - the routed-demo example's (app_base) group.",
17
17
  checkpoint: "Dashboard",
18
18
  verify: [Trait.visible(DashboardSel.heading)],
19
19
  methods: {