@graphrefly/graphrefly 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +8 -0
  2. package/dist/{chunk-QOWVNWOC.js → chunk-3ZWCKRHX.js} +27 -25
  3. package/dist/{chunk-QOWVNWOC.js.map → chunk-3ZWCKRHX.js.map} +1 -1
  4. package/dist/chunk-6LDQFTYS.js +102 -0
  5. package/dist/chunk-6LDQFTYS.js.map +1 -0
  6. package/dist/{chunk-5WGT55R4.js → chunk-AMCG74RZ.js} +195 -24
  7. package/dist/chunk-AMCG74RZ.js.map +1 -0
  8. package/dist/{chunk-AOCBDH4T.js → chunk-BVZYTZ5H.js} +76 -103
  9. package/dist/chunk-BVZYTZ5H.js.map +1 -0
  10. package/dist/chunk-FQMKGR6L.js +330 -0
  11. package/dist/chunk-FQMKGR6L.js.map +1 -0
  12. package/dist/chunk-HXZEYDUR.js +94 -0
  13. package/dist/chunk-HXZEYDUR.js.map +1 -0
  14. package/dist/{chunk-IPLKX3L2.js → chunk-IZYUSJC7.js} +16 -14
  15. package/dist/{chunk-IPLKX3L2.js.map → chunk-IZYUSJC7.js.map} +1 -1
  16. package/dist/chunk-J22W6HV3.js +107 -0
  17. package/dist/chunk-J22W6HV3.js.map +1 -0
  18. package/dist/{chunk-HWPIFSW2.js → chunk-JSCT3CR4.js} +6 -4
  19. package/dist/{chunk-HWPIFSW2.js.map → chunk-JSCT3CR4.js.map} +1 -1
  20. package/dist/chunk-JYXEWPH4.js +62 -0
  21. package/dist/chunk-JYXEWPH4.js.map +1 -0
  22. package/dist/chunk-LCE3GF5P.js +866 -0
  23. package/dist/chunk-LCE3GF5P.js.map +1 -0
  24. package/dist/chunk-MJ2NKQQL.js +119 -0
  25. package/dist/chunk-MJ2NKQQL.js.map +1 -0
  26. package/dist/chunk-N6UR7YVY.js +198 -0
  27. package/dist/chunk-N6UR7YVY.js.map +1 -0
  28. package/dist/chunk-OHISZPOJ.js +97 -0
  29. package/dist/chunk-OHISZPOJ.js.map +1 -0
  30. package/dist/{chunk-5DJTTKX3.js → chunk-PHOUUNK7.js} +74 -111
  31. package/dist/chunk-PHOUUNK7.js.map +1 -0
  32. package/dist/{chunk-PY4XCDLR.js → chunk-RB6QPHJ7.js} +8 -6
  33. package/dist/{chunk-PY4XCDLR.js.map → chunk-RB6QPHJ7.js.map} +1 -1
  34. package/dist/chunk-SN4YWWYO.js +171 -0
  35. package/dist/chunk-SN4YWWYO.js.map +1 -0
  36. package/dist/chunk-SX52TAR4.js +110 -0
  37. package/dist/chunk-SX52TAR4.js.map +1 -0
  38. package/dist/{chunk-XOFWRC73.js → chunk-THTWHNU4.js} +319 -24
  39. package/dist/chunk-THTWHNU4.js.map +1 -0
  40. package/dist/{chunk-H4RVA4VE.js → chunk-VYPWMZ6H.js} +2 -2
  41. package/dist/chunk-XGPU467M.js +136 -0
  42. package/dist/chunk-XGPU467M.js.map +1 -0
  43. package/dist/{chunk-TDEXAMGO.js → chunk-ZQMEI34O.js} +206 -574
  44. package/dist/chunk-ZQMEI34O.js.map +1 -0
  45. package/dist/compat/index.cjs +7656 -0
  46. package/dist/compat/index.cjs.map +1 -0
  47. package/dist/compat/index.d.cts +18 -0
  48. package/dist/compat/index.d.ts +18 -0
  49. package/dist/compat/index.js +49 -0
  50. package/dist/compat/index.js.map +1 -0
  51. package/dist/compat/jotai/index.cjs +2048 -0
  52. package/dist/compat/jotai/index.cjs.map +1 -0
  53. package/dist/compat/jotai/index.d.cts +2 -0
  54. package/dist/compat/jotai/index.d.ts +2 -0
  55. package/dist/compat/jotai/index.js +9 -0
  56. package/dist/compat/jotai/index.js.map +1 -0
  57. package/dist/compat/nanostores/index.cjs +2175 -0
  58. package/dist/compat/nanostores/index.cjs.map +1 -0
  59. package/dist/compat/nanostores/index.d.cts +2 -0
  60. package/dist/compat/nanostores/index.d.ts +2 -0
  61. package/dist/compat/nanostores/index.js +23 -0
  62. package/dist/compat/nanostores/index.js.map +1 -0
  63. package/dist/compat/nestjs/index.cjs +350 -16
  64. package/dist/compat/nestjs/index.cjs.map +1 -1
  65. package/dist/compat/nestjs/index.d.cts +6 -6
  66. package/dist/compat/nestjs/index.d.ts +6 -6
  67. package/dist/compat/nestjs/index.js +10 -9
  68. package/dist/compat/react/index.cjs +141 -0
  69. package/dist/compat/react/index.cjs.map +1 -0
  70. package/dist/compat/react/index.d.cts +2 -0
  71. package/dist/compat/react/index.d.ts +2 -0
  72. package/dist/compat/react/index.js +12 -0
  73. package/dist/compat/react/index.js.map +1 -0
  74. package/dist/compat/solid/index.cjs +128 -0
  75. package/dist/compat/solid/index.cjs.map +1 -0
  76. package/dist/compat/solid/index.d.cts +2 -0
  77. package/dist/compat/solid/index.d.ts +2 -0
  78. package/dist/compat/solid/index.js +12 -0
  79. package/dist/compat/solid/index.js.map +1 -0
  80. package/dist/compat/svelte/index.cjs +131 -0
  81. package/dist/compat/svelte/index.cjs.map +1 -0
  82. package/dist/compat/svelte/index.d.cts +2 -0
  83. package/dist/compat/svelte/index.d.ts +2 -0
  84. package/dist/compat/svelte/index.js +12 -0
  85. package/dist/compat/svelte/index.js.map +1 -0
  86. package/dist/compat/vue/index.cjs +146 -0
  87. package/dist/compat/vue/index.cjs.map +1 -0
  88. package/dist/compat/vue/index.d.cts +3 -0
  89. package/dist/compat/vue/index.d.ts +3 -0
  90. package/dist/compat/vue/index.js +12 -0
  91. package/dist/compat/vue/index.js.map +1 -0
  92. package/dist/compat/zustand/index.cjs +4931 -0
  93. package/dist/compat/zustand/index.cjs.map +1 -0
  94. package/dist/compat/zustand/index.d.cts +5 -0
  95. package/dist/compat/zustand/index.d.ts +5 -0
  96. package/dist/compat/zustand/index.js +12 -0
  97. package/dist/compat/zustand/index.js.map +1 -0
  98. package/dist/core/index.cjs +53 -4
  99. package/dist/core/index.cjs.map +1 -1
  100. package/dist/core/index.d.cts +3 -3
  101. package/dist/core/index.d.ts +3 -3
  102. package/dist/core/index.js +26 -24
  103. package/dist/demo-shell-26p5fVxn.d.cts +102 -0
  104. package/dist/demo-shell-DEp-nMTl.d.ts +102 -0
  105. package/dist/extra/index.cjs +290 -110
  106. package/dist/extra/index.cjs.map +1 -1
  107. package/dist/extra/index.d.cts +5 -4
  108. package/dist/extra/index.d.ts +5 -4
  109. package/dist/extra/index.js +8 -5
  110. package/dist/extra/sources.cjs +2486 -0
  111. package/dist/extra/sources.cjs.map +1 -0
  112. package/dist/extra/sources.d.cts +465 -0
  113. package/dist/extra/sources.d.ts +465 -0
  114. package/dist/extra/sources.js +57 -0
  115. package/dist/extra/sources.js.map +1 -0
  116. package/dist/graph/index.cjs +408 -14
  117. package/dist/graph/index.cjs.map +1 -1
  118. package/dist/graph/index.d.cts +5 -5
  119. package/dist/graph/index.d.ts +5 -5
  120. package/dist/graph/index.js +13 -5
  121. package/dist/{graph-D-3JIQme.d.cts → graph-6tZ5jEzr.d.cts} +195 -4
  122. package/dist/{graph-B6NFqv3z.d.ts → graph-DQ69XU0g.d.ts} +195 -4
  123. package/dist/index-B4MP_8V_.d.cts +37 -0
  124. package/dist/index-BEfE8H_G.d.cts +121 -0
  125. package/dist/{index-D7XgsUt7.d.ts → index-BW1z3BN9.d.ts} +169 -127
  126. package/dist/index-BYOHF0zP.d.ts +34 -0
  127. package/dist/index-B_IP40nB.d.cts +36 -0
  128. package/dist/index-Bd_fwmLf.d.cts +45 -0
  129. package/dist/{index-BysCTzJz.d.ts → index-BeIdBfcb.d.cts} +121 -547
  130. package/dist/index-BjI6ty9z.d.ts +121 -0
  131. package/dist/index-Bxb5ZYc9.d.cts +34 -0
  132. package/dist/{index-BJB7t9gg.d.cts → index-C0ZXMaXO.d.cts} +2 -2
  133. package/dist/{index-b5BYtczN.d.cts → index-C8mdwMXc.d.cts} +169 -127
  134. package/dist/index-CDAjUFIv.d.ts +36 -0
  135. package/dist/index-CPgZ5wRl.d.ts +44 -0
  136. package/dist/{index-AMWewNDe.d.cts → index-CUwyr1Kk.d.cts} +33 -4
  137. package/dist/index-CUyrtuOf.d.cts +127 -0
  138. package/dist/{index-C-TXEa7C.d.ts → index-CY2TljO4.d.ts} +2 -2
  139. package/dist/index-CmnuOibw.d.ts +37 -0
  140. package/dist/{index-DiobMNwE.d.ts → index-CuYwdKO-.d.ts} +3 -3
  141. package/dist/index-DFhjO4Gg.d.cts +44 -0
  142. package/dist/{index-1z8vRTCt.d.cts → index-DdD5MVDL.d.ts} +121 -547
  143. package/dist/index-DrISNAOm.d.ts +45 -0
  144. package/dist/index-QBpffFW-.d.cts +86 -0
  145. package/dist/{index-J7Kc0oIQ.d.cts → index-_oMEWlDq.d.cts} +3 -3
  146. package/dist/{index-CYkjxu3s.d.ts → index-eJ6T_qGM.d.ts} +33 -4
  147. package/dist/index-qldRdbQw.d.ts +86 -0
  148. package/dist/index-xdGjv0nO.d.ts +127 -0
  149. package/dist/index.cjs +2334 -195
  150. package/dist/index.cjs.map +1 -1
  151. package/dist/index.d.cts +1007 -648
  152. package/dist/index.d.ts +1007 -648
  153. package/dist/index.js +1204 -1172
  154. package/dist/index.js.map +1 -1
  155. package/dist/{meta-CnkLA_43.d.ts → meta-BGqSZ7mt.d.ts} +1 -1
  156. package/dist/{meta-DWbkoq1s.d.cts → meta-C0-8XW6Q.d.cts} +1 -1
  157. package/dist/{node-B-f-Lu-k.d.cts → node-C_IBuvX2.d.cts} +26 -1
  158. package/dist/{node-B-f-Lu-k.d.ts → node-C_IBuvX2.d.ts} +26 -1
  159. package/dist/{observable-DBnrwcar.d.cts → observable-Crr1jgzx.d.cts} +1 -1
  160. package/dist/{observable-uP-wy_uK.d.ts → observable-DCk45RH5.d.ts} +1 -1
  161. package/dist/patterns/demo-shell.cjs +5604 -0
  162. package/dist/patterns/demo-shell.cjs.map +1 -0
  163. package/dist/patterns/demo-shell.d.cts +6 -0
  164. package/dist/patterns/demo-shell.d.ts +6 -0
  165. package/dist/patterns/demo-shell.js +15 -0
  166. package/dist/patterns/demo-shell.js.map +1 -0
  167. package/dist/patterns/reactive-layout/index.cjs +843 -29
  168. package/dist/patterns/reactive-layout/index.cjs.map +1 -1
  169. package/dist/patterns/reactive-layout/index.d.cts +6 -5
  170. package/dist/patterns/reactive-layout/index.d.ts +6 -5
  171. package/dist/patterns/reactive-layout/index.js +25 -10
  172. package/dist/reactive-layout-BaOQefHu.d.cts +183 -0
  173. package/dist/reactive-layout-D9gejYXE.d.ts +183 -0
  174. package/dist/{storage-BuTdpCI1.d.cts → storage-BMycWEh2.d.ts} +9 -1
  175. package/dist/{storage-F2X1U1x0.d.ts → storage-DiqWHzVI.d.cts} +9 -1
  176. package/package.json +32 -2
  177. package/dist/chunk-5DJTTKX3.js.map +0 -1
  178. package/dist/chunk-5WGT55R4.js.map +0 -1
  179. package/dist/chunk-AOCBDH4T.js.map +0 -1
  180. package/dist/chunk-MW4VAKAO.js +0 -47
  181. package/dist/chunk-MW4VAKAO.js.map +0 -1
  182. package/dist/chunk-TDEXAMGO.js.map +0 -1
  183. package/dist/chunk-XOFWRC73.js.map +0 -1
  184. /package/dist/{chunk-H4RVA4VE.js.map → chunk-VYPWMZ6H.js.map} +0 -0
