@fgv/ts-agent-memory 5.1.0-50 → 5.1.0-51

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.
Files changed (46) hide show
  1. package/dist/packlets/retrieve/fragmentSemanticRetriever.js +94 -5
  2. package/dist/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
  3. package/dist/packlets/store/fileTreeMemoryStore.js +13 -24
  4. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  5. package/dist/packlets/store/memoryStore.js.map +1 -1
  6. package/dist/packlets/store/storeIdentity.js +62 -0
  7. package/dist/packlets/store/storeIdentity.js.map +1 -0
  8. package/dist/packlets/types/identityResolver.js +6 -0
  9. package/dist/packlets/types/identityResolver.js.map +1 -0
  10. package/dist/packlets/types/index.js +1 -0
  11. package/dist/packlets/types/index.js.map +1 -1
  12. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +42 -2
  13. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  14. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  15. package/dist/ts-agent-memory.d.ts +192 -9
  16. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts +79 -1
  17. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts.map +1 -1
  18. package/lib/packlets/retrieve/fragmentSemanticRetriever.js +94 -5
  19. package/lib/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
  20. package/lib/packlets/store/fileTreeMemoryStore.d.ts +4 -2
  21. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  22. package/lib/packlets/store/fileTreeMemoryStore.js +13 -24
  23. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  24. package/lib/packlets/store/memoryStore.d.ts +2 -2
  25. package/lib/packlets/store/memoryStore.d.ts.map +1 -1
  26. package/lib/packlets/store/memoryStore.js.map +1 -1
  27. package/lib/packlets/store/storeIdentity.d.ts +38 -0
  28. package/lib/packlets/store/storeIdentity.d.ts.map +1 -0
  29. package/lib/packlets/store/storeIdentity.js +67 -0
  30. package/lib/packlets/store/storeIdentity.js.map +1 -0
  31. package/lib/packlets/types/identityResolver.d.ts +42 -0
  32. package/lib/packlets/types/identityResolver.d.ts.map +1 -0
  33. package/lib/packlets/types/identityResolver.js +7 -0
  34. package/lib/packlets/types/identityResolver.js.map +1 -0
  35. package/lib/packlets/types/index.d.ts +1 -0
  36. package/lib/packlets/types/index.d.ts.map +1 -1
  37. package/lib/packlets/types/index.js +1 -0
  38. package/lib/packlets/types/index.js.map +1 -1
  39. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +13 -2
  40. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
  41. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +42 -2
  42. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  43. package/lib/packlets/vector/vectorIndex.d.ts +57 -6
  44. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  45. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  46. package/package.json +7 -7
@@ -445,6 +445,9 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
445
445
  listScoped(): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>>;
446
446
  /** {@inheritDoc IMemoryRecordResolver.resolveRecord} */
447
447
  resolveRecord(scope: MemoryScopeKey, id: MemoryId): Result<IMemoryRecord<unknown> | undefined>;
448
+ private _codec;
449
+ /** {@inheritDoc IIdentityResolver.resolveIdentity} */
450
+ resolveIdentity(kind: Kind, entityId: EntityId): Result<IIdentityCodecResult>;
448
451
  /**
449
452
  * Materialize a selected set of entries into records, dropping any that have
450
453
  * vanished since selection.
@@ -717,7 +720,6 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
717
720
  * than preserving it), so a ranking bug never loses an authoritative write.
718
721
  */
719
722
  private _stampRank;
720
- private _codecFor;
721
723
  private _policyFor;
722
724
  /** {@inheritDoc IMemoryStore.dedupScopeFor} */
723
725
  dedupScopeFor(kind: Kind): DedupScope;
@@ -777,6 +779,28 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
777
779
  private _loadRecordFile;
778
780
  }
779
781
 
782
+ /**
783
+ * The message returned when exactly one of `entityId` / `kind` is supplied.
784
+ *
785
+ * @remarks
786
+ * They travel together because `kind` is what selects the identity codec, and the
787
+ * codec is what makes the resolution unambiguous. One without the other is not a
788
+ * partial narrowing that could be honored best-effort — it is not a narrowing at all.
789
+ * @public
790
+ */
791
+ export declare const FRAGMENT_NARROWING_INCOMPLETE_MESSAGE: string;
792
+
793
+ /**
794
+ * The loud-degradation message returned when a query carries a record narrowing but
795
+ * no {@link IIdentityResolver} is wired to resolve it.
796
+ *
797
+ * @remarks
798
+ * Deliberately a `Failure` rather than a silently-global search: answering a scoped
799
+ * question with an unscoped result is the failure this narrowing exists to remove.
800
+ * @public
801
+ */
802
+ export declare const FRAGMENT_NARROWING_UNRESOLVABLE_MESSAGE: string;
803
+
780
804
  /**
781
805
  * The loud-degradation message a {@link FragmentSemanticRetriever} returns when a
782
806
  * fragment query is issued but no {@link IFragmentSemanticBackend | backend} is
@@ -829,18 +853,44 @@ export declare const fragmentLocatorConverter: Converter<IFragmentLocator>;
829
853
  */
