@ersbeth/picoflow 2.3.2 → 3.0.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 (70) hide show
  1. package/README.md +34 -5
  2. package/SKILL.md +130 -16
  3. package/dist/index.d.ts +767 -0
  4. package/dist/index.js +544 -0
  5. package/dist/inspect.d.ts +252 -0
  6. package/dist/inspect.js +300 -0
  7. package/dist/shared.d.ts +361 -0
  8. package/dist/shared.js +1496 -0
  9. package/dist/shared2.d.ts +93 -0
  10. package/dist/shared2.js +59 -0
  11. package/dist/shared3.d.ts +54 -0
  12. package/dist/shared3.js +17 -0
  13. package/dist/{types/converters/solid.d.ts → solid.d.ts} +10 -8
  14. package/dist/solid.js +63 -0
  15. package/package.json +37 -25
  16. package/dist/picoflow.js +0 -1283
  17. package/dist/types/converters/index.d.ts +0 -1
  18. package/dist/types/core/api/base/flowConfig.d.ts +0 -17
  19. package/dist/types/core/api/base/flowDisposable.d.ts +0 -40
  20. package/dist/types/core/api/base/flowErrors.d.ts +0 -1
  21. package/dist/types/core/api/base/flowObservable.d.ts +0 -26
  22. package/dist/types/core/api/base/flowPrimitiveOptions.d.ts +0 -14
  23. package/dist/types/core/api/base/flowSubscribable.d.ts +0 -78
  24. package/dist/types/core/api/base/flowTracker.d.ts +0 -7
  25. package/dist/types/core/api/base/index.d.ts +0 -7
  26. package/dist/types/core/api/index.d.ts +0 -2
  27. package/dist/types/core/api/nodes/async/flowConstantAsync.d.ts +0 -32
  28. package/dist/types/core/api/nodes/async/flowDerivationAsync.d.ts +0 -37
  29. package/dist/types/core/api/nodes/async/flowStateAsync.d.ts +0 -42
  30. package/dist/types/core/api/nodes/async/flowWritableDerivationAsync.d.ts +0 -31
  31. package/dist/types/core/api/nodes/async/index.d.ts +0 -4
  32. package/dist/types/core/api/nodes/collections/flowArray.d.ts +0 -138
  33. package/dist/types/core/api/nodes/collections/flowMap.d.ts +0 -100
  34. package/dist/types/core/api/nodes/collections/index.d.ts +0 -2
  35. package/dist/types/core/api/nodes/flowEffect.d.ts +0 -28
  36. package/dist/types/core/api/nodes/flowSignal.d.ts +0 -26
  37. package/dist/types/core/api/nodes/flowValue.d.ts +0 -35
  38. package/dist/types/core/api/nodes/index.d.ts +0 -7
  39. package/dist/types/core/api/nodes/sync/flowConstant.d.ts +0 -30
  40. package/dist/types/core/api/nodes/sync/flowDerivation.d.ts +0 -37
  41. package/dist/types/core/api/nodes/sync/flowState.d.ts +0 -40
  42. package/dist/types/core/api/nodes/sync/flowWritableDerivation.d.ts +0 -29
  43. package/dist/types/core/api/nodes/sync/index.d.ts +0 -4
  44. package/dist/types/core/api/nodes/utils.d.ts +0 -22
  45. package/dist/types/core/base/dependenciesRegistry.d.ts +0 -1
  46. package/dist/types/core/base/dependentsRegistry.d.ts +0 -1
  47. package/dist/types/core/base/disposable.d.ts +0 -15
  48. package/dist/types/core/base/errors.d.ts +0 -31
  49. package/dist/types/core/base/executionStack.d.ts +0 -31
  50. package/dist/types/core/base/index.d.ts +0 -8
  51. package/dist/types/core/base/node.d.ts +0 -27
  52. package/dist/types/core/base/observable.d.ts +0 -34
  53. package/dist/types/core/base/observer.d.ts +0 -33
  54. package/dist/types/core/nodes/actionNode.d.ts +0 -1
  55. package/dist/types/core/nodes/arrayNode.d.ts +0 -1
  56. package/dist/types/core/nodes/effectNode.d.ts +0 -1
  57. package/dist/types/core/nodes/index.d.ts +0 -8
  58. package/dist/types/core/nodes/mapNode.d.ts +0 -1
  59. package/dist/types/core/nodes/signalNode.d.ts +0 -1
  60. package/dist/types/core/nodes/valueAsyncNode.d.ts +0 -1
  61. package/dist/types/core/nodes/valueNode.d.ts +0 -1
  62. package/dist/types/core/nodes/valueSyncNode.d.ts +0 -1
  63. package/dist/types/core/schedulers/asyncResolver.d.ts +0 -1
  64. package/dist/types/core/schedulers/asyncScheduler.d.ts +0 -1
  65. package/dist/types/core/schedulers/index.d.ts +0 -4
  66. package/dist/types/core/schedulers/pendingError.d.ts +0 -1
  67. package/dist/types/core/schedulers/scheduler.d.ts +0 -1
  68. package/dist/types/core/schedulers/syncResolver.d.ts +0 -1
  69. package/dist/types/core/schedulers/syncScheduler.d.ts +0 -1
  70. package/dist/types/index.d.ts +0 -2
