@graphrefly/graphrefly 0.21.0 → 0.23.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 (101) hide show
  1. package/README.md +7 -5
  2. package/dist/chunk-263BEJJO.js +115 -0
  3. package/dist/chunk-263BEJJO.js.map +1 -0
  4. package/dist/chunk-2GQLMQVJ.js +47 -0
  5. package/dist/chunk-2GQLMQVJ.js.map +1 -0
  6. package/dist/chunk-32N5A454.js +36 -0
  7. package/dist/chunk-32N5A454.js.map +1 -0
  8. package/dist/chunk-7TAQJHQV.js +103 -0
  9. package/dist/chunk-7TAQJHQV.js.map +1 -0
  10. package/dist/{chunk-VOQFK7YN.js → chunk-CWYPA63G.js} +109 -259
  11. package/dist/chunk-CWYPA63G.js.map +1 -0
  12. package/dist/{chunk-7IGHIFTT.js → chunk-HVBX5KIW.js} +15 -26
  13. package/dist/chunk-HVBX5KIW.js.map +1 -0
  14. package/dist/chunk-JFONSPNF.js +391 -0
  15. package/dist/chunk-JFONSPNF.js.map +1 -0
  16. package/dist/chunk-NZMBRXQV.js +2330 -0
  17. package/dist/chunk-NZMBRXQV.js.map +1 -0
  18. package/dist/{chunk-XWBVAO2R.js → chunk-PNUZM7PC.js} +20 -30
  19. package/dist/chunk-PNUZM7PC.js.map +1 -0
  20. package/dist/{chunk-ZTCDY5NQ.js → chunk-PX6PDUJ5.js} +34 -50
  21. package/dist/chunk-PX6PDUJ5.js.map +1 -0
  22. package/dist/chunk-XRFJJ2IU.js +2417 -0
  23. package/dist/chunk-XRFJJ2IU.js.map +1 -0
  24. package/dist/chunk-XTLYW4FR.js +6829 -0
  25. package/dist/chunk-XTLYW4FR.js.map +1 -0
  26. package/dist/compat/nestjs/index.cjs +3489 -2286
  27. package/dist/compat/nestjs/index.cjs.map +1 -1
  28. package/dist/compat/nestjs/index.d.cts +6 -4
  29. package/dist/compat/nestjs/index.d.ts +6 -4
  30. package/dist/compat/nestjs/index.js +10 -8
  31. package/dist/core/index.cjs +1706 -1217
  32. package/dist/core/index.cjs.map +1 -1
  33. package/dist/core/index.d.cts +3 -2
  34. package/dist/core/index.d.ts +3 -2
  35. package/dist/core/index.js +37 -34
  36. package/dist/extra/index.cjs +7519 -6125
  37. package/dist/extra/index.cjs.map +1 -1
  38. package/dist/extra/index.d.cts +4 -4
  39. package/dist/extra/index.d.ts +4 -4
  40. package/dist/extra/index.js +63 -34
  41. package/dist/graph/index.cjs +3199 -2212
  42. package/dist/graph/index.cjs.map +1 -1
  43. package/dist/graph/index.d.cts +5 -3
  44. package/dist/graph/index.d.ts +5 -3
  45. package/dist/graph/index.js +24 -11
  46. package/dist/graph-BtdSRHUc.d.cts +1128 -0
  47. package/dist/graph-CEO2FkLY.d.ts +1128 -0
  48. package/dist/{index-DuN3bhtm.d.ts → index-B0tfuXwV.d.cts} +1697 -586
  49. package/dist/index-BFGjXbiP.d.cts +315 -0
  50. package/dist/{index-CgSiUouz.d.ts → index-BPlWVAKY.d.cts} +4 -4
  51. package/dist/index-BUj3ASVe.d.cts +406 -0
  52. package/dist/{index-VHA43cGP.d.cts → index-C59uSJAH.d.cts} +2 -2
  53. package/dist/index-CkElcUY6.d.ts +315 -0
  54. package/dist/index-DSPc5rkv.d.ts +406 -0
  55. package/dist/{index-BjtlNirP.d.cts → index-DgscL7v0.d.ts} +4 -4
  56. package/dist/{index-SFzE_KTa.d.cts → index-RXN94sHK.d.ts} +1697 -586
  57. package/dist/{index-8a605sg9.d.ts → index-jEtF4N7L.d.ts} +2 -2
  58. package/dist/index.cjs +9947 -7949
  59. package/dist/index.cjs.map +1 -1
  60. package/dist/index.d.cts +214 -37
  61. package/dist/index.d.ts +214 -37
  62. package/dist/index.js +919 -648
  63. package/dist/index.js.map +1 -1
  64. package/dist/meta-3QjzotRv.d.ts +41 -0
  65. package/dist/meta-B-Lbs4-O.d.cts +41 -0
  66. package/dist/node-C7PD3sn9.d.cts +1188 -0
  67. package/dist/node-C7PD3sn9.d.ts +1188 -0
  68. package/dist/{observable-DcBwQY7t.d.ts → observable-EyO-moQY.d.ts} +1 -1
  69. package/dist/{observable-C8Kx_O6k.d.cts → observable-axpzv1K2.d.cts} +1 -1
  70. package/dist/patterns/reactive-layout/index.cjs +3205 -2138
  71. package/dist/patterns/reactive-layout/index.cjs.map +1 -1
  72. package/dist/patterns/reactive-layout/index.d.cts +5 -3
  73. package/dist/patterns/reactive-layout/index.d.ts +5 -3
  74. package/dist/patterns/reactive-layout/index.js +7 -4
  75. package/dist/storage-CHT5WE9m.d.ts +182 -0
  76. package/dist/storage-DIgAr7M_.d.cts +182 -0
  77. package/package.json +2 -1
  78. package/dist/chunk-2UDLYZHT.js +0 -2117
  79. package/dist/chunk-2UDLYZHT.js.map +0 -1
  80. package/dist/chunk-4MQ2J6IG.js +0 -1631
  81. package/dist/chunk-4MQ2J6IG.js.map +0 -1
  82. package/dist/chunk-7IGHIFTT.js.map +0 -1
  83. package/dist/chunk-DOSLSFKL.js +0 -162
  84. package/dist/chunk-DOSLSFKL.js.map +0 -1
  85. package/dist/chunk-ECN37NVS.js +0 -6227
  86. package/dist/chunk-ECN37NVS.js.map +0 -1
  87. package/dist/chunk-G66H6ZRK.js +0 -111
  88. package/dist/chunk-G66H6ZRK.js.map +0 -1
  89. package/dist/chunk-VOQFK7YN.js.map +0 -1
  90. package/dist/chunk-WZ2Z2CRV.js +0 -32
  91. package/dist/chunk-WZ2Z2CRV.js.map +0 -1
  92. package/dist/chunk-XWBVAO2R.js.map +0 -1
  93. package/dist/chunk-ZTCDY5NQ.js.map +0 -1
  94. package/dist/graph-KsTe57nI.d.cts +0 -750
  95. package/dist/graph-mILUUqW8.d.ts +0 -750
  96. package/dist/index-B2SvPEbc.d.ts +0 -257
  97. package/dist/index-BHfg_Ez3.d.ts +0 -629
  98. package/dist/index-Bc_diYYJ.d.cts +0 -629
  99. package/dist/index-UudxGnzc.d.cts +0 -257
  100. package/dist/meta-BnG7XAaE.d.cts +0 -778
  101. package/dist/meta-BnG7XAaE.d.ts +0 -778
@@ -0,0 +1,1188 @@
1
+ /**
2
+ * Who is performing an operation (attribution + ABAC input).
3
+ *
4
+ * @see GRAPHREFLY-SPEC — roadmap Phase 1.5 (Actor & Guard).
5
+ */
6
+ type Actor = {
7
+ type: "human" | "llm" | "wallet" | "system" | string;
8
+ id: string;
9
+ } & Record<string, unknown>;
10
+ /** Default actor when none is passed ({@link normalizeActor}). */
11
+ declare const DEFAULT_ACTOR: Actor;
12
+ /**
13
+ * Fills missing `type` / `id` on an actor and returns {@link DEFAULT_ACTOR} when input is undefined.
14
+ *
15
+ * @param actor - Optional partial actor from a transport hint.
16
+ * @returns A normalized `Actor` safe to pass to guards and graph APIs.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { normalizeActor } from "@graphrefly/graphrefly-ts";
21
+ *
22
+ * normalizeActor({ type: "human", id: "u1" });
23
+ * ```
24
+ */
25
+ declare function normalizeActor(actor?: Actor): Actor;
26
+
27
+ /**
28
+ * GraphReFly message protocol — §1 `~/src/graphrefly/GRAPHREFLY-SPEC.md`.
29
+ * Emissions are always `[[Type, Data?], ...]` (no single-tuple shorthand).
30
+ *
31
+ * This file is protocol-pure:
32
+ * - Message type symbols (10 built-ins).
33
+ * - `Message` / `Messages` tuple types.
34
+ * - `MessageTypeRegistration` interface (shape of a registry entry).
35
+ *
36
+ * It does NOT own the registry, tier lookups, or any cross-cutting singleton
37
+ * state — that lives in `GraphReFlyConfig` (see `config.ts`) so custom
38
+ * protocols can build isolated instances. Import this module when you need
39
+ * the symbol constants or the tuple types; import `config.ts` when you need
40
+ * tier / wire-crossing / registry lookups.
41
+ */
42
+ /** Subscribe-time handshake. Delivered to each new sink at the top of `subscribe()`. Tier 0. */
43
+ declare const START: unique symbol;
44
+ /** Value delivery (`DATA`, value). Tier 3 — deferred inside `batch()`. */
45
+ declare const DATA: unique symbol;
46
+ /** Phase 1: value about to change. Tier 1 — immediate. */
47
+ declare const DIRTY: unique symbol;
48
+ /** Phase 2: dirty pass completed, value unchanged. Tier 3 — deferred inside `batch()`. */
49
+ declare const RESOLVED: unique symbol;
50
+ /** Clear cached state; do not auto-emit. Tier 1 — immediate. */
51
+ declare const INVALIDATE: unique symbol;
52
+ /** Suspend activity. Tier 2 — immediate. */
53
+ declare const PAUSE: unique symbol;
54
+ /** Resume after pause. Tier 2 — immediate. */
55
+ declare const RESUME: unique symbol;
56
+ /** Permanent cleanup. Tier 5 — deferred to batch phase 4. */
57
+ declare const TEARDOWN: unique symbol;
58
+ /** Clean termination. Tier 4 — deferred to batch phase 3. */
59
+ declare const COMPLETE: unique symbol;
60
+ /** Error termination. Tier 4 — deferred to batch phase 3. */
61
+ declare const ERROR: unique symbol;
62
+ /** One protocol tuple: `[Type, optional payload]`. */
63
+ type Message = readonly [symbol, unknown?];
64
+ /** A batch of tuples — the wire shape for `node.down()` / `node.up()`. */
65
+ type Messages = readonly Message[];
66
+ /** Singleton `[DIRTY]` tuple — payload-free, interned. */
67
+ declare const DIRTY_MSG: Message;
68
+ /** Singleton `[RESOLVED]` tuple — payload-free, interned. */
69
+ declare const RESOLVED_MSG: Message;
70
+ /** Singleton `[INVALIDATE]` tuple — payload-free, interned. */
71
+ declare const INVALIDATE_MSG: Message;
72
+ /** Singleton `[START]` tuple — payload-free, interned. */
73
+ declare const START_MSG: Message;
74
+ /** Singleton `[COMPLETE]` tuple — payload-free, interned. */
75
+ declare const COMPLETE_MSG: Message;
76
+ /** Singleton `[TEARDOWN]` tuple — payload-free, interned. */
77
+ declare const TEARDOWN_MSG: Message;
78
+ /** Pre-wrapped `[[DIRTY]]` for `_emit([DIRTY_ONLY_BATCH])`-style callers. */
79
+ declare const DIRTY_ONLY_BATCH: Messages;
80
+ /** Pre-wrapped `[[RESOLVED]]`. */
81
+ declare const RESOLVED_ONLY_BATCH: Messages;
82
+ /** Pre-wrapped `[[INVALIDATE]]`. */
83
+ declare const INVALIDATE_ONLY_BATCH: Messages;
84
+ /** Pre-wrapped `[[COMPLETE]]`. */
85
+ declare const COMPLETE_ONLY_BATCH: Messages;
86
+ /** Pre-wrapped `[[TEARDOWN]]`. */
87
+ declare const TEARDOWN_ONLY_BATCH: Messages;
88
+ /**
89
+ * Per-type record stored in a {@link GraphReFlyConfig}'s registry.
90
+ *
91
+ * - `tier` — signal tier (0–5 built-in; custom tiers allowed but should fit
92
+ * the phase model used by `batch.ts`).
93
+ * - `wireCrossing` — when `true`, forwarded across SSE/WebSocket/worker
94
+ * adapters. Defaults to `tier >= 3` if omitted in registration input.
95
+ * - `metaPassthrough` — when `false`, this message type is filtered out of
96
+ * `Graph.signal` deliveries to meta companion nodes (spec §2.3). Meta
97
+ * companions still receive everything via their primary's own cascade.
98
+ * Defaults to `true` (meta receives the message).
99
+ */
100
+ interface MessageTypeRegistration {
101
+ tier: number;
102
+ wireCrossing: boolean;
103
+ metaPassthrough: boolean;
104
+ }
105
+ /**
106
+ * Input accepted by {@link GraphReFlyConfig.registerMessageType}. Only `tier`
107
+ * is required; `wireCrossing` defaults to `tier >= 3`; `metaPassthrough`
108
+ * defaults to `true`.
109
+ */
110
+ interface MessageTypeRegistrationInput {
111
+ tier: number;
112
+ wireCrossing?: boolean;
113
+ metaPassthrough?: boolean;
114
+ }
115
+
116
+ /**
117
+ * Node versioning — GRAPHREFLY-SPEC §7.
118
+ *
119
+ * Progressive, optional versioning for node identity and change tracking.
120
+ *
121
+ * - **V0**: `id` + `version` — identity & change detection (~16 bytes overhead)
122
+ * - **V1**: + `cid` + `prev` — content addressing & linked history (~60 bytes overhead)
123
+ *
124
+ * **Lifecycle notes:**
125
+ * - Version advances only on DATA (not RESOLVED, INVALIDATE, or TEARDOWN).
126
+ * - `resetOnTeardown` clears the cached value but does NOT reset versioning state.
127
+ * After teardown, `v.cid` still reflects the last DATA value, not the cleared cache.
128
+ * The invariant `hash(node.cache) === v.cid` only holds in `settled`/`resolved` status.
129
+ * - Resubscribable nodes preserve versioning across subscription lifetimes (monotonic counter).
130
+ */
131
+ /** V0: identity + monotonic version counter. */
132
+ type V0 = {
133
+ readonly id: string;
134
+ version: number;
135
+ };
136
+ /** V1: V0 + content-addressed identifier + previous cid link. */
137
+ type V1 = V0 & {
138
+ cid: string;
139
+ prev: string | null;
140
+ };
141
+ /** Union of all versioning info shapes. */
142
+ type NodeVersionInfo = V0 | V1;
143
+ /** Supported versioning levels (extensible to 2, 3 later). */
144
+ type VersioningLevel = 0 | 1;
145
+ /** Function that hashes a value to a hex string (for V1 cid). */
146
+ type HashFn = (value: unknown) => string;
147
+ interface VersioningOptions {
148
+ /** Override auto-generated id. */
149
+ id?: string;
150
+ /** Custom hash function for V1 cid (default: SHA-256 truncated to 16 hex chars). */
151
+ hash?: HashFn;
152
+ }
153
+ /**
154
+ * Default content hash: SHA-256 of deterministic JSON, truncated to 16 hex
155
+ * chars (~64-bit). Uses {@link canonicalizeForHash} for cross-language parity
156
+ * with Python `default_hash`.
157
+ */
158
+ declare function defaultHash(value: unknown): string;
159
+ /**
160
+ * Create initial versioning state for a node.
161
+ *
162
+ * @param level - 0 for V0, 1 for V1.
163
+ * @param initialValue - The node's initial cached value (used for V1 cid).
164
+ * @param opts - Optional overrides (id, hash).
165
+ */
166
+ declare function createVersioning(level: VersioningLevel, initialValue: unknown, opts?: VersioningOptions): NodeVersionInfo;
167
+ /**
168
+ * Advance versioning state after a DATA emission (value changed).
169
+ *
170
+ * Mutates `info` in place for performance (called on every DATA).
171
+ * Only call when the cached value has actually changed (not on RESOLVED).
172
+ *
173
+ * @param info - The node's current versioning state.
174
+ * @param newValue - The new cached value.
175
+ * @param hashFn - Hash function (only used for V1).
176
+ */
177
+ declare function advanceVersion(info: NodeVersionInfo, newValue: unknown, hashFn: HashFn): void;
178
+ /** Type guard: is this V1 versioning info? */
179
+ declare function isV1(info: NodeVersionInfo): info is V1;
180
+
181
+ /**
182
+ * Singleton protocol config. Holds the message-type registry, the
183
+ * `onMessage` / `onSubscribe` hooks, versioning defaults, and the freeze flag.
184
+ *
185
+ * Layering: this file is protocol-pure. It imports only from `messages.ts`
186
+ * and declares opaque type shapes for handlers — the concrete default
187
+ * implementations and the `defaultConfig` instance live in `node.ts` so that
188
+ * handler bodies can touch `NodeImpl` internals without creating a cycle.
189
+ *
190
+ * Two access paths:
191
+ * 1. **Default instance** (`defaultConfig` in `node.ts`) — use
192
+ * `configure((cfg) => ...)` at app startup; every node implicitly binds to it.
193
+ * 2. **Isolated instance** (`new GraphReFlyConfig(...)`) — pass via
194
+ * `opts.config` for test isolation or custom protocol stacks.
195
+ *
196
+ * A config **freezes on first getter read** of any hook (`onMessage`,
197
+ * `onSubscribe`). `NodeImpl`'s constructor intentionally touches one of these
198
+ * on first use so configuration cannot drift once nodes exist.
199
+ */
200
+
201
+ /**
202
+ * Minimal node surface visible to default handlers. Concrete `NodeImpl`
203
+ * implements this plus a large set of package-private fields; handlers that
204
+ * need the richer surface cast to the concrete type in `node.ts`.
205
+ */
206
+ interface NodeCtx {
207
+ readonly name?: string;
208
+ readonly status: string;
209
+ readonly cache: unknown;
210
+ }
211
+ /** Imperative actions available inside a node's compute function (§5). */
212
+ interface NodeActions {
213
+ /**
214
+ * Sugar for `down([[DATA, value]])`. One call = one wave with a
215
+ * single DATA payload. The emit pipeline auto-prefixes `[DIRTY]`,
216
+ * runs equals substitution against the live cache, and dispatches
217
+ * to sinks with phase deferral. Diamond-safe by construction.
218
+ */
219
+ emit(value: unknown): void;
220
+ /**
221
+ * Send one or more messages downstream. Accepts either a single
222
+ * {@link Message} tuple or a {@link Messages} array of tuples. One
223
+ * call = one wave: the emit pipeline tier-sorts the input,
224
+ * auto-prefixes `[DIRTY]` when a tier-3 payload is present and the
225
+ * node isn't already dirty, runs equals substitution, then
226
+ * dispatches. Multiple calls produce multiple waves.
227
+ */
228
+ down(messageOrMessages: Message | Messages): void;
229
+ /**
230
+ * Send one or more messages upstream. Accepts the same shapes as
231
+ * {@link down}. Tier 3 (DATA/RESOLVED) and tier 4 (COMPLETE/ERROR)
232
+ * are downstream-only and will throw — up is for DIRTY, INVALIDATE,
233
+ * PAUSE, RESUME, and TEARDOWN only. No cache advance, no equals,
234
+ * no framing — a plain forward to every dep.
235
+ */
236
+ up(messageOrMessages: Message | Messages): void;
237
+ }
238
+ /**
239
+ * Message-flow context passed to {@link OnMessageHandler}.
240
+ *
241
+ * - `"down-in"` — message arriving from a dep (identified by `depIndex`).
242
+ * - `"up-in"` — message arriving from a sink.
243
+ */
244
+ type MessageContext = {
245
+ direction: "down-in";
246
+ depIndex: number;
247
+ } | {
248
+ direction: "up-in";
249
+ };
250
+ /**
251
+ * Per-sink context passed to {@link OnSubscribeHandler}.
252
+ */
253
+ interface SubscribeContext {
254
+ /** Post-subscribe sink count. `1` means first subscriber after 0. */
255
+ sinkCount: number;
256
+ /** True when this subscribe cleared a resubscribable terminal state. */
257
+ afterTerminalReset: boolean;
258
+ }
259
+ /**
260
+ * Singleton message interceptor. Called for every message in either direction
261
+ * before the default per-tier dispatch runs. Return `"consume"` to suppress
262
+ * default handling.
263
+ */
264
+ type OnMessageHandler = (node: NodeCtx, msg: Message, ctx: MessageContext, actions: NodeActions) => "consume" | undefined;
265
+ /**
266
+ * Singleton subscribe ceremony. Fires for every sink subscribe on every node.
267
+ * Default implementation emits the START handshake (+ cached DATA when
268
+ * present) to the new sink. Return a cleanup function to run on unsubscribe.
269
+ */
270
+ type OnSubscribeHandler = (node: NodeCtx, sink: (messages: Messages) => void, ctx: SubscribeContext, actions: NodeActions) => (() => void) | undefined;
271
+ /**
272
+ * Event payload for {@link GlobalInspectorHook}. One event fires per outgoing
273
+ * message batch from any node bound to the config.
274
+ *
275
+ * - `kind: "emit"` — fires after equals substitution (`finalMessages`) and
276
+ * before sink dispatch. `messages` is the exact batch sinks see.
277
+ */
278
+ type GlobalInspectorEvent = {
279
+ kind: "emit";
280
+ node: NodeCtx;
281
+ messages: Messages;
282
+ };
283
+ /**
284
+ * Process-global observability hook for full-graph tracing
285
+ * (Redux-DevTools-style action history). Fires from every node's `_emit`
286
+ * waist whenever {@link GraphReFlyConfig.inspectorEnabled} is `true`.
287
+ *
288
+ * Distinct from per-node `_setInspectorHook` (which is the dep-message /
289
+ * fn-run causal trace used by `Graph.observe(path, { causal, derived })`).
290
+ * Use the global hook to record every emission across every node — useful for
291
+ * time-travel debuggers, tracers, and replay tooling. Use the per-node hook
292
+ * to attribute a single subscribed path's wave to its driving deps.
293
+ *
294
+ * Errors thrown from the hook are swallowed — instrumentation must not break
295
+ * the data plane.
296
+ */
297
+ type GlobalInspectorHook = (event: GlobalInspectorEvent) => void;
298
+ /**
299
+ * Singleton protocol config.
300
+ *
301
+ * A config freezes on first getter read of any hook. After freeze, any
302
+ * attempt to mutate (register a message type, set a hook) throws.
303
+ */
304
+ declare class GraphReFlyConfig {
305
+ private _messageTypes;
306
+ private _codecs;
307
+ private _onMessage;
308
+ private _onSubscribe;
309
+ private _defaultVersioning;
310
+ private _defaultHashFn;
311
+ private _inspectorEnabled;
312
+ private _globalInspector?;
313
+ private _frozen;
314
+ /**
315
+ * Pre-bound tier lookup — shared by every node bound to this config. Since
316
+ * the registry is frozen on first hook access, this closure can be built
317
+ * once in the constructor and handed directly to `downWithBatch` /
318
+ * `_frameBatch` paths without per-node or per-emission `.bind(config)`
319
+ * allocation.
320
+ */
321
+ readonly tierOf: (t: symbol) => number;
322
+ constructor(init: {
323
+ onMessage: OnMessageHandler;
324
+ onSubscribe: OnSubscribeHandler;
325
+ defaultVersioning?: VersioningLevel;
326
+ defaultHashFn?: HashFn;
327
+ });
328
+ get onMessage(): OnMessageHandler;
329
+ get onSubscribe(): OnSubscribeHandler;
330
+ set onMessage(v: OnMessageHandler);
331
+ set onSubscribe(v: OnSubscribeHandler);
332
+ /**
333
+ * Default versioning level applied to every node bound to this config,
334
+ * unless the node's own `opts.versioning` provides an explicit override.
335
+ * Setting this is only allowed before the config freezes (i.e., before
336
+ * the first node is created) so every node in the graph sees a
337
+ * consistent starting level. Individual nodes can still opt into a
338
+ * higher level via `opts.versioning`, or post-hoc via
339
+ * `NodeImpl._applyVersioning(level)` when the node is quiescent.
340
+ *
341
+ * v0 is the minimum opt-in — unversioned nodes (`undefined`) skip
342
+ * the version counter entirely. v1 adds content-addressed cid.
343
+ * Future levels (v2, v3) are reserved for linked-history and
344
+ * cryptographic attestation extensions.
345
+ */
346
+ get defaultVersioning(): VersioningLevel | undefined;
347
+ set defaultVersioning(v: VersioningLevel | undefined);
348
+ /**
349
+ * Default content-hash function applied to every versioned node bound
350
+ * to this config, unless the node's own `opts.versioningHash` provides
351
+ * an explicit override. Use this when a graph needs a non-default hash
352
+ * — e.g., swap the vendored sync SHA-256 for a faster non-crypto hash
353
+ * (xxHash, FNV-1a) in hot-path workloads, or a stronger hash when
354
+ * versioning v1 cids are used as audit anchors.
355
+ *
356
+ * Only settable before the config freezes. Individual nodes can still
357
+ * override via `opts.versioningHash`.
358
+ */
359
+ get defaultHashFn(): HashFn | undefined;
360
+ set defaultHashFn(v: HashFn | undefined);
361
+ /**
362
+ * When `false`, structured observation options (`causal`, `timeline`)
363
+ * and `Graph.trace()` writes are no-ops. Raw `Graph.observe()` always
364
+ * works. Default: `true` outside production (`NODE_ENV !== "production"`).
365
+ *
366
+ * Settable at any time — inspector gating is an operational concern, not
367
+ * a protocol invariant, so it does NOT require freeze before node creation.
368
+ */
369
+ get inspectorEnabled(): boolean;
370
+ set inspectorEnabled(v: boolean);
371
+ /**
372
+ * Process-global observability hook (Redux-DevTools-style full-graph
373
+ * tracer). Fires once per outgoing batch from every node bound to this
374
+ * config, gated by {@link inspectorEnabled}. See {@link GlobalInspectorHook}.
375
+ *
376
+ * Settable at any time — like {@link inspectorEnabled} this is operational,
377
+ * not protocol-shaping, so it does NOT trigger config freeze.
378
+ */
379
+ get globalInspector(): GlobalInspectorHook | undefined;
380
+ set globalInspector(v: GlobalInspectorHook | undefined);
381
+ /**
382
+ * Register a custom message type. Must be called before any node that
383
+ * uses this config has been created — otherwise throws. Default
384
+ * `wireCrossing` is `tier >= 3`.
385
+ */
386
+ registerMessageType(t: symbol, input: MessageTypeRegistrationInput): this;
387
+ /** Tier for `t`. Unknown types default to tier 1 (immediate, after START). */
388
+ messageTier(t: symbol): number;
389
+ /**
390
+ * Whether `t` is registered as wire-crossing. Unknown types default to
391
+ * `true` (spec §1.3.6 forward-compat — unknowns cross the wire).
392
+ */
393
+ isWireCrossing(t: symbol): boolean;
394
+ /** Convenience inverse of {@link isWireCrossing}. */
395
+ isLocalOnly(t: symbol): boolean;
396
+ /**
397
+ * Whether `t` is forwarded to meta companions by `Graph.signal`. Defaults
398
+ * to `true` for unknowns (forward-compat — new types pass through meta by
399
+ * default; opt-in filter via `registerMessageType({metaPassthrough: false})`).
400
+ */
401
+ isMetaPassthrough(t: symbol): boolean;
402
+ /** Whether `t` is a registered (built-in or custom) type. */
403
+ isKnownMessageType(t: symbol): boolean;
404
+ /**
405
+ * Register a graph codec by `codec.name`. Used by the envelope-based
406
+ * `graph.snapshot({format: "bytes", codec: name})` path and
407
+ * `Graph.decode(bytes)` auto-dispatch. Must be called before any node
408
+ * bound to this config is created — otherwise throws.
409
+ *
410
+ * Re-registering the same name overwrites, so user codecs can shadow
411
+ * built-in ones before freeze (e.g., to swap a zstd-wrapped dag-cbor in
412
+ * for `"dag-cbor"`).
413
+ */
414
+ registerCodec<T extends {
415
+ readonly name: string;
416
+ readonly version: number;
417
+ }>(codec: T): this;
418
+ /**
419
+ * Resolve a registered codec by name. Returns `undefined` for unknown
420
+ * names. Typed callers cast to their concrete codec interface (e.g.,
421
+ * `config.lookupCodec<GraphCodec>("json")`) — this method stays
422
+ * layer-pure (no import of graph-layer types into `core/`).
423
+ */
424
+ lookupCodec<T = {
425
+ readonly name: string;
426
+ readonly version: number;
427
+ }>(name: string): T | undefined;
428
+ /** @internal Used by tests and dev tooling — check freeze state without triggering it. */
429
+ _isFrozen(): boolean;
430
+ private _assertUnfrozen;
431
+ }
432
+ /**
433
+ * Register the 10 built-in message types on a fresh config. Called by
434
+ * `node.ts` when it constructs `defaultConfig` and by test code / advanced
435
+ * users after `new GraphReFlyConfig(...)`.
436
+ */
437
+ declare function registerBuiltins(cfg: GraphReFlyConfig): void;
438
+
439
+ /**
440
+ * Actions checked by {@link NodeGuard}. `write` covers both {@link Node.down} and
441
+ * {@link Node.up} today; finer-grained strings may be added later (e.g. `"write.data"`).
442
+ */
443
+ type GuardAction = "write" | "signal" | "observe" | (string & {});
444
+ type NodeGuard = (actor: Actor, action: GuardAction) => boolean;
445
+ type GuardDeniedDetails = {
446
+ actor: Actor;
447
+ action: GuardAction;
448
+ /** Registry or options name when known */
449
+ nodeName?: string;
450
+ };
451
+ /**
452
+ * Thrown when a {@link NodeGuard} denies an action for a given actor.
453
+ *
454
+ * Carries the rejected `actor`, `action`, and optional `nodeName` for diagnostic
455
+ * messages and middleware error handling.
456
+ *
457
+ * @example
458
+ * ```ts
459
+ * import { GuardDenied, policy } from "@graphrefly/graphrefly-ts";
460
+ *
461
+ * const guard = policy((allow) => { allow("observe"); });
462
+ * try {
463
+ * if (!guard({ type: "llm", id: "agent-1" }, "write")) {
464
+ * throw new GuardDenied(
465
+ * { actor: { type: "llm", id: "agent-1" }, action: "write", nodeName: "userInput" },
466
+ * );
467
+ * }
468
+ * } catch (e) {
469
+ * if (e instanceof GuardDenied) console.error(e.action, e.actor.type); // "write" "llm"
470
+ * }
471
+ * ```
472
+ */
473
+ declare class GuardDenied extends Error {
474
+ readonly actor: Actor;
475
+ readonly action: GuardAction;
476
+ readonly nodeName?: string;
477
+ /**
478
+ * @param details - Actor, action, and optional node name for the denial.
479
+ * @param message - Optional override for the default error message.
480
+ */
481
+ constructor(details: GuardDeniedDetails, message?: string);
482
+ /** Qualified registry path when known (roadmap diagnostics: same as {@link nodeName}). */
483
+ get node(): string | undefined;
484
+ }
485
+ type Where = (actor: Actor) => boolean;
486
+ type PolicyAllow = (action: GuardAction | readonly GuardAction[], opts?: {
487
+ where?: Where;
488
+ }) => void;
489
+ type PolicyDeny = (action: GuardAction | readonly GuardAction[], opts?: {
490
+ where?: Where;
491
+ }) => void;
492
+ type PolicyRuleData = {
493
+ effect: "allow" | "deny";
494
+ action: GuardAction | readonly GuardAction[];
495
+ actorType?: string | readonly string[];
496
+ actorId?: string | readonly string[];
497
+ claims?: Record<string, unknown>;
498
+ };
499
+ /**
500
+ * Declarative guard builder. Precedence: any matching **deny** blocks even if an allow also matches.
501
+ * If no rule matches, the guard returns `false` (deny-by-default). Aligned with graphrefly-py `policy()`.
502
+ *
503
+ * @param build - Callback that registers `allow(...)` / `deny(...)` rules in order.
504
+ * @returns A `NodeGuard` for use as `node({ guard })`.
505
+ *
506
+ * @example
507
+ * ```ts
508
+ * const guard = policy((allow, deny) => {
509
+ * allow("observe");
510
+ * deny("write", { where: (a) => a.type === "llm" });
511
+ * });
512
+ * ```
513
+ */
514
+ declare function policy(build: (allow: PolicyAllow, deny: PolicyDeny) => void): NodeGuard;
515
+ /**
516
+ * Rebuild a declarative guard from persisted policy data (snapshot-safe).
517
+ *
518
+ * Rules are deny-overrides, same semantics as {@link policy}.
519
+ */
520
+ declare function policyFromRules(rules: readonly PolicyRuleData[]): NodeGuard;
521
+ /**
522
+ * Derives a best-effort `meta.access` hint string by probing `guard` with the
523
+ * standard actor types `human`, `llm`, `wallet`, `system` for the `"write"` action
524
+ * (roadmap 1.5). Aligned with graphrefly-py `access_hint_for_guard`.
525
+ *
526
+ * @param guard - Guard function to probe (typically from {@link policy}).
527
+ * @returns `"restricted"` when no standard type is allowed; `"both"` when both
528
+ * `human` and `llm` are allowed (plus optionally `system`); the single allowed
529
+ * type name when only one passes; or a `"+"` joined list otherwise.
530
+ *
531
+ * @example
532
+ * ```ts
533
+ * import { policy, accessHintForGuard } from "@graphrefly/graphrefly-ts";
534
+ *
535
+ * const guardBoth = policy((allow) => { allow("write"); });
536
+ * accessHintForGuard(guardBoth); // "both"
537
+ *
538
+ * const guardHuman = policy((allow) => {
539
+ * allow("write", { where: (a) => a.type === "human" });
540
+ * });
541
+ * accessHintForGuard(guardHuman); // "human"
542
+ * ```
543
+ */
544
+ declare function accessHintForGuard(guard: NodeGuard): string;
545
+
546
+ /**
547
+ * `NodeImpl` — the single GraphReFly node primitive.
548
+ *
549
+ * Per-dep state lives in a `DepRecord[]` — one entry per declared dep —
550
+ * consolidating subscription cleanup, latest-data tracking, dirty/settled
551
+ * flags, and terminal state into a single structure per dep.
552
+ *
553
+ * This file also owns the default singleton handlers (`defaultOnMessage`,
554
+ * `defaultOnSubscribe`), the `defaultConfig` instance, and the public
555
+ * `configure(...)` entry point. They live here because their bodies touch
556
+ * `NodeImpl` internals; `config.ts` stays NodeImpl-agnostic.
557
+ *
558
+ * See GRAPHREFLY-SPEC §2 and COMPOSITION-GUIDE §1/§9 for the behavior
559
+ * contract. See SESSION-foundation-redesign.md §§1–10 for design history.
560
+ */
561
+
562
+ /** Lifecycle status of a node (GRAPHREFLY-SPEC §2.2). */
563
+ type NodeStatus = "sentinel" | "pending" | "dirty" | "settled" | "resolved" | "completed" | "errored";
564
+ /** Callback that receives downstream message batches. */
565
+ type NodeSink = (messages: Messages) => void;
566
+ /**
567
+ * Observability hook events fired by a per-node inspector. Used by
568
+ * `Graph.observe(path, { causal, derived })` to build causal traces.
569
+ *
570
+ * - `"dep_message"` — fires in `_onDepMessage` before default dispatch,
571
+ * one event per message received from a dep. Includes `depIndex` and
572
+ * the raw `Message` tuple.
573
+ * - `"run"` — fires in `_execFn` just before the user fn runs. Includes
574
+ * the per-dep `prevData` snapshot that will be passed to fn.
575
+ */
576
+ type NodeInspectorHookEvent = {
577
+ kind: "dep_message";
578
+ depIndex: number;
579
+ message: Message;
580
+ } | {
581
+ kind: "run";
582
+ batchData: readonly (readonly unknown[] | undefined)[];
583
+ prevData: readonly unknown[];
584
+ };
585
+ /** Callback attached to a node for per-message/per-run inspection. */
586
+ type NodeInspectorHook = (event: NodeInspectorHookEvent) => void;
587
+ /** Describe `type` for `Graph.describe` (GRAPHREFLY-SPEC Appendix B). */
588
+ type NodeDescribeKind = "state" | "derived" | "producer" | "effect";
589
+ /** Actor/delivery context for {@link Node.down} and {@link Node.up}. */
590
+ type NodeTransportOptions = {
591
+ actor?: Actor;
592
+ /** When `true`, skips guard checks. */
593
+ internal?: boolean;
594
+ /** `signal` for `Graph.signal` deliveries; default `write`. */
595
+ delivery?: "write" | "signal";
596
+ };
597
+ /**
598
+ * Cleanup return shape from a node {@link NodeFn}.
599
+ * - `() => void` — fires before the next fn run AND on deactivation (default).
600
+ * - `{ deactivation: () => void }` — fires only on deactivation (persistent
601
+ * resources that should survive across fn re-runs).
602
+ */
603
+ type NodeFnCleanup = (() => void) | {
604
+ deactivation: () => void;
605
+ };
606
+ /**
607
+ * Fn-time context exposing per-wave metadata and a per-node persistent
608
+ * scratch pad.
609
+ *
610
+ * - `prevData[i]` — last DATA value from dep `i` as of the END of the
611
+ * previous wave (i.e. the value that was stable before this wave started).
612
+ * Use as the fallback when `data[i]` is `undefined` (not involved) or
613
+ * `[]` (RESOLVED, no new values this wave).
614
+ * `undefined` means dep `i` has never produced DATA (sentinel state).
615
+ * `null` is a valid DATA value. `undefined` is not a valid DATA value —
616
+ * the protocol reserves it as the "never sent" sentinel.
617
+ * - `ctx.prevData[i] === undefined` → dep has never produced DATA
618
+ * - `ctx.prevData[i] !== undefined` → last DATA value (may be `null`)
619
+ * - `terminalDeps[i]` — runtime shape:
620
+ * - `undefined` → dep `i` is still live.
621
+ * - `true` → dep `i` sent COMPLETE.
622
+ * - anything else → dep `i` sent ERROR, value is the error payload.
623
+ * Type is `readonly unknown[]` because `true | unknown` collapses to
624
+ * `unknown` anyway; the three states are documented contract, not type.
625
+ * - `store` — mutable bag that persists across fn runs within one activation
626
+ * cycle. Wiped on deactivation and on resubscribable terminal reset.
627
+ */
628
+ interface FnCtx {
629
+ readonly prevData: readonly unknown[];
630
+ readonly terminalDeps: readonly unknown[];
631
+ readonly store: Record<string, unknown>;
632
+ }
633
+ /**
634
+ * Compute function passed to `node(deps, fn, opts?)`.
635
+ *
636
+ * `data[i]` holds the batch of DATA values received from dep `i` during the
637
+ * current wave. Shape contract:
638
+ * - `undefined` — dep `i` was not involved in this wave (no DIRTY received).
639
+ * - `[]` — dep `i` was involved (dirtied), but settled as RESOLVED (value
640
+ * unchanged). Use `ctx.prevData[i]` to read its last known value from the
641
+ * previous wave.
642
+ * - `[v1, v2, ...]` — dep `i` sent one or more DATA values. `at(-1)` gives
643
+ * the latest; iterate for multi-emission processing.
644
+ *
645
+ * Emission is explicit via `actions.emit(v)` (sugar: equals + framing) or
646
+ * `actions.down(msgs)` (raw). Return a cleanup function (or
647
+ * `{ deactivation }`) to register teardown — any non-cleanup return value
648
+ * is ignored. The `| void` leg lets arrow-block bodies satisfy `NodeFn`
649
+ * without an explicit `return undefined`.
650
+ *
651
+ * Sugar constructors (`derived`, `effect`, `dynamicNode`) unwrap `data[i]`
652
+ * to a single scalar (`at(-1)` with `ctx.prevData[i]` fallback) so their
653
+ * user-facing fn signatures stay unchanged. Use raw `node()` when you need
654
+ * the full batch array.
655
+ */
656
+ type NodeFn = (data: readonly (readonly unknown[] | undefined)[], actions: NodeActions, ctx: FnCtx) => NodeFnCleanup | void;
657
+ /** Options accepted by every node constructor. */
658
+ interface NodeOptions<T = unknown> {
659
+ name?: string;
660
+ describeKind?: NodeDescribeKind;
661
+ equals?: (a: T, b: T) => boolean;
662
+ /**
663
+ * Pre-populate the cache at construction. `null` is a valid initial value.
664
+ * `undefined` is treated as absent (not a valid DATA payload).
665
+ */
666
+ initial?: T | null;
667
+ meta?: Record<string, unknown>;
668
+ resubscribable?: boolean;
669
+ resetOnTeardown?: boolean;
670
+ /** Auto-emit `[[COMPLETE]]` when all deps complete. Default `true`. */
671
+ completeWhenDepsComplete?: boolean;
672
+ /**
673
+ * Auto-propagate `[[ERROR]]` when any dep errors. Default `true`.
674
+ * Set `false` only for rescue/catchError operators that handle errors
675
+ * explicitly via `ctx.terminalDeps`.
676
+ */
677
+ errorWhenDepsError?: boolean;
678
+ /**
679
+ * Tier-2 PAUSE/RESUME handling.
680
+ * - `true` (default): wave completion suppressed while paused; fn fires
681
+ * once on RESUME if gate is satisfied.
682
+ * - `false`: node ignores PAUSE (sources like timers that must keep running).
683
+ * - `"resumeAll"`: on RESUME, replay every buffered DATA (future).
684
+ */
685
+ pausable?: boolean | "resumeAll";
686
+ guard?: NodeGuard;
687
+ versioning?: VersioningLevel;
688
+ versioningId?: string;
689
+ versioningHash?: HashFn;
690
+ /**
691
+ * Override the config instance this node binds to. Defaults to
692
+ * {@link defaultConfig}. Useful for test isolation and custom protocol
693
+ * stacks. The first node that reads any hook on the config freezes it.
694
+ */
695
+ config?: GraphReFlyConfig;
696
+ }
697
+ /** A reactive node in the GraphReFly protocol. */
698
+ interface Node<T = unknown> {
699
+ readonly name?: string;
700
+ readonly status: NodeStatus;
701
+ /**
702
+ * Current cached value. Returns `undefined` when the node is in
703
+ * `"sentinel"` state (no DATA ever emitted). v5 reserves `undefined`
704
+ * globally as the sentinel value — the valid DATA type is `T | null`.
705
+ * Therefore `node.cache === undefined` is a valid "never emitted" guard.
706
+ * `node.status` distinguishes the richer states (`"sentinel"`,
707
+ * `"settled"`, `"errored"`, etc.) when you need more than has-value.
708
+ */
709
+ readonly cache: T | null | undefined;
710
+ readonly meta: Record<string, Node>;
711
+ readonly lastMutation: Readonly<{
712
+ actor: Actor;
713
+ timestamp_ns: number;
714
+ }> | undefined;
715
+ readonly v: Readonly<NodeVersionInfo> | undefined;
716
+ /**
717
+ * Send one or more messages downstream. Accepts either a single
718
+ * {@link Message} tuple (e.g. `node.down([DATA, 42])`) or a
719
+ * {@link Messages} array of tuples (e.g.
720
+ * `node.down([[DIRTY], [DATA, 42]])`). One call = one wave: the
721
+ * emit pipeline tier-sorts the input, auto-prefixes `[DIRTY]` when
722
+ * any tier-3 payload is present and the node is not already dirty,
723
+ * runs equals substitution against the live cache (§3.5.1), then
724
+ * dispatches to sinks with phase deferral.
725
+ */
726
+ down(messageOrMessages: Message | Messages, options?: NodeTransportOptions): void;
727
+ /**
728
+ * Sugar for `down([[DATA, value]])`. One wave with a single DATA
729
+ * payload — the pipeline adds the synthetic DIRTY prefix and runs
730
+ * equals substitution against the live cache.
731
+ */
732
+ emit(value: T | undefined | null, options?: NodeTransportOptions): void;
733
+ /**
734
+ * Send one or more messages upstream. Accepts the same shapes as
735
+ * {@link down}. Upstream messages are tier <3 + tier 5 only
736
+ * (DIRTY, INVALIDATE, PAUSE, RESUME, TEARDOWN); tier-3/4 payloads
737
+ * throw — DATA/RESOLVED/COMPLETE/ERROR are downstream-only in this
738
+ * protocol. No equals substitution, no cache advance, no DIRTY
739
+ * auto-prefix — the up direction just forwards to every dep.
740
+ */
741
+ up?(messageOrMessages: Message | Messages, options?: NodeTransportOptions): void;
742
+ subscribe(sink: NodeSink, actor?: Actor): () => void;
743
+ allowsObserve(actor: Actor): boolean;
744
+ hasGuard(): boolean;
745
+ }
746
+ /**
747
+ * Per-dep runtime state. One entry per upstream node.
748
+ *
749
+ * `terminal` is the single terminal-state slot, shaped to match
750
+ * {@link FnCtx.terminalDeps}:
751
+ * - `undefined` — dep is still live.
752
+ * - `true` — dep sent COMPLETE.
753
+ * - anything else — dep sent ERROR with that payload.
754
+ *
755
+ * Edge case: an ERROR carrying an `undefined` payload is indistinguishable
756
+ * from "live". Pass meaningful error values (Error objects, domain tags).
757
+ */
758
+ interface DepRecord {
759
+ readonly node: Node;
760
+ unsub: (() => void) | null;
761
+ /**
762
+ * Last DATA value from this dep as of the end of the previous completed
763
+ * wave. `undefined` until dep has produced at least one DATA (sentinel).
764
+ * Committed by `_execFn` after snapshotting `ctx.prevData` and before
765
+ * `_clearWaveFlags`. `undefined` is reserved as the "never sent" sentinel —
766
+ * `undefined` is not a valid DATA payload.
767
+ */
768
+ prevData: unknown;
769
+ /** True while awaiting DATA/RESOLVED for the current wave. */
770
+ dirty: boolean;
771
+ /**
772
+ * True if this dep was dirtied in the current wave (set in `_depDirtied`,
773
+ * cleared in `_clearWaveFlags`). Distinguishes "RESOLVED" (`involvedThisWave
774
+ * && dataBatch.length === 0`) from "not involved" (`!involvedThisWave`) in
775
+ * the `data[i]` batch snapshot passed to fn.
776
+ */
777
+ involvedThisWave: boolean;
778
+ /**
779
+ * DATA values accumulated from this dep during the current wave.
780
+ * Populated by `_depSettledAsData`, cleared by `_clearWaveFlags`.
781
+ * Snapshotted (copied) by `_execFn` before `_clearWaveFlags` runs so
782
+ * that fn always sees the full wave batch.
783
+ */
784
+ dataBatch: unknown[];
785
+ /** Terminal-state slot — see JSDoc on {@link DepRecord}. */
786
+ terminal: unknown;
787
+ }
788
+ /**
789
+ * Default {@link GraphReFlyConfig} instance. Every `NodeImpl` constructed
790
+ * without an explicit `opts.config` binds to this instance and freezes it
791
+ * on first hook access.
792
+ */
793
+ declare const defaultConfig: GraphReFlyConfig;
794
+ /**
795
+ * Apply configuration to {@link defaultConfig}. Must be called before the
796
+ * first node is created — otherwise throws. Custom message types, hook
797
+ * overrides, etc. go through here at app startup.
798
+ *
799
+ * ```ts
800
+ * configure((cfg) => {
801
+ * cfg.registerMessageType(MY_TYPE, { tier: 3 });
802
+ * cfg.onMessage = (node, msg, ctx, actions) => { ... };
803
+ * });
804
+ * ```
805
+ */
806
+ declare function configure(fn: (cfg: GraphReFlyConfig) => void): void;
807
+
808
+ /**
809
+ * Single-class node implementation. Covers state, producer, derived, effect,
810
+ * and passthrough shapes. See `sugar.ts` for ergonomic factories and
811
+ * `dynamicNode()` (sugar-level wrapper around plain `NodeImpl`).
812
+ */
813
+ declare class NodeImpl<T = unknown> implements Node<T> {
814
+ readonly _optsName: string | undefined;
815
+ readonly _describeKind: NodeDescribeKind | undefined;
816
+ readonly meta: Record<string, Node>;
817
+ /**
818
+ * Cached `Object.keys(meta).length > 0` check. `meta` is frozen at
819
+ * construction so this boolean never flips. Used by `_emit` to skip
820
+ * the meta TEARDOWN fan-out block allocation on the common "no meta"
821
+ * hot path.
822
+ */
823
+ readonly _hasMeta: boolean;
824
+ readonly _config: GraphReFlyConfig;
825
+ /** Mutable for autoTrackNode / Graph.connect() post-construction dep addition. */
826
+ _deps: DepRecord[];
827
+ _sinks: NodeSink | Set<NodeSink> | null;
828
+ _sinkCount: number;
829
+ _cached: T | undefined;
830
+ _status: NodeStatus;
831
+ _cleanup: NodeFnCleanup | undefined;
832
+ _store: Record<string, unknown>;
833
+ _waveHasNewData: boolean;
834
+ _hasNewTerminal: boolean;
835
+ _hasCalledFnOnce: boolean;
836
+ _paused: boolean;
837
+ _pendingWave: boolean;
838
+ _isExecutingFn: boolean;
839
+ _pendingRerun: boolean;
840
+ _rerunDepth: number;
841
+ /**
842
+ * Count of deps currently in `dirty === true`. `_maybeRunFnOnSettlement`
843
+ * treats `0` as "wave settled" — O(1) check for full dep settlement.
844
+ */
845
+ _dirtyDepCount: number;
846
+ /**
847
+ * Inside an explicit `batch(() => ...)` scope, every `_emit` accumulates
848
+ * its already-framed messages here instead of dispatching synchronously.
849
+ * At batch end, `_flushBatchPending` runs (registered via
850
+ * `registerBatchFlushHook`) and delivers the whole accumulated batch as
851
+ * one `downWithBatch` call — collapsing what would otherwise be K
852
+ * separate sink invocations into one. This is the fix for the diamond
853
+ * fan-in K+1 over-fire.
854
+ *
855
+ * `null` outside batch (or after flush). Only ever appended to within
856
+ * a single explicit batch lifetime; reset to `null` on flush. State
857
+ * updates (cache, version, status) still happen per-emit via
858
+ * `_updateState` — only the downstream delivery is coalesced.
859
+ */
860
+ _batchPendingMessages: Message[] | null;
861
+ /**
862
+ * Set of active pause locks held against this node. Every `[PAUSE, lockId]`
863
+ * adds its `lockId` to the set; every `[RESUME, lockId]` removes it.
864
+ * `_paused` is a derived quantity: `_pauseLocks.size > 0`. Multi-pauser
865
+ * correctness — one controller releasing its lock does NOT resume the
866
+ * node while another controller still holds its lock.
867
+ */
868
+ _pauseLocks: Set<unknown> | null;
869
+ /**
870
+ * Buffered DATA messages held while paused. Only populated when
871
+ * `_pausable === "resumeAll"` (bufferAll mode). On final lock release
872
+ * the buffer is replayed through the node's outgoing pipeline in the
873
+ * order received. Non-bufferAll pause mode drops DATA on the floor
874
+ * (upstream is expected to honor PAUSE by suppressing production).
875
+ */
876
+ _pauseBuffer: Message[] | null;
877
+ readonly _fn: NodeFn | undefined;
878
+ readonly _equals: (a: T, b: T) => boolean;
879
+ readonly _resubscribable: boolean;
880
+ readonly _resetOnTeardown: boolean;
881
+ readonly _autoComplete: boolean;
882
+ readonly _autoError: boolean;
883
+ readonly _pausable: boolean | "resumeAll";
884
+ readonly _guard: NodeGuard | undefined;
885
+ _hashFn: HashFn;
886
+ _versioning: NodeVersionInfo | undefined;
887
+ /**
888
+ * Explicit versioning level, tracked separately from `_versioning` so
889
+ * monotonicity checks and future v2/v3 extensions don't rely on the
890
+ * fragile `"cid" in _versioning` shape discriminator. `undefined` means
891
+ * the node has no versioning attached; `0` / `1` / future levels name
892
+ * the tier. Mutated in lockstep with `_versioning` by the constructor
893
+ * and by `_applyVersioning`.
894
+ */
895
+ _versioningLevel: VersioningLevel | undefined;
896
+ _lastMutation: {
897
+ actor: Actor;
898
+ timestamp_ns: number;
899
+ } | undefined;
900
+ /**
901
+ * @internal Per-node inspector hooks for `Graph.observe(path,
902
+ * { causal, derived })`. Fires in `_onDepMessage` and `_execFn`.
903
+ * Attached via `_setInspectorHook` (returns a disposer). Multiple
904
+ * observers can attach simultaneously — all registered hooks fire for
905
+ * every event.
906
+ */
907
+ _inspectorHooks: Set<NodeInspectorHook> | undefined;
908
+ readonly _actions: NodeActions;
909
+ constructor(deps: readonly Node[], fn: NodeFn | undefined, opts: NodeOptions<T>);
910
+ private get _isTerminal();
911
+ get name(): string | undefined;
912
+ get status(): NodeStatus;
913
+ get cache(): T | undefined | null;
914
+ get lastMutation(): Readonly<{
915
+ actor: Actor;
916
+ timestamp_ns: number;
917
+ }> | undefined;
918
+ get v(): Readonly<NodeVersionInfo> | undefined;
919
+ hasGuard(): boolean;
920
+ /**
921
+ * @internal Retroactively attach (or upgrade) versioning state on this
922
+ * node. Intended for `Graph.setVersioning(level)` bulk application and
923
+ * for rare cases where a specific node needs to be bumped to a higher
924
+ * level (e.g., `v0 → v1`) after construction.
925
+ *
926
+ * **Safety:** the mutation is rejected mid-wave. Specifically,
927
+ * throws if the node is currently executing its fn (`_isExecutingFn`).
928
+ * Callers at quiescent points — before the first sink subscribes, or
929
+ * after all sinks unsubscribe, or between external `down()` / `emit()`
930
+ * invocations — are safe. The re-entrance window that motivated §10.6.4
931
+ * removal was the "transition `_versioning` from `undefined` to a fresh
932
+ * object mid-`_updateState`" case; that path is now guarded.
933
+ *
934
+ * **Monotonicity:** levels can only go up. Downgrade (e.g., `v1 → v0`)
935
+ * is a no-op — once a node carries higher-level metadata, dropping it
936
+ * mid-graph would tear the linked-history invariant for v1 and above.
937
+ *
938
+ * **Linked-history boundary (D1, 2026-04-13):** upgrading v0 → v1
939
+ * produces a **fresh history root**. The new v1 state has `cid =
940
+ * hash(currentCachedValue)` and `prev = null`, not a synthetic `prev`
941
+ * anchored to any previous v0 value. The v0 monotonic `version` counter
942
+ * is preserved across the upgrade, but the linked-cid chain (spec §7)
943
+ * starts fresh at the upgrade point. Downstream audit tools that walk
944
+ * `v.cid.prev` backwards through time will see a `null` boundary at
945
+ * the upgrade — **this is intentional**: v0 had no cid to link to, and
946
+ * fabricating one would lie about the hash. Callers that require an
947
+ * unbroken cid chain from birth must attach versioning at construction
948
+ * via `opts.versioning` or `config.defaultVersioning`, not retroactively.
949
+ *
950
+ * @param level - New minimum versioning level.
951
+ * @param opts - Optional id / hash overrides; applied only if the
952
+ * node currently has no versioning state.
953
+ */
954
+ _applyVersioning(level: VersioningLevel, opts?: {
955
+ id?: string;
956
+ hash?: HashFn;
957
+ }): void;
958
+ /**
959
+ * @internal Attach an inspector hook. Returns a disposer that removes
960
+ * the hook. Used by `Graph.observe(path, { causal, derived })` to build
961
+ * causal traces. Multiple hooks may be attached concurrently — all fire
962
+ * for every event in registration order. Passing `undefined` is a no-op
963
+ * and returns a no-op disposer.
964
+ */
965
+ _setInspectorHook(hook?: NodeInspectorHook): () => void;
966
+ allowsObserve(actor: Actor): boolean;
967
+ private _checkGuard;
968
+ down(messageOrMessages: Message | Messages, options?: NodeTransportOptions): void;
969
+ emit(value: T | undefined | null, options?: NodeTransportOptions): void;
970
+ up(messageOrMessages: Message | Messages, options?: NodeTransportOptions): void;
971
+ /**
972
+ * @internal Internal up-path used by `actions.up(...)` from inside fn.
973
+ * Same tier validation as public `up`, but bypasses the guard check
974
+ * since the fn context is already inside an authorized operation.
975
+ */
976
+ private _emitUp;
977
+ /**
978
+ * @internal Enforce spec §1.2 — up-direction messages are restricted to
979
+ * tier 0–2 and tier 5 (START, DIRTY, INVALIDATE, PAUSE, RESUME,
980
+ * TEARDOWN). Tier 3 (DATA/RESOLVED) and tier 4 (COMPLETE/ERROR) are
981
+ * downstream-only. Emitting tier-3/4 via `up` would bypass equals
982
+ * substitution and cache advance entirely and is a protocol bug.
983
+ */
984
+ private _validateUpTiers;
985
+ subscribe(sink: NodeSink, actor?: Actor): () => void;
986
+ private _removeSink;
987
+ /**
988
+ * @internal First-sink activation. For a producer (no deps + fn),
989
+ * invokes fn once. For a compute node (has deps), subscribes to every
990
+ * dep with the pre-set-dirty trick so the first-run gate waits for
991
+ * every dep to settle at least once.
992
+ */
993
+ _activate(): void;
994
+ /**
995
+ * @internal Append a dep post-construction. Used by `autoTrackNode`
996
+ * (runtime dep discovery) and `Graph.connect()` (post-construction
997
+ * wiring). Subscribes immediately — if DATA arrives synchronously
998
+ * during subscribe and fn is currently executing, the re-run is
999
+ * deferred via `_pendingRerun` flag (see `_execFn` guard).
1000
+ *
1001
+ * **Dedup:** idempotent on duplicate `depNode` — if `depNode` is
1002
+ * already in `_deps`, returns the existing index without mutating
1003
+ * state. Callers can safely invoke `_addDep` without their own
1004
+ * "already added" check. `autoTrackNode` still keeps a `depIndexMap`
1005
+ * as a fast-path lookup for known deps (returning cached `data[idx]`
1006
+ * without calling `_addDep` at all); this internal dedup is the
1007
+ * backstop for any caller that doesn't track its own dep set.
1008
+ *
1009
+ * @returns The index of the new dep in `_deps`, or the existing index
1010
+ * if the dep was already present.
1011
+ */
1012
+ _addDep(depNode: Node): number;
1013
+ /**
1014
+ * @internal Unsubscribes from deps, fires fn cleanup (both shapes),
1015
+ * clears wave/store state, and (for compute nodes) drops `_cached` per
1016
+ * the ROM/RAM rule. Idempotent: second call is a no-op.
1017
+ *
1018
+ * @param skipStatusUpdate — When `true`, the caller takes responsibility
1019
+ * for setting `_status` after deactivation (e.g. TEARDOWN always sets
1020
+ * `"sentinel"` unconditionally). When `false` (default), deactivation
1021
+ * applies the ROM rule: compute nodes → `"sentinel"`, state nodes
1022
+ * preserve their current status.
1023
+ */
1024
+ _deactivate(skipStatusUpdate?: boolean): void;
1025
+ /**
1026
+ * @internal Default per-tier dispatch for incoming dep messages. Called
1027
+ * by `defaultOnMessage`. Updates the DepRecord, triggers wave
1028
+ * completion, and forwards passthrough traffic.
1029
+ */
1030
+ _onDepMessage(depIndex: number, msg: Message): void;
1031
+ /**
1032
+ * Called when a dep transitions `dirty: false → true` (either from an
1033
+ * incoming DIRTY, or pre-set during `_activate` / `_addDep` /
1034
+ * `_depInvalidated`). No-op if the dep is already dirty. Fires the
1035
+ * downstream DIRTY emit if we're the first to dirty this wave.
1036
+ */
1037
+ private _depDirtied;
1038
+ /**
1039
+ * Called when a dep delivers new DATA: clears dirty, stores the payload,
1040
+ * marks wave-has-data, and — if this is the dep's first DATA — clears
1041
+ * its sentinel slot so the first-run gate can open.
1042
+ */
1043
+ private _depSettledAsData;
1044
+ /**
1045
+ * Called when a dep emits RESOLVED (wave settled, value unchanged).
1046
+ * Clears dirty; does NOT touch `prevData` / `terminal` / sentinel
1047
+ * count — sentinel only exits on first DATA or terminal, not RESOLVED.
1048
+ */
1049
+ private _depSettledAsResolved;
1050
+ /**
1051
+ * Called when a dep delivers COMPLETE (`terminal = true`) or ERROR
1052
+ * (`terminal = errorPayload`). Clears dirty, stores the terminal, and
1053
+ * — if the dep had never contributed a DATA yet — leaves sentinel
1054
+ * since the gate treats "terminated without data" as gate-open too.
1055
+ */
1056
+ private _depSettledAsTerminal;
1057
+ /**
1058
+ * Called when a dep emits INVALIDATE: clears prevData, terminal, and
1059
+ * dataBatch. The dep is now back in the "never delivered a real value"
1060
+ * state — `prevData === undefined` so the sentinel check in fn will fire.
1061
+ */
1062
+ private _depInvalidated;
1063
+ private _maybeRunFnOnSettlement;
1064
+ private _maybeAutoTerminalAfterWave;
1065
+ /**
1066
+ * @internal Runs the node fn once. Default cleanup (function form) fires
1067
+ * before the new run; `{ deactivation }` cleanup survives.
1068
+ */
1069
+ private _execFn;
1070
+ private _clearWaveFlags;
1071
+ private _wrapFnError;
1072
+ /**
1073
+ * @internal Stable tier sort + synthetic DIRTY prefix for an outgoing
1074
+ * batch. Fast path: already-monotone single-tier batches (the common
1075
+ * case from interned singletons like `DIRTY_ONLY_BATCH`) return the
1076
+ * input unchanged. General path: decorate-sort-undecorate into a new
1077
+ * array, then prepend `[DIRTY]` after any tier-0 START entries when
1078
+ * a tier-3 payload is present and the node isn't already dirty.
1079
+ *
1080
+ * Single source of truth for the spec §1.3.1 framing invariant. Every
1081
+ * outgoing path hits `_frameBatch` exactly once via `_emit`.
1082
+ */
1083
+ private _frameBatch;
1084
+ /**
1085
+ * @internal The unified dispatch waist — one call = one wave.
1086
+ *
1087
+ * Pipeline stages, in order:
1088
+ *
1089
+ * 1. Early-return on empty batch.
1090
+ * 2. Terminal filter — post-COMPLETE/ERROR only TEARDOWN/INVALIDATE
1091
+ * still propagate so graph teardown and cache-clear still work.
1092
+ * 3. Tier sort (stable) — the batch can be in any order when it
1093
+ * arrives; the walker downstream (`downWithBatch`) assumes
1094
+ * ascending tier monotone, and so does `_updateState`'s tier-3
1095
+ * slice walk. This is the single source of truth for ordering.
1096
+ * 4. Synthetic DIRTY prefix — if a tier-3 payload is present, no
1097
+ * DIRTY is already in the batch, and the node isn't already in
1098
+ * `"dirty"` status, prepend `[DIRTY]` after any tier-0 START
1099
+ * entries. Guarantees spec §1.3.1 (DIRTY precedes DATA within
1100
+ * the same batch) uniformly across every entry point.
1101
+ * 5. PAUSE/RESUME lock bookkeeping (C0) — update `_pauseLocks`,
1102
+ * derive `_paused`, filter unknown-lockId RESUME, replay
1103
+ * bufferAll buffer on final lock release.
1104
+ * 6. Meta TEARDOWN fan-out — notify meta children before
1105
+ * `_updateState`'s TEARDOWN branch calls `_deactivate`. Hoisted
1106
+ * out of the walk to keep `_updateState` re-entrance-free.
1107
+ * 7. `_updateState` — walk the batch in tier order, advancing
1108
+ * `_cached` / `_status` / `_versioning` and running equals
1109
+ * substitution on tier-3 DATA (§3.5.1). Returns
1110
+ * `{finalMessages, equalsError?}`.
1111
+ * 8. `downWithBatch` dispatch (or bufferAll capture if paused with
1112
+ * `pausable: "resumeAll"`).
1113
+ * 9. Recursive ERROR emission if equals threw mid-walk.
1114
+ *
1115
+ * `node.down` / `node.emit` / `actions.down` / `actions.emit` all
1116
+ * converge here — the unified `_emit` waist (spec §1.3.1).
1117
+ */
1118
+ _emit(messages: Messages): void;
1119
+ /**
1120
+ * @internal Walk an outgoing (already-framed) batch, updating own
1121
+ * cache / status / versioning and running equals substitution on
1122
+ * every tier-3 DATA (§3.5.1). Framing — tier sort and synthetic
1123
+ * DIRTY prefix — has already happened upstream in `_frameBatch`.
1124
+ * This walk trusts the input is in monotone tier order and that the
1125
+ * spec §1.3.1 DIRTY/RESOLVED precedence invariant is already
1126
+ * satisfied by the frame.
1127
+ *
1128
+ * Equals substitution: every DATA payload is compared against the
1129
+ * live `_cached`; when equal, the tuple is rewritten to `[RESOLVED]`
1130
+ * in a per-call copy and cache is not re-advanced. `.cache` remains
1131
+ * coherent with "the last DATA payload this node actually sent
1132
+ * downstream".
1133
+ *
1134
+ * Returns `{ finalMessages, equalsError? }`:
1135
+ * - `finalMessages` — the array to deliver to sinks (may be
1136
+ * `messages` unchanged, a rewritten copy with DATA→RESOLVED
1137
+ * substitutions, or a truncated prefix when equals throws mid-walk).
1138
+ * - `equalsError` — present only when the configured `equals` function
1139
+ * threw on some DATA message. `_emit` delivers the prefix first,
1140
+ * then emits a fresh ERROR batch via a recursive `_emit` call so
1141
+ * subscribers observe `[...walked_prefix, ERROR]` in order.
1142
+ */
1143
+ private _updateState;
1144
+ private _deliverToSinks;
1145
+ /**
1146
+ * @internal Dispatch entry point that respects the per-batch emit
1147
+ * accumulator (Bug 2). Inside an explicit `batch()` scope, append to
1148
+ * `_batchPendingMessages` and register a flush hook on first append.
1149
+ * Outside batch — or during a drain (where `flushInProgress` is true
1150
+ * but `batchDepth` is 0) — dispatch synchronously through `downWithBatch`.
1151
+ *
1152
+ * Per-emit state updates (`_frameBatch`, `_updateState`) have already
1153
+ * happened by the time we reach here; only the **downstream delivery**
1154
+ * is coalesced. Cache, version, and status are visible mid-batch on
1155
+ * the emitting node itself.
1156
+ */
1157
+ private _dispatchOrAccumulate;
1158
+ /**
1159
+ * @internal Flushes the accumulated batch through `downWithBatch` and
1160
+ * clears the pending state. Idempotent — safe to call when pending is
1161
+ * already null or empty (e.g. on a `batch()` throw, where the hook
1162
+ * fires for cleanup but the drainPhase queues are wiped after).
1163
+ *
1164
+ * Critical: the accumulated batch is interleaved per-emit framings like
1165
+ * `[DIRTY, DATA(1), DIRTY, DATA(2)]` — non-monotone tier order. We must
1166
+ * re-frame to sort by tier before handing to `downWithBatch`, which
1167
+ * assumes pre-sorted input. `_frameBatch` also handles the synthetic
1168
+ * DIRTY prepend rule (no-op here — `hasDirty` is true since each
1169
+ * accumulated emit already carries its own DIRTY prefix).
1170
+ */
1171
+ private _flushBatchPending;
1172
+ }
1173
+ /**
1174
+ * Creates a reactive {@link Node} — the single GraphReFly primitive (§2).
1175
+ *
1176
+ * Typical shapes:
1177
+ * - `node([])` / `node({ initial: v })` — a manual source (state node).
1178
+ * - `node(producerFn, opts)` — a producer that runs on first-subscribe.
1179
+ * - `node(deps, computeFn, opts)` — a derived / effect node.
1180
+ *
1181
+ * For value-returning computations, prefer the sugar factories in `sugar.ts`
1182
+ * (`state`, `derived`, `effect`, `producer`, `dynamicNode`), which wrap user
1183
+ * fns with `actions.emit(userFn(data))`. Calling `node()` directly gives you
1184
+ * the raw `NodeFn` contract: explicit emission via `actions`, cleanup return.
1185
+ */
1186
+ declare function node<T = unknown>(depsOrFn?: readonly Node[] | NodeFn | NodeOptions<T>, fnOrOpts?: NodeFn | NodeOptions<T>, optsArg?: NodeOptions<T>): Node<T>;
1187
+
1188
+ export { START_MSG as $, type Actor as A, type NodeInspectorHook as B, COMPLETE as C, DATA as D, ERROR as E, type FnCtx as F, type GlobalInspectorEvent as G, type HashFn as H, INVALIDATE as I, type NodeInspectorHookEvent as J, type NodeSink as K, type NodeStatus as L, type Message as M, type Node as N, type NodeTransportOptions as O, type NodeVersionInfo as P, type OnMessageHandler as Q, type OnSubscribeHandler as R, PAUSE as S, type PolicyAllow as T, type PolicyDeny as U, type PolicyRuleData as V, RESOLVED as W, RESOLVED_MSG as X, RESOLVED_ONLY_BATCH as Y, RESUME as Z, START as _, type NodeOptions as a, type SubscribeContext as a0, TEARDOWN as a1, TEARDOWN_MSG as a2, TEARDOWN_ONLY_BATCH as a3, type V0 as a4, type V1 as a5, type VersioningLevel as a6, type VersioningOptions as a7, accessHintForGuard as a8, advanceVersion as a9, configure as aa, createVersioning as ab, defaultConfig as ac, defaultHash as ad, isV1 as ae, node as af, normalizeActor as ag, policy as ah, policyFromRules as ai, registerBuiltins as aj, type NodeActions as b, COMPLETE_MSG as c, COMPLETE_ONLY_BATCH as d, DEFAULT_ACTOR as e, DIRTY as f, DIRTY_MSG as g, DIRTY_ONLY_BATCH as h, type DepRecord as i, type GlobalInspectorHook as j, GraphReFlyConfig as k, type GuardAction as l, GuardDenied as m, type GuardDeniedDetails as n, INVALIDATE_MSG as o, INVALIDATE_ONLY_BATCH as p, type MessageContext as q, type MessageTypeRegistration as r, type MessageTypeRegistrationInput as s, type Messages as t, type NodeCtx as u, type NodeDescribeKind as v, type NodeFn as w, type NodeFnCleanup as x, type NodeGuard as y, NodeImpl as z };