@graphrefly/graphrefly 0.47.0 → 0.47.2

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 (187) hide show
  1. package/dist/base/composition/index.cjs +24 -16
  2. package/dist/base/composition/index.cjs.map +1 -1
  3. package/dist/base/composition/index.js +6 -6
  4. package/dist/base/index.cjs +142 -86
  5. package/dist/base/index.cjs.map +1 -1
  6. package/dist/base/index.js +11 -11
  7. package/dist/base/io/index.cjs +114 -68
  8. package/dist/base/io/index.cjs.map +1 -1
  9. package/dist/base/io/index.js +5 -5
  10. package/dist/base/sources/browser/index.cjs +13 -9
  11. package/dist/base/sources/browser/index.cjs.map +1 -1
  12. package/dist/base/sources/browser/index.js +13 -9
  13. package/dist/base/sources/browser/index.js.map +1 -1
  14. package/dist/base/sources/event/index.cjs +1 -1
  15. package/dist/base/sources/event/index.cjs.map +1 -1
  16. package/dist/base/sources/event/index.js +1 -1
  17. package/dist/base/sources/index.cjs +21 -13
  18. package/dist/base/sources/index.cjs.map +1 -1
  19. package/dist/base/sources/index.js +3 -3
  20. package/dist/base/sources/node/index.cjs +43 -37
  21. package/dist/base/sources/node/index.cjs.map +1 -1
  22. package/dist/base/sources/node/index.js +43 -37
  23. package/dist/base/sources/node/index.js.map +1 -1
  24. package/dist/{chunk-VLAGJZSL.js → chunk-3O3NKZJW.js} +2 -2
  25. package/dist/{chunk-YJ4U2D2C.js → chunk-446I4EGD.js} +9 -7
  26. package/dist/chunk-446I4EGD.js.map +1 -0
  27. package/dist/{chunk-DKNHAICT.js → chunk-5GVURVIG.js} +14 -8
  28. package/dist/chunk-5GVURVIG.js.map +1 -0
  29. package/dist/{chunk-2OB3CEJS.js → chunk-6MRSX3YK.js} +2 -2
  30. package/dist/{chunk-EVYY4X5A.js → chunk-6ZLCPUXS.js} +2 -2
  31. package/dist/{chunk-ZVXXDWIB.js → chunk-7AVQIGF6.js} +514 -33
  32. package/dist/chunk-7AVQIGF6.js.map +1 -0
  33. package/dist/{chunk-7EGRP2VX.js → chunk-7BULJTL6.js} +2 -2
  34. package/dist/{chunk-7EGRP2VX.js.map → chunk-7BULJTL6.js.map} +1 -1
  35. package/dist/{chunk-FW23JYNQ.js → chunk-CEVNQ74M.js} +2 -2
  36. package/dist/{chunk-CGHORL6G.js → chunk-DDTS7F5O.js} +7 -5
  37. package/dist/chunk-DDTS7F5O.js.map +1 -0
  38. package/dist/{chunk-OCUDSN63.js → chunk-EL5VHUGK.js} +79 -47
  39. package/dist/chunk-EL5VHUGK.js.map +1 -0
  40. package/dist/{chunk-4GYMCUDZ.js → chunk-EP4WVQLX.js} +5 -5
  41. package/dist/{chunk-SOOKUYVM.js → chunk-F7EKHR32.js} +13 -9
  42. package/dist/chunk-F7EKHR32.js.map +1 -0
  43. package/dist/{chunk-JKTC747G.js → chunk-FQSQONOU.js} +4 -4
  44. package/dist/{chunk-JGFRAFDL.js → chunk-FVINAAKA.js} +3 -3
  45. package/dist/{chunk-RAGGHLCV.js → chunk-GUNIRPEJ.js} +8 -6
  46. package/dist/{chunk-RAGGHLCV.js.map → chunk-GUNIRPEJ.js.map} +1 -1
  47. package/dist/{chunk-BU3SEFA5.js → chunk-IOJDYUA7.js} +2 -2
  48. package/dist/{chunk-Y52CS6YA.js → chunk-JA67ZQG2.js} +2 -2
  49. package/dist/{chunk-Y52CS6YA.js.map → chunk-JA67ZQG2.js.map} +1 -1
  50. package/dist/{chunk-DM4OMPWK.js → chunk-KNU73RZW.js} +2 -2
  51. package/dist/{chunk-GWRNLJNW.js → chunk-KRFGO5QH.js} +19 -15
  52. package/dist/{chunk-GWRNLJNW.js.map → chunk-KRFGO5QH.js.map} +1 -1
  53. package/dist/{chunk-Z4YXAUDN.js → chunk-KUFXLAEY.js} +11 -7
  54. package/dist/{chunk-Z4YXAUDN.js.map → chunk-KUFXLAEY.js.map} +1 -1
  55. package/dist/{chunk-Z6EGP5D7.js → chunk-LDCSZ72P.js} +2 -2
  56. package/dist/{chunk-5IMMNARC.js → chunk-MS3WPRJR.js} +37 -25
  57. package/dist/chunk-MS3WPRJR.js.map +1 -0
  58. package/dist/{chunk-CXANAIZU.js → chunk-N65E26UL.js} +3 -3
  59. package/dist/{chunk-O3MT7DYI.js → chunk-N6MNJNHB.js} +2 -2
  60. package/dist/{chunk-Q3EYOCZB.js → chunk-NPRP3MCV.js} +111 -2
  61. package/dist/chunk-NPRP3MCV.js.map +1 -0
  62. package/dist/{chunk-A7KV5UK4.js → chunk-OXD5LFQP.js} +2 -2
  63. package/dist/{chunk-ZT4WMQW4.js → chunk-PTWADEH3.js} +9 -7
  64. package/dist/chunk-PTWADEH3.js.map +1 -0
  65. package/dist/{chunk-IHTWQEDR.js → chunk-QFE5BQH7.js} +2 -2
  66. package/dist/{chunk-IHTWQEDR.js.map → chunk-QFE5BQH7.js.map} +1 -1
  67. package/dist/{chunk-22SG74BD.js → chunk-R6ZCSXKX.js} +3 -3
  68. package/dist/{chunk-PZWISPIQ.js → chunk-S7HN5FHL.js} +17 -11
  69. package/dist/chunk-S7HN5FHL.js.map +1 -0
  70. package/dist/{chunk-RJOG4IJU.js → chunk-T7SP3EYR.js} +18 -12
  71. package/dist/chunk-T7SP3EYR.js.map +1 -0
  72. package/dist/{chunk-4S53H2KR.js → chunk-VAZXUK6G.js} +2 -2
  73. package/dist/{chunk-IJRR6YAI.js → chunk-VLDRAMP7.js} +18 -12
  74. package/dist/chunk-VLDRAMP7.js.map +1 -0
  75. package/dist/{chunk-TNX5ZGDJ.js → chunk-VNXAF2KE.js} +4 -4
  76. package/dist/{chunk-EHRRQ4IC.js → chunk-VP3TIUDF.js} +2 -2
  77. package/dist/{chunk-6XZYT4SW.js → chunk-WGDEBIP4.js} +5 -5
  78. package/dist/{chunk-E5OZPDIW.js → chunk-X7BA5PWG.js} +7 -5
  79. package/dist/chunk-X7BA5PWG.js.map +1 -0
  80. package/dist/compat/index.cjs +1 -1
  81. package/dist/compat/index.cjs.map +1 -1
  82. package/dist/compat/index.js +2 -2
  83. package/dist/compat/nestjs/index.cjs +1 -1
  84. package/dist/compat/nestjs/index.cjs.map +1 -1
  85. package/dist/compat/nestjs/index.js +2 -2
  86. package/dist/index.cjs +1657 -982
  87. package/dist/index.cjs.map +1 -1
  88. package/dist/index.d.cts +2 -2
  89. package/dist/index.d.ts +2 -2
  90. package/dist/index.js +58 -36
  91. package/dist/index.js.map +1 -1
  92. package/dist/presets/ai/index.cjs +42 -26
  93. package/dist/presets/ai/index.cjs.map +1 -1
  94. package/dist/presets/ai/index.js +11 -11
  95. package/dist/presets/harness/index.cjs +53 -33
  96. package/dist/presets/harness/index.cjs.map +1 -1
  97. package/dist/presets/harness/index.js +22 -22
  98. package/dist/presets/index.cjs +76 -48
  99. package/dist/presets/index.cjs.map +1 -1
  100. package/dist/presets/index.js +28 -28
  101. package/dist/presets/inspect/index.cjs.map +1 -1
  102. package/dist/presets/inspect/index.js +4 -4
  103. package/dist/presets/resilience/index.cjs +35 -23
  104. package/dist/presets/resilience/index.cjs.map +1 -1
  105. package/dist/presets/resilience/index.js +5 -5
  106. package/dist/solutions/index.cjs +71 -45
  107. package/dist/solutions/index.cjs.map +1 -1
  108. package/dist/solutions/index.js +24 -24
  109. package/dist/{timeout-U5O4ESK3.js → timeout-BEABACRP.js} +2 -2
  110. package/dist/utils/ai/browser.cjs.map +1 -1
  111. package/dist/utils/ai/browser.js +9 -9
  112. package/dist/utils/ai/index.cjs +41 -25
  113. package/dist/utils/ai/index.cjs.map +1 -1
  114. package/dist/utils/ai/index.js +18 -18
  115. package/dist/utils/ai/node.js +3 -3
  116. package/dist/utils/domain-templates/index.cjs +1 -1
  117. package/dist/utils/domain-templates/index.cjs.map +1 -1
  118. package/dist/utils/domain-templates/index.js +2 -2
  119. package/dist/utils/graphspec/index.cjs +1 -1
  120. package/dist/utils/graphspec/index.cjs.map +1 -1
  121. package/dist/utils/graphspec/index.js +2 -2
  122. package/dist/utils/harness/index.cjs +16 -10
  123. package/dist/utils/harness/index.cjs.map +1 -1
  124. package/dist/utils/harness/index.js +1 -1
  125. package/dist/utils/index.cjs +1069 -452
  126. package/dist/utils/index.cjs.map +1 -1
  127. package/dist/utils/index.d.cts +2 -2
  128. package/dist/utils/index.d.ts +2 -2
  129. package/dist/utils/index.js +45 -23
  130. package/dist/utils/inspect/index.cjs.map +1 -1
  131. package/dist/utils/inspect/index.js +2 -2
  132. package/dist/utils/memory/index.cjs +513 -37
  133. package/dist/utils/memory/index.cjs.map +1 -1
  134. package/dist/utils/memory/index.d.cts +641 -3
  135. package/dist/utils/memory/index.d.ts +641 -3
  136. package/dist/utils/memory/index.js +19 -1
  137. package/dist/utils/messaging/index.cjs +109 -0
  138. package/dist/utils/messaging/index.cjs.map +1 -1
  139. package/dist/utils/messaging/index.d.cts +115 -2
  140. package/dist/utils/messaging/index.d.ts +115 -2
  141. package/dist/utils/messaging/index.js +5 -1
  142. package/dist/utils/orchestration/index.cjs +5 -3
  143. package/dist/utils/orchestration/index.cjs.map +1 -1
  144. package/dist/utils/orchestration/index.js +2 -2
  145. package/dist/utils/process/index.js +2 -2
  146. package/dist/utils/reduction/index.cjs +1 -1
  147. package/dist/utils/reduction/index.cjs.map +1 -1
  148. package/dist/utils/reduction/index.js +1 -1
  149. package/dist/utils/resilience/index.cjs +35 -23
  150. package/dist/utils/resilience/index.cjs.map +1 -1
  151. package/dist/utils/resilience/index.js +4 -4
  152. package/dist/utils/surface/index.cjs +1 -1
  153. package/dist/utils/surface/index.cjs.map +1 -1
  154. package/dist/utils/surface/index.js +3 -3
  155. package/package.json +1 -1
  156. package/dist/chunk-5IMMNARC.js.map +0 -1
  157. package/dist/chunk-CGHORL6G.js.map +0 -1
  158. package/dist/chunk-DKNHAICT.js.map +0 -1
  159. package/dist/chunk-E5OZPDIW.js.map +0 -1
  160. package/dist/chunk-IJRR6YAI.js.map +0 -1
  161. package/dist/chunk-OCUDSN63.js.map +0 -1
  162. package/dist/chunk-PZWISPIQ.js.map +0 -1
  163. package/dist/chunk-Q3EYOCZB.js.map +0 -1
  164. package/dist/chunk-RJOG4IJU.js.map +0 -1
  165. package/dist/chunk-SOOKUYVM.js.map +0 -1
  166. package/dist/chunk-YJ4U2D2C.js.map +0 -1
  167. package/dist/chunk-ZT4WMQW4.js.map +0 -1
  168. package/dist/chunk-ZVXXDWIB.js.map +0 -1
  169. /package/dist/{chunk-VLAGJZSL.js.map → chunk-3O3NKZJW.js.map} +0 -0
  170. /package/dist/{chunk-2OB3CEJS.js.map → chunk-6MRSX3YK.js.map} +0 -0
  171. /package/dist/{chunk-EVYY4X5A.js.map → chunk-6ZLCPUXS.js.map} +0 -0
  172. /package/dist/{chunk-FW23JYNQ.js.map → chunk-CEVNQ74M.js.map} +0 -0
  173. /package/dist/{chunk-4GYMCUDZ.js.map → chunk-EP4WVQLX.js.map} +0 -0
  174. /package/dist/{chunk-JKTC747G.js.map → chunk-FQSQONOU.js.map} +0 -0
  175. /package/dist/{chunk-JGFRAFDL.js.map → chunk-FVINAAKA.js.map} +0 -0
  176. /package/dist/{chunk-BU3SEFA5.js.map → chunk-IOJDYUA7.js.map} +0 -0
  177. /package/dist/{chunk-DM4OMPWK.js.map → chunk-KNU73RZW.js.map} +0 -0
  178. /package/dist/{chunk-Z6EGP5D7.js.map → chunk-LDCSZ72P.js.map} +0 -0
  179. /package/dist/{chunk-CXANAIZU.js.map → chunk-N65E26UL.js.map} +0 -0
  180. /package/dist/{chunk-O3MT7DYI.js.map → chunk-N6MNJNHB.js.map} +0 -0
  181. /package/dist/{chunk-A7KV5UK4.js.map → chunk-OXD5LFQP.js.map} +0 -0
  182. /package/dist/{chunk-22SG74BD.js.map → chunk-R6ZCSXKX.js.map} +0 -0
  183. /package/dist/{chunk-4S53H2KR.js.map → chunk-VAZXUK6G.js.map} +0 -0
  184. /package/dist/{chunk-TNX5ZGDJ.js.map → chunk-VNXAF2KE.js.map} +0 -0
  185. /package/dist/{chunk-EHRRQ4IC.js.map → chunk-VP3TIUDF.js.map} +0 -0
  186. /package/dist/{chunk-6XZYT4SW.js.map → chunk-WGDEBIP4.js.map} +0 -0
  187. /package/dist/{timeout-U5O4ESK3.js.map → timeout-BEABACRP.js.map} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { Node } from '@graphrefly/pure-ts/core';