830
854
  export declare class FragmentSemanticRetriever {
831
855
  private readonly _backend;
856
+ private readonly _identityResolver;
832
857
  private constructor();
833
858
  /** What this retriever can do given its wiring. */
834
859
  get capabilities(): IFragmentRetrieverCapabilities;
835
- /** Family-convention factory. */
860
+ /**
861
+ * Family-convention factory.
862
+ *
863
+ * @param params - `backend` wires fragment recall itself. `identityResolver`
864
+ * resolves a query's `(kind, entityId)` narrowing to a storage address;
865
+ * `IMemoryStore` implements it, so the usual wiring is
866
+ * `{ backend, identityResolver: store }`. It is optional because an unscoped
867
+ * fragment search needs nothing to resolve — but a query that *does* carry a
868
+ * narrowing fails loudly without it rather than quietly searching everything.
869
+ */
836
870
  static create(params: {
837
871
  readonly backend?: IFragmentSemanticBackend;
872
+ readonly identityResolver?: IIdentityResolver;
838
873
  }): Result<FragmentSemanticRetriever>;
839
874
  /**
840
875
  * Embed `query.semantic`, query the fragment index, and return the per-fragment
841
876
  * hits in descending score order. Fails loudly when no backend is wired.
842
877
  */
843
878
  retrieve(query: IFragmentQuery): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
879
+ /**
880
+ * Turn the query's consumer-facing narrowing into the storage-address narrowing
881
+ * the index understands.
882
+ *
883
+ * @remarks
884
+ * `kind` selects the identity codec and the codec computes the record's storage
885
+ * address, so this is a deterministic resolution rather than a search —
886
+ * which is what makes a colliding `entityId` across kinds a non-issue.
887
+ *
888
+ * A **versioned** kind resolves to the entity's own subtree scope and deliberately
889
+ * carries no `id`, so the narrowing covers every version of the entity — including
890
+ * superseded ones, which are invalidated but never pruned from the index. A
891
+ * non-versioned kind resolves to exactly one record.
892
+ */
893
+ private _resolveOptions;
844
894
  /**
845
895
  * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`
846
896
  * and a thrown/rejected promise into a single `fragment recall: <label> failed`
@@ -1558,6 +1608,85 @@ export declare interface IFragmentQuery {
1558
1608
  * Applied during selection (before the `topK` cut). Omit for uncapped.
1559
1609
  */
1560
1610
  readonly maxPerRecord?: number;
1611
+ /**
1612
+ * Narrow the search to one record's fragments: the consumer-supplied domain key
1613
+ * of the record to search within. **Must be supplied with
1614
+ * {@link IFragmentQuery.kind}.**
1615
+ *
1616
+ * @remarks
1617
+ * The narrowing is applied **during selection, before the `topK` cut**, so the
1618
+ * `topK` you ask for is the `topK` you get. Filtering a global result afterwards
1619
+ * is not equivalent: it truncates to `topK` across every record first, so a scoped
1620
+ * search would come back short whenever other records outscored this one's
1621
+ * fragments.
1622
+ *
1623
+ * For a versioned kind this narrows to **every version of the entity** — literally
1624
+ * every version, superseded ones included, because invalidation stamps `invalid_at`
1625
+ * without pruning that version's fragments. Nothing on a hit distinguishes a
1626
+ * current fragment from a historical one. That matches the record-granular vector
1627
+ * lane; it is not currency filtering.
1628
+ */
1629
+ readonly entityId?: EntityId;
1630
+ /**
1631
+ * The kind of the record named by {@link IFragmentQuery.entityId}. **Must be
1632
+ * supplied with it.**
1633
+ *
1634
+ * @remarks
1635
+ * This is not decoration and not a filter: `kind` **selects the identity codec**,
1636
+ * and the codec computes the storage address. An `EntityId` promises no uniqueness
1637
+ * beyond a scope — the same id under two kinds is the ordinary case, not a
1638
+ * pathological one — so without `kind` the resolution is ambiguous, and with it
1639
+ * ambiguity is structurally impossible.
1640
+ */
1641
+ readonly kind?: Kind;
1642
+ }
1643
+
1644
+ /**
1645
+ * Selection-time narrowing for {@link IFragmentVectorIndex.query}. Every member is
1646
+ * applied **before** the `topK` cut.
1647
+ *
1648
+ * @remarks
1649
+ * `scope` (optionally with `id`) is the record narrowing. It is expressed as a
1650
+ * storage address rather than as a consumer-facing `(kind, entityId)` because that
1651
+ * is what the index is keyed by; resolving one to the other is
1652
+ * `IIdentityResolver.resolveIdentity`'s job, and doing it above the index keeps the
1653
+ * index dealing only in the keys it actually holds.
1654
+ *
1655
+ * **`scope` alone is not a coarser accident — it is what a versioned kind needs.**
1656
+ * `TemporalIdentityCodec` files every version of an entity in its own per-entity
1657
+ * subtree, so for a versioned kind the entity's subtree *is* the narrowing and
1658
+ * `{ scope }` means "every version of this entity". A non-versioned kind resolves to
1659
+ * a single record and supplies `{ scope, id }`.
1660
+ *
1661
+ * **"Every version" is literal, and includes superseded ones.** Invalidation stamps
1662
+ * `invalid_at` on an envelope; it does not prune that version's fragments from the
1663
+ * index. So a scope-narrowed query returns current and historical fragments alike,
1664
+ * and a hit carries nothing that distinguishes them — `IVectorQueryHit` has no
1665
+ * temporal fields. This is the record-granular vector lane's existing behaviour
1666
+ * rather than something this narrowing introduces, but do not read the subtree
1667
+ * narrowing as currency filtering: it is not, and there is no `asOf` axis here to
1668
+ * make it so.
1669
+ * @public
1670
+ */
1671
+ export declare interface IFragmentQueryOptions {
1672
+ /**
1673
+ * Maximum number of fragments any single record may contribute. Omit for
1674
+ * uncapped. With a single-record narrowing (`scope` + `id`) this caps the one
1675
+ * record's contribution, which is a second cap on the same axis rather than a
1676
+ * cross-record fairness knob — usually you want one or the other, not both.
1677
+ */
1678
+ readonly maxPerRecord?: number;
1679
+ /**
1680
+ * Restrict the search to fragments of records in this scope. Omit to search every
1681
+ * record.
1682
+ */
1683
+ readonly scope?: MemoryScopeKey;
1684
+ /**
1685
+ * With {@link IFragmentQueryOptions.scope}, restrict further to the single record
1686
+ * at `(scope, id)`. Ignored — and meaningless — without `scope`, since a bare `id`
1687
+ * does not address a record.
1688
+ */
1689
+ readonly id?: MemoryId;
1561
1690
  }
1562
1691
 
1563
1692
  /**
@@ -1637,12 +1766,16 @@ export declare interface IFragmentVectorIndex {
1637
1766
  /**
1638
1767
  * Return the `topK` nearest fragments to `vector`, in descending score order,
1639
1768
  * each hit carrying its record `target` plus whichever of `locator` /
1640
- * `fragmentId` the stored fragment was added with. When
1641
- * `maxPerRecord` is supplied, no more than that many fragments of any single
1642
- * record appear in the result — the cap is applied during selection (before the
1643
- * `topK` cut) so one long document cannot crowd out others.
1769
+ * `fragmentId` the stored fragment was added with.
1770
+ *
1771
+ * @remarks
1772
+ * **Every member of `options` is applied during selection, before the `topK`
1773
+ * cut.** That ordering is the contract, not an implementation detail: a narrowing
1774
+ * applied afterwards would mean the caller's `topK` is not the `topK` that reached
1775
+ * the index, so a scoped search would be exact only when a global over-fetch
1776
+ * happened to be generous enough. See {@link IFragmentQueryOptions}.
1644
1777
  */
