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

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 (82) hide show
  1. package/.rush/temp/{cbbdbe09515171b4eba8f2592be2dace1e4e8142.tar.log → 285f03271c27ef724d49e730c0db58d9e4ac1a44.tar.log} +38 -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/retrieve/fragmentSemanticRetriever.js +78 -0
  7. package/dist/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -0
  8. package/dist/packlets/retrieve/index.js +1 -0
  9. package/dist/packlets/retrieve/index.js.map +1 -1
  10. package/dist/packlets/store/fileTreeMemoryStore.js +105 -14
  11. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  12. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +200 -0
  13. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -0
  14. package/dist/packlets/vector/index.js +1 -0
  15. package/dist/packlets/vector/index.js.map +1 -1
  16. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  17. package/dist/test/unit/retrieve/fragmentSemanticRetriever.test.js +116 -0
  18. package/dist/test/unit/retrieve/fragmentSemanticRetriever.test.js.map +1 -0
  19. package/dist/test/unit/store/fragmentEmbedOnWrite.test.js +255 -0
  20. package/dist/test/unit/store/fragmentEmbedOnWrite.test.js.map +1 -0
  21. package/dist/test/unit/store/lenientOpen.test.js +248 -0
  22. package/dist/test/unit/store/lenientOpen.test.js.map +1 -0
  23. package/dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js +297 -0
  24. package/dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js.map +1 -0
  25. package/dist/ts-agent-memory.d.ts +367 -4
  26. package/etc/ts-agent-memory.api.md +78 -0
  27. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts +90 -0
  28. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts.map +1 -0
  29. package/lib/packlets/retrieve/fragmentSemanticRetriever.js +82 -0
  30. package/lib/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -0
  31. package/lib/packlets/retrieve/index.d.ts +1 -0
  32. package/lib/packlets/retrieve/index.d.ts.map +1 -1
  33. package/lib/packlets/retrieve/index.js +1 -0
  34. package/lib/packlets/retrieve/index.js.map +1 -1
  35. package/lib/packlets/store/fileTreeMemoryStore.d.ts +115 -1
  36. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  37. package/lib/packlets/store/fileTreeMemoryStore.js +104 -13
  38. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  39. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +74 -0
  40. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -0
  41. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +204 -0
  42. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -0
  43. package/lib/packlets/vector/index.d.ts +1 -0
  44. package/lib/packlets/vector/index.d.ts.map +1 -1
  45. package/lib/packlets/vector/index.js +1 -0
  46. package/lib/packlets/vector/index.js.map +1 -1
  47. package/lib/packlets/vector/vectorIndex.d.ts +85 -4
  48. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  49. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  50. package/lib/test/unit/retrieve/fragmentSemanticRetriever.test.d.ts +2 -0
  51. package/lib/test/unit/retrieve/fragmentSemanticRetriever.test.d.ts.map +1 -0
  52. package/lib/test/unit/retrieve/fragmentSemanticRetriever.test.js +118 -0
  53. package/lib/test/unit/retrieve/fragmentSemanticRetriever.test.js.map +1 -0
  54. package/lib/test/unit/store/fragmentEmbedOnWrite.test.d.ts +2 -0
  55. package/lib/test/unit/store/fragmentEmbedOnWrite.test.d.ts.map +1 -0
  56. package/lib/test/unit/store/fragmentEmbedOnWrite.test.js +257 -0
  57. package/lib/test/unit/store/fragmentEmbedOnWrite.test.js.map +1 -0
  58. package/lib/test/unit/store/lenientOpen.test.d.ts +2 -0
  59. package/lib/test/unit/store/lenientOpen.test.d.ts.map +1 -0
  60. package/lib/test/unit/store/lenientOpen.test.js +250 -0
  61. package/lib/test/unit/store/lenientOpen.test.js.map +1 -0
  62. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.d.ts +2 -0
  63. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.d.ts.map +1 -0
  64. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.js +299 -0
  65. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.js.map +1 -0
  66. package/package.json +7 -7
  67. package/rush-logs/ts-agent-memory.build.cache.log +1 -1
  68. package/rush-logs/ts-agent-memory.build.log +2 -2
  69. package/src/packlets/retrieve/fragmentSemanticRetriever.ts +135 -0
  70. package/src/packlets/retrieve/index.ts +1 -0
  71. package/src/packlets/store/fileTreeMemoryStore.ts +208 -16
  72. package/src/packlets/vector/inMemoryFragmentCosineIndex.ts +262 -0
  73. package/src/packlets/vector/index.ts +1 -0
  74. package/src/packlets/vector/vectorIndex.ts +97 -4
  75. package/src/test/unit/retrieve/fragmentSemanticRetriever.test.ts +163 -0
  76. package/src/test/unit/store/fragmentEmbedOnWrite.test.ts +349 -0
  77. package/src/test/unit/store/lenientOpen.test.ts +292 -0
  78. package/src/test/unit/vector/inMemoryFragmentCosineIndex.test.ts +389 -0
  79. package/temp/build/lint/_eslint-5eVG3S6w.json +29 -5
  80. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  81. package/temp/ts-agent-memory.api.json +4584 -2859
  82. package/temp/ts-agent-memory.api.md +78 -0
