@fgv/ts-agent-memory 5.1.0-44 → 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/{1341a168eb616364e2042445bc2dd1efb770748f.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
@@ -1,5 +1,5 @@
1
- Start time: Mon Jul 27 2026 03:02:31 GMT+0000 (Coordinated Universal Time)
2
- Invoking "/usr/bin/tar -c -f /home/runner/work/fgv/fgv/common/temp/build-cache/1341a168eb616364e2042445bc2dd1efb770748f-436d875cd3de7be5.temp -z --files-from=-"
1
+ Start time: Sat Aug 01 2026 02:41:15 GMT+0000 (Coordinated Universal Time)
2
+ Invoking "/usr/bin/tar -c -f /home/runner/work/fgv/fgv/common/temp/build-cache/6e1299d1cebb080772163a532c221cd181b54f9f-dad54f1e2d176bf4.temp -z --files-from=-"
3
3
 
4
4
  ======= BEGIN PROCESS INPUT ======
5
5
  .rush/temp/operation/build/all.log
@@ -77,6 +77,8 @@ dist/packlets/types/temporal.js
77
77
  dist/packlets/types/temporal.js.map
78
78
  dist/packlets/types/writePolicy.js
79
79
  dist/packlets/types/writePolicy.js.map
80
+ dist/packlets/vector/fragmentConverters.js
81
+ dist/packlets/vector/fragmentConverters.js.map
80
82
  dist/packlets/vector/inMemoryCosineIndex.js
81
83
  dist/packlets/vector/inMemoryCosineIndex.js.map
82
84
  dist/packlets/vector/inMemoryFragmentCosineIndex.js
@@ -117,6 +119,8 @@ dist/test/unit/store/fileTreeMemoryStore.test.js
117
119
  dist/test/unit/store/fileTreeMemoryStore.test.js.map
118
120
  dist/test/unit/store/fragmentEmbedOnWrite.test.js
119
121
  dist/test/unit/store/fragmentEmbedOnWrite.test.js.map
122
+ dist/test/unit/store/indexInjection.test.js
123
+ dist/test/unit/store/indexInjection.test.js.map
120
124
  dist/test/unit/store/lenientOpen.test.js
121
125
  dist/test/unit/store/lenientOpen.test.js.map
122
126
  dist/test/unit/store/listScoped.test.js
@@ -141,6 +145,8 @@ dist/test/unit/types/temporalPolicy.test.js
141
145
  dist/test/unit/types/temporalPolicy.test.js.map
142
146
  dist/test/unit/types/writePolicy.test.js
143
147
  dist/test/unit/types/writePolicy.test.js.map
148
+ dist/test/unit/vector/fragmentConverters.test.js
149
+ dist/test/unit/vector/fragmentConverters.test.js.map
144
150
  dist/test/unit/vector/inMemoryCosineIndex.test.js
145
151
  dist/test/unit/vector/inMemoryCosineIndex.test.js.map
146
152
  dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js
@@ -293,6 +299,10 @@ lib/packlets/types/writePolicy.d.ts
293
299
  lib/packlets/types/writePolicy.d.ts.map
294
300
  lib/packlets/types/writePolicy.js
295
301
  lib/packlets/types/writePolicy.js.map
302
+ lib/packlets/vector/fragmentConverters.d.ts
303
+ lib/packlets/vector/fragmentConverters.d.ts.map
304
+ lib/packlets/vector/fragmentConverters.js
305
+ lib/packlets/vector/fragmentConverters.js.map
296
306
  lib/packlets/vector/inMemoryCosineIndex.d.ts
297
307
  lib/packlets/vector/inMemoryCosineIndex.d.ts.map
298
308
  lib/packlets/vector/inMemoryCosineIndex.js
@@ -373,6 +383,10 @@ lib/test/unit/store/fragmentEmbedOnWrite.test.d.ts
373
383
  lib/test/unit/store/fragmentEmbedOnWrite.test.d.ts.map
374
384
  lib/test/unit/store/fragmentEmbedOnWrite.test.js
375
385
  lib/test/unit/store/fragmentEmbedOnWrite.test.js.map
386
+ lib/test/unit/store/indexInjection.test.d.ts
387
+ lib/test/unit/store/indexInjection.test.d.ts.map
388
+ lib/test/unit/store/indexInjection.test.js
389
+ lib/test/unit/store/indexInjection.test.js.map
376
390
  lib/test/unit/store/lenientOpen.test.d.ts
377
391
  lib/test/unit/store/lenientOpen.test.d.ts.map
378
392
  lib/test/unit/store/lenientOpen.test.js
@@ -421,6 +435,10 @@ lib/test/unit/types/writePolicy.test.d.ts
421
435
  lib/test/unit/types/writePolicy.test.d.ts.map
422
436
  lib/test/unit/types/writePolicy.test.js
423
437
  lib/test/unit/types/writePolicy.test.js.map
438
+ lib/test/unit/vector/fragmentConverters.test.d.ts
439
+ lib/test/unit/vector/fragmentConverters.test.d.ts.map
440
+ lib/test/unit/vector/fragmentConverters.test.js
441
+ lib/test/unit/vector/fragmentConverters.test.js.map
424
442
  lib/test/unit/vector/inMemoryCosineIndex.test.d.ts
425
443
  lib/test/unit/vector/inMemoryCosineIndex.test.d.ts.map
426
444
  lib/test/unit/vector/inMemoryCosineIndex.test.js
@@ -5,5 +5,5 @@
5
5
  {"kind":"O","text":"[build:lint] Using ESLint version 9.39.5\n"}
6
6
  {"kind":"O","text":"[build:api-extractor] Using API Extractor version 7.58.9\n"}
7
7
  {"kind":"O","text":"[build:api-extractor] Analysis will use the bundled TypeScript version 5.9.3\n"}
8
- {"kind":"O","text":" ---- build finished (31.973s) ---- \n"}
9
- {"kind":"O","text":"-------------------- Finished (31.982s) --------------------\n"}
8
+ {"kind":"O","text":" ---- build finished (28.708s) ---- \n"}
9
+ {"kind":"O","text":"-------------------- Finished (28.714s) --------------------\n"}
@@ -5,5 +5,5 @@ Invoking: heft build --clean
5
5
  [build:lint] Using ESLint version 9.39.5
6
6
  [build:api-extractor] Using API Extractor version 7.58.9
7
7
  [build:api-extractor] Analysis will use the bundled TypeScript version 5.9.3
8
- ---- build finished (31.973s) ----
9
- -------------------- Finished (31.982s) --------------------
8
+ ---- build finished (28.708s) ----
9
+ -------------------- Finished (28.714s) --------------------
@@ -5,5 +5,5 @@
5
5
  {"kind":"O","text":"[build:lint] Using ESLint version 9.39.5\n"}
6
6
  {"kind":"O","text":"[build:api-extractor] Using API Extractor version 7.58.9\n"}
7
7
  {"kind":"O","text":"[build:api-extractor] Analysis will use the bundled TypeScript version 5.9.3\n"}
8
- {"kind":"O","text":" ---- build finished (31.973s) ---- \n"}
9
- {"kind":"O","text":"-------------------- Finished (31.982s) --------------------\n"}
8
+ {"kind":"O","text":" ---- build finished (28.708s) ---- \n"}
9
+ {"kind":"O","text":"-------------------- Finished (28.714s) --------------------\n"}
@@ -1,3 +1,3 @@
1
1
  {
2
- "nonCachedDurationMs": 32990.523678
2
+ "nonCachedDurationMs": 29559.986292
3
3
  }
package/README.md CHANGED
@@ -6,11 +6,12 @@ edges, content-hash dedup, and the optional-layer seams (vector / temporal /
6
6
  observe). It owns none of a consumer's processing, transformation, or
7
7
  composition logic.
8
8
 
9
- > **Status:** v0.1, under active development. The package is being built
10
- > knowledge-first. This tier (B0) ships the foundational type model and
11
- > converters only — no store, index, retrieval, observe, or vector code yet.
9
+ > **Status:** under active development. The package is being built
10
+ > knowledge-first. The surface listing below covers the foundational type model
11
+ > and converters; the store, index, retrieval, observe, and vector layers ship
12
+ > alongside them and are not itemized here.
12
13
 
13
- ## B0 surface (this tier)
14
+ ## Foundational surface
14
15
 
15
16
  ### `types` packlet
16
17
 
@@ -34,6 +35,80 @@ composition logic.
34
35
  `joinFrontmatter` / `parseMemoryFile` / `serializeMemoryFile` helpers for
35
36
  the `---\n<yaml>\n---\n<body>` memory-file format.
36
37
 
38
+ ## Record updates — the merge contract
39
+
40
+ When a `put` targets an `entityId` that already exists, the write is an
41
+ **update**, not a replace. The store projects the incoming record's mutable
42
+ fields into a patch and hands it to the kind's `IWritePolicy.applyUpdate`, which
43
+ applies it as an **RFC-7386 JSON Merge Patch** over the policy's declared
44
+ mutable surface. This is a **pinned contract**, not an artifact of the current
45
+ merge configuration — the shipped policies compose `@fgv/ts-json`'s `JsonEditor`
46
+ with `{ nullAsDelete: true, arrayMergeBehavior: 'replace' }` specifically to get
47
+ RFC-7386 semantics, and `applyUpdate` documents them as the interface contract.
48
+
49
+ Within the policy's declared mutable surface:
50
+
51
+ - **Objects merge per key.** Keys you supply overwrite; keys you omit are
52
+ **preserved**, not dropped. An update that sets one provenance field leaves
53
+ every sibling field intact.
54
+ - **An explicit `null` on a sub-key clears that sub-key.** This is the
55
+ sanctioned way to remove a single key from a nested object such as
56
+ `provenance`. The cleared key is gone from the persisted record and does not
57
+ reappear on reload.
58
+ - **Arrays replace wholesale.** `tags` and `links` are not element-merged.
59
+ - **A whole-block `null` on a required field is rejected loudly**, never
60
+ silently accepted. `body`, `tags`, `links`, and `provenance` are required; a
61
+ patch that deletes one fails with
62
+ `... merge patch may not delete required field(s): <names>`. (`embeddingRef`
63
+ is optional and *may* be cleared by a `null`, which restores it to absent
64
+ rather than to `null`.)
65
+ - **Keys outside the mutable surface are ignored.** Identity and
66
+ transaction-time envelope fields (`id`, `entityId`, `kind`, `created`,
67
+ `updated`, `seq`, `contentHash`) are preserved verbatim; the store stamps
68
+ `updated` / `seq` / `contentHash` on write.
69
+
70
+ ### `provenance` is on the pinned surface
71
+
72
+ `KnowledgeLwwPolicy` and `TemporalVersionedPolicy` both hard-code their mutable
73
+ surface as `['body', 'tags', 'links', 'provenance', 'embeddingRef']`. For those
74
+ policies the guarantees above apply to `provenance` specifically: per-key merge,
75
+ sub-key clearing via `null`, and loud rejection of a whole-block delete are all
76
+ part of the contract and are pinned by tests.
77
+
78
+ ```ts
79
+ // existing.envelope.provenance: { source: 'agent', confidence: 0.9, note: 'stale' }
80
+
81
+ // clear one sub-key; siblings survive
82
+ policy.applyUpdate(existing, { provenance: { note: null } });
83
+ // => provenance: { source: 'agent', confidence: 0.9 }
84
+
85
+ // revise one sub-key; siblings survive
86
+ policy.applyUpdate(existing, { provenance: { confidence: 0.5 } });
87
+ // => provenance: { source: 'agent', confidence: 0.5, note: 'stale' }
88
+
89
+ // delete the whole block — rejected, not silently accepted
90
+ policy.applyUpdate(existing, { provenance: null });
91
+ // => Result.fail('knowledge LWW: merge patch may not delete required field(s): provenance')
92
+ ```
93
+
94
+ Going through the store rather than calling a policy directly, the same three
95
+ cases are expressed as the `provenance` value on the record you `put`: include a
96
+ key to set it, set a key to `null` to clear it, and omit a key to leave it
97
+ alone. A whole-block delete is not expressible there at all —
98
+ `IMemoryEnvelope.provenance` is non-nullable and `envelopeConverter` rejects
99
+ `null` — so that case fails at the converter instead of at the policy. Either
100
+ way it fails loudly.
101
+
102
+ ### The surface is policy-dependent
103
+
104
+ `MemoryCapCullPolicy` takes its `mutableFields` from the caller, so **what it
105
+ guarantees depends on what you declared**. A field you did not declare mutable
106
+ is not merged at all: patch keys naming it are dropped before the merge runs, so
107
+ a `null` on it neither clears anything nor raises an error — it is simply inert,
108
+ and the existing value is preserved verbatim. If you want the guarantees above
109
+ for `provenance` under a cap-cull policy, `provenance` must appear in the
110
+ `mutableFields` you pass to `MemoryCapCullPolicy.create`.
111
+
37
112
  ## Conventions
38
113
 
39
114
  `Result<T>` on every fallible operation; no `any`; Converters/Validators for
@@ -14,16 +14,19 @@ export const FRAGMENT_SEMANTIC_UNWIRED_MESSAGE = 'fragment recall: no fragment i
14
14
  * The sub-document semantic-search retriever — the "discovery" half of a
15
15
  * search-then-read contract. It embeds a fragment query, queries the
16
16
  * {@link IFragmentVectorIndex}, and returns the raw per-fragment
17
- * {@link IVectorQueryHit | hits} (each carrying a record `target` AND the matched
18
- * `locator`), NOT resolved records: the consumer re-reads each record and slices it
19
- * by the locator on its own read side.
17
+ * {@link IVectorQueryHit | hits} (each carrying a record `target` AND whichever of
18
+ * `locator` / `fragmentId` the stored fragment was indexed with), NOT resolved
19
+ * records: the consumer re-reads each record and resolves the fragment on its own
20
+ * read side. Note the `locator` span is advisory — see {@link IFragmentLocator}; it
21
+ * is not a slice guaranteed to reproduce the fragment's text.
20
22
  *
21
23
  * @remarks
22
24
  * Deliberately NOT an {@link IMemoryRetriever}: memory recall is record-granular and
23
- * returns records; fragment discovery is span-granular and returns locators. Keeping
24
- * it a distinct surface matches the consumer contract (memory stays record-granular;
25
- * sub-document knowledge uses a separate fragment index) and avoids overloading the
26
- * record retriever's return type with a locator that only makes sense here.
25
+ * returns records; fragment discovery is fragment-granular and returns fragment
26
+ * identities. Keeping it a distinct surface matches the consumer contract (memory
27
+ * stays record-granular; sub-document knowledge uses a separate fragment index) and
28
+ * avoids overloading the record retriever's return type with identity fields that
29
+ * only make sense here.
27
30
  *
28
31
  * When no backend is wired, `supportsFragmentRecall` is `false` and any fragment
29
32
  * 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,OAAO,EAAU,IAAI,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAItD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAC5C,6GAA6G,CAAC;AA0ChH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,OAAO,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,OAAO,CAAC,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,IAAI,CAAC,iCAAiC,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,IAAI,CAAC,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,IAAI,CAAC,oBAAoB,KAAK,YAAY,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;CACF","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,OAAO,EAAU,IAAI,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAItD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAC5C,6GAA6G,CAAC;AA0ChH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,OAAO,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,OAAO,CAAC,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,IAAI,CAAC,iCAAiC,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,IAAI,CAAC,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,IAAI,CAAC,oBAAoB,KAAK,YAAY,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;CACF","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"]}
@@ -60,12 +60,14 @@ export class FileTreeMemoryStore {
60
60
  return this._skippedRecords;
61
61
  }
62
62
  /**
63
- * Family-convention factory. Builds the derived index and a default LWW
64
- * policy, then performs an initial FileTree walk so an existing vault is
65
- * indexed (and the `seq` counter resumes past the highest persisted `seq`).
63
+ * Family-convention factory. Resolves the derived index (the caller's
64
+ * {@link IFileTreeMemoryStoreCreateParams.index | index} when supplied, a fresh
65
+ * {@link MemoryIndex} otherwise) and a default LWW policy, then performs an
66
+ * initial FileTree walk so an existing vault is indexed (and the `seq` counter
67
+ * resumes past the highest persisted `seq`).
66
68
  */
67
69
  static create(params) {
68
- return KnowledgeLwwPolicy.create().onSuccess((defaultPolicy) => MemoryIndex.create().onSuccess((index) => {
70
+ return KnowledgeLwwPolicy.create().onSuccess((defaultPolicy) => FileTreeMemoryStore._resolveIndex(params.index).onSuccess((index) => {
69
71
  var _a, _b, _c, _d, _e, _f, _g, _h;
70
72
  const store = new FileTreeMemoryStore({
71
73
  root: params.root,
@@ -88,6 +90,20 @@ export class FileTreeMemoryStore {
88
90
  return store._initialIndex((_h = params.onRecordError) !== null && _h !== void 0 ? _h : 'fail').onSuccess(() => succeed(store));
89
91
  }));
90
92
  }
93
+ /**
94
+ * Resolve the derived index for a `create()`: the caller's injected
95
+ * {@link IMemoryIndex} verbatim, or a fresh {@link MemoryIndex} when none was
96
+ * supplied (the default that keeps an omitting caller byte-identical).
97
+ */
98
+ static _resolveIndex(index) {
99
+ // Nullish rather than strictly-undefined, matching how every sibling optional
100
+ // param in `create()` handles absence (`params.codecs ?? new Map()`, and so on).
101
+ // A JS caller — or a TS caller arriving through an `unknown` escape hatch —
102
+ // passing `null` otherwise gets `null` installed as the store's index and fails
103
+ // later inside `entries()` with a message that names neither the param nor the
104
+ // cause.
105
+ return index ? succeed(index) : MemoryIndex.create();
106
+ }
91
107
  /** {@inheritDoc IMemoryStore.get} */
92
108
  async get(kind, entityId) {
93
109
  var _a;