@fgv/ts-agent-memory 5.1.0-41 → 5.1.0-43

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 (82) hide show
  1. package/.rush/temp/{cbbdbe09515171b4eba8f2592be2dace1e4e8142.tar.log → 285f03271c27ef724d49e730c0db58d9e4ac1a44.tar.log} +38 -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/dist/packlets/retrieve/fragmentSemanticRetriever.js +78 -0
  7. package/dist/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -0
  8. package/dist/packlets/retrieve/index.js +1 -0
  9. package/dist/packlets/retrieve/index.js.map +1 -1
  10. package/dist/packlets/store/fileTreeMemoryStore.js +105 -14
  11. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  12. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +200 -0
  13. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -0
  14. package/dist/packlets/vector/index.js +1 -0
  15. package/dist/packlets/vector/index.js.map +1 -1
  16. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  17. package/dist/test/unit/retrieve/fragmentSemanticRetriever.test.js +116 -0
  18. package/dist/test/unit/retrieve/fragmentSemanticRetriever.test.js.map +1 -0
  19. package/dist/test/unit/store/fragmentEmbedOnWrite.test.js +255 -0
  20. package/dist/test/unit/store/fragmentEmbedOnWrite.test.js.map +1 -0
  21. package/dist/test/unit/store/lenientOpen.test.js +248 -0
  22. package/dist/test/unit/store/lenientOpen.test.js.map +1 -0
  23. package/dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js +297 -0
  24. package/dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js.map +1 -0
  25. package/dist/ts-agent-memory.d.ts +367 -4
  26. package/etc/ts-agent-memory.api.md +78 -0
  27. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts +90 -0
  28. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts.map +1 -0
  29. package/lib/packlets/retrieve/fragmentSemanticRetriever.js +82 -0
  30. package/lib/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -0
  31. package/lib/packlets/retrieve/index.d.ts +1 -0
  32. package/lib/packlets/retrieve/index.d.ts.map +1 -1
  33. package/lib/packlets/retrieve/index.js +1 -0
  34. package/lib/packlets/retrieve/index.js.map +1 -1
  35. package/lib/packlets/store/fileTreeMemoryStore.d.ts +115 -1
  36. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  37. package/lib/packlets/store/fileTreeMemoryStore.js +104 -13
  38. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  39. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +74 -0
  40. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -0
  41. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +204 -0
  42. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -0
  43. package/lib/packlets/vector/index.d.ts +1 -0
  44. package/lib/packlets/vector/index.d.ts.map +1 -1
  45. package/lib/packlets/vector/index.js +1 -0
  46. package/lib/packlets/vector/index.js.map +1 -1
  47. package/lib/packlets/vector/vectorIndex.d.ts +85 -4
  48. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  49. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  50. package/lib/test/unit/retrieve/fragmentSemanticRetriever.test.d.ts +2 -0
  51. package/lib/test/unit/retrieve/fragmentSemanticRetriever.test.d.ts.map +1 -0
  52. package/lib/test/unit/retrieve/fragmentSemanticRetriever.test.js +118 -0
  53. package/lib/test/unit/retrieve/fragmentSemanticRetriever.test.js.map +1 -0
  54. package/lib/test/unit/store/fragmentEmbedOnWrite.test.d.ts +2 -0
  55. package/lib/test/unit/store/fragmentEmbedOnWrite.test.d.ts.map +1 -0
  56. package/lib/test/unit/store/fragmentEmbedOnWrite.test.js +257 -0
  57. package/lib/test/unit/store/fragmentEmbedOnWrite.test.js.map +1 -0
  58. package/lib/test/unit/store/lenientOpen.test.d.ts +2 -0
  59. package/lib/test/unit/store/lenientOpen.test.d.ts.map +1 -0
  60. package/lib/test/unit/store/lenientOpen.test.js +250 -0
  61. package/lib/test/unit/store/lenientOpen.test.js.map +1 -0
  62. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.d.ts +2 -0
  63. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.d.ts.map +1 -0
  64. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.js +299 -0
  65. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.js.map +1 -0
  66. package/package.json +7 -7
  67. package/rush-logs/ts-agent-memory.build.cache.log +1 -1
  68. package/rush-logs/ts-agent-memory.build.log +2 -2
  69. package/src/packlets/retrieve/fragmentSemanticRetriever.ts +135 -0
  70. package/src/packlets/retrieve/index.ts +1 -0
  71. package/src/packlets/store/fileTreeMemoryStore.ts +208 -16
  72. package/src/packlets/vector/inMemoryFragmentCosineIndex.ts +262 -0
  73. package/src/packlets/vector/index.ts +1 -0
  74. package/src/packlets/vector/vectorIndex.ts +97 -4
  75. package/src/test/unit/retrieve/fragmentSemanticRetriever.test.ts +163 -0
  76. package/src/test/unit/store/fragmentEmbedOnWrite.test.ts +349 -0
  77. package/src/test/unit/store/lenientOpen.test.ts +292 -0
  78. package/src/test/unit/vector/inMemoryFragmentCosineIndex.test.ts +389 -0
  79. package/temp/build/lint/_eslint-5eVG3S6w.json +29 -5
  80. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  81. package/temp/ts-agent-memory.api.json +4584 -2859
  82. package/temp/ts-agent-memory.api.md +78 -0
