@fgv/ts-agent-memory 5.1.0-49 → 5.1.0-50

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 (164) hide show
  1. package/dist/packlets/index/memoryIndex.js +23 -16
  2. package/dist/packlets/index/memoryIndex.js.map +1 -1
  3. package/dist/packlets/ingest/orchestrator.js +13 -1
  4. package/dist/packlets/ingest/orchestrator.js.map +1 -1
  5. package/dist/packlets/retrieve/linkTraversalRetriever.js +12 -26
  6. package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  7. package/dist/packlets/retrieve/recencyRetriever.js +7 -7
  8. package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
  9. package/dist/packlets/retrieve/retriever.js +91 -10
  10. package/dist/packlets/retrieve/retriever.js.map +1 -1
  11. package/dist/packlets/retrieve/semanticRetriever.js +16 -16
  12. package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
  13. package/dist/packlets/retrieve/structuredFilterRetriever.js +7 -7
  14. package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  15. package/dist/packlets/retrieve/tagRetriever.js +7 -7
  16. package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
  17. package/dist/packlets/retrieve/temporalRetrievers.js +23 -20
  18. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
  19. package/dist/packlets/store/coverage.js +6 -0
  20. package/dist/packlets/store/coverage.js.map +1 -0
  21. package/dist/packlets/store/fileTreeMemoryStore.js +221 -79
  22. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  23. package/dist/packlets/store/index.js +4 -0
  24. package/dist/packlets/store/index.js.map +1 -1
  25. package/dist/packlets/store/listSelection.js +36 -0
  26. package/dist/packlets/store/listSelection.js.map +1 -0
  27. package/dist/packlets/store/memoryStore.js +6 -0
  28. package/dist/packlets/store/memoryStore.js.map +1 -0
  29. package/dist/packlets/store/reconcile.js +6 -0
  30. package/dist/packlets/store/reconcile.js.map +1 -0
  31. package/dist/packlets/store/storeCoverage.js +102 -0
  32. package/dist/packlets/store/storeCoverage.js.map +1 -0
  33. package/dist/packlets/store/storeReconcile.js +122 -0
  34. package/dist/packlets/store/storeReconcile.js.map +1 -0
  35. package/dist/packlets/store/vectorMaintenance.js +116 -8
  36. package/dist/packlets/store/vectorMaintenance.js.map +1 -1
  37. package/dist/packlets/store/vectorRecordSource.js +44 -0
  38. package/dist/packlets/store/vectorRecordSource.js.map +1 -0
  39. package/dist/packlets/tools/memoryTools.js +25 -2
  40. package/dist/packlets/tools/memoryTools.js.map +1 -1
  41. package/dist/packlets/types/envelope.js +25 -0
  42. package/dist/packlets/types/envelope.js.map +1 -1
  43. package/dist/packlets/types/index.js +1 -0
  44. package/dist/packlets/types/index.js.map +1 -1
  45. package/dist/packlets/types/recordResolver.js +6 -0
  46. package/dist/packlets/types/recordResolver.js.map +1 -0
  47. package/dist/packlets/types/temporal.js.map +1 -1
  48. package/dist/packlets/vector/inMemoryCosineIndex.js +39 -18
  49. package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  50. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +67 -12
  51. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  52. package/dist/packlets/vector/rebuildHelpers.js +38 -0
  53. package/dist/packlets/vector/rebuildHelpers.js.map +1 -0
  54. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  55. package/dist/ts-agent-memory.d.ts +1035 -106
  56. package/lib/packlets/index/memoryIndex.d.ts +118 -27
  57. package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
  58. package/lib/packlets/index/memoryIndex.js +23 -16
  59. package/lib/packlets/index/memoryIndex.js.map +1 -1
  60. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
  61. package/lib/packlets/ingest/orchestrator.js +13 -1
  62. package/lib/packlets/ingest/orchestrator.js.map +1 -1
  63. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +3 -10
  64. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
  65. package/lib/packlets/retrieve/linkTraversalRetriever.js +11 -25
  66. package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  67. package/lib/packlets/retrieve/recencyRetriever.d.ts +3 -3
  68. package/lib/packlets/retrieve/recencyRetriever.d.ts.map +1 -1
  69. package/lib/packlets/retrieve/recencyRetriever.js +6 -6
  70. package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
  71. package/lib/packlets/retrieve/retriever.d.ts +88 -7
  72. package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
  73. package/lib/packlets/retrieve/retriever.js +94 -9
  74. package/lib/packlets/retrieve/retriever.js.map +1 -1
  75. package/lib/packlets/retrieve/semanticRetriever.d.ts +3 -5
  76. package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
  77. package/lib/packlets/retrieve/semanticRetriever.js +15 -15
  78. package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
  79. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts +3 -3
  80. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts.map +1 -1
  81. package/lib/packlets/retrieve/structuredFilterRetriever.js +6 -6
  82. package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  83. package/lib/packlets/retrieve/tagRetriever.d.ts +3 -3
  84. package/lib/packlets/retrieve/tagRetriever.d.ts.map +1 -1
  85. package/lib/packlets/retrieve/tagRetriever.js +6 -6
  86. package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
  87. package/lib/packlets/retrieve/temporalRetrievers.d.ts +7 -5
  88. package/lib/packlets/retrieve/temporalRetrievers.d.ts.map +1 -1
  89. package/lib/packlets/retrieve/temporalRetrievers.js +22 -19
  90. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
  91. package/lib/packlets/store/coverage.d.ts +102 -0
  92. package/lib/packlets/store/coverage.d.ts.map +1 -0
  93. package/lib/packlets/store/coverage.js +7 -0
  94. package/lib/packlets/store/coverage.js.map +1 -0
  95. package/lib/packlets/store/fileTreeMemoryStore.d.ts +53 -166
  96. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  97. package/lib/packlets/store/fileTreeMemoryStore.js +221 -79
  98. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  99. package/lib/packlets/store/index.d.ts +4 -0
  100. package/lib/packlets/store/index.d.ts.map +1 -1
  101. package/lib/packlets/store/index.js +4 -0
  102. package/lib/packlets/store/index.js.map +1 -1
  103. package/lib/packlets/store/listSelection.d.ts +101 -0
  104. package/lib/packlets/store/listSelection.d.ts.map +1 -0
  105. package/lib/packlets/store/listSelection.js +40 -0
  106. package/lib/packlets/store/listSelection.js.map +1 -0
  107. package/lib/packlets/store/memoryStore.d.ts +237 -0
  108. package/lib/packlets/store/memoryStore.d.ts.map +1 -0
  109. package/lib/packlets/store/memoryStore.js +7 -0
  110. package/lib/packlets/store/memoryStore.js.map +1 -0
  111. package/lib/packlets/store/reconcile.d.ts +82 -0
  112. package/lib/packlets/store/reconcile.d.ts.map +1 -0
  113. package/lib/packlets/store/reconcile.js +7 -0
  114. package/lib/packlets/store/reconcile.js.map +1 -0
  115. package/lib/packlets/store/storeCoverage.d.ts +45 -0
  116. package/lib/packlets/store/storeCoverage.d.ts.map +1 -0
  117. package/lib/packlets/store/storeCoverage.js +105 -0
  118. package/lib/packlets/store/storeCoverage.js.map +1 -0
  119. package/lib/packlets/store/storeReconcile.d.ts +41 -0
  120. package/lib/packlets/store/storeReconcile.d.ts.map +1 -0
  121. package/lib/packlets/store/storeReconcile.js +125 -0
  122. package/lib/packlets/store/storeReconcile.js.map +1 -0
  123. package/lib/packlets/store/vectorMaintenance.d.ts +74 -0
  124. package/lib/packlets/store/vectorMaintenance.d.ts.map +1 -1
  125. package/lib/packlets/store/vectorMaintenance.js +117 -8
  126. package/lib/packlets/store/vectorMaintenance.js.map +1 -1
  127. package/lib/packlets/store/vectorRecordSource.d.ts +36 -0
  128. package/lib/packlets/store/vectorRecordSource.d.ts.map +1 -0
  129. package/lib/packlets/store/vectorRecordSource.js +47 -0
  130. package/lib/packlets/store/vectorRecordSource.js.map +1 -0
  131. package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
  132. package/lib/packlets/tools/memoryTools.js +25 -2
  133. package/lib/packlets/tools/memoryTools.js.map +1 -1
  134. package/lib/packlets/types/envelope.d.ts +24 -2
  135. package/lib/packlets/types/envelope.d.ts.map +1 -1
  136. package/lib/packlets/types/envelope.js +26 -0
  137. package/lib/packlets/types/envelope.js.map +1 -1
  138. package/lib/packlets/types/index.d.ts +1 -0
  139. package/lib/packlets/types/index.d.ts.map +1 -1
  140. package/lib/packlets/types/index.js +1 -0
  141. package/lib/packlets/types/index.js.map +1 -1
  142. package/lib/packlets/types/recordResolver.d.ts +39 -0
  143. package/lib/packlets/types/recordResolver.d.ts.map +1 -0
  144. package/lib/packlets/types/recordResolver.js +7 -0
  145. package/lib/packlets/types/recordResolver.js.map +1 -0
  146. package/lib/packlets/types/temporal.d.ts +26 -6
  147. package/lib/packlets/types/temporal.d.ts.map +1 -1
  148. package/lib/packlets/types/temporal.js.map +1 -1
  149. package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -2
  150. package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
  151. package/lib/packlets/vector/inMemoryCosineIndex.js +40 -19
  152. package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  153. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +6 -3
  154. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
  155. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +66 -11
  156. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  157. package/lib/packlets/vector/rebuildHelpers.d.ts +30 -0
  158. package/lib/packlets/vector/rebuildHelpers.d.ts.map +1 -0
  159. package/lib/packlets/vector/rebuildHelpers.js +42 -0
  160. package/lib/packlets/vector/rebuildHelpers.js.map +1 -0
  161. package/lib/packlets/vector/vectorIndex.d.ts +270 -15
  162. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  163. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  164. package/package.json +7 -7
