@fgv/ts-agent-memory 5.1.0-39 → 5.1.0-41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/{b82cf6bdece20481260e6bab946179eeec9d7b46.tar.log → cbbdbe09515171b4eba8f2592be2dace1e4e8142.tar.log} +14 -2
- package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +2 -2
- package/.rush/temp/operation/build/all.log +2 -2
- package/.rush/temp/operation/build/log-chunks.jsonl +2 -2
- package/.rush/temp/operation/build/state.json +1 -1
- package/dist/packlets/converters/envelopeConverter.js +17 -3
- package/dist/packlets/converters/envelopeConverter.js.map +1 -1
- package/dist/packlets/index/memoryIndex.js +58 -10
- package/dist/packlets/index/memoryIndex.js.map +1 -1
- package/dist/packlets/ingest/cycleGuard.js +13 -6
- package/dist/packlets/ingest/cycleGuard.js.map +1 -1
- package/dist/packlets/ingest/hostStages.js.map +1 -1
- package/dist/packlets/ingest/model.js.map +1 -1
- package/dist/packlets/ingest/orchestrator.js +94 -49
- package/dist/packlets/ingest/orchestrator.js.map +1 -1
- package/dist/packlets/retrieve/hybridRetriever.js +13 -2
- package/dist/packlets/retrieve/hybridRetriever.js.map +1 -1
- package/dist/packlets/retrieve/linkTraversalRetriever.js +46 -57
- package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
- package/dist/packlets/retrieve/recencyRetriever.js +3 -3
- package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
- package/dist/packlets/retrieve/retriever.js +50 -7
- package/dist/packlets/retrieve/retriever.js.map +1 -1
- package/dist/packlets/retrieve/semanticRetriever.js +9 -3
- package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
- package/dist/packlets/retrieve/structuredFilterRetriever.js +3 -3
- package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
- package/dist/packlets/retrieve/tagRetriever.js +3 -3
- package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
- package/dist/packlets/retrieve/temporalRetrievers.js +3 -3
- package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
- package/dist/packlets/store/fileTreeMemoryStore.js +72 -16
- package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/dist/packlets/tools/memoryTools.js +104 -21
- package/dist/packlets/tools/memoryTools.js.map +1 -1
- package/dist/packlets/types/envelope.js +13 -1
- package/dist/packlets/types/envelope.js.map +1 -1
- package/dist/packlets/vector/inMemoryCosineIndex.js +22 -17
- package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
- package/dist/packlets/vector/vectorIndex.js.map +1 -1
- package/dist/test/unit/converters/antagonistRoundTrip.test.js +3 -3
- package/dist/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
- package/dist/test/unit/converters/envelopeConverter.test.js +125 -8
- package/dist/test/unit/converters/envelopeConverter.test.js.map +1 -1
- package/dist/test/unit/index/memoryIndex.test.js +87 -25
- package/dist/test/unit/index/memoryIndex.test.js.map +1 -1
- package/dist/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
- package/dist/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
- package/dist/test/unit/ingest/cycleGuard.test.js +28 -1
- package/dist/test/unit/ingest/cycleGuard.test.js.map +1 -1
- package/dist/test/unit/ingest/orchestrator.test.js +187 -45
- package/dist/test/unit/ingest/orchestrator.test.js.map +1 -1
- package/dist/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
- package/dist/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
- package/dist/test/unit/retrieve/retrievers.test.js +299 -37
- package/dist/test/unit/retrieve/retrievers.test.js.map +1 -1
- package/dist/test/unit/store/embedOnWrite.test.js +69 -12
- package/dist/test/unit/store/embedOnWrite.test.js.map +1 -1
- package/dist/test/unit/store/listScoped.test.js +109 -0
- package/dist/test/unit/store/listScoped.test.js.map +1 -0
- package/dist/test/unit/store/rankAxis.test.js +254 -0
- package/dist/test/unit/store/rankAxis.test.js.map +1 -0
- package/dist/test/unit/tools/memoryTools.test.js +280 -11
- package/dist/test/unit/tools/memoryTools.test.js.map +1 -1
- package/dist/test/unit/types/writePolicy.test.js +9 -2
- package/dist/test/unit/types/writePolicy.test.js.map +1 -1
- package/dist/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
- package/dist/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
- package/dist/test/unit/vector/vectorIndex.test.js +24 -15
- package/dist/test/unit/vector/vectorIndex.test.js.map +1 -1
- package/dist/ts-agent-memory.d.ts +384 -97
- package/etc/ts-agent-memory.api.md +64 -22
- package/lib/packlets/converters/envelopeConverter.d.ts +8 -1
- package/lib/packlets/converters/envelopeConverter.d.ts.map +1 -1
- package/lib/packlets/converters/envelopeConverter.js +18 -4
- package/lib/packlets/converters/envelopeConverter.js.map +1 -1
- package/lib/packlets/index/memoryIndex.d.ts +42 -10
- package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
- package/lib/packlets/index/memoryIndex.js +58 -10
- package/lib/packlets/index/memoryIndex.js.map +1 -1
- package/lib/packlets/ingest/cycleGuard.d.ts +5 -5
- package/lib/packlets/ingest/cycleGuard.d.ts.map +1 -1
- package/lib/packlets/ingest/cycleGuard.js +13 -6
- package/lib/packlets/ingest/cycleGuard.js.map +1 -1
- package/lib/packlets/ingest/hostStages.d.ts +3 -3
- package/lib/packlets/ingest/hostStages.d.ts.map +1 -1
- package/lib/packlets/ingest/hostStages.js.map +1 -1
- package/lib/packlets/ingest/model.d.ts +25 -14
- package/lib/packlets/ingest/model.d.ts.map +1 -1
- package/lib/packlets/ingest/model.js.map +1 -1
- package/lib/packlets/ingest/orchestrator.d.ts +18 -4
- package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
- package/lib/packlets/ingest/orchestrator.js +93 -48
- package/lib/packlets/ingest/orchestrator.js.map +1 -1
- package/lib/packlets/retrieve/hybridRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/hybridRetriever.js +12 -1
- package/lib/packlets/retrieve/hybridRetriever.js.map +1 -1
- package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +18 -23
- package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/linkTraversalRetriever.js +45 -56
- package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
- package/lib/packlets/retrieve/recencyRetriever.js +2 -2
- package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
- package/lib/packlets/retrieve/retriever.d.ts +70 -11
- package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/retriever.js +52 -7
- package/lib/packlets/retrieve/retriever.js.map +1 -1
- package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/semanticRetriever.js +9 -3
- package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
- package/lib/packlets/retrieve/structuredFilterRetriever.js +2 -2
- package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
- package/lib/packlets/retrieve/tagRetriever.js +2 -2
- package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
- package/lib/packlets/retrieve/temporalRetrievers.js +3 -3
- package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.d.ts +57 -3
- package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.js +72 -16
- package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/lib/packlets/tools/memoryTools.d.ts +24 -0
- package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
- package/lib/packlets/tools/memoryTools.js +104 -21
- package/lib/packlets/tools/memoryTools.js.map +1 -1
- package/lib/packlets/types/envelope.d.ts +61 -8
- package/lib/packlets/types/envelope.d.ts.map +1 -1
- package/lib/packlets/types/envelope.js +14 -0
- package/lib/packlets/types/envelope.js.map +1 -1
- package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -5
- package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
- package/lib/packlets/vector/inMemoryCosineIndex.js +22 -17
- package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
- package/lib/packlets/vector/vectorIndex.d.ts +46 -20
- package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
- package/lib/packlets/vector/vectorIndex.js.map +1 -1
- package/lib/test/unit/converters/antagonistRoundTrip.test.js +3 -3
- package/lib/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
- package/lib/test/unit/converters/envelopeConverter.test.js +124 -7
- package/lib/test/unit/converters/envelopeConverter.test.js.map +1 -1
- package/lib/test/unit/index/memoryIndex.test.js +86 -24
- package/lib/test/unit/index/memoryIndex.test.js.map +1 -1
- package/lib/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
- package/lib/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
- package/lib/test/unit/ingest/cycleGuard.test.js +28 -1
- package/lib/test/unit/ingest/cycleGuard.test.js.map +1 -1
- package/lib/test/unit/ingest/orchestrator.test.js +186 -44
- package/lib/test/unit/ingest/orchestrator.test.js.map +1 -1
- package/lib/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
- package/lib/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
- package/lib/test/unit/retrieve/retrievers.test.js +298 -36
- package/lib/test/unit/retrieve/retrievers.test.js.map +1 -1
- package/lib/test/unit/store/embedOnWrite.test.js +68 -11
- package/lib/test/unit/store/embedOnWrite.test.js.map +1 -1
- package/lib/test/unit/store/listScoped.test.d.ts +2 -0
- package/lib/test/unit/store/listScoped.test.d.ts.map +1 -0
- package/lib/test/unit/store/listScoped.test.js +111 -0
- package/lib/test/unit/store/listScoped.test.js.map +1 -0
- package/lib/test/unit/store/rankAxis.test.d.ts +2 -0
- package/lib/test/unit/store/rankAxis.test.d.ts.map +1 -0
- package/lib/test/unit/store/rankAxis.test.js +256 -0
- package/lib/test/unit/store/rankAxis.test.js.map +1 -0
- package/lib/test/unit/tools/memoryTools.test.js +280 -11
- package/lib/test/unit/tools/memoryTools.test.js.map +1 -1
- package/lib/test/unit/types/writePolicy.test.js +9 -2
- package/lib/test/unit/types/writePolicy.test.js.map +1 -1
- package/lib/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
- package/lib/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
- package/lib/test/unit/vector/vectorIndex.test.js +24 -15
- package/lib/test/unit/vector/vectorIndex.test.js.map +1 -1
- package/package.json +7 -7
- package/rush-logs/ts-agent-memory.build.cache.log +1 -1
- package/rush-logs/ts-agent-memory.build.log +2 -2
- package/src/packlets/converters/envelopeConverter.ts +27 -4
- package/src/packlets/index/memoryIndex.ts +86 -22
- package/src/packlets/ingest/cycleGuard.ts +22 -11
- package/src/packlets/ingest/hostStages.ts +3 -3
- package/src/packlets/ingest/model.ts +25 -14
- package/src/packlets/ingest/orchestrator.ts +143 -67
- package/src/packlets/retrieve/hybridRetriever.ts +14 -1
- package/src/packlets/retrieve/linkTraversalRetriever.ts +51 -62
- package/src/packlets/retrieve/recencyRetriever.ts +3 -3
- package/src/packlets/retrieve/retriever.ts +97 -13
- package/src/packlets/retrieve/semanticRetriever.ts +10 -5
- package/src/packlets/retrieve/structuredFilterRetriever.ts +3 -3
- package/src/packlets/retrieve/tagRetriever.ts +3 -3
- package/src/packlets/retrieve/temporalRetrievers.ts +3 -3
- package/src/packlets/store/fileTreeMemoryStore.ts +117 -12
- package/src/packlets/tools/memoryTools.ts +152 -25
- package/src/packlets/types/envelope.ts +66 -8
- package/src/packlets/vector/inMemoryCosineIndex.ts +45 -22
- package/src/packlets/vector/vectorIndex.ts +47 -20
- package/src/test/unit/converters/antagonistRoundTrip.test.ts +3 -3
- package/src/test/unit/converters/envelopeConverter.test.ts +168 -11
- package/src/test/unit/index/memoryIndex.test.ts +99 -14
- package/src/test/unit/ingest/antagonistCycleAndParity.test.ts +23 -18
- package/src/test/unit/ingest/cycleGuard.test.ts +44 -2
- package/src/test/unit/ingest/orchestrator.test.ts +234 -41
- package/src/test/unit/retrieve/linkTraversalRetriever.test.ts +134 -35
- package/src/test/unit/retrieve/retrievers.test.ts +381 -25
- package/src/test/unit/store/embedOnWrite.test.ts +83 -11
- package/src/test/unit/store/listScoped.test.ts +138 -0
- package/src/test/unit/store/rankAxis.test.ts +349 -0
- package/src/test/unit/tools/memoryTools.test.ts +362 -13
- package/src/test/unit/types/writePolicy.test.ts +11 -2
- package/src/test/unit/vector/inMemoryCosineIndex.test.ts +115 -39
- package/src/test/unit/vector/vectorIndex.test.ts +33 -17
- package/temp/build/lint/_eslint-5eVG3S6w.json +41 -33
- package/temp/build/typescript/ts_8nwakTlr.json +1 -1
- package/temp/ts-agent-memory.api.json +1080 -136
- package/temp/ts-agent-memory.api.md +64 -22
|
@@ -46,7 +46,7 @@ describe('antagonist — full on-disk round-trip preserves every optional field'
|
|
|
46
46
|
links: [
|
|
47
47
|
{
|
|
48
48
|
type: 'rel' as never,
|
|
49
|
-
target: 'doc-2' as never,
|
|
49
|
+
target: { scope: 'knowledge', id: 'doc-2' } as never,
|
|
50
50
|
confidence: 0.42,
|
|
51
51
|
provenance: { source: 'agent', by: 'curator', extra: { nested: true } },
|
|
52
52
|
valid_at: 111,
|
|
@@ -63,7 +63,7 @@ describe('antagonist — full on-disk round-trip preserves every optional field'
|
|
|
63
63
|
by: 'erik',
|
|
64
64
|
model: 'gpt-5',
|
|
65
65
|
confidence: 0.87,
|
|
66
|
-
derivedFrom: 'turn-3' as never,
|
|
66
|
+
derivedFrom: { scope: 'conversations/c1', id: 'turn-3' } as never,
|
|
67
67
|
// Opaque extension keys (per IProvenance's `[key: string]: unknown` arm).
|
|
68
68
|
sentiment: { score: 0.5 },
|
|
69
69
|
epistemic: 'belief'
|
|
@@ -89,7 +89,7 @@ describe('antagonist — full on-disk round-trip preserves every optional field'
|
|
|
89
89
|
entityId: 'doc-2' as IMemoryEnvelope['entityId'],
|
|
90
90
|
kind,
|
|
91
91
|
tags: [],
|
|
92
|
-
links: [{ type: 'rel' as never, target: 'doc-3' as never }],
|
|
92
|
+
links: [{ type: 'rel' as never, target: { scope: 'knowledge', id: 'doc-3' } as never }],
|
|
93
93
|
created: 0,
|
|
94
94
|
updated: 0,
|
|
95
95
|
seq: 0,
|
|
@@ -8,6 +8,7 @@ import { Converters } from '@fgv/ts-utils';
|
|
|
8
8
|
import {
|
|
9
9
|
BodyConverterRegistry,
|
|
10
10
|
edgeConverter,
|
|
11
|
+
edgeTargetConverter,
|
|
11
12
|
envelopeConverter,
|
|
12
13
|
envelopeYamlConverter,
|
|
13
14
|
joinFrontmatter,
|
|
@@ -24,7 +25,7 @@ const validEnvelopeObject: Record<string, unknown> = {
|
|
|
24
25
|
entityId: 'intro-to-rust',
|
|
25
26
|
kind: 'knowledge',
|
|
26
27
|
tags: ['rust', 'systems'],
|
|
27
|
-
links: [{ type: 'related', target: 'ownership' }],
|
|
28
|
+
links: [{ type: 'related', target: { scope: 'knowledge', id: 'ownership' } }],
|
|
28
29
|
created: 1000,
|
|
29
30
|
updated: 2000,
|
|
30
31
|
seq: 5,
|
|
@@ -53,6 +54,33 @@ describe('provenanceConverter', () => {
|
|
|
53
54
|
expect(provenanceConverter.convert({ source: 'host-ingest' })).toSucceedWith({ source: 'host-ingest' });
|
|
54
55
|
});
|
|
55
56
|
|
|
57
|
+
test('validates a scope-qualified derivedFrom (nested { scope, id })', () => {
|
|
58
|
+
expect(
|
|
59
|
+
provenanceConverter.convert({
|
|
60
|
+
source: 'host-ingest',
|
|
61
|
+
derivedFrom: { scope: 'conversations/conv-a', id: 'turn-3' }
|
|
62
|
+
})
|
|
63
|
+
).toSucceedAndSatisfy((p) => {
|
|
64
|
+
expect(p.derivedFrom).toEqual({ scope: 'conversations/conv-a', id: 'turn-3' });
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('rejects a bare-string derivedFrom (the pre-scoped format)', () => {
|
|
69
|
+
expect(provenanceConverter.convert({ source: 'host-ingest', derivedFrom: 'turn-3' })).toFail();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('rejects a derivedFrom missing scope', () => {
|
|
73
|
+
expect(provenanceConverter.convert({ source: 'host-ingest', derivedFrom: { id: 'turn-3' } })).toFailWith(
|
|
74
|
+
/scope/i
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('rejects a derivedFrom missing id', () => {
|
|
79
|
+
expect(
|
|
80
|
+
provenanceConverter.convert({ source: 'host-ingest', derivedFrom: { scope: 'conversations/conv-a' } })
|
|
81
|
+
).toFailWith(/id/i);
|
|
82
|
+
});
|
|
83
|
+
|
|
56
84
|
test('fails when source is missing', () => {
|
|
57
85
|
expect(provenanceConverter.convert({ by: 'erik' })).toFail();
|
|
58
86
|
});
|
|
@@ -62,12 +90,34 @@ describe('provenanceConverter', () => {
|
|
|
62
90
|
});
|
|
63
91
|
});
|
|
64
92
|
|
|
93
|
+
describe('edgeTargetConverter', () => {
|
|
94
|
+
test('validates a scope-qualified target', () => {
|
|
95
|
+
expect(edgeTargetConverter.convert({ scope: 'conversations/conv-a', id: 'turn-3' })).toSucceedAndSatisfy(
|
|
96
|
+
(t) => {
|
|
97
|
+
expect(t).toEqual({ scope: 'conversations/conv-a', id: 'turn-3' });
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('rejects a target missing scope', () => {
|
|
103
|
+
expect(edgeTargetConverter.convert({ id: 'turn-3' })).toFailWith(/scope/i);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('rejects a target missing id', () => {
|
|
107
|
+
expect(edgeTargetConverter.convert({ scope: 'conversations/conv-a' })).toFailWith(/id/i);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('rejects a bare-string target (the pre-scoped format)', () => {
|
|
111
|
+
expect(edgeTargetConverter.convert('turn-3')).toFail();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
65
115
|
describe('edgeConverter', () => {
|
|
66
116
|
test('validates a full attributed edge', () => {
|
|
67
117
|
expect(
|
|
68
118
|
edgeConverter.convert({
|
|
69
119
|
type: 'mtm-ref',
|
|
70
|
-
target: 'turn-3',
|
|
120
|
+
target: { scope: 'conversations/conv-a', id: 'turn-3' },
|
|
71
121
|
confidence: 0.8,
|
|
72
122
|
provenance: { source: 'agent' },
|
|
73
123
|
valid_at: 100,
|
|
@@ -75,7 +125,7 @@ describe('edgeConverter', () => {
|
|
|
75
125
|
})
|
|
76
126
|
).toSucceedAndSatisfy((edge) => {
|
|
77
127
|
expect(edge.type).toBe('mtm-ref');
|
|
78
|
-
expect(edge.target).
|
|
128
|
+
expect(edge.target).toEqual({ scope: 'conversations/conv-a', id: 'turn-3' });
|
|
79
129
|
expect(edge.confidence).toBe(0.8);
|
|
80
130
|
expect(edge.provenance).toEqual({ source: 'agent' });
|
|
81
131
|
expect(edge.valid_at).toBe(100);
|
|
@@ -84,28 +134,36 @@ describe('edgeConverter', () => {
|
|
|
84
134
|
});
|
|
85
135
|
|
|
86
136
|
test('validates a minimal edge', () => {
|
|
87
|
-
expect(
|
|
137
|
+
expect(
|
|
138
|
+
edgeConverter.convert({ type: 'related', target: { scope: 'knowledge', id: 'ownership' } })
|
|
139
|
+
).toSucceedAndSatisfy((edge) => {
|
|
88
140
|
expect(edge.type).toBe('related');
|
|
89
|
-
expect(edge.target).
|
|
141
|
+
expect(edge.target).toEqual({ scope: 'knowledge', id: 'ownership' });
|
|
90
142
|
expect(edge.confidence).toBeUndefined();
|
|
91
143
|
});
|
|
92
144
|
});
|
|
93
145
|
|
|
94
146
|
test('accepts a null invalid_at (still valid)', () => {
|
|
95
|
-
expect(
|
|
96
|
-
(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
);
|
|
147
|
+
expect(
|
|
148
|
+
edgeConverter.convert({ type: 'related', target: { scope: 'knowledge', id: 'x' }, invalid_at: null })
|
|
149
|
+
).toSucceedAndSatisfy((edge) => {
|
|
150
|
+
expect(edge.invalid_at).toBeNull();
|
|
151
|
+
});
|
|
100
152
|
});
|
|
101
153
|
|
|
102
154
|
test('fails when invalid_at is neither a number nor null', () => {
|
|
103
|
-
expect(
|
|
155
|
+
expect(
|
|
156
|
+
edgeConverter.convert({ type: 'related', target: { scope: 'knowledge', id: 'x' }, invalid_at: 'soon' })
|
|
157
|
+
).toFail();
|
|
104
158
|
});
|
|
105
159
|
|
|
106
160
|
test('fails when target is missing', () => {
|
|
107
161
|
expect(edgeConverter.convert({ type: 'related' })).toFail();
|
|
108
162
|
});
|
|
163
|
+
|
|
164
|
+
test('fails when target is a bare string (rejects the pre-scoped format)', () => {
|
|
165
|
+
expect(edgeConverter.convert({ type: 'related', target: 'ownership' })).toFail();
|
|
166
|
+
});
|
|
109
167
|
});
|
|
110
168
|
|
|
111
169
|
describe('temporalConverter', () => {
|
|
@@ -157,6 +215,34 @@ describe('envelopeConverter', () => {
|
|
|
157
215
|
expect(envelopeConverter.convert({ ...validEnvelopeObject, embeddingRef: 42 })).toFail();
|
|
158
216
|
});
|
|
159
217
|
|
|
218
|
+
test('rank is absent when not present', () => {
|
|
219
|
+
expect(envelopeConverter.convert(validEnvelopeObject)).toSucceedAndSatisfy((envelope) => {
|
|
220
|
+
expect(envelope.rank).toBeUndefined();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('validates and preserves a numeric rank', () => {
|
|
225
|
+
expect(envelopeConverter.convert({ ...validEnvelopeObject, rank: 42 })).toSucceedAndSatisfy(
|
|
226
|
+
(envelope) => {
|
|
227
|
+
expect(envelope.rank).toBe(42);
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('serialize/parse round-trips rank through frontmatter', () => {
|
|
233
|
+
const registry = BodyConverterRegistry.create().orThrow();
|
|
234
|
+
registry.register('knowledge' as Kind, Converters.string);
|
|
235
|
+
const envelope = envelopeConverter.convert({ ...validEnvelopeObject, rank: 7 }).orThrow();
|
|
236
|
+
const file = serializeMemoryFile(envelope, 'body').orThrow();
|
|
237
|
+
expect(parseMemoryFile(file, registry)).toSucceedAndSatisfy((record) => {
|
|
238
|
+
expect(record.envelope.rank).toBe(7);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('fails when rank is not a number', () => {
|
|
243
|
+
expect(envelopeConverter.convert({ ...validEnvelopeObject, rank: 'high' })).toFail();
|
|
244
|
+
});
|
|
245
|
+
|
|
160
246
|
test('fails when a required field is missing', () => {
|
|
161
247
|
const missing: Record<string, unknown> = { ...validEnvelopeObject };
|
|
162
248
|
delete missing.contentHash;
|
|
@@ -239,6 +325,77 @@ describe('parseMemoryFile / serializeMemoryFile', () => {
|
|
|
239
325
|
});
|
|
240
326
|
});
|
|
241
327
|
|
|
328
|
+
test('round-trips a scope-qualified edge target through the frontmatter intact', () => {
|
|
329
|
+
const envelope = envelopeConverter
|
|
330
|
+
.convert({
|
|
331
|
+
...validEnvelopeObject,
|
|
332
|
+
links: [{ type: 'mtm-ref', target: { scope: 'conversations/conv-a', id: 'turn-3' } }]
|
|
333
|
+
})
|
|
334
|
+
.orThrow();
|
|
335
|
+
const file = serializeMemoryFile(envelope, 'body').orThrow();
|
|
336
|
+
// On-wire the target is a nested object, NOT a bare scalar.
|
|
337
|
+
expect(file).toMatch(/scope: conversations\/conv-a/);
|
|
338
|
+
expect(file).toMatch(/id: turn-3/);
|
|
339
|
+
expect(parseMemoryFile(file, registry)).toSucceedAndSatisfy((record) => {
|
|
340
|
+
expect(record.envelope.links).toEqual([
|
|
341
|
+
{ type: 'mtm-ref', target: { scope: 'conversations/conv-a', id: 'turn-3' } }
|
|
342
|
+
]);
|
|
343
|
+
// Serializing the reparsed record reproduces byte-identical output.
|
|
344
|
+
expect(serializeMemoryFile(record.envelope, record.body as string)).toSucceedWith(file);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('round-trips a scope-qualified provenance.derivedFrom through the frontmatter intact', () => {
|
|
349
|
+
const envelope = envelopeConverter
|
|
350
|
+
.convert({
|
|
351
|
+
...validEnvelopeObject,
|
|
352
|
+
provenance: {
|
|
353
|
+
source: 'host-ingest',
|
|
354
|
+
derivedFrom: { scope: 'conversations/conv-a', id: 'turn-3' }
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
.orThrow();
|
|
358
|
+
const file = serializeMemoryFile(envelope, 'body').orThrow();
|
|
359
|
+
// On-wire the back-reference is a nested object, NOT a bare scalar.
|
|
360
|
+
expect(file).toMatch(/scope: conversations\/conv-a/);
|
|
361
|
+
expect(parseMemoryFile(file, registry)).toSucceedAndSatisfy((record) => {
|
|
362
|
+
expect(record.envelope.provenance.derivedFrom).toEqual({
|
|
363
|
+
scope: 'conversations/conv-a',
|
|
364
|
+
id: 'turn-3'
|
|
365
|
+
});
|
|
366
|
+
expect(serializeMemoryFile(record.envelope, record.body as string)).toSucceedWith(file);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('two records whose derivedFrom share a stem across scopes round-trip as distinct scoped refs', () => {
|
|
371
|
+
// Same idStem ('turn-3'), different scopes — exactly the ambiguity a bare
|
|
372
|
+
// MemoryId could not disambiguate. Each must round-trip to ITS OWN scope.
|
|
373
|
+
const makeFile = (scope: string): string => {
|
|
374
|
+
const envelope = envelopeConverter
|
|
375
|
+
.convert({
|
|
376
|
+
...validEnvelopeObject,
|
|
377
|
+
provenance: { source: 'host-ingest', derivedFrom: { scope, id: 'turn-3' } }
|
|
378
|
+
})
|
|
379
|
+
.orThrow();
|
|
380
|
+
return serializeMemoryFile(envelope, 'body').orThrow();
|
|
381
|
+
};
|
|
382
|
+
const fileA = makeFile('conversations/conv-a');
|
|
383
|
+
const fileB = makeFile('conversations/conv-b');
|
|
384
|
+
expect(fileA).not.toBe(fileB);
|
|
385
|
+
expect(parseMemoryFile(fileA, registry)).toSucceedAndSatisfy((record) => {
|
|
386
|
+
expect(record.envelope.provenance.derivedFrom).toEqual({
|
|
387
|
+
scope: 'conversations/conv-a',
|
|
388
|
+
id: 'turn-3'
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
expect(parseMemoryFile(fileB, registry)).toSucceedAndSatisfy((record) => {
|
|
392
|
+
expect(record.envelope.provenance.derivedFrom).toEqual({
|
|
393
|
+
scope: 'conversations/conv-b',
|
|
394
|
+
id: 'turn-3'
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
242
399
|
test('fails when the frontmatter cannot be split', () => {
|
|
243
400
|
expect(parseMemoryFile('no delimiters', registry)).toFailWith(/missing opening frontmatter delimiter/i);
|
|
244
401
|
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import '@fgv/ts-utils-jest';
|
|
7
7
|
import {
|
|
8
|
+
IEdgeTarget,
|
|
8
9
|
IIndexedMemoryRecord,
|
|
9
10
|
IMemoryRecord,
|
|
10
11
|
Kind,
|
|
@@ -12,9 +13,27 @@ import {
|
|
|
12
13
|
MemoryIndex,
|
|
13
14
|
MemoryScopeKey,
|
|
14
15
|
Tag,
|
|
15
|
-
envelopeConverter
|
|
16
|
+
envelopeConverter,
|
|
17
|
+
rankCompare
|
|
16
18
|
} from '../../../index';
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The scope every link target in this fixture lives under. Links are authored as
|
|
22
|
+
* bare id strings and resolved to `{ scope: LINK_TARGET_SCOPE, id }`, so the
|
|
23
|
+
* cross-scope source tests can share one target while their sources differ.
|
|
24
|
+
*/
|
|
25
|
+
const LINK_TARGET_SCOPE: string = 'knowledge';
|
|
26
|
+
|
|
27
|
+
/** A scope-qualified backlink-target query for the fixed link-target scope. */
|
|
28
|
+
function bt(id: string): IEdgeTarget {
|
|
29
|
+
return { scope: LINK_TARGET_SCOPE as MemoryScopeKey, id: id as MemoryId };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Extract the bare ids from a set of scope-qualified targets (backlink results). */
|
|
33
|
+
function targetIds(targets: ReadonlyArray<IEdgeTarget>): string[] {
|
|
34
|
+
return targets.map((t) => t.id as string);
|
|
35
|
+
}
|
|
36
|
+
|
|
18
37
|
interface IRecordSpec {
|
|
19
38
|
readonly id: string;
|
|
20
39
|
readonly scope?: string;
|
|
@@ -23,6 +42,7 @@ interface IRecordSpec {
|
|
|
23
42
|
readonly links?: ReadonlyArray<string>;
|
|
24
43
|
readonly updated?: number;
|
|
25
44
|
readonly seq?: number;
|
|
45
|
+
readonly rank?: number;
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
function makeEntry(spec: IRecordSpec): IIndexedMemoryRecord {
|
|
@@ -33,11 +53,15 @@ function makeEntry(spec: IRecordSpec): IIndexedMemoryRecord {
|
|
|
33
53
|
entityId: spec.id,
|
|
34
54
|
kind: spec.kind ?? 'knowledge',
|
|
35
55
|
tags: spec.tags ?? [],
|
|
36
|
-
links: (spec.links ?? []).map((target) => ({
|
|
56
|
+
links: (spec.links ?? []).map((target) => ({
|
|
57
|
+
type: 'rel',
|
|
58
|
+
target: { scope: LINK_TARGET_SCOPE, id: target }
|
|
59
|
+
})),
|
|
37
60
|
created: 0,
|
|
38
61
|
updated: spec.updated ?? 0,
|
|
39
62
|
seq: spec.seq ?? 0,
|
|
40
63
|
contentHash: 'h',
|
|
64
|
+
...(spec.rank !== undefined ? { rank: spec.rank } : {}),
|
|
41
65
|
provenance: { source: 'agent' }
|
|
42
66
|
})
|
|
43
67
|
.orThrow(),
|
|
@@ -63,7 +87,7 @@ describe('MemoryIndex', () => {
|
|
|
63
87
|
expect(index.byKind('knowledge' as Kind)).toHaveLength(0);
|
|
64
88
|
expect(index.byTag('any' as Tag)).toHaveLength(0);
|
|
65
89
|
expect(index.byRecency()).toHaveLength(0);
|
|
66
|
-
expect(index.backlinks('none'
|
|
90
|
+
expect(index.backlinks(bt('none'))).toHaveLength(0);
|
|
67
91
|
});
|
|
68
92
|
});
|
|
69
93
|
|
|
@@ -78,7 +102,7 @@ describe('MemoryIndex', () => {
|
|
|
78
102
|
expect([...ids(index.byKind('knowledge' as Kind))].sort()).toEqual(['a', 'b']);
|
|
79
103
|
expect([...ids(index.byTag('x' as Tag))].sort()).toEqual(['a', 'b']);
|
|
80
104
|
expect(ids(index.byTag('y' as Tag))).toEqual(['b']);
|
|
81
|
-
expect(index.backlinks('b'
|
|
105
|
+
expect(targetIds(index.backlinks(bt('b')))).toEqual(['a']);
|
|
82
106
|
});
|
|
83
107
|
|
|
84
108
|
test('clears prior state', () => {
|
|
@@ -94,7 +118,7 @@ describe('MemoryIndex', () => {
|
|
|
94
118
|
const entry = makeEntry({ id: 'a', tags: ['x'], links: ['b'] });
|
|
95
119
|
expect(index.patch('put', entry)).toSucceedWith(entry);
|
|
96
120
|
expect(ids(index.byKind('knowledge' as Kind))).toEqual(['a']);
|
|
97
|
-
expect(index.backlinks('b'
|
|
121
|
+
expect(targetIds(index.backlinks(bt('b')))).toEqual(['a']);
|
|
98
122
|
});
|
|
99
123
|
|
|
100
124
|
test('put on an existing key replaces stale associations', () => {
|
|
@@ -105,8 +129,8 @@ describe('MemoryIndex', () => {
|
|
|
105
129
|
expect(ids(index.byTag('new' as Tag))).toEqual(['a']);
|
|
106
130
|
expect(index.byKind('knowledge' as Kind)).toHaveLength(0);
|
|
107
131
|
expect(ids(index.byKind('note' as Kind))).toEqual(['a']);
|
|
108
|
-
expect(index.backlinks('b'
|
|
109
|
-
expect(index.backlinks('c'
|
|
132
|
+
expect(index.backlinks(bt('b'))).toHaveLength(0);
|
|
133
|
+
expect(targetIds(index.backlinks(bt('c')))).toEqual(['a']);
|
|
110
134
|
});
|
|
111
135
|
|
|
112
136
|
test('delete removes the entry and its associations', () => {
|
|
@@ -115,7 +139,7 @@ describe('MemoryIndex', () => {
|
|
|
115
139
|
expect(index.patch('delete', entry)).toSucceedWith(entry);
|
|
116
140
|
expect(index.entries()).toHaveLength(0);
|
|
117
141
|
expect(index.byTag('x' as Tag)).toHaveLength(0);
|
|
118
|
-
expect(index.backlinks('b'
|
|
142
|
+
expect(index.backlinks(bt('b'))).toHaveLength(0);
|
|
119
143
|
});
|
|
120
144
|
|
|
121
145
|
test('delete of an absent key is a no-op that still succeeds', () => {
|
|
@@ -145,19 +169,80 @@ describe('MemoryIndex', () => {
|
|
|
145
169
|
});
|
|
146
170
|
});
|
|
147
171
|
|
|
172
|
+
describe('byRank', () => {
|
|
173
|
+
test('orders by rank descending, absent-rank records last, recency tiebreak', () => {
|
|
174
|
+
index
|
|
175
|
+
.rebuild([
|
|
176
|
+
makeEntry({ id: 'mid', rank: 5, updated: 100, seq: 1 }),
|
|
177
|
+
makeEntry({ id: 'top', rank: 9, updated: 100, seq: 2 }),
|
|
178
|
+
makeEntry({ id: 'tie-lo', rank: 3, updated: 200, seq: 4 }),
|
|
179
|
+
makeEntry({ id: 'tie-hi', rank: 3, updated: 200, seq: 9 }),
|
|
180
|
+
makeEntry({ id: 'unranked-old', updated: 100, seq: 5 }),
|
|
181
|
+
makeEntry({ id: 'unranked-new', updated: 400, seq: 6 })
|
|
182
|
+
])
|
|
183
|
+
.orThrow();
|
|
184
|
+
// ranked (desc): top(9), mid(5), tie-hi(3,seq9), tie-lo(3,seq4);
|
|
185
|
+
// then unranked by recency: unranked-new(updated400), unranked-old(updated100).
|
|
186
|
+
expect(ids(index.byRank())).toEqual(['top', 'mid', 'tie-hi', 'tie-lo', 'unranked-new', 'unranked-old']);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('is empty on an empty index', () => {
|
|
190
|
+
expect(index.byRank()).toHaveLength(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('sorts a ranked record ahead of an earlier-listed absent-rank record', () => {
|
|
194
|
+
// Input lists the absent-rank record first so the sort compares (ranked, absent),
|
|
195
|
+
// exercising the ranked-before-absent direction of the comparator.
|
|
196
|
+
index
|
|
197
|
+
.rebuild([
|
|
198
|
+
makeEntry({ id: 'unranked', updated: 100, seq: 1 }),
|
|
199
|
+
makeEntry({ id: 'ranked', rank: 5, updated: 100, seq: 2 })
|
|
200
|
+
])
|
|
201
|
+
.orThrow();
|
|
202
|
+
expect(ids(index.byRank())).toEqual(['ranked', 'unranked']);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('orders two absent-rank records by recency alone', () => {
|
|
206
|
+
index
|
|
207
|
+
.rebuild([makeEntry({ id: 'a', updated: 100, seq: 1 }), makeEntry({ id: 'b', updated: 300, seq: 2 })])
|
|
208
|
+
.orThrow();
|
|
209
|
+
expect(ids(index.byRank())).toEqual(['b', 'a']);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("index byRank agrees pairwise with the retrieve packlet's rankCompare", () => {
|
|
213
|
+
// Guard against the two hand-duplicated comparators (index `_compareByRank`
|
|
214
|
+
// and retrieve `rankCompare`) drifting: a future tie-break edit to one that
|
|
215
|
+
// diverges from the other is caught here on a shared mixed fixture.
|
|
216
|
+
const fixture = [
|
|
217
|
+
makeEntry({ id: 'r-top', rank: 9, updated: 100, seq: 1 }),
|
|
218
|
+
makeEntry({ id: 'r-mid', rank: 5, updated: 400, seq: 2 }),
|
|
219
|
+
makeEntry({ id: 'r-tie-a', rank: 3, updated: 200, seq: 3 }),
|
|
220
|
+
makeEntry({ id: 'r-tie-b', rank: 3, updated: 200, seq: 8 }),
|
|
221
|
+
makeEntry({ id: 'absent-old', updated: 100, seq: 4 }),
|
|
222
|
+
makeEntry({ id: 'absent-new', updated: 500, seq: 5 })
|
|
223
|
+
];
|
|
224
|
+
index.rebuild(fixture).orThrow();
|
|
225
|
+
const viaIndex = ids(index.byRank());
|
|
226
|
+
const viaRankCompare = ids(fixture.map((e) => e.record).sort(rankCompare));
|
|
227
|
+
expect(viaIndex).toEqual(viaRankCompare);
|
|
228
|
+
// And the shared expected ordering is what both must produce.
|
|
229
|
+
expect(viaIndex).toEqual(['r-top', 'r-mid', 'r-tie-b', 'r-tie-a', 'absent-new', 'absent-old']);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
148
233
|
describe('backlinks', () => {
|
|
149
234
|
test('aggregates multiple sources pointing at one target', () => {
|
|
150
235
|
index
|
|
151
236
|
.rebuild([makeEntry({ id: 'a', links: ['target'] }), makeEntry({ id: 'b', links: ['target'] })])
|
|
152
237
|
.orThrow();
|
|
153
|
-
expect(
|
|
238
|
+
expect(targetIds(index.backlinks(bt('target'))).sort()).toEqual(['a', 'b']);
|
|
154
239
|
});
|
|
155
240
|
|
|
156
241
|
test('drops the target set once the last source is removed', () => {
|
|
157
242
|
const a = makeEntry({ id: 'a', links: ['target'] });
|
|
158
243
|
index.patch('put', a).orThrow();
|
|
159
244
|
index.patch('delete', a).orThrow();
|
|
160
|
-
expect(index.backlinks('target'
|
|
245
|
+
expect(index.backlinks(bt('target'))).toHaveLength(0);
|
|
161
246
|
});
|
|
162
247
|
|
|
163
248
|
test('tracks same-id sources across scopes independently', () => {
|
|
@@ -167,9 +252,9 @@ describe('MemoryIndex', () => {
|
|
|
167
252
|
const a2 = makeEntry({ id: 'a', scope: 'conversations/c2', links: ['target'] });
|
|
168
253
|
index.patch('put', a1).orThrow();
|
|
169
254
|
index.patch('put', a2).orThrow();
|
|
170
|
-
expect(index.backlinks('target'
|
|
255
|
+
expect(targetIds(index.backlinks(bt('target')))).toEqual(['a', 'a']);
|
|
171
256
|
index.patch('delete', a1).orThrow();
|
|
172
|
-
expect(index.backlinks('target'
|
|
257
|
+
expect(targetIds(index.backlinks(bt('target')))).toEqual(['a']);
|
|
173
258
|
});
|
|
174
259
|
});
|
|
175
260
|
|
|
@@ -178,10 +263,10 @@ describe('MemoryIndex', () => {
|
|
|
178
263
|
const entry = makeEntry({ id: 'a', tags: ['dup', 'dup'], links: ['t', 't'] });
|
|
179
264
|
index.patch('put', entry).orThrow();
|
|
180
265
|
expect(ids(index.byTag('dup' as Tag))).toEqual(['a']);
|
|
181
|
-
expect(index.backlinks('t'
|
|
266
|
+
expect(targetIds(index.backlinks(bt('t')))).toEqual(['a']);
|
|
182
267
|
index.patch('delete', entry).orThrow();
|
|
183
268
|
expect(index.byTag('dup' as Tag)).toHaveLength(0);
|
|
184
|
-
expect(index.backlinks('t'
|
|
269
|
+
expect(index.backlinks(bt('t'))).toHaveLength(0);
|
|
185
270
|
});
|
|
186
271
|
});
|
|
187
272
|
});
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
IBodyConverterRegistry,
|
|
22
22
|
ICandidateEdge,
|
|
23
23
|
ICandidateRecord,
|
|
24
|
+
IEdgeTarget,
|
|
24
25
|
IEntityResolutionCandidate,
|
|
25
26
|
IEntityResolver,
|
|
26
27
|
IFactExtractor,
|
|
@@ -38,12 +39,18 @@ import {
|
|
|
38
39
|
KnowledgeIdentityCodec,
|
|
39
40
|
MemoryId,
|
|
40
41
|
MemoryIngestOrchestrator,
|
|
42
|
+
MemoryScopeKey,
|
|
41
43
|
ResolutionVerdict,
|
|
42
44
|
Tag
|
|
43
45
|
} from '../../../index';
|
|
44
46
|
|
|
45
47
|
const noteKind: Kind = 'note' as Kind;
|
|
46
48
|
|
|
49
|
+
/** A scope-qualified target for the note fixtures (all under the knowledge scope). */
|
|
50
|
+
function kt(id: string): IEdgeTarget {
|
|
51
|
+
return { scope: 'knowledge' as MemoryScopeKey, id: id as MemoryId };
|
|
52
|
+
}
|
|
53
|
+
|
|
47
54
|
function mutableRoot(): FileTree.IMutableFileTreeDirectoryItem {
|
|
48
55
|
const tree = FileTree.inMemory([], { mutable: true }).orThrow();
|
|
49
56
|
const root = tree.getDirectory('/').orThrow();
|
|
@@ -131,7 +138,7 @@ function candidate(
|
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
function candEdge(source: string, type: string, target: string): ICandidateEdge {
|
|
134
|
-
return { source: source
|
|
141
|
+
return { source: kt(source), edge: { type: type as never, target: kt(target) } };
|
|
135
142
|
}
|
|
136
143
|
|
|
137
144
|
function buildOrchestrator(params: {
|
|
@@ -159,7 +166,7 @@ describe('antagonist — cycle guard through EXISTING store edges (integration)'
|
|
|
159
166
|
test('a proposed edge that closes a cycle through a pre-existing on-disk edge is rejected', async () => {
|
|
160
167
|
const store = buildStore();
|
|
161
168
|
// Existing on-disk edge: doc-a -> doc-b (persisted before this ingest runs).
|
|
162
|
-
await putFull(store, 'doc-a', 'anchor-a', [{ type: 'rel' as never, target: 'doc-b'
|
|
169
|
+
await putFull(store, 'doc-a', 'anchor-a', [{ type: 'rel' as never, target: kt('doc-b') }]);
|
|
163
170
|
await putFull(store, 'doc-b', 'anchor-b');
|
|
164
171
|
|
|
165
172
|
// This ingest re-writes doc-b (a 'new'-verdict same-id update, since no
|
|
@@ -183,9 +190,9 @@ describe('antagonist — cycle guard through EXISTING store edges (integration)'
|
|
|
183
190
|
const store = buildStore();
|
|
184
191
|
// Existing chain: a -> b -> c -> d (four pre-existing on-disk edges spanning
|
|
185
192
|
// three hops), persisted before this ingest.
|
|
186
|
-
await putFull(store, 'chain-a', 'a', [{ type: 'rel' as never, target: 'chain-b'
|
|
187
|
-
await putFull(store, 'chain-b', 'b', [{ type: 'rel' as never, target: 'chain-c'
|
|
188
|
-
await putFull(store, 'chain-c', 'c', [{ type: 'rel' as never, target: 'chain-d'
|
|
193
|
+
await putFull(store, 'chain-a', 'a', [{ type: 'rel' as never, target: kt('chain-b') }]);
|
|
194
|
+
await putFull(store, 'chain-b', 'b', [{ type: 'rel' as never, target: kt('chain-c') }]);
|
|
195
|
+
await putFull(store, 'chain-c', 'c', [{ type: 'rel' as never, target: kt('chain-d') }]);
|
|
189
196
|
await putFull(store, 'chain-d', 'd');
|
|
190
197
|
|
|
191
198
|
// Proposed: chain-d -> chain-a, closing the whole loop through the existing chain.
|
|
@@ -199,7 +206,7 @@ describe('antagonist — cycle guard through EXISTING store edges (integration)'
|
|
|
199
206
|
|
|
200
207
|
test('cycleGuard "off" admits a cycle that closes through an existing edge', async () => {
|
|
201
208
|
const store = buildStore();
|
|
202
|
-
await putFull(store, 'doc-a', 'anchor-a', [{ type: 'rel' as never, target: 'doc-b'
|
|
209
|
+
await putFull(store, 'doc-a', 'anchor-a', [{ type: 'rel' as never, target: kt('doc-b') }]);
|
|
203
210
|
await putFull(store, 'doc-b', 'anchor-b');
|
|
204
211
|
const orch = MemoryIngestOrchestrator.create({
|
|
205
212
|
store,
|
|
@@ -211,7 +218,7 @@ describe('antagonist — cycle guard through EXISTING store edges (integration)'
|
|
|
211
218
|
cycleGuard: 'off'
|
|
212
219
|
}).orThrow();
|
|
213
220
|
expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
|
|
214
|
-
expect(r.records[0].record?.envelope.links).toEqual([{ type: 'rel', target: 'doc-a' }]);
|
|
221
|
+
expect(r.records[0].record?.envelope.links).toEqual([{ type: 'rel', target: kt('doc-a') }]);
|
|
215
222
|
});
|
|
216
223
|
});
|
|
217
224
|
});
|
|
@@ -236,7 +243,7 @@ describe('antagonist — resolver target need not be a surfaced `similar` candid
|
|
|
236
243
|
Promise.resolve(succeed(Float32Array.from([1, 0, 0])));
|
|
237
244
|
// Only doc-a is embedded/indexed; doc-q is never surfaced by similarity —
|
|
238
245
|
// it can only be reached by the resolver's own out-of-band choice.
|
|
239
|
-
(await vectorIndex.add('doc-a'
|
|
246
|
+
(await vectorIndex.add(kt('doc-a'), Float32Array.from([1, 0, 0]))).orThrow();
|
|
240
247
|
|
|
241
248
|
const orch = MemoryIngestOrchestrator.create({
|
|
242
249
|
store,
|
|
@@ -247,7 +254,7 @@ describe('antagonist — resolver target need not be a surfaced `similar` candid
|
|
|
247
254
|
relationExtractor: relater(() => succeed([])),
|
|
248
255
|
vectorIndex,
|
|
249
256
|
embed,
|
|
250
|
-
entityResolver: resolver(() => succeed({ verdict, target: 'doc-q'
|
|
257
|
+
entityResolver: resolver(() => succeed({ verdict, target: kt('doc-q') } as ResolutionVerdict))
|
|
251
258
|
}).orThrow();
|
|
252
259
|
|
|
253
260
|
const result = await orch.ingestItem({ id: 'i', content: 'x' });
|
|
@@ -259,7 +266,7 @@ describe('antagonist — resolver target need not be a surfaced `similar` candid
|
|
|
259
266
|
} else if (verdict === 'supersede') {
|
|
260
267
|
expect(result).toSucceedAndSatisfy((r: IIngestItemResult) => {
|
|
261
268
|
expect(r.records[0].disposition).toBe('written');
|
|
262
|
-
expect(r.records[0].resolution).toEqual({ verdict: 'supersede', target: 'doc-q' });
|
|
269
|
+
expect(r.records[0].resolution).toEqual({ verdict: 'supersede', target: kt('doc-q') });
|
|
263
270
|
});
|
|
264
271
|
} else {
|
|
265
272
|
expect(result).toSucceedAndSatisfy((r: IIngestItemResult) => {
|
|
@@ -297,11 +304,11 @@ describe('antagonist — enum-branch parity: uniform structural rejection across
|
|
|
297
304
|
relationExtractor: relater(() => succeed([])),
|
|
298
305
|
vectorIndex,
|
|
299
306
|
embed,
|
|
300
|
-
entityResolver: resolver(() => succeed({ verdict, target: 'ghost'
|
|
307
|
+
entityResolver: resolver(() => succeed({ verdict, target: kt('ghost') } as ResolutionVerdict))
|
|
301
308
|
}).orThrow();
|
|
302
|
-
(await vectorIndex.add('doc-a'
|
|
309
|
+
(await vectorIndex.add(kt('doc-a'), Float32Array.from([1, 0, 0]))).orThrow();
|
|
303
310
|
expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
|
|
304
|
-
`ingest 'i': ${verdict} target 'ghost' does not exist in the store`
|
|
311
|
+
`ingest 'i': ${verdict} target 'knowledge/ghost' does not exist in the store`
|
|
305
312
|
);
|
|
306
313
|
}
|
|
307
314
|
);
|
|
@@ -349,13 +356,11 @@ describe('antagonist — enum-branch parity: uniform structural rejection across
|
|
|
349
356
|
relationExtractor: relater(() => succeed([])),
|
|
350
357
|
vectorIndex,
|
|
351
358
|
embed,
|
|
352
|
-
entityResolver: resolver(() =>
|
|
353
|
-
succeed({ verdict, target: 'foreign-1' as MemoryId } as ResolutionVerdict)
|
|
354
|
-
)
|
|
359
|
+
entityResolver: resolver(() => succeed({ verdict, target: kt('foreign-1') } as ResolutionVerdict))
|
|
355
360
|
}).orThrow();
|
|
356
|
-
(await vectorIndex.add('foreign-1'
|
|
361
|
+
(await vectorIndex.add(kt('foreign-1'), Float32Array.from([1, 0, 0]))).orThrow();
|
|
357
362
|
expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
|
|
358
|
-
`ingest 'i': ${verdict} target 'foreign-1' is kind 'other' but the candidate is kind 'note'`
|
|
363
|
+
`ingest 'i': ${verdict} target 'knowledge/foreign-1' is kind 'other' but the candidate is kind 'note'`
|
|
359
364
|
);
|
|
360
365
|
}
|
|
361
366
|
);
|