@@ -323,6 +323,13 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
323
323
  private readonly _logger;
324
324
  private readonly _vectorIndex;
325
325
  private readonly _embed;
326
+ private readonly _fragmentIndex;
327
+ private readonly _fragmentEmbedder;
328
+ /**
329
+ * Records the initial walk could not load. Populated during `create()` in
330
+ * {@link MemoryRecordErrorMode | `'skip'` mode}; empty otherwise.
331
+ */
332
+ private readonly _skippedRecords;
326
333
  /** Monotonic write counter; incremented inside the write-lock on each put. */
327
334
  private _seq;
328
335
  /**
@@ -343,6 +350,15 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
343
350
  */
344
351
  private static readonly _mutableFieldAccessors;
345
352
  private constructor();
353
+ /**
354
+ * Records the initial vault walk could not parse or validate and quarantined
355
+ * (not indexed). Non-empty only when the store was opened with
356
+ * {@link MemoryRecordErrorMode | `onRecordError: 'skip'`} AND at least one
357
+ * record failed to load. Each entry identifies the offending file so a host
358
+ * can repair it; the file itself is never deleted or mutated, so a later open
359
+ * (after the body converter is fixed) re-indexes it.
360
+ */
361
+ get skippedRecords(): ReadonlyArray<ISkippedRecord>;
346
362
  /**
347
363
  * Family-convention factory. Builds the derived index and a default LWW
348
364
  * policy, then performs an initial FileTree walk so an existing vault is
@@ -430,6 +446,26 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
430
446
  * failure).
431
447
  */
432
448
  private _embedOnWrite;
449
+ /**
450
+ * Best-effort fragment-embed-on-write. When a fragment index AND a fragment
451
+ * embedder are wired, chunks + embeds the built record and replaces its
452
+ * fragments in the index (`addFragments` is whole-record-replace, so a re-authored
453
+ * document never leaves stale fragments behind — no explicit remove needed). A
454
+ * failure (returned `fail` OR a thrown/rejected hook) is logged and the record is
455
+ * returned unchanged — the put still persists, and the fragment index is a derived
456
+ * view a later `rebuild` reconciles. Unlike {@link FileTreeMemoryStore._embedOnWrite}
457
+ * it stamps nothing on the record (fragments have no per-record `embeddingRef`
458
+ * analog). A pass-through no-op when unwired (byte-identical record).
459
+ */
460
+ private _embedFragmentsOnWrite;
461
+ /**
462
+ * Best-effort fragment removal. A no-op unless the full fragment lifecycle is
463
+ * wired (both an index AND an embedder), so an unwired store does no fragment
464
+ * work and behaves byte-identically. Failures are logged, never surfaced — a
465
+ * committed delete/eviction must not fail because a derived fragment index could
466
+ * not be pruned.
467
+ */
468
+ private _removeFragmentsBestEffort;
433
469
  /**
434
470
  * Evict the records named by a `cull-oldest` decision, best-effort. Runs only
435
471
  * after the authoritative `_persist`, so a failed eviction is logged (never
@@ -625,10 +661,83 @@ export declare class FileTreeMemoryStore implements IMemoryStore {
625
661
  /**
626
662
  * Walk the FileTree once and rebuild the index. Also resumes the `seq`
627
663
  * counter past the highest persisted `seq` so new writes stay monotonic.
664
+ *
665
+ * In `'skip'` mode each per-record failure is captured structurally on
666
+ * `this._skippedRecords` (path + scope + path-tagged error) at its failure
667
+ * site and logged at `warn`; the walk keeps every record that loaded.
628
668
  */
629
669
  private _initialIndex;
630
670
  /** Recursively collect every `.md` record under `dir` (scope = path segments). */
631
671
  private _collectEntries;
