@graphrefly/graphrefly 0.47.0 → 0.47.1

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 (66) hide show
  1. package/dist/{chunk-CGHORL6G.js → chunk-7ADWWI2T.js} +2 -2
  2. package/dist/{chunk-TNX5ZGDJ.js → chunk-B4AKFXGE.js} +4 -4
  3. package/dist/{chunk-FW23JYNQ.js → chunk-CEVNQ74M.js} +2 -2
  4. package/dist/{chunk-JGFRAFDL.js → chunk-FVINAAKA.js} +3 -3
  5. package/dist/{chunk-22SG74BD.js → chunk-J5WFUEO4.js} +2 -2
  6. package/dist/{chunk-GWRNLJNW.js → chunk-K7PDZYQE.js} +4 -4
  7. package/dist/{chunk-Z6EGP5D7.js → chunk-LDCSZ72P.js} +2 -2
  8. package/dist/{chunk-EHRRQ4IC.js → chunk-MTTRCEJT.js} +2 -2
  9. package/dist/{chunk-Q3EYOCZB.js → chunk-NPRP3MCV.js} +111 -2
  10. package/dist/chunk-NPRP3MCV.js.map +1 -0
  11. package/dist/{chunk-JKTC747G.js → chunk-RGMTUZCL.js} +3 -3
  12. package/dist/{chunk-ZVXXDWIB.js → chunk-U225SKB4.js} +455 -25
  13. package/dist/chunk-U225SKB4.js.map +1 -0
  14. package/dist/{chunk-ZT4WMQW4.js → chunk-V4Y3TM7U.js} +5 -5
  15. package/dist/{chunk-5IMMNARC.js → chunk-YXCPV26R.js} +2 -2
  16. package/dist/index.cjs +1374 -840
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +2 -2
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.js +35 -13
  21. package/dist/index.js.map +1 -1
  22. package/dist/presets/ai/index.cjs.map +1 -1
  23. package/dist/presets/ai/index.js +6 -6
  24. package/dist/presets/harness/index.cjs.map +1 -1
  25. package/dist/presets/harness/index.js +9 -9
  26. package/dist/presets/index.cjs.map +1 -1
  27. package/dist/presets/index.js +13 -13
  28. package/dist/presets/inspect/index.cjs.map +1 -1
  29. package/dist/presets/inspect/index.js +4 -4
  30. package/dist/solutions/index.cjs.map +1 -1
  31. package/dist/solutions/index.js +10 -10
  32. package/dist/utils/ai/index.cjs.map +1 -1
  33. package/dist/utils/ai/index.js +5 -5
  34. package/dist/utils/index.cjs +931 -397
  35. package/dist/utils/index.cjs.map +1 -1
  36. package/dist/utils/index.d.cts +2 -2
  37. package/dist/utils/index.d.ts +2 -2
  38. package/dist/utils/index.js +28 -6
  39. package/dist/utils/inspect/index.cjs.map +1 -1
  40. package/dist/utils/inspect/index.js +2 -2
  41. package/dist/utils/memory/index.cjs +462 -37
  42. package/dist/utils/memory/index.cjs.map +1 -1
  43. package/dist/utils/memory/index.d.cts +591 -2
  44. package/dist/utils/memory/index.d.ts +591 -2
  45. package/dist/utils/memory/index.js +19 -1
  46. package/dist/utils/messaging/index.cjs +109 -0
  47. package/dist/utils/messaging/index.cjs.map +1 -1
  48. package/dist/utils/messaging/index.d.cts +115 -2
  49. package/dist/utils/messaging/index.d.ts +115 -2
  50. package/dist/utils/messaging/index.js +5 -1
  51. package/dist/utils/orchestration/index.cjs.map +1 -1
  52. package/dist/utils/orchestration/index.js +2 -2
  53. package/package.json +1 -1
  54. package/dist/chunk-Q3EYOCZB.js.map +0 -1
  55. package/dist/chunk-ZVXXDWIB.js.map +0 -1
  56. /package/dist/{chunk-CGHORL6G.js.map → chunk-7ADWWI2T.js.map} +0 -0
  57. /package/dist/{chunk-TNX5ZGDJ.js.map → chunk-B4AKFXGE.js.map} +0 -0
  58. /package/dist/{chunk-FW23JYNQ.js.map → chunk-CEVNQ74M.js.map} +0 -0
  59. /package/dist/{chunk-JGFRAFDL.js.map → chunk-FVINAAKA.js.map} +0 -0
  60. /package/dist/{chunk-22SG74BD.js.map → chunk-J5WFUEO4.js.map} +0 -0
  61. /package/dist/{chunk-GWRNLJNW.js.map → chunk-K7PDZYQE.js.map} +0 -0
  62. /package/dist/{chunk-Z6EGP5D7.js.map → chunk-LDCSZ72P.js.map} +0 -0
  63. /package/dist/{chunk-EHRRQ4IC.js.map → chunk-MTTRCEJT.js.map} +0 -0
  64. /package/dist/{chunk-JKTC747G.js.map → chunk-RGMTUZCL.js.map} +0 -0
  65. /package/dist/{chunk-ZT4WMQW4.js.map → chunk-V4Y3TM7U.js.map} +0 -0
  66. /package/dist/{chunk-5IMMNARC.js.map → chunk-YXCPV26R.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';
@@ -316,6 +316,595 @@ interface ReactiveFactStoreGraph<T> extends Graph {
316
316
  */
317
317
  declare function reactiveFactStore<T>(config: ReactiveFactStoreConfig<T>): ReactiveFactStoreGraph<T>;
318
318
 
319
+ /**
320
+ * Promotion 1 (memo:Re Story 6.4 back-derivation; DS-14.7 follow-up #2 —
321
+ * the persistence half of `simpleFactStore()`). Design-review-locked
322
+ * 2026-05-16.
323
+ *
324
+ * memo:Re hand-rolled `createPersistentMemoryStore` (217 LOC): a
325
+ * `reactiveLog.attach(ingest)` side-log + an `appendLogStorage` tier +
326
+ * paginated replay + a `persistedCount` suffix-cursor for replay-dedup +
327
+ * flush/dispose lifecycle. Its code-review's *only High* finding (flush
328
+ * partial-failure double-append) + several Meds (concurrent-flush race,
329
+ * `persistedCount` baseline fragility, `loadAllHistory` silent truncation)
330
+ * were all artifacts of the first consumer hand-rolling substrate-general
331
+ * orchestration. This factory owns log↔store↔replay↔dedup correctly so the
332
+ * whole silent-corruption bug class disappears for every future consumer.
333
+ *
334
+ * **Key design properties (locked):**
335
+ * - **Synchronous factory.** Returns the `Graph` immediately like every other
336
+ * `utils/memory` factory — `utils/` is composed by presets/solutions and
337
+ * must stay reactive (no `await` in a construction path). The inherently-IO
338
+ * durable load + replay is the ONE async boundary and it lives in a single
339
+ * isolated reactive source node (`fromAny` over a paginated `loadEntries`
340
+ * async-iterator), per spec §5.10 — not in the factory signature.
341
+ * - **Substrate-owned durable cursor.** Persistence is wired AFTER the replay
342
+ * source drains; `ReactiveLogBundle.attachStorage` initialises its
343
+ * delivered-cursor to `ingestLog.size` at attach time, so the replayed
344
+ * history is NOT re-persisted and only post-replay live fragments ship.
345
+ * The cursor is entirely internal — the consumer tracks nothing (no
346
+ * `persistedCount`). Observability is the read-only `position` Node.
347
+ * - **Flush delegated** to the already-QA-hardened `appendLogStorage.flush`
348
+ * (reject-on-prior-failure + rollback-epoch + chained-drain) — the High
349
+ * double-append dissolves because nothing here hand-rolls `appendEntries`
350
+ * + a cursor.
351
+ * - **No silent partial-history loss.** Replay reads the durable history in a
352
+ * single `tier.loadEntries()` call — the substrate append-log tier returns
353
+ * the COMPLETE log (it does not paginate / windowed-truncate), so memo:Re's
354
+ * `loadAllHistory` partial-page-truncation bug class is absent by
355
+ * construction (not "detected"). Any `loadEntries` rejection propagates as
356
+ * `ERROR` on the replay source (observable), not a swallowed partial load.
357
+ * (Real cursor pagination for very large logs is a deferred substrate
358
+ * enhancement — see `docs/optimizations.md`.)
359
+ *
360
+ * Determinism: `reactiveFactStore`'s cascade `validTo` is derived from the
361
+ * triggering root (not wall-clock), so replaying the persisted ingest stream
362
+ * rebuilds a byte-identical store — the rebuildable-projection contract
363
+ * documented on {@link ReactiveFactStoreConfig.recordIngest}.
364
+ *
365
+ * Only the bytes-`StorageBackend` adapter stays userland; the BigInt-safe
366
+ * codec is upstream (`bigintJsonCodecFor`, the default here).
367
+ *
368
+ * @module
369
+ */
370
+
371
+ interface PersistentReactiveFactStoreConfig<T> extends Omit<ReactiveFactStoreConfig<T>, "recordIngest" | "admissionFilter"> {
372
+ /**
373
+ * Bytes backend the durable ingest log is persisted through. The ONLY
374
+ * userland piece — e.g. memo:Re's Drizzle/Expo-SQLite `StorageBackend`,
375
+ * or `memoryBackend()` for tests.
376
+ */
377
+ readonly storage: StorageBackend;
378
+ /** Backend key / tier name. Default `"fact_store_ingest"`. */
379
+ readonly persistName?: string;
380
+ /** Codec for the durable bucket. Default `bigintJsonCodecFor` (BigInt-safe). */
381
+ readonly codec?: Codec<readonly MemoryFragment<T>[]>;
382
+ }
383
+ interface PersistentReactiveFactStoreGraph<T> extends ReactiveFactStoreGraph<T> {
384
+ /**
385
+ * Reactive count of durably-persisted fragments. `0` until startup replay
386
+ * completes; thereafter the committed-fragment count (replayed history —
387
+ * loaded FROM the durable tier — plus live fragments shipped by the
388
+ * substrate-owned `attachStorage` cursor; call {@link flush} to force them
389
+ * physically durable). Observability only — the cursor is internal.
390
+ */
391
+ readonly position: Node<number>;
392
+ /**
393
+ * Reactive count of fragments rebuilt from durable history at startup.
394
+ * `0` until the first replayed fragment; final value once replay
395
+ * `COMPLETE`s.
396
+ */
397
+ readonly replayedCount: Node<number>;
398
+ /** The durable append-log tier (shared backend; e.g. for projector cursors). */
399
+ readonly tier: AppendLogStorageTier<MemoryFragment<T>>;
400
+ /**
401
+ * Force-drain the durable tier. Delegates to the QA-hardened
402
+ * `appendLogStorage.flush` (rejects if a prior in-flight write failed;
403
+ * honours the rollback epoch). Resolves once all shipped fragments are
404
+ * physically durable.
405
+ *
406
+ * **Pre-attach-live durability (QA-E):** a fragment emitted into
407
+ * `config.ingest` in the same synchronous tick as construction (before the
408
+ * async replay drains) is shipped by a one-shot, fire-and-forget
409
+ * reconciliation write whose rejection is NOT surfaced inline. Call
410
+ * `flush()` after construction settles to (a) confirm that slice is
411
+ * physically durable and (b) surface any write failure as a rejection. The
412
+ * normal pattern — observe `replayedCount`/`position` before feeding live
413
+ * ingest — avoids the window entirely.
414
+ */
415
+ flush(): Promise<void>;
416
+ }
417
+ /**
418
+ * Build a durable, event-sourced {@link reactiveFactStore} that owns
419
+ * log↔store↔replay↔dedup correctly. Synchronous factory; the only async is
420
+ * an isolated internal replay source. See module docstring for the locked
421
+ * design rationale.
422
+ *
423
+ * @example
424
+ * ```ts
425
+ * import { persistentReactiveFactStore } from "@graphrefly/graphrefly";
426
+ * import { memoryBackend } from "@graphrefly/pure-ts/extra";
427
+ *
428
+ * const ingest = node<MemoryFragment<Doc>>([], { initial: undefined });
429
+ * const mem = persistentReactiveFactStore<Doc>({
430
+ * ingest,
431
+ * extractDependencies: (f) => f.sources,
432
+ * storage: memoryBackend(),
433
+ * });
434
+ * // Restart is automatic: the durable history is replayed through `ingest`
435
+ * // on construction; observe `mem.replayedCount` / `mem.position`.
436
+ * ingest.emit(myFragment); // live — persisted (not re-persisted)
437
+ * await mem.flush(); // force physically durable
438
+ * ```
439
+ *
440
+ * @category memory
441
+ */
442
+ declare function persistentReactiveFactStore<T>(config: PersistentReactiveFactStoreConfig<T>): PersistentReactiveFactStoreGraph<T>;
443
+
444
+ /**
445
+ * Recipe — **LLM gatekeeper** admission filter.
446
+ *
447
+ * `ReactiveFactStoreConfig.admissionFilter` is a **synchronous** face — it runs
448
+ * inside `extract_op` at ingest time, so it cannot itself `await` an LLM (spec
449
+ * §5.10 forbids raw async in the reactive layer; the LLM call belongs in a
450
+ * source / `promptNode` upstream). This recipe adapts a **precomputed verdict
451
+ * stream** to the sync face: the caller runs the judge upstream (a `promptNode`
452
+ * over the candidate fragment stream) and feeds its `Map<FactId, boolean>`
453
+ * verdicts in; the recipe returns a `Node<AdmissionFilter<T>>` that consults
454
+ * the latest verdicts synchronously.
455
+ *
456
+ * ```ts
457
+ * // upstream: async LLM judge produces verdicts (id → admit?)
458
+ * const verdicts = promptNode(adapter, [candidates], judgeFragmentsFn); // Node<Map<FactId,boolean>>
459
+ * const mem = reactiveFactStore<Doc>({
460
+ * ingest, extractDependencies,
461
+ * admissionFilter: admissionLlmJudge<Doc>(verdicts), // ② sync-face adapter
462
+ * });
463
+ * ```
464
+ *
465
+ * **Deny-by-default.** A fragment with no verdict yet is rejected
466
+ * (`defaultVerdict` default `false`) — the store never admits an unjudged fact.
467
+ * Set `defaultVerdict: true` for an allow-then-prune posture instead.
468
+ *
469
+ * **⚠️ The filter only gates admission AT ingest, synchronously.**
470
+ * - A fragment ingested **before** its verdict lands is rejected (with the
471
+ * default `false`) and is **permanently lost** — there is no buffering or
472
+ * retry; a later verdict admitting that id does NOT retroactively ingest it.
473
+ * If you cannot guarantee verdict-before-fragment ordering, buffer/replay
474
+ * the candidate stream upstream (e.g. gate ingest behind the verdict Node).
475
+ * - "Prune" in `defaultVerdict: true` is a misnomer for *post-hoc removal*: a
476
+ * verdict flipping `true → false` after a fact is committed does nothing
477
+ * (the fact stays). It only means "admit unjudged, reject explicitly-denied
478
+ * at ingest". True pruning requires a separate obsoletion path.
479
+ *
480
+ * @module
481
+ */
482
+
483
+ interface AdmissionLlmJudgeOptions {
484
+ /** Verdict for a fragment the judge hasn't ruled on yet. Default `false` (strict gate). */
485
+ readonly defaultVerdict?: boolean;
486
+ /** Node name. Default `admission_llm_judge`. */
487
+ readonly name?: string;
488
+ }
489
+ /**
490
+ * Adapt an upstream LLM-verdict stream to the synchronous `admissionFilter`
491
+ * face. `verdicts` is a Node carrying the current `factId → admit?` map (e.g.
492
+ * a `promptNode` accumulating judgements).
493
+ *
494
+ * @category memory
495
+ */
496
+ declare function admissionLlmJudge<T>(verdicts: Node<ReadonlyMap<FactId, boolean>>, opts?: AdmissionLlmJudgeOptions): Node<AdmissionFilter<T>>;
497
+
498
+ /**
499
+ * Recipe — **"as of t" historical view** (MEME L3 obsolescence reasoning).
500
+ *
501
+ * `reactiveFactStore`'s built-in `query`/`answer` face answers a structured
502
+ * `MemoryQuery` (which already supports `asOf`), but a common need is a
503
+ * *standing, reactive* historical projection that re-derives whenever either
504
+ * the store changes **or** the as-of instant moves (e.g. a scrubber in a debug
505
+ * UI). This recipe is that projection over face ④ + a reactive `asOf` input:
506
+ * `derived([asOf, mem.factStore])` → only fragments valid at `asOf`
507
+ * (bi-temporal `[validFrom, validTo)` test), giving Graphiti/Zep-style
508
+ * "what did we believe at time T" without committing anything to the spec.
509
+ *
510
+ * ```ts
511
+ * const asOf = node<bigint>([], { initial: undefined });
512
+ * const view = bitemporalQuery(mem, asOf, { tags: ["policy"] });
513
+ * asOf.emit(lastTuesdayNs); // view re-derives to the policy facts valid then
514
+ * ```
515
+ *
516
+ * Pass `asOf` SENTINEL (no emit yet) → the view is the **currently-valid** set
517
+ * (live facts, `validTo` unset), so it's useful before any scrub too.
518
+ *
519
+ * @module
520
+ */
521
+
522
+ interface BitemporalQueryOptions {
523
+ /** Restrict to fragments carrying any of these tags (OR). */
524
+ readonly tags?: readonly string[];
525
+ /** Minimum confidence (inclusive). */
526
+ readonly minConfidence?: number;
527
+ /** Node name. Default `bitemporal_query`. */
528
+ readonly name?: string;
529
+ }
530
+ /**
531
+ * Build a standing bi-temporal historical view over a {@link reactiveFactStore}.
532
+ * Emits the fragments valid at the latest `asOf` (sorted confidence desc, then
533
+ * `t_ns` desc — same order as the built-in `answer`).
534
+ *
535
+ * @category memory
536
+ */
537
+ declare function bitemporalQuery<T>(mem: ReactiveFactStoreGraph<T>, asOf: Node<bigint>, opts?: BitemporalQueryOptions): Node<readonly MemoryFragment<T>[]>;
538
+
539
+ /**
540
+ * Recipe — **REM-style replay consolidation**.
541
+ *
542
+ * Hassabis's sleep-consolidation frame: periodically replay the
543
+ * highest-confidence × most-recent facts and synthesize a compressed successor
544
+ * fragment (version-chained via `parent_fragment_id`). Builds the two config
545
+ * fields the `consolidate` extension face needs — a reactive `consolidateTrigger`
546
+ * (a `fromTimer` source, spec §5.8-compliant) and the `consolidate(store)`
547
+ * summarizer — so the caller just spreads them in:
548
+ *
549
+ * ```ts
550
+ * const mem = reactiveFactStore<Doc>({
551
+ * ingest, extractDependencies,
552
+ * ...consolidationRem<Doc>({
553
+ * periodMs: 60_000,
554
+ * topK: 8,
555
+ * recentWindowNs: 3_600_000_000_000n, // 1h
556
+ * summarize: (frags) => mergeDocs(frags), // domain-specific
557
+ * }),
558
+ * });
559
+ * ```
560
+ *
561
+ * The pattern default-wires consolidator output back into ingest (Q9-open-6),
562
+ * so the successor fragment becomes a first-class fact; the originals are left
563
+ * intact (callers obsolete them via their own `validTo` policy if desired —
564
+ * keeping that out of the recipe preserves "the recipe never decides what to
565
+ * forget").
566
+ *
567
+ * **Selection order & assumptions.** `recentWindowNs` filters the *pool* first
568
+ * (facts within `recentWindowNs` of the newest live fact's `t_ns`); the pool
569
+ * is then sorted by `confidence desc, t_ns desc` and `topK`-sliced — so an
570
+ * old-but-in-window high-confidence fact can beat a brand-new low-confidence
571
+ * one (recency gates the pool, confidence picks the winners). If every fact
572
+ * falls outside the window the pool is empty and `summarize` is not called
573
+ * (the consolidator emits `[]` — indistinguishable from an empty store).
574
+ * `recentWindowNs` math assumes all fragments' `t_ns` come from the **same
575
+ * clock** (`monotonicNs` xor `wallClockNs`, not mixed) — the window is
576
+ * meaningless across mixed clocks.
577
+ *
578
+ * @module
579
+ */
580
+
581
+ interface ConsolidationRemOptions<T> {
582
+ /** Replay period in milliseconds. */
583
+ readonly periodMs: number;
584
+ /** How many top facts to replay per pass. */
585
+ readonly topK: number;
586
+ /**
587
+ * Synthesize successor fragment(s) from the replayed set. Domain-specific —
588
+ * required (the pattern's default `consolidate` is a no-op). Set
589
+ * `parent_fragment_id` on the result to chain versions.
590
+ */
591
+ readonly summarize: (replayed: readonly MemoryFragment<T>[]) => readonly MemoryFragment<T>[];
592
+ /**
593
+ * Only replay facts whose `t_ns` is within this many ns of the most-recent
594
+ * fact's `t_ns` (recency gate). Omit to consider all live facts.
595
+ */
596
+ readonly recentWindowNs?: bigint;
597
+ }
598
+ interface ConsolidationRemConfig<T> {
599
+ readonly consolidateTrigger: Node<number>;
600
+ readonly consolidate: (store: StoreReadHandle<T>) => readonly MemoryFragment<T>[];
601
+ }
602
+ /**
603
+ * Build the `{ consolidateTrigger, consolidate }` pair for a REM-replay
604
+ * consolidation policy. Spread into {@link reactiveFactStore}'s config.
605
+ *
606
+ * @category memory
607
+ */
608
+ declare function consolidationRem<T>(opts: ConsolidationRemOptions<T>): ConsolidationRemConfig<T>;
609
+
610
+ /**
611
+ * Recipe — **standard forgetting curve** (Hassabis confidence drift).
612
+ *
613
+ * Periodically decays every live fact's `confidence` toward a floor on an
614
+ * exponential half-life schedule, re-ingesting the drifted fragment (MEME L1
615
+ * direct-replace). The periodic trigger is a reactive `fromTimer` source
616
+ * (spec §5.8 no-polling / §5.10 no raw `setTimeout` — `fromTimer` is the
617
+ * sanctioned reactive timer) and the only reactive dep, so there is no
618
+ * within-wave feedback loop (COMPOSITION-GUIDE-GRAPH §7): the store snapshot is
619
+ * read **advisory** off `mem.factStore.cache`, never as a reactive dep; the
620
+ * re-ingest write commits **synchronously this tick** (graphrefly push is
621
+ * synchronous) and the next tick decays from there.
622
+ *
623
+ * > **Why a recipe, not the `decay` face.** `ReactiveFactStoreConfig.decay:
624
+ * > Node<DecayPolicy>` is a *locked design face* (DS-14.7 PART 4.1) but the
625
+ * > shipped `reactiveFactStore()` v1 factory does **not** consume it (tracked
626
+ * > as a factory-gap item in `docs/optimizations.md`). This recipe delivers the
627
+ * > §5.8 "`decay` recipe uses `fromTimer` for periodic confidence drift"
628
+ * > behavior over the **wired** `ingest` face instead — fully spec-compliant
629
+ * > and zero-core-change. It composes regardless of whether the `decay` face is
630
+ * > later wired.
631
+ *
632
+ * **Convergence (conditional — read this).** Per-id decay is computed against
633
+ * the elapsed time since this fact was last decayed (recipe-owned closure
634
+ * clock — `t_ns` provenance is preserved, never overwritten; a re-ingested
635
+ * *new version* — fresher `t_ns` than the last tick — restarts from its own
636
+ * `t_ns`, not the stale prior-version tick). A fragment is skipped (no
637
+ * re-ingest) when it is obsolete (`validTo` set), already at/below `floor`, or
638
+ * the drift is below `epsilon`. Quiescence ("timer keeps ticking, recipe emits
639
+ * `[]`, no churn") therefore holds **only when `epsilon > 0` AND `floor` is
640
+ * reachable** — with `epsilon <= 0` and a finite half-life the geometric
641
+ * drift toward `floor` is always `> 0`, so the loop re-ingests every live fact
642
+ * forever (silent CPU/GC churn, never an error). `epsilon` is clamped to a
643
+ * tiny positive minimum to make accidental non-quiescence impossible.
644
+ * Obsoletion safety: the obsolete guard is re-checked against the **live**
645
+ * store immediately before re-ingest, so a fact obsoleted by an in-flight
646
+ * cascade earlier in the same tick is never resurrected by a stale snapshot
647
+ * read.
648
+ *
649
+ * @module
650
+ */
651
+
652
+ interface DecayExponentialOptions {
653
+ /** Half-life in nanoseconds — confidence halves every `halfLifeNs` of fact age. */
654
+ readonly halfLifeNs: bigint;
655
+ /** Timer period in milliseconds (how often the forgetting pass runs). */
656
+ readonly periodMs: number;
657
+ /** Confidence floor; facts at/below it are left untouched. Default `0`. */
658
+ readonly floor?: number;
659
+ /**
660
+ * Minimum confidence drift to bother re-ingesting. Default `1e-4`. Clamped
661
+ * to a tiny positive minimum (`<= 0` would prevent quiescence — see the
662
+ * module "Convergence" note).
663
+ */
664
+ readonly epsilon?: number;
665
+ /** Node name. Default `decay_exponential`. */
666
+ readonly name?: string;
667
+ }
668
+ /**
669
+ * Wire an exponential-decay forgetting loop onto a {@link reactiveFactStore}.
670
+ * Self-adds a driver Node to the store's graph (`describe()`-visible) and
671
+ * returns it; each emission is the batch of fragments decayed that tick (also
672
+ * fed back through `ingest`).
673
+ *
674
+ * @category memory
675
+ */
676
+ declare function decayExponential<T>(mem: ReactiveFactStoreGraph<T>, ingest: Node<MemoryFragment<T>>, opts: DecayExponentialOptions): Node<readonly MemoryFragment<T>[]>;
677
+
678
+ /**
679
+ * Recipe — **write-time influence analysis** (MEME write-time `reachable`).
680
+ *
681
+ * The store's `dependents_index` is a one-hop reverse-dependency map; "if I
682
+ * obsolete fact X, what *transitively* gets invalidated?" is its closure. This
683
+ * recipe exposes that closure as a reactive projection over face ④
684
+ * (`mem.dependentsIndex`), so a writer can see blast-radius **before**
685
+ * committing an obsolescence — the same reachability the cascade loop walks,
686
+ * surfaced for inspection.
687
+ *
688
+ * ```ts
689
+ * const inf = influenceAnalysis(mem);
690
+ * inf.influenceOf("home").subscribe(ids => console.log("obsoleting home hits", ids));
691
+ * inf.ranked.subscribe(rows => rows.slice(0,5)); // most-influential facts
692
+ * ```
693
+ *
694
+ * Pure observer — never writes back, so it cannot perturb cascade
695
+ * convergence. **Single-apply per store** (it adds an `${name}_ranked` node);
696
+ * apply twice on the same `mem` only with a distinct `opts.name`.
697
+ *
698
+ * **Cost.** `ranked` runs a BFS closure per index key on every
699
+ * `dependents_index` emit — O(keys · (V+E)), NOT O(maxRanked); `maxRanked`
700
+ * caps only the emitted slice, not the computation. Acceptable because
701
+ * `dependents_index` is metadata (≪ the fact store, per DS-14.7 Q9-open-1);
702
+ * for a pathologically dense index prefer `influenceOf(id)` (single-root BFS)
703
+ * over subscribing `ranked`. In a cyclic reverse-dep graph
704
+ * (LLM-extracted `A→B→A`) `closureOf` still terminates (`seen` set) but every
705
+ * SCC member reports the same closure size — a *reachability* count, not the
706
+ * cascade's actual obsolete-only propagation reach (which `processedRoots`
707
+ * bounds further).
708
+ *
709
+ * @module
710
+ */
711
+
712
+ interface InfluenceRow {
713
+ readonly factId: FactId;
714
+ /** Size of the transitive dependent closure (blast radius if obsoleted). */
715
+ readonly influence: number;
716
+ }
717
+ interface InfluenceAnalysisOptions {
718
+ /** Cap on rows emitted by `ranked` (top-N by influence). Default `64`. */
719
+ readonly maxRanked?: number;
720
+ /** Node name prefix. Default `influence`. */
721
+ readonly name?: string;
722
+ }
723
+ interface InfluenceAnalysis {
724
+ /** Reactive transitive dependent closure of `rootId` (excludes the root). */
725
+ influenceOf(rootId: FactId): Node<readonly FactId[]>;
726
+ /** Facts ranked by transitive-closure size (desc), capped at `maxRanked`. */
727
+ readonly ranked: Node<readonly InfluenceRow[]>;
728
+ }
729
+ /**
730
+ * Attach influence/blast-radius analysis to a {@link reactiveFactStore}.
731
+ *
732
+ * @category memory
733
+ */
734
+ declare function influenceAnalysis<T>(mem: ReactiveFactStoreGraph<T>, opts?: InfluenceAnalysisOptions): InfluenceAnalysis;
735
+
736
+ /**
737
+ * Recipe — **cascade-event tracer** (debugging the invisible edge).
738
+ *
739
+ * The `dependents_index` lookup that drives cascade invalidation is a fn-body
740
+ * map read, not a topology edge — `describe()` / `explain()` can't see *why*
741
+ * fact C got invalidated. This recipe is the pure-observer (face ④) companion
742
+ * that subscribes `mem.cascade` + `mem.cascadeOverflow` and keeps a bounded
743
+ * ring of human-readable trace entries (each carries the `causalReason` string
744
+ * the store stamps for exactly this purpose), so a developer can answer
745
+ * "what obsoleted this?" without instrumenting the store.
746
+ *
747
+ * ```ts
748
+ * const trace = invalidationTracer(mem, { limit: 256 });
749
+ * trace.subscribe((entries) => entries.forEach(e => log(e.causalReason)));
750
+ * ```
751
+ *
752
+ * Read-only: it never writes back into the store, so it cannot perturb cascade
753
+ * convergence.
754
+ *
755
+ * @module
756
+ */
757
+
758
+ interface InvalidationTraceEntry {
759
+ readonly kind: "cascade" | "overflow";
760
+ readonly factId: FactId;
761
+ readonly rootFactId: FactId;
762
+ readonly reason: CascadeReason | "overflow";
763
+ readonly iteration?: number;
764
+ readonly causalReason: string;
765
+ }
766
+ interface InvalidationTracerOptions {
767
+ /** Ring-buffer size (most-recent N trace entries retained). Default `256`. */
768
+ readonly limit?: number;
769
+ /** Node name. Default `invalidation_tracer`. */
770
+ readonly name?: string;
771
+ }
772
+ /**
773
+ * Attach a bounded cascade-event tracer to a {@link reactiveFactStore}.
774
+ * Self-adds a `describe()`-visible observer Node and returns it; each emission
775
+ * is the current trace ring (oldest → newest).
776
+ *
777
+ * @category memory
778
+ */
779
+ declare function invalidationTracer<T>(mem: ReactiveFactStoreGraph<T>, opts?: InvalidationTracerOptions): Node<readonly InvalidationTraceEntry[]>;
780
+
781
+ /**
782
+ * Recipe — **self-evolving scoring policy** (continual learning).
783
+ *
784
+ * Closes Hassabis's continual-learning frame against the `scoring` extension
785
+ * face (②): an outcome / RL signal stream feeds back into the policy that
786
+ * `reactiveFactStore` applies on `outcome` write-back, so the agent's memory
787
+ * *learns* which facts proved useful.
788
+ *
789
+ * ```ts
790
+ * const outcomes = node<OutcomeSignal>([], { initial: undefined });
791
+ * const mem = reactiveFactStore<Doc>({
792
+ * ingest, extractDependencies,
793
+ * outcome: outcomes, // ③ topic input (RL signal)
794
+ * scoring: scoringByOutcome(outcomes), // ② policy that evolves with it
795
+ * });
796
+ * ```
797
+ *
798
+ * The returned `Node<ScoringPolicy<T>>` re-emits a fresh policy closure every
799
+ * time an `OutcomeSignal` arrives; the closure scores a fragment as
800
+ * `clamp01(base(f) + learningRate · Σ reward(f.id))`. The reward accumulator is
801
+ * a recipe-owned closure Map (sole-owner mutation inside the derived fn — the
802
+ * COMPOSITION-GUIDE-sanctioned pattern for a fold the node alone advances),
803
+ * keyed by `factId`, so learning is cumulative across episodes and survives
804
+ * policy re-emission.
805
+ *
806
+ * **Contract (load-bearing — do not "normalize"):**
807
+ * - The fn folds **every signal in the wave** (`for (const sig of batchData[0])`),
808
+ * NOT `lastOf` last-only like the other recipes — a batched wave carrying N
809
+ * `OutcomeSignal`s must accumulate all N. Replacing this with `lastOf` would
810
+ * silently drop all-but-last reward in a batch.
811
+ * - `outcomes` MUST be a non-replaying event source (each physical signal
812
+ * delivered once). The fold has no per-signal idempotency key (two identical
813
+ * `{factId, reward}` are legitimately distinct events); a source that
814
+ * push-on-subscribe **re-delivers** a cached signal to a re-subscribe would
815
+ * double-count it. The normal wiring (single keepalive'd consumer via the
816
+ * factory's `outcomeProcessor`, subscribed at construction before any emit)
817
+ * is safe.
818
+ * - The policy node carries **no `equals`** by design: every emit is a fresh
819
+ * closure identity so the factory's `outcomeProcessor` always re-fires and
820
+ * re-reads the freshly-folded `acc` (the closure reads `acc` lazily at call
821
+ * time). Adding an `equals` would make outcome write-back lag one signal.
822
+ * - `acc` retains one entry per rewarded `factId` for the store's lifetime
823
+ * (same bounded-growth class as `ingestLog`; acceptable — it is metadata).
824
+ *
825
+ * @module
826
+ */
827
+
828
+ interface ScoringByOutcomeOptions<T> {
829
+ /**
830
+ * Base score before accumulated reward. Default: the fragment's own
831
+ * `confidence` (so an un-rewarded fact keeps its ingested confidence).
832
+ */
833
+ readonly base?: (f: MemoryFragment<T>) => number;
834
+ /** Multiplier on accumulated reward. Default `1`. */
835
+ readonly learningRate?: number;
836
+ /** Node name (collision-safe if you build more than one). Default `scoring_by_outcome`. */
837
+ readonly name?: string;
838
+ }
839
+ /**
840
+ * Build a continual-learning {@link ScoringPolicy} Node from an
841
+ * {@link OutcomeSignal} stream. Pass the SAME `outcomes` Node as both
842
+ * `config.outcome` and (via this recipe) `config.scoring`.
843
+ *
844
+ * @category memory
845
+ */
846
+ declare function scoringByOutcome<T>(outcomes: Node<OutcomeSignal>, opts?: ScoringByOutcomeOptions<T>): Node<ScoringPolicy<T>>;
847
+
848
+ /**
849
+ * Recipe — **multi-tenant isolation** sharding.
850
+ *
851
+ * Two postures over the `shardBy` + `shardCount` faces (plain-fn face ①):
852
+ *
853
+ * - **Strict isolation** (`tenants` listed): one shard per tenant, fixed
854
+ * `tenant → index` mapping. A tenant's facts never share a shard with
855
+ * another's — the strongest in-process isolation the static-topology store
856
+ * offers (per-shard `state<FactStore>` nodes). Unknown tenants fall to a
857
+ * dedicated overflow shard (last index).
858
+ * - **Soft partition** (no `tenants`): hash the tenant key into `shardCount`
859
+ * buckets — even load, tenants *may* co-reside (collision), but cross-tenant
860
+ * reads still require an explicit query (no accidental leakage of the *index*,
861
+ * just shared physical storage).
862
+ *
863
+ * **Caveats.** Soft "even load" holds only at sufficient tenant cardinality —
864
+ * with few tenants vs `shardCount`, FNV-1a-mod can collide several onto one
865
+ * shard (use **strict** mode for a guaranteed-per-tenant shard). `tenantOf`
866
+ * must return a defined, stable key: a `undefined`/`null` return is coerced to
867
+ * the string `"undefined"` by the factory's hash and silently pools all
868
+ * tenant-less facts together. Strict mode's overflow shard intentionally pools
869
+ * **all** unconfigured tenants together (isolated from configured ones, not
870
+ * from each other).
871
+ *
872
+ * ```ts
873
+ * const mem = reactiveFactStore<Doc>({
874
+ * ingest, extractDependencies,
875
+ * ...shardByTenant<Doc>((f) => f.payload.tenantId, {
876
+ * tenants: ["acme", "globex"], // strict: 3 shards (2 + overflow)
877
+ * }),
878
+ * });
879
+ * ```
880
+ *
881
+ * @module
882
+ */
883
+
884
+ interface ShardByTenantOptions {
885
+ /**
886
+ * Known tenants for strict 1-shard-per-tenant isolation. Omit for soft
887
+ * hash partitioning. The strict layout adds one trailing **overflow** shard
888
+ * for tenants not in this list (so an unconfigured tenant is still isolated
889
+ * from the configured ones, just pooled together).
890
+ */
891
+ readonly tenants?: readonly string[];
892
+ /** Bucket count for the soft (non-strict) posture. Default `4`. */
893
+ readonly shardCount?: number;
894
+ }
895
+ interface ShardByTenantConfig<T> {
896
+ readonly shardBy: (f: MemoryFragment<T>) => ShardKey;
897
+ readonly shardCount: number;
898
+ }
899
+ /**
900
+ * Build the `{ shardBy, shardCount }` pair for tenant-isolated sharding.
901
+ * `tenantOf` extracts the tenant key from a fragment. Spread into
902
+ * {@link reactiveFactStore}'s config.
903
+ *
904
+ * @category memory
905
+ */
906
+ declare function shardByTenant<T>(tenantOf: (f: MemoryFragment<T>) => string, opts?: ShardByTenantOptions): ShardByTenantConfig<T>;
907
+
319
908
  /**
320
909
  * Memory patterns (roadmap §4.3) — public-face Phase-4 primitives audited under
321
910
  * `archive/docs/SESSION-public-face-blocks-review.md` (Wave A, locked 2026-04-25).
@@ -657,4 +1246,4 @@ type KnowledgeGraph<TEntity, TRelation extends string = string> = Graph & {
657
1246
  */
658
1247
  declare function knowledgeGraph<TEntity, TRelation extends string = string>(name: string, opts?: KnowledgeGraphOptions): KnowledgeGraph<TEntity, TRelation>;
659
1248
 
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 };
1249
+ 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 };