@fgv/ts-agent-memory 5.1.0-39 → 5.1.0-41

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 (210) hide show
  1. package/.rush/temp/{b82cf6bdece20481260e6bab946179eeec9d7b46.tar.log → cbbdbe09515171b4eba8f2592be2dace1e4e8142.tar.log} +14 -2
  2. package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +2 -2
  3. package/.rush/temp/operation/build/all.log +2 -2
  4. package/.rush/temp/operation/build/log-chunks.jsonl +2 -2
  5. package/.rush/temp/operation/build/state.json +1 -1
  6. package/dist/packlets/converters/envelopeConverter.js +17 -3
  7. package/dist/packlets/converters/envelopeConverter.js.map +1 -1
  8. package/dist/packlets/index/memoryIndex.js +58 -10
  9. package/dist/packlets/index/memoryIndex.js.map +1 -1
  10. package/dist/packlets/ingest/cycleGuard.js +13 -6
  11. package/dist/packlets/ingest/cycleGuard.js.map +1 -1
  12. package/dist/packlets/ingest/hostStages.js.map +1 -1
  13. package/dist/packlets/ingest/model.js.map +1 -1
  14. package/dist/packlets/ingest/orchestrator.js +94 -49
  15. package/dist/packlets/ingest/orchestrator.js.map +1 -1
  16. package/dist/packlets/retrieve/hybridRetriever.js +13 -2
  17. package/dist/packlets/retrieve/hybridRetriever.js.map +1 -1
  18. package/dist/packlets/retrieve/linkTraversalRetriever.js +46 -57
  19. package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  20. package/dist/packlets/retrieve/recencyRetriever.js +3 -3
  21. package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
  22. package/dist/packlets/retrieve/retriever.js +50 -7
  23. package/dist/packlets/retrieve/retriever.js.map +1 -1
  24. package/dist/packlets/retrieve/semanticRetriever.js +9 -3
  25. package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
  26. package/dist/packlets/retrieve/structuredFilterRetriever.js +3 -3
  27. package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  28. package/dist/packlets/retrieve/tagRetriever.js +3 -3
  29. package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
  30. package/dist/packlets/retrieve/temporalRetrievers.js +3 -3
  31. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
  32. package/dist/packlets/store/fileTreeMemoryStore.js +72 -16
  33. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  34. package/dist/packlets/tools/memoryTools.js +104 -21
  35. package/dist/packlets/tools/memoryTools.js.map +1 -1
  36. package/dist/packlets/types/envelope.js +13 -1
  37. package/dist/packlets/types/envelope.js.map +1 -1
  38. package/dist/packlets/vector/inMemoryCosineIndex.js +22 -17
  39. package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  40. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  41. package/dist/test/unit/converters/antagonistRoundTrip.test.js +3 -3
  42. package/dist/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
  43. package/dist/test/unit/converters/envelopeConverter.test.js +125 -8
  44. package/dist/test/unit/converters/envelopeConverter.test.js.map +1 -1
  45. package/dist/test/unit/index/memoryIndex.test.js +87 -25
  46. package/dist/test/unit/index/memoryIndex.test.js.map +1 -1
  47. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
  48. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
  49. package/dist/test/unit/ingest/cycleGuard.test.js +28 -1
  50. package/dist/test/unit/ingest/cycleGuard.test.js.map +1 -1
  51. package/dist/test/unit/ingest/orchestrator.test.js +187 -45
  52. package/dist/test/unit/ingest/orchestrator.test.js.map +1 -1
  53. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
  54. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
  55. package/dist/test/unit/retrieve/retrievers.test.js +299 -37
  56. package/dist/test/unit/retrieve/retrievers.test.js.map +1 -1
  57. package/dist/test/unit/store/embedOnWrite.test.js +69 -12
  58. package/dist/test/unit/store/embedOnWrite.test.js.map +1 -1
  59. package/dist/test/unit/store/listScoped.test.js +109 -0
  60. package/dist/test/unit/store/listScoped.test.js.map +1 -0
  61. package/dist/test/unit/store/rankAxis.test.js +254 -0
  62. package/dist/test/unit/store/rankAxis.test.js.map +1 -0
  63. package/dist/test/unit/tools/memoryTools.test.js +280 -11
  64. package/dist/test/unit/tools/memoryTools.test.js.map +1 -1
  65. package/dist/test/unit/types/writePolicy.test.js +9 -2
  66. package/dist/test/unit/types/writePolicy.test.js.map +1 -1
  67. package/dist/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
  68. package/dist/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
  69. package/dist/test/unit/vector/vectorIndex.test.js +24 -15
  70. package/dist/test/unit/vector/vectorIndex.test.js.map +1 -1
  71. package/dist/ts-agent-memory.d.ts +384 -97
  72. package/etc/ts-agent-memory.api.md +64 -22
  73. package/lib/packlets/converters/envelopeConverter.d.ts +8 -1
  74. package/lib/packlets/converters/envelopeConverter.d.ts.map +1 -1
  75. package/lib/packlets/converters/envelopeConverter.js +18 -4
  76. package/lib/packlets/converters/envelopeConverter.js.map +1 -1
  77. package/lib/packlets/index/memoryIndex.d.ts +42 -10
  78. package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
  79. package/lib/packlets/index/memoryIndex.js +58 -10
  80. package/lib/packlets/index/memoryIndex.js.map +1 -1
  81. package/lib/packlets/ingest/cycleGuard.d.ts +5 -5
  82. package/lib/packlets/ingest/cycleGuard.d.ts.map +1 -1
  83. package/lib/packlets/ingest/cycleGuard.js +13 -6
  84. package/lib/packlets/ingest/cycleGuard.js.map +1 -1
  85. package/lib/packlets/ingest/hostStages.d.ts +3 -3
  86. package/lib/packlets/ingest/hostStages.d.ts.map +1 -1
  87. package/lib/packlets/ingest/hostStages.js.map +1 -1
  88. package/lib/packlets/ingest/model.d.ts +25 -14
  89. package/lib/packlets/ingest/model.d.ts.map +1 -1
  90. package/lib/packlets/ingest/model.js.map +1 -1
  91. package/lib/packlets/ingest/orchestrator.d.ts +18 -4
  92. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
  93. package/lib/packlets/ingest/orchestrator.js +93 -48
  94. package/lib/packlets/ingest/orchestrator.js.map +1 -1
  95. package/lib/packlets/retrieve/hybridRetriever.d.ts.map +1 -1
  96. package/lib/packlets/retrieve/hybridRetriever.js +12 -1
  97. package/lib/packlets/retrieve/hybridRetriever.js.map +1 -1
  98. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +18 -23
  99. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
  100. package/lib/packlets/retrieve/linkTraversalRetriever.js +45 -56
  101. package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  102. package/lib/packlets/retrieve/recencyRetriever.js +2 -2
  103. package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
  104. package/lib/packlets/retrieve/retriever.d.ts +70 -11
  105. package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
  106. package/lib/packlets/retrieve/retriever.js +52 -7
  107. package/lib/packlets/retrieve/retriever.js.map +1 -1
  108. package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
  109. package/lib/packlets/retrieve/semanticRetriever.js +9 -3
  110. package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
  111. package/lib/packlets/retrieve/structuredFilterRetriever.js +2 -2
  112. package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  113. package/lib/packlets/retrieve/tagRetriever.js +2 -2
  114. package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
  115. package/lib/packlets/retrieve/temporalRetrievers.js +3 -3
  116. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
  117. package/lib/packlets/store/fileTreeMemoryStore.d.ts +57 -3
  118. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  119. package/lib/packlets/store/fileTreeMemoryStore.js +72 -16
  120. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  121. package/lib/packlets/tools/memoryTools.d.ts +24 -0
  122. package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
  123. package/lib/packlets/tools/memoryTools.js +104 -21
  124. package/lib/packlets/tools/memoryTools.js.map +1 -1
  125. package/lib/packlets/types/envelope.d.ts +61 -8
  126. package/lib/packlets/types/envelope.d.ts.map +1 -1
  127. package/lib/packlets/types/envelope.js +14 -0
  128. package/lib/packlets/types/envelope.js.map +1 -1
  129. package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -5
  130. package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
  131. package/lib/packlets/vector/inMemoryCosineIndex.js +22 -17
  132. package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  133. package/lib/packlets/vector/vectorIndex.d.ts +46 -20
  134. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  135. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  136. package/lib/test/unit/converters/antagonistRoundTrip.test.js +3 -3
  137. package/lib/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
  138. package/lib/test/unit/converters/envelopeConverter.test.js +124 -7
  139. package/lib/test/unit/converters/envelopeConverter.test.js.map +1 -1
  140. package/lib/test/unit/index/memoryIndex.test.js +86 -24
  141. package/lib/test/unit/index/memoryIndex.test.js.map +1 -1
  142. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
  143. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
  144. package/lib/test/unit/ingest/cycleGuard.test.js +28 -1
  145. package/lib/test/unit/ingest/cycleGuard.test.js.map +1 -1
  146. package/lib/test/unit/ingest/orchestrator.test.js +186 -44
  147. package/lib/test/unit/ingest/orchestrator.test.js.map +1 -1
  148. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
  149. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
  150. package/lib/test/unit/retrieve/retrievers.test.js +298 -36
  151. package/lib/test/unit/retrieve/retrievers.test.js.map +1 -1
  152. package/lib/test/unit/store/embedOnWrite.test.js +68 -11
  153. package/lib/test/unit/store/embedOnWrite.test.js.map +1 -1
  154. package/lib/test/unit/store/listScoped.test.d.ts +2 -0
  155. package/lib/test/unit/store/listScoped.test.d.ts.map +1 -0
  156. package/lib/test/unit/store/listScoped.test.js +111 -0
  157. package/lib/test/unit/store/listScoped.test.js.map +1 -0
  158. package/lib/test/unit/store/rankAxis.test.d.ts +2 -0
  159. package/lib/test/unit/store/rankAxis.test.d.ts.map +1 -0
  160. package/lib/test/unit/store/rankAxis.test.js +256 -0
  161. package/lib/test/unit/store/rankAxis.test.js.map +1 -0
  162. package/lib/test/unit/tools/memoryTools.test.js +280 -11
  163. package/lib/test/unit/tools/memoryTools.test.js.map +1 -1
  164. package/lib/test/unit/types/writePolicy.test.js +9 -2
  165. package/lib/test/unit/types/writePolicy.test.js.map +1 -1
  166. package/lib/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
  167. package/lib/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
  168. package/lib/test/unit/vector/vectorIndex.test.js +24 -15
  169. package/lib/test/unit/vector/vectorIndex.test.js.map +1 -1
  170. package/package.json +7 -7
  171. package/rush-logs/ts-agent-memory.build.cache.log +1 -1
  172. package/rush-logs/ts-agent-memory.build.log +2 -2
  173. package/src/packlets/converters/envelopeConverter.ts +27 -4
  174. package/src/packlets/index/memoryIndex.ts +86 -22
  175. package/src/packlets/ingest/cycleGuard.ts +22 -11
  176. package/src/packlets/ingest/hostStages.ts +3 -3
  177. package/src/packlets/ingest/model.ts +25 -14
  178. package/src/packlets/ingest/orchestrator.ts +143 -67
  179. package/src/packlets/retrieve/hybridRetriever.ts +14 -1
  180. package/src/packlets/retrieve/linkTraversalRetriever.ts +51 -62
  181. package/src/packlets/retrieve/recencyRetriever.ts +3 -3
  182. package/src/packlets/retrieve/retriever.ts +97 -13
  183. package/src/packlets/retrieve/semanticRetriever.ts +10 -5
  184. package/src/packlets/retrieve/structuredFilterRetriever.ts +3 -3
  185. package/src/packlets/retrieve/tagRetriever.ts +3 -3
  186. package/src/packlets/retrieve/temporalRetrievers.ts +3 -3
  187. package/src/packlets/store/fileTreeMemoryStore.ts +117 -12
  188. package/src/packlets/tools/memoryTools.ts +152 -25
  189. package/src/packlets/types/envelope.ts +66 -8
  190. package/src/packlets/vector/inMemoryCosineIndex.ts +45 -22
  191. package/src/packlets/vector/vectorIndex.ts +47 -20
  192. package/src/test/unit/converters/antagonistRoundTrip.test.ts +3 -3
  193. package/src/test/unit/converters/envelopeConverter.test.ts +168 -11
  194. package/src/test/unit/index/memoryIndex.test.ts +99 -14
  195. package/src/test/unit/ingest/antagonistCycleAndParity.test.ts +23 -18
  196. package/src/test/unit/ingest/cycleGuard.test.ts +44 -2
  197. package/src/test/unit/ingest/orchestrator.test.ts +234 -41
  198. package/src/test/unit/retrieve/linkTraversalRetriever.test.ts +134 -35
  199. package/src/test/unit/retrieve/retrievers.test.ts +381 -25
  200. package/src/test/unit/store/embedOnWrite.test.ts +83 -11
  201. package/src/test/unit/store/listScoped.test.ts +138 -0
  202. package/src/test/unit/store/rankAxis.test.ts +349 -0
  203. package/src/test/unit/tools/memoryTools.test.ts +362 -13
  204. package/src/test/unit/types/writePolicy.test.ts +11 -2
  205. package/src/test/unit/vector/inMemoryCosineIndex.test.ts +115 -39
  206. package/src/test/unit/vector/vectorIndex.test.ts +33 -17
  207. package/temp/build/lint/_eslint-5eVG3S6w.json +41 -33
  208. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  209. package/temp/ts-agent-memory.api.json +1080 -136
  210. package/temp/ts-agent-memory.api.md +64 -22
