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

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 (197) hide show
  1. package/.rush/temp/{6dfc331ddf74dc2db0d4483d0c96ea971f351c83.tar.log → b82cf6bdece20481260e6bab946179eeec9d7b46.tar.log} +116 -2
  2. package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +4 -4
  3. package/.rush/temp/operation/build/all.log +4 -4
  4. package/.rush/temp/operation/build/log-chunks.jsonl +4 -4
  5. package/.rush/temp/operation/build/state.json +1 -1
  6. package/.rush/temp/shrinkwrap-deps.json +222 -221
  7. package/config/jest.config.json +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/packlets/ingest/cycleGuard.js +111 -0
  11. package/dist/packlets/ingest/cycleGuard.js.map +1 -0
  12. package/dist/packlets/ingest/hostStages.js +6 -0
  13. package/dist/packlets/ingest/hostStages.js.map +1 -0
  14. package/dist/packlets/ingest/index.js +9 -0
  15. package/dist/packlets/ingest/index.js.map +1 -0
  16. package/dist/packlets/ingest/model.js +6 -0
  17. package/dist/packlets/ingest/model.js.map +1 -0
  18. package/dist/packlets/ingest/orchestrator.js +438 -0
  19. package/dist/packlets/ingest/orchestrator.js.map +1 -0
  20. package/dist/packlets/retrieve/index.js +1 -0
  21. package/dist/packlets/retrieve/index.js.map +1 -1
  22. package/dist/packlets/retrieve/temporalRetrievers.js +172 -0
  23. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -0
  24. package/dist/packlets/store/fileTreeMemoryStore.js +323 -41
  25. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  26. package/dist/packlets/tools/index.js +6 -0
  27. package/dist/packlets/tools/index.js.map +1 -0
  28. package/dist/packlets/tools/memoryTools.js +337 -0
  29. package/dist/packlets/tools/memoryTools.js.map +1 -0
  30. package/dist/packlets/types/identityCodec.js +115 -0
  31. package/dist/packlets/types/identityCodec.js.map +1 -1
  32. package/dist/packlets/types/index.js +1 -0
  33. package/dist/packlets/types/index.js.map +1 -1
  34. package/dist/packlets/types/temporal.js +85 -0
  35. package/dist/packlets/types/temporal.js.map +1 -0
  36. package/dist/packlets/types/writePolicy.js +96 -0
  37. package/dist/packlets/types/writePolicy.js.map +1 -1
  38. package/dist/test/unit/converters/antagonistRoundTrip.test.js +95 -0
  39. package/dist/test/unit/converters/antagonistRoundTrip.test.js.map +1 -0
  40. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js +273 -0
  41. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -0
  42. package/dist/test/unit/ingest/cycleGuard.test.js +54 -0
  43. package/dist/test/unit/ingest/cycleGuard.test.js.map +1 -0
  44. package/dist/test/unit/ingest/orchestrator.test.js +913 -0
  45. package/dist/test/unit/ingest/orchestrator.test.js.map +1 -0
  46. package/dist/test/unit/retrieve/temporalRetrievers.test.js +182 -0
  47. package/dist/test/unit/retrieve/temporalRetrievers.test.js.map +1 -0
  48. package/dist/test/unit/store/antagonistTemporalBoundary.test.js +120 -0
  49. package/dist/test/unit/store/antagonistTemporalBoundary.test.js.map +1 -0
  50. package/dist/test/unit/store/fileTreeMemoryStore.test.js +74 -7
  51. package/dist/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
  52. package/dist/test/unit/store/temporalStore.test.js +398 -0
  53. package/dist/test/unit/store/temporalStore.test.js.map +1 -0
  54. package/dist/test/unit/tools/memoryTools.test.js +572 -0
  55. package/dist/test/unit/tools/memoryTools.test.js.map +1 -0
  56. package/dist/test/unit/types/temporalCodec.test.js +203 -0
  57. package/dist/test/unit/types/temporalCodec.test.js.map +1 -0
  58. package/dist/test/unit/types/temporalPolicy.test.js +62 -0
  59. package/dist/test/unit/types/temporalPolicy.test.js.map +1 -0
  60. package/dist/ts-agent-memory.d.ts +1042 -10
  61. package/dist/tsdoc-metadata.json +1 -1
  62. package/etc/ts-agent-memory.api.md +284 -0
  63. package/lib/index.d.ts +2 -0
  64. package/lib/index.d.ts.map +1 -1
  65. package/lib/index.js +2 -0
  66. package/lib/index.js.map +1 -1
  67. package/lib/packlets/ingest/cycleGuard.d.ts +41 -0
  68. package/lib/packlets/ingest/cycleGuard.d.ts.map +1 -0
  69. package/lib/packlets/ingest/cycleGuard.js +115 -0
  70. package/lib/packlets/ingest/cycleGuard.js.map +1 -0
  71. package/lib/packlets/ingest/hostStages.d.ts +88 -0
  72. package/lib/packlets/ingest/hostStages.d.ts.map +1 -0
  73. package/lib/packlets/ingest/hostStages.js +7 -0
  74. package/lib/packlets/ingest/hostStages.js.map +1 -0
  75. package/lib/packlets/ingest/index.d.ts +5 -0
  76. package/lib/packlets/ingest/index.d.ts.map +1 -0
  77. package/lib/packlets/ingest/index.js +25 -0
  78. package/lib/packlets/ingest/index.js.map +1 -0
  79. package/lib/packlets/ingest/model.d.ts +177 -0
  80. package/lib/packlets/ingest/model.d.ts.map +1 -0
  81. package/lib/packlets/ingest/model.js +7 -0
  82. package/lib/packlets/ingest/model.js.map +1 -0
  83. package/lib/packlets/ingest/orchestrator.d.ts +206 -0
  84. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -0
  85. package/lib/packlets/ingest/orchestrator.js +442 -0
  86. package/lib/packlets/ingest/orchestrator.js.map +1 -0
  87. package/lib/packlets/retrieve/index.d.ts +1 -0
  88. package/lib/packlets/retrieve/index.d.ts.map +1 -1
  89. package/lib/packlets/retrieve/index.js +1 -0
  90. package/lib/packlets/retrieve/index.js.map +1 -1
  91. package/lib/packlets/retrieve/temporalRetrievers.d.ts +78 -0
  92. package/lib/packlets/retrieve/temporalRetrievers.d.ts.map +1 -0
  93. package/lib/packlets/retrieve/temporalRetrievers.js +178 -0
  94. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -0
  95. package/lib/packlets/store/fileTreeMemoryStore.d.ts +118 -9
  96. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  97. package/lib/packlets/store/fileTreeMemoryStore.js +322 -40
  98. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  99. package/lib/packlets/tools/index.d.ts +2 -0
  100. package/lib/packlets/tools/index.d.ts.map +1 -0
  101. package/lib/packlets/tools/index.js +22 -0
  102. package/lib/packlets/tools/index.js.map +1 -0
  103. package/lib/packlets/tools/memoryTools.d.ts +139 -0
  104. package/lib/packlets/tools/memoryTools.d.ts.map +1 -0
  105. package/lib/packlets/tools/memoryTools.js +341 -0
  106. package/lib/packlets/tools/memoryTools.js.map +1 -0
  107. package/lib/packlets/types/identityCodec.d.ts +86 -0
  108. package/lib/packlets/types/identityCodec.d.ts.map +1 -1
  109. package/lib/packlets/types/identityCodec.js +118 -1
  110. package/lib/packlets/types/identityCodec.js.map +1 -1
  111. package/lib/packlets/types/index.d.ts +1 -0
  112. package/lib/packlets/types/index.d.ts.map +1 -1
  113. package/lib/packlets/types/index.js +1 -0
  114. package/lib/packlets/types/index.js.map +1 -1
  115. package/lib/packlets/types/temporal.d.ts +40 -0
  116. package/lib/packlets/types/temporal.d.ts.map +1 -0
  117. package/lib/packlets/types/temporal.js +92 -0
  118. package/lib/packlets/types/temporal.js.map +1 -0
  119. package/lib/packlets/types/writePolicy.d.ts +49 -0
  120. package/lib/packlets/types/writePolicy.d.ts.map +1 -1
  121. package/lib/packlets/types/writePolicy.js +98 -1
  122. package/lib/packlets/types/writePolicy.js.map +1 -1
  123. package/lib/test/unit/converters/antagonistRoundTrip.test.d.ts +10 -0
  124. package/lib/test/unit/converters/antagonistRoundTrip.test.d.ts.map +1 -0
  125. package/lib/test/unit/converters/antagonistRoundTrip.test.js +97 -0
  126. package/lib/test/unit/converters/antagonistRoundTrip.test.js.map +1 -0
  127. package/lib/test/unit/ingest/antagonistCycleAndParity.test.d.ts +9 -0
  128. package/lib/test/unit/ingest/antagonistCycleAndParity.test.d.ts.map +1 -0
  129. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js +275 -0
  130. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -0
  131. package/lib/test/unit/ingest/cycleGuard.test.d.ts +2 -0
  132. package/lib/test/unit/ingest/cycleGuard.test.d.ts.map +1 -0
  133. package/lib/test/unit/ingest/cycleGuard.test.js +56 -0
  134. package/lib/test/unit/ingest/cycleGuard.test.js.map +1 -0
  135. package/lib/test/unit/ingest/orchestrator.test.d.ts +2 -0
  136. package/lib/test/unit/ingest/orchestrator.test.d.ts.map +1 -0
  137. package/lib/test/unit/ingest/orchestrator.test.js +915 -0
  138. package/lib/test/unit/ingest/orchestrator.test.js.map +1 -0
  139. package/lib/test/unit/retrieve/temporalRetrievers.test.d.ts +2 -0
  140. package/lib/test/unit/retrieve/temporalRetrievers.test.d.ts.map +1 -0
  141. package/lib/test/unit/retrieve/temporalRetrievers.test.js +184 -0
  142. package/lib/test/unit/retrieve/temporalRetrievers.test.js.map +1 -0
  143. package/lib/test/unit/store/antagonistTemporalBoundary.test.d.ts +9 -0
  144. package/lib/test/unit/store/antagonistTemporalBoundary.test.d.ts.map +1 -0
  145. package/lib/test/unit/store/antagonistTemporalBoundary.test.js +122 -0
  146. package/lib/test/unit/store/antagonistTemporalBoundary.test.js.map +1 -0
  147. package/lib/test/unit/store/fileTreeMemoryStore.test.js +74 -7
  148. package/lib/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
  149. package/lib/test/unit/store/temporalStore.test.d.ts +2 -0
  150. package/lib/test/unit/store/temporalStore.test.d.ts.map +1 -0
  151. package/lib/test/unit/store/temporalStore.test.js +400 -0
  152. package/lib/test/unit/store/temporalStore.test.js.map +1 -0
  153. package/lib/test/unit/tools/memoryTools.test.d.ts +2 -0
  154. package/lib/test/unit/tools/memoryTools.test.d.ts.map +1 -0
  155. package/lib/test/unit/tools/memoryTools.test.js +574 -0
  156. package/lib/test/unit/tools/memoryTools.test.js.map +1 -0
  157. package/lib/test/unit/types/temporalCodec.test.d.ts +2 -0
  158. package/lib/test/unit/types/temporalCodec.test.d.ts.map +1 -0
  159. package/lib/test/unit/types/temporalCodec.test.js +205 -0
  160. package/lib/test/unit/types/temporalCodec.test.js.map +1 -0
  161. package/lib/test/unit/types/temporalPolicy.test.d.ts +2 -0
  162. package/lib/test/unit/types/temporalPolicy.test.d.ts.map +1 -0
  163. package/lib/test/unit/types/temporalPolicy.test.js +64 -0
  164. package/lib/test/unit/types/temporalPolicy.test.js.map +1 -0
  165. package/package.json +7 -7
  166. package/rush-logs/ts-agent-memory.build.cache.log +1 -1
  167. package/rush-logs/ts-agent-memory.build.log +4 -4
  168. package/src/index.ts +2 -0
  169. package/src/packlets/ingest/cycleGuard.ts +142 -0
  170. package/src/packlets/ingest/hostStages.ts +111 -0
  171. package/src/packlets/ingest/index.ts +9 -0
  172. package/src/packlets/ingest/model.ts +184 -0
  173. package/src/packlets/ingest/orchestrator.ts +797 -0
  174. package/src/packlets/retrieve/index.ts +1 -0
  175. package/src/packlets/retrieve/temporalRetrievers.ts +210 -0
  176. package/src/packlets/store/fileTreeMemoryStore.ts +460 -66
  177. package/src/packlets/tools/index.ts +6 -0
  178. package/src/packlets/tools/memoryTools.ts +579 -0
  179. package/src/packlets/types/identityCodec.ts +184 -0
  180. package/src/packlets/types/index.ts +1 -0
  181. package/src/packlets/types/temporal.ts +96 -0
  182. package/src/packlets/types/writePolicy.ts +127 -0
  183. package/src/test/unit/converters/antagonistRoundTrip.test.ts +110 -0
  184. package/src/test/unit/ingest/antagonistCycleAndParity.test.ts +362 -0
  185. package/src/test/unit/ingest/cycleGuard.test.ts +68 -0
  186. package/src/test/unit/ingest/orchestrator.test.ts +1158 -0
  187. package/src/test/unit/retrieve/temporalRetrievers.test.ts +226 -0
  188. package/src/test/unit/store/antagonistTemporalBoundary.test.ts +158 -0
  189. package/src/test/unit/store/fileTreeMemoryStore.test.ts +98 -7
  190. package/src/test/unit/store/temporalStore.test.ts +469 -0
  191. package/src/test/unit/tools/memoryTools.test.ts +771 -0
  192. package/src/test/unit/types/temporalCodec.test.ts +259 -0
  193. package/src/test/unit/types/temporalPolicy.test.ts +96 -0
  194. package/temp/build/lint/_eslint-5eVG3S6w.json +85 -9
  195. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  196. package/temp/ts-agent-memory.api.json +11984 -6314
  197. package/temp/ts-agent-memory.api.md +284 -0