@@ -5,6 +5,7 @@
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.edgeTargetKey = edgeTargetKey;
8
+ exports.embeddingRefOf = embeddingRefOf;
8
9
  /**
9
10
  * The canonical composite-key string for an {@link IEdgeTarget}: scope + id,
10
11
  * NUL-separated. NUL is excluded from both components (scope segments are
@@ -18,4 +19,29 @@ exports.edgeTargetKey = edgeTargetKey;
18
19
  function edgeTargetKey(target) {
19
20
  return `${target.scope}\0${target.id}`;
20
21
  }
22
+ /**
23
+ * The envelope's embedding reference if it carries a usable one, `undefined`
24
+ * otherwise — **the one place `null`-vs-absent is collapsed.**
25
+ *
26
+ * @remarks
27
+ * {@link IMemoryEnvelope.embeddingRef} is `string | null | undefined`, where
28
+ * `null` and absent both mean *not embedded* (`null` is the explicit sentinel;
29
+ * absent is the backwards-compat seam). That makes the obvious presence check
30
+ * wrong in **both** directions, and both mistakes were shipped before this
31
+ * accessor existed: `!== undefined` counts a `null` as an embedding that is not
32
+ * there, and `=== undefined` misses a `null` when looking for one that is
33
+ * missing. Neither is a type error, and neither is visible to a coverage gate,
34
+ * because the sentinel is a *value* rather than a branch.
35
+ *
36
+ * Returning the reference rather than a boolean is deliberate: a caller that
37
+ * needs the string gets the check for free, so there is no second, weaker way
38
+ * to ask.
39
+ *
40
+ * **Do not test `embeddingRef` for presence directly — call this.**
41
+ * @public
42
+ */
43
+ function embeddingRefOf(envelope) {
44
+ var _a;
45
+ return (_a = envelope.embeddingRef) !== null && _a !== void 0 ? _a : undefined;
46
+ }
21
47
  //# sourceMappingURL=envelope.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../../src/packlets/types/envelope.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAmEH,sCAEC;AAZD;;;;;;;;;GASG;AACH,SAAgB,aAAa,CAAC,MAAmB;IAC/C,OAAO,GAAG,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC;AACzC,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { EntityId, Kind, LinkType, MemoryId, MemoryScopeKey, Tag } from './ids';\n\n/**\n * Origin of a provenance attribution. Open vocabulary: the three named\n * sources are conventional, but the `(string & {})` arm admits any other\n * source string without resignature.\n * @public\n */\nexport type ProvenanceSource = 'agent' | 'host-ingest' | 'human' | (string & {});\n\n/**\n * Structured provenance for a record or an edge. Never a flat enum — the\n * `[key: string]: unknown` index signature lets a consumer attach an opaque\n * domain payload (e.g. PersonAIlity's sentiment / epistemic blocks) without\n * changing this interface, while still satisfying the no-`any` rule.\n * @public\n */\nexport interface IProvenance {\n /** Where the attribution came from. */\n readonly source: ProvenanceSource;\n /** Optional human or agent identifier responsible for the write. */\n readonly by?: string;\n /** Optional model identifier, when a model produced the content. */\n readonly model?: string;\n /** Optional confidence in `[0, 1]`. */\n readonly confidence?: number;\n /**\n * Scope-qualified back-link to the source record. Enables the cross-kind\n * provenance spine. A scope-qualified {@link IEdgeTarget} (not a bare\n * {@link MemoryId}) because per-scope codecs (e.g. the MTM codec's `turn-<n>`\n * stems) legally reuse a stem across scopes, so a bare id would be ambiguous —\n * the same reason {@link IEdge.target} is scope-qualified.\n */\n readonly derivedFrom?: IEdgeTarget;\n /** Opaque extension payload — consumer-owned, never interpreted by the store. */\n readonly [key: string]: unknown;\n}\n\n/**\n * The physical address of a linked-to record: the `(scope, id)` pair that\n * uniquely identifies it. Both components are required because a bare\n * {@link MemoryId} is NOT unique across scopes — per-scope codecs (e.g. the\n * medium-term codec's `turn-<n>` stems) legally mint the same stem under\n * different scopes, so an edge that carried only the id would be ambiguous.\n * `(scope, id)` matches the store's `getById(scope, id)` addressing and the\n * index's composite primary key.\n * @public\n */\nexport interface IEdgeTarget {\n /** The scope the target record lives under. */\n readonly scope: MemoryScopeKey;\n /** The target record's stable file-stem id (unique WITHIN {@link IEdgeTarget.scope}). */\n readonly id: MemoryId;\n}\n\n/**\n * The canonical composite-key string for an {@link IEdgeTarget}: scope + id,\n * NUL-separated. NUL is excluded from both components (scope segments are\n * filename-safe; {@link MemoryId} is portable-filename-safe), so it is a\n * collision-proof separator. This is the ONE canonicalization every consumer\n * that keys on a scoped target uses — the backlink index, the cycle guard, and\n * the ingest edge-validation path all route through it so their notions of\n * \"same target\" cannot drift.\n * @public\n */\nexport function edgeTargetKey(target: IEdgeTarget): string {\n return `${target.scope}\\0${target.id}`;\n}\n\n/**\n * An attributed link between two records. Carries the relation type, the\n * scope-qualified {@link IEdgeTarget | target}, and optional confidence /\n * provenance / world-truth validity. Replaces bare string references (e.g.\n * PersonAIlity's `IMtmRef` becomes an `IEdge` with `type: LinkType('mtm-ref')`).\n * @public\n */\nexport interface IEdge {\n /** Open-vocabulary relation type. */\n readonly type: LinkType;\n /** The scope-qualified address of the linked-to record. */\n readonly target: IEdgeTarget;\n /** Optional confidence in `[0, 1]`. */\n readonly confidence?: number;\n /** Optional structured provenance for the link itself. */\n readonly provenance?: IProvenance;\n /** World-truth validity start (epoch ms). Present only on temporal edges. */\n readonly valid_at?: number;\n /**\n * World-truth validity end (epoch ms). `null` = still valid; absent = no\n * temporal extent.\n */\n // eslint-disable-next-line @rushstack/no-new-null -- null is a meaningful value here (still-valid) distinct from absent (no temporal extent); design-lock §2.3\n readonly invalid_at?: number | null;\n}\n\n/**\n * Optional bi-temporal validity block on an envelope. Present only on\n * temporal kinds; absent = zero cost for atemporal kinds.\n * @public\n */\nexport interface ITemporalBlock {\n /** World-truth validity start (epoch ms). */\n readonly valid_at?: number;\n /** World-truth validity end (epoch ms). `null` = still valid. */\n // eslint-disable-next-line @rushstack/no-new-null -- null is a meaningful value here (still-valid) distinct from absent; design-lock §2.4\n readonly invalid_at?: number | null;\n}\n\n/**\n * The invariant identity + transaction-time envelope carried by every memory\n * record, independent of the per-kind body.\n * @public\n */\nexport interface IMemoryEnvelope {\n // --- Core identity ---\n /** Stable file-stem identifier. MUST equal the on-disk filename stem. */\n readonly id: MemoryId;\n /** Consumer-supplied domain key. Equals {@link IMemoryEnvelope.id | id} for non-temporal kinds. */\n readonly entityId: EntityId;\n /** Consumer-registered kind; dispatches the body Converter. */\n readonly kind: Kind;\n /** Open-vocabulary tags. */\n readonly tags: ReadonlyArray<Tag>;\n /** Attributed outbound edges. */\n readonly links: ReadonlyArray<IEdge>;\n\n // --- Transaction-time metadata (always present) ---\n /** Epoch ms of the first write. Immutable after creation. */\n readonly created: number;\n /** Epoch ms of the most recent write. */\n readonly updated: number;\n /**\n * Monotonic write counter within the store instance, assigned by the store\n * on every successful put. Enables stable cursor paging over observation\n * records without a full walk.\n */\n readonly seq: number;\n /**\n * Content hash over the canonical `{ kind, body, links }`. The dedup key:\n * an exact match is a no-op upsert that returns the existing record.\n */\n readonly contentHash: string;\n /**\n * Store-computed host-defined ordering value, produced by the kind's\n * {@link RankProjector} on every put/update and stamped into the envelope in\n * the same pass that recomputes {@link IMemoryEnvelope.contentHash | contentHash}.\n * Absent when the kind has no registered projector (or the projector threw on\n * this record). Ordered retrieval (`orderBy: 'rank'`) and the index's rank view\n * sort by this value descending, placing records with an absent `rank` last.\n *\n * **The projector runs on the write path only — registering one does not rank\n * records already in the store.** Because absent sorts last, those records land\n * below every subsequently-written one regardless of what the projector would\n * have scored them, so the result is not a partial ordering but one inverted\n * with respect to the projector's intent, with nothing failing to say so. Call\n * `IMemoryStore.reconcileRank` after registering a projector against a\n * populated store.\n */\n readonly rank?: number;\n /** Structured provenance (never a flat enum). */\n readonly provenance: IProvenance;\n\n // --- Optional temporal block ---\n /** Bi-temporal validity. Present only on temporal kinds. */\n readonly temporal?: ITemporalBlock;\n\n /**\n * Vector-index entry reference, set by the vector index on write. `null` =\n * not embedded; absent = same as `null` (backwards-compat seam).\n */\n // eslint-disable-next-line @rushstack/no-new-null -- null is the explicit \"not embedded\" sentinel distinct from absent (backwards-compat seam); design-lock §2.5\n readonly embeddingRef?: string | null;\n}\n\n/**\n * A complete memory record: the invariant {@link IMemoryEnvelope} plus the\n * typed, per-kind body. The store's public surface uses\n * `IMemoryRecord<unknown>`; consumers narrow `TBody` by checking\n * `envelope.kind` and validating through the registered Converter.\n * @public\n */\nexport interface IMemoryRecord<TBody = unknown> {\n /** The invariant identity + transaction-time envelope. */\n readonly envelope: IMemoryEnvelope;\n /** The per-kind, Converter-validated body. */\n readonly body: TBody;\n}\n\n/**\n * A per-kind host projection from a fully-resolved (post-merge) memory record\n * to a numeric ordering value. Registered per kind at store construction (see\n * `rankProjectors`); the store runs it on every put/update over the same\n * resolved record whose `contentHash` it computes, stamping the result into\n * {@link IMemoryEnvelope.rank}. The store never interprets the body — the host\n * owns what the number means. A projector that throws is treated as \"no rank\n * for this record\" (logged at `warn`), never failing the write.\n *\n * Runs on writes only. To apply a newly-registered projector to records that\n * already exist, call `IMemoryStore.reconcileRank`.\n * @public\n */\nexport type RankProjector = (record: IMemoryRecord<unknown>) => number;\n"]}
