@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
@@ -0,0 +1,138 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import '@fgv/ts-utils-jest';
7
+ import { Converters, Result, succeed } from '@fgv/ts-utils';
8
+ import { FileTree } from '@fgv/ts-json-base';
9
+ import {
10
+ BodyConverterRegistry,
11
+ FileTreeMemoryStore,
12
+ IBodyConverterRegistry,
13
+ IIdentityCodec,
14
+ IMemoryRecord,
15
+ IScopedMemoryRecord,
16
+ IVectorQueryHit,
17
+ InMemoryCosineIndex,
18
+ Kind,
19
+ MemoryScopeKey,
20
+ MtmIdentityCodec,
21
+ envelopeConverter
22
+ } from '../../../index';
23
+
24
+ const mtmKind: Kind = 'mtm' as Kind;
25
+
26
+ function mutableRoot(): FileTree.IMutableFileTreeDirectoryItem {
27
+ const tree = FileTree.inMemory([], { mutable: true }).orThrow();
28
+ const root = tree.getDirectory('/').orThrow();
29
+ if (!FileTree.isMutableDirectoryItem(root)) {
30
+ throw new Error('expected a mutable root directory');
31
+ }
32
+ return root;
33
+ }
34
+
35
+ function mtmRecord(entityId: string, idStem: string, body: string): IMemoryRecord<unknown> {
36
+ return {
37
+ envelope: envelopeConverter
38
+ .convert({
39
+ id: idStem,
40
+ entityId,
41
+ kind: 'mtm',
42
+ tags: [],
43
+ links: [],
44
+ created: 0,
45
+ updated: 0,
46
+ seq: 0,
47
+ contentHash: '',
48
+ provenance: { source: 'agent' }
49
+ })
50
+ .orThrow(),
51
+ body
52
+ };
53
+ }
54
+
55
+ // Deterministic keyword-count embedder (no network): the record's vector and a
56
+ // query's vector live in the same space so cosine ordering is predictable.
57
+ const MARKERS: ReadonlyArray<string> = ['cat', 'dog', 'fish'];
58
+ function featureVector(text: string): Float32Array {
59
+ const lower: string = text.toLowerCase();
60
+ return Float32Array.from(MARKERS.map((m) => lower.split(m).length - 1));
61
+ }
62
+ const recordEmbed = (r: IMemoryRecord<unknown>): Promise<Result<Float32Array>> =>
63
+ Promise.resolve(Converters.string.convert(r.body).onSuccess((body) => succeed(featureVector(body))));
64
+
65
+ function mtmStore(): FileTreeMemoryStore {
66
+ const registry: IBodyConverterRegistry = BodyConverterRegistry.create().orThrow();
67
+ registry.register(mtmKind, Converters.string);
68
+ return FileTreeMemoryStore.create({
69
+ root: mutableRoot(),
70
+ registry,
71
+ codecs: new Map<Kind, IIdentityCodec>([[mtmKind, new MtmIdentityCodec()]])
72
+ }).orThrow();
73
+ }
74
+
75
+ describe('FileTreeMemoryStore.listScoped', () => {
76
+ test('yields an empty list for an empty store', async () => {
77
+ expect(await mtmStore().listScoped()).toSucceedWith([]);
78
+ });
79
+
80
+ test('pairs each record with its scope-qualified (scope, id) address', async () => {
81
+ const store = mtmStore();
82
+ (await store.put(mtmRecord('conv-1:3', 'turn-3', 'cat cat'))).orThrow();
83
+ expect(await store.listScoped()).toSucceedAndSatisfy((scoped: ReadonlyArray<IScopedMemoryRecord>) => {
84
+ expect(scoped).toHaveLength(1);
85
+ expect(scoped[0].target).toEqual({ scope: 'conversations/conv-1', id: 'turn-3' });
86
+ expect(scoped[0].record.envelope.id).toBe('turn-3');
87
+ expect(scoped[0].record.body).toBe('cat cat');
88
+ });
89
+ });
90
+
91
+ test('lists two same-stem records in different scopes as distinct entries', async () => {
92
+ // conv-1:3 and conv-2:3 both mint the idStem 'turn-3' but under different
93
+ // scopes — exactly the ambiguity a bare MemoryId could not disambiguate.
94
+ const store = mtmStore();
95
+ (await store.put(mtmRecord('conv-1:3', 'turn-3', 'cat'))).orThrow();
96
+ (await store.put(mtmRecord('conv-2:3', 'turn-3', 'dog'))).orThrow();
97
+ expect(await store.listScoped()).toSucceedAndSatisfy((scoped: ReadonlyArray<IScopedMemoryRecord>) => {
98
+ expect(scoped).toHaveLength(2);
99
+ const byScope = new Map(scoped.map((s) => [s.target.scope, s.target]));
100
+ expect(byScope.get('conversations/conv-1' as MemoryScopeKey)).toEqual({
101
+ scope: 'conversations/conv-1',
102
+ id: 'turn-3'
103
+ });
104
+ expect(byScope.get('conversations/conv-2' as MemoryScopeKey)).toEqual({
105
+ scope: 'conversations/conv-2',
106
+ id: 'turn-3'
107
+ });
108
+ });
109
+ });
110
+ });
111
+
112
+ describe('FileTreeMemoryStore.asRecordSource — drives an IVectorIndex rebuild', () => {
113
+ test('a real store re-indexes an InMemoryCosineIndex on the scoped key', async () => {
114
+ const store = mtmStore();
115
+ (await store.put(mtmRecord('conv-1:3', 'turn-3', 'cat'))).orThrow();
116
+ (await store.put(mtmRecord('conv-2:3', 'turn-3', 'dog'))).orThrow();
117
+
118
+ const index = InMemoryCosineIndex.create().orThrow();
119
+ // The store satisfies IMemoryRecordSource via asRecordSource(); rebuild
120
+ // re-embeds every record and keys each vector on the scoped (scope, id).
121
+ expect(await index.rebuild(store.asRecordSource(), recordEmbed)).toSucceedWith(2);
122
+ expect(index.size).toBe(2);
123
+
124
+ // The two same-stem records occupy DISTINCT vector entries: a query for 'cat'
125
+ // resolves the conv-1 record; a query for 'dog' resolves conv-2 — each hit
126
+ // carries the full scoped target, not a bare (colliding) id.
127
+ expect(await index.query(featureVector('cat'), 1)).toSucceedAndSatisfy(
128
+ (hits: ReadonlyArray<IVectorQueryHit>) => {
129
+ expect(hits[0].target).toEqual({ scope: 'conversations/conv-1', id: 'turn-3' });
130
+ }
131
+ );
132
+ expect(await index.query(featureVector('dog'), 1)).toSucceedAndSatisfy(
133
+ (hits: ReadonlyArray<IVectorQueryHit>) => {
134
+ expect(hits[0].target).toEqual({ scope: 'conversations/conv-2', id: 'turn-3' });
135
+ }
136
+ );
137
+ });
138
+ });
@@ -0,0 +1,349 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import '@fgv/ts-utils-jest';
7
+ import { Converters, Logging, Result } from '@fgv/ts-utils';
8
+ import { FileTree } from '@fgv/ts-json-base';
9
+ import {
10
+ BodyConverterRegistry,
11
+ EntityId,
12
+ FileTreeMemoryStore,
13
+ IBodyConverterRegistry,
14
+ IIdentityCodec,
15
+ IMemoryRecord,
16
+ IWritePolicy,
17
+ Kind,
18
+ KnowledgeIdentityCodec,
19
+ MemoryIndex,
20
+ MemoryScopeKey,
21
+ RankProjector,
22
+ RecencyRetriever,
23
+ TemporalIdentityCodec,
24
+ TemporalVersionedPolicy,
25
+ parseMemoryFile
26
+ } from '../../../index';
27
+
28
+ const knowledgeKind: Kind = 'knowledge' as Kind;
29
+ const plainKind: Kind = 'plain' as Kind;
30
+ const factKind: Kind = 'fact' as Kind;
31
+
32
+ /** Host projector: rank = body length. Generic numeric ordering — the store never interprets meaning. */
33
+ const lengthProjector: RankProjector = (record) => (record.body as string).length;
34
+
35
+ /** A projector that always throws — exercises the guard-host-callback path. */
36
+ const throwingProjector: RankProjector = () => {
37
+ throw new Error('projector boom');
38
+ };
39
+
40
+ function mutableRoot(
41
+ files: ReadonlyArray<{ path: string; contents: string }> = []
42
+ ): FileTree.IMutableFileTreeDirectoryItem {
43
+ const tree = FileTree.inMemory([...files], { mutable: true }).orThrow();
44
+ const root = tree.getDirectory('/').orThrow();
45
+ if (!FileTree.isMutableDirectoryItem(root)) {
46
+ throw new Error('expected a mutable root directory');
47
+ }
48
+ return root;
49
+ }
50
+
51
+ function registry(): IBodyConverterRegistry {
52
+ const reg = BodyConverterRegistry.create().orThrow();
53
+ reg.register(knowledgeKind, Converters.string);
54
+ reg.register(plainKind, Converters.string);
55
+ reg.register(factKind, Converters.string);
56
+ return reg;
57
+ }
58
+
59
+ const codecs: ReadonlyMap<Kind, IIdentityCodec> = new Map<Kind, IIdentityCodec>([
60
+ [knowledgeKind, new KnowledgeIdentityCodec()],
61
+ [plainKind, new KnowledgeIdentityCodec()],
62
+ [factKind, TemporalIdentityCodec.create('facts').orThrow()]
63
+ ]);
64
+
65
+ const temporalPolicies: ReadonlyMap<Kind, IWritePolicy> = new Map<Kind, IWritePolicy>([
66
+ [factKind, TemporalVersionedPolicy.create().orThrow()]
67
+ ]);
68
+
69
+ interface IRecordSpec {
70
+ readonly id: string;
71
+ readonly entityId?: string;
72
+ readonly kind?: Kind;
73
+ readonly body: string;
74
+ readonly tags?: ReadonlyArray<string>;
75
+ }
76
+
77
+ function makeRecord(spec: IRecordSpec): IMemoryRecord<unknown> {
78
+ return {
79
+ envelope: {
80
+ id: spec.id as unknown as IMemoryRecord<unknown>['envelope']['id'],
81
+ entityId: (spec.entityId ?? spec.id) as EntityId,
82
+ kind: spec.kind ?? knowledgeKind,
83
+ tags: (spec.tags ?? []) as unknown as IMemoryRecord<unknown>['envelope']['tags'],
84
+ links: [],
85
+ created: 0,
86
+ updated: 0,
87
+ seq: 0,
88
+ contentHash: '',
89
+ provenance: { source: 'agent' }
90
+ },
91
+ body: spec.body
92
+ };
93
+ }
94
+
95
+ describe('FileTreeMemoryStore rank axis', () => {
96
+ let clockValue: number;
97
+ const clock = (): number => clockValue;
98
+
99
+ beforeEach(() => {
100
+ clockValue = 1000;
101
+ });
102
+
103
+ function createStore(
104
+ params: {
105
+ root?: FileTree.IMutableFileTreeDirectoryItem;
106
+ rankProjectors?: ReadonlyMap<Kind, RankProjector>;
107
+ writePolicies?: ReadonlyMap<Kind, IWritePolicy>;
108
+ logger?: Logging.ILogger;
109
+ } = {}
110
+ ): Result<FileTreeMemoryStore> {
111
+ return FileTreeMemoryStore.create({
112
+ root: params.root ?? mutableRoot(),
113
+ registry: registry(),
114
+ codecs,
115
+ writePolicies: params.writePolicies,
116
+ rankProjectors: params.rankProjectors,
117
+ logger: params.logger,
118
+ clock
119
+ });
120
+ }
121
+
122
+ const knowledgeProjectors: ReadonlyMap<Kind, RankProjector> = new Map<Kind, RankProjector>([
123
+ [knowledgeKind, lengthProjector]
124
+ ]);
125
+
126
+ describe('put stamps rank (flat / non-versioned path)', () => {
127
+ test('runs the kind projector and stamps the numeric result', async () => {
128
+ const store = createStore({ rankProjectors: knowledgeProjectors }).orThrow();
129
+ expect(await store.put(makeRecord({ id: 'doc-a', body: 'abcd' }))).toSucceedAndSatisfy(
130
+ (rec: IMemoryRecord<unknown>) => {
131
+ expect(rec.envelope.rank).toBe(4);
132
+ }
133
+ );
134
+ });
135
+
136
+ test('leaves rank absent for a kind with no projector', async () => {
137
+ const store = createStore({ rankProjectors: knowledgeProjectors }).orThrow();
138
+ expect(
139
+ await store.put(makeRecord({ id: 'p-1', kind: plainKind, body: 'anything' }))
140
+ ).toSucceedAndSatisfy((rec: IMemoryRecord<unknown>) => {
141
+ expect(rec.envelope.rank).toBeUndefined();
142
+ });
143
+ });
144
+
145
+ test('is byte-identical (rank absent) when no projector map is wired', async () => {
146
+ const store = createStore().orThrow();
147
+ expect(await store.put(makeRecord({ id: 'doc-a', body: 'abcd' }))).toSucceedAndSatisfy(
148
+ (rec: IMemoryRecord<unknown>) => {
149
+ expect(rec.envelope.rank).toBeUndefined();
150
+ }
151
+ );
152
+ });
153
+ });
154
+
155
+ describe('body revision re-stamps rank (staleness contract)', () => {
156
+ test('a body-only revision recomputes rank against the current body', async () => {
157
+ const store = createStore({ rankProjectors: knowledgeProjectors }).orThrow();
158
+ expect(await store.put(makeRecord({ id: 'doc-a', body: 'ab' }))).toSucceedAndSatisfy(
159
+ (rec: IMemoryRecord<unknown>) => {
160
+ expect(rec.envelope.rank).toBe(2);
161
+ }
162
+ );
163
+ clockValue = 2000;
164
+ expect(await store.put(makeRecord({ id: 'doc-a', body: 'abcdef' }))).toSucceedAndSatisfy(
165
+ (rec: IMemoryRecord<unknown>) => {
166
+ expect(rec.envelope.rank).toBe(6);
167
+ expect(rec.envelope.updated).toBe(2000);
168
+ }
169
+ );
170
+ });
171
+ });
172
+
173
+ describe('throwing projector degrades to rank-absent', () => {
174
+ test('logs a warn and leaves rank absent without failing the write', async () => {
175
+ const logger = new Logging.InMemoryLogger();
176
+ const store = createStore({
177
+ rankProjectors: new Map<Kind, RankProjector>([[knowledgeKind, throwingProjector]]),
178
+ logger
179
+ }).orThrow();
180
+ expect(await store.put(makeRecord({ id: 'doc-a', body: 'abcd' }))).toSucceedAndSatisfy(
181
+ (rec: IMemoryRecord<unknown>) => {
182
+ expect(rec.envelope.rank).toBeUndefined();
183
+ }
184
+ );
185
+ expect(logger.logged.some((m) => /rank projector threw/i.test(m))).toBe(true);
186
+ });
187
+
188
+ test('an UPDATE whose projector throws CLEARS the prior rank (flat path staleness contract)', async () => {
189
+ let shouldThrow: boolean = false;
190
+ const flakyProjector: RankProjector = (record) => {
191
+ if (shouldThrow) {
192
+ throw new Error('projector boom');
193
+ }
194
+ return (record.body as string).length;
195
+ };
196
+ const store = createStore({
197
+ rankProjectors: new Map<Kind, RankProjector>([[knowledgeKind, flakyProjector]])
198
+ }).orThrow();
199
+ // First write succeeds → rank stamped to a nonzero value.
200
+ expect(await store.put(makeRecord({ id: 'doc-a', body: 'abcd' }))).toSucceedAndSatisfy(
201
+ (rec: IMemoryRecord<unknown>) => {
202
+ expect(rec.envelope.rank).toBe(4);
203
+ }
204
+ );
205
+ // Update with a body revision while the projector now throws → the prior
206
+ // rank (4) must NOT survive; the record is stamped rank-absent.
207
+ shouldThrow = true;
208
+ clockValue = 2000;
209
+ expect(await store.put(makeRecord({ id: 'doc-a', body: 'abcdefgh' }))).toSucceedAndSatisfy(
210
+ (rec: IMemoryRecord<unknown>) => {
211
+ expect(rec.envelope.rank).toBeUndefined();
212
+ expect(rec.envelope.updated).toBe(2000);
213
+ }
214
+ );
215
+ });
216
+
217
+ test('an UPDATE whose projector throws CLEARS the prior rank (versioned path staleness contract)', async () => {
218
+ let shouldThrow: boolean = false;
219
+ const flakyProjector: RankProjector = (record) => {
220
+ if (shouldThrow) {
221
+ throw new Error('projector boom');
222
+ }
223
+ return (record.body as string).length;
224
+ };
225
+ const store = createStore({
226
+ rankProjectors: new Map<Kind, RankProjector>([[factKind, flakyProjector]]),
227
+ writePolicies: temporalPolicies
228
+ }).orThrow();
229
+ expect(await store.put(makeRecord({ id: 'fact-1', kind: factKind, body: 'aa' }))).toSucceedAndSatisfy(
230
+ (rec: IMemoryRecord<unknown>) => {
231
+ expect(rec.envelope.rank).toBe(2);
232
+ }
233
+ );
234
+ shouldThrow = true;
235
+ clockValue = 2000;
236
+ // The new version merges over the current version (which carried rank 2);
237
+ // the throwing projector must clear it so the new version is rank-absent.
238
+ expect(
239
+ await store.put(makeRecord({ id: 'fact-1', kind: factKind, body: 'aaaaa' }))
240
+ ).toSucceedAndSatisfy((rec: IMemoryRecord<unknown>) => {
241
+ expect(rec.envelope.rank).toBeUndefined();
242
+ });
243
+ });
244
+ });
245
+
246
+ describe('put stamps rank (versioned / temporal path)', () => {
247
+ const factProjectors: ReadonlyMap<Kind, RankProjector> = new Map<Kind, RankProjector>([
248
+ [factKind, lengthProjector]
249
+ ]);
250
+
251
+ test('stamps rank on the first version and re-stamps on a subsequent version', async () => {
252
+ const store = createStore({
253
+ rankProjectors: factProjectors,
254
+ writePolicies: temporalPolicies
255
+ }).orThrow();
256
+ expect(await store.put(makeRecord({ id: 'fact-1', kind: factKind, body: 'aa' }))).toSucceedAndSatisfy(
257
+ (rec: IMemoryRecord<unknown>) => {
258
+ expect(rec.envelope.rank).toBe(2);
259
+ }
260
+ );
261
+ clockValue = 2000;
262
+ expect(
263
+ await store.put(makeRecord({ id: 'fact-1', kind: factKind, body: 'aaaaa' }))
264
+ ).toSucceedAndSatisfy((rec: IMemoryRecord<unknown>) => {
265
+ expect(rec.envelope.rank).toBe(5);
266
+ });
267
+ });
268
+
269
+ test('leaves rank absent on the versioned path when the kind has no projector', async () => {
270
+ const store = createStore({ writePolicies: temporalPolicies }).orThrow();
271
+ expect(await store.put(makeRecord({ id: 'fact-1', kind: factKind, body: 'aa' }))).toSucceedAndSatisfy(
272
+ (rec: IMemoryRecord<unknown>) => {
273
+ expect(rec.envelope.rank).toBeUndefined();
274
+ }
275
+ );
276
+ });
277
+ });
278
+
279
+ describe('persistence round-trip', () => {
280
+ test('rank serializes to frontmatter and reloads on rebuild (no recompute-on-load)', async () => {
281
+ const root = mutableRoot();
282
+ const store = createStore({ root, rankProjectors: knowledgeProjectors }).orThrow();
283
+ await store.put(makeRecord({ id: 'doc-a', body: 'abcd' }));
284
+
285
+ // Read the raw persisted file and confirm rank is in the frontmatter.
286
+ const scopeDir = root
287
+ .getChildren()
288
+ .orThrow()
289
+ .find((c): c is FileTree.IFileTreeDirectoryItem => c.type === 'directory' && c.name === 'knowledge');
290
+ expect(scopeDir).toBeDefined();
291
+ const file = scopeDir!
292
+ .getChildren()
293
+ .orThrow()
294
+ .find((c): c is FileTree.IFileTreeFileItem => c.type === 'file' && c.name === 'doc-a.md');
295
+ expect(file).toBeDefined();
296
+ const raw = file!.getRawContents().orThrow();
297
+ expect(parseMemoryFile(raw, registry())).toSucceedAndSatisfy((rec: IMemoryRecord<unknown>) => {
298
+ expect(rec.envelope.rank).toBe(4);
299
+ });
300
+
301
+ // A brand-new store over the same root reloads rank from frontmatter WITHOUT any projector wired.
302
+ const reloaded = createStore({ root }).orThrow();
303
+ expect(await reloaded.getById('knowledge' as MemoryScopeKey, 'doc-a' as never)).toSucceedAndSatisfy(
304
+ (rec: IMemoryRecord<unknown> | undefined) => {
305
+ expect(rec?.envelope.rank).toBe(4);
306
+ }
307
+ );
308
+ });
309
+ });
310
+
311
+ describe('orderBy: rank retrieval (store-stamped ranks flow end-to-end)', () => {
312
+ async function indexFromStore(store: FileTreeMemoryStore): Promise<MemoryIndex> {
313
+ const records = (await store.list()).orThrow();
314
+ const index = MemoryIndex.create().orThrow();
315
+ // Scope is not returned by `list`; a per-id synthetic scope keeps the
316
+ // (scope, id) index keys distinct, which is all ordering needs here.
317
+ index
318
+ .rebuild(
319
+ records.map((record) => ({ scope: record.envelope.id as unknown as MemoryScopeKey, record }))
320
+ )
321
+ .orThrow();
322
+ return index;
323
+ }
324
+
325
+ test('orders by store-stamped rank descending, honoring kinds + limit + offset', async () => {
326
+ const store = createStore({ rankProjectors: knowledgeProjectors }).orThrow();
327
+ // ranks: a=1, bbb=3, cc=2 (by body length)
328
+ await store.put(makeRecord({ id: 'a', body: 'a' }));
329
+ await store.put(makeRecord({ id: 'bbb', body: 'bbb' }));
330
+ await store.put(makeRecord({ id: 'cc', body: 'cc' }));
331
+ // A plain-kind record has no projector → rank absent → sorts last.
332
+ await store.put(makeRecord({ id: 'p', kind: plainKind, body: 'zzzz' }));
333
+
334
+ const retriever = RecencyRetriever.create(await indexFromStore(store)).orThrow();
335
+ expect(await retriever.retrieve({ orderBy: 'rank' })).toSucceedAndSatisfy(
336
+ (records: ReadonlyArray<IMemoryRecord<unknown>>) => {
337
+ expect(records.map((r) => r.envelope.id)).toEqual(['bbb', 'cc', 'a', 'p']);
338
+ }
339
+ );
340
+
341
+ // Kind-set scoped, rank-ordered, paged: skip the top, take one.
342
+ expect(
343
+ await retriever.retrieve({ kinds: [knowledgeKind], orderBy: 'rank', limit: 1, offset: 1 })
344
+ ).toSucceedAndSatisfy((records: ReadonlyArray<IMemoryRecord<unknown>>) => {
345
+ expect(records.map((r) => r.envelope.id)).toEqual(['cc']);
346
+ });
347
+ });
348
+ });
349
+ });