@oscharko-dev/keiko-local-knowledge 0.2.0

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 (290) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/bounded-document-extraction.d.ts +27 -0
  3. package/dist/bounded-document-extraction.d.ts.map +1 -0
  4. package/dist/bounded-document-extraction.js +214 -0
  5. package/dist/capsule-lifecycle.d.ts +33 -0
  6. package/dist/capsule-lifecycle.d.ts.map +1 -0
  7. package/dist/capsule-lifecycle.js +292 -0
  8. package/dist/capsule-set-lifecycle.d.ts +15 -0
  9. package/dist/capsule-set-lifecycle.d.ts.map +1 -0
  10. package/dist/capsule-set-lifecycle.js +158 -0
  11. package/dist/chunking/chunker-persist.d.ts +36 -0
  12. package/dist/chunking/chunker-persist.d.ts.map +1 -0
  13. package/dist/chunking/chunker-persist.js +74 -0
  14. package/dist/chunking/chunker-runner.d.ts +9 -0
  15. package/dist/chunking/chunker-runner.d.ts.map +1 -0
  16. package/dist/chunking/chunker-runner.js +218 -0
  17. package/dist/chunking/chunker.d.ts +7 -0
  18. package/dist/chunking/chunker.d.ts.map +1 -0
  19. package/dist/chunking/chunker.js +139 -0
  20. package/dist/chunking/citation-mapper.d.ts +4 -0
  21. package/dist/chunking/citation-mapper.d.ts.map +1 -0
  22. package/dist/chunking/citation-mapper.js +180 -0
  23. package/dist/chunking/index.d.ts +6 -0
  24. package/dist/chunking/index.d.ts.map +1 -0
  25. package/dist/chunking/index.js +8 -0
  26. package/dist/chunking/token-estimator.d.ts +3 -0
  27. package/dist/chunking/token-estimator.d.ts.map +1 -0
  28. package/dist/chunking/token-estimator.js +26 -0
  29. package/dist/chunking/types.d.ts +49 -0
  30. package/dist/chunking/types.d.ts.map +1 -0
  31. package/dist/chunking/types.js +26 -0
  32. package/dist/composition.d.ts +57 -0
  33. package/dist/composition.d.ts.map +1 -0
  34. package/dist/composition.js +310 -0
  35. package/dist/conversation/citation-attacher.d.ts +8 -0
  36. package/dist/conversation/citation-attacher.d.ts.map +1 -0
  37. package/dist/conversation/citation-attacher.js +55 -0
  38. package/dist/conversation/citation-excerpts.d.ts +4 -0
  39. package/dist/conversation/citation-excerpts.d.ts.map +1 -0
  40. package/dist/conversation/citation-excerpts.js +41 -0
  41. package/dist/conversation/grounded-answer-runner.d.ts +9 -0
  42. package/dist/conversation/grounded-answer-runner.d.ts.map +1 -0
  43. package/dist/conversation/grounded-answer-runner.js +61 -0
  44. package/dist/conversation/index.d.ts +5 -0
  45. package/dist/conversation/index.d.ts.map +1 -0
  46. package/dist/conversation/index.js +7 -0
  47. package/dist/conversation/model-gateway-answer-generator.d.ts +28 -0
  48. package/dist/conversation/model-gateway-answer-generator.d.ts.map +1 -0
  49. package/dist/conversation/model-gateway-answer-generator.js +105 -0
  50. package/dist/conversation/types.d.ts +35 -0
  51. package/dist/conversation/types.d.ts.map +1 -0
  52. package/dist/conversation/types.js +24 -0
  53. package/dist/discovery/discovery-runner.d.ts +23 -0
  54. package/dist/discovery/discovery-runner.d.ts.map +1 -0
  55. package/dist/discovery/discovery-runner.js +109 -0
  56. package/dist/discovery/extract-progressive.d.ts +17 -0
  57. package/dist/discovery/extract-progressive.d.ts.map +1 -0
  58. package/dist/discovery/extract-progressive.js +522 -0
  59. package/dist/discovery/extract.d.ts +26 -0
  60. package/dist/discovery/extract.d.ts.map +1 -0
  61. package/dist/discovery/extract.js +906 -0
  62. package/dist/discovery/glob.d.ts +10 -0
  63. package/dist/discovery/glob.d.ts.map +1 -0
  64. package/dist/discovery/glob.js +72 -0
  65. package/dist/discovery/index.d.ts +6 -0
  66. package/dist/discovery/index.d.ts.map +1 -0
  67. package/dist/discovery/index.js +8 -0
  68. package/dist/discovery/media-type.d.ts +4 -0
  69. package/dist/discovery/media-type.d.ts.map +1 -0
  70. package/dist/discovery/media-type.js +62 -0
  71. package/dist/discovery/persist.d.ts +63 -0
  72. package/dist/discovery/persist.d.ts.map +1 -0
  73. package/dist/discovery/persist.js +345 -0
  74. package/dist/discovery/test-support.d.ts +16 -0
  75. package/dist/discovery/test-support.d.ts.map +1 -0
  76. package/dist/discovery/test-support.js +127 -0
  77. package/dist/discovery/types.d.ts +63 -0
  78. package/dist/discovery/types.d.ts.map +1 -0
  79. package/dist/discovery/types.js +28 -0
  80. package/dist/discovery/walk.d.ts +12 -0
  81. package/dist/discovery/walk.d.ts.map +1 -0
  82. package/dist/discovery/walk.js +302 -0
  83. package/dist/errors.d.ts +13 -0
  84. package/dist/errors.d.ts.map +1 -0
  85. package/dist/errors.js +22 -0
  86. package/dist/evaluations/dimensions.d.ts +14 -0
  87. package/dist/evaluations/dimensions.d.ts.map +1 -0
  88. package/dist/evaluations/dimensions.js +191 -0
  89. package/dist/evaluations/fixtures.d.ts +18 -0
  90. package/dist/evaluations/fixtures.d.ts.map +1 -0
  91. package/dist/evaluations/fixtures.js +858 -0
  92. package/dist/evaluations/index.d.ts +7 -0
  93. package/dist/evaluations/index.d.ts.map +1 -0
  94. package/dist/evaluations/index.js +10 -0
  95. package/dist/evaluations/report.d.ts +3 -0
  96. package/dist/evaluations/report.d.ts.map +1 -0
  97. package/dist/evaluations/report.js +31 -0
  98. package/dist/evaluations/runner-seed.d.ts +12 -0
  99. package/dist/evaluations/runner-seed.d.ts.map +1 -0
  100. package/dist/evaluations/runner-seed.js +175 -0
  101. package/dist/evaluations/runner.d.ts +8 -0
  102. package/dist/evaluations/runner.d.ts.map +1 -0
  103. package/dist/evaluations/runner.js +205 -0
  104. package/dist/evaluations/scripted-embedding-adapter.d.ts +13 -0
  105. package/dist/evaluations/scripted-embedding-adapter.d.ts.map +1 -0
  106. package/dist/evaluations/scripted-embedding-adapter.js +163 -0
  107. package/dist/evaluations/types.d.ts +116 -0
  108. package/dist/evaluations/types.d.ts.map +1 -0
  109. package/dist/evaluations/types.js +27 -0
  110. package/dist/index.d.ts +23 -0
  111. package/dist/index.d.ts.map +1 -0
  112. package/dist/index.js +41 -0
  113. package/dist/indexing/bounded-indexing.d.ts +41 -0
  114. package/dist/indexing/bounded-indexing.d.ts.map +1 -0
  115. package/dist/indexing/bounded-indexing.js +240 -0
  116. package/dist/indexing/checkpoint-persist.d.ts +8 -0
  117. package/dist/indexing/checkpoint-persist.d.ts.map +1 -0
  118. package/dist/indexing/checkpoint-persist.js +135 -0
  119. package/dist/indexing/checkpoint-resume.d.ts +20 -0
  120. package/dist/indexing/checkpoint-resume.d.ts.map +1 -0
  121. package/dist/indexing/checkpoint-resume.js +50 -0
  122. package/dist/indexing/embedding-batcher.d.ts +3 -0
  123. package/dist/indexing/embedding-batcher.d.ts.map +1 -0
  124. package/dist/indexing/embedding-batcher.js +390 -0
  125. package/dist/indexing/index.d.ts +7 -0
  126. package/dist/indexing/index.d.ts.map +1 -0
  127. package/dist/indexing/index.js +11 -0
  128. package/dist/indexing/job-persist.d.ts +46 -0
  129. package/dist/indexing/job-persist.d.ts.map +1 -0
  130. package/dist/indexing/job-persist.js +157 -0
  131. package/dist/indexing/job-resume.d.ts +4 -0
  132. package/dist/indexing/job-resume.d.ts.map +1 -0
  133. package/dist/indexing/job-resume.js +14 -0
  134. package/dist/indexing/orchestrator.d.ts +3 -0
  135. package/dist/indexing/orchestrator.d.ts.map +1 -0
  136. package/dist/indexing/orchestrator.js +1151 -0
  137. package/dist/indexing/types.d.ts +156 -0
  138. package/dist/indexing/types.d.ts.map +1 -0
  139. package/dist/indexing/types.js +30 -0
  140. package/dist/indexing/vector-persist.d.ts +32 -0
  141. package/dist/indexing/vector-persist.d.ts.map +1 -0
  142. package/dist/indexing/vector-persist.js +105 -0
  143. package/dist/parsers/_internal.d.ts +20 -0
  144. package/dist/parsers/_internal.d.ts.map +1 -0
  145. package/dist/parsers/_internal.js +122 -0
  146. package/dist/parsers/csv-parser.d.ts +3 -0
  147. package/dist/parsers/csv-parser.d.ts.map +1 -0
  148. package/dist/parsers/csv-parser.js +202 -0
  149. package/dist/parsers/docx-parser.d.ts +3 -0
  150. package/dist/parsers/docx-parser.d.ts.map +1 -0
  151. package/dist/parsers/docx-parser.js +390 -0
  152. package/dist/parsers/html-parser.d.ts +3 -0
  153. package/dist/parsers/html-parser.d.ts.map +1 -0
  154. package/dist/parsers/html-parser.js +310 -0
  155. package/dist/parsers/index.d.ts +15 -0
  156. package/dist/parsers/index.d.ts.map +1 -0
  157. package/dist/parsers/index.js +41 -0
  158. package/dist/parsers/json-parser.d.ts +3 -0
  159. package/dist/parsers/json-parser.d.ts.map +1 -0
  160. package/dist/parsers/json-parser.js +192 -0
  161. package/dist/parsers/large-document/capability-discovery.d.ts +27 -0
  162. package/dist/parsers/large-document/capability-discovery.d.ts.map +1 -0
  163. package/dist/parsers/large-document/capability-discovery.js +76 -0
  164. package/dist/parsers/large-document/diagnostics.d.ts +3 -0
  165. package/dist/parsers/large-document/diagnostics.d.ts.map +1 -0
  166. package/dist/parsers/large-document/diagnostics.js +11 -0
  167. package/dist/parsers/large-document/index.d.ts +15 -0
  168. package/dist/parsers/large-document/index.d.ts.map +1 -0
  169. package/dist/parsers/large-document/index.js +10 -0
  170. package/dist/parsers/large-document/legacy-format.d.ts +5 -0
  171. package/dist/parsers/large-document/legacy-format.d.ts.map +1 -0
  172. package/dist/parsers/large-document/legacy-format.js +25 -0
  173. package/dist/parsers/large-document/preflight.d.ts +9 -0
  174. package/dist/parsers/large-document/preflight.d.ts.map +1 -0
  175. package/dist/parsers/large-document/preflight.js +43 -0
  176. package/dist/parsers/large-document/progressive-extraction.d.ts +55 -0
  177. package/dist/parsers/large-document/progressive-extraction.d.ts.map +1 -0
  178. package/dist/parsers/large-document/progressive-extraction.js +123 -0
  179. package/dist/parsers/large-document/progressive-pdf.d.ts +20 -0
  180. package/dist/parsers/large-document/progressive-pdf.d.ts.map +1 -0
  181. package/dist/parsers/large-document/progressive-pdf.js +145 -0
  182. package/dist/parsers/large-document/synthetic-source.d.ts +9 -0
  183. package/dist/parsers/large-document/synthetic-source.d.ts.map +1 -0
  184. package/dist/parsers/large-document/synthetic-source.js +101 -0
  185. package/dist/parsers/large-document/window-builder.d.ts +24 -0
  186. package/dist/parsers/large-document/window-builder.d.ts.map +1 -0
  187. package/dist/parsers/large-document/window-builder.js +75 -0
  188. package/dist/parsers/ocr/index.d.ts +4 -0
  189. package/dist/parsers/ocr/index.d.ts.map +1 -0
  190. package/dist/parsers/ocr/index.js +4 -0
  191. package/dist/parsers/ocr/null-ocr-adapter.d.ts +3 -0
  192. package/dist/parsers/ocr/null-ocr-adapter.d.ts.map +1 -0
  193. package/dist/parsers/ocr/null-ocr-adapter.js +14 -0
  194. package/dist/parsers/ocr/ocr-pipeline-parser.d.ts +8 -0
  195. package/dist/parsers/ocr/ocr-pipeline-parser.d.ts.map +1 -0
  196. package/dist/parsers/ocr/ocr-pipeline-parser.js +147 -0
  197. package/dist/parsers/ocr/types.d.ts +16 -0
  198. package/dist/parsers/ocr/types.d.ts.map +1 -0
  199. package/dist/parsers/ocr/types.js +4 -0
  200. package/dist/parsers/parser-test-fixtures.d.ts +28 -0
  201. package/dist/parsers/parser-test-fixtures.d.ts.map +1 -0
  202. package/dist/parsers/parser-test-fixtures.js +139 -0
  203. package/dist/parsers/pdf-parser.d.ts +43 -0
  204. package/dist/parsers/pdf-parser.d.ts.map +1 -0
  205. package/dist/parsers/pdf-parser.js +388 -0
  206. package/dist/parsers/registry.d.ts +8 -0
  207. package/dist/parsers/registry.d.ts.map +1 -0
  208. package/dist/parsers/registry.js +57 -0
  209. package/dist/parsers/text-parser.d.ts +3 -0
  210. package/dist/parsers/text-parser.d.ts.map +1 -0
  211. package/dist/parsers/text-parser.js +214 -0
  212. package/dist/parsers/types.d.ts +53 -0
  213. package/dist/parsers/types.d.ts.map +1 -0
  214. package/dist/parsers/types.js +21 -0
  215. package/dist/parsers/unsupported-parser.d.ts +4 -0
  216. package/dist/parsers/unsupported-parser.d.ts.map +1 -0
  217. package/dist/parsers/unsupported-parser.js +97 -0
  218. package/dist/parsers/xlsx-parser.d.ts +3 -0
  219. package/dist/parsers/xlsx-parser.d.ts.map +1 -0
  220. package/dist/parsers/xlsx-parser.js +425 -0
  221. package/dist/privacy/audit-emitter.d.ts +5 -0
  222. package/dist/privacy/audit-emitter.d.ts.map +1 -0
  223. package/dist/privacy/audit-emitter.js +93 -0
  224. package/dist/privacy/diagnostic-redactor.d.ts +2 -0
  225. package/dist/privacy/diagnostic-redactor.d.ts.map +1 -0
  226. package/dist/privacy/diagnostic-redactor.js +153 -0
  227. package/dist/privacy/index.d.ts +5 -0
  228. package/dist/privacy/index.d.ts.map +1 -0
  229. package/dist/privacy/index.js +6 -0
  230. package/dist/privacy/retention-applier.d.ts +5 -0
  231. package/dist/privacy/retention-applier.d.ts.map +1 -0
  232. package/dist/privacy/retention-applier.js +88 -0
  233. package/dist/privacy/types.d.ts +98 -0
  234. package/dist/privacy/types.d.ts.map +1 -0
  235. package/dist/privacy/types.js +12 -0
  236. package/dist/qualityIntelligence/capsuleCorpus.d.ts +27 -0
  237. package/dist/qualityIntelligence/capsuleCorpus.d.ts.map +1 -0
  238. package/dist/qualityIntelligence/capsuleCorpus.js +58 -0
  239. package/dist/qualityIntelligence/index.d.ts +3 -0
  240. package/dist/qualityIntelligence/index.d.ts.map +1 -0
  241. package/dist/qualityIntelligence/index.js +5 -0
  242. package/dist/qualityIntelligence/qiHandoff.d.ts +36 -0
  243. package/dist/qualityIntelligence/qiHandoff.d.ts.map +1 -0
  244. package/dist/qualityIntelligence/qiHandoff.js +82 -0
  245. package/dist/retrieval/answer-grounding.d.ts +9 -0
  246. package/dist/retrieval/answer-grounding.d.ts.map +1 -0
  247. package/dist/retrieval/answer-grounding.js +31 -0
  248. package/dist/retrieval/context-pack-assembler.d.ts +24 -0
  249. package/dist/retrieval/context-pack-assembler.d.ts.map +1 -0
  250. package/dist/retrieval/context-pack-assembler.js +50 -0
  251. package/dist/retrieval/index.d.ts +6 -0
  252. package/dist/retrieval/index.d.ts.map +1 -0
  253. package/dist/retrieval/index.js +9 -0
  254. package/dist/retrieval/retrieval-runner.d.ts +10 -0
  255. package/dist/retrieval/retrieval-runner.d.ts.map +1 -0
  256. package/dist/retrieval/retrieval-runner.js +163 -0
  257. package/dist/retrieval/scoped-vector-search.d.ts +24 -0
  258. package/dist/retrieval/scoped-vector-search.d.ts.map +1 -0
  259. package/dist/retrieval/scoped-vector-search.js +864 -0
  260. package/dist/retrieval/types.d.ts +28 -0
  261. package/dist/retrieval/types.d.ts.map +1 -0
  262. package/dist/retrieval/types.js +33 -0
  263. package/dist/section-path-hash.d.ts +3 -0
  264. package/dist/section-path-hash.d.ts.map +1 -0
  265. package/dist/section-path-hash.js +9 -0
  266. package/dist/source-lifecycle.d.ts +14 -0
  267. package/dist/source-lifecycle.d.ts.map +1 -0
  268. package/dist/source-lifecycle.js +155 -0
  269. package/dist/source-routing-validation.d.ts +11 -0
  270. package/dist/source-routing-validation.d.ts.map +1 -0
  271. package/dist/source-routing-validation.js +140 -0
  272. package/dist/store-content-cipher.d.ts +11 -0
  273. package/dist/store-content-cipher.d.ts.map +1 -0
  274. package/dist/store-content-cipher.js +67 -0
  275. package/dist/store-content-encryption.d.ts +12 -0
  276. package/dist/store-content-encryption.d.ts.map +1 -0
  277. package/dist/store-content-encryption.js +275 -0
  278. package/dist/store-paths.d.ts +6 -0
  279. package/dist/store-paths.d.ts.map +1 -0
  280. package/dist/store-paths.js +61 -0
  281. package/dist/store.d.ts +30 -0
  282. package/dist/store.d.ts.map +1 -0
  283. package/dist/store.js +219 -0
  284. package/dist/testing.d.ts +47 -0
  285. package/dist/testing.d.ts.map +1 -0
  286. package/dist/testing.js +170 -0
  287. package/dist/version.d.ts +2 -0
  288. package/dist/version.d.ts.map +1 -0
  289. package/dist/version.js +4 -0
  290. package/package.json +43 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/chunking/types.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,OAAO,EACP,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAGnD,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;AAEtD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;CAC1C;AAGD,eAAO,MAAM,kBAAkB,MAAM,CAAC;AACtC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,sBAAsB,KAAK,CAAC;AACzC,eAAO,MAAM,kBAAkB,QAAS,CAAC;AACzC,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AACtC,eAAO,MAAM,kBAAkB,OAAQ,CAAC;AACxC,eAAO,MAAM,yBAAyB,EAAG,cAAuB,CAAC;AACjE,eAAO,MAAM,6BAA6B,gGACmK,CAAC;AAE9M,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;CACzC;AAID,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IACrC,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAIhC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IAEtC,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CACnC;AAKD,qBAAa,aAAc,SAAQ,mBAAmB;IACpD,SAAyB,IAAI,EAAE,MAAM,CAAmB;CACzD;AAKD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC"}