1
+ {"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../../src/packlets/types/envelope.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAmEH,sCAEC;AA8ID,wCAEC;AA5JD;;;;;;;;;GASG;AACH,SAAgB,aAAa,CAAC,MAAmB;IAC/C,OAAO,GAAG,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC;AACzC,CAAC;AAyHD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,SAAgB,cAAc,CAAC,QAAyB;;IACtD,OAAO,MAAA,QAAQ,CAAC,YAAY,mCAAI,SAAS,CAAC;AAC5C,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { EntityId, Kind, LinkType, MemoryId, MemoryScopeKey, Tag } from './ids';\n\n/**\n * Origin of a provenance attribution. Open vocabulary: the three named\n * sources are conventional, but the `(string & {})` arm admits any other\n * source string without resignature.\n * @public\n */\nexport type ProvenanceSource = 'agent' | 'host-ingest' | 'human' | (string & {});\n\n/**\n * Structured provenance for a record or an edge. Never a flat enum — the\n * `[key: string]: unknown` index signature lets a consumer attach an opaque\n * domain payload (e.g. PersonAIlity's sentiment / epistemic blocks) without\n * changing this interface, while still satisfying the no-`any` rule.\n * @public\n */\nexport interface IProvenance {\n /** Where the attribution came from. */\n readonly source: ProvenanceSource;\n /** Optional human or agent identifier responsible for the write. */\n readonly by?: string;\n /** Optional model identifier, when a model produced the content. */\n readonly model?: string;\n /** Optional confidence in `[0, 1]`. */\n readonly confidence?: number;\n /**\n * Scope-qualified back-link to the source record. Enables the cross-kind\n * provenance spine. A scope-qualified {@link IEdgeTarget} (not a bare\n * {@link MemoryId}) because per-scope codecs (e.g. the MTM codec's `turn-<n>`\n * stems) legally reuse a stem across scopes, so a bare id would be ambiguous —\n * the same reason {@link IEdge.target} is scope-qualified.\n */\n readonly derivedFrom?: IEdgeTarget;\n /** Opaque extension payload — consumer-owned, never interpreted by the store. */\n readonly [key: string]: unknown;\n}\n\n/**\n * The physical address of a linked-to record: the `(scope, id)` pair that\n * uniquely identifies it. Both components are required because a bare\n * {@link MemoryId} is NOT unique across scopes — per-scope codecs (e.g. the\n * medium-term codec's `turn-<n>` stems) legally mint the same stem under\n * different scopes, so an edge that carried only the id would be ambiguous.\n * `(scope, id)` matches the store's `getById(scope, id)` addressing and the\n * index's composite primary key.\n * @public\n */\nexport interface IEdgeTarget {\n /** The scope the target record lives under. */\n readonly scope: MemoryScopeKey;\n /** The target record's stable file-stem id (unique WITHIN {@link IEdgeTarget.scope}). */\n readonly id: MemoryId;\n}\n\n/**\n * The canonical composite-key string for an {@link IEdgeTarget}: scope + id,\n * NUL-separated. NUL is excluded from both components (scope segments are\n * filename-safe; {@link MemoryId} is portable-filename-safe), so it is a\n * collision-proof separator. This is the ONE canonicalization every consumer\n * that keys on a scoped target uses — the backlink index, the cycle guard, and\n * the ingest edge-validation path all route through it so their notions of\n * \"same target\" cannot drift.\n * @public\n */\nexport function edgeTargetKey(target: IEdgeTarget): string {\n return `${target.scope}\\0${target.id}`;\n}\n\n/**\n * An attributed link between two records. Carries the relation type, the\n * scope-qualified {@link IEdgeTarget | target}, and optional confidence /\n * provenance / world-truth validity. Replaces bare string references (e.g.\n * PersonAIlity's `IMtmRef` becomes an `IEdge` with `type: LinkType('mtm-ref')`).\n * @public\n */\nexport interface IEdge {\n /** Open-vocabulary relation type. */\n readonly type: LinkType;\n /** The scope-qualified address of the linked-to record. */\n readonly target: IEdgeTarget;\n /** Optional confidence in `[0, 1]`. */\n readonly confidence?: number;\n /** Optional structured provenance for the link itself. */\n readonly provenance?: IProvenance;\n /** World-truth validity start (epoch ms). Present only on temporal edges. */\n readonly valid_at?: number;\n /**\n * World-truth validity end (epoch ms). `null` = still valid; absent = no\n * temporal extent.\n */\n // eslint-disable-next-line @rushstack/no-new-null -- null is a meaningful value here (still-valid) distinct from absent (no temporal extent); design-lock §2.3\n readonly invalid_at?: number | null;\n}\n\n/**\n * Optional bi-temporal validity block on an envelope. Present only on\n * temporal kinds; absent = zero cost for atemporal kinds.\n * @public\n */\nexport interface ITemporalBlock {\n /** World-truth validity start (epoch ms). */\n readonly valid_at?: number;\n /** World-truth validity end (epoch ms). `null` = still valid. */\n // eslint-disable-next-line @rushstack/no-new-null -- null is a meaningful value here (still-valid) distinct from absent; design-lock §2.4\n readonly invalid_at?: number | null;\n}\n\n/**\n * The invariant identity + transaction-time envelope carried by every memory\n * record, independent of the per-kind body.\n * @public\n */\nexport interface IMemoryEnvelope {\n // --- Core identity ---\n /** Stable file-stem identifier. MUST equal the on-disk filename stem. */\n readonly id: MemoryId;\n /** Consumer-supplied domain key. Equals {@link IMemoryEnvelope.id | id} for non-temporal kinds. */\n readonly entityId: EntityId;\n /** Consumer-registered kind; dispatches the body Converter. */\n readonly kind: Kind;\n /** Open-vocabulary tags. */\n readonly tags: ReadonlyArray<Tag>;\n /** Attributed outbound edges. */\n readonly links: ReadonlyArray<IEdge>;\n\n // --- Transaction-time metadata (always present) ---\n /** Epoch ms of the first write. Immutable after creation. */\n readonly created: number;\n /** Epoch ms of the most recent write. */\n readonly updated: number;\n /**\n * Monotonic write counter within the store instance, assigned by the store\n * on every successful put. Enables stable cursor paging over observation\n * records without a full walk.\n */\n readonly seq: number;\n /**\n * Content hash over the canonical `{ kind, body, links }`. The dedup key:\n * an exact match is a no-op upsert that returns the existing record.\n */\n readonly contentHash: string;\n /**\n * Store-computed host-defined ordering value, produced by the kind's\n * {@link RankProjector} on every put/update and stamped into the envelope in\n * the same pass that recomputes {@link IMemoryEnvelope.contentHash | contentHash}.\n * Absent when the kind has no registered projector (or the projector threw on\n * this record). Ordered retrieval (`orderBy: 'rank'`) and the index's rank view\n * sort by this value descending, placing records with an absent `rank` last.\n *\n * **The projector runs on the write path only — registering one does not rank\n * records already in the store.** Because absent sorts last, those records land\n * below every subsequently-written one regardless of what the projector would\n * have scored them, so the result is not a partial ordering but one inverted\n * with respect to the projector's intent, with nothing failing to say so. Call\n * `IMemoryStore.reconcile(kind, 'rank')` after registering a projector against a\n * populated store.\n */\n readonly rank?: number;\n /** Structured provenance (never a flat enum). */\n readonly provenance: IProvenance;\n\n // --- Optional temporal block ---\n /** Bi-temporal validity. Present only on temporal kinds. */\n readonly temporal?: ITemporalBlock;\n\n /**\n * Vector-index entry reference, set by the vector index on write. `null` =\n * not embedded; absent = same as `null` (backwards-compat seam).\n */\n // eslint-disable-next-line @rushstack/no-new-null -- null is the explicit \"not embedded\" sentinel distinct from absent (backwards-compat seam); design-lock §2.5\n readonly embeddingRef?: string | null;\n}\n\n/**\n * A complete memory record: the invariant {@link IMemoryEnvelope} plus the\n * typed, per-kind body. The store's public surface uses\n * `IMemoryRecord<unknown>`; consumers narrow `TBody` by checking\n * `envelope.kind` and validating through the registered Converter.\n * @public\n */\nexport interface IMemoryRecord<TBody = unknown> {\n /** The invariant identity + transaction-time envelope. */\n readonly envelope: IMemoryEnvelope;\n /** The per-kind, Converter-validated body. */\n readonly body: TBody;\n}\n\n/**\n * The envelope's embedding reference if it carries a usable one, `undefined`\n * otherwise — **the one place `null`-vs-absent is collapsed.**\n *\n * @remarks\n * {@link IMemoryEnvelope.embeddingRef} is `string | null | undefined`, where\n * `null` and absent both mean *not embedded* (`null` is the explicit sentinel;\n * absent is the backwards-compat seam). That makes the obvious presence check\n * wrong in **both** directions, and both mistakes were shipped before this\n * accessor existed: `!== undefined` counts a `null` as an embedding that is not\n * there, and `=== undefined` misses a `null` when looking for one that is\n * missing. Neither is a type error, and neither is visible to a coverage gate,\n * because the sentinel is a *value* rather than a branch.\n *\n * Returning the reference rather than a boolean is deliberate: a caller that\n * needs the string gets the check for free, so there is no second, weaker way\n * to ask.\n *\n * **Do not test `embeddingRef` for presence directly — call this.**\n * @public\n */\nexport function embeddingRefOf(envelope: IMemoryEnvelope): string | undefined {\n return envelope.embeddingRef ?? undefined;\n}\n\n/**\n * A per-kind host projection from a fully-resolved (post-merge) memory record\n * to a numeric ordering value. Registered per kind at store construction (see\n * `rankProjectors`); the store runs it on every put/update over the same\n * resolved record whose `contentHash` it computes, stamping the result into\n * {@link IMemoryEnvelope.rank}. The store never interprets the body — the host\n * owns what the number means. A projector that throws is treated as \"no rank\n * for this record\" (logged at `warn`), never failing the write.\n *\n * Runs on writes only. To apply a newly-registered projector to records that\n * already exist, call `IMemoryStore.reconcile(kind, 'rank')`.\n * @public\n */\nexport type RankProjector = (record: IMemoryRecord<unknown>) => number;\n"]}
@@ -2,6 +2,7 @@ export * from './ids';
2
2
  export * from './envelope';
3
3
  export * from './filenameSafety';
4
4
  export * from './identityCodec';
5
+ export * from './recordResolver';
5
6
  export * from './temporal';
6
7
  export * from './writePolicy';
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/packlets/types/index.ts"],"names":[],"mappings":"AAKA,cAAc,OAAO,CAAC;AACtB,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/packlets/types/index.ts"],"names":[],"mappings":"AAKA,cAAc,OAAO,CAAC;AACtB,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC"}
@@ -22,6 +22,7 @@ __exportStar(require("./ids"), exports);
22
22
  __exportStar(require("./envelope"), exports);
23
23
  __exportStar(require("./filenameSafety"), exports);
24
24
  __exportStar(require("./identityCodec"), exports);
25
+ __exportStar(require("./recordResolver"), exports);
25
26
  __exportStar(require("./temporal"), exports);
26
27
  __exportStar(require("./writePolicy"), exports);