1645
- query(vector: Float32Array, topK: number, maxPerRecord?: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
1778
+ query(vector: Float32Array, topK: number, options?: IFragmentQueryOptions): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
1646
1779
  /**
1647
1780
  * Whether this index holds **any** fragment for the scope-qualified `target`.
1648
1781
  *
@@ -1794,6 +1927,45 @@ export declare interface IIdentityCodecResult {
1794
1927
  readonly isVersioned: boolean;
1795
1928
  }
1796
1929
 
1930
+ /**
1931
+ * Resolves a consumer-facing `(kind, entityId)` address to the storage address the
1932
+ * vault files it under, **without reading the record**.
1933
+ *
1934
+ * @remarks
1935
+ * This is the resolution `IMemoryStore.get(kind, entityId)` already performs before
1936
+ * it reads anything: `kind` selects the kind's `IIdentityCodec`, and the codec's
1937
+ * `encode` computes `{ scope, idStem, isVersioned }`. Exposing it separately lets a
1938
+ * caller that needs only the *address* — a fragment query narrowing to one record,
1939
+ * say — avoid paying for a read it does not want.
1940
+ *
1941
+ * **The resolution is a function, not a search, and that is the load-bearing
1942
+ * property.** A consumer holds an `EntityId`; a vector hit is addressed by a
1943
+ * `(scope, id)` pair; and `EntityId` promises no uniqueness beyond a scope, so the
1944
+ * same id may legitimately appear in several scopes (a document `acme-corp` under
1945
+ * one kind and the entity `acme-corp` under another is the ordinary case, not a
1946
+ * pathological one). Supplying `kind` selects one codec, and a codec cannot return
1947
+ * two answers — so ambiguity is structurally impossible rather than merely unlikely,
1948
+ * and no disambiguation pass is needed anywhere downstream.
1949
+ *
1950
+ * Narrow by design, mirroring `IMemoryRecordResolver`: a component that needs to
1951
+ * turn an entity address into a storage address should depend on this rather than on
1952
+ * the whole store.
1953
+ * @public
1954
+ */
1955
+ export declare interface IIdentityResolver {
1956
+ /**
1957
+ * The storage address `(kind, entityId)` maps to, without reading the record.
1958
+ *
1959
+ * @param kind - Selects the identity codec. Required: it is what makes the
1960
+ * resolution unambiguous.
1961
+ * @param entityId - The consumer-supplied domain key.
1962
+ * @returns `Success` with the codec's `{ scope, idStem, isVersioned }`, or
1963
+ * `Failure` if no codec is registered for `kind` (and no default is wired) or the
1964
+ * codec rejects the id. Both are caller errors and both are loud.
1965
+ */
1966
+ resolveIdentity(kind: Kind, entityId: EntityId): Result<IIdentityCodecResult>;
1967
+ }
1968
+
1797
1969
  /**
1798
1970
  * Coverage of the record-granular vector index.
1799
1971
  * @public
@@ -2608,7 +2780,7 @@ export declare interface IMemoryRetrieverCapabilities {
2608
2780
  * a consumer actually holds, not only for the concrete `FileTreeMemoryStore`.
2609
2781
  * @public
2610
2782
  */
2611
- export declare interface IMemoryStore extends IMemoryRecordResolver {
2783
+ export declare interface IMemoryStore extends IMemoryRecordResolver, IIdentityResolver {
2612
2784
  /**
2613
2785
  * Keyed read by entity id. Resolves `entityId` to a storage address via the
2614
2786
  * registered {@link IIdentityCodec} for `kind`. Returns `undefined` when no
@@ -3086,7 +3258,18 @@ export declare class InMemoryFragmentCosineIndex implements IFragmentVectorIndex
3086
3258
  /** {@inheritDoc IFragmentVectorIndex.remove} */
3087
3259
  remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
3088
3260
  /** {@inheritDoc IFragmentVectorIndex.query} */
3089
- query(vector: Float32Array, topK: number, maxPerRecord?: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
3261
+ query(vector: Float32Array, topK: number, options?: IFragmentQueryOptions): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
3262
+ /**
3263
+ * The records a query is allowed to score, honoring the `scope` / `id` narrowing.
3264
+ *
3265
+ * @remarks
3266
+ * The single-record case is an O(1) map lookup rather than a scan, because the
3267
+ * record map is keyed by `edgeTargetKey`. The scope-only case (a versioned kind's
3268
+ * per-entity subtree) is a filtered walk — still bounded by the vault, but it
3269
+ * scores only the entity's own fragments, which is what makes the caller's `topK`
3270
+ * meaningful.
3271
+ */
3272
+ private _selectRecords;
3090
3273
  /**
3091
3274
  * Re-embed every record from `source` and rebuild the fragment index from
3092
3275
  * scratch. Clears the current contents (and the established dimension) first, so
@@ -1,4 +1,5 @@
1
1
  import { Result } from '@fgv/ts-utils';
2
+ import { EntityId, IIdentityResolver, Kind } from '../types';
2
3
  import { IFragmentVectorIndex, IVectorQueryHit } from '../vector';
3
4
  import { QueryEmbedder } from './semanticRetriever';
4
5
  /**
@@ -8,6 +9,26 @@ import { QueryEmbedder } from './semanticRetriever';
8
9
  * @public
9
10
  */
10
11
  export declare const FRAGMENT_SEMANTIC_UNWIRED_MESSAGE: string;
12
+ /**
13
+ * The loud-degradation message returned when a query carries a record narrowing but
14
+ * no {@link IIdentityResolver} is wired to resolve it.
15
+ *
16
+ * @remarks
17
+ * Deliberately a `Failure` rather than a silently-global search: answering a scoped
18
+ * question with an unscoped result is the failure this narrowing exists to remove.
19
+ * @public
20
+ */
21
+ export declare const FRAGMENT_NARROWING_UNRESOLVABLE_MESSAGE: string;
22
+ /**
23
+ * The message returned when exactly one of `entityId` / `kind` is supplied.
24
+ *
25
+ * @remarks
26
+ * They travel together because `kind` is what selects the identity codec, and the
27
+ * codec is what makes the resolution unambiguous. One without the other is not a
28
+ * partial narrowing that could be honored best-effort — it is not a narrowing at all.
29
+ * @public
30
+ */
31
+ export declare const FRAGMENT_NARROWING_INCOMPLETE_MESSAGE: string;
11
32
  /**
12
33
  * The fragment backend wired into a {@link FragmentSemanticRetriever}: the fragment
13
34
  * index to query and the embedder that turns the query text into a vector. Both are
@@ -36,6 +57,37 @@ export interface IFragmentQuery {
36
57
  * Applied during selection (before the `topK` cut). Omit for uncapped.
37
58
  */
38
59
  readonly maxPerRecord?: number;
60
+ /**
61
+ * Narrow the search to one record's fragments: the consumer-supplied domain key
62
+ * of the record to search within. **Must be supplied with
63
+ * {@link IFragmentQuery.kind}.**
64
+ *
65
+ * @remarks
66
+ * The narrowing is applied **during selection, before the `topK` cut**, so the
67
+ * `topK` you ask for is the `topK` you get. Filtering a global result afterwards
68
+ * is not equivalent: it truncates to `topK` across every record first, so a scoped
69
+ * search would come back short whenever other records outscored this one's
70
+ * fragments.
71
+ *
72
+ * For a versioned kind this narrows to **every version of the entity** — literally
73
+ * every version, superseded ones included, because invalidation stamps `invalid_at`
74
+ * without pruning that version's fragments. Nothing on a hit distinguishes a
75
+ * current fragment from a historical one. That matches the record-granular vector
76
+ * lane; it is not currency filtering.
77
+ */
78
+ readonly entityId?: EntityId;
79
+ /**
80
+ * The kind of the record named by {@link IFragmentQuery.entityId}. **Must be
81
+ * supplied with it.**
82
+ *
83
+ * @remarks
84
+ * This is not decoration and not a filter: `kind` **selects the identity codec**,
85
+ * and the codec computes the storage address. An `EntityId` promises no uniqueness
86
+ * beyond a scope — the same id under two kinds is the ordinary case, not a
87
+ * pathological one — so without `kind` the resolution is ambiguous, and with it
88
+ * ambiguity is structurally impossible.
89
+ */
90
+ readonly kind?: Kind;
39
91
  }
40
92
  /**
41
93
  * What a {@link FragmentSemanticRetriever} can do given its wiring.
@@ -71,18 +123,44 @@ export interface IFragmentRetrieverCapabilities {
71
123
  */
72
124
  export declare class FragmentSemanticRetriever {
73
125
  private readonly _backend;
126
+ private readonly _identityResolver;
74
127
  private constructor();
75
128
  /** What this retriever can do given its wiring. */
76
129
  get capabilities(): IFragmentRetrieverCapabilities;
77
- /** Family-convention factory. */
130
+ /**
131
+ * Family-convention factory.
132
+ *
133
+ * @param params - `backend` wires fragment recall itself. `identityResolver`
134
+ * resolves a query's `(kind, entityId)` narrowing to a storage address;
135
+ * `IMemoryStore` implements it, so the usual wiring is
136
+ * `{ backend, identityResolver: store }`. It is optional because an unscoped
137
+ * fragment search needs nothing to resolve — but a query that *does* carry a
138
+ * narrowing fails loudly without it rather than quietly searching everything.
139
+ */
78
140
  static create(params: {
79
141
  readonly backend?: IFragmentSemanticBackend;
142
+ readonly identityResolver?: IIdentityResolver;
80
143
  }): Result<FragmentSemanticRetriever>;
81
144
  /**
82
145
  * Embed `query.semantic`, query the fragment index, and return the per-fragment
83
146
  * hits in descending score order. Fails loudly when no backend is wired.
84
147
  */
85
148
  retrieve(query: IFragmentQuery): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
149
+ /**
150
+ * Turn the query's consumer-facing narrowing into the storage-address narrowing
151
+ * the index understands.
152
+ *
153
+ * @remarks
154
+ * `kind` selects the identity codec and the codec computes the record's storage
155
+ * address, so this is a deterministic resolution rather than a search —
156
+ * which is what makes a colliding `entityId` across kinds a non-issue.
157
+ *
158
+ * A **versioned** kind resolves to the entity's own subtree scope and deliberately
159
+ * carries no `id`, so the narrowing covers every version of the entity — including
160
+ * superseded ones, which are invalidated but never pruned from the index. A
161
+ * non-versioned kind resolves to exactly one record.
162
+ */
163
+ private _resolveOptions;
86
164
  /**
87
165
  * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`
88
166
  * and a thrown/rejected promise into a single `fragment recall: <label> failed`
@@ -1 +1 @@
1
- {"version":3,"file":"fragmentSemanticRetriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/fragmentSemanticRetriever.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAiB,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,iCAAiC,EAAE,MAC+D,CAAC;AAEhH;;;;;GAKG;AACH,MAAM,WAAW,wBAAwB;IACvC,mDAAmD;IACnD,QAAQ,CAAC,aAAa,EAAE,oBAAoB,CAAC;IAC7C,0CAA0C;IAC1C,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;CACpC;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,iEAAiE;IACjE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,8BAA8B;IAC7C,kFAAkF;IAClF,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,yBAAyB;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuC;IAEhE,OAAO;IAIP,mDAAmD;IACnD,IAAW,YAAY,IAAI,8BAA8B,CAExD;IAED,iCAAiC;WACnB,MAAM,CAAC,MAAM,EAAE;QAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,wBAAwB,CAAC;KAC7C,GAAG,MAAM,CAAC,yBAAyB,CAAC;IAIrC;;;OAGG;IACU,QAAQ,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAoB7F;;;;OAIG;mBACkB,YAAY;CAOlC"}
1
+ {"version":3,"file":"fragmentSemanticRetriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/fragmentSemanticRetriever.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAgC,MAAM,eAAe,CAAC;AACrE,OAAO,EAAW,QAAQ,EAAwB,iBAAiB,EAAE,IAAI,EAAY,MAAM,UAAU,CAAC;AACtG,OAAO,EAAyB,oBAAoB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACzF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,iCAAiC,EAAE,MAC+D,CAAC;AAEhH;;;;;;;;GAQG;AACH,eAAO,MAAM,uCAAuC,EAAE,MAC8E,CAAC;AAErI;;;;;;;;GAQG;AACH,eAAO,MAAM,qCAAqC,EAAE,MACgF,CAAC;AAErI;;;;;GAKG;AACH,MAAM,WAAW,wBAAwB;IACvC,mDAAmD;IACnD,QAAQ,CAAC,aAAa,EAAE,oBAAoB,CAAC;IAC7C,0CAA0C;IAC1C,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;CACpC;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,iEAAiE;IACjE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAE7B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,8BAA8B;IAC7C,kFAAkF;IAClF,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,yBAAyB;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuC;IAChE,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAgC;IAElE,OAAO;IAQP,mDAAmD;IACnD,IAAW,YAAY,IAAI,8BAA8B,CAExD;IAED;;;;;;;;;OASG;WACW,MAAM,CAAC,MAAM,EAAE;QAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,wBAAwB,CAAC;QAC5C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,iBAAiB,CAAC;KAC/C,GAAG,MAAM,CAAC,yBAAyB,CAAC;IAIrC;;;OAGG;IACU,QAAQ,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IA2B7F;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,eAAe;IAwCvB;;;;OAIG;mBACkB,YAAY;CAOlC"}
@@ -4,8 +4,9 @@
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.FragmentSemanticRetriever = exports.FRAGMENT_SEMANTIC_UNWIRED_MESSAGE = void 0;
7
+ exports.FragmentSemanticRetriever = exports.FRAGMENT_NARROWING_INCOMPLETE_MESSAGE = exports.FRAGMENT_NARROWING_UNRESOLVABLE_MESSAGE = exports.FRAGMENT_SEMANTIC_UNWIRED_MESSAGE = void 0;
8
8
  const ts_utils_1 = require("@fgv/ts-utils");
9
+ const types_1 = require("../types");
9
10
  /**
10
11
  * The loud-degradation message a {@link FragmentSemanticRetriever} returns when a
11
12
  * fragment query is issued but no {@link IFragmentSemanticBackend | backend} is
@@ -13,6 +14,26 @@ const ts_utils_1 = require("@fgv/ts-utils");
13
14
  * @public
14
15
  */
15
16
  exports.FRAGMENT_SEMANTIC_UNWIRED_MESSAGE = 'fragment recall: no fragment index is wired; wire an IFragmentSemanticBackend to enable sub-document search';
17
+ /**
18
+ * The loud-degradation message returned when a query carries a record narrowing but
19
+ * no {@link IIdentityResolver} is wired to resolve it.
20
+ *
21
+ * @remarks
22
+ * Deliberately a `Failure` rather than a silently-global search: answering a scoped
23
+ * question with an unscoped result is the failure this narrowing exists to remove.
24
+ * @public
25
+ */
26
+ exports.FRAGMENT_NARROWING_UNRESOLVABLE_MESSAGE = 'fragment recall: a record narrowing was supplied but no identity resolver is wired; pass one to FragmentSemanticRetriever.create';
27
+ /**
28
+ * The message returned when exactly one of `entityId` / `kind` is supplied.
29
+ *
30
+ * @remarks
31
+ * They travel together because `kind` is what selects the identity codec, and the
32
+ * codec is what makes the resolution unambiguous. One without the other is not a
33
+ * partial narrowing that could be honored best-effort — it is not a narrowing at all.
34
+ * @public
35
+ */
36
+ exports.FRAGMENT_NARROWING_INCOMPLETE_MESSAGE = 'fragment recall: `entityId` and `kind` must be supplied together — `kind` selects the identity codec that resolves the narrowing';
16
37
  /**
17
38
  * The sub-document semantic-search retriever — the "discovery" half of a
18
39
  * search-then-read contract. It embeds a fragment query, queries the
@@ -38,16 +59,26 @@ exports.FRAGMENT_SEMANTIC_UNWIRED_MESSAGE = 'fragment recall: no fragment index
38
59
  * @public
39
60
  */
40
61
  class FragmentSemanticRetriever {
41
- constructor(backend) {
62
+ constructor(backend, identityResolver) {
42
63
  this._backend = backend;
64
+ this._identityResolver = identityResolver;
43
65
  }
44
66
  /** What this retriever can do given its wiring. */
45
67
  get capabilities() {
46
68
  return { supportsFragmentRecall: this._backend !== undefined };
47
69
  }
48
- /** Family-convention factory. */
70
+ /**
71
+ * Family-convention factory.
72
+ *
73
+ * @param params - `backend` wires fragment recall itself. `identityResolver`
74
+ * resolves a query's `(kind, entityId)` narrowing to a storage address;
75
+ * `IMemoryStore` implements it, so the usual wiring is
76
+ * `{ backend, identityResolver: store }`. It is optional because an unscoped
77
+ * fragment search needs nothing to resolve — but a query that *does* carry a
78
+ * narrowing fails loudly without it rather than quietly searching everything.
79
+ */
49
80
  static create(params) {
50
- return (0, ts_utils_1.succeed)(new FragmentSemanticRetriever(params.backend));
81
+ return (0, ts_utils_1.succeed)(new FragmentSemanticRetriever(params.backend, params.identityResolver));
51
82
  }
52
83
  /**
53
84
  * Embed `query.semantic`, query the fragment index, and return the per-fragment
@@ -58,6 +89,13 @@ class FragmentSemanticRetriever {
58
89
  return (0, ts_utils_1.fail)(exports.FRAGMENT_SEMANTIC_UNWIRED_MESSAGE);
59
90
  }
60
91
  const backend = this._backend;
92
+ // Resolve the narrowing FIRST. It is synchronous, local, and cheap, while
93
+ // `embedQuery` is typically a paid network round trip — so a typo'd `kind`, a
94
+ // missing resolver, or a half-supplied narrowing should cost nothing.
95
+ const options = this._resolveOptions(query);
96
+ if (options.isFailure()) {
97
+ return (0, ts_utils_1.fail)(options.message);
98
+ }
61
99
  // Consumer-supplied hooks may throw; normalize both a returned `fail` and a
62
100
  // rejection into a single `fragment recall: <label> failed` Failure so
63
101
  // `retrieve` always honors its `Promise<Result<...>>` contract.
@@ -65,7 +103,58 @@ class FragmentSemanticRetriever {
65
103
  if (embedded.isFailure()) {
66
104
  return (0, ts_utils_1.fail)(embedded.message);
67
105
  }
68
- return FragmentSemanticRetriever._callBackend('fragment query', () => { var _a; return backend.fragmentIndex.query(embedded.value, (_a = query.topK) !== null && _a !== void 0 ? _a : 10, query.maxPerRecord); });
106
+ return FragmentSemanticRetriever._callBackend('fragment query', () => { var _a; return backend.fragmentIndex.query(embedded.value, (_a = query.topK) !== null && _a !== void 0 ? _a : 10, options.value); });
107
+ }
108
+ /**
109
+ * Turn the query's consumer-facing narrowing into the storage-address narrowing
110
+ * the index understands.
111
+ *
112
+ * @remarks
113
+ * `kind` selects the identity codec and the codec computes the record's storage
114
+ * address, so this is a deterministic resolution rather than a search —
115
+ * which is what makes a colliding `entityId` across kinds a non-issue.
116
+ *
117
+ * A **versioned** kind resolves to the entity's own subtree scope and deliberately
118
+ * carries no `id`, so the narrowing covers every version of the entity — including
119
+ * superseded ones, which are invalidated but never pruned from the index. A
120
+ * non-versioned kind resolves to exactly one record.
121
+ */
122
+ _resolveOptions(query) {
123
+ const { entityId, kind, maxPerRecord } = query;
124
+ if (entityId === undefined && kind === undefined) {
125
+ return (0, ts_utils_1.succeed)({ maxPerRecord });
126
+ }
127
+ if (entityId === undefined || kind === undefined) {
128
+ return (0, ts_utils_1.fail)(exports.FRAGMENT_NARROWING_INCOMPLETE_MESSAGE);
129
+ }
130
+ if (this._identityResolver === undefined) {
131
+ return (0, ts_utils_1.fail)(exports.FRAGMENT_NARROWING_UNRESOLVABLE_MESSAGE);
132
+ }
133
+ const resolver = this._identityResolver;
134
+ // `identityResolver` is a consumer-injectable seam like the two backend hooks,
135
+ // so a throw has to become a `Failure` here rather than escaping `retrieve()`
136
+ // and breaking its `Promise<Result<...>>` contract. `captureResult` yields a
137
+ // nested `Result`, which the identity `onSuccess` flattens.
138
+ return (0, ts_utils_1.captureResult)(() => resolver.resolveIdentity(kind, entityId))
139
+ .onSuccess((resolved) => resolved)
140
+ .withErrorFormat((msg) => `fragment recall: cannot resolve '${kind}'/'${entityId}': ${msg}`)
141
+ .onSuccess((address) => {
142
+ // A versioned kind's every version lives under the entity subtree the
143
+ // codec returned, so omitting `id` is what makes the narrowing mean
144
+ // "this entity" rather than "one of its versions".
145
+ if (address.isVersioned) {
146
+ return (0, ts_utils_1.succeed)({ maxPerRecord, scope: address.scope });
147
+ }
148
+ // `idStem` is a plain `string` on the codec result, but a `MemoryId` IS the
149
+ // filename stem by contract and `Convert.memoryId` is what enforces that —
150
+ // so validate rather than assert. A resolver that returned a path-unsafe
151
+ // stem could otherwise smuggle it into the index query, where it would
152
+ // match nothing and look like an empty result rather than a caller bug.
153
+ return types_1.Convert.memoryId
154
+ .convert(address.idStem)
155
+ .withErrorFormat((msg) => `fragment recall: '${kind}'/'${entityId}' resolved to an unusable record id: ${msg}`)
156
+ .onSuccess((id) => (0, ts_utils_1.succeed)({ maxPerRecord, scope: address.scope, id }));
157
+ });
69
158
  }
70
159
  /**
71
160
  * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`
@@ -1 +1 @@
1
- {"version":3,"file":"fragmentSemanticRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/fragmentSemanticRetriever.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,4CAAsD;AAItD;;;;;GAKG;AACU,QAAA,iCAAiC,GAC5C,6GAA6G,CAAC;AA0ChH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAa,yBAAyB;IAGpC,YAAoB,OAA6C;QAC/D,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1B,CAAC;IAED,mDAAmD;IACnD,IAAW,YAAY;QACrB,OAAO,EAAE,sBAAsB,EAAE,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;IACjE,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,MAEpB;QACC,OAAO,IAAA,kBAAO,EAAC,IAAI,yBAAyB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAChE,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,QAAQ,CAAC,KAAqB;QACzC,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAChC,OAAO,IAAA,eAAI,EAAC,yCAAiC,CAAC,CAAC;QACjD,CAAC;QACD,MAAM,OAAO,GAA6B,IAAI,CAAC,QAAQ,CAAC;QACxD,4EAA4E;QAC5E,uEAAuE;QACvE,gEAAgE;QAChE,MAAM,QAAQ,GAAyB,MAAM,yBAAyB,CAAC,YAAY,CACjF,iBAAiB,EACjB,GAAG,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,CACzC,CAAC;QACF,IAAI,QAAQ,CAAC,SAAS,EAAE,EAAE,CAAC;YACzB,OAAO,IAAA,eAAI,EAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,yBAAyB,CAAC,YAAY,CAAC,gBAAgB,EAAE,GAAG,EAAE,WACnE,OAAA,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAA,KAAK,CAAC,IAAI,mCAAI,EAAE,EAAE,KAAK,CAAC,YAAY,CAAC,CAAA,EAAA,CAClF,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACK,MAAM,CAAC,KAAK,CAAC,YAAY,CAAI,KAAa,EAAE,EAA4B;QAC9E,IAAI,CAAC;YACH,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,oBAAoB,KAAK,YAAY,GAAG,EAAE,CAAC,CAAC;QAC3F,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,IAAA,eAAI,EAAC,oBAAoB,KAAK,YAAY,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;CACF;AAvDD,8DAuDC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { IFragmentVectorIndex, IVectorQueryHit } from '../vector';\nimport { QueryEmbedder } from './semanticRetriever';\n\n/**\n * The loud-degradation message a {@link FragmentSemanticRetriever} returns when a\n * fragment query is issued but no {@link IFragmentSemanticBackend | backend} is\n * wired — the discovery surface NEVER answers a fragment query with a silent empty.\n * @public\n */\nexport const FRAGMENT_SEMANTIC_UNWIRED_MESSAGE: string =\n 'fragment recall: no fragment index is wired; wire an IFragmentSemanticBackend to enable sub-document search';\n\n/**\n * The fragment backend wired into a {@link FragmentSemanticRetriever}: the fragment\n * index to query and the embedder that turns the query text into a vector. Both are\n * required together — a fragment index is useless without a way to embed the query.\n * @public\n */\nexport interface IFragmentSemanticBackend {\n /** The fragment-granular vector index to query. */\n readonly fragmentIndex: IFragmentVectorIndex;\n /** Turns the query text into a vector. */\n readonly embedQuery: QueryEmbedder;\n}\n\n/**\n * A sub-document semantic-search request: the natural-language `semantic` text to\n * match, an optional `topK` result cap (default 10), and an optional\n * `maxPerRecord` cap that keeps one long document from monopolizing the result.\n * @public\n */\nexport interface IFragmentQuery {\n /** The natural-language text to embed and match against stored fragments. */\n readonly semantic: string;\n /** Maximum number of fragment hits to return. Defaults to 10. */\n readonly topK?: number;\n /**\n * Maximum number of fragments any single record may contribute to the result.\n * Applied during selection (before the `topK` cut). Omit for uncapped.\n */\n readonly maxPerRecord?: number;\n}\n\n/**\n * What a {@link FragmentSemanticRetriever} can do given its wiring.\n * @public\n */\nexport interface IFragmentRetrieverCapabilities {\n /** `true` when a fragment backend is wired and fragment recall is operational. */\n readonly supportsFragmentRecall: boolean;\n}\n\n/**\n * The sub-document semantic-search retriever — the \"discovery\" half of a\n * search-then-read contract. It embeds a fragment query, queries the\n * {@link IFragmentVectorIndex}, and returns the raw per-fragment\n * {@link IVectorQueryHit | hits} (each carrying a record `target` AND whichever of\n * `locator` / `fragmentId` the stored fragment was indexed with), NOT resolved\n * records: the consumer re-reads each record and resolves the fragment on its own\n * read side. Note the `locator` span is advisory — see {@link IFragmentLocator}; it\n * is not a slice guaranteed to reproduce the fragment's text.\n *\n * @remarks\n * Deliberately NOT an {@link IMemoryRetriever}: memory recall is record-granular and\n * returns records; fragment discovery is fragment-granular and returns fragment\n * identities. Keeping it a distinct surface matches the consumer contract (memory\n * stays record-granular; sub-document knowledge uses a separate fragment index) and\n * avoids overloading the record retriever's return type with identity fields that\n * only make sense here.\n *\n * When no backend is wired, `supportsFragmentRecall` is `false` and any fragment\n * query degrades loudly ({@link FRAGMENT_SEMANTIC_UNWIRED_MESSAGE}) — it NEVER\n * returns a silent empty. A consumer-supplied backend that rejects (throws) is\n * normalized into a `Failure`.\n * @public\n */\nexport class FragmentSemanticRetriever {\n private readonly _backend: IFragmentSemanticBackend | undefined;\n\n private constructor(backend: IFragmentSemanticBackend | undefined) {\n this._backend = backend;\n }\n\n /** What this retriever can do given its wiring. */\n public get capabilities(): IFragmentRetrieverCapabilities {\n return { supportsFragmentRecall: this._backend !== undefined };\n }\n\n /** Family-convention factory. */\n public static create(params: {\n readonly backend?: IFragmentSemanticBackend;\n }): Result<FragmentSemanticRetriever> {\n return succeed(new FragmentSemanticRetriever(params.backend));\n }\n\n /**\n * Embed `query.semantic`, query the fragment index, and return the per-fragment\n * hits in descending score order. Fails loudly when no backend is wired.\n */\n public async retrieve(query: IFragmentQuery): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (this._backend === undefined) {\n return fail(FRAGMENT_SEMANTIC_UNWIRED_MESSAGE);\n }\n const backend: IFragmentSemanticBackend = this._backend;\n // Consumer-supplied hooks may throw; normalize both a returned `fail` and a\n // rejection into a single `fragment recall: <label> failed` Failure so\n // `retrieve` always honors its `Promise<Result<...>>` contract.\n const embedded: Result<Float32Array> = await FragmentSemanticRetriever._callBackend(\n 'query embedding',\n () => backend.embedQuery(query.semantic)\n );\n if (embedded.isFailure()) {\n return fail(embedded.message);\n }\n return FragmentSemanticRetriever._callBackend('fragment query', () =>\n backend.fragmentIndex.query(embedded.value, query.topK ?? 10, query.maxPerRecord)\n );\n }\n\n /**\n * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`\n * and a thrown/rejected promise into a single `fragment recall: <label> failed`\n * `Failure`.\n */\n private static async _callBackend<T>(label: string, op: () => Promise<Result<T>>): Promise<Result<T>> {\n try {\n return (await op()).withErrorFormat((msg) => `fragment recall: ${label} failed: ${msg}`);\n } catch (err) {\n return fail(`fragment recall: ${label} failed: ${String(err)}`);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"fragmentSemanticRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/fragmentSemanticRetriever.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, captureResult, fail, succeed } from '@fgv/ts-utils';\nimport { Convert, EntityId, IIdentityCodecResult, IIdentityResolver, Kind, MemoryId } from '../types';\nimport { IFragmentQueryOptions, IFragmentVectorIndex, IVectorQueryHit } from '../vector';\nimport { QueryEmbedder } from './semanticRetriever';\n\n/**\n * The loud-degradation message a {@link FragmentSemanticRetriever} returns when a\n * fragment query is issued but no {@link IFragmentSemanticBackend | backend} is\n * wired — the discovery surface NEVER answers a fragment query with a silent empty.\n * @public\n */\nexport const FRAGMENT_SEMANTIC_UNWIRED_MESSAGE: string =\n 'fragment recall: no fragment index is wired; wire an IFragmentSemanticBackend to enable sub-document search';\n\n/**\n * The loud-degradation message returned when a query carries a record narrowing but\n * no {@link IIdentityResolver} is wired to resolve it.\n *\n * @remarks\n * Deliberately a `Failure` rather than a silently-global search: answering a scoped\n * question with an unscoped result is the failure this narrowing exists to remove.\n * @public\n */\nexport const FRAGMENT_NARROWING_UNRESOLVABLE_MESSAGE: string =\n 'fragment recall: a record narrowing was supplied but no identity resolver is wired; pass one to FragmentSemanticRetriever.create';\n\n/**\n * The message returned when exactly one of `entityId` / `kind` is supplied.\n *\n * @remarks\n * They travel together because `kind` is what selects the identity codec, and the\n * codec is what makes the resolution unambiguous. One without the other is not a\n * partial narrowing that could be honored best-effort — it is not a narrowing at all.\n * @public\n */\nexport const FRAGMENT_NARROWING_INCOMPLETE_MESSAGE: string =\n 'fragment recall: `entityId` and `kind` must be supplied together — `kind` selects the identity codec that resolves the narrowing';\n\n/**\n * The fragment backend wired into a {@link FragmentSemanticRetriever}: the fragment\n * index to query and the embedder that turns the query text into a vector. Both are\n * required together — a fragment index is useless without a way to embed the query.\n * @public\n */\nexport interface IFragmentSemanticBackend {\n /** The fragment-granular vector index to query. */\n readonly fragmentIndex: IFragmentVectorIndex;\n /** Turns the query text into a vector. */\n readonly embedQuery: QueryEmbedder;\n}\n\n/**\n * A sub-document semantic-search request: the natural-language `semantic` text to\n * match, an optional `topK` result cap (default 10), and an optional\n * `maxPerRecord` cap that keeps one long document from monopolizing the result.\n * @public\n */\nexport interface IFragmentQuery {\n /** The natural-language text to embed and match against stored fragments. */\n readonly semantic: string;\n /** Maximum number of fragment hits to return. Defaults to 10. */\n readonly topK?: number;\n /**\n * Maximum number of fragments any single record may contribute to the result.\n * Applied during selection (before the `topK` cut). Omit for uncapped.\n */\n readonly maxPerRecord?: number;\n\n /**\n * Narrow the search to one record's fragments: the consumer-supplied domain key\n * of the record to search within. **Must be supplied with\n * {@link IFragmentQuery.kind}.**\n *\n * @remarks\n * The narrowing is applied **during selection, before the `topK` cut**, so the\n * `topK` you ask for is the `topK` you get. Filtering a global result afterwards\n * is not equivalent: it truncates to `topK` across every record first, so a scoped\n * search would come back short whenever other records outscored this one's\n * fragments.\n *\n * For a versioned kind this narrows to **every version of the entity** — literally\n * every version, superseded ones included, because invalidation stamps `invalid_at`\n * without pruning that version's fragments. Nothing on a hit distinguishes a\n * current fragment from a historical one. That matches the record-granular vector\n * lane; it is not currency filtering.\n */\n readonly entityId?: EntityId;\n\n /**\n * The kind of the record named by {@link IFragmentQuery.entityId}. **Must be\n * supplied with it.**\n *\n * @remarks\n * This is not decoration and not a filter: `kind` **selects the identity codec**,\n * and the codec computes the storage address. An `EntityId` promises no uniqueness\n * beyond a scope — the same id under two kinds is the ordinary case, not a\n * pathological one — so without `kind` the resolution is ambiguous, and with it\n * ambiguity is structurally impossible.\n */\n readonly kind?: Kind;\n}\n\n/**\n * What a {@link FragmentSemanticRetriever} can do given its wiring.\n * @public\n */\nexport interface IFragmentRetrieverCapabilities {\n /** `true` when a fragment backend is wired and fragment recall is operational. */\n readonly supportsFragmentRecall: boolean;\n}\n\n/**\n * The sub-document semantic-search retriever — the \"discovery\" half of a\n * search-then-read contract. It embeds a fragment query, queries the\n * {@link IFragmentVectorIndex}, and returns the raw per-fragment\n * {@link IVectorQueryHit | hits} (each carrying a record `target` AND whichever of\n * `locator` / `fragmentId` the stored fragment was indexed with), NOT resolved\n * records: the consumer re-reads each record and resolves the fragment on its own\n * read side. Note the `locator` span is advisory — see {@link IFragmentLocator}; it\n * is not a slice guaranteed to reproduce the fragment's text.\n *\n * @remarks\n * Deliberately NOT an {@link IMemoryRetriever}: memory recall is record-granular and\n * returns records; fragment discovery is fragment-granular and returns fragment\n * identities. Keeping it a distinct surface matches the consumer contract (memory\n * stays record-granular; sub-document knowledge uses a separate fragment index) and\n * avoids overloading the record retriever's return type with identity fields that\n * only make sense here.\n *\n * When no backend is wired, `supportsFragmentRecall` is `false` and any fragment\n * query degrades loudly ({@link FRAGMENT_SEMANTIC_UNWIRED_MESSAGE}) — it NEVER\n * returns a silent empty. A consumer-supplied backend that rejects (throws) is\n * normalized into a `Failure`.\n * @public\n */\nexport class FragmentSemanticRetriever {\n private readonly _backend: IFragmentSemanticBackend | undefined;\n private readonly _identityResolver: IIdentityResolver | undefined;\n\n private constructor(\n backend: IFragmentSemanticBackend | undefined,\n identityResolver: IIdentityResolver | undefined\n ) {\n this._backend = backend;\n this._identityResolver = identityResolver;\n }\n\n /** What this retriever can do given its wiring. */\n public get capabilities(): IFragmentRetrieverCapabilities {\n return { supportsFragmentRecall: this._backend !== undefined };\n }\n\n /**\n * Family-convention factory.\n *\n * @param params - `backend` wires fragment recall itself. `identityResolver`\n * resolves a query's `(kind, entityId)` narrowing to a storage address;\n * `IMemoryStore` implements it, so the usual wiring is\n * `{ backend, identityResolver: store }`. It is optional because an unscoped\n * fragment search needs nothing to resolve — but a query that *does* carry a\n * narrowing fails loudly without it rather than quietly searching everything.\n */\n public static create(params: {\n readonly backend?: IFragmentSemanticBackend;\n readonly identityResolver?: IIdentityResolver;\n }): Result<FragmentSemanticRetriever> {\n return succeed(new FragmentSemanticRetriever(params.backend, params.identityResolver));\n }\n\n /**\n * Embed `query.semantic`, query the fragment index, and return the per-fragment\n * hits in descending score order. Fails loudly when no backend is wired.\n */\n public async retrieve(query: IFragmentQuery): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (this._backend === undefined) {\n return fail(FRAGMENT_SEMANTIC_UNWIRED_MESSAGE);\n }\n const backend: IFragmentSemanticBackend = this._backend;\n // Resolve the narrowing FIRST. It is synchronous, local, and cheap, while\n // `embedQuery` is typically a paid network round trip — so a typo'd `kind`, a\n // missing resolver, or a half-supplied narrowing should cost nothing.\n const options: Result<IFragmentQueryOptions> = this._resolveOptions(query);\n if (options.isFailure()) {\n return fail(options.message);\n }\n // Consumer-supplied hooks may throw; normalize both a returned `fail` and a\n // rejection into a single `fragment recall: <label> failed` Failure so\n // `retrieve` always honors its `Promise<Result<...>>` contract.\n const embedded: Result<Float32Array> = await FragmentSemanticRetriever._callBackend(\n 'query embedding',\n () => backend.embedQuery(query.semantic)\n );\n if (embedded.isFailure()) {\n return fail(embedded.message);\n }\n return FragmentSemanticRetriever._callBackend('fragment query', () =>\n backend.fragmentIndex.query(embedded.value, query.topK ?? 10, options.value)\n );\n }\n\n /**\n * Turn the query's consumer-facing narrowing into the storage-address narrowing\n * the index understands.\n *\n * @remarks\n * `kind` selects the identity codec and the codec computes the record's storage\n * address, so this is a deterministic resolution rather than a search —\n * which is what makes a colliding `entityId` across kinds a non-issue.\n *\n * A **versioned** kind resolves to the entity's own subtree scope and deliberately\n * carries no `id`, so the narrowing covers every version of the entity — including\n * superseded ones, which are invalidated but never pruned from the index. A\n * non-versioned kind resolves to exactly one record.\n */\n private _resolveOptions(query: IFragmentQuery): Result<IFragmentQueryOptions> {\n const { entityId, kind, maxPerRecord } = query;\n if (entityId === undefined && kind === undefined) {\n return succeed({ maxPerRecord });\n }\n if (entityId === undefined || kind === undefined) {\n return fail(FRAGMENT_NARROWING_INCOMPLETE_MESSAGE);\n }\n if (this._identityResolver === undefined) {\n return fail(FRAGMENT_NARROWING_UNRESOLVABLE_MESSAGE);\n }\n const resolver: IIdentityResolver = this._identityResolver;\n // `identityResolver` is a consumer-injectable seam like the two backend hooks,\n // so a throw has to become a `Failure` here rather than escaping `retrieve()`\n // and breaking its `Promise<Result<...>>` contract. `captureResult` yields a\n // nested `Result`, which the identity `onSuccess` flattens.\n return captureResult(() => resolver.resolveIdentity(kind, entityId))\n .onSuccess((resolved: Result<IIdentityCodecResult>) => resolved)\n .withErrorFormat((msg) => `fragment recall: cannot resolve '${kind}'/'${entityId}': ${msg}`)\n .onSuccess((address: IIdentityCodecResult) => {\n // A versioned kind's every version lives under the entity subtree the\n // codec returned, so omitting `id` is what makes the narrowing mean\n // \"this entity\" rather than \"one of its versions\".\n if (address.isVersioned) {\n return succeed({ maxPerRecord, scope: address.scope });\n }\n // `idStem` is a plain `string` on the codec result, but a `MemoryId` IS the\n // filename stem by contract and `Convert.memoryId` is what enforces that —\n // so validate rather than assert. A resolver that returned a path-unsafe\n // stem could otherwise smuggle it into the index query, where it would\n // match nothing and look like an empty result rather than a caller bug.\n return Convert.memoryId\n .convert(address.idStem)\n .withErrorFormat(\n (msg) => `fragment recall: '${kind}'/'${entityId}' resolved to an unusable record id: ${msg}`\n )\n .onSuccess((id: MemoryId) => succeed({ maxPerRecord, scope: address.scope, id }));\n });\n }\n\n /**\n * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`\n * and a thrown/rejected promise into a single `fragment recall: <label> failed`\n * `Failure`.\n */\n private static async _callBackend<T>(label: string, op: () => Promise<Result<T>>): Promise<Result<T>> {\n try {\n return (await op()).withErrorFormat((msg) => `fragment recall: ${label} failed: ${msg}`);\n } catch (err) {\n return fail(`fragment recall: ${label} failed: ${String(err)}`);\n }\n }\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import { Logging, Result } from '@fgv/ts-utils';
2
2
  import { FileTree } from '@fgv/ts-json-base';
3
- import { DedupScope, EntityId, IIdentityCodec, IMemoryRecord, IWritePolicy, Kind, MemoryId, MemoryScopeKey, RankProjector } from '../types';
3
+ import { DedupScope, EntityId, IIdentityCodecResult, IIdentityCodec, IMemoryRecord, IWritePolicy, Kind, MemoryId, MemoryScopeKey, RankProjector } from '../types';
4
4
  import { IBodyConverterRegistry as IRegistry } from '../converters';
5
5
  import { IDerivedStateCoverage } from './coverage';
6
6
  import { DerivedArtifact, ReconcileReport } from './reconcile';
@@ -325,6 +325,9 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
325
325
  listScoped(): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>>;
326
326
  /** {@inheritDoc IMemoryRecordResolver.resolveRecord} */
327
327
  resolveRecord(scope: MemoryScopeKey, id: MemoryId): Result<IMemoryRecord<unknown> | undefined>;
328
+ private _codec;
329
+ /** {@inheritDoc IIdentityResolver.resolveIdentity} */
330
+ resolveIdentity(kind: Kind, entityId: EntityId): Result<IIdentityCodecResult>;
328
331
  /**
329
332
  * Materialize a selected set of entries into records, dropping any that have
330
333
  * vanished since selection.
@@ -597,7 +600,6 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
597
600
  * than preserving it), so a ranking bug never loses an authoritative write.
598
601
  */
599
602
  private _stampRank;
600
- private _codecFor;
601
603
  private _policyFor;
602
604
  /** {@inheritDoc IMemoryStore.dedupScopeFor} */
603
605
  dedupScopeFor(kind: Kind): DedupScope;