@@ -0,0 +1,255 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ import '@fgv/ts-utils-jest';
6
+ import { Converters, Logging, fail, succeed } from '@fgv/ts-utils';
7
+ import { FileTree } from '@fgv/ts-json-base';
8
+ import { BodyConverterRegistry, FileTreeMemoryStore, FragmentSemanticRetriever, InMemoryFragmentCosineIndex, KnowledgeIdentityCodec, MemoryCapCullPolicy, MtmIdentityCodec, envelopeConverter } from '../../../index';
9
+ const knowledgeKind = 'knowledge';
10
+ const mtmKind = 'mtm';
11
+ function mutableRoot() {
12
+ const tree = FileTree.inMemory([], { mutable: true }).orThrow();
13
+ const root = tree.getDirectory('/').orThrow();
14
+ if (!FileTree.isMutableDirectoryItem(root)) {
15
+ throw new Error('expected a mutable root directory');
16
+ }
17
+ return root;
18
+ }
19
+ function makeRecord(id, body, kind = 'knowledge', entityId) {
20
+ return {
21
+ envelope: envelopeConverter
22
+ .convert({
23
+ id,
24
+ entityId: entityId !== null && entityId !== void 0 ? entityId : id,
25
+ kind,
26
+ tags: [],
27
+ links: [],
28
+ created: 0,
29
+ updated: 0,
30
+ seq: 0,
31
+ contentHash: '',
32
+ provenance: { source: 'agent' }
33
+ })
34
+ .orThrow(),
35
+ body
36
+ };
37
+ }
38
+ /** Deterministic keyword-count feature space (no network), shared with the query side. */
39
+ const MARKERS = ['cat', 'dog', 'fish'];
40
+ function featureVector(text) {
41
+ const lower = text.toLowerCase();
42
+ return Float32Array.from(MARKERS.map((m) => lower.split(m).length - 1));
43
+ }
44
+ /**
45
+ * Fragment embedder: chunks the body into whitespace-delimited words, emitting one
46
+ * fragment per word with its true `[start, end)` character offsets and a feature
47
+ * vector for that word. Models the consumer-owned chunking policy.
48
+ */
49
+ const fragmentEmbed = (r) => Promise.resolve(Converters.string
50
+ .convert(r.body)
51
+ .withErrorFormat((msg) => `cannot embed fragments: body is not a string: ${msg}`)
52
+ .onSuccess((body) => {
53
+ const fragments = [];
54
+ const re = /\S+/g;
55
+ let m = re.exec(body);
56
+ while (m !== null) {
57
+ fragments.push({
58
+ locator: { start: m.index, end: m.index + m[0].length },
59
+ vector: featureVector(m[0])
60
+ });
61
+ m = re.exec(body);
62
+ }
63
+ return succeed(fragments);
64
+ }));
65
+ const queryEmbed = (text) => Promise.resolve(succeed(featureVector(text)));
66
+ /** Records addFragments/remove call order and can be configured to fail either op. */
67
+ class SpyFragmentIndex {
68
+ constructor() {
69
+ this.calls = [];
70
+ this.failAdd = false;
71
+ this.failRemove = false;
72
+ this._inner = InMemoryFragmentCosineIndex.create().orThrow();
73
+ }
74
+ async addFragments(target, fragments) {
75
+ this.calls.push(`add:${target.id}:${fragments.length}`);
76
+ return this.failAdd ? fail('add boom') : this._inner.addFragments(target, fragments);
77
+ }
78
+ async remove(target) {
79
+ this.calls.push(`remove:${target.id}`);
80
+ return this.failRemove ? fail('remove boom') : this._inner.remove(target);
81
+ }
82
+ query(vector, topK, maxPerRecord) {
83
+ return this._inner.query(vector, topK, maxPerRecord);
84
+ }
85
+ }
86
+ function knowledgeStore(params) {
87
+ const registry = BodyConverterRegistry.create().orThrow();
88
+ registry.register(knowledgeKind, Converters.string);
89
+ return FileTreeMemoryStore.create({
90
+ root: mutableRoot(),
91
+ registry,
92
+ codecs: new Map([[knowledgeKind, new KnowledgeIdentityCodec()]]),
93
+ fragmentIndex: params.fragmentIndex,
94
+ fragmentEmbedder: params.fragmentEmbedder,
95
+ logger: params.logger
96
+ }).orThrow();
97
+ }
98
+ describe('FileTreeMemoryStore fragment-embed-on-write', () => {
99
+ describe('when wired', () => {
100
+ test('embeds fragments on write and indexes them (queryable by span)', async () => {
101
+ const index = InMemoryFragmentCosineIndex.create().orThrow();
102
+ const store = knowledgeStore({ fragmentIndex: index, fragmentEmbedder: fragmentEmbed });
103
+ expect(await store.put(makeRecord('doc-a', 'cat dog'))).toSucceed();
104
+ // Two words → two fragments.
105
+ expect(index.recordCount).toBe(1);
106
+ expect(index.fragmentCount).toBe(2);
107
+ // A query for 'cat' surfaces the 'cat' span [0,3], not the 'dog' span.
108
+ expect(await index.query(featureVector('cat'), 1)).toSucceedAndSatisfy((hits) => {
109
+ expect(hits[0].target.id).toBe('doc-a');
110
+ expect(hits[0].locator).toEqual({ start: 0, end: 3 });
111
+ });
112
+ });
113
+ test('stamps nothing on the record (fragments have no per-record embeddingRef)', async () => {
114
+ const index = InMemoryFragmentCosineIndex.create().orThrow();
115
+ const store = knowledgeStore({ fragmentIndex: index, fragmentEmbedder: fragmentEmbed });
116
+ expect(await store.put(makeRecord('doc-a', 'cat dog'))).toSucceedAndSatisfy((record) => {
117
+ expect(record.envelope.embeddingRef).toBeUndefined();
118
+ });
119
+ });
120
+ test('re-embeds on a content change via whole-record replace', async () => {
121
+ const spy = new SpyFragmentIndex();
122
+ const store = knowledgeStore({ fragmentIndex: spy, fragmentEmbedder: fragmentEmbed });
123
+ (await store.put(makeRecord('doc-a', 'cat'))).orThrow();
124
+ (await store.put(makeRecord('doc-a', 'dog fish'))).orThrow();
125
+ // First write: 1 fragment; second: 2 fragments, replacing the first.
126
+ expect(spy.calls).toEqual(['add:doc-a:1', 'add:doc-a:2']);
127
+ // The 'cat' fragment is gone — the new 'dog'/'fish' spans are orthogonal to a
128
+ // 'cat' query (best score 0), proving the whole-record replace dropped it.
129
+ expect(await spy.query(featureVector('cat'), 5)).toSucceedAndSatisfy((hits) => {
130
+ expect(hits.every((h) => h.score === 0)).toBe(true);
131
+ });
132
+ expect(await spy.query(featureVector('fish'), 1)).toSucceedAndSatisfy((hits) => {
133
+ expect(hits[0].score).toBeCloseTo(1);
134
+ });
135
+ });
136
+ test('does not re-embed a dedup no-op (identical content)', async () => {
137
+ const spy = new SpyFragmentIndex();
138
+ const store = knowledgeStore({ fragmentIndex: spy, fragmentEmbedder: fragmentEmbed });
139
+ (await store.put(makeRecord('doc-a', 'cat'))).orThrow();
140
+ (await store.put(makeRecord('doc-a', 'cat'))).orThrow();
141
+ expect(spy.calls).toEqual(['add:doc-a:1']);
142
+ });
143
+ test('removes the fragments on delete', async () => {
144
+ const spy = new SpyFragmentIndex();
145
+ const store = knowledgeStore({ fragmentIndex: spy, fragmentEmbedder: fragmentEmbed });
146
+ (await store.put(makeRecord('doc-a', 'cat dog'))).orThrow();
147
+ expect(await store.delete(knowledgeKind, 'doc-a')).toSucceedWith('doc-a');
148
+ expect(spy.calls).toEqual(['add:doc-a:2', 'remove:doc-a']);
149
+ expect(await spy.query(featureVector('cat'), 5)).toSucceedWith([]);
150
+ });
151
+ test('removes evicted fragments on cap-cull eviction', async () => {
152
+ const index = InMemoryFragmentCosineIndex.create().orThrow();
153
+ const capCull = MemoryCapCullPolicy.create({
154
+ maxRecords: 2,
155
+ mutableFields: ['body', 'tags', 'links', 'provenance', 'embeddingRef']
156
+ }).orThrow();
157
+ const registry = BodyConverterRegistry.create().orThrow();
158
+ registry.register(mtmKind, Converters.string);
159
+ const store = FileTreeMemoryStore.create({
160
+ root: mutableRoot(),
161
+ registry,
162
+ codecs: new Map([[mtmKind, new MtmIdentityCodec()]]),
163
+ writePolicies: new Map([[mtmKind, capCull]]),
164
+ fragmentIndex: index,
165
+ fragmentEmbedder: fragmentEmbed
166
+ }).orThrow();
167
+ (await store.put(makeRecord('turn-0', 'cat', 'mtm', 'conv-1:0'))).orThrow();
168
+ (await store.put(makeRecord('turn-1', 'dog', 'mtm', 'conv-1:1'))).orThrow();
169
+ (await store.put(makeRecord('turn-2', 'fish', 'mtm', 'conv-1:2'))).orThrow();
170
+ // turn-0 evicted from the store AND its fragments from the index.
171
+ expect(index.recordCount).toBe(2);
172
+ expect(await index.query(featureVector('cat'), 5)).toSucceedAndSatisfy((hits) => {
173
+ expect(hits.map((h) => h.target.id)).not.toContain('turn-0');
174
+ });
175
+ });
176
+ test('persists the record (best-effort) and logs when fragment embedding fails', async () => {
177
+ const index = InMemoryFragmentCosineIndex.create().orThrow();
178
+ const logger = new Logging.InMemoryLogger();
179
+ const failEmbed = () => Promise.resolve(fail('no model'));
180
+ const store = knowledgeStore({ fragmentIndex: index, fragmentEmbedder: failEmbed, logger });
181
+ expect(await store.put(makeRecord('doc-a', 'cat'))).toSucceed();
182
+ expect(await store.getById('knowledge', 'doc-a')).toSucceedAndSatisfy((record) => {
183
+ expect(record === null || record === void 0 ? void 0 : record.envelope.id).toBe('doc-a');
184
+ });
185
+ expect(index.recordCount).toBe(0);
186
+ expect(logger.logged.some((m) => /fragment embedding 'doc-a' failed.*no model/i.test(m))).toBe(true);
187
+ });
188
+ test('persists the record (best-effort) and logs when the fragment add fails', async () => {
189
+ const spy = new SpyFragmentIndex();
190
+ spy.failAdd = true;
191
+ const logger = new Logging.InMemoryLogger();
192
+ const store = knowledgeStore({ fragmentIndex: spy, fragmentEmbedder: fragmentEmbed, logger });
193
+ expect(await store.put(makeRecord('doc-a', 'cat'))).toSucceed();
194
+ expect(await store.getById('knowledge', 'doc-a')).toSucceedAndSatisfy((record) => {
195
+ expect(record === null || record === void 0 ? void 0 : record.envelope.id).toBe('doc-a');
196
+ });
197
+ expect(logger.logged.some((m) => /fragment add for 'doc-a' failed.*add boom/i.test(m))).toBe(true);
198
+ });
199
+ test('best-effort embed survives a throwing/rejecting fragment embedder', async () => {
200
+ const index = InMemoryFragmentCosineIndex.create().orThrow();
201
+ const logger = new Logging.InMemoryLogger();
202
+ const throwingEmbed = () => Promise.reject(new Error('embedder kaboom'));
203
+ const store = knowledgeStore({ fragmentIndex: index, fragmentEmbedder: throwingEmbed, logger });
204
+ expect(await store.put(makeRecord('doc-a', 'cat'))).toSucceed();
205
+ expect(index.recordCount).toBe(0);
206
+ expect(logger.logged.some((m) => /fragment embedding 'doc-a' threw.*embedder kaboom/i.test(m))).toBe(true);
207
+ });
208
+ test('a committed delete succeeds (best-effort) and logs when fragment removal fails', async () => {
209
+ const spy = new SpyFragmentIndex();
210
+ const logger = new Logging.InMemoryLogger();
211
+ const store = knowledgeStore({ fragmentIndex: spy, fragmentEmbedder: fragmentEmbed, logger });
212
+ (await store.put(makeRecord('doc-a', 'cat'))).orThrow();
213
+ spy.failRemove = true;
214
+ expect(await store.delete(knowledgeKind, 'doc-a')).toSucceedWith('doc-a');
215
+ expect(await store.getById('knowledge', 'doc-a')).toSucceedWith(undefined);
216
+ expect(logger.logged.some((m) => /fragment removal for 'doc-a' failed.*remove boom/i.test(m))).toBe(true);
217
+ });
218
+ });
219
+ describe('when unwired', () => {
220
+ test('a store without a fragment index or embedder does no fragment work', async () => {
221
+ const store = knowledgeStore({});
222
+ expect(await store.put(makeRecord('doc-a', 'cat'))).toSucceedAndSatisfy((record) => {
223
+ expect(record.envelope.embeddingRef).toBeUndefined();
224
+ });
225
+ });
226
+ test('a fragment index without an embedder is fully inert (no add or remove)', async () => {
227
+ const spy = new SpyFragmentIndex();
228
+ const store = knowledgeStore({ fragmentIndex: spy });
229
+ (await store.put(makeRecord('doc-a', 'cat'))).orThrow();
230
+ (await store.delete(knowledgeKind, 'doc-a')).orThrow();
231
+ expect(spy.calls).toEqual([]);
232
+ });
233
+ });
234
+ describe('fragment discovery end-to-end', () => {
235
+ test('a wired store + InMemoryFragmentCosineIndex + FragmentSemanticRetriever returns per-span hits', async () => {
236
+ const index = InMemoryFragmentCosineIndex.create().orThrow();
237
+ const store = knowledgeStore({ fragmentIndex: index, fragmentEmbedder: fragmentEmbed });
238
+ (await store.put(makeRecord('doc-a', 'cat cat dog'))).orThrow();
239
+ (await store.put(makeRecord('doc-b', 'fish fish'))).orThrow();
240
+ const retriever = FragmentSemanticRetriever.create({
241
+ backend: { fragmentIndex: index, embedQuery: queryEmbed }
242
+ }).orThrow();
243
+ expect(retriever.capabilities.supportsFragmentRecall).toBe(true);
244
+ // maxPerRecord=1 keeps doc-a's two 'cat' spans from crowding out the field.
245
+ expect(await retriever.retrieve({ semantic: 'cat', topK: 5, maxPerRecord: 1 })).toSucceedAndSatisfy((hits) => {
246
+ // The top hit is a 'cat' span in doc-a; the doc-a cap is honored.
247
+ expect(hits[0].target.id).toBe('doc-a');
248
+ expect(hits[0].locator).toEqual({ start: 0, end: 3 });
249
+ const perRecord = hits.filter((h) => h.target.id === 'doc-a');
250
+ expect(perRecord).toHaveLength(1);
251
+ });
252
+ });
253
+ });
254
+ });
255
+ //# sourceMappingURL=fragmentEmbedOnWrite.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fragmentEmbedOnWrite.test.js","sourceRoot":"","sources":["../../../../src/test/unit/store/fragmentEmbedOnWrite.test.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport '@fgv/ts-utils-jest';\nimport { Converters, Logging, Result, fail, succeed } from '@fgv/ts-utils';\nimport { FileTree } from '@fgv/ts-json-base';\nimport {\n BodyConverterRegistry,\n EntityId,\n FileTreeMemoryStore,\n FragmentSemanticRetriever,\n IBodyConverterRegistry,\n IEdgeTarget,\n IEmbeddedFragment,\n IFragmentVectorIndex,\n IIdentityCodec,\n IMemoryRecord,\n IVectorQueryHit,\n InMemoryFragmentCosineIndex,\n Kind,\n KnowledgeIdentityCodec,\n MemoryCapCullPolicy,\n MemoryId,\n MemoryScopeKey,\n MtmIdentityCodec,\n IWritePolicy,\n envelopeConverter\n} from '../../../index';\n\nconst knowledgeKind: Kind = 'knowledge' as Kind;\nconst mtmKind: Kind = 'mtm' as Kind;\n\nfunction mutableRoot(): FileTree.IMutableFileTreeDirectoryItem {\n const tree = FileTree.inMemory([], { mutable: true }).orThrow();\n const root = tree.getDirectory('/').orThrow();\n if (!FileTree.isMutableDirectoryItem(root)) {\n throw new Error('expected a mutable root directory');\n }\n return root;\n}\n\nfunction makeRecord(\n id: string,\n body: string,\n kind: string = 'knowledge',\n entityId?: string\n): IMemoryRecord<unknown> {\n return {\n envelope: envelopeConverter\n .convert({\n id,\n entityId: entityId ?? id,\n kind,\n tags: [],\n links: [],\n created: 0,\n updated: 0,\n seq: 0,\n contentHash: '',\n provenance: { source: 'agent' }\n })\n .orThrow(),\n body\n };\n}\n\n/** Deterministic keyword-count feature space (no network), shared with the query side. */\nconst MARKERS: ReadonlyArray<string> = ['cat', 'dog', 'fish'];\nfunction featureVector(text: string): Float32Array {\n const lower: string = text.toLowerCase();\n return Float32Array.from(MARKERS.map((m) => lower.split(m).length - 1));\n}\n\n/**\n * Fragment embedder: chunks the body into whitespace-delimited words, emitting one\n * fragment per word with its true `[start, end)` character offsets and a feature\n * vector for that word. Models the consumer-owned chunking policy.\n */\nconst fragmentEmbed = (r: IMemoryRecord<unknown>): Promise<Result<ReadonlyArray<IEmbeddedFragment>>> =>\n Promise.resolve(\n Converters.string\n .convert(r.body)\n .withErrorFormat((msg) => `cannot embed fragments: body is not a string: ${msg}`)\n .onSuccess((body) => {\n const fragments: IEmbeddedFragment[] = [];\n const re: RegExp = /\\S+/g;\n let m: RegExpExecArray | null = re.exec(body);\n while (m !== null) {\n fragments.push({\n locator: { start: m.index, end: m.index + m[0].length },\n vector: featureVector(m[0])\n });\n m = re.exec(body);\n }\n return succeed(fragments);\n })\n );\n\nconst queryEmbed = (text: string): Promise<Result<Float32Array>> =>\n Promise.resolve(succeed(featureVector(text)));\n\n/** Records addFragments/remove call order and can be configured to fail either op. */\nclass SpyFragmentIndex implements IFragmentVectorIndex {\n public readonly calls: string[] = [];\n public failAdd: boolean = false;\n public failRemove: boolean = false;\n private readonly _inner: InMemoryFragmentCosineIndex = InMemoryFragmentCosineIndex.create().orThrow();\n\n public async addFragments(\n target: IEdgeTarget,\n fragments: ReadonlyArray<IEmbeddedFragment>\n ): Promise<Result<number>> {\n this.calls.push(`add:${target.id}:${fragments.length}`);\n return this.failAdd ? fail('add boom') : this._inner.addFragments(target, fragments);\n }\n public async remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>> {\n this.calls.push(`remove:${target.id}`);\n return this.failRemove ? fail('remove boom') : this._inner.remove(target);\n }\n public query(\n vector: Float32Array,\n topK: number,\n maxPerRecord?: number\n ): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n return this._inner.query(vector, topK, maxPerRecord);\n }\n}\n\nfunction knowledgeStore(params: {\n fragmentIndex?: IFragmentVectorIndex;\n fragmentEmbedder?: (r: IMemoryRecord<unknown>) => Promise<Result<ReadonlyArray<IEmbeddedFragment>>>;\n logger?: Logging.ILogger;\n}): FileTreeMemoryStore {\n const registry: IBodyConverterRegistry = BodyConverterRegistry.create().orThrow();\n registry.register(knowledgeKind, Converters.string);\n return FileTreeMemoryStore.create({\n root: mutableRoot(),\n registry,\n codecs: new Map<Kind, IIdentityCodec>([[knowledgeKind, new KnowledgeIdentityCodec()]]),\n fragmentIndex: params.fragmentIndex,\n fragmentEmbedder: params.fragmentEmbedder,\n logger: params.logger\n }).orThrow();\n}\n\ndescribe('FileTreeMemoryStore fragment-embed-on-write', () => {\n describe('when wired', () => {\n test('embeds fragments on write and indexes them (queryable by span)', async () => {\n const index = InMemoryFragmentCosineIndex.create().orThrow();\n const store = knowledgeStore({ fragmentIndex: index, fragmentEmbedder: fragmentEmbed });\n expect(await store.put(makeRecord('doc-a', 'cat dog'))).toSucceed();\n // Two words → two fragments.\n expect(index.recordCount).toBe(1);\n expect(index.fragmentCount).toBe(2);\n // A query for 'cat' surfaces the 'cat' span [0,3], not the 'dog' span.\n expect(await index.query(featureVector('cat'), 1)).toSucceedAndSatisfy(\n (hits: ReadonlyArray<IVectorQueryHit>) => {\n expect(hits[0].target.id).toBe('doc-a');\n expect(hits[0].locator).toEqual({ start: 0, end: 3 });\n }\n );\n });\n\n test('stamps nothing on the record (fragments have no per-record embeddingRef)', async () => {\n const index = InMemoryFragmentCosineIndex.create().orThrow();\n const store = knowledgeStore({ fragmentIndex: index, fragmentEmbedder: fragmentEmbed });\n expect(await store.put(makeRecord('doc-a', 'cat dog'))).toSucceedAndSatisfy(\n (record: IMemoryRecord<unknown>) => {\n expect(record.envelope.embeddingRef).toBeUndefined();\n }\n );\n });\n\n test('re-embeds on a content change via whole-record replace', async () => {\n const spy = new SpyFragmentIndex();\n const store = knowledgeStore({ fragmentIndex: spy, fragmentEmbedder: fragmentEmbed });\n (await store.put(makeRecord('doc-a', 'cat'))).orThrow();\n (await store.put(makeRecord('doc-a', 'dog fish'))).orThrow();\n // First write: 1 fragment; second: 2 fragments, replacing the first.\n expect(spy.calls).toEqual(['add:doc-a:1', 'add:doc-a:2']);\n // The 'cat' fragment is gone — the new 'dog'/'fish' spans are orthogonal to a\n // 'cat' query (best score 0), proving the whole-record replace dropped it.\n expect(await spy.query(featureVector('cat'), 5)).toSucceedAndSatisfy(\n (hits: ReadonlyArray<IVectorQueryHit>) => {\n expect(hits.every((h) => h.score === 0)).toBe(true);\n }\n );\n expect(await spy.query(featureVector('fish'), 1)).toSucceedAndSatisfy(\n (hits: ReadonlyArray<IVectorQueryHit>) => {\n expect(hits[0].score).toBeCloseTo(1);\n }\n );\n });\n\n test('does not re-embed a dedup no-op (identical content)', async () => {\n const spy = new SpyFragmentIndex();\n const store = knowledgeStore({ fragmentIndex: spy, fragmentEmbedder: fragmentEmbed });\n (await store.put(makeRecord('doc-a', 'cat'))).orThrow();\n (await store.put(makeRecord('doc-a', 'cat'))).orThrow();\n expect(spy.calls).toEqual(['add:doc-a:1']);\n });\n\n test('removes the fragments on delete', async () => {\n const spy = new SpyFragmentIndex();\n const store = knowledgeStore({ fragmentIndex: spy, fragmentEmbedder: fragmentEmbed });\n (await store.put(makeRecord('doc-a', 'cat dog'))).orThrow();\n expect(await store.delete(knowledgeKind, 'doc-a' as unknown as EntityId)).toSucceedWith(\n 'doc-a' as MemoryId\n );\n expect(spy.calls).toEqual(['add:doc-a:2', 'remove:doc-a']);\n expect(await spy.query(featureVector('cat'), 5)).toSucceedWith([]);\n });\n\n test('removes evicted fragments on cap-cull eviction', async () => {\n const index = InMemoryFragmentCosineIndex.create().orThrow();\n const capCull: IWritePolicy = MemoryCapCullPolicy.create({\n maxRecords: 2,\n mutableFields: ['body', 'tags', 'links', 'provenance', 'embeddingRef']\n }).orThrow();\n const registry: IBodyConverterRegistry = BodyConverterRegistry.create().orThrow();\n registry.register(mtmKind, Converters.string);\n const store = FileTreeMemoryStore.create({\n root: mutableRoot(),\n registry,\n codecs: new Map<Kind, IIdentityCodec>([[mtmKind, new MtmIdentityCodec()]]),\n writePolicies: new Map<Kind, IWritePolicy>([[mtmKind, capCull]]),\n fragmentIndex: index,\n fragmentEmbedder: fragmentEmbed\n }).orThrow();\n\n (await store.put(makeRecord('turn-0', 'cat', 'mtm', 'conv-1:0'))).orThrow();\n (await store.put(makeRecord('turn-1', 'dog', 'mtm', 'conv-1:1'))).orThrow();\n (await store.put(makeRecord('turn-2', 'fish', 'mtm', 'conv-1:2'))).orThrow();\n // turn-0 evicted from the store AND its fragments from the index.\n expect(index.recordCount).toBe(2);\n expect(await index.query(featureVector('cat'), 5)).toSucceedAndSatisfy(\n (hits: ReadonlyArray<IVectorQueryHit>) => {\n expect(hits.map((h) => h.target.id)).not.toContain('turn-0');\n }\n );\n });\n\n test('persists the record (best-effort) and logs when fragment embedding fails', async () => {\n const index = InMemoryFragmentCosineIndex.create().orThrow();\n const logger = new Logging.InMemoryLogger();\n const failEmbed = (): Promise<Result<ReadonlyArray<IEmbeddedFragment>>> =>\n Promise.resolve(fail('no model'));\n const store = knowledgeStore({ fragmentIndex: index, fragmentEmbedder: failEmbed, logger });\n expect(await store.put(makeRecord('doc-a', 'cat'))).toSucceed();\n expect(await store.getById('knowledge' as MemoryScopeKey, 'doc-a' as MemoryId)).toSucceedAndSatisfy(\n (record: IMemoryRecord<unknown> | undefined) => {\n expect(record?.envelope.id).toBe('doc-a');\n }\n );\n expect(index.recordCount).toBe(0);\n expect(logger.logged.some((m) => /fragment embedding 'doc-a' failed.*no model/i.test(m))).toBe(true);\n });\n\n test('persists the record (best-effort) and logs when the fragment add fails', async () => {\n const spy = new SpyFragmentIndex();\n spy.failAdd = true;\n const logger = new Logging.InMemoryLogger();\n const store = knowledgeStore({ fragmentIndex: spy, fragmentEmbedder: fragmentEmbed, logger });\n expect(await store.put(makeRecord('doc-a', 'cat'))).toSucceed();\n expect(await store.getById('knowledge' as MemoryScopeKey, 'doc-a' as MemoryId)).toSucceedAndSatisfy(\n (record: IMemoryRecord<unknown> | undefined) => {\n expect(record?.envelope.id).toBe('doc-a');\n }\n );\n expect(logger.logged.some((m) => /fragment add for 'doc-a' failed.*add boom/i.test(m))).toBe(true);\n });\n\n test('best-effort embed survives a throwing/rejecting fragment embedder', async () => {\n const index = InMemoryFragmentCosineIndex.create().orThrow();\n const logger = new Logging.InMemoryLogger();\n const throwingEmbed = (): Promise<Result<ReadonlyArray<IEmbeddedFragment>>> =>\n Promise.reject(new Error('embedder kaboom'));\n const store = knowledgeStore({ fragmentIndex: index, fragmentEmbedder: throwingEmbed, logger });\n expect(await store.put(makeRecord('doc-a', 'cat'))).toSucceed();\n expect(index.recordCount).toBe(0);\n expect(logger.logged.some((m) => /fragment embedding 'doc-a' threw.*embedder kaboom/i.test(m))).toBe(\n true\n );\n });\n\n test('a committed delete succeeds (best-effort) and logs when fragment removal fails', async () => {\n const spy = new SpyFragmentIndex();\n const logger = new Logging.InMemoryLogger();\n const store = knowledgeStore({ fragmentIndex: spy, fragmentEmbedder: fragmentEmbed, logger });\n (await store.put(makeRecord('doc-a', 'cat'))).orThrow();\n spy.failRemove = true;\n expect(await store.delete(knowledgeKind, 'doc-a' as unknown as EntityId)).toSucceedWith(\n 'doc-a' as MemoryId\n );\n expect(await store.getById('knowledge' as MemoryScopeKey, 'doc-a' as MemoryId)).toSucceedWith(\n undefined\n );\n expect(logger.logged.some((m) => /fragment removal for 'doc-a' failed.*remove boom/i.test(m))).toBe(\n true\n );\n });\n });\n\n describe('when unwired', () => {\n test('a store without a fragment index or embedder does no fragment work', async () => {\n const store = knowledgeStore({});\n expect(await store.put(makeRecord('doc-a', 'cat'))).toSucceedAndSatisfy(\n (record: IMemoryRecord<unknown>) => {\n expect(record.envelope.embeddingRef).toBeUndefined();\n }\n );\n });\n\n test('a fragment index without an embedder is fully inert (no add or remove)', async () => {\n const spy = new SpyFragmentIndex();\n const store = knowledgeStore({ fragmentIndex: spy });\n (await store.put(makeRecord('doc-a', 'cat'))).orThrow();\n (await store.delete(knowledgeKind, 'doc-a' as unknown as EntityId)).orThrow();\n expect(spy.calls).toEqual([]);\n });\n });\n\n describe('fragment discovery end-to-end', () => {\n test('a wired store + InMemoryFragmentCosineIndex + FragmentSemanticRetriever returns per-span hits', async () => {\n const index = InMemoryFragmentCosineIndex.create().orThrow();\n const store = knowledgeStore({ fragmentIndex: index, fragmentEmbedder: fragmentEmbed });\n (await store.put(makeRecord('doc-a', 'cat cat dog'))).orThrow();\n (await store.put(makeRecord('doc-b', 'fish fish'))).orThrow();\n\n const retriever = FragmentSemanticRetriever.create({\n backend: { fragmentIndex: index, embedQuery: queryEmbed }\n }).orThrow();\n expect(retriever.capabilities.supportsFragmentRecall).toBe(true);\n\n // maxPerRecord=1 keeps doc-a's two 'cat' spans from crowding out the field.\n expect(await retriever.retrieve({ semantic: 'cat', topK: 5, maxPerRecord: 1 })).toSucceedAndSatisfy(\n (hits: ReadonlyArray<IVectorQueryHit>) => {\n // The top hit is a 'cat' span in doc-a; the doc-a cap is honored.\n expect(hits[0].target.id).toBe('doc-a');\n expect(hits[0].locator).toEqual({ start: 0, end: 3 });\n const perRecord = hits.filter((h) => h.target.id === 'doc-a');\n expect(perRecord).toHaveLength(1);\n }\n );\n });\n });\n});\n"]}
@@ -0,0 +1,248 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ import '@fgv/ts-utils-jest';
6
+ import { Converters, Logging, fail, succeed } from '@fgv/ts-utils';
7
+ import { FileTree } from '@fgv/ts-json-base';
8
+ import { BodyConverterRegistry, FileTreeMemoryStore, joinFrontmatter } from '../../../index';
9
+ /**
10
+ * The incident: one vault holds every kind (mtm, ltm, conversation, knowledge,
11
+ * claim) in ONE store. A required field was added to the `claim` body; every
12
+ * pre-existing `claim` failed validation; and because the initial walk collapsed
13
+ * per-record results with `mapResults` (fail-the-whole-array on any element),
14
+ * every OTHER valid record became unreadable behind the failed open. These tests
15
+ * reproduce that shape (a poison-rejecting body converter standing in for the
16
+ * migrated converter) and pin the lenient (`onRecordError: 'skip'`) behavior.
17
+ */
18
+ const noteKind = 'note';
19
+ const POISON = 'POISON';
20
+ /**
21
+ * A permissive identity codec that treats the full `<scope>/<stem>` path as the
22
+ * entity id, so records can live at any nesting depth (the shipped codecs pin a
23
+ * fixed scope shape; this test needs arbitrary-depth scopes to prove the
24
+ * recursion threads skips out of nested subtrees).
25
+ */
26
+ class PathIdentityCodec {
27
+ encode(entityId) {
28
+ const value = String(entityId);
29
+ const cut = value.lastIndexOf('/');
30
+ const scope = (cut >= 0 ? value.slice(0, cut) : '');
31
+ const idStem = cut >= 0 ? value.slice(cut + 1) : value;
32
+ return succeed({ scope, idStem, isVersioned: false });
33
+ }
34
+ decode(scope, encodedStem) {
35
+ return succeed(`${scope}/${encodedStem}`);
36
+ }
37
+ verifyRoundTrip() {
38
+ return succeed(true);
39
+ }
40
+ }
41
+ /** A body converter that rejects any body containing the poison marker. */
42
+ const poisonRejectingConverter = Converters.string.map((body) => body.includes(POISON) ? fail(`note body rejected: contains ${POISON}`) : succeed(body));
43
+ function registry(bodyConverter) {
44
+ const created = BodyConverterRegistry.create().orThrow();
45
+ created.register(noteKind, bodyConverter);
46
+ return created;
47
+ }
48
+ const codecs = new Map([
49
+ [noteKind, new PathIdentityCodec()]
50
+ ]);
51
+ /** Serialize a note record at `<scope>/<stem>.md` with the given body. */
52
+ function noteFile(scope, stem, body) {
53
+ const envelope = {
54
+ id: stem,
55
+ entityId: `${scope}/${stem}`,
56
+ kind: 'note',
57
+ tags: [],
58
+ links: [],
59
+ created: 1,
60
+ updated: 1,
61
+ seq: 4,
62
+ contentHash: 'seed',
63
+ provenance: { source: 'agent' }
64
+ };
65
+ const frontmatter = Object.entries(envelope)
66
+ .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
67
+ .join('\n');
68
+ return { path: `${scope}/${stem}.md`, contents: joinFrontmatter(frontmatter, body) };
69
+ }
70
+ function mutableRoot(files) {
71
+ const tree = FileTree.inMemory([...files], { mutable: true }).orThrow();
72
+ const root = tree.getDirectory('/').orThrow();
73
+ if (!FileTree.isMutableDirectoryItem(root)) {
74
+ throw new Error('expected a mutable root directory');
75
+ }
76
+ return root;
77
+ }
78
+ describe('FileTreeMemoryStore lenient open (onRecordError)', () => {
79
+ test('incident: one bad record does not make the whole store unopenable in skip mode', async () => {
80
+ const files = [
81
+ noteFile('mtm', 'turn-1', 'valid mtm body'),
82
+ noteFile('ltm', 'summary', 'valid ltm body'),
83
+ noteFile('knowledge', 'doc-a', 'valid knowledge body'),
84
+ // The migrated `claim` converter now rejects the pre-existing record.
85
+ noteFile('claim', 'claim-1', `${POISON} missing required field`)
86
+ ];
87
+ // Default (== 'fail') still fails the whole open, exactly as it did before.
88
+ expect(FileTreeMemoryStore.create({
89
+ root: mutableRoot(files),
90
+ registry: registry(poisonRejectingConverter),
91
+ codecs
92
+ })).toFailWith(/POISON/);
93
+ // 'skip' mode opens: every valid record loads; the bad one is quarantined.
94
+ const store = FileTreeMemoryStore.create({
95
+ root: mutableRoot(files),
96
+ registry: registry(poisonRejectingConverter),
97
+ codecs,
98
+ onRecordError: 'skip'
99
+ }).orThrow();
100
+ expect(await store.list()).toSucceedAndSatisfy((listed) => {
101
+ expect(listed.map((r) => r.envelope.id).sort()).toEqual(['doc-a', 'summary', 'turn-1']);
102
+ });
103
+ expect(store.skippedRecords).toHaveLength(1);
104
+ const skip = store.skippedRecords[0];
105
+ expect(skip.path).toBe('claim/claim-1.md');
106
+ expect(skip.scope).toBe('claim');
107
+ expect(skip.error).toContain('claim/claim-1.md');
108
+ expect(skip.error).toContain(POISON);
109
+ });
110
+ test('default mode is byte-identical to explicit fail mode', () => {
111
+ const files = [noteFile('note', 'good', 'ok'), noteFile('note', 'bad', POISON)];
112
+ const asDefault = FileTreeMemoryStore.create({
113
+ root: mutableRoot(files),
114
+ registry: registry(poisonRejectingConverter),
115
+ codecs
116
+ });
117
+ const asFail = FileTreeMemoryStore.create({
118
+ root: mutableRoot(files),
119
+ registry: registry(poisonRejectingConverter),
120
+ codecs,
121
+ onRecordError: 'fail'
122
+ });
123
+ expect(asDefault).toFail();
124
+ expect(asFail).toFail();
125
+ // Same exact failure message — the load path is untouched in fail mode.
126
+ expect(asDefault.isFailure() && asFail.isFailure() && asDefault.message === asFail.message).toBe(true);
127
+ expect(asDefault).toFailWith(/POISON/);
128
+ });
129
+ test('a bad record in a nested scope is skipped while valid siblings load', async () => {
130
+ const files = [
131
+ noteFile('alpha/beta', 'ok-1', 'valid'),
132
+ noteFile('alpha/beta', 'bad-1', POISON),
133
+ noteFile('alpha/beta/gamma', 'ok-2', 'valid deep')
134
+ ];
135
+ const store = FileTreeMemoryStore.create({
136
+ root: mutableRoot(files),
137
+ registry: registry(poisonRejectingConverter),
138
+ codecs,
139
+ onRecordError: 'skip'
140
+ }).orThrow();
141
+ expect(await store.list()).toSucceedAndSatisfy((listed) => {
142
+ expect(listed.map((r) => r.envelope.id).sort()).toEqual(['ok-1', 'ok-2']);
143
+ });
144
+ expect(store.skippedRecords.map((s) => s.path)).toEqual(['alpha/beta/bad-1.md']);
145
+ expect(store.skippedRecords[0].scope).toBe('alpha/beta');
146
+ });
147
+ test('multiple skips across different scopes all surface; all valid records load', async () => {
148
+ const files = [
149
+ noteFile('scope-a', 'good-a', 'a-ok'),
150
+ noteFile('scope-a', 'bad-a', POISON),
151
+ noteFile('scope-b', 'good-b', 'b-ok'),
152
+ noteFile('scope-b', 'bad-b', POISON)
153
+ ];
154
+ const store = FileTreeMemoryStore.create({
155
+ root: mutableRoot(files),
156
+ registry: registry(poisonRejectingConverter),
157
+ codecs,
158
+ onRecordError: 'skip'
159
+ }).orThrow();
160
+ expect(await store.list()).toSucceedAndSatisfy((listed) => {
161
+ expect(listed.map((r) => r.envelope.id).sort()).toEqual(['good-a', 'good-b']);
162
+ });
163
+ expect(store.skippedRecords.map((s) => s.path).sort()).toEqual(['scope-a/bad-a.md', 'scope-b/bad-b.md']);
164
+ });
165
+ test('an all-invalid subtree opens with everything skipped (mapSuccess orDefault edge)', async () => {
166
+ const files = [
167
+ noteFile('bad-scope', 'x', POISON),
168
+ noteFile('bad-scope', 'y', POISON),
169
+ noteFile('good-scope', 'z', 'fine')
170
+ ];
171
+ const store = FileTreeMemoryStore.create({
172
+ root: mutableRoot(files),
173
+ registry: registry(poisonRejectingConverter),
174
+ codecs,
175
+ onRecordError: 'skip'
176
+ }).orThrow();
177
+ expect(await store.list()).toSucceedAndSatisfy((listed) => {
178
+ expect(listed.map((r) => r.envelope.id)).toEqual(['z']);
179
+ });
180
+ expect(store.skippedRecords.map((s) => s.path).sort()).toEqual(['bad-scope/x.md', 'bad-scope/y.md']);
181
+ });
182
+ test('an empty vault opens with no skips in either mode', async () => {
183
+ const skip = FileTreeMemoryStore.create({
184
+ root: mutableRoot([]),
185
+ registry: registry(poisonRejectingConverter),
186
+ codecs,
187
+ onRecordError: 'skip'
188
+ }).orThrow();
189
+ expect(await skip.list()).toSucceedWith([]);
190
+ expect(skip.skippedRecords).toEqual([]);
191
+ const strict = FileTreeMemoryStore.create({
192
+ root: mutableRoot([]),
193
+ registry: registry(poisonRejectingConverter),
194
+ codecs
195
+ }).orThrow();
196
+ expect(strict.skippedRecords).toEqual([]);
197
+ });
198
+ test('each skip is logged at warn; with the default NoOpLogger skippedRecords is still populated', async () => {
199
+ const files = [noteFile('note', 'good', 'ok'), noteFile('note', 'bad', POISON)];
200
+ const logger = new Logging.InMemoryLogger('warning');
201
+ const withLogger = FileTreeMemoryStore.create({
202
+ root: mutableRoot(files),
203
+ registry: registry(poisonRejectingConverter),
204
+ codecs,
205
+ onRecordError: 'skip',
206
+ logger
207
+ }).orThrow();
208
+ expect(withLogger.skippedRecords).toHaveLength(1);
209
+ expect(logger.logged.some((m) => m.includes('note/bad.md') && m.includes(POISON))).toBe(true);
210
+ // No logger wired (default NoOpLogger): create still succeeds and the
211
+ // structured collection is still populated (observable without a logger).
212
+ const silent = FileTreeMemoryStore.create({
213
+ root: mutableRoot(files),
214
+ registry: registry(poisonRejectingConverter),
215
+ codecs,
216
+ onRecordError: 'skip'
217
+ }).orThrow();
218
+ expect(silent.skippedRecords.map((s) => s.path)).toEqual(['note/bad.md']);
219
+ });
220
+ test('re-open after the converter is fixed re-indexes the quarantined record (non-destructive)', async () => {
221
+ const files = [noteFile('note', 'good', 'ok'), noteFile('note', 'later', POISON)];
222
+ const root = mutableRoot(files);
223
+ // First open with the rejecting converter: the record is quarantined.
224
+ const before = FileTreeMemoryStore.create({
225
+ root,
226
+ registry: registry(poisonRejectingConverter),
227
+ codecs,
228
+ onRecordError: 'skip'
229
+ }).orThrow();
230
+ expect(before.skippedRecords).toHaveLength(1);
231
+ expect(await before.list()).toSucceedAndSatisfy((listed) => {
232
+ expect(listed.map((r) => r.envelope.id)).toEqual(['good']);
233
+ });
234
+ // The file was never deleted or mutated, so a fresh open with the fixed
235
+ // (accept-all) converter re-indexes it — quarantine is non-destructive.
236
+ const after = FileTreeMemoryStore.create({
237
+ root: mutableRoot(files),
238
+ registry: registry(Converters.string),
239
+ codecs,
240
+ onRecordError: 'skip'
241
+ }).orThrow();
242
+ expect(after.skippedRecords).toEqual([]);
243
+ expect(await after.list()).toSucceedAndSatisfy((listed) => {
244
+ expect(listed.map((r) => r.envelope.id).sort()).toEqual(['good', 'later']);
245
+ });
246
+ });
247
+ });
248
+ //# sourceMappingURL=lenientOpen.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lenientOpen.test.js","sourceRoot":"","sources":["../../../../src/test/unit/store/lenientOpen.test.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport '@fgv/ts-utils-jest';\nimport { Converter, Converters, Logging, Result, fail, succeed } from '@fgv/ts-utils';\nimport { FileTree } from '@fgv/ts-json-base';\nimport {\n BodyConverterRegistry,\n EntityId,\n FileTreeMemoryStore,\n IBodyConverterRegistry,\n IIdentityCodec,\n IIdentityCodecResult,\n IMemoryRecord,\n ISkippedRecord,\n Kind,\n MemoryScopeKey,\n joinFrontmatter\n} from '../../../index';\n\n/**\n * The incident: one vault holds every kind (mtm, ltm, conversation, knowledge,\n * claim) in ONE store. A required field was added to the `claim` body; every\n * pre-existing `claim` failed validation; and because the initial walk collapsed\n * per-record results with `mapResults` (fail-the-whole-array on any element),\n * every OTHER valid record became unreadable behind the failed open. These tests\n * reproduce that shape (a poison-rejecting body converter standing in for the\n * migrated converter) and pin the lenient (`onRecordError: 'skip'`) behavior.\n */\n\nconst noteKind: Kind = 'note' as Kind;\nconst POISON: string = 'POISON';\n\n/**\n * A permissive identity codec that treats the full `<scope>/<stem>` path as the\n * entity id, so records can live at any nesting depth (the shipped codecs pin a\n * fixed scope shape; this test needs arbitrary-depth scopes to prove the\n * recursion threads skips out of nested subtrees).\n */\nclass PathIdentityCodec implements IIdentityCodec {\n public encode(entityId: EntityId): Result<IIdentityCodecResult> {\n const value: string = String(entityId);\n const cut: number = value.lastIndexOf('/');\n const scope: MemoryScopeKey = (cut >= 0 ? value.slice(0, cut) : '') as MemoryScopeKey;\n const idStem: string = cut >= 0 ? value.slice(cut + 1) : value;\n return succeed({ scope, idStem, isVersioned: false });\n }\n public decode(scope: MemoryScopeKey, encodedStem: string): Result<EntityId> {\n return succeed(`${scope}/${encodedStem}` as EntityId);\n }\n public verifyRoundTrip(): Result<true> {\n return succeed(true);\n }\n}\n\n/** A body converter that rejects any body containing the poison marker. */\nconst poisonRejectingConverter: Converter<string> = Converters.string.map((body) =>\n body.includes(POISON) ? fail(`note body rejected: contains ${POISON}`) : succeed(body)\n);\n\nfunction registry(bodyConverter: Converter<string>): IBodyConverterRegistry {\n const created = BodyConverterRegistry.create().orThrow();\n created.register(noteKind, bodyConverter);\n return created;\n}\n\nconst codecs: ReadonlyMap<Kind, IIdentityCodec> = new Map<Kind, IIdentityCodec>([\n [noteKind, new PathIdentityCodec()]\n]);\n\n/** Serialize a note record at `<scope>/<stem>.md` with the given body. */\nfunction noteFile(scope: string, stem: string, body: string): { path: string; contents: string } {\n const envelope = {\n id: stem,\n entityId: `${scope}/${stem}`,\n kind: 'note',\n tags: [],\n links: [],\n created: 1,\n updated: 1,\n seq: 4,\n contentHash: 'seed',\n provenance: { source: 'agent' }\n };\n const frontmatter = Object.entries(envelope)\n .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)\n .join('\\n');\n return { path: `${scope}/${stem}.md`, contents: joinFrontmatter(frontmatter, body) };\n}\n\nfunction mutableRoot(\n files: ReadonlyArray<{ path: string; contents: string }>\n): FileTree.IMutableFileTreeDirectoryItem {\n const tree = FileTree.inMemory([...files], { mutable: true }).orThrow();\n const root = tree.getDirectory('/').orThrow();\n if (!FileTree.isMutableDirectoryItem(root)) {\n throw new Error('expected a mutable root directory');\n }\n return root;\n}\n\ndescribe('FileTreeMemoryStore lenient open (onRecordError)', () => {\n test('incident: one bad record does not make the whole store unopenable in skip mode', async () => {\n const files = [\n noteFile('mtm', 'turn-1', 'valid mtm body'),\n noteFile('ltm', 'summary', 'valid ltm body'),\n noteFile('knowledge', 'doc-a', 'valid knowledge body'),\n // The migrated `claim` converter now rejects the pre-existing record.\n noteFile('claim', 'claim-1', `${POISON} missing required field`)\n ];\n\n // Default (== 'fail') still fails the whole open, exactly as it did before.\n expect(\n FileTreeMemoryStore.create({\n root: mutableRoot(files),\n registry: registry(poisonRejectingConverter),\n codecs\n })\n ).toFailWith(/POISON/);\n\n // 'skip' mode opens: every valid record loads; the bad one is quarantined.\n const store = FileTreeMemoryStore.create({\n root: mutableRoot(files),\n registry: registry(poisonRejectingConverter),\n codecs,\n onRecordError: 'skip'\n }).orThrow();\n\n expect(await store.list()).toSucceedAndSatisfy((listed: ReadonlyArray<IMemoryRecord<unknown>>) => {\n expect(listed.map((r) => r.envelope.id).sort()).toEqual(['doc-a', 'summary', 'turn-1']);\n });\n expect(store.skippedRecords).toHaveLength(1);\n const skip: ISkippedRecord = store.skippedRecords[0];\n expect(skip.path).toBe('claim/claim-1.md');\n expect(skip.scope).toBe('claim');\n expect(skip.error).toContain('claim/claim-1.md');\n expect(skip.error).toContain(POISON);\n });\n\n test('default mode is byte-identical to explicit fail mode', () => {\n const files = [noteFile('note', 'good', 'ok'), noteFile('note', 'bad', POISON)];\n const asDefault = FileTreeMemoryStore.create({\n root: mutableRoot(files),\n registry: registry(poisonRejectingConverter),\n codecs\n });\n const asFail = FileTreeMemoryStore.create({\n root: mutableRoot(files),\n registry: registry(poisonRejectingConverter),\n codecs,\n onRecordError: 'fail'\n });\n expect(asDefault).toFail();\n expect(asFail).toFail();\n // Same exact failure message — the load path is untouched in fail mode.\n expect(asDefault.isFailure() && asFail.isFailure() && asDefault.message === asFail.message).toBe(true);\n expect(asDefault).toFailWith(/POISON/);\n });\n\n test('a bad record in a nested scope is skipped while valid siblings load', async () => {\n const files = [\n noteFile('alpha/beta', 'ok-1', 'valid'),\n noteFile('alpha/beta', 'bad-1', POISON),\n noteFile('alpha/beta/gamma', 'ok-2', 'valid deep')\n ];\n const store = FileTreeMemoryStore.create({\n root: mutableRoot(files),\n registry: registry(poisonRejectingConverter),\n codecs,\n onRecordError: 'skip'\n }).orThrow();\n\n expect(await store.list()).toSucceedAndSatisfy((listed: ReadonlyArray<IMemoryRecord<unknown>>) => {\n expect(listed.map((r) => r.envelope.id).sort()).toEqual(['ok-1', 'ok-2']);\n });\n expect(store.skippedRecords.map((s) => s.path)).toEqual(['alpha/beta/bad-1.md']);\n expect(store.skippedRecords[0].scope).toBe('alpha/beta');\n });\n\n test('multiple skips across different scopes all surface; all valid records load', async () => {\n const files = [\n noteFile('scope-a', 'good-a', 'a-ok'),\n noteFile('scope-a', 'bad-a', POISON),\n noteFile('scope-b', 'good-b', 'b-ok'),\n noteFile('scope-b', 'bad-b', POISON)\n ];\n const store = FileTreeMemoryStore.create({\n root: mutableRoot(files),\n registry: registry(poisonRejectingConverter),\n codecs,\n onRecordError: 'skip'\n }).orThrow();\n\n expect(await store.list()).toSucceedAndSatisfy((listed: ReadonlyArray<IMemoryRecord<unknown>>) => {\n expect(listed.map((r) => r.envelope.id).sort()).toEqual(['good-a', 'good-b']);\n });\n expect(store.skippedRecords.map((s) => s.path).sort()).toEqual(['scope-a/bad-a.md', 'scope-b/bad-b.md']);\n });\n\n test('an all-invalid subtree opens with everything skipped (mapSuccess orDefault edge)', async () => {\n const files = [\n noteFile('bad-scope', 'x', POISON),\n noteFile('bad-scope', 'y', POISON),\n noteFile('good-scope', 'z', 'fine')\n ];\n const store = FileTreeMemoryStore.create({\n root: mutableRoot(files),\n registry: registry(poisonRejectingConverter),\n codecs,\n onRecordError: 'skip'\n }).orThrow();\n\n expect(await store.list()).toSucceedAndSatisfy((listed: ReadonlyArray<IMemoryRecord<unknown>>) => {\n expect(listed.map((r) => r.envelope.id)).toEqual(['z']);\n });\n expect(store.skippedRecords.map((s) => s.path).sort()).toEqual(['bad-scope/x.md', 'bad-scope/y.md']);\n });\n\n test('an empty vault opens with no skips in either mode', async () => {\n const skip = FileTreeMemoryStore.create({\n root: mutableRoot([]),\n registry: registry(poisonRejectingConverter),\n codecs,\n onRecordError: 'skip'\n }).orThrow();\n expect(await skip.list()).toSucceedWith([]);\n expect(skip.skippedRecords).toEqual([]);\n\n const strict = FileTreeMemoryStore.create({\n root: mutableRoot([]),\n registry: registry(poisonRejectingConverter),\n codecs\n }).orThrow();\n expect(strict.skippedRecords).toEqual([]);\n });\n\n test('each skip is logged at warn; with the default NoOpLogger skippedRecords is still populated', async () => {\n const files = [noteFile('note', 'good', 'ok'), noteFile('note', 'bad', POISON)];\n const logger = new Logging.InMemoryLogger('warning');\n const withLogger = FileTreeMemoryStore.create({\n root: mutableRoot(files),\n registry: registry(poisonRejectingConverter),\n codecs,\n onRecordError: 'skip',\n logger\n }).orThrow();\n expect(withLogger.skippedRecords).toHaveLength(1);\n expect(logger.logged.some((m) => m.includes('note/bad.md') && m.includes(POISON))).toBe(true);\n\n // No logger wired (default NoOpLogger): create still succeeds and the\n // structured collection is still populated (observable without a logger).\n const silent = FileTreeMemoryStore.create({\n root: mutableRoot(files),\n registry: registry(poisonRejectingConverter),\n codecs,\n onRecordError: 'skip'\n }).orThrow();\n expect(silent.skippedRecords.map((s) => s.path)).toEqual(['note/bad.md']);\n });\n\n test('re-open after the converter is fixed re-indexes the quarantined record (non-destructive)', async () => {\n const files = [noteFile('note', 'good', 'ok'), noteFile('note', 'later', POISON)];\n const root = mutableRoot(files);\n\n // First open with the rejecting converter: the record is quarantined.\n const before = FileTreeMemoryStore.create({\n root,\n registry: registry(poisonRejectingConverter),\n codecs,\n onRecordError: 'skip'\n }).orThrow();\n expect(before.skippedRecords).toHaveLength(1);\n expect(await before.list()).toSucceedAndSatisfy((listed: ReadonlyArray<IMemoryRecord<unknown>>) => {\n expect(listed.map((r) => r.envelope.id)).toEqual(['good']);\n });\n\n // The file was never deleted or mutated, so a fresh open with the fixed\n // (accept-all) converter re-indexes it — quarantine is non-destructive.\n const after = FileTreeMemoryStore.create({\n root: mutableRoot(files),\n registry: registry(Converters.string),\n codecs,\n onRecordError: 'skip'\n }).orThrow();\n expect(after.skippedRecords).toEqual([]);\n expect(await after.list()).toSucceedAndSatisfy((listed: ReadonlyArray<IMemoryRecord<unknown>>) => {\n expect(listed.map((r) => r.envelope.id).sort()).toEqual(['good', 'later']);\n });\n });\n});\n"]}