27
28
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/types/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;;;;;;;;;;;;AAEH,wCAAsB;AACtB,6CAA2B;AAC3B,mDAAiC;AACjC,kDAAgC;AAChC,6CAA2B;AAC3B,gDAA8B","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './ids';\nexport * from './envelope';\nexport * from './filenameSafety';\nexport * from './identityCodec';\nexport * from './temporal';\nexport * from './writePolicy';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/types/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;;;;;;;;;;;;AAEH,wCAAsB;AACtB,6CAA2B;AAC3B,mDAAiC;AACjC,kDAAgC;AAChC,mDAAiC;AACjC,6CAA2B;AAC3B,gDAA8B","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './ids';\nexport * from './envelope';\nexport * from './filenameSafety';\nexport * from './identityCodec';\nexport * from './recordResolver';\nexport * from './temporal';\nexport * from './writePolicy';\n"]}
@@ -0,0 +1,39 @@
1
+ import { Result } from '@fgv/ts-utils';
2
+ import { IMemoryRecord } from './envelope';
3
+ import { MemoryId, MemoryScopeKey } from './ids';
4
+ /**
5
+ * Materializes one record's body from its scope-qualified address — the other
6
+ * half of the partial-read split, and the narrowest seam that can be.
7
+ *
8
+ * @remarks
9
+ * The index holds `IIndexedMemoryEntry` (scope + envelope, no body), so anything
10
+ * that *selects* works from envelopes and anything that must *return records*
11
+ * resolves the survivors through this. `FileTreeMemoryStore` implements it over
12
+ * the same read path `getById` uses, so a resolved record is byte-identical to a
13
+ * keyed read, verification included.
14
+ *
15
+ * **Deliberately one method, and deliberately not the store.** A retriever needs
16
+ * exactly this capability; handing it an `IMemoryStore` would hand it `put` and
17
+ * `delete` as well, and would invert the construction direction (the store builds
18
+ * retrievers today, not the reverse). One method also keeps a test double
19
+ * trivial.
20
+ *
21
+ * Synchronous `Result` rather than `Promise<Result>` because every shipped
22
+ * `FileTree` backend resolves without awaiting, and an async signature here would
23
+ * have rippled through retrievers that are otherwise synchronous over the index.
24
+ * A future backend that genuinely needs I/O should be adapted at its own boundary
25
+ * rather than by making this contract async for everyone.
26
+ *
27
+ * Resolving an address the vault does not hold is `succeed(undefined)`, not a
28
+ * failure — an entry can legitimately vanish between selection and
29
+ * materialization (a concurrent delete), and that is a miss rather than a fault.
30
+ * A failure means the record is there and could not be read.
31
+ * @public
32
+ */
33
+ export interface IMemoryRecordResolver {
34
+ /**
35
+ * The record at `(scope, id)`, or `undefined` if the vault does not hold one.
36
+ */
37
+ resolveRecord(scope: MemoryScopeKey, id: MemoryId): Result<IMemoryRecord<unknown> | undefined>;
38
+ }
39
+ //# sourceMappingURL=recordResolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recordResolver.d.ts","sourceRoot":"","sources":["../../../src/packlets/types/recordResolver.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,aAAa,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,QAAQ,GAAG,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC;CAChG"}
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2026 Erik Fortune
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ //# sourceMappingURL=recordResolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recordResolver.js","sourceRoot":"","sources":["../../../src/packlets/types/recordResolver.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result } from '@fgv/ts-utils';\nimport { IMemoryRecord } from './envelope';\nimport { MemoryId, MemoryScopeKey } from './ids';\n\n/**\n * Materializes one record's body from its scope-qualified address — the other\n * half of the partial-read split, and the narrowest seam that can be.\n *\n * @remarks\n * The index holds `IIndexedMemoryEntry` (scope + envelope, no body), so anything\n * that *selects* works from envelopes and anything that must *return records*\n * resolves the survivors through this. `FileTreeMemoryStore` implements it over\n * the same read path `getById` uses, so a resolved record is byte-identical to a\n * keyed read, verification included.\n *\n * **Deliberately one method, and deliberately not the store.** A retriever needs\n * exactly this capability; handing it an `IMemoryStore` would hand it `put` and\n * `delete` as well, and would invert the construction direction (the store builds\n * retrievers today, not the reverse). One method also keeps a test double\n * trivial.\n *\n * Synchronous `Result` rather than `Promise<Result>` because every shipped\n * `FileTree` backend resolves without awaiting, and an async signature here would\n * have rippled through retrievers that are otherwise synchronous over the index.\n * A future backend that genuinely needs I/O should be adapted at its own boundary\n * rather than by making this contract async for everyone.\n *\n * Resolving an address the vault does not hold is `succeed(undefined)`, not a\n * failure — an entry can legitimately vanish between selection and\n * materialization (a concurrent delete), and that is a miss rather than a fault.\n * A failure means the record is there and could not be read.\n * @public\n */\nexport interface IMemoryRecordResolver {\n /**\n * The record at `(scope, id)`, or `undefined` if the vault does not hold one.\n */\n resolveRecord(scope: MemoryScopeKey, id: MemoryId): Result<IMemoryRecord<unknown> | undefined>;\n}\n"]}
@@ -1,4 +1,24 @@
1
- import { IMemoryRecord } from './envelope';
1
+ import { IMemoryEnvelope } from './envelope';
2
+ /**
3
+ * The minimum a temporal helper needs: something carrying an envelope.
4
+ *
5
+ * @remarks
6
+ * Every predicate and selector in this module reads `envelope.temporal`,
7
+ * `envelope.created` and `envelope.seq` and **nothing else** — no body has ever
8
+ * been consulted. Taking the structural shape rather than `IMemoryRecord` lets
9
+ * the same helpers serve both an `IMemoryRecord<unknown>` (which satisfies it)
10
+ * and an `IIndexedMemoryEntry` from the index's projected read surface, so the
11
+ * temporal `asOf` projection can run over envelopes and materialize only the
12
+ * versions that survive it.
13
+ *
14
+ * The selectors are generic in this shape so they return exactly what they were
15
+ * given rather than widening to the constraint.
16
+ * @public
17
+ */
18
+ export interface IEnvelopeCarrier {
19
+ /** The envelope the temporal predicates read. */
20
+ readonly envelope: IMemoryEnvelope;
21
+ }
2
22
  /**
3
23
  * Whether a record participates in the versioned (temporal) layout. A temporal
4
24
  * record always carries a {@link ITemporalBlock | temporal} block (the store
@@ -7,14 +27,14 @@ import { IMemoryRecord } from './envelope';
7
27
  * divergence (MTM is flat yet has `entityId !== id`).
8
28
  * @public
9
29
  */
10
- export declare function isTemporalRecord(record: IMemoryRecord<unknown>): boolean;
30
+ export declare function isTemporalRecord(record: IEnvelopeCarrier): boolean;
11
31
  /**
12
32
  * Whether a temporal record is a *current* version — its `temporal.invalid_at`
13
33
  * is `null` or absent (the still-valid sentinel). A non-temporal record is never
14
34
  * current in this sense (returns `false`).
15
35
  * @public
16
36
  */
17
- export declare function isVersionCurrent(record: IMemoryRecord<unknown>): boolean;
37
+ export declare function isVersionCurrent(record: IEnvelopeCarrier): boolean;
18
38
  /**
19
39
  * Whether a temporal record's validity interval contains `asOf` (epoch ms):
20
40
  * `valid_at <= asOf` and (`invalid_at` is null/absent OR `asOf < invalid_at`).
@@ -22,19 +42,19 @@ export declare function isVersionCurrent(record: IMemoryRecord<unknown>): boolea
22
42
  * record is never "valid at" a point (returns `false`).
23
43
  * @public
24
44
  */
25
- export declare function isVersionValidAt(record: IMemoryRecord<unknown>, asOf: number): boolean;
45
+ export declare function isVersionValidAt(record: IEnvelopeCarrier, asOf: number): boolean;
26
46
  /**
27
47
  * Select the current version from a set of an entity's versions: the newest
28
48
  * (highest `seq`) version whose `invalid_at` is null/absent. `undefined` when the
29
49
  * entity has no current version (fully invalidated / soft-deleted, or empty).
30
50
  * @public
31
51
  */
32
- export declare function selectCurrentVersion(versions: ReadonlyArray<IMemoryRecord<unknown>>): IMemoryRecord<unknown> | undefined;
52
+ export declare function selectCurrentVersion<T extends IEnvelopeCarrier>(versions: ReadonlyArray<T>): T | undefined;
33
53
  /**
34
54
  * Select the version of an entity valid at `asOf` (epoch ms): the newest
35
55
  * (highest `seq`) version whose validity interval contains `asOf`. `undefined`
36
56
  * when no version was valid at that instant.
37
57
  * @public
38
58
  */
39
- export declare function selectVersionAsOf(versions: ReadonlyArray<IMemoryRecord<unknown>>, asOf: number): IMemoryRecord<unknown> | undefined;
59
+ export declare function selectVersionAsOf<T extends IEnvelopeCarrier>(versions: ReadonlyArray<T>, asOf: number): T | undefined;
40
60
  //# sourceMappingURL=temporal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"temporal.d.ts","sourceRoot":"","sources":["../../../src/packlets/types/temporal.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAExE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAMxE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CActF;AAkBD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,GAC9C,aAAa,CAAC,OAAO,CAAC,GAAG,SAAS,CAEpC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,EAC/C,IAAI,EAAE,MAAM,GACX,aAAa,CAAC,OAAO,CAAC,GAAG,SAAS,CAEpC"}
