@fgv/ts-agent-memory 5.1.0-45 → 5.1.0-46

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 (96) hide show
  1. package/.rush/temp/{bc5b0f2098f7df2a5c498ea77b0de13316349b96.tar.log → 6e1299d1cebb080772163a532c221cd181b54f9f.tar.log} +20 -2
  2. package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +2 -2
  3. package/.rush/temp/operation/build/all.log +2 -2
  4. package/.rush/temp/operation/build/log-chunks.jsonl +2 -2
  5. package/.rush/temp/operation/build/state.json +1 -1
  6. package/README.md +79 -4
  7. package/dist/packlets/retrieve/fragmentSemanticRetriever.js +10 -7
  8. package/dist/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
  9. package/dist/packlets/store/fileTreeMemoryStore.js +20 -4
  10. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  11. package/dist/packlets/types/writePolicy.js +10 -2
  12. package/dist/packlets/types/writePolicy.js.map +1 -1
  13. package/dist/packlets/vector/fragmentConverters.js +41 -0
  14. package/dist/packlets/vector/fragmentConverters.js.map +1 -0
  15. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +17 -9
  16. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  17. package/dist/packlets/vector/index.js +1 -0
  18. package/dist/packlets/vector/index.js.map +1 -1
  19. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  20. package/dist/test/unit/store/fileTreeMemoryStore.test.js +64 -0
  21. package/dist/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
  22. package/dist/test/unit/store/indexInjection.test.js +432 -0
  23. package/dist/test/unit/store/indexInjection.test.js.map +1 -0
  24. package/dist/test/unit/types/temporalPolicy.test.js +21 -0
  25. package/dist/test/unit/types/temporalPolicy.test.js.map +1 -1
  26. package/dist/test/unit/types/writePolicy.test.js +83 -0
  27. package/dist/test/unit/types/writePolicy.test.js.map +1 -1
  28. package/dist/test/unit/vector/fragmentConverters.test.js +58 -0
  29. package/dist/test/unit/vector/fragmentConverters.test.js.map +1 -0
  30. package/dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js +65 -0
  31. package/dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js.map +1 -1
  32. package/dist/ts-agent-memory.d.ts +240 -37
  33. package/etc/ts-agent-memory.api.md +10 -1
  34. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts +10 -7
  35. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts.map +1 -1
  36. package/lib/packlets/retrieve/fragmentSemanticRetriever.js +10 -7
  37. package/lib/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
  38. package/lib/packlets/store/fileTreeMemoryStore.d.ts +65 -3
  39. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  40. package/lib/packlets/store/fileTreeMemoryStore.js +20 -4
  41. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  42. package/lib/packlets/types/writePolicy.d.ts +40 -2
  43. package/lib/packlets/types/writePolicy.d.ts.map +1 -1
  44. package/lib/packlets/types/writePolicy.js +10 -2
  45. package/lib/packlets/types/writePolicy.js.map +1 -1
  46. package/lib/packlets/vector/fragmentConverters.d.ts +25 -0
  47. package/lib/packlets/vector/fragmentConverters.d.ts.map +1 -0
  48. package/lib/packlets/vector/fragmentConverters.js +44 -0
  49. package/lib/packlets/vector/fragmentConverters.js.map +1 -0
  50. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +5 -3
  51. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
  52. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +17 -9
  53. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  54. package/lib/packlets/vector/index.d.ts +1 -0
  55. package/lib/packlets/vector/index.d.ts.map +1 -1
  56. package/lib/packlets/vector/index.js +1 -0
  57. package/lib/packlets/vector/index.js.map +1 -1
  58. package/lib/packlets/vector/vectorIndex.d.ts +97 -22
  59. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  60. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  61. package/lib/test/unit/store/fileTreeMemoryStore.test.js +64 -0
  62. package/lib/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
  63. package/lib/test/unit/store/indexInjection.test.d.ts +2 -0
  64. package/lib/test/unit/store/indexInjection.test.d.ts.map +1 -0
  65. package/lib/test/unit/store/indexInjection.test.js +434 -0
  66. package/lib/test/unit/store/indexInjection.test.js.map +1 -0
  67. package/lib/test/unit/types/temporalPolicy.test.js +21 -0
  68. package/lib/test/unit/types/temporalPolicy.test.js.map +1 -1
  69. package/lib/test/unit/types/writePolicy.test.js +83 -0
  70. package/lib/test/unit/types/writePolicy.test.js.map +1 -1
  71. package/lib/test/unit/vector/fragmentConverters.test.d.ts +2 -0
  72. package/lib/test/unit/vector/fragmentConverters.test.d.ts.map +1 -0
  73. package/lib/test/unit/vector/fragmentConverters.test.js +60 -0
  74. package/lib/test/unit/vector/fragmentConverters.test.js.map +1 -0
  75. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.js +65 -0
  76. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.js.map +1 -1
  77. package/package.json +7 -7
  78. package/rush-logs/ts-agent-memory.build.cache.log +1 -1
  79. package/rush-logs/ts-agent-memory.build.log +2 -2
  80. package/src/packlets/retrieve/fragmentSemanticRetriever.ts +10 -7
  81. package/src/packlets/store/fileTreeMemoryStore.ts +74 -4
  82. package/src/packlets/types/writePolicy.ts +40 -2
  83. package/src/packlets/vector/fragmentConverters.ts +55 -0
  84. package/src/packlets/vector/inMemoryFragmentCosineIndex.ts +35 -8
  85. package/src/packlets/vector/index.ts +1 -0
  86. package/src/packlets/vector/vectorIndex.ts +97 -22
  87. package/src/test/unit/store/fileTreeMemoryStore.test.ts +75 -0
  88. package/src/test/unit/store/indexInjection.test.ts +550 -0
  89. package/src/test/unit/types/temporalPolicy.test.ts +28 -0
  90. package/src/test/unit/types/writePolicy.test.ts +112 -0
  91. package/src/test/unit/vector/fragmentConverters.test.ts +80 -0
  92. package/src/test/unit/vector/inMemoryFragmentCosineIndex.test.ts +88 -0
  93. package/temp/build/lint/_eslint-5eVG3S6w.json +23 -11
  94. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  95. package/temp/ts-agent-memory.api.json +177 -21
  96. package/temp/ts-agent-memory.api.md +10 -1