@@ -27,14 +27,20 @@ __export(reactive_layout_exports, {
27
27
  PrecomputedAdapter: () => PrecomputedAdapter,
28
28
  SvgBoundsAdapter: () => SvgBoundsAdapter,
29
29
  analyzeAndMeasure: () => analyzeAndMeasure,
30
+ carveTextLineSlots: () => carveTextLineSlots,
31
+ circleIntervalForBand: () => circleIntervalForBand,
30
32
  computeBlockFlow: () => computeBlockFlow,
31
33
  computeCharPositions: () => computeCharPositions,
34
+ computeFlowLines: () => computeFlowLines,
32
35
  computeLineBreaks: () => computeLineBreaks,
33
36
  computeTotalHeight: () => computeTotalHeight,
37
+ layoutNextLine: () => layoutNextLine,
34
38
  measureBlock: () => measureBlock,
35
39
  measureBlocks: () => measureBlocks,
36
40
  reactiveBlockLayout: () => reactiveBlockLayout,
37
- reactiveLayout: () => reactiveLayout
41
+ reactiveFlowLayout: () => reactiveFlowLayout,
42
+ reactiveLayout: () => reactiveLayout,
43
+ rectIntervalForBand: () => rectIntervalForBand
38
44
  });
39
45
  module.exports = __toCommonJS(reactive_layout_exports);
40
46
 
@@ -232,7 +238,7 @@ var CanvasMeasureAdapter = class {
232
238
  this.currentFont = font;
233
239
  }
234
240
  let width = ctx.measureText(text).width;