@@ -0,0 +1,26 @@
1
+ // Type contracts for the chunking layer (Epic #189, Issue #195). Chunks are the unit of
2
+ // retrieval — each row in the `chunks` table references exactly one parsed_unit (the
3
+ // citation hop entrypoint) and carries `safeExcerptHash` (SHA-256 hex) instead of raw
4
+ // text so the row is safe to copy across the trust boundary. Raw text is reconstructable
5
+ // at retrieval time via parsed_unit → document → bytes.
6
+ //
7
+ // `tokenEstimator` is injected so future tokenizer upgrades (#196, #199) can swap in a
8
+ // real tokenizer (e.g. tiktoken) without rewiring callers. The default estimator in
9
+ // `token-estimator.ts` is intentionally crude (~4 chars per token) — see that file's
10
+ // header for the documented limitation.
11
+ import { KnowledgeStoreError } from "../errors.js";
12
+ // Defaults documented inline so a caller passing `{}` gets predictable behaviour.
13
+ export const DEFAULT_MAX_TOKENS = 400;
14
+ export const DEFAULT_MIN_TOKENS = 64;
15
+ export const DEFAULT_OVERLAP_TOKENS = 32;
16
+ export const DEFAULT_MAX_CHUNKS = 50_000;
17
+ export const MAX_CHUNK_TOKENS = 2_048;
18
+ export const MAX_OVERLAP_TOKENS = 1_024;
19
+ export const CHUNKING_STRATEGY_VERSION = "issue-195-v2";
20
+ export const DEFAULT_CHUNKING_STRATEGY_KEY = `${CHUNKING_STRATEGY_VERSION}|max=${String(DEFAULT_MAX_TOKENS)}|min=${String(DEFAULT_MIN_TOKENS)}|overlap=${String(DEFAULT_OVERLAP_TOKENS)}|limit=${String(DEFAULT_MAX_CHUNKS)}|estimator=default`;
21
+ // Distinct from KnowledgeStoreError so a test asserting "chunking guard fail-closed" cannot
22
+ // accidentally accept any other error class. Extends KnowledgeStoreError so callers that
23
+ // only catch the parent class still see the failure.
24
+ export class ChunkingError extends KnowledgeStoreError {
25
+ name = "ChunkingError";
26
+ }
@@ -0,0 +1,57 @@
1
+ import { type CapsuleSet, type CapsuleSetId, type KnowledgeCapsuleId, type KnowledgeSourceId, type KnowledgeSourceScopeKind } from "@oscharko-dev/keiko-contracts";
2
+ import { type AddCapsuleSourceInput } from "./source-lifecycle.js";
3
+ import type { KnowledgeStore } from "./store.js";
4
+ export type CompositionErrorCode = "empty-source-batch" | "duplicate-source-in-batch" | "source-already-linked" | "empty-capsule-list" | "duplicate-capsule-in-batch";
5
+ export declare class CompositionError extends Error {
6
+ readonly code: CompositionErrorCode;
7
+ constructor(code: CompositionErrorCode, message: string);
8
+ }
9
+ export interface ComposedRetrievalScope {
10
+ readonly capsuleSetId: CapsuleSetId;
11
+ readonly capsuleIds: readonly KnowledgeCapsuleId[];
12
+ readonly sourceIds: readonly KnowledgeSourceId[];
13
+ readonly alwaysQueryCapsuleIds: readonly KnowledgeCapsuleId[];
14
+ readonly sourceRoutingByCapsule: ReadonlyMap<KnowledgeCapsuleId, string>;
15
+ }
16
+ export interface RetrievalCapsuleSummary {
17
+ readonly id: KnowledgeCapsuleId;
18
+ readonly displayName: string;
19
+ readonly sourceCount: number;
20
+ readonly alwaysQuery: boolean;
21
+ }
22
+ export interface RetrievalSourceSummary {
23
+ readonly id: KnowledgeSourceId;
24
+ readonly displayName: string;
25
+ readonly capsuleId: KnowledgeCapsuleId;
26
+ readonly scopeKind: KnowledgeSourceScopeKind;
27
+ }
28
+ export interface RetrievalScopeDisclosure {
29
+ readonly capsuleSetId: CapsuleSetId;
30
+ readonly capsuleSummaries: readonly RetrievalCapsuleSummary[];
31
+ readonly sourceSummaries: readonly RetrievalSourceSummary[];
32
+ }
33
+ export type CapsuleMembershipChangeKind = "add-source" | "remove-source" | "compose-set";
34
+ export interface CapsuleMembershipChange {
35
+ readonly id: string;
36
+ readonly capsuleId: KnowledgeCapsuleId;
37
+ readonly changeKind: CapsuleMembershipChangeKind;
38
+ readonly sourceId: KnowledgeSourceId | undefined;
39
+ readonly detailsJson: string | undefined;
40
+ readonly occurredAt: number;
41
+ }
42
+ export interface AddSourcesToCapsuleResult {
43
+ readonly capsuleId: KnowledgeCapsuleId;
44
+ readonly addedSourceIds: readonly KnowledgeSourceId[];
45
+ }
46
+ export interface ComposeCapsulesOptions {
47
+ readonly displayName: string;
48
+ readonly description?: string;
49
+ readonly capsuleIds: readonly KnowledgeCapsuleId[];
50
+ readonly tags?: readonly string[];
51
+ }
52
+ export declare function buildComposedRetrievalScope(store: KnowledgeStore, capsuleSetId: CapsuleSetId): ComposedRetrievalScope;
53
+ export declare function describeRetrievalScope(scope: ComposedRetrievalScope, store: KnowledgeStore): RetrievalScopeDisclosure;
54
+ export declare function addSourcesToCapsule(store: KnowledgeStore, capsuleId: KnowledgeCapsuleId, inputs: readonly AddCapsuleSourceInput[]): AddSourcesToCapsuleResult;
55
+ export declare function composeCapsules(store: KnowledgeStore, opts: ComposeCapsulesOptions): CapsuleSet;
56
+ export declare function listCapsuleMembershipChanges(store: KnowledgeStore, capsuleId: KnowledgeCapsuleId): readonly CapsuleMembershipChange[];
57
+ //# sourceMappingURL=composition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composition.d.ts","sourceRoot":"","sources":["../src/composition.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,310 @@
1
+ // composition.ts — Knowledge Capsule composition layer (Epic #189, Issue #263).
2
+ //
3
+ // Foundry-IQ "no global pool" rule lives in the type system (every record carries
4
+ // capsuleId/sourceId) and the SQL FKs. This module is the in-memory union and the
5
+ // audit-trail-emitting batch operations on top of that — pure functions over the store's
6
+ // existing capsule, source, and CapsuleSet primitives; no new vectors, no SQL joins
7
+ // across capsules.
8
+ //
9
+ // Exports:
10
+ // * buildComposedRetrievalScope(store, setId) → in-memory {capsuleIds, sourceIds, …}
11
+ // * describeRetrievalScope(scope, store) → UI-safe disclosure for the future #198
12
+ // * addSourcesToCapsule(store, capsuleId, …) → multi-source link + audit + updated_at
13
+ // * composeCapsules(store, opts) → createCapsuleSet + audit in one transaction
14
+ // * listCapsuleMembershipChanges(store, id) → audit reader for capsule history view
15
+ // * CompositionError / CompositionErrorCode → typed error surface
16
+ // * ComposedRetrievalScope / RetrievalScopeDisclosure / CapsuleMembershipChange / …
17
+ //
18
+ // All multi-row writes wrap in BEGIN/COMMIT/ROLLBACK so a partial batch never lands.
19
+ import { randomUUID } from "node:crypto";
20
+ import { isSafeDisplaySummary, validateKnowledgeSourceScope, } from "@oscharko-dev/keiko-contracts";
21
+ import { getCapsule } from "./capsule-lifecycle.js";
22
+ import { getCapsuleSet, createCapsuleSet, createCapsuleSetWithinTxn, } from "./capsule-set-lifecycle.js";
23
+ import { KnowledgeNotFoundError, KnowledgeStoreError } from "./errors.js";
24
+ import { listCapsuleSources } from "./source-lifecycle.js";
25
+ import { validateSourceRoutingForCapsule } from "./source-routing-validation.js";
26
+ export class CompositionError extends Error {
27
+ code;
28
+ constructor(code, message) {
29
+ super(message);
30
+ this.name = "CompositionError";
31
+ this.code = code;
32
+ }
33
+ }
34
+ // ─── buildComposedRetrievalScope ────────────────────────────────────────────────
35
+ export function buildComposedRetrievalScope(store, capsuleSetId) {
36
+ const set = getCapsuleSet(store, capsuleSetId);
37
+ if (set === undefined) {
38
+ throw new KnowledgeNotFoundError(`CapsuleSet not found: ${String(capsuleSetId)}`);
39
+ }
40
+ return composeScopeFromSet(store, set);
41
+ }
42
+ function composeScopeFromSet(store, set) {
43
+ const capsules = loadMemberCapsules(store, set.capsuleIds);
44
+ const sourceIds = collectUniqueSourceIds(capsules);
45
+ const alwaysQueryCapsuleIds = capsules.filter((c) => c.alwaysQuery === true).map((c) => c.id);
46
+ const routingMap = new Map();
47
+ for (const capsule of capsules) {
48
+ validateSourceRoutingForCapsule(capsule, listCapsuleSources(store, capsule.id));
49
+ if (capsule.sourceRoutingInstructions !== undefined) {
50
+ routingMap.set(capsule.id, capsule.sourceRoutingInstructions);
51
+ }
52
+ }
53
+ return {
54
+ capsuleSetId: set.id,
55
+ capsuleIds: set.capsuleIds,
56
+ sourceIds,
57
+ alwaysQueryCapsuleIds,
58
+ sourceRoutingByCapsule: routingMap,
59
+ };
60
+ }
61
+ function loadMemberCapsules(store, ids) {
62
+ const out = [];
63
+ for (const id of ids) {
64
+ const capsule = getCapsule(store, id);
65
+ if (capsule !== undefined)
66
+ out.push(capsule);
67
+ }
68
+ return out;
69
+ }
70
+ function collectUniqueSourceIds(capsules) {
71
+ const seen = new Set();
72
+ const out = [];
73
+ for (const capsule of capsules) {
74
+ for (const sid of capsule.sourceIds) {
75
+ const key = String(sid);
76
+ if (!seen.has(key)) {
77
+ seen.add(key);
78
+ out.push(sid);
79
+ }
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+ // ─── describeRetrievalScope ─────────────────────────────────────────────────────
85
+ export function describeRetrievalScope(scope, store) {
86
+ const alwaysQuerySet = new Set(scope.alwaysQueryCapsuleIds.map(String));
87
+ const capsuleSummaries = [];
88
+ const sourceSummaries = [];
89
+ for (const capsuleId of scope.capsuleIds) {
90
+ const capsule = getCapsule(store, capsuleId);
91
+ if (capsule === undefined)
92
+ continue;
93
+ const sources = listCapsuleSources(store, capsuleId);
94
+ capsuleSummaries.push({
95
+ id: capsule.id,
96
+ displayName: capsule.displayName,
97
+ sourceCount: sources.length,
98
+ alwaysQuery: alwaysQuerySet.has(String(capsule.id)),
99
+ });
100
+ for (const source of sources) {
101
+ sourceSummaries.push(summariseSource(source, capsule.id));
102
+ }
103
+ }
104
+ return {
105
+ capsuleSetId: scope.capsuleSetId,
106
+ capsuleSummaries,
107
+ sourceSummaries,
108
+ };
109
+ }
110
+ function summariseSource(source, capsuleId) {
111
+ return {
112
+ id: source.id,
113
+ displayName: source.displayName,
114
+ capsuleId,
115
+ scopeKind: source.scope.kind,
116
+ };
117
+ }
118
+ // ─── addSourcesToCapsule ────────────────────────────────────────────────────────
119
+ const TOUCH_UPDATED_AT_SQL = "UPDATE capsules SET updated_at = :now WHERE id = :id";
120
+ const INSERT_AUDIT_SQL = "INSERT INTO capsule_membership_changes (id, capsule_id, change_kind, source_id, details_json, occurred_at) VALUES (:id, :capsule_id, :change_kind, :source_id, :details_json, :occurred_at)";
121
+ export function addSourcesToCapsule(store, capsuleId, inputs) {
122
+ if (inputs.length === 0) {
123
+ throw new CompositionError("empty-source-batch", `addSourcesToCapsule called with an empty source list for capsule ${String(capsuleId)}.`);
124
+ }
125
+ assertNoDuplicateInputIds(inputs);
126
+ const capsule = getCapsule(store, capsuleId);
127
+ if (capsule === undefined) {
128
+ throw new KnowledgeNotFoundError(`Capsule not found: ${String(capsuleId)}`);
129
+ }
130
+ assertNoneAlreadyLinked(store, capsuleId, inputs);
131
+ assertSafeSourceInputs(inputs);
132
+ return runAddSourcesTransaction(store, capsuleId, inputs);
133
+ }
134
+ function assertSafeSourceInputs(inputs) {
135
+ for (const input of inputs) {
136
+ if (input.displayName.trim().length === 0 || !isSafeDisplaySummary(input.displayName)) {
137
+ throw new KnowledgeStoreError("displayName must be a browser-safe non-empty string");
138
+ }
139
+ if (input.description !== undefined && !isSafeDisplaySummary(input.description)) {
140
+ throw new KnowledgeStoreError("description must be browser-safe when set");
141
+ }
142
+ for (const tag of input.tags) {
143
+ if (tag.trim().length === 0 || !isSafeDisplaySummary(tag)) {
144
+ throw new KnowledgeStoreError("tag must be a browser-safe non-empty string");
145
+ }
146
+ }
147
+ const result = validateKnowledgeSourceScope(input.scope);
148
+ if (!result.ok) {
149
+ throw new KnowledgeStoreError(result.errors.join(" "));
150
+ }
151
+ }
152
+ }
153
+ function assertNoDuplicateInputIds(inputs) {
154
+ const seen = new Set();
155
+ for (const input of inputs) {
156
+ const key = String(input.id);
157
+ if (seen.has(key)) {
158
+ throw new CompositionError("duplicate-source-in-batch", `Source id ${key} appears more than once in the batch.`);
159
+ }
160
+ seen.add(key);
161
+ }
162
+ }
163
+ function assertNoneAlreadyLinked(store, capsuleId, inputs) {
164
+ const existing = new Set(listCapsuleSources(store, capsuleId).map((s) => String(s.id)));
165
+ for (const input of inputs) {
166
+ if (existing.has(String(input.id))) {
167
+ throw new CompositionError("source-already-linked", `Source ${String(input.id)} is already linked to capsule ${String(capsuleId)}.`);
168
+ }
169
+ }
170
+ }
171
+ function runAddSourcesTransaction(store, capsuleId, inputs) {
172
+ const db = store._internal.db;
173
+ const now = store._internal.now();
174
+ // addSourceToCapsule already opens its own BEGIN/COMMIT — we cannot nest. Instead we
175
+ // open ONE explicit transaction here and bypass the per-source helper, inlining the
176
+ // insert against the same `capsule_sources` table.
177
+ db.exec("BEGIN");
178
+ try {
179
+ const insertKnowledgeSource = db.prepare("INSERT INTO knowledge_sources (id, display_name, description, tags_json, scope_kind, scope_json, created_at, updated_at) VALUES (:id, :display_name, :description, :tags_json, :scope_kind, :scope_json, :created_at, :updated_at) ON CONFLICT(id) DO UPDATE SET display_name = excluded.display_name, description = excluded.description, tags_json = excluded.tags_json, scope_kind = excluded.scope_kind, scope_json = excluded.scope_json, updated_at = excluded.updated_at");
180
+ const insertSource = db.prepare("INSERT INTO capsule_sources (id, capsule_id, display_name, description, tags_json, scope_kind, scope_json, created_at, updated_at) VALUES (:id, :capsule_id, :display_name, :description, :tags_json, :scope_kind, :scope_json, :created_at, :updated_at)");
181
+ const insertAudit = db.prepare(INSERT_AUDIT_SQL);
182
+ for (const input of inputs) {
183
+ const params = {
184
+ id: input.id,
185
+ display_name: input.displayName,
186
+ description: input.description ?? null,
187
+ tags_json: JSON.stringify(input.tags),
188
+ scope_kind: input.scope.kind,
189
+ scope_json: scopeJsonWithoutKind(input.scope),
190
+ created_at: now,
191
+ updated_at: now,
192
+ };
193
+ insertKnowledgeSource.run(params);
194
+ insertSource.run({ ...params, capsule_id: capsuleId });
195
+ insertAudit.run({
196
+ id: randomUUID(),
197
+ capsule_id: capsuleId,
198
+ change_kind: "add-source",
199
+ source_id: input.id,
200
+ details_json: null,
201
+ occurred_at: now,
202
+ });
203
+ }
204
+ db.prepare(TOUCH_UPDATED_AT_SQL).run({ now, id: capsuleId });
205
+ db.exec("COMMIT");
206
+ }
207
+ catch (error) {
208
+ db.exec("ROLLBACK");
209
+ throw error;
210
+ }
211
+ return {
212
+ capsuleId,
213
+ addedSourceIds: inputs.map((i) => i.id),
214
+ };
215
+ }
216
+ function scopeJsonWithoutKind(scope) {
217
+ const copy = {};
218
+ for (const [key, value] of Object.entries(scope)) {
219
+ if (key === "kind")
220
+ continue;
221
+ copy[key] = value;
222
+ }
223
+ return JSON.stringify(copy);
224
+ }
225
+ // ─── composeCapsules ────────────────────────────────────────────────────────────
226
+ export function composeCapsules(store, opts) {
227
+ if (opts.capsuleIds.length === 0) {
228
+ throw new CompositionError("empty-capsule-list", "composeCapsules requires at least one capsule id.");
229
+ }
230
+ assertNoDuplicateCapsuleIds(opts.capsuleIds);
231
+ // Verify EVERY capsule id resolves BEFORE we open the CapsuleSet transaction. The
232
+ // create+audit pair must be atomic; failing partway through with the set already on
233
+ // disk would leave a dangling reference.
234
+ for (const id of opts.capsuleIds) {
235
+ const exists = getCapsule(store, id);
236
+ if (exists === undefined) {
237
+ throw new KnowledgeNotFoundError(`Capsule not found: ${String(id)}`);
238
+ }
239
+ }
240
+ return runComposeTransaction(store, opts);
241
+ }
242
+ function assertNoDuplicateCapsuleIds(ids) {
243
+ const seen = new Set();
244
+ for (const id of ids) {
245
+ const key = String(id);
246
+ if (seen.has(key)) {
247
+ throw new CompositionError("duplicate-capsule-in-batch", `Capsule id ${key} appears more than once in the composition request.`);
248
+ }
249
+ seen.add(key);
250
+ }
251
+ }
252
+ function runComposeTransaction(store, opts) {
253
+ // A single BEGIN/COMMIT wraps both the CapsuleSet write and the audit rows so a crash
254
+ // between them cannot leave a set with no audit trail. `createCapsuleSetWithinTxn` skips
255
+ // its own BEGIN/COMMIT to make this nesting possible (SQLite forbids nested transactions).
256
+ const setInput = buildCreateCapsuleSetInput(opts);
257
+ const db = store._internal.db;
258
+ const now = store._internal.now();
259
+ db.exec("BEGIN");
260
+ try {
261
+ createCapsuleSetWithinTxn(store, setInput, now);
262
+ const details = JSON.stringify({ setId: String(setInput.id) });
263
+ const stmt = db.prepare(INSERT_AUDIT_SQL);
264
+ for (const capsuleId of opts.capsuleIds) {
265
+ stmt.run({
266
+ id: randomUUID(),
267
+ capsule_id: capsuleId,
268
+ change_kind: "compose-set",
269
+ source_id: null,
270
+ details_json: details,
271
+ occurred_at: now,
272
+ });
273
+ }
274
+ db.exec("COMMIT");
275
+ }
276
+ catch (error) {
277
+ db.exec("ROLLBACK");
278
+ throw error;
279
+ }
280
+ const set = getCapsuleSet(store, setInput.id);
281
+ if (set === undefined) {
282
+ throw new Error(`runComposeTransaction: row not found after commit for ${String(setInput.id)}`);
283
+ }
284
+ return set;
285
+ }
286
+ function buildCreateCapsuleSetInput(opts) {
287
+ const id = randomUUID();
288
+ const base = {
289
+ id,
290
+ displayName: opts.displayName,
291
+ tags: opts.tags ?? [],
292
+ capsuleIds: opts.capsuleIds,
293
+ };
294
+ return opts.description !== undefined ? { ...base, description: opts.description } : base;
295
+ }
296
+ const SELECT_AUDIT_SQL = "SELECT id, capsule_id, change_kind, source_id, details_json, occurred_at FROM capsule_membership_changes WHERE capsule_id = :c ORDER BY occurred_at ASC, id ASC";
297
+ export function listCapsuleMembershipChanges(store, capsuleId) {
298
+ const rows = store._internal.db.prepare(SELECT_AUDIT_SQL).all({ c: capsuleId });
299
+ return rows.map((row) => mapAuditRow(row));
300
+ }
301
+ function mapAuditRow(row) {
302
+ return {
303
+ id: row.id,
304
+ capsuleId: row.capsule_id,
305
+ changeKind: row.change_kind,
306
+ sourceId: row.source_id === null ? undefined : row.source_id,
307
+ detailsJson: row.details_json ?? undefined,
308
+ occurredAt: row.occurred_at,
309
+ };
310
+ }
@@ -0,0 +1,8 @@
1
+ import type { RetrievalReference } from "@oscharko-dev/keiko-contracts";
2
+ import type { ConversationCitationReference } from "./types.js";
3
+ export interface AttachCitationsResult {
4
+ readonly text: string;
5
+ readonly citations: readonly ConversationCitationReference[];
6
+ }
7
+ export declare function attachCitationsToAnswer(answer: string, references: readonly RetrievalReference[]): AttachCitationsResult;
8
+ //# sourceMappingURL=citation-attacher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"citation-attacher.d.ts","sourceRoot":"","sources":["../../src/conversation/citation-attacher.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAqB,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAE3F,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,YAAY,CAAC;AAEhE,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,SAAS,6BAA6B,EAAE,CAAC;CAC9D;AAYD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,SAAS,kBAAkB,EAAE,GACxC,qBAAqB,CAkBvB"}
@@ -0,0 +1,55 @@
1
+ // Citation attachment (Epic #189, Issue #200). Extracts inline `[n]` markers from the
2
+ // answer text and maps each marker to its `RetrievalReference` by 1-based index. The
3
+ // Conversation Center UI surfaces the returned `citations` array as clickable footnotes;
4
+ // `text` is the (unchanged) answer string the BFF persists to the chat row.
5
+ //
6
+ // Tolerance rules (the markers are LLM output — never assume well-formed):
7
+ // * `[0]` and `[n]` for n > references.length are silently dropped. We do NOT mutate
8
+ // the answer text — keeping the original prose means the UI can still display the
9
+ // stray marker; the citations array just won't link it.
10
+ // * Duplicate markers (`[1]` appearing twice) produce two entries in the citations
11
+ // array, in document order. The UI is responsible for de-duplicating if it wants a
12
+ // "unique footnotes" view.
13
+ // * Markers with leading zeros (`[01]`) are accepted to match what some models emit;
14
+ // the parsed integer is what's matched against the reference list.
15
+ // * Bracket glyphs beyond ASCII `[ ]` are accepted: CJK lenticular `【n】` and fullwidth
16
+ // `[n]`. Some models (e.g. gpt-oss) emit these instead of ASCII brackets; without
17
+ // this tolerance their citations would be lost and the caller would fall back to
18
+ // attaching every reference. The original glyph is preserved in `marker`.
19
+ // * No regex backtracking traps — the pattern is `[bracket](\d+)[bracket]`, linear in the
20
+ // answer length, bounded by digit count (each class matches exactly one character).
21
+ // Linear scan; the pattern has no alternation or unbounded lookbehind so backtracking is
22
+ // O(n) in the answer length. ECMAScript's `RegExp` with the `g` flag is the simplest
23
+ // portable implementation; we cannot share a single static instance across calls because
24
+ // `lastIndex` is mutated during iteration (a single shared instance would corrupt under
25
+ // concurrent generator runs).
26
+ // Open ∈ { [ , 【 (U+3010), [ (U+FF3B) }; close ∈ { ] , 】 (U+3011), ] (U+FF3D) }. Each
27
+ // class matches exactly one character so the linear-scan / no-backtracking property holds.
28
+ // Mismatched pairs (`[1】`) are accepted intentionally — markers are untrusted LLM output.
29
+ const MARKER_PATTERN = /[[【[](\d+)[\]】]]/g;
30
+ export function attachCitationsToAnswer(answer, references) {
31
+ if (answer.length === 0 || references.length === 0) {
32
+ return { text: answer, citations: [] };
33
+ }
34
+ const citations = [];
35
+ const re = new RegExp(MARKER_PATTERN.source, "g");
36
+ let match;
37
+ while ((match = re.exec(answer)) !== null) {
38
+ const marker = match[0];
39
+ const raw = match[1];
40
+ if (raw === undefined)
41
+ continue;
42
+ const index = Number.parseInt(raw, 10);
43
+ if (!Number.isFinite(index) || index < 1 || index > references.length)
44
+ continue;
45
+ const reference = references[index - 1];
46
+ if (reference === undefined)
47
+ continue;
48
+ citations.push(buildCitationEntry(marker, index, reference));
49
+ }
50
+ return { text: answer, citations };
51
+ }
52
+ function buildCitationEntry(marker, index, reference) {
53
+ const citation = reference.citation;
54
+ return { marker, index, citation, reference };
55
+ }
@@ -0,0 +1,4 @@
1
+ import type { CitationReference, KnowledgeCapsuleId } from "@oscharko-dev/keiko-contracts";
2
+ import type { KnowledgeStore } from "../store.js";
3
+ export declare function readCitationExcerpt(store: KnowledgeStore, capsuleId: KnowledgeCapsuleId, citation: CitationReference, maxChars?: number): string;
4
+ //# sourceMappingURL=citation-excerpts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"citation-excerpts.d.ts","sourceRoot":"","sources":["../../src/conversation/citation-excerpts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAG3F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAgDlD,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,cAAc,EACrB,SAAS,EAAE,kBAAkB,EAC7B,QAAQ,EAAE,iBAAiB,EAC3B,QAAQ,SAA4B,GACnC,MAAM,CAgBR"}
@@ -0,0 +1,41 @@
1
+ import { readDocumentTextSpan } from "../discovery/persist.js";
2
+ const DEFAULT_MAX_EXCERPT_CHARS = 900;
3
+ function readDocumentText(store, capsuleId, citation) {
4
+ const row = store._internal.db
5
+ .prepare("SELECT normalized_text FROM document_texts WHERE capsule_id = :capsule_id AND document_id = :document_id")
6
+ .get({
7
+ capsule_id: capsuleId,
8
+ document_id: citation.documentId,
9
+ });
10
+ const stored = row?.normalized_text;
11
+ return typeof stored === "string" ? store._internal.contentCipher.openText(stored) : undefined;
12
+ }
13
+ function capExcerpt(raw, maxChars) {
14
+ const trimmed = raw.trim();
15
+ if (trimmed.length <= maxChars)
16
+ return trimmed;
17
+ return `${trimmed.slice(0, maxChars).trimEnd()}…`;
18
+ }
19
+ function sliceExcerpt(text, start, end, maxChars) {
20
+ if (text.length === 0)
21
+ return "";
22
+ const safeStart = Math.max(0, Math.min(text.length, start ?? 0));
23
+ const safeEnd = Math.max(safeStart, Math.min(text.length, end ?? safeStart + maxChars));
24
+ return capExcerpt(text.slice(safeStart, safeEnd), maxChars);
25
+ }
26
+ // Reads a citation's excerpt as a bounded span. When the chunk carries character offsets the span is
27
+ // read through `readDocumentTextSpan` (SUBSTR over `document_texts` for small files OR over the
28
+ // bounded `document_text_windows` rows for progressively-extracted large files), so a large
29
+ // document's citation excerpt renders without materializing the whole document text. Chunks without
30
+ // offsets fall back to the full small-file text projection.
31
+ export function readCitationExcerpt(store, capsuleId, citation, maxChars = DEFAULT_MAX_EXCERPT_CHARS) {
32
+ const { characterStart, characterEnd } = citation;
33
+ if (characterStart !== undefined && characterEnd !== undefined) {
34
+ const span = readDocumentTextSpan(store._internal.db, store._internal.contentCipher, capsuleId, citation.documentId, characterStart, characterEnd);
35
+ return span === undefined ? "" : capExcerpt(span, maxChars);
36
+ }
37
+ const text = readDocumentText(store, capsuleId, citation);
38
+ if (text === undefined)
39
+ return "";
40
+ return sliceExcerpt(text, characterStart, characterEnd, maxChars);
41
+ }
@@ -0,0 +1,9 @@
1
+ import type { RetrievalDependencies } from "../retrieval/retrieval-runner.js";
2
+ import type { AnswerGenerator, ConversationGroundedAnswer, ConversationGroundedQuery } from "./types.js";
3
+ export interface GroundedAnswerDependencies {
4
+ readonly retrieval: RetrievalDependencies;
5
+ readonly answerGenerator: AnswerGenerator;
6
+ readonly signal?: AbortSignal;
7
+ }
8
+ export declare function runGroundedAnswer(deps: GroundedAnswerDependencies, query: ConversationGroundedQuery): Promise<ConversationGroundedAnswer>;
9
+ //# sourceMappingURL=grounded-answer-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grounded-answer-runner.d.ts","sourceRoot":"","sources":["../../src/conversation/grounded-answer-runner.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AAG9E,OAAO,KAAK,EACV,eAAe,EACf,0BAA0B,EAC1B,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,SAAS,EAAE,qBAAqB,CAAC;IAC1C,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;IAG1C,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,0BAA0B,EAChC,KAAK,EAAE,yBAAyB,GAC/B,OAAO,CAAC,0BAA0B,CAAC,CA4CrC"}
@@ -0,0 +1,61 @@
1
+ // Grounded-answer runner (Epic #189, Issue #200). The single entry point the
2
+ // Conversation Center BFF will call: question text + capsule scope ⇒ structured answer
3
+ // with attached citations, ready to persist to the chat row and the audit ledger.
4
+ //
5
+ // Composition (all dependencies UNCHANGED from #199):
6
+ // 1. `runLocalKnowledgeRetrieval` resolves scope + policy and produces ranked refs.
7
+ // 2. `assembleGroundedContext` projects the refs into a `LocalKnowledgeGroundedContextPack`.
8
+ // 3. The injected `AnswerGenerator` turns the pack into an answer string. The runner
9
+ // passes through the AbortSignal so cancellation reaches the model call.
10
+ // 4. `attachCitationsToAnswer` scans the answer for `[n]` markers and pairs them with
11
+ // the original reference array.
12
+ //
13
+ // No-evidence short-circuit: if retrieval returns `noEvidence: true` the runner returns
14
+ // immediately WITHOUT invoking the generator. The audit ledger / UI surfaces the
15
+ // `reason` so the user sees an honest "we found nothing" message rather than a
16
+ // hallucinated answer.
17
+ //
18
+ // This module owns NO new business logic — it is wiring. Every behaviour invariant
19
+ // (scope resolution, embedding identity check, strictest-policy floor, answer-grounding
20
+ // rejection) is enforced by the underlying retrieval layer; the runner merely composes.
21
+ import { assembleGroundedContext } from "../retrieval/context-pack-assembler.js";
22
+ import { runLocalKnowledgeRetrieval } from "../retrieval/retrieval-runner.js";
23
+ import { attachCitationsToAnswer } from "./citation-attacher.js";
24
+ export async function runGroundedAnswer(deps, query) {
25
+ const retrieval = await runLocalKnowledgeRetrieval({
26
+ store: deps.retrieval.store,
27
+ embeddingAdapter: deps.retrieval.embeddingAdapter,
28
+ ...(deps.signal !== undefined ? { signal: deps.signal } : {}),
29
+ }, {
30
+ text: query.text,
31
+ ...(query.capsuleId !== undefined ? { capsuleId: query.capsuleId } : {}),
32
+ ...(query.capsuleSetId !== undefined ? { capsuleSetId: query.capsuleSetId } : {}),
33
+ ...(query.topK !== undefined ? { topK: query.topK } : {}),
34
+ ...(query.minScore !== undefined ? { minScore: query.minScore } : {}),
35
+ });
36
+ const pack = assembleGroundedContext(retrieval.references);
37
+ if (retrieval.noEvidence) {
38
+ return {
39
+ answer: "",
40
+ references: retrieval.references,
41
+ citations: [],
42
+ pack,
43
+ noEvidence: true,
44
+ ...(retrieval.reason !== undefined ? { reason: retrieval.reason } : {}),
45
+ };
46
+ }
47
+ const answerText = await deps.answerGenerator.generate({
48
+ query,
49
+ pack,
50
+ references: retrieval.references,
51
+ ...(deps.signal !== undefined ? { signal: deps.signal } : {}),
52
+ });
53
+ const attached = attachCitationsToAnswer(answerText, retrieval.references);
54
+ return {
55
+ answer: attached.text,
56
+ references: retrieval.references,
57
+ citations: attached.citations,
58
+ pack,
59
+ noEvidence: false,
60
+ };
61
+ }
@@ -0,0 +1,5 @@
1
+ export { runGroundedAnswer, type GroundedAnswerDependencies } from "./grounded-answer-runner.js";
2
+ export { ModelGatewayAnswerGenerator, AnswerGroundingRejectedError, buildPromptMessages, type ChatGateway, type ModelGatewayAnswerGeneratorOptions, } from "./model-gateway-answer-generator.js";
3
+ export { attachCitationsToAnswer, type AttachCitationsResult } from "./citation-attacher.js";
4
+ export type { AnswerGenerator, AnswerGeneratorInput, ConversationCitationReference, ConversationGroundedAnswer, ConversationGroundedQuery, } from "./types.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/conversation/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,iBAAiB,EAAE,KAAK,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AAEjG,OAAO,EACL,2BAA2B,EAC3B,4BAA4B,EAC5B,mBAAmB,EACnB,KAAK,WAAW,EAChB,KAAK,kCAAkC,GACxC,MAAM,qCAAqC,CAAC;AAE7C,OAAO,EAAE,uBAAuB,EAAE,KAAK,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAE7F,YAAY,EACV,eAAe,EACf,oBAAoB,EACpB,6BAA6B,EAC7B,0BAA0B,EAC1B,yBAAyB,GAC1B,MAAM,YAAY,CAAC"}
@@ -0,0 +1,7 @@
1
+ // Public surface of the conversation/ layer (Epic #189, Issue #200). Wires the #199
2
+ // retrieval pipeline into the Conversation Center BFF. Consumers outside this package
3
+ // import from the package barrel in ../index.ts; the subdirectory is never imported
4
+ // directly (ADR-0019 direction rule 3e + trust-8).
5
+ export { runGroundedAnswer } from "./grounded-answer-runner.js";
6
+ export { ModelGatewayAnswerGenerator, AnswerGroundingRejectedError, buildPromptMessages, } from "./model-gateway-answer-generator.js";
7
+ export { attachCitationsToAnswer } from "./citation-attacher.js";
@@ -0,0 +1,28 @@
1
+ import type { CapsuleAnswerGroundingPolicy, ChatMessage, GatewayRequest, NormalizedResponse } from "@oscharko-dev/keiko-contracts";
2
+ import type { LocalKnowledgeGroundedContextPack } from "../retrieval/context-pack-assembler.js";
3
+ import type { AnswerGenerator, AnswerGeneratorInput } from "./types.js";
4
+ type CitationMetadataRedactor = (value: string) => string;
5
+ export interface ChatGateway {
6
+ readonly chat: (request: GatewayRequest) => Promise<NormalizedResponse>;
7
+ }
8
+ export interface ModelGatewayAnswerGeneratorOptions {
9
+ readonly chatGateway: ChatGateway;
10
+ readonly modelId: string;
11
+ readonly policy: CapsuleAnswerGroundingPolicy;
12
+ readonly redactCitationMetadata?: CitationMetadataRedactor;
13
+ }
14
+ export declare class AnswerGroundingRejectedError extends Error {
15
+ readonly name: string;
16
+ constructor();
17
+ }
18
+ export declare class ModelGatewayAnswerGenerator implements AnswerGenerator {
19
+ private readonly chatGateway;
20
+ private readonly modelId;
21
+ private readonly policy;
22
+ private readonly redactCitationMetadata;
23
+ constructor(options: ModelGatewayAnswerGeneratorOptions);
24
+ generate(input: AnswerGeneratorInput): Promise<string>;
25
+ }
26
+ export declare function buildPromptMessages(question: string, pack: LocalKnowledgeGroundedContextPack, redactCitationMetadata?: CitationMetadataRedactor): readonly ChatMessage[];
27
+ export {};
28
+ //# sourceMappingURL=model-gateway-answer-generator.d.ts.map