@graphrefly/graphrefly 0.1.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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +234 -0
  3. package/dist/chunk-5X3LAO3B.js +1571 -0
  4. package/dist/chunk-5X3LAO3B.js.map +1 -0
  5. package/dist/chunk-6W5SGIGB.js +1793 -0
  6. package/dist/chunk-6W5SGIGB.js.map +1 -0
  7. package/dist/chunk-CP6MNKAA.js +97 -0
  8. package/dist/chunk-CP6MNKAA.js.map +1 -0
  9. package/dist/chunk-HP7OKEOE.js +107 -0
  10. package/dist/chunk-HP7OKEOE.js.map +1 -0
  11. package/dist/chunk-KWXPDASV.js +781 -0
  12. package/dist/chunk-KWXPDASV.js.map +1 -0
  13. package/dist/chunk-O3PI7W45.js +68 -0
  14. package/dist/chunk-O3PI7W45.js.map +1 -0
  15. package/dist/chunk-QW7H3ICI.js +1372 -0
  16. package/dist/chunk-QW7H3ICI.js.map +1 -0
  17. package/dist/chunk-VPS7L64N.js +4785 -0
  18. package/dist/chunk-VPS7L64N.js.map +1 -0
  19. package/dist/chunk-Z4Y4FMQN.js +1097 -0
  20. package/dist/chunk-Z4Y4FMQN.js.map +1 -0
  21. package/dist/compat/nestjs/index.cjs +4883 -0
  22. package/dist/compat/nestjs/index.cjs.map +1 -0
  23. package/dist/compat/nestjs/index.d.cts +7 -0
  24. package/dist/compat/nestjs/index.d.ts +7 -0
  25. package/dist/compat/nestjs/index.js +84 -0
  26. package/dist/compat/nestjs/index.js.map +1 -0
  27. package/dist/core/index.cjs +1632 -0
  28. package/dist/core/index.cjs.map +1 -0
  29. package/dist/core/index.d.cts +2 -0
  30. package/dist/core/index.d.ts +2 -0
  31. package/dist/core/index.js +90 -0
  32. package/dist/core/index.js.map +1 -0
  33. package/dist/extra/index.cjs +6885 -0
  34. package/dist/extra/index.cjs.map +1 -0
  35. package/dist/extra/index.d.cts +5 -0
  36. package/dist/extra/index.d.ts +5 -0
  37. package/dist/extra/index.js +290 -0
  38. package/dist/extra/index.js.map +1 -0
  39. package/dist/graph/index.cjs +3225 -0
  40. package/dist/graph/index.cjs.map +1 -0
  41. package/dist/graph/index.d.cts +3 -0
  42. package/dist/graph/index.d.ts +3 -0
  43. package/dist/graph/index.js +25 -0
  44. package/dist/graph/index.js.map +1 -0
  45. package/dist/graph-CL_ZDAj9.d.cts +605 -0
  46. package/dist/graph-D18qmsNm.d.ts +605 -0
  47. package/dist/index-B6SsZs2h.d.cts +3463 -0
  48. package/dist/index-B7eOdgEx.d.ts +449 -0
  49. package/dist/index-BHUvlQ3v.d.ts +3463 -0
  50. package/dist/index-BtK55IE2.d.ts +231 -0
  51. package/dist/index-BvhgZRHK.d.cts +231 -0
  52. package/dist/index-Bvy_6CaN.d.ts +452 -0
  53. package/dist/index-C3BMRmmp.d.cts +449 -0
  54. package/dist/index-C5mqLhMX.d.cts +452 -0
  55. package/dist/index-CP_QvbWu.d.ts +940 -0
  56. package/dist/index-D_geH2Bm.d.cts +940 -0
  57. package/dist/index.cjs +14843 -0
  58. package/dist/index.cjs.map +1 -0
  59. package/dist/index.d.cts +1517 -0
  60. package/dist/index.d.ts +1517 -0
  61. package/dist/index.js +3649 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/meta-BsF6Sag9.d.cts +607 -0
  64. package/dist/meta-BsF6Sag9.d.ts +607 -0
  65. package/dist/patterns/reactive-layout/index.cjs +4143 -0
  66. package/dist/patterns/reactive-layout/index.cjs.map +1 -0
  67. package/dist/patterns/reactive-layout/index.d.cts +3 -0
  68. package/dist/patterns/reactive-layout/index.d.ts +3 -0
  69. package/dist/patterns/reactive-layout/index.js +38 -0
  70. package/dist/patterns/reactive-layout/index.js.map +1 -0
  71. package/dist/reactive-log-BfvfNWQh.d.cts +137 -0
  72. package/dist/reactive-log-ohLmTXoZ.d.ts +137 -0
  73. package/package.json +256 -0
