@fgv/ts-agent-memory 5.1.0-36
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.
- package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +9 -0
- package/.rush/temp/f6a88bfdd66517ccb98c2c7ae1be6e6fe9e15d38.tar.log +282 -0
- package/.rush/temp/operation/build/all.log +9 -0
- package/.rush/temp/operation/build/log-chunks.jsonl +9 -0
- package/.rush/temp/operation/build/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +688 -0
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/config/api-extractor.json +343 -0
- package/config/jest.config.json +14 -0
- package/config/rig.json +4 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/packlets/converters/bodyConverterRegistry.js +51 -0
- package/dist/packlets/converters/bodyConverterRegistry.js.map +1 -0
- package/dist/packlets/converters/envelopeConverter.js +159 -0
- package/dist/packlets/converters/envelopeConverter.js.map +1 -0
- package/dist/packlets/converters/index.js +7 -0
- package/dist/packlets/converters/index.js.map +1 -0
- package/dist/packlets/index/index.js +6 -0
- package/dist/packlets/index/index.js.map +1 -0
- package/dist/packlets/index/memoryIndex.js +170 -0
- package/dist/packlets/index/memoryIndex.js.map +1 -0
- package/dist/packlets/observe/index.js +7 -0
- package/dist/packlets/observe/index.js.map +1 -0
- package/dist/packlets/observe/memoryObservationStore.js +119 -0
- package/dist/packlets/observe/memoryObservationStore.js.map +1 -0
- package/dist/packlets/observe/observer.js +6 -0
- package/dist/packlets/observe/observer.js.map +1 -0
- package/dist/packlets/retrieve/hybridRetriever.js +135 -0
- package/dist/packlets/retrieve/hybridRetriever.js.map +1 -0
- package/dist/packlets/retrieve/index.js +12 -0
- package/dist/packlets/retrieve/index.js.map +1 -0
- package/dist/packlets/retrieve/linkTraversalRetriever.js +143 -0
- package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -0
- package/dist/packlets/retrieve/recencyRetriever.js +33 -0
- package/dist/packlets/retrieve/recencyRetriever.js.map +1 -0
- package/dist/packlets/retrieve/retriever.js +110 -0
- package/dist/packlets/retrieve/retriever.js.map +1 -0
- package/dist/packlets/retrieve/semanticRetriever.js +86 -0
- package/dist/packlets/retrieve/semanticRetriever.js.map +1 -0
- package/dist/packlets/retrieve/structuredFilterRetriever.js +37 -0
- package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -0
- package/dist/packlets/retrieve/tagRetriever.js +37 -0
- package/dist/packlets/retrieve/tagRetriever.js.map +1 -0
- package/dist/packlets/store/fileTreeMemoryStore.js +698 -0
- package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -0
- package/dist/packlets/store/index.js +7 -0
- package/dist/packlets/store/index.js.map +1 -0
- package/dist/packlets/store/scopeEncoding.js +31 -0
- package/dist/packlets/store/scopeEncoding.js.map +1 -0
- package/dist/packlets/types/envelope.js +6 -0
- package/dist/packlets/types/envelope.js.map +1 -0
- package/dist/packlets/types/filenameSafety.js +52 -0
- package/dist/packlets/types/filenameSafety.js.map +1 -0
- package/dist/packlets/types/identityCodec.js +184 -0
- package/dist/packlets/types/identityCodec.js.map +1 -0
- package/dist/packlets/types/ids.js +67 -0
- package/dist/packlets/types/ids.js.map +1 -0
- package/dist/packlets/types/index.js +10 -0
- package/dist/packlets/types/index.js.map +1 -0
- package/dist/packlets/types/writePolicy.js +263 -0
- package/dist/packlets/types/writePolicy.js.map +1 -0
- package/dist/packlets/vector/inMemoryCosineIndex.js +150 -0
- package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -0
- package/dist/packlets/vector/index.js +7 -0
- package/dist/packlets/vector/index.js.map +1 -0
- package/dist/packlets/vector/vectorIndex.js +6 -0
- package/dist/packlets/vector/vectorIndex.js.map +1 -0
- package/dist/test/unit/converters/bodyConverterRegistry.test.js +72 -0
- package/dist/test/unit/converters/bodyConverterRegistry.test.js.map +1 -0
- package/dist/test/unit/converters/envelopeConverter.test.js +196 -0
- package/dist/test/unit/converters/envelopeConverter.test.js.map +1 -0
- package/dist/test/unit/index/memoryIndex.test.js +152 -0
- package/dist/test/unit/index/memoryIndex.test.js.map +1 -0
- package/dist/test/unit/observe/memoryObservationStore.test.js +118 -0
- package/dist/test/unit/observe/memoryObservationStore.test.js.map +1 -0
- package/dist/test/unit/retrieve/linkTraversalRetriever.test.js +182 -0
- package/dist/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -0
- package/dist/test/unit/retrieve/retrievers.test.js +506 -0
- package/dist/test/unit/retrieve/retrievers.test.js.map +1 -0
- package/dist/test/unit/store/embedOnWrite.test.js +260 -0
- package/dist/test/unit/store/embedOnWrite.test.js.map +1 -0
- package/dist/test/unit/store/fileTreeMemoryStore.test.js +647 -0
- package/dist/test/unit/store/fileTreeMemoryStore.test.js.map +1 -0
- package/dist/test/unit/store/observations.test.js +239 -0
- package/dist/test/unit/store/observations.test.js.map +1 -0
- package/dist/test/unit/store/scopeEncoding.test.js +24 -0
- package/dist/test/unit/store/scopeEncoding.test.js.map +1 -0
- package/dist/test/unit/types/identityCodec.test.js +187 -0
- package/dist/test/unit/types/identityCodec.test.js.map +1 -0
- package/dist/test/unit/types/ids.test.js +84 -0
- package/dist/test/unit/types/ids.test.js.map +1 -0
- package/dist/test/unit/types/writePolicy.test.js +241 -0
- package/dist/test/unit/types/writePolicy.test.js.map +1 -0
- package/dist/test/unit/vector/inMemoryCosineIndex.test.js +192 -0
- package/dist/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -0
- package/dist/test/unit/vector/vectorIndex.test.js +42 -0
- package/dist/test/unit/vector/vectorIndex.test.js.map +1 -0
- package/dist/ts-agent-memory.d.ts +1901 -0
- package/dist/tsdoc-metadata.json +11 -0
- package/eslint.config.js +15 -0
- package/etc/ts-agent-memory.api.md +525 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +28 -0
- package/lib/index.js.map +1 -0
- package/lib/packlets/converters/bodyConverterRegistry.d.ts +64 -0
- package/lib/packlets/converters/bodyConverterRegistry.d.ts.map +1 -0
- package/lib/packlets/converters/bodyConverterRegistry.js +55 -0
- package/lib/packlets/converters/bodyConverterRegistry.js.map +1 -0
- package/lib/packlets/converters/envelopeConverter.d.ts +70 -0
- package/lib/packlets/converters/envelopeConverter.d.ts.map +1 -0
- package/lib/packlets/converters/envelopeConverter.js +166 -0
- package/lib/packlets/converters/envelopeConverter.js.map +1 -0
- package/lib/packlets/converters/index.d.ts +3 -0
- package/lib/packlets/converters/index.d.ts.map +1 -0
- package/lib/packlets/converters/index.js +23 -0
- package/lib/packlets/converters/index.js.map +1 -0
- package/lib/packlets/index/index.d.ts +2 -0
- package/lib/packlets/index/index.d.ts.map +1 -0
- package/lib/packlets/index/index.js +22 -0
- package/lib/packlets/index/index.js.map +1 -0
- package/lib/packlets/index/memoryIndex.d.ts +127 -0
- package/lib/packlets/index/memoryIndex.d.ts.map +1 -0
- package/lib/packlets/index/memoryIndex.js +174 -0
- package/lib/packlets/index/memoryIndex.js.map +1 -0
- package/lib/packlets/observe/index.d.ts +3 -0
- package/lib/packlets/observe/index.d.ts.map +1 -0
- package/lib/packlets/observe/index.js +23 -0
- package/lib/packlets/observe/index.js.map +1 -0
- package/lib/packlets/observe/memoryObservationStore.d.ts +91 -0
- package/lib/packlets/observe/memoryObservationStore.d.ts.map +1 -0
- package/lib/packlets/observe/memoryObservationStore.js +123 -0
- package/lib/packlets/observe/memoryObservationStore.js.map +1 -0
- package/lib/packlets/observe/observer.d.ts +110 -0
- package/lib/packlets/observe/observer.d.ts.map +1 -0
- package/lib/packlets/observe/observer.js +7 -0
- package/lib/packlets/observe/observer.js.map +1 -0
- package/lib/packlets/retrieve/hybridRetriever.d.ts +79 -0
- package/lib/packlets/retrieve/hybridRetriever.d.ts.map +1 -0
- package/lib/packlets/retrieve/hybridRetriever.js +140 -0
- package/lib/packlets/retrieve/hybridRetriever.js.map +1 -0
- package/lib/packlets/retrieve/index.d.ts +8 -0
- package/lib/packlets/retrieve/index.d.ts.map +1 -0
- package/lib/packlets/retrieve/index.js +28 -0
- package/lib/packlets/retrieve/index.js.map +1 -0
- package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +61 -0
- package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -0
- package/lib/packlets/retrieve/linkTraversalRetriever.js +147 -0
- package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -0
- package/lib/packlets/retrieve/recencyRetriever.d.ts +21 -0
- package/lib/packlets/retrieve/recencyRetriever.d.ts.map +1 -0
- package/lib/packlets/retrieve/recencyRetriever.js +37 -0
- package/lib/packlets/retrieve/recencyRetriever.js.map +1 -0
- package/lib/packlets/retrieve/retriever.d.ts +133 -0
- package/lib/packlets/retrieve/retriever.d.ts.map +1 -0
- package/lib/packlets/retrieve/retriever.js +119 -0
- package/lib/packlets/retrieve/retriever.js.map +1 -0
- package/lib/packlets/retrieve/semanticRetriever.d.ts +69 -0
- package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -0
- package/lib/packlets/retrieve/semanticRetriever.js +90 -0
- package/lib/packlets/retrieve/semanticRetriever.js.map +1 -0
- package/lib/packlets/retrieve/structuredFilterRetriever.d.ts +22 -0
- package/lib/packlets/retrieve/structuredFilterRetriever.d.ts.map +1 -0
- package/lib/packlets/retrieve/structuredFilterRetriever.js +41 -0
- package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -0
- package/lib/packlets/retrieve/tagRetriever.d.ts +22 -0
- package/lib/packlets/retrieve/tagRetriever.d.ts.map +1 -0
- package/lib/packlets/retrieve/tagRetriever.js +41 -0
- package/lib/packlets/retrieve/tagRetriever.js.map +1 -0
- package/lib/packlets/store/fileTreeMemoryStore.d.ts +327 -0
- package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -0
- package/lib/packlets/store/fileTreeMemoryStore.js +702 -0
- package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -0
- package/lib/packlets/store/index.d.ts +3 -0
- package/lib/packlets/store/index.d.ts.map +1 -0
- package/lib/packlets/store/index.js +23 -0
- package/lib/packlets/store/index.js.map +1 -0
- package/lib/packlets/store/scopeEncoding.d.ts +19 -0
- package/lib/packlets/store/scopeEncoding.d.ts.map +1 -0
- package/lib/packlets/store/scopeEncoding.js +34 -0
- package/lib/packlets/store/scopeEncoding.js.map +1 -0
- package/lib/packlets/types/envelope.d.ts +119 -0
- package/lib/packlets/types/envelope.d.ts.map +1 -0
- package/lib/packlets/types/envelope.js +7 -0
- package/lib/packlets/types/envelope.js.map +1 -0
- package/lib/packlets/types/filenameSafety.d.ts +16 -0
- package/lib/packlets/types/filenameSafety.d.ts.map +1 -0
- package/lib/packlets/types/filenameSafety.js +55 -0
- package/lib/packlets/types/filenameSafety.js.map +1 -0
- package/lib/packlets/types/identityCodec.d.ts +136 -0
- package/lib/packlets/types/identityCodec.d.ts.map +1 -0
- package/lib/packlets/types/identityCodec.js +190 -0
- package/lib/packlets/types/identityCodec.js.map +1 -0
- package/lib/packlets/types/ids.d.ts +55 -0
- package/lib/packlets/types/ids.d.ts.map +1 -0
- package/lib/packlets/types/ids.js +70 -0
- package/lib/packlets/types/ids.js.map +1 -0
- package/lib/packlets/types/index.d.ts +6 -0
- package/lib/packlets/types/index.d.ts.map +1 -0
- package/lib/packlets/types/index.js +26 -0
- package/lib/packlets/types/index.js.map +1 -0
- package/lib/packlets/types/writePolicy.d.ts +213 -0
- package/lib/packlets/types/writePolicy.d.ts.map +1 -0
- package/lib/packlets/types/writePolicy.js +268 -0
- package/lib/packlets/types/writePolicy.js.map +1 -0
- package/lib/packlets/vector/inMemoryCosineIndex.d.ts +69 -0
- package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -0
- package/lib/packlets/vector/inMemoryCosineIndex.js +154 -0
- package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -0
- package/lib/packlets/vector/index.d.ts +3 -0
- package/lib/packlets/vector/index.d.ts.map +1 -0
- package/lib/packlets/vector/index.js +23 -0
- package/lib/packlets/vector/index.js.map +1 -0
- package/lib/packlets/vector/vectorIndex.d.ts +68 -0
- package/lib/packlets/vector/vectorIndex.d.ts.map +1 -0
- package/lib/packlets/vector/vectorIndex.js +7 -0
- package/lib/packlets/vector/vectorIndex.js.map +1 -0
- package/lib/test/unit/converters/bodyConverterRegistry.test.d.ts +2 -0
- package/lib/test/unit/converters/bodyConverterRegistry.test.d.ts.map +1 -0
- package/lib/test/unit/converters/bodyConverterRegistry.test.js +74 -0
- package/lib/test/unit/converters/bodyConverterRegistry.test.js.map +1 -0
- package/lib/test/unit/converters/envelopeConverter.test.d.ts +2 -0
- package/lib/test/unit/converters/envelopeConverter.test.d.ts.map +1 -0
- package/lib/test/unit/converters/envelopeConverter.test.js +198 -0
- package/lib/test/unit/converters/envelopeConverter.test.js.map +1 -0
- package/lib/test/unit/index/memoryIndex.test.d.ts +2 -0
- package/lib/test/unit/index/memoryIndex.test.d.ts.map +1 -0
- package/lib/test/unit/index/memoryIndex.test.js +154 -0
- package/lib/test/unit/index/memoryIndex.test.js.map +1 -0
- package/lib/test/unit/observe/memoryObservationStore.test.d.ts +2 -0
- package/lib/test/unit/observe/memoryObservationStore.test.d.ts.map +1 -0
- package/lib/test/unit/observe/memoryObservationStore.test.js +120 -0
- package/lib/test/unit/observe/memoryObservationStore.test.js.map +1 -0
- package/lib/test/unit/retrieve/linkTraversalRetriever.test.d.ts +2 -0
- package/lib/test/unit/retrieve/linkTraversalRetriever.test.d.ts.map +1 -0
- package/lib/test/unit/retrieve/linkTraversalRetriever.test.js +184 -0
- package/lib/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -0
- package/lib/test/unit/retrieve/retrievers.test.d.ts +2 -0
- package/lib/test/unit/retrieve/retrievers.test.d.ts.map +1 -0
- package/lib/test/unit/retrieve/retrievers.test.js +508 -0
- package/lib/test/unit/retrieve/retrievers.test.js.map +1 -0
- package/lib/test/unit/store/embedOnWrite.test.d.ts +2 -0
- package/lib/test/unit/store/embedOnWrite.test.d.ts.map +1 -0
- package/lib/test/unit/store/embedOnWrite.test.js +262 -0
- package/lib/test/unit/store/embedOnWrite.test.js.map +1 -0
- package/lib/test/unit/store/fileTreeMemoryStore.test.d.ts +2 -0
- package/lib/test/unit/store/fileTreeMemoryStore.test.d.ts.map +1 -0
- package/lib/test/unit/store/fileTreeMemoryStore.test.js +649 -0
- package/lib/test/unit/store/fileTreeMemoryStore.test.js.map +1 -0
- package/lib/test/unit/store/observations.test.d.ts +2 -0
- package/lib/test/unit/store/observations.test.d.ts.map +1 -0
- package/lib/test/unit/store/observations.test.js +241 -0
- package/lib/test/unit/store/observations.test.js.map +1 -0
- package/lib/test/unit/store/scopeEncoding.test.d.ts +2 -0
- package/lib/test/unit/store/scopeEncoding.test.d.ts.map +1 -0
- package/lib/test/unit/store/scopeEncoding.test.js +26 -0
- package/lib/test/unit/store/scopeEncoding.test.js.map +1 -0
- package/lib/test/unit/types/identityCodec.test.d.ts +2 -0
- package/lib/test/unit/types/identityCodec.test.d.ts.map +1 -0
- package/lib/test/unit/types/identityCodec.test.js +189 -0
- package/lib/test/unit/types/identityCodec.test.js.map +1 -0
- package/lib/test/unit/types/ids.test.d.ts +2 -0
- package/lib/test/unit/types/ids.test.d.ts.map +1 -0
- package/lib/test/unit/types/ids.test.js +86 -0
- package/lib/test/unit/types/ids.test.js.map +1 -0
- package/lib/test/unit/types/writePolicy.test.d.ts +2 -0
- package/lib/test/unit/types/writePolicy.test.d.ts.map +1 -0
- package/lib/test/unit/types/writePolicy.test.js +243 -0
- package/lib/test/unit/types/writePolicy.test.js.map +1 -0
- package/lib/test/unit/vector/inMemoryCosineIndex.test.d.ts +2 -0
- package/lib/test/unit/vector/inMemoryCosineIndex.test.d.ts.map +1 -0
- package/lib/test/unit/vector/inMemoryCosineIndex.test.js +194 -0
- package/lib/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -0
- package/lib/test/unit/vector/vectorIndex.test.d.ts +2 -0
- package/lib/test/unit/vector/vectorIndex.test.d.ts.map +1 -0
- package/lib/test/unit/vector/vectorIndex.test.js +44 -0
- package/lib/test/unit/vector/vectorIndex.test.js.map +1 -0
- package/package.json +81 -0
- package/rush-logs/ts-agent-memory.build.cache.log +3 -0
- package/rush-logs/ts-agent-memory.build.log +9 -0
- package/src/index.ts +12 -0
- package/src/packlets/converters/bodyConverterRegistry.ts +105 -0
- package/src/packlets/converters/envelopeConverter.ts +210 -0
- package/src/packlets/converters/index.ts +7 -0
- package/src/packlets/index/index.ts +6 -0
- package/src/packlets/index/memoryIndex.ts +268 -0
- package/src/packlets/observe/index.ts +7 -0
- package/src/packlets/observe/memoryObservationStore.ts +153 -0
- package/src/packlets/observe/observer.ts +119 -0
- package/src/packlets/retrieve/hybridRetriever.ts +181 -0
- package/src/packlets/retrieve/index.ts +12 -0
- package/src/packlets/retrieve/linkTraversalRetriever.ts +169 -0
- package/src/packlets/retrieve/recencyRetriever.ts +54 -0
- package/src/packlets/retrieve/retriever.ts +207 -0
- package/src/packlets/retrieve/semanticRetriever.ts +147 -0
- package/src/packlets/retrieve/structuredFilterRetriever.ts +58 -0
- package/src/packlets/retrieve/tagRetriever.ts +58 -0
- package/src/packlets/store/fileTreeMemoryStore.ts +1073 -0
- package/src/packlets/store/index.ts +7 -0
- package/src/packlets/store/scopeEncoding.ts +36 -0
- package/src/packlets/types/envelope.ts +138 -0
- package/src/packlets/types/filenameSafety.ts +57 -0
- package/src/packlets/types/identityCodec.ts +263 -0
- package/src/packlets/types/ids.ts +124 -0
- package/src/packlets/types/index.ts +10 -0
- package/src/packlets/types/writePolicy.ts +447 -0
- package/src/packlets/vector/inMemoryCosineIndex.ts +173 -0
- package/src/packlets/vector/index.ts +7 -0
- package/src/packlets/vector/vectorIndex.ts +78 -0
- package/src/test/unit/converters/bodyConverterRegistry.test.ts +89 -0
- package/src/test/unit/converters/envelopeConverter.test.ts +261 -0
- package/src/test/unit/index/memoryIndex.test.ts +187 -0
- package/src/test/unit/observe/memoryObservationStore.test.ts +158 -0
- package/src/test/unit/retrieve/linkTraversalRetriever.test.ts +230 -0
- package/src/test/unit/retrieve/retrievers.test.ts +662 -0
- package/src/test/unit/store/embedOnWrite.test.ts +346 -0
- package/src/test/unit/store/fileTreeMemoryStore.test.ts +875 -0
- package/src/test/unit/store/observations.test.ts +290 -0
- package/src/test/unit/store/scopeEncoding.test.ts +37 -0
- package/src/test/unit/types/identityCodec.test.ts +266 -0
- package/src/test/unit/types/ids.test.ts +94 -0
- package/src/test/unit/types/writePolicy.test.ts +325 -0
- package/src/test/unit/vector/inMemoryCosineIndex.test.ts +242 -0
- package/src/test/unit/vector/vectorIndex.test.ts +48 -0
- package/temp/build/lint/_eslint-5eVG3S6w.json +182 -0
- package/temp/build/typescript/ts_8nwakTlr.json +1 -0
- package/temp/ts-agent-memory.api.json +12438 -0
- package/temp/ts-agent-memory.api.md +525 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Result } from '@fgv/ts-utils';
|
|
7
|
+
import { IMemoryRecord, MemoryId } from '../types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A single hit returned by {@link IVectorIndex.query}: the matched record id and
|
|
11
|
+
* the backend's similarity score (higher = more similar; the exact scale is
|
|
12
|
+
* backend-defined). Hits are returned in descending score order.
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
export interface IVectorQueryHit {
|
|
16
|
+
/** The id of the matched record. */
|
|
17
|
+
readonly id: MemoryId;
|
|
18
|
+
/** Backend similarity score; higher is more similar. */
|
|
19
|
+
readonly score: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The vector-index seam an embedding backend implements to make
|
|
24
|
+
* {@link SemanticRetriever | semantic recall} operational.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* Vectors cross this seam as `Float32Array` (the in-memory representation an
|
|
28
|
+
* embedding model produces); `number[]` is reserved for the JSON-wire edges
|
|
29
|
+
* (e.g. a provider's embedding response). The in-package brute-force cosine
|
|
30
|
+
* implementation is {@link InMemoryCosineIndex}; a consumer can swap an external
|
|
31
|
+
* ANN backend behind the same seam once N grows beyond the in-memory regime.
|
|
32
|
+
*
|
|
33
|
+
* Every operation returns a `Result` (async, since a real backend does I/O) so
|
|
34
|
+
* failure is explicit and never throws across the seam.
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
export interface IVectorIndex {
|
|
38
|
+
/**
|
|
39
|
+
* Add (or replace) the embedding for `id`. Returns the opaque
|
|
40
|
+
* {@link IMemoryEnvelope.embeddingRef | embeddingRef} the store stamps onto
|
|
41
|
+
* the envelope so a later read knows the record is embedded.
|
|
42
|
+
*/
|
|
43
|
+
add(id: MemoryId, vector: Float32Array): Promise<Result<string>>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Remove the embedding for `id`. Returns the removed id. Idempotent — removing
|
|
47
|
+
* an id with no embedding still succeeds (returns the id).
|
|
48
|
+
*/
|
|
49
|
+
remove(id: MemoryId): Promise<Result<MemoryId>>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Return the `topK` nearest records to `vector`, in descending score order.
|
|
53
|
+
*/
|
|
54
|
+
query(vector: Float32Array, topK: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Embeds a complete record into a vector for the store's embed-on-write hook.
|
|
59
|
+
* Async and `Result`-returning, since a real embedder does a network call (cloud
|
|
60
|
+
* provider) or in-process model inference. The consumer wires this — the core
|
|
61
|
+
* package never calls an embedding provider directly, staying embedder-agnostic.
|
|
62
|
+
* @public
|
|
63
|
+
*/
|
|
64
|
+
export type MemoryEmbedder = (record: IMemoryRecord<unknown>) => Promise<Result<Float32Array>>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The minimal record-source surface {@link InMemoryCosineIndex.rebuild} reads to
|
|
68
|
+
* re-embed an entire vault. {@link IMemoryStore} satisfies it structurally (its
|
|
69
|
+
* `list` accepts an optional filter, which is assignable to this no-argument
|
|
70
|
+
* shape), so a consumer passes the store directly — without the vector packlet
|
|
71
|
+
* taking a dependency on the store packlet (which depends on the vector packlet
|
|
72
|
+
* for {@link IVectorIndex}, so the reverse import would be a cycle).
|
|
73
|
+
* @public
|
|
74
|
+
*/
|
|
75
|
+
export interface IMemoryRecordSource {
|
|
76
|
+
/** List every record in the vault. */
|
|
77
|
+
list(): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;
|
|
78
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import '@fgv/ts-utils-jest';
|
|
7
|
+
import { Converters } from '@fgv/ts-utils';
|
|
8
|
+
import { JsonSchema } from '@fgv/ts-json-base';
|
|
9
|
+
import { BodyConverterRegistry } from '../../../packlets/converters';
|
|
10
|
+
import { Kind } from '../../../packlets/types';
|
|
11
|
+
|
|
12
|
+
const knowledgeKind: Kind = 'knowledge' as Kind;
|
|
13
|
+
const profileKind: Kind = 'profile' as Kind;
|
|
14
|
+
|
|
15
|
+
describe('BodyConverterRegistry', () => {
|
|
16
|
+
let registry: BodyConverterRegistry;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
registry = BodyConverterRegistry.create().orThrow();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('create succeeds', () => {
|
|
23
|
+
expect(BodyConverterRegistry.create()).toSucceed();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('register / has / convert', () => {
|
|
27
|
+
test('registers a Converter and converts a matching body', () => {
|
|
28
|
+
registry.register(knowledgeKind, Converters.string);
|
|
29
|
+
expect(registry.has(knowledgeKind)).toBe(true);
|
|
30
|
+
expect(registry.convert(knowledgeKind, 'a knowledge doc')).toSucceedWith('a knowledge doc');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('a registered Converter rejects an invalid body', () => {
|
|
34
|
+
registry.register(knowledgeKind, Converters.string);
|
|
35
|
+
expect(registry.convert(knowledgeKind, 42)).toFail();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('has returns false for an unregistered kind', () => {
|
|
39
|
+
expect(registry.has(knowledgeKind)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('convert fails for an unregistered kind', () => {
|
|
43
|
+
expect(registry.convert(knowledgeKind, 'x')).toFailWith(
|
|
44
|
+
/no converter registered for kind 'knowledge'/i
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('register replaces a prior registration for the same kind', () => {
|
|
49
|
+
registry.register(knowledgeKind, Converters.string);
|
|
50
|
+
registry.register(knowledgeKind, Converters.number);
|
|
51
|
+
expect(registry.convert(knowledgeKind, 7)).toSucceedWith(7);
|
|
52
|
+
expect(registry.convert(knowledgeKind, 'x')).toFail();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('getConverter', () => {
|
|
57
|
+
test('returns the registered converter', () => {
|
|
58
|
+
registry.register(knowledgeKind, Converters.string);
|
|
59
|
+
expect(registry.getConverter(knowledgeKind)).toSucceedAndSatisfy((converter) => {
|
|
60
|
+
expect(converter.convert('hello')).toSucceedWith('hello');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('fails for an unregistered kind', () => {
|
|
65
|
+
expect(registry.getConverter(knowledgeKind)).toFailWith(/no converter registered for kind/i);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('registerSchema', () => {
|
|
70
|
+
const profileSchema = JsonSchema.object({
|
|
71
|
+
name: JsonSchema.string(),
|
|
72
|
+
age: JsonSchema.integer()
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('registers a JsonSchema validator and validates a matching body', () => {
|
|
76
|
+
registry.registerSchema(profileKind, profileSchema);
|
|
77
|
+
expect(registry.has(profileKind)).toBe(true);
|
|
78
|
+
expect(registry.convert(profileKind, { name: 'Ada', age: 36 })).toSucceedWith({
|
|
79
|
+
name: 'Ada',
|
|
80
|
+
age: 36
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('a registered schema rejects an invalid body', () => {
|
|
85
|
+
registry.registerSchema(profileKind, profileSchema);
|
|
86
|
+
expect(registry.convert(profileKind, { name: 'Ada' })).toFail();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import '@fgv/ts-utils-jest';
|
|
7
|
+
import { Converters } from '@fgv/ts-utils';
|
|
8
|
+
import {
|
|
9
|
+
BodyConverterRegistry,
|
|
10
|
+
edgeConverter,
|
|
11
|
+
envelopeConverter,
|
|
12
|
+
envelopeYamlConverter,
|
|
13
|
+
joinFrontmatter,
|
|
14
|
+
parseMemoryFile,
|
|
15
|
+
provenanceConverter,
|
|
16
|
+
serializeMemoryFile,
|
|
17
|
+
splitFrontmatter,
|
|
18
|
+
temporalConverter
|
|
19
|
+
} from '../../../packlets/converters';
|
|
20
|
+
import { IMemoryEnvelope, Kind } from '../../../packlets/types';
|
|
21
|
+
|
|
22
|
+
const validEnvelopeObject: Record<string, unknown> = {
|
|
23
|
+
id: 'intro-to-rust',
|
|
24
|
+
entityId: 'intro-to-rust',
|
|
25
|
+
kind: 'knowledge',
|
|
26
|
+
tags: ['rust', 'systems'],
|
|
27
|
+
links: [{ type: 'related', target: 'ownership' }],
|
|
28
|
+
created: 1000,
|
|
29
|
+
updated: 2000,
|
|
30
|
+
seq: 5,
|
|
31
|
+
contentHash: 'crc-abc',
|
|
32
|
+
provenance: { source: 'agent', by: 'curator' }
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('provenanceConverter', () => {
|
|
36
|
+
test('validates the known fields', () => {
|
|
37
|
+
expect(
|
|
38
|
+
provenanceConverter.convert({ source: 'human', by: 'erik', model: 'gpt', confidence: 0.9 })
|
|
39
|
+
).toSucceedWith({ source: 'human', by: 'erik', model: 'gpt', confidence: 0.9 });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('preserves opaque extension keys verbatim', () => {
|
|
43
|
+
expect(
|
|
44
|
+
provenanceConverter.convert({ source: 'agent', sentiment: { score: 0.5 }, epistemic: 'belief' })
|
|
45
|
+
).toSucceedAndSatisfy((p) => {
|
|
46
|
+
expect(p.source).toBe('agent');
|
|
47
|
+
expect(p.sentiment).toEqual({ score: 0.5 });
|
|
48
|
+
expect(p.epistemic).toBe('belief');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('accepts an arbitrary source string (open vocabulary)', () => {
|
|
53
|
+
expect(provenanceConverter.convert({ source: 'host-ingest' })).toSucceedWith({ source: 'host-ingest' });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('fails when source is missing', () => {
|
|
57
|
+
expect(provenanceConverter.convert({ by: 'erik' })).toFail();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('fails on a non-object', () => {
|
|
61
|
+
expect(provenanceConverter.convert('nope')).toFail();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('edgeConverter', () => {
|
|
66
|
+
test('validates a full attributed edge', () => {
|
|
67
|
+
expect(
|
|
68
|
+
edgeConverter.convert({
|
|
69
|
+
type: 'mtm-ref',
|
|
70
|
+
target: 'turn-3',
|
|
71
|
+
confidence: 0.8,
|
|
72
|
+
provenance: { source: 'agent' },
|
|
73
|
+
valid_at: 100,
|
|
74
|
+
invalid_at: 200
|
|
75
|
+
})
|
|
76
|
+
).toSucceedAndSatisfy((edge) => {
|
|
77
|
+
expect(edge.type).toBe('mtm-ref');
|
|
78
|
+
expect(edge.target).toBe('turn-3');
|
|
79
|
+
expect(edge.confidence).toBe(0.8);
|
|
80
|
+
expect(edge.provenance).toEqual({ source: 'agent' });
|
|
81
|
+
expect(edge.valid_at).toBe(100);
|
|
82
|
+
expect(edge.invalid_at).toBe(200);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('validates a minimal edge', () => {
|
|
87
|
+
expect(edgeConverter.convert({ type: 'related', target: 'ownership' })).toSucceedAndSatisfy((edge) => {
|
|
88
|
+
expect(edge.type).toBe('related');
|
|
89
|
+
expect(edge.target).toBe('ownership');
|
|
90
|
+
expect(edge.confidence).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('accepts a null invalid_at (still valid)', () => {
|
|
95
|
+
expect(edgeConverter.convert({ type: 'related', target: 'x', invalid_at: null })).toSucceedAndSatisfy(
|
|
96
|
+
(edge) => {
|
|
97
|
+
expect(edge.invalid_at).toBeNull();
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('fails when invalid_at is neither a number nor null', () => {
|
|
103
|
+
expect(edgeConverter.convert({ type: 'related', target: 'x', invalid_at: 'soon' })).toFail();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('fails when target is missing', () => {
|
|
107
|
+
expect(edgeConverter.convert({ type: 'related' })).toFail();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('temporalConverter', () => {
|
|
112
|
+
test('validates a temporal block', () => {
|
|
113
|
+
expect(temporalConverter.convert({ valid_at: 1, invalid_at: null })).toSucceedWith({
|
|
114
|
+
valid_at: 1,
|
|
115
|
+
invalid_at: null
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('validates an empty temporal block', () => {
|
|
120
|
+
expect(temporalConverter.convert({})).toSucceedWith({});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('envelopeConverter', () => {
|
|
125
|
+
test('validates a full envelope without optional blocks', () => {
|
|
126
|
+
expect(envelopeConverter.convert(validEnvelopeObject)).toSucceedAndSatisfy((envelope) => {
|
|
127
|
+
expect(envelope.id).toBe('intro-to-rust');
|
|
128
|
+
expect(envelope.tags).toEqual(['rust', 'systems']);
|
|
129
|
+
expect(envelope.links).toHaveLength(1);
|
|
130
|
+
expect(envelope.temporal).toBeUndefined();
|
|
131
|
+
expect(envelope.embeddingRef).toBeUndefined();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('validates a temporal envelope with a string embeddingRef', () => {
|
|
136
|
+
expect(
|
|
137
|
+
envelopeConverter.convert({
|
|
138
|
+
...validEnvelopeObject,
|
|
139
|
+
temporal: { valid_at: 10 },
|
|
140
|
+
embeddingRef: 'vec-1'
|
|
141
|
+
})
|
|
142
|
+
).toSucceedAndSatisfy((envelope) => {
|
|
143
|
+
expect(envelope.temporal).toEqual({ valid_at: 10 });
|
|
144
|
+
expect(envelope.embeddingRef).toBe('vec-1');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('accepts a null embeddingRef (not embedded)', () => {
|
|
149
|
+
expect(envelopeConverter.convert({ ...validEnvelopeObject, embeddingRef: null })).toSucceedAndSatisfy(
|
|
150
|
+
(envelope) => {
|
|
151
|
+
expect(envelope.embeddingRef).toBeNull();
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('fails when embeddingRef is neither a string nor null', () => {
|
|
157
|
+
expect(envelopeConverter.convert({ ...validEnvelopeObject, embeddingRef: 42 })).toFail();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('fails when a required field is missing', () => {
|
|
161
|
+
const missing: Record<string, unknown> = { ...validEnvelopeObject };
|
|
162
|
+
delete missing.contentHash;
|
|
163
|
+
expect(envelopeConverter.convert(missing)).toFail();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('envelopeYamlConverter', () => {
|
|
168
|
+
test('parses an envelope from a YAML string', () => {
|
|
169
|
+
const yaml = serializeMemoryFile(
|
|
170
|
+
envelopeConverter.convert(validEnvelopeObject).orThrow(),
|
|
171
|
+
'body text'
|
|
172
|
+
).orThrow();
|
|
173
|
+
const frontmatter = splitFrontmatter(yaml).orThrow().frontmatter;
|
|
174
|
+
expect(envelopeYamlConverter.convert(frontmatter)).toSucceedAndSatisfy((envelope) => {
|
|
175
|
+
expect(envelope.id).toBe('intro-to-rust');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('fails on a YAML string that is not an object', () => {
|
|
180
|
+
expect(envelopeYamlConverter.convert('just a scalar')).toFail();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('splitFrontmatter / joinFrontmatter', () => {
|
|
185
|
+
test('splits a well-formed memory file', () => {
|
|
186
|
+
const raw = '---\nid: x\nkind: knowledge\n---\nThe body.\nSecond line.';
|
|
187
|
+
expect(splitFrontmatter(raw)).toSucceedWith({
|
|
188
|
+
frontmatter: 'id: x\nkind: knowledge',
|
|
189
|
+
body: 'The body.\nSecond line.'
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('strips trailing CR so CRLF-authored files parse identically to LF', () => {
|
|
194
|
+
const raw = '---\r\nid: x\r\nkind: knowledge\r\n---\r\nThe body.\r\nSecond line.';
|
|
195
|
+
expect(splitFrontmatter(raw)).toSucceedWith({
|
|
196
|
+
frontmatter: 'id: x\nkind: knowledge',
|
|
197
|
+
body: 'The body.\nSecond line.'
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('fails when the opening delimiter is missing', () => {
|
|
202
|
+
expect(splitFrontmatter('no frontmatter here')).toFailWith(/missing opening frontmatter delimiter/i);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('fails when the closing delimiter is missing', () => {
|
|
206
|
+
expect(splitFrontmatter('---\nid: x\nstill going')).toFailWith(/missing closing frontmatter delimiter/i);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('join adds a trailing newline to the frontmatter when absent', () => {
|
|
210
|
+
expect(joinFrontmatter('id: x', 'body')).toBe('---\nid: x\n---\nbody');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('join does not double the trailing newline when present', () => {
|
|
214
|
+
expect(joinFrontmatter('id: x\n', 'body')).toBe('---\nid: x\n---\nbody');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('split and join round-trip', () => {
|
|
218
|
+
const raw = '---\nid: x\n---\nbody content';
|
|
219
|
+
const parts = splitFrontmatter(raw).orThrow();
|
|
220
|
+
expect(joinFrontmatter(parts.frontmatter, parts.body)).toBe(raw);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('parseMemoryFile / serializeMemoryFile', () => {
|
|
225
|
+
let registry: BodyConverterRegistry;
|
|
226
|
+
const knowledgeKind: Kind = 'knowledge' as Kind;
|
|
227
|
+
|
|
228
|
+
beforeEach(() => {
|
|
229
|
+
registry = BodyConverterRegistry.create().orThrow();
|
|
230
|
+
registry.register(knowledgeKind, Converters.string);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('serializes then parses a record round-trip', () => {
|
|
234
|
+
const envelope = envelopeConverter.convert(validEnvelopeObject).orThrow();
|
|
235
|
+
const file = serializeMemoryFile(envelope, 'The knowledge body.').orThrow();
|
|
236
|
+
expect(parseMemoryFile(file, registry)).toSucceedAndSatisfy((record) => {
|
|
237
|
+
expect(record.envelope.id).toBe('intro-to-rust');
|
|
238
|
+
expect(record.body).toBe('The knowledge body.');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('fails when the frontmatter cannot be split', () => {
|
|
243
|
+
expect(parseMemoryFile('no delimiters', registry)).toFailWith(/missing opening frontmatter delimiter/i);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('fails with envelope context when the envelope is invalid', () => {
|
|
247
|
+
const file = '---\nid: x\n---\nbody';
|
|
248
|
+
expect(parseMemoryFile(file, registry)).toFailWith(/invalid envelope/i);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('fails with body context when the body fails the registered converter', () => {
|
|
252
|
+
const envelope = envelopeConverter.convert({ ...validEnvelopeObject, kind: 'unregistered' }).orThrow();
|
|
253
|
+
const file = serializeMemoryFile(envelope, 'body').orThrow();
|
|
254
|
+
expect(parseMemoryFile(file, registry)).toFailWith(/invalid body.*no converter registered/i);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('serialize fails (with id context) when the envelope cannot be dumped to YAML', () => {
|
|
258
|
+
const unserializable = { ...validEnvelopeObject, weird: BigInt(10) } as unknown as IMemoryEnvelope;
|
|
259
|
+
expect(serializeMemoryFile(unserializable, 'body')).toFailWith(/failed to serialize envelope/i);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import '@fgv/ts-utils-jest';
|
|
7
|
+
import {
|
|
8
|
+
IIndexedMemoryRecord,
|
|
9
|
+
IMemoryRecord,
|
|
10
|
+
Kind,
|
|
11
|
+
MemoryId,
|
|
12
|
+
MemoryIndex,
|
|
13
|
+
MemoryScopeKey,
|
|
14
|
+
Tag,
|
|
15
|
+
envelopeConverter
|
|
16
|
+
} from '../../../index';
|
|
17
|
+
|
|
18
|
+
interface IRecordSpec {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly scope?: string;
|
|
21
|
+
readonly kind?: string;
|
|
22
|
+
readonly tags?: ReadonlyArray<string>;
|
|
23
|
+
readonly links?: ReadonlyArray<string>;
|
|
24
|
+
readonly updated?: number;
|
|
25
|
+
readonly seq?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeEntry(spec: IRecordSpec): IIndexedMemoryRecord {
|
|
29
|
+
const record: IMemoryRecord<unknown> = {
|
|
30
|
+
envelope: envelopeConverter
|
|
31
|
+
.convert({
|
|
32
|
+
id: spec.id,
|
|
33
|
+
entityId: spec.id,
|
|
34
|
+
kind: spec.kind ?? 'knowledge',
|
|
35
|
+
tags: spec.tags ?? [],
|
|
36
|
+
links: (spec.links ?? []).map((target) => ({ type: 'rel', target })),
|
|
37
|
+
created: 0,
|
|
38
|
+
updated: spec.updated ?? 0,
|
|
39
|
+
seq: spec.seq ?? 0,
|
|
40
|
+
contentHash: 'h',
|
|
41
|
+
provenance: { source: 'agent' }
|
|
42
|
+
})
|
|
43
|
+
.orThrow(),
|
|
44
|
+
body: `body-${spec.id}`
|
|
45
|
+
};
|
|
46
|
+
return { scope: (spec.scope ?? 'knowledge') as MemoryScopeKey, record };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ids(records: ReadonlyArray<IMemoryRecord<unknown>>): ReadonlyArray<string> {
|
|
50
|
+
return records.map((r) => r.envelope.id as string);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('MemoryIndex', () => {
|
|
54
|
+
let index: MemoryIndex;
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
index = MemoryIndex.create().orThrow();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('create', () => {
|
|
61
|
+
test('starts empty', () => {
|
|
62
|
+
expect(index.entries()).toHaveLength(0);
|
|
63
|
+
expect(index.byKind('knowledge' as Kind)).toHaveLength(0);
|
|
64
|
+
expect(index.byTag('any' as Tag)).toHaveLength(0);
|
|
65
|
+
expect(index.byRecency()).toHaveLength(0);
|
|
66
|
+
expect(index.backlinks('none' as MemoryId)).toHaveLength(0);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('rebuild', () => {
|
|
71
|
+
test('indexes every entry and reports the count', () => {
|
|
72
|
+
const entries = [
|
|
73
|
+
makeEntry({ id: 'a', tags: ['x'], links: ['b'] }),
|
|
74
|
+
makeEntry({ id: 'b', tags: ['x', 'y'] })
|
|
75
|
+
];
|
|
76
|
+
expect(index.rebuild(entries)).toSucceedWith(2);
|
|
77
|
+
expect(index.entries()).toHaveLength(2);
|
|
78
|
+
expect([...ids(index.byKind('knowledge' as Kind))].sort()).toEqual(['a', 'b']);
|
|
79
|
+
expect([...ids(index.byTag('x' as Tag))].sort()).toEqual(['a', 'b']);
|
|
80
|
+
expect(ids(index.byTag('y' as Tag))).toEqual(['b']);
|
|
81
|
+
expect(index.backlinks('b' as MemoryId)).toEqual(['a']);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('clears prior state', () => {
|
|
85
|
+
index.rebuild([makeEntry({ id: 'a', tags: ['x'] })]).orThrow();
|
|
86
|
+
expect(index.rebuild([makeEntry({ id: 'c', tags: ['z'] })])).toSucceedWith(1);
|
|
87
|
+
expect(ids(index.entries().map((e) => e.record))).toEqual(['c']);
|
|
88
|
+
expect(index.byTag('x' as Tag)).toHaveLength(0);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('patch', () => {
|
|
93
|
+
test('put inserts a new entry and returns it', () => {
|
|
94
|
+
const entry = makeEntry({ id: 'a', tags: ['x'], links: ['b'] });
|
|
95
|
+
expect(index.patch('put', entry)).toSucceedWith(entry);
|
|
96
|
+
expect(ids(index.byKind('knowledge' as Kind))).toEqual(['a']);
|
|
97
|
+
expect(index.backlinks('b' as MemoryId)).toEqual(['a']);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('put on an existing key replaces stale associations', () => {
|
|
101
|
+
index.patch('put', makeEntry({ id: 'a', kind: 'knowledge', tags: ['old'], links: ['b'] })).orThrow();
|
|
102
|
+
index.patch('put', makeEntry({ id: 'a', kind: 'note', tags: ['new'], links: ['c'] })).orThrow();
|
|
103
|
+
expect(index.entries()).toHaveLength(1);
|
|
104
|
+
expect(index.byTag('old' as Tag)).toHaveLength(0);
|
|
105
|
+
expect(ids(index.byTag('new' as Tag))).toEqual(['a']);
|
|
106
|
+
expect(index.byKind('knowledge' as Kind)).toHaveLength(0);
|
|
107
|
+
expect(ids(index.byKind('note' as Kind))).toEqual(['a']);
|
|
108
|
+
expect(index.backlinks('b' as MemoryId)).toHaveLength(0);
|
|
109
|
+
expect(index.backlinks('c' as MemoryId)).toEqual(['a']);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('delete removes the entry and its associations', () => {
|
|
113
|
+
const entry = makeEntry({ id: 'a', tags: ['x'], links: ['b'] });
|
|
114
|
+
index.patch('put', entry).orThrow();
|
|
115
|
+
expect(index.patch('delete', entry)).toSucceedWith(entry);
|
|
116
|
+
expect(index.entries()).toHaveLength(0);
|
|
117
|
+
expect(index.byTag('x' as Tag)).toHaveLength(0);
|
|
118
|
+
expect(index.backlinks('b' as MemoryId)).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('delete of an absent key is a no-op that still succeeds', () => {
|
|
122
|
+
const entry = makeEntry({ id: 'ghost' });
|
|
123
|
+
expect(index.patch('delete', entry)).toSucceedWith(entry);
|
|
124
|
+
expect(index.entries()).toHaveLength(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('distinguishes entries with the same id across scopes', () => {
|
|
128
|
+
index.patch('put', makeEntry({ id: 'turn-0', scope: 'conversations/c1' })).orThrow();
|
|
129
|
+
index.patch('put', makeEntry({ id: 'turn-0', scope: 'conversations/c2' })).orThrow();
|
|
130
|
+
expect(index.entries()).toHaveLength(2);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('byRecency', () => {
|
|
135
|
+
test('orders most-recently-updated first, breaking ties by seq', () => {
|
|
136
|
+
index
|
|
137
|
+
.rebuild([
|
|
138
|
+
makeEntry({ id: 'old', updated: 100, seq: 1 }),
|
|
139
|
+
makeEntry({ id: 'newest', updated: 300, seq: 3 }),
|
|
140
|
+
makeEntry({ id: 'tie-lo', updated: 200, seq: 4 }),
|
|
141
|
+
makeEntry({ id: 'tie-hi', updated: 200, seq: 9 })
|
|
142
|
+
])
|
|
143
|
+
.orThrow();
|
|
144
|
+
expect(ids(index.byRecency())).toEqual(['newest', 'tie-hi', 'tie-lo', 'old']);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('backlinks', () => {
|
|
149
|
+
test('aggregates multiple sources pointing at one target', () => {
|
|
150
|
+
index
|
|
151
|
+
.rebuild([makeEntry({ id: 'a', links: ['target'] }), makeEntry({ id: 'b', links: ['target'] })])
|
|
152
|
+
.orThrow();
|
|
153
|
+
expect([...index.backlinks('target' as MemoryId)].sort()).toEqual(['a', 'b']);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('drops the target set once the last source is removed', () => {
|
|
157
|
+
const a = makeEntry({ id: 'a', links: ['target'] });
|
|
158
|
+
index.patch('put', a).orThrow();
|
|
159
|
+
index.patch('delete', a).orThrow();
|
|
160
|
+
expect(index.backlinks('target' as MemoryId)).toHaveLength(0);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('tracks same-id sources across scopes independently', () => {
|
|
164
|
+
// Two distinct records that share the id 'a' under different scopes, both
|
|
165
|
+
// linking 'target'. Removing one must NOT drop the other's inbound edge.
|
|
166
|
+
const a1 = makeEntry({ id: 'a', scope: 'conversations/c1', links: ['target'] });
|
|
167
|
+
const a2 = makeEntry({ id: 'a', scope: 'conversations/c2', links: ['target'] });
|
|
168
|
+
index.patch('put', a1).orThrow();
|
|
169
|
+
index.patch('put', a2).orThrow();
|
|
170
|
+
expect(index.backlinks('target' as MemoryId)).toEqual(['a', 'a']);
|
|
171
|
+
index.patch('delete', a1).orThrow();
|
|
172
|
+
expect(index.backlinks('target' as MemoryId)).toEqual(['a']);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('duplicate associations', () => {
|
|
177
|
+
test('a record with duplicate tags and links is cleaned up exactly once on delete', () => {
|
|
178
|
+
const entry = makeEntry({ id: 'a', tags: ['dup', 'dup'], links: ['t', 't'] });
|
|
179
|
+
index.patch('put', entry).orThrow();
|
|
180
|
+
expect(ids(index.byTag('dup' as Tag))).toEqual(['a']);
|
|
181
|
+
expect(index.backlinks('t' as MemoryId)).toEqual(['a']);
|
|
182
|
+
index.patch('delete', entry).orThrow();
|
|
183
|
+
expect(index.byTag('dup' as Tag)).toHaveLength(0);
|
|
184
|
+
expect(index.backlinks('t' as MemoryId)).toHaveLength(0);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|