waygraph 0.12.17 → 0.12.19

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
@@ -238,8 +238,9 @@ snapshot, partitions edges by hash, and claims via in-process edge leases (audit
238
238
  `tone: "info" | "warning" | "danger" | "success"` (iconified captions), plus
239
239
  `size: "sm" | "md" | "lg"` and `weight: "normal" | "bold"`. Flow episode defaults:
240
240
  `withHighlightStyle(flow, { size, weight, tone? })` (slot/fixture wins). Episode pacing:
241
- `withDemoPace(flow, "fast" | "slow" | "normal" | "blitz")` or `withBlockPace(block, ...)`
242
- (FFCompose stays blitz). Living objectives: `docs/proposals/OBJECTIVES.md`.
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`.
243
244
  `spawnTab(entry, page, mem)` drives a genuinely separate second tab through its own
244
245
  Block/Flow, in the same browser context an existing `page` already belongs to - the
245
246
  pattern `observe()` could already reach for (it's the only phase allowed to touch
package/dist/cli.js CHANGED
@@ -217,6 +217,12 @@ import {
217
217
  normalizeHighlightWeight,
218
218
  applyHighlightStyleDefaults,
219
219
  resolveStepDemoPace,
220
+ demoPaceGateMs,
221
+ demoPaceIsBlitz,
222
+ demoPaceIsFast,
223
+ demoPaceIsSlow,
224
+ formatDemoPaceBadge,
225
+ formatDemoPaceLabel,
220
226
  } from "waygraph";
221
227
 
222
228
  function walkDir(dir, pattern) {
@@ -487,6 +493,18 @@ const RING_CSS =
487
493
  // block chain shows no episode heading at all.
488
494
  "#wg-panel .wg-episode{margin:0 0 6px;font:700 15px system-ui,sans-serif;color:#fff;" +
489
495
  "padding-bottom:6px;border-bottom:1px solid rgba(124,58,237,.35);}" +
496
+ // Pace chip - numbers speak in the overlay (2.5x / 4500ms / slow).
497
+ "#wg-panel .wg-pace{display:inline-flex;align-items:center;gap:8px;margin:0 0 10px;" +
498
+ "padding:6px 10px;border-radius:8px;background:rgba(124,58,237,.22);" +
499
+ "border:1px solid rgba(124,58,237,.45);font:600 12px/1.35 system-ui,sans-serif;color:#f0e8ff;}" +
500
+ "#wg-panel .wg-pace-badge{display:inline-block;padding:2px 8px;border-radius:999px;" +
501
+ "background:#7C3AED;color:#fff;font:800 11px/1.2 system-ui,sans-serif;letter-spacing:.04em;" +
502
+ "text-transform:uppercase;}" +
503
+ "#wg-panel .wg-pace[data-pace-kind=slow] .wg-pace-badge," +
504
+ "#wg-panel .wg-pace[data-pace-kind=num-slow] .wg-pace-badge{background:#EAB308;color:#1c1917;}" +
505
+ "#wg-panel .wg-pace[data-pace-kind=fast] .wg-pace-badge," +
506
+ "#wg-panel .wg-pace[data-pace-kind=blitz] .wg-pace-badge{background:#22C55E;color:#052e16;}" +
507
+ "#wg-panel .wg-pace[data-pace-kind=ms] .wg-pace-badge{background:#3B82F6;color:#fff;}" +
490
508
  "#wg-panel .wg-narration{margin:0 0 12px;font:italic 14px/1.4 system-ui,sans-serif;color:#f0e8ff;}" +
491
509
  "#wg-progress{height:4px;background:#2a1650;border-radius:2px;margin:0 0 12px;overflow:hidden;}" +
492
510
  "#wg-progress-bar{height:100%;background:#7C3AED;border-radius:2px;transition:width .3s ease;}" +
@@ -953,9 +971,20 @@ async function renderBeforeStep(page, info) {
953
971
  const narrationHtml = info.description
954
972
  ? "<div class=\\"wg-narration\\">" + esc(info.description) + "</div>"
955
973
  : "";
974
+ const paceHtml =
975
+ info.paceBadge || info.paceLabel
976
+ ? "<div class=\\"wg-pace\\" data-pace-kind=\\"" +
977
+ esc(info.paceKind || "normal") +
978
+ "\\"><span class=\\"wg-pace-badge\\">" +
979
+ esc(info.paceBadge || "1x") +
980
+ "</span><span>" +
981
+ esc(info.paceLabel || "") +
982
+ "</span></div>"
983
+ : "";
956
984
  let html =
957
985
  "<div id=\\"wg-progress\\"><div id=\\"wg-progress-bar\\" style=\\"width:" + pct + "%\\"></div></div>" +
958
986
  episodesHtml +
987
+ paceHtml +
959
988
  "<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
960
989
  "<h3>Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + "</h3>" +
961
990
  narrationHtml;
@@ -991,7 +1020,12 @@ async function renderBeforeStep(page, info) {
991
1020
  }
992
1021
  if (window.__wgWirePanelChrome) {
993
1022
  window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
994
- stepLabel: (info.index + 1) + " / " + info.total + (info.blockName ? " · " + info.blockName : ""),
1023
+ stepLabel:
1024
+ (info.index + 1) +
1025
+ " / " +
1026
+ info.total +
1027
+ (info.blockName ? " · " + info.blockName : "") +
1028
+ (info.paceBadge ? " · " + info.paceBadge : ""),
995
1029
  forceCollapsed: info.forceCollapsed === true ? true : info.forceCollapsed === false ? false : undefined,
996
1030
  });
997
1031
  }
@@ -1073,6 +1107,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1073
1107
  // defaultHoldMs: when set (fail path), use instead of legacy 900/200 so BUG
1074
1108
  // rings stay visible ~2s before the error panel (PIA stubOnError RFC).
1075
1109
  const defaultHoldMs = opts && opts.defaultHoldMs != null ? opts.defaultHoldMs : null;
1110
+ const pace = opts && opts.pace !== undefined ? opts.pace : undefined;
1076
1111
  for (let i = 0; i < list.length; i++) {
1077
1112
  const h = list[i];
1078
1113
  try {
@@ -1082,7 +1117,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1082
1117
  size: h.size,
1083
1118
  weight: h.weight,
1084
1119
  });
1085
- const authored = resolveFixtureDwellMs(h, { gatesFast });
1120
+ const authored = resolveFixtureDwellMs(h, { gatesFast, pace });
1086
1121
  const legacyMs = i === list.length - 1 ? 200 : 900;
1087
1122
  const holdMs =
1088
1123
  authored != null ? authored : defaultHoldMs != null ? defaultHoldMs : legacyMs;
@@ -1139,7 +1174,9 @@ async function renderAfterStep(page, info) {
1139
1174
  // it highlights something [else] and next," not just the first one.
1140
1175
  // duration / fastMode on stubAfter (or flow fixtures) override the legacy
1141
1176
  // 900ms / 200ms holds when set.
1142
- await cycleHighlightRings(page, highlights, gatesFast, null);
1177
+ await cycleHighlightRings(page, highlights, gatesFast, {
1178
+ pace: info.pace,
1179
+ });
1143
1180
  await page
1144
1181
  .evaluate((info) => {
1145
1182
  // Reused in place - see renderBeforeStep's own comment on this.
@@ -1207,9 +1244,20 @@ async function renderAfterStep(page, info) {
1207
1244
  .join("") +
1208
1245
  "</div>"
1209
1246
  : "";
1247
+ const paceHtml =
1248
+ info.paceBadge || info.paceLabel
1249
+ ? "<div class=\\"wg-pace\\" data-pace-kind=\\"" +
1250
+ escA(info.paceKind || "normal") +
1251
+ "\\"><span class=\\"wg-pace-badge\\">" +
1252
+ escA(info.paceBadge || "1x") +
1253
+ "</span><span>" +
1254
+ escA(info.paceLabel || "") +
1255
+ "</span></div>"
1256
+ : "";
1210
1257
  panel.innerHTML =
1211
1258
  "<div id=\\"wg-progress\\"><div id=\\"wg-progress-bar\\" style=\\"width:" + pct + "%\\"></div></div>" +
1212
1259
  episodesHtml +
1260
+ paceHtml +
1213
1261
  "<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
1214
1262
  "<h3>" + heading + "</h3>" +
1215
1263
  resultHtml +
@@ -1220,7 +1268,12 @@ async function renderAfterStep(page, info) {
1220
1268
  }
1221
1269
  if (window.__wgWirePanelChrome) {
1222
1270
  window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
1223
- stepLabel: (info.index + 1) + " / " + info.total + (info.blockName ? " · " + info.blockName : ""),
1271
+ stepLabel:
1272
+ (info.index + 1) +
1273
+ " / " +
1274
+ info.total +
1275
+ (info.blockName ? " · " + info.blockName : "") +
1276
+ (info.paceBadge ? " · " + info.paceBadge : ""),
1224
1277
  forceCollapsed: info.forceCollapsed === true ? true : info.forceCollapsed === false ? false : undefined,
1225
1278
  });
1226
1279
  }
@@ -1322,7 +1375,12 @@ async function renderStepError(page, info) {
1322
1375
  }
1323
1376
  if (window.__wgWirePanelChrome) {
1324
1377
  window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
1325
- stepLabel: (info.index + 1) + " / " + info.total + (info.blockName ? " · " + info.blockName : ""),
1378
+ stepLabel:
1379
+ (info.index + 1) +
1380
+ " / " +
1381
+ info.total +
1382
+ (info.blockName ? " · " + info.blockName : "") +
1383
+ (info.paceBadge ? " · " + info.paceBadge : ""),
1326
1384
  forceCollapsed: info.forceCollapsed === true ? true : info.forceCollapsed === false ? false : undefined,
1327
1385
  });
1328
1386
  }
@@ -1530,14 +1588,13 @@ async function presentSlides(page, slides, _gate, opts) {
1530
1588
  const episodeTitle = (opts && opts.episodeTitle) || "";
1531
1589
  const gatesFast = !!(opts && opts.fast);
1532
1590
  const demoPace = normalizeDemoPace((opts && opts.pace) || (gatesFast ? "fast" : "normal"));
1533
- const autoplayMs =
1591
+ const baseAutoplay =
1534
1592
  opts && opts.autoplayMs
1535
1593
  ? Number(opts.autoplayMs)
1536
1594
  : process.env.WAYGRAPH_AUTOPLAY_MS
1537
1595
  ? Number(process.env.WAYGRAPH_AUTOPLAY_MS)
1538
- : demoPace === "slow"
1539
- ? 3500
1540
- : 1800;
1596
+ : 1800;
1597
+ const autoplayMs = demoPaceGateMs(demoPace, baseAutoplay);
1541
1598
  for (let i = 0; i < slides.length; i++) {
1542
1599
  const s = slides[i];
1543
1600
  const caption = formatHighlightCaption(s);
@@ -1577,11 +1634,22 @@ async function presentSlides(page, slides, _gate, opts) {
1577
1634
  (info.episodeTitle ? ": " + esc(info.episodeTitle) : "") +
1578
1635
  " · yap slides</div>"
1579
1636
  : "";
1637
+ const paceHtml =
1638
+ info.paceBadge || info.paceLabel
1639
+ ? "<div class=\\"wg-pace\\" data-pace-kind=\\"" +
1640
+ esc(info.paceKind || "normal") +
1641
+ "\\"><span class=\\"wg-pace-badge\\">" +
1642
+ esc(info.paceBadge || "1x") +
1643
+ "</span><span>" +
1644
+ esc(info.paceLabel || "") +
1645
+ "</span></div>"
1646
+ : "";
1580
1647
  panel.innerHTML =
1581
1648
  "<div class=\\"wg-chrome\\"><span class=\\"wg-chrome-title\\">waygraph demo</span>" +
1582
1649
  "<button type=\\"button\\" class=\\"wg-hide-btn\\" data-wg-toggle=\\"1\\">Hide</button></div>" +
1583
1650
  "<div class=\\"wg-body\\">" +
1584
1651
  episodeLine +
1652
+ paceHtml +
1585
1653
  "<h3>Slide " +
1586
1654
  (info.index + 1) +
1587
1655
  " / " +
@@ -1699,6 +1767,9 @@ async function presentSlides(page, slides, _gate, opts) {
1699
1767
  dwellMs,
1700
1768
  episodeNumber: episodeNumber !== undefined ? episodeNumber : null,
1701
1769
  episodeTitle,
1770
+ paceBadge: formatDemoPaceBadge(demoPace, baseAutoplay),
1771
+ paceLabel: formatDemoPaceLabel(demoPace, baseAutoplay),
1772
+ paceKind: demoPaceKind(demoPace),
1702
1773
  },
1703
1774
  );
1704
1775
  const started = Date.now();
@@ -1731,6 +1802,25 @@ async function presentSlides(page, slides, _gate, opts) {
1731
1802
  await hideRing(page);
1732
1803
  }
1733
1804
 
1805
+ function demoPaceKind(pace) {
1806
+ const p = normalizeDemoPace(pace);
1807
+ if (typeof p === "number" && p > 20) return "ms";
1808
+ if (typeof p === "number") return demoPaceIsSlow(p) ? "num-slow" : demoPaceIsFast(p) ? "fast" : "normal";
1809
+ if (p === "blitz" || p === "fast" || p === "slow") return p;
1810
+ return "normal";
1811
+ }
1812
+
1813
+ function paceSpeakFields(pace, baseAutoplayMs) {
1814
+ const p = normalizeDemoPace(pace);
1815
+ const base = Number.isFinite(baseAutoplayMs) && baseAutoplayMs > 0 ? baseAutoplayMs : 1800;
1816
+ return {
1817
+ pace: p,
1818
+ paceBadge: formatDemoPaceBadge(p, base),
1819
+ paceLabel: formatDemoPaceLabel(p, base),
1820
+ paceKind: demoPaceKind(p),
1821
+ };
1822
+ }
1823
+
1734
1824
  async function hideRing(page) {
1735
1825
  await page
1736
1826
  .evaluate(() => {
@@ -2160,13 +2250,10 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2160
2250
  // gates too, not just skip its interaction dwell - otherwise autoplay
2161
2251
  // still stalls the full autoplayMs admiring a step that intentionally
2162
2252
  // ran too fast to watch.
2163
- const ms = pacing.skipTheater
2164
- ? Math.min(80, autoplayMs)
2165
- : pacing.gatesFast
2166
- ? Math.min(400, autoplayMs)
2167
- : pacing.gatesSlow
2168
- ? Math.max(autoplayMs * 2, 3500)
2169
- : autoplayMs;
2253
+ const ms = demoPaceGateMs(
2254
+ pacing.skipTheater ? "blitz" : pacing.demoPace,
2255
+ autoplayMs,
2256
+ );
2170
2257
  let elapsed = 0;
2171
2258
  for (;;) {
2172
2259
  // Re-read the checkbox EVERY loop tick, not once up front - a human
@@ -2249,31 +2336,32 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2249
2336
  if ((clearSession || r.resetSession) && i > 0) {
2250
2337
  await resetPageState(context, page, baseURL);
2251
2338
  }
2252
- // gatesFast / skipTheater: opaque FF, former-FF inners (--ff-disabled/--ff-expand),
2253
- // WAYGRAPH_FAST_BLOCKS, or withDemoPace/withBlockPace. --fast alone = shorter
2254
- // gates only (keeps theater). Episode pace: block > flow > CLI --fast.
2339
+ // Episode pace: block > flow > CLI --fast (only when no authored pace).
2340
+ // Authoring wins: withDemoPace("slow"|2|4500) keeps slow dwell even under --fast.
2255
2341
  const stepPace = resolveStepDemoPace({
2256
2342
  blockPace: r.block.demoPace,
2257
2343
  flowPace: r.demoPace,
2258
2344
  fastForward: !!r.block.fastForward,
2259
2345
  wasFastForward: !!r.wasFastForward,
2260
2346
  });
2347
+ const authoredPace =
2348
+ (r.block.demoPace != null && r.block.demoPace !== "") ||
2349
+ (r.demoPace != null && r.demoPace !== "");
2261
2350
  const cliFast = demoFast || fastBlockNames.has(r.block.name);
2262
2351
  pacing.demoPace =
2263
- stepPace === "normal" && cliFast
2352
+ stepPace === "normal" && cliFast && !authoredPace
2264
2353
  ? fastBlockNames.has(r.block.name)
2265
2354
  ? "blitz"
2266
2355
  : "fast"
2267
2356
  : stepPace;
2268
2357
  pacing.gatesFast =
2269
- pacing.demoPace === "blitz" ||
2270
- pacing.demoPace === "fast" ||
2358
+ demoPaceIsFast(pacing.demoPace) ||
2271
2359
  !!r.block.fastForward ||
2272
2360
  !!r.wasFastForward ||
2273
- demoFast;
2274
- pacing.gatesSlow = pacing.demoPace === "slow";
2361
+ (!authoredPace && demoFast);
2362
+ pacing.gatesSlow = demoPaceIsSlow(pacing.demoPace);
2275
2363
  pacing.skipTheater =
2276
- pacing.demoPace === "blitz" ||
2364
+ demoPaceIsBlitz(pacing.demoPace) ||
2277
2365
  fastBlockNames.has(r.block.name) ||
2278
2366
  !!r.block.fastForward ||
2279
2367
  !!r.wasFastForward;
@@ -2297,6 +2385,20 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2297
2385
  }
2298
2386
  return { name: k.name, value };
2299
2387
  });
2388
+ const paceSpeak = paceSpeakFields(pacing.demoPace, autoplayMs);
2389
+ if (process.env.WAYGRAPH_JSON !== "1") {
2390
+ console.log(
2391
+ "waygraph demo: step " +
2392
+ (i + 1) +
2393
+ "/" +
2394
+ resolved.length +
2395
+ " " +
2396
+ r.block.name +
2397
+ " · " +
2398
+ paceSpeak.paceLabel +
2399
+ (r.episodeNumber ? " · episode " + r.episodeNumber : ""),
2400
+ );
2401
+ }
2300
2402
  await renderBeforeStep(page, {
2301
2403
  index: i,
2302
2404
  total: resolved.length,
@@ -2313,6 +2415,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2313
2415
  episodeTitle: r.episodeTitle,
2314
2416
  stepperMode,
2315
2417
  forceCollapsed,
2418
+ ...paceSpeak,
2316
2419
  });
2317
2420
  const edits = await gate();
2318
2421
  for (const k of requires) {
@@ -2387,6 +2490,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2387
2490
  });
2388
2491
  await cycleHighlightRings(page, errHighlights, !!pacing.gatesFast, {
2389
2492
  defaultHoldMs: 2000,
2493
+ pace: pacing.demoPace,
2390
2494
  });
2391
2495
  }
2392
2496
  // A thrown act()/observe()/a failed verify Trait used to just crash
@@ -2480,6 +2584,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2480
2584
  resultTag: JSON.stringify(result),
2481
2585
  highlights,
2482
2586
  gatesFast: pacing.gatesFast,
2587
+ pace: pacing.demoPace,
2588
+ ...paceSpeakFields(pacing.demoPace, autoplayMs),
2483
2589
  isLast: i === resolved.length - 1,
2484
2590
  allNames: moduleNames,
2485
2591
  allDescriptions: moduleDescriptions,
package/dist/engine.d.ts CHANGED
@@ -269,6 +269,9 @@ export declare function withTitle<Out extends Checkpoint<string>>(flow: Flow<Out
269
269
  * Non-destructive. `fastForwardComposeBlock` is always blitz for its own step;
270
270
  * use this when an entire episode should be fast or slow.
271
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
272
275
  */
273
276
  export declare function withDemoPace<Out extends Checkpoint<string>>(flow: Flow<Out>, pace: DemoPace): Flow<Out>;
274
277
  /**
package/dist/engine.js CHANGED
@@ -357,6 +357,9 @@ export function withTitle(flow, title) {
357
357
  * Non-destructive. `fastForwardComposeBlock` is always blitz for its own step;
358
358
  * use this when an entire episode should be fast or slow.
359
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
360
363
  */
361
364
  export function withDemoPace(flow, pace) {
362
365
  const demoPace = pace;
@@ -106,12 +106,14 @@ export declare function toneIconPrefix(tone: HighlightTone): string;
106
106
  /**
107
107
  * Demo pacing for a Flow episode or a Block/compose group.
108
108
  * - `blitz` - FF-like: skip theater + short gates (same as fastForwardComposeBlock)
109
- * - `fast` - shorter gates, keep cursor theater
110
- * - `normal` - default demo pace
111
- * - `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`)
112
114
  */
113
- export type DemoPace = "blitz" | "fast" | "normal" | "slow";
114
- export declare function normalizeDemoPace(raw: unknown): DemoPace;
115
+ export type DemoPaceName = "blitz" | "fast" | "normal" | "slow";
116
+ export type DemoPace = DemoPaceName | number;
115
117
  /** Default fixture dwell when `duration: true`. */
116
118
  export declare const FIXTURE_DURATION_MS_DEFAULT = 2000;
117
119
  /** Default fixture dwell under `--fast` when duration is enabled. */
@@ -122,9 +124,31 @@ export declare const FIXTURE_DURATION_SLOW_MS_DEFAULT = 4000;
122
124
  export declare const SLIDE_DURATION_MS_DEFAULT = 2000;
123
125
  /** @deprecated Prefer FIXTURE_DURATION_FAST_MS_DEFAULT */
124
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;
136
+ /**
137
+ * Short chip for the demo panel (e.g. `2.5x`, `4500ms`, `slow`).
138
+ * Always printable - numbers speak in the UI, not only in code.
139
+ */
140
+ export declare function formatDemoPaceBadge(pace: DemoPace, baseAutoplayMs?: number): string;
141
+ /**
142
+ * Loud one-line pace description for panel + console
143
+ * (e.g. `pace 2.5x (~4500ms gates)` / `pace 4500ms gates (~2.25x)`).
144
+ */
145
+ export declare function formatDemoPaceLabel(pace: DemoPace, baseAutoplayMs?: number): string;
125
146
  /**
126
147
  * Resolve min dwell ms for any fixture (stub / flow fixture / yap slide),
127
148
  * or `null` when duration is unset (caller keeps legacy timing).
149
+ *
150
+ * Authoring wins: an explicit slow/numeric pace is not crushed by CLI `--fast`
151
+ * (`gatesFast`). `--fast` only shortens when pace is unset/normal.
128
152
  */
129
153
  export declare function resolveFixtureDwellMs(fixture: FixtureDurationFields, opts?: {
130
154
  gatesFast?: boolean;
@@ -140,8 +164,8 @@ export declare function resolveSlideDwellMs(slide: FixtureDurationFields, opts?:
140
164
  * Opaque FF / wasFastForward always win as blitz.
141
165
  */
142
166
  export declare function resolveStepDemoPace(opts: {
143
- blockPace?: DemoPace | string | null;
144
- flowPace?: DemoPace | string | null;
167
+ blockPace?: DemoPace | string | number | null;
168
+ flowPace?: DemoPace | string | number | null;
145
169
  fastForward?: boolean;
146
170
  wasFastForward?: boolean;
147
171
  }): DemoPace;
@@ -79,18 +79,6 @@ export function toneIconPrefix(tone) {
79
79
  const icon = TONE_ICONS[tone];
80
80
  return icon ? `[${icon}] ` : "";
81
81
  }
82
- export function normalizeDemoPace(raw) {
83
- if (typeof raw !== "string")
84
- return "normal";
85
- const t = raw.trim().toLowerCase();
86
- if (t === "blitz" || t === "ff")
87
- return "blitz";
88
- if (t === "fast" || t === "quick")
89
- return "fast";
90
- if (t === "slow" || t === "careful" || t === "review")
91
- return "slow";
92
- return "normal";
93
- }
94
82
  /** Default fixture dwell when `duration: true`. */
95
83
  export const FIXTURE_DURATION_MS_DEFAULT = 2000;
96
84
  /** Default fixture dwell under `--fast` when duration is enabled. */
@@ -101,24 +89,157 @@ export const FIXTURE_DURATION_SLOW_MS_DEFAULT = 4000;
101
89
  export const SLIDE_DURATION_MS_DEFAULT = FIXTURE_DURATION_MS_DEFAULT;
102
90
  /** @deprecated Prefer FIXTURE_DURATION_FAST_MS_DEFAULT */
103
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
+ }
165
+ /**
166
+ * Short chip for the demo panel (e.g. `2.5x`, `4500ms`, `slow`).
167
+ * Always printable - numbers speak in the UI, not only in code.
168
+ */
169
+ export function formatDemoPaceBadge(pace, baseAutoplayMs = 1800) {
170
+ const p = normalizeDemoPace(pace);
171
+ if (typeof p === "number" && p > 20)
172
+ return `${Math.round(p)}ms`;
173
+ if (typeof p === "number") {
174
+ const s = Number.isInteger(p) ? String(p) : String(Math.round(p * 100) / 100);
175
+ return `${s}x`;
176
+ }
177
+ if (p === "blitz")
178
+ return "blitz";
179
+ if (p === "fast")
180
+ return "fast";
181
+ if (p === "slow")
182
+ return "slow";
183
+ void baseAutoplayMs;
184
+ return "1x";
185
+ }
186
+ /**
187
+ * Loud one-line pace description for panel + console
188
+ * (e.g. `pace 2.5x (~4500ms gates)` / `pace 4500ms gates (~2.25x)`).
189
+ */
190
+ export function formatDemoPaceLabel(pace, baseAutoplayMs = 1800) {
191
+ const p = normalizeDemoPace(pace);
192
+ const gate = demoPaceGateMs(p, baseAutoplayMs);
193
+ const scale = demoPaceScale(p);
194
+ const scaleTxt = Number.isInteger(scale) ? String(scale) : (Math.round(scale * 100) / 100).toString();
195
+ if (typeof p === "number" && p > 20) {
196
+ return `pace ${Math.round(p)}ms gates (~${scaleTxt}x)`;
197
+ }
198
+ if (typeof p === "number") {
199
+ const s = Number.isInteger(p) ? String(p) : String(Math.round(p * 100) / 100);
200
+ return `pace ${s}x (~${gate}ms gates)`;
201
+ }
202
+ if (p === "blitz")
203
+ return `pace blitz (~${gate}ms, skip theater)`;
204
+ if (p === "fast")
205
+ return `pace fast (~${scaleTxt}x, ~${gate}ms gates)`;
206
+ if (p === "slow")
207
+ return `pace slow (~${scaleTxt}x, ~${gate}ms gates)`;
208
+ return `pace normal (1x, ~${gate}ms gates)`;
209
+ }
104
210
  /**
105
211
  * Resolve min dwell ms for any fixture (stub / flow fixture / yap slide),
106
212
  * or `null` when duration is unset (caller keeps legacy timing).
213
+ *
214
+ * Authoring wins: an explicit slow/numeric pace is not crushed by CLI `--fast`
215
+ * (`gatesFast`). `--fast` only shortens when pace is unset/normal.
107
216
  */
108
217
  export function resolveFixtureDwellMs(fixture, opts) {
109
218
  if (fixture.duration === undefined || fixture.duration === false)
110
219
  return null;
111
- const pace = normalizeDemoPace(opts?.pace ?? (opts?.gatesFast ? "fast" : "normal"));
220
+ const pace = opts?.pace !== undefined && opts.pace !== null
221
+ ? normalizeDemoPace(opts.pace)
222
+ : opts?.gatesFast
223
+ ? "fast"
224
+ : "normal";
112
225
  const fastMs = typeof fixture.fastMode === "number" && Number.isFinite(fixture.fastMode) && fixture.fastMode >= 0
113
226
  ? fixture.fastMode
114
227
  : FIXTURE_DURATION_FAST_MS_DEFAULT;
115
- if (pace === "blitz" || pace === "fast" || opts?.gatesFast)
116
- return fastMs;
117
228
  const normalMs = fixture.duration === true
118
229
  ? FIXTURE_DURATION_MS_DEFAULT
119
230
  : typeof fixture.duration === "number" && Number.isFinite(fixture.duration) && fixture.duration >= 0
120
231
  ? fixture.duration
121
232
  : FIXTURE_DURATION_MS_DEFAULT;
233
+ if (typeof pace === "number") {
234
+ if (pace > 20)
235
+ return Math.round(pace);
236
+ return Math.max(0, Math.round(normalMs * Math.min(10, Math.max(0.05, pace))));
237
+ }
238
+ if (pace === "blitz" || pace === "fast")
239
+ return fastMs;
240
+ // CLI --fast only when author left pace at normal
241
+ if (opts?.gatesFast && pace === "normal")
242
+ return fastMs;
122
243
  if (pace === "slow")
123
244
  return Math.max(normalMs, FIXTURE_DURATION_SLOW_MS_DEFAULT);
124
245
  return normalMs;
package/dist/index.d.ts CHANGED
@@ -3,8 +3,8 @@ 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
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, 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, } from "./highlights.js";
6
+ export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, 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
@@ -2,7 +2,7 @@ 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
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, 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";
5
+ export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, 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.17",
3
+ "version": "0.12.19",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",