@@ -0,0 +1,1793 @@
1
+ import {
2
+ describeNode
3
+ } from "./chunk-O3PI7W45.js";
4
+ import {
5
+ COMPLETE,
6
+ DATA,
7
+ DIRTY,
8
+ ERROR,
9
+ GuardDenied,
10
+ INVALIDATE,
11
+ NodeImpl,
12
+ RESOLVED,
13
+ TEARDOWN,
14
+ isBatching,
15
+ messageTier,
16
+ monotonicNs,
17
+ state
18
+ } from "./chunk-5X3LAO3B.js";
19
+
20
+ // src/graph/graph.ts
21
+ var PATH_SEP = "::";
22
+ var GRAPH_META_SEGMENT = "__meta__";
23
+ var SNAPSHOT_VERSION = 1;
24
+ function parseSnapshotEnvelope(data) {
25
+ if (data.version !== SNAPSHOT_VERSION) {
26
+ throw new Error(
27
+ `unsupported snapshot version ${String(data.version)} (expected ${SNAPSHOT_VERSION})`
28
+ );
29
+ }
30
+ for (const key of ["name", "nodes", "edges", "subgraphs"]) {
31
+ if (!(key in data)) {
32
+ throw new Error(`snapshot missing required key "${key}"`);
33
+ }
34
+ }
35
+ if (typeof data.name !== "string") {
36
+ throw new TypeError(`snapshot 'name' must be a string`);
37
+ }
38
+ if (typeof data.nodes !== "object" || data.nodes === null || Array.isArray(data.nodes)) {
39
+ throw new TypeError(`snapshot 'nodes' must be an object`);
40
+ }
41
+ if (!Array.isArray(data.edges)) {
42
+ throw new TypeError(`snapshot 'edges' must be an array`);
43
+ }
44
+ if (!Array.isArray(data.subgraphs)) {
45
+ throw new TypeError(`snapshot 'subgraphs' must be an array`);
46
+ }
47
+ }
48
+ function sortJsonValue(value) {
49
+ if (value === null || typeof value !== "object") {
50
+ return value;
51
+ }
52
+ if (Array.isArray(value)) {
53
+ return value.map(sortJsonValue);
54
+ }
55
+ const obj = value;
56
+ const keys = Object.keys(obj).sort();
57
+ const out = {};
58
+ for (const k of keys) {
59
+ out[k] = sortJsonValue(obj[k]);
60
+ }
61
+ return out;
62
+ }
63
+ function stableJsonStringify(value) {
64
+ return `${JSON.stringify(sortJsonValue(value))}
65
+ `;
66
+ }
67
+ function escapeMermaidLabel(value) {
68
+ return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
69
+ }
70
+ function escapeD2Label(value) {
71
+ return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
72
+ }
73
+ function d2DirectionFromGraphDirection(direction) {
74
+ if (direction === "TD") return "down";
75
+ if (direction === "BT") return "up";
76
+ if (direction === "RL") return "left";
77
+ return "right";
78
+ }
79
+ function collectDiagramArrows(described) {
80
+ const seen = /* @__PURE__ */ new Set();
81
+ const arrows = [];
82
+ function add(from, to) {
83
+ const key = `${from}\0${to}`;
84
+ if (seen.has(key)) return;
85
+ seen.add(key);
86
+ arrows.push([from, to]);
87
+ }
88
+ for (const [path, info] of Object.entries(described.nodes)) {
89
+ const deps = info.deps;
90
+ if (deps) {
91
+ for (const dep of deps) add(dep, path);
92
+ }
93
+ }
94
+ for (const edge of described.edges) add(edge.from, edge.to);
95
+ return arrows;
96
+ }
97
+ function normalizeDiagramDirection(direction) {
98
+ if (direction === void 0) return "LR";
99
+ if (direction === "TD" || direction === "LR" || direction === "BT" || direction === "RL") {
100
+ return direction;
101
+ }
102
+ throw new Error(
103
+ `invalid diagram direction ${String(direction)}; expected one of: TD, LR, BT, RL`
104
+ );
105
+ }
106
+ function escapeRegexLiteral(value) {
107
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
108
+ }
109
+ function globToRegex(pattern) {
110
+ let re = "^";
111
+ for (let i = 0; i < pattern.length; i += 1) {
112
+ const ch = pattern[i];
113
+ if (ch === "*") {
114
+ re += ".*";
115
+ continue;
116
+ }
117
+ if (ch === "?") {
118
+ re += ".";
119
+ continue;
120
+ }
121
+ if (ch === "[") {
122
+ const end = pattern.indexOf("]", i + 1);
123
+ if (end <= i + 1) {
124
+ re += "\\[";
125
+ continue;
126
+ }
127
+ let cls = pattern.slice(i + 1, end);
128
+ if (cls.startsWith("!")) cls = `^${cls.slice(1)}`;
129
+ cls = cls.replace(/\\/g, "\\\\");
130
+ re += `[${cls}]`;
131
+ i = end;
132
+ continue;
133
+ }
134
+ re += escapeRegexLiteral(ch);
135
+ }
136
+ re += "$";
137
+ return new RegExp(re);
138
+ }
139
+ var RingBuffer = class {
140
+ constructor(capacity) {
141
+ this.capacity = capacity;
142
+ this.buf = new Array(capacity);
143
+ }
144
+ buf;
145
+ head = 0;
146
+ _size = 0;
147
+ get size() {
148
+ return this._size;
149
+ }
150
+ push(item) {
151
+ const idx = (this.head + this._size) % this.capacity;
152
+ this.buf[idx] = item;
153
+ if (this._size < this.capacity) this._size++;
154
+ else this.head = (this.head + 1) % this.capacity;
155
+ }
156
+ toArray() {
157
+ const result = [];
158
+ for (let i = 0; i < this._size; i++) result.push(this.buf[(this.head + i) % this.capacity]);
159
+ return result;
160
+ }
161
+ };
162
+ var SPY_ANSI_THEME = {
163
+ data: "\x1B[32m",
164
+ dirty: "\x1B[33m",
165
+ resolved: "\x1B[36m",
166
+ complete: "\x1B[34m",
167
+ error: "\x1B[31m",
168
+ derived: "\x1B[35m",
169
+ path: "\x1B[90m",
170
+ reset: "\x1B[0m"
171
+ };
172
+ var SPY_NO_COLOR_THEME = {
173
+ data: "",
174
+ dirty: "",
175
+ resolved: "",
176
+ complete: "",
177
+ error: "",
178
+ derived: "",
179
+ path: "",
180
+ reset: ""
181
+ };
182
+ function describeData(value) {
183
+ if (typeof value === "string") return JSON.stringify(value);
184
+ if (typeof value === "number" || typeof value === "boolean" || value == null)
185
+ return String(value);
186
+ try {
187
+ return JSON.stringify(value);
188
+ } catch {
189
+ return "[unserializable]";
190
+ }
191
+ }
192
+ function resolveSpyTheme(theme) {
193
+ if (theme === "none") return SPY_NO_COLOR_THEME;
194
+ if (theme === "ansi" || theme == null) return SPY_ANSI_THEME;
195
+ return {
196
+ data: theme.data ?? "",
197
+ dirty: theme.dirty ?? "",
198
+ resolved: theme.resolved ?? "",
199
+ complete: theme.complete ?? "",
200
+ error: theme.error ?? "",
201
+ derived: theme.derived ?? "",
202
+ path: theme.path ?? "",
203
+ reset: theme.reset ?? ""
204
+ };
205
+ }
206
+ function assertLocalName(name, graphName, label) {
207
+ if (name === "") {
208
+ throw new Error(`Graph "${graphName}": ${label} name must be non-empty`);
209
+ }
210
+ }
211
+ function assertNoPathSep(name, graphName, label) {
212
+ if (name.includes(PATH_SEP)) {
213
+ throw new Error(
214
+ `Graph "${graphName}": ${label} "${name}" must not contain '${PATH_SEP}' (path separator)`
215
+ );
216
+ }
217
+ }
218
+ function assertNotReservedMetaSegment(name, graphName, label) {
219
+ if (name === GRAPH_META_SEGMENT) {
220
+ throw new Error(
221
+ `Graph "${graphName}": ${label} name "${GRAPH_META_SEGMENT}" is reserved for meta companion paths`
222
+ );
223
+ }
224
+ }
225
+ function assertConnectPathNotMeta(path, graphName) {
226
+ if (path.split(PATH_SEP).includes(GRAPH_META_SEGMENT)) {
227
+ throw new Error(
228
+ `Graph "${graphName}": connect/disconnect endpoints must be registered graph nodes, not meta paths (got "${path}")`
229
+ );
230
+ }
231
+ }
232
+ function splitPath(path, graphName) {
233
+ if (path === "") {
234
+ throw new Error(`Graph "${graphName}": resolve path must be non-empty`);
235
+ }
236
+ const segments = path.split(PATH_SEP);
237
+ for (const s of segments) {
238
+ if (s === "") {
239
+ throw new Error(`Graph "${graphName}": resolve path has empty segment`);
240
+ }
241
+ }
242
+ return segments;
243
+ }
244
+ function edgeKey(from, to) {
245
+ return `${from} ${to}`;
246
+ }
247
+ function parseEdgeKey(key) {
248
+ const i = key.indexOf(" ");
249
+ return [key.slice(0, i), key.slice(i + 1)];
250
+ }
251
+ var META_FILTERED_TYPES = /* @__PURE__ */ new Set([TEARDOWN, INVALIDATE, COMPLETE, ERROR]);
252
+ function filterMetaMessages(messages) {
253
+ const kept = messages.filter((m) => !META_FILTERED_TYPES.has(m[0]));
254
+ return kept;
255
+ }
256
+ function teardownMountedGraph(root) {
257
+ for (const child of root._mounts.values()) {
258
+ teardownMountedGraph(child);
259
+ }
260
+ for (const n of root._nodes.values()) {
261
+ n.down([[TEARDOWN]], { internal: true });
262
+ }
263
+ }
264
+ var Graph = class _Graph {
265
+ static _factories = [];
266
+ name;
267
+ opts;
268
+ /** @internal — exposed for {@link teardownMountedGraph} and cross-graph helpers. */
269
+ _nodes = /* @__PURE__ */ new Map();
270
+ _edges = /* @__PURE__ */ new Set();
271
+ /** @internal — exposed for {@link teardownMountedGraph}. */
272
+ _mounts = /* @__PURE__ */ new Map();
273
+ _autoCheckpointDisposers = /* @__PURE__ */ new Set();
274
+ _defaultVersioningLevel;
275
+ static registerFactory(pattern, factory) {
276
+ if (!pattern) {
277
+ throw new Error("Graph.registerFactory requires a non-empty pattern");
278
+ }
279
+ _Graph.unregisterFactory(pattern);
280
+ _Graph._factories.push({ pattern, re: globToRegex(pattern), factory });
281
+ }
282
+ static unregisterFactory(pattern) {
283
+ const i = _Graph._factories.findIndex((entry) => entry.pattern === pattern);
284
+ if (i >= 0) _Graph._factories.splice(i, 1);
285
+ }
286
+ /**
287
+ * @param name - Non-empty graph id (must not contain `::`).
288
+ * @param opts - Reserved for future hooks; currently unused.
289
+ */
290
+ constructor(name, opts) {
291
+ if (name === "") {
292
+ throw new Error("Graph name must be non-empty");
293
+ }
294
+ if (name.includes(PATH_SEP)) {
295
+ throw new Error(`Graph name must not contain '${PATH_SEP}' (got "${name}")`);
296
+ }
297
+ this.name = name;
298
+ this.opts = opts ?? {};
299
+ }
300
+ static _factoryForPath(path) {
301
+ for (let i = _Graph._factories.length - 1; i >= 0; i -= 1) {
302
+ const entry = _Graph._factories[i];
303
+ if (entry.re.test(path)) return entry.factory;
304
+ }
305
+ return void 0;
306
+ }
307
+ static _ownerForPath(root, path) {
308
+ const segments = path.split(PATH_SEP);
309
+ const local = segments.pop();
310
+ if (local == null || local.length === 0) {
311
+ throw new Error(`invalid snapshot path "${path}"`);
312
+ }
313
+ let owner = root;
314
+ for (const seg of segments) {
315
+ const next = owner._mounts.get(seg);
316
+ if (!next) throw new Error(`unknown mount "${seg}" in path "${path}"`);
317
+ owner = next;
318
+ }
319
+ return [owner, local];
320
+ }
321
+ /**
322
+ * Graphs reachable from this instance via nested {@link Graph.mount} (includes `this`).
323
+ */
324
+ _graphsReachableViaMounts(seen = /* @__PURE__ */ new Set()) {
325
+ if (seen.has(this)) return seen;
326
+ seen.add(this);
327
+ for (const child of this._mounts.values()) {
328
+ child._graphsReachableViaMounts(seen);
329
+ }
330
+ return seen;
331
+ }
332
+ /**
333
+ * Resolve an endpoint: returns `[owningGraph, localName, node]`.
334
+ * Accepts both local names and `::` qualified paths.
335
+ */
336
+ _resolveEndpoint(path) {
337
+ if (!path.includes(PATH_SEP)) {
338
+ const n = this._nodes.get(path);
339
+ if (!n) {
340
+ throw new Error(`Graph "${this.name}": unknown node "${path}"`);
341
+ }
342
+ return [this, path, n];
343
+ }
344
+ const segments = splitPath(path, this.name);
345
+ return this._resolveEndpointFromSegments(segments, path);
346
+ }
347
+ _resolveEndpointFromSegments(segments, fullPath) {
348
+ const head = segments[0];
349
+ const rest = segments.slice(1);
350
+ if (rest.length === 0) {
351
+ const n = this._nodes.get(head);
352
+ if (n) return [this, head, n];
353
+ throw new Error(`Graph "${this.name}": unknown node "${head}" (from path "${fullPath}")`);
354
+ }
355
+ const localN = this._nodes.get(head);
356
+ if (localN && rest.length > 0 && rest[0] === GRAPH_META_SEGMENT) {
357
+ return this._resolveMetaEndpointKeys(localN, head, rest, fullPath);
358
+ }
359
+ const child = this._mounts.get(head);
360
+ if (!child) {
361
+ if (this._nodes.has(head)) {
362
+ throw new Error(
363
+ `Graph "${this.name}": "${head}" is a node; trailing path "${rest.join(PATH_SEP)}" is invalid`
364
+ );
365
+ }
366
+ throw new Error(`Graph "${this.name}": unknown mount or node "${head}"`);
367
+ }
368
+ return child._resolveEndpointFromSegments(rest, fullPath);
369
+ }
370
+ // ——————————————————————————————————————————————————————————————
371
+ // Node registry
372
+ // ——————————————————————————————————————————————————————————————
373
+ /**
374
+ * Registers a node under a local name. Fails if the name is already used,
375
+ * reserved by a mount, or the same node instance is already registered.
376
+ *
377
+ * @param name - Local key (no `::`).
378
+ * @param node - Node instance to own.
379
+ */
380
+ add(name, node) {
381
+ assertLocalName(name, this.name, "add");
382
+ assertNoPathSep(name, this.name, "add");
383
+ assertNotReservedMetaSegment(name, this.name, "node");
384
+ if (this._mounts.has(name)) {
385
+ throw new Error(`Graph "${this.name}": name "${name}" is already a mount point`);
386
+ }
387
+ if (this._nodes.has(name)) {
388
+ throw new Error(`Graph "${this.name}": node "${name}" already exists`);
389
+ }
390
+ for (const [existingName, existing] of this._nodes) {
391
+ if (existing === node) {
392
+ throw new Error(
393
+ `Graph "${this.name}": node instance already registered as "${existingName}"`
394
+ );
395
+ }
396
+ }
397
+ this._nodes.set(name, node);
398
+ if (node instanceof NodeImpl) {
399
+ node._assignRegistryName(name);
400
+ if (this._defaultVersioningLevel != null) {
401
+ node._applyVersioning(this._defaultVersioningLevel);
402
+ }
403
+ }
404
+ }
405
+ /**
406
+ * Set a default versioning level for all nodes added to this graph (roadmap §6.0).
407
+ *
408
+ * Nodes already registered are retroactively upgraded. Nodes added later via
409
+ * {@link add} will inherit this level unless they already have versioning.
410
+ *
411
+ * **Scope:** Does not propagate to mounted subgraphs. Call `setVersioning`
412
+ * on each child graph separately if needed.
413
+ *
414
+ * @param level - `0` for V0, `1` for V1, or `undefined` to clear.
415
+ */
416
+ setVersioning(level) {
417
+ this._defaultVersioningLevel = level;
418
+ if (level == null) return;
419
+ for (const n of this._nodes.values()) {
420
+ if (n instanceof NodeImpl) {
421
+ n._applyVersioning(level);
422
+ }
423
+ }
424
+ }
425
+ /**
426
+ * Unregisters a node or unmounts a subgraph, drops incident edges, and sends
427
+ * `[[TEARDOWN]]` to the removed node or recursively through the mounted subtree (§3.2).
428
+ *
429
+ * @param name - Local mount or node name.
430
+ */
431
+ remove(name) {
432
+ assertLocalName(name, this.name, "remove");
433
+ assertNoPathSep(name, this.name, "remove");
434
+ const child = this._mounts.get(name);
435
+ if (child) {
436
+ this._mounts.delete(name);
437
+ const prefix = `${name}${PATH_SEP}`;
438
+ for (const key of [...this._edges]) {
439
+ const [from, to] = parseEdgeKey(key);
440
+ if (from === name || to === name || from.startsWith(prefix) || to.startsWith(prefix)) {
441
+ this._edges.delete(key);
442
+ }
443
+ }
444
+ teardownMountedGraph(child);
445
+ return;
446
+ }
447
+ const node = this._nodes.get(name);
448
+ if (!node) {
449
+ throw new Error(`Graph "${this.name}": unknown node or mount "${name}"`);
450
+ }
451
+ this._nodes.delete(name);
452
+ for (const key of [...this._edges]) {
453
+ const [from, to] = parseEdgeKey(key);
454
+ if (from === name || to === name) this._edges.delete(key);
455
+ }
456
+ node.down([[TEARDOWN]], { internal: true });
457
+ }
458
+ /**
459
+ * Returns a node by local name or `::` qualified path.
460
+ * Local names are looked up directly; paths with `::` delegate to {@link resolve}.
461
+ *
462
+ * @param name - Local name or qualified path.
463
+ */
464
+ node(name) {
465
+ if (name === "") {
466
+ throw new Error(`Graph "${this.name}": node name must be non-empty`);
467
+ }
468
+ if (name.includes(PATH_SEP)) {
469
+ return this.resolve(name);
470
+ }
471
+ const n = this._nodes.get(name);
472
+ if (!n) {
473
+ throw new Error(`Graph "${this.name}": unknown node "${name}"`);
474
+ }
475
+ return n;
476
+ }
477
+ /**
478
+ * Reads `graph.node(name).get()` — accepts `::` qualified paths (§3.2).
479
+ *
480
+ * @param name - Local name or qualified path.
481
+ * @returns Cached value or `undefined`.
482
+ */
483
+ get(name) {
484
+ return this.node(name).get();
485
+ }
486
+ /**
487
+ * Shorthand for `graph.node(name).down([[DATA, value]], { actor })` — accepts `::` qualified paths (§3.2).
488
+ *
489
+ * @param name - Local name or qualified path.
490
+ * @param value - Next `DATA` payload.
491
+ * @param options - Optional `actor` and `internal` guard bypass.
492
+ */
493
+ set(name, value, options) {
494
+ const internal = options?.internal === true;
495
+ this.node(name).down([[DATA, value]], {
496
+ actor: options?.actor,
497
+ internal,
498
+ delivery: "write"
499
+ });
500
+ }
501
+ // ——————————————————————————————————————————————————————————————
502
+ // Edges
503
+ // ——————————————————————————————————————————————————————————————
504
+ /**
505
+ * Record a wire from `fromPath` → `toPath` (§3.3). Accepts local names or
506
+ * `::` qualified paths. The target must be a {@link NodeImpl} whose `_deps`
507
+ * includes the source node (same reference). Idempotent.
508
+ *
509
+ * Same-owner edges are stored on the owning child graph; cross-subgraph edges
510
+ * are stored on this (parent) graph's registry.
511
+ *
512
+ * @param fromPath - Source endpoint (local or qualified).
513
+ * @param toPath - Target endpoint whose deps already include the source node.
514
+ */
515
+ connect(fromPath, toPath) {
516
+ if (!fromPath || !toPath) {
517
+ throw new Error(`Graph "${this.name}": connect paths must be non-empty`);
518
+ }
519
+ assertConnectPathNotMeta(fromPath, this.name);
520
+ assertConnectPathNotMeta(toPath, this.name);
521
+ const [fromGraph, fromLocal, fromNode] = this._resolveEndpoint(fromPath);
522
+ const [toGraph, toLocal, toNode] = this._resolveEndpoint(toPath);
523
+ if (fromNode === toNode) {
524
+ throw new Error(`Graph "${this.name}": cannot connect a node to itself`);
525
+ }
526
+ if (!(toNode instanceof NodeImpl)) {
527
+ throw new Error(
528
+ `Graph "${this.name}": connect(${fromPath}, ${toPath}) requires the target to be a graphrefly NodeImpl so deps can be validated`
529
+ );
530
+ }
531
+ if (!toNode._deps.includes(fromNode)) {
532
+ throw new Error(
533
+ `Graph "${this.name}": connect(${fromPath}, ${toPath}) \u2014 target must include source in its constructor deps (same node reference)`
534
+ );
535
+ }
536
+ if (fromGraph === toGraph) {
537
+ const key = edgeKey(fromLocal, toLocal);
538
+ fromGraph._edges.add(key);
539
+ } else {
540
+ const key = edgeKey(fromPath, toPath);
541
+ this._edges.add(key);
542
+ }
543
+ }
544
+ /**
545
+ * Remove a registered edge (§3.3). Accepts local names or `::` qualified paths.
546
+ *
547
+ * **Registry-only (§C resolved):** This drops the edge from the graph's edge
548
+ * registry only. It does **not** mutate the target node's constructor-time
549
+ * dependency list, bitmasks, or upstream subscriptions. Message flow follows
550
+ * constructor-time deps, not the edge registry. For runtime dep rewiring, use
551
+ * {@link dynamicNode}.
552
+ *
553
+ * @param fromPath - Registered edge tail.
554
+ * @param toPath - Registered edge head.
555
+ */
556
+ disconnect(fromPath, toPath) {
557
+ if (!fromPath || !toPath) {
558
+ throw new Error(`Graph "${this.name}": disconnect paths must be non-empty`);
559
+ }
560
+ assertConnectPathNotMeta(fromPath, this.name);
561
+ assertConnectPathNotMeta(toPath, this.name);
562
+ const [fromGraph, fromLocal] = this._resolveEndpoint(fromPath);
563
+ const [toGraph, toLocal] = this._resolveEndpoint(toPath);
564
+ if (fromGraph === toGraph) {
565
+ const key = edgeKey(fromLocal, toLocal);
566
+ if (!fromGraph._edges.delete(key)) {
567
+ throw new Error(`Graph "${this.name}": no registered edge ${fromPath} \u2192 ${toPath}`);
568
+ }
569
+ } else {
570
+ const key = edgeKey(fromPath, toPath);
571
+ if (!this._edges.delete(key)) {
572
+ throw new Error(`Graph "${this.name}": no registered edge ${fromPath} \u2192 ${toPath}`);
573
+ }
574
+ }
575
+ }
576
+ /**
577
+ * Returns registered `[from, to]` edge pairs (read-only snapshot).
578
+ *
579
+ * @returns Edge pairs recorded on this graph instance’s local `_edges` set.
580
+ */
581
+ edges() {
582
+ const result = [];
583
+ for (const key of this._edges) {
584
+ result.push(parseEdgeKey(key));
585
+ }
586
+ return result;
587
+ }
588
+ // ——————————————————————————————————————————————————————————————
589
+ // Composition
590
+ // ——————————————————————————————————————————————————————————————
591
+ /**
592
+ * Embed a child graph at a local mount name (§3.4). Child nodes are reachable via
593
+ * {@link Graph.resolve} using `::` delimited paths (§3.5). Lifecycle
594
+ * {@link Graph.signal} visits mounted subgraphs recursively.
595
+ *
596
+ * Rejects: same name as existing node or mount, self-mount, mount cycles,
597
+ * and the same child graph instance mounted twice on one parent.
598
+ *
599
+ * @param name - Local mount point.
600
+ * @param child - Nested `Graph` instance.
601
+ */
602
+ mount(name, child) {
603
+ assertLocalName(name, this.name, "mount");
604
+ assertNoPathSep(name, this.name, "mount");
605
+ assertNotReservedMetaSegment(name, this.name, "mount");
606
+ if (this._nodes.has(name)) {
607
+ throw new Error(
608
+ `Graph "${this.name}": cannot mount at "${name}" \u2014 node with that name exists`
609
+ );
610
+ }
611
+ if (this._mounts.has(name)) {
612
+ throw new Error(`Graph "${this.name}": mount "${name}" already exists`);
613
+ }
614
+ if (child === this) {
615
+ throw new Error(`Graph "${this.name}": cannot mount a graph into itself`);
616
+ }
617
+ for (const existing of this._mounts.values()) {
618
+ if (existing === child) {
619
+ throw new Error(`Graph "${this.name}": this child graph is already mounted on this graph`);
620
+ }
621
+ }
622
+ if (child._graphsReachableViaMounts().has(this)) {
623
+ throw new Error(`Graph "${this.name}": mount("${name}", \u2026) would create a mount cycle`);
624
+ }
625
+ this._mounts.set(name, child);
626
+ }
627
+ /**
628
+ * Look up a node by qualified path (§3.5). Segments are separated by `::`.
629
+ *
630
+ * If the first segment equals this graph's {@link Graph.name}, it is stripped
631
+ * (so `root.resolve("app::a")` works when `root.name === "app"`).
632
+ *
633
+ * @param path - Qualified `::` path or local name.
634
+ * @returns The resolved `Node`.
635
+ */
636
+ resolve(path) {
637
+ let segments = splitPath(path, this.name);
638
+ if (segments[0] === this.name) {
639
+ segments = segments.slice(1);
640
+ if (segments.length === 0) {
641
+ throw new Error(`Graph "${this.name}": resolve path ends at graph name only`);
642
+ }
643
+ }
644
+ return this._resolveFromSegments(segments);
645
+ }
646
+ _resolveFromSegments(segments) {
647
+ const head = segments[0];
648
+ const rest = segments.slice(1);
649
+ if (rest.length === 0) {
650
+ const n = this._nodes.get(head);
651
+ if (n) return n;
652
+ if (this._mounts.has(head)) {
653
+ throw new Error(
654
+ `Graph "${this.name}": path ends at subgraph "${head}" \u2014 not a node (GRAPHREFLY-SPEC \xA73.5)`
655
+ );
656
+ }
657
+ throw new Error(`Graph "${this.name}": unknown name "${head}"`);
658
+ }
659
+ const localN = this._nodes.get(head);
660
+ if (localN && rest.length > 0 && rest[0] === GRAPH_META_SEGMENT) {
661
+ return this._resolveMetaChainFromNode(localN, rest, segments.join(PATH_SEP));
662
+ }
663
+ const child = this._mounts.get(head);
664
+ if (!child) {
665
+ if (this._nodes.has(head)) {
666
+ throw new Error(
667
+ `Graph "${this.name}": "${head}" is a node; trailing path "${rest.join(PATH_SEP)}" is invalid`
668
+ );
669
+ }
670
+ throw new Error(`Graph "${this.name}": unknown mount or node "${head}"`);
671
+ }
672
+ return child.resolve(rest.join(PATH_SEP));
673
+ }
674
+ /**
675
+ * Resolve `::__meta__::key` segments from a registered primary node (possibly chained).
676
+ */
677
+ _resolveMetaChainFromNode(n, parts, fullPath) {
678
+ let current = n;
679
+ let i = 0;
680
+ const p = [...parts];
681
+ while (i < p.length) {
682
+ if (p[i] !== GRAPH_META_SEGMENT) {
683
+ throw new Error(
684
+ `Graph "${this.name}": expected ${GRAPH_META_SEGMENT} segment in meta path "${fullPath}"`
685
+ );
686
+ }
687
+ if (i + 1 >= p.length) {
688
+ throw new Error(
689
+ `Graph "${this.name}": meta path requires a key after ${GRAPH_META_SEGMENT} in "${fullPath}"`
690
+ );
691
+ }
692
+ const key = p[i + 1];
693
+ const next = current.meta[key];
694
+ if (!next) {
695
+ throw new Error(`Graph "${this.name}": unknown meta "${key}" in path "${fullPath}"`);
696
+ }
697
+ current = next;
698
+ i += 2;
699
+ }
700
+ return current;
701
+ }
702
+ _resolveMetaEndpointKeys(baseNode, baseLocalKey, parts, fullPath) {
703
+ let current = baseNode;
704
+ let localKey = baseLocalKey;
705
+ let i = 0;
706
+ const p = [...parts];
707
+ while (i < p.length) {
708
+ if (p[i] !== GRAPH_META_SEGMENT) {
709
+ throw new Error(
710
+ `Graph "${this.name}": expected ${GRAPH_META_SEGMENT} segment in meta path "${fullPath}"`
711
+ );
712
+ }
713
+ if (i + 1 >= p.length) {
714
+ throw new Error(
715
+ `Graph "${this.name}": meta path requires a key after ${GRAPH_META_SEGMENT} in "${fullPath}"`
716
+ );
717
+ }
718
+ const metaKey = p[i + 1];
719
+ const next = current.meta[metaKey];
720
+ if (!next) {
721
+ throw new Error(
722
+ `Graph "${this.name}": unknown meta "${metaKey}" on node (in "${fullPath}")`
723
+ );
724
+ }
725
+ localKey = `${localKey}${PATH_SEP}${GRAPH_META_SEGMENT}${PATH_SEP}${metaKey}`;
726
+ current = next;
727
+ i += 2;
728
+ }
729
+ return [this, localKey, current];
730
+ }
731
+ /**
732
+ * Deliver a message batch to every registered node in this graph and, recursively,
733
+ * in mounted child graphs (§3.7). Recurses into mounts first, then delivers to
734
+ * local nodes (sorted by name). Each {@link Node} receives at most one delivery
735
+ * per call (deduped by reference).
736
+ *
737
+ * Companion `meta` nodes receive the same batch for control-plane types (e.g.
738
+ * PAUSE) that the primary does not forward. **TEARDOWN-only** batches skip the
739
+ * extra meta pass — the primary’s `down()` already cascades TEARDOWN to meta.
740
+ *
741
+ * @param messages - Batch to deliver to every registered node (and mounts, recursively).
742
+ * @param options - Optional `actor` / `internal` for transport.
743
+ */
744
+ signal(messages, options) {
745
+ this._signalDeliver(messages, options ?? {}, /* @__PURE__ */ new Set());
746
+ }
747
+ _signalDeliver(messages, opts, vis) {
748
+ for (const sub of this._mounts.values()) {
749
+ sub._signalDeliver(messages, opts, vis);
750
+ }
751
+ const internal = opts.internal === true;
752
+ const downOpts = internal ? { internal: true } : { actor: opts.actor, delivery: "signal" };
753
+ const metaMessages = filterMetaMessages(messages);
754
+ for (const localName of [...this._nodes.keys()].sort()) {
755
+ const n = this._nodes.get(localName);
756
+ if (vis.has(n)) continue;
757
+ vis.add(n);
758
+ n.down(messages, downOpts);
759
+ if (metaMessages.length === 0) continue;
760
+ this._signalMetaSubtree(n, metaMessages, vis, downOpts);
761
+ }
762
+ }
763
+ _signalMetaSubtree(root, messages, vis, downOpts) {
764
+ for (const mk of Object.keys(root.meta).sort()) {
765
+ const mnode = root.meta[mk];
766
+ if (vis.has(mnode)) continue;
767
+ vis.add(mnode);
768
+ mnode.down(messages, downOpts);
769
+ this._signalMetaSubtree(mnode, messages, vis, downOpts);
770
+ }
771
+ }
772
+ /**
773
+ * Static structure snapshot: qualified node keys, edges, mount names (GRAPHREFLY-SPEC §3.6, Appendix B).
774
+ *
775
+ * @param options - Optional `actor` for guard-scoped visibility and/or `filter` for selective output.
776
+ * @returns JSON-shaped describe payload for this graph tree.
777
+ *
778
+ * @example
779
+ * ```ts
780
+ * graph.describe() // full snapshot
781
+ * graph.describe({ actor: llm }) // guard-scoped
782
+ * graph.describe({ filter: { status: "errored" } }) // only errored nodes
783
+ * graph.describe({ filter: (n) => n.type === "state" }) // predicate filter
784
+ * ```
785
+ */
786
+ describe(options) {
787
+ const actor = options?.actor;
788
+ const filter = options?.filter;
789
+ const targets = [];
790
+ this._collectObserveTargets("", targets);
791
+ const nodeToPath = /* @__PURE__ */ new Map();
792
+ for (const [p, n] of targets) {
793
+ nodeToPath.set(n, p);
794
+ }
795
+ const nodes = {};
796
+ for (const [p, n] of targets) {
797
+ if (actor != null && !n.allowsObserve(actor)) continue;
798
+ const raw = describeNode(n);
799
+ const deps = n instanceof NodeImpl ? n._deps.map((d) => nodeToPath.get(d) ?? d.name ?? "") : [];
800
+ const { name: _name, ...rest } = raw;
801
+ const entry = { ...rest, deps };
802
+ if (filter != null) {
803
+ if (typeof filter === "function") {
804
+ const fn = filter;
805
+ const pass = fn.length >= 2 ? fn(p, entry) : fn(entry);
806
+ if (!pass) continue;
807
+ } else {
808
+ let match = true;
809
+ for (const [fk, fv] of Object.entries(filter)) {
810
+ const normalizedKey = fk === "deps_includes" ? "depsIncludes" : fk === "meta_has" ? "metaHas" : fk;
811
+ if (normalizedKey === "depsIncludes") {
812
+ if (!entry.deps.includes(String(fv))) {
813
+ match = false;
814
+ break;
815
+ }
816
+ continue;
817
+ }
818
+ if (normalizedKey === "metaHas") {
819
+ if (!Object.hasOwn(entry.meta, String(fv))) {
820
+ match = false;
821
+ break;
822
+ }
823
+ continue;
824
+ }
825
+ if (entry[normalizedKey] !== fv) {
826
+ match = false;
827
+ break;
828
+ }
829
+ }
830
+ if (!match) continue;
831
+ }
832
+ }
833
+ nodes[p] = entry;
834
+ }
835
+ const nodeKeys = new Set(Object.keys(nodes));
836
+ let edges = this._collectAllEdges("");
837
+ if (actor != null || filter != null) {
838
+ edges = edges.filter((e) => nodeKeys.has(e.from) && nodeKeys.has(e.to));
839
+ }
840
+ edges.sort((a, b) => {
841
+ if (a.from < b.from) return -1;
842
+ if (a.from > b.from) return 1;
843
+ if (a.to < b.to) return -1;
844
+ if (a.to > b.to) return 1;
845
+ return 0;
846
+ });
847
+ const allSubgraphs = this._collectSubgraphs("");
848
+ const subgraphs = actor != null || filter != null ? allSubgraphs.filter((sg) => {
849
+ const prefix = `${sg}${PATH_SEP}`;
850
+ return [...nodeKeys].some((k) => k === sg || k.startsWith(prefix));
851
+ }) : allSubgraphs;
852
+ return {
853
+ name: this.name,
854
+ nodes,
855
+ edges,
856
+ subgraphs
857
+ };
858
+ }
859
+ _collectSubgraphs(prefix) {
860
+ const out = [];
861
+ for (const m of [...this._mounts.keys()].sort()) {
862
+ const q = prefix === "" ? m : `${prefix}${m}`;
863
+ out.push(q);
864
+ out.push(...this._mounts.get(m)._collectSubgraphs(`${q}${PATH_SEP}`));
865
+ }
866
+ return out;
867
+ }
868
+ _collectAllEdges(prefix) {
869
+ const out = [];
870
+ for (const m of [...this._mounts.keys()].sort()) {
871
+ const p2 = prefix === "" ? m : `${prefix}${PATH_SEP}${m}`;
872
+ out.push(...this._mounts.get(m)._collectAllEdges(p2));
873
+ }
874
+ for (const [f, t] of this.edges()) {
875
+ out.push({
876
+ from: this._qualifyEdgeEndpoint(f, prefix),
877
+ to: this._qualifyEdgeEndpoint(t, prefix)
878
+ });
879
+ }
880
+ return out;
881
+ }
882
+ _qualifyEdgeEndpoint(part, prefix) {
883
+ if (part.includes(PATH_SEP)) return part;
884
+ return prefix === "" ? part : `${prefix}${PATH_SEP}${part}`;
885
+ }
886
+ _collectObserveTargets(prefix, out) {
887
+ for (const m of [...this._mounts.keys()].sort()) {
888
+ const p2 = prefix === "" ? m : `${prefix}${PATH_SEP}${m}`;
889
+ this._mounts.get(m)._collectObserveTargets(p2, out);
890
+ }
891
+ for (const loc of [...this._nodes.keys()].sort()) {
892
+ const n = this._nodes.get(loc);
893
+ const p = prefix === "" ? loc : `${prefix}${PATH_SEP}${loc}`;
894
+ out.push([p, n]);
895
+ this._appendMetaObserveTargets(p, n, out);
896
+ }
897
+ }
898
+ _appendMetaObserveTargets(basePath, n, out) {
899
+ for (const mk of Object.keys(n.meta).sort()) {
900
+ const m = n.meta[mk];
901
+ const mp = `${basePath}${PATH_SEP}${GRAPH_META_SEGMENT}${PATH_SEP}${mk}`;
902
+ out.push([mp, m]);
903
+ this._appendMetaObserveTargets(mp, m, out);
904
+ }
905
+ }
906
+ observe(pathOrOpts, options) {
907
+ if (typeof pathOrOpts === "string") {
908
+ const path = pathOrOpts;
909
+ const actor2 = options?.actor;
910
+ const target = this.resolve(path);
911
+ if (actor2 != null && !target.allowsObserve(actor2)) {
912
+ throw new GuardDenied({ actor: actor2, action: "observe", nodeName: path });
913
+ }
914
+ const wantsStructured2 = options?.structured === true || options?.timeline === true || options?.causal === true || options?.derived === true;
915
+ if (wantsStructured2 && _Graph.inspectorEnabled) {
916
+ return this._createObserveResult(path, target, options);
917
+ }
918
+ return {
919
+ subscribe(sink) {
920
+ return target.subscribe(sink);
921
+ },
922
+ up(messages) {
923
+ try {
924
+ target.up?.(messages);
925
+ } catch (err) {
926
+ if (err instanceof GuardDenied) return;
927
+ throw err;
928
+ }
929
+ }
930
+ };
931
+ }
932
+ const opts = pathOrOpts;
933
+ const actor = opts?.actor;
934
+ const wantsStructured = opts?.structured === true || opts?.timeline === true || opts?.causal === true || opts?.derived === true;
935
+ if (wantsStructured && _Graph.inspectorEnabled) {
936
+ return this._createObserveResultForAll(opts ?? {});
937
+ }
938
+ return {
939
+ subscribe: (sink) => {
940
+ const targets = [];
941
+ this._collectObserveTargets("", targets);
942
+ targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
943
+ const picked = actor == null ? targets : targets.filter(([, nd]) => nd.allowsObserve(actor));
944
+ const unsubs = picked.map(
945
+ ([p, nd]) => nd.subscribe((msgs) => {
946
+ sink(p, msgs);
947
+ })
948
+ );
949
+ return () => {
950
+ for (const u of unsubs) u();
951
+ };
952
+ },
953
+ up: (upPath, messages) => {
954
+ try {
955
+ const nd = this.resolve(upPath);
956
+ nd.up?.(messages);
957
+ } catch (err) {
958
+ if (err instanceof GuardDenied) return;
959
+ throw err;
960
+ }
961
+ }
962
+ };
963
+ }
964
+ _createObserveResult(path, target, options) {
965
+ const timeline = options.timeline === true;
966
+ const causal = options.causal === true;
967
+ const derived = options.derived === true;
968
+ const result = {
969
+ values: {},
970
+ dirtyCount: 0,
971
+ resolvedCount: 0,
972
+ events: [],
973
+ completedCleanly: false,
974
+ errored: false
975
+ };
976
+ let lastTriggerDepIndex;
977
+ let lastRunDepValues;
978
+ let detachInspectorHook;
979
+ if ((causal || derived) && target instanceof NodeImpl) {
980
+ detachInspectorHook = target._setInspectorHook((event) => {
981
+ if (event.kind === "dep_message") {
982
+ lastTriggerDepIndex = event.depIndex;
983
+ return;
984
+ }
985
+ lastRunDepValues = [...event.depValues];
986
+ if (derived) {
987
+ result.events.push({
988
+ type: "derived",
989
+ path,
990
+ dep_values: [...event.depValues],
991
+ ...timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {}
992
+ });
993
+ }
994
+ });
995
+ }
996
+ const unsub = target.subscribe((msgs) => {
997
+ for (const m of msgs) {
998
+ const t = m[0];
999
+ const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
1000
+ const withCausal = causal && lastRunDepValues != null ? (() => {
1001
+ const triggerDep = lastTriggerDepIndex != null && lastTriggerDepIndex >= 0 && target instanceof NodeImpl ? target._deps[lastTriggerDepIndex] : void 0;
1002
+ const tv = triggerDep?.v;
1003
+ return {
1004
+ trigger_dep_index: lastTriggerDepIndex,
1005
+ trigger_dep_name: triggerDep?.name,
1006
+ ...tv != null ? { trigger_version: { id: tv.id, version: tv.version } } : {},
1007
+ dep_values: [...lastRunDepValues]
1008
+ };
1009
+ })() : {};
1010
+ if (t === DATA) {
1011
+ result.values[path] = m[1];
1012
+ result.events.push({ type: "data", path, data: m[1], ...base, ...withCausal });
1013
+ } else if (t === DIRTY) {
1014
+ result.dirtyCount++;
1015
+ result.events.push({ type: "dirty", path, ...base });
1016
+ } else if (t === RESOLVED) {
1017
+ result.resolvedCount++;
1018
+ result.events.push({ type: "resolved", path, ...base, ...withCausal });
1019
+ } else if (t === COMPLETE) {
1020
+ if (!result.errored) result.completedCleanly = true;
1021
+ result.events.push({ type: "complete", path, ...base });
1022
+ } else if (t === ERROR) {
1023
+ result.errored = true;
1024
+ result.events.push({ type: "error", path, data: m[1], ...base });
1025
+ }
1026
+ }
1027
+ });
1028
+ return {
1029
+ get values() {
1030
+ return result.values;
1031
+ },
1032
+ get dirtyCount() {
1033
+ return result.dirtyCount;
1034
+ },
1035
+ get resolvedCount() {
1036
+ return result.resolvedCount;
1037
+ },
1038
+ get events() {
1039
+ return result.events;
1040
+ },
1041
+ get completedCleanly() {
1042
+ return result.completedCleanly;
1043
+ },
1044
+ get errored() {
1045
+ return result.errored;
1046
+ },
1047
+ dispose() {
1048
+ unsub();
1049
+ detachInspectorHook?.();
1050
+ }
1051
+ };
1052
+ }
1053
+ _createObserveResultForAll(options) {
1054
+ const timeline = options.timeline === true;
1055
+ const result = {
1056
+ values: {},
1057
+ dirtyCount: 0,
1058
+ resolvedCount: 0,
1059
+ events: [],
1060
+ completedCleanly: false,
1061
+ errored: false
1062
+ };
1063
+ const actor = options.actor;
1064
+ const targets = [];
1065
+ this._collectObserveTargets("", targets);
1066
+ targets.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
1067
+ const picked = actor == null ? targets : targets.filter(([, nd]) => nd.allowsObserve(actor));
1068
+ const unsubs = picked.map(
1069
+ ([path, nd]) => nd.subscribe((msgs) => {
1070
+ for (const m of msgs) {
1071
+ const t = m[0];
1072
+ const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
1073
+ if (t === DATA) {
1074
+ result.values[path] = m[1];
1075
+ result.events.push({ type: "data", path, data: m[1], ...base });
1076
+ } else if (t === DIRTY) {
1077
+ result.dirtyCount++;
1078
+ result.events.push({ type: "dirty", path, ...base });
1079
+ } else if (t === RESOLVED) {
1080
+ result.resolvedCount++;
1081
+ result.events.push({ type: "resolved", path, ...base });
1082
+ } else if (t === COMPLETE) {
1083
+ if (!result.errored) result.completedCleanly = true;
1084
+ result.events.push({ type: "complete", path, ...base });
1085
+ } else if (t === ERROR) {
1086
+ result.errored = true;
1087
+ result.events.push({ type: "error", path, data: m[1], ...base });
1088
+ }
1089
+ }
1090
+ })
1091
+ );
1092
+ return {
1093
+ get values() {
1094
+ return result.values;
1095
+ },
1096
+ get dirtyCount() {
1097
+ return result.dirtyCount;
1098
+ },
1099
+ get resolvedCount() {
1100
+ return result.resolvedCount;
1101
+ },
1102
+ get events() {
1103
+ return result.events;
1104
+ },
1105
+ get completedCleanly() {
1106
+ return result.completedCleanly;
1107
+ },
1108
+ get errored() {
1109
+ return result.errored;
1110
+ },
1111
+ dispose() {
1112
+ for (const u of unsubs) u();
1113
+ }
1114
+ };
1115
+ }
1116
+ /**
1117
+ * Convenience live debugger over {@link Graph.observe}. Logs protocol events as they flow.
1118
+ *
1119
+ * Supports one-node (`path`) and graph-wide modes, event filtering, and JSON/pretty rendering.
1120
+ * Color themes are built in (`ansi` / `none`) to avoid external dependencies.
1121
+ *
1122
+ * @param options - Spy configuration.
1123
+ * @returns Disposable handle plus a structured observation accumulator.
1124
+ */
1125
+ spy(options = {}) {
1126
+ const include = options.includeTypes ? new Set(options.includeTypes) : null;
1127
+ const exclude = options.excludeTypes ? new Set(options.excludeTypes) : null;
1128
+ const theme = resolveSpyTheme(options.theme);
1129
+ const format = options.format ?? "pretty";
1130
+ const logger = options.logger ?? ((line) => console.log(line));
1131
+ const shouldLog = (type) => {
1132
+ if (include?.has(type) === false) return false;
1133
+ if (exclude?.has(type) === true) return false;
1134
+ return true;
1135
+ };
1136
+ const renderEvent = (event) => {
1137
+ if (format === "json") {
1138
+ try {
1139
+ return JSON.stringify(event);
1140
+ } catch {
1141
+ return JSON.stringify({
1142
+ type: event.type,
1143
+ path: event.path,
1144
+ data: "[unserializable]"
1145
+ });
1146
+ }
1147
+ }
1148
+ const color = theme[event.type] ?? "";
1149
+ const pathPart = event.path ? `${theme.path}${event.path}${theme.reset} ` : "";
1150
+ const dataPart = event.data !== void 0 ? ` ${describeData(event.data)}` : "";
1151
+ const triggerPart = event.trigger_dep_name != null ? ` <- ${event.trigger_dep_name}` : event.trigger_dep_index != null ? ` <- #${event.trigger_dep_index}` : "";
1152
+ const batchPart = event.in_batch ? " [batch]" : "";
1153
+ return `${pathPart}${color}${event.type.toUpperCase()}${theme.reset}${dataPart}${triggerPart}${batchPart}`;
1154
+ };
1155
+ if (!_Graph.inspectorEnabled) {
1156
+ const timeline = options.timeline ?? true;
1157
+ const acc = {
1158
+ values: {},
1159
+ dirtyCount: 0,
1160
+ resolvedCount: 0,
1161
+ events: [],
1162
+ completedCleanly: false,
1163
+ errored: false
1164
+ };
1165
+ let stop2 = () => {
1166
+ };
1167
+ const result2 = {
1168
+ get values() {
1169
+ return acc.values;
1170
+ },
1171
+ get dirtyCount() {
1172
+ return acc.dirtyCount;
1173
+ },
1174
+ get resolvedCount() {
1175
+ return acc.resolvedCount;
1176
+ },
1177
+ get events() {
1178
+ return acc.events;
1179
+ },
1180
+ get completedCleanly() {
1181
+ return acc.completedCleanly;
1182
+ },
1183
+ get errored() {
1184
+ return acc.errored;
1185
+ },
1186
+ dispose() {
1187
+ stop2();
1188
+ }
1189
+ };
1190
+ const pushEvent = (path, message) => {
1191
+ const t = message[0];
1192
+ const base = timeline ? { timestamp_ns: monotonicNs(), in_batch: isBatching() } : {};
1193
+ let event;
1194
+ if (t === DATA) {
1195
+ if (path != null) acc.values[path] = message[1];
1196
+ event = { type: "data", ...path != null ? { path } : {}, data: message[1], ...base };
1197
+ } else if (t === DIRTY) {
1198
+ acc.dirtyCount += 1;
1199
+ event = { type: "dirty", ...path != null ? { path } : {}, ...base };
1200
+ } else if (t === RESOLVED) {
1201
+ acc.resolvedCount += 1;
1202
+ event = { type: "resolved", ...path != null ? { path } : {}, ...base };
1203
+ } else if (t === COMPLETE) {
1204
+ if (!acc.errored) acc.completedCleanly = true;
1205
+ event = { type: "complete", ...path != null ? { path } : {}, ...base };
1206
+ } else if (t === ERROR) {
1207
+ acc.errored = true;
1208
+ event = {
1209
+ type: "error",
1210
+ ...path != null ? { path } : {},
1211
+ data: message[1],
1212
+ ...base
1213
+ };
1214
+ }
1215
+ if (!event) return;
1216
+ acc.events.push(event);
1217
+ if (!shouldLog(event.type)) return;
1218
+ logger(renderEvent(event), event);
1219
+ };
1220
+ if (options.path != null) {
1221
+ const stream2 = this.observe(options.path, {
1222
+ actor: options.actor,
1223
+ structured: false
1224
+ });
1225
+ stop2 = stream2.subscribe((messages) => {
1226
+ for (const m of messages) {
1227
+ pushEvent(options.path, m);
1228
+ }
1229
+ });
1230
+ } else {
1231
+ const stream2 = this.observe({ actor: options.actor, structured: false });
1232
+ stop2 = stream2.subscribe((path, messages) => {
1233
+ for (const m of messages) {
1234
+ pushEvent(path, m);
1235
+ }
1236
+ });
1237
+ }
1238
+ return {
1239
+ result: result2,
1240
+ dispose() {
1241
+ result2.dispose();
1242
+ }
1243
+ };
1244
+ }
1245
+ const structuredObserveOptions = {
1246
+ actor: options.actor,
1247
+ structured: true,
1248
+ ...options.timeline !== false ? { timeline: true } : {},
1249
+ ...options.causal ? { causal: true } : {},
1250
+ ...options.derived ? { derived: true } : {}
1251
+ };
1252
+ const result = options.path != null ? this.observe(options.path, structuredObserveOptions) : this.observe(structuredObserveOptions);
1253
+ let cursor = 0;
1254
+ const flushNewEvents = () => {
1255
+ const nextEvents = result.events.slice(cursor);
1256
+ cursor = result.events.length;
1257
+ for (const event of nextEvents) {
1258
+ if (!shouldLog(event.type)) continue;
1259
+ logger(renderEvent(event), event);
1260
+ }
1261
+ };
1262
+ const stream = options.path != null ? this.observe(options.path, { actor: options.actor, structured: false }) : this.observe({ actor: options.actor, structured: false });
1263
+ const stop = options.path != null ? stream.subscribe((messages) => {
1264
+ if (messages.length > 0) {
1265
+ flushNewEvents();
1266
+ }
1267
+ }) : stream.subscribe((_path, messages) => {
1268
+ if (messages.length > 0) {
1269
+ flushNewEvents();
1270
+ }
1271
+ });
1272
+ return {
1273
+ result,
1274
+ dispose() {
1275
+ stop();
1276
+ flushNewEvents();
1277
+ result.dispose();
1278
+ }
1279
+ };
1280
+ }
1281
+ /**
1282
+ * CLI/debug-friendly graph dump built on {@link Graph.describe}.
1283
+ *
1284
+ * @param options - Optional actor/filter/format toggles.
1285
+ * @returns Rendered graph text.
1286
+ */
1287
+ dumpGraph(options = {}) {
1288
+ const described = this.describe({
1289
+ actor: options.actor,
1290
+ filter: options.filter
1291
+ });
1292
+ const includeEdges = options.includeEdges ?? true;
1293
+ const includeSubgraphs = options.includeSubgraphs ?? true;
1294
+ if (options.format === "json") {
1295
+ const payload = {
1296
+ name: described.name,
1297
+ nodes: described.nodes,
1298
+ edges: includeEdges ? described.edges : [],
1299
+ subgraphs: includeSubgraphs ? described.subgraphs : []
1300
+ };
1301
+ const text2 = JSON.stringify(sortJsonValue(payload), null, options.indent ?? 2);
1302
+ options.logger?.(text2);
1303
+ return text2;
1304
+ }
1305
+ const lines = [];
1306
+ lines.push(`Graph ${described.name}`);
1307
+ lines.push("Nodes:");
1308
+ for (const path of Object.keys(described.nodes).sort()) {
1309
+ const n = described.nodes[path];
1310
+ lines.push(`- ${path} (${n.type}/${n.status}): ${describeData(n.value)}`);
1311
+ }
1312
+ if (includeEdges) {
1313
+ lines.push("Edges:");
1314
+ for (const edge of described.edges) {
1315
+ lines.push(`- ${edge.from} -> ${edge.to}`);
1316
+ }
1317
+ }
1318
+ if (includeSubgraphs) {
1319
+ lines.push("Subgraphs:");
1320
+ for (const sg of described.subgraphs) {
1321
+ lines.push(`- ${sg}`);
1322
+ }
1323
+ }
1324
+ const text = lines.join("\n");
1325
+ options.logger?.(text);
1326
+ return text;
1327
+ }
1328
+ // ——————————————————————————————————————————————————————————————
1329
+ // Lifecycle & persistence (§3.7–§3.8)
1330
+ // ——————————————————————————————————————————————————————————————
1331
+ /**
1332
+ * Sends `[[TEARDOWN]]` to all nodes, then clears registries on this graph and every
1333
+ * mounted subgraph (§3.7). The instance is left empty and may be reused with {@link Graph.add}.
1334
+ */
1335
+ destroy() {
1336
+ this.signal([[TEARDOWN]], { internal: true });
1337
+ for (const dispose of [...this._autoCheckpointDisposers]) {
1338
+ try {
1339
+ dispose();
1340
+ } catch {
1341
+ }
1342
+ }
1343
+ this._autoCheckpointDisposers.clear();
1344
+ for (const child of [...this._mounts.values()]) {
1345
+ child._destroyClearOnly();
1346
+ }
1347
+ this._mounts.clear();
1348
+ this._nodes.clear();
1349
+ this._edges.clear();
1350
+ }
1351
+ /** Clear structure after parent already signaled TEARDOWN through this subtree. */
1352
+ _destroyClearOnly() {
1353
+ for (const child of [...this._mounts.values()]) {
1354
+ child._destroyClearOnly();
1355
+ }
1356
+ this._mounts.clear();
1357
+ this._nodes.clear();
1358
+ this._edges.clear();
1359
+ }
1360
+ /**
1361
+ * Serializes structure and current values to JSON-shaped data (§3.8). Same information
1362
+ * as {@link Graph.describe} plus a `version` field for format evolution.
1363
+ *
1364
+ * @returns Persistable snapshot with sorted keys.
1365
+ */
1366
+ snapshot() {
1367
+ const d = this.describe();
1368
+ const sortedNodes = {};
1369
+ for (const key of Object.keys(d.nodes).sort()) {
1370
+ sortedNodes[key] = d.nodes[key];
1371
+ }
1372
+ const sortedSubgraphs = [...d.subgraphs].sort();
1373
+ return { ...d, version: 1, nodes: sortedNodes, subgraphs: sortedSubgraphs };
1374
+ }
1375
+ /**
1376
+ * Apply persisted values onto an existing graph whose topology matches the snapshot
1377
+ * (§3.8). Only {@link DescribeNodeOutput.type} `state` and `producer` entries with a
1378
+ * `value` field are written; `derived` / `operator` / `effect` are skipped so deps
1379
+ * drive recomputation. Unknown paths are ignored.
1380
+ *
1381
+ * @param data - Snapshot envelope with matching `name` and node slices.
1382
+ * @throws If `data.name` does not equal {@link Graph.name}.
1383
+ */
1384
+ restore(data, options) {
1385
+ parseSnapshotEnvelope(data);
1386
+ if (data.name !== this.name) {
1387
+ throw new Error(
1388
+ `Graph "${this.name}": restore snapshot name "${data.name}" does not match this graph`
1389
+ );
1390
+ }
1391
+ const onlyPatterns = options?.only == null ? null : (Array.isArray(options.only) ? options.only : [options.only]).map((p) => globToRegex(p));
1392
+ for (const path of Object.keys(data.nodes).sort()) {
1393
+ if (onlyPatterns !== null && !onlyPatterns.some((re) => re.test(path))) continue;
1394
+ const slice = data.nodes[path];
1395
+ if (slice === void 0 || slice.value === void 0) continue;
1396
+ if (slice.type === "derived" || slice.type === "operator" || slice.type === "effect") {
1397
+ continue;
1398
+ }
1399
+ try {
1400
+ this.set(path, slice.value);
1401
+ } catch {
1402
+ }
1403
+ }
1404
+ }
1405
+ /**
1406
+ * Creates a graph named from the snapshot, optionally runs `build` to register nodes
1407
+ * and mounts, then {@link Graph.restore} values (§3.8).
1408
+ *
1409
+ * @param data - Snapshot envelope (`version` checked).
1410
+ * @param build - Optional callback to construct topology before values are applied.
1411
+ * @returns Hydrated `Graph` instance.
1412
+ */
1413
+ static fromSnapshot(data, build) {
1414
+ parseSnapshotEnvelope(data);
1415
+ const g = new _Graph(data.name);
1416
+ if (build) {
1417
+ build(g);
1418
+ g.restore(data);
1419
+ return g;
1420
+ }
1421
+ for (const mount of [...data.subgraphs].sort((a, b) => {
1422
+ const da = a.split(PATH_SEP).length;
1423
+ const db = b.split(PATH_SEP).length;
1424
+ if (da !== db) return da - db;
1425
+ if (a < b) return -1;
1426
+ if (a > b) return 1;
1427
+ return 0;
1428
+ })) {
1429
+ const parts = mount.split(PATH_SEP);
1430
+ let target = g;
1431
+ for (const seg of parts) {
1432
+ if (!target._mounts.has(seg)) {
1433
+ target.mount(seg, new _Graph(seg));
1434
+ }
1435
+ target = target._mounts.get(seg);
1436
+ }
1437
+ }
1438
+ const primaryEntries = Object.entries(data.nodes).filter(([path]) => !path.includes(`${PATH_SEP}${GRAPH_META_SEGMENT}${PATH_SEP}`)).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
1439
+ const pending = new Map(primaryEntries);
1440
+ const created = /* @__PURE__ */ new Map();
1441
+ let progressed = true;
1442
+ while (pending.size > 0 && progressed) {
1443
+ progressed = false;
1444
+ for (const [path, slice] of [...pending.entries()]) {
1445
+ const deps = slice?.deps ?? [];
1446
+ if (!deps.every((dep) => created.has(dep))) continue;
1447
+ const [owner, localName] = _Graph._ownerForPath(g, path);
1448
+ const meta = { ...slice?.meta ?? {} };
1449
+ const factory = _Graph._factoryForPath(path);
1450
+ let node;
1451
+ if (slice?.type === "state") {
1452
+ node = state(slice.value, { meta });
1453
+ } else {
1454
+ if (factory == null) continue;
1455
+ node = factory(localName, {
1456
+ path,
1457
+ type: slice.type,
1458
+ value: slice.value,
1459
+ meta,
1460
+ deps,
1461
+ resolvedDeps: deps.map((dep) => created.get(dep))
1462
+ });
1463
+ }
1464
+ owner.add(localName, node);
1465
+ created.set(path, node);
1466
+ pending.delete(path);
1467
+ progressed = true;
1468
+ }
1469
+ }
1470
+ if (pending.size > 0) {
1471
+ const unresolved = [...pending.keys()].sort().join(", ");
1472
+ throw new Error(
1473
+ `Graph.fromSnapshot could not reconstruct nodes without build callback: ${unresolved}. Register matching factories with Graph.registerFactory(pattern, factory).`
1474
+ );
1475
+ }
1476
+ for (const edge of data.edges) {
1477
+ try {
1478
+ g.connect(edge.from, edge.to);
1479
+ } catch {
1480
+ }
1481
+ }
1482
+ g.restore(data);
1483
+ return g;
1484
+ }
1485
+ /**
1486
+ * Plain snapshot with **recursively sorted object keys** for deterministic serialization (§3.8).
1487
+ *
1488
+ * @remarks
1489
+ * ECMAScript `JSON.stringify(graph)` invokes this method; it must return a plain object, not an
1490
+ * already-stringified JSON string (otherwise the graph would be double-encoded).
1491
+ * For a single UTF-8 string with a trailing newline (convenient for git), use {@link Graph.toJSONString}.
1492
+ *
1493
+ * @returns Same object as {@link Graph.snapshot}.
1494
+ */
1495
+ toJSON() {
1496
+ return this.snapshot();
1497
+ }
1498
+ /**
1499
+ * Deterministic JSON **text**: `JSON.stringify` of {@link Graph.toJSON} plus a trailing newline (§3.8).
1500
+ *
1501
+ * @returns Stable string suitable for diffs.
1502
+ */
1503
+ toJSONString() {
1504
+ return stableJsonStringify(this.snapshot());
1505
+ }
1506
+ /**
1507
+ * Debounced persistence wired to graph-wide observe stream (spec §3.8 auto-checkpoint).
1508
+ *
1509
+ * Checkpoint trigger uses {@link messageTier}: only batches containing tier >= 2 messages
1510
+ * schedule a save (`DATA`/`RESOLVED`/terminal/destruction), never pure tier-0/1 control waves.
1511
+ */
1512
+ autoCheckpoint(adapter, options = {}) {
1513
+ const debounceMs = Math.max(0, options.debounceMs ?? 500);
1514
+ const compactEvery = Math.max(1, options.compactEvery ?? 10);
1515
+ let timer;
1516
+ let seq = 0;
1517
+ let pending = false;
1518
+ let lastDescribe;
1519
+ const flush = () => {
1520
+ timer = void 0;
1521
+ if (!pending) return;
1522
+ pending = false;
1523
+ try {
1524
+ const described = this.describe();
1525
+ const snapshot = { ...described, version: SNAPSHOT_VERSION };
1526
+ seq += 1;
1527
+ const shouldCompact = lastDescribe == null || seq % compactEvery === 0;
1528
+ if (shouldCompact) {
1529
+ adapter.save({ mode: "full", snapshot, seq });
1530
+ } else {
1531
+ const previous = lastDescribe;
1532
+ if (previous == null) return;
1533
+ adapter.save({
1534
+ mode: "diff",
1535
+ diff: _Graph.diff(previous, described),
1536
+ snapshot,
1537
+ seq
1538
+ });
1539
+ }
1540
+ lastDescribe = described;
1541
+ } catch (error) {
1542
+ options.onError?.(error);
1543
+ }
1544
+ };
1545
+ const schedule = () => {
1546
+ pending = true;
1547
+ if (timer !== void 0) clearTimeout(timer);
1548
+ timer = setTimeout(flush, debounceMs);
1549
+ };
1550
+ const off = this.observe().subscribe((path, messages) => {
1551
+ const triggeredByTier = messages.some((m) => messageTier(m[0]) >= 2);
1552
+ if (!triggeredByTier) return;
1553
+ if (options.filter) {
1554
+ const described = this.describe().nodes[path];
1555
+ if (described == null || !options.filter(path, described)) return;
1556
+ }
1557
+ schedule();
1558
+ });
1559
+ const dispose = () => {
1560
+ off();
1561
+ if (timer !== void 0) {
1562
+ clearTimeout(timer);
1563
+ timer = void 0;
1564
+ }
1565
+ this._autoCheckpointDisposers.delete(dispose);
1566
+ };
1567
+ this._autoCheckpointDisposers.add(dispose);
1568
+ return { dispose };
1569
+ }
1570
+ /**
1571
+ * Export the current graph topology as Mermaid flowchart text.
1572
+ *
1573
+ * Renders qualified node paths and registered edges from {@link Graph.describe}.
1574
+ *
1575
+ * @param options - Optional diagram direction (`LR` by default).
1576
+ * @returns Mermaid flowchart source.
1577
+ */
1578
+ toMermaid(options) {
1579
+ const direction = normalizeDiagramDirection(options?.direction);
1580
+ const described = this.describe();
1581
+ const paths = Object.keys(described.nodes).sort();
1582
+ const ids = /* @__PURE__ */ new Map();
1583
+ for (let i = 0; i < paths.length; i += 1) {
1584
+ ids.set(paths[i], `n${i}`);
1585
+ }
1586
+ const lines = [`flowchart ${direction}`];
1587
+ for (const path of paths) {
1588
+ const id = ids.get(path);
1589
+ lines.push(` ${id}["${escapeMermaidLabel(path)}"]`);
1590
+ }
1591
+ for (const [from, to] of collectDiagramArrows(described)) {
1592
+ const fromId = ids.get(from);
1593
+ const toId = ids.get(to);
1594
+ if (!fromId || !toId) continue;
1595
+ lines.push(` ${fromId} --> ${toId}`);
1596
+ }
1597
+ return lines.join("\n");
1598
+ }
1599
+ /**
1600
+ * Export the current graph topology as D2 diagram text.
1601
+ *
1602
+ * Renders qualified node paths, constructor deps, and registered edges from {@link Graph.describe}.
1603
+ *
1604
+ * @param options - Optional diagram direction (`LR` by default).
1605
+ * @returns D2 source text.
1606
+ */
1607
+ toD2(options) {
1608
+ const direction = normalizeDiagramDirection(options?.direction);
1609
+ const described = this.describe();
1610
+ const paths = Object.keys(described.nodes).sort();
1611
+ const ids = /* @__PURE__ */ new Map();
1612
+ for (let i = 0; i < paths.length; i += 1) {
1613
+ ids.set(paths[i], `n${i}`);
1614
+ }
1615
+ const lines = [`direction: ${d2DirectionFromGraphDirection(direction)}`];
1616
+ for (const path of paths) {
1617
+ const id = ids.get(path);
1618
+ lines.push(`${id}: "${escapeD2Label(path)}"`);
1619
+ }
1620
+ for (const [from, to] of collectDiagramArrows(described)) {
1621
+ const fromId = ids.get(from);
1622
+ const toId = ids.get(to);
1623
+ if (!fromId || !toId) continue;
1624
+ lines.push(`${fromId} -> ${toId}`);
1625
+ }
1626
+ return lines.join("\n");
1627
+ }
1628
+ // ——————————————————————————————————————————————————————————————
1629
+ // Inspector (roadmap 3.3) — reasoning trace, overhead gating
1630
+ // ——————————————————————————————————————————————————————————————
1631
+ /**
1632
+ * When `false`, structured observation options (`causal`, `timeline`),
1633
+ * `annotate()`, and `traceLog()` are no-ops. Raw `observe()` always works.
1634
+ *
1635
+ * Default: `true` outside production (`process.env.NODE_ENV !== "production"`).
1636
+ */
1637
+ static inspectorEnabled = !(typeof process !== "undefined" && process.env?.NODE_ENV === "production");
1638
+ _annotations = /* @__PURE__ */ new Map();
1639
+ _traceRing = new RingBuffer(1e3);
1640
+ /**
1641
+ * Attaches a reasoning annotation to a node — captures *why* an AI agent set a value.
1642
+ *
1643
+ * No-op when {@link Graph.inspectorEnabled} is `false`.
1644
+ *
1645
+ * @param path - Qualified node path.
1646
+ * @param reason - Free-text note stored in the trace ring buffer.
1647
+ */
1648
+ annotate(path, reason) {
1649
+ if (!_Graph.inspectorEnabled) return;
1650
+ this.resolve(path);
1651
+ this._annotations.set(path, reason);
1652
+ this._traceRing.push({ path, reason, timestamp_ns: monotonicNs() });
1653
+ }
1654
+ /**
1655
+ * Returns a chronological log of all reasoning annotations (ring buffer).
1656
+ *
1657
+ * @returns `[]` when {@link Graph.inspectorEnabled} is `false`.
1658
+ */
1659
+ traceLog() {
1660
+ if (!_Graph.inspectorEnabled) return [];
1661
+ return this._traceRing.toArray();
1662
+ }
1663
+ /**
1664
+ * Computes structural + value diff between two {@link Graph.describe} snapshots.
1665
+ *
1666
+ * @param a - Earlier describe output.
1667
+ * @param b - Later describe output.
1668
+ * @returns Added/removed nodes, changed fields, and edge deltas.
1669
+ */
1670
+ static diff(a, b) {
1671
+ const aKeys = new Set(Object.keys(a.nodes));
1672
+ const bKeys = new Set(Object.keys(b.nodes));
1673
+ const nodesAdded = [...bKeys].filter((k) => !aKeys.has(k)).sort();
1674
+ const nodesRemoved = [...aKeys].filter((k) => !bKeys.has(k)).sort();
1675
+ const nodesChanged = [];
1676
+ for (const key of aKeys) {
1677
+ if (!bKeys.has(key)) continue;
1678
+ const na = a.nodes[key];
1679
+ const nb = b.nodes[key];
1680
+ const av = na.v;
1681
+ const bv = nb.v;
1682
+ if (av != null && bv != null && av.id === bv.id && av.version === bv.version) {
1683
+ for (const field of ["type", "status"]) {
1684
+ const va = na[field];
1685
+ const vb = nb[field];
1686
+ if (va !== vb) {
1687
+ nodesChanged.push({ path: key, field, from: va, to: vb });
1688
+ }
1689
+ }
1690
+ continue;
1691
+ }
1692
+ for (const field of ["type", "status", "value"]) {
1693
+ const va = na[field];
1694
+ const vb = nb[field];
1695
+ if (!Object.is(va, vb) && JSON.stringify(va) !== JSON.stringify(vb)) {
1696
+ nodesChanged.push({ path: key, field, from: va, to: vb });
1697
+ }
1698
+ }
1699
+ }
1700
+ const edgeKey2 = (e) => `${e.from} ${e.to}`;
1701
+ const aEdges = new Set(a.edges.map(edgeKey2));
1702
+ const bEdges = new Set(b.edges.map(edgeKey2));
1703
+ const edgesAdded = b.edges.filter((e) => !aEdges.has(edgeKey2(e)));
1704
+ const edgesRemoved = a.edges.filter((e) => !bEdges.has(edgeKey2(e)));
1705
+ const aSubgraphs = new Set(a.subgraphs);
1706
+ const bSubgraphs = new Set(b.subgraphs);
1707
+ const subgraphsAdded = [...bSubgraphs].filter((s) => !aSubgraphs.has(s)).sort();
1708
+ const subgraphsRemoved = [...aSubgraphs].filter((s) => !bSubgraphs.has(s)).sort();
1709
+ return {
1710
+ nodesAdded,
1711
+ nodesRemoved,
1712
+ nodesChanged,
1713
+ edgesAdded,
1714
+ edgesRemoved,
1715
+ subgraphsAdded,
1716
+ subgraphsRemoved
1717
+ };
1718
+ }
1719
+ };
1720
+ function reachable(described, from, direction, options = {}) {
1721
+ if (!from) return [];
1722
+ if (direction !== "upstream" && direction !== "downstream") {
1723
+ throw new Error(`reachable: direction must be "upstream" or "downstream"`);
1724
+ }
1725
+ const maxDepth = options.maxDepth;
1726
+ if (maxDepth != null && (!Number.isInteger(maxDepth) || maxDepth < 0)) {
1727
+ throw new Error(`reachable: maxDepth must be an integer >= 0`);
1728
+ }
1729
+ if (maxDepth === 0) return [];
1730
+ const depsByPath = /* @__PURE__ */ new Map();
1731
+ const reverseDeps = /* @__PURE__ */ new Map();
1732
+ const incomingEdges = /* @__PURE__ */ new Map();
1733
+ const outgoingEdges = /* @__PURE__ */ new Map();
1734
+ const universe = /* @__PURE__ */ new Set();
1735
+ const nodesRaw = described != null && typeof described === "object" && "nodes" in described && typeof described.nodes === "object" && described.nodes !== null && !Array.isArray(described.nodes) ? described.nodes : {};
1736
+ const edgesRaw = described != null && typeof described === "object" && "edges" in described && Array.isArray(described.edges) ? described.edges : [];
1737
+ for (const [path, node] of Object.entries(nodesRaw)) {
1738
+ if (!path) continue;
1739
+ universe.add(path);
1740
+ const deps = node != null && typeof node === "object" && Array.isArray(node.deps) ? node.deps : [];
1741
+ const cleanDeps = deps.filter((d) => typeof d === "string" && d.length > 0);
1742
+ depsByPath.set(path, cleanDeps);
1743
+ for (const dep of cleanDeps) {
1744
+ universe.add(dep);
1745
+ if (!reverseDeps.has(dep)) reverseDeps.set(dep, /* @__PURE__ */ new Set());
1746
+ reverseDeps.get(dep).add(path);
1747
+ }
1748
+ }
1749
+ for (const edge of edgesRaw) {
1750
+ if (edge == null || typeof edge !== "object") continue;
1751
+ const edgeFrom = "from" in edge && typeof edge.from === "string" ? edge.from : "";
1752
+ const edgeTo = "to" in edge && typeof edge.to === "string" ? edge.to : "";
1753
+ if (!edgeFrom || !edgeTo) continue;
1754
+ universe.add(edgeFrom);
1755
+ universe.add(edgeTo);
1756
+ if (!outgoingEdges.has(edgeFrom)) outgoingEdges.set(edgeFrom, /* @__PURE__ */ new Set());
1757
+ outgoingEdges.get(edgeFrom).add(edgeTo);
1758
+ if (!incomingEdges.has(edgeTo)) incomingEdges.set(edgeTo, /* @__PURE__ */ new Set());
1759
+ incomingEdges.get(edgeTo).add(edgeFrom);
1760
+ }
1761
+ if (!universe.has(from)) return [];
1762
+ const neighbors = (path) => {
1763
+ if (direction === "upstream") {
1764
+ const depNeighbors2 = depsByPath.get(path) ?? [];
1765
+ const edgeNeighbors2 = [...incomingEdges.get(path) ?? []];
1766
+ return [...depNeighbors2, ...edgeNeighbors2];
1767
+ }
1768
+ const depNeighbors = [...reverseDeps.get(path) ?? []];
1769
+ const edgeNeighbors = [...outgoingEdges.get(path) ?? []];
1770
+ return [...depNeighbors, ...edgeNeighbors];
1771
+ };
1772
+ const visited = /* @__PURE__ */ new Set([from]);
1773
+ const out = /* @__PURE__ */ new Set();
1774
+ const queue = [{ path: from, depth: 0 }];
1775
+ while (queue.length > 0) {
1776
+ const next = queue.shift();
1777
+ if (maxDepth != null && next.depth >= maxDepth) continue;
1778
+ for (const nb of neighbors(next.path)) {
1779
+ if (!nb || visited.has(nb)) continue;
1780
+ visited.add(nb);
1781
+ out.add(nb);
1782
+ queue.push({ path: nb, depth: next.depth + 1 });
1783
+ }
1784
+ }
1785
+ return [...out].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
1786
+ }
1787
+
1788
+ export {
1789
+ GRAPH_META_SEGMENT,
1790
+ Graph,
1791
+ reachable
1792
+ };
1793
+ //# sourceMappingURL=chunk-6W5SGIGB.js.map