672
+ /**
673
+ * Load and verify one record file. On failure in `'skip'` mode, records the
674
+ * structured {@link ISkippedRecord} identity (path + scope + path-tagged
675
+ * error) and logs the skip at `warn`; the failure is still returned so the
676
+ * caller's `mapSuccess` drops it from the loaded set. In `'fail'` mode the
677
+ * failure passes through untouched so the historical error is byte-identical.
678
+ */
679
+ private _loadRecordFile;
680
+ }
681
+
682
+ /**
683
+ * The loud-degradation message a {@link FragmentSemanticRetriever} returns when a
684
+ * fragment query is issued but no {@link IFragmentSemanticBackend | backend} is
685
+ * wired — the discovery surface NEVER answers a fragment query with a silent empty.
686
+ * @public
687
+ */
688
+ export declare const FRAGMENT_SEMANTIC_UNWIRED_MESSAGE: string;
689
+
690
+ /**
691
+ * The fragment-granular sibling of {@link MemoryEmbedder}: chunks a record's body
692
+ * and embeds each chunk, returning one {@link IEmbeddedFragment} per chunk. The
693
+ * chunking policy (window size, overlap) lives entirely in the consumer's embedder
694
+ * — the core stays chunking-agnostic, exactly as it stays embedder-agnostic for
695
+ * the record-granular path. Used by the store's fragment-embed-on-write hook.
696
+ * @public
697
+ */
698
+ export declare type FragmentEmbedder = (record: IMemoryRecord<unknown>) => Promise<Result<ReadonlyArray<IEmbeddedFragment>>>;
699
+
700
+ /**
701
+ * The sub-document semantic-search retriever — the "discovery" half of a
702
+ * search-then-read contract. It embeds a fragment query, queries the
703
+ * {@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.
707
+ *
708
+ * @remarks
709
+ * 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.
714
+ *
715
+ * When no backend is wired, `supportsFragmentRecall` is `false` and any fragment
716
+ * query degrades loudly ({@link FRAGMENT_SEMANTIC_UNWIRED_MESSAGE}) — it NEVER
717
+ * returns a silent empty. A consumer-supplied backend that rejects (throws) is
718
+ * normalized into a `Failure`.
719
+ * @public
720
+ */
721
+ export declare class FragmentSemanticRetriever {
722
+ private readonly _backend;
723
+ private constructor();
724
+ /** What this retriever can do given its wiring. */
725
+ get capabilities(): IFragmentRetrieverCapabilities;
726
+ /** Family-convention factory. */
727
+ static create(params: {
728
+ readonly backend?: IFragmentSemanticBackend;
729
+ }): Result<FragmentSemanticRetriever>;
730
+ /**
731
+ * Embed `query.semantic`, query the fragment index, and return the per-fragment
732
+ * hits in descending score order. Fails loudly when no backend is wired.
733
+ */
734
+ retrieve(query: IFragmentQuery): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
735
+ /**
736
+ * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`
737
+ * and a thrown/rejected promise into a single `fragment recall: <label> failed`
738
+ * `Failure`.
739
+ */
740
+ private static _callBackend;
632
741
  }
633
742
 
634
743
  /**
@@ -906,6 +1015,19 @@ export declare interface IEdgeTarget {
906
1015
  readonly id: MemoryId;
907
1016
  }
908
1017
 
1018
+ /**
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}.
1022
+ * @public
1023
+ */
1024
+ export declare interface IEmbeddedFragment {
1025
+ /** The fragment's in-record span. */
1026
+ readonly locator: IFragmentLocator;
1027
+ /** The embedding vector for that span. */
1028
+ readonly vector: Float32Array;
1029
+ }
1030
+
909
1031
  /**
910
1032
  * A near-duplicate candidate surfaced to the {@link IEntityResolver} by stage-4
911
1033
  * layer-2 similarity search: an existing record whose embedding is within the
@@ -1031,6 +1153,136 @@ export declare interface IFileTreeMemoryStoreCreateParams {
1031
1153
  * `rebuild` reconciles, so a vector failure never fails an authoritative write.
1032
1154
  */
1033
1155
  readonly embed?: MemoryEmbedder;