@@ -1,5 +1,5 @@
1
- Start time: Sun Jul 12 2026 01:54:47 GMT+0000 (Coordinated Universal Time)
2
- Invoking "/usr/bin/tar -c -f /home/runner/work/fgv/fgv/common/temp/build-cache/b82cf6bdece20481260e6bab946179eeec9d7b46-3f506efaa8dd0c6f.temp -z --files-from=-"
1
+ Start time: Tue Jul 14 2026 06:13:34 GMT+0000 (Coordinated Universal Time)
2
+ Invoking "/usr/bin/tar -c -f /home/runner/work/fgv/fgv/common/temp/build-cache/cbbdbe09515171b4eba8f2592be2dace1e4e8142-c6a08a434367e77c.temp -z --files-from=-"
3
3
 
4
4
  ======= BEGIN PROCESS INPUT ======
5
5
  .rush/temp/operation/build/all.log
@@ -109,8 +109,12 @@ dist/test/unit/store/embedOnWrite.test.js
109
109
  dist/test/unit/store/embedOnWrite.test.js.map
110
110
  dist/test/unit/store/fileTreeMemoryStore.test.js
111
111
  dist/test/unit/store/fileTreeMemoryStore.test.js.map
112
+ dist/test/unit/store/listScoped.test.js
113
+ dist/test/unit/store/listScoped.test.js.map
112
114
  dist/test/unit/store/observations.test.js
113
115
  dist/test/unit/store/observations.test.js.map
116
+ dist/test/unit/store/rankAxis.test.js
117
+ dist/test/unit/store/rankAxis.test.js.map
114
118
  dist/test/unit/store/scopeEncoding.test.js
115
119
  dist/test/unit/store/scopeEncoding.test.js.map
116
120
  dist/test/unit/store/temporalStore.test.js
@@ -341,10 +345,18 @@ lib/test/unit/store/fileTreeMemoryStore.test.d.ts
341
345
  lib/test/unit/store/fileTreeMemoryStore.test.d.ts.map
342
346
  lib/test/unit/store/fileTreeMemoryStore.test.js
343
347
  lib/test/unit/store/fileTreeMemoryStore.test.js.map
348
+ lib/test/unit/store/listScoped.test.d.ts
349
+ lib/test/unit/store/listScoped.test.d.ts.map
350
+ lib/test/unit/store/listScoped.test.js
351
+ lib/test/unit/store/listScoped.test.js.map
344
352
  lib/test/unit/store/observations.test.d.ts
345
353
  lib/test/unit/store/observations.test.d.ts.map
346
354
  lib/test/unit/store/observations.test.js
347
355
  lib/test/unit/store/observations.test.js.map
356
+ lib/test/unit/store/rankAxis.test.d.ts
357
+ lib/test/unit/store/rankAxis.test.d.ts.map
358
+ lib/test/unit/store/rankAxis.test.js
359
+ lib/test/unit/store/rankAxis.test.js.map
348
360
  lib/test/unit/store/scopeEncoding.test.d.ts
349
361
  lib/test/unit/store/scopeEncoding.test.d.ts.map
350
362
  lib/test/unit/store/scopeEncoding.test.js
@@ -5,5 +5,5 @@
5
5
  {"kind":"O","text":"[build:lint] Using ESLint version 9.39.5\n"}
6
6
  {"kind":"O","text":"[build:api-extractor] Using API Extractor version 7.58.9\n"}
7
7
  {"kind":"O","text":"[build:api-extractor] Analysis will use the bundled TypeScript version 5.9.3\n"}
8
- {"kind":"O","text":" ---- build finished (27.335s) ---- \n"}
9
- {"kind":"O","text":"-------------------- Finished (27.352s) --------------------\n"}
8
+ {"kind":"O","text":" ---- build finished (26.015s) ---- \n"}
9
+ {"kind":"O","text":"-------------------- Finished (26.02s) --------------------\n"}
@@ -5,5 +5,5 @@ Invoking: heft build --clean
5
5
  [build:lint] Using ESLint version 9.39.5
6
6
  [build:api-extractor] Using API Extractor version 7.58.9
7
7
  [build:api-extractor] Analysis will use the bundled TypeScript version 5.9.3
8
- ---- build finished (27.335s) ----
9
- -------------------- Finished (27.352s) --------------------
8
+ ---- build finished (26.015s) ----
9
+ -------------------- Finished (26.02s) --------------------
@@ -5,5 +5,5 @@
5
5
  {"kind":"O","text":"[build:lint] Using ESLint version 9.39.5\n"}
6
6
  {"kind":"O","text":"[build:api-extractor] Using API Extractor version 7.58.9\n"}
7
7
  {"kind":"O","text":"[build:api-extractor] Analysis will use the bundled TypeScript version 5.9.3\n"}
8
- {"kind":"O","text":" ---- build finished (27.335s) ---- \n"}
9
- {"kind":"O","text":"-------------------- Finished (27.352s) --------------------\n"}
8
+ {"kind":"O","text":" ---- build finished (26.015s) ---- \n"}
9
+ {"kind":"O","text":"-------------------- Finished (26.02s) --------------------\n"}
@@ -1,3 +1,3 @@
1
1
  {
2
- "nonCachedDurationMs": 28318.857426000002
2
+ "nonCachedDurationMs": 26865.358066
3
3
  }
@@ -20,16 +20,29 @@ const stringOrNull = Converters.oneOf([
20
20
  Converters.string,
21
21
  nullConverter
22
22
  ]);
23
+ /**
24
+ * Converter for a scope-qualified {@link IEdgeTarget}. Both `scope` and `id`
25
+ * are required — the whole point of the scoped target is that a bare id is
26
+ * ambiguous across scopes.
27
+ * @public
28
+ */
29
+ export const edgeTargetConverter = Converters.object({
30
+ scope: Convert.scopeKey,
31
+ id: Convert.memoryId
32
+ });
23
33
  /**
24
34
  * Converter for the known {@link IProvenance} fields. The full
25
35
  * {@link provenanceConverter} layers extension-key preservation on top.
36
+ * `derivedFrom` is serialized as the nested `{ scope, id }` object (mirroring
37
+ * {@link edgeConverter | edge}'s scope-qualified `target`) so the provenance
38
+ * back-reference is unambiguous across scopes.
26
39
  */
27
40
  const knownProvenanceConverter = Converters.object({
28
41
  source: Converters.string,
29
42
  by: Converters.string.optional(),
30
43
  model: Converters.string.optional(),
31
44
  confidence: Converters.number.optional(),
32
- derivedFrom: Convert.memoryId.optional()
45
+ derivedFrom: edgeTargetConverter.optional()
33
46
  }, { optionalFields: ['by', 'model', 'confidence', 'derivedFrom'] });
34
47
  /**
35
48
  * Converter for {@link IProvenance}. Validates the known fields and preserves
@@ -51,7 +64,7 @@ export const provenanceConverter = Converters.generic((from) => knownProvenanceC
51
64
  */