@@ -271,6 +271,22 @@ export declare const edgeTargetConverter: Converter<IEdgeTarget>;
271
271
  */
272
272
  export declare function edgeTargetKey(target: IEdgeTarget): string;
273
273
 
274
+ /**
275
+ * Converter for an {@link IEmbeddedFragment}, and the boundary that enforces the
276
+ * fragment-identifiability invariant: **at least one of `locator` / `fragmentId`
277
+ * must be present.**
278
+ *
279
+ * @remarks
280
+ * The invariant lives here rather than in the type. A conditional-required union
281
+ * (`{ locator; fragmentId? } | { locator?; fragmentId }`) was considered and
282
+ * declined: it costs at every construction site and buys nothing at the read site,
283
+ * where each field reads as `… | undefined` either way. What it must not cost is
284
+ * identifiability — a fragment carrying neither identity cannot be resolved back to
285
+ * anything by a consumer holding the query hit, so it is rejected here.
286
+ * @public
287
+ */
288
+ export declare const embeddedFragmentConverter: Converter<IEmbeddedFragment>;
289
+
274
290
  /**
275
291
  * Consumer-supplied domain key. The stable entity identity across versions;
276
292
  * the package never mints identity. Equals {@link MemoryId} for non-temporal
@@ -360,11 +376,19 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
360
376
  */
361
377
  get skippedRecords(): ReadonlyArray<ISkippedRecord>;
362
378
  /**
363
- * Family-convention factory. Builds the derived index and a default LWW
364
- * policy, then performs an initial FileTree walk so an existing vault is
365
- * indexed (and the `seq` counter resumes past the highest persisted `seq`).
379
+ * Family-convention factory. Resolves the derived index (the caller's
380
+ * {@link IFileTreeMemoryStoreCreateParams.index | index} when supplied, a fresh
381
+ * {@link MemoryIndex} otherwise) and a default LWW policy, then performs an
382
+ * initial FileTree walk so an existing vault is indexed (and the `seq` counter
383
+ * resumes past the highest persisted `seq`).
366
384
  */
367
385
  static create(params: IFileTreeMemoryStoreCreateParams): Result<FileTreeMemoryStore>;
386
+ /**
387
+ * Resolve the derived index for a `create()`: the caller's injected
388
+ * {@link IMemoryIndex} verbatim, or a fresh {@link MemoryIndex} when none was
389
+ * supplied (the default that keeps an omitting caller byte-identical).
390
+ */
391
+ private static _resolveIndex;
368
392
  /** {@inheritDoc IMemoryStore.get} */
369
393
  get(kind: Kind, entityId: EntityId): Promise<Result<IMemoryRecord<unknown> | undefined>>;
370
394
  /** {@inheritDoc IMemoryStore.getById} */
@@ -697,20 +721,31 @@ export declare const FRAGMENT_SEMANTIC_UNWIRED_MESSAGE: string;
697
721
  */
698
722
  export declare type FragmentEmbedder = (record: IMemoryRecord<unknown>) => Promise<Result<ReadonlyArray<IEmbeddedFragment>>>;
699
723
 
724
+ /**
725
+ * Converter for an {@link IFragmentLocator}. Validates the two offsets only — the
726
+ * span's *meaning* (character / byte / token unit) is the consumer's, and the index
727
+ * never interprets it. Note the span is advisory: see {@link IFragmentLocator}.
728
+ * @public
729
+ */
730
+ export declare const fragmentLocatorConverter: Converter<IFragmentLocator>;
731
+
700
732
  /**
701
733
  * The sub-document semantic-search retriever — the "discovery" half of a
702
734
  * search-then-read contract. It embeds a fragment query, queries the
703
735
  * {@link IFragmentVectorIndex}, and returns the raw per-fragment
704
- * {@link IVectorQueryHit | hits} (each carrying a record `target` AND the matched
705
- * `locator`), NOT resolved records: the consumer re-reads each record and slices it
706
- * by the locator on its own read side.
736
+ * {@link IVectorQueryHit | hits} (each carrying a record `target` AND whichever of
737
+ * `locator` / `fragmentId` the stored fragment was indexed with), NOT resolved
738
+ * records: the consumer re-reads each record and resolves the fragment on its own
739
+ * read side. Note the `locator` span is advisory — see {@link IFragmentLocator}; it
740
+ * is not a slice guaranteed to reproduce the fragment's text.
707
741
  *
708
742
  * @remarks
709
743
  * Deliberately NOT an {@link IMemoryRetriever}: memory recall is record-granular and
710
- * returns records; fragment discovery is span-granular and returns locators. Keeping
711
- * it a distinct surface matches the consumer contract (memory stays record-granular;
712
- * sub-document knowledge uses a separate fragment index) and avoids overloading the
713
- * record retriever's return type with a locator that only makes sense here.
744
+ * returns records; fragment discovery is fragment-granular and returns fragment
745
+ * identities. Keeping it a distinct surface matches the consumer contract (memory
746
+ * stays record-granular; sub-document knowledge uses a separate fragment index) and
747
+ * avoids overloading the record retriever's return type with identity fields that
748
+ * only make sense here.
714
749
  *
715
750
  * When no backend is wired, `supportsFragmentRecall` is `false` and any fragment
716
751
  * query degrades loudly ({@link FRAGMENT_SEMANTIC_UNWIRED_MESSAGE}) — it NEVER
@@ -1016,15 +1051,44 @@ export declare interface IEdgeTarget {
1016
1051
  }
1017
1052
 
1018
1053
  /**
1019
- * One embedded fragment of a record: its in-record {@link IFragmentLocator | span}
1020
- * and the vector for that span. Produced by a {@link FragmentEmbedder} and stored
1021
- * via {@link IFragmentVectorIndex.addFragments}.
1054
+ * One embedded fragment of a record: the fragment's vector, plus at least one of the
1055
+ * two ways to identify it its advisory in-record {@link IFragmentLocator | span}
1056
+ * and/or an opaque consumer-minted {@link IEmbeddedFragment.fragmentId | fragmentId}.
1057
+ * Produced by a {@link FragmentEmbedder} and stored via
1058
+ * {@link IFragmentVectorIndex.addFragments}.
1059
+ *
1060
+ * @remarks
1061
+ * Both identity fields are optional **in the type**, but the "at least one"
1062
+ * requirement is real — a fragment carrying neither is unidentifiable at the read
1063
+ * side. It is enforced by {@link embeddedFragmentConverter} (and re-checked by the
1064
+ * in-package index implementations) rather than by a conditional-required union
1065
+ * (`{ locator; fragmentId? } | { locator?; fragmentId }`), which was considered and
1066
+ * declined: the union costs at every construction site and buys nothing at the read
1067
+ * site, where each field reads as `… | undefined` either way.
1022
1068
  * @public
1023
1069
  */
