@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../../src/packlets/types/envelope.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAyDH;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,MAAmB;IAC/C,OAAO,GAAG,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC;AACzC,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { EntityId, Kind, LinkType, MemoryId, MemoryScopeKey, Tag } from './ids';\n\n/**\n * Origin of a provenance attribution. Open vocabulary: the three named\n * sources are conventional, but the `(string & {})` arm admits any other\n * source string without resignature.\n * @public\n */\nexport type ProvenanceSource = 'agent' | 'host-ingest' | 'human' | (string & {});\n\n/**\n * Structured provenance for a record or an edge. Never a flat enum — the\n * `[key: string]: unknown` index signature lets a consumer attach an opaque\n * domain payload (e.g. PersonAIlity's sentiment / epistemic blocks) without\n * changing this interface, while still satisfying the no-`any` rule.\n * @public\n */\nexport interface IProvenance {\n /** Where the attribution came from. */\n readonly source: ProvenanceSource;\n /** Optional human or agent identifier responsible for the write. */\n readonly by?: string;\n /** Optional model identifier, when a model produced the content. */\n readonly model?: string;\n /** Optional confidence in `[0, 1]`. */\n readonly confidence?: number;\n /**\n * Scope-qualified back-link to the source record. Enables the cross-kind\n * provenance spine. A scope-qualified {@link IEdgeTarget} (not a bare\n * {@link MemoryId}) because per-scope codecs (e.g. the MTM codec's `turn-<n>`\n * stems) legally reuse a stem across scopes, so a bare id would be ambiguous —\n * the same reason {@link IEdge.target} is scope-qualified.\n */\n readonly derivedFrom?: IEdgeTarget;\n /** Opaque extension payload — consumer-owned, never interpreted by the store. */\n readonly [key: string]: unknown;\n}\n\n/**\n * The physical address of a linked-to record: the `(scope, id)` pair that\n * uniquely identifies it. Both components are required because a bare\n * {@link MemoryId} is NOT unique across scopes — per-scope codecs (e.g. the\n * medium-term codec's `turn-<n>` stems) legally mint the same stem under\n * different scopes, so an edge that carried only the id would be ambiguous.\n * `(scope, id)` matches the store's `getById(scope, id)` addressing and the\n * index's composite primary key.\n * @public\n */\nexport interface IEdgeTarget {\n /** The scope the target record lives under. */\n readonly scope: MemoryScopeKey;\n /** The target record's stable file-stem id (unique WITHIN {@link IEdgeTarget.scope}). */\n readonly id: MemoryId;\n}\n\n/**\n * The canonical composite-key string for an {@link IEdgeTarget}: scope + id,\n * NUL-separated. NUL is excluded from both components (scope segments are\n * filename-safe; {@link MemoryId} is portable-filename-safe), so it is a\n * collision-proof separator. This is the ONE canonicalization every consumer\n * that keys on a scoped target uses — the backlink index, the cycle guard, and\n * the ingest edge-validation path all route through it so their notions of\n * \"same target\" cannot drift.\n * @public\n */\nexport function edgeTargetKey(target: IEdgeTarget): string {\n return `${target.scope}\\0${target.id}`;\n}\n\n/**\n * An attributed link between two records. Carries the relation type, the\n * scope-qualified {@link IEdgeTarget | target}, and optional confidence /\n * provenance / world-truth validity. Replaces bare string references (e.g.\n * PersonAIlity's `IMtmRef` becomes an `IEdge` with `type: LinkType('mtm-ref')`).\n * @public\n */\nexport interface IEdge {\n /** Open-vocabulary relation type. */\n readonly type: LinkType;\n /** The scope-qualified address of the linked-to record. */\n readonly target: IEdgeTarget;\n /** Optional confidence in `[0, 1]`. */\n readonly confidence?: number;\n /** Optional structured provenance for the link itself. */\n readonly provenance?: IProvenance;\n /** World-truth validity start (epoch ms). Present only on temporal edges. */\n readonly valid_at?: number;\n /**\n * World-truth validity end (epoch ms). `null` = still valid; absent = no\n * temporal extent.\n */\n // eslint-disable-next-line @rushstack/no-new-null -- null is a meaningful value here (still-valid) distinct from absent (no temporal extent); design-lock §2.3\n readonly invalid_at?: number | null;\n}\n\n/**\n * Optional bi-temporal validity block on an envelope. Present only on\n * temporal kinds; absent = zero cost for atemporal kinds.\n * @public\n */\nexport interface ITemporalBlock {\n /** World-truth validity start (epoch ms). */\n readonly valid_at?: number;\n /** World-truth validity end (epoch ms). `null` = still valid. */\n // eslint-disable-next-line @rushstack/no-new-null -- null is a meaningful value here (still-valid) distinct from absent; design-lock §2.4\n readonly invalid_at?: number | null;\n}\n\n/**\n * The invariant identity + transaction-time envelope carried by every memory\n * record, independent of the per-kind body.\n * @public\n */\nexport interface IMemoryEnvelope {\n // --- Core identity ---\n /** Stable file-stem identifier. MUST equal the on-disk filename stem. */\n readonly id: MemoryId;\n /** Consumer-supplied domain key. Equals {@link IMemoryEnvelope.id | id} for non-temporal kinds. */\n readonly entityId: EntityId;\n /** Consumer-registered kind; dispatches the body Converter. */\n readonly kind: Kind;\n /** Open-vocabulary tags. */\n readonly tags: ReadonlyArray<Tag>;\n /** Attributed outbound edges. */\n readonly links: ReadonlyArray<IEdge>;\n\n // --- Transaction-time metadata (always present) ---\n /** Epoch ms of the first write. Immutable after creation. */\n readonly created: number;\n /** Epoch ms of the most recent write. */\n readonly updated: number;\n /**\n * Monotonic write counter within the store instance, assigned by the store\n * on every successful put. Enables stable cursor paging over observation\n * records without a full walk.\n */\n readonly seq: number;\n /**\n * Content hash over the canonical `{ kind, body, links }`. The dedup key:\n * an exact match is a no-op upsert that returns the existing record.\n */\n readonly contentHash: string;\n /**\n * Store-computed host-defined ordering value, produced by the kind's\n * {@link RankProjector} on every put/update and stamped into the envelope in\n * the same pass that recomputes {@link IMemoryEnvelope.contentHash | contentHash}.\n * Absent when the kind has no registered projector (or the projector threw on\n * this record). Ordered retrieval (`orderBy: 'rank'`) and the index's rank view\n * sort by this value descending, placing records with an absent `rank` last.\n */\n readonly rank?: number;\n /** Structured provenance (never a flat enum). */\n readonly provenance: IProvenance;\n\n // --- Optional temporal block ---\n /** Bi-temporal validity. Present only on temporal kinds. */\n readonly temporal?: ITemporalBlock;\n\n /**\n * Vector-index entry reference, set by the vector index on write. `null` =\n * not embedded; absent = same as `null` (backwards-compat seam).\n */\n // eslint-disable-next-line @rushstack/no-new-null -- null is the explicit \"not embedded\" sentinel distinct from absent (backwards-compat seam); design-lock §2.5\n readonly embeddingRef?: string | null;\n}\n\n/**\n * A complete memory record: the invariant {@link IMemoryEnvelope} plus the\n * typed, per-kind body. The store's public surface uses\n * `IMemoryRecord<unknown>`; consumers narrow `TBody` by checking\n * `envelope.kind` and validating through the registered Converter.\n * @public\n */\nexport interface IMemoryRecord<TBody = unknown> {\n /** The invariant identity + transaction-time envelope. */\n readonly envelope: IMemoryEnvelope;\n /** The per-kind, Converter-validated body. */\n readonly body: TBody;\n}\n\n/**\n * A per-kind host projection from a fully-resolved (post-merge) memory record\n * to a numeric ordering value. Registered per kind at store construction (see\n * `rankProjectors`); the store runs it on every put/update over the same\n * resolved record whose `contentHash` it computes, stamping the result into\n * {@link IMemoryEnvelope.rank}. The store never interprets the body — the host\n * owns what the number means. A projector that throws is treated as \"no rank\n * for this record\" (logged at `warn`), never failing the write.\n * @public\n */\nexport type RankProjector = (record: IMemoryRecord<unknown>) => number;\n"]}
|
|
1
|
+
{"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../../src/packlets/types/envelope.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAyDH;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,MAAmB;IAC/C,OAAO,GAAG,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC;AACzC,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { EntityId, Kind, LinkType, MemoryId, MemoryScopeKey, Tag } from './ids';\n\n/**\n * Origin of a provenance attribution. Open vocabulary: the three named\n * sources are conventional, but the `(string & {})` arm admits any other\n * source string without resignature.\n * @public\n */\nexport type ProvenanceSource = 'agent' | 'host-ingest' | 'human' | (string & {});\n\n/**\n * Structured provenance for a record or an edge. Never a flat enum — the\n * `[key: string]: unknown` index signature lets a consumer attach an opaque\n * domain payload (e.g. PersonAIlity's sentiment / epistemic blocks) without\n * changing this interface, while still satisfying the no-`any` rule.\n * @public\n */\nexport interface IProvenance {\n /** Where the attribution came from. */\n readonly source: ProvenanceSource;\n /** Optional human or agent identifier responsible for the write. */\n readonly by?: string;\n /** Optional model identifier, when a model produced the content. */\n readonly model?: string;\n /** Optional confidence in `[0, 1]`. */\n readonly confidence?: number;\n /**\n * Scope-qualified back-link to the source record. Enables the cross-kind\n * provenance spine. A scope-qualified {@link IEdgeTarget} (not a bare\n * {@link MemoryId}) because per-scope codecs (e.g. the MTM codec's `turn-<n>`\n * stems) legally reuse a stem across scopes, so a bare id would be ambiguous —\n * the same reason {@link IEdge.target} is scope-qualified.\n */\n readonly derivedFrom?: IEdgeTarget;\n /** Opaque extension payload — consumer-owned, never interpreted by the store. */\n readonly [key: string]: unknown;\n}\n\n/**\n * The physical address of a linked-to record: the `(scope, id)` pair that\n * uniquely identifies it. Both components are required because a bare\n * {@link MemoryId} is NOT unique across scopes — per-scope codecs (e.g. the\n * medium-term codec's `turn-<n>` stems) legally mint the same stem under\n * different scopes, so an edge that carried only the id would be ambiguous.\n * `(scope, id)` matches the store's `getById(scope, id)` addressing and the\n * index's composite primary key.\n * @public\n */\nexport interface IEdgeTarget {\n /** The scope the target record lives under. */\n readonly scope: MemoryScopeKey;\n /** The target record's stable file-stem id (unique WITHIN {@link IEdgeTarget.scope}). */\n readonly id: MemoryId;\n}\n\n/**\n * The canonical composite-key string for an {@link IEdgeTarget}: scope + id,\n * NUL-separated. NUL is excluded from both components (scope segments are\n * filename-safe; {@link MemoryId} is portable-filename-safe), so it is a\n * collision-proof separator. This is the ONE canonicalization every consumer\n * that keys on a scoped target uses — the backlink index, the cycle guard, and\n * the ingest edge-validation path all route through it so their notions of\n * \"same target\" cannot drift.\n * @public\n */\nexport function edgeTargetKey(target: IEdgeTarget): string {\n return `${target.scope}\\0${target.id}`;\n}\n\n/**\n * An attributed link between two records. Carries the relation type, the\n * scope-qualified {@link IEdgeTarget | target}, and optional confidence /\n * provenance / world-truth validity. Replaces bare string references (e.g.\n * PersonAIlity's `IMtmRef` becomes an `IEdge` with `type: LinkType('mtm-ref')`).\n * @public\n */\nexport interface IEdge {\n /** Open-vocabulary relation type. */\n readonly type: LinkType;\n /** The scope-qualified address of the linked-to record. */\n readonly target: IEdgeTarget;\n /** Optional confidence in `[0, 1]`. */\n readonly confidence?: number;\n /** Optional structured provenance for the link itself. */\n readonly provenance?: IProvenance;\n /** World-truth validity start (epoch ms). Present only on temporal edges. */\n readonly valid_at?: number;\n /**\n * World-truth validity end (epoch ms). `null` = still valid; absent = no\n * temporal extent.\n */\n // eslint-disable-next-line @rushstack/no-new-null -- null is a meaningful value here (still-valid) distinct from absent (no temporal extent); design-lock §2.3\n readonly invalid_at?: number | null;\n}\n\n/**\n * Optional bi-temporal validity block on an envelope. Present only on\n * temporal kinds; absent = zero cost for atemporal kinds.\n * @public\n */\nexport interface ITemporalBlock {\n /** World-truth validity start (epoch ms). */\n readonly valid_at?: number;\n /** World-truth validity end (epoch ms). `null` = still valid. */\n // eslint-disable-next-line @rushstack/no-new-null -- null is a meaningful value here (still-valid) distinct from absent; design-lock §2.4\n readonly invalid_at?: number | null;\n}\n\n/**\n * The invariant identity + transaction-time envelope carried by every memory\n * record, independent of the per-kind body.\n * @public\n */\nexport interface IMemoryEnvelope {\n // --- Core identity ---\n /** Stable file-stem identifier. MUST equal the on-disk filename stem. */\n readonly id: MemoryId;\n /** Consumer-supplied domain key. Equals {@link IMemoryEnvelope.id | id} for non-temporal kinds. */\n readonly entityId: EntityId;\n /** Consumer-registered kind; dispatches the body Converter. */\n readonly kind: Kind;\n /** Open-vocabulary tags. */\n readonly tags: ReadonlyArray<Tag>;\n /** Attributed outbound edges. */\n readonly links: ReadonlyArray<IEdge>;\n\n // --- Transaction-time metadata (always present) ---\n /** Epoch ms of the first write. Immutable after creation. */\n readonly created: number;\n /** Epoch ms of the most recent write. */\n readonly updated: number;\n /**\n * Monotonic write counter within the store instance, assigned by the store\n * on every successful put. Enables stable cursor paging over observation\n * records without a full walk.\n */\n readonly seq: number;\n /**\n * Content hash over the canonical `{ kind, body, links }`. The dedup key:\n * an exact match is a no-op upsert that returns the existing record.\n */\n readonly contentHash: string;\n /**\n * Store-computed host-defined ordering value, produced by the kind's\n * {@link RankProjector} on every put/update and stamped into the envelope in\n * the same pass that recomputes {@link IMemoryEnvelope.contentHash | contentHash}.\n * Absent when the kind has no registered projector (or the projector threw on\n * this record). Ordered retrieval (`orderBy: 'rank'`) and the index's rank view\n * sort by this value descending, placing records with an absent `rank` last.\n *\n * **The projector runs on the write path only — registering one does not rank\n * records already in the store.** Because absent sorts last, those records land\n * below every subsequently-written one regardless of what the projector would\n * have scored them, so the result is not a partial ordering but one inverted\n * with respect to the projector's intent, with nothing failing to say so. Call\n * `IMemoryStore.reconcileRank` after registering a projector against a\n * populated store.\n */\n readonly rank?: number;\n /** Structured provenance (never a flat enum). */\n readonly provenance: IProvenance;\n\n // --- Optional temporal block ---\n /** Bi-temporal validity. Present only on temporal kinds. */\n readonly temporal?: ITemporalBlock;\n\n /**\n * Vector-index entry reference, set by the vector index on write. `null` =\n * not embedded; absent = same as `null` (backwards-compat seam).\n */\n // eslint-disable-next-line @rushstack/no-new-null -- null is the explicit \"not embedded\" sentinel distinct from absent (backwards-compat seam); design-lock §2.5\n readonly embeddingRef?: string | null;\n}\n\n/**\n * A complete memory record: the invariant {@link IMemoryEnvelope} plus the\n * typed, per-kind body. The store's public surface uses\n * `IMemoryRecord<unknown>`; consumers narrow `TBody` by checking\n * `envelope.kind` and validating through the registered Converter.\n * @public\n */\nexport interface IMemoryRecord<TBody = unknown> {\n /** The invariant identity + transaction-time envelope. */\n readonly envelope: IMemoryEnvelope;\n /** The per-kind, Converter-validated body. */\n readonly body: TBody;\n}\n\n/**\n * A per-kind host projection from a fully-resolved (post-merge) memory record\n * to a numeric ordering value. Registered per kind at store construction (see\n * `rankProjectors`); the store runs it on every put/update over the same\n * resolved record whose `contentHash` it computes, stamping the result into\n * {@link IMemoryEnvelope.rank}. The store never interprets the body — the host\n * owns what the number means. A projector that throws is treated as \"no rank\n * for this record\" (logged at `warn`), never failing the write.\n *\n * Runs on writes only. To apply a newly-registered projector to records that\n * already exist, call `IMemoryStore.reconcileRank`.\n * @public\n */\nexport type RankProjector = (record: IMemoryRecord<unknown>) => number;\n"]}
|
|
@@ -407,6 +407,8 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
|
|
|
407
407
|
* contributes nothing.
|
|
408
408
|
*/
|
|
409
409
|
private static _projectAsOf;
|
|
410
|
+
/** {@inheritDoc IMemoryStore.reconcileRank} */
|
|
411
|
+
reconcileRank(kind: Kind): Promise<Result<number>>;
|
|
410
412
|
/** {@inheritDoc IMemoryStore.put} */
|
|
411
413
|
put(record: IMemoryRecord<unknown>): Promise<Result<IMemoryRecord<unknown>>>;
|
|
412
414
|
/** {@inheritDoc IMemoryStore.delete} */
|
|
@@ -581,6 +583,43 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
|
|
|
581
583
|
*/
|
|
582
584
|
private _isMutableMetadataUnchanged;
|
|
583
585
|
private _contentHash;
|
|
586
|
+
/**
|
|
587
|
+
* The locked body of {@link FileTreeMemoryStore.reconcileRank}.
|
|
588
|
+
*
|
|
589
|
+
* @remarks
|
|
590
|
+
* Re-reads each record's file rather than trusting the in-memory index, for
|
|
591
|
+
* two reasons: the index holds converted bodies on some paths and raw ones on
|
|
592
|
+
* others, and re-serializing a *converted* body could change the bytes on disk
|
|
593
|
+
* — a reconcile of an ordering field has no business rewriting content.
|
|
594
|
+
* `splitFrontmatter` hands back the body text unconverted, so the round trip
|
|
595
|
+
* carries the authored characters through untouched and only the envelope
|
|
596
|
+
* moves. The parsed record is additionally put through `_verifyLoaded`, the
|
|
597
|
+
* same id-vs-filename and scope-derived-entityId check the load paths apply —
|
|
598
|
+
* without it, reconcile would be the one path that accepts and rewrites a file
|
|
599
|
+
* the store would refuse to load.
|
|
600
|
+
*
|
|
601
|
+
* "Untouched" is not quite "byte-identical", and the exception is line
|
|
602
|
+
* endings: `splitFrontmatter` strips a trailing `\r` per line and
|
|
603
|
+
* `joinFrontmatter` writes `\n` delimiters, so a CRLF-authored file comes back
|
|
604
|
+
* LF-normalized. That is **the store's behavior on every write path, not
|
|
605
|
+
* something reconcile introduces** — an ordinary `put` normalizes the same way
|
|
606
|
+
* — so reconcile does not rewrite content that a subsequent write would have
|
|
607
|
+
* left alone. The property being claimed here is the narrower and load-bearing
|
|
608
|
+
* one: no body is round-tripped through its registered Converter.
|
|
609
|
+
*
|
|
610
|
+
* The projector is fed an `IMemoryRecord<string>` carrying that raw body,
|
|
611
|
+
* which is exactly the shape {@link FileTreeMemoryStore._stampRank} hands it
|
|
612
|
+
* on the write path — so a projector cannot see one thing on a write and
|
|
613
|
+
* another on a reconcile. `_stampRank` itself is reused verbatim, which also
|
|
614
|
+
* inherits its throw semantics (logged at `warn`, `rank` cleared).
|
|
615
|
+
*/
|
|
616
|
+
private _reconcileRankLocked;
|
|
617
|
+
/**
|
|
618
|
+
* Re-apply the rank projector to one record on disk. Returns whether `rank`
|
|
619
|
+
* actually changed — an unchanged rank writes nothing, so a reconcile over an
|
|
620
|
+
* already-consistent store touches no files.
|
|
621
|
+
*/
|
|
622
|
+
private _restampOne;
|
|
584
623
|
/**
|
|
585
624
|
* Stamp the store-computed {@link IMemoryEnvelope.rank} onto a fully-built,
|
|
586
625
|
* fully-stamped record by running the kind's registered {@link RankProjector}.
|
|
@@ -1644,6 +1683,14 @@ export declare interface IMemoryEnvelope {
|
|
|
1644
1683
|
* Absent when the kind has no registered projector (or the projector threw on
|
|
1645
1684
|
* this record). Ordered retrieval (`orderBy: 'rank'`) and the index's rank view
|
|
1646
1685
|
* sort by this value descending, placing records with an absent `rank` last.
|
|
1686
|
+
*
|
|
1687
|
+
* **The projector runs on the write path only — registering one does not rank
|
|
1688
|
+
* records already in the store.** Because absent sorts last, those records land
|
|
1689
|
+
* below every subsequently-written one regardless of what the projector would
|
|
1690
|
+
* have scored them, so the result is not a partial ordering but one inverted
|
|
1691
|
+
* with respect to the projector's intent, with nothing failing to say so. Call
|
|
1692
|
+
* `IMemoryStore.reconcileRank` after registering a projector against a
|
|
1693
|
+
* populated store.
|
|
1647
1694
|
*/
|
|
1648
1695
|
readonly rank?: number;
|
|
1649
1696
|
/** Structured provenance (never a flat enum). */
|
|
@@ -1916,6 +1963,23 @@ export declare interface IMemoryQuery {
|
|
|
1916
1963
|
readonly scope?: MemoryScopeKey;
|
|
1917
1964
|
/** Restrict to records carrying this tag (exact match). */
|
|
1918
1965
|
readonly tag?: Tag;
|
|
1966
|
+
/**
|
|
1967
|
+
* Restrict to records whose {@link IProvenance.source | provenance.source} is
|
|
1968
|
+
* exactly this value — the "show me everything this source produced" axis, for
|
|
1969
|
+
* review, attribution, and retraction after a bad ingest.
|
|
1970
|
+
*
|
|
1971
|
+
* @remarks
|
|
1972
|
+
* Exact match on `source` only, deliberately: not a subset match over the whole
|
|
1973
|
+
* {@link IProvenance} block, not a presence check, and not a match on `by` /
|
|
1974
|
+
* `model` / the consumer-owned extension keys. `source` is the only field of
|
|
1975
|
+
* that block the library assigns meaning to, and every record carries one
|
|
1976
|
+
* (`envelope.provenance` is required), so this axis is total — it partitions
|
|
1977
|
+
* the vault rather than filtering out records that merely omit a field.
|
|
1978
|
+
*
|
|
1979
|
+
* Applied in the shared pre-filter alongside `scope` / `kind` / `tag`, so every
|
|
1980
|
+
* retriever honours it and combining axes composes as AND.
|
|
1981
|
+
*/
|
|
1982
|
+
readonly provenanceSource?: ProvenanceSource;
|
|
1919
1983
|
/**
|
|
1920
1984
|
* Restrict to records of this kind — the single-kind shorthand for
|
|
1921
1985
|
* {@link IMemoryQuery.kinds | kinds}. When both are set they compose as AND
|
|
@@ -1976,7 +2040,7 @@ export declare interface IMemoryQuery {
|
|
|
1976
2040
|
* empty page, never a throw.
|
|
1977
2041
|
*/
|
|
1978
2042
|
readonly offset?: number;
|
|
1979
|
-
/** Arbitrary predicate applied after the scope / kind / tag pre-filter. */
|
|
2043
|
+
/** Arbitrary predicate applied after the scope / kind / tag / provenance-source pre-filter. */
|
|
1980
2044
|
readonly filter?: (record: IMemoryRecord<unknown>) => boolean;
|
|
1981
2045
|
}
|
|
1982
2046
|
|
|
@@ -2144,6 +2208,36 @@ export declare interface IMemoryStore {
|
|
|
2144
2208
|
* existing record unchanged on a dedup no-op.
|
|
2145
2209
|
*/
|
|
2146
2210
|
put(record: IMemoryRecord<unknown>): Promise<Result<IMemoryRecord<unknown>>>;
|
|
2211
|
+
/**
|
|
2212
|
+
* Re-apply the kind's {@link RankProjector} to every record of `kind` already
|
|
2213
|
+
* in the store, restamping {@link IMemoryEnvelope.rank}. Returns the number of
|
|
2214
|
+
* records whose `rank` actually changed.
|
|
2215
|
+
*
|
|
2216
|
+
* @remarks
|
|
2217
|
+
* **This exists because `rank` is otherwise new-store-only, and fails in a way
|
|
2218
|
+
* that looks like it works.** The projector runs on the write path only, so
|
|
2219
|
+
* registering one against a populated store ranks nothing already written —
|
|
2220
|
+
* and because an absent `rank` sorts *last*, every pre-registration record
|
|
2221
|
+
* lands below every post-registration one no matter what the projector would
|
|
2222
|
+
* have scored it. The ordering is not merely partial; it is **inverted with
|
|
2223
|
+
* respect to the projector's own intent**, with no failure anywhere to say so.
|
|
2224
|
+
*
|
|
2225
|
+
* Deliberately **does not** touch `created` / `updated` / `seq`, and fires no
|
|
2226
|
+
* `'write'` observation. Routing a reconcile through {@link IMemoryStore.put}
|
|
2227
|
+
* would bump transaction time on every record — trading a wrong `rank` order
|
|
2228
|
+
* for a wrong recency order, and flooding any wired observer with writes that
|
|
2229
|
+
* are not writes. The body is re-serialized verbatim from the file's own
|
|
2230
|
+
* bytes; only the envelope's `rank` moves.
|
|
2231
|
+
*
|
|
2232
|
+
* Fails loudly if `kind` has no registered projector: asking to reconcile a
|
|
2233
|
+
* kind you never configured is a caller error, not a no-op.
|
|
2234
|
+
*
|
|
2235
|
+
* **Not atomic, and safe for it.** A failure part-way leaves earlier records
|
|
2236
|
+
* restamped. That is benign because restamping is idempotent — re-running
|
|
2237
|
+
* converges — which is also why no report shape is offered here. A count is
|
|
2238
|
+
* enough.
|
|
2239
|
+
*/
|
|
2240
|
+
reconcileRank(kind: Kind): Promise<Result<number>>;
|
|
2147
2241
|
/**
|
|
2148
2242
|
* Delete a record by `(kind, entityId)`. Non-temporal kinds physically delete
|
|
2149
2243
|
* the file and return the deleted record's {@link MemoryId}. Temporal
|
|
@@ -2223,9 +2317,10 @@ export declare interface IMergeStrategy {
|
|
|
2223
2317
|
}
|
|
2224
2318
|
|
|
2225
2319
|
/**
|
|
2226
|
-
* Whether an indexed entry satisfies a query's scope / kind / tag /
|
|
2227
|
-
* pre-filter (the axes shared by every v1
|
|
2228
|
-
* link axes are NOT applied here — those
|
|
2320
|
+
* Whether an indexed entry satisfies a query's scope / kind / tag /
|
|
2321
|
+
* provenance-source / predicate pre-filter (the axes shared by every v1
|
|
2322
|
+
* retriever). The `semantic` / `asOf` / link axes are NOT applied here — those
|
|
2323
|
+
* are each retriever's own concern.
|
|
2229
2324
|
* @public
|
|
2230
2325
|
*/
|
|
2231
2326
|
export declare function indexedRecordMatchesQuery(entry: IIndexedMemoryRecord, query: IMemoryQuery): boolean;
|
|
@@ -3029,7 +3124,7 @@ export declare const LINK_TRAVERSAL_UNWIRED_MESSAGE: string;
|
|
|
3029
3124
|
* is the exact, collision-free cycle key — no structural hashing (e.g.
|
|
3030
3125
|
* `Crc32Normalizer`) is needed. A self-loop or any multi-hop cycle terminates
|
|
3031
3126
|
* because a revisited node is never re-expanded.
|
|
3032
|
-
* - **Post-filter.** The scope / kind / tag / predicate axes of the query are
|
|
3127
|
+
* - **Post-filter.** The scope / kind / tag / provenance-source / predicate axes of the query are
|
|
3033
3128
|
* applied to the reached records (the link axes are the traversal itself).
|
|
3034
3129
|
* @public
|
|
3035
3130
|
*/
|
|
@@ -3767,6 +3862,9 @@ export declare function rankCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<
|
|
|
3767
3862
|
* {@link IMemoryEnvelope.rank}. The store never interprets the body — the host
|
|
3768
3863
|
* owns what the number means. A projector that throws is treated as "no rank
|
|
3769
3864
|
* for this record" (logged at `warn`), never failing the write.
|
|
3865
|
+
*
|
|
3866
|
+
* Runs on writes only. To apply a newly-registered projector to records that
|
|
3867
|
+
* already exist, call `IMemoryStore.reconcileRank`.
|
|
3770
3868
|
* @public
|
|
3771
3869
|
*/
|
|
3772
3870
|
export declare type RankProjector = (record: IMemoryRecord<unknown>) => number;
|
|
@@ -3779,7 +3877,7 @@ export declare type RankProjector = (record: IMemoryRecord<unknown>) => number;
|
|
|
3779
3877
|
export declare function recencyCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number;
|
|
3780
3878
|
|
|
3781
3879
|
/**
|
|
3782
|
-
* Returns records matching the query's scope / kind / tag / predicate filters,
|
|
3880
|
+
* Returns records matching the query's scope / kind / tag / provenance-source / predicate filters,
|
|
3783
3881
|
* ordered most-recently-updated first. The universal v1 retriever — an empty
|
|
3784
3882
|
* query returns the whole vault in recency order.
|
|
3785
3883
|
* @public
|
|
@@ -3837,8 +3935,9 @@ export declare class ScoreUnionMergeStrategy implements IMergeStrategy {
|
|
|
3837
3935
|
}
|
|
3838
3936
|
|
|
3839
3937
|
/**
|
|
3840
|
-
* Apply the shared scope / kind / tag / predicate pre-filter
|
|
3841
|
-
* entries, returning the surviving records (unordered,
|
|
3938
|
+
* Apply the shared scope / kind / tag / provenance-source / predicate pre-filter
|
|
3939
|
+
* to a set of indexed entries, returning the surviving records (unordered,
|
|
3940
|
+
* unlimited).
|
|
3842
3941
|
* @public
|
|
3843
3942
|
*/
|
|
3844
3943
|
export declare function selectByQuery(entries: ReadonlyArray<IIndexedMemoryRecord>, query: IMemoryQuery): IMemoryRecord<unknown>[];
|
|
@@ -3921,10 +4020,32 @@ export declare function splitFrontmatter(raw: string): Result<IMemoryFileParts>;
|
|
|
3921
4020
|
export declare type StoreStampedEnvelopeField = 'id' | 'seq' | 'contentHash' | 'created' | 'updated';
|
|
3922
4021
|
|
|
3923
4022
|
/**
|
|
3924
|
-
* Returns records matching `query.filter`, narrowed
|
|
3925
|
-
* pre-filter and recency-ordered.
|
|
3926
|
-
*
|
|
3927
|
-
*
|
|
4023
|
+
* Returns records matching `query.filter` or `query.provenanceSource`, narrowed
|
|
4024
|
+
* by any scope / kind / tag / provenance-source pre-filter and recency-ordered.
|
|
4025
|
+
* Those two are this retriever's axes: a query carrying neither is not its
|
|
4026
|
+
* concern and yields an empty success (so it contributes nothing to a
|
|
4027
|
+
* {@link HybridRetriever}, rather than failing).
|
|
4028
|
+
*
|
|
4029
|
+
* Note `provenanceSource` appears on both sides of that sentence, and the
|
|
4030
|
+
* duplication is real rather than sloppy: it is one of this retriever's two
|
|
4031
|
+
* *dispatch* axes (it decides whether the query is this retriever's concern at
|
|
4032
|
+
* all) **and** a member of the shared *pre-filter* (it narrows the result set,
|
|
4033
|
+
* as it does for every other retriever). `filter` is likewise both.
|
|
4034
|
+
*
|
|
4035
|
+
* @remarks
|
|
4036
|
+
* `provenanceSource` is *applied* by the shared pre-filter, so every retriever
|
|
4037
|
+
* narrows by it. What this retriever adds is *answering* a query whose only axis
|
|
4038
|
+
* is `provenanceSource` — the "show me everything this source produced" request,
|
|
4039
|
+
* which would otherwise fall through the `filter`-absent guard and come back
|
|
4040
|
+
* empty.
|
|
4041
|
+
*
|
|
4042
|
+
* Consequently, inside a {@link HybridRetriever} composed with the universal
|
|
4043
|
+
* {@link RecencyRetriever}, a `provenanceSource`-only query is answered by both
|
|
4044
|
+
* children and every matching record scores twice under a score-union merge.
|
|
4045
|
+
* That is the established behavior for a dedicated-axis retriever composed with
|
|
4046
|
+
* the universal one — {@link TagRetriever} double-scores a `tag`-only query the
|
|
4047
|
+
* same way — and is intentional here, not an artifact of grafting a second axis
|
|
4048
|
+
* onto a retriever whose original concern was arbitrary predicates.
|
|
3928
4049
|
* @public
|
|
3929
4050
|
*/
|
|
3930
4051
|
export declare class StructuredFilterRetriever implements IMemoryRetriever {
|
|
@@ -3946,7 +4067,7 @@ export declare type Tag = Brand<string, 'Tag'>;
|
|
|
3946
4067
|
|
|
3947
4068
|
/**
|
|
3948
4069
|
* Returns records carrying `query.tag`, recency-ordered within the tag and
|
|
3949
|
-
* narrowed by any scope / kind / predicate filters. Tag is this retriever's
|
|
4070
|
+
* narrowed by any scope / kind / provenance-source / predicate filters. Tag is this retriever's
|
|
3950
4071
|
* axis: a query without a `tag` is not its concern and yields an empty success
|
|
3951
4072
|
* (so it contributes nothing to a {@link HybridRetriever}, rather than failing).
|
|
3952
4073
|
* @public
|
|
@@ -26,7 +26,7 @@ export declare const LINK_TRAVERSAL_NO_SEED_MESSAGE: string;
|
|
|
26
26
|
* is the exact, collision-free cycle key — no structural hashing (e.g.
|
|
27
27
|
* `Crc32Normalizer`) is needed. A self-loop or any multi-hop cycle terminates
|
|
28
28
|
* because a revisited node is never re-expanded.
|
|
29
|
-
* - **Post-filter.** The scope / kind / tag / predicate axes of the query are
|
|
29
|
+
* - **Post-filter.** The scope / kind / tag / provenance-source / predicate axes of the query are
|
|
30
30
|
* applied to the reached records (the link axes are the traversal itself).
|
|
31
31
|
* @public
|
|
32
32
|
*/
|
|
@@ -40,7 +40,7 @@ exports.LINK_TRAVERSAL_NO_SEED_MESSAGE = 'link traversal requires a seed id (lin
|
|
|
40
40
|
* is the exact, collision-free cycle key — no structural hashing (e.g.
|
|
41
41
|
* `Crc32Normalizer`) is needed. A self-loop or any multi-hop cycle terminates
|
|
42
42
|
* because a revisited node is never re-expanded.
|
|
43
|
-
* - **Post-filter.** The scope / kind / tag / predicate axes of the query are
|
|
43
|
+
* - **Post-filter.** The scope / kind / tag / provenance-source / predicate axes of the query are
|
|
44
44
|
* applied to the reached records (the link axes are the traversal itself).
|
|
45
45
|
* @public
|
|
46
46
|
*/
|
|
@@ -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"]}
|
|
@@ -3,7 +3,7 @@ import { IMemoryRecord } from '../types';
|
|
|
3
3
|
import { IMemoryIndex } from '../index';
|
|
4
4
|
import { IMemoryQuery, IMemoryRetriever, IMemoryRetrieverCapabilities } from './retriever';
|
|
5
5
|
/**
|
|
6
|
-
* Returns records matching the query's scope / kind / tag / predicate filters,
|
|
6
|
+
* Returns records matching the query's scope / kind / tag / provenance-source / predicate filters,
|
|
7
7
|
* ordered most-recently-updated first. The universal v1 retriever — an empty
|
|
8
8
|
* query returns the whole vault in recency order.
|
|
9
9
|
* @public
|
|
@@ -8,7 +8,7 @@ exports.RecencyRetriever = void 0;
|
|
|
8
8
|
const ts_utils_1 = require("@fgv/ts-utils");
|
|
9
9
|
const retriever_1 = require("./retriever");
|
|
10
10
|
/**
|
|
11
|
-
* Returns records matching the query's scope / kind / tag / predicate filters,
|
|
11
|
+
* Returns records matching the query's scope / kind / tag / provenance-source / predicate filters,
|
|
12
12
|
* ordered most-recently-updated first. The universal v1 retriever — an empty
|
|
13
13
|
* query returns the whole vault in recency order.
|
|
14
14
|
* @public
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recencyRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/recencyRetriever.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,4CAAgD;AAGhD,2CASqB;AAErB;;;;;GAKG;AACH,MAAa,gBAAgB;IAG3B,YAAoB,KAAmB;QACrC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,qCAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,KAAmB;QACtC,OAAO,IAAA,kBAAO,EAAC,IAAI,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,IAAA,sCAA0B,EAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,MAAM,OAAO,GAA6B,IAAA,yBAAa,EAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,IAAI,CACxF,IAAA,2BAAe,EAAC,KAAK,CAAC,OAAO,CAAC,CAC/B,CAAC;YACF,OAAO,IAAA,kBAAO,EAAC,IAAA,wBAAY,EAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACnE,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF;AA5BD,4CA4BC","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,4CAAgD;AAGhD,2CASqB;AAErB;;;;;GAKG;AACH,MAAa,gBAAgB;IAG3B,YAAoB,KAAmB;QACrC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,qCAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,KAAmB;QACtC,OAAO,IAAA,kBAAO,EAAC,IAAI,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,IAAA,sCAA0B,EAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,MAAM,OAAO,GAA6B,IAAA,yBAAa,EAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,IAAI,CACxF,IAAA,2BAAe,EAAC,KAAK,CAAC,OAAO,CAAC,CAC/B,CAAC;YACF,OAAO,IAAA,kBAAO,EAAC,IAAA,wBAAY,EAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACnE,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF;AA5BD,4CA4BC","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"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Result } from '@fgv/ts-utils';
|
|
2
|
-
import { IEdgeTarget, IMemoryRecord, Kind, MemoryScopeKey, Tag } from '../types';
|
|
2
|
+
import { IEdgeTarget, IMemoryRecord, Kind, MemoryScopeKey, ProvenanceSource, Tag } from '../types';
|
|
3
3
|
import { IIndexedMemoryRecord } from '../index';
|
|
4
4
|
/**
|
|
5
5
|
* The capabilities a retriever exposes. A consumer probes these before
|
|
@@ -27,6 +27,23 @@ export interface IMemoryQuery {
|
|
|
27
27
|
readonly scope?: MemoryScopeKey;
|
|
28
28
|
/** Restrict to records carrying this tag (exact match). */
|
|
29
29
|
readonly tag?: Tag;
|
|
30
|
+
/**
|
|
31
|
+
* Restrict to records whose {@link IProvenance.source | provenance.source} is
|
|
32
|
+
* exactly this value — the "show me everything this source produced" axis, for
|
|
33
|
+
* review, attribution, and retraction after a bad ingest.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* Exact match on `source` only, deliberately: not a subset match over the whole
|
|
37
|
+
* {@link IProvenance} block, not a presence check, and not a match on `by` /
|
|
38
|
+
* `model` / the consumer-owned extension keys. `source` is the only field of
|
|
39
|
+
* that block the library assigns meaning to, and every record carries one
|
|
40
|
+
* (`envelope.provenance` is required), so this axis is total — it partitions
|
|
41
|
+
* the vault rather than filtering out records that merely omit a field.
|
|
42
|
+
*
|
|
43
|
+
* Applied in the shared pre-filter alongside `scope` / `kind` / `tag`, so every
|
|
44
|
+
* retriever honours it and combining axes composes as AND.
|
|
45
|
+
*/
|
|
46
|
+
readonly provenanceSource?: ProvenanceSource;
|
|
30
47
|
/**
|
|
31
48
|
* Restrict to records of this kind — the single-kind shorthand for
|
|
32
49
|
* {@link IMemoryQuery.kinds | kinds}. When both are set they compose as AND
|
|
@@ -87,7 +104,7 @@ export interface IMemoryQuery {
|
|
|
87
104
|
* empty page, never a throw.
|
|
88
105
|
*/
|
|
89
106
|
readonly offset?: number;
|
|
90
|
-
/** Arbitrary predicate applied after the scope / kind / tag pre-filter. */
|
|
107
|
+
/** Arbitrary predicate applied after the scope / kind / tag / provenance-source pre-filter. */
|
|
91
108
|
readonly filter?: (record: IMemoryRecord<unknown>) => boolean;
|
|
92
109
|
}
|
|
93
110
|
/**
|
|
@@ -161,15 +178,17 @@ export declare function rankCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<
|
|
|
161
178
|
*/
|
|
162
179
|
export declare function orderingCompare(orderBy?: IMemoryQuery['orderBy']): (a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>) => number;
|
|
163
180
|
/**
|
|
164
|
-
* Whether an indexed entry satisfies a query's scope / kind / tag /
|
|
165
|
-
* pre-filter (the axes shared by every v1
|
|
166
|
-
* link axes are NOT applied here — those
|
|
181
|
+
* Whether an indexed entry satisfies a query's scope / kind / tag /
|
|
182
|
+
* provenance-source / predicate pre-filter (the axes shared by every v1
|
|
183
|
+
* retriever). The `semantic` / `asOf` / link axes are NOT applied here — those
|
|
184
|
+
* are each retriever's own concern.
|
|
167
185
|
* @public
|
|
168
186
|
*/
|
|
169
187
|
export declare function indexedRecordMatchesQuery(entry: IIndexedMemoryRecord, query: IMemoryQuery): boolean;
|
|
170
188
|
/**
|
|
171
|
-
* Apply the shared scope / kind / tag / predicate pre-filter
|
|
172
|
-
* entries, returning the surviving records (unordered,
|
|
189
|
+
* Apply the shared scope / kind / tag / provenance-source / predicate pre-filter
|
|
190
|
+
* to a set of indexed entries, returning the surviving records (unordered,
|
|
191
|
+
* unlimited).
|
|
173
192
|
* @public
|
|
174
193
|
*/
|
|
175
194
|
export declare function selectByQuery(entries: ReadonlyArray<IIndexedMemoryRecord>, query: IMemoryQuery): IMemoryRecord<unknown>[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/retriever.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAiB,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"retriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/retriever.ts"],"names":[],"mappings":"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"}
|
|
@@ -104,9 +104,10 @@ function orderingCompare(orderBy) {
|
|
|
104
104
|
return orderBy === 'rank' ? rankCompare : recencyCompare;
|
|
105
105
|
}
|
|
106
106
|
/**
|
|
107
|
-
* Whether an indexed entry satisfies a query's scope / kind / tag /
|
|
108
|
-
* pre-filter (the axes shared by every v1
|
|
109
|
-
* link axes are NOT applied here — those
|
|
107
|
+
* Whether an indexed entry satisfies a query's scope / kind / tag /
|
|
108
|
+
* provenance-source / predicate pre-filter (the axes shared by every v1
|
|
109
|
+
* retriever). The `semantic` / `asOf` / link axes are NOT applied here — those
|
|
110
|
+
* are each retriever's own concern.
|
|
110
111
|
* @public
|
|
111
112
|
*/
|
|
112
113
|
function indexedRecordMatchesQuery(entry, query) {
|
|
@@ -122,14 +123,19 @@ function indexedRecordMatchesQuery(entry, query) {
|
|
|
122
123
|
if (query.tag !== undefined && !entry.record.envelope.tags.includes(query.tag)) {
|
|
123
124
|
return false;
|
|
124
125
|
}
|
|
126
|
+
if (query.provenanceSource !== undefined &&
|
|
127
|
+
entry.record.envelope.provenance.source !== query.provenanceSource) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
125
130
|
if (query.filter !== undefined && !query.filter(entry.record)) {
|
|
126
131
|
return false;
|
|
127
132
|
}
|
|
128
133
|
return true;
|
|
129
134
|
}
|
|
130
135
|
/**
|
|
131
|
-
* Apply the shared scope / kind / tag / predicate pre-filter
|
|
132
|
-
* entries, returning the surviving records (unordered,
|
|
136
|
+
* Apply the shared scope / kind / tag / provenance-source / predicate pre-filter
|
|
137
|
+
* to a set of indexed entries, returning the surviving records (unordered,
|
|
138
|
+
* unlimited).
|
|
133
139
|
* @public
|
|
134
140
|
*/
|
|
135
141
|
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"]}
|