52
65
  export const edgeConverter = Converters.object({
53
66
  type: Convert.linkType,
54
- target: Convert.memoryId,
67
+ target: edgeTargetConverter,
55
68
  confidence: Converters.number.optional(),
56
69
  provenance: provenanceConverter.optional(),
57
70
  valid_at: Converters.number.optional(),
@@ -80,10 +93,11 @@ export const envelopeConverter = Converters.object({
80
93
  updated: Converters.number,
81
94
  seq: Converters.number,
82
95
  contentHash: Converters.string,
96
+ rank: Converters.number.optional(),
83
97
  provenance: provenanceConverter,
84
98
  temporal: temporalConverter.optional(),
85
99
  embeddingRef: stringOrNull.optional()
86
- }, { optionalFields: ['temporal', 'embeddingRef'] });
100
+ }, { optionalFields: ['rank', 'temporal', 'embeddingRef'] });
87
101
  /**
88
102
  * Converter that parses a YAML frontmatter string into an
89
103
  * {@link IMemoryEnvelope}.
@@ -1 +1 @@
1
- {"version":3,"file":"envelopeConverter.js","sourceRoot":"","sources":["../../../src/packlets/converters/envelopeConverter.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Converter, Converters, Result, fail, succeed } from '@fgv/ts-utils';\nimport { Yaml } from '@fgv/ts-extras';\nimport { Convert, IEdge, IMemoryEnvelope, IMemoryRecord, IProvenance, ITemporalBlock } from '../types';\nimport { IBodyConverterRegistry } from './bodyConverterRegistry';\n\n/** Matches exactly the JSON `null` literal. */\n// eslint-disable-next-line @rushstack/no-new-null -- the envelope nullable fields (invalid_at, embeddingRef) carry meaningful null; design-lock §2.3-2.5\nconst nullConverter: Converter<null> = Converters.generic<null>((from: unknown) =>\n from === null ? succeed(null) : fail('expected null')\n);\n\n/** A nullable epoch-ms value (`number | null`). */\n// eslint-disable-next-line @rushstack/no-new-null -- meaningful null for IEdge/ITemporalBlock.invalid_at; design-lock §2.3-2.4\nconst numberOrNull: Converter<number | null> = Converters.oneOf<number | null>([\n Converters.number,\n nullConverter\n]);\n\n/** A nullable string value (`string | null`). */\n// eslint-disable-next-line @rushstack/no-new-null -- meaningful null for IMemoryEnvelope.embeddingRef; design-lock §2.5\nconst stringOrNull: Converter<string | null> = Converters.oneOf<string | null>([\n Converters.string,\n nullConverter\n]);\n\n/**\n * Converter for the known {@link IProvenance} fields. The full\n * {@link provenanceConverter} layers extension-key preservation on top.\n */\nconst knownProvenanceConverter: Converter<IProvenance> = Converters.object<IProvenance>(\n {\n source: Converters.string,\n by: Converters.string.optional(),\n model: Converters.string.optional(),\n confidence: Converters.number.optional(),\n derivedFrom: Convert.memoryId.optional()\n },\n { optionalFields: ['by', 'model', 'confidence', 'derivedFrom'] }\n);\n\n/**\n * Converter for {@link IProvenance}. Validates the known fields and preserves\n * any extension keys verbatim (the `[key: string]: unknown` opaque payload),\n * so a round-trip never drops consumer-attached data.\n * @public\n */\nexport const provenanceConverter: Converter<IProvenance> = Converters.generic<IProvenance>(\n (from: unknown): Result<IProvenance> =>\n knownProvenanceConverter.convert(from).onSuccess((known) => {\n // `from` is guaranteed to be a non-null object here — the known-field\n // converter only succeeds on objects. Spread the original keys first,\n // then the validated/normalized known fields on top, so extension keys\n // survive while the known fields carry their converted values.\n const merged: IProvenance = { ...(from as Record<string, unknown>), ...known };\n return succeed(merged);\n })\n);\n\n/**\n * Converter for an attributed {@link IEdge}.\n * @public\n */\nexport const edgeConverter: Converter<IEdge> = Converters.object<IEdge>(\n {\n type: Convert.linkType,\n target: Convert.memoryId,\n confidence: Converters.number.optional(),\n provenance: provenanceConverter.optional(),\n valid_at: Converters.number.optional(),\n invalid_at: numberOrNull.optional()\n },\n { optionalFields: ['confidence', 'provenance', 'valid_at', 'invalid_at'] }\n);\n\n/**\n * Converter for the optional {@link ITemporalBlock}.\n * @public\n */\nexport const temporalConverter: Converter<ITemporalBlock> = Converters.object<ITemporalBlock>(\n {\n valid_at: Converters.number.optional(),\n invalid_at: numberOrNull.optional()\n },\n { optionalFields: ['valid_at', 'invalid_at'] }\n);\n\n/**\n * Converter for the invariant {@link IMemoryEnvelope}. Validates a plain\n * object (e.g. parsed YAML frontmatter) into a typed envelope.\n * @public\n */\nexport const envelopeConverter: Converter<IMemoryEnvelope> = Converters.object<IMemoryEnvelope>(\n {\n id: Convert.memoryId,\n entityId: Convert.entityId,\n kind: Convert.kind,\n tags: Converters.arrayOf(Convert.tag),\n links: Converters.arrayOf(edgeConverter),\n created: Converters.number,\n updated: Converters.number,\n seq: Converters.number,\n contentHash: Converters.string,\n provenance: provenanceConverter,\n temporal: temporalConverter.optional(),\n embeddingRef: stringOrNull.optional()\n },\n { optionalFields: ['temporal', 'embeddingRef'] }\n);\n\n/**\n * Converter that parses a YAML frontmatter string into an\n * {@link IMemoryEnvelope}.\n * @public\n */\nexport const envelopeYamlConverter: Converter<IMemoryEnvelope> = Yaml.yamlConverter(envelopeConverter);\n\n/** The frontmatter delimiter line. */\nconst FRONTMATTER_DELIMITER: string = '---';\n\n/**\n * The two parts of a memory file: the YAML frontmatter (between the `---`\n * delimiters) and the markdown body (everything after the closing delimiter).\n * @public\n */\nexport interface IMemoryFileParts {\n /** The raw YAML frontmatter (delimiters stripped). */\n readonly frontmatter: string;\n /** The raw body text following the closing delimiter. */\n readonly body: string;\n}\n\n/**\n * Split a `---\\n<yaml>\\n---\\n<body>` memory file into its frontmatter and\n * body parts. Pure string handling — no external dependency.\n * @public\n */\nexport function splitFrontmatter(raw: string): Result<IMemoryFileParts> {\n // Split on '\\n' and strip a trailing '\\r' per line so CRLF-authored files\n // parse identically to LF — otherwise the '\\r' would ride along into the\n // returned frontmatter (perturbing YAML parsing) and body (corrupting\n // round-trip fidelity).\n const lines: string[] = raw.split('\\n').map((line) => (line.endsWith('\\r') ? line.slice(0, -1) : line));\n if (lines[0].trim() !== FRONTMATTER_DELIMITER) {\n return fail(\"memory file: missing opening frontmatter delimiter '---'\");\n }\n let closeIndex: number = -1;\n for (let i = 1; i < lines.length; i++) {\n if (lines[i].trim() === FRONTMATTER_DELIMITER) {\n closeIndex = i;\n break;\n }\n }\n if (closeIndex === -1) {\n return fail(\"memory file: missing closing frontmatter delimiter '---'\");\n }\n return succeed({\n frontmatter: lines.slice(1, closeIndex).join('\\n'),\n body: lines.slice(closeIndex + 1).join('\\n')\n });\n}\n\n/**\n * Join a YAML frontmatter string and a body into the canonical\n * `---\\n<yaml>\\n---\\n<body>` memory-file format.\n * @public\n */\nexport function joinFrontmatter(frontmatter: string, body: string): string {\n const normalized: string = frontmatter.endsWith('\\n') ? frontmatter : `${frontmatter}\\n`;\n return `${FRONTMATTER_DELIMITER}\\n${normalized}${FRONTMATTER_DELIMITER}\\n${body}`;\n}\n\n/**\n * Parse a complete memory file into a typed {@link IMemoryRecord}. Splits the\n * frontmatter, validates the envelope, and dispatches the body through the\n * registered Converter for the envelope's kind.\n * @public\n */\nexport function parseMemoryFile(\n raw: string,\n registry: IBodyConverterRegistry\n): Result<IMemoryRecord<unknown>> {\n return splitFrontmatter(raw).onSuccess((parts) =>\n envelopeYamlConverter\n .convert(parts.frontmatter)\n .withErrorFormat((msg) => `memory file: invalid envelope: ${msg}`)\n .onSuccess((envelope) =>\n registry\n .convert(envelope.kind, parts.body)\n .withErrorFormat((msg) => `memory file '${envelope.id}': invalid body: ${msg}`)\n .onSuccess((body) => succeed({ envelope, body }))\n )\n );\n}\n\n/**\n * Serialize an envelope and its rendered body text into a memory file. The\n * envelope is emitted as YAML frontmatter; the body is written verbatim after\n * the closing delimiter.\n * @public\n */\nexport function serializeMemoryFile(envelope: IMemoryEnvelope, body: string): Result<string> {\n return Yaml.yamlStringify(envelope)\n .withErrorFormat((msg) => `memory file '${envelope.id}': failed to serialize envelope: ${msg}`)\n .onSuccess((frontmatter) => succeed(joinFrontmatter(frontmatter, body)));\n}\n"]}
1
+ {"version":3,"file":"envelopeConverter.js","sourceRoot":"","sources":["../../../src/packlets/converters/envelopeConverter.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Converter, Converters, Result, fail, succeed } from '@fgv/ts-utils';\nimport { Yaml } from '@fgv/ts-extras';\nimport {\n Convert,\n IEdge,\n IEdgeTarget,\n IMemoryEnvelope,\n IMemoryRecord,\n IProvenance,\n ITemporalBlock\n} from '../types';\nimport { IBodyConverterRegistry } from './bodyConverterRegistry';\n\n/** Matches exactly the JSON `null` literal. */\n// eslint-disable-next-line @rushstack/no-new-null -- the envelope nullable fields (invalid_at, embeddingRef) carry meaningful null; design-lock §2.3-2.5\nconst nullConverter: Converter<null> = Converters.generic<null>((from: unknown) =>\n from === null ? succeed(null) : fail('expected null')\n);\n\n/** A nullable epoch-ms value (`number | null`). */\n// eslint-disable-next-line @rushstack/no-new-null -- meaningful null for IEdge/ITemporalBlock.invalid_at; design-lock §2.3-2.4\nconst numberOrNull: Converter<number | null> = Converters.oneOf<number | null>([\n Converters.number,\n nullConverter\n]);\n\n/** A nullable string value (`string | null`). */\n// eslint-disable-next-line @rushstack/no-new-null -- meaningful null for IMemoryEnvelope.embeddingRef; design-lock §2.5\nconst stringOrNull: Converter<string | null> = Converters.oneOf<string | null>([\n Converters.string,\n nullConverter\n]);\n\n/**\n * Converter for a scope-qualified {@link IEdgeTarget}. Both `scope` and `id`\n * are required — the whole point of the scoped target is that a bare id is\n * ambiguous across scopes.\n * @public\n */\nexport const edgeTargetConverter: Converter<IEdgeTarget> = Converters.object<IEdgeTarget>({\n scope: Convert.scopeKey,\n id: Convert.memoryId\n});\n\n/**\n * Converter for the known {@link IProvenance} fields. The full\n * {@link provenanceConverter} layers extension-key preservation on top.\n * `derivedFrom` is serialized as the nested `{ scope, id }` object (mirroring\n * {@link edgeConverter | edge}'s scope-qualified `target`) so the provenance\n * back-reference is unambiguous across scopes.\n */\nconst knownProvenanceConverter: Converter<IProvenance> = Converters.object<IProvenance>(\n {\n source: Converters.string,\n by: Converters.string.optional(),\n model: Converters.string.optional(),\n confidence: Converters.number.optional(),\n derivedFrom: edgeTargetConverter.optional()\n },\n { optionalFields: ['by', 'model', 'confidence', 'derivedFrom'] }\n);\n\n/**\n * Converter for {@link IProvenance}. Validates the known fields and preserves\n * any extension keys verbatim (the `[key: string]: unknown` opaque payload),\n * so a round-trip never drops consumer-attached data.\n * @public\n */\nexport const provenanceConverter: Converter<IProvenance> = Converters.generic<IProvenance>(\n (from: unknown): Result<IProvenance> =>\n knownProvenanceConverter.convert(from).onSuccess((known) => {\n // `from` is guaranteed to be a non-null object here — the known-field\n // converter only succeeds on objects. Spread the original keys first,\n // then the validated/normalized known fields on top, so extension keys\n // survive while the known fields carry their converted values.\n const merged: IProvenance = { ...(from as Record<string, unknown>), ...known };\n return succeed(merged);\n })\n);\n\n/**\n * Converter for an attributed {@link IEdge}.\n * @public\n */\nexport const edgeConverter: Converter<IEdge> = Converters.object<IEdge>(\n {\n type: Convert.linkType,\n target: edgeTargetConverter,\n confidence: Converters.number.optional(),\n provenance: provenanceConverter.optional(),\n valid_at: Converters.number.optional(),\n invalid_at: numberOrNull.optional()\n },\n { optionalFields: ['confidence', 'provenance', 'valid_at', 'invalid_at'] }\n);\n\n/**\n * Converter for the optional {@link ITemporalBlock}.\n * @public\n */\nexport const temporalConverter: Converter<ITemporalBlock> = Converters.object<ITemporalBlock>(\n {\n valid_at: Converters.number.optional(),\n invalid_at: numberOrNull.optional()\n },\n { optionalFields: ['valid_at', 'invalid_at'] }\n);\n\n/**\n * Converter for the invariant {@link IMemoryEnvelope}. Validates a plain\n * object (e.g. parsed YAML frontmatter) into a typed envelope.\n * @public\n */\nexport const envelopeConverter: Converter<IMemoryEnvelope> = Converters.object<IMemoryEnvelope>(\n {\n id: Convert.memoryId,\n entityId: Convert.entityId,\n kind: Convert.kind,\n tags: Converters.arrayOf(Convert.tag),\n links: Converters.arrayOf(edgeConverter),\n created: Converters.number,\n updated: Converters.number,\n seq: Converters.number,\n contentHash: Converters.string,\n rank: Converters.number.optional(),\n provenance: provenanceConverter,\n temporal: temporalConverter.optional(),\n embeddingRef: stringOrNull.optional()\n },\n { optionalFields: ['rank', 'temporal', 'embeddingRef'] }\n);\n\n/**\n * Converter that parses a YAML frontmatter string into an\n * {@link IMemoryEnvelope}.\n * @public\n */\nexport const envelopeYamlConverter: Converter<IMemoryEnvelope> = Yaml.yamlConverter(envelopeConverter);\n\n/** The frontmatter delimiter line. */\nconst FRONTMATTER_DELIMITER: string = '---';\n\n/**\n * The two parts of a memory file: the YAML frontmatter (between the `---`\n * delimiters) and the markdown body (everything after the closing delimiter).\n * @public\n */\nexport interface IMemoryFileParts {\n /** The raw YAML frontmatter (delimiters stripped). */\n readonly frontmatter: string;\n /** The raw body text following the closing delimiter. */\n readonly body: string;\n}\n\n/**\n * Split a `---\\n<yaml>\\n---\\n<body>` memory file into its frontmatter and\n * body parts. Pure string handling — no external dependency.\n * @public\n */\nexport function splitFrontmatter(raw: string): Result<IMemoryFileParts> {\n // Split on '\\n' and strip a trailing '\\r' per line so CRLF-authored files\n // parse identically to LF — otherwise the '\\r' would ride along into the\n // returned frontmatter (perturbing YAML parsing) and body (corrupting\n // round-trip fidelity).\n const lines: string[] = raw.split('\\n').map((line) => (line.endsWith('\\r') ? line.slice(0, -1) : line));\n if (lines[0].trim() !== FRONTMATTER_DELIMITER) {\n return fail(\"memory file: missing opening frontmatter delimiter '---'\");\n }\n let closeIndex: number = -1;\n for (let i = 1; i < lines.length; i++) {\n if (lines[i].trim() === FRONTMATTER_DELIMITER) {\n closeIndex = i;\n break;\n }\n }\n if (closeIndex === -1) {\n return fail(\"memory file: missing closing frontmatter delimiter '---'\");\n }\n return succeed({\n frontmatter: lines.slice(1, closeIndex).join('\\n'),\n body: lines.slice(closeIndex + 1).join('\\n')\n });\n}\n\n/**\n * Join a YAML frontmatter string and a body into the canonical\n * `---\\n<yaml>\\n---\\n<body>` memory-file format.\n * @public\n */\nexport function joinFrontmatter(frontmatter: string, body: string): string {\n const normalized: string = frontmatter.endsWith('\\n') ? frontmatter : `${frontmatter}\\n`;\n return `${FRONTMATTER_DELIMITER}\\n${normalized}${FRONTMATTER_DELIMITER}\\n${body}`;\n}\n\n/**\n * Parse a complete memory file into a typed {@link IMemoryRecord}. Splits the\n * frontmatter, validates the envelope, and dispatches the body through the\n * registered Converter for the envelope's kind.\n * @public\n */\nexport function parseMemoryFile(\n raw: string,\n registry: IBodyConverterRegistry\n): Result<IMemoryRecord<unknown>> {\n return splitFrontmatter(raw).onSuccess((parts) =>\n envelopeYamlConverter\n .convert(parts.frontmatter)\n .withErrorFormat((msg) => `memory file: invalid envelope: ${msg}`)\n .onSuccess((envelope) =>\n registry\n .convert(envelope.kind, parts.body)\n .withErrorFormat((msg) => `memory file '${envelope.id}': invalid body: ${msg}`)\n .onSuccess((body) => succeed({ envelope, body }))\n )\n );\n}\n\n/**\n * Serialize an envelope and its rendered body text into a memory file. The\n * envelope is emitted as YAML frontmatter; the body is written verbatim after\n * the closing delimiter.\n * @public\n */\nexport function serializeMemoryFile(envelope: IMemoryEnvelope, body: string): Result<string> {\n return Yaml.yamlStringify(envelope)\n .withErrorFormat((msg) => `memory file '${envelope.id}': failed to serialize envelope: ${msg}`)\n .onSuccess((frontmatter) => succeed(joinFrontmatter(frontmatter, body)));\n}\n"]}
@@ -3,6 +3,7 @@
3
3
  * SPDX-License-Identifier: MIT
4
4
  */
5
5
  import { succeed } from '@fgv/ts-utils';
6
+ import { edgeTargetKey } from '../types';
6
7
  /** Shared empty key set for `byKind` / `byTag` misses (avoids per-call allocation). */
7
8
  const EMPTY_KEY_SET = new Set();
8
9
  /**
@@ -30,7 +31,7 @@ export class MemoryIndex {
30
31
  * is a collision-proof separator across every scope/id pair the codecs produce.
31
32
  */
32
33
  static _keyOf(scope, id) {
33
- return `${scope}\0${id}`;
34
+ return edgeTargetKey({ scope, id });
34
35
  }
35
36
  /** {@inheritDoc IMemoryIndex.rebuild} */