1024
1070
  export declare interface IEmbeddedFragment {
1025
- /** The fragment's in-record span. */
1026
- readonly locator: IFragmentLocator;
1027
- /** The embedding vector for that span. */
1071
+ /**
1072
+ * The region of the record body this fragment was derived from, when one can be
1073
+ * stated honestly. Advisory see {@link IFragmentLocator}; it is NOT a slice that
1074
+ * reproduces the fragment text. Omit it for a fragment with no honest body span (a
1075
+ * rewriting segmenter), in which case `fragmentId` must be supplied.
1076
+ */
1077
+ readonly locator?: IFragmentLocator;
1078
+ /**
1079
+ * An opaque, consumer-minted identity for this fragment, carried verbatim through
1080
+ * the index and returned on the corresponding {@link IVectorQueryHit}. The index
1081
+ * **never parses it, never filters on it, and never assigns meaning to it** — it is
1082
+ * a bytestring, not part of the query path. It exists so a fragment stays
1083
+ * identifiable when its text is not re-derivable from the record body.
1084
+ *
1085
+ * The guarantee is "we never parse it", NOT "we keep it stable". Because
1086
+ * `addFragments` is whole-record-replace, an updated record re-emits its entire
1087
+ * fragment set, so **any stability of a fragment id across re-embeds is the
1088
+ * consumer's responsibility**, not the index's.
1089
+ */
1090
+ readonly fragmentId?: string;
1091
+ /** The embedding vector for this fragment. */
1028
1092
  readonly vector: Float32Array;
1029
1093
  }
1030
1094
 