@@ -1,5 +1,5 @@
1
1
  {
2
- "extends": "@rushstack/heft-node-rig/profiles/default/config/jest.config.json",
2
+ "extends": "@fgv/heft-dual-rig/profiles/default/config/jest.config.json",
3
3
  "coveragePathIgnorePatterns": ["index.js"],
4
4
  "coverageThreshold": {
5
5
  "global": {
package/dist/index.js CHANGED
@@ -9,4 +9,6 @@ export * from './packlets/index';
9
9
  export * from './packlets/retrieve';
10
10
  export * from './packlets/observe';
11
11
  export * from './packlets/vector';
12
+ export * from './packlets/tools';
13
+ export * from './packlets/ingest';
12
14
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './packlets/types';\nexport * from './packlets/converters';\nexport * from './packlets/store';\nexport * from './packlets/index';\nexport * from './packlets/retrieve';\nexport * from './packlets/observe';\nexport * from './packlets/vector';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './packlets/types';\nexport * from './packlets/converters';\nexport * from './packlets/store';\nexport * from './packlets/index';\nexport * from './packlets/retrieve';\nexport * from './packlets/observe';\nexport * from './packlets/vector';\nexport * from './packlets/tools';\nexport * from './packlets/ingest';\n"]}
@@ -0,0 +1,111 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ import { Hash, fail, mapResults, succeed } from '@fgv/ts-utils';
6
+ /**
7
+ * Build the design's `buildCycleKey` — a deterministic, canonical (RFC-8785,
8
+ * via `Crc32Normalizer` from `@fgv/ts-utils`) identity for a directed edge. Used
9
+ * to de-duplicate proposed edges so a repeated `(source, target, type)` proposal
10
+ * contributes a single graph edge (and never spuriously "re-closes" a cycle).
11
+ * @public
12
+ */
13
+ export function buildCycleKey(edge) {
14
+ return new Hash.Crc32Normalizer().computeHash({
15
+ source: edge.source,
16
+ target: edge.target,
17
+ type: edge.type
18
+ });
19
+ }
20
+ /**
21
+ * Write-time cycle guard. Given the graph's EXISTING directed edges plus a set of
22
+ * PROPOSED edges, verify that adding the proposed edges keeps the union graph a
23
+ * DAG. Fails loudly, naming the first proposed edge that would close a directed
24
+ * cycle.
25
+ *
26
+ * @remarks
27
+ * Distinct from the read-time BFS in `LinkTraversalRetriever` (which tolerates
28
+ * cycles via a visited-set): this is a WRITE-time admission check. Cycle
29
+ * detection is DFS reachability — a proposed `source -> target` edge closes a
30
+ * cycle iff `target` already reaches `source` in the graph built so far. Proposed
31
+ * edges are folded in one at a time (in order), so an intra-batch cycle
32
+ * (`A->B`, `B->A` both proposed) is caught as well as a batch-vs-existing cycle.
33
+ *
34
+ * See the design note §4: this enforces GLOBAL directed-acyclicity over all link
35
+ * types (conservative — a mutual associative pair is rejected). Callers that need
36
+ * mutual links disable the guard.
37
+ * @public
38
+ */
39
+ export function assertNoCycles(existing, proposed) {
40
+ // Canonically key every edge up front (the only fallible step); the graph
41
+ // building + cycle detection below is then pure.
42
+ return keyEdges(existing).onSuccess((existingKeyed) => keyEdges(proposed).onSuccess((proposedKeyed) => {
43
+ // Adjacency map, de-duplicated by canonical edge key so a repeated edge is
44
+ // one arc. Seed with the existing edges, then fold in each proposed edge,
45
+ // checking reachability BEFORE inserting it.
46
+ const adjacency = new Map();
47
+ const seenKeys = new Set();
48
+ for (const keyed of existingKeyed) {
49
+ addEdge(adjacency, seenKeys, keyed);
50
+ }
51
+ for (const keyed of proposedKeyed) {
52
+ const edge = keyed.edge;
53
+ // 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`);
56
+ }
57
+ // Reachability: does `target` already reach `source`? If so, adding
58
+ // `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`);
61
+ }
62
+ addEdge(adjacency, seenKeys, keyed);
63
+ }
64
+ return succeed(true);
65
+ }));
66
+ }
67
+ /** Canonically key a set of edges (the guard's only fallible step). */
68
+ function keyEdges(edges) {
69
+ return mapResults(edges.map((edge) => buildCycleKey(edge).onSuccess((key) => succeed({ edge, key }))));
70
+ }
71
+ /** Add an edge to the adjacency map, de-duplicated by its canonical key. */
72
+ function addEdge(adjacency, seenKeys, keyed) {
73
+ if (seenKeys.has(keyed.key)) {
74
+ return;
75
+ }
76
+ seenKeys.add(keyed.key);
77
+ const source = keyed.edge.source;
78
+ const target = keyed.edge.target;
79
+ const targets = adjacency.get(source);
80
+ if (targets === undefined) {
81
+ adjacency.set(source, new Set([target]));
82
+ }
83
+ else {
84
+ targets.add(target);
85
+ }
86
+ }
87
+ /** DFS reachability: is `to` reachable from `from` over `adjacency`? */
88
+ function reaches(adjacency, from, to) {
89
+ const visited = new Set();
90
+ const stack = [from];
91
+ while (stack.length > 0) {
92
+ const node = stack.pop();
93
+ if (node === to) {
94
+ return true;
95
+ }
96
+ if (visited.has(node)) {
97
+ continue;
98
+ }
99
+ visited.add(node);
100
+ const neighbors = adjacency.get(node);
101
+ if (neighbors !== undefined) {
102
+ for (const neighbor of neighbors) {
103
+ if (!visited.has(neighbor)) {
104
+ stack.push(neighbor);
105
+ }
106
+ }
107
+ }
108
+ }
109
+ return false;
110
+ }
111
+ //# sourceMappingURL=cycleGuard.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,6 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=hostStages.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,9 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ export * from './model';
6
+ export * from './hostStages';
7
+ export * from './cycleGuard';
8
+ export * from './orchestrator';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/ingest/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './model';\nexport * from './hostStages';\nexport * from './cycleGuard';\nexport * from './orchestrator';\n"]}
@@ -0,0 +1,6 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=model.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,438 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ import { Hash, Logging, fail, mapResults, succeed } from '@fgv/ts-utils';
6
+ import { Convert, isTemporalRecord, isVersionCurrent } from '../types';
7
+ import { assertNoCycles } from './cycleGuard';
8
+ /**
9
+ * The provenance source stamped on every record the ingest pipeline writes.
10
+ * @public
11
+ */
12
+ export const HOST_INGEST_PROVENANCE_SOURCE = 'host-ingest';
13
+ /**
14
+ * The link type whose presence on a temporal-kind candidate fires the
15
+ * contradicts→temporal-versioned interlock.
16
+ * @public
17
+ */
18
+ export const CONTRADICTS_LINK_TYPE = 'contradicts';
19
+ /**
20
+ * The default stage-4 layer-2 cosine similarity threshold (design note §2). A
21
+ * near-duplicate candidate is surfaced to the {@link IEntityResolver} only when
22
+ * its nearest neighbor scores `>= 0.85`.
23
+ * @public
24
+ */
25
+ export const DEFAULT_SIMILARITY_THRESHOLD = 0.85;
26
+ /**
27
+ * The default top-K for stage-4 layer-2 similarity candidate generation.
28
+ * @public
29
+ */
30
+ export const DEFAULT_SIMILARITY_TOP_K = 5;
31
+ /**
32
+ * Default {@link IMemoryIngestOrchestrator}.
33
+ * @public
34
+ */
35
+ export class MemoryIngestOrchestrator {
36
+ constructor(params) {
37
+ var _a, _b, _c, _d, _e;
38
+ this._store = params.store;
39
+ this._registry = params.registry;
40
+ this._codecs = (_a = params.codecs) !== null && _a !== void 0 ? _a : new Map();
41
+ this._defaultCodec = params.defaultCodec;
42
+ this._classifier = params.classifier;
43
+ this._extractor = params.extractor;
44
+ this._relationExtractor = params.relationExtractor;
45
+ this._similarity =
46
+ params.entityResolver !== undefined && params.vectorIndex !== undefined && params.embed !== undefined
47
+ ? { resolver: params.entityResolver, vectorIndex: params.vectorIndex, embed: params.embed }
48
+ : undefined;
49
+ this._similarityThreshold = (_b = params.similarityThreshold) !== null && _b !== void 0 ? _b : DEFAULT_SIMILARITY_THRESHOLD;
50
+ this._similarityTopK = (_c = params.similarityTopK) !== null && _c !== void 0 ? _c : DEFAULT_SIMILARITY_TOP_K;
51
+ this._cycleGuard = (_d = params.cycleGuard) !== null && _d !== void 0 ? _d : 'reject';
52
+ this._logger = (_e = params.logger) !== null && _e !== void 0 ? _e : new Logging.NoOpLogger();
53
+ this._hasher = new Hash.Crc32Normalizer();
54
+ }
55
+ /** Family-convention factory. */
56
+ static create(params) {
57
+ return succeed(new MemoryIngestOrchestrator(params));
58
+ }
59
+ /** {@inheritDoc IMemoryIngestOrchestrator.ingestItem} */
60
+ async ingestItem(item) {
61
+ return (await this._classify(item))
62
+ .thenOnSuccess((classification) => this._extract(item, classification))
63
+ .thenOnSuccess((candidates) => this._processCandidates(item, candidates));
64
+ }
65
+ /** {@inheritDoc IMemoryIngestOrchestrator.ingestBatch} */
66
+ async ingestBatch(items) {
67
+ const results = [];
68
+ for (const item of items) {
69
+ const result = await this.ingestItem(item);
70
+ if (result.isFailure()) {
71
+ return fail(result.message);
72
+ }
73
+ results.push(result.value);
74
+ }
75
+ return succeed(results);
76
+ }
77
+ /** Stage 2 — classify (host), normalizing a rejected promise into a Failure. */
78
+ async _classify(item) {
79
+ return this._capture(() => this._classifier.classify(item), `ingest '${item.id}': classify`);
80
+ }
81
+ /** Stage 3 — extract (host), normalizing a rejected promise into a Failure. */
82
+ async _extract(item, classification) {
83
+ return this._capture(() => this._extractor.extract(item, classification), `ingest '${item.id}': extract`);
84
+ }
85
+ /**
86
+ * Stages 3b-6 over the extracted candidates: validate bodies, resolve/dedup
87
+ * (stage 4), relate + cycle guard (stage 5), and load-with-provenance (stage 6).
88
+ */
89
+ async _processCandidates(item, candidates) {
90
+ // Snapshot the store once; stage-4 resolution and the cycle guard reason over
91
+ // the pre-ingest state.
92
+ const snapshotResult = await this._store.list();
93
+ if (snapshotResult.isFailure()) {
94
+ return fail(`ingest '${item.id}': failed to snapshot store: ${snapshotResult.message}`);
95
+ }
96
+ const snapshot = snapshotResult.value;
97
+ const byId = MemoryIngestOrchestrator._indexById(snapshot);
98
+ // Stage 3b + 4: validate each body and resolve a verdict/plan.
99
+ const plans = [];
100
+ for (const candidate of candidates) {
101
+ const planResult = await this._planCandidate(item, candidate, snapshot, byId);
102
+ if (planResult.isFailure()) {
103
+ return fail(planResult.message);
104
+ }
105
+ plans.push(planResult.value);
106
+ }
107
+ // Stage 5: relate over the writable candidates, validate + cycle-guard.
108
+ const writablePlans = plans.filter((plan) => plan.verdict.verdict !== 'duplicate-of');
109
+ const edgesResult = await this._relate(item, writablePlans, snapshot, byId);
110
+ if (edgesResult.isFailure()) {
111
+ return fail(edgesResult.message);
112
+ }
113
+ const edges = edgesResult.value;
114
+ // Stage 6: load-with-provenance, in extraction order.
115
+ const records = [];
116
+ for (const plan of plans) {
117
+ const outcome = await this._loadCandidate(item, plan, edges);
118
+ if (outcome.isFailure()) {
119
+ return fail(outcome.message);
120
+ }
121
+ records.push(outcome.value);
122
+ }
123
+ return succeed({ item, records });
124
+ }
125
+ /** Stage 3b + 4 for one candidate: validate body, resolve address, resolve verdict. */
126
+ async _planCandidate(item, candidate, snapshot, byId) {
127
+ const kind = candidate.envelope.kind;
128
+ // Stage 3b: the typed validation boundary — no unchecked host body reaches the store.
129
+ const bodyResult = this._registry
130
+ .convert(kind, candidate.body)
131
+ .withErrorFormat((msg) => `ingest '${item.id}': candidate body for kind '${kind}' is invalid: ${msg}`)
132
+ .onSuccess(() => MemoryIngestOrchestrator._asStringBody(item, candidate));
133
+ if (bodyResult.isFailure()) {
134
+ return fail(bodyResult.message);
135
+ }
136
+ const body = bodyResult.value;
137
+ const addrResult = this._resolveAddress(candidate.envelope.entityId, kind);
138
+ if (addrResult.isFailure()) {
139
+ return fail(`ingest '${item.id}': ${addrResult.message}`);
140
+ }
141
+ const addr = addrResult.value;
142
+ return (await this._resolveVerdict(candidate, kind, body, addr, snapshot, byId)).onSuccess((verdict) => this._planFromVerdict(item, candidate, addr, verdict, byId));
143
+ }
144
+ /**
145
+ * Turn a resolved verdict into a candidate plan. A `new` verdict writes under the
146
+ * candidate's own address. Every TARGET-bearing verdict
147
+ * (`duplicate-of` / `supersede` / `merge-into`) is validated uniformly: the
148
+ * target must be a real store record (fgv owns validation — a non-compliant host
149
+ * resolver never smuggles a bogus id through) AND its kind must equal the
150
+ * candidate's kind (a cross-kind target would write to the wrong scope). Only
151
+ * `merge-into` re-addresses the write to the target's entity.
152
+ */
153
+ _planFromVerdict(item, candidate, addr, verdict, byId) {
154
+ if (verdict.verdict === 'new') {
155
+ return Convert.memoryId.convert(addr.idStem).onSuccess((refId) => succeed({
156
+ candidate,
157
+ writeAddress: addr,
158
+ writeEntityId: candidate.envelope.entityId,
159
+ refId,
160
+ verdict
161
+ }));
162
+ }
163
+ // duplicate-of | supersede | merge-into: the target must exist and share the
164
+ // candidate's kind.
165
+ const target = byId.get(verdict.target);
166
+ if (target === undefined) {
167
+ return fail(`ingest '${item.id}': ${verdict.verdict} target '${verdict.target}' does not exist in the store`);
168
+ }
169
+ if (target.envelope.kind !== candidate.envelope.kind) {
170
+ return fail(`ingest '${item.id}': ${verdict.verdict} target '${verdict.target}' is kind '${target.envelope.kind}' but the candidate is kind '${candidate.envelope.kind}'`);
171
+ }
172
+ if (verdict.verdict === 'merge-into') {
173
+ // Re-address the write to the target's entity, carrying the target record so
174
+ // stage 6 can UNION its existing tags/links (never overwrite them).
175
+ const targetEntityId = target.envelope.entityId;
176
+ return this._resolveAddress(targetEntityId, target.envelope.kind)
177
+ .withErrorFormat((msg) => `ingest '${item.id}': ${msg}`)
178
+ .onSuccess((targetAddr) => Convert.memoryId.convert(targetAddr.idStem).onSuccess((refId) => succeed({
179
+ candidate,
180
+ writeAddress: targetAddr,
181
+ writeEntityId: targetEntityId,
182
+ refId,
183
+ verdict,
184
+ mergeTarget: target
185
+ })));
186
+ }
187
+ // duplicate-of | supersede: write under the candidate's own address.
188
+ return Convert.memoryId.convert(addr.idStem).onSuccess((refId) => succeed({
189
+ candidate,
190
+ writeAddress: addr,
191
+ writeEntityId: candidate.envelope.entityId,
192
+ refId,
193
+ verdict
194
+ }));
195
+ }
196
+ /**
197
+ * Stage 4 — resolve a dedup verdict. Layer 1: an exact `{ kind, body }` match in
198
+ * the candidate's scope is a `duplicate-of` (design note §1). Layer 2 (only when
199
+ * a resolver + vector index + embedder are all wired): embed the candidate,
200
+ * surface over-threshold neighbors, and dispatch to the {@link IEntityResolver}.
201
+ * Otherwise the verdict is `new` (the exact-only fall-back path).
202
+ */
203
+ async _resolveVerdict(candidate, kind, body, addr, snapshot, byId) {
204
+ return this._findExactMatch(kind, body, addr.scope, snapshot).thenOnSuccess(async (matchId) => {
205
+ if (matchId !== undefined) {
206
+ return succeed({ verdict: 'duplicate-of', target: matchId });
207
+ }
208
+ const layer2 = this._similarity;
209
+ if (layer2 === undefined) {
210
+ // No layer-2: exact-only fall-back (the deterministic-identity host path).
211
+ return succeed({ verdict: 'new' });
212
+ }
213
+ return this._resolveViaSimilarity(candidate, addr, body, layer2, byId);
214
+ });
215
+ }
216
+ /** Layer-2 similarity candidate-gen + resolver dispatch. */
217
+ async _resolveViaSimilarity(candidate, addr, body, wiring, byId) {
218
+ return MemoryIngestOrchestrator._provisionalRecord(candidate, addr.idStem, body).thenOnSuccess((provisional) => this._resolveViaSimilarityEmbedded(candidate, addr, wiring, byId, provisional));
219
+ }
220
+ /** Layer-2 continuation once the candidate has a provisional record to embed. */
221
+ async _resolveViaSimilarityEmbedded(candidate, addr, wiring, byId, provisional) {
222
+ const embedded = await this._capture(() => wiring.embed(provisional), `ingest '${candidate.envelope.entityId}': embed candidate`);
223
+ if (embedded.isFailure()) {
224
+ return fail(embedded.message);
225
+ }
226
+ const queried = await this._capture(() => wiring.vectorIndex.query(embedded.value, this._similarityTopK), `ingest '${candidate.envelope.entityId}': similarity query`);
227
+ if (queried.isFailure()) {
228
+ return fail(queried.message);
229
+ }
230
+ const similar = [];
231
+ for (const hit of queried.value) {
232
+ if (hit.score < this._similarityThreshold || hit.id === addr.idStem) {
233
+ continue;
234
+ }
235
+ const record = byId.get(hit.id);
236
+ if (record !== undefined) {
237
+ similar.push({ id: hit.id, record, score: hit.score });
238
+ }
239
+ }
240
+ if (similar.length === 0) {
241
+ return succeed({ verdict: 'new' });
242
+ }
243
+ return this._capture(() => wiring.resolver.resolve(candidate, similar), `ingest '${candidate.envelope.entityId}': resolve`);
244
+ }
245
+ /**
246
+ * Find an existing record in `scope` whose `{ kind, body }` hash matches the
247
+ * candidate's (layer-1 exact dedup). Invalidated temporal versions are excluded
248
+ * — only a live (non-temporal or current) record deduplicates a candidate.
249
+ */
250
+ _findExactMatch(kind, body, scope, snapshot) {
251
+ // Same-kind, same-scope, LIVE (non-temporal or current) records are the exact
252
+ // cohort. `_resolveAddress(...).map(...).orDefault()` collapses an unresolved
253
+ // codec to a non-matching scope with no explicit failure branch.
254
+ const cohort = snapshot.filter((record) => record.envelope.kind === kind &&
255
+ !(isTemporalRecord(record) && !isVersionCurrent(record)) &&
256
+ this._resolveAddress(record.envelope.entityId, record.envelope.kind)
257
+ .onSuccess((addr) => succeed(addr.scope))
258
+ .orDefault() === scope);
259
+ return this._exactKey(kind, body).onSuccess((key) => mapResults(cohort.map((record) => MemoryIngestOrchestrator._recordBodyString(record).onSuccess((recordBody) => this._exactKey(record.envelope.kind, recordBody).onSuccess((recordKey) => succeed({ id: record.envelope.id, key: recordKey }))))).onSuccess((keyed) => { var _a; return succeed((_a = keyed.find((entry) => entry.key === key)) === null || _a === void 0 ? void 0 : _a.id); }));
260
+ }
261
+ /**
262
+ * The body of a persisted record, required to be a string (the store persists
263
+ * only string bodies). Fails loudly rather than blind-casting an `unknown` body
264
+ * into the exact-dedup hash — a non-string existing body is a store-integrity
265
+ * fault, surfaced with context, not a silent miscompute.
266
+ */
267
+ static _recordBodyString(record) {
268
+ if (typeof record.body !== 'string') {
269
+ return fail(`ingest: stored record '${record.envelope.id}' has a non-string body (got ${typeof record.body})`);
270
+ }
271
+ return succeed(record.body);
272
+ }
273
+ /** The stage-4 exact-dedup key over `{ kind, body }` (design note §1). */
274
+ _exactKey(kind, body) {
275
+ return this._hasher.computeHash({ kind, body });
276
+ }
277
+ /** Stage 5 — relate (host), validate edges, and run the write-time cycle guard. */
278
+ async _relate(item, writablePlans, snapshot, byId) {
279
+ const relationCandidates = writablePlans.map((plan) => ({
280
+ candidate: plan.candidate,
281
+ id: plan.refId
282
+ }));
283
+ const proposed = await this._capture(() => this._relationExtractor.relate({ item, candidates: relationCandidates }), `ingest '${item.id}': relate`);
284
+ if (proposed.isFailure()) {
285
+ return proposed;
286
+ }
287
+ const refIds = new Set(writablePlans.map((plan) => plan.refId));
288
+ const validation = this._validateEdges(item, proposed.value, refIds, byId);
289
+ if (validation.isFailure()) {
290
+ return fail(validation.message);
291
+ }
292
+ if (this._cycleGuard === 'reject') {
293
+ const guard = assertNoCycles(MemoryIngestOrchestrator._existingEdges(snapshot), proposed.value.map((e) => ({ source: e.source, target: e.edge.target, type: e.edge.type })));
294
+ if (guard.isFailure()) {
295
+ return fail(`ingest '${item.id}': ${guard.message}`);
296
+ }
297
+ }
298
+ return succeed(proposed.value);
299
+ }
300
+ /**
301
+ * Validate stage-5 edges: each `source` must be a candidate being written; each
302
+ * `target` must resolve to a sibling candidate or an existing store record.
303
+ */
304
+ _validateEdges(item, edges, refIds, byId) {
305
+ return mapResults(edges.map((edge) => {
306
+ if (!refIds.has(edge.source)) {
307
+ return fail(`ingest '${item.id}': edge source '${edge.source}' is not a candidate being written`);
308
+ }
309
+ if (!refIds.has(edge.edge.target) && byId.get(edge.edge.target) === undefined) {
310
+ return fail(`ingest '${item.id}': edge target '${edge.edge.target}' resolves to neither a sibling candidate nor an existing record`);
311
+ }
312
+ return succeed(true);
313
+ })).onSuccess(() => succeed(true));
314
+ }
315
+ /** Stage 6 — stamp provenance + edges, admit through the store, record the outcome. */
316
+ async _loadCandidate(item, plan, allEdges) {
317
+ const myEdges = allEdges.filter((e) => e.source === plan.refId);
318
+ if (plan.verdict.verdict === 'duplicate-of') {
319
+ // No write: the existing target satisfied the candidate. The target id is a
320
+ // typed field of the narrowed `duplicate-of` verdict — no cast, no guard.
321
+ return succeed({
322
+ candidate: plan.candidate,
323
+ resolution: plan.verdict,
324
+ disposition: 'deduped',
325
+ id: plan.verdict.target,
326
+ edges: myEdges
327
+ });
328
+ }
329
+ return this._buildRecord(item, plan, myEdges).thenOnSuccess(async (record) => (await this._store.put(record))
330
+ .withErrorFormat((msg) => `ingest '${item.id}': write failed: ${msg}`)
331
+ .onSuccess((persisted) => {
332
+ const disposition = plan.verdict.verdict === 'merge-into' ? 'merged' : 'written';
333
+ const interlock = plan.writeAddress.isVersioned && myEdges.some((e) => e.edge.type === CONTRADICTS_LINK_TYPE)
334
+ ? 'temporal-versioned'
335
+ : undefined;
336
+ return succeed(Object.assign({ candidate: plan.candidate, resolution: plan.verdict, disposition, id: persisted.envelope.id, record: persisted, edges: myEdges }, (interlock !== undefined ? { interlock } : {})));
337
+ }));
338
+ }
339
+ /** Build the fully-stamped record to persist (provenance + edges + placeholder txn fields). */
340
+ _buildRecord(item, plan, myEdges) {
341
+ const base = plan.candidate.envelope;
342
+ // For merge-into the store's `applyUpdate` REPLACES array fields wholesale, so
343
+ // the persisted envelope must already carry the UNION of the target's existing
344
+ // tags/links/provenance and the candidate's — otherwise the merge silently
345
+ // wipes the target's prior links/tags.
346
+ const target = plan.mergeTarget;
347
+ const priorLinks = target !== undefined ? target.envelope.links : [];
348
+ const priorTags = target !== undefined ? target.envelope.tags : [];
349
+ const priorProvenance = target !== undefined ? target.envelope.provenance : { source: '' };
350
+ return Convert.memoryId.convert(plan.writeAddress.idStem).onSuccess((id) =>
351
+ // Dedup the combined links by canonical edge key: the target's prior links, the
352
+ // candidate's own links, and the stage-5 edges (two candidates that resolve
353
+ // merge-into the SAME target share a `refId` — hence the same `myEdges` — and a
354
+ // relation extractor may repeat an edge) each appear exactly once.
355
+ this._dedupEdges([...priorLinks, ...base.links, ...myEdges.map((e) => e.edge)]).onSuccess((links) => {
356
+ const provenance = Object.assign(Object.assign(Object.assign(Object.assign({}, (target !== undefined ? priorProvenance : {})), base.provenance), { source: HOST_INGEST_PROVENANCE_SOURCE }), (item.sourceId !== undefined ? { derivedFrom: item.sourceId } : {}));
357
+ const envelope = Object.assign(Object.assign({}, base), { id, entityId: plan.writeEntityId, seq: 0, contentHash: '', created: 0, updated: 0, provenance, tags: MemoryIngestOrchestrator._unionTags(priorTags, base.tags), links });
358
+ return succeed({ envelope, body: plan.candidate.body });
359
+ }));
360
+ }
361
+ /** Union two tag lists, de-duplicated, preserving first-occurrence order. */
362
+ static _unionTags(prior, incoming) {
363
+ const seen = new Set();
364
+ const out = [];
365
+ for (const tag of [...prior, ...incoming]) {
366
+ if (!seen.has(tag)) {
367
+ seen.add(tag);
368
+ out.push(tag);
369
+ }
370
+ }
371
+ return out;
372
+ }
373
+ /** De-duplicate a link list by canonical edge hash, preserving first-occurrence order. */
374
+ _dedupEdges(edges) {
375
+ const seen = new Set();
376
+ const deduped = [];
377
+ return mapResults(edges.map((edge) => this._hasher.computeHash(edge).onSuccess((key) => {
378
+ if (!seen.has(key)) {
379
+ seen.add(key);
380
+ deduped.push(edge);
381
+ }
382
+ return succeed(key);
383
+ }))).onSuccess(() => succeed(deduped));
384
+ }
385
+ /** Resolve a `(kind, entityId)` to its storage address via the registered codec. */
386
+ _resolveAddress(entityId, kind) {
387
+ var _a;
388
+ const codec = (_a = this._codecs.get(kind)) !== null && _a !== void 0 ? _a : this._defaultCodec;
389
+ if (codec === undefined) {
390
+ return fail(`no identity codec registered for kind '${kind}'`);
391
+ }
392
+ return codec.encode(entityId);
393
+ }
394
+ /** Run a host hook, normalizing a thrown/rejected hook into a Failure (never throws across the seam). */
395
+ async _capture(op, label) {
396
+ try {
397
+ return await op();
398
+ }
399
+ catch (err) {
400
+ const message = `${label} threw: ${String(err)}`;
401
+ this._logger.warn(message);
402
+ return fail(message);
403
+ }
404
+ }
405
+ /** Index a record snapshot by id (last write wins on an id collision across scopes). */
406
+ static _indexById(records) {
407
+ const byId = new Map();
408
+ for (const record of records) {
409
+ byId.set(record.envelope.id, record);
410
+ }
411
+ return byId;
412
+ }
413
+ /** Every existing outbound edge in the snapshot, as cycle-guard edges. */
414
+ static _existingEdges(records) {
415
+ const edges = [];
416
+ for (const record of records) {
417
+ for (const edge of record.envelope.links) {
418
+ edges.push({ source: record.envelope.id, target: edge.target, type: edge.type });
419
+ }
420
+ }
421
+ return edges;
422
+ }
423
+ /** A provisional record for embedding a candidate (placeholder txn-time fields). */
424
+ static _provisionalRecord(candidate, idStem, body) {
425
+ return Convert.memoryId.convert(idStem).onSuccess((id) => {
426
+ const envelope = Object.assign(Object.assign({}, candidate.envelope), { id, seq: 0, contentHash: '', created: 0, updated: 0 });
427
+ return succeed({ envelope, body });
428
+ });
429
+ }
430
+ /** Require a candidate body to be a string (the store persists only string bodies). */
431
+ static _asStringBody(item, candidate) {
432
+ if (typeof candidate.body !== 'string') {
433
+ return fail(`ingest '${item.id}': candidate body for kind '${candidate.envelope.kind}' must be a string (got ${typeof candidate.body})`);
434
+ }
435
+ return succeed(candidate.body);
436
+ }
437
+ }
438
+ //# sourceMappingURL=orchestrator.js.map