1
+ {"version":3,"file":"temporal.d.ts","sourceRoot":"","sources":["../../../src/packlets/types/temporal.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAElE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAMlE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAchF;AAkBD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,gBAAgB,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,SAAS,CAE1G;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,gBAAgB,EAC1D,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,EAC1B,IAAI,EAAE,MAAM,GACX,CAAC,GAAG,SAAS,CAEf"}
@@ -1 +1 @@
1
- {"version":3,"file":"temporal.js","sourceRoot":"","sources":["../../../src/packlets/types/temporal.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAYH,4CAEC;AAQD,4CAMC;AASD,4CAcC;AAwBD,oDAIC;AAQD,8CAKC;AAxFD;;;;;;;GAOG;AACH,SAAgB,gBAAgB,CAAC,MAA8B;IAC7D,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,KAAK,SAAS,CAAC;AAChD,CAAC;AAED;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,MAA8B;IAC7D,MAAM,QAAQ,GAAmD,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAC1F,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,QAAQ,CAAC,UAAU,KAAK,IAAI,IAAI,QAAQ,CAAC,UAAU,KAAK,SAAS,CAAC;AAC3E,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,gBAAgB,CAAC,MAA8B,EAAE,IAAY;;IAC3E,MAAM,QAAQ,GAAmD,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAC1F,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,KAAK,GAAW,MAAA,QAAQ,CAAC,QAAQ,mCAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;IACnE,IAAI,KAAK,GAAG,IAAI,EAAE,CAAC;QACjB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,GAAG,GAA8B,QAAQ,CAAC,UAAU,CAAC;IAC3D,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,IAAI,GAAG,GAAG,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAS,UAAU,CAAC,UAAiD;IACnE,IAAI,IAAwC,CAAC;IAC7C,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,IAAI,KAAK,SAAS,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;YACrE,IAAI,GAAG,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,SAAgB,oBAAoB,CAClC,QAA+C;IAE/C,OAAO,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC;AACvD,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAC/B,QAA+C,EAC/C,IAAY;IAEZ,OAAO,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;AACnF,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { IMemoryRecord } from './envelope';\n\n/**\n * Whether a record participates in the versioned (temporal) layout. A temporal\n * record always carries a {@link ITemporalBlock | temporal} block (the store\n * stamps `valid_at` on every versioned write); an atemporal record never does,\n * so presence of `temporal` is the discriminator. Independent of `id`/`entityId`\n * divergence (MTM is flat yet has `entityId !== id`).\n * @public\n */\nexport function isTemporalRecord(record: IMemoryRecord<unknown>): boolean {\n return record.envelope.temporal !== undefined;\n}\n\n/**\n * Whether a temporal record is a *current* version — its `temporal.invalid_at`\n * is `null` or absent (the still-valid sentinel). A non-temporal record is never\n * current in this sense (returns `false`).\n * @public\n */\nexport function isVersionCurrent(record: IMemoryRecord<unknown>): boolean {\n const temporal: IMemoryRecord<unknown>['envelope']['temporal'] = record.envelope.temporal;\n if (temporal === undefined) {\n return false;\n }\n return temporal.invalid_at === null || temporal.invalid_at === undefined;\n}\n\n/**\n * Whether a temporal record's validity interval contains `asOf` (epoch ms):\n * `valid_at <= asOf` and (`invalid_at` is null/absent OR `asOf < invalid_at`).\n * The version's `valid_at` defaults to its `created` when absent; a non-temporal\n * record is never \"valid at\" a point (returns `false`).\n * @public\n */\nexport function isVersionValidAt(record: IMemoryRecord<unknown>, asOf: number): boolean {\n const temporal: IMemoryRecord<unknown>['envelope']['temporal'] = record.envelope.temporal;\n if (temporal === undefined) {\n return false;\n }\n const start: number = temporal.valid_at ?? record.envelope.created;\n if (start > asOf) {\n return false;\n }\n const end: number | null | undefined = temporal.invalid_at;\n if (end === null || end === undefined) {\n return true;\n }\n return asOf < end;\n}\n\n/**\n * The version with the highest `seq` among `candidates` (undefined when empty).\n * `seq` is the store's monotonic write counter, so highest `seq` is the newest\n * version. Shared tiebreak for {@link selectCurrentVersion} /\n * {@link selectVersionAsOf}.\n */\nfunction highestSeq(candidates: ReadonlyArray<IMemoryRecord<unknown>>): IMemoryRecord<unknown> | undefined {\n let best: IMemoryRecord<unknown> | undefined;\n for (const candidate of candidates) {\n if (best === undefined || candidate.envelope.seq > best.envelope.seq) {\n best = candidate;\n }\n }\n return best;\n}\n\n/**\n * Select the current version from a set of an entity's versions: the newest\n * (highest `seq`) version whose `invalid_at` is null/absent. `undefined` when the\n * entity has no current version (fully invalidated / soft-deleted, or empty).\n * @public\n */\nexport function selectCurrentVersion(\n versions: ReadonlyArray<IMemoryRecord<unknown>>\n): IMemoryRecord<unknown> | undefined {\n return highestSeq(versions.filter(isVersionCurrent));\n}\n\n/**\n * Select the version of an entity valid at `asOf` (epoch ms): the newest\n * (highest `seq`) version whose validity interval contains `asOf`. `undefined`\n * when no version was valid at that instant.\n * @public\n */\nexport function selectVersionAsOf(\n versions: ReadonlyArray<IMemoryRecord<unknown>>,\n asOf: number\n): IMemoryRecord<unknown> | undefined {\n return highestSeq(versions.filter((version) => isVersionValidAt(version, asOf)));\n}\n"]}
1
+ {"version":3,"file":"temporal.js","sourceRoot":"","sources":["../../../src/packlets/types/temporal.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAiCH,4CAEC;AAQD,4CAMC;AASD,4CAcC;AAwBD,oDAEC;AAQD,8CAKC;AAtFD;;;;;;;GAOG;AACH,SAAgB,gBAAgB,CAAC,MAAwB;IACvD,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,KAAK,SAAS,CAAC;AAChD,CAAC;AAED;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,MAAwB;IACvD,MAAM,QAAQ,GAAgC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;IACvE,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,QAAQ,CAAC,UAAU,KAAK,IAAI,IAAI,QAAQ,CAAC,UAAU,KAAK,SAAS,CAAC;AAC3E,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,gBAAgB,CAAC,MAAwB,EAAE,IAAY;;IACrE,MAAM,QAAQ,GAAgC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;IACvE,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,KAAK,GAAW,MAAA,QAAQ,CAAC,QAAQ,mCAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;IACnE,IAAI,KAAK,GAAG,IAAI,EAAE,CAAC;QACjB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,GAAG,GAA8B,QAAQ,CAAC,UAAU,CAAC;IAC3D,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,IAAI,GAAG,GAAG,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAS,UAAU,CAA6B,UAA4B;IAC1E,IAAI,IAAmB,CAAC;IACxB,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,IAAI,KAAK,SAAS,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;YACrE,IAAI,GAAG,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,SAAgB,oBAAoB,CAA6B,QAA0B;IACzF,OAAO,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC;AACvD,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAC/B,QAA0B,EAC1B,IAAY;IAEZ,OAAO,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;AACnF,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { IMemoryEnvelope } from './envelope';\n\n/**\n * The minimum a temporal helper needs: something carrying an envelope.\n *\n * @remarks\n * Every predicate and selector in this module reads `envelope.temporal`,\n * `envelope.created` and `envelope.seq` and **nothing else** — no body has ever\n * been consulted. Taking the structural shape rather than `IMemoryRecord` lets\n * the same helpers serve both an `IMemoryRecord<unknown>` (which satisfies it)\n * and an `IIndexedMemoryEntry` from the index's projected read surface, so the\n * temporal `asOf` projection can run over envelopes and materialize only the\n * versions that survive it.\n *\n * The selectors are generic in this shape so they return exactly what they were\n * given rather than widening to the constraint.\n * @public\n */\nexport interface IEnvelopeCarrier {\n /** The envelope the temporal predicates read. */\n readonly envelope: IMemoryEnvelope;\n}\n\n/**\n * Whether a record participates in the versioned (temporal) layout. A temporal\n * record always carries a {@link ITemporalBlock | temporal} block (the store\n * stamps `valid_at` on every versioned write); an atemporal record never does,\n * so presence of `temporal` is the discriminator. Independent of `id`/`entityId`\n * divergence (MTM is flat yet has `entityId !== id`).\n * @public\n */\nexport function isTemporalRecord(record: IEnvelopeCarrier): boolean {\n return record.envelope.temporal !== undefined;\n}\n\n/**\n * Whether a temporal record is a *current* version — its `temporal.invalid_at`\n * is `null` or absent (the still-valid sentinel). A non-temporal record is never\n * current in this sense (returns `false`).\n * @public\n */\nexport function isVersionCurrent(record: IEnvelopeCarrier): boolean {\n const temporal: IMemoryEnvelope['temporal'] = record.envelope.temporal;\n if (temporal === undefined) {\n return false;\n }\n return temporal.invalid_at === null || temporal.invalid_at === undefined;\n}\n\n/**\n * Whether a temporal record's validity interval contains `asOf` (epoch ms):\n * `valid_at <= asOf` and (`invalid_at` is null/absent OR `asOf < invalid_at`).\n * The version's `valid_at` defaults to its `created` when absent; a non-temporal\n * record is never \"valid at\" a point (returns `false`).\n * @public\n */\nexport function isVersionValidAt(record: IEnvelopeCarrier, asOf: number): boolean {\n const temporal: IMemoryEnvelope['temporal'] = record.envelope.temporal;\n if (temporal === undefined) {\n return false;\n }\n const start: number = temporal.valid_at ?? record.envelope.created;\n if (start > asOf) {\n return false;\n }\n const end: number | null | undefined = temporal.invalid_at;\n if (end === null || end === undefined) {\n return true;\n }\n return asOf < end;\n}\n\n/**\n * The version with the highest `seq` among `candidates` (undefined when empty).\n * `seq` is the store's monotonic write counter, so highest `seq` is the newest\n * version. Shared tiebreak for {@link selectCurrentVersion} /\n * {@link selectVersionAsOf}.\n */\nfunction highestSeq<T extends IEnvelopeCarrier>(candidates: ReadonlyArray<T>): T | undefined {\n let best: T | undefined;\n for (const candidate of candidates) {\n if (best === undefined || candidate.envelope.seq > best.envelope.seq) {\n best = candidate;\n }\n }\n return best;\n}\n\n/**\n * Select the current version from a set of an entity's versions: the newest\n * (highest `seq`) version whose `invalid_at` is null/absent. `undefined` when the\n * entity has no current version (fully invalidated / soft-deleted, or empty).\n * @public\n */\nexport function selectCurrentVersion<T extends IEnvelopeCarrier>(versions: ReadonlyArray<T>): T | undefined {\n return highestSeq(versions.filter(isVersionCurrent));\n}\n\n/**\n * Select the version of an entity valid at `asOf` (epoch ms): the newest\n * (highest `seq`) version whose validity interval contains `asOf`. `undefined`\n * when no version was valid at that instant.\n * @public\n */\nexport function selectVersionAsOf<T extends IEnvelopeCarrier>(\n versions: ReadonlyArray<T>,\n asOf: number\n): T | undefined {\n return highestSeq(versions.filter((version) => isVersionValidAt(version, asOf)));\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { Result } from '@fgv/ts-utils';
1
+ import { DetailedResult, Result } from '@fgv/ts-utils';
2
2
  import { IEdgeTarget } from '../types';
3
3
  import { IMemoryRecordSource, IVectorIndex, IVectorQueryHit, IVectorRebuildOptions, IVectorRebuildReport, MemoryEmbedder } from './vectorIndex';
4
4
  /**
@@ -40,6 +40,8 @@ export declare class InMemoryCosineIndex implements IVectorIndex {
40
40
  static create(): Result<InMemoryCosineIndex>;
41
41
  /** {@inheritDoc IVectorIndex.add} */
42
42
  add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>>;
43
+ /** {@inheritDoc IVectorIndex.has} */
44
+ has(target: IEdgeTarget): Promise<Result<boolean>>;
43
45
  /** {@inheritDoc IVectorIndex.remove} */
44
46
  remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
45
47
  /** {@inheritDoc IVectorIndex.query} */
@@ -72,6 +74,11 @@ export declare class InMemoryCosineIndex implements IVectorIndex {
72
74
  * A {@link MemoryEmbedder} decline is not a failure under either mode: it is
73
75
  * counted on {@link IVectorRebuildReport.declined} and never appears in `skipped`.
74
76
  *
77
+ * **A `'fail'` failure carries the partial report on its `detail`** — the
78
+ * rollback still runs, so that report describes the aborted attempt rather than
79
+ * the (now empty) index. The one failure with no detail is a `list` failure,
80
+ * which disturbs nothing and has nothing to describe.
81
+ *
75
82
  * Both consumer-supplied hooks are capture-wrapped, so a `source` or `embed`
76
83
  * that throws or rejects becomes a `Failure` on the path above rather than an
77
84
  * exception escaping mid-rebuild — which would bypass the rollback entirely.
@@ -80,7 +87,7 @@ export declare class InMemoryCosineIndex implements IVectorIndex {
80
87
  * @param embed - The embedder applied to each record.
81
88
  * @param options - Rebuild options; omit for the historical `'fail'` behavior.
82
89
  */
83
- rebuild(source: IMemoryRecordSource, embed: MemoryEmbedder, options?: IVectorRebuildOptions): Promise<Result<IVectorRebuildReport>>;
90
+ rebuild(source: IMemoryRecordSource, embed: MemoryEmbedder, options?: IVectorRebuildOptions): Promise<DetailedResult<IVectorRebuildReport, IVectorRebuildReport>>;
84
91
  /** Empty the index and forget the established dimension. */
85
92
  private _reset;
86
93
  /** The Euclidean magnitude (L2 norm) of a vector. */
@@ -1 +1 @@
1
- {"version":3,"file":"inMemoryCosineIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryCosineIndex.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAqC,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAiB,MAAM,UAAU,CAAC;AACtD,OAAO,EACL,mBAAmB,EAGnB,YAAY,EACZ,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,cAAc,EACf,MAAM,eAAe,CAAC;AAkBvB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,mBAAoB,YAAW,YAAY;IACtD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IACtD,+EAA+E;IAC/E,OAAO,CAAC,UAAU,CAAqB;IAEvC,OAAO;IAKP,4CAA4C;IAC5C,IAAW,IAAI,IAAI,MAAM,CAExB;IAED,iCAAiC;WACnB,MAAM,IAAI,MAAM,CAAC,mBAAmB,CAAC;IAInD,qCAAqC;IAC9B,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAsB9E,wCAAwC;IACjC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAKhE,uCAAuC;IAChC,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAyBjG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmCG;IACU,OAAO,CAClB,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,cAAc,EACrB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAwDxC,4DAA4D;IAC5D,OAAO,CAAC,MAAM;IAKd,qDAAqD;IACrD,OAAO,CAAC,MAAM,CAAC,UAAU;IAQzB;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,OAAO;CAWvB"}
1
+ {"version":3,"file":"inMemoryCosineIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryCosineIndex.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAoD,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,WAAW,EAAuB,MAAM,UAAU,CAAC;AAC5D,OAAO,EAEL,mBAAmB,EAEnB,YAAY,EACZ,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,cAAc,EACf,MAAM,eAAe,CAAC;AASvB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,mBAAoB,YAAW,YAAY;IACtD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IACtD,+EAA+E;IAC/E,OAAO,CAAC,UAAU,CAAqB;IAEvC,OAAO;IAKP,4CAA4C;IAC5C,IAAW,IAAI,IAAI,MAAM,CAExB;IAED,iCAAiC;WACnB,MAAM,IAAI,MAAM,CAAC,mBAAmB,CAAC;IAInD,qCAAqC;IAC9B,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAsB9E,qCAAqC;IAC9B,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAIzD,wCAAwC;IACjC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAKhE,uCAAuC;IAChC,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAyBjG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAwCG;IACU,OAAO,CAClB,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,cAAc,EACrB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,cAAc,CAAC,oBAAoB,EAAE,oBAAoB,CAAC,CAAC;IA4EtE,4DAA4D;IAC5D,OAAO,CAAC,MAAM;IAKd,qDAAqD;IACrD,OAAO,CAAC,MAAM,CAAC,UAAU;IAQzB;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,OAAO;CAWvB"}
@@ -7,15 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.InMemoryCosineIndex = void 0;
8
8
  const ts_utils_1 = require("@fgv/ts-utils");
9
9
  const types_1 = require("../types");
10
- /**
11
- * Invoke a consumer-supplied hook that already returns a `Result`, converting a
12
- * synchronous throw or a promise rejection into a `Failure` rather than letting
13
- * it escape. `captureAsyncResult` wraps the hook's own `Result`, so the outcome
14
- * is flattened back to one level.
15
- */
16
- async function invokeHook(hook) {
17
- return (await (0, ts_utils_1.captureAsyncResult)(hook)).onSuccess((inner) => inner);
18
- }
10
+ const rebuildHelpers_1 = require("./rebuildHelpers");
19
11
  /**
20
12
  * The brute-force, in-memory cosine {@link IVectorIndex}. Stores one
21
13
  * `Float32Array` per record and answers a query by computing cosine similarity
@@ -71,6 +63,10 @@ class InMemoryCosineIndex {
71
63
  // that key IS the entry reference.
72
64
  return Promise.resolve((0, ts_utils_1.succeed)(key));
73
65
  }
66
+ /** {@inheritDoc IVectorIndex.has} */
67
+ has(target) {
68
+ return Promise.resolve((0, ts_utils_1.succeed)(this._vectors.has((0, types_1.edgeTargetKey)(target))));
69
+ }
74
70
  /** {@inheritDoc IVectorIndex.remove} */
75
71
  remove(target) {
76
72
  this._vectors.delete((0, types_1.edgeTargetKey)(target));
@@ -125,6 +121,11 @@ class InMemoryCosineIndex {
125
121
  * A {@link MemoryEmbedder} decline is not a failure under either mode: it is
126
122
  * counted on {@link IVectorRebuildReport.declined} and never appears in `skipped`.
127
123
  *
124
+ * **A `'fail'` failure carries the partial report on its `detail`** — the
125
+ * rollback still runs, so that report describes the aborted attempt rather than
126
+ * the (now empty) index. The one failure with no detail is a `list` failure,
127
+ * which disturbs nothing and has nothing to describe.
128
+ *
128
129
  * Both consumer-supplied hooks are capture-wrapped, so a `source` or `embed`
129
130
  * that throws or rejects becomes a `Failure` on the path above rather than an
130
131
  * exception escaping mid-rebuild — which would bypass the rollback entirely.
@@ -136,7 +137,7 @@ class InMemoryCosineIndex {
136
137
  async rebuild(source, embed, options) {
137
138
  var _a;
138
139
  const lenient = ((_a = options === null || options === void 0 ? void 0 : options.onRecordError) !== null && _a !== void 0 ? _a : 'fail') === 'skip';
139
- const listed = await invokeHook(() => source.list());
140
+ const listed = await (0, rebuildHelpers_1.invokeHook)(() => source.list());
140
141
  if (listed.isFailure()) {
141
142
  // Deliberately BEFORE the reset. This used to reset first, on the reasoning
142
143
  // that no stale vectors should survive a failed rebuild — but a failed list
@@ -144,35 +145,50 @@ class InMemoryCosineIndex {
144
145
  // re-embedded yet, so there is no half-rebuilt state to guard against.
145
146
  // Leaving the prior contents intact is the more correct answer, and on the
146
147
  // durable sibling (`SqliteVecVectorIndex`) resetting here was data loss.
147
- return (0, ts_utils_1.fail)(`vector index rebuild: failed to list records: ${listed.message}`);
148
+ //
149
+ // No detail, for the same reason: an all-zero report would describe an index
150
+ // this call never touched.
151
+ return (0, ts_utils_1.failWithDetail)(`vector index rebuild: failed to list records: ${listed.message}`);
148
152
  }
149
153
  // From here a rebuild is genuinely starting, so clear. A mid-loop failure
150
154
  // under `'fail'` still resets, which is what keeps that contract honest.
151
155
  this._reset();
152
- let declined = 0;
156
+ const indexed = new Map();
157
+ const declined = new Map();
153
158
  const skipped = [];
154
- for (const scoped of listed.value) {
159
+ // Only the source knows what it filtered, so an absent `excluded` propagates
160
+ // as absent rather than becoming an empty map — "cannot say" and "excluded
161
+ // nothing" are different answers.
162
+ const report = () => ({
163
+ indexed,
164
+ declined,
165
+ excluded: listed.value.excluded,
166
+ skipped
167
+ });
168
+ for (const scoped of listed.value.records) {
169
+ const kind = scoped.record.envelope.kind;
155
170
  // Both hooks are consumer-supplied, so a throw or rejection is captured
156
171
  // into a `Failure` rather than escaping as an exception — otherwise a
157
172
  // badly-behaved embedder would reject out of `rebuild` mid-loop and leave
158
173
  // the index half-populated, which is precisely what the rollback below
159
174
  // exists to prevent.
160
- const embedded = await invokeHook(() => embed(scoped.record));
175
+ const embedded = await (0, rebuildHelpers_1.invokeHook)(() => embed(scoped.record));
161
176
  if (embedded.isFailure()) {
162
177
  const error = `vector index rebuild: embedding '${(0, types_1.edgeTargetKey)(scoped.target)}' failed: ${embedded.message}`;
163
178
  // `'fail'` keeps the historical all-or-nothing contract EXACTLY: reset and
164
179
  // abort, so a caller that retries a query sees a clean empty index rather
165
- // than a partially-rebuilt one it cannot reason about.
180
+ // than a partially-rebuilt one it cannot reason about. What is new is only
181
+ // that the failure also carries what the attempt had established.
166
182
  if (!lenient) {
167
183
  this._reset();
168
- return (0, ts_utils_1.fail)(error);
184
+ return (0, ts_utils_1.failWithDetail)(error, report());
169
185
  }
170
186
  skipped.push({ target: scoped.target, error });
171
187
  continue;
172
188
  }
173
189
  // A decline is not an error under either mode — it is counted, never skipped.
174
190
  if (embedded.value === undefined) {
175
- declined++;
191
+ (0, rebuildHelpers_1.tally)(declined, kind);
176
192
  continue;
177
193
  }
178
194
  const added = await this.add(scoped.target, embedded.value);
@@ -180,12 +196,17 @@ class InMemoryCosineIndex {
180
196
  const error = `vector index rebuild: ${added.message}`;
181
197
  if (!lenient) {
182
198
  this._reset();
183
- return (0, ts_utils_1.fail)(error);
199
+ return (0, ts_utils_1.failWithDetail)(error, report());
184
200
  }
185
201
  skipped.push({ target: scoped.target, error });
202
+ continue;
186
203
  }
204
+ // Tallied per successful add rather than read back off `size` at the end:
205
+ // `size` counts distinct addresses, and this count must line up with its
206
+ // per-record siblings so the three still sum to the records seen.
207
+ (0, rebuildHelpers_1.tally)(indexed, kind);
187
208
  }
188
- return (0, ts_utils_1.succeed)({ indexed: this._vectors.size, declined, skipped });
209
+ return (0, ts_utils_1.succeedWithDetail)(report());
189
210
  }
190
211
  /** Empty the index and forget the established dimension. */
191
212
  _reset() {
@@ -1 +1 @@
1
- {"version":3,"file":"inMemoryCosineIndex.js","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryCosineIndex.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, captureAsyncResult, fail, succeed } from '@fgv/ts-utils';\nimport { IEdgeTarget, edgeTargetKey } from '../types';\nimport {\n IMemoryRecordSource,\n IScopedMemoryRecord,\n ISkippedVectorRecord,\n IVectorIndex,\n IVectorQueryHit,\n IVectorRebuildOptions,\n IVectorRebuildReport,\n MemoryEmbedder\n} from './vectorIndex';\n\n/**\n * Invoke a consumer-supplied hook that already returns a `Result`, converting a\n * synchronous throw or a promise rejection into a `Failure` rather than letting\n * it escape. `captureAsyncResult` wraps the hook's own `Result`, so the outcome\n * is flattened back to one level.\n */\nasync function invokeHook<T>(hook: () => Promise<Result<T>>): Promise<Result<T>> {\n return (await captureAsyncResult(hook)).onSuccess((inner) => inner);\n}\n\n/** One stored embedding: the scope-qualified address plus its vector. */\ninterface IStoredVector {\n readonly target: IEdgeTarget;\n readonly vector: Float32Array;\n}\n\n/**\n * The brute-force, in-memory cosine {@link IVectorIndex}. Stores one\n * `Float32Array` per record and answers a query by computing cosine similarity\n * against every stored vector, returning the top-k by descending score.\n *\n * @remarks\n * This is the **complete** vector implementation for the fgv regime — large-N is\n * explicitly out of scope (the seam stays open for a consumer to swap an external\n * ANN backend once N grows beyond \"thousands of records\"). No external dependency\n * and no ANN structure: a linear scan over a few thousand vectors is well within\n * an interactive budget.\n *\n * The index has a single dimension established by the first vector added; every\n * subsequent `add` and every `query` vector must match that dimension or fail\n * loudly — a mismatched dimension is an embedder-wiring bug, never a silent\n * zero-similarity result. {@link InMemoryCosineIndex.rebuild | rebuild} clears\n * the index, so a re-embed with a different model (hence dimension) is supported.\n *\n * Persistence (a JSON sidecar) is deliberately out of scope for this layer — the\n * index is in-memory and rebuilt from the store via `rebuild`; a sidecar is a\n * future nicety.\n * @public\n */\nexport class InMemoryCosineIndex implements IVectorIndex {\n /**\n * Stored embeddings keyed by the canonical {@link edgeTargetKey} of the\n * record's scope-qualified address, so two records that share a filename stem\n * across scopes occupy distinct entries and never overwrite each other.\n */\n private readonly _vectors: Map<string, IStoredVector>;\n /** The dimension of every stored vector; `undefined` until the first `add`. */\n private _dimension: number | undefined;\n\n private constructor() {\n this._vectors = new Map<string, IStoredVector>();\n this._dimension = undefined;\n }\n\n /** The number of vectors currently held. */\n public get size(): number {\n return this._vectors.size;\n }\n\n /** Family-convention factory. */\n public static create(): Result<InMemoryCosineIndex> {\n return succeed(new InMemoryCosineIndex());\n }\n\n /** {@inheritDoc IVectorIndex.add} */\n public add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>> {\n const key: string = edgeTargetKey(target);\n if (vector.length === 0) {\n return Promise.resolve(fail(`vector index: cannot add '${key}': empty vector`));\n }\n if (this._dimension === undefined) {\n this._dimension = vector.length;\n } else if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `vector index: cannot add '${key}': dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n // Defensive copy: the caller may reuse or mutate the buffer after `add`, and\n // the index must keep serving the embedding it was given.\n this._vectors.set(key, { target, vector: Float32Array.from(vector) });\n // The in-memory index keys entries by the canonical scoped-target string, so\n // that key IS the entry reference.\n return Promise.resolve(succeed(key));\n }\n\n /** {@inheritDoc IVectorIndex.remove} */\n public remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>> {\n this._vectors.delete(edgeTargetKey(target));\n return Promise.resolve(succeed(target));\n }\n\n /** {@inheritDoc IVectorIndex.query} */\n public query(vector: Float32Array, topK: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (topK <= 0 || this._vectors.size === 0) {\n return Promise.resolve(succeed([]));\n }\n if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `vector index: query dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n const queryMagnitude: number = InMemoryCosineIndex._magnitude(vector);\n const hits: IVectorQueryHit[] = [];\n for (const stored of this._vectors.values()) {\n hits.push({\n target: stored.target,\n score: InMemoryCosineIndex._cosine(vector, queryMagnitude, stored.vector)\n });\n }\n // Descending by score; a `seq`-free tiebreak is unnecessary here because the\n // caller (SemanticRetriever) re-resolves hits against the record index.\n hits.sort((a, b) => b.score - a.score);\n return Promise.resolve(succeed(hits.length > topK ? hits.slice(0, topK) : hits));\n }\n\n /**\n * Re-embed every record from `source` and rebuild the index from scratch.\n * Clears the current contents (and the established dimension) first, so a\n * re-embed with a different model is supported. Returns an\n * {@link IVectorRebuildReport} describing what was indexed, declined and skipped.\n *\n * @remarks\n * **A failure to LIST is always fatal**, under either mode — and **leaves the\n * existing index untouched**: an unreadable source says nothing about which\n * records exist, so there is neither an honest partial to report nor any reason\n * to discard what is already held.\n *\n * Per-record embed/add failures are governed by\n * {@link IVectorRebuildOptions.onRecordError}, which defaults to `'fail'` —\n * **the historical all-or-nothing contract, unchanged**: the index is rolled back\n * to empty rather than left partially rebuilt, so a caller that retries a query\n * sees a clean empty index it can reason about.\n *\n * `'skip'` opts into the lenient shape the store's own open already uses: the\n * rebuild continues and every casualty is returned structurally on\n * {@link IVectorRebuildReport.skipped}. **It reports more, it does not report\n * less** — the point is to stop one bad record emptying an entire index, not to\n * make failures quieter. A caller that ignores `skipped` under `'skip'` has\n * chosen to, rather than been given no way to know.\n *\n * A {@link MemoryEmbedder} decline is not a failure under either mode: it is\n * counted on {@link IVectorRebuildReport.declined} and never appears in `skipped`.\n *\n * Both consumer-supplied hooks are capture-wrapped, so a `source` or `embed`\n * that throws or rejects becomes a `Failure` on the path above rather than an\n * exception escaping mid-rebuild — which would bypass the rollback entirely.\n *\n * @param source - The scope-qualified record source to re-embed.\n * @param embed - The embedder applied to each record.\n * @param options - Rebuild options; omit for the historical `'fail'` behavior.\n */\n public async rebuild(\n source: IMemoryRecordSource,\n embed: MemoryEmbedder,\n options?: IVectorRebuildOptions\n ): Promise<Result<IVectorRebuildReport>> {\n const lenient: boolean = (options?.onRecordError ?? 'fail') === 'skip';\n const listed: Result<ReadonlyArray<IScopedMemoryRecord>> = await invokeHook(() => source.list());\n if (listed.isFailure()) {\n // Deliberately BEFORE the reset. This used to reset first, on the reasoning\n // that no stale vectors should survive a failed rebuild — but a failed list\n // is no evidence about the vectors already held, and nothing has been\n // re-embedded yet, so there is no half-rebuilt state to guard against.\n // Leaving the prior contents intact is the more correct answer, and on the\n // durable sibling (`SqliteVecVectorIndex`) resetting here was data loss.\n return fail(`vector index rebuild: failed to list records: ${listed.message}`);\n }\n // From here a rebuild is genuinely starting, so clear. A mid-loop failure\n // under `'fail'` still resets, which is what keeps that contract honest.\n this._reset();\n let declined: number = 0;\n const skipped: ISkippedVectorRecord[] = [];\n for (const scoped of listed.value) {\n // Both hooks are consumer-supplied, so a throw or rejection is captured\n // into a `Failure` rather than escaping as an exception — otherwise a\n // badly-behaved embedder would reject out of `rebuild` mid-loop and leave\n // the index half-populated, which is precisely what the rollback below\n // exists to prevent.\n const embedded: Result<Float32Array | undefined> = await invokeHook(() => embed(scoped.record));\n if (embedded.isFailure()) {\n const error: string = `vector index rebuild: embedding '${edgeTargetKey(scoped.target)}' failed: ${\n embedded.message\n }`;\n // `'fail'` keeps the historical all-or-nothing contract EXACTLY: reset and\n // abort, so a caller that retries a query sees a clean empty index rather\n // than a partially-rebuilt one it cannot reason about.\n if (!lenient) {\n this._reset();\n return fail(error);\n }\n skipped.push({ target: scoped.target, error });\n continue;\n }\n // A decline is not an error under either mode — it is counted, never skipped.\n if (embedded.value === undefined) {\n declined++;\n continue;\n }\n const added: Result<string> = await this.add(scoped.target, embedded.value);\n if (added.isFailure()) {\n const error: string = `vector index rebuild: ${added.message}`;\n if (!lenient) {\n this._reset();\n return fail(error);\n }\n skipped.push({ target: scoped.target, error });\n }\n }\n return succeed({ indexed: this._vectors.size, declined, skipped });\n }\n\n /** Empty the index and forget the established dimension. */\n private _reset(): void {\n this._vectors.clear();\n this._dimension = undefined;\n }\n\n /** The Euclidean magnitude (L2 norm) of a vector. */\n private static _magnitude(vector: Float32Array): number {\n let sum: number = 0;\n for (let i: number = 0; i < vector.length; i++) {\n sum += vector[i] * vector[i];\n }\n return Math.sqrt(sum);\n }\n\n /**\n * Cosine similarity between the query (whose magnitude is precomputed once and\n * reused across the scan) and a stored vector. A zero-magnitude vector on\n * either side yields `0` rather than `NaN` — a degenerate vector is simply\n * maximally dissimilar, not an error.\n */\n private static _cosine(query: Float32Array, queryMagnitude: number, stored: Float32Array): number {\n const storedMagnitude: number = InMemoryCosineIndex._magnitude(stored);\n if (queryMagnitude === 0 || storedMagnitude === 0) {\n return 0;\n }\n let dot: number = 0;\n for (let i: number = 0; i < query.length; i++) {\n dot += query[i] * stored[i];\n }\n return dot / (queryMagnitude * storedMagnitude);\n }\n}\n"]}
1
+ {"version":3,"file":"inMemoryCosineIndex.js","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryCosineIndex.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { DetailedResult, Result, fail, failWithDetail, succeed, succeedWithDetail } from '@fgv/ts-utils';\nimport { IEdgeTarget, Kind, edgeTargetKey } from '../types';\nimport {\n IMemoryRecordListing,\n IMemoryRecordSource,\n ISkippedVectorRecord,\n IVectorIndex,\n IVectorQueryHit,\n IVectorRebuildOptions,\n IVectorRebuildReport,\n MemoryEmbedder\n} from './vectorIndex';\nimport { invokeHook, tally } from './rebuildHelpers';\n\n/** One stored embedding: the scope-qualified address plus its vector. */\ninterface IStoredVector {\n readonly target: IEdgeTarget;\n readonly vector: Float32Array;\n}\n\n/**\n * The brute-force, in-memory cosine {@link IVectorIndex}. Stores one\n * `Float32Array` per record and answers a query by computing cosine similarity\n * against every stored vector, returning the top-k by descending score.\n *\n * @remarks\n * This is the **complete** vector implementation for the fgv regime — large-N is\n * explicitly out of scope (the seam stays open for a consumer to swap an external\n * ANN backend once N grows beyond \"thousands of records\"). No external dependency\n * and no ANN structure: a linear scan over a few thousand vectors is well within\n * an interactive budget.\n *\n * The index has a single dimension established by the first vector added; every\n * subsequent `add` and every `query` vector must match that dimension or fail\n * loudly — a mismatched dimension is an embedder-wiring bug, never a silent\n * zero-similarity result. {@link InMemoryCosineIndex.rebuild | rebuild} clears\n * the index, so a re-embed with a different model (hence dimension) is supported.\n *\n * Persistence (a JSON sidecar) is deliberately out of scope for this layer — the\n * index is in-memory and rebuilt from the store via `rebuild`; a sidecar is a\n * future nicety.\n * @public\n */\nexport class InMemoryCosineIndex implements IVectorIndex {\n /**\n * Stored embeddings keyed by the canonical {@link edgeTargetKey} of the\n * record's scope-qualified address, so two records that share a filename stem\n * across scopes occupy distinct entries and never overwrite each other.\n */\n private readonly _vectors: Map<string, IStoredVector>;\n /** The dimension of every stored vector; `undefined` until the first `add`. */\n private _dimension: number | undefined;\n\n private constructor() {\n this._vectors = new Map<string, IStoredVector>();\n this._dimension = undefined;\n }\n\n /** The number of vectors currently held. */\n public get size(): number {\n return this._vectors.size;\n }\n\n /** Family-convention factory. */\n public static create(): Result<InMemoryCosineIndex> {\n return succeed(new InMemoryCosineIndex());\n }\n\n /** {@inheritDoc IVectorIndex.add} */\n public add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>> {\n const key: string = edgeTargetKey(target);\n if (vector.length === 0) {\n return Promise.resolve(fail(`vector index: cannot add '${key}': empty vector`));\n }\n if (this._dimension === undefined) {\n this._dimension = vector.length;\n } else if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `vector index: cannot add '${key}': dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n // Defensive copy: the caller may reuse or mutate the buffer after `add`, and\n // the index must keep serving the embedding it was given.\n this._vectors.set(key, { target, vector: Float32Array.from(vector) });\n // The in-memory index keys entries by the canonical scoped-target string, so\n // that key IS the entry reference.\n return Promise.resolve(succeed(key));\n }\n\n /** {@inheritDoc IVectorIndex.has} */\n public has(target: IEdgeTarget): Promise<Result<boolean>> {\n return Promise.resolve(succeed(this._vectors.has(edgeTargetKey(target))));\n }\n\n /** {@inheritDoc IVectorIndex.remove} */\n public remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>> {\n this._vectors.delete(edgeTargetKey(target));\n return Promise.resolve(succeed(target));\n }\n\n /** {@inheritDoc IVectorIndex.query} */\n public query(vector: Float32Array, topK: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (topK <= 0 || this._vectors.size === 0) {\n return Promise.resolve(succeed([]));\n }\n if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `vector index: query dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n const queryMagnitude: number = InMemoryCosineIndex._magnitude(vector);\n const hits: IVectorQueryHit[] = [];\n for (const stored of this._vectors.values()) {\n hits.push({\n target: stored.target,\n score: InMemoryCosineIndex._cosine(vector, queryMagnitude, stored.vector)\n });\n }\n // Descending by score; a `seq`-free tiebreak is unnecessary here because the\n // caller (SemanticRetriever) re-resolves hits against the record index.\n hits.sort((a, b) => b.score - a.score);\n return Promise.resolve(succeed(hits.length > topK ? hits.slice(0, topK) : hits));\n }\n\n /**\n * Re-embed every record from `source` and rebuild the index from scratch.\n * Clears the current contents (and the established dimension) first, so a\n * re-embed with a different model is supported. Returns an\n * {@link IVectorRebuildReport} describing what was indexed, declined and skipped.\n *\n * @remarks\n * **A failure to LIST is always fatal**, under either mode — and **leaves the\n * existing index untouched**: an unreadable source says nothing about which\n * records exist, so there is neither an honest partial to report nor any reason\n * to discard what is already held.\n *\n * Per-record embed/add failures are governed by\n * {@link IVectorRebuildOptions.onRecordError}, which defaults to `'fail'` —\n * **the historical all-or-nothing contract, unchanged**: the index is rolled back\n * to empty rather than left partially rebuilt, so a caller that retries a query\n * sees a clean empty index it can reason about.\n *\n * `'skip'` opts into the lenient shape the store's own open already uses: the\n * rebuild continues and every casualty is returned structurally on\n * {@link IVectorRebuildReport.skipped}. **It reports more, it does not report\n * less** — the point is to stop one bad record emptying an entire index, not to\n * make failures quieter. A caller that ignores `skipped` under `'skip'` has\n * chosen to, rather than been given no way to know.\n *\n * A {@link MemoryEmbedder} decline is not a failure under either mode: it is\n * counted on {@link IVectorRebuildReport.declined} and never appears in `skipped`.\n *\n * **A `'fail'` failure carries the partial report on its `detail`** — the\n * rollback still runs, so that report describes the aborted attempt rather than\n * the (now empty) index. The one failure with no detail is a `list` failure,\n * which disturbs nothing and has nothing to describe.\n *\n * Both consumer-supplied hooks are capture-wrapped, so a `source` or `embed`\n * that throws or rejects becomes a `Failure` on the path above rather than an\n * exception escaping mid-rebuild — which would bypass the rollback entirely.\n *\n * @param source - The scope-qualified record source to re-embed.\n * @param embed - The embedder applied to each record.\n * @param options - Rebuild options; omit for the historical `'fail'` behavior.\n */\n public async rebuild(\n source: IMemoryRecordSource,\n embed: MemoryEmbedder,\n options?: IVectorRebuildOptions\n ): Promise<DetailedResult<IVectorRebuildReport, IVectorRebuildReport>> {\n const lenient: boolean = (options?.onRecordError ?? 'fail') === 'skip';\n const listed: Result<IMemoryRecordListing> = await invokeHook(() => source.list());\n if (listed.isFailure()) {\n // Deliberately BEFORE the reset. This used to reset first, on the reasoning\n // that no stale vectors should survive a failed rebuild — but a failed list\n // is no evidence about the vectors already held, and nothing has been\n // re-embedded yet, so there is no half-rebuilt state to guard against.\n // Leaving the prior contents intact is the more correct answer, and on the\n // durable sibling (`SqliteVecVectorIndex`) resetting here was data loss.\n //\n // No detail, for the same reason: an all-zero report would describe an index\n // this call never touched.\n return failWithDetail(`vector index rebuild: failed to list records: ${listed.message}`);\n }\n // From here a rebuild is genuinely starting, so clear. A mid-loop failure\n // under `'fail'` still resets, which is what keeps that contract honest.\n this._reset();\n const indexed: Map<Kind, number> = new Map<Kind, number>();\n const declined: Map<Kind, number> = new Map<Kind, number>();\n const skipped: ISkippedVectorRecord[] = [];\n // Only the source knows what it filtered, so an absent `excluded` propagates\n // as absent rather than becoming an empty map — \"cannot say\" and \"excluded\n // nothing\" are different answers.\n const report = (): IVectorRebuildReport => ({\n indexed,\n declined,\n excluded: listed.value.excluded,\n skipped\n });\n for (const scoped of listed.value.records) {\n const kind: Kind = scoped.record.envelope.kind;\n // Both hooks are consumer-supplied, so a throw or rejection is captured\n // into a `Failure` rather than escaping as an exception — otherwise a\n // badly-behaved embedder would reject out of `rebuild` mid-loop and leave\n // the index half-populated, which is precisely what the rollback below\n // exists to prevent.\n const embedded: Result<Float32Array | undefined> = await invokeHook(() => embed(scoped.record));\n if (embedded.isFailure()) {\n const error: string = `vector index rebuild: embedding '${edgeTargetKey(scoped.target)}' failed: ${\n embedded.message\n }`;\n // `'fail'` keeps the historical all-or-nothing contract EXACTLY: reset and\n // abort, so a caller that retries a query sees a clean empty index rather\n // than a partially-rebuilt one it cannot reason about. What is new is only\n // that the failure also carries what the attempt had established.\n if (!lenient) {\n this._reset();\n return failWithDetail(error, report());\n }\n skipped.push({ target: scoped.target, error });\n continue;\n }\n // A decline is not an error under either mode — it is counted, never skipped.\n if (embedded.value === undefined) {\n tally(declined, kind);\n continue;\n }\n const added: Result<string> = await this.add(scoped.target, embedded.value);\n if (added.isFailure()) {\n const error: string = `vector index rebuild: ${added.message}`;\n if (!lenient) {\n this._reset();\n return failWithDetail(error, report());\n }\n skipped.push({ target: scoped.target, error });\n continue;\n }\n // Tallied per successful add rather than read back off `size` at the end:\n // `size` counts distinct addresses, and this count must line up with its\n // per-record siblings so the three still sum to the records seen.\n tally(indexed, kind);\n }\n return succeedWithDetail(report());\n }\n\n /** Empty the index and forget the established dimension. */\n private _reset(): void {\n this._vectors.clear();\n this._dimension = undefined;\n }\n\n /** The Euclidean magnitude (L2 norm) of a vector. */\n private static _magnitude(vector: Float32Array): number {\n let sum: number = 0;\n for (let i: number = 0; i < vector.length; i++) {\n sum += vector[i] * vector[i];\n }\n return Math.sqrt(sum);\n }\n\n /**\n * Cosine similarity between the query (whose magnitude is precomputed once and\n * reused across the scan) and a stored vector. A zero-magnitude vector on\n * either side yields `0` rather than `NaN` — a degenerate vector is simply\n * maximally dissimilar, not an error.\n */\n private static _cosine(query: Float32Array, queryMagnitude: number, stored: Float32Array): number {\n const storedMagnitude: number = InMemoryCosineIndex._magnitude(stored);\n if (queryMagnitude === 0 || storedMagnitude === 0) {\n return 0;\n }\n let dot: number = 0;\n for (let i: number = 0; i < query.length; i++) {\n dot += query[i] * stored[i];\n }\n return dot / (queryMagnitude * storedMagnitude);\n }\n}\n"]}
@@ -1,6 +1,6 @@
1
- import { Result } from '@fgv/ts-utils';
1
+ import { DetailedResult, Result } from '@fgv/ts-utils';
2
2
  import { IEdgeTarget } from '../types';
3
- import { FragmentEmbedder, IEmbeddedFragment, IFragmentVectorIndex, IMemoryRecordSource, IVectorQueryHit } from './vectorIndex';
3
+ import { FragmentEmbedder, IEmbeddedFragment, IFragmentVectorIndex, IFragmentVectorRebuildReport, IMemoryRecordSource, IVectorQueryHit, IVectorRebuildOptions } from './vectorIndex';
4
4
  /**
5
5
  * The brute-force, in-memory cosine {@link IFragmentVectorIndex} — the
6
6
  * fragment-granular sibling of {@link InMemoryCosineIndex}. Stores many
@@ -45,6 +45,8 @@ export declare class InMemoryFragmentCosineIndex implements IFragmentVectorIndex
45
45
  static create(): Result<InMemoryFragmentCosineIndex>;
46
46
  /** {@inheritDoc IFragmentVectorIndex.addFragments} */
47
47
  addFragments(target: IEdgeTarget, fragments: ReadonlyArray<IEmbeddedFragment>): Promise<Result<number>>;
48
+ /** {@inheritDoc IFragmentVectorIndex.has} */
49
+ has(target: IEdgeTarget): Promise<Result<boolean>>;
48
50
  /** {@inheritDoc IFragmentVectorIndex.remove} */
49
51
  remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
50
52
  /** {@inheritDoc IFragmentVectorIndex.query} */
@@ -68,7 +70,8 @@ export declare class InMemoryFragmentCosineIndex implements IFragmentVectorIndex
68
70
  * @param source - The scope-qualified record source to re-embed.
69
71
  * @param embed - The fragment embedder applied to each record.
70
72
  */
71
- rebuild(source: IMemoryRecordSource, embed: FragmentEmbedder): Promise<Result<number>>;
73
+ /** {@inheritDoc IFragmentVectorIndex.rebuild} */
74
+ rebuild(source: IMemoryRecordSource, embed: FragmentEmbedder, options?: IVectorRebuildOptions): Promise<DetailedResult<IFragmentVectorRebuildReport, IFragmentVectorRebuildReport>>;
72
75
  /** Empty the index and forget the established dimension. */
73
76
  private _reset;
74
77
  /** The Euclidean magnitude (L2 norm) of a vector. */
@@ -1 +1 @@
1
- {"version":3,"file":"inMemoryFragmentCosineIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryFragmentCosineIndex.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAiB,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,WAAW,EAAiB,MAAM,UAAU,CAAC;AACtD,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,oBAAoB,EACpB,mBAAmB,EAEnB,eAAe,EAChB,MAAM,eAAe,CAAC;AAoCvB;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,2BAA4B,YAAW,oBAAoB;IACtE;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsC;IAC/D,wFAAwF;IACxF,OAAO,CAAC,UAAU,CAAqB;IAEvC,OAAO;IAKP,8EAA8E;IAC9E,IAAW,WAAW,IAAI,MAAM,CAE/B;IAED,uEAAuE;IACvE,IAAW,aAAa,IAAI,MAAM,CAMjC;IAED,iCAAiC;WACnB,MAAM,IAAI,MAAM,CAAC,2BAA2B,CAAC;IAI3D,sDAAsD;IAC/C,YAAY,CACjB,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAC1C,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IA+C1B,gDAAgD;IACzC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAKhE,+CAA+C;IACxC,KAAK,CACV,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAkDlD;;;;;;;;;;;;;;;;;;OAkBG;IACU,OAAO,CAAC,MAAM,EAAE,mBAAmB,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAuBnG,4DAA4D;IAC5D,OAAO,CAAC,MAAM;IAKd,qDAAqD;IACrD,OAAO,CAAC,MAAM,CAAC,UAAU;IAQzB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,OAAO;CAWvB"}
1
+ {"version":3,"file":"inMemoryFragmentCosineIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryFragmentCosineIndex.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAoD,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,WAAW,EAAuB,MAAM,UAAU,CAAC;AAC5D,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,oBAAoB,EACpB,4BAA4B,EAE5B,mBAAmB,EAEnB,eAAe,EACf,qBAAqB,EACtB,MAAM,eAAe,CAAC;AAqCvB;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,2BAA4B,YAAW,oBAAoB;IACtE;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsC;IAC/D,wFAAwF;IACxF,OAAO,CAAC,UAAU,CAAqB;IAEvC,OAAO;IAKP,8EAA8E;IAC9E,IAAW,WAAW,IAAI,MAAM,CAE/B;IAED,uEAAuE;IACvE,IAAW,aAAa,IAAI,MAAM,CAMjC;IAED,iCAAiC;WACnB,MAAM,IAAI,MAAM,CAAC,2BAA2B,CAAC;IAI3D,sDAAsD;IAC/C,YAAY,CACjB,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAC1C,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IA+C1B,6CAA6C;IACtC,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAIzD,gDAAgD;IACzC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAKhE,+CAA+C;IACxC,KAAK,CACV,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAkDlD;;;;;;;;;;;;;;;;;;OAkBG;IACH,iDAAiD;IACpC,OAAO,CAClB,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,gBAAgB,EACvB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,cAAc,CAAC,4BAA4B,EAAE,4BAA4B,CAAC,CAAC;IAuEtF,4DAA4D;IAC5D,OAAO,CAAC,MAAM;IAKd,qDAAqD;IACrD,OAAO,CAAC,MAAM,CAAC,UAAU;IAQzB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,OAAO;CAWvB"}