@fgv/ts-agent-memory 5.1.0-48 → 5.1.0-49
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/linkTraversalRetriever.js +1 -1
- package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
- package/dist/packlets/retrieve/recencyRetriever.js +1 -1
- package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
- package/dist/packlets/retrieve/retriever.js +11 -5
- package/dist/packlets/retrieve/retriever.js.map +1 -1
- package/dist/packlets/retrieve/structuredFilterRetriever.js +27 -5
- package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
- package/dist/packlets/retrieve/tagRetriever.js +1 -1
- package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
- package/dist/packlets/retrieve/temporalRetrievers.js +1 -1
- package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
- package/dist/packlets/store/fileTreeMemoryStore.js +100 -1
- package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/dist/packlets/types/envelope.js.map +1 -1
- package/dist/ts-agent-memory.d.ts +134 -13
- package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +1 -1
- package/lib/packlets/retrieve/linkTraversalRetriever.js +1 -1
- package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
- package/lib/packlets/retrieve/recencyRetriever.d.ts +1 -1
- package/lib/packlets/retrieve/recencyRetriever.js +1 -1
- package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
- package/lib/packlets/retrieve/retriever.d.ts +26 -7
- package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/retriever.js +11 -5
- package/lib/packlets/retrieve/retriever.js.map +1 -1
- package/lib/packlets/retrieve/structuredFilterRetriever.d.ts +26 -4
- package/lib/packlets/retrieve/structuredFilterRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/structuredFilterRetriever.js +27 -5
- package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
- package/lib/packlets/retrieve/tagRetriever.d.ts +1 -1
- package/lib/packlets/retrieve/tagRetriever.js +1 -1
- package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
- package/lib/packlets/retrieve/temporalRetrievers.js +1 -1
- package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.d.ts +69 -0
- package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.js +99 -0
- package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/lib/packlets/types/envelope.d.ts +11 -0
- package/lib/packlets/types/envelope.d.ts.map +1 -1
- package/lib/packlets/types/envelope.js.map +1 -1
- package/package.json +7 -7
|
@@ -37,7 +37,7 @@ export const LINK_TRAVERSAL_NO_SEED_MESSAGE = 'link traversal requires a seed id
|
|
|
37
37
|
* is the exact, collision-free cycle key — no structural hashing (e.g.
|
|
38
38
|
* `Crc32Normalizer`) is needed. A self-loop or any multi-hop cycle terminates
|
|
39
39
|
* because a revisited node is never re-expanded.
|
|
40
|
-
* - **Post-filter.** The scope / kind / tag / predicate axes of the query are
|
|
40
|
+
* - **Post-filter.** The scope / kind / tag / provenance-source / predicate axes of the query are
|
|
41
41
|
* applied to the reached records (the link axes are the traversal itself).
|
|
42
42
|
* @public
|
|
43
43
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linkTraversalRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/linkTraversalRetriever.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { IEdgeTarget, IMemoryRecord, edgeTargetKey } from '../types';\nimport { IIndexedMemoryRecord, IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n guardRetrieverCapabilities,\n indexedRecordMatchesQuery,\n limitRecords,\n orderingCompare\n} from './retriever';\n\n/** The capabilities a link-traversal retriever exposes (link traversal only). */\nconst LINK_TRAVERSAL_CAPABILITIES: IMemoryRetrieverCapabilities = {\n supportsSemanticRecall: false,\n supportsTemporalQuery: false,\n supportsLinkTraversal: true\n};\n\n/** Default BFS hop count when `query.hops` is not supplied. */\nconst DEFAULT_HOPS: number = 1;\n\n/**\n * The loud-degradation message returned when a link-traversal query supplies no\n * seed (`linkedFrom` / `linkedTo`).\n * @public\n */\nexport const LINK_TRAVERSAL_NO_SEED_MESSAGE: string =\n 'link traversal requires a seed id (linkedFrom or linkedTo)';\n\n/**\n * Breadth-first link-traversal retriever. From a scope-qualified\n * {@link IEdgeTarget} seed it walks the link graph up to `query.hops` levels and\n * returns the records reached (excluding the seed), recency-ordered and limited.\n *\n * @remarks\n * - **Direction.** `linkedFrom` walks OUTBOUND edges (each record's\n * `envelope.links[].target`); `linkedTo` walks INBOUND edges (the index's\n * `backlinks`). Exactly one is the seed; `linkedFrom` wins if both are set.\n * - **Scope-qualified nodes.** Every graph node is an {@link IEdgeTarget}\n * `(scope, id)` pair, so following an edge to `turn-3` reaches ONLY the record\n * in the edge's own scope — never a same-stem record in another scope.\n * - **Bound + cycle safety.** Traversal is bounded by `hops` (default `1` — a\n * single hop) and a visited-set guard. Nodes are canonicalized to their\n * `(scope, id)` string via {@link edgeTargetKey}, so a `Set<string>` visited-set\n * is the exact, collision-free cycle key — no structural hashing (e.g.\n * `Crc32Normalizer`) is needed. A self-loop or any multi-hop cycle terminates\n * because a revisited node is never re-expanded.\n * - **Post-filter.** The scope / kind / tag / predicate axes of the query are\n * applied to the reached records (the link axes are the traversal itself).\n * @public\n */\nexport class LinkTraversalRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<LinkTraversalRetriever> {\n return succeed(new LinkTraversalRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return LINK_TRAVERSAL_CAPABILITIES;\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => this._traverse(query))\n );\n }\n\n /** Run the bounded, cycle-safe BFS and post-filter the reached records. */\n private _traverse(query: IMemoryQuery): Result<ReadonlyArray<IMemoryRecord<unknown>>> {\n const outbound: boolean = query.linkedFrom !== undefined;\n const seed: IEdgeTarget | undefined = query.linkedFrom ?? query.linkedTo;\n if (seed === undefined) {\n return fail(LINK_TRAVERSAL_NO_SEED_MESSAGE);\n }\n const hops: number = query.hops ?? DEFAULT_HOPS;\n const byKey: ReadonlyMap<string, IIndexedMemoryRecord> = this._indexByKey();\n\n // The visited-set IS the cycle guard: nodes are canonicalized to their\n // `(scope, id)` string, so set membership is an exact identity check. The\n // seed is pre-marked so it is never re-added.\n const visited: Set<string> = new Set<string>([edgeTargetKey(seed)]);\n const reached: IEdgeTarget[] = [];\n let frontier: IEdgeTarget[] = [seed];\n for (let hop = 0; hop < hops && frontier.length > 0; hop++) {\n const next: IEdgeTarget[] = [];\n for (const node of frontier) {\n for (const neighbor of outbound ? this._outbound(node, byKey) : this._inbound(node)) {\n const neighborKey: string = edgeTargetKey(neighbor);\n if (!visited.has(neighborKey)) {\n visited.add(neighborKey);\n reached.push(neighbor);\n next.push(neighbor);\n }\n }\n }\n frontier = next;\n }\n\n const entries: IIndexedMemoryRecord[] = [];\n for (const node of reached) {\n const match: IIndexedMemoryRecord | undefined = byKey.get(edgeTargetKey(node));\n if (match !== undefined) {\n entries.push(match);\n }\n }\n const ordered: IMemoryRecord<unknown>[] = entries\n .filter((entry) => indexedRecordMatchesQuery(entry, query))\n .map((entry) => entry.record)\n .sort(orderingCompare(query.orderBy));\n return succeed(limitRecords(ordered, query.limit, query.offset));\n }\n\n /**\n * Group the index's entries by their scope-qualified {@link edgeTargetKey}\n * `(scope, id)` composite. Each composite is the index's primary key, so it maps\n * to exactly one entry — two records that reuse a filename stem across scopes\n * (e.g. `turn-0` in two conversations) get distinct keys and never collide.\n */\n private _indexByKey(): ReadonlyMap<string, IIndexedMemoryRecord> {\n const byKey: Map<string, IIndexedMemoryRecord> = new Map<string, IIndexedMemoryRecord>();\n for (const entry of this._index.entries()) {\n byKey.set(edgeTargetKey({ scope: entry.scope, id: entry.record.envelope.id }), entry);\n }\n return byKey;\n }\n\n /** Outbound neighbors: the scope-qualified targets of every edge on the record at `node`. */\n private _outbound(node: IEdgeTarget, byKey: ReadonlyMap<string, IIndexedMemoryRecord>): IEdgeTarget[] {\n const targets: IEdgeTarget[] = [];\n const match: IIndexedMemoryRecord | undefined = byKey.get(edgeTargetKey(node));\n if (match !== undefined) {\n for (const edge of match.record.envelope.links) {\n targets.push(edge.target);\n }\n }\n return targets;\n }\n\n /** Inbound neighbors: the scope-qualified sources whose edges point AT `node` (the backlinks). */\n private _inbound(node: IEdgeTarget): ReadonlyArray<IEdgeTarget> {\n return this._index.backlinks(node);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"linkTraversalRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/linkTraversalRetriever.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { IEdgeTarget, IMemoryRecord, edgeTargetKey } from '../types';\nimport { IIndexedMemoryRecord, IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n guardRetrieverCapabilities,\n indexedRecordMatchesQuery,\n limitRecords,\n orderingCompare\n} from './retriever';\n\n/** The capabilities a link-traversal retriever exposes (link traversal only). */\nconst LINK_TRAVERSAL_CAPABILITIES: IMemoryRetrieverCapabilities = {\n supportsSemanticRecall: false,\n supportsTemporalQuery: false,\n supportsLinkTraversal: true\n};\n\n/** Default BFS hop count when `query.hops` is not supplied. */\nconst DEFAULT_HOPS: number = 1;\n\n/**\n * The loud-degradation message returned when a link-traversal query supplies no\n * seed (`linkedFrom` / `linkedTo`).\n * @public\n */\nexport const LINK_TRAVERSAL_NO_SEED_MESSAGE: string =\n 'link traversal requires a seed id (linkedFrom or linkedTo)';\n\n/**\n * Breadth-first link-traversal retriever. From a scope-qualified\n * {@link IEdgeTarget} seed it walks the link graph up to `query.hops` levels and\n * returns the records reached (excluding the seed), recency-ordered and limited.\n *\n * @remarks\n * - **Direction.** `linkedFrom` walks OUTBOUND edges (each record's\n * `envelope.links[].target`); `linkedTo` walks INBOUND edges (the index's\n * `backlinks`). Exactly one is the seed; `linkedFrom` wins if both are set.\n * - **Scope-qualified nodes.** Every graph node is an {@link IEdgeTarget}\n * `(scope, id)` pair, so following an edge to `turn-3` reaches ONLY the record\n * in the edge's own scope — never a same-stem record in another scope.\n * - **Bound + cycle safety.** Traversal is bounded by `hops` (default `1` — a\n * single hop) and a visited-set guard. Nodes are canonicalized to their\n * `(scope, id)` string via {@link edgeTargetKey}, so a `Set<string>` visited-set\n * is the exact, collision-free cycle key — no structural hashing (e.g.\n * `Crc32Normalizer`) is needed. A self-loop or any multi-hop cycle terminates\n * because a revisited node is never re-expanded.\n * - **Post-filter.** The scope / kind / tag / provenance-source / predicate axes of the query are\n * applied to the reached records (the link axes are the traversal itself).\n * @public\n */\nexport class LinkTraversalRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<LinkTraversalRetriever> {\n return succeed(new LinkTraversalRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return LINK_TRAVERSAL_CAPABILITIES;\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => this._traverse(query))\n );\n }\n\n /** Run the bounded, cycle-safe BFS and post-filter the reached records. */\n private _traverse(query: IMemoryQuery): Result<ReadonlyArray<IMemoryRecord<unknown>>> {\n const outbound: boolean = query.linkedFrom !== undefined;\n const seed: IEdgeTarget | undefined = query.linkedFrom ?? query.linkedTo;\n if (seed === undefined) {\n return fail(LINK_TRAVERSAL_NO_SEED_MESSAGE);\n }\n const hops: number = query.hops ?? DEFAULT_HOPS;\n const byKey: ReadonlyMap<string, IIndexedMemoryRecord> = this._indexByKey();\n\n // The visited-set IS the cycle guard: nodes are canonicalized to their\n // `(scope, id)` string, so set membership is an exact identity check. The\n // seed is pre-marked so it is never re-added.\n const visited: Set<string> = new Set<string>([edgeTargetKey(seed)]);\n const reached: IEdgeTarget[] = [];\n let frontier: IEdgeTarget[] = [seed];\n for (let hop = 0; hop < hops && frontier.length > 0; hop++) {\n const next: IEdgeTarget[] = [];\n for (const node of frontier) {\n for (const neighbor of outbound ? this._outbound(node, byKey) : this._inbound(node)) {\n const neighborKey: string = edgeTargetKey(neighbor);\n if (!visited.has(neighborKey)) {\n visited.add(neighborKey);\n reached.push(neighbor);\n next.push(neighbor);\n }\n }\n }\n frontier = next;\n }\n\n const entries: IIndexedMemoryRecord[] = [];\n for (const node of reached) {\n const match: IIndexedMemoryRecord | undefined = byKey.get(edgeTargetKey(node));\n if (match !== undefined) {\n entries.push(match);\n }\n }\n const ordered: IMemoryRecord<unknown>[] = entries\n .filter((entry) => indexedRecordMatchesQuery(entry, query))\n .map((entry) => entry.record)\n .sort(orderingCompare(query.orderBy));\n return succeed(limitRecords(ordered, query.limit, query.offset));\n }\n\n /**\n * Group the index's entries by their scope-qualified {@link edgeTargetKey}\n * `(scope, id)` composite. Each composite is the index's primary key, so it maps\n * to exactly one entry — two records that reuse a filename stem across scopes\n * (e.g. `turn-0` in two conversations) get distinct keys and never collide.\n */\n private _indexByKey(): ReadonlyMap<string, IIndexedMemoryRecord> {\n const byKey: Map<string, IIndexedMemoryRecord> = new Map<string, IIndexedMemoryRecord>();\n for (const entry of this._index.entries()) {\n byKey.set(edgeTargetKey({ scope: entry.scope, id: entry.record.envelope.id }), entry);\n }\n return byKey;\n }\n\n /** Outbound neighbors: the scope-qualified targets of every edge on the record at `node`. */\n private _outbound(node: IEdgeTarget, byKey: ReadonlyMap<string, IIndexedMemoryRecord>): IEdgeTarget[] {\n const targets: IEdgeTarget[] = [];\n const match: IIndexedMemoryRecord | undefined = byKey.get(edgeTargetKey(node));\n if (match !== undefined) {\n for (const edge of match.record.envelope.links) {\n targets.push(edge.target);\n }\n }\n return targets;\n }\n\n /** Inbound neighbors: the scope-qualified sources whose edges point AT `node` (the backlinks). */\n private _inbound(node: IEdgeTarget): ReadonlyArray<IEdgeTarget> {\n return this._index.backlinks(node);\n }\n}\n"]}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { succeed } from '@fgv/ts-utils';
|
|
6
6
|
import { NON_SEMANTIC_CAPABILITIES, guardRetrieverCapabilities, limitRecords, orderingCompare, selectByQuery } from './retriever';
|
|
7
7
|
/**
|
|
8
|
-
* Returns records matching the query's scope / kind / tag / predicate filters,
|
|
8
|
+
* Returns records matching the query's scope / kind / tag / provenance-source / predicate filters,
|
|
9
9
|
* ordered most-recently-updated first. The universal v1 retriever — an empty
|
|
10
10
|
* query returns the whole vault in recency order.
|
|
11
11
|
* @public
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recencyRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/recencyRetriever.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAU,OAAO,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAIL,yBAAyB,EACzB,0BAA0B,EAC1B,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,aAAa,CAAC;AAErB;;;;;GAKG;AACH,MAAM,OAAO,gBAAgB;IAG3B,YAAoB,KAAmB;QACrC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,yBAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,KAAmB;QACtC,OAAO,OAAO,CAAC,IAAI,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,0BAA0B,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,MAAM,OAAO,GAA6B,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,IAAI,CACxF,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,CAC/B,CAAC;YACF,OAAO,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACnE,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord } from '../types';\nimport { IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n NON_SEMANTIC_CAPABILITIES,\n guardRetrieverCapabilities,\n limitRecords,\n orderingCompare,\n selectByQuery\n} from './retriever';\n\n/**\n * Returns records matching the query's scope / kind / tag / predicate filters,\n * ordered most-recently-updated first. The universal v1 retriever — an empty\n * query returns the whole vault in recency order.\n * @public\n */\nexport class RecencyRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return NON_SEMANTIC_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<RecencyRetriever> {\n return succeed(new RecencyRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n const ordered: IMemoryRecord<unknown>[] = selectByQuery(this._index.entries(), query).sort(\n orderingCompare(query.orderBy)\n );\n return succeed(limitRecords(ordered, query.limit, query.offset));\n })\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"recencyRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/recencyRetriever.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAU,OAAO,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAIL,yBAAyB,EACzB,0BAA0B,EAC1B,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,aAAa,CAAC;AAErB;;;;;GAKG;AACH,MAAM,OAAO,gBAAgB;IAG3B,YAAoB,KAAmB;QACrC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,yBAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,KAAmB;QACtC,OAAO,OAAO,CAAC,IAAI,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,0BAA0B,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,MAAM,OAAO,GAA6B,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,IAAI,CACxF,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,CAC/B,CAAC;YACF,OAAO,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACnE,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord } from '../types';\nimport { IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n NON_SEMANTIC_CAPABILITIES,\n guardRetrieverCapabilities,\n limitRecords,\n orderingCompare,\n selectByQuery\n} from './retriever';\n\n/**\n * Returns records matching the query's scope / kind / tag / provenance-source / predicate filters,\n * ordered most-recently-updated first. The universal v1 retriever — an empty\n * query returns the whole vault in recency order.\n * @public\n */\nexport class RecencyRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return NON_SEMANTIC_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<RecencyRetriever> {\n return succeed(new RecencyRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n const ordered: IMemoryRecord<unknown>[] = selectByQuery(this._index.entries(), query).sort(\n orderingCompare(query.orderBy)\n );\n return succeed(limitRecords(ordered, query.limit, query.offset));\n })\n );\n }\n}\n"]}
|
|
@@ -93,9 +93,10 @@ export function orderingCompare(orderBy) {
|
|
|
93
93
|
return orderBy === 'rank' ? rankCompare : recencyCompare;
|
|
94
94
|
}
|
|
95
95
|
/**
|
|
96
|
-
* Whether an indexed entry satisfies a query's scope / kind / tag /
|
|
97
|
-
* pre-filter (the axes shared by every v1
|
|
98
|
-
* link axes are NOT applied here — those
|
|
96
|
+
* Whether an indexed entry satisfies a query's scope / kind / tag /
|
|
97
|
+
* provenance-source / predicate pre-filter (the axes shared by every v1
|
|
98
|
+
* retriever). The `semantic` / `asOf` / link axes are NOT applied here — those
|
|
99
|
+
* are each retriever's own concern.
|
|
99
100
|
* @public
|
|
100
101
|
*/
|
|
101
102
|
export function indexedRecordMatchesQuery(entry, query) {
|
|
@@ -111,14 +112,19 @@ export function indexedRecordMatchesQuery(entry, query) {
|
|
|
111
112
|
if (query.tag !== undefined && !entry.record.envelope.tags.includes(query.tag)) {
|
|
112
113
|
return false;
|
|
113
114
|
}
|
|
115
|
+
if (query.provenanceSource !== undefined &&
|
|
116
|
+
entry.record.envelope.provenance.source !== query.provenanceSource) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
114
119
|
if (query.filter !== undefined && !query.filter(entry.record)) {
|
|
115
120
|
return false;
|
|
116
121
|
}
|
|
117
122
|
return true;
|
|
118
123
|
}
|
|
119
124
|
/**
|
|
120
|
-
* Apply the shared scope / kind / tag / predicate pre-filter
|
|
121
|
-
* entries, returning the surviving records (unordered,
|
|
125
|
+
* Apply the shared scope / kind / tag / provenance-source / predicate pre-filter
|
|
126
|
+
* to a set of indexed entries, returning the surviving records (unordered,
|
|
127
|
+
* unlimited).
|
|
122
128
|
* @public
|
|
123
129
|
*/
|
|
124
130
|
export function selectByQuery(entries, query) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/retriever.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { IEdgeTarget, IMemoryRecord, Kind, MemoryScopeKey, Tag } from '../types';\nimport { IIndexedMemoryRecord } from '../index';\n\n/**\n * The capabilities a retriever exposes. A consumer probes these before\n * dispatching a query so it never silently gets an empty result for a\n * capability the retriever does not implement.\n * @public\n */\nexport interface IMemoryRetrieverCapabilities {\n /** Semantic / vector recall is operational (an {@link IVectorIndex} is wired). */\n readonly supportsSemanticRecall: boolean;\n /** Temporal \"as-of\" queries are operational (a temporal index is wired). */\n readonly supportsTemporalQuery: boolean;\n /** Link traversal is supported (an in-memory backlink index is present). */\n readonly supportsLinkTraversal: boolean;\n}\n\n/**\n * A retrieval query. Every field is optional; an empty query is the \"recency\n * over everything\" request. `semantic` and `asOf` are present from day one (the\n * no-resignature guarantee): a backend that adds semantic or temporal recall\n * sets the matching capability flag, with no interface change.\n * @public\n */\nexport interface IMemoryQuery {\n /** Restrict to records in this scope. */\n readonly scope?: MemoryScopeKey;\n /** Restrict to records carrying this tag (exact match). */\n readonly tag?: Tag;\n /**\n * Restrict to records of this kind — the single-kind shorthand for\n * {@link IMemoryQuery.kinds | kinds}. When both are set they compose as AND\n * (the record's kind must satisfy both), so `kind` must itself be a member of\n * `kinds` for anything to match.\n */\n readonly kind?: Kind;\n /**\n * Restrict to records in ANY of these kinds — the general (multi-kind) form of\n * {@link IMemoryQuery.kind | kind}. Absent → no kind-set constraint (today's\n * behavior). An explicit empty array `[]` matches NOTHING (mirroring the\n * non-positive-`limit` \"explicit empty\" convention), never \"match all\".\n */\n readonly kinds?: ReadonlyArray<Kind>;\n /** Restrict to records linked FROM this scope-qualified seed (outbound). */\n readonly linkedFrom?: IEdgeTarget;\n /** Restrict to records linked TO this scope-qualified seed (inbound / backlinks). */\n readonly linkedTo?: IEdgeTarget;\n /** BFS hop count for link traversal. Default: 1. */\n readonly hops?: number;\n /**\n * Text query for semantic / vector recall. If set and the retriever's\n * `supportsSemanticRecall` is `false`, the retriever returns a loud\n * `Result.fail` ({@link SEMANTIC_UNWIRED_MESSAGE}) — never a silent empty.\n */\n readonly semantic?: string;\n /** Top-K for semantic recall. Default: 10. */\n readonly topK?: number;\n /**\n * As-of epoch ms for temporal \"valid at\" queries. If set and the retriever's\n * `supportsTemporalQuery` is `false`, the retriever returns a loud\n * `Result.fail` — never a silent empty.\n */\n readonly asOf?: number;\n /**\n * Ordering for the result set. `'recency'` (the default when absent — today's\n * exact behavior) orders most-recently-updated first; `'rank'` orders by the\n * store-computed {@link IMemoryEnvelope.rank} descending (records with an absent\n * `rank` last), with recency as the tiebreak. Combined with `{ limit, offset }`\n * this yields a bounded top-M rank-ordered page with no full-vault scan.\n *\n * @remarks\n * `orderBy` governs the ordered non-semantic retrievers (recency / tag /\n * structured-filter / link-traversal) and the {@link HybridRetriever}'s\n * post-merge ordering. The {@link SemanticRetriever} is the sole exception: it\n * preserves its native vector-similarity order regardless of `orderBy` —\n * re-sorting semantic hits by `rank` would discard the similarity ranking that\n * is the whole point of that path; a consumer that wants rank ordering uses a\n * non-semantic query.\n */\n readonly orderBy?: 'recency' | 'rank';\n /** Maximum records to return. Applied after all other filters. */\n readonly limit?: number;\n /**\n * Records to skip after ordering, before `limit` — so `{ offset, limit }` is a\n * stable page window over the ordered result set. Default 0. A non-positive or\n * absent offset is today's behavior (no skip); an offset past the end yields an\n * empty page, never a throw.\n */\n readonly offset?: number;\n /** Arbitrary predicate applied after the scope / kind / tag pre-filter. */\n readonly filter?: (record: IMemoryRecord<unknown>) => boolean;\n}\n\n/**\n * The retrieval contract. A retriever exposes its {@link\n * IMemoryRetrieverCapabilities | capabilities} and answers\n * {@link IMemoryRetriever.retrieve | queries}, degrading loudly (never silently\n * empty) when a requested capability is not wired.\n * @public\n */\nexport interface IMemoryRetriever {\n /** The capabilities this retriever exposes. Probe before dispatch. */\n readonly capabilities: IMemoryRetrieverCapabilities;\n /**\n * Retrieve records matching `query`. Returns a `Result.fail` with a\n * diagnostic message when the query requests a capability this retriever does\n * not support (never an empty success).\n */\n retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;\n}\n\n/**\n * The loud-degradation message a retriever returns when `query.semantic` is set\n * but no {@link IVectorIndex} is wired.\n * @public\n */\nexport const SEMANTIC_UNWIRED_MESSAGE: string = 'semantic recall requires a vector index; none configured';\n\n/**\n * The loud-degradation message a retriever returns when a link-traversal axis\n * (`linkedFrom` / `linkedTo` / `hops`) is requested but no backlink index is\n * wired.\n * @public\n */\nexport const LINK_TRAVERSAL_UNWIRED_MESSAGE: string =\n 'link traversal requires a backlink index; none configured';\n\n/**\n * The capabilities every non-semantic, non-temporal, non-link v1 retriever\n * exposes (all three flags `false`).\n * @public\n */\nexport const NON_SEMANTIC_CAPABILITIES: IMemoryRetrieverCapabilities = {\n supportsSemanticRecall: false,\n supportsTemporalQuery: false,\n supportsLinkTraversal: false\n};\n\n/**\n * Build the loud-degradation message a retriever returns when `query.asOf` is\n * set but no temporal index is wired.\n * @public\n */\nexport function temporalUnwiredMessage(kind?: Kind): string {\n return `temporal query requires temporal index; none configured${\n kind !== undefined ? ` for kind ${kind}` : ''\n }`;\n}\n\n/**\n * Enforce the loud-degradation contract for the `semantic` and `asOf` axes\n * against a retriever's `capabilities`: a requested capability the retriever\n * does not support fails loudly rather than returning a silent empty result.\n * @public\n */\nexport function guardRetrieverCapabilities(\n query: IMemoryQuery,\n capabilities: IMemoryRetrieverCapabilities\n): Result<true> {\n if (query.semantic !== undefined && !capabilities.supportsSemanticRecall) {\n return fail(SEMANTIC_UNWIRED_MESSAGE);\n }\n if (query.asOf !== undefined && !capabilities.supportsTemporalQuery) {\n return fail(temporalUnwiredMessage(query.kind));\n }\n const requestsLinkTraversal: boolean =\n query.linkedFrom !== undefined || query.linkedTo !== undefined || query.hops !== undefined;\n if (requestsLinkTraversal && !capabilities.supportsLinkTraversal) {\n return fail(LINK_TRAVERSAL_UNWIRED_MESSAGE);\n }\n return succeed(true);\n}\n\n/**\n * Recency comparator: most-recently-updated first, with a `seq` tiebreak so\n * equal-`updated` records sort deterministically. Mirrors the B1 index ordering.\n * @public\n */\nexport function recencyCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number {\n const byUpdated: number = b.envelope.updated - a.envelope.updated;\n return byUpdated !== 0 ? byUpdated : b.envelope.seq - a.envelope.seq;\n}\n\n/**\n * Rank comparator: store-computed {@link IMemoryEnvelope.rank} descending, with\n * {@link recencyCompare} as the tiebreak. Records with an absent `rank` sort LAST\n * (after every ranked record), then by recency among themselves. Mirrors the\n * index's rank-view ordering.\n * @public\n */\nexport function rankCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number {\n const ra: number | undefined = a.envelope.rank;\n const rb: number | undefined = b.envelope.rank;\n if (ra === undefined && rb !== undefined) {\n return 1;\n }\n if (rb === undefined && ra !== undefined) {\n return -1;\n }\n if (ra !== undefined && rb !== undefined && ra !== rb) {\n return rb - ra;\n }\n return recencyCompare(a, b);\n}\n\n/**\n * Select the record comparator for a query's {@link IMemoryQuery.orderBy | orderBy}\n * axis: {@link rankCompare} for `'rank'`, {@link recencyCompare} otherwise (the\n * default, byte-identical to the pre-`orderBy` behavior).\n * @public\n */\nexport function orderingCompare(\n orderBy?: IMemoryQuery['orderBy']\n): (a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>) => number {\n return orderBy === 'rank' ? rankCompare : recencyCompare;\n}\n\n/**\n * Whether an indexed entry satisfies a query's scope / kind / tag / predicate\n * pre-filter (the axes shared by every v1 retriever). The `semantic` / `asOf` /\n * link axes are NOT applied here — those are each retriever's own concern.\n * @public\n */\nexport function indexedRecordMatchesQuery(entry: IIndexedMemoryRecord, query: IMemoryQuery): boolean {\n if (query.scope !== undefined && entry.scope !== query.scope) {\n return false;\n }\n if (query.kind !== undefined && entry.record.envelope.kind !== query.kind) {\n return false;\n }\n if (query.kinds !== undefined && !query.kinds.includes(entry.record.envelope.kind)) {\n return false;\n }\n if (query.tag !== undefined && !entry.record.envelope.tags.includes(query.tag)) {\n return false;\n }\n if (query.filter !== undefined && !query.filter(entry.record)) {\n return false;\n }\n return true;\n}\n\n/**\n * Apply the shared scope / kind / tag / predicate pre-filter to a set of indexed\n * entries, returning the surviving records (unordered, unlimited).\n * @public\n */\nexport function selectByQuery(\n entries: ReadonlyArray<IIndexedMemoryRecord>,\n query: IMemoryQuery\n): IMemoryRecord<unknown>[] {\n return entries.filter((entry) => indexedRecordMatchesQuery(entry, query)).map((entry) => entry.record);\n}\n\n/**\n * Apply the `{ offset, limit }` page window to an ordered record set. Applied\n * last, after ordering, so it always takes a stable window of the ordered\n * result. `offset` is applied first (records to skip), then `limit` (top-N of\n * the remainder).\n *\n * @remarks\n * Both bounds are public query input and are guarded against non-positive\n * values slipping into `slice`:\n * - `offset` absent or non-positive → no skip (today's behavior). An offset past\n * the end yields an empty page rather than a throw.\n * - `limit` absent → no truncation; a non-positive `limit` means \"no records\"\n * and returns an empty array.\n * @public\n */\nexport function limitRecords(\n records: ReadonlyArray<IMemoryRecord<unknown>>,\n limit?: number,\n offset?: number\n): ReadonlyArray<IMemoryRecord<unknown>> {\n const skip: number = offset !== undefined && offset > 0 ? offset : 0;\n const windowed: ReadonlyArray<IMemoryRecord<unknown>> = skip > 0 ? records.slice(skip) : records;\n if (limit === undefined) {\n return windowed;\n }\n if (limit <= 0) {\n return [];\n }\n return windowed.length > limit ? windowed.slice(0, limit) : windowed;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"retriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/retriever.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { IEdgeTarget, IMemoryRecord, Kind, MemoryScopeKey, ProvenanceSource, Tag } from '../types';\nimport { IIndexedMemoryRecord } from '../index';\n\n/**\n * The capabilities a retriever exposes. A consumer probes these before\n * dispatching a query so it never silently gets an empty result for a\n * capability the retriever does not implement.\n * @public\n */\nexport interface IMemoryRetrieverCapabilities {\n /** Semantic / vector recall is operational (an {@link IVectorIndex} is wired). */\n readonly supportsSemanticRecall: boolean;\n /** Temporal \"as-of\" queries are operational (a temporal index is wired). */\n readonly supportsTemporalQuery: boolean;\n /** Link traversal is supported (an in-memory backlink index is present). */\n readonly supportsLinkTraversal: boolean;\n}\n\n/**\n * A retrieval query. Every field is optional; an empty query is the \"recency\n * over everything\" request. `semantic` and `asOf` are present from day one (the\n * no-resignature guarantee): a backend that adds semantic or temporal recall\n * sets the matching capability flag, with no interface change.\n * @public\n */\nexport interface IMemoryQuery {\n /** Restrict to records in this scope. */\n readonly scope?: MemoryScopeKey;\n /** Restrict to records carrying this tag (exact match). */\n readonly tag?: Tag;\n /**\n * Restrict to records whose {@link IProvenance.source | provenance.source} is\n * exactly this value — the \"show me everything this source produced\" axis, for\n * review, attribution, and retraction after a bad ingest.\n *\n * @remarks\n * Exact match on `source` only, deliberately: not a subset match over the whole\n * {@link IProvenance} block, not a presence check, and not a match on `by` /\n * `model` / the consumer-owned extension keys. `source` is the only field of\n * that block the library assigns meaning to, and every record carries one\n * (`envelope.provenance` is required), so this axis is total — it partitions\n * the vault rather than filtering out records that merely omit a field.\n *\n * Applied in the shared pre-filter alongside `scope` / `kind` / `tag`, so every\n * retriever honours it and combining axes composes as AND.\n */\n readonly provenanceSource?: ProvenanceSource;\n /**\n * Restrict to records of this kind — the single-kind shorthand for\n * {@link IMemoryQuery.kinds | kinds}. When both are set they compose as AND\n * (the record's kind must satisfy both), so `kind` must itself be a member of\n * `kinds` for anything to match.\n */\n readonly kind?: Kind;\n /**\n * Restrict to records in ANY of these kinds — the general (multi-kind) form of\n * {@link IMemoryQuery.kind | kind}. Absent → no kind-set constraint (today's\n * behavior). An explicit empty array `[]` matches NOTHING (mirroring the\n * non-positive-`limit` \"explicit empty\" convention), never \"match all\".\n */\n readonly kinds?: ReadonlyArray<Kind>;\n /** Restrict to records linked FROM this scope-qualified seed (outbound). */\n readonly linkedFrom?: IEdgeTarget;\n /** Restrict to records linked TO this scope-qualified seed (inbound / backlinks). */\n readonly linkedTo?: IEdgeTarget;\n /** BFS hop count for link traversal. Default: 1. */\n readonly hops?: number;\n /**\n * Text query for semantic / vector recall. If set and the retriever's\n * `supportsSemanticRecall` is `false`, the retriever returns a loud\n * `Result.fail` ({@link SEMANTIC_UNWIRED_MESSAGE}) — never a silent empty.\n */\n readonly semantic?: string;\n /** Top-K for semantic recall. Default: 10. */\n readonly topK?: number;\n /**\n * As-of epoch ms for temporal \"valid at\" queries. If set and the retriever's\n * `supportsTemporalQuery` is `false`, the retriever returns a loud\n * `Result.fail` — never a silent empty.\n */\n readonly asOf?: number;\n /**\n * Ordering for the result set. `'recency'` (the default when absent — today's\n * exact behavior) orders most-recently-updated first; `'rank'` orders by the\n * store-computed {@link IMemoryEnvelope.rank} descending (records with an absent\n * `rank` last), with recency as the tiebreak. Combined with `{ limit, offset }`\n * this yields a bounded top-M rank-ordered page with no full-vault scan.\n *\n * @remarks\n * `orderBy` governs the ordered non-semantic retrievers (recency / tag /\n * structured-filter / link-traversal) and the {@link HybridRetriever}'s\n * post-merge ordering. The {@link SemanticRetriever} is the sole exception: it\n * preserves its native vector-similarity order regardless of `orderBy` —\n * re-sorting semantic hits by `rank` would discard the similarity ranking that\n * is the whole point of that path; a consumer that wants rank ordering uses a\n * non-semantic query.\n */\n readonly orderBy?: 'recency' | 'rank';\n /** Maximum records to return. Applied after all other filters. */\n readonly limit?: number;\n /**\n * Records to skip after ordering, before `limit` — so `{ offset, limit }` is a\n * stable page window over the ordered result set. Default 0. A non-positive or\n * absent offset is today's behavior (no skip); an offset past the end yields an\n * empty page, never a throw.\n */\n readonly offset?: number;\n /** Arbitrary predicate applied after the scope / kind / tag / provenance-source pre-filter. */\n readonly filter?: (record: IMemoryRecord<unknown>) => boolean;\n}\n\n/**\n * The retrieval contract. A retriever exposes its {@link\n * IMemoryRetrieverCapabilities | capabilities} and answers\n * {@link IMemoryRetriever.retrieve | queries}, degrading loudly (never silently\n * empty) when a requested capability is not wired.\n * @public\n */\nexport interface IMemoryRetriever {\n /** The capabilities this retriever exposes. Probe before dispatch. */\n readonly capabilities: IMemoryRetrieverCapabilities;\n /**\n * Retrieve records matching `query`. Returns a `Result.fail` with a\n * diagnostic message when the query requests a capability this retriever does\n * not support (never an empty success).\n */\n retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;\n}\n\n/**\n * The loud-degradation message a retriever returns when `query.semantic` is set\n * but no {@link IVectorIndex} is wired.\n * @public\n */\nexport const SEMANTIC_UNWIRED_MESSAGE: string = 'semantic recall requires a vector index; none configured';\n\n/**\n * The loud-degradation message a retriever returns when a link-traversal axis\n * (`linkedFrom` / `linkedTo` / `hops`) is requested but no backlink index is\n * wired.\n * @public\n */\nexport const LINK_TRAVERSAL_UNWIRED_MESSAGE: string =\n 'link traversal requires a backlink index; none configured';\n\n/**\n * The capabilities every non-semantic, non-temporal, non-link v1 retriever\n * exposes (all three flags `false`).\n * @public\n */\nexport const NON_SEMANTIC_CAPABILITIES: IMemoryRetrieverCapabilities = {\n supportsSemanticRecall: false,\n supportsTemporalQuery: false,\n supportsLinkTraversal: false\n};\n\n/**\n * Build the loud-degradation message a retriever returns when `query.asOf` is\n * set but no temporal index is wired.\n * @public\n */\nexport function temporalUnwiredMessage(kind?: Kind): string {\n return `temporal query requires temporal index; none configured${\n kind !== undefined ? ` for kind ${kind}` : ''\n }`;\n}\n\n/**\n * Enforce the loud-degradation contract for the `semantic` and `asOf` axes\n * against a retriever's `capabilities`: a requested capability the retriever\n * does not support fails loudly rather than returning a silent empty result.\n * @public\n */\nexport function guardRetrieverCapabilities(\n query: IMemoryQuery,\n capabilities: IMemoryRetrieverCapabilities\n): Result<true> {\n if (query.semantic !== undefined && !capabilities.supportsSemanticRecall) {\n return fail(SEMANTIC_UNWIRED_MESSAGE);\n }\n if (query.asOf !== undefined && !capabilities.supportsTemporalQuery) {\n return fail(temporalUnwiredMessage(query.kind));\n }\n const requestsLinkTraversal: boolean =\n query.linkedFrom !== undefined || query.linkedTo !== undefined || query.hops !== undefined;\n if (requestsLinkTraversal && !capabilities.supportsLinkTraversal) {\n return fail(LINK_TRAVERSAL_UNWIRED_MESSAGE);\n }\n return succeed(true);\n}\n\n/**\n * Recency comparator: most-recently-updated first, with a `seq` tiebreak so\n * equal-`updated` records sort deterministically. Mirrors the B1 index ordering.\n * @public\n */\nexport function recencyCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number {\n const byUpdated: number = b.envelope.updated - a.envelope.updated;\n return byUpdated !== 0 ? byUpdated : b.envelope.seq - a.envelope.seq;\n}\n\n/**\n * Rank comparator: store-computed {@link IMemoryEnvelope.rank} descending, with\n * {@link recencyCompare} as the tiebreak. Records with an absent `rank` sort LAST\n * (after every ranked record), then by recency among themselves. Mirrors the\n * index's rank-view ordering.\n * @public\n */\nexport function rankCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number {\n const ra: number | undefined = a.envelope.rank;\n const rb: number | undefined = b.envelope.rank;\n if (ra === undefined && rb !== undefined) {\n return 1;\n }\n if (rb === undefined && ra !== undefined) {\n return -1;\n }\n if (ra !== undefined && rb !== undefined && ra !== rb) {\n return rb - ra;\n }\n return recencyCompare(a, b);\n}\n\n/**\n * Select the record comparator for a query's {@link IMemoryQuery.orderBy | orderBy}\n * axis: {@link rankCompare} for `'rank'`, {@link recencyCompare} otherwise (the\n * default, byte-identical to the pre-`orderBy` behavior).\n * @public\n */\nexport function orderingCompare(\n orderBy?: IMemoryQuery['orderBy']\n): (a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>) => number {\n return orderBy === 'rank' ? rankCompare : recencyCompare;\n}\n\n/**\n * Whether an indexed entry satisfies a query's scope / kind / tag /\n * provenance-source / predicate pre-filter (the axes shared by every v1\n * retriever). The `semantic` / `asOf` / link axes are NOT applied here — those\n * are each retriever's own concern.\n * @public\n */\nexport function indexedRecordMatchesQuery(entry: IIndexedMemoryRecord, query: IMemoryQuery): boolean {\n if (query.scope !== undefined && entry.scope !== query.scope) {\n return false;\n }\n if (query.kind !== undefined && entry.record.envelope.kind !== query.kind) {\n return false;\n }\n if (query.kinds !== undefined && !query.kinds.includes(entry.record.envelope.kind)) {\n return false;\n }\n if (query.tag !== undefined && !entry.record.envelope.tags.includes(query.tag)) {\n return false;\n }\n if (\n query.provenanceSource !== undefined &&\n entry.record.envelope.provenance.source !== query.provenanceSource\n ) {\n return false;\n }\n if (query.filter !== undefined && !query.filter(entry.record)) {\n return false;\n }\n return true;\n}\n\n/**\n * Apply the shared scope / kind / tag / provenance-source / predicate pre-filter\n * to a set of indexed entries, returning the surviving records (unordered,\n * unlimited).\n * @public\n */\nexport function selectByQuery(\n entries: ReadonlyArray<IIndexedMemoryRecord>,\n query: IMemoryQuery\n): IMemoryRecord<unknown>[] {\n return entries.filter((entry) => indexedRecordMatchesQuery(entry, query)).map((entry) => entry.record);\n}\n\n/**\n * Apply the `{ offset, limit }` page window to an ordered record set. Applied\n * last, after ordering, so it always takes a stable window of the ordered\n * result. `offset` is applied first (records to skip), then `limit` (top-N of\n * the remainder).\n *\n * @remarks\n * Both bounds are public query input and are guarded against non-positive\n * values slipping into `slice`:\n * - `offset` absent or non-positive → no skip (today's behavior). An offset past\n * the end yields an empty page rather than a throw.\n * - `limit` absent → no truncation; a non-positive `limit` means \"no records\"\n * and returns an empty array.\n * @public\n */\nexport function limitRecords(\n records: ReadonlyArray<IMemoryRecord<unknown>>,\n limit?: number,\n offset?: number\n): ReadonlyArray<IMemoryRecord<unknown>> {\n const skip: number = offset !== undefined && offset > 0 ? offset : 0;\n const windowed: ReadonlyArray<IMemoryRecord<unknown>> = skip > 0 ? records.slice(skip) : records;\n if (limit === undefined) {\n return windowed;\n }\n if (limit <= 0) {\n return [];\n }\n return windowed.length > limit ? windowed.slice(0, limit) : windowed;\n}\n"]}
|
|
@@ -5,10 +5,32 @@
|
|
|
5
5
|
import { succeed } from '@fgv/ts-utils';
|
|
6
6
|
import { NON_SEMANTIC_CAPABILITIES, guardRetrieverCapabilities, limitRecords, orderingCompare, selectByQuery } from './retriever';
|
|
7
7
|
/**
|
|
8
|
-
* Returns records matching `query.filter`, narrowed
|
|
9
|
-
* pre-filter and recency-ordered.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Returns records matching `query.filter` or `query.provenanceSource`, narrowed
|
|
9
|
+
* by any scope / kind / tag / provenance-source pre-filter and recency-ordered.
|
|
10
|
+
* Those two are this retriever's axes: a query carrying neither is not its
|
|
11
|
+
* concern and yields an empty success (so it contributes nothing to a
|
|
12
|
+
* {@link HybridRetriever}, rather than failing).
|
|
13
|
+
*
|
|
14
|
+
* Note `provenanceSource` appears on both sides of that sentence, and the
|
|
15
|
+
* duplication is real rather than sloppy: it is one of this retriever's two
|
|
16
|
+
* *dispatch* axes (it decides whether the query is this retriever's concern at
|
|
17
|
+
* all) **and** a member of the shared *pre-filter* (it narrows the result set,
|
|
18
|
+
* as it does for every other retriever). `filter` is likewise both.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* `provenanceSource` is *applied* by the shared pre-filter, so every retriever
|
|
22
|
+
* narrows by it. What this retriever adds is *answering* a query whose only axis
|
|
23
|
+
* is `provenanceSource` — the "show me everything this source produced" request,
|
|
24
|
+
* which would otherwise fall through the `filter`-absent guard and come back
|
|
25
|
+
* empty.
|
|
26
|
+
*
|
|
27
|
+
* Consequently, inside a {@link HybridRetriever} composed with the universal
|
|
28
|
+
* {@link RecencyRetriever}, a `provenanceSource`-only query is answered by both
|
|
29
|
+
* children and every matching record scores twice under a score-union merge.
|
|
30
|
+
* That is the established behavior for a dedicated-axis retriever composed with
|
|
31
|
+
* the universal one — {@link TagRetriever} double-scores a `tag`-only query the
|
|
32
|
+
* same way — and is intentional here, not an artifact of grafting a second axis
|
|
33
|
+
* onto a retriever whose original concern was arbitrary predicates.
|
|
12
34
|
* @public
|
|
13
35
|
*/
|
|
14
36
|
export class StructuredFilterRetriever {
|
|
@@ -26,7 +48,7 @@ export class StructuredFilterRetriever {
|
|
|
26
48
|
/** {@inheritDoc IMemoryRetriever.retrieve} */
|
|
27
49
|
retrieve(query) {
|
|
28
50
|
return Promise.resolve(guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {
|
|
29
|
-
if (query.filter === undefined) {
|
|
51
|
+
if (query.filter === undefined && query.provenanceSource === undefined) {
|
|
30
52
|
return succeed([]);
|
|
31
53
|
}
|
|
32
54
|
const ordered = selectByQuery(this._index.entries(), query).sort(orderingCompare(query.orderBy));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"structuredFilterRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/structuredFilterRetriever.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAU,OAAO,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAIL,yBAAyB,EACzB,0BAA0B,EAC1B,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,aAAa,CAAC;AAErB
|
|
1
|
+
{"version":3,"file":"structuredFilterRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/structuredFilterRetriever.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAU,OAAO,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAIL,yBAAyB,EACzB,0BAA0B,EAC1B,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,OAAO,yBAAyB;IAGpC,YAAoB,KAAmB;QACrC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,yBAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,KAAmB;QACtC,OAAO,OAAO,CAAC,IAAI,yBAAyB,CAAC,KAAK,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,0BAA0B,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;gBACvE,OAAO,OAAO,CAAC,EAAE,CAAC,CAAC;YACrB,CAAC;YACD,MAAM,OAAO,GAA6B,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,IAAI,CACxF,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,CAC/B,CAAC;YACF,OAAO,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACnE,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord } from '../types';\nimport { IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n NON_SEMANTIC_CAPABILITIES,\n guardRetrieverCapabilities,\n limitRecords,\n orderingCompare,\n selectByQuery\n} from './retriever';\n\n/**\n * Returns records matching `query.filter` or `query.provenanceSource`, narrowed\n * by any scope / kind / tag / provenance-source pre-filter and recency-ordered.\n * Those two are this retriever's axes: a query carrying neither is not its\n * concern and yields an empty success (so it contributes nothing to a\n * {@link HybridRetriever}, rather than failing).\n *\n * Note `provenanceSource` appears on both sides of that sentence, and the\n * duplication is real rather than sloppy: it is one of this retriever's two\n * *dispatch* axes (it decides whether the query is this retriever's concern at\n * all) **and** a member of the shared *pre-filter* (it narrows the result set,\n * as it does for every other retriever). `filter` is likewise both.\n *\n * @remarks\n * `provenanceSource` is *applied* by the shared pre-filter, so every retriever\n * narrows by it. What this retriever adds is *answering* a query whose only axis\n * is `provenanceSource` — the \"show me everything this source produced\" request,\n * which would otherwise fall through the `filter`-absent guard and come back\n * empty.\n *\n * Consequently, inside a {@link HybridRetriever} composed with the universal\n * {@link RecencyRetriever}, a `provenanceSource`-only query is answered by both\n * children and every matching record scores twice under a score-union merge.\n * That is the established behavior for a dedicated-axis retriever composed with\n * the universal one — {@link TagRetriever} double-scores a `tag`-only query the\n * same way — and is intentional here, not an artifact of grafting a second axis\n * onto a retriever whose original concern was arbitrary predicates.\n * @public\n */\nexport class StructuredFilterRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return NON_SEMANTIC_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<StructuredFilterRetriever> {\n return succeed(new StructuredFilterRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n if (query.filter === undefined && query.provenanceSource === undefined) {\n return succeed([]);\n }\n const ordered: IMemoryRecord<unknown>[] = selectByQuery(this._index.entries(), query).sort(\n orderingCompare(query.orderBy)\n );\n return succeed(limitRecords(ordered, query.limit, query.offset));\n })\n );\n }\n}\n"]}
|
|
@@ -6,7 +6,7 @@ import { succeed } from '@fgv/ts-utils';
|
|
|
6
6
|
import { NON_SEMANTIC_CAPABILITIES, guardRetrieverCapabilities, limitRecords, orderingCompare, selectByQuery } from './retriever';
|
|
7
7
|
/**
|
|
8
8
|
* Returns records carrying `query.tag`, recency-ordered within the tag and
|
|
9
|
-
* narrowed by any scope / kind / predicate filters. Tag is this retriever's
|
|
9
|
+
* narrowed by any scope / kind / provenance-source / predicate filters. Tag is this retriever's
|
|
10
10
|
* axis: a query without a `tag` is not its concern and yields an empty success
|
|
11
11
|
* (so it contributes nothing to a {@link HybridRetriever}, rather than failing).
|
|
12
12
|
* @public
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tagRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/tagRetriever.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAU,OAAO,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAIL,yBAAyB,EACzB,0BAA0B,EAC1B,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,MAAM,OAAO,YAAY;IAGvB,YAAoB,KAAmB;QACrC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,yBAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,KAAmB;QACtC,OAAO,OAAO,CAAC,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1C,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,0BAA0B,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,IAAI,KAAK,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;gBAC5B,OAAO,OAAO,CAAC,EAAE,CAAC,CAAC;YACrB,CAAC;YACD,MAAM,OAAO,GAA6B,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,IAAI,CACxF,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,CAC/B,CAAC;YACF,OAAO,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACnE,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord } from '../types';\nimport { IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n NON_SEMANTIC_CAPABILITIES,\n guardRetrieverCapabilities,\n limitRecords,\n orderingCompare,\n selectByQuery\n} from './retriever';\n\n/**\n * Returns records carrying `query.tag`, recency-ordered within the tag and\n * narrowed by any scope / kind / predicate filters. Tag is this retriever's\n * axis: a query without a `tag` is not its concern and yields an empty success\n * (so it contributes nothing to a {@link HybridRetriever}, rather than failing).\n * @public\n */\nexport class TagRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return NON_SEMANTIC_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<TagRetriever> {\n return succeed(new TagRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n if (query.tag === undefined) {\n return succeed([]);\n }\n const ordered: IMemoryRecord<unknown>[] = selectByQuery(this._index.entries(), query).sort(\n orderingCompare(query.orderBy)\n );\n return succeed(limitRecords(ordered, query.limit, query.offset));\n })\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"tagRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/tagRetriever.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAU,OAAO,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAIL,yBAAyB,EACzB,0BAA0B,EAC1B,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,MAAM,OAAO,YAAY;IAGvB,YAAoB,KAAmB;QACrC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,yBAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,KAAmB;QACtC,OAAO,OAAO,CAAC,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1C,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,0BAA0B,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,IAAI,KAAK,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;gBAC5B,OAAO,OAAO,CAAC,EAAE,CAAC,CAAC;YACrB,CAAC;YACD,MAAM,OAAO,GAA6B,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,IAAI,CACxF,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,CAC/B,CAAC;YACF,OAAO,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACnE,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord } from '../types';\nimport { IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n NON_SEMANTIC_CAPABILITIES,\n guardRetrieverCapabilities,\n limitRecords,\n orderingCompare,\n selectByQuery\n} from './retriever';\n\n/**\n * Returns records carrying `query.tag`, recency-ordered within the tag and\n * narrowed by any scope / kind / provenance-source / predicate filters. Tag is this retriever's\n * axis: a query without a `tag` is not its concern and yields an empty success\n * (so it contributes nothing to a {@link HybridRetriever}, rather than failing).\n * @public\n */\nexport class TagRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return NON_SEMANTIC_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<TagRetriever> {\n return succeed(new TagRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n if (query.tag === undefined) {\n return succeed([]);\n }\n const ordered: IMemoryRecord<unknown>[] = selectByQuery(this._index.entries(), query).sort(\n orderingCompare(query.orderBy)\n );\n return succeed(limitRecords(ordered, query.limit, query.offset));\n })\n );\n }\n}\n"]}
|
|
@@ -17,7 +17,7 @@ export const TEMPORAL_CAPABILITIES = {
|
|
|
17
17
|
supportsLinkTraversal: false
|
|
18
18
|
};
|
|
19
19
|
/**
|
|
20
|
-
* Group the temporal records surviving a query's scope / kind / tag / predicate
|
|
20
|
+
* Group the temporal records surviving a query's scope / kind / tag / provenance-source / predicate
|
|
21
21
|
* pre-filter by entity (`kind` + `entityId`). Non-temporal records are excluded —
|
|
22
22
|
* the temporal retrievers operate only over versioned entities. The map values
|
|
23
23
|
* are each entity's versions (unordered).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"temporalRetrievers.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/temporalRetrievers.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord, isTemporalRecord, selectCurrentVersion, selectVersionAsOf } from '../types';\nimport { IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n guardRetrieverCapabilities,\n indexedRecordMatchesQuery,\n limitRecords,\n recencyCompare\n} from './retriever';\n\n/**\n * The capabilities every temporal retriever exposes: temporal \"as-of\" queries\n * are operational (semantic recall and link traversal are not this retriever's\n * concern).\n * @public\n */\nexport const TEMPORAL_CAPABILITIES: IMemoryRetrieverCapabilities = {\n supportsSemanticRecall: false,\n supportsTemporalQuery: true,\n supportsLinkTraversal: false\n};\n\n/**\n * Group the temporal records surviving a query's scope / kind / tag / predicate\n * pre-filter by entity (`kind` + `entityId`). Non-temporal records are excluded —\n * the temporal retrievers operate only over versioned entities. The map values\n * are each entity's versions (unordered).\n */\nfunction groupTemporalVersionsByEntity(\n index: IMemoryIndex,\n query: IMemoryQuery\n): Map<string, IMemoryRecord<unknown>[]> {\n const groups: Map<string, IMemoryRecord<unknown>[]> = new Map<string, IMemoryRecord<unknown>[]>();\n for (const entry of index.entries()) {\n if (!isTemporalRecord(entry.record) || !indexedRecordMatchesQuery(entry, query)) {\n continue;\n }\n const key: string = `${entry.record.envelope.kind}\\0${entry.record.envelope.entityId}`;\n const existing: IMemoryRecord<unknown>[] | undefined = groups.get(key);\n if (existing === undefined) {\n groups.set(key, [entry.record]);\n } else {\n existing.push(entry.record);\n }\n }\n return groups;\n}\n\n/**\n * Temporal retriever returning the **current** version of each temporal entity\n * matching the query (the newest version whose `invalid_at` is null/absent),\n * recency-ordered and limited. A fully-invalidated (soft-deleted) entity\n * contributes nothing. Non-temporal records are not this retriever's concern.\n * @public\n */\nexport class CurrentValidRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return TEMPORAL_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<CurrentValidRetriever> {\n return succeed(new CurrentValidRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n const selected: IMemoryRecord<unknown>[] = [];\n for (const versions of groupTemporalVersionsByEntity(this._index, query).values()) {\n const current: IMemoryRecord<unknown> | undefined = selectCurrentVersion(versions);\n if (current !== undefined) {\n selected.push(current);\n }\n }\n selected.sort(recencyCompare);\n return succeed(limitRecords(selected, query.limit, query.offset));\n })\n );\n }\n}\n\n/**\n * Temporal retriever returning, for each temporal entity matching the query, the\n * version valid at `query.asOf` (epoch ms). `asOf` is this retriever's axis: a\n * query without it yields an empty success (a no-op contribution to a\n * {@link HybridRetriever}, not a failure). Results are recency-ordered and\n * limited.\n * @public\n */\nexport class AsOfRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return TEMPORAL_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<AsOfRetriever> {\n return succeed(new AsOfRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n if (query.asOf === undefined) {\n return succeed([]);\n }\n const asOf: number = query.asOf;\n const selected: IMemoryRecord<unknown>[] = [];\n for (const versions of groupTemporalVersionsByEntity(this._index, query).values()) {\n const valid: IMemoryRecord<unknown> | undefined = selectVersionAsOf(versions, asOf);\n if (valid !== undefined) {\n selected.push(valid);\n }\n }\n selected.sort(recencyCompare);\n return succeed(limitRecords(selected, query.limit, query.offset));\n })\n );\n }\n}\n\n/**\n * Temporal retriever returning **every** version of each temporal entity matching\n * the query, ordered ascending by `valid_at` (then `seq` as a stable tiebreak) —\n * the entity's full history. Limited after ordering.\n *\n * @remarks\n * When the query matches more than one entity, the result is a single\n * CROSS-entity list globally sorted by `valid_at` — versions of different\n * entities interleave, NOT grouped per entity — so `limit` truncates that\n * globally-sorted list. Constrain to one entity (e.g. via `query.scope`) for a\n * single entity's contiguous history.\n * @public\n */\nexport class HistoryRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return TEMPORAL_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<HistoryRetriever> {\n return succeed(new HistoryRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n const history: IMemoryRecord<unknown>[] = [];\n for (const versions of groupTemporalVersionsByEntity(this._index, query).values()) {\n history.push(...versions);\n }\n history.sort(HistoryRetriever._byValidAtAscending);\n return succeed(limitRecords(history, query.limit, query.offset));\n })\n );\n }\n\n /** A version's world-truth start: its `valid_at`, defaulting to `created` when absent. */\n private static _startOf(record: IMemoryRecord<unknown>): number {\n const temporal: IMemoryRecord<unknown>['envelope']['temporal'] = record.envelope.temporal;\n /* c8 ignore next 3 -- unreachable: HistoryRetriever orders only temporal records (pre-filtered by isTemporalRecord), so `temporal` is always present; the guard keeps the type honest */\n if (temporal === undefined) {\n return record.envelope.created;\n }\n return temporal.valid_at ?? record.envelope.created;\n }\n\n /**\n * Ascending-by-`valid_at` comparator (a version's `valid_at` defaults to its\n * `created` when absent), with `seq` as a stable ascending tiebreak so\n * same-instant versions order by write sequence.\n */\n private static _byValidAtAscending(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number {\n const aStart: number = HistoryRetriever._startOf(a);\n const bStart: number = HistoryRetriever._startOf(b);\n return aStart !== bStart ? aStart - bStart : a.envelope.seq - b.envelope.seq;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"temporalRetrievers.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/temporalRetrievers.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord, isTemporalRecord, selectCurrentVersion, selectVersionAsOf } from '../types';\nimport { IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n guardRetrieverCapabilities,\n indexedRecordMatchesQuery,\n limitRecords,\n recencyCompare\n} from './retriever';\n\n/**\n * The capabilities every temporal retriever exposes: temporal \"as-of\" queries\n * are operational (semantic recall and link traversal are not this retriever's\n * concern).\n * @public\n */\nexport const TEMPORAL_CAPABILITIES: IMemoryRetrieverCapabilities = {\n supportsSemanticRecall: false,\n supportsTemporalQuery: true,\n supportsLinkTraversal: false\n};\n\n/**\n * Group the temporal records surviving a query's scope / kind / tag / provenance-source / predicate\n * pre-filter by entity (`kind` + `entityId`). Non-temporal records are excluded —\n * the temporal retrievers operate only over versioned entities. The map values\n * are each entity's versions (unordered).\n */\nfunction groupTemporalVersionsByEntity(\n index: IMemoryIndex,\n query: IMemoryQuery\n): Map<string, IMemoryRecord<unknown>[]> {\n const groups: Map<string, IMemoryRecord<unknown>[]> = new Map<string, IMemoryRecord<unknown>[]>();\n for (const entry of index.entries()) {\n if (!isTemporalRecord(entry.record) || !indexedRecordMatchesQuery(entry, query)) {\n continue;\n }\n const key: string = `${entry.record.envelope.kind}\\0${entry.record.envelope.entityId}`;\n const existing: IMemoryRecord<unknown>[] | undefined = groups.get(key);\n if (existing === undefined) {\n groups.set(key, [entry.record]);\n } else {\n existing.push(entry.record);\n }\n }\n return groups;\n}\n\n/**\n * Temporal retriever returning the **current** version of each temporal entity\n * matching the query (the newest version whose `invalid_at` is null/absent),\n * recency-ordered and limited. A fully-invalidated (soft-deleted) entity\n * contributes nothing. Non-temporal records are not this retriever's concern.\n * @public\n */\nexport class CurrentValidRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return TEMPORAL_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<CurrentValidRetriever> {\n return succeed(new CurrentValidRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n const selected: IMemoryRecord<unknown>[] = [];\n for (const versions of groupTemporalVersionsByEntity(this._index, query).values()) {\n const current: IMemoryRecord<unknown> | undefined = selectCurrentVersion(versions);\n if (current !== undefined) {\n selected.push(current);\n }\n }\n selected.sort(recencyCompare);\n return succeed(limitRecords(selected, query.limit, query.offset));\n })\n );\n }\n}\n\n/**\n * Temporal retriever returning, for each temporal entity matching the query, the\n * version valid at `query.asOf` (epoch ms). `asOf` is this retriever's axis: a\n * query without it yields an empty success (a no-op contribution to a\n * {@link HybridRetriever}, not a failure). Results are recency-ordered and\n * limited.\n * @public\n */\nexport class AsOfRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return TEMPORAL_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<AsOfRetriever> {\n return succeed(new AsOfRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n if (query.asOf === undefined) {\n return succeed([]);\n }\n const asOf: number = query.asOf;\n const selected: IMemoryRecord<unknown>[] = [];\n for (const versions of groupTemporalVersionsByEntity(this._index, query).values()) {\n const valid: IMemoryRecord<unknown> | undefined = selectVersionAsOf(versions, asOf);\n if (valid !== undefined) {\n selected.push(valid);\n }\n }\n selected.sort(recencyCompare);\n return succeed(limitRecords(selected, query.limit, query.offset));\n })\n );\n }\n}\n\n/**\n * Temporal retriever returning **every** version of each temporal entity matching\n * the query, ordered ascending by `valid_at` (then `seq` as a stable tiebreak) —\n * the entity's full history. Limited after ordering.\n *\n * @remarks\n * When the query matches more than one entity, the result is a single\n * CROSS-entity list globally sorted by `valid_at` — versions of different\n * entities interleave, NOT grouped per entity — so `limit` truncates that\n * globally-sorted list. Constrain to one entity (e.g. via `query.scope`) for a\n * single entity's contiguous history.\n * @public\n */\nexport class HistoryRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return TEMPORAL_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<HistoryRetriever> {\n return succeed(new HistoryRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n const history: IMemoryRecord<unknown>[] = [];\n for (const versions of groupTemporalVersionsByEntity(this._index, query).values()) {\n history.push(...versions);\n }\n history.sort(HistoryRetriever._byValidAtAscending);\n return succeed(limitRecords(history, query.limit, query.offset));\n })\n );\n }\n\n /** A version's world-truth start: its `valid_at`, defaulting to `created` when absent. */\n private static _startOf(record: IMemoryRecord<unknown>): number {\n const temporal: IMemoryRecord<unknown>['envelope']['temporal'] = record.envelope.temporal;\n /* c8 ignore next 3 -- unreachable: HistoryRetriever orders only temporal records (pre-filtered by isTemporalRecord), so `temporal` is always present; the guard keeps the type honest */\n if (temporal === undefined) {\n return record.envelope.created;\n }\n return temporal.valid_at ?? record.envelope.created;\n }\n\n /**\n * Ascending-by-`valid_at` comparator (a version's `valid_at` defaults to its\n * `created` when absent), with `seq` as a stable ascending tiebreak so\n * same-instant versions order by write sequence.\n */\n private static _byValidAtAscending(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number {\n const aStart: number = HistoryRetriever._startOf(a);\n const bStart: number = HistoryRetriever._startOf(b);\n return aStart !== bStart ? aStart - bStart : a.envelope.seq - b.envelope.seq;\n }\n}\n"]}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Hash, Logging, fail, mapResults, mapSuccess, succeed } from '@fgv/ts-utils';
|
|
6
6
|
import { FileTree } from '@fgv/ts-json-base';
|
|
7
7
|
import { DEFAULT_DEDUP_SCOPE, KnowledgeLwwPolicy, isTemporalIdentityCodec, isTemporalRecord, isVersionCurrent, selectCurrentVersion, selectVersionAsOf } from '../types';
|
|
8
|
-
import { parseMemoryFile, serializeMemoryFile } from '../converters';
|
|
8
|
+
import { parseMemoryFile, serializeMemoryFile, splitFrontmatter } from '../converters';
|
|
9
9
|
import { VectorMaintenance } from './vectorMaintenance';
|
|
10
10
|
import { MemoryIndex } from '../index';
|
|
11
11
|
import { defaultMemoryScopeEncoding } from './scopeEncoding';
|
|
@@ -218,6 +218,10 @@ export class FileTreeMemoryStore {
|
|
|
218
218
|
}
|
|
219
219
|
return result;
|
|
220
220
|
}
|
|
221
|
+
/** {@inheritDoc IMemoryStore.reconcileRank} */
|
|
222
|
+
async reconcileRank(kind) {
|
|
223
|
+
return this._enqueue(() => this._reconcileRankLocked(kind));
|
|
224
|
+
}
|
|
221
225
|
/** {@inheritDoc IMemoryStore.put} */
|
|
222
226
|
async put(record) {
|
|
223
227
|
const result = await this._enqueue(() => this._putLocked(record));
|
|
@@ -808,6 +812,101 @@ export class FileTreeMemoryStore {
|
|
|
808
812
|
_contentHash(kind, body, links) {
|
|
809
813
|
return this._hasher.computeHash({ kind, body, links });
|
|
810
814
|
}
|
|
815
|
+
/**
|
|
816
|
+
* The locked body of {@link FileTreeMemoryStore.reconcileRank}.
|
|
817
|
+
*
|
|
818
|
+
* @remarks
|
|
819
|
+
* Re-reads each record's file rather than trusting the in-memory index, for
|
|
820
|
+
* two reasons: the index holds converted bodies on some paths and raw ones on
|
|
821
|
+
* others, and re-serializing a *converted* body could change the bytes on disk
|
|
822
|
+
* — a reconcile of an ordering field has no business rewriting content.
|
|
823
|
+
* `splitFrontmatter` hands back the body text unconverted, so the round trip
|
|
824
|
+
* carries the authored characters through untouched and only the envelope
|
|
825
|
+
* moves. The parsed record is additionally put through `_verifyLoaded`, the
|
|
826
|
+
* same id-vs-filename and scope-derived-entityId check the load paths apply —
|
|
827
|
+
* without it, reconcile would be the one path that accepts and rewrites a file
|
|
828
|
+
* the store would refuse to load.
|
|
829
|
+
*
|
|
830
|
+
* "Untouched" is not quite "byte-identical", and the exception is line
|
|
831
|
+
* endings: `splitFrontmatter` strips a trailing `\r` per line and
|
|
832
|
+
* `joinFrontmatter` writes `\n` delimiters, so a CRLF-authored file comes back
|
|
833
|
+
* LF-normalized. That is **the store's behavior on every write path, not
|
|
834
|
+
* something reconcile introduces** — an ordinary `put` normalizes the same way
|
|
835
|
+
* — so reconcile does not rewrite content that a subsequent write would have
|
|
836
|
+
* left alone. The property being claimed here is the narrower and load-bearing
|
|
837
|
+
* one: no body is round-tripped through its registered Converter.
|
|
838
|
+
*
|
|
839
|
+
* The projector is fed an `IMemoryRecord<string>` carrying that raw body,
|
|
840
|
+
* which is exactly the shape {@link FileTreeMemoryStore._stampRank} hands it
|
|
841
|
+
* on the write path — so a projector cannot see one thing on a write and
|
|
842
|
+
* another on a reconcile. `_stampRank` itself is reused verbatim, which also
|
|
843
|
+
* inherits its throw semantics (logged at `warn`, `rank` cleared).
|
|
844
|
+
*/
|
|
845
|
+
async _reconcileRankLocked(kind) {
|
|
846
|
+
if (!this._rankProjectors.has(kind)) {
|
|
847
|
+
return fail(`memory reconcileRank '${kind}': no rank projector is registered for this kind`);
|
|
848
|
+
}
|
|
849
|
+
const targets = this._index
|
|
850
|
+
.entries()
|
|
851
|
+
.filter((entry) => entry.record.envelope.kind === kind);
|
|
852
|
+
let restamped = 0;
|
|
853
|
+
for (const target of targets) {
|
|
854
|
+
const applied = this._restampOne(target.scope, target.record.envelope.id);
|
|
855
|
+
if (applied.isFailure()) {
|
|
856
|
+
return fail(`memory reconcileRank '${kind}': ${applied.message}`);
|
|
857
|
+
}
|
|
858
|
+
if (applied.value) {
|
|
859
|
+
restamped++;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return succeed(restamped);
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Re-apply the rank projector to one record on disk. Returns whether `rank`
|
|
866
|
+
* actually changed — an unchanged rank writes nothing, so a reconcile over an
|
|
867
|
+
* already-consistent store touches no files.
|
|
868
|
+
*/
|
|
869
|
+
_restampOne(scope, id) {
|
|
870
|
+
return this._resolveScopeDir(scope).onSuccess((scopeDir) => {
|
|
871
|
+
/* c8 ignore next 3 - defensive: the scope dir exists for any indexed record */
|
|
872
|
+
if (scopeDir === undefined) {
|
|
873
|
+
return fail(`'${id}': scope '${scope}' not found`);
|
|
874
|
+
}
|
|
875
|
+
return scopeDir.getChildren().onSuccess((children) => {
|
|
876
|
+
const targetName = `${id}${MEMORY_FILE_EXTENSION}`;
|
|
877
|
+
const file = children.find((c) => c.type === 'file' && c.name === targetName);
|
|
878
|
+
/* c8 ignore next 3 - defensive: the file exists for any indexed record */
|
|
879
|
+
if (file === undefined) {
|
|
880
|
+
return fail(`'${id}': file not found`);
|
|
881
|
+
}
|
|
882
|
+
return file.getRawContents().onSuccess((raw) =>
|
|
883
|
+
// `parseMemoryFile` validates the body through the registered Converter,
|
|
884
|
+
// so a corrupt record is refused rather than silently rewritten, and
|
|
885
|
+
// `_verifyLoaded` re-applies the same id-vs-filename and
|
|
886
|
+
// scope-derived-entityId checks the two load paths apply. Reconcile is a
|
|
887
|
+
// read-then-write, and a file can change on disk after the index was
|
|
888
|
+
// built, so skipping them would make this the one path that accepts —
|
|
889
|
+
// and rewrites — a record the store would otherwise refuse to load.
|
|
890
|
+
// `splitFrontmatter` then supplies the body text for the write.
|
|
891
|
+
parseMemoryFile(raw, this._registry)
|
|
892
|
+
.withErrorFormat((msg) => `'${id}': ${msg}`)
|
|
893
|
+
.onSuccess((parsedRecord) => this._verifyLoaded(scope, file, parsedRecord))
|
|
894
|
+
.onSuccess((parsed) => splitFrontmatter(raw)
|
|
895
|
+
.withErrorFormat((msg) => `'${id}': ${msg}`)
|
|
896
|
+
.onSuccess((parts) => {
|
|
897
|
+
const before = parsed.envelope.rank;
|
|
898
|
+
const stamped = this._stampRank({
|
|
899
|
+
envelope: parsed.envelope,
|
|
900
|
+
body: parts.body
|
|
901
|
+
});
|
|
902
|
+
if (stamped.envelope.rank === before) {
|
|
903
|
+
return succeed(false);
|
|
904
|
+
}
|
|
905
|
+
return this._persist(stamped, scope, id).onSuccess(() => succeed(true));
|
|
906
|
+
})));
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
}
|
|
811
910
|
/**
|
|
812
911
|
* Stamp the store-computed {@link IMemoryEnvelope.rank} onto a fully-built,
|
|
813
912
|
* fully-stamped record by running the kind's registered {@link RankProjector}.
|