235
- if (this.emojiCorrection !== 1 && /\p{Emoji_Presentation}/u.test(text)) {
241
+ if (this.emojiCorrection !== 1 && new RegExp("\\p{Emoji_Presentation}", "u").test(text)) {
236
242
  width *= this.emojiCorrection;
237
243
  }
238
244
  return { width };
@@ -1159,6 +1165,12 @@ var NodeImpl = class _NodeImpl {
1159
1165
  _autoError;
1160
1166
  _pausable;
1161
1167
  _guard;
1168
+ /**
1169
+ * @internal Additional guards stacked at runtime via {@link NodeImpl._pushGuard}
1170
+ * (e.g. by `policyEnforcer({ mode: "enforce" })`, roadmap §9.2). Effective
1171
+ * write/signal/observe checks AND the original `_guard` with every entry here.
1172
+ */
1173
+ _extraGuards;
1162
1174
  _hashFn;
1163
1175
  _versioning;
1164
1176
  /**
@@ -1332,18 +1344,61 @@ var NodeImpl = class _NodeImpl {
1332
1344
  if (this._inspectorHooks?.size === 0) this._inspectorHooks = void 0;
1333
1345
  };
1334
1346
  }
1347
+ /**
1348
+ * @internal Push an additional guard onto this node. Effective enforcement
1349
+ * is the AND of `_guard` and every guard pushed via this hook — any one
1350
+ * rejecting throws {@link GuardDenied}. Returns a disposer that removes
1351
+ * the pushed guard. Multiple guards may be stacked simultaneously.
1352
+ *
1353
+ * Used by `policyEnforcer({ mode: "enforce" })` (roadmap §9.2) to overlay
1354
+ * runtime constraint enforcement onto an existing graph without rebuilding
1355
+ * its nodes. Pre-1.0 internal API; not part of the public surface.
1356
+ *
1357
+ * **Identity semantics:** guards are tracked in a `Set`, so pushing the
1358
+ * same `NodeGuard` reference twice is a single registration. Wrap each
1359
+ * push in a unique closure if independent stacking is needed.
1360
+ *
1361
+ * **Iteration order:** insertion-ordered (`Set` semantics). Determinism
1362
+ * follows from single-threaded JS execution; nested re-entry from inside
1363
+ * a guard body (push/pop while iterating) is undefined-but-survivable.
1364
+ */
1365
+ _pushGuard(guard) {
1366
+ if (this._extraGuards == null) this._extraGuards = /* @__PURE__ */ new Set();
1367
+ this._extraGuards.add(guard);
1368
+ return () => {
1369
+ this._extraGuards?.delete(guard);
1370
+ if (this._extraGuards?.size === 0) this._extraGuards = void 0;
1371
+ };
1372
+ }
1335
1373
  allowsObserve(actor) {
1336
- if (this._guard == null) return true;
1337
- return this._guard(normalizeActor(actor), "observe");
1374
+ if (this._guard == null && this._extraGuards == null) return true;
1375
+ const a = normalizeActor(actor);
1376
+ if (this._guard != null && !this._guard(a, "observe")) return false;
1377
+ if (this._extraGuards != null) {
1378
+ for (const eg of this._extraGuards) {
1379
+ if (!eg(a, "observe")) return false;
1380
+ }
1381
+ }
1382
+ return true;
1338
1383
  }
1339
1384
  // --- Guard helper ---
1340
1385
  _checkGuard(options) {
1341
- if (options?.internal || this._guard == null) return;
1386
+ if (options?.internal) return;
1387
+ const hasGuard = this._guard != null || this._extraGuards != null;
1388
+ const hasActor = options?.actor != null;
1389
+ if (!hasGuard && !hasActor) return;
1342
1390
  const actor = normalizeActor(options?.actor);
1343
1391
  const action = options?.delivery === "signal" ? "signal" : "write";
1344
- if (!this._guard(actor, action)) {
1392
+ if (this._guard != null && !this._guard(actor, action)) {
1345
1393
  throw new GuardDenied({ actor, action, nodeName: this.name });
1346
1394
  }
1395
+ if (this._extraGuards != null) {
1396
+ for (const eg of this._extraGuards) {
1397
+ if (!eg(actor, action)) {
1398
+ throw new GuardDenied({ actor, action, nodeName: this.name });
1399
+ }
1400
+ }
1401
+ }
1347
1402
  this._lastMutation = { actor, timestamp_ns: wallClockNs() };
1348
1403
  }
1349
1404
  // --- Public transport ---
@@ -2275,6 +2330,10 @@ function sentinelGuard(batchData, ctx, allowPartial) {
2275
2330
  function state(initial, opts) {
2276
2331
  return node([], { ...opts, initial });
2277
2332
  }
2333
+ function producer(fn, opts) {
2334
+ const wrapped = (_data, actions, ctx) => fn(actions, ctx) ?? void 0;
2335
+ return node(wrapped, { describeKind: "producer", ...opts });
2336
+ }
2278
2337
  function derived(deps, fn, opts) {
2279
2338
  const allowPartial = opts?.partial ?? false;
2280
2339
  const wrapped = (batchData, actions, ctx) => {
@@ -2477,6 +2536,200 @@ var RingBuffer = class {
2477
2536
  }
2478
2537
  };
2479
2538
 
2539
+ // src/graph/explain.ts
2540
+ function explainPath(described, from, to, opts = {}) {
2541
+ const fromExists = from in described.nodes;
2542
+ const toExists = to in described.nodes;
2543
+ if (!fromExists) return makeFailure(from, to, "no-such-from");
2544
+ if (!toExists) return makeFailure(from, to, "no-such-to");
2545
+ const maxDepth = opts.maxDepth;
2546
+ if (maxDepth != null && (!Number.isInteger(maxDepth) || maxDepth < 0)) {
2547
+ throw new Error(`explainPath: maxDepth must be an integer >= 0`);
2548
+ }
2549
+ if (from === to) {
2550
+ if (opts.findCycle === true) {
2551
+ const cycle = findShortestCycle(described, from, opts);
2552
+ if (cycle != null) return cycle;
2553
+ }
2554
+ const step = buildStep(from, described.nodes[from], 0, opts);
2555
+ return makeSuccess(from, to, [step]);
2556
+ }
2557
+ if (maxDepth === 0) return makeFailure(from, to, "no-path");
2558
+ const result = bfsShortestPath(described, from, to, maxDepth);
2559
+ if (!result.found) {
2560
+ return makeFailure(from, to, result.truncated ? "max-depth-exceeded" : "no-path");
2561
+ }
2562
+ return makeSuccess(from, to, materializeSteps(described, result.pathOrder, opts));
2563
+ }
2564
+ function bfsShortestPath(described, from, to, maxDepth) {
2565
+ const pred = /* @__PURE__ */ new Map();
2566
+ const queue = [{ path: to, depth: 0 }];
2567
+ const visited = /* @__PURE__ */ new Set([to]);
2568
+ let head = 0;
2569
+ let truncated = false;
2570
+ while (head < queue.length) {
2571
+ const cur = queue[head++];
2572
+ if (cur.path === from) break;
2573
+ if (maxDepth != null && cur.depth >= maxDepth) {
2574
+ const node3 = described.nodes[cur.path];
2575
+ if (node3?.deps && node3.deps.length > 0) truncated = true;
2576
+ continue;
2577
+ }
2578
+ const node2 = described.nodes[cur.path];
2579
+ if (node2 == null) continue;
2580
+ const deps = node2.deps ?? [];
2581
+ const slots = /* @__PURE__ */ new Map();
2582
+ for (let i = 0; i < deps.length; i++) {
2583
+ const dep = deps[i];
2584
+ if (!dep) continue;
2585
+ let arr = slots.get(dep);
2586
+ if (arr == null) {
2587
+ arr = [];
2588
+ slots.set(dep, arr);
2589
+ }
2590
+ arr.push(i);
2591
+ }
2592
+ for (const [dep, indices] of slots) {
2593
+ if (visited.has(dep)) continue;
2594
+ visited.add(dep);
2595
+ pred.set(dep, { from: cur.path, depIndices: indices });
2596
+ queue.push({ path: dep, depth: cur.depth + 1 });
2597
+ }
2598
+ }
2599
+ if (!pred.has(from)) {
2600
+ return { found: false, pathOrder: [], truncated };
2601
+ }
2602
+ const pathOrder = [{ path: from }];
2603
+ let cursor = from;
2604
+ while (cursor !== to) {
2605
+ const p = pred.get(cursor);
2606
+ if (p == null) return { found: false, pathOrder: [], truncated: false };
2607
+ pathOrder[pathOrder.length - 1].depIndices = p.depIndices;
2608
+ pathOrder.push({ path: p.from });
2609
+ cursor = p.from;
2610
+ }
2611
+ return { found: true, pathOrder, truncated: false };
2612
+ }
2613
+ function findShortestCycle(described, start, opts) {
2614
+ const startNode = described.nodes[start];
2615
+ if (startNode == null) return null;
2616
+ const startDeps = startNode.deps ?? [];
2617
+ const selfSlots = [];
2618
+ for (let i = 0; i < startDeps.length; i++) if (startDeps[i] === start) selfSlots.push(i);
2619
+ if (selfSlots.length > 0) {
2620
+ const step0 = buildStep(start, startNode, 0, opts);
2621
+ step0.dep_index = selfSlots[0];
2622
+ const step1 = buildStep(start, startNode, 1, opts);
2623
+ return makeSuccess(start, start, [step0, step1]);
2624
+ }
2625
+ let best = null;
2626
+ for (let i = 0; i < startDeps.length; i++) {
2627
+ const dep = startDeps[i];
2628
+ if (!dep || dep === start) continue;
2629
+ const sub = bfsShortestPath(described, dep, start, opts.maxDepth);
2630
+ if (!sub.found) continue;
2631
+ if (best == null || sub.pathOrder.length < best.pathOrder.length) {
2632
+ best = sub;
2633
+ best = {
2634
+ found: true,
2635
+ pathOrder: [{ path: start, depIndices: [i] }, ...sub.pathOrder],
2636
+ truncated: false
2637
+ };
2638
+ }
2639
+ }
2640
+ if (best == null) return null;
2641
+ return makeSuccess(start, start, materializeSteps(described, best.pathOrder, opts));
2642
+ }
2643
+ function materializeSteps(described, pathOrder, opts) {
2644
+ return pathOrder.map((entry, i) => {
2645
+ const node2 = described.nodes[entry.path];
2646
+ const step = buildStep(entry.path, node2, i, opts);
2647
+ if (entry.depIndices != null && entry.depIndices.length > 0) {
2648
+ step.dep_index = entry.depIndices[0];
2649
+ if (entry.depIndices.length > 1) step.dep_indices = [...entry.depIndices];
2650
+ }
2651
+ return step;
2652
+ });
2653
+ }
2654
+ function buildStep(path, node2, hop, opts) {
2655
+ const step = {
2656
+ path,
2657
+ type: node2.type,
2658
+ hop
2659
+ };
2660
+ if (node2.status !== void 0) step.status = node2.status;
2661
+ if ("value" in node2) step.value = node2.value;
2662
+ if (node2.v != null) step.v = node2.v;
2663
+ const annotation = opts.annotations?.get(path) ?? node2.reason;
2664
+ if (annotation != null) step.reason = annotation;
2665
+ const lastMutation = opts.lastMutations?.get(path) ?? node2.lastMutation;
2666
+ if (lastMutation != null) step.lastMutation = lastMutation;
2667
+ return step;
2668
+ }
2669
+ function makeSuccess(from, to, steps) {
2670
+ return finalize(from, to, true, "ok", steps);
2671
+ }
2672
+ function makeFailure(from, to, reason) {
2673
+ return finalize(from, to, false, reason, []);
2674
+ }
2675
+ function finalize(from, to, found, reason, steps) {
2676
+ const text = renderChain(from, to, found, reason, steps);
2677
+ return {
2678
+ from,
2679
+ to,
2680
+ found,
2681
+ reason,
2682
+ steps,
2683
+ text,
2684
+ toJSON() {
2685
+ return { from, to, found, reason, steps };
2686
+ }
2687
+ };
2688
+ }
2689
+ function renderChain(from, to, found, reason, steps) {
2690
+ if (!found) {
2691
+ switch (reason) {
2692
+ case "no-such-from":
2693
+ return `explainPath: no node named "${from}"`;
2694
+ case "no-such-to":
2695
+ return `explainPath: no node named "${to}"`;
2696
+ case "max-depth-exceeded":
2697
+ return `explainPath: no path from "${from}" to "${to}" within maxDepth`;
2698
+ default:
2699
+ return `explainPath: no path from "${from}" to "${to}"`;
2700
+ }
2701
+ }
2702
+ const lines = [`Causal path: ${from} \u2192 ${to} (${steps.length} step(s))`];
2703
+ for (const step of steps) {
2704
+ const arrow = step.hop === 0 ? "\xB7" : "\u2193";
2705
+ const head = ` ${arrow} ${step.path} (${step.type}${step.status ? `/${step.status}` : ""})`;
2706
+ lines.push(head);
2707
+ if ("value" in step) {
2708
+ lines.push(` value: ${formatValue(step.value)}`);
2709
+ }
2710
+ if (step.reason != null) {
2711
+ lines.push(` reason: ${step.reason}`);
2712
+ }
2713
+ if (step.lastMutation != null) {
2714
+ const a = step.lastMutation.actor;
2715
+ lines.push(` actor: ${a.type}${a.id ? `:${a.id}` : ""}`);
2716
+ }
2717
+ }
2718
+ return lines.join("\n");
2719
+ }
2720
+ function formatValue(v) {
2721
+ if (v === void 0) return "<sentinel>";
2722
+ if (v === null) return "null";
2723
+ if (typeof v === "string") return JSON.stringify(v);
2724
+ if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
2725
+ try {
2726
+ const s = JSON.stringify(v);
2727
+ return s.length > 80 ? `${s.slice(0, 77)}...` : s;
2728
+ } catch {
2729
+ return String(v);
2730
+ }
2731
+ }
2732
+
2480
2733
  // src/extra/utils/sizeof.ts
2481
2734
  var OVERHEAD = {
2482
2735
  object: 56,
@@ -3110,6 +3363,20 @@ var Graph = class _Graph {
3110
3363
  _parent = void 0;
3111
3364
  _storageDisposers = /* @__PURE__ */ new Set();
3112
3365
  _disposers = /* @__PURE__ */ new Set();
3366
+ /**
3367
+ * @internal Lazy `TopologyEvent` producer. Created on first `.topology`
3368
+ * access. Zero cost until something subscribes — producer fn only runs when
3369
+ * the first sink attaches, registering one handler into
3370
+ * {@link Graph._topologyEmitters}.
3371
+ */
3372
+ _topology;
3373
+ /**
3374
+ * @internal Active emit handlers for the topology producer. Each entry is
3375
+ * the closure registered by the producer fn on activation; cleared on
3376
+ * deactivation. `_emitTopology` broadcasts through every entry (there is at
3377
+ * most one per activation cycle of the producer).
3378
+ */
3379
+ _topologyEmitters = /* @__PURE__ */ new Set();
3113
3380
  /**
3114
3381
  * @param name - Non-empty graph id (must not contain `::` and must not
3115
3382
  * equal the reserved meta segment `__meta__`).
@@ -3149,6 +3416,55 @@ var Graph = class _Graph {
3149
3416
  return out;
3150
3417
  }
3151
3418
  // ——————————————————————————————————————————————————————————————
3419
+ // Topology companion (structural-change event stream)
3420
+ // ——————————————————————————————————————————————————————————————
3421
+ /**
3422
+ * Reactive stream of structural changes to this graph's own registry
3423
+ * (add / mount / remove). Value mutations live on `observe()`; this
3424
+ * companion only fires when the topology shape changes.
3425
+ *
3426
+ * Lazy: the underlying node is created on first access and activates when
3427
+ * something subscribes. No emission replay — late subscribers do not
3428
+ * receive historical events and should snapshot via {@link Graph.describe}
3429
+ * before listening for incremental changes. Events that fire while the
3430
+ * producer has zero subscribers are dropped (no retention).
3431
+ *
3432
+ * Own-graph only: a parent's `topology` does NOT emit for structural
3433
+ * changes inside a mounted child. Transitive consumers subscribe to each
3434
+ * child's topology separately (recurse through `topology`'s own "added"
3435
+ * events with `nodeKind: "mount"` to discover new children).
3436
+ *
3437
+ * See {@link TopologyEvent} for payload shape.
3438
+ *
3439
+ * @category observability
3440
+ */
3441
+ get topology() {
3442
+ if (this._topology == null) {
3443
+ this._topology = producer(
3444
+ (actions) => {
3445
+ const handler = (event) => {
3446
+ actions.emit(event);
3447
+ };
3448
+ this._topologyEmitters.add(handler);
3449
+ return () => {
3450
+ this._topologyEmitters.delete(handler);
3451
+ };
3452
+ },
3453
+ { name: `${this.name}_topology` }
3454
+ );
3455
+ }
3456
+ return this._topology;
3457
+ }
3458
+ /**
3459
+ * @internal Fire a {@link TopologyEvent} to every active subscriber of
3460
+ * `this.topology`. No-op when the topology node has never been accessed or
3461
+ * currently has no sinks — zero cost for graphs nobody observes.
3462
+ */
3463
+ _emitTopology(event) {
3464
+ if (this._topology == null || this._topologyEmitters.size === 0) return;
3465
+ for (const h of this._topologyEmitters) h(event);
3466
+ }
3467
+ // ——————————————————————————————————————————————————————————————
3152
3468
  // Node registry
3153
3469
  // ——————————————————————————————————————————————————————————————
3154
3470
  /**
@@ -3178,6 +3494,7 @@ var Graph = class _Graph {
3178
3494
  }
3179
3495
  this._nodes.set(name, node2);
3180
3496
  this._nodeToName.set(node2, name);
3497
+ this._emitTopology({ kind: "added", name, nodeKind: "node" });
3181
3498
  return node2;
3182
3499
  }
3183
3500
  /**
@@ -3218,22 +3535,23 @@ var Graph = class _Graph {
3218
3535
  assertRegisterableName(name, this.name, "remove");
3219
3536
  const child = this._mounts.get(name);
3220
3537
  if (child) {
3221
- const audit = { kind: "mount", nodes: [], mounts: [] };
3538
+ const audit2 = { kind: "mount", nodes: [], mounts: [] };
3222
3539
  const targets = [];
3223
3540
  child._collectObserveTargets("", targets);
3224
3541
  for (const [p, n] of targets) {
3225
3542
  if (!p.includes(`${PATH_SEP}${GRAPH_META_SEGMENT}${PATH_SEP}`)) {
3226
- audit.nodes.push(p);
3543
+ audit2.nodes.push(p);
3227
3544
  }
3228
3545
  void n;
3229
3546
  }
3230
- audit.nodes.sort();
3231
- audit.mounts.push(name);
3232
- audit.mounts.push(...child._collectSubgraphs(`${name}${PATH_SEP}`));
3547
+ audit2.nodes.sort();
3548
+ audit2.mounts.push(name);
3549
+ audit2.mounts.push(...child._collectSubgraphs(`${name}${PATH_SEP}`));
3233
3550
  this._mounts.delete(name);
3234
3551
  child._parent = void 0;
3235
3552
  teardownMountedGraph(child);
3236
- return audit;
3553
+ this._emitTopology({ kind: "removed", name, nodeKind: "mount", audit: audit2 });
3554
+ return audit2;
3237
3555
  }
3238
3556
  const node2 = this._nodes.get(name);
3239
3557
  if (!node2) {
@@ -3242,7 +3560,9 @@ var Graph = class _Graph {
3242
3560
  this._nodes.delete(name);
3243
3561
  this._nodeToName.delete(node2);
3244
3562
  node2.down([[TEARDOWN]], { internal: true });
3245
- return { kind: "node", nodes: [name], mounts: [] };
3563
+ const audit = { kind: "node", nodes: [name], mounts: [] };
3564
+ this._emitTopology({ kind: "removed", name, nodeKind: "node", audit });
3565
+ return audit;
3246
3566
  }
3247
3567
  /**
3248
3568
  * Bulk remove — invokes {@link Graph.remove} for every local name matching
@@ -3471,6 +3791,7 @@ var Graph = class _Graph {
3471
3791
  }
3472
3792
  this._mounts.set(name, child);
3473
3793
  child._parent = this;
3794
+ this._emitTopology({ kind: "added", name, nodeKind: "mount" });
3474
3795
  return child;
3475
3796
  }
3476
3797
  /**
@@ -3761,6 +4082,33 @@ var Graph = class _Graph {
3761
4082
  }
3762
4083
  return reachable(this.describe(), from, direction, opts);
3763
4084
  }
4085
+ /**
4086
+ * Causal walkback: shortest dep-chain from `from` to `to`, enriched with
4087
+ * each node's value, status, last-mutation actor, and reasoning annotation
4088
+ * from {@link Graph.trace}. Wraps {@link explainPath} (roadmap §9.2).
4089
+ *
4090
+ * @param from - Upstream node (the cause).
4091
+ * @param to - Downstream node (the effect).
4092
+ * @param opts - Optional `maxDepth` and `findCycle`. When `findCycle:true`
4093
+ * and `from === to`, returns the shortest cycle through other nodes
4094
+ * (useful for diagnosing feedback loops, COMPOSITION-GUIDE §7).
4095
+ * Annotations and lastMutations are collected automatically from the
4096
+ * live graph.
4097
+ */
4098
+ explain(from, to, opts) {
4099
+ const described = this.describe({ detail: "full" });
4100
+ const annotations = new Map(this._annotations);
4101
+ const lastMutations = /* @__PURE__ */ new Map();
4102
+ for (const [path, n] of Object.entries(described.nodes)) {
4103
+ if (n.lastMutation != null) lastMutations.set(path, n.lastMutation);
4104
+ }
4105
+ return explainPath(described, from, to, {
4106
+ ...opts?.maxDepth != null ? { maxDepth: opts.maxDepth } : {},
4107
+ ...opts?.findCycle === true ? { findCycle: true } : {},
4108
+ annotations,
4109
+ lastMutations
4110
+ });
4111
+ }
3764
4112
  /**
3765
4113
  * @internal Collect all qualified paths in this graph tree matching a
3766
4114
  * glob pattern. Used by scoped autoCheckpoint subscription.
@@ -4439,7 +4787,7 @@ var Graph = class _Graph {
4439
4787
  return;
4440
4788
  }
4441
4789
  const nextSeq = s.seq + 1;
4442
- const timestamp_ns = monotonicNs();
4790
+ const timestamp_ns = wallClockNs();
4443
4791
  const isFirst = s.lastSnapshot == null;
4444
4792
  const shouldCompact = isFirst || nextSeq % s.compactEvery === 0;
4445
4793
  const record = shouldCompact ? {
@@ -4958,7 +5306,7 @@ function analyzeAndMeasure(text, font, adapter, cache, stats) {
4958
5306
  const normalized = normalizeWhitespace(text);
4959
5307
  if (normalized.length === 0) return [];
4960
5308
  const pieces = segmentText(normalized);
4961
- const graphemeSegmenter = new Intl.Segmenter(void 0, {
5309
+ const graphemeSegmenter2 = new Intl.Segmenter(void 0, {
4962
5310
  granularity: "grapheme"
4963
5311
  });
4964
5312
  const rawTexts = [];
@@ -5002,7 +5350,8 @@ function analyzeAndMeasure(text, font, adapter, cache, stats) {
5002
5350
  let w = fontCache.get(seg);
5003
5351
  if (w === void 0) {
5004
5352
  if (stats) stats.misses += 1;
5005
- w = adapter.measureSegment(seg, font).width;
5353
+ const raw = adapter.measureSegment(seg, font).width;
5354
+ w = Number.isFinite(raw) && raw >= 0 ? raw : 0;
5006
5355
  fontCache.set(seg, w);
5007
5356
  } else if (stats) {
5008
5357
  stats.hits += 1;
@@ -5024,7 +5373,7 @@ function analyzeAndMeasure(text, font, adapter, cache, stats) {
5024
5373
  }
5025
5374
  if (isCJK(t)) {
5026
5375
  let unitText = "";
5027
- for (const gs of graphemeSegmenter.segment(t)) {
5376
+ for (const gs of graphemeSegmenter2.segment(t)) {
5028
5377
  const grapheme = gs.segment;
5029
5378
  if (unitText.length > 0 && kinsokuStart.has(grapheme)) {
5030
5379
  unitText += grapheme;
@@ -5056,7 +5405,7 @@ function analyzeAndMeasure(text, font, adapter, cache, stats) {
5056
5405
  let graphemeWidths = null;
5057
5406
  if (mergedWordLike[i] && t.length > 1) {
5058
5407
  const gWidths = [];
5059
- for (const gs of graphemeSegmenter.segment(t)) {
5408
+ for (const gs of graphemeSegmenter2.segment(t)) {
5060
5409
  gWidths.push(measureCached(gs.segment));
5061
5410
  }
5062
5411
  if (gWidths.length > 1) {
@@ -5096,10 +5445,10 @@ function computeLineBreaks(segments, maxWidth, adapter, font, cache) {
5096
5445
  const seg = segments[i];
5097
5446
  if (seg.kind === "soft-hyphen" || seg.kind === "hard-break") continue;
5098
5447
  if (i === lineStartSeg && lineStartGrapheme > 0 && seg.graphemeWidths) {
5099
- const graphemeSegmenter = new Intl.Segmenter(void 0, {
5448
+ const graphemeSegmenter2 = new Intl.Segmenter(void 0, {
5100
5449
  granularity: "grapheme"
5101
5450
  });
5102
- const graphemes = [...graphemeSegmenter.segment(seg.text)].map((g) => g.segment);
5451
+ const graphemes = [...graphemeSegmenter2.segment(seg.text)].map((g) => g.segment);
5103
5452
  text += graphemes.slice(lineStartGrapheme).join("");
5104
5453
  } else {
5105
5454
  text += seg.text;
@@ -5107,10 +5456,10 @@ function computeLineBreaks(segments, maxWidth, adapter, font, cache) {
5107
5456
  }
5108
5457
  if (endGrapheme > 0 && endSeg < segments.length) {
5109
5458
  const seg = segments[endSeg];
5110
- const graphemeSegmenter = new Intl.Segmenter(void 0, {
5459
+ const graphemeSegmenter2 = new Intl.Segmenter(void 0, {
5111
5460
  granularity: "grapheme"
5112
5461
  });
5113
- const graphemes = [...graphemeSegmenter.segment(seg.text)].map((g) => g.segment);
5462
+ const graphemes = [...graphemeSegmenter2.segment(seg.text)].map((g) => g.segment);
5114
5463
  const startG = lineStartSeg === endSeg ? lineStartGrapheme : 0;
5115
5464
  text += graphemes.slice(startG, endGrapheme).join("");
5116
5465
  }
@@ -5130,7 +5479,7 @@ function computeLineBreaks(segments, maxWidth, adapter, font, cache) {
5130
5479
  pendingBreakSeg = -1;
5131
5480
  pendingBreakWidth = 0;
5132
5481
  }
5133
- function canBreakAfter(kind) {
5482
+ function canBreakAfter2(kind) {
5134
5483
  return kind === "space" || kind === "zero-width-break" || kind === "soft-hyphen";
5135
5484
  }
5136
5485
  function startLine(segIdx, graphemeIdx, width) {
@@ -5175,7 +5524,7 @@ function computeLineBreaks(segments, maxWidth, adapter, font, cache) {
5175
5524
  } else {
5176
5525
  startLine(i, 0, w);
5177
5526
  }
5178
- if (canBreakAfter(seg.kind)) {
5527
+ if (canBreakAfter2(seg.kind)) {
5179
5528
  pendingBreakSeg = i + 1;
5180
5529
  pendingBreakWidth = seg.kind === "space" ? lineW - w : lineW;
5181
5530
  }
@@ -5183,7 +5532,7 @@ function computeLineBreaks(segments, maxWidth, adapter, font, cache) {
5183
5532
  }
5184
5533
  const newW = lineW + w;
5185
5534
  if (newW > maxWidth + 5e-3) {
5186
- if (canBreakAfter(seg.kind)) {
5535
+ if (canBreakAfter2(seg.kind)) {
5187
5536
  lineW += w;
5188
5537
  lineEndSeg = i + 1;
5189
5538
  lineEndGrapheme = 0;
@@ -5207,7 +5556,7 @@ function computeLineBreaks(segments, maxWidth, adapter, font, cache) {
5207
5556
  lineW = newW;
5208
5557
  lineEndSeg = i + 1;
5209
5558
  lineEndGrapheme = 0;
5210
- if (canBreakAfter(seg.kind)) {
5559
+ if (canBreakAfter2(seg.kind)) {
5211
5560
  pendingBreakSeg = i + 1;
5212
5561
  pendingBreakWidth = seg.kind === "space" ? lineW - w : lineW;
5213
5562
  }
@@ -5238,9 +5587,289 @@ function computeLineBreaks(segments, maxWidth, adapter, font, cache) {
5238
5587
  }
5239
5588
  }
5240
5589
  }
5590
+ function canBreakAfter(kind) {
5591
+ return kind === "space" || kind === "zero-width-break" || kind === "soft-hyphen";
5592
+ }
5593
+ var _graphemeSegmenter = null;
5594
+ function graphemeSegmenter() {
5595
+ if (_graphemeSegmenter === null) {
5596
+ _graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
5597
+ }
5598
+ return _graphemeSegmenter;
5599
+ }
5600
+ function sliceSegmentText(seg, startG, endG) {
5601
+ if (startG === 0 && endG < 0) return seg.text;
5602
+ const graphemes = [...graphemeSegmenter().segment(seg.text)].map((g) => g.segment);
5603
+ const stop = endG < 0 ? graphemes.length : endG;
5604
+ return graphemes.slice(startG, stop).join("");
5605
+ }
5606
+ function buildLineText(segments, startSeg, startG, endSeg, endG, appendHyphen) {
5607
+ let text = "";
5608
+ for (let i = startSeg; i < endSeg; i++) {
5609
+ const seg = segments[i];
5610
+ if (seg.kind === "soft-hyphen" || seg.kind === "hard-break") continue;
5611
+ if (i === startSeg && startG > 0) {
5612
+ text += sliceSegmentText(seg, startG, -1);
5613
+ } else {
5614
+ text += seg.text;
5615
+ }
5616
+ }
5617
+ if (endG > 0 && endSeg < segments.length) {
5618
+ const seg = segments[endSeg];
5619
+ const from = startSeg === endSeg ? startG : 0;
5620
+ text += sliceSegmentText(seg, from, endG);
5621
+ }
5622
+ if (appendHyphen) text += "-";
5623
+ return text;
5624
+ }
5625
+ function resolveHyphenWidth(ctx) {
5626
+ if (!ctx || !ctx.adapter || !ctx.font) return 0;
5627
+ const cache = ctx.cache;
5628
+ if (cache) {
5629
+ let fc = cache.get(ctx.font);
5630
+ if (!fc) {
5631
+ fc = /* @__PURE__ */ new Map();
5632
+ cache.set(ctx.font, fc);
5633
+ }
5634
+ let hw = fc.get("-");
5635
+ if (hw === void 0) {
5636
+ hw = ctx.adapter.measureSegment("-", ctx.font).width;
5637
+ fc.set("-", hw);
5638
+ }
5639
+ return hw;
5640
+ }
5641
+ return ctx.adapter.measureSegment("-", ctx.font).width;
5642
+ }
5643
+ function layoutNextLine(segments, cursor, slotWidth, ctx) {
5644
+ let i = cursor.segmentIndex;
5645
+ const initialG = cursor.graphemeIndex;
5646
+ if (i >= segments.length) return null;
5647
+ if (initialG === 0) {
5648
+ while (i < segments.length) {
5649
+ const seg = segments[i];
5650
+ if (seg.kind === "hard-break") {
5651
+ return {
5652
+ text: "",
5653
+ width: 0,
5654
+ start: { segmentIndex: cursor.segmentIndex, graphemeIndex: 0 },
5655
+ end: { segmentIndex: i + 1, graphemeIndex: 0 }
5656
+ };
5657
+ }
5658
+ if (seg.kind === "space" || seg.kind === "zero-width-break" || seg.kind === "soft-hyphen") {
5659
+ i += 1;
5660
+ continue;
5661
+ }
5662
+ break;
5663
+ }
5664
+ if (i >= segments.length) return null;
5665
+ }
5666
+ const hyphenWidth = resolveHyphenWidth(ctx);
5667
+ const startSeg = i;
5668
+ const startG = i === cursor.segmentIndex ? initialG : 0;
5669
+ let lineW = 0;
5670
+ let lineEndSeg = startSeg;
5671
+ let lineEndG = 0;
5672
+ let hasContent = false;
5673
+ let pendingBreakSeg = -1;
5674
+ let pendingBreakG = 0;
5675
+ let pendingBreakWidth = 0;
5676
+ let pendingBreakSoftHyphen = false;
5677
+ const recordPending = (sIdx, gIdx, widthAtBreak, kind) => {
5678
+ pendingBreakSeg = sIdx;
5679
+ pendingBreakG = gIdx;
5680
+ pendingBreakWidth = widthAtBreak;
5681
+ pendingBreakSoftHyphen = kind === "soft-hyphen";
5682
+ };
5683
+ const consumeBreakable = (segIdx, gStart, gWidths) => {
5684
+ for (let g = gStart; g < gWidths.length; g++) {
5685
+ const gw = gWidths[g];
5686
+ if (!hasContent) {
5687
+ lineW = gw;
5688
+ lineEndSeg = segIdx;
5689
+ lineEndG = g + 1;
5690
+ hasContent = true;
5691
+ continue;
5692
+ }
5693
+ if (lineW + gw > slotWidth + 5e-3) {
5694
+ return true;
5695
+ }
5696
+ lineW += gw;
5697
+ lineEndSeg = segIdx;
5698
+ lineEndG = g + 1;
5699
+ }
5700
+ if (lineEndSeg === segIdx && lineEndG === gWidths.length) {
5701
+ lineEndSeg = segIdx + 1;
5702
+ lineEndG = 0;
5703
+ }
5704
+ return false;
5705
+ };
5706
+ if (startG > 0 && startSeg < segments.length) {
5707
+ const seg = segments[startSeg];
5708
+ if (seg.graphemeWidths) {
5709
+ const overflowed = consumeBreakable(startSeg, startG, seg.graphemeWidths);
5710
+ if (overflowed) {
5711
+ const text2 = buildLineText(segments, startSeg, startG, lineEndSeg, lineEndG, false);
5712
+ return {
5713
+ text: text2,
5714
+ width: lineW,
5715
+ start: { segmentIndex: startSeg, graphemeIndex: startG },
5716
+ end: { segmentIndex: lineEndSeg, graphemeIndex: lineEndG }
5717
+ };
5718
+ }
5719
+ i = lineEndSeg;
5720
+ } else {
5721
+ }
5722
+ }
5723
+ for (; i < segments.length; ) {
5724
+ const seg = segments[i];
5725
+ if (seg.kind === "hard-break") {
5726
+ if (hasContent) {
5727
+ const endsAtSoftHyphen2 = lineEndSeg > 0 && segments[lineEndSeg - 1]?.kind === "soft-hyphen";
5728
+ const text2 = buildLineText(
5729
+ segments,
5730
+ startSeg,
5731
+ startG,
5732
+ lineEndSeg,
5733
+ lineEndG,
5734
+ endsAtSoftHyphen2
5735
+ );
5736
+ return {
5737
+ text: text2,
5738
+ width: lineW + (endsAtSoftHyphen2 ? hyphenWidth : 0),
5739
+ start: { segmentIndex: startSeg, graphemeIndex: startG },
5740
+ end: { segmentIndex: lineEndSeg, graphemeIndex: lineEndG }
5741
+ };
5742
+ }
5743
+ return {
5744
+ text: "",
5745
+ width: 0,
5746
+ start: { segmentIndex: startSeg, graphemeIndex: startG },
5747
+ end: { segmentIndex: i + 1, graphemeIndex: 0 }
5748
+ };
5749
+ }
5750
+ const w = seg.width;
5751
+ if (!hasContent) {
5752
+ if (w > slotWidth && seg.graphemeWidths) {
5753
+ const overflowed = consumeBreakable(i, 0, seg.graphemeWidths);
5754
+ if (overflowed) {
5755
+ const text2 = buildLineText(segments, startSeg, startG, lineEndSeg, lineEndG, false);
5756
+ return {
5757
+ text: text2,
5758
+ width: lineW,
5759
+ start: { segmentIndex: startSeg, graphemeIndex: startG },
5760
+ end: { segmentIndex: lineEndSeg, graphemeIndex: lineEndG }
5761
+ };
5762
+ }
5763
+ i = lineEndSeg;
5764
+ continue;
5765
+ }
5766
+ lineW = w;
5767
+ lineEndSeg = i + 1;
5768
+ lineEndG = 0;
5769
+ hasContent = true;
5770
+ if (canBreakAfter(seg.kind)) {
5771
+ recordPending(i + 1, 0, seg.kind === "space" ? lineW - w : lineW, seg.kind);
5772
+ }
5773
+ i += 1;
5774
+ continue;
5775
+ }
5776
+ const newW = lineW + w;
5777
+ if (newW > slotWidth + 5e-3) {
5778
+ if (canBreakAfter(seg.kind)) {
5779
+ lineEndSeg = i + 1;
5780
+ lineEndG = 0;
5781
+ const endsAtSoftHyphen2 = seg.kind === "soft-hyphen";
5782
+ const finalWidth = seg.kind === "space" ? lineW : lineW + (endsAtSoftHyphen2 ? hyphenWidth : 0);
5783
+ const text3 = buildLineText(
5784
+ segments,
5785
+ startSeg,
5786
+ startG,
5787
+ lineEndSeg,
5788
+ lineEndG,
5789
+ endsAtSoftHyphen2
5790
+ );
5791
+ return {
5792
+ text: text3,
5793
+ width: finalWidth,
5794
+ start: { segmentIndex: startSeg, graphemeIndex: startG },
5795
+ end: { segmentIndex: lineEndSeg, graphemeIndex: lineEndG }
5796
+ };
5797
+ }
5798
+ if (pendingBreakSeg >= 0) {
5799
+ const text3 = buildLineText(
5800
+ segments,
5801
+ startSeg,
5802
+ startG,
5803
+ pendingBreakSeg,
5804
+ pendingBreakG,
5805
+ pendingBreakSoftHyphen
5806
+ );
5807
+ return {
5808
+ text: text3,
5809
+ width: pendingBreakWidth + (pendingBreakSoftHyphen ? hyphenWidth : 0),
5810
+ start: { segmentIndex: startSeg, graphemeIndex: startG },
5811
+ end: { segmentIndex: pendingBreakSeg, graphemeIndex: pendingBreakG }
5812
+ };
5813
+ }
5814
+ if (w > slotWidth && seg.graphemeWidths) {
5815
+ const text3 = buildLineText(segments, startSeg, startG, lineEndSeg, lineEndG, false);
5816
+ return {
5817
+ text: text3,
5818
+ width: lineW,
5819
+ start: { segmentIndex: startSeg, graphemeIndex: startG },
5820
+ end: { segmentIndex: lineEndSeg, graphemeIndex: lineEndG }
5821
+ };
5822
+ }
5823
+ const text2 = buildLineText(segments, startSeg, startG, lineEndSeg, lineEndG, false);
5824
+ return {
5825
+ text: text2,
5826
+ width: lineW,
5827
+ start: { segmentIndex: startSeg, graphemeIndex: startG },
5828
+ end: { segmentIndex: lineEndSeg, graphemeIndex: lineEndG }
5829
+ };
5830
+ }
5831
+ lineW = newW;
5832
+ lineEndSeg = i + 1;
5833
+ lineEndG = 0;
5834
+ if (canBreakAfter(seg.kind)) {
5835
+ recordPending(i + 1, 0, seg.kind === "space" ? lineW - w : lineW, seg.kind);
5836
+ }
5837
+ i += 1;
5838
+ }
5839
+ if (!hasContent) return null;
5840
+ const endsAtSoftHyphen = lineEndSeg > 0 && segments[lineEndSeg - 1]?.kind === "soft-hyphen";
5841
+ const text = buildLineText(segments, startSeg, startG, lineEndSeg, lineEndG, endsAtSoftHyphen);
5842
+ return {
5843
+ text,
5844
+ width: lineW + (endsAtSoftHyphen ? hyphenWidth : 0),
5845
+ start: { segmentIndex: startSeg, graphemeIndex: startG },
5846
+ end: { segmentIndex: lineEndSeg, graphemeIndex: lineEndG }
5847
+ };
5848
+ }
5849
+ function carveTextLineSlots(base, blocked, minSlotWidth = 0) {
5850
+ let slots = [base];
5851
+ for (let bi = 0; bi < blocked.length; bi++) {
5852
+ const block = blocked[bi];
5853
+ const next = [];
5854
+ for (let si = 0; si < slots.length; si++) {
5855
+ const slot = slots[si];
5856
+ if (block.right <= slot.left || block.left >= slot.right) {
5857
+ next.push(slot);
5858
+ continue;
5859
+ }
5860
+ if (block.left > slot.left) next.push({ left: slot.left, right: block.left });
5861
+ if (block.right < slot.right) next.push({ left: block.right, right: slot.right });
5862
+ }
5863
+ slots = next;
5864
+ }
5865
+ if (minSlotWidth > 0) {
5866
+ return slots.filter((s) => s.right - s.left >= minSlotWidth);
5867
+ }
5868
+ return slots;
5869
+ }
5241
5870
  function computeCharPositions(lineBreaks, segments, lineHeight) {
5242
5871
  const positions = [];
5243
- const graphemeSegmenter = new Intl.Segmenter(void 0, {
5872
+ const graphemeSegmenter2 = new Intl.Segmenter(void 0, {
5244
5873
  granularity: "grapheme"
5245
5874
  });
5246
5875
  for (let lineIdx = 0; lineIdx < lineBreaks.lines.length; lineIdx++) {
@@ -5253,7 +5882,7 @@ function computeCharPositions(lineBreaks, segments, lineHeight) {
5253
5882
  if (si >= line.endSegment && line.endGrapheme === 0) break;
5254
5883
  continue;
5255
5884
  }
5256
- const graphemes = [...graphemeSegmenter.segment(seg.text)].map((g) => g.segment);
5885
+ const graphemes = [...graphemeSegmenter2.segment(seg.text)].map((g) => g.segment);
5257
5886
  if (graphemes.length === 0) continue;
5258
5887
  const startG = si === line.startSegment ? line.startGrapheme : 0;
5259
5888
  let endG;
@@ -5609,6 +6238,185 @@ function reactiveBlockLayout(opts) {
5609
6238
  totalHeight: totalHeightNode
5610
6239
  };
5611
6240
  }
6241
+
6242
+ // src/patterns/reactive-layout/reactive-flow-layout.ts
6243
+ function circleIntervalForBand(o, bandTop, bandBottom) {
6244
+ const hPad = o.hPad ?? 0;
6245
+ const vPad = o.vPad ?? 0;
6246
+ const top = bandTop - vPad;
6247
+ const bottom = bandBottom + vPad;
6248
+ if (top >= o.cy + o.r || bottom <= o.cy - o.r) return null;
6249
+ const minDy = o.cy >= top && o.cy <= bottom ? 0 : o.cy < top ? top - o.cy : o.cy - bottom;
6250
+ if (minDy >= o.r) return null;
6251
+ const maxDx = Math.sqrt(o.r * o.r - minDy * minDy);
6252
+ return { left: o.cx - maxDx - hPad, right: o.cx + maxDx + hPad };
6253
+ }
6254
+ function rectIntervalForBand(o, bandTop, bandBottom) {
6255
+ const hPad = o.hPad ?? 0;
6256
+ const vPad = o.vPad ?? 0;
6257
+ if (bandBottom <= o.y - vPad) return null;
6258
+ if (bandTop >= o.y + o.h + vPad) return null;
6259
+ return { left: o.x - hPad, right: o.x + o.w + hPad };
6260
+ }
6261
+ function obstacleIntervalForBand(o, bandTop, bandBottom) {
6262
+ return o.kind === "circle" ? circleIntervalForBand(o, bandTop, bandBottom) : rectIntervalForBand(o, bandTop, bandBottom);
6263
+ }
6264
+ function computeFlowLines(segments, container, columns, obstacles, lineHeight, minSlotWidth) {
6265
+ const lines = [];
6266
+ let cursor = { segmentIndex: 0, graphemeIndex: 0 };
6267
+ if (segments.length === 0 || columns.count <= 0 || lineHeight <= 0) {
6268
+ return { lines, cursor };
6269
+ }
6270
+ const padX = container.paddingX ?? 0;
6271
+ const padY = container.paddingY ?? 0;
6272
+ const availWidth = Math.max(0, container.width - padX * 2);
6273
+ const availHeight = Math.max(0, container.height - padY * 2);
6274
+ const gapTotal = columns.gap * Math.max(0, columns.count - 1);
6275
+ const colWidth = Math.max(0, (availWidth - gapTotal) / columns.count);
6276
+ if (colWidth <= 0) return { lines, cursor };
6277
+ outerCol: for (let col = 0; col < columns.count; col++) {
6278
+ const colLeft = padX + col * (colWidth + columns.gap);
6279
+ const colRight = colLeft + colWidth;
6280
+ let bandTop = padY;
6281
+ while (bandTop + lineHeight <= padY + availHeight) {
6282
+ const bandBottom = bandTop + lineHeight;
6283
+ const blocked = [];
6284
+ for (let oi = 0; oi < obstacles.length; oi++) {
6285
+ const iv = obstacleIntervalForBand(obstacles[oi], bandTop, bandBottom);
6286
+ if (iv !== null) blocked.push(iv);
6287
+ }
6288
+ const slots = carveTextLineSlots({ left: colLeft, right: colRight }, blocked, minSlotWidth);
6289
+ if (slots.length === 0) {
6290
+ bandTop += lineHeight;
6291
+ continue;
6292
+ }
6293
+ let hardBreakThisBand = false;
6294
+ for (let si = 0; si < slots.length; si++) {
6295
+ const slot = slots[si];
6296
+ const slotW = slot.right - slot.left;
6297
+ const line = layoutNextLine(segments, cursor, slotW);
6298
+ if (line === null) {
6299
+ return { lines, cursor };
6300
+ }
6301
+ if (line.text.length === 0 && line.width === 0) {
6302
+ cursor = line.end;
6303
+ hardBreakThisBand = true;
6304
+ break;
6305
+ }
6306
+ lines.push({
6307
+ x: slot.left,
6308
+ y: bandTop,
6309
+ width: line.width,
6310
+ slotWidth: slotW,
6311
+ text: line.text,
6312
+ columnIndex: col,
6313
+ flushToRight: slot.right < colRight - 0.5
6314
+ });
6315
+ cursor = line.end;
6316
+ }
6317
+ bandTop += lineHeight;
6318
+ if (hardBreakThisBand) continue;
6319
+ if (cursor.segmentIndex >= segments.length) break outerCol;
6320
+ }
6321
+ if (cursor.segmentIndex >= segments.length) break;
6322
+ }
6323
+ return { lines, cursor };
6324
+ }
6325
+ function reactiveFlowLayout(opts) {
6326
+ const { adapter, name = "reactive-flow-layout", minSlotWidth = 20 } = opts;
6327
+ const g = new Graph(name);
6328
+ const measureCache = /* @__PURE__ */ new Map();
6329
+ const textNode = state(opts.text ?? "", { name: "text" });
6330
+ const fontNode = state(opts.font ?? "16px sans-serif", { name: "font" });
6331
+ const lineHeightNode = state(opts.lineHeight ?? 20, { name: "line-height" });
6332
+ const containerNode = state(
6333
+ opts.container ?? { width: 800, height: 600, paddingX: 0, paddingY: 0 },
6334
+ { name: "container" }
6335
+ );
6336
+ const columnsNode = state(opts.columns ?? { count: 1, gap: 0 }, {
6337
+ name: "columns"
6338
+ });
6339
+ const obstaclesNode = state(opts.obstacles ?? [], { name: "obstacles" });
6340
+ const segmentsNode = node(
6341
+ [textNode, fontNode],
6342
+ (data, actions, ctx) => {
6343
+ const b0 = data[0];
6344
+ const textVal = b0 != null && b0.length > 0 ? b0.at(-1) : ctx.prevData[0];
6345
+ const b1 = data[1];
6346
+ const fontVal = b1 != null && b1.length > 0 ? b1.at(-1) : ctx.prevData[1];
6347
+ const result = analyzeAndMeasure(textVal, fontVal, adapter, measureCache);
6348
+ actions.emit(result);
6349
+ return () => {
6350
+ measureCache.clear();
6351
+ adapter.clearCache?.();
6352
+ };
6353
+ },
6354
+ { name: "segments", describeKind: "derived" }
6355
+ );
6356
+ const flowLinesNode = derived(
6357
+ [segmentsNode, containerNode, columnsNode, obstaclesNode, lineHeightNode],
6358
+ ([segs, cont, cols, obs, lh]) => {
6359
+ const segments = segs;
6360
+ const t0 = monotonicNs();
6361
+ const { lines: result, cursor } = computeFlowLines(
6362
+ segments,
6363
+ cont,
6364
+ cols,
6365
+ obs,
6366
+ lh,
6367
+ minSlotWidth
6368
+ );
6369
+ const elapsed = monotonicNs() - t0;
6370
+ const overflow = Math.max(0, segments.length - cursor.segmentIndex);
6371
+ const meta = flowLinesNode.meta;
6372
+ if (meta) {
6373
+ emitToMeta(meta["line-count"], result.length);
6374
+ emitToMeta(meta["layout-time-ns"], elapsed);
6375
+ emitToMeta(meta["overflow-segments"], overflow);
6376
+ }
6377
+ return result;
6378
+ },
6379
+ {
6380
+ name: "flow-lines",
6381
+ meta: {
6382
+ "line-count": 0,
6383
+ "layout-time-ns": 0,
6384
+ "overflow-segments": 0
6385
+ },
6386
+ equals: (a, b) => {
6387
+ const la = a;
6388
+ const lb = b;
6389
+ if (la.length !== lb.length) return false;
6390
+ for (let i = 0; i < la.length; i++) {
6391
+ const pa = la[i];
6392
+ const pb = lb[i];
6393
+ if (pa.x !== pb.x || pa.y !== pb.y || pa.width !== pb.width || pa.slotWidth !== pb.slotWidth || pa.text !== pb.text || pa.columnIndex !== pb.columnIndex || pa.flushToRight !== pb.flushToRight)
6394
+ return false;
6395
+ }
6396
+ return true;
6397
+ }
6398
+ }
6399
+ );
6400
+ g.add("text", textNode);
6401
+ g.add("font", fontNode);
6402
+ g.add("line-height", lineHeightNode);
6403
+ g.add("container", containerNode);
6404
+ g.add("columns", columnsNode);
6405
+ g.add("obstacles", obstaclesNode);
6406
+ g.add("segments", segmentsNode);
6407
+ g.add("flow-lines", flowLinesNode);
6408
+ return {
6409
+ graph: g,
6410
+ setText: (t) => g.set("text", t),
6411
+ setFont: (f) => g.set("font", f),
6412
+ setLineHeight: (lh) => g.set("line-height", lh),
6413
+ setContainer: (c) => g.set("container", c),
6414
+ setColumns: (c) => g.set("columns", c),
6415
+ setObstacles: (o) => g.set("obstacles", o),
6416
+ segments: segmentsNode,
6417
+ flowLines: flowLinesNode
6418
+ };
6419
+ }
5612
6420
  // Annotate the CommonJS export names for ESM import in node:
5613
6421
  0 && (module.exports = {
5614
6422
  CanvasMeasureAdapter,
@@ -5618,13 +6426,19 @@ function reactiveBlockLayout(opts) {
5618
6426
  PrecomputedAdapter,
5619
6427
  SvgBoundsAdapter,
5620
6428
  analyzeAndMeasure,
6429
+ carveTextLineSlots,
6430
+ circleIntervalForBand,
5621
6431
  computeBlockFlow,
5622
6432
  computeCharPositions,
6433
+ computeFlowLines,
5623
6434
  computeLineBreaks,
5624
6435
  computeTotalHeight,
6436
+ layoutNextLine,
5625
6437
  measureBlock,
5626
6438
  measureBlocks,
5627
6439
  reactiveBlockLayout,
5628
- reactiveLayout
6440
+ reactiveFlowLayout,
6441
+ reactiveLayout,
6442
+ rectIntervalForBand
5629
6443
  });
5630
6444
  //# sourceMappingURL=index.cjs.map