@@ -1110,6 +1174,59 @@ export declare interface IFileTreeMemoryStoreCreateParams {
1110
1174
  readonly rankProjectors?: ReadonlyMap<Kind, RankProjector>;
1111
1175
  /** Default codec for kinds without an explicit entry. */
1112
1176
  readonly defaultCodec?: IIdentityCodec;
1177
+ /**
1178
+ * Optional derived-index implementation. Defaults to a fresh {@link MemoryIndex} —
1179
+ * omitting this parameter is byte-identical to the store's behavior before the
1180
+ * parameter existed. When supplied, the store uses it for EVERY index operation
1181
+ * it performs and never holds a second index, so an injected index is the store's
1182
+ * only view of its own records:
1183
+ *
1184
+ * - `rebuild` — once, from the initial vault walk in `create()`.
1185
+ * - `patch` — on every persisted write, delete, version invalidation, and
1186
+ * cap-cull eviction.
1187
+ * - `entries` — behind {@link IMemoryStore.list | list} /
1188
+ * {@link IMemoryStore.listScoped | listScoped}, the keyed temporal reads, the
1189
+ * write path's content-hash dedup and write-policy admission cohort, AND the
1190
+ * temporal (versioned) write and delete paths, which resolve an entity's
1191
+ * version history entirely from the index.
1192
+ *
1193
+ * That last group is the one to weigh before injecting anything other than a
1194
+ * pass-through decorator: an index that filters, reorders, or otherwise reshapes
1195
+ * `entries()` changes WRITE semantics, not just what reads return. Concretely,
1196
+ * on a versioned kind the store derives an entity's whole version history from
1197
+ * `entries()` filtered by scope, and that derivation decides which version a
1198
+ * `put` treats as current (so what it dedups against and what it merges its
1199
+ * patch over), which prior versions it stamps `invalid_at` on, what the
1200
+ * admission cohort is, and which versions a `delete` tombstones. An index that
1201
+ * hides a version makes it invisible to all of those — the FileTree still holds
1202
+ * it, but the store will not supersede, invalidate, or tombstone it. On flat
1203
+ * kinds the same reshaping changes what dedups and what a cap-cull policy
1204
+ * evicts. A faithful delegating decorator — the intended use below — has no such
1205
+ * effect. Note the store's keyed reads ({@link IMemoryStore.get | get} on a flat
1206
+ * kind, and {@link IMemoryStore.getById | getById}) go to the FileTree, not the
1207
+ * index; the FileTree remains the source of truth and the index stays a derived
1208
+ * view.
1209
+ *
1210
+ * @remarks
1211
+ * **This is an instrumentation seam, NOT a resident-memory fix.** The intended
1212
+ * use is wrapping the shipped {@link MemoryIndex} in a decorator that counts and
1213
+ * times the calls the store makes — resident bytes by kind, open cost against
1214
+ * vault size, where the curve actually bends — so a decision about a partial-read
1215
+ * redesign can be driven by measurements instead of estimates.
1216
+ *
1217
+ * It does NOT lower the store's resident-memory ceiling, and injecting a
1218
+ * "persisted" or "lazy" index will not change that. {@link IMemoryIndex}'s read
1219
+ * surface returns whole records by construction: `entries()` yields
1220
+ * {@link IIndexedMemoryRecord}s and `byKind` / `byTag` / `byRecency` / `byRank`
1221
+ * yield `IMemoryRecord<unknown>` — `{ envelope, body }` pairs with the body
1222
+ * materialized. Any implementation satisfying the current contract must therefore
1223
+ * be able to produce every body on demand. An injected index changes WHERE records
1224
+ * come from; it does not change WHETHER bodies are held. Lowering the ceiling
1225
+ * requires a partial-read (id-or-envelope-only) redesign of `IMemoryIndex` itself,
1226
+ * which is separate, breaking, design-first work and is deliberately not part of
1227
+ * this seam.
1228
+ */
1229
+ readonly index?: IMemoryIndex;
1113
1230
  /** Scope encoding. Defaults to {@link defaultMemoryScopeEncoding}. */
1114
1231
  readonly scopeEncoding?: (scope: MemoryScopeKey) => Result<string>;
1115
1232
  /**
@@ -1192,10 +1309,24 @@ export declare interface IFileTreeMemoryStoreCreateParams {
1192
1309
 
1193
1310
  /**
1194
1311
  * A half-open `[start, end)` span into a record's body — the in-record locator a
1195
- * {@link IFragmentVectorIndex} carries on each fragment hit. `start` is inclusive,
1312
+ * {@link IFragmentVectorIndex} may carry on a fragment. `start` is inclusive,
1196
1313
  * `end` exclusive. The unit (character / byte / token offsets) is the consumer's
1197
1314
  * choice: the index stores the two integers opaquely and never interprets them,
1198
1315
  * so they line up with whatever locator the consumer's own read side uses.
1316
+ *
1317
+ * @remarks
1318
+ * **The span is advisory.** It names the region of the body a fragment was
1319
+ * *derived from*; it is NOT a slice guaranteed to reproduce the fragment's text.
1320
+ * `body.slice(start, end)` round-trips only under a segmenter that merely chooses
1321
+ * boundaries. Under a **rewriting** segmenter — one that turns a span into a
1322
+ * curated block, an increasingly common ingestion shape when a model both selects
1323
+ * and rewrites — the fragment text is not a substring of the body at all, and the
1324
+ * fragmentation is not re-derivable from the body. Treat the span as a pointer for
1325
+ * locating context, never as an extraction recipe.
1326
+ *
1327
+ * A fragment whose provenance cannot honestly be expressed as a body span should
1328
+ * omit the locator entirely and carry an {@link IEmbeddedFragment.fragmentId}
1329
+ * instead.
1199
1330
  * @public
1200
1331
  */
1201
1332
  export declare interface IFragmentLocator {
@@ -1247,18 +1378,21 @@ export declare interface IFragmentSemanticBackend {
1247
1378
 
1248
1379
  /**
1249
1380
  * The fragment-granular sibling of {@link IVectorIndex}: instead of one vector per
1250
- * record it holds many vectors per record, each tagged with an in-record
1251
- * {@link IFragmentLocator}, and its `query` returns per-fragment hits carrying that
1252
- * locator. This is the seam behind sub-document semantic search — the "discovery"
1253
- * half of a search-then-read contract, where a hit's `(target, locator)` tells the
1254
- * consumer which record AND which span to read.
1381
+ * record it holds many vectors per record, each tagged with the identity its
1382
+ * {@link IEmbeddedFragment} carried, and its `query` returns per-fragment hits
1383
+ * carrying that identity back. This is the seam behind sub-document semantic search
1384
+ * — the "discovery" half of a search-then-read contract, where a hit tells the
1385
+ * consumer which record AND which fragment of it to read.
1255
1386
  *
1256
1387
  * @remarks
1257
- * Deliberately NOT `extends IVectorIndex`: an index keyed by `(target, locator)`
1258
- * has no well-defined single-vector `add(target, vector)`. It is a parallel
1259
- * contract with three operations — `addFragments`, `remove`, `query` — reusing
1260
- * {@link IVectorQueryHit} (whose `locator` is always populated here). Kept distinct
1261
- * from the record-granular index per the consumer contract: memory recall stays
1388
+ * Deliberately NOT `extends IVectorIndex`: an index holding many vectors per record
1389
+ * has no well-defined single-vector `add(target, vector)`. It is a parallel contract
1390
+ * with three operations — `addFragments`, `remove`, `query` — reusing
1391
+ * {@link IVectorQueryHit}, on which both `locator` and `fragmentId` are optional. A
1392
+ * fragment hit populates whichever of the two its stored fragment carried; see
1393
+ * {@link IVectorQueryHit} for why that is not a discriminator and why fragment-ness
1394
+ * is determined by the index queried, not by field presence. Kept distinct from the
1395
+ * record-granular index per the consumer contract: memory recall stays
1262
1396
  * record-granular; sub-document knowledge uses a separate fragment index.
1263
1397
  * @public
1264
1398
  */
@@ -1277,7 +1411,8 @@ export declare interface IFragmentVectorIndex {
1277
1411
  remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
1278
1412
  /**
1279
1413
  * Return the `topK` nearest fragments to `vector`, in descending score order,
1280
- * each hit carrying its record `target` and fragment `locator`. When
1414
+ * each hit carrying its record `target` plus whichever of `locator` /
1415
+ * `fragmentId` the stored fragment was added with. When
1281
1416
  * `maxPerRecord` is supplied, no more than that many fragments of any single
1282
1417
  * record appear in the result — the cap is applied during selection (before the
1283
1418
  * `topK` cut) so one long document cannot crowd out others.
@@ -1441,6 +1576,15 @@ export declare interface IMemoryCapCullPolicyParams {
1441
1576
  * The fields a merge-patch update may touch (drawn from the record-level
1442
1577
  * mutable vocabulary: `body` / `tags` / `links` / `provenance` /
1443
1578
  * `embeddingRef`). Fields outside this list are immutable.
1579
+ *
1580
+ * @remarks
1581
+ * This list — unlike {@link KnowledgeLwwPolicy}'s pinned surface — is
1582
+ * caller-supplied, so it is what decides which fields get the RFC-7386
1583
+ * guarantees documented on {@link IWritePolicy.applyUpdate}. Declare
1584
+ * `provenance` here to get per-key provenance merging and `null` sub-key
1585
+ * clearing; omit it and every provenance patch key is inert (dropped before
1586
+ * the merge, so the existing value is preserved verbatim and even a
1587
+ * whole-block `null` is a silent no-op rather than an error).
1444
1588
  */
1445
1589
  readonly mutableFields: ReadonlyArray<string>;
1446
1590
  }
@@ -2122,9 +2266,11 @@ export declare class InMemoryCosineIndex implements IVectorIndex {
2122
2266
  /**
2123
2267
  * The brute-force, in-memory cosine {@link IFragmentVectorIndex} — the
2124
2268
  * fragment-granular sibling of {@link InMemoryCosineIndex}. Stores many
2125
- * `Float32Array`s per record (one per in-record {@link IFragmentLocator | span})
2126
- * and answers a query by computing cosine similarity against every stored
2127
- * fragment, returning the top-k fragment hits by descending score.
2269
+ * `Float32Array`s per record (one per {@link IEmbeddedFragment | fragment}) and
2270
+ * answers a query by computing cosine similarity against every stored fragment,
2271
+ * returning the top-k fragment hits by descending score. Each hit carries back
2272
+ * whichever of {@link IFragmentLocator | locator} / `fragmentId` its fragment was
2273
+ * added with; a fragment must carry at least one of the two.
2128
2274
  *
2129
2275
  * @remarks
2130
2276
  * Same regime and same non-goals as {@link InMemoryCosineIndex}: no external
@@ -2452,9 +2598,27 @@ export declare interface IVectorIndex {
2452
2598
  * that share a stem. The caller re-resolves the hit against the record index by
2453
2599
  * the same scoped address.
2454
2600
  *
2455
- * `locator` is present only on hits from a {@link IFragmentVectorIndex} — it
2456
- * identifies WHICH fragment of the record matched. Record-granular
2457
- * {@link IVectorIndex} hits omit it.
2601
+ * **No single field discriminates a fragment hit from a record-granular hit.** A
2602
+ * record hit carries neither `locator` nor `fragmentId`; a fragment hit carries at
2603
+ * least one of the two, but not necessarily any particular one — a fragment with a
2604
+ * body span but no consumer-minted id, and a fragment with an id but no honest span,
2605
+ * are both legal. Testing one field for presence therefore cannot tell you which
2606
+ * kind of hit you hold.
2607
+ *
2608
+ * That "at least one" requirement is enforced on the upsert side by
2609
+ * {@link embeddedFragmentConverter} — a different boundary from this type — and is
2610
+ * deliberately NOT offered here as a discriminator either. A caller keyed off it
2611
+ * would be coupled to an invariant this type does not own, and would fail silently
2612
+ * if the invariant were ever relaxed.
2613
+ *
2614
+ * **The robust rule is that fragment-ness is determined by which index produced the
2615
+ * hit**: {@link IFragmentVectorIndex.query} returns fragment hits and
2616
+ * {@link IVectorIndex.query} returns record hits. The caller chose the index it
2617
+ * queried, so it already knows which kind it is holding.
2618
+ *
2619
+ * Note in particular that an absent `locator` now carries **two** distinct meanings
2620
+ * — a record-granular hit, or a fragment with no honest body span (see
2621
+ * {@link IFragmentLocator}) — which is precisely why presence-branching is unsafe.
2458
2622
  * @public
2459
2623
  */
2460
2624
  export declare interface IVectorQueryHit {
@@ -2462,8 +2626,18 @@ export declare interface IVectorQueryHit {
2462
2626
  readonly target: IEdgeTarget;
2463
2627
  /** Backend similarity score; higher is more similar. */
2464
2628
  readonly score: number;
2465
- /** The matched fragment's in-record span; present only for fragment-index hits. */
2629
+ /**
2630
+ * The advisory in-record span the matched fragment was derived from, when the
2631
+ * producing fragment carried one. Absent on record-granular hits AND on fragment
2632
+ * hits with no honest span — see the remarks above; do not branch on its presence.
2633
+ */
2466
2634
  readonly locator?: IFragmentLocator;
2635
+ /**
2636
+ * The opaque identity the producing fragment was stored with, carried back
2637
+ * verbatim. Absent on record-granular hits AND on fragment hits stored without
2638
+ * one — see the remarks above; do not branch on its presence.
2639
+ */
2640
+ readonly fragmentId?: string;
2467
2641
  }
2468
2642
 
2469
2643
  /**
@@ -2504,6 +2678,27 @@ export declare interface IWritePolicy {
2504
2678
  * record. Called when admission is `accept` AND a record with the same
2505
2679
  * `entityId` already exists (an update, not a first write).
2506
2680
  *
2681
+ * @remarks
2682
+ * **RFC-7386 semantics are the contract here, not an artifact of the shipped
2683
+ * policies' merge configuration.** Implementations are expected to honor them,
2684
+ * and consumers may rely on them:
2685
+ *
2686
+ * - **Objects merge per key.** A supplied key overwrites; an omitted key is
2687
+ * PRESERVED, not dropped. Patching one key of a nested object (e.g. one
2688
+ * field of `provenance`) leaves its siblings intact.
2689
+ * - **An explicit `null` on a sub-key clears exactly that sub-key.** This is
2690
+ * the sanctioned way to remove a single key from a nested object.
2691
+ * - **Arrays replace wholesale** — `tags` / `links` are never element-merged.
2692
+ * - **A whole-block `null` that would delete a REQUIRED field is rejected
2693
+ * loudly** (`Result.fail`), never silently accepted. `body` / `tags` /
2694
+ * `links` / `provenance` are required; `embeddingRef` is optional and a
2695
+ * `null` restores it to absent (NOT to `null`), so it stays hash-stable.
2696
+ *
2697
+ * Which fields these guarantees cover is **policy-dependent**: they apply to
2698
+ * the fields the policy declares in {@link IWritePolicy.mutableFields}, and a
2699
+ * field outside that list is inert — its patch key is dropped before the merge,
2700
+ * so a `null` on it neither clears the value nor raises an error.
2701
+ *
2507
2702
  * @param existing - The current persisted record.
2508
2703
  * @param patch - A partial JSON object in Merge Patch format. `null`
2509
2704
  * deletes the corresponding key; arrays replace wholesale; nested objects
@@ -2564,13 +2759,21 @@ export declare class KnowledgeIdentityCodec implements IIdentityCodec {
2564
2759
  * view, runs the merge over that view, then rebuilds a coherent record. The
2565
2760
  * identity and transaction-time envelope fields (`id`, `entityId`, `kind`,
2566
2761
  * `created`, `updated`, `seq`, `contentHash`) are NOT mutable and are
2567
- * preserved verbatim; the store stamps `updated` / `seq` on write.
2762
+ * preserved verbatim; the store stamps `updated` / `seq` / `contentHash` on write.
2763
+ *
2764
+ * Because the surface is pinned rather than caller-supplied, the RFC-7386
2765
+ * guarantees documented on {@link IWritePolicy.applyUpdate} apply to every field
2766
+ * listed above — in particular to `provenance`, whose keys merge individually, a
2767
+ * `null` on any one of which clears that key alone, and a `null` on the whole
2768
+ * block of which is rejected loudly (it is a required field). Consumers may
2769
+ * depend on this; it is covered by tests.
2568
2770
  * @public
2569
2771
  */
2570
2772
  export declare class KnowledgeLwwPolicy implements IWritePolicy {
2571
2773
  /**
2572
2774
  * The knowledge mutable surface: the body plus the envelope metadata a
2573
- * consumer may revise without minting a new entity.
2775
+ * consumer may revise without minting a new entity. Pinned, not
2776
+ * caller-supplied — see the class remarks for what that guarantees.
2574
2777
  */
2575
2778
  readonly mutableFields: ReadonlyArray<string>;
2576
2779
  /**
@@ -102,6 +102,9 @@ export const edgeTargetConverter: Converter<IEdgeTarget>;
102
102
  // @public
103
103
  export function edgeTargetKey(target: IEdgeTarget): string;
104
104
 
105
+ // @public
106
+ export const embeddedFragmentConverter: Converter<IEmbeddedFragment>;
107
+
105
108
  // @public
106
109
  export type EntityId = Brand<string, 'EntityId'>;
107
110
 
@@ -130,6 +133,9 @@ export const FRAGMENT_SEMANTIC_UNWIRED_MESSAGE: string;
130
133
  // @public
131
134
  export type FragmentEmbedder = (record: IMemoryRecord<unknown>) => Promise<Result<ReadonlyArray<IEmbeddedFragment>>>;
132
135
 
136
+ // @public
137
+ export const fragmentLocatorConverter: Converter<IFragmentLocator>;
138
+
133
139
  // @public
134
140
  export class FragmentSemanticRetriever {
135
141
  get capabilities(): IFragmentRetrieverCapabilities;
@@ -221,7 +227,8 @@ export interface IEdgeTarget {
221
227
 
222
228
  // @public
223
229
  export interface IEmbeddedFragment {
224
- readonly locator: IFragmentLocator;
230
+ readonly fragmentId?: string;
231
+ readonly locator?: IFragmentLocator;
225
232
  readonly vector: Float32Array;
226
233
  }
227
234
 
@@ -250,6 +257,7 @@ export interface IFileTreeMemoryStoreCreateParams {
250
257
  readonly embed?: MemoryEmbedder;
251
258
  readonly fragmentEmbedder?: FragmentEmbedder;
252
259
  readonly fragmentIndex?: IFragmentVectorIndex;
260
+ readonly index?: IMemoryIndex;
253
261
  readonly logger?: Logging.ILogger;
254
262
  readonly observers?: ReadonlyArray<IMemoryObserver>;
255
263
  readonly onRecordError?: MemoryRecordErrorMode;
@@ -653,6 +661,7 @@ export interface IVectorIndex {
653
661
 
654
662
  // @public
655
663
  export interface IVectorQueryHit {
664
+ readonly fragmentId?: string;
656
665
  readonly locator?: IFragmentLocator;
657
666
  readonly score: number;
658
667
  readonly target: IEdgeTarget;
@@ -49,16 +49,19 @@ export interface IFragmentRetrieverCapabilities {
49
49
  * The sub-document semantic-search retriever — the "discovery" half of a
50
50
  * search-then-read contract. It embeds a fragment query, queries the
51
51
  * {@link IFragmentVectorIndex}, and returns the raw per-fragment
52
- * {@link IVectorQueryHit | hits} (each carrying a record `target` AND the matched
53
- * `locator`), NOT resolved records: the consumer re-reads each record and slices it
54
- * by the locator on its own read side.
52
+ * {@link IVectorQueryHit | hits} (each carrying a record `target` AND whichever of
53
+ * `locator` / `fragmentId` the stored fragment was indexed with), NOT resolved
54
+ * records: the consumer re-reads each record and resolves the fragment on its own
55
+ * read side. Note the `locator` span is advisory — see {@link IFragmentLocator}; it
56
+ * is not a slice guaranteed to reproduce the fragment's text.
55
57
  *
56
58
  * @remarks
57
59
  * Deliberately NOT an {@link IMemoryRetriever}: memory recall is record-granular and
58
- * returns records; fragment discovery is span-granular and returns locators. Keeping
59
- * it a distinct surface matches the consumer contract (memory stays record-granular;
60
- * sub-document knowledge uses a separate fragment index) and avoids overloading the
61
- * record retriever's return type with a locator that only makes sense here.
60
+ * returns records; fragment discovery is fragment-granular and returns fragment
61
+ * identities. Keeping it a distinct surface matches the consumer contract (memory
62
+ * stays record-granular; sub-document knowledge uses a separate fragment index) and
63
+ * avoids overloading the record retriever's return type with identity fields that
64
+ * only make sense here.
62
65
  *
63
66
  * When no backend is wired, `supportsFragmentRecall` is `false` and any fragment
64
67
  * query degrades loudly ({@link FRAGMENT_SEMANTIC_UNWIRED_MESSAGE}) — it NEVER
@@ -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;;;;;;;;;;;;;;;;;;;;GAoBG;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,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"}
@@ -17,16 +17,19 @@ exports.FRAGMENT_SEMANTIC_UNWIRED_MESSAGE = 'fragment recall: no fragment index
17
17
  * The sub-document semantic-search retriever — the "discovery" half of a
18
18
  * search-then-read contract. It embeds a fragment query, queries the
19
19
  * {@link IFragmentVectorIndex}, and returns the raw per-fragment
20
- * {@link IVectorQueryHit | hits} (each carrying a record `target` AND the matched
21
- * `locator`), NOT resolved records: the consumer re-reads each record and slices it
22
- * by the locator on its own read side.
20
+ * {@link IVectorQueryHit | hits} (each carrying a record `target` AND whichever of
21
+ * `locator` / `fragmentId` the stored fragment was indexed with), NOT resolved
22
+ * records: the consumer re-reads each record and resolves the fragment on its own
23
+ * read side. Note the `locator` span is advisory — see {@link IFragmentLocator}; it
24
+ * is not a slice guaranteed to reproduce the fragment's text.
23
25
  *
24
26
  * @remarks
25
27
  * Deliberately NOT an {@link IMemoryRetriever}: memory recall is record-granular and
26
- * returns records; fragment discovery is span-granular and returns locators. Keeping
27
- * it a distinct surface matches the consumer contract (memory stays record-granular;
28
- * sub-document knowledge uses a separate fragment index) and avoids overloading the
29
- * record retriever's return type with a locator that only makes sense here.
28
+ * returns records; fragment discovery is fragment-granular and returns fragment
29
+ * identities. Keeping it a distinct surface matches the consumer contract (memory
30
+ * stays record-granular; sub-document knowledge uses a separate fragment index) and
31
+ * avoids overloading the record retriever's return type with identity fields that
32
+ * only make sense here.
30
33
  *
31
34
  * When no backend is wired, `supportsFragmentRecall` is `false` and any fragment
32
35
  * query degrades loudly ({@link FRAGMENT_SEMANTIC_UNWIRED_MESSAGE}) — it NEVER
@@ -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;;;;;;;;;;;;;;;;;;;;GAoBG;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 the matched\n * `locator`), NOT resolved records: the consumer re-reads each record and slices it\n * by the locator on its own read side.\n *\n * @remarks\n * Deliberately NOT an {@link IMemoryRetriever}: memory recall is record-granular and\n * returns records; fragment discovery is span-granular and returns locators. Keeping\n * it a distinct surface matches the consumer contract (memory stays record-granular;\n * sub-document knowledge uses a separate fragment index) and avoids overloading the\n * record retriever's return type with a locator that only makes 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":";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"]}
@@ -2,6 +2,7 @@ import { Logging, Result } from '@fgv/ts-utils';
2
2
  import { FileTree } from '@fgv/ts-json-base';
3
3
  import { EntityId, IIdentityCodec, IMemoryRecord, IWritePolicy, Kind, MemoryId, MemoryScopeKey, RankProjector, Tag } from '../types';
4
4
  import { IBodyConverterRegistry as IRegistry } from '../converters';
5
+ import { IMemoryIndex } from '../index';
5
6
  import { IMemoryObserver } from '../observe';
6
7
  import { FragmentEmbedder, IFragmentVectorIndex, IMemoryRecordSource, IScopedMemoryRecord, IVectorIndex, MemoryEmbedder } from '../vector';
7
8
  /**
@@ -137,6 +138,59 @@ export interface IFileTreeMemoryStoreCreateParams {
137
138
  readonly rankProjectors?: ReadonlyMap<Kind, RankProjector>;
138
139
  /** Default codec for kinds without an explicit entry. */
139
140
  readonly defaultCodec?: IIdentityCodec;
141
+ /**
142
+ * Optional derived-index implementation. Defaults to a fresh {@link MemoryIndex} —
143
+ * omitting this parameter is byte-identical to the store's behavior before the
144
+ * parameter existed. When supplied, the store uses it for EVERY index operation
145
+ * it performs and never holds a second index, so an injected index is the store's
146
+ * only view of its own records:
147
+ *
148
+ * - `rebuild` — once, from the initial vault walk in `create()`.
149
+ * - `patch` — on every persisted write, delete, version invalidation, and
150
+ * cap-cull eviction.
151
+ * - `entries` — behind {@link IMemoryStore.list | list} /
152
+ * {@link IMemoryStore.listScoped | listScoped}, the keyed temporal reads, the
153
+ * write path's content-hash dedup and write-policy admission cohort, AND the
154
+ * temporal (versioned) write and delete paths, which resolve an entity's
155
+ * version history entirely from the index.
156
+ *
157
+ * That last group is the one to weigh before injecting anything other than a
158
+ * pass-through decorator: an index that filters, reorders, or otherwise reshapes
159
+ * `entries()` changes WRITE semantics, not just what reads return. Concretely,
160
+ * on a versioned kind the store derives an entity's whole version history from
161
+ * `entries()` filtered by scope, and that derivation decides which version a
162
+ * `put` treats as current (so what it dedups against and what it merges its
163
+ * patch over), which prior versions it stamps `invalid_at` on, what the
164
+ * admission cohort is, and which versions a `delete` tombstones. An index that
165
+ * hides a version makes it invisible to all of those — the FileTree still holds
166
+ * it, but the store will not supersede, invalidate, or tombstone it. On flat
167
+ * kinds the same reshaping changes what dedups and what a cap-cull policy
168
+ * evicts. A faithful delegating decorator — the intended use below — has no such
169
+ * effect. Note the store's keyed reads ({@link IMemoryStore.get | get} on a flat
170
+ * kind, and {@link IMemoryStore.getById | getById}) go to the FileTree, not the
171
+ * index; the FileTree remains the source of truth and the index stays a derived
172
+ * view.
173
+ *
174
+ * @remarks
175
+ * **This is an instrumentation seam, NOT a resident-memory fix.** The intended
176
+ * use is wrapping the shipped {@link MemoryIndex} in a decorator that counts and
177
+ * times the calls the store makes — resident bytes by kind, open cost against
178
+ * vault size, where the curve actually bends — so a decision about a partial-read
179
+ * redesign can be driven by measurements instead of estimates.
180
+ *
181
+ * It does NOT lower the store's resident-memory ceiling, and injecting a
182
+ * "persisted" or "lazy" index will not change that. {@link IMemoryIndex}'s read
183
+ * surface returns whole records by construction: `entries()` yields
184
+ * {@link IIndexedMemoryRecord}s and `byKind` / `byTag` / `byRecency` / `byRank`
185
+ * yield `IMemoryRecord<unknown>` — `{ envelope, body }` pairs with the body
186
+ * materialized. Any implementation satisfying the current contract must therefore
187
+ * be able to produce every body on demand. An injected index changes WHERE records
188
+ * come from; it does not change WHETHER bodies are held. Lowering the ceiling
189
+ * requires a partial-read (id-or-envelope-only) redesign of `IMemoryIndex` itself,
190
+ * which is separate, breaking, design-first work and is deliberately not part of
191
+ * this seam.
192
+ */
193
+ readonly index?: IMemoryIndex;
140
194
  /** Scope encoding. Defaults to {@link defaultMemoryScopeEncoding}. */
141
195
  readonly scopeEncoding?: (scope: MemoryScopeKey) => Result<string>;
142
196
  /**
@@ -283,11 +337,19 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
283
337
  */
284
338
  get skippedRecords(): ReadonlyArray<ISkippedRecord>;
285
339
  /**
286
- * Family-convention factory. Builds the derived index and a default LWW
287
- * policy, then performs an initial FileTree walk so an existing vault is
288
- * indexed (and the `seq` counter resumes past the highest persisted `seq`).
340
+ * Family-convention factory. Resolves the derived index (the caller's
341
+ * {@link IFileTreeMemoryStoreCreateParams.index | index} when supplied, a fresh
342
+ * {@link MemoryIndex} otherwise) and a default LWW policy, then performs an
343
+ * initial FileTree walk so an existing vault is indexed (and the `seq` counter
344
+ * resumes past the highest persisted `seq`).
289
345
  */
290
346
  static create(params: IFileTreeMemoryStoreCreateParams): Result<FileTreeMemoryStore>;
347
+ /**
348
+ * Resolve the derived index for a `create()`: the caller's injected
349
+ * {@link IMemoryIndex} verbatim, or a fresh {@link MemoryIndex} when none was
350
+ * supplied (the default that keeps an omitting caller byte-identical).
351
+ */
352
+ private static _resolveIndex;
291
353
  /** {@inheritDoc IMemoryStore.get} */
292
354
  get(kind: Kind, entityId: EntityId): Promise<Result<IMemoryRecord<unknown> | undefined>>;
293
355
  /** {@inheritDoc IMemoryStore.getById} */