@fgv/ts-agent-memory 5.1.0-36 → 5.1.0-39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/{f6a88bfdd66517ccb98c2c7ae1be6e6fe9e15d38.tar.log → b82cf6bdece20481260e6bab946179eeec9d7b46.tar.log} +116 -2
- package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +4 -4
- package/.rush/temp/operation/build/all.log +4 -4
- package/.rush/temp/operation/build/log-chunks.jsonl +4 -4
- package/.rush/temp/operation/build/state.json +1 -1
- package/.rush/temp/shrinkwrap-deps.json +222 -221
- package/config/jest.config.json +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/packlets/ingest/cycleGuard.js +111 -0
- package/dist/packlets/ingest/cycleGuard.js.map +1 -0
- package/dist/packlets/ingest/hostStages.js +6 -0
- package/dist/packlets/ingest/hostStages.js.map +1 -0
- package/dist/packlets/ingest/index.js +9 -0
- package/dist/packlets/ingest/index.js.map +1 -0
- package/dist/packlets/ingest/model.js +6 -0
- package/dist/packlets/ingest/model.js.map +1 -0
- package/dist/packlets/ingest/orchestrator.js +438 -0
- package/dist/packlets/ingest/orchestrator.js.map +1 -0
- package/dist/packlets/retrieve/index.js +1 -0
- package/dist/packlets/retrieve/index.js.map +1 -1
- package/dist/packlets/retrieve/temporalRetrievers.js +172 -0
- package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -0
- package/dist/packlets/store/fileTreeMemoryStore.js +323 -41
- package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/dist/packlets/tools/index.js +6 -0
- package/dist/packlets/tools/index.js.map +1 -0
- package/dist/packlets/tools/memoryTools.js +337 -0
- package/dist/packlets/tools/memoryTools.js.map +1 -0
- package/dist/packlets/types/identityCodec.js +115 -0
- package/dist/packlets/types/identityCodec.js.map +1 -1
- package/dist/packlets/types/index.js +1 -0
- package/dist/packlets/types/index.js.map +1 -1
- package/dist/packlets/types/temporal.js +85 -0
- package/dist/packlets/types/temporal.js.map +1 -0
- package/dist/packlets/types/writePolicy.js +96 -0
- package/dist/packlets/types/writePolicy.js.map +1 -1
- package/dist/test/unit/converters/antagonistRoundTrip.test.js +95 -0
- package/dist/test/unit/converters/antagonistRoundTrip.test.js.map +1 -0
- package/dist/test/unit/ingest/antagonistCycleAndParity.test.js +273 -0
- package/dist/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -0
- package/dist/test/unit/ingest/cycleGuard.test.js +54 -0
- package/dist/test/unit/ingest/cycleGuard.test.js.map +1 -0
- package/dist/test/unit/ingest/orchestrator.test.js +913 -0
- package/dist/test/unit/ingest/orchestrator.test.js.map +1 -0
- package/dist/test/unit/retrieve/temporalRetrievers.test.js +182 -0
- package/dist/test/unit/retrieve/temporalRetrievers.test.js.map +1 -0
- package/dist/test/unit/store/antagonistTemporalBoundary.test.js +120 -0
- package/dist/test/unit/store/antagonistTemporalBoundary.test.js.map +1 -0
- package/dist/test/unit/store/fileTreeMemoryStore.test.js +74 -7
- package/dist/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
- package/dist/test/unit/store/temporalStore.test.js +398 -0
- package/dist/test/unit/store/temporalStore.test.js.map +1 -0
- package/dist/test/unit/tools/memoryTools.test.js +572 -0
- package/dist/test/unit/tools/memoryTools.test.js.map +1 -0
- package/dist/test/unit/types/temporalCodec.test.js +203 -0
- package/dist/test/unit/types/temporalCodec.test.js.map +1 -0
- package/dist/test/unit/types/temporalPolicy.test.js +62 -0
- package/dist/test/unit/types/temporalPolicy.test.js.map +1 -0
- package/dist/ts-agent-memory.d.ts +1042 -10
- package/dist/tsdoc-metadata.json +1 -1
- package/etc/ts-agent-memory.api.md +284 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/packlets/ingest/cycleGuard.d.ts +41 -0
- package/lib/packlets/ingest/cycleGuard.d.ts.map +1 -0
- package/lib/packlets/ingest/cycleGuard.js +115 -0
- package/lib/packlets/ingest/cycleGuard.js.map +1 -0
- package/lib/packlets/ingest/hostStages.d.ts +88 -0
- package/lib/packlets/ingest/hostStages.d.ts.map +1 -0
- package/lib/packlets/ingest/hostStages.js +7 -0
- package/lib/packlets/ingest/hostStages.js.map +1 -0
- package/lib/packlets/ingest/index.d.ts +5 -0
- package/lib/packlets/ingest/index.d.ts.map +1 -0
- package/lib/packlets/ingest/index.js +25 -0
- package/lib/packlets/ingest/index.js.map +1 -0
- package/lib/packlets/ingest/model.d.ts +177 -0
- package/lib/packlets/ingest/model.d.ts.map +1 -0
- package/lib/packlets/ingest/model.js +7 -0
- package/lib/packlets/ingest/model.js.map +1 -0
- package/lib/packlets/ingest/orchestrator.d.ts +206 -0
- package/lib/packlets/ingest/orchestrator.d.ts.map +1 -0
- package/lib/packlets/ingest/orchestrator.js +442 -0
- package/lib/packlets/ingest/orchestrator.js.map +1 -0
- package/lib/packlets/retrieve/index.d.ts +1 -0
- package/lib/packlets/retrieve/index.d.ts.map +1 -1
- package/lib/packlets/retrieve/index.js +1 -0
- package/lib/packlets/retrieve/index.js.map +1 -1
- package/lib/packlets/retrieve/temporalRetrievers.d.ts +78 -0
- package/lib/packlets/retrieve/temporalRetrievers.d.ts.map +1 -0
- package/lib/packlets/retrieve/temporalRetrievers.js +178 -0
- package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -0
- package/lib/packlets/store/fileTreeMemoryStore.d.ts +118 -9
- package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.js +322 -40
- package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/lib/packlets/tools/index.d.ts +2 -0
- package/lib/packlets/tools/index.d.ts.map +1 -0
- package/lib/packlets/tools/index.js +22 -0
- package/lib/packlets/tools/index.js.map +1 -0
- package/lib/packlets/tools/memoryTools.d.ts +139 -0
- package/lib/packlets/tools/memoryTools.d.ts.map +1 -0
- package/lib/packlets/tools/memoryTools.js +341 -0
- package/lib/packlets/tools/memoryTools.js.map +1 -0
- package/lib/packlets/types/identityCodec.d.ts +86 -0
- package/lib/packlets/types/identityCodec.d.ts.map +1 -1
- package/lib/packlets/types/identityCodec.js +118 -1
- package/lib/packlets/types/identityCodec.js.map +1 -1
- package/lib/packlets/types/index.d.ts +1 -0
- package/lib/packlets/types/index.d.ts.map +1 -1
- package/lib/packlets/types/index.js +1 -0
- package/lib/packlets/types/index.js.map +1 -1
- package/lib/packlets/types/temporal.d.ts +40 -0
- package/lib/packlets/types/temporal.d.ts.map +1 -0
- package/lib/packlets/types/temporal.js +92 -0
- package/lib/packlets/types/temporal.js.map +1 -0
- package/lib/packlets/types/writePolicy.d.ts +49 -0
- package/lib/packlets/types/writePolicy.d.ts.map +1 -1
- package/lib/packlets/types/writePolicy.js +98 -1
- package/lib/packlets/types/writePolicy.js.map +1 -1
- package/lib/test/unit/converters/antagonistRoundTrip.test.d.ts +10 -0
- package/lib/test/unit/converters/antagonistRoundTrip.test.d.ts.map +1 -0
- package/lib/test/unit/converters/antagonistRoundTrip.test.js +97 -0
- package/lib/test/unit/converters/antagonistRoundTrip.test.js.map +1 -0
- package/lib/test/unit/ingest/antagonistCycleAndParity.test.d.ts +9 -0
- package/lib/test/unit/ingest/antagonistCycleAndParity.test.d.ts.map +1 -0
- package/lib/test/unit/ingest/antagonistCycleAndParity.test.js +275 -0
- package/lib/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -0
- package/lib/test/unit/ingest/cycleGuard.test.d.ts +2 -0
- package/lib/test/unit/ingest/cycleGuard.test.d.ts.map +1 -0
- package/lib/test/unit/ingest/cycleGuard.test.js +56 -0
- package/lib/test/unit/ingest/cycleGuard.test.js.map +1 -0
- package/lib/test/unit/ingest/orchestrator.test.d.ts +2 -0
- package/lib/test/unit/ingest/orchestrator.test.d.ts.map +1 -0
- package/lib/test/unit/ingest/orchestrator.test.js +915 -0
- package/lib/test/unit/ingest/orchestrator.test.js.map +1 -0
- package/lib/test/unit/retrieve/temporalRetrievers.test.d.ts +2 -0
- package/lib/test/unit/retrieve/temporalRetrievers.test.d.ts.map +1 -0
- package/lib/test/unit/retrieve/temporalRetrievers.test.js +184 -0
- package/lib/test/unit/retrieve/temporalRetrievers.test.js.map +1 -0
- package/lib/test/unit/store/antagonistTemporalBoundary.test.d.ts +9 -0
- package/lib/test/unit/store/antagonistTemporalBoundary.test.d.ts.map +1 -0
- package/lib/test/unit/store/antagonistTemporalBoundary.test.js +122 -0
- package/lib/test/unit/store/antagonistTemporalBoundary.test.js.map +1 -0
- package/lib/test/unit/store/fileTreeMemoryStore.test.js +74 -7
- package/lib/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
- package/lib/test/unit/store/temporalStore.test.d.ts +2 -0
- package/lib/test/unit/store/temporalStore.test.d.ts.map +1 -0
- package/lib/test/unit/store/temporalStore.test.js +400 -0
- package/lib/test/unit/store/temporalStore.test.js.map +1 -0
- package/lib/test/unit/tools/memoryTools.test.d.ts +2 -0
- package/lib/test/unit/tools/memoryTools.test.d.ts.map +1 -0
- package/lib/test/unit/tools/memoryTools.test.js +574 -0
- package/lib/test/unit/tools/memoryTools.test.js.map +1 -0
- package/lib/test/unit/types/temporalCodec.test.d.ts +2 -0
- package/lib/test/unit/types/temporalCodec.test.d.ts.map +1 -0
- package/lib/test/unit/types/temporalCodec.test.js +205 -0
- package/lib/test/unit/types/temporalCodec.test.js.map +1 -0
- package/lib/test/unit/types/temporalPolicy.test.d.ts +2 -0
- package/lib/test/unit/types/temporalPolicy.test.d.ts.map +1 -0
- package/lib/test/unit/types/temporalPolicy.test.js +64 -0
- package/lib/test/unit/types/temporalPolicy.test.js.map +1 -0
- package/package.json +19 -19
- package/rush-logs/ts-agent-memory.build.cache.log +1 -1
- package/rush-logs/ts-agent-memory.build.log +4 -4
- package/src/index.ts +2 -0
- package/src/packlets/ingest/cycleGuard.ts +142 -0
- package/src/packlets/ingest/hostStages.ts +111 -0
- package/src/packlets/ingest/index.ts +9 -0
- package/src/packlets/ingest/model.ts +184 -0
- package/src/packlets/ingest/orchestrator.ts +797 -0
- package/src/packlets/retrieve/index.ts +1 -0
- package/src/packlets/retrieve/temporalRetrievers.ts +210 -0
- package/src/packlets/store/fileTreeMemoryStore.ts +460 -66
- package/src/packlets/tools/index.ts +6 -0
- package/src/packlets/tools/memoryTools.ts +579 -0
- package/src/packlets/types/identityCodec.ts +184 -0
- package/src/packlets/types/index.ts +1 -0
- package/src/packlets/types/temporal.ts +96 -0
- package/src/packlets/types/writePolicy.ts +127 -0
- package/src/test/unit/converters/antagonistRoundTrip.test.ts +110 -0
- package/src/test/unit/ingest/antagonistCycleAndParity.test.ts +362 -0
- package/src/test/unit/ingest/cycleGuard.test.ts +68 -0
- package/src/test/unit/ingest/orchestrator.test.ts +1158 -0
- package/src/test/unit/retrieve/temporalRetrievers.test.ts +226 -0
- package/src/test/unit/store/antagonistTemporalBoundary.test.ts +158 -0
- package/src/test/unit/store/fileTreeMemoryStore.test.ts +98 -7
- package/src/test/unit/store/temporalStore.test.ts +469 -0
- package/src/test/unit/tools/memoryTools.test.ts +771 -0
- package/src/test/unit/types/temporalCodec.test.ts +259 -0
- package/src/test/unit/types/temporalPolicy.test.ts +96 -0
- package/temp/build/lint/_eslint-5eVG3S6w.json +122 -46
- package/temp/build/typescript/ts_8nwakTlr.json +1 -1
- package/temp/ts-agent-memory.api.json +11984 -6314
- package/temp/ts-agent-memory.api.md +284 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Hash, Result, fail, mapResults, succeed } from '@fgv/ts-utils';
|
|
7
|
+
import { LinkType, MemoryId } from '../types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A directed edge in the link graph the cycle guard reasons over: `source` links
|
|
11
|
+
* to `target` under relation `type`.
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
export interface ICycleGuardEdge {
|
|
15
|
+
readonly source: MemoryId;
|
|
16
|
+
readonly target: MemoryId;
|
|
17
|
+
readonly type: LinkType;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the design's `buildCycleKey` — a deterministic, canonical (RFC-8785,
|
|
22
|
+
* via `Crc32Normalizer` from `@fgv/ts-utils`) identity for a directed edge. Used
|
|
23
|
+
* to de-duplicate proposed edges so a repeated `(source, target, type)` proposal
|
|
24
|
+
* contributes a single graph edge (and never spuriously "re-closes" a cycle).
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
export function buildCycleKey(edge: ICycleGuardEdge): Result<string> {
|
|
28
|
+
return new Hash.Crc32Normalizer().computeHash({
|
|
29
|
+
source: edge.source,
|
|
30
|
+
target: edge.target,
|
|
31
|
+
type: edge.type
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Write-time cycle guard. Given the graph's EXISTING directed edges plus a set of
|
|
37
|
+
* PROPOSED edges, verify that adding the proposed edges keeps the union graph a
|
|
38
|
+
* DAG. Fails loudly, naming the first proposed edge that would close a directed
|
|
39
|
+
* cycle.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* Distinct from the read-time BFS in `LinkTraversalRetriever` (which tolerates
|
|
43
|
+
* cycles via a visited-set): this is a WRITE-time admission check. Cycle
|
|
44
|
+
* detection is DFS reachability — a proposed `source -> target` edge closes a
|
|
45
|
+
* cycle iff `target` already reaches `source` in the graph built so far. Proposed
|
|
46
|
+
* edges are folded in one at a time (in order), so an intra-batch cycle
|
|
47
|
+
* (`A->B`, `B->A` both proposed) is caught as well as a batch-vs-existing cycle.
|
|
48
|
+
*
|
|
49
|
+
* See the design note §4: this enforces GLOBAL directed-acyclicity over all link
|
|
50
|
+
* types (conservative — a mutual associative pair is rejected). Callers that need
|
|
51
|
+
* mutual links disable the guard.
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
export function assertNoCycles(
|
|
55
|
+
existing: ReadonlyArray<ICycleGuardEdge>,
|
|
56
|
+
proposed: ReadonlyArray<ICycleGuardEdge>
|
|
57
|
+
): Result<true> {
|
|
58
|
+
// Canonically key every edge up front (the only fallible step); the graph
|
|
59
|
+
// building + cycle detection below is then pure.
|
|
60
|
+
return keyEdges(existing).onSuccess((existingKeyed) =>
|
|
61
|
+
keyEdges(proposed).onSuccess((proposedKeyed) => {
|
|
62
|
+
// Adjacency map, de-duplicated by canonical edge key so a repeated edge is
|
|
63
|
+
// one arc. Seed with the existing edges, then fold in each proposed edge,
|
|
64
|
+
// checking reachability BEFORE inserting it.
|
|
65
|
+
const adjacency: Map<string, Set<string>> = new Map<string, Set<string>>();
|
|
66
|
+
const seenKeys: Set<string> = new Set<string>();
|
|
67
|
+
for (const keyed of existingKeyed) {
|
|
68
|
+
addEdge(adjacency, seenKeys, keyed);
|
|
69
|
+
}
|
|
70
|
+
for (const keyed of proposedKeyed) {
|
|
71
|
+
const edge: ICycleGuardEdge = keyed.edge;
|
|
72
|
+
// A self-loop is the degenerate one-node cycle.
|
|
73
|
+
if (edge.source === edge.target) {
|
|
74
|
+
return fail(
|
|
75
|
+
`ingest cycle guard: edge '${edge.source}' -${edge.type}-> '${edge.target}' is a self-loop`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
// Reachability: does `target` already reach `source`? If so, adding
|
|
79
|
+
// `source -> target` closes a directed cycle.
|
|
80
|
+
if (reaches(adjacency, edge.target, edge.source)) {
|
|
81
|
+
return fail(
|
|
82
|
+
`ingest cycle guard: edge '${edge.source}' -${edge.type}-> '${edge.target}' would create a cycle`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
addEdge(adjacency, seenKeys, keyed);
|
|
86
|
+
}
|
|
87
|
+
return succeed(true);
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** An edge paired with its canonical cycle key. */
|
|
93
|
+
interface IKeyedEdge {
|
|
94
|
+
readonly edge: ICycleGuardEdge;
|
|
95
|
+
readonly key: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Canonically key a set of edges (the guard's only fallible step). */
|
|
99
|
+
function keyEdges(edges: ReadonlyArray<ICycleGuardEdge>): Result<ReadonlyArray<IKeyedEdge>> {
|
|
100
|
+
return mapResults(edges.map((edge) => buildCycleKey(edge).onSuccess((key) => succeed({ edge, key }))));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Add an edge to the adjacency map, de-duplicated by its canonical key. */
|
|
104
|
+
function addEdge(adjacency: Map<string, Set<string>>, seenKeys: Set<string>, keyed: IKeyedEdge): void {
|
|
105
|
+
if (seenKeys.has(keyed.key)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
seenKeys.add(keyed.key);
|
|
109
|
+
const source: string = keyed.edge.source;
|
|
110
|
+
const target: string = keyed.edge.target;
|
|
111
|
+
const targets: Set<string> | undefined = adjacency.get(source);
|
|
112
|
+
if (targets === undefined) {
|
|
113
|
+
adjacency.set(source, new Set<string>([target]));
|
|
114
|
+
} else {
|
|
115
|
+
targets.add(target);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** DFS reachability: is `to` reachable from `from` over `adjacency`? */
|
|
120
|
+
function reaches(adjacency: ReadonlyMap<string, Set<string>>, from: string, to: string): boolean {
|
|
121
|
+
const visited: Set<string> = new Set<string>();
|
|
122
|
+
const stack: string[] = [from];
|
|
123
|
+
while (stack.length > 0) {
|
|
124
|
+
const node: string = stack.pop() as string;
|
|
125
|
+
if (node === to) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
if (visited.has(node)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
visited.add(node);
|
|
132
|
+
const neighbors: Set<string> | undefined = adjacency.get(node);
|
|
133
|
+
if (neighbors !== undefined) {
|
|
134
|
+
for (const neighbor of neighbors) {
|
|
135
|
+
if (!visited.has(neighbor)) {
|
|
136
|
+
stack.push(neighbor);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Result } from '@fgv/ts-utils';
|
|
7
|
+
import { MemoryId } from '../types';
|
|
8
|
+
import {
|
|
9
|
+
ICandidateEdge,
|
|
10
|
+
ICandidateRecord,
|
|
11
|
+
IEntityResolutionCandidate,
|
|
12
|
+
IIngestItem,
|
|
13
|
+
IMemoryClassification,
|
|
14
|
+
ResolutionVerdict
|
|
15
|
+
} from './model';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Stage 2 — the host's classifier. Decides what {@link IIngestItem} maps to
|
|
19
|
+
* which memory {@link Kind} (and optional tags / confidence). LOCKED as a
|
|
20
|
+
* separate staged interface (OQ-10): the host plugs its existing classifier
|
|
21
|
+
* machinery in here rather than surrendering to an opaque ingestor.
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
export interface IMemoryClassifier {
|
|
25
|
+
/**
|
|
26
|
+
* Classify one item. A `fail` aborts the item's ingest loudly (fgv never
|
|
27
|
+
* guesses a kind).
|
|
28
|
+
*/
|
|
29
|
+
classify(item: IIngestItem): Promise<Result<IMemoryClassification>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Stage 3 — the host's fact extractor. Turns a classified item into zero or more
|
|
34
|
+
* {@link ICandidateRecord}s. Each candidate's body is validated against the
|
|
35
|
+
* kind's registered Converter by fgv before it can reach the store (the typed
|
|
36
|
+
* validation boundary — no unchecked host data is persisted).
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
export interface IFactExtractor {
|
|
40
|
+
/**
|
|
41
|
+
* Extract candidate records from a classified item. An empty array is a valid
|
|
42
|
+
* result (the item yielded nothing memorable).
|
|
43
|
+
*/
|
|
44
|
+
extract(
|
|
45
|
+
item: IIngestItem,
|
|
46
|
+
classification: IMemoryClassification
|
|
47
|
+
): Promise<Result<ReadonlyArray<ICandidateRecord>>>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Stage 4 (optional) — the host's entity resolver (OQ-13, LOCKED OPTIONAL). When
|
|
52
|
+
* supplied, fgv surfaces near-duplicate {@link IEntityResolutionCandidate}s (from
|
|
53
|
+
* layer-2 similarity search) and the resolver returns a
|
|
54
|
+
* {@link ResolutionVerdict}. When ABSENT, stage-4 dedup falls back to
|
|
55
|
+
* exact-`{ kind, body }`-hash only — the deterministic-identity host path.
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
export interface IEntityResolver {
|
|
59
|
+
/**
|
|
60
|
+
* Decide whether `candidate` is new, a duplicate of / supersedes / merges into
|
|
61
|
+
* one of the surfaced `similar` records. `similar` is non-empty and ordered by
|
|
62
|
+
* descending score when the resolver is invoked (fgv only calls it when
|
|
63
|
+
* layer-2 surfaces at least one over-threshold neighbor).
|
|
64
|
+
*/
|
|
65
|
+
resolve(
|
|
66
|
+
candidate: ICandidateRecord,
|
|
67
|
+
similar: ReadonlyArray<IEntityResolutionCandidate>
|
|
68
|
+
): Promise<Result<ResolutionVerdict>>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The context fgv hands the host's relation extractor (stage 5): the source item
|
|
73
|
+
* plus the candidates fgv is about to write, each paired with its resolved
|
|
74
|
+
* reference id (the codec `idStem`). The extractor proposes attributed edges over
|
|
75
|
+
* these ids and existing store records.
|
|
76
|
+
* @public
|
|
77
|
+
*/
|
|
78
|
+
export interface IRelationContext {
|
|
79
|
+
/** The item being ingested. */
|
|
80
|
+
readonly item: IIngestItem;
|
|
81
|
+
/** The candidates fgv is about to write, each with its resolved reference id. */
|
|
82
|
+
readonly candidates: ReadonlyArray<IRelationCandidate>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A candidate paired with its resolved reference id, handed to the relation
|
|
87
|
+
* extractor so it can source edges from it.
|
|
88
|
+
* @public
|
|
89
|
+
*/
|
|
90
|
+
export interface IRelationCandidate {
|
|
91
|
+
/** The candidate about to be written. */
|
|
92
|
+
readonly candidate: ICandidateRecord;
|
|
93
|
+
/** Its resolved reference id (codec `idStem` — the stable entity reference). */
|
|
94
|
+
readonly id: MemoryId;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Stage 5 — the host's relation extractor. Proposes attributed edges among the
|
|
99
|
+
* candidates and existing records. fgv owns the validation, the write-time cycle
|
|
100
|
+
* guard, and the actual persistence of the edges (the host brings only the
|
|
101
|
+
* relationship judgment).
|
|
102
|
+
* @public
|
|
103
|
+
*/
|
|
104
|
+
export interface IRelationExtractor {
|
|
105
|
+
/**
|
|
106
|
+
* Propose the edges to attach for this ingest. An empty array is valid (no
|
|
107
|
+
* relations). Every proposed {@link ICandidateEdge.source | source} must be one
|
|
108
|
+
* of the context's candidate reference ids.
|
|
109
|
+
*/
|
|
110
|
+
relate(context: IRelationContext): Promise<Result<ReadonlyArray<ICandidateEdge>>>;
|
|
111
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IEdge, IMemoryEnvelope, IMemoryRecord, Kind, MemoryId, Tag } from '../types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A single unit of raw source material handed to the ingest pipeline. The host
|
|
10
|
+
* owns the shape of {@link IIngestItem.content | content} — fgv never interprets
|
|
11
|
+
* it; it flows opaquely into the host's classifier and extractor (stages 2-3).
|
|
12
|
+
*
|
|
13
|
+
* Single-item ingest is FIRST-CLASS: the orchestrator's primary entry point
|
|
14
|
+
* takes one `IIngestItem` (per-turn streaming), and the batch entry point is a
|
|
15
|
+
* convenience loop over it.
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
export interface IIngestItem {
|
|
19
|
+
/**
|
|
20
|
+
* Host-owned identity for this source item. Opaque to fgv; used only in
|
|
21
|
+
* diagnostics and echoed back on the {@link IIngestItemResult}.
|
|
22
|
+
*/
|
|
23
|
+
readonly id: string;
|
|
24
|
+
/**
|
|
25
|
+
* The opaque source payload the host's classifier / extractor understand
|
|
26
|
+
* (e.g. a raw turn, a document, a tool-call transcript). Never interpreted by
|
|
27
|
+
* fgv.
|
|
28
|
+
*/
|
|
29
|
+
readonly content: unknown;
|
|
30
|
+
/**
|
|
31
|
+
* Optional back-link to the memory record this item was derived from (e.g. the
|
|
32
|
+
* MTM turn an extracted fact came from). When present, fgv stamps it as
|
|
33
|
+
* {@link IProvenance.derivedFrom | provenance.derivedFrom} on every record
|
|
34
|
+
* ingested from this item (stage 6) — the cross-kind provenance spine.
|
|
35
|
+
*/
|
|
36
|
+
readonly sourceId?: MemoryId;
|
|
37
|
+
/** Optional opaque metadata carried alongside the item; never interpreted by fgv. */
|
|
38
|
+
readonly metadata?: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The host classifier's verdict for an {@link IIngestItem} (stage 2). Guides the
|
|
43
|
+
* host's own extractor (stage 3) and supplies the default `kind` / `tags` /
|
|
44
|
+
* `confidence` fgv stamps when the extractor does not override them.
|
|
45
|
+
*
|
|
46
|
+
* Extensible: the `[key: string]: unknown` arm lets the host attach an opaque
|
|
47
|
+
* classification payload without changing this interface.
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
export interface IMemoryClassification {
|
|
51
|
+
/** The record kind the item classifies as. */
|
|
52
|
+
readonly kind: Kind;
|
|
53
|
+
/** Optional tags the classifier assigns. */
|
|
54
|
+
readonly tags?: ReadonlyArray<Tag>;
|
|
55
|
+
/** Optional classifier confidence in `[0, 1]`; flows to `provenance.confidence`. */
|
|
56
|
+
readonly confidence?: number;
|
|
57
|
+
/** Opaque, host-owned extension payload — never interpreted by fgv. */
|
|
58
|
+
readonly [key: string]: unknown;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The store-owned envelope fields fgv derives or stamps: `id` (from the codec),
|
|
63
|
+
* and the transaction-time metadata (`seq` / `contentHash` / `created` /
|
|
64
|
+
* `updated`). A {@link ICandidateRecord} supplies everything EXCEPT these.
|
|
65
|
+
* @public
|
|
66
|
+
*/
|
|
67
|
+
export type StoreStampedEnvelopeField = 'id' | 'seq' | 'contentHash' | 'created' | 'updated';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A host-extracted candidate record (stage 3 output). The host supplies the
|
|
71
|
+
* full envelope MINUS the {@link StoreStampedEnvelopeField | store-stamped
|
|
72
|
+
* fields} (fgv derives `id` from the codec; the store stamps the rest) plus the
|
|
73
|
+
* typed body. Every candidate body is validated against the kind's registered
|
|
74
|
+
* Converter before it can reach the store (the typed validation boundary).
|
|
75
|
+
* @public
|
|
76
|
+
*/
|
|
77
|
+
export interface ICandidateRecord {
|
|
78
|
+
/** The host-supplied envelope, minus the store-stamped fields. */
|
|
79
|
+
readonly envelope: Omit<IMemoryEnvelope, StoreStampedEnvelopeField>;
|
|
80
|
+
/** The per-kind body (a markdown string in v1), validated on ingest. */
|
|
81
|
+
readonly body: unknown;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A near-duplicate candidate surfaced to the {@link IEntityResolver} by stage-4
|
|
86
|
+
* layer-2 similarity search: an existing record whose embedding is within the
|
|
87
|
+
* similarity threshold of the incoming candidate.
|
|
88
|
+
* @public
|
|
89
|
+
*/
|
|
90
|
+
export interface IEntityResolutionCandidate {
|
|
91
|
+
/** The existing record's id. */
|
|
92
|
+
readonly id: MemoryId;
|
|
93
|
+
/** The existing record. */
|
|
94
|
+
readonly record: IMemoryRecord<unknown>;
|
|
95
|
+
/** The backend similarity score (higher = more similar). */
|
|
96
|
+
readonly score: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The four dedup verdicts a {@link IEntityResolver} (or fgv's exact-match layer)
|
|
101
|
+
* returns for a candidate. See the design note §3 for the verdict → write
|
|
102
|
+
* disposition mapping.
|
|
103
|
+
* @public
|
|
104
|
+
*/
|
|
105
|
+
export type ResolutionVerdict =
|
|
106
|
+
| { readonly verdict: 'new' }
|
|
107
|
+
| { readonly verdict: 'duplicate-of'; readonly target: MemoryId }
|
|
108
|
+
| { readonly verdict: 'supersede'; readonly target: MemoryId }
|
|
109
|
+
| { readonly verdict: 'merge-into'; readonly target: MemoryId };
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* How a candidate was ultimately written (or not) after resolution.
|
|
113
|
+
*
|
|
114
|
+
* - `written` — persisted as a fresh record (verdict `new`), or as a superseding
|
|
115
|
+
* record (verdict `supersede`).
|
|
116
|
+
* - `deduped` — not written; an existing record satisfied it (verdict
|
|
117
|
+
* `duplicate-of`, incl. every layer-1 exact `{ kind, body }` match).
|
|
118
|
+
* - `merged` — merged into an existing target entity (verdict `merge-into`).
|
|
119
|
+
* @public
|
|
120
|
+
*/
|
|
121
|
+
export type IngestDisposition = 'written' | 'deduped' | 'merged';
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A stage-5 attributed edge proposal: the {@link ICandidateEdge.edge | edge} to
|
|
125
|
+
* add, sourced from {@link ICandidateEdge.source | source}. The source MUST be a
|
|
126
|
+
* candidate being written in this ingest (edges land on the source record's
|
|
127
|
+
* `envelope.links`); the edge's `target` must resolve to a sibling candidate or
|
|
128
|
+
* an existing store record.
|
|
129
|
+
* @public
|
|
130
|
+
*/
|
|
131
|
+
export interface ICandidateEdge {
|
|
132
|
+
/** The reference id (codec `idStem`) of the candidate the edge originates from. */
|
|
133
|
+
readonly source: MemoryId;
|
|
134
|
+
/** The attributed edge (type / target / optional confidence / provenance). */
|
|
135
|
+
readonly edge: IEdge;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Per-candidate outcome of an ingest run.
|
|
140
|
+
* @public
|
|
141
|
+
*/
|
|
142
|
+
export interface IIngestedRecordResult {
|
|
143
|
+
/** The candidate the outcome is for. */
|
|
144
|
+
readonly candidate: ICandidateRecord;
|
|
145
|
+
/** The resolution verdict fgv reached (or the resolver returned). */
|
|
146
|
+
readonly resolution: ResolutionVerdict;
|
|
147
|
+
/** What the write ultimately did. */
|
|
148
|
+
readonly disposition: IngestDisposition;
|
|
149
|
+
/**
|
|
150
|
+
* The stored record's id: the newly-written id (`written` / `merged`), or the
|
|
151
|
+
* existing target's id (`deduped`).
|
|
152
|
+
*/
|
|
153
|
+
readonly id: MemoryId;
|
|
154
|
+
/** The persisted record, when a write happened (`written` / `merged`). */
|
|
155
|
+
readonly record?: IMemoryRecord<unknown>;
|
|
156
|
+
/** The stage-5 edges attached to this candidate before the write. */
|
|
157
|
+
readonly edges: ReadonlyArray<ICandidateEdge>;
|
|
158
|
+
/**
|
|
159
|
+
* Informational diagnostic, set to `'temporal-versioned'` when the
|
|
160
|
+
* contradicts→temporal interlock is recognized: a `contradicts` edge was
|
|
161
|
+
* attached to a candidate of a temporal kind.
|
|
162
|
+
*
|
|
163
|
+
* @remarks
|
|
164
|
+
* A temporal kind ALWAYS writes through the store's versioned put path (that is
|
|
165
|
+
* the codec's `isVersioned` behavior — the prior version is invalidated and a
|
|
166
|
+
* new version written on every write, contradicts edge or not). This flag does
|
|
167
|
+
* NOT cause that routing; it is a diagnostic marker that the contradicts-driven
|
|
168
|
+
* scenario occurred, so callers can distinguish a contradiction-superseding
|
|
169
|
+
* version from an ordinary revision.
|
|
170
|
+
*/
|
|
171
|
+
readonly interlock?: 'temporal-versioned';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The result of ingesting a single {@link IIngestItem}: the item plus the
|
|
176
|
+
* per-candidate outcomes (one item can yield zero or many candidate records).
|
|
177
|
+
* @public
|
|
178
|
+
*/
|
|
179
|
+
export interface IIngestItemResult {
|
|
180
|
+
/** The item that was ingested. */
|
|
181
|
+
readonly item: IIngestItem;
|
|
182
|
+
/** The per-candidate outcomes, in extraction order. */
|
|
183
|
+
readonly records: ReadonlyArray<IIngestedRecordResult>;
|
|
184
|
+
}
|