waygraph 0.12.16 → 0.12.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
@@ -236,9 +236,11 @@ snapshot, partitions edges by hash, and claims via in-process edge leases (audit
236
236
 
237
237
  **Richer demo narration:** automation rings are **gray**; authored stubs/slides can use
238
238
  `tone: "info" | "warning" | "danger" | "success"` (iconified captions), plus
239
- `size: "sm" | "md" | "lg"` and `weight: "normal" | "bold"`. Episode pacing:
240
- `withDemoPace(flow, "fast" | "slow" | "normal" | "blitz")` or `withBlockPace(block, ...)`
241
- (FFCompose stays blitz). Living objectives: `docs/proposals/OBJECTIVES.md`.
239
+ `size: "sm" | "md" | "lg"` and `weight: "normal" | "bold"`. Flow episode defaults:
240
+ `withHighlightStyle(flow, { size, weight, tone? })` (slot/fixture wins). Episode pacing:
241
+ `withDemoPace(flow, "fast" | "slow" | "normal" | "blitz" | number)` or `withBlockPace(block, ...)`
242
+ (`number` `<=20` = scale vs normal, e.g. `0.5` / `2`; `>20` = absolute ms, e.g. `4500`).
243
+ FFCompose stays blitz. Living objectives: `docs/proposals/OBJECTIVES.md`.
242
244
  `spawnTab(entry, page, mem)` drives a genuinely separate second tab through its own
243
245
  Block/Flow, in the same browser context an existing `page` already belongs to - the
244
246
  pattern `observe()` could already reach for (it's the only phase allowed to touch
package/dist/cli.js CHANGED
@@ -215,7 +215,12 @@ import {
215
215
  normalizeDemoPace,
216
216
  normalizeHighlightSize,
217
217
  normalizeHighlightWeight,
218
+ applyHighlightStyleDefaults,
218
219
  resolveStepDemoPace,
220
+ demoPaceGateMs,
221
+ demoPaceIsBlitz,
222
+ demoPaceIsFast,
223
+ demoPaceIsSlow,
219
224
  } from "waygraph";
220
225
 
221
226
  function walkDir(dir, pattern) {
@@ -1072,6 +1077,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1072
1077
  // defaultHoldMs: when set (fail path), use instead of legacy 900/200 so BUG
1073
1078
  // rings stay visible ~2s before the error panel (PIA stubOnError RFC).
1074
1079
  const defaultHoldMs = opts && opts.defaultHoldMs != null ? opts.defaultHoldMs : null;
1080
+ const pace = opts && opts.pace !== undefined ? opts.pace : undefined;
1075
1081
  for (let i = 0; i < list.length; i++) {
1076
1082
  const h = list[i];
1077
1083
  try {
@@ -1081,7 +1087,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1081
1087
  size: h.size,
1082
1088
  weight: h.weight,
1083
1089
  });
1084
- const authored = resolveFixtureDwellMs(h, { gatesFast });
1090
+ const authored = resolveFixtureDwellMs(h, { gatesFast, pace });
1085
1091
  const legacyMs = i === list.length - 1 ? 200 : 900;
1086
1092
  const holdMs =
1087
1093
  authored != null ? authored : defaultHoldMs != null ? defaultHoldMs : legacyMs;
@@ -1138,7 +1144,9 @@ async function renderAfterStep(page, info) {
1138
1144
  // it highlights something [else] and next," not just the first one.
1139
1145
  // duration / fastMode on stubAfter (or flow fixtures) override the legacy
1140
1146
  // 900ms / 200ms holds when set.
1141
- await cycleHighlightRings(page, highlights, gatesFast, null);
1147
+ await cycleHighlightRings(page, highlights, gatesFast, {
1148
+ pace: info.pace,
1149
+ });
1142
1150
  await page
1143
1151
  .evaluate((info) => {
1144
1152
  // Reused in place - see renderBeforeStep's own comment on this.
@@ -2159,13 +2167,10 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2159
2167
  // gates too, not just skip its interaction dwell - otherwise autoplay
2160
2168
  // still stalls the full autoplayMs admiring a step that intentionally
2161
2169
  // ran too fast to watch.
2162
- const ms = pacing.skipTheater
2163
- ? Math.min(80, autoplayMs)
2164
- : pacing.gatesFast
2165
- ? Math.min(400, autoplayMs)
2166
- : pacing.gatesSlow
2167
- ? Math.max(autoplayMs * 2, 3500)
2168
- : autoplayMs;
2170
+ const ms = demoPaceGateMs(
2171
+ pacing.skipTheater ? "blitz" : pacing.demoPace,
2172
+ autoplayMs,
2173
+ );
2169
2174
  let elapsed = 0;
2170
2175
  for (;;) {
2171
2176
  // Re-read the checkbox EVERY loop tick, not once up front - a human
@@ -2248,36 +2253,40 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2248
2253
  if ((clearSession || r.resetSession) && i > 0) {
2249
2254
  await resetPageState(context, page, baseURL);
2250
2255
  }
2251
- // gatesFast / skipTheater: opaque FF, former-FF inners (--ff-disabled/--ff-expand),
2252
- // WAYGRAPH_FAST_BLOCKS, or withDemoPace/withBlockPace. --fast alone = shorter
2253
- // gates only (keeps theater). Episode pace: block > flow > CLI --fast.
2256
+ // Episode pace: block > flow > CLI --fast (only when no authored pace).
2257
+ // Authoring wins: withDemoPace("slow"|2|4500) keeps slow dwell even under --fast.
2254
2258
  const stepPace = resolveStepDemoPace({
2255
2259
  blockPace: r.block.demoPace,
2256
2260
  flowPace: r.demoPace,
2257
2261
  fastForward: !!r.block.fastForward,
2258
2262
  wasFastForward: !!r.wasFastForward,
2259
2263
  });
2264
+ const authoredPace =
2265
+ (r.block.demoPace != null && r.block.demoPace !== "") ||
2266
+ (r.demoPace != null && r.demoPace !== "");
2260
2267
  const cliFast = demoFast || fastBlockNames.has(r.block.name);
2261
2268
  pacing.demoPace =
2262
- stepPace === "normal" && cliFast
2269
+ stepPace === "normal" && cliFast && !authoredPace
2263
2270
  ? fastBlockNames.has(r.block.name)
2264
2271
  ? "blitz"
2265
2272
  : "fast"
2266
2273
  : stepPace;
2267
2274
  pacing.gatesFast =
2268
- pacing.demoPace === "blitz" ||
2269
- pacing.demoPace === "fast" ||
2275
+ demoPaceIsFast(pacing.demoPace) ||
2270
2276
  !!r.block.fastForward ||
2271
2277
  !!r.wasFastForward ||
2272
- demoFast;
2273
- pacing.gatesSlow = pacing.demoPace === "slow";
2278
+ (!authoredPace && demoFast);
2279
+ pacing.gatesSlow = demoPaceIsSlow(pacing.demoPace);
2274
2280
  pacing.skipTheater =
2275
- pacing.demoPace === "blitz" ||
2281
+ demoPaceIsBlitz(pacing.demoPace) ||
2276
2282
  fastBlockNames.has(r.block.name) ||
2277
2283
  !!r.block.fastForward ||
2278
2284
  !!r.wasFastForward;
2279
2285
  const fixtures = r.highlightFixtures;
2280
- stubBeforeRef.current = resolveHighlightSlots(r.block, "stubBefore", { fixtures });
2286
+ const flowStyle = r.highlightStyle;
2287
+ stubBeforeRef.current = resolveHighlightSlots(r.block, "stubBefore", { fixtures }).map((h) =>
2288
+ applyHighlightStyleDefaults(h, flowStyle),
2289
+ );
2281
2290
  const isNavBlock = r.block.__waygraphKind === "nav";
2282
2291
  const autoNow = await currentAutoplay();
2283
2292
  // Auto-next: tuck stepper on NavBlocks so the page transition fills the frame.
@@ -2369,17 +2378,21 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2369
2378
  .catch(() => {});
2370
2379
  const errHighlights = resolveHighlightSlots(r.block, "stubOnError", {
2371
2380
  fixtures: fixturesOnError,
2372
- }).map((h) => ({
2373
- selector: h.selector,
2374
- label: formatHighlightCaption(h),
2375
- duration: h.duration,
2376
- fastMode: h.fastMode,
2377
- tone: normalizeHighlightTone(h.tone),
2378
- size: normalizeHighlightSize(h.size),
2379
- weight: normalizeHighlightWeight(h.weight),
2380
- }));
2381
+ }).map((h) => {
2382
+ const styled = applyHighlightStyleDefaults(h, r.highlightStyle);
2383
+ return {
2384
+ selector: styled.selector,
2385
+ label: formatHighlightCaption(styled),
2386
+ duration: styled.duration,
2387
+ fastMode: styled.fastMode,
2388
+ tone: styled.tone,
2389
+ size: styled.size,
2390
+ weight: styled.weight,
2391
+ };
2392
+ });
2381
2393
  await cycleHighlightRings(page, errHighlights, !!pacing.gatesFast, {
2382
2394
  defaultHoldMs: 2000,
2395
+ pace: pacing.demoPace,
2383
2396
  });
2384
2397
  }
2385
2398
  // A thrown act()/observe()/a failed verify Trait used to just crash
@@ -2430,7 +2443,9 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2430
2443
  .catch(() => {});
2431
2444
  result = stepOutcome.result;
2432
2445
  const fixturesAfter = r.highlightFixtures;
2433
- const slides = resolveSlides(r.block, { out: result, fixtures: fixturesAfter });
2446
+ const slides = resolveSlides(r.block, { out: result, fixtures: fixturesAfter }).map((s) =>
2447
+ applyHighlightStyleDefaults(s, r.highlightStyle),
2448
+ );
2434
2449
  if (slides.length > 0) {
2435
2450
  await presentSlides(page, slides, gate, {
2436
2451
  title,
@@ -2445,18 +2460,23 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2445
2460
  let highlights;
2446
2461
  if (hasAuthoredStubAfter(r.block, result, fixturesAfter)) {
2447
2462
  highlights = resolveHighlightSlots(r.block, "stubAfter", { out: result, fixtures: fixturesAfter }).map(
2448
- (h) => ({
2449
- selector: h.selector,
2450
- label: formatHighlightCaption(h),
2451
- duration: h.duration,
2452
- fastMode: h.fastMode,
2453
- tone: normalizeHighlightTone(h.tone),
2454
- size: normalizeHighlightSize(h.size),
2455
- weight: normalizeHighlightWeight(h.weight),
2456
- }),
2463
+ (h) => {
2464
+ const styled = applyHighlightStyleDefaults(h, r.highlightStyle);
2465
+ return {
2466
+ selector: styled.selector,
2467
+ label: formatHighlightCaption(styled),
2468
+ duration: styled.duration,
2469
+ fastMode: styled.fastMode,
2470
+ tone: styled.tone,
2471
+ size: styled.size,
2472
+ weight: styled.weight,
2473
+ };
2474
+ },
2457
2475
  );
2458
2476
  } else {
2459
- highlights = extractVerifyHighlights(r.block, result.__state);
2477
+ highlights = extractVerifyHighlights(r.block, result.__state).map((h) =>
2478
+ applyHighlightStyleDefaults(h, r.highlightStyle),
2479
+ );
2460
2480
  }
2461
2481
  await renderAfterStep(page, {
2462
2482
  index: i,
@@ -2466,6 +2486,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2466
2486
  resultTag: JSON.stringify(result),
2467
2487
  highlights,
2468
2488
  gatesFast: pacing.gatesFast,
2489
+ pace: pacing.demoPace,
2469
2490
  isLast: i === resolved.length - 1,
2470
2491
  allNames: moduleNames,
2471
2492
  allDescriptions: moduleDescriptions,
@@ -2638,6 +2659,7 @@ async function main() {
2638
2659
  seedMem: idx === 0 ? seedMem : undefined,
2639
2660
  highlightFixtures: flow.highlightFixtures,
2640
2661
  demoPace: flow.demoPace,
2662
+ highlightStyle: flow.highlightStyle,
2641
2663
  expectedFailureReason:
2642
2664
  flow.expectedFailureReason && idx === blockInfos.length - 1
2643
2665
  ? flow.expectedFailureReason
@@ -2707,6 +2729,7 @@ async function main() {
2707
2729
  expectedFailureReason: flow.expectedFailureReason,
2708
2730
  highlightFixtures: flow.highlightFixtures,
2709
2731
  demoPace: flow.demoPace,
2732
+ highlightStyle: flow.highlightStyle,
2710
2733
  seedMem: () => seedMemForFlow(mem, flow.blocks(), seg.json, seg.ref),
2711
2734
  });
2712
2735
  } else {
@@ -2717,6 +2740,7 @@ async function main() {
2717
2740
  episodeTitle: undefined,
2718
2741
  highlightFixtures: undefined,
2719
2742
  demoPace: undefined,
2743
+ highlightStyle: undefined,
2720
2744
  seedMem: () => seedMemForBlock(mem, r, seg.json),
2721
2745
  });
2722
2746
  }
@@ -2742,6 +2766,7 @@ async function main() {
2742
2766
  episodeTitle: meta.episodeTitle,
2743
2767
  highlightFixtures: meta.highlightFixtures,
2744
2768
  demoPace: meta.demoPace,
2769
+ highlightStyle: meta.highlightStyle,
2745
2770
  expectedFailureReason:
2746
2771
  meta.expectedFailureReason && remainingInFlow === 1 ? meta.expectedFailureReason : undefined,
2747
2772
  seedMem: isFirstOfSegment ? meta.seedMem : undefined,
package/dist/engine.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { Block, Checkpoint, Instruction, DefinedBlock, ActionPage, Waygraph
3
3
  import { MemPage } from "./mem-page.js";
4
4
  import type { MemKey } from "./mem-page.js";
5
5
  import type { Trait } from "./trait.js";
6
+ import { type HighlightStyleDefaults, type DemoPace } from "./highlights.js";
6
7
  /**
7
8
  * Checks `block.requires` against `mem` and throws, naming every missing key at
8
9
  * once, if any are unset. `connect()` unions each Block's `requires` into the
@@ -191,6 +192,11 @@ export interface Flow<Out extends Checkpoint<string>> {
191
192
  * `run()` ignores; step/demo applies per Block in the episode.
192
193
  */
193
194
  readonly demoPace?: import("./highlights.js").DemoPace;
195
+ /**
196
+ * Default highlight size/weight/tone for this Flow's episode (`withHighlightStyle`).
197
+ * Per-slot stub/fixture values win. `run()` ignores.
198
+ */
199
+ readonly highlightStyle?: import("./highlights.js").HighlightStyleDefaults;
194
200
  /**
195
201
  * This Flow's constituent Blocks, in order between `start`/`end`, as plain
196
202
  * data - for introspection/visualization tools, without running anything.
@@ -263,13 +269,22 @@ export declare function withTitle<Out extends Checkpoint<string>>(flow: Flow<Out
263
269
  * Non-destructive. `fastForwardComposeBlock` is always blitz for its own step;
264
270
  * use this when an entire episode should be fast or slow.
265
271
  * @example withDemoPace(withTitle(gapFlow, "Episode 2 - gaps"), "slow")
272
+ * @example withDemoPace(happyFlow, 0.5) // 2x faster than normal
273
+ * @example withDemoPace(gapFlow, 2.5) // 2.5x slower
274
+ * @example withDemoPace(gapFlow, 4500) // absolute ~4.5s gates/dwells
275
+ */
276
+ export declare function withDemoPace<Out extends Checkpoint<string>>(flow: Flow<Out>, pace: DemoPace): Flow<Out>;
277
+ /**
278
+ * Default highlight look for a Flow episode (size / weight / optional tone).
279
+ * Per-slot stub or {@link withHighlightFixtures} values win when set.
280
+ * @example withHighlightStyle(withDemoPace(gapFlow, "slow"), { size: "lg", weight: "bold" })
266
281
  */
267
- export declare function withDemoPace<Out extends Checkpoint<string>>(flow: Flow<Out>, pace: import("./highlights.js").DemoPace): Flow<Out>;
282
+ export declare function withHighlightStyle<Out extends Checkpoint<string>>(flow: Flow<Out>, style: HighlightStyleDefaults): Flow<Out>;
268
283
  /**
269
284
  * Demo pacing for one Block or compose unit (not FF - use fastForwardComposeBlock for blitz).
270
285
  * @example withBlockPace(composeBlock("gap-review", [...]), "slow")
271
286
  */
272
- export declare function withBlockPace<In extends Checkpoint<string>, Out extends Checkpoint<string>>(block: Block<In, Out>, pace: import("./highlights.js").DemoPace): Block<In, Out>;
287
+ export declare function withBlockPace<In extends Checkpoint<string>, Out extends Checkpoint<string>>(block: Block<In, Out>, pace: DemoPace): Block<In, Out>;
273
288
  /**
274
289
  * Marks a Flow as one that's SUPPOSED to throw - e.g. a locked-out user's
275
290
  * login attempt genuinely failing, demonstrating the app's own real
package/dist/engine.js CHANGED
@@ -2,6 +2,7 @@ import { chromium, firefox, webkit } from "@playwright/test";
2
2
  import { connect, checkpoint } from "./types.js";
3
3
  import { MemPage } from "./mem-page.js";
4
4
  import { runVerify, runPrecondition } from "./trait.js";
5
+ import { normalizeHighlightTone, normalizeHighlightSize, normalizeHighlightWeight, } from "./highlights.js";
5
6
  const LAUNCHERS = { chromium, firefox, webkit };
6
7
  /**
7
8
  * Resolves which `BrowserType` actually launches for a `mem`-only run (no
@@ -356,6 +357,9 @@ export function withTitle(flow, title) {
356
357
  * Non-destructive. `fastForwardComposeBlock` is always blitz for its own step;
357
358
  * use this when an entire episode should be fast or slow.
358
359
  * @example withDemoPace(withTitle(gapFlow, "Episode 2 - gaps"), "slow")
360
+ * @example withDemoPace(happyFlow, 0.5) // 2x faster than normal
361
+ * @example withDemoPace(gapFlow, 2.5) // 2.5x slower
362
+ * @example withDemoPace(gapFlow, 4500) // absolute ~4.5s gates/dwells
359
363
  */
360
364
  export function withDemoPace(flow, pace) {
361
365
  const demoPace = pace;
@@ -366,6 +370,24 @@ export function withDemoPace(flow, pace) {
366
370
  modBlockVerify: (block, nameOrIndex, newCheck) => withDemoPace(flow.modBlockVerify(block, nameOrIndex, newCheck), demoPace),
367
371
  };
368
372
  }
373
+ /**
374
+ * Default highlight look for a Flow episode (size / weight / optional tone).
375
+ * Per-slot stub or {@link withHighlightFixtures} values win when set.
376
+ * @example withHighlightStyle(withDemoPace(gapFlow, "slow"), { size: "lg", weight: "bold" })
377
+ */
378
+ export function withHighlightStyle(flow, style) {
379
+ const highlightStyle = {
380
+ ...(style.tone !== undefined ? { tone: normalizeHighlightTone(style.tone) } : {}),
381
+ ...(style.size !== undefined ? { size: normalizeHighlightSize(style.size) } : {}),
382
+ ...(style.weight !== undefined ? { weight: normalizeHighlightWeight(style.weight) } : {}),
383
+ };
384
+ return {
385
+ ...flow,
386
+ highlightStyle,
387
+ withBlockVerify: (block, verify) => withHighlightStyle(flow.withBlockVerify(block, verify), highlightStyle),
388
+ modBlockVerify: (block, nameOrIndex, newCheck) => withHighlightStyle(flow.modBlockVerify(block, nameOrIndex, newCheck), highlightStyle),
389
+ };
390
+ }
369
391
  /**
370
392
  * Demo pacing for one Block or compose unit (not FF - use fastForwardComposeBlock for blitz).
371
393
  * @example withBlockPace(composeBlock("gap-review", [...]), "slow")
@@ -79,6 +79,22 @@ export type HighlightTone = "planned" | "auto" | "info" | "warning" | "danger" |
79
79
  export type HighlightSize = "sm" | "md" | "lg";
80
80
  /** Label font weight. */
81
81
  export type HighlightWeight = "normal" | "bold";
82
+ /** Optional Flow-level defaults for demo rings (episode-wide). Stub/fixture wins. */
83
+ export type HighlightStyleDefaults = {
84
+ tone?: HighlightTone;
85
+ size?: HighlightSize;
86
+ weight?: HighlightWeight;
87
+ };
88
+ /** Apply Flow defaults under per-slot authored values (slot wins when set). */
89
+ export declare function applyHighlightStyleDefaults<T extends {
90
+ tone?: HighlightTone | string;
91
+ size?: HighlightSize | string;
92
+ weight?: HighlightWeight | string;
93
+ }>(slot: T, defaults?: HighlightStyleDefaults | null): T & {
94
+ tone: HighlightTone;
95
+ size: HighlightSize;
96
+ weight: HighlightWeight;
97
+ };
82
98
  /** Map author aliases (`error` -> danger) to a canonical tone. */
83
99
  export declare function normalizeHighlightTone(raw: unknown): HighlightTone;
84
100
  /** Map size aliases to sm/md/lg. */
@@ -90,12 +106,14 @@ export declare function toneIconPrefix(tone: HighlightTone): string;
90
106
  /**
91
107
  * Demo pacing for a Flow episode or a Block/compose group.
92
108
  * - `blitz` - FF-like: skip theater + short gates (same as fastForwardComposeBlock)
93
- * - `fast` - shorter gates, keep cursor theater
94
- * - `normal` - default demo pace
95
- * - `slow` - longer autoplay + fixture dwell (gaps / wrongs review)
109
+ * - `fast` - shorter gates, keep cursor theater (~0.33x normal)
110
+ * - `normal` - default demo pace (1x)
111
+ * - `slow` - longer autoplay + fixture dwell (~2x)
112
+ * - `number` - scale vs normal when `<= 20` (e.g. `0.5`, `1.5`, `3`);
113
+ * absolute gate/dwell ms when `> 20` (e.g. `4500`)
96
114
  */
97
- export type DemoPace = "blitz" | "fast" | "normal" | "slow";
98
- export declare function normalizeDemoPace(raw: unknown): DemoPace;
115
+ export type DemoPaceName = "blitz" | "fast" | "normal" | "slow";
116
+ export type DemoPace = DemoPaceName | number;
99
117
  /** Default fixture dwell when `duration: true`. */
100
118
  export declare const FIXTURE_DURATION_MS_DEFAULT = 2000;
101
119
  /** Default fixture dwell under `--fast` when duration is enabled. */
@@ -106,9 +124,21 @@ export declare const FIXTURE_DURATION_SLOW_MS_DEFAULT = 4000;
106
124
  export declare const SLIDE_DURATION_MS_DEFAULT = 2000;
107
125
  /** @deprecated Prefer FIXTURE_DURATION_FAST_MS_DEFAULT */
108
126
  export declare const SLIDE_DURATION_FAST_MS_DEFAULT = 600;
127
+ export declare function normalizeDemoPace(raw: unknown): DemoPace;
128
+ /** Named / numeric pace -> scale vs normal (1 = normal). Absolute ms (>20) maps via default dwell. */
129
+ export declare function demoPaceScale(pace: DemoPace): number;
130
+ export declare function demoPaceIsBlitz(pace: DemoPace): boolean;
131
+ /** True when pace should use the short-gate / fast fixture path (not slow authored). */
132
+ export declare function demoPaceIsFast(pace: DemoPace): boolean;
133
+ export declare function demoPaceIsSlow(pace: DemoPace): boolean;
134
+ /** Autoplay / Next-gate ms for this pace (base is WAYGRAPH_AUTOPLAY_MS or 1800). */
135
+ export declare function demoPaceGateMs(pace: DemoPace, baseAutoplayMs: number): number;
109
136
  /**
110
137
  * Resolve min dwell ms for any fixture (stub / flow fixture / yap slide),
111
138
  * or `null` when duration is unset (caller keeps legacy timing).
139
+ *
140
+ * Authoring wins: an explicit slow/numeric pace is not crushed by CLI `--fast`
141
+ * (`gatesFast`). `--fast` only shortens when pace is unset/normal.
112
142
  */
113
143
  export declare function resolveFixtureDwellMs(fixture: FixtureDurationFields, opts?: {
114
144
  gatesFast?: boolean;
@@ -124,8 +154,8 @@ export declare function resolveSlideDwellMs(slide: FixtureDurationFields, opts?:
124
154
  * Opaque FF / wasFastForward always win as blitz.
125
155
  */
126
156
  export declare function resolveStepDemoPace(opts: {
127
- blockPace?: DemoPace | string | null;
128
- flowPace?: DemoPace | string | null;
157
+ blockPace?: DemoPace | string | number | null;
158
+ flowPace?: DemoPace | string | number | null;
129
159
  fastForward?: boolean;
130
160
  wasFastForward?: boolean;
131
161
  }): DemoPace;
@@ -8,6 +8,25 @@
8
8
  * All fixture shapes share optional {@link FixtureDurationFields} so demo dwell
9
9
  * before advancing is authored the same way on stubs, flow fixtures, and slides.
10
10
  */
11
+ /** Apply Flow defaults under per-slot authored values (slot wins when set). */
12
+ export function applyHighlightStyleDefaults(slot, defaults) {
13
+ const tone = slot.tone !== undefined && slot.tone !== ""
14
+ ? normalizeHighlightTone(slot.tone)
15
+ : defaults?.tone !== undefined
16
+ ? normalizeHighlightTone(defaults.tone)
17
+ : "planned";
18
+ const size = slot.size !== undefined && slot.size !== ""
19
+ ? normalizeHighlightSize(slot.size)
20
+ : defaults?.size !== undefined
21
+ ? normalizeHighlightSize(defaults.size)
22
+ : "md";
23
+ const weight = slot.weight !== undefined && slot.weight !== ""
24
+ ? normalizeHighlightWeight(slot.weight)
25
+ : defaults?.weight !== undefined
26
+ ? normalizeHighlightWeight(defaults.weight)
27
+ : "normal";
28
+ return { ...slot, tone, size, weight };
29
+ }
11
30
  const TONE_ICONS = {
12
31
  planned: "",
13
32
  auto: "",
@@ -60,18 +79,6 @@ export function toneIconPrefix(tone) {
60
79
  const icon = TONE_ICONS[tone];
61
80
  return icon ? `[${icon}] ` : "";
62
81
  }
63
- export function normalizeDemoPace(raw) {
64
- if (typeof raw !== "string")
65
- return "normal";
66
- const t = raw.trim().toLowerCase();
67
- if (t === "blitz" || t === "ff")
68
- return "blitz";
69
- if (t === "fast" || t === "quick")
70
- return "fast";
71
- if (t === "slow" || t === "careful" || t === "review")
72
- return "slow";
73
- return "normal";
74
- }
75
82
  /** Default fixture dwell when `duration: true`. */
76
83
  export const FIXTURE_DURATION_MS_DEFAULT = 2000;
77
84
  /** Default fixture dwell under `--fast` when duration is enabled. */
@@ -82,24 +89,112 @@ export const FIXTURE_DURATION_SLOW_MS_DEFAULT = 4000;
82
89
  export const SLIDE_DURATION_MS_DEFAULT = FIXTURE_DURATION_MS_DEFAULT;
83
90
  /** @deprecated Prefer FIXTURE_DURATION_FAST_MS_DEFAULT */
84
91
  export const SLIDE_DURATION_FAST_MS_DEFAULT = FIXTURE_DURATION_FAST_MS_DEFAULT;
92
+ export function normalizeDemoPace(raw) {
93
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0)
94
+ return raw;
95
+ if (typeof raw === "string") {
96
+ const t = raw.trim().toLowerCase();
97
+ if (t === "blitz" || t === "ff")
98
+ return "blitz";
99
+ if (t === "fast" || t === "quick")
100
+ return "fast";
101
+ if (t === "slow" || t === "careful" || t === "review")
102
+ return "slow";
103
+ if (t === "normal" || t === "default" || t === "1x")
104
+ return "normal";
105
+ // "2x" / "0.5x"
106
+ if (t.endsWith("x")) {
107
+ const n = Number(t.slice(0, -1));
108
+ if (Number.isFinite(n) && n > 0)
109
+ return n;
110
+ }
111
+ const n = Number(t);
112
+ if (Number.isFinite(n) && n > 0)
113
+ return n;
114
+ }
115
+ return "normal";
116
+ }
117
+ /** Named / numeric pace -> scale vs normal (1 = normal). Absolute ms (>20) maps via default dwell. */
118
+ export function demoPaceScale(pace) {
119
+ if (typeof pace === "number") {
120
+ if (pace > 20)
121
+ return Math.min(10, Math.max(0.05, pace / FIXTURE_DURATION_MS_DEFAULT));
122
+ return Math.min(10, Math.max(0.05, pace));
123
+ }
124
+ if (pace === "blitz")
125
+ return 0.05;
126
+ if (pace === "fast")
127
+ return 0.33;
128
+ if (pace === "slow")
129
+ return 2;
130
+ return 1;
131
+ }
132
+ export function demoPaceIsBlitz(pace) {
133
+ return pace === "blitz" || (typeof pace === "number" && pace > 0 && pace <= 0.1);
134
+ }
135
+ /** True when pace should use the short-gate / fast fixture path (not slow authored). */
136
+ export function demoPaceIsFast(pace) {
137
+ if (demoPaceIsBlitz(pace))
138
+ return true;
139
+ if (pace === "fast")
140
+ return true;
141
+ if (typeof pace === "number" && pace <= 20)
142
+ return pace < 0.75;
143
+ return false;
144
+ }
145
+ export function demoPaceIsSlow(pace) {
146
+ if (pace === "slow")
147
+ return true;
148
+ if (typeof pace === "number") {
149
+ if (pace > 20)
150
+ return pace >= 3000;
151
+ return pace > 1.25;
152
+ }
153
+ return false;
154
+ }
155
+ /** Autoplay / Next-gate ms for this pace (base is WAYGRAPH_AUTOPLAY_MS or 1800). */
156
+ export function demoPaceGateMs(pace, baseAutoplayMs) {
157
+ if (demoPaceIsBlitz(pace))
158
+ return Math.min(80, baseAutoplayMs);
159
+ if (typeof pace === "number" && pace > 20) {
160
+ return Math.min(30000, Math.max(80, Math.round(pace)));
161
+ }
162
+ const scale = demoPaceScale(pace);
163
+ return Math.min(30000, Math.max(80, Math.round(baseAutoplayMs * scale)));
164
+ }
85
165
  /**
86
166
  * Resolve min dwell ms for any fixture (stub / flow fixture / yap slide),
87
167
  * or `null` when duration is unset (caller keeps legacy timing).
168
+ *
169
+ * Authoring wins: an explicit slow/numeric pace is not crushed by CLI `--fast`
170
+ * (`gatesFast`). `--fast` only shortens when pace is unset/normal.
88
171
  */
89
172
  export function resolveFixtureDwellMs(fixture, opts) {
90
173
  if (fixture.duration === undefined || fixture.duration === false)
91
174
  return null;
92
- const pace = normalizeDemoPace(opts?.pace ?? (opts?.gatesFast ? "fast" : "normal"));
175
+ const pace = opts?.pace !== undefined && opts.pace !== null
176
+ ? normalizeDemoPace(opts.pace)
177
+ : opts?.gatesFast
178
+ ? "fast"
179
+ : "normal";
93
180
  const fastMs = typeof fixture.fastMode === "number" && Number.isFinite(fixture.fastMode) && fixture.fastMode >= 0
94
181
  ? fixture.fastMode
95
182
  : FIXTURE_DURATION_FAST_MS_DEFAULT;
96
- if (pace === "blitz" || pace === "fast" || opts?.gatesFast)
97
- return fastMs;
98
183
  const normalMs = fixture.duration === true
99
184
  ? FIXTURE_DURATION_MS_DEFAULT
100
185
  : typeof fixture.duration === "number" && Number.isFinite(fixture.duration) && fixture.duration >= 0
101
186
  ? fixture.duration
102
187
  : FIXTURE_DURATION_MS_DEFAULT;
188
+ if (typeof pace === "number") {
189
+ if (pace > 20)
190
+ return Math.round(pace);
191
+ return Math.max(0, Math.round(normalMs * Math.min(10, Math.max(0.05, pace))));
192
+ }
193
+ if (pace === "blitz" || pace === "fast")
194
+ return fastMs;
195
+ // CLI --fast only when author left pace at normal
196
+ if (opts?.gatesFast && pace === "normal")
197
+ return fastMs;
103
198
  if (pace === "slow")
104
199
  return Math.max(normalMs, FIXTURE_DURATION_SLOW_MS_DEFAULT);
105
200
  return normalMs;
package/dist/index.d.ts CHANGED
@@ -2,9 +2,9 @@ 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 } from "./trait.js";
5
- export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
6
- export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, toneIconPrefix, resolveStepDemoPace, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
7
- export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, DemoPace, } from "./highlights.js";
5
+ export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
6
+ export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
7
+ export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, } from "./highlights.js";
8
8
  export type { Flow, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, } from "./engine.js";
9
9
  export type { ActionPage } from "./types.js";
10
10
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
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 } from "./trait.js";
4
- export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
5
- export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, toneIconPrefix, resolveStepDemoPace, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
4
+ export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
5
+ export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
6
6
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
7
7
  export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp, } from "./blocks-select.js";
8
8
  export { EdgeLeaseCoordinator } from "./traverse-lease.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.16",
3
+ "version": "0.12.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",