1156
+ /**
1157
+ * Optional fragment-granular vector index for sub-document semantic search.
1158
+ * Wired together with
1159
+ * {@link IFileTreeMemoryStoreCreateParams.fragmentEmbedder | fragmentEmbedder}:
1160
+ * when both are present the store chunks + embeds each written record and
1161
+ * maintains the fragment index on `put` / `delete` / cap-cull eviction — the
1162
+ * "discovery" half of a search-then-read contract, queried through a
1163
+ * {@link FragmentSemanticRetriever}. Independent of the record-granular
1164
+ * {@link IFileTreeMemoryStoreCreateParams.vectorIndex | vectorIndex} pair: a
1165
+ * store may wire record vectors, fragment vectors, both, or neither. Absent (or
1166
+ * `fragmentEmbedder` absent) → no fragment work happens and the store behaves
1167
+ * byte-identically (the additive, zero-overhead-when-unwired default).
1168
+ */
1169
+ readonly fragmentIndex?: IFragmentVectorIndex;
1170
+ /**
1171
+ * Optional fragment embedder applied to each record on write, wired together
1172
+ * with {@link IFileTreeMemoryStoreCreateParams.fragmentIndex | fragmentIndex}.
1173
+ * The consumer owns the chunking policy (window size, overlap) and the embedding
1174
+ * call; the store stays chunking- and embedder-agnostic. Fragment index
1175
+ * maintenance is **best-effort**, exactly like the record-vector path: a failed
1176
+ * (or throwing) `fragmentEmbedder` / `addFragments` / `remove` is logged at
1177
+ * `warn` and the record operation still succeeds — the fragment index is a
1178
+ * derived view a later `rebuild` reconciles.
1179
+ */
1180
+ readonly fragmentEmbedder?: FragmentEmbedder;
1181
+ /**
1182
+ * How the initial vault walk reacts to a record that fails to parse or
1183
+ * validate. Defaults to `'fail'` — one bad record fails the whole open, the
1184
+ * historical behavior, preserved byte-for-byte. Set `'skip'` to quarantine
1185
+ * unreadable records instead: valid records still load, each skip is logged
1186
+ * at `warn` and surfaced on {@link FileTreeMemoryStore.skippedRecords}, and
1187
+ * the offending file is left untouched for a later (post-fix) re-index. See
1188
+ * {@link MemoryRecordErrorMode}.
1189
+ */
1190
+ readonly onRecordError?: MemoryRecordErrorMode;
1191
+ }
1192
+
1193
+ /**
1194
+ * 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,
1196
+ * `end` exclusive. The unit (character / byte / token offsets) is the consumer's
1197
+ * choice: the index stores the two integers opaquely and never interprets them,
1198
+ * so they line up with whatever locator the consumer's own read side uses.
1199
+ * @public
1200
+ */
1201
+ export declare interface IFragmentLocator {
1202
+ /** Inclusive start offset into the record body. */
1203
+ readonly start: number;
1204
+ /** Exclusive end offset into the record body. */
1205
+ readonly end: number;
1206
+ }
1207
+
1208
+ /**
1209
+ * A sub-document semantic-search request: the natural-language `semantic` text to
1210
+ * match, an optional `topK` result cap (default 10), and an optional
1211
+ * `maxPerRecord` cap that keeps one long document from monopolizing the result.
1212
+ * @public
1213
+ */
1214
+ export declare interface IFragmentQuery {
1215
+ /** The natural-language text to embed and match against stored fragments. */
1216
+ readonly semantic: string;
1217
+ /** Maximum number of fragment hits to return. Defaults to 10. */
1218
+ readonly topK?: number;
1219
+ /**
1220
+ * Maximum number of fragments any single record may contribute to the result.
1221
+ * Applied during selection (before the `topK` cut). Omit for uncapped.
1222
+ */
1223
+ readonly maxPerRecord?: number;
1224
+ }
1225
+
1226
+ /**
1227
+ * What a {@link FragmentSemanticRetriever} can do given its wiring.
1228
+ * @public
1229
+ */
1230
+ export declare interface IFragmentRetrieverCapabilities {
1231
+ /** `true` when a fragment backend is wired and fragment recall is operational. */
1232
+ readonly supportsFragmentRecall: boolean;
1233
+ }
1234
+
1235
+ /**
1236
+ * The fragment backend wired into a {@link FragmentSemanticRetriever}: the fragment
1237
+ * index to query and the embedder that turns the query text into a vector. Both are
1238
+ * required together — a fragment index is useless without a way to embed the query.
1239
+ * @public
1240
+ */
1241
+ export declare interface IFragmentSemanticBackend {
1242
+ /** The fragment-granular vector index to query. */
1243
+ readonly fragmentIndex: IFragmentVectorIndex;
1244
+ /** Turns the query text into a vector. */
1245
+ readonly embedQuery: QueryEmbedder;
1246
+ }
1247
+
1248
+ /**
1249
+ * 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.
1255
+ *
1256
+ * @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
1262
+ * record-granular; sub-document knowledge uses a separate fragment index.
1263
+ * @public
1264
+ */
1265
+ export declare interface IFragmentVectorIndex {
1266
+ /**
1267
+ * Add (or replace) all fragments for the scope-qualified `target`. Whole-record
1268
+ * semantics: every fragment previously held for `target` is dropped and replaced
1269
+ * by `fragments`, so a re-authored document never leaves stale fragments behind.
1270
+ * Returns the number of fragments now held for the record.
1271
+ */
1272
+ addFragments(target: IEdgeTarget, fragments: ReadonlyArray<IEmbeddedFragment>): Promise<Result<number>>;
1273
+ /**
1274
+ * Remove every fragment for the scope-qualified `target`. Returns the removed
1275
+ * target. Idempotent — removing a target with no fragments still succeeds.
1276
+ */
1277
+ remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
1278
+ /**
1279
+ * Return the `topK` nearest fragments to `vector`, in descending score order,
1280
+ * each hit carrying its record `target` and fragment `locator`. When
1281
+ * `maxPerRecord` is supplied, no more than that many fragments of any single
1282
+ * record appear in the result — the cap is applied during selection (before the
1283
+ * `topK` cut) so one long document cannot crowd out others.
1284
+ */
1285
+ query(vector: Float32Array, topK: number, maxPerRecord?: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
1034
1286
  }
1035
1287
 
1036
1288
  /**
@@ -1867,6 +2119,77 @@ export declare class InMemoryCosineIndex implements IVectorIndex {
1867
2119
  private static _cosine;
1868
2120
  }
1869
2121
 
2122
+ /**
2123
+ * The brute-force, in-memory cosine {@link IFragmentVectorIndex} — the
2124
+ * 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.
2128
+ *
2129
+ * @remarks
2130
+ * Same regime and same non-goals as {@link InMemoryCosineIndex}: no external
2131
+ * dependency, no ANN structure, a linear scan over the stored fragments — the seam
2132
+ * ({@link IFragmentVectorIndex}) stays open for a consumer to swap a persistent /
2133
+ * ANN backend once N grows. `addFragments` is whole-record-replace, so re-authoring
2134
+ * a document never leaves stale fragments behind. The index has a single dimension
2135
+ * established by the first fragment added; every subsequent fragment and every
2136
+ * `query` vector must match it or fail loudly — a mismatched dimension is an
2137
+ * embedder-wiring bug, never a silent zero-similarity result.
2138
+ * {@link InMemoryFragmentCosineIndex.rebuild | rebuild} clears the index (and the
2139
+ * established dimension), so a re-embed with a different model is supported.
2140
+ *
2141
+ * The optional `maxPerRecord` cap on `query` is applied **during selection**, before
2142
+ * the `topK` cut, so one long document with many strong fragments cannot crowd every
2143
+ * other record out of the result.
2144
+ * @public
2145
+ */
2146
+ export declare class InMemoryFragmentCosineIndex implements IFragmentVectorIndex {
2147
+ /**
2148
+ * Stored fragments keyed by the canonical {@link edgeTargetKey} of the record's
2149
+ * scope-qualified address, so two records that share a filename stem across
2150
+ * scopes occupy distinct entries and never overwrite each other's fragments.
2151
+ */
2152
+ private readonly _records;
2153
+ /** The dimension of every stored fragment vector; `undefined` until the first `add`. */
2154
+ private _dimension;
2155
+ private constructor();
2156
+ /** The number of records that currently have at least one stored fragment. */
2157
+ get recordCount(): number;
2158
+ /** The total number of fragments currently held across all records. */
2159
+ get fragmentCount(): number;
2160
+ /** Family-convention factory. */
2161
+ static create(): Result<InMemoryFragmentCosineIndex>;
2162
+ /** {@inheritDoc IFragmentVectorIndex.addFragments} */
2163
+ addFragments(target: IEdgeTarget, fragments: ReadonlyArray<IEmbeddedFragment>): Promise<Result<number>>;
2164
+ /** {@inheritDoc IFragmentVectorIndex.remove} */
2165
+ remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
2166
+ /** {@inheritDoc IFragmentVectorIndex.query} */
2167
+ query(vector: Float32Array, topK: number, maxPerRecord?: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
2168
+ /**
2169
+ * Re-embed every record from `source` and rebuild the fragment index from
2170
+ * scratch. Clears the current contents (and the established dimension) first, so
2171
+ * a re-embed with a different model is supported. Returns the total number of
2172
+ * fragments indexed.
2173
+ *
2174
+ * On any failure (list, embed, or add) the index is rolled back to empty rather
2175
+ * than left in a partially-rebuilt state.
2176
+ *
2177
+ * @param source - The scope-qualified record source to re-embed.
2178
+ * @param embed - The fragment embedder applied to each record.
2179
+ */
2180
+ rebuild(source: IMemoryRecordSource, embed: FragmentEmbedder): Promise<Result<number>>;
2181
+ /** Empty the index and forget the established dimension. */
2182
+ private _reset;
2183
+ /** The Euclidean magnitude (L2 norm) of a vector. */
2184
+ private static _magnitude;
2185
+ /**
2186
+ * Cosine similarity between the query (whose magnitude is precomputed once and
2187
+ * reused across the scan) and a stored fragment vector. A zero-magnitude vector
2188
+ * on either side yields `0` rather than `NaN`.
2189
+ */
2190
+ private static _cosine;
2191
+ }
2192
+
1870
2193
  /**
1871
2194
  * Structured provenance for a record or an edge. Never a flat enum — the
1872
2195
  * `[key: string]: unknown` index signature lets a consumer attach an opaque
@@ -1981,6 +2304,21 @@ export declare interface ISemanticRetrieverCreateParams {
1981
2304
  readonly backend?: ISemanticBackend;
1982
2305
  }
1983
2306
 
2307
+ /**
2308
+ * A record that {@link FileTreeMemoryStore.create} could not load and
2309
+ * quarantined (only produced in {@link MemoryRecordErrorMode | `'skip'` mode}).
2310
+ * The `path` identifies WHICH record was skipped so a host can repair it.
2311
+ * @public
2312
+ */
2313
+ export declare interface ISkippedRecord {
2314
+ /** The record file's path within the vault (`<scope>/<filename>.md`). */
2315
+ readonly path: string;
2316
+ /** The scope the record lives under (its parent directory path). */
2317
+ readonly scope: MemoryScopeKey;
2318
+ /** The parse/validation failure message (includes the record path). */
2319
+ readonly error: string;
2320
+ }
2321
+
1984
2322
  /**
1985
2323
  * Narrow an {@link IIdentityCodec} to {@link ITemporalIdentityCodec} by probing
1986
2324
  * for the versioned methods. Used by the store when an `encode` result reports
@@ -2101,10 +2439,11 @@ export declare interface IVectorIndex {
2101
2439
  }
2102
2440
 
2103
2441
  /**
2104
- * A single hit returned by {@link IVectorIndex.query}: the matched record's
2105
- * scope-qualified {@link IEdgeTarget | address} and the backend's similarity
2106
- * score (higher = more similar; the exact scale is backend-defined). Hits are
2107
- * returned in descending score order.
2442
+ * A single hit returned by {@link IVectorIndex.query} (or
2443
+ * {@link IFragmentVectorIndex.query}): the matched record's scope-qualified
2444
+ * {@link IEdgeTarget | address} and the backend's similarity score (higher = more
2445
+ * similar; the exact scale is backend-defined). Hits are returned in descending
2446
+ * score order.
2108
2447
  *
2109
2448
  * @remarks
2110
2449
  * The address is a `(scope, id)` pair, NOT a bare {@link MemoryId} — per-scope
@@ -2112,6 +2451,10 @@ export declare interface IVectorIndex {
2112
2451
  * stem under different scopes, so a bare id could not disambiguate two records
2113
2452
  * that share a stem. The caller re-resolves the hit against the record index by
2114
2453
  * the same scoped address.
2454
+ *
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.
2115
2458
  * @public
2116
2459
  */
2117
2460
  export declare interface IVectorQueryHit {
@@ -2119,6 +2462,8 @@ export declare interface IVectorQueryHit {
2119
2462
  readonly target: IEdgeTarget;
2120
2463
  /** Backend similarity score; higher is more similar. */
2121
2464
  readonly score: number;
2465
+ /** The matched fragment's in-record span; present only for fragment-index hits. */
2466
+ readonly locator?: IFragmentLocator;
2122
2467
  }
2123
2468
 
2124
2469
  /**
@@ -2761,6 +3106,24 @@ export declare class MemoryObservationStore implements IMemoryObserver {
2761
3106
  private static _matches;
2762
3107
  }
2763
3108
 
3109
+ /**
3110
+ * Policy for how {@link FileTreeMemoryStore.create}'s initial vault walk reacts
3111
+ * to a record that fails to parse or validate.
3112
+ *
3113
+ * - `'fail'` (the default) — one unreadable record fails the whole open. The
3114
+ * walk collapses per-record results with `mapResults`, so any single failure
3115
+ * aborts `create()`. This is the historical behavior, preserved byte-for-byte.
3116
+ * - `'skip'` — an unreadable record is quarantined (not indexed) rather than
3117
+ * failing the open. Every record that DOES parse loads normally; each skip is
3118
+ * logged at `warn` and surfaced structurally on
3119
+ * {@link FileTreeMemoryStore.skippedRecords}. The offending file is never
3120
+ * deleted or mutated, so a later open (after the body converter is fixed)
3121
+ * re-indexes it. A vault holds every kind in one store, so a required-field
3122
+ * migration on one kind must not make every other record unreadable.
3123
+ * @public
3124
+ */
3125
+ export declare type MemoryRecordErrorMode = 'skip' | 'fail';
3126
+
2764
3127
  /**
2765
3128
  * Scope path segment. May be multi-segment (`/`-separated) for kinds whose
2766
3129
  * codec maps an entity into a sub-tree (e.g. MTM: `conversations/<id>`). The
@@ -121,6 +121,22 @@ export class FileTreeMemoryStore implements IMemoryStore {
121
121
  list(filter?: IMemoryStoreListFilter): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;
122
122
  listScoped(): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>>;
123
123
  put(record: IMemoryRecord<unknown>): Promise<Result<IMemoryRecord<unknown>>>;
124
+ get skippedRecords(): ReadonlyArray<ISkippedRecord>;
125
+ }
126
+
127
+ // @public
128
+ export const FRAGMENT_SEMANTIC_UNWIRED_MESSAGE: string;
129
+
130
+ // @public
131
+ export type FragmentEmbedder = (record: IMemoryRecord<unknown>) => Promise<Result<ReadonlyArray<IEmbeddedFragment>>>;
132
+
133
+ // @public
134
+ export class FragmentSemanticRetriever {
135
+ get capabilities(): IFragmentRetrieverCapabilities;
136
+ static create(params: {
137
+ readonly backend?: IFragmentSemanticBackend;
138
+ }): Result<FragmentSemanticRetriever>;
139
+ retrieve(query: IFragmentQuery): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
124
140
  }
125
141
 
126
142
  // @public
@@ -203,6 +219,12 @@ export interface IEdgeTarget {
203
219
  readonly scope: MemoryScopeKey;
204
220
  }
205
221
 
222
+ // @public
223
+ export interface IEmbeddedFragment {
224
+ readonly locator: IFragmentLocator;
225
+ readonly vector: Float32Array;
226
+ }
227
+
206
228
  // @public
207
229
  export interface IEntityResolutionCandidate {
208
230
  readonly record: IMemoryRecord<unknown>;
@@ -226,8 +248,11 @@ export interface IFileTreeMemoryStoreCreateParams {
226
248
  readonly codecs?: ReadonlyMap<Kind, IIdentityCodec>;
227
249
  readonly defaultCodec?: IIdentityCodec;
228
250
  readonly embed?: MemoryEmbedder;
251
+ readonly fragmentEmbedder?: FragmentEmbedder;
252
+ readonly fragmentIndex?: IFragmentVectorIndex;
229
253
  readonly logger?: Logging.ILogger;
230
254
  readonly observers?: ReadonlyArray<IMemoryObserver>;
255
+ readonly onRecordError?: MemoryRecordErrorMode;
231
256
  readonly rankProjectors?: ReadonlyMap<Kind, RankProjector>;
232
257
  readonly registry: IBodyConverterRegistry;
233
258
  readonly root: FileTree.IMutableFileTreeDirectoryItem;
@@ -236,6 +261,37 @@ export interface IFileTreeMemoryStoreCreateParams {
236
261
  readonly writePolicies?: ReadonlyMap<Kind, IWritePolicy>;
237
262
  }
238
263
 
264
+ // @public
265
+ export interface IFragmentLocator {
266
+ readonly end: number;
267
+ readonly start: number;
268
+ }
269
+
270
+ // @public
271
+ export interface IFragmentQuery {
272
+ readonly maxPerRecord?: number;
273
+ readonly semantic: string;
274
+ readonly topK?: number;
275
+ }
276
+
277
+ // @public
278
+ export interface IFragmentRetrieverCapabilities {
279
+ readonly supportsFragmentRecall: boolean;
280
+ }
281
+
282
+ // @public
283
+ export interface IFragmentSemanticBackend {
284
+ readonly embedQuery: QueryEmbedder;
285
+ readonly fragmentIndex: IFragmentVectorIndex;
286
+ }
287
+
288
+ // @public
289
+ export interface IFragmentVectorIndex {
290
+ addFragments(target: IEdgeTarget, fragments: ReadonlyArray<IEmbeddedFragment>): Promise<Result<number>>;
291
+ query(vector: Float32Array, topK: number, maxPerRecord?: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
292
+ remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
293
+ }
294
+
239
295
  // @public
240
296
  export interface IIdentityCodec {
241
297
  decode(scope: MemoryScopeKey, encodedStem: string): Result<EntityId>;
@@ -495,6 +551,17 @@ export class InMemoryCosineIndex implements IVectorIndex {
495
551
  get size(): number;
496
552
  }
497
553
 
554
+ // @public
555
+ export class InMemoryFragmentCosineIndex implements IFragmentVectorIndex {
556
+ addFragments(target: IEdgeTarget, fragments: ReadonlyArray<IEmbeddedFragment>): Promise<Result<number>>;
557
+ static create(): Result<InMemoryFragmentCosineIndex>;
558
+ get fragmentCount(): number;
559
+ query(vector: Float32Array, topK: number, maxPerRecord?: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
560
+ rebuild(source: IMemoryRecordSource, embed: FragmentEmbedder): Promise<Result<number>>;
561
+ get recordCount(): number;
562
+ remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
563
+ }
564
+
498
565
  // @public
499
566
  export interface IProvenance {
500
567
  readonly [key: string]: unknown;
@@ -540,6 +607,13 @@ export interface ISemanticRetrieverCreateParams {
540
607
  readonly index: IMemoryIndex;
541
608
  }
542
609
 
610
+ // @public
611
+ export interface ISkippedRecord {
612
+ readonly error: string;
613
+ readonly path: string;
614
+ readonly scope: MemoryScopeKey;
615
+ }
616
+
543
617
  // @public
544
618
  export function isTemporalIdentityCodec(codec: IIdentityCodec): codec is ITemporalIdentityCodec;
545
619
 
@@ -579,6 +653,7 @@ export interface IVectorIndex {
579
653
 
580
654
  // @public
581
655
  export interface IVectorQueryHit {
656
+ readonly locator?: IFragmentLocator;
582
657
  readonly score: number;
583
658
  readonly target: IEdgeTarget;
584
659
  }
@@ -698,6 +773,9 @@ export class MemoryObservationStore implements IMemoryObserver {
698
773
  get size(): number;
699
774
  }
700
775
 
776
+ // @public
777
+ export type MemoryRecordErrorMode = 'skip' | 'fail';
778
+
701
779
  // @public
702
780
  export type MemoryScopeKey = Brand<string, 'MemoryScopeKey'>;
703
781
 
@@ -0,0 +1,90 @@
1
+ import { Result } from '@fgv/ts-utils';
2
+ import { IFragmentVectorIndex, IVectorQueryHit } from '../vector';
3
+ import { QueryEmbedder } from './semanticRetriever';
4
+ /**
5
+ * The loud-degradation message a {@link FragmentSemanticRetriever} returns when a
6
+ * fragment query is issued but no {@link IFragmentSemanticBackend | backend} is
7
+ * wired — the discovery surface NEVER answers a fragment query with a silent empty.
8
+ * @public
9
+ */
10
+ export declare const FRAGMENT_SEMANTIC_UNWIRED_MESSAGE: string;
11
+ /**
12
+ * The fragment backend wired into a {@link FragmentSemanticRetriever}: the fragment
13
+ * index to query and the embedder that turns the query text into a vector. Both are
14
+ * required together — a fragment index is useless without a way to embed the query.
15
+ * @public
16
+ */
17
+ export interface IFragmentSemanticBackend {
18
+ /** The fragment-granular vector index to query. */
19
+ readonly fragmentIndex: IFragmentVectorIndex;
20
+ /** Turns the query text into a vector. */
21
+ readonly embedQuery: QueryEmbedder;
22
+ }
23
+ /**
24
+ * A sub-document semantic-search request: the natural-language `semantic` text to
25
+ * match, an optional `topK` result cap (default 10), and an optional
26
+ * `maxPerRecord` cap that keeps one long document from monopolizing the result.
27
+ * @public
28
+ */
29
+ export interface IFragmentQuery {
30
+ /** The natural-language text to embed and match against stored fragments. */
31
+ readonly semantic: string;
32
+ /** Maximum number of fragment hits to return. Defaults to 10. */
33
+ readonly topK?: number;
34
+ /**
35
+ * Maximum number of fragments any single record may contribute to the result.
36
+ * Applied during selection (before the `topK` cut). Omit for uncapped.
37
+ */
38
+ readonly maxPerRecord?: number;
39
+ }
40
+ /**
41
+ * What a {@link FragmentSemanticRetriever} can do given its wiring.
42
+ * @public
43
+ */
44
+ export interface IFragmentRetrieverCapabilities {
45
+ /** `true` when a fragment backend is wired and fragment recall is operational. */
46
+ readonly supportsFragmentRecall: boolean;
47
+ }
48
+ /**
49
+ * The sub-document semantic-search retriever — the "discovery" half of a
50
+ * search-then-read contract. It embeds a fragment query, queries the
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.
55
+ *
56
+ * @remarks
57
+ * 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.
62
+ *
63
+ * When no backend is wired, `supportsFragmentRecall` is `false` and any fragment
64
+ * query degrades loudly ({@link FRAGMENT_SEMANTIC_UNWIRED_MESSAGE}) — it NEVER
65
+ * returns a silent empty. A consumer-supplied backend that rejects (throws) is
66
+ * normalized into a `Failure`.
67
+ * @public
68
+ */
69
+ export declare class FragmentSemanticRetriever {
70
+ private readonly _backend;
71
+ private constructor();
72
+ /** What this retriever can do given its wiring. */
73
+ get capabilities(): IFragmentRetrieverCapabilities;
74
+ /** Family-convention factory. */
75
+ static create(params: {
76
+ readonly backend?: IFragmentSemanticBackend;
77
+ }): Result<FragmentSemanticRetriever>;
78
+ /**
79
+ * Embed `query.semantic`, query the fragment index, and return the per-fragment
80
+ * hits in descending score order. Fails loudly when no backend is wired.
81
+ */
82
+ retrieve(query: IFragmentQuery): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
83
+ /**
84
+ * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`
85
+ * and a thrown/rejected promise into a single `fragment recall: <label> failed`
86
+ * `Failure`.
87
+ */
88
+ private static _callBackend;
89
+ }
90
+ //# sourceMappingURL=fragmentSemanticRetriever.d.ts.map
@@ -0,0 +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"}