@fgv/ts-agent-memory 5.1.0-50 → 5.1.0-52
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/dist/packlets/retrieve/fragmentSemanticRetriever.js +94 -5
- package/dist/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
- package/dist/packlets/store/fileTreeMemoryStore.js +36 -123
- package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/dist/packlets/store/memoryStore.js.map +1 -1
- package/dist/packlets/store/storeFileAccess.js +99 -0
- package/dist/packlets/store/storeFileAccess.js.map +1 -0
- package/dist/packlets/store/storeIdentity.js +88 -0
- package/dist/packlets/store/storeIdentity.js.map +1 -0
- package/dist/packlets/types/identityResolver.js +6 -0
- package/dist/packlets/types/identityResolver.js.map +1 -0
- package/dist/packlets/types/index.js +1 -0
- package/dist/packlets/types/index.js.map +1 -1
- package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +38 -17
- package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
- package/dist/packlets/vector/vectorIndex.js.map +1 -1
- package/dist/ts-agent-memory.d.ts +188 -41
- package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts +79 -1
- package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/fragmentSemanticRetriever.js +94 -5
- package/lib/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.d.ts +4 -19
- package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.js +36 -123
- package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/lib/packlets/store/memoryStore.d.ts +2 -2
- package/lib/packlets/store/memoryStore.d.ts.map +1 -1
- package/lib/packlets/store/memoryStore.js.map +1 -1
- package/lib/packlets/store/storeFileAccess.d.ts +40 -0
- package/lib/packlets/store/storeFileAccess.d.ts.map +1 -0
- package/lib/packlets/store/storeFileAccess.js +105 -0
- package/lib/packlets/store/storeFileAccess.js.map +1 -0
- package/lib/packlets/store/storeIdentity.d.ts +63 -0
- package/lib/packlets/store/storeIdentity.d.ts.map +1 -0
- package/lib/packlets/store/storeIdentity.js +94 -0
- package/lib/packlets/store/storeIdentity.js.map +1 -0
- package/lib/packlets/types/identityResolver.d.ts +42 -0
- package/lib/packlets/types/identityResolver.d.ts.map +1 -0
- package/lib/packlets/types/identityResolver.js +7 -0
- package/lib/packlets/types/identityResolver.js.map +1 -0
- 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/vector/inMemoryFragmentCosineIndex.d.ts +9 -17
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +38 -17
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
- package/lib/packlets/vector/vectorIndex.d.ts +57 -6
- package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
- package/lib/packlets/vector/vectorIndex.js.map +1 -1
- package/package.json +7 -7
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.FragmentSemanticRetriever = exports.FRAGMENT_SEMANTIC_UNWIRED_MESSAGE = void 0;
|
|
7
|
+
exports.FragmentSemanticRetriever = exports.FRAGMENT_NARROWING_INCOMPLETE_MESSAGE = exports.FRAGMENT_NARROWING_UNRESOLVABLE_MESSAGE = exports.FRAGMENT_SEMANTIC_UNWIRED_MESSAGE = void 0;
|
|
8
8
|
const ts_utils_1 = require("@fgv/ts-utils");
|
|
9
|
+
const types_1 = require("../types");
|
|
9
10
|
/**
|
|
10
11
|
* The loud-degradation message a {@link FragmentSemanticRetriever} returns when a
|
|
11
12
|
* fragment query is issued but no {@link IFragmentSemanticBackend | backend} is
|
|
@@ -13,6 +14,26 @@ const ts_utils_1 = require("@fgv/ts-utils");
|
|
|
13
14
|
* @public
|
|
14
15
|
*/
|
|
15
16
|
exports.FRAGMENT_SEMANTIC_UNWIRED_MESSAGE = 'fragment recall: no fragment index is wired; wire an IFragmentSemanticBackend to enable sub-document search';
|
|
17
|
+
/**
|
|
18
|
+
* The loud-degradation message returned when a query carries a record narrowing but
|
|
19
|
+
* no {@link IIdentityResolver} is wired to resolve it.
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* Deliberately a `Failure` rather than a silently-global search: answering a scoped
|
|
23
|
+
* question with an unscoped result is the failure this narrowing exists to remove.
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
exports.FRAGMENT_NARROWING_UNRESOLVABLE_MESSAGE = 'fragment recall: a record narrowing was supplied but no identity resolver is wired; pass one to FragmentSemanticRetriever.create';
|
|
27
|
+
/**
|
|
28
|
+
* The message returned when exactly one of `entityId` / `kind` is supplied.
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* They travel together because `kind` is what selects the identity codec, and the
|
|
32
|
+
* codec is what makes the resolution unambiguous. One without the other is not a
|
|
33
|
+
* partial narrowing that could be honored best-effort — it is not a narrowing at all.
|
|
34
|
+
* @public
|
|
35
|
+
*/
|
|
36
|
+
exports.FRAGMENT_NARROWING_INCOMPLETE_MESSAGE = 'fragment recall: `entityId` and `kind` must be supplied together — `kind` selects the identity codec that resolves the narrowing';
|
|
16
37
|
/**
|
|
17
38
|
* The sub-document semantic-search retriever — the "discovery" half of a
|
|
18
39
|
* search-then-read contract. It embeds a fragment query, queries the
|
|
@@ -38,16 +59,26 @@ exports.FRAGMENT_SEMANTIC_UNWIRED_MESSAGE = 'fragment recall: no fragment index
|
|
|
38
59
|
* @public
|
|
39
60
|
*/
|
|
40
61
|
class FragmentSemanticRetriever {
|
|
41
|
-
constructor(backend) {
|
|
62
|
+
constructor(backend, identityResolver) {
|
|
42
63
|
this._backend = backend;
|
|
64
|
+
this._identityResolver = identityResolver;
|
|
43
65
|
}
|
|
44
66
|
/** What this retriever can do given its wiring. */
|
|
45
67
|
get capabilities() {
|
|
46
68
|
return { supportsFragmentRecall: this._backend !== undefined };
|
|
47
69
|
}
|
|
48
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Family-convention factory.
|
|
72
|
+
*
|
|
73
|
+
* @param params - `backend` wires fragment recall itself. `identityResolver`
|
|
74
|
+
* resolves a query's `(kind, entityId)` narrowing to a storage address;
|
|
75
|
+
* `IMemoryStore` implements it, so the usual wiring is
|
|
76
|
+
* `{ backend, identityResolver: store }`. It is optional because an unscoped
|
|
77
|
+
* fragment search needs nothing to resolve — but a query that *does* carry a
|
|
78
|
+
* narrowing fails loudly without it rather than quietly searching everything.
|
|
79
|
+
*/
|
|
49
80
|
static create(params) {
|
|
50
|
-
return (0, ts_utils_1.succeed)(new FragmentSemanticRetriever(params.backend));
|
|
81
|
+
return (0, ts_utils_1.succeed)(new FragmentSemanticRetriever(params.backend, params.identityResolver));
|
|
51
82
|
}
|
|
52
83
|
/**
|
|
53
84
|
* Embed `query.semantic`, query the fragment index, and return the per-fragment
|
|
@@ -58,6 +89,13 @@ class FragmentSemanticRetriever {
|
|
|
58
89
|
return (0, ts_utils_1.fail)(exports.FRAGMENT_SEMANTIC_UNWIRED_MESSAGE);
|
|
59
90
|
}
|
|
60
91
|
const backend = this._backend;
|
|
92
|
+
// Resolve the narrowing FIRST. It is synchronous, local, and cheap, while
|
|
93
|
+
// `embedQuery` is typically a paid network round trip — so a typo'd `kind`, a
|
|
94
|
+
// missing resolver, or a half-supplied narrowing should cost nothing.
|
|
95
|
+
const options = this._resolveOptions(query);
|
|
96
|
+
if (options.isFailure()) {
|
|
97
|
+
return (0, ts_utils_1.fail)(options.message);
|
|
98
|
+
}
|
|
61
99
|
// Consumer-supplied hooks may throw; normalize both a returned `fail` and a
|
|
62
100
|
// rejection into a single `fragment recall: <label> failed` Failure so
|
|
63
101
|
// `retrieve` always honors its `Promise<Result<...>>` contract.
|
|
@@ -65,7 +103,58 @@ class FragmentSemanticRetriever {
|
|
|
65
103
|
if (embedded.isFailure()) {
|
|
66
104
|
return (0, ts_utils_1.fail)(embedded.message);
|
|
67
105
|
}
|
|
68
|
-
return FragmentSemanticRetriever._callBackend('fragment query', () => { var _a; return backend.fragmentIndex.query(embedded.value, (_a = query.topK) !== null && _a !== void 0 ? _a : 10,
|
|
106
|
+
return FragmentSemanticRetriever._callBackend('fragment query', () => { var _a; return backend.fragmentIndex.query(embedded.value, (_a = query.topK) !== null && _a !== void 0 ? _a : 10, options.value); });
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Turn the query's consumer-facing narrowing into the storage-address narrowing
|
|
110
|
+
* the index understands.
|
|
111
|
+
*
|
|
112
|
+
* @remarks
|
|
113
|
+
* `kind` selects the identity codec and the codec computes the record's storage
|
|
114
|
+
* address, so this is a deterministic resolution rather than a search —
|
|
115
|
+
* which is what makes a colliding `entityId` across kinds a non-issue.
|
|
116
|
+
*
|
|
117
|
+
* A **versioned** kind resolves to the entity's own subtree scope and deliberately
|
|
118
|
+
* carries no `id`, so the narrowing covers every version of the entity — including
|
|
119
|
+
* superseded ones, which are invalidated but never pruned from the index. A
|
|
120
|
+
* non-versioned kind resolves to exactly one record.
|
|
121
|
+
*/
|
|
122
|
+
_resolveOptions(query) {
|
|
123
|
+
const { entityId, kind, maxPerRecord } = query;
|
|
124
|
+
if (entityId === undefined && kind === undefined) {
|
|
125
|
+
return (0, ts_utils_1.succeed)({ maxPerRecord });
|
|
126
|
+
}
|
|
127
|
+
if (entityId === undefined || kind === undefined) {
|
|
128
|
+
return (0, ts_utils_1.fail)(exports.FRAGMENT_NARROWING_INCOMPLETE_MESSAGE);
|
|
129
|
+
}
|
|
130
|
+
if (this._identityResolver === undefined) {
|
|
131
|
+
return (0, ts_utils_1.fail)(exports.FRAGMENT_NARROWING_UNRESOLVABLE_MESSAGE);
|
|
132
|
+
}
|
|
133
|
+
const resolver = this._identityResolver;
|
|
134
|
+
// `identityResolver` is a consumer-injectable seam like the two backend hooks,
|
|
135
|
+
// so a throw has to become a `Failure` here rather than escaping `retrieve()`
|
|
136
|
+
// and breaking its `Promise<Result<...>>` contract. `captureResult` yields a
|
|
137
|
+
// nested `Result`, which the identity `onSuccess` flattens.
|
|
138
|
+
return (0, ts_utils_1.captureResult)(() => resolver.resolveIdentity(kind, entityId))
|
|
139
|
+
.onSuccess((resolved) => resolved)
|
|
140
|
+
.withErrorFormat((msg) => `fragment recall: cannot resolve '${kind}'/'${entityId}': ${msg}`)
|
|
141
|
+
.onSuccess((address) => {
|
|
142
|
+
// A versioned kind's every version lives under the entity subtree the
|
|
143
|
+
// codec returned, so omitting `id` is what makes the narrowing mean
|
|
144
|
+
// "this entity" rather than "one of its versions".
|
|
145
|
+
if (address.isVersioned) {
|
|
146
|
+
return (0, ts_utils_1.succeed)({ maxPerRecord, scope: address.scope });
|
|
147
|
+
}
|
|
148
|
+
// `idStem` is a plain `string` on the codec result, but a `MemoryId` IS the
|
|
149
|
+
// filename stem by contract and `Convert.memoryId` is what enforces that —
|
|
150
|
+
// so validate rather than assert. A resolver that returned a path-unsafe
|
|
151
|
+
// stem could otherwise smuggle it into the index query, where it would
|
|
152
|
+
// match nothing and look like an empty result rather than a caller bug.
|
|
153
|
+
return types_1.Convert.memoryId
|
|
154
|
+
.convert(address.idStem)
|
|
155
|
+
.withErrorFormat((msg) => `fragment recall: '${kind}'/'${entityId}' resolved to an unusable record id: ${msg}`)
|
|
156
|
+
.onSuccess((id) => (0, ts_utils_1.succeed)({ maxPerRecord, scope: address.scope, id }));
|
|
157
|
+
});
|
|
69
158
|
}
|
|
70
159
|
/**
|
|
71
160
|
* Invoke a consumer-supplied backend hook, normalizing both a returned `fail`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fragmentSemanticRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/fragmentSemanticRetriever.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,4CAAsD;AAItD;;;;;GAKG;AACU,QAAA,iCAAiC,GAC5C,6GAA6G,CAAC;AA0ChH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAa,yBAAyB;IAGpC,YAAoB,OAA6C;QAC/D,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1B,CAAC;IAED,mDAAmD;IACnD,IAAW,YAAY;QACrB,OAAO,EAAE,sBAAsB,EAAE,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;IACjE,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,MAEpB;QACC,OAAO,IAAA,kBAAO,EAAC,IAAI,yBAAyB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAChE,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,QAAQ,CAAC,KAAqB;QACzC,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAChC,OAAO,IAAA,eAAI,EAAC,yCAAiC,CAAC,CAAC;QACjD,CAAC;QACD,MAAM,OAAO,GAA6B,IAAI,CAAC,QAAQ,CAAC;QACxD,4EAA4E;QAC5E,uEAAuE;QACvE,gEAAgE;QAChE,MAAM,QAAQ,GAAyB,MAAM,yBAAyB,CAAC,YAAY,CACjF,iBAAiB,EACjB,GAAG,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,CACzC,CAAC;QACF,IAAI,QAAQ,CAAC,SAAS,EAAE,EAAE,CAAC;YACzB,OAAO,IAAA,eAAI,EAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,yBAAyB,CAAC,YAAY,CAAC,gBAAgB,EAAE,GAAG,EAAE,WACnE,OAAA,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAA,KAAK,CAAC,IAAI,mCAAI,EAAE,EAAE,KAAK,CAAC,YAAY,CAAC,CAAA,EAAA,CAClF,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACK,MAAM,CAAC,KAAK,CAAC,YAAY,CAAI,KAAa,EAAE,EAA4B;QAC9E,IAAI,CAAC;YACH,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,oBAAoB,KAAK,YAAY,GAAG,EAAE,CAAC,CAAC;QAC3F,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,IAAA,eAAI,EAAC,oBAAoB,KAAK,YAAY,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;CACF;AAvDD,8DAuDC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { IFragmentVectorIndex, IVectorQueryHit } from '../vector';\nimport { QueryEmbedder } from './semanticRetriever';\n\n/**\n * The loud-degradation message a {@link FragmentSemanticRetriever} returns when a\n * fragment query is issued but no {@link IFragmentSemanticBackend | backend} is\n * wired — the discovery surface NEVER answers a fragment query with a silent empty.\n * @public\n */\nexport const FRAGMENT_SEMANTIC_UNWIRED_MESSAGE: string =\n 'fragment recall: no fragment index is wired; wire an IFragmentSemanticBackend to enable sub-document search';\n\n/**\n * The fragment backend wired into a {@link FragmentSemanticRetriever}: the fragment\n * index to query and the embedder that turns the query text into a vector. Both are\n * required together — a fragment index is useless without a way to embed the query.\n * @public\n */\nexport interface IFragmentSemanticBackend {\n /** The fragment-granular vector index to query. */\n readonly fragmentIndex: IFragmentVectorIndex;\n /** Turns the query text into a vector. */\n readonly embedQuery: QueryEmbedder;\n}\n\n/**\n * A sub-document semantic-search request: the natural-language `semantic` text to\n * match, an optional `topK` result cap (default 10), and an optional\n * `maxPerRecord` cap that keeps one long document from monopolizing the result.\n * @public\n */\nexport interface IFragmentQuery {\n /** The natural-language text to embed and match against stored fragments. */\n readonly semantic: string;\n /** Maximum number of fragment hits to return. Defaults to 10. */\n readonly topK?: number;\n /**\n * Maximum number of fragments any single record may contribute to the result.\n * Applied during selection (before the `topK` cut). Omit for uncapped.\n */\n readonly maxPerRecord?: number;\n}\n\n/**\n * What a {@link FragmentSemanticRetriever} can do given its wiring.\n * @public\n */\nexport interface IFragmentRetrieverCapabilities {\n /** `true` when a fragment backend is wired and fragment recall is operational. */\n readonly supportsFragmentRecall: boolean;\n}\n\n/**\n * The sub-document semantic-search retriever — the \"discovery\" half of a\n * search-then-read contract. It embeds a fragment query, queries the\n * {@link IFragmentVectorIndex}, and returns the raw per-fragment\n * {@link IVectorQueryHit | hits} (each carrying a record `target` AND whichever of\n * `locator` / `fragmentId` the stored fragment was indexed with), NOT resolved\n * records: the consumer re-reads each record and resolves the fragment on its own\n * read side. Note the `locator` span is advisory — see {@link IFragmentLocator}; it\n * is not a slice guaranteed to reproduce the fragment's text.\n *\n * @remarks\n * Deliberately NOT an {@link IMemoryRetriever}: memory recall is record-granular and\n * returns records; fragment discovery is fragment-granular and returns fragment\n * identities. Keeping it a distinct surface matches the consumer contract (memory\n * stays record-granular; sub-document knowledge uses a separate fragment index) and\n * avoids overloading the record retriever's return type with identity fields that\n * only make sense here.\n *\n * When no backend is wired, `supportsFragmentRecall` is `false` and any fragment\n * query degrades loudly ({@link FRAGMENT_SEMANTIC_UNWIRED_MESSAGE}) — it NEVER\n * returns a silent empty. A consumer-supplied backend that rejects (throws) is\n * normalized into a `Failure`.\n * @public\n */\nexport class FragmentSemanticRetriever {\n private readonly _backend: IFragmentSemanticBackend | undefined;\n\n private constructor(backend: IFragmentSemanticBackend | undefined) {\n this._backend = backend;\n }\n\n /** What this retriever can do given its wiring. */\n public get capabilities(): IFragmentRetrieverCapabilities {\n return { supportsFragmentRecall: this._backend !== undefined };\n }\n\n /** Family-convention factory. */\n public static create(params: {\n readonly backend?: IFragmentSemanticBackend;\n }): Result<FragmentSemanticRetriever> {\n return succeed(new FragmentSemanticRetriever(params.backend));\n }\n\n /**\n * Embed `query.semantic`, query the fragment index, and return the per-fragment\n * hits in descending score order. Fails loudly when no backend is wired.\n */\n public async retrieve(query: IFragmentQuery): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (this._backend === undefined) {\n return fail(FRAGMENT_SEMANTIC_UNWIRED_MESSAGE);\n }\n const backend: IFragmentSemanticBackend = this._backend;\n // Consumer-supplied hooks may throw; normalize both a returned `fail` and a\n // rejection into a single `fragment recall: <label> failed` Failure so\n // `retrieve` always honors its `Promise<Result<...>>` contract.\n const embedded: Result<Float32Array> = await FragmentSemanticRetriever._callBackend(\n 'query embedding',\n () => backend.embedQuery(query.semantic)\n );\n if (embedded.isFailure()) {\n return fail(embedded.message);\n }\n return FragmentSemanticRetriever._callBackend('fragment query', () =>\n backend.fragmentIndex.query(embedded.value, query.topK ?? 10, query.maxPerRecord)\n );\n }\n\n /**\n * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`\n * and a thrown/rejected promise into a single `fragment recall: <label> failed`\n * `Failure`.\n */\n private static async _callBackend<T>(label: string, op: () => Promise<Result<T>>): Promise<Result<T>> {\n try {\n return (await op()).withErrorFormat((msg) => `fragment recall: ${label} failed: ${msg}`);\n } catch (err) {\n return fail(`fragment recall: ${label} failed: ${String(err)}`);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fragmentSemanticRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/fragmentSemanticRetriever.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, captureResult, fail, succeed } from '@fgv/ts-utils';\nimport { Convert, EntityId, IIdentityCodecResult, IIdentityResolver, Kind, MemoryId } from '../types';\nimport { IFragmentQueryOptions, IFragmentVectorIndex, IVectorQueryHit } from '../vector';\nimport { QueryEmbedder } from './semanticRetriever';\n\n/**\n * The loud-degradation message a {@link FragmentSemanticRetriever} returns when a\n * fragment query is issued but no {@link IFragmentSemanticBackend | backend} is\n * wired — the discovery surface NEVER answers a fragment query with a silent empty.\n * @public\n */\nexport const FRAGMENT_SEMANTIC_UNWIRED_MESSAGE: string =\n 'fragment recall: no fragment index is wired; wire an IFragmentSemanticBackend to enable sub-document search';\n\n/**\n * The loud-degradation message returned when a query carries a record narrowing but\n * no {@link IIdentityResolver} is wired to resolve it.\n *\n * @remarks\n * Deliberately a `Failure` rather than a silently-global search: answering a scoped\n * question with an unscoped result is the failure this narrowing exists to remove.\n * @public\n */\nexport const FRAGMENT_NARROWING_UNRESOLVABLE_MESSAGE: string =\n 'fragment recall: a record narrowing was supplied but no identity resolver is wired; pass one to FragmentSemanticRetriever.create';\n\n/**\n * The message returned when exactly one of `entityId` / `kind` is supplied.\n *\n * @remarks\n * They travel together because `kind` is what selects the identity codec, and the\n * codec is what makes the resolution unambiguous. One without the other is not a\n * partial narrowing that could be honored best-effort — it is not a narrowing at all.\n * @public\n */\nexport const FRAGMENT_NARROWING_INCOMPLETE_MESSAGE: string =\n 'fragment recall: `entityId` and `kind` must be supplied together — `kind` selects the identity codec that resolves the narrowing';\n\n/**\n * The fragment backend wired into a {@link FragmentSemanticRetriever}: the fragment\n * index to query and the embedder that turns the query text into a vector. Both are\n * required together — a fragment index is useless without a way to embed the query.\n * @public\n */\nexport interface IFragmentSemanticBackend {\n /** The fragment-granular vector index to query. */\n readonly fragmentIndex: IFragmentVectorIndex;\n /** Turns the query text into a vector. */\n readonly embedQuery: QueryEmbedder;\n}\n\n/**\n * A sub-document semantic-search request: the natural-language `semantic` text to\n * match, an optional `topK` result cap (default 10), and an optional\n * `maxPerRecord` cap that keeps one long document from monopolizing the result.\n * @public\n */\nexport interface IFragmentQuery {\n /** The natural-language text to embed and match against stored fragments. */\n readonly semantic: string;\n /** Maximum number of fragment hits to return. Defaults to 10. */\n readonly topK?: number;\n /**\n * Maximum number of fragments any single record may contribute to the result.\n * Applied during selection (before the `topK` cut). Omit for uncapped.\n */\n readonly maxPerRecord?: number;\n\n /**\n * Narrow the search to one record's fragments: the consumer-supplied domain key\n * of the record to search within. **Must be supplied with\n * {@link IFragmentQuery.kind}.**\n *\n * @remarks\n * The narrowing is applied **during selection, before the `topK` cut**, so the\n * `topK` you ask for is the `topK` you get. Filtering a global result afterwards\n * is not equivalent: it truncates to `topK` across every record first, so a scoped\n * search would come back short whenever other records outscored this one's\n * fragments.\n *\n * For a versioned kind this narrows to **every version of the entity** — literally\n * every version, superseded ones included, because invalidation stamps `invalid_at`\n * without pruning that version's fragments. Nothing on a hit distinguishes a\n * current fragment from a historical one. That matches the record-granular vector\n * lane; it is not currency filtering.\n */\n readonly entityId?: EntityId;\n\n /**\n * The kind of the record named by {@link IFragmentQuery.entityId}. **Must be\n * supplied with it.**\n *\n * @remarks\n * This is not decoration and not a filter: `kind` **selects the identity codec**,\n * and the codec computes the storage address. An `EntityId` promises no uniqueness\n * beyond a scope — the same id under two kinds is the ordinary case, not a\n * pathological one — so without `kind` the resolution is ambiguous, and with it\n * ambiguity is structurally impossible.\n */\n readonly kind?: Kind;\n}\n\n/**\n * What a {@link FragmentSemanticRetriever} can do given its wiring.\n * @public\n */\nexport interface IFragmentRetrieverCapabilities {\n /** `true` when a fragment backend is wired and fragment recall is operational. */\n readonly supportsFragmentRecall: boolean;\n}\n\n/**\n * The sub-document semantic-search retriever — the \"discovery\" half of a\n * search-then-read contract. It embeds a fragment query, queries the\n * {@link IFragmentVectorIndex}, and returns the raw per-fragment\n * {@link IVectorQueryHit | hits} (each carrying a record `target` AND whichever of\n * `locator` / `fragmentId` the stored fragment was indexed with), NOT resolved\n * records: the consumer re-reads each record and resolves the fragment on its own\n * read side. Note the `locator` span is advisory — see {@link IFragmentLocator}; it\n * is not a slice guaranteed to reproduce the fragment's text.\n *\n * @remarks\n * Deliberately NOT an {@link IMemoryRetriever}: memory recall is record-granular and\n * returns records; fragment discovery is fragment-granular and returns fragment\n * identities. Keeping it a distinct surface matches the consumer contract (memory\n * stays record-granular; sub-document knowledge uses a separate fragment index) and\n * avoids overloading the record retriever's return type with identity fields that\n * only make sense here.\n *\n * When no backend is wired, `supportsFragmentRecall` is `false` and any fragment\n * query degrades loudly ({@link FRAGMENT_SEMANTIC_UNWIRED_MESSAGE}) — it NEVER\n * returns a silent empty. A consumer-supplied backend that rejects (throws) is\n * normalized into a `Failure`.\n * @public\n */\nexport class FragmentSemanticRetriever {\n private readonly _backend: IFragmentSemanticBackend | undefined;\n private readonly _identityResolver: IIdentityResolver | undefined;\n\n private constructor(\n backend: IFragmentSemanticBackend | undefined,\n identityResolver: IIdentityResolver | undefined\n ) {\n this._backend = backend;\n this._identityResolver = identityResolver;\n }\n\n /** What this retriever can do given its wiring. */\n public get capabilities(): IFragmentRetrieverCapabilities {\n return { supportsFragmentRecall: this._backend !== undefined };\n }\n\n /**\n * Family-convention factory.\n *\n * @param params - `backend` wires fragment recall itself. `identityResolver`\n * resolves a query's `(kind, entityId)` narrowing to a storage address;\n * `IMemoryStore` implements it, so the usual wiring is\n * `{ backend, identityResolver: store }`. It is optional because an unscoped\n * fragment search needs nothing to resolve — but a query that *does* carry a\n * narrowing fails loudly without it rather than quietly searching everything.\n */\n public static create(params: {\n readonly backend?: IFragmentSemanticBackend;\n readonly identityResolver?: IIdentityResolver;\n }): Result<FragmentSemanticRetriever> {\n return succeed(new FragmentSemanticRetriever(params.backend, params.identityResolver));\n }\n\n /**\n * Embed `query.semantic`, query the fragment index, and return the per-fragment\n * hits in descending score order. Fails loudly when no backend is wired.\n */\n public async retrieve(query: IFragmentQuery): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (this._backend === undefined) {\n return fail(FRAGMENT_SEMANTIC_UNWIRED_MESSAGE);\n }\n const backend: IFragmentSemanticBackend = this._backend;\n // Resolve the narrowing FIRST. It is synchronous, local, and cheap, while\n // `embedQuery` is typically a paid network round trip — so a typo'd `kind`, a\n // missing resolver, or a half-supplied narrowing should cost nothing.\n const options: Result<IFragmentQueryOptions> = this._resolveOptions(query);\n if (options.isFailure()) {\n return fail(options.message);\n }\n // Consumer-supplied hooks may throw; normalize both a returned `fail` and a\n // rejection into a single `fragment recall: <label> failed` Failure so\n // `retrieve` always honors its `Promise<Result<...>>` contract.\n const embedded: Result<Float32Array> = await FragmentSemanticRetriever._callBackend(\n 'query embedding',\n () => backend.embedQuery(query.semantic)\n );\n if (embedded.isFailure()) {\n return fail(embedded.message);\n }\n return FragmentSemanticRetriever._callBackend('fragment query', () =>\n backend.fragmentIndex.query(embedded.value, query.topK ?? 10, options.value)\n );\n }\n\n /**\n * Turn the query's consumer-facing narrowing into the storage-address narrowing\n * the index understands.\n *\n * @remarks\n * `kind` selects the identity codec and the codec computes the record's storage\n * address, so this is a deterministic resolution rather than a search —\n * which is what makes a colliding `entityId` across kinds a non-issue.\n *\n * A **versioned** kind resolves to the entity's own subtree scope and deliberately\n * carries no `id`, so the narrowing covers every version of the entity — including\n * superseded ones, which are invalidated but never pruned from the index. A\n * non-versioned kind resolves to exactly one record.\n */\n private _resolveOptions(query: IFragmentQuery): Result<IFragmentQueryOptions> {\n const { entityId, kind, maxPerRecord } = query;\n if (entityId === undefined && kind === undefined) {\n return succeed({ maxPerRecord });\n }\n if (entityId === undefined || kind === undefined) {\n return fail(FRAGMENT_NARROWING_INCOMPLETE_MESSAGE);\n }\n if (this._identityResolver === undefined) {\n return fail(FRAGMENT_NARROWING_UNRESOLVABLE_MESSAGE);\n }\n const resolver: IIdentityResolver = this._identityResolver;\n // `identityResolver` is a consumer-injectable seam like the two backend hooks,\n // so a throw has to become a `Failure` here rather than escaping `retrieve()`\n // and breaking its `Promise<Result<...>>` contract. `captureResult` yields a\n // nested `Result`, which the identity `onSuccess` flattens.\n return captureResult(() => resolver.resolveIdentity(kind, entityId))\n .onSuccess((resolved: Result<IIdentityCodecResult>) => resolved)\n .withErrorFormat((msg) => `fragment recall: cannot resolve '${kind}'/'${entityId}': ${msg}`)\n .onSuccess((address: IIdentityCodecResult) => {\n // A versioned kind's every version lives under the entity subtree the\n // codec returned, so omitting `id` is what makes the narrowing mean\n // \"this entity\" rather than \"one of its versions\".\n if (address.isVersioned) {\n return succeed({ maxPerRecord, scope: address.scope });\n }\n // `idStem` is a plain `string` on the codec result, but a `MemoryId` IS the\n // filename stem by contract and `Convert.memoryId` is what enforces that —\n // so validate rather than assert. A resolver that returned a path-unsafe\n // stem could otherwise smuggle it into the index query, where it would\n // match nothing and look like an empty result rather than a caller bug.\n return Convert.memoryId\n .convert(address.idStem)\n .withErrorFormat(\n (msg) => `fragment recall: '${kind}'/'${entityId}' resolved to an unusable record id: ${msg}`\n )\n .onSuccess((id: MemoryId) => succeed({ maxPerRecord, scope: address.scope, id }));\n });\n }\n\n /**\n * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`\n * and a thrown/rejected promise into a single `fragment recall: <label> failed`\n * `Failure`.\n */\n private static async _callBackend<T>(label: string, op: () => Promise<Result<T>>): Promise<Result<T>> {\n try {\n return (await op()).withErrorFormat((msg) => `fragment recall: ${label} failed: ${msg}`);\n } catch (err) {\n return fail(`fragment recall: ${label} failed: ${String(err)}`);\n }\n }\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Logging, Result } from '@fgv/ts-utils';
|
|
2
2
|
import { FileTree } from '@fgv/ts-json-base';
|
|
3
|
-
import { DedupScope, EntityId, IIdentityCodec, IMemoryRecord, IWritePolicy, Kind, MemoryId, MemoryScopeKey, RankProjector } from '../types';
|
|
3
|
+
import { DedupScope, EntityId, IIdentityCodecResult, IIdentityCodec, IMemoryRecord, IWritePolicy, Kind, MemoryId, MemoryScopeKey, RankProjector } from '../types';
|
|
4
4
|
import { IBodyConverterRegistry as IRegistry } from '../converters';
|
|
5
5
|
import { IDerivedStateCoverage } from './coverage';
|
|
6
6
|
import { DerivedArtifact, ReconcileReport } from './reconcile';
|
|
@@ -325,6 +325,9 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
|
|
|
325
325
|
listScoped(): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>>;
|
|
326
326
|
/** {@inheritDoc IMemoryRecordResolver.resolveRecord} */
|
|
327
327
|
resolveRecord(scope: MemoryScopeKey, id: MemoryId): Result<IMemoryRecord<unknown> | undefined>;
|
|
328
|
+
private _codec;
|
|
329
|
+
/** {@inheritDoc IIdentityResolver.resolveIdentity} */
|
|
330
|
+
resolveIdentity(kind: Kind, entityId: EntityId): Result<IIdentityCodecResult>;
|
|
328
331
|
/**
|
|
329
332
|
* Materialize a selected set of entries into records, dropping any that have
|
|
330
333
|
* vanished since selection.
|
|
@@ -597,7 +600,6 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
|
|
|
597
600
|
* than preserving it), so a ranking bug never loses an authoritative write.
|
|
598
601
|
*/
|
|
599
602
|
private _stampRank;
|
|
600
|
-
private _codecFor;
|
|
601
603
|
private _policyFor;
|
|
602
604
|
/** {@inheritDoc IMemoryStore.dedupScopeFor} */
|
|
603
605
|
dedupScopeFor(kind: Kind): DedupScope;
|
|
@@ -619,23 +621,6 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
|
|
|
619
621
|
* verbatim downstream (e.g. merge-into re-addressing). Cross-check it here.
|
|
620
622
|
*/
|
|
621
623
|
private _verifyLoaded;
|
|
622
|
-
/**
|
|
623
|
-
* Resolve the directory for a scope, returning `undefined` when it does not
|
|
624
|
-
* exist. Navigation only — does not create. Folds the path segments through
|
|
625
|
-
* `getChildren` so an absent segment short-circuits to `undefined`.
|
|
626
|
-
*/
|
|
627
|
-
private _resolveScopeDir;
|
|
628
|
-
/** Ensure the scope directory exists, creating segments as needed. */
|
|
629
|
-
private _ensureScopeDir;
|
|
630
|
-
/** Write (create or overwrite) `<scope>/<idStem>.md` with `raw`. */
|
|
631
|
-
private _writeFile;
|
|
632
|
-
/**
|
|
633
|
-
* Physically delete `<scope>/<idStem>.md`. The scope-missing and file-missing
|
|
634
|
-
* guards are unreachable through the callers (`delete` / `_evict` both read the
|
|
635
|
-
* record first, so the directory and file exist) but are kept so a future
|
|
636
|
-
* direct caller degrades loudly rather than silently.
|
|
637
|
-
*/
|
|
638
|
-
private _deleteFile;
|
|
639
624
|
/**
|
|
640
625
|
* Walk the FileTree once and rebuild the index. Also resumes the `seq`
|
|
641
626
|
* counter past the highest persisted `seq` so new writes stay monotonic.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileTreeMemoryStore.d.ts","sourceRoot":"","sources":["../../../src/packlets/store/fileTreeMemoryStore.ts"],"names":[],"mappings":"AAKA,OAAO,EAAQ,OAAO,EAAE,MAAM,EAAyC,MAAM,eAAe,CAAC;AAC7F,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAGL,UAAU,EACV,QAAQ,
|
|
1
|
+
{"version":3,"file":"fileTreeMemoryStore.d.ts","sourceRoot":"","sources":["../../../src/packlets/store/fileTreeMemoryStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.FileTreeMemoryStore = void 0;
|
|
8
8
|
const ts_utils_1 = require("@fgv/ts-utils");
|
|
9
|
-
const ts_json_base_1 = require("@fgv/ts-json-base");
|
|
10
9
|
const types_1 = require("../types");
|
|
11
10
|
const converters_1 = require("../converters");
|
|
12
11
|
const vectorMaintenance_1 = require("./vectorMaintenance");
|
|
13
12
|
const storeCoverage_1 = require("./storeCoverage");
|
|
13
|
+
const storeIdentity_1 = require("./storeIdentity");
|
|
14
|
+
const storeFileAccess_1 = require("./storeFileAccess");
|
|
14
15
|
const storeReconcile_1 = require("./storeReconcile");
|
|
15
16
|
const index_1 = require("../index");
|
|
16
17
|
const scopeEncoding_1 = require("./scopeEncoding");
|
|
@@ -122,7 +123,7 @@ class FileTreeMemoryStore {
|
|
|
122
123
|
/** {@inheritDoc IMemoryStore.get} */
|
|
123
124
|
async get(kind, entityId) {
|
|
124
125
|
var _a;
|
|
125
|
-
const result = this.
|
|
126
|
+
const result = (0, storeIdentity_1.codecFor)(this._codecs, this._defaultCodec, kind).onSuccess((codec) => codec.encode(entityId).onSuccess((addr) => {
|
|
126
127
|
if (addr.isVersioned) {
|
|
127
128
|
if (!(0, types_1.isTemporalIdentityCodec)(codec)) {
|
|
128
129
|
return (0, ts_utils_1.fail)(`memory get '${entityId}': codec for versioned kind '${kind}' does not implement the temporal codec interface`);
|
|
@@ -131,9 +132,9 @@ class FileTreeMemoryStore {
|
|
|
131
132
|
// version whose `invalid_at` is null/absent) from the entity subtree,
|
|
132
133
|
// read off the derived index. `asOf` resolution is via `list({ asOf })`
|
|
133
134
|
// and the temporal retrievers.
|
|
134
|
-
return this._readVersionedCurrent(addr.scope);
|
|
135
|
+
return this._readVersionedCurrent(addr.scope, kind);
|
|
135
136
|
}
|
|
136
|
-
return this._readRecord(addr.scope, addr.idStem);
|
|
137
|
+
return this._readRecord(addr.scope, addr.idStem, kind);
|
|
137
138
|
}));
|
|
138
139
|
await this._fireObservation('read', kind, entityId, {
|
|
139
140
|
outcome: result.isSuccess() ? 'success' : 'failure',
|
|
@@ -228,6 +229,13 @@ class FileTreeMemoryStore {
|
|
|
228
229
|
resolveRecord(scope, id) {
|
|
229
230
|
return this._readRecord(scope, id);
|
|
230
231
|
}
|
|
232
|
+
_codec(kind) {
|
|
233
|
+
return (0, storeIdentity_1.codecFor)(this._codecs, this._defaultCodec, kind);
|
|
234
|
+
}
|
|
235
|
+
/** {@inheritDoc IIdentityResolver.resolveIdentity} */
|
|
236
|
+
resolveIdentity(kind, entityId) {
|
|
237
|
+
return (0, storeIdentity_1.resolveIdentity)(this._codecs, this._defaultCodec, kind, entityId);
|
|
238
|
+
}
|
|
231
239
|
/**
|
|
232
240
|
* Materialize a selected set of entries into records, dropping any that have
|
|
233
241
|
* vanished since selection.
|
|
@@ -417,7 +425,7 @@ class FileTreeMemoryStore {
|
|
|
417
425
|
}
|
|
418
426
|
/** Resolve a scope for an observation, best-effort (undefined when unresolvable). */
|
|
419
427
|
_scopeBestEffort(kind, entityId) {
|
|
420
|
-
return this.
|
|
428
|
+
return this._codec(kind)
|
|
421
429
|
.onSuccess((codec) => codec.encode(entityId))
|
|
422
430
|
.onSuccess((addr) => (0, ts_utils_1.succeed)(addr.scope))
|
|
423
431
|
.orDefault();
|
|
@@ -435,7 +443,7 @@ class FileTreeMemoryStore {
|
|
|
435
443
|
return this._registry
|
|
436
444
|
.convert(envelope.kind, body)
|
|
437
445
|
.withErrorFormat((msg) => `memory put '${envelope.id}': invalid body: ${msg}`)
|
|
438
|
-
.onSuccess(() => this.
|
|
446
|
+
.onSuccess(() => this._codec(envelope.kind))
|
|
439
447
|
.thenOnSuccess((codec) => codec.encode(envelope.entityId).thenOnSuccess((addr) => {
|
|
440
448
|
if (addr.isVersioned) {
|
|
441
449
|
if (!(0, types_1.isTemporalIdentityCodec)(codec)) {
|
|
@@ -485,7 +493,7 @@ class FileTreeMemoryStore {
|
|
|
485
493
|
return (0, ts_utils_1.succeed)({ record: duplicate.value, evicted: [] });
|
|
486
494
|
}
|
|
487
495
|
}
|
|
488
|
-
return this._readRecord(scope, idStem).thenOnSuccess((existing) => {
|
|
496
|
+
return this._readRecord(scope, idStem, record.envelope.kind).thenOnSuccess((existing) => {
|
|
489
497
|
// Same-id re-put is a no-op ONLY when the content hash matches AND the
|
|
490
498
|
// mutable metadata is also unchanged. The content hash covers
|
|
491
499
|
// { kind, body, links }; a matching hash with revised tags/provenance is a
|
|
@@ -611,19 +619,19 @@ class FileTreeMemoryStore {
|
|
|
611
619
|
/** Serialize and write a fully-stamped record, then patch the index. */
|
|
612
620
|
_persist(record, scope, idStem) {
|
|
613
621
|
return (0, converters_1.serializeMemoryFile)(record.envelope, record.body)
|
|
614
|
-
.onSuccess((raw) => this.
|
|
622
|
+
.onSuccess((raw) => (0, storeFileAccess_1.writeRecordFile)(this._root, this._scopeEncoding, scope, idStem, raw))
|
|
615
623
|
.onSuccess(() => this._index.patch('put', { scope, record }))
|
|
616
624
|
.onSuccess(() => (0, ts_utils_1.succeed)(record));
|
|
617
625
|
}
|
|
618
626
|
async _deleteLocked(kind, entityId) {
|
|
619
|
-
return this.
|
|
627
|
+
return this._codec(kind).thenOnSuccess((codec) => codec.encode(entityId).thenOnSuccess((addr) => {
|
|
620
628
|
if (addr.isVersioned) {
|
|
621
629
|
if (!(0, types_1.isTemporalIdentityCodec)(codec)) {
|
|
622
630
|
return Promise.resolve((0, ts_utils_1.fail)(`memory delete '${entityId}': codec for versioned kind '${kind}' does not implement the temporal codec interface`));
|
|
623
631
|
}
|
|
624
|
-
return this._deleteVersioned(entityId, addr.scope);
|
|
632
|
+
return this._deleteVersioned(entityId, addr.scope, kind);
|
|
625
633
|
}
|
|
626
|
-
return this._deleteFlat(entityId, addr.scope, addr.idStem);
|
|
634
|
+
return this._deleteFlat(entityId, addr.scope, addr.idStem, kind);
|
|
627
635
|
}));
|
|
628
636
|
}
|
|
629
637
|
/**
|
|
@@ -631,15 +639,15 @@ class FileTreeMemoryStore {
|
|
|
631
639
|
* entry, then prune the vector best-effort. Structurally unchanged from the
|
|
632
640
|
* pre-temporal delete path.
|
|
633
641
|
*/
|
|
634
|
-
async _deleteFlat(entityId, scope, idStem) {
|
|
635
|
-
return this._readRecord(scope, idStem).thenOnSuccess((existing) => {
|
|
642
|
+
async _deleteFlat(entityId, scope, idStem, kind) {
|
|
643
|
+
return this._readRecord(scope, idStem, kind).thenOnSuccess((existing) => {
|
|
636
644
|
if (existing === undefined) {
|
|
637
645
|
return Promise.resolve((0, ts_utils_1.fail)(`memory delete '${entityId}': no record found`));
|
|
638
646
|
}
|
|
639
647
|
// Delete the record file + index entry (authoritative), then prune the
|
|
640
648
|
// vector best-effort: a committed delete must not fail because the
|
|
641
649
|
// derived index could not be pruned.
|
|
642
|
-
return this.
|
|
650
|
+
return (0, storeFileAccess_1.deleteRecordFile)(this._root, this._scopeEncoding, scope, idStem)
|
|
643
651
|
.onSuccess(() => this._index.patch('delete', { scope, record: existing }))
|
|
644
652
|
.thenOnSuccess(async () => {
|
|
645
653
|
await this._vectors.removeAll({ scope, id: existing.envelope.id });
|
|
@@ -653,7 +661,7 @@ class FileTreeMemoryStore {
|
|
|
653
661
|
* null/absent. `undefined` when the entity has no current version (never
|
|
654
662
|
* written, or fully invalidated / soft-deleted).
|
|
655
663
|
*/
|
|
656
|
-
_readVersionedCurrent(scope) {
|
|
664
|
+
_readVersionedCurrent(scope, expectedKind) {
|
|
657
665
|
// Select over ENVELOPES, then materialize the one winner — which is what
|
|
658
666
|
// `IMemoryStore.get`'s docstring promises and what `selectCurrentVersion`
|
|
659
667
|
// being generic over `IEnvelopeCarrier` exists for. Materializing every
|
|
@@ -663,21 +671,21 @@ class FileTreeMemoryStore {
|
|
|
663
671
|
if (current === undefined) {
|
|
664
672
|
return (0, ts_utils_1.succeed)(undefined);
|
|
665
673
|
}
|
|
666
|
-
return this._resolveRequired(current);
|
|
674
|
+
return this._resolveRequired(current).onSuccess((materialized) => (0, storeIdentity_1.verifyOccupantKind)(expectedKind, scope, materialized.envelope.id, materialized));
|
|
667
675
|
}
|
|
668
676
|
/**
|
|
669
677
|
* Every persisted version of the entity whose subtree is `scope`. All version
|
|
670
678
|
* files for one entity live under exactly that scope (which encodes the
|
|
671
679
|
* entityId), so a scope filter over the index isolates one entity's versions.
|
|
672
680
|
*/
|
|
673
|
-
_versionsForEntity(scope) {
|
|
681
|
+
_versionsForEntity(scope, expectedKind) {
|
|
674
682
|
// Selected on the envelope (scope), materialized after — so a versioned write
|
|
675
683
|
// reads only that entity's versions, never the vault. Bounded by the entity's
|
|
676
684
|
// version count.
|
|
677
685
|
return (0, ts_utils_1.mapResults)(this._index
|
|
678
686
|
.entries()
|
|
679
687
|
.filter((entry) => entry.scope === scope)
|
|
680
|
-
.map((entry) => this._resolveRequired(entry)));
|
|
688
|
+
.map((entry) => this._resolveRequired(entry).onSuccess((r) => (0, storeIdentity_1.verifyOccupantKind)(expectedKind, scope, r.envelope.id, r))));
|
|
681
689
|
}
|
|
682
690
|
/**
|
|
683
691
|
* Versioned write (invalidate-don't-delete). Builds the new version's content
|
|
@@ -699,7 +707,7 @@ class FileTreeMemoryStore {
|
|
|
699
707
|
// still-current version at snapshot time — normally one, but two-or-more if a
|
|
700
708
|
// prior invalidation partially failed; invalidating all of them lets the write
|
|
701
709
|
// self-heal a stuck state (P2-7).
|
|
702
|
-
const snapshot = this._versionsForEntity(scope);
|
|
710
|
+
const snapshot = this._versionsForEntity(scope, kind);
|
|
703
711
|
if (snapshot.isFailure()) {
|
|
704
712
|
return (0, ts_utils_1.fail)(snapshot.message);
|
|
705
713
|
}
|
|
@@ -828,8 +836,8 @@ class FileTreeMemoryStore {
|
|
|
828
836
|
* kinds exist to preserve the audit trail (and the L3 `contradicts` interlock
|
|
829
837
|
* builds on it), so a hard delete would defeat the purpose.
|
|
830
838
|
*/
|
|
831
|
-
async _deleteVersioned(entityId, scope) {
|
|
832
|
-
const snapshot = this._versionsForEntity(scope);
|
|
839
|
+
async _deleteVersioned(entityId, scope, kind) {
|
|
840
|
+
const snapshot = this._versionsForEntity(scope, kind);
|
|
833
841
|
if (snapshot.isFailure()) {
|
|
834
842
|
return (0, ts_utils_1.fail)(snapshot.message);
|
|
835
843
|
}
|
|
@@ -852,7 +860,7 @@ class FileTreeMemoryStore {
|
|
|
852
860
|
if (existing === undefined) {
|
|
853
861
|
return (0, ts_utils_1.fail)(`memory put: cannot evict '${id}' in scope '${scope}': not found`);
|
|
854
862
|
}
|
|
855
|
-
return this.
|
|
863
|
+
return (0, storeFileAccess_1.deleteRecordFile)(this._root, this._scopeEncoding, scope, id)
|
|
856
864
|
.onSuccess(() => this._index.patch('delete', { scope, record: existing }))
|
|
857
865
|
.onSuccess(() => (0, ts_utils_1.succeed)(id));
|
|
858
866
|
});
|
|
@@ -1002,7 +1010,7 @@ class FileTreeMemoryStore {
|
|
|
1002
1010
|
* already-consistent store touches no files.
|
|
1003
1011
|
*/
|
|
1004
1012
|
_rewriteEnvelope(scope, id, mutate) {
|
|
1005
|
-
return this.
|
|
1013
|
+
return (0, storeFileAccess_1.resolveScopeDir)(this._root, this._scopeEncoding, scope).onSuccess((scopeDir) => {
|
|
1006
1014
|
/* c8 ignore next 3 - defensive: the scope dir exists for any indexed record */
|
|
1007
1015
|
if (scopeDir === undefined) {
|
|
1008
1016
|
return (0, ts_utils_1.fail)(`'${id}': scope '${scope}' not found`);
|
|
@@ -1077,14 +1085,6 @@ class FileTreeMemoryStore {
|
|
|
1077
1085
|
return { envelope: Object.assign(Object.assign({}, record.envelope), { rank: undefined }), body: record.body };
|
|
1078
1086
|
}
|
|
1079
1087
|
}
|
|
1080
|
-
_codecFor(kind) {
|
|
1081
|
-
var _a;
|
|
1082
|
-
const codec = (_a = this._codecs.get(kind)) !== null && _a !== void 0 ? _a : this._defaultCodec;
|
|
1083
|
-
if (codec === undefined) {
|
|
1084
|
-
return (0, ts_utils_1.fail)(`no identity codec registered for kind '${kind}'`);
|
|
1085
|
-
}
|
|
1086
|
-
return (0, ts_utils_1.succeed)(codec);
|
|
1087
|
-
}
|
|
1088
1088
|
_policyFor(kind) {
|
|
1089
1089
|
var _a;
|
|
1090
1090
|
return (_a = this._writePolicies.get(kind)) !== null && _a !== void 0 ? _a : this._defaultPolicy;
|
|
@@ -1105,8 +1105,8 @@ class FileTreeMemoryStore {
|
|
|
1105
1105
|
* when the scope directory or file is absent. Verifies the on-disk id ↔
|
|
1106
1106
|
* filename round-trip on every load.
|
|
1107
1107
|
*/
|
|
1108
|
-
_readRecord(scope, idStem) {
|
|
1109
|
-
return this.
|
|
1108
|
+
_readRecord(scope, idStem, expectedKind) {
|
|
1109
|
+
return (0, storeFileAccess_1.resolveScopeDir)(this._root, this._scopeEncoding, scope).onSuccess((scopeDir) => {
|
|
1110
1110
|
if (scopeDir === undefined) {
|
|
1111
1111
|
return (0, ts_utils_1.succeed)(undefined);
|
|
1112
1112
|
}
|
|
@@ -1119,7 +1119,8 @@ class FileTreeMemoryStore {
|
|
|
1119
1119
|
return file
|
|
1120
1120
|
.getRawContents()
|
|
1121
1121
|
.onSuccess((raw) => (0, converters_1.parseMemoryFile)(raw, this._registry))
|
|
1122
|
-
.onSuccess((parsedRecord) => this._verifyLoaded(scope, file, parsedRecord))
|
|
1122
|
+
.onSuccess((parsedRecord) => this._verifyLoaded(scope, file, parsedRecord))
|
|
1123
|
+
.onSuccess((loaded) => (0, storeIdentity_1.verifyOccupantKind)(expectedKind, scope, idStem, loaded));
|
|
1123
1124
|
});
|
|
1124
1125
|
});
|
|
1125
1126
|
}
|
|
@@ -1133,95 +1134,7 @@ class FileTreeMemoryStore {
|
|
|
1133
1134
|
* verbatim downstream (e.g. merge-into re-addressing). Cross-check it here.
|
|
1134
1135
|
*/
|
|
1135
1136
|
_verifyLoaded(scope, file, record) {
|
|
1136
|
-
|
|
1137
|
-
return (0, ts_utils_1.fail)(`memory file '${file.absolutePath}': envelope id '${record.envelope.id}' does not match filename stem '${file.baseName}'`);
|
|
1138
|
-
}
|
|
1139
|
-
return this._codecFor(record.envelope.kind)
|
|
1140
|
-
.onSuccess((codec) => codec.verifyRoundTrip(scope, file.baseName).onSuccess(() => codec.decode(scope, file.baseName)))
|
|
1141
|
-
.withErrorFormat((msg) => `memory file '${file.absolutePath}': ${msg}`)
|
|
1142
|
-
.onSuccess((decodedEntityId) => {
|
|
1143
|
-
if (decodedEntityId !== record.envelope.entityId) {
|
|
1144
|
-
return (0, ts_utils_1.fail)(`memory file '${file.absolutePath}': envelope entityId '${record.envelope.entityId}' does not match scope-derived entityId '${decodedEntityId}'`);
|
|
1145
|
-
}
|
|
1146
|
-
return (0, ts_utils_1.succeed)(record);
|
|
1147
|
-
});
|
|
1148
|
-
}
|
|
1149
|
-
/**
|
|
1150
|
-
* Resolve the directory for a scope, returning `undefined` when it does not
|
|
1151
|
-
* exist. Navigation only — does not create. Folds the path segments through
|
|
1152
|
-
* `getChildren` so an absent segment short-circuits to `undefined`.
|
|
1153
|
-
*/
|
|
1154
|
-
_resolveScopeDir(scope) {
|
|
1155
|
-
return this._scopeEncoding(scope).onSuccess((encoded) => {
|
|
1156
|
-
const segments = encoded.split('/').filter((s) => s.length > 0);
|
|
1157
|
-
return segments.reduce((acc, segment) => acc.onSuccess((current) => {
|
|
1158
|
-
if (current === undefined) {
|
|
1159
|
-
return (0, ts_utils_1.succeed)(undefined);
|
|
1160
|
-
}
|
|
1161
|
-
return current
|
|
1162
|
-
.getChildren()
|
|
1163
|
-
.onSuccess((children) => (0, ts_utils_1.succeed)(children.find((c) => c.type === 'directory' && c.name === segment)));
|
|
1164
|
-
}), (0, ts_utils_1.succeed)(this._root));
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
/** Ensure the scope directory exists, creating segments as needed. */
|
|
1168
|
-
_ensureScopeDir(scope) {
|
|
1169
|
-
return this._scopeEncoding(scope).onSuccess((encoded) => {
|
|
1170
|
-
const segments = encoded.split('/').filter((s) => s.length > 0);
|
|
1171
|
-
return segments.reduce((acc, segment) => acc.onSuccess((current) => current.getChildren().onSuccess((children) => {
|
|
1172
|
-
const existing = children.find((c) => c.type === 'directory' && c.name === segment);
|
|
1173
|
-
if (existing === undefined) {
|
|
1174
|
-
return current.createChildDirectory(segment);
|
|
1175
|
-
}
|
|
1176
|
-
/* c8 ignore next 3 -- defensive: a child of a mutable in-memory/fs tree is itself mutable; the guard protects against a read-only adapter handed in as root */
|
|
1177
|
-
if (!ts_json_base_1.FileTree.isMutableDirectoryItem(existing)) {
|
|
1178
|
-
return (0, ts_utils_1.fail)(`${existing.absolutePath}: directory is not mutable`);
|
|
1179
|
-
}
|
|
1180
|
-
return (0, ts_utils_1.succeed)(existing);
|
|
1181
|
-
})), (0, ts_utils_1.succeed)(this._root));
|
|
1182
|
-
});
|
|
1183
|
-
}
|
|
1184
|
-
/** Write (create or overwrite) `<scope>/<idStem>.md` with `raw`. */
|
|
1185
|
-
_writeFile(scope, idStem, raw) {
|
|
1186
|
-
return this._ensureScopeDir(scope).onSuccess((scopeDir) => scopeDir.getChildren().onSuccess((children) => {
|
|
1187
|
-
const fileName = `${idStem}${MEMORY_FILE_EXTENSION}`;
|
|
1188
|
-
const existing = children.find((c) => c.type === 'file' && c.name === fileName);
|
|
1189
|
-
if (existing === undefined) {
|
|
1190
|
-
return scopeDir.createChildFile(fileName, raw).onSuccess(() => (0, ts_utils_1.succeed)(true));
|
|
1191
|
-
}
|
|
1192
|
-
/* c8 ignore next 3 -- defensive: a file in a mutable tree is mutable; guards a read-only adapter */
|
|
1193
|
-
if (!ts_json_base_1.FileTree.isMutableFileItem(existing)) {
|
|
1194
|
-
return (0, ts_utils_1.fail)(`${existing.absolutePath}: file is not mutable`);
|
|
1195
|
-
}
|
|
1196
|
-
return existing.setRawContents(raw).onSuccess(() => (0, ts_utils_1.succeed)(true));
|
|
1197
|
-
}));
|
|
1198
|
-
}
|
|
1199
|
-
/**
|
|
1200
|
-
* Physically delete `<scope>/<idStem>.md`. The scope-missing and file-missing
|
|
1201
|
-
* guards are unreachable through the callers (`delete` / `_evict` both read the
|
|
1202
|
-
* record first, so the directory and file exist) but are kept so a future
|
|
1203
|
-
* direct caller degrades loudly rather than silently.
|
|
1204
|
-
*/
|
|
1205
|
-
_deleteFile(scope, idStem) {
|
|
1206
|
-
return this._resolveScopeDir(scope).onSuccess((scopeDir) => {
|
|
1207
|
-
/* c8 ignore next 3 -- unreachable: callers read the record (hence the scope dir) first */
|
|
1208
|
-
if (scopeDir === undefined) {
|
|
1209
|
-
return (0, ts_utils_1.fail)(`memory delete: scope '${scope}' not found`);
|
|
1210
|
-
}
|
|
1211
|
-
const fileName = `${idStem}${MEMORY_FILE_EXTENSION}`;
|
|
1212
|
-
return scopeDir.getChildren().onSuccess((children) => {
|
|
1213
|
-
const file = children.find((c) => c.type === 'file' && c.name === fileName);
|
|
1214
|
-
/* c8 ignore next 3 -- unreachable: callers read the record (hence the file) first */
|
|
1215
|
-
if (file === undefined) {
|
|
1216
|
-
return (0, ts_utils_1.fail)(`memory delete: file '${fileName}' not found in scope '${scope}'`);
|
|
1217
|
-
}
|
|
1218
|
-
/* c8 ignore next 3 -- defensive: a file in a mutable tree is mutable; guards a read-only adapter */
|
|
1219
|
-
if (!ts_json_base_1.FileTree.isMutableFileItem(file)) {
|
|
1220
|
-
return (0, ts_utils_1.fail)(`${file.absolutePath}: file is not mutable`);
|
|
1221
|
-
}
|
|
1222
|
-
return file.delete().onSuccess(() => (0, ts_utils_1.succeed)(true));
|
|
1223
|
-
});
|
|
1224
|
-
});
|
|
1137
|
+
return (0, storeIdentity_1.verifyLoadedIdentity)(this._codec(record.envelope.kind), scope, file, record);
|
|
1225
1138
|
}
|
|
1226
1139
|
/**
|
|
1227
1140
|
* Walk the FileTree once and rebuild the index. Also resumes the `seq`
|