36
37
  rebuild(entries) {
@@ -72,9 +73,13 @@ export class MemoryIndex {
72
73
  byRecency() {
73
74
  return this._recencyOrdered(this._byKey.keys());
74
75
  }
76
+ /** {@inheritDoc IMemoryIndex.byRank} */
77
+ byRank() {
78
+ return this._rankOrdered(this._byKey.keys());
79
+ }
75
80
  /** {@inheritDoc IMemoryIndex.backlinks} */
76
81
  backlinks(target) {
77
- const sources = this._backlinks.get(target);
82
+ const sources = this._backlinks.get(edgeTargetKey(target));
78
83
  return sources === undefined ? [] : Array.from(sources.values());
79
84
  }
80
85
  /**
@@ -95,6 +100,47 @@ export class MemoryIndex {
95
100
  return byUpdated !== 0 ? byUpdated : b.envelope.seq - a.envelope.seq;
96
101
  });
97
102
  }
103
+ /**
104
+ * Resolve a set of composite keys to their records, ordered by
105
+ * {@link IMemoryEnvelope.rank} descending with recency (`updated`, then `seq`)
106
+ * as the tiebreak. Records with an absent `rank` sort LAST, then by recency
107
+ * among themselves. Computed on call (mirrors {@link MemoryIndex._recencyOrdered}) —
108
+ * no incremental rank-ordered view is maintained, matching the recency view's
109
+ * approach; the sort is over the in-memory index, never a filesystem walk.
110
+ */
111
+ _rankOrdered(keys) {
112
+ const records = [];
113
+ for (const key of keys) {
114
+ const entry = this._byKey.get(key);
115
+ if (entry !== undefined) {
116
+ records.push(entry.record);
117
+ }
118
+ }
119
+ return records.sort(MemoryIndex._compareByRank);
120
+ }
121
+ /**
122
+ * Rank-descending comparator with an absent-`rank`-last rule and a recency
123
+ * (`updated`, then `seq`) tiebreak. Duplicated from the retrieve packlet's
124
+ * `rankCompare` deliberately: the index must not depend on `retrieve` (that
125
+ * package depends on the index), mirroring how `_recencyOrdered` inlines the
126
+ * recency ordering rather than importing `recencyCompare`.
127
+ */
128
+ static _compareByRank(a, b) {
129
+ const ra = a.envelope.rank;
130
+ const rb = b.envelope.rank;
131
+ // Absent rank sorts last; two absent ranks fall through to the recency tiebreak.
132
+ if (ra === undefined && rb !== undefined) {
133
+ return 1;
134
+ }
135
+ if (rb === undefined && ra !== undefined) {
136
+ return -1;
137
+ }
138
+ if (ra !== undefined && rb !== undefined && ra !== rb) {
139
+ return rb - ra;
140
+ }
141
+ const byUpdated = b.envelope.updated - a.envelope.updated;
142
+ return byUpdated !== 0 ? byUpdated : b.envelope.seq - a.envelope.seq;
143
+ }
98
144
  /** Insert an entry and register all its derived associations. */
99
145
  _add(entry) {
100
146
  const key = MemoryIndex._keyOf(entry.scope, entry.record.envelope.id);
@@ -105,7 +151,7 @@ export class MemoryIndex {
105
151
  this._addToSetMap(this._byTag, tag, key);
106
152
  }
107
153
  for (const edge of envelope.links) {
108
- this._addBacklink(edge.target, key, envelope.id);
154
+ this._addBacklink(edge.target, key, { scope: entry.scope, id: envelope.id });
109
155
  }
110
156
  }
111
157
  /** Remove the entry at `key` (if present) and all its derived associations. */
@@ -124,25 +170,27 @@ export class MemoryIndex {
124
170
  this._removeBacklink(edge.target, key);
125
171
  }
126
172
  }
127
- /** Register `sourceId` (keyed by its composite `sourceKey`) as linking at `target`. */
128
- _addBacklink(target, sourceKey, sourceId) {
129
- const existing = this._backlinks.get(target);
173
+ /** Register `source` (keyed by its composite `sourceKey`) as linking at `target`. */
174
+ _addBacklink(target, sourceKey, source) {
175
+ const targetKey = edgeTargetKey(target);
176
+ const existing = this._backlinks.get(targetKey);
130
177
  if (existing === undefined) {
131
- this._backlinks.set(target, new Map([[sourceKey, sourceId]]));
178
+ this._backlinks.set(targetKey, new Map([[sourceKey, source]]));
132
179
  }
133
180
  else {
134
- existing.set(sourceKey, sourceId);
181
+ existing.set(sourceKey, source);
135
182
  }
136
183
  }
137
184
  /** Drop the backlink from `sourceKey` to `target`, removing the target map when empty. */
138
185
  _removeBacklink(target, sourceKey) {
139
- const existing = this._backlinks.get(target);
186
+ const targetKey = edgeTargetKey(target);
187
+ const existing = this._backlinks.get(targetKey);
140
188
  if (existing === undefined) {
141
189
  return;
142
190
  }
143
191
  existing.delete(sourceKey);
144
192
  if (existing.size === 0) {
145
- this._backlinks.delete(target);
193
+ this._backlinks.delete(targetKey);
146
194
  }
147
195
  }
148
196
  /** Add `member` to the set at `mapKey`, creating the set on first use. */
@@ -1 +1 @@
1
- {"version":3,"file":"memoryIndex.js","sourceRoot":"","sources":["../../../src/packlets/index/memoryIndex.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord, Kind, MemoryId, MemoryScopeKey, Tag } from '../types';\n\n/**\n * The mutation a {@link IMemoryIndex.patch | patch} applies: a record was\n * written (`'put'`) or removed (`'delete'`).\n * @public\n */\nexport type MemoryIndexPatchOp = 'put' | 'delete';\n\n/** Shared empty key set for `byKind` / `byTag` misses (avoids per-call allocation). */\nconst EMPTY_KEY_SET: ReadonlySet<string> = new Set<string>();\n\n/**\n * A memory record paired with the {@link MemoryScopeKey | scope} it lives\n * under. The scope is not carried on the {@link IMemoryEnvelope} (it is derived\n * from the entity id by the codec), so the store threads it alongside the\n * record when patching or rebuilding the index. The `(scope, id)` pair is the\n * index's primary key, keeping records distinct across scopes that reuse a\n * filename stem (e.g. `turn-0` under different conversations in Phase C).\n * @public\n */\nexport interface IIndexedMemoryRecord {\n /** The scope the record is stored under. */\n readonly scope: MemoryScopeKey;\n /** The memory record itself. */\n readonly record: IMemoryRecord<unknown>;\n}\n\n/**\n * The derived, in-memory secondary indexes the store maintains over its\n * records. Never the source of truth — the FileTree is. The index is fully\n * rebuildable from a walk of the store ({@link IMemoryIndex.rebuild}) and is\n * patched incrementally on every write ({@link IMemoryIndex.patch}).\n *\n * @remarks\n * B1 builds the maps; link-traversal BFS over {@link IMemoryIndex.backlinks}\n * is B2. The accessors return records (not bare ids) so the B2 retrievers can\n * consume them directly.\n * @public\n */\nexport interface IMemoryIndex {\n /**\n * Replace the entire index from a full set of records (a store walk).\n * @returns The number of records indexed.\n */\n rebuild(entries: ReadonlyArray<IIndexedMemoryRecord>): Result<number>;\n\n /**\n * Apply a single incremental change. `'put'` inserts or replaces the entry\n * at its `(scope, id)` key (removing any prior associations first); `'delete'`\n * removes it.\n * @returns The entry that was applied.\n */\n patch(op: MemoryIndexPatchOp, entry: IIndexedMemoryRecord): Result<IIndexedMemoryRecord>;\n\n /** Every indexed entry (scope + record). Primary read surface for the store. */\n entries(): ReadonlyArray<IIndexedMemoryRecord>;\n\n /** Records of the given kind, in recency order (most-recently-updated first). */\n byKind(kind: Kind): ReadonlyArray<IMemoryRecord<unknown>>;\n\n /** Records carrying the given tag, in recency order. */\n byTag(tag: Tag): ReadonlyArray<IMemoryRecord<unknown>>;\n\n /** All records in recency order (most-recently-updated first). */\n byRecency(): ReadonlyArray<IMemoryRecord<unknown>>;\n\n /**\n * The ids of records whose `links` point AT `target` (inbound edges).\n * The seed map for B2 link-traversal.\n */\n backlinks(target: MemoryId): ReadonlyArray<MemoryId>;\n}\n\n/**\n * Default in-memory {@link IMemoryIndex}. Maintains the derived `byKind` /\n * `byTag` / `byRecency` / `backlinks` views incrementally; a `'put'` for an\n * already-indexed key first removes the prior entry's associations so a changed\n * kind / tag / link set never leaves a stale reference behind.\n * @public\n */\nexport class MemoryIndex implements IMemoryIndex {\n /** Primary store: `(scope, id)` composite key → indexed entry. */\n private readonly _byKey: Map<string, IIndexedMemoryRecord>;\n /** kind → set of composite keys. */\n private readonly _byKind: Map<Kind, Set<string>>;\n /** tag → set of composite keys. */\n private readonly _byTag: Map<Tag, Set<string>>;\n /**\n * link target id → (source composite key → source id). Keyed by the source's\n * `(scope, id)` composite — NOT its bare id — so two distinct source records\n * that share an id across scopes (e.g. `turn-0` in different conversations)\n * are tracked independently and removing one never drops the other's edge.\n */\n private readonly _backlinks: Map<MemoryId, Map<string, MemoryId>>;\n\n private constructor() {\n this._byKey = new Map<string, IIndexedMemoryRecord>();\n this._byKind = new Map<Kind, Set<string>>();\n this._byTag = new Map<Tag, Set<string>>();\n this._backlinks = new Map<MemoryId, Map<string, MemoryId>>();\n }\n\n /** Family-convention factory. */\n public static create(): Result<MemoryIndex> {\n return succeed(new MemoryIndex());\n }\n\n /**\n * The composite primary key for an entry: scope + id, NUL-separated. NUL\n * is excluded from both components (scope segments pass\n * `assertPortableFilenameStem`; `MemoryId` is portable-filename-safe), so it\n * is a collision-proof separator across every scope/id pair the codecs produce.\n */\n private static _keyOf(scope: MemoryScopeKey, id: MemoryId): string {\n return `${scope}\\0${id}`;\n }\n\n /** {@inheritDoc IMemoryIndex.rebuild} */\n public rebuild(entries: ReadonlyArray<IIndexedMemoryRecord>): Result<number> {\n this._byKey.clear();\n this._byKind.clear();\n this._byTag.clear();\n this._backlinks.clear();\n for (const entry of entries) {\n this._add(entry);\n }\n return succeed(this._byKey.size);\n }\n\n /** {@inheritDoc IMemoryIndex.patch} */\n public patch(op: MemoryIndexPatchOp, entry: IIndexedMemoryRecord): Result<IIndexedMemoryRecord> {\n const key: string = MemoryIndex._keyOf(entry.scope, entry.record.envelope.id);\n // Always drop any prior associations for this key first, so a 'put' that\n // changes kind/tags/links cannot strand a stale reference.\n this._remove(key);\n if (op === 'put') {\n this._add(entry);\n }\n return succeed(entry);\n }\n\n /** {@inheritDoc IMemoryIndex.entries} */\n public entries(): ReadonlyArray<IIndexedMemoryRecord> {\n return Array.from(this._byKey.values());\n }\n\n /** {@inheritDoc IMemoryIndex.byKind} */\n public byKind(kind: Kind): ReadonlyArray<IMemoryRecord<unknown>> {\n return this._recencyOrdered(this._byKind.get(kind) ?? EMPTY_KEY_SET);\n }\n\n /** {@inheritDoc IMemoryIndex.byTag} */\n public byTag(tag: Tag): ReadonlyArray<IMemoryRecord<unknown>> {\n return this._recencyOrdered(this._byTag.get(tag) ?? EMPTY_KEY_SET);\n }\n\n /** {@inheritDoc IMemoryIndex.byRecency} */\n public byRecency(): ReadonlyArray<IMemoryRecord<unknown>> {\n return this._recencyOrdered(this._byKey.keys());\n }\n\n /** {@inheritDoc IMemoryIndex.backlinks} */\n public backlinks(target: MemoryId): ReadonlyArray<MemoryId> {\n const sources: Map<string, MemoryId> | undefined = this._backlinks.get(target);\n return sources === undefined ? [] : Array.from(sources.values());\n }\n\n /**\n * Resolve a set of composite keys to their records, ordered\n * most-recently-updated first (with a `seq` tiebreak so equal-`updated`\n * records sort deterministically).\n */\n private _recencyOrdered(keys: Iterable<string>): ReadonlyArray<IMemoryRecord<unknown>> {\n const records: IMemoryRecord<unknown>[] = [];\n for (const key of keys) {\n const entry: IIndexedMemoryRecord | undefined = this._byKey.get(key);\n if (entry !== undefined) {\n records.push(entry.record);\n }\n }\n return records.sort((a, b) => {\n const byUpdated: number = b.envelope.updated - a.envelope.updated;\n return byUpdated !== 0 ? byUpdated : b.envelope.seq - a.envelope.seq;\n });\n }\n\n /** Insert an entry and register all its derived associations. */\n private _add(entry: IIndexedMemoryRecord): void {\n const key: string = MemoryIndex._keyOf(entry.scope, entry.record.envelope.id);\n const envelope: IMemoryRecord<unknown>['envelope'] = entry.record.envelope;\n this._byKey.set(key, entry);\n this._addToSetMap(this._byKind, envelope.kind, key);\n for (const tag of envelope.tags) {\n this._addToSetMap(this._byTag, tag, key);\n }\n for (const edge of envelope.links) {\n this._addBacklink(edge.target, key, envelope.id);\n }\n }\n\n /** Remove the entry at `key` (if present) and all its derived associations. */\n private _remove(key: string): void {\n const entry: IIndexedMemoryRecord | undefined = this._byKey.get(key);\n if (entry === undefined) {\n return;\n }\n const envelope: IMemoryRecord<unknown>['envelope'] = entry.record.envelope;\n this._byKey.delete(key);\n this._removeFromSetMap(this._byKind, envelope.kind, key);\n for (const tag of envelope.tags) {\n this._removeFromSetMap(this._byTag, tag, key);\n }\n for (const edge of envelope.links) {\n this._removeBacklink(edge.target, key);\n }\n }\n\n /** Register `sourceId` (keyed by its composite `sourceKey`) as linking at `target`. */\n private _addBacklink(target: MemoryId, sourceKey: string, sourceId: MemoryId): void {\n const existing: Map<string, MemoryId> | undefined = this._backlinks.get(target);\n if (existing === undefined) {\n this._backlinks.set(target, new Map<string, MemoryId>([[sourceKey, sourceId]]));\n } else {\n existing.set(sourceKey, sourceId);\n }\n }\n\n /** Drop the backlink from `sourceKey` to `target`, removing the target map when empty. */\n private _removeBacklink(target: MemoryId, sourceKey: string): void {\n const existing: Map<string, MemoryId> | undefined = this._backlinks.get(target);\n if (existing === undefined) {\n return;\n }\n existing.delete(sourceKey);\n if (existing.size === 0) {\n this._backlinks.delete(target);\n }\n }\n\n /** Add `member` to the set at `mapKey`, creating the set on first use. */\n private _addToSetMap<K, M>(map: Map<K, Set<M>>, mapKey: K, member: M): void {\n const existing: Set<M> | undefined = map.get(mapKey);\n if (existing === undefined) {\n map.set(mapKey, new Set<M>([member]));\n } else {\n existing.add(member);\n }\n }\n\n /** Remove `member` from the set at `mapKey`, dropping the set when empty. */\n private _removeFromSetMap<K, M>(map: Map<K, Set<M>>, mapKey: K, member: M): void {\n const existing: Set<M> | undefined = map.get(mapKey);\n if (existing === undefined) {\n return;\n }\n existing.delete(member);\n if (existing.size === 0) {\n map.delete(mapKey);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"memoryIndex.js","sourceRoot":"","sources":["../../../src/packlets/index/memoryIndex.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IEdgeTarget, IMemoryRecord, Kind, MemoryId, MemoryScopeKey, Tag, edgeTargetKey } from '../types';\n\n/**\n * The mutation a {@link IMemoryIndex.patch | patch} applies: a record was\n * written (`'put'`) or removed (`'delete'`).\n * @public\n */\nexport type MemoryIndexPatchOp = 'put' | 'delete';\n\n/** Shared empty key set for `byKind` / `byTag` misses (avoids per-call allocation). */\nconst EMPTY_KEY_SET: ReadonlySet<string> = new Set<string>();\n\n/**\n * A memory record paired with the {@link MemoryScopeKey | scope} it lives\n * under. The scope is not carried on the {@link IMemoryEnvelope} (it is derived\n * from the entity id by the codec), so the store threads it alongside the\n * record when patching or rebuilding the index. The `(scope, id)` pair is the\n * index's primary key, keeping records distinct across scopes that reuse a\n * filename stem (e.g. `turn-0` under different conversations in Phase C).\n * @public\n */\nexport interface IIndexedMemoryRecord {\n /** The scope the record is stored under. */\n readonly scope: MemoryScopeKey;\n /** The memory record itself. */\n readonly record: IMemoryRecord<unknown>;\n}\n\n/**\n * The derived, in-memory secondary indexes the store maintains over its\n * records. Never the source of truth — the FileTree is. The index is fully\n * rebuildable from a walk of the store ({@link IMemoryIndex.rebuild}) and is\n * patched incrementally on every write ({@link IMemoryIndex.patch}).\n *\n * @remarks\n * B1 builds the maps; link-traversal BFS over {@link IMemoryIndex.backlinks}\n * is B2. The accessors return records (not bare ids) so the B2 retrievers can\n * consume them directly.\n * @public\n */\nexport interface IMemoryIndex {\n /**\n * Replace the entire index from a full set of records (a store walk).\n * @returns The number of records indexed.\n */\n rebuild(entries: ReadonlyArray<IIndexedMemoryRecord>): Result<number>;\n\n /**\n * Apply a single incremental change. `'put'` inserts or replaces the entry\n * at its `(scope, id)` key (removing any prior associations first); `'delete'`\n * removes it.\n * @returns The entry that was applied.\n */\n patch(op: MemoryIndexPatchOp, entry: IIndexedMemoryRecord): Result<IIndexedMemoryRecord>;\n\n /** Every indexed entry (scope + record). Primary read surface for the store. */\n entries(): ReadonlyArray<IIndexedMemoryRecord>;\n\n /** Records of the given kind, in recency order (most-recently-updated first). */\n byKind(kind: Kind): ReadonlyArray<IMemoryRecord<unknown>>;\n\n /** Records carrying the given tag, in recency order. */\n byTag(tag: Tag): ReadonlyArray<IMemoryRecord<unknown>>;\n\n /** All records in recency order (most-recently-updated first). */\n byRecency(): ReadonlyArray<IMemoryRecord<unknown>>;\n\n /**\n * All records ordered by store-computed {@link IMemoryEnvelope.rank} descending,\n * with recency (most-recently-updated, then `seq`) as a tiebreak. Records with\n * an absent `rank` sort LAST (after every ranked record), then by recency among\n * themselves. Serves a bounded top-M ({@link IMemoryEnvelope.rank}-ordered) page\n * from the in-memory index with no full-vault (filesystem) scan.\n */\n byRank(): ReadonlyArray<IMemoryRecord<unknown>>;\n\n /**\n * The scope-qualified sources of records whose `links` point AT `target`\n * (inbound edges), keyed on the target's `(scope, id)` address. The seed map\n * for B2 link-traversal; results are {@link IEdgeTarget}s so a caller can feed\n * them straight back in as further traversal seeds.\n */\n backlinks(target: IEdgeTarget): ReadonlyArray<IEdgeTarget>;\n}\n\n/**\n * Default in-memory {@link IMemoryIndex}. Maintains the derived `byKind` /\n * `byTag` / `byRecency` / `backlinks` views incrementally; a `'put'` for an\n * already-indexed key first removes the prior entry's associations so a changed\n * kind / tag / link set never leaves a stale reference behind.\n * @public\n */\nexport class MemoryIndex implements IMemoryIndex {\n /** Primary store: `(scope, id)` composite key → indexed entry. */\n private readonly _byKey: Map<string, IIndexedMemoryRecord>;\n /** kind → set of composite keys. */\n private readonly _byKind: Map<Kind, Set<string>>;\n /** tag → set of composite keys. */\n private readonly _byTag: Map<Tag, Set<string>>;\n /**\n * canonical target key (`edgeTargetKey`) → (source composite key → source\n * {@link IEdgeTarget}). The OUTER map is keyed on the scope-qualified target's\n * canonical `(scope, id)` string — NOT the target's bare id — so an edge to\n * `turn-3` in one conversation is tracked separately from `turn-3` in another.\n * The INNER map is keyed by the source's `(scope, id)` composite so two distinct\n * source records that share an id across scopes are tracked independently and\n * removing one never drops the other's edge.\n */\n private readonly _backlinks: Map<string, Map<string, IEdgeTarget>>;\n\n private constructor() {\n this._byKey = new Map<string, IIndexedMemoryRecord>();\n this._byKind = new Map<Kind, Set<string>>();\n this._byTag = new Map<Tag, Set<string>>();\n this._backlinks = new Map<string, Map<string, IEdgeTarget>>();\n }\n\n /** Family-convention factory. */\n public static create(): Result<MemoryIndex> {\n return succeed(new MemoryIndex());\n }\n\n /**\n * The composite primary key for an entry: scope + id, NUL-separated. NUL\n * is excluded from both components (scope segments pass\n * `assertPortableFilenameStem`; `MemoryId` is portable-filename-safe), so it\n * is a collision-proof separator across every scope/id pair the codecs produce.\n */\n private static _keyOf(scope: MemoryScopeKey, id: MemoryId): string {\n return edgeTargetKey({ scope, id });\n }\n\n /** {@inheritDoc IMemoryIndex.rebuild} */\n public rebuild(entries: ReadonlyArray<IIndexedMemoryRecord>): Result<number> {\n this._byKey.clear();\n this._byKind.clear();\n this._byTag.clear();\n this._backlinks.clear();\n for (const entry of entries) {\n this._add(entry);\n }\n return succeed(this._byKey.size);\n }\n\n /** {@inheritDoc IMemoryIndex.patch} */\n public patch(op: MemoryIndexPatchOp, entry: IIndexedMemoryRecord): Result<IIndexedMemoryRecord> {\n const key: string = MemoryIndex._keyOf(entry.scope, entry.record.envelope.id);\n // Always drop any prior associations for this key first, so a 'put' that\n // changes kind/tags/links cannot strand a stale reference.\n this._remove(key);\n if (op === 'put') {\n this._add(entry);\n }\n return succeed(entry);\n }\n\n /** {@inheritDoc IMemoryIndex.entries} */\n public entries(): ReadonlyArray<IIndexedMemoryRecord> {\n return Array.from(this._byKey.values());\n }\n\n /** {@inheritDoc IMemoryIndex.byKind} */\n public byKind(kind: Kind): ReadonlyArray<IMemoryRecord<unknown>> {\n return this._recencyOrdered(this._byKind.get(kind) ?? EMPTY_KEY_SET);\n }\n\n /** {@inheritDoc IMemoryIndex.byTag} */\n public byTag(tag: Tag): ReadonlyArray<IMemoryRecord<unknown>> {\n return this._recencyOrdered(this._byTag.get(tag) ?? EMPTY_KEY_SET);\n }\n\n /** {@inheritDoc IMemoryIndex.byRecency} */\n public byRecency(): ReadonlyArray<IMemoryRecord<unknown>> {\n return this._recencyOrdered(this._byKey.keys());\n }\n\n /** {@inheritDoc IMemoryIndex.byRank} */\n public byRank(): ReadonlyArray<IMemoryRecord<unknown>> {\n return this._rankOrdered(this._byKey.keys());\n }\n\n /** {@inheritDoc IMemoryIndex.backlinks} */\n public backlinks(target: IEdgeTarget): ReadonlyArray<IEdgeTarget> {\n const sources: Map<string, IEdgeTarget> | undefined = this._backlinks.get(edgeTargetKey(target));\n return sources === undefined ? [] : Array.from(sources.values());\n }\n\n /**\n * Resolve a set of composite keys to their records, ordered\n * most-recently-updated first (with a `seq` tiebreak so equal-`updated`\n * records sort deterministically).\n */\n private _recencyOrdered(keys: Iterable<string>): ReadonlyArray<IMemoryRecord<unknown>> {\n const records: IMemoryRecord<unknown>[] = [];\n for (const key of keys) {\n const entry: IIndexedMemoryRecord | undefined = this._byKey.get(key);\n if (entry !== undefined) {\n records.push(entry.record);\n }\n }\n return records.sort((a, b) => {\n const byUpdated: number = b.envelope.updated - a.envelope.updated;\n return byUpdated !== 0 ? byUpdated : b.envelope.seq - a.envelope.seq;\n });\n }\n\n /**\n * Resolve a set of composite keys to their records, ordered by\n * {@link IMemoryEnvelope.rank} descending with recency (`updated`, then `seq`)\n * as the tiebreak. Records with an absent `rank` sort LAST, then by recency\n * among themselves. Computed on call (mirrors {@link MemoryIndex._recencyOrdered}) —\n * no incremental rank-ordered view is maintained, matching the recency view's\n * approach; the sort is over the in-memory index, never a filesystem walk.\n */\n private _rankOrdered(keys: Iterable<string>): ReadonlyArray<IMemoryRecord<unknown>> {\n const records: IMemoryRecord<unknown>[] = [];\n for (const key of keys) {\n const entry: IIndexedMemoryRecord | undefined = this._byKey.get(key);\n if (entry !== undefined) {\n records.push(entry.record);\n }\n }\n return records.sort(MemoryIndex._compareByRank);\n }\n\n /**\n * Rank-descending comparator with an absent-`rank`-last rule and a recency\n * (`updated`, then `seq`) tiebreak. Duplicated from the retrieve packlet's\n * `rankCompare` deliberately: the index must not depend on `retrieve` (that\n * package depends on the index), mirroring how `_recencyOrdered` inlines the\n * recency ordering rather than importing `recencyCompare`.\n */\n private static _compareByRank(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number {\n const ra: number | undefined = a.envelope.rank;\n const rb: number | undefined = b.envelope.rank;\n // Absent rank sorts last; two absent ranks fall through to the recency tiebreak.\n if (ra === undefined && rb !== undefined) {\n return 1;\n }\n if (rb === undefined && ra !== undefined) {\n return -1;\n }\n if (ra !== undefined && rb !== undefined && ra !== rb) {\n return rb - ra;\n }\n const byUpdated: number = b.envelope.updated - a.envelope.updated;\n return byUpdated !== 0 ? byUpdated : b.envelope.seq - a.envelope.seq;\n }\n\n /** Insert an entry and register all its derived associations. */\n private _add(entry: IIndexedMemoryRecord): void {\n const key: string = MemoryIndex._keyOf(entry.scope, entry.record.envelope.id);\n const envelope: IMemoryRecord<unknown>['envelope'] = entry.record.envelope;\n this._byKey.set(key, entry);\n this._addToSetMap(this._byKind, envelope.kind, key);\n for (const tag of envelope.tags) {\n this._addToSetMap(this._byTag, tag, key);\n }\n for (const edge of envelope.links) {\n this._addBacklink(edge.target, key, { scope: entry.scope, id: envelope.id });\n }\n }\n\n /** Remove the entry at `key` (if present) and all its derived associations. */\n private _remove(key: string): void {\n const entry: IIndexedMemoryRecord | undefined = this._byKey.get(key);\n if (entry === undefined) {\n return;\n }\n const envelope: IMemoryRecord<unknown>['envelope'] = entry.record.envelope;\n this._byKey.delete(key);\n this._removeFromSetMap(this._byKind, envelope.kind, key);\n for (const tag of envelope.tags) {\n this._removeFromSetMap(this._byTag, tag, key);\n }\n for (const edge of envelope.links) {\n this._removeBacklink(edge.target, key);\n }\n }\n\n /** Register `source` (keyed by its composite `sourceKey`) as linking at `target`. */\n private _addBacklink(target: IEdgeTarget, sourceKey: string, source: IEdgeTarget): void {\n const targetKey: string = edgeTargetKey(target);\n const existing: Map<string, IEdgeTarget> | undefined = this._backlinks.get(targetKey);\n if (existing === undefined) {\n this._backlinks.set(targetKey, new Map<string, IEdgeTarget>([[sourceKey, source]]));\n } else {\n existing.set(sourceKey, source);\n }\n }\n\n /** Drop the backlink from `sourceKey` to `target`, removing the target map when empty. */\n private _removeBacklink(target: IEdgeTarget, sourceKey: string): void {\n const targetKey: string = edgeTargetKey(target);\n const existing: Map<string, IEdgeTarget> | undefined = this._backlinks.get(targetKey);\n if (existing === undefined) {\n return;\n }\n existing.delete(sourceKey);\n if (existing.size === 0) {\n this._backlinks.delete(targetKey);\n }\n }\n\n /** Add `member` to the set at `mapKey`, creating the set on first use. */\n private _addToSetMap<K, M>(map: Map<K, Set<M>>, mapKey: K, member: M): void {\n const existing: Set<M> | undefined = map.get(mapKey);\n if (existing === undefined) {\n map.set(mapKey, new Set<M>([member]));\n } else {\n existing.add(member);\n }\n }\n\n /** Remove `member` from the set at `mapKey`, dropping the set when empty. */\n private _removeFromSetMap<K, M>(map: Map<K, Set<M>>, mapKey: K, member: M): void {\n const existing: Set<M> | undefined = map.get(mapKey);\n if (existing === undefined) {\n return;\n }\n existing.delete(member);\n if (existing.size === 0) {\n map.delete(mapKey);\n }\n }\n}\n"]}
@@ -3,6 +3,11 @@
3
3
  * SPDX-License-Identifier: MIT
4
4
  */
5
5
  import { Hash, fail, mapResults, succeed } from '@fgv/ts-utils';
6
+ import { edgeTargetKey } from '../types';
7
+ /** Human-readable `scope/id` rendering of a scoped node, for cycle-guard diagnostics. */
8
+ function formatNode(node) {
9
+ return `${node.scope}/${node.id}`;
10
+ }
6
11
  /**
7
12
  * Build the design's `buildCycleKey` — a deterministic, canonical (RFC-8785,
8
13
  * via `Crc32Normalizer` from `@fgv/ts-utils`) identity for a directed edge. Used
@@ -50,14 +55,16 @@ export function assertNoCycles(existing, proposed) {
50
55
  }
51
56
  for (const keyed of proposedKeyed) {
52
57
  const edge = keyed.edge;
58
+ const sourceKey = edgeTargetKey(edge.source);
59
+ const targetKey = edgeTargetKey(edge.target);
53
60
  // A self-loop is the degenerate one-node cycle.
54
- if (edge.source === edge.target) {
55
- return fail(`ingest cycle guard: edge '${edge.source}' -${edge.type}-> '${edge.target}' is a self-loop`);
61
+ if (sourceKey === targetKey) {
62
+ return fail(`ingest cycle guard: edge '${formatNode(edge.source)}' -${edge.type}-> '${formatNode(edge.target)}' is a self-loop`);
56
63
  }
57
64
  // Reachability: does `target` already reach `source`? If so, adding
58
65
  // `source -> target` closes a directed cycle.
59
- if (reaches(adjacency, edge.target, edge.source)) {
60
- return fail(`ingest cycle guard: edge '${edge.source}' -${edge.type}-> '${edge.target}' would create a cycle`);
66
+ if (reaches(adjacency, targetKey, sourceKey)) {
67
+ return fail(`ingest cycle guard: edge '${formatNode(edge.source)}' -${edge.type}-> '${formatNode(edge.target)}' would create a cycle`);
61
68
  }
62
69
  addEdge(adjacency, seenKeys, keyed);
63
70
  }
@@ -74,8 +81,8 @@ function addEdge(adjacency, seenKeys, keyed) {
74
81
  return;
75
82
  }
76
83
  seenKeys.add(keyed.key);
77
- const source = keyed.edge.source;
78
- const target = keyed.edge.target;
84
+ const source = edgeTargetKey(keyed.edge.source);
85
+ const target = edgeTargetKey(keyed.edge.target);
79
86
  const targets = adjacency.get(source);
80
87
  if (targets === undefined) {
81
88
  adjacency.set(source, new Set([target]));
@@ -1 +1 @@
1
- {"version":3,"file":"cycleGuard.js","sourceRoot":"","sources":["../../../src/packlets/ingest/cycleGuard.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Hash, Result, fail, mapResults, succeed } from '@fgv/ts-utils';\nimport { LinkType, MemoryId } from '../types';\n\n/**\n * A directed edge in the link graph the cycle guard reasons over: `source` links\n * to `target` under relation `type`.\n * @public\n */\nexport interface ICycleGuardEdge {\n readonly source: MemoryId;\n readonly target: MemoryId;\n readonly type: LinkType;\n}\n\n/**\n * Build the design's `buildCycleKey` — a deterministic, canonical (RFC-8785,\n * via `Crc32Normalizer` from `@fgv/ts-utils`) identity for a directed edge. Used\n * to de-duplicate proposed edges so a repeated `(source, target, type)` proposal\n * contributes a single graph edge (and never spuriously \"re-closes\" a cycle).\n * @public\n */\nexport function buildCycleKey(edge: ICycleGuardEdge): Result<string> {\n return new Hash.Crc32Normalizer().computeHash({\n source: edge.source,\n target: edge.target,\n type: edge.type\n });\n}\n\n/**\n * Write-time cycle guard. Given the graph's EXISTING directed edges plus a set of\n * PROPOSED edges, verify that adding the proposed edges keeps the union graph a\n * DAG. Fails loudly, naming the first proposed edge that would close a directed\n * cycle.\n *\n * @remarks\n * Distinct from the read-time BFS in `LinkTraversalRetriever` (which tolerates\n * cycles via a visited-set): this is a WRITE-time admission check. Cycle\n * detection is DFS reachability — a proposed `source -> target` edge closes a\n * cycle iff `target` already reaches `source` in the graph built so far. Proposed\n * edges are folded in one at a time (in order), so an intra-batch cycle\n * (`A->B`, `B->A` both proposed) is caught as well as a batch-vs-existing cycle.\n *\n * See the design note §4: this enforces GLOBAL directed-acyclicity over all link\n * types (conservative — a mutual associative pair is rejected). Callers that need\n * mutual links disable the guard.\n * @public\n */\nexport function assertNoCycles(\n existing: ReadonlyArray<ICycleGuardEdge>,\n proposed: ReadonlyArray<ICycleGuardEdge>\n): Result<true> {\n // Canonically key every edge up front (the only fallible step); the graph\n // building + cycle detection below is then pure.\n return keyEdges(existing).onSuccess((existingKeyed) =>\n keyEdges(proposed).onSuccess((proposedKeyed) => {\n // Adjacency map, de-duplicated by canonical edge key so a repeated edge is\n // one arc. Seed with the existing edges, then fold in each proposed edge,\n // checking reachability BEFORE inserting it.\n const adjacency: Map<string, Set<string>> = new Map<string, Set<string>>();\n const seenKeys: Set<string> = new Set<string>();\n for (const keyed of existingKeyed) {\n addEdge(adjacency, seenKeys, keyed);\n }\n for (const keyed of proposedKeyed) {\n const edge: ICycleGuardEdge = keyed.edge;\n // A self-loop is the degenerate one-node cycle.\n if (edge.source === edge.target) {\n return fail(\n `ingest cycle guard: edge '${edge.source}' -${edge.type}-> '${edge.target}' is a self-loop`\n );\n }\n // Reachability: does `target` already reach `source`? If so, adding\n // `source -> target` closes a directed cycle.\n if (reaches(adjacency, edge.target, edge.source)) {\n return fail(\n `ingest cycle guard: edge '${edge.source}' -${edge.type}-> '${edge.target}' would create a cycle`\n );\n }\n addEdge(adjacency, seenKeys, keyed);\n }\n return succeed(true);\n })\n );\n}\n\n/** An edge paired with its canonical cycle key. */\ninterface IKeyedEdge {\n readonly edge: ICycleGuardEdge;\n readonly key: string;\n}\n\n/** Canonically key a set of edges (the guard's only fallible step). */\nfunction keyEdges(edges: ReadonlyArray<ICycleGuardEdge>): Result<ReadonlyArray<IKeyedEdge>> {\n return mapResults(edges.map((edge) => buildCycleKey(edge).onSuccess((key) => succeed({ edge, key }))));\n}\n\n/** Add an edge to the adjacency map, de-duplicated by its canonical key. */\nfunction addEdge(adjacency: Map<string, Set<string>>, seenKeys: Set<string>, keyed: IKeyedEdge): void {\n if (seenKeys.has(keyed.key)) {\n return;\n }\n seenKeys.add(keyed.key);\n const source: string = keyed.edge.source;\n const target: string = keyed.edge.target;\n const targets: Set<string> | undefined = adjacency.get(source);\n if (targets === undefined) {\n adjacency.set(source, new Set<string>([target]));\n } else {\n targets.add(target);\n }\n}\n\n/** DFS reachability: is `to` reachable from `from` over `adjacency`? */\nfunction reaches(adjacency: ReadonlyMap<string, Set<string>>, from: string, to: string): boolean {\n const visited: Set<string> = new Set<string>();\n const stack: string[] = [from];\n while (stack.length > 0) {\n const node: string = stack.pop() as string;\n if (node === to) {\n return true;\n }\n if (visited.has(node)) {\n continue;\n }\n visited.add(node);\n const neighbors: Set<string> | undefined = adjacency.get(node);\n if (neighbors !== undefined) {\n for (const neighbor of neighbors) {\n if (!visited.has(neighbor)) {\n stack.push(neighbor);\n }\n }\n }\n }\n return false;\n}\n"]}
1
+ {"version":3,"file":"cycleGuard.js","sourceRoot":"","sources":["../../../src/packlets/ingest/cycleGuard.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Hash, Result, fail, mapResults, succeed } from '@fgv/ts-utils';\nimport { IEdgeTarget, LinkType, edgeTargetKey } from '../types';\n\n/**\n * A directed edge in the link graph the cycle guard reasons over: scope-qualified\n * `source` links to scope-qualified `target` under relation `type`.\n * @public\n */\nexport interface ICycleGuardEdge {\n readonly source: IEdgeTarget;\n readonly target: IEdgeTarget;\n readonly type: LinkType;\n}\n\n/** Human-readable `scope/id` rendering of a scoped node, for cycle-guard diagnostics. */\nfunction formatNode(node: IEdgeTarget): string {\n return `${node.scope}/${node.id}`;\n}\n\n/**\n * Build the design's `buildCycleKey` — a deterministic, canonical (RFC-8785,\n * via `Crc32Normalizer` from `@fgv/ts-utils`) identity for a directed edge. Used\n * to de-duplicate proposed edges so a repeated `(source, target, type)` proposal\n * contributes a single graph edge (and never spuriously \"re-closes\" a cycle).\n * @public\n */\nexport function buildCycleKey(edge: ICycleGuardEdge): Result<string> {\n return new Hash.Crc32Normalizer().computeHash({\n source: edge.source,\n target: edge.target,\n type: edge.type\n });\n}\n\n/**\n * Write-time cycle guard. Given the graph's EXISTING directed edges plus a set of\n * PROPOSED edges, verify that adding the proposed edges keeps the union graph a\n * DAG. Fails loudly, naming the first proposed edge that would close a directed\n * cycle.\n *\n * @remarks\n * Distinct from the read-time BFS in `LinkTraversalRetriever` (which tolerates\n * cycles via a visited-set): this is a WRITE-time admission check. Cycle\n * detection is DFS reachability — a proposed `source -> target` edge closes a\n * cycle iff `target` already reaches `source` in the graph built so far. Proposed\n * edges are folded in one at a time (in order), so an intra-batch cycle\n * (`A->B`, `B->A` both proposed) is caught as well as a batch-vs-existing cycle.\n *\n * See the design note §4: this enforces GLOBAL directed-acyclicity over all link\n * types (conservative — a mutual associative pair is rejected). Callers that need\n * mutual links disable the guard.\n * @public\n */\nexport function assertNoCycles(\n existing: ReadonlyArray<ICycleGuardEdge>,\n proposed: ReadonlyArray<ICycleGuardEdge>\n): Result<true> {\n // Canonically key every edge up front (the only fallible step); the graph\n // building + cycle detection below is then pure.\n return keyEdges(existing).onSuccess((existingKeyed) =>\n keyEdges(proposed).onSuccess((proposedKeyed) => {\n // Adjacency map, de-duplicated by canonical edge key so a repeated edge is\n // one arc. Seed with the existing edges, then fold in each proposed edge,\n // checking reachability BEFORE inserting it.\n const adjacency: Map<string, Set<string>> = new Map<string, Set<string>>();\n const seenKeys: Set<string> = new Set<string>();\n for (const keyed of existingKeyed) {\n addEdge(adjacency, seenKeys, keyed);\n }\n for (const keyed of proposedKeyed) {\n const edge: ICycleGuardEdge = keyed.edge;\n const sourceKey: string = edgeTargetKey(edge.source);\n const targetKey: string = edgeTargetKey(edge.target);\n // A self-loop is the degenerate one-node cycle.\n if (sourceKey === targetKey) {\n return fail(\n `ingest cycle guard: edge '${formatNode(edge.source)}' -${edge.type}-> '${formatNode(\n edge.target\n )}' is a self-loop`\n );\n }\n // Reachability: does `target` already reach `source`? If so, adding\n // `source -> target` closes a directed cycle.\n if (reaches(adjacency, targetKey, sourceKey)) {\n return fail(\n `ingest cycle guard: edge '${formatNode(edge.source)}' -${edge.type}-> '${formatNode(\n edge.target\n )}' would create a cycle`\n );\n }\n addEdge(adjacency, seenKeys, keyed);\n }\n return succeed(true);\n })\n );\n}\n\n/** An edge paired with its canonical cycle key. */\ninterface IKeyedEdge {\n readonly edge: ICycleGuardEdge;\n readonly key: string;\n}\n\n/** Canonically key a set of edges (the guard's only fallible step). */\nfunction keyEdges(edges: ReadonlyArray<ICycleGuardEdge>): Result<ReadonlyArray<IKeyedEdge>> {\n return mapResults(edges.map((edge) => buildCycleKey(edge).onSuccess((key) => succeed({ edge, key }))));\n}\n\n/** Add an edge to the adjacency map, de-duplicated by its canonical key. */\nfunction addEdge(adjacency: Map<string, Set<string>>, seenKeys: Set<string>, keyed: IKeyedEdge): void {\n if (seenKeys.has(keyed.key)) {\n return;\n }\n seenKeys.add(keyed.key);\n const source: string = edgeTargetKey(keyed.edge.source);\n const target: string = edgeTargetKey(keyed.edge.target);\n const targets: Set<string> | undefined = adjacency.get(source);\n if (targets === undefined) {\n adjacency.set(source, new Set<string>([target]));\n } else {\n targets.add(target);\n }\n}\n\n/** DFS reachability: is `to` reachable from `from` over `adjacency`? */\nfunction reaches(adjacency: ReadonlyMap<string, Set<string>>, from: string, to: string): boolean {\n const visited: Set<string> = new Set<string>();\n const stack: string[] = [from];\n while (stack.length > 0) {\n const node: string = stack.pop() as string;\n if (node === to) {\n return true;\n }\n if (visited.has(node)) {\n continue;\n }\n visited.add(node);\n const neighbors: Set<string> | undefined = adjacency.get(node);\n if (neighbors !== undefined) {\n for (const neighbor of neighbors) {\n if (!visited.has(neighbor)) {\n stack.push(neighbor);\n }\n }\n }\n }\n return false;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"hostStages.js","sourceRoot":"","sources":["../../../src/packlets/ingest/hostStages.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 { MemoryId } from '../types';\nimport {\n ICandidateEdge,\n ICandidateRecord,\n IEntityResolutionCandidate,\n IIngestItem,\n IMemoryClassification,\n ResolutionVerdict\n} from './model';\n\n/**\n * Stage 2 — the host's classifier. Decides what {@link IIngestItem} maps to\n * which memory {@link Kind} (and optional tags / confidence). LOCKED as a\n * separate staged interface (OQ-10): the host plugs its existing classifier\n * machinery in here rather than surrendering to an opaque ingestor.\n * @public\n */\nexport interface IMemoryClassifier {\n /**\n * Classify one item. A `fail` aborts the item's ingest loudly (fgv never\n * guesses a kind).\n */\n classify(item: IIngestItem): Promise<Result<IMemoryClassification>>;\n}\n\n/**\n * Stage 3 — the host's fact extractor. Turns a classified item into zero or more\n * {@link ICandidateRecord}s. Each candidate's body is validated against the\n * kind's registered Converter by fgv before it can reach the store (the typed\n * validation boundary — no unchecked host data is persisted).\n * @public\n */\nexport interface IFactExtractor {\n /**\n * Extract candidate records from a classified item. An empty array is a valid\n * result (the item yielded nothing memorable).\n */\n extract(\n item: IIngestItem,\n classification: IMemoryClassification\n ): Promise<Result<ReadonlyArray<ICandidateRecord>>>;\n}\n\n/**\n * Stage 4 (optional) — the host's entity resolver (OQ-13, LOCKED OPTIONAL). When\n * supplied, fgv surfaces near-duplicate {@link IEntityResolutionCandidate}s (from\n * layer-2 similarity search) and the resolver returns a\n * {@link ResolutionVerdict}. When ABSENT, stage-4 dedup falls back to\n * exact-`{ kind, body }`-hash only — the deterministic-identity host path.\n * @public\n */\nexport interface IEntityResolver {\n /**\n * Decide whether `candidate` is new, a duplicate of / supersedes / merges into\n * one of the surfaced `similar` records. `similar` is non-empty and ordered by\n * descending score when the resolver is invoked (fgv only calls it when\n * layer-2 surfaces at least one over-threshold neighbor).\n */\n resolve(\n candidate: ICandidateRecord,\n similar: ReadonlyArray<IEntityResolutionCandidate>\n ): Promise<Result<ResolutionVerdict>>;\n}\n\n/**\n * The context fgv hands the host's relation extractor (stage 5): the source item\n * plus the candidates fgv is about to write, each paired with its resolved\n * reference id (the codec `idStem`). The extractor proposes attributed edges over\n * these ids and existing store records.\n * @public\n */\nexport interface IRelationContext {\n /** The item being ingested. */\n readonly item: IIngestItem;\n /** The candidates fgv is about to write, each with its resolved reference id. */\n readonly candidates: ReadonlyArray<IRelationCandidate>;\n}\n\n/**\n * A candidate paired with its resolved reference id, handed to the relation\n * extractor so it can source edges from it.\n * @public\n */\nexport interface IRelationCandidate {\n /** The candidate about to be written. */\n readonly candidate: ICandidateRecord;\n /** Its resolved reference id (codec `idStem` — the stable entity reference). */\n readonly id: MemoryId;\n}\n\n/**\n * Stage 5 — the host's relation extractor. Proposes attributed edges among the\n * candidates and existing records. fgv owns the validation, the write-time cycle\n * guard, and the actual persistence of the edges (the host brings only the\n * relationship judgment).\n * @public\n */\nexport interface IRelationExtractor {\n /**\n * Propose the edges to attach for this ingest. An empty array is valid (no\n * relations). Every proposed {@link ICandidateEdge.source | source} must be one\n * of the context's candidate reference ids.\n */\n relate(context: IRelationContext): Promise<Result<ReadonlyArray<ICandidateEdge>>>;\n}\n"]}
1
+ {"version":3,"file":"hostStages.js","sourceRoot":"","sources":["../../../src/packlets/ingest/hostStages.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 { IEdgeTarget } from '../types';\nimport {\n ICandidateEdge,\n ICandidateRecord,\n IEntityResolutionCandidate,\n IIngestItem,\n IMemoryClassification,\n ResolutionVerdict\n} from './model';\n\n/**\n * Stage 2 — the host's classifier. Decides what {@link IIngestItem} maps to\n * which memory {@link Kind} (and optional tags / confidence). LOCKED as a\n * separate staged interface (OQ-10): the host plugs its existing classifier\n * machinery in here rather than surrendering to an opaque ingestor.\n * @public\n */\nexport interface IMemoryClassifier {\n /**\n * Classify one item. A `fail` aborts the item's ingest loudly (fgv never\n * guesses a kind).\n */\n classify(item: IIngestItem): Promise<Result<IMemoryClassification>>;\n}\n\n/**\n * Stage 3 — the host's fact extractor. Turns a classified item into zero or more\n * {@link ICandidateRecord}s. Each candidate's body is validated against the\n * kind's registered Converter by fgv before it can reach the store (the typed\n * validation boundary — no unchecked host data is persisted).\n * @public\n */\nexport interface IFactExtractor {\n /**\n * Extract candidate records from a classified item. An empty array is a valid\n * result (the item yielded nothing memorable).\n */\n extract(\n item: IIngestItem,\n classification: IMemoryClassification\n ): Promise<Result<ReadonlyArray<ICandidateRecord>>>;\n}\n\n/**\n * Stage 4 (optional) — the host's entity resolver (OQ-13, LOCKED OPTIONAL). When\n * supplied, fgv surfaces near-duplicate {@link IEntityResolutionCandidate}s (from\n * layer-2 similarity search) and the resolver returns a\n * {@link ResolutionVerdict}. When ABSENT, stage-4 dedup falls back to\n * exact-`{ kind, body }`-hash only — the deterministic-identity host path.\n * @public\n */\nexport interface IEntityResolver {\n /**\n * Decide whether `candidate` is new, a duplicate of / supersedes / merges into\n * one of the surfaced `similar` records. `similar` is non-empty and ordered by\n * descending score when the resolver is invoked (fgv only calls it when\n * layer-2 surfaces at least one over-threshold neighbor).\n */\n resolve(\n candidate: ICandidateRecord,\n similar: ReadonlyArray<IEntityResolutionCandidate>\n ): Promise<Result<ResolutionVerdict>>;\n}\n\n/**\n * The context fgv hands the host's relation extractor (stage 5): the source item\n * plus the candidates fgv is about to write, each paired with its resolved\n * reference id (the codec `idStem`). The extractor proposes attributed edges over\n * these ids and existing store records.\n * @public\n */\nexport interface IRelationContext {\n /** The item being ingested. */\n readonly item: IIngestItem;\n /** The candidates fgv is about to write, each with its resolved reference id. */\n readonly candidates: ReadonlyArray<IRelationCandidate>;\n}\n\n/**\n * A candidate paired with its resolved reference id, handed to the relation\n * extractor so it can source edges from it.\n * @public\n */\nexport interface IRelationCandidate {\n /** The candidate about to be written. */\n readonly candidate: ICandidateRecord;\n /** Its resolved scope-qualified reference (codec `(scope, idStem)` — the stable entity address). */\n readonly id: IEdgeTarget;\n}\n\n/**\n * Stage 5 — the host's relation extractor. Proposes attributed edges among the\n * candidates and existing records. fgv owns the validation, the write-time cycle\n * guard, and the actual persistence of the edges (the host brings only the\n * relationship judgment).\n * @public\n */\nexport interface IRelationExtractor {\n /**\n * Propose the edges to attach for this ingest. An empty array is valid (no\n * relations). Every proposed {@link ICandidateEdge.source | source} must be one\n * of the context's candidate reference ids.\n */\n relate(context: IRelationContext): Promise<Result<ReadonlyArray<ICandidateEdge>>>;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"model.js","sourceRoot":"","sources":["../../../src/packlets/ingest/model.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { IEdge, IMemoryEnvelope, IMemoryRecord, Kind, MemoryId, Tag } from '../types';\n\n/**\n * A single unit of raw source material handed to the ingest pipeline. The host\n * owns the shape of {@link IIngestItem.content | content} — fgv never interprets\n * it; it flows opaquely into the host's classifier and extractor (stages 2-3).\n *\n * Single-item ingest is FIRST-CLASS: the orchestrator's primary entry point\n * takes one `IIngestItem` (per-turn streaming), and the batch entry point is a\n * convenience loop over it.\n * @public\n */\nexport interface IIngestItem {\n /**\n * Host-owned identity for this source item. Opaque to fgv; used only in\n * diagnostics and echoed back on the {@link IIngestItemResult}.\n */\n readonly id: string;\n /**\n * The opaque source payload the host's classifier / extractor understand\n * (e.g. a raw turn, a document, a tool-call transcript). Never interpreted by\n * fgv.\n */\n readonly content: unknown;\n /**\n * Optional back-link to the memory record this item was derived from (e.g. the\n * MTM turn an extracted fact came from). When present, fgv stamps it as\n * {@link IProvenance.derivedFrom | provenance.derivedFrom} on every record\n * ingested from this item (stage 6) — the cross-kind provenance spine.\n */\n readonly sourceId?: MemoryId;\n /** Optional opaque metadata carried alongside the item; never interpreted by fgv. */\n readonly metadata?: Record<string, unknown>;\n}\n\n/**\n * The host classifier's verdict for an {@link IIngestItem} (stage 2). Guides the\n * host's own extractor (stage 3) and supplies the default `kind` / `tags` /\n * `confidence` fgv stamps when the extractor does not override them.\n *\n * Extensible: the `[key: string]: unknown` arm lets the host attach an opaque\n * classification payload without changing this interface.\n * @public\n */\nexport interface IMemoryClassification {\n /** The record kind the item classifies as. */\n readonly kind: Kind;\n /** Optional tags the classifier assigns. */\n readonly tags?: ReadonlyArray<Tag>;\n /** Optional classifier confidence in `[0, 1]`; flows to `provenance.confidence`. */\n readonly confidence?: number;\n /** Opaque, host-owned extension payload — never interpreted by fgv. */\n readonly [key: string]: unknown;\n}\n\n/**\n * The store-owned envelope fields fgv derives or stamps: `id` (from the codec),\n * and the transaction-time metadata (`seq` / `contentHash` / `created` /\n * `updated`). A {@link ICandidateRecord} supplies everything EXCEPT these.\n * @public\n */\nexport type StoreStampedEnvelopeField = 'id' | 'seq' | 'contentHash' | 'created' | 'updated';\n\n/**\n * A host-extracted candidate record (stage 3 output). The host supplies the\n * full envelope MINUS the {@link StoreStampedEnvelopeField | store-stamped\n * fields} (fgv derives `id` from the codec; the store stamps the rest) plus the\n * typed body. Every candidate body is validated against the kind's registered\n * Converter before it can reach the store (the typed validation boundary).\n * @public\n */\nexport interface ICandidateRecord {\n /** The host-supplied envelope, minus the store-stamped fields. */\n readonly envelope: Omit<IMemoryEnvelope, StoreStampedEnvelopeField>;\n /** The per-kind body (a markdown string in v1), validated on ingest. */\n readonly body: unknown;\n}\n\n/**\n * A near-duplicate candidate surfaced to the {@link IEntityResolver} by stage-4\n * layer-2 similarity search: an existing record whose embedding is within the\n * similarity threshold of the incoming candidate.\n * @public\n */\nexport interface IEntityResolutionCandidate {\n /** The existing record's id. */\n readonly id: MemoryId;\n /** The existing record. */\n readonly record: IMemoryRecord<unknown>;\n /** The backend similarity score (higher = more similar). */\n readonly score: number;\n}\n\n/**\n * The four dedup verdicts a {@link IEntityResolver} (or fgv's exact-match layer)\n * returns for a candidate. See the design note §3 for the verdict → write\n * disposition mapping.\n * @public\n */\nexport type ResolutionVerdict =\n | { readonly verdict: 'new' }\n | { readonly verdict: 'duplicate-of'; readonly target: MemoryId }\n | { readonly verdict: 'supersede'; readonly target: MemoryId }\n | { readonly verdict: 'merge-into'; readonly target: MemoryId };\n\n/**\n * How a candidate was ultimately written (or not) after resolution.\n *\n * - `written` — persisted as a fresh record (verdict `new`), or as a superseding\n * record (verdict `supersede`).\n * - `deduped` — not written; an existing record satisfied it (verdict\n * `duplicate-of`, incl. every layer-1 exact `{ kind, body }` match).\n * - `merged` — merged into an existing target entity (verdict `merge-into`).\n * @public\n */\nexport type IngestDisposition = 'written' | 'deduped' | 'merged';\n\n/**\n * A stage-5 attributed edge proposal: the {@link ICandidateEdge.edge | edge} to\n * add, sourced from {@link ICandidateEdge.source | source}. The source MUST be a\n * candidate being written in this ingest (edges land on the source record's\n * `envelope.links`); the edge's `target` must resolve to a sibling candidate or\n * an existing store record.\n * @public\n */\nexport interface ICandidateEdge {\n /** The reference id (codec `idStem`) of the candidate the edge originates from. */\n readonly source: MemoryId;\n /** The attributed edge (type / target / optional confidence / provenance). */\n readonly edge: IEdge;\n}\n\n/**\n * Per-candidate outcome of an ingest run.\n * @public\n */\nexport interface IIngestedRecordResult {\n /** The candidate the outcome is for. */\n readonly candidate: ICandidateRecord;\n /** The resolution verdict fgv reached (or the resolver returned). */\n readonly resolution: ResolutionVerdict;\n /** What the write ultimately did. */\n readonly disposition: IngestDisposition;\n /**\n * The stored record's id: the newly-written id (`written` / `merged`), or the\n * existing target's id (`deduped`).\n */\n readonly id: MemoryId;\n /** The persisted record, when a write happened (`written` / `merged`). */\n readonly record?: IMemoryRecord<unknown>;\n /** The stage-5 edges attached to this candidate before the write. */\n readonly edges: ReadonlyArray<ICandidateEdge>;\n /**\n * Informational diagnostic, set to `'temporal-versioned'` when the\n * contradicts→temporal interlock is recognized: a `contradicts` edge was\n * attached to a candidate of a temporal kind.\n *\n * @remarks\n * A temporal kind ALWAYS writes through the store's versioned put path (that is\n * the codec's `isVersioned` behavior — the prior version is invalidated and a\n * new version written on every write, contradicts edge or not). This flag does\n * NOT cause that routing; it is a diagnostic marker that the contradicts-driven\n * scenario occurred, so callers can distinguish a contradiction-superseding\n * version from an ordinary revision.\n */\n readonly interlock?: 'temporal-versioned';\n}\n\n/**\n * The result of ingesting a single {@link IIngestItem}: the item plus the\n * per-candidate outcomes (one item can yield zero or many candidate records).\n * @public\n */\nexport interface IIngestItemResult {\n /** The item that was ingested. */\n readonly item: IIngestItem;\n /** The per-candidate outcomes, in extraction order. */\n readonly records: ReadonlyArray<IIngestedRecordResult>;\n}\n"]}
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../../../src/packlets/ingest/model.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { IEdge, IEdgeTarget, IMemoryEnvelope, IMemoryRecord, Kind, MemoryId, Tag } from '../types';\n\n/**\n * A single unit of raw source material handed to the ingest pipeline. The host\n * owns the shape of {@link IIngestItem.content | content} — fgv never interprets\n * it; it flows opaquely into the host's classifier and extractor (stages 2-3).\n *\n * Single-item ingest is FIRST-CLASS: the orchestrator's primary entry point\n * takes one `IIngestItem` (per-turn streaming), and the batch entry point is a\n * convenience loop over it.\n * @public\n */\nexport interface IIngestItem {\n /**\n * Host-owned identity for this source item. Opaque to fgv; used only in\n * diagnostics and echoed back on the {@link IIngestItemResult}.\n */\n readonly id: string;\n /**\n * The opaque source payload the host's classifier / extractor understand\n * (e.g. a raw turn, a document, a tool-call transcript). Never interpreted by\n * fgv.\n */\n readonly content: unknown;\n /**\n * Optional scope-qualified back-link to the memory record this item was derived\n * from (e.g. the MTM turn an extracted fact came from). When present, fgv stamps\n * it as {@link IProvenance.derivedFrom | provenance.derivedFrom} on every record\n * ingested from this item (stage 6) — the cross-kind provenance spine. A\n * scope-qualified {@link IEdgeTarget} (not a bare {@link MemoryId}) because\n * per-scope codecs legally reuse a stem across scopes, so a bare id would be\n * ambiguous.\n */\n readonly sourceId?: IEdgeTarget;\n /** Optional opaque metadata carried alongside the item; never interpreted by fgv. */\n readonly metadata?: Record<string, unknown>;\n}\n\n/**\n * The host classifier's verdict for an {@link IIngestItem} (stage 2). Guides the\n * host's own extractor (stage 3) and supplies the default `kind` / `tags` /\n * `confidence` fgv stamps when the extractor does not override them.\n *\n * Extensible: the `[key: string]: unknown` arm lets the host attach an opaque\n * classification payload without changing this interface.\n * @public\n */\nexport interface IMemoryClassification {\n /** The record kind the item classifies as. */\n readonly kind: Kind;\n /** Optional tags the classifier assigns. */\n readonly tags?: ReadonlyArray<Tag>;\n /** Optional classifier confidence in `[0, 1]`; flows to `provenance.confidence`. */\n readonly confidence?: number;\n /** Opaque, host-owned extension payload — never interpreted by fgv. */\n readonly [key: string]: unknown;\n}\n\n/**\n * The store-owned envelope fields fgv derives or stamps: `id` (from the codec),\n * and the transaction-time metadata (`seq` / `contentHash` / `created` /\n * `updated`). A {@link ICandidateRecord} supplies everything EXCEPT these.\n * @public\n */\nexport type StoreStampedEnvelopeField = 'id' | 'seq' | 'contentHash' | 'created' | 'updated';\n\n/**\n * A host-extracted candidate record (stage 3 output). The host supplies the\n * full envelope MINUS the {@link StoreStampedEnvelopeField | store-stamped\n * fields} (fgv derives `id` from the codec; the store stamps the rest) plus the\n * typed body. Every candidate body is validated against the kind's registered\n * Converter before it can reach the store (the typed validation boundary).\n * @public\n */\nexport interface ICandidateRecord {\n /** The host-supplied envelope, minus the store-stamped fields. */\n readonly envelope: Omit<IMemoryEnvelope, StoreStampedEnvelopeField>;\n /** The per-kind body (a markdown string in v1), validated on ingest. */\n readonly body: unknown;\n}\n\n/**\n * A near-duplicate candidate surfaced to the {@link IEntityResolver} by stage-4\n * layer-2 similarity search: an existing record whose embedding is within the\n * similarity threshold of the incoming candidate.\n * @public\n */\nexport interface IEntityResolutionCandidate {\n /**\n * The existing record's scope-qualified `(scope, id)` address. Scope-qualified\n * (not a bare {@link MemoryId}) because per-scope codecs legally mint the same\n * stem under different scopes — the {@link ResolutionVerdict} target the resolver\n * returns must round-trip this exact address so the write binds the intended\n * record.\n */\n readonly target: IEdgeTarget;\n /** The existing record. */\n readonly record: IMemoryRecord<unknown>;\n /** The backend similarity score (higher = more similar). */\n readonly score: number;\n}\n\n/**\n * The four dedup verdicts a {@link IEntityResolver} (or fgv's exact-match layer)\n * returns for a candidate. See the design note §3 for the verdict → write\n * disposition mapping. Each target-bearing arm carries a scope-qualified\n * {@link IEdgeTarget} (not a bare {@link MemoryId}) so the verdict resolves to a\n * single record even when a filename stem is reused across scopes.\n * @public\n */\nexport type ResolutionVerdict =\n | { readonly verdict: 'new' }\n | { readonly verdict: 'duplicate-of'; readonly target: IEdgeTarget }\n | { readonly verdict: 'supersede'; readonly target: IEdgeTarget }\n | { readonly verdict: 'merge-into'; readonly target: IEdgeTarget };\n\n/**\n * How a candidate was ultimately written (or not) after resolution.\n *\n * - `written` — persisted as a fresh record (verdict `new`), or as a superseding\n * record (verdict `supersede`).\n * - `deduped` — not written; an existing record satisfied it (verdict\n * `duplicate-of`, incl. every layer-1 exact `{ kind, body }` match).\n * - `merged` — merged into an existing target entity (verdict `merge-into`).\n * @public\n */\nexport type IngestDisposition = 'written' | 'deduped' | 'merged';\n\n/**\n * A stage-5 attributed edge proposal: the {@link ICandidateEdge.edge | edge} to\n * add, sourced from {@link ICandidateEdge.source | source}. The source MUST be a\n * candidate being written in this ingest (edges land on the source record's\n * `envelope.links`); the edge's `target` must resolve to a sibling candidate or\n * an existing store record.\n * @public\n */\nexport interface ICandidateEdge {\n /** The scope-qualified reference (codec `(scope, idStem)`) of the candidate the edge originates from. */\n readonly source: IEdgeTarget;\n /** The attributed edge (type / target / optional confidence / provenance). */\n readonly edge: IEdge;\n}\n\n/**\n * Per-candidate outcome of an ingest run.\n * @public\n */\nexport interface IIngestedRecordResult {\n /** The candidate the outcome is for. */\n readonly candidate: ICandidateRecord;\n /** The resolution verdict fgv reached (or the resolver returned). */\n readonly resolution: ResolutionVerdict;\n /** What the write ultimately did. */\n readonly disposition: IngestDisposition;\n /**\n * The stored record's id: the newly-written id (`written` / `merged`), or the\n * existing target's id (`deduped`).\n */\n readonly id: MemoryId;\n /** The persisted record, when a write happened (`written` / `merged`). */\n readonly record?: IMemoryRecord<unknown>;\n /** The stage-5 edges attached to this candidate before the write. */\n readonly edges: ReadonlyArray<ICandidateEdge>;\n /**\n * Informational diagnostic, set to `'temporal-versioned'` when the\n * contradicts→temporal interlock is recognized: a `contradicts` edge was\n * attached to a candidate of a temporal kind.\n *\n * @remarks\n * A temporal kind ALWAYS writes through the store's versioned put path (that is\n * the codec's `isVersioned` behavior — the prior version is invalidated and a\n * new version written on every write, contradicts edge or not). This flag does\n * NOT cause that routing; it is a diagnostic marker that the contradicts-driven\n * scenario occurred, so callers can distinguish a contradiction-superseding\n * version from an ordinary revision.\n */\n readonly interlock?: 'temporal-versioned';\n}\n\n/**\n * The result of ingesting a single {@link IIngestItem}: the item plus the\n * per-candidate outcomes (one item can yield zero or many candidate records).\n * @public\n */\nexport interface IIngestItemResult {\n /** The item that was ingested. */\n readonly item: IIngestItem;\n /** The per-candidate outcomes, in extraction order. */\n readonly records: ReadonlyArray<IIngestedRecordResult>;\n}\n"]}