@@ -0,0 +1,252 @@
1
+ /// <reference lib="esnext.disposable" preserve="true" />
2
+ import { d as FlowEffect, h as ObserverStatus, m as ObservableStatus, n as FlowValue } from "./shared.js";
3
+ import { t as FlowSignal } from "./shared2.js";
4
+ //#region src/core/base/disposable.d.ts
5
+ /**
6
+ * Which primitive a node came from. Declared with the engine that carries it, and surfaced to
7
+ * consumers by the inspection layer (#50, A2-DX-2).
8
+ *
9
+ * `unknown` is what a core built on its own reports — the kind is a property of the primitive a
10
+ * facade makes of it, not of the engine.
11
+ *
12
+ * @public
13
+ */
14
+ type FlowNodeKind = "constant" | "constantAsync" | "derivation" | "derivationAsync" | "state" | "stateAsync" | "writableDerivation" | "writableDerivationAsync" | "array" | "map" | "action" | "signal" | "effect" | "unknown";
15
+ //#endregion
16
+ //#region src/core/base/traceHub.d.ts
17
+ /**
18
+ * The moments in a node's life a trace can record (#50, A2-DX-2).
19
+ *
20
+ * Between them they spell out one propagation: a write notifies its dependents, each is queued for
21
+ * the coming flush, the flush executes them, each settles, and the effects run last.
22
+ *
23
+ * Declared here, where the engine emits them, and re-exported by the inspection layer rather than
24
+ * aliased there: an alias would leave the name TypeDoc documents pointing at a declaration outside
25
+ * every entry point, and an internal-tagged one would be stripped from the published declarations
26
+ * while the public alias still referenced it. The same shape as `ObserverStatus`, declared in
27
+ * `observer.ts` and re-exported by the API layer, prefix included since this is a public name.
28
+ *
29
+ * @public
30
+ */
31
+ type FlowTraceEventType = "notify" | "queue-pending" | "queue-effect" | "execute" | "resolve" | "reject" | "effect-run" | "cycle-error";
32
+ //#endregion
33
+ //#region src/core/inspect/snapshot.d.ts
34
+ /**
35
+ * How a node is named in a dump, and everything needed to recognise it — the shape used both for a
36
+ * node itself and for each of its neighbours.
37
+ *
38
+ * @public
39
+ */
40
+ interface FlowNodeRef {
41
+ /**
42
+ * Identity of this node, assigned at creation and never reused. Two dumps taken at different
43
+ * moments describe the same node under the same `id`, which is what lets a diff of them mean
44
+ * something.
45
+ */
46
+ id: number;
47
+ /** Which primitive this is — the factory it came from, or `"effect"` for a subscription. */
48
+ kind: FlowNodeKind;
49
+ /** The `name` option given at creation, absent when none was. */
50
+ name?: string;
51
+ /** How to display this node: its `name` when it has one, `kind#id` otherwise. */
52
+ label: string;
53
+ /** The node's current status, always reported — including `"disposed"`. */
54
+ status: ObservableStatus | ObserverStatus;
55
+ }
56
+ /**
57
+ * A node and its edges at one instant.
58
+ *
59
+ * Edges are a snapshot, not a fixture: a computation rebuilds its dependency set from scratch on
60
+ * every run, so this describes the graph as of each node's last computation.
61
+ *
62
+ * @public
63
+ */
64
+ interface FlowNodeSnapshot extends FlowNodeRef {
65
+ /** The sources this node currently reads. */
66
+ dependencies: FlowNodeRef[];
67
+ /** The observers currently registered on this node. */
68
+ dependents: FlowNodeRef[];
69
+ /**
70
+ * The node's value, rendered as a short string — present only when `status` is `"resolved"`,
71
+ * mirroring the discrimination of the public `value` snapshot. A pending, dirty or disposed node
72
+ * has no value worth reporting, and a stale cached one would be actively misleading.
73
+ */
74
+ value?: string;
75
+ /** The error's message — present only when `status` is `"error"`. */
76
+ error?: string;
77
+ /** Present and `true` only on a derivation created with `eager: true`. */
78
+ eager?: boolean;
79
+ }
80
+ /**
81
+ * Describes one primitive and its immediate neighbours, without touching the reactive system (#50,
82
+ * A2-DX-2).
83
+ *
84
+ * Strictly passive: it reads `status` and the passive value snapshot, and iterates the dependency
85
+ * registries. It never calls `get()`, `pick()`, `watch()` or `subscribe()`, so it computes nothing,
86
+ * registers no dependency and moves no node — inspecting a graph cannot change what that graph then
87
+ * does. Inspecting a disposed primitive is fine too, and reports `status: "disposed"`.
88
+ *
89
+ * One consequence is worth stating plainly: a lazy derivation that has never been read has never
90
+ * computed, so it has **no dependencies yet** and reports `status: "dirty"` with no value. That is
91
+ * the truth about the graph, not a gap in the report — exercise the application first, or create the
92
+ * derivations you mean to watch with `eager: true`.
93
+ *
94
+ * @param target - The primitive to describe: a value, a signal, or the effect `subscribe()` returned
95
+ * @returns A serialisable description of that node
96
+ *
97
+ * @public
98
+ */
99
+ export declare function inspect(target: FlowValue<unknown> | FlowEffect | FlowSignal): FlowNodeSnapshot;
100
+ //#endregion
101
+ //#region src/core/inspect/graph.d.ts
102
+ /**
103
+ * Options accepted by {@link graphSnapshot}.
104
+ *
105
+ * @public
106
+ */
107
+ export interface FlowGraphOptions {
108
+ /**
109
+ * Include nodes already disposed. Off by default: a disposed node is inert and its edges are
110
+ * gone, so it is noise in an ordinary dump — but seeing one still listed means something holds a
111
+ * reference to it, which is worth knowing when hunting a retained object.
112
+ */
113
+ includeDisposed?: boolean;
114
+ }
115
+ /**
116
+ * Counts that describe the graph at a glance.
117
+ *
118
+ * @public
119
+ */
120
+ export interface FlowGraphSummary {
121
+ /** How many nodes the snapshot covers — the length of {@link FlowGraphSnapshot.nodes}. */
122
+ total: number;
123
+ /** How many nodes of each {@link FlowNodeKind} — keys are absent rather than zero. */
124
+ byKind: Record<string, number>;
125
+ /** How many nodes in each status — keys are absent rather than zero. */
126
+ byStatus: Record<string, number>;
127
+ /**
128
+ * Nodes with no edge at all, in either direction.
129
+ *
130
+ * The number to watch: a primitive nothing reads and that reads nothing is either a lazy
131
+ * derivation not yet exercised, or a node that outlived its purpose and was never disposed. A
132
+ * count that grows as an application runs is the visible form of a leak.
133
+ */
134
+ orphans: number;
135
+ }
136
+ /**
137
+ * The whole graph at one instant.
138
+ *
139
+ * Every field is a plain number, string or array, so the snapshot survives `JSON.stringify` intact —
140
+ * it can be logged, sent to a bug report, or handed to a tool that compares two of them by node id.
141
+ *
142
+ * @public
143
+ */
144
+ export interface FlowGraphSnapshot {
145
+ /** Every node the snapshot covers, in creation order. */
146
+ nodes: FlowNodeSnapshot[];
147
+ /** Counts over {@link FlowGraphSnapshot.nodes}, for reading the shape without walking it. */
148
+ summary: FlowGraphSummary;
149
+ }
150
+ /**
151
+ * Describes every reactive node alive in this process, without touching the reactive system (#50,
152
+ * A2-DX-2).
153
+ *
154
+ * There is no way to name where to start, and that is the point: the graph has no roots, and the
155
+ * nodes worth finding — the ones leaked because nothing disposed them — are exactly the ones no
156
+ * handle points at any more. So the dump comes from a census kept as nodes are created, held weakly:
157
+ * a node the garbage collector has taken is gone from it, and one that lingers shows up as an orphan.
158
+ *
159
+ * Passivity is the same contract as {@link inspect}: statuses are read, registries iterated, nothing
160
+ * computed. In particular a lazy derivation never read has no dependencies yet and appears as an
161
+ * orphan — see {@link inspect} for what to do about it.
162
+ *
163
+ * @param options - See {@link FlowGraphOptions}
164
+ * @returns Every node, in creation order, with a {@link FlowGraphSummary}
165
+ *
166
+ * @public
167
+ */
168
+ export declare function graphSnapshot(options?: FlowGraphOptions): FlowGraphSnapshot;
169
+ //#endregion
170
+ //#region src/core/inspect/format.d.ts
171
+ /**
172
+ * Renders a graph as readable text (#50, A2-DX-2).
173
+ *
174
+ * The intended use is a developer with the browser console open, mid-session, asking what the graph
175
+ * looks like right now: `console.log(formatGraph())`. It returns a string rather than printing one,
176
+ * so the same call works in a terminal, in a test, or pasted into a bug report — and so the library
177
+ * never writes to anyone's console on its own.
178
+ *
179
+ * One block per node: a line naming it, its kind and its status, followed by its value when it has
180
+ * one (`= …`) or its error (`! …`), then an indented line of edges — `←` for what it reads, `→` for
181
+ * what reads it. Nodes with no edges at all are gathered at the end under `orphans`, where a leak
182
+ * shows up as a list that keeps growing.
183
+ *
184
+ * @param graph - A snapshot to render; taken with {@link graphSnapshot} when omitted
185
+ * @returns The rendered listing
186
+ *
187
+ * @public
188
+ */
189
+ export declare function formatGraph(graph?: FlowGraphSnapshot): string;
190
+ //#endregion
191
+ //#region src/core/inspect/trace.d.ts
192
+ /**
193
+ * One recorded moment: what happened, to which node, and where it falls in the order.
194
+ *
195
+ * @public
196
+ */
197
+ export interface FlowTraceEvent {
198
+ /** Position in the trace, counted from the moment tracing started. Never reused. */
199
+ seq: number;
200
+ /** What the engine did — see {@link FlowTraceEventType} for the vocabulary. */
201
+ type: FlowTraceEventType;
202
+ /** The node it happened to, described as of the moment it was recorded. */
203
+ node: FlowNodeRef;
204
+ }
205
+ /**
206
+ * A running trace. Stop it with `dispose()` — a trace left on keeps recording for the life of the
207
+ * process, which is fine while debugging and waste afterwards.
208
+ *
209
+ * @public
210
+ */
211
+ export interface FlowTrace {
212
+ /** What has been recorded, oldest first, up to the capacity given at creation. */
213
+ readonly events: readonly FlowTraceEvent[];
214
+ /** Forgets everything recorded so far, without stopping the trace. */
215
+ clear(): void;
216
+ /** Stops recording. */
217
+ dispose(): void;
218
+ }
219
+ /**
220
+ * Options accepted by {@link enableTrace}.
221
+ *
222
+ * @public
223
+ */
224
+ export interface FlowTraceOptions {
225
+ /**
226
+ * How many events to keep. Older ones are dropped as newer arrive, so memory is bounded however
227
+ * long the trace runs — and what remains is the end of the story, which is the part that
228
+ * explains the symptom. Defaults to 500.
229
+ */
230
+ capacity?: number;
231
+ }
232
+ /**
233
+ * Records what the engine does, so that a question about *time* can be answered (#50, A2-DX-2).
234
+ *
235
+ * `inspect()` and `graphSnapshot()` describe the graph's shape at an instant; some questions are not
236
+ * about shape. "I wrote to `$count` and my effect never ran" is answered by reading the sequence: a
237
+ * `notify` on `$count` with no `queue-effect` behind it means the edge was never there, while a
238
+ * `queue-effect` with no `effect-run` means the flush never reached it. A feedback loop reads as an
239
+ * alternation of `notify` and `effect-run` ending in `cycle-error`, which is how the nodes involved
240
+ * get named — the error alone names neither.
241
+ *
242
+ * Tracing is off until this is called, and costs one boolean check per engine event while it is.
243
+ * Recording does not change what the engine does: events are observed as they pass, never injected.
244
+ *
245
+ * @param options - See {@link FlowTraceOptions}
246
+ * @returns The running trace; call `dispose()` on it to stop
247
+ *
248
+ * @public
249
+ */
250
+ export declare function enableTrace(options?: FlowTraceOptions): FlowTrace;
251
+ //#endregion
252
+ export type { FlowNodeKind, FlowNodeRef, FlowNodeSnapshot, FlowTraceEventType };
@@ -0,0 +1,300 @@
1
+ import { a as ReadableNode, c as Observable, d as TraceHub, f as LiveNodeRegistry, h as PicoFlowError, l as Observer, o as EffectNode, s as Node, t as ValueCore } from "./shared.js";
2
+ import { t as SignalNode } from "./shared3.js";
3
+
4
+ //#region src/core/inspect/renderValue.ts
5
+ /** Longest rendering kept; anything past this is cut and marked with an ellipsis. */
6
+ const MAX_LENGTH = 80;
7
+ /** How deep the renderer descends before collapsing a nested value to `{…}` / `[…]`. */
8
+ const MAX_DEPTH = 2;
9
+ /** How many entries of an array, map or set are rendered before the rest is elided. */
10
+ const MAX_ENTRIES = 10;
11
+ /**
12
+ * Renders a value for an inspection dump: short, safe, and never a hazard of its own (#50).
13
+ *
14
+ * A dump must survive whatever a graph happens to hold — a megabyte of data, a cyclic object, a
15
+ * getter that throws — so this is deliberately not `JSON.stringify`. Depth and entry count are
16
+ * bounded (which is also what makes a cycle harmless, without the false "[Circular]" a seen-set
17
+ * reports for a merely repeated object), the result is truncated, and anything unexpected degrades
18
+ * to a placeholder rather than throwing inside the debugging tool.
19
+ * @internal
20
+ */
21
+ function renderValue(data) {
22
+ let text;
23
+ try {
24
+ text = render(data, MAX_DEPTH);
25
+ } catch {
26
+ text = "[unrenderable]";
27
+ }
28
+ return text.length > MAX_LENGTH ? `${text.slice(0, 79)}…` : text;
29
+ }
30
+ function render(data, depth) {
31
+ switch (typeof data) {
32
+ case "string": return JSON.stringify(data);
33
+ case "number":
34
+ case "boolean": return String(data);
35
+ case "undefined": return "undefined";
36
+ case "bigint": return `${data}n`;
37
+ case "symbol": return data.toString();
38
+ case "function": return data.name ? `[Function ${data.name}]` : "[Function]";
39
+ }
40
+ if (data === null) return "null";
41
+ if (data instanceof Error) return `${data.name}: ${data.message}`;
42
+ if (data instanceof Date) return data.toISOString();
43
+ const isArray = Array.isArray(data);
44
+ const isMap = data instanceof Map;
45
+ const isSet = data instanceof Set;
46
+ if (depth === 0) return isArray ? "[…]" : "{…}";
47
+ if (isArray) return `[${list(data, (item) => render(item, depth - 1))}]`;
48
+ if (isSet) return `Set(${data.size}) {${list([...data], (item) => render(item, depth - 1))}}`;
49
+ if (isMap) {
50
+ const entries = list([...data], ([key, value]) => `${render(key, depth - 1)} => ${render(value, depth - 1)}`);
51
+ return `Map(${data.size}) {${entries}}`;
52
+ }
53
+ return `{${list(Object.entries(data), ([key, value]) => `${key}: ${render(value, depth - 1)}`)}}`;
54
+ }
55
+ /** Joins at most {@link MAX_ENTRIES} rendered items, marking the elided tail. */
56
+ function list(items, renderItem) {
57
+ const shown = items.slice(0, MAX_ENTRIES).map(renderItem);
58
+ if (items.length > MAX_ENTRIES) shown.push(`…${items.length - MAX_ENTRIES} more`);
59
+ return shown.join(", ");
60
+ }
61
+
62
+ //#endregion
63
+ //#region src/core/inspect/snapshot.ts
64
+ /**
65
+ * Describes one primitive and its immediate neighbours, without touching the reactive system (#50,
66
+ * A2-DX-2).
67
+ *
68
+ * Strictly passive: it reads `status` and the passive value snapshot, and iterates the dependency
69
+ * registries. It never calls `get()`, `pick()`, `watch()` or `subscribe()`, so it computes nothing,
70
+ * registers no dependency and moves no node — inspecting a graph cannot change what that graph then
71
+ * does. Inspecting a disposed primitive is fine too, and reports `status: "disposed"`.
72
+ *
73
+ * One consequence is worth stating plainly: a lazy derivation that has never been read has never
74
+ * computed, so it has **no dependencies yet** and reports `status: "dirty"` with no value. That is
75
+ * the truth about the graph, not a gap in the report — exercise the application first, or create the
76
+ * derivations you mean to watch with `eager: true`.
77
+ *
78
+ * @param target - The primitive to describe: a value, a signal, or the effect `subscribe()` returned
79
+ * @returns A serialisable description of that node
80
+ *
81
+ * @public
82
+ */
83
+ function inspect(target) {
84
+ return snapshotOf(participantOf(target));
85
+ }
86
+ /**
87
+ * Resolves a public handle to the node that actually sits in the graph.
88
+ *
89
+ * A value primitive is a facade owning a {@link ValueCore}, so the map built at construction is the
90
+ * way across; a signal and an effect are their own graph node. Nothing else is accepted — in
91
+ * particular not the tracker a compute function receives, even though reading it would be harmless:
92
+ * introspection stays outside the reactive system, and an entry point that takes a tracker would
93
+ * suggest otherwise.
94
+ */
95
+ function participantOf(target) {
96
+ if (target instanceof ReadableNode) return target._core;
97
+ if (target instanceof EffectNode || target instanceof SignalNode) return target;
98
+ throw new PicoFlowError("[PicoFlow] inspect() expects a primitive, a signal or an effect — not the tracker a compute function receives");
99
+ }
100
+ /** Builds the full description of a graph participant. @internal */
101
+ function snapshotOf(participant) {
102
+ const snapshot = {
103
+ ...refOf(participant),
104
+ dependencies: [],
105
+ dependents: []
106
+ };
107
+ if (participant instanceof Node || participant instanceof Observer) participant._forEachDependency((dependency) => {
108
+ snapshot.dependencies.push(refOf(dependency));
109
+ });
110
+ if (participant instanceof Observable) participant._forEachDependent((dependent) => {
111
+ snapshot.dependents.push(refOf(dependent));
112
+ });
113
+ if (participant instanceof ValueCore) {
114
+ const value = participant.snapshot();
115
+ if (value.status === "resolved") snapshot.value = renderValue(value.data);
116
+ if (value.status === "error") snapshot.error = value.error.message;
117
+ if (participant._isEager) snapshot.eager = true;
118
+ }
119
+ return snapshot;
120
+ }
121
+ /** Builds the short form: what a node is called, and where it stands. @internal */
122
+ function refOf(participant) {
123
+ const kind = participant._kind;
124
+ const name = participant._inspectName;
125
+ const status = statusOf(participant);
126
+ const ref = {
127
+ id: participant._id,
128
+ kind,
129
+ label: name ?? `${kind}#${participant._id}`,
130
+ status
131
+ };
132
+ if (name !== void 0) ref.name = name;
133
+ return ref;
134
+ }
135
+ /** `status` is the one reading every participant offers, and it never computes nor throws. */
136
+ function statusOf(participant) {
137
+ if (participant instanceof Observable) return participant.status;
138
+ return participant.status;
139
+ }
140
+
141
+ //#endregion
142
+ //#region src/core/inspect/graph.ts
143
+ /**
144
+ * Describes every reactive node alive in this process, without touching the reactive system (#50,
145
+ * A2-DX-2).
146
+ *
147
+ * There is no way to name where to start, and that is the point: the graph has no roots, and the
148
+ * nodes worth finding — the ones leaked because nothing disposed them — are exactly the ones no
149
+ * handle points at any more. So the dump comes from a census kept as nodes are created, held weakly:
150
+ * a node the garbage collector has taken is gone from it, and one that lingers shows up as an orphan.
151
+ *
152
+ * Passivity is the same contract as {@link inspect}: statuses are read, registries iterated, nothing
153
+ * computed. In particular a lazy derivation never read has no dependencies yet and appears as an
154
+ * orphan — see {@link inspect} for what to do about it.
155
+ *
156
+ * @param options - See {@link FlowGraphOptions}
157
+ * @returns Every node, in creation order, with a {@link FlowGraphSummary}
158
+ *
159
+ * @public
160
+ */
161
+ function graphSnapshot(options) {
162
+ const includeDisposed = options?.includeDisposed ?? false;
163
+ const nodes = [];
164
+ LiveNodeRegistry.forEach((participant) => {
165
+ const snapshot = snapshotOf(participant);
166
+ if (!includeDisposed && snapshot.status === "disposed") return;
167
+ nodes.push(snapshot);
168
+ });
169
+ const byKind = {};
170
+ const byStatus = {};
171
+ let orphans = 0;
172
+ for (const node of nodes) {
173
+ byKind[node.kind] = (byKind[node.kind] ?? 0) + 1;
174
+ byStatus[node.status] = (byStatus[node.status] ?? 0) + 1;
175
+ if (node.dependencies.length === 0 && node.dependents.length === 0) orphans++;
176
+ }
177
+ return {
178
+ nodes,
179
+ summary: {
180
+ total: nodes.length,
181
+ byKind,
182
+ byStatus,
183
+ orphans
184
+ }
185
+ };
186
+ }
187
+
188
+ //#endregion
189
+ //#region src/core/inspect/format.ts
190
+ /** Widest a column is padded to; a longer label simply pushes its own line out. */
191
+ const MAX_COLUMN = 24;
192
+ /**
193
+ * Renders a graph as readable text (#50, A2-DX-2).
194
+ *
195
+ * The intended use is a developer with the browser console open, mid-session, asking what the graph
196
+ * looks like right now: `console.log(formatGraph())`. It returns a string rather than printing one,
197
+ * so the same call works in a terminal, in a test, or pasted into a bug report — and so the library
198
+ * never writes to anyone's console on its own.
199
+ *
200
+ * One block per node: a line naming it, its kind and its status, followed by its value when it has
201
+ * one (`= …`) or its error (`! …`), then an indented line of edges — `←` for what it reads, `→` for
202
+ * what reads it. Nodes with no edges at all are gathered at the end under `orphans`, where a leak
203
+ * shows up as a list that keeps growing.
204
+ *
205
+ * @param graph - A snapshot to render; taken with {@link graphSnapshot} when omitted
206
+ * @returns The rendered listing
207
+ *
208
+ * @public
209
+ */
210
+ function formatGraph(graph = graphSnapshot()) {
211
+ const { nodes, summary } = graph;
212
+ const counts = Object.entries(summary.byStatus).map(([status, count]) => `${status}:${count}`);
213
+ if (summary.orphans > 0) counts.push(`orphans:${summary.orphans}`);
214
+ const header = `picoflow graph — ${summary.total} node${summary.total === 1 ? "" : "s"}${counts.length > 0 ? ` · ${counts.join(" · ")}` : ""}`;
215
+ if (nodes.length === 0) return header;
216
+ const connected = nodes.filter((node) => node.dependencies.length > 0 || node.dependents.length > 0);
217
+ const orphans = nodes.filter((node) => node.dependencies.length === 0 && node.dependents.length === 0);
218
+ const labelWidth = width(nodes, (node) => node.label);
219
+ const kindWidth = width(nodes, (node) => node.kind);
220
+ const lines = [header, ""];
221
+ for (const node of connected) lines.push(...block(node, labelWidth, kindWidth));
222
+ if (orphans.length > 0) {
223
+ if (connected.length > 0) lines.push("");
224
+ lines.push("orphans — no edges:");
225
+ for (const node of orphans) lines.push(...block(node, labelWidth, kindWidth));
226
+ }
227
+ return lines.join("\n");
228
+ }
229
+ /** The node's own line, plus its edge line when it has any. */
230
+ function block(node, labelWidth, kindWidth) {
231
+ const status = node.eager ? `${node.status} (eager)` : node.status;
232
+ let line = `${pad(node.label, labelWidth)} ${pad(node.kind, kindWidth)} ${pad(status, 8)}`;
233
+ if (node.value !== void 0) line += ` = ${node.value}`;
234
+ if (node.error !== void 0) line += ` ! ${node.error}`;
235
+ const lines = [line.trimEnd()];
236
+ const edges = [];
237
+ if (node.dependencies.length > 0) edges.push(`← ${node.dependencies.map((ref) => ref.label).join(", ")}`);
238
+ if (node.dependents.length > 0) edges.push(`→ ${node.dependents.map((ref) => ref.label).join(", ")}`);
239
+ if (edges.length > 0) lines.push(` ${edges.join(" ")}`);
240
+ return lines;
241
+ }
242
+ function width(nodes, of) {
243
+ return Math.min(MAX_COLUMN, Math.max(...nodes.map((node) => of(node).length)));
244
+ }
245
+ function pad(text, to) {
246
+ return text.length >= to ? text : text + " ".repeat(to - text.length);
247
+ }
248
+
249
+ //#endregion
250
+ //#region src/core/inspect/trace.ts
251
+ /**
252
+ * Records what the engine does, so that a question about *time* can be answered (#50, A2-DX-2).
253
+ *
254
+ * `inspect()` and `graphSnapshot()` describe the graph's shape at an instant; some questions are not
255
+ * about shape. "I wrote to `$count` and my effect never ran" is answered by reading the sequence: a
256
+ * `notify` on `$count` with no `queue-effect` behind it means the edge was never there, while a
257
+ * `queue-effect` with no `effect-run` means the flush never reached it. A feedback loop reads as an
258
+ * alternation of `notify` and `effect-run` ending in `cycle-error`, which is how the nodes involved
259
+ * get named — the error alone names neither.
260
+ *
261
+ * Tracing is off until this is called, and costs one boolean check per engine event while it is.
262
+ * Recording does not change what the engine does: events are observed as they pass, never injected.
263
+ *
264
+ * @param options - See {@link FlowTraceOptions}
265
+ * @returns The running trace; call `dispose()` on it to stop
266
+ *
267
+ * @public
268
+ */
269
+ function enableTrace(options) {
270
+ const capacity = Math.max(1, Math.trunc(options?.capacity ?? 500));
271
+ let buffer = [];
272
+ let next = 0;
273
+ let seq = 0;
274
+ const listener = (type, node) => {
275
+ const event = {
276
+ seq: seq++,
277
+ type,
278
+ node: refOf(node)
279
+ };
280
+ if (buffer.length < capacity) buffer.push(event);
281
+ else buffer[next] = event;
282
+ next = (next + 1) % capacity;
283
+ };
284
+ TraceHub.listen(listener);
285
+ return {
286
+ get events() {
287
+ return buffer.length < capacity ? [...buffer] : [...buffer.slice(next), ...buffer.slice(0, next)];
288
+ },
289
+ clear() {
290
+ buffer = [];
291
+ next = 0;
292
+ },
293
+ dispose() {
294
+ TraceHub.unlisten(listener);
295
+ }
296
+ };
297
+ }
298
+
299
+ //#endregion
300
+ export { enableTrace, formatGraph, graphSnapshot, inspect };