2
- import { ReactiveLogBundle } from '@graphrefly/pure-ts/extra';
2
+ import { ReactiveLogBundle, StorageBackend, Codec, AppendLogStorageTier } from '@graphrefly/pure-ts/extra';
3
3
  import { Graph } from '@graphrefly/pure-ts/graph';
4
4
  import { BaseAuditRecord } from '../../base/mutation/index.cjs';
5
5
  import { N as NodeOrValue } from '../../_internal-B23BagFd.cjs';
@@ -170,7 +170,7 @@ interface ReviewRequest {
170
170
  readonly threshold: number;
171
171
  }
172
172
  interface FactStoreAuditRecord extends BaseAuditRecord {
173
- readonly action: "ingest" | "invalidate" | "outcome" | "consolidate" | "overflow";
173
+ readonly action: "ingest" | "invalidate" | "outcome" | "consolidate" | "decay" | "overflow";
174
174
  readonly id?: FactId;
175
175
  readonly reason?: CascadeReason;
176
176
  }
@@ -181,11 +181,59 @@ interface ReactiveFactStoreConfig<T> {
181
181
  /** Shard count for the default hash-mod sharder. Default 4 (§3.2). */
182
182
  readonly shardCount?: number;
183
183
  readonly scoring?: Node<ScoringPolicy<T>>;
184
+ /**
185
+ * Forgetting-curve policy `(confidence, ageNs) => confidence'` (MEME L1
186
+ * time-drift / Hassabis forgetting curve, DS-14.7 PART 4.1 face ②). `ageNs`
187
+ * is the fact's monotonic age (`monotonicNs() - t_ns`, a `bigint`
188
+ * nanosecond duration — `Number()` it for float math; precision degrades
189
+ * for ages beyond `Number.MAX_SAFE_INTEGER` ns ≈ 104 days of process
190
+ * uptime). Reactive: the policy itself can evolve (e.g.
191
+ * `derived([outcome], …)` for continual learning — spec §1.4
192
+ * push-on-update).
193
+ *
194
+ * **Activation contract.** The decay processor exists only when
195
+ * {@link decayTrigger} is *configured*. Once it is, **both** a
196
+ * `decayTrigger` tick **and** a `decay` policy emit drive a full pass
197
+ * (push-on-update — the `outcomeProcessor`+`scoring` precedent). A policy
198
+ * Node's *initial* value therefore runs a pass on first resolution: if you
199
+ * build `decay` via `derived(...)`, expect a store-wide pass when it first
200
+ * settles. With no `decayTrigger` configured the face is fully inert
201
+ * (mirrors {@link consolidate}/{@link consolidateTrigger}).
202
+ *
203
+ * Each pass applies the current policy to every *live* fact (`validTo`
204
+ * unset), clamps to `[0, 1]`, and writes back only facts whose confidence
205
+ * actually changed (a fact obsoleted earlier in the same tick is never
206
+ * resurrected). A drop below {@link reviewThreshold} surfaces via the
207
+ * `review` topic; decay never sets `validTo`, so it cannot itself drive the
208
+ * cascade.
209
+ *
210
+ * **Quiescence is the policy's responsibility.** The only no-op guard is
211
+ * exact-equality (`next === confidence`). A policy with no reachable
212
+ * fixpoint (e.g. plain geometric `c => c * 0.5`) therefore re-decays and
213
+ * re-emits **every** live fact on **every** tick forever (silent CPU/GC +
214
+ * audit-log churn — never an error). Bound it in the policy: clamp to a
215
+ * floor and return `confidence` unchanged once at/below it (so the
216
+ * exact-equality guard quiesces the store). The {@link decayExponential}
217
+ * recipe is the ergonomic alternative that owns its own `fromTimer` +
218
+ * `floor`/`epsilon` and writes through the `ingest` face (no
219
+ * `decay`/`decayTrigger` wiring). Use this face when you want the store to
220
+ * own decay as a reactive, swappable policy and you are providing the floor.
221
+ */
184
222
  readonly decay?: Node<DecayPolicy>;
185
223
  readonly admissionFilter?: Node<AdmissionFilter<T>>;
186
224
  readonly ingest: Node<MemoryFragment<T>>;
187
225
  readonly outcome?: Node<OutcomeSignal>;
188
226
  readonly query?: Node<MemoryQuery>;
227
+ /**
228
+ * Decay trigger — a reactive timer/cron Node (e.g. `fromTimer(...)` /
229
+ * `fromCron(...)`). Configuring it creates the decay processor; each tick
230
+ * then re-applies the current {@link decay} policy to every live fact (a
231
+ * `decay` policy emit also drives a pass — see {@link decay}'s "Activation
232
+ * contract"). A tick before any `decay` policy has emitted is a no-op.
233
+ * Without this field the `decay` face is fully inert (mirrors
234
+ * {@link consolidateTrigger}/{@link consolidate}).
235
+ */
236
+ readonly decayTrigger?: Node<unknown>;
189
237
  /**
190
238
  * Consolidator trigger — a reactive timer/cron Node (e.g. `fromCron(...)`).
191
239
  * When supplied, the `consolidated` node maps each tick to summarized
@@ -316,6 +364,596 @@ interface ReactiveFactStoreGraph<T> extends Graph {
316
364
  */
317
365
  declare function reactiveFactStore<T>(config: ReactiveFactStoreConfig<T>): ReactiveFactStoreGraph<T>;
318
366
 
367
+ /**
368
+ * Promotion 1 (memo:Re Story 6.4 back-derivation; DS-14.7 follow-up #2 —
369
+ * the persistence half of `simpleFactStore()`). Design-review-locked
370
+ * 2026-05-16.
371
+ *
372
+ * memo:Re hand-rolled `createPersistentMemoryStore` (217 LOC): a
373
+ * `reactiveLog.attach(ingest)` side-log + an `appendLogStorage` tier +
374
+ * paginated replay + a `persistedCount` suffix-cursor for replay-dedup +
375
+ * flush/dispose lifecycle. Its code-review's *only High* finding (flush
376
+ * partial-failure double-append) + several Meds (concurrent-flush race,
377
+ * `persistedCount` baseline fragility, `loadAllHistory` silent truncation)
378
+ * were all artifacts of the first consumer hand-rolling substrate-general
379
+ * orchestration. This factory owns log↔store↔replay↔dedup correctly so the
380
+ * whole silent-corruption bug class disappears for every future consumer.
381
+ *
382
+ * **Key design properties (locked):**
383
+ * - **Synchronous factory.** Returns the `Graph` immediately like every other
384
+ * `utils/memory` factory — `utils/` is composed by presets/solutions and
385
+ * must stay reactive (no `await` in a construction path). The inherently-IO
386
+ * durable load + replay is the ONE async boundary and it lives in a single
387
+ * isolated reactive source node (`fromAny` over a paginated `loadEntries`
388
+ * async-iterator), per spec §5.10 — not in the factory signature.
389
+ * - **Substrate-owned durable cursor.** Persistence is wired AFTER the replay
390
+ * source drains; `ReactiveLogBundle.attachStorage` initialises its
391
+ * delivered-cursor to `ingestLog.size` at attach time, so the replayed
392
+ * history is NOT re-persisted and only post-replay live fragments ship.
393
+ * The cursor is entirely internal — the consumer tracks nothing (no
394
+ * `persistedCount`). Observability is the read-only `position` Node.
395
+ * - **Flush delegated** to the already-QA-hardened `appendLogStorage.flush`
396
+ * (reject-on-prior-failure + rollback-epoch + chained-drain) — the High
397
+ * double-append dissolves because nothing here hand-rolls `appendEntries`
398
+ * + a cursor.
399
+ * - **No silent partial-history loss.** Replay reads the durable history in a
400
+ * single `tier.loadEntries()` call — the substrate append-log tier returns
401
+ * the COMPLETE log (it does not paginate / windowed-truncate), so memo:Re's
402
+ * `loadAllHistory` partial-page-truncation bug class is absent by
403
+ * construction (not "detected"). Any `loadEntries` rejection propagates as
404
+ * `ERROR` on the replay source (observable), not a swallowed partial load.
405
+ * (Real cursor pagination for very large logs is a deferred substrate
406
+ * enhancement — see `docs/optimizations.md`.)
407
+ *
408
+ * Determinism: `reactiveFactStore`'s cascade `validTo` is derived from the
409
+ * triggering root (not wall-clock), so replaying the persisted ingest stream
410
+ * rebuilds a byte-identical store — the rebuildable-projection contract
411
+ * documented on {@link ReactiveFactStoreConfig.recordIngest}.
412
+ *
413
+ * Only the bytes-`StorageBackend` adapter stays userland; the BigInt-safe
414
+ * codec is upstream (`bigintJsonCodecFor`, the default here).
415
+ *
416
+ * @module
417
+ */
418
+
419
+ interface PersistentReactiveFactStoreConfig<T> extends Omit<ReactiveFactStoreConfig<T>, "recordIngest" | "admissionFilter"> {
420
+ /**
421
+ * Bytes backend the durable ingest log is persisted through. The ONLY
422
+ * userland piece — e.g. memo:Re's Drizzle/Expo-SQLite `StorageBackend`,
423
+ * or `memoryBackend()` for tests.
424
+ */
425
+ readonly storage: StorageBackend;
426
+ /** Backend key / tier name. Default `"fact_store_ingest"`. */
427
+ readonly persistName?: string;
428
+ /** Codec for the durable bucket. Default `bigintJsonCodecFor` (BigInt-safe). */
429
+ readonly codec?: Codec<readonly MemoryFragment<T>[]>;
430
+ }
431
+ interface PersistentReactiveFactStoreGraph<T> extends ReactiveFactStoreGraph<T> {
432
+ /**
433
+ * Reactive count of durably-persisted fragments. `0` until startup replay
434
+ * completes; thereafter the committed-fragment count (replayed history —
435
+ * loaded FROM the durable tier — plus live fragments shipped by the
436
+ * substrate-owned `attachStorage` cursor; call {@link flush} to force them
437
+ * physically durable). Observability only — the cursor is internal.
438
+ */
439
+ readonly position: Node<number>;
440
+ /**
441
+ * Reactive count of fragments rebuilt from durable history at startup.
442
+ * `0` until the first replayed fragment; final value once replay
443
+ * `COMPLETE`s.
444
+ */
445
+ readonly replayedCount: Node<number>;
446
+ /** The durable append-log tier (shared backend; e.g. for projector cursors). */
447
+ readonly tier: AppendLogStorageTier<MemoryFragment<T>>;
448
+ /**
449
+ * Force-drain the durable tier. Delegates to the QA-hardened
450
+ * `appendLogStorage.flush` (rejects if a prior in-flight write failed;
451
+ * honours the rollback epoch). Resolves once all shipped fragments are
452
+ * physically durable.
453
+ *
454
+ * **Pre-attach-live durability (QA-E):** a fragment emitted into
455
+ * `config.ingest` in the same synchronous tick as construction (before the
456
+ * async replay drains) is shipped by a one-shot, fire-and-forget
457
+ * reconciliation write whose rejection is NOT surfaced inline. Call
458
+ * `flush()` after construction settles to (a) confirm that slice is
459
+ * physically durable and (b) surface any write failure as a rejection. The
460
+ * normal pattern — observe `replayedCount`/`position` before feeding live
461
+ * ingest — avoids the window entirely.
462
+ */
463
+ flush(): Promise<void>;
464
+ }
465
+ /**
466
+ * Build a durable, event-sourced {@link reactiveFactStore} that owns
467
+ * log↔store↔replay↔dedup correctly. Synchronous factory; the only async is
468
+ * an isolated internal replay source. See module docstring for the locked
469
+ * design rationale.
470
+ *
471
+ * @example
472
+ * ```ts
473
+ * import { persistentReactiveFactStore } from "@graphrefly/graphrefly";
474
+ * import { memoryBackend } from "@graphrefly/pure-ts/extra";
475
+ *
476
+ * const ingest = node<MemoryFragment<Doc>>([], { initial: undefined });
477
+ * const mem = persistentReactiveFactStore<Doc>({
478
+ * ingest,
479
+ * extractDependencies: (f) => f.sources,
480
+ * storage: memoryBackend(),
481
+ * });
482
+ * // Restart is automatic: the durable history is replayed through `ingest`
483
+ * // on construction; observe `mem.replayedCount` / `mem.position`.
484
+ * ingest.emit(myFragment); // live — persisted (not re-persisted)
485
+ * await mem.flush(); // force physically durable
486
+ * ```
487
+ *
488
+ * @category memory
489
+ */
490
+ declare function persistentReactiveFactStore<T>(config: PersistentReactiveFactStoreConfig<T>): PersistentReactiveFactStoreGraph<T>;
491
+
492
+ /**
493
+ * Recipe — **LLM gatekeeper** admission filter.
494
+ *
495
+ * `ReactiveFactStoreConfig.admissionFilter` is a **synchronous** face — it runs
496
+ * inside `extract_op` at ingest time, so it cannot itself `await` an LLM (spec
497
+ * §5.10 forbids raw async in the reactive layer; the LLM call belongs in a
498
+ * source / `promptNode` upstream). This recipe adapts a **precomputed verdict
499
+ * stream** to the sync face: the caller runs the judge upstream (a `promptNode`
500
+ * over the candidate fragment stream) and feeds its `Map<FactId, boolean>`
501
+ * verdicts in; the recipe returns a `Node<AdmissionFilter<T>>` that consults
502
+ * the latest verdicts synchronously.
503
+ *
504
+ * ```ts
505
+ * // upstream: async LLM judge produces verdicts (id → admit?)
506
+ * const verdicts = promptNode(adapter, [candidates], judgeFragmentsFn); // Node<Map<FactId,boolean>>
507
+ * const mem = reactiveFactStore<Doc>({
508
+ * ingest, extractDependencies,
509
+ * admissionFilter: admissionLlmJudge<Doc>(verdicts), // ② sync-face adapter
510
+ * });
511
+ * ```
512
+ *
513
+ * **Deny-by-default.** A fragment with no verdict yet is rejected
514
+ * (`defaultVerdict` default `false`) — the store never admits an unjudged fact.
515
+ * Set `defaultVerdict: true` for an allow-then-prune posture instead.
516
+ *
517
+ * **⚠️ The filter only gates admission AT ingest, synchronously.**
518
+ * - A fragment ingested **before** its verdict lands is rejected (with the
519
+ * default `false`) and is **permanently lost** — there is no buffering or
520
+ * retry; a later verdict admitting that id does NOT retroactively ingest it.
521
+ * If you cannot guarantee verdict-before-fragment ordering, buffer/replay
522
+ * the candidate stream upstream (e.g. gate ingest behind the verdict Node).
523
+ * - "Prune" in `defaultVerdict: true` is a misnomer for *post-hoc removal*: a
524
+ * verdict flipping `true → false` after a fact is committed does nothing
525
+ * (the fact stays). It only means "admit unjudged, reject explicitly-denied
526
+ * at ingest". True pruning requires a separate obsoletion path.
527
+ *
528
+ * @module
529
+ */
530
+
531
+ interface AdmissionLlmJudgeOptions {
532
+ /** Verdict for a fragment the judge hasn't ruled on yet. Default `false` (strict gate). */
533
+ readonly defaultVerdict?: boolean;
534
+ /** Node name. Default `admission_llm_judge`. */
535
+ readonly name?: string;
536
+ }
537
+ /**
538
+ * Adapt an upstream LLM-verdict stream to the synchronous `admissionFilter`
539
+ * face. `verdicts` is a Node carrying the current `factId → admit?` map (e.g.
540
+ * a `promptNode` accumulating judgements).
541
+ *
542
+ * @category memory
543
+ */
544
+ declare function admissionLlmJudge<T>(verdicts: Node<ReadonlyMap<FactId, boolean>>, opts?: AdmissionLlmJudgeOptions): Node<AdmissionFilter<T>>;
545
+
546
+ /**
547
+ * Recipe — **"as of t" historical view** (MEME L3 obsolescence reasoning).
548
+ *
549
+ * `reactiveFactStore`'s built-in `query`/`answer` face answers a structured
550
+ * `MemoryQuery` (which already supports `asOf`), but a common need is a
551
+ * *standing, reactive* historical projection that re-derives whenever either
552
+ * the store changes **or** the as-of instant moves (e.g. a scrubber in a debug
553
+ * UI). This recipe is that projection over face ④ + a reactive `asOf` input:
554
+ * `derived([asOf, mem.factStore])` → only fragments valid at `asOf`
555
+ * (bi-temporal `[validFrom, validTo)` test), giving Graphiti/Zep-style
556
+ * "what did we believe at time T" without committing anything to the spec.
557
+ *
558
+ * ```ts
559
+ * const asOf = node<bigint>([], { initial: undefined });
560
+ * const view = bitemporalQuery(mem, asOf, { tags: ["policy"] });
561
+ * asOf.emit(lastTuesdayNs); // view re-derives to the policy facts valid then
562
+ * ```
563
+ *
564
+ * Pass `asOf` SENTINEL (no emit yet) → the view is the **currently-valid** set
565
+ * (live facts, `validTo` unset), so it's useful before any scrub too.
566
+ *
567
+ * @module
568
+ */
569
+
570
+ interface BitemporalQueryOptions {
571
+ /** Restrict to fragments carrying any of these tags (OR). */
572
+ readonly tags?: readonly string[];
573
+ /** Minimum confidence (inclusive). */
574
+ readonly minConfidence?: number;
575
+ /** Node name. Default `bitemporal_query`. */
576
+ readonly name?: string;
577
+ }
578
+ /**
579
+ * Build a standing bi-temporal historical view over a {@link reactiveFactStore}.
580
+ * Emits the fragments valid at the latest `asOf` (sorted confidence desc, then
581
+ * `t_ns` desc — same order as the built-in `answer`).
582
+ *
583
+ * @category memory
584
+ */
585
+ declare function bitemporalQuery<T>(mem: ReactiveFactStoreGraph<T>, asOf: Node<bigint>, opts?: BitemporalQueryOptions): Node<readonly MemoryFragment<T>[]>;
586
+
587
+ /**
588
+ * Recipe — **REM-style replay consolidation**.
589
+ *
590
+ * Hassabis's sleep-consolidation frame: periodically replay the
591
+ * highest-confidence × most-recent facts and synthesize a compressed successor
592
+ * fragment (version-chained via `parent_fragment_id`). Builds the two config
593
+ * fields the `consolidate` extension face needs — a reactive `consolidateTrigger`
594
+ * (a `fromTimer` source, spec §5.8-compliant) and the `consolidate(store)`
595
+ * summarizer — so the caller just spreads them in:
596
+ *
597
+ * ```ts
598
+ * const mem = reactiveFactStore<Doc>({
599
+ * ingest, extractDependencies,
600
+ * ...consolidationRem<Doc>({
601
+ * periodMs: 60_000,
602
+ * topK: 8,
603
+ * recentWindowNs: 3_600_000_000_000n, // 1h
604
+ * summarize: (frags) => mergeDocs(frags), // domain-specific
605
+ * }),
606
+ * });
607
+ * ```
608
+ *
609
+ * The pattern default-wires consolidator output back into ingest (Q9-open-6),
610
+ * so the successor fragment becomes a first-class fact; the originals are left
611
+ * intact (callers obsolete them via their own `validTo` policy if desired —
612
+ * keeping that out of the recipe preserves "the recipe never decides what to
613
+ * forget").
614
+ *
615
+ * **Selection order & assumptions.** `recentWindowNs` filters the *pool* first
616
+ * (facts within `recentWindowNs` of the newest live fact's `t_ns`); the pool
617
+ * is then sorted by `confidence desc, t_ns desc` and `topK`-sliced — so an
618
+ * old-but-in-window high-confidence fact can beat a brand-new low-confidence
619
+ * one (recency gates the pool, confidence picks the winners). If every fact
620
+ * falls outside the window the pool is empty and `summarize` is not called
621
+ * (the consolidator emits `[]` — indistinguishable from an empty store).
622
+ * `recentWindowNs` math assumes all fragments' `t_ns` come from the **same
623
+ * clock** (`monotonicNs` xor `wallClockNs`, not mixed) — the window is
624
+ * meaningless across mixed clocks.
625
+ *
626
+ * @module
627
+ */
628
+
629
+ interface ConsolidationRemOptions<T> {
630
+ /** Replay period in milliseconds. */
631
+ readonly periodMs: number;
632
+ /** How many top facts to replay per pass. */
633
+ readonly topK: number;
634
+ /**
635
+ * Synthesize successor fragment(s) from the replayed set. Domain-specific —
636
+ * required (the pattern's default `consolidate` is a no-op). Set
637
+ * `parent_fragment_id` on the result to chain versions.
638
+ */
639
+ readonly summarize: (replayed: readonly MemoryFragment<T>[]) => readonly MemoryFragment<T>[];
640
+ /**
641
+ * Only replay facts whose `t_ns` is within this many ns of the most-recent
642
+ * fact's `t_ns` (recency gate). Omit to consider all live facts.
643
+ */
644
+ readonly recentWindowNs?: bigint;
645
+ }
646
+ interface ConsolidationRemConfig<T> {
647
+ readonly consolidateTrigger: Node<number>;
648
+ readonly consolidate: (store: StoreReadHandle<T>) => readonly MemoryFragment<T>[];
649
+ }
650
+ /**
651
+ * Build the `{ consolidateTrigger, consolidate }` pair for a REM-replay
652
+ * consolidation policy. Spread into {@link reactiveFactStore}'s config.
653
+ *
654
+ * @category memory
655
+ */
656
+ declare function consolidationRem<T>(opts: ConsolidationRemOptions<T>): ConsolidationRemConfig<T>;
657
+
658
+ /**
659
+ * Recipe — **standard forgetting curve** (Hassabis confidence drift).
660
+ *
661
+ * Periodically decays every live fact's `confidence` toward a floor on an
662
+ * exponential half-life schedule, re-ingesting the drifted fragment (MEME L1
663
+ * direct-replace). The periodic trigger is a reactive `fromTimer` source
664
+ * (spec §5.8 no-polling / §5.10 no raw `setTimeout` — `fromTimer` is the
665
+ * sanctioned reactive timer) and the only reactive dep, so there is no
666
+ * within-wave feedback loop (COMPOSITION-GUIDE-GRAPH §7): the store snapshot is
667
+ * read **advisory** off `mem.factStore.cache`, never as a reactive dep; the
668
+ * re-ingest write commits **synchronously this tick** (graphrefly push is
669
+ * synchronous) and the next tick decays from there.
670
+ *
671
+ * > **Recipe vs the `decay` face.** `ReactiveFactStoreConfig.decay:
672
+ * > Node<DecayPolicy>` (DS-14.7 PART 4.1 face ②) **is wired** in the factory —
673
+ * > pair it with `decayTrigger` and the store owns decay as a reactive,
674
+ * > swappable policy. This recipe is the **ergonomic alternative**: it owns its
675
+ * > own `fromTimer` and writes through the already-wired `ingest` face, so the
676
+ * > caller wires no `decay`/`decayTrigger` and the recipe composes whether or
677
+ * > not the face is also used. Both deliver the §5.8 "`fromTimer` for periodic
678
+ * > confidence drift" behavior; pick the face for a caller-controlled policy
679
+ * > Node, this recipe for a self-contained drop-in forgetting curve.
680
+ *
681
+ * **Convergence (conditional — read this).** Per-id decay is computed against
682
+ * the elapsed time since this fact was last decayed (recipe-owned closure
683
+ * clock — `t_ns` provenance is preserved, never overwritten; a re-ingested
684
+ * *new version* — fresher `t_ns` than the last tick — restarts from its own
685
+ * `t_ns`, not the stale prior-version tick). A fragment is skipped (no
686
+ * re-ingest) when it is obsolete (`validTo` set), already at/below `floor`, or
687
+ * the drift is below `epsilon`. Quiescence ("timer keeps ticking, recipe emits
688
+ * `[]`, no churn") therefore holds **only when `epsilon > 0` AND `floor` is
689
+ * reachable** — with `epsilon <= 0` and a finite half-life the geometric
690
+ * drift toward `floor` is always `> 0`, so the loop re-ingests every live fact
691
+ * forever (silent CPU/GC churn, never an error). `epsilon` is clamped to a
692
+ * tiny positive minimum to make accidental non-quiescence impossible.
693
+ * Obsoletion safety: the obsolete guard is re-checked against the **live**
694
+ * store immediately before re-ingest, so a fact obsoleted by an in-flight
695
+ * cascade earlier in the same tick is never resurrected by a stale snapshot
696
+ * read.
697
+ *
698
+ * @module
699
+ */
700
+
701
+ interface DecayExponentialOptions {
702
+ /** Half-life in nanoseconds — confidence halves every `halfLifeNs` of fact age. */
703
+ readonly halfLifeNs: bigint;
704
+ /** Timer period in milliseconds (how often the forgetting pass runs). */
705
+ readonly periodMs: number;
706
+ /** Confidence floor; facts at/below it are left untouched. Default `0`. */
707
+ readonly floor?: number;
708
+ /**
709
+ * Minimum confidence drift to bother re-ingesting. Default `1e-4`. Clamped
710
+ * to a tiny positive minimum (`<= 0` would prevent quiescence — see the
711
+ * module "Convergence" note).
712
+ */
713
+ readonly epsilon?: number;
714
+ /** Node name. Default `decay_exponential`. */
715
+ readonly name?: string;
716
+ }
717
+ /**
718
+ * Wire an exponential-decay forgetting loop onto a {@link reactiveFactStore}.
719
+ * Self-adds a driver Node to the store's graph (`describe()`-visible) and
720
+ * returns it; each emission is the batch of fragments decayed that tick (also
721
+ * fed back through `ingest`).
722
+ *
723
+ * @category memory
724
+ */
725
+ declare function decayExponential<T>(mem: ReactiveFactStoreGraph<T>, ingest: Node<MemoryFragment<T>>, opts: DecayExponentialOptions): Node<readonly MemoryFragment<T>[]>;
726
+
727
+ /**
728
+ * Recipe — **write-time influence analysis** (MEME write-time `reachable`).
729
+ *
730
+ * The store's `dependents_index` is a one-hop reverse-dependency map; "if I
731
+ * obsolete fact X, what *transitively* gets invalidated?" is its closure. This
732
+ * recipe exposes that closure as a reactive projection over face ④
733
+ * (`mem.dependentsIndex`), so a writer can see blast-radius **before**
734
+ * committing an obsolescence — the same reachability the cascade loop walks,
735
+ * surfaced for inspection.
736
+ *
737
+ * ```ts
738
+ * const inf = influenceAnalysis(mem);
739
+ * inf.influenceOf("home").subscribe(ids => console.log("obsoleting home hits", ids));
740
+ * inf.ranked.subscribe(rows => rows.slice(0,5)); // most-influential facts
741
+ * ```
742
+ *
743
+ * Pure observer — never writes back, so it cannot perturb cascade
744
+ * convergence. **Single-apply per store** (it adds an `${name}_ranked` node);
745
+ * apply twice on the same `mem` only with a distinct `opts.name`.
746
+ *
747
+ * **Cost.** `ranked` runs a BFS closure per index key on every
748
+ * `dependents_index` emit — O(keys · (V+E)), NOT O(maxRanked); `maxRanked`
749
+ * caps only the emitted slice, not the computation. Acceptable because
750
+ * `dependents_index` is metadata (≪ the fact store, per DS-14.7 Q9-open-1);
751
+ * for a pathologically dense index prefer `influenceOf(id)` (single-root BFS)
752
+ * over subscribing `ranked`. In a cyclic reverse-dep graph
753
+ * (LLM-extracted `A→B→A`) `closureOf` still terminates (`seen` set) but every
754
+ * SCC member reports the same closure size — a *reachability* count, not the
755
+ * cascade's actual obsolete-only propagation reach (which `processedRoots`
756
+ * bounds further).
757
+ *
758
+ * @module
759
+ */
760
+
761
+ interface InfluenceRow {
762
+ readonly factId: FactId;
763
+ /** Size of the transitive dependent closure (blast radius if obsoleted). */
764
+ readonly influence: number;
765
+ }
766
+ interface InfluenceAnalysisOptions {
767
+ /** Cap on rows emitted by `ranked` (top-N by influence). Default `64`. */
768
+ readonly maxRanked?: number;
769
+ /** Node name prefix. Default `influence`. */
770
+ readonly name?: string;
771
+ }
772
+ interface InfluenceAnalysis {
773
+ /** Reactive transitive dependent closure of `rootId` (excludes the root). */
774
+ influenceOf(rootId: FactId): Node<readonly FactId[]>;
775
+ /** Facts ranked by transitive-closure size (desc), capped at `maxRanked`. */
776
+ readonly ranked: Node<readonly InfluenceRow[]>;
777
+ }
778
+ /**
779
+ * Attach influence/blast-radius analysis to a {@link reactiveFactStore}.
780
+ *
781
+ * @category memory
782
+ */
783
+ declare function influenceAnalysis<T>(mem: ReactiveFactStoreGraph<T>, opts?: InfluenceAnalysisOptions): InfluenceAnalysis;
784
+
785
+ /**
786
+ * Recipe — **cascade-event tracer** (debugging the invisible edge).
787
+ *
788
+ * The `dependents_index` lookup that drives cascade invalidation is a fn-body
789
+ * map read, not a topology edge — `describe()` / `explain()` can't see *why*
790
+ * fact C got invalidated. This recipe is the pure-observer (face ④) companion
791
+ * that subscribes `mem.cascade` + `mem.cascadeOverflow` and keeps a bounded
792
+ * ring of human-readable trace entries (each carries the `causalReason` string
793
+ * the store stamps for exactly this purpose), so a developer can answer
794
+ * "what obsoleted this?" without instrumenting the store.
795
+ *
796
+ * ```ts
797
+ * const trace = invalidationTracer(mem, { limit: 256 });
798
+ * trace.subscribe((entries) => entries.forEach(e => log(e.causalReason)));
799
+ * ```
800
+ *
801
+ * Read-only: it never writes back into the store, so it cannot perturb cascade
802
+ * convergence.
803
+ *
804
+ * @module
805
+ */
806
+
807
+ interface InvalidationTraceEntry {
808
+ readonly kind: "cascade" | "overflow";
809
+ readonly factId: FactId;
810
+ readonly rootFactId: FactId;
811
+ readonly reason: CascadeReason | "overflow";
812
+ readonly iteration?: number;
813
+ readonly causalReason: string;
814
+ }
815
+ interface InvalidationTracerOptions {
816
+ /** Ring-buffer size (most-recent N trace entries retained). Default `256`. */
817
+ readonly limit?: number;
818
+ /** Node name. Default `invalidation_tracer`. */
819
+ readonly name?: string;
820
+ }
821
+ /**
822
+ * Attach a bounded cascade-event tracer to a {@link reactiveFactStore}.
823
+ * Self-adds a `describe()`-visible observer Node and returns it; each emission
824
+ * is the current trace ring (oldest → newest).
825
+ *
826
+ * @category memory
827
+ */
828
+ declare function invalidationTracer<T>(mem: ReactiveFactStoreGraph<T>, opts?: InvalidationTracerOptions): Node<readonly InvalidationTraceEntry[]>;
829
+
830
+ /**
831
+ * Recipe — **self-evolving scoring policy** (continual learning).
832
+ *
833
+ * Closes Hassabis's continual-learning frame against the `scoring` extension
834
+ * face (②): an outcome / RL signal stream feeds back into the policy that
835
+ * `reactiveFactStore` applies on `outcome` write-back, so the agent's memory
836
+ * *learns* which facts proved useful.
837
+ *
838
+ * ```ts
839
+ * const outcomes = node<OutcomeSignal>([], { initial: undefined });
840
+ * const mem = reactiveFactStore<Doc>({
841
+ * ingest, extractDependencies,
842
+ * outcome: outcomes, // ③ topic input (RL signal)
843
+ * scoring: scoringByOutcome(outcomes), // ② policy that evolves with it
844
+ * });
845
+ * ```
846
+ *
847
+ * The returned `Node<ScoringPolicy<T>>` re-emits a fresh policy closure every
848
+ * time an `OutcomeSignal` arrives; the closure scores a fragment as
849
+ * `clamp01(base(f) + learningRate · Σ reward(f.id))`. The reward accumulator is
850
+ * a recipe-owned closure Map (sole-owner mutation inside the derived fn — the
851
+ * COMPOSITION-GUIDE-sanctioned pattern for a fold the node alone advances),
852
+ * keyed by `factId`, so learning is cumulative across episodes and survives
853
+ * policy re-emission.
854
+ *
855
+ * **Contract (load-bearing — do not "normalize"):**
856
+ * - The fn folds **every signal in the wave** (`for (const sig of batchData[0])`),
857
+ * NOT `lastOf` last-only like the other recipes — a batched wave carrying N
858
+ * `OutcomeSignal`s must accumulate all N. Replacing this with `lastOf` would
859
+ * silently drop all-but-last reward in a batch.
860
+ * - `outcomes` MUST be a non-replaying event source (each physical signal
861
+ * delivered once). The fold has no per-signal idempotency key (two identical
862
+ * `{factId, reward}` are legitimately distinct events); a source that
863
+ * push-on-subscribe **re-delivers** a cached signal to a re-subscribe would
864
+ * double-count it. The normal wiring (single keepalive'd consumer via the
865
+ * factory's `outcomeProcessor`, subscribed at construction before any emit)
866
+ * is safe.
867
+ * - The policy node carries **no `equals`** by design: every emit is a fresh
868
+ * closure identity so the factory's `outcomeProcessor` always re-fires and
869
+ * re-reads the freshly-folded `acc` (the closure reads `acc` lazily at call
870
+ * time). Adding an `equals` would make outcome write-back lag one signal.
871
+ * - `acc` retains one entry per rewarded `factId` for the store's lifetime
872
+ * (same bounded-growth class as `ingestLog`; acceptable — it is metadata).
873
+ *
874
+ * @module
875
+ */
876
+
877
+ interface ScoringByOutcomeOptions<T> {
878
+ /**
879
+ * Base score before accumulated reward. Default: the fragment's own
880
+ * `confidence` (so an un-rewarded fact keeps its ingested confidence).
881
+ */
882
+ readonly base?: (f: MemoryFragment<T>) => number;
883
+ /** Multiplier on accumulated reward. Default `1`. */
884
+ readonly learningRate?: number;
885
+ /** Node name (collision-safe if you build more than one). Default `scoring_by_outcome`. */
886
+ readonly name?: string;
887
+ }
888
+ /**
889
+ * Build a continual-learning {@link ScoringPolicy} Node from an
890
+ * {@link OutcomeSignal} stream. Pass the SAME `outcomes` Node as both
891
+ * `config.outcome` and (via this recipe) `config.scoring`.
892
+ *
893
+ * @category memory
894
+ */
895
+ declare function scoringByOutcome<T>(outcomes: Node<OutcomeSignal>, opts?: ScoringByOutcomeOptions<T>): Node<ScoringPolicy<T>>;
896
+
897
+ /**
898
+ * Recipe — **multi-tenant isolation** sharding.
899
+ *
900
+ * Two postures over the `shardBy` + `shardCount` faces (plain-fn face ①):
901
+ *
902
+ * - **Strict isolation** (`tenants` listed): one shard per tenant, fixed
903
+ * `tenant → index` mapping. A tenant's facts never share a shard with
904
+ * another's — the strongest in-process isolation the static-topology store
905
+ * offers (per-shard `state<FactStore>` nodes). Unknown tenants fall to a
906
+ * dedicated overflow shard (last index).
907
+ * - **Soft partition** (no `tenants`): hash the tenant key into `shardCount`
908
+ * buckets — even load, tenants *may* co-reside (collision), but cross-tenant
909
+ * reads still require an explicit query (no accidental leakage of the *index*,
910
+ * just shared physical storage).
911
+ *
912
+ * **Caveats.** Soft "even load" holds only at sufficient tenant cardinality —
913
+ * with few tenants vs `shardCount`, FNV-1a-mod can collide several onto one
914
+ * shard (use **strict** mode for a guaranteed-per-tenant shard). `tenantOf`
915
+ * must return a defined, stable key: a `undefined`/`null` return is coerced to
916
+ * the string `"undefined"` by the factory's hash and silently pools all
917
+ * tenant-less facts together. Strict mode's overflow shard intentionally pools
918
+ * **all** unconfigured tenants together (isolated from configured ones, not
919
+ * from each other).
920
+ *
921
+ * ```ts
922
+ * const mem = reactiveFactStore<Doc>({
923
+ * ingest, extractDependencies,
924
+ * ...shardByTenant<Doc>((f) => f.payload.tenantId, {
925
+ * tenants: ["acme", "globex"], // strict: 3 shards (2 + overflow)
926
+ * }),
927
+ * });
928
+ * ```
929
+ *
930
+ * @module
931
+ */
932
+
933
+ interface ShardByTenantOptions {
934
+ /**
935
+ * Known tenants for strict 1-shard-per-tenant isolation. Omit for soft
936
+ * hash partitioning. The strict layout adds one trailing **overflow** shard
937
+ * for tenants not in this list (so an unconfigured tenant is still isolated
938
+ * from the configured ones, just pooled together).
939
+ */
940
+ readonly tenants?: readonly string[];
941
+ /** Bucket count for the soft (non-strict) posture. Default `4`. */
942
+ readonly shardCount?: number;
943
+ }
944
+ interface ShardByTenantConfig<T> {
945
+ readonly shardBy: (f: MemoryFragment<T>) => ShardKey;
946
+ readonly shardCount: number;
947
+ }
948
+ /**
949
+ * Build the `{ shardBy, shardCount }` pair for tenant-isolated sharding.
950
+ * `tenantOf` extracts the tenant key from a fragment. Spread into
951
+ * {@link reactiveFactStore}'s config.
952
+ *
953
+ * @category memory
954
+ */
955
+ declare function shardByTenant<T>(tenantOf: (f: MemoryFragment<T>) => string, opts?: ShardByTenantOptions): ShardByTenantConfig<T>;
956
+
319
957
  /**
320
958
  * Memory patterns (roadmap §4.3) — public-face Phase-4 primitives audited under
321
959
  * `archive/docs/SESSION-public-face-blocks-review.md` (Wave A, locked 2026-04-25).
@@ -657,4 +1295,4 @@ type KnowledgeGraph<TEntity, TRelation extends string = string> = Graph & {
657
1295
  */
658
1296
  declare function knowledgeGraph<TEntity, TRelation extends string = string>(name: string, opts?: KnowledgeGraphOptions): KnowledgeGraph<TEntity, TRelation>;
659
1297
 
660
- export { type AdmissionFilter, type CascadeEvent, type CascadeOverflow, type CascadeReason, type CollectionAuditRecord, type CollectionEntry, type CollectionGraph, type CollectionOptions, type CollectionScoreFn, type DecayPolicy, type DependentsIndex, type FactId, type FactStore, type FactStoreAuditRecord, type HnswAdapter, type KnowledgeEdge, type KnowledgeGraph, type KnowledgeGraphAuditRecord, type KnowledgeGraphOptions, type MemoryAnswer, type MemoryFragment, type MemoryQuery, NodeOrValue, type OutcomeSignal, type RankedCollectionEntry, type ReactiveFactStoreConfig, type ReactiveFactStoreGraph, type ReviewRequest, type ScoringPolicy, type ShardKey, type StoreReadHandle, type VectorBackend, type VectorIndexAuditRecord, type VectorIndexGraph, type VectorIndexOptions, type VectorRecord, type VectorSearchResult, collection, cosineSimilarity, knowledgeGraph, reactiveFactStore, vectorIndex };
1298
+ export { type AdmissionFilter, type AdmissionLlmJudgeOptions, type BitemporalQueryOptions, type CascadeEvent, type CascadeOverflow, type CascadeReason, type CollectionAuditRecord, type CollectionEntry, type CollectionGraph, type CollectionOptions, type CollectionScoreFn, type ConsolidationRemConfig, type ConsolidationRemOptions, type DecayExponentialOptions, type DecayPolicy, type DependentsIndex, type FactId, type FactStore, type FactStoreAuditRecord, type HnswAdapter, type InfluenceAnalysis, type InfluenceAnalysisOptions, type InfluenceRow, type InvalidationTraceEntry, type InvalidationTracerOptions, type KnowledgeEdge, type KnowledgeGraph, type KnowledgeGraphAuditRecord, type KnowledgeGraphOptions, type MemoryAnswer, type MemoryFragment, type MemoryQuery, NodeOrValue, type OutcomeSignal, type PersistentReactiveFactStoreConfig, type PersistentReactiveFactStoreGraph, type RankedCollectionEntry, type ReactiveFactStoreConfig, type ReactiveFactStoreGraph, type ReviewRequest, type ScoringByOutcomeOptions, type ScoringPolicy, type ShardByTenantConfig, type ShardByTenantOptions, type ShardKey, type StoreReadHandle, type VectorBackend, type VectorIndexAuditRecord, type VectorIndexGraph, type VectorIndexOptions, type VectorRecord, type VectorSearchResult, admissionLlmJudge, bitemporalQuery, collection, consolidationRem, cosineSimilarity, decayExponential, influenceAnalysis, invalidationTracer, knowledgeGraph, persistentReactiveFactStore, reactiveFactStore, scoringByOutcome, shardByTenant, vectorIndex };