@oscharko-dev/keiko-server 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 (509) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/assistant-response.d.ts +6 -0
  3. package/dist/assistant-response.d.ts.map +1 -0
  4. package/dist/assistant-response.js +12 -0
  5. package/dist/browser.d.ts +11 -0
  6. package/dist/browser.d.ts.map +1 -0
  7. package/dist/browser.js +245 -0
  8. package/dist/chat-handlers.d.ts +48 -0
  9. package/dist/chat-handlers.d.ts.map +1 -0
  10. package/dist/chat-handlers.js +821 -0
  11. package/dist/chat-stream-handlers.d.ts +4 -0
  12. package/dist/chat-stream-handlers.d.ts.map +1 -0
  13. package/dist/chat-stream-handlers.js +136 -0
  14. package/dist/conversation-prompt.d.ts +8 -0
  15. package/dist/conversation-prompt.d.ts.map +1 -0
  16. package/dist/conversation-prompt.js +36 -0
  17. package/dist/conversation-validation.d.ts +26 -0
  18. package/dist/conversation-validation.d.ts.map +1 -0
  19. package/dist/conversation-validation.js +125 -0
  20. package/dist/credentialPersistence.d.ts +23 -0
  21. package/dist/credentialPersistence.d.ts.map +1 -0
  22. package/dist/credentialPersistence.js +93 -0
  23. package/dist/credentialVault.d.ts +30 -0
  24. package/dist/credentialVault.d.ts.map +1 -0
  25. package/dist/credentialVault.js +206 -0
  26. package/dist/csp.d.ts +3 -0
  27. package/dist/csp.d.ts.map +1 -0
  28. package/dist/csp.js +75 -0
  29. package/dist/deps.d.ts +78 -0
  30. package/dist/deps.d.ts.map +1 -0
  31. package/dist/deps.js +457 -0
  32. package/dist/editor/agentRoutes.d.ts +7 -0
  33. package/dist/editor/agentRoutes.d.ts.map +1 -0
  34. package/dist/editor/agentRoutes.js +197 -0
  35. package/dist/editor/assuredGateRunner.d.ts +36 -0
  36. package/dist/editor/assuredGateRunner.d.ts.map +1 -0
  37. package/dist/editor/assuredGateRunner.js +100 -0
  38. package/dist/editor/assuredPreFilter.d.ts +34 -0
  39. package/dist/editor/assuredPreFilter.d.ts.map +1 -0
  40. package/dist/editor/assuredPreFilter.js +134 -0
  41. package/dist/editor/assuredPreFilterRunner.d.ts +31 -0
  42. package/dist/editor/assuredPreFilterRunner.d.ts.map +1 -0
  43. package/dist/editor/assuredPreFilterRunner.js +312 -0
  44. package/dist/editor/builtinLanguageProviders.d.ts +6 -0
  45. package/dist/editor/builtinLanguageProviders.d.ts.map +1 -0
  46. package/dist/editor/builtinLanguageProviders.js +221 -0
  47. package/dist/editor/codingContext.d.ts +12 -0
  48. package/dist/editor/codingContext.d.ts.map +1 -0
  49. package/dist/editor/codingContext.js +121 -0
  50. package/dist/editor/codingContextEvidence.d.ts +7 -0
  51. package/dist/editor/codingContextEvidence.d.ts.map +1 -0
  52. package/dist/editor/codingContextEvidence.js +52 -0
  53. package/dist/editor/codingContextProviders.d.ts +36 -0
  54. package/dist/editor/codingContextProviders.d.ts.map +1 -0
  55. package/dist/editor/codingContextProviders.js +348 -0
  56. package/dist/editor/completionModelEvidence.d.ts +16 -0
  57. package/dist/editor/completionModelEvidence.d.ts.map +1 -0
  58. package/dist/editor/completionModelEvidence.js +50 -0
  59. package/dist/editor/completionRoutes.d.ts +37 -0
  60. package/dist/editor/completionRoutes.d.ts.map +1 -0
  61. package/dist/editor/completionRoutes.js +411 -0
  62. package/dist/editor/contextRoutes.d.ts +6 -0
  63. package/dist/editor/contextRoutes.d.ts.map +1 -0
  64. package/dist/editor/contextRoutes.js +411 -0
  65. package/dist/editor/disposableAssuredExecution.d.ts +22 -0
  66. package/dist/editor/disposableAssuredExecution.d.ts.map +1 -0
  67. package/dist/editor/disposableAssuredExecution.js +57 -0
  68. package/dist/editor/editorCompletionModel.d.ts +47 -0
  69. package/dist/editor/editorCompletionModel.d.ts.map +1 -0
  70. package/dist/editor/editorCompletionModel.js +156 -0
  71. package/dist/editor/editorInlineCompletionModel.d.ts +34 -0
  72. package/dist/editor/editorInlineCompletionModel.d.ts.map +1 -0
  73. package/dist/editor/editorInlineCompletionModel.js +112 -0
  74. package/dist/editor/editorModelTokenBudget.d.ts +46 -0
  75. package/dist/editor/editorModelTokenBudget.d.ts.map +1 -0
  76. package/dist/editor/editorModelTokenBudget.js +121 -0
  77. package/dist/editor/inlineCompletionRateLimiter.d.ts +19 -0
  78. package/dist/editor/inlineCompletionRateLimiter.d.ts.map +1 -0
  79. package/dist/editor/inlineCompletionRateLimiter.js +46 -0
  80. package/dist/editor/inlineCompletionRoutes.d.ts +26 -0
  81. package/dist/editor/inlineCompletionRoutes.d.ts.map +1 -0
  82. package/dist/editor/inlineCompletionRoutes.js +404 -0
  83. package/dist/editor/inlineCompletionTelemetryEvidence.d.ts +5 -0
  84. package/dist/editor/inlineCompletionTelemetryEvidence.d.ts.map +1 -0
  85. package/dist/editor/inlineCompletionTelemetryEvidence.js +42 -0
  86. package/dist/editor/languageCancellation.d.ts +19 -0
  87. package/dist/editor/languageCancellation.d.ts.map +1 -0
  88. package/dist/editor/languageCancellation.js +48 -0
  89. package/dist/editor/languageProvider.d.ts +39 -0
  90. package/dist/editor/languageProvider.d.ts.map +1 -0
  91. package/dist/editor/languageProvider.js +11 -0
  92. package/dist/editor/languageRoutes.d.ts +15 -0
  93. package/dist/editor/languageRoutes.d.ts.map +1 -0
  94. package/dist/editor/languageRoutes.js +106 -0
  95. package/dist/editor/languageSanitize.d.ts +8 -0
  96. package/dist/editor/languageSanitize.d.ts.map +1 -0
  97. package/dist/editor/languageSanitize.js +101 -0
  98. package/dist/editor/languageService.d.ts +36 -0
  99. package/dist/editor/languageService.d.ts.map +1 -0
  100. package/dist/editor/languageService.js +93 -0
  101. package/dist/editor/languageServiceHost.d.ts +14 -0
  102. package/dist/editor/languageServiceHost.d.ts.map +1 -0
  103. package/dist/editor/languageServiceHost.js +242 -0
  104. package/dist/editor/localKnowledgeRetrieval.d.ts +21 -0
  105. package/dist/editor/localKnowledgeRetrieval.d.ts.map +1 -0
  106. package/dist/editor/localKnowledgeRetrieval.js +44 -0
  107. package/dist/editor/patchApplyEvidence.d.ts +21 -0
  108. package/dist/editor/patchApplyEvidence.d.ts.map +1 -0
  109. package/dist/editor/patchApplyEvidence.js +87 -0
  110. package/dist/editor/patchApplyRoutes.d.ts +16 -0
  111. package/dist/editor/patchApplyRoutes.d.ts.map +1 -0
  112. package/dist/editor/patchApplyRoutes.js +307 -0
  113. package/dist/editor/postApplyVerification.d.ts +42 -0
  114. package/dist/editor/postApplyVerification.d.ts.map +1 -0
  115. package/dist/editor/postApplyVerification.js +177 -0
  116. package/dist/editor/testGenerationEvidence.d.ts +6 -0
  117. package/dist/editor/testGenerationEvidence.d.ts.map +1 -0
  118. package/dist/editor/testGenerationEvidence.js +72 -0
  119. package/dist/editor/testGenerationPatch.d.ts +10 -0
  120. package/dist/editor/testGenerationPatch.d.ts.map +1 -0
  121. package/dist/editor/testGenerationPatch.js +66 -0
  122. package/dist/editor/testGenerationRoutes.d.ts +21 -0
  123. package/dist/editor/testGenerationRoutes.d.ts.map +1 -0
  124. package/dist/editor/testGenerationRoutes.js +254 -0
  125. package/dist/editor/testGenerationRunner.d.ts +23 -0
  126. package/dist/editor/testGenerationRunner.d.ts.map +1 -0
  127. package/dist/editor/testGenerationRunner.js +120 -0
  128. package/dist/editor/textOffsets.d.ts +6 -0
  129. package/dist/editor/textOffsets.d.ts.map +1 -0
  130. package/dist/editor/textOffsets.js +82 -0
  131. package/dist/editor/typescriptLanguageProvider.d.ts +3 -0
  132. package/dist/editor/typescriptLanguageProvider.d.ts.map +1 -0
  133. package/dist/editor/typescriptLanguageProvider.js +217 -0
  134. package/dist/evidence.d.ts +28 -0
  135. package/dist/evidence.d.ts.map +1 -0
  136. package/dist/evidence.js +145 -0
  137. package/dist/files-deny.d.ts +3 -0
  138. package/dist/files-deny.d.ts.map +1 -0
  139. package/dist/files-deny.js +12 -0
  140. package/dist/files.d.ts +97 -0
  141. package/dist/files.d.ts.map +1 -0
  142. package/dist/files.js +733 -0
  143. package/dist/gateway-setup.d.ts +10 -0
  144. package/dist/gateway-setup.d.ts.map +1 -0
  145. package/dist/gateway-setup.js +896 -0
  146. package/dist/governed-workflow.d.ts +17 -0
  147. package/dist/governed-workflow.d.ts.map +1 -0
  148. package/dist/governed-workflow.js +147 -0
  149. package/dist/grounded-answer.d.ts +12 -0
  150. package/dist/grounded-answer.d.ts.map +1 -0
  151. package/dist/grounded-answer.js +69 -0
  152. package/dist/grounded-context-index.d.ts +25 -0
  153. package/dist/grounded-context-index.d.ts.map +1 -0
  154. package/dist/grounded-context-index.js +169 -0
  155. package/dist/grounded-document-evidence.d.ts +28 -0
  156. package/dist/grounded-document-evidence.d.ts.map +1 -0
  157. package/dist/grounded-document-evidence.js +430 -0
  158. package/dist/grounded-handoff.d.ts +4 -0
  159. package/dist/grounded-handoff.d.ts.map +1 -0
  160. package/dist/grounded-handoff.js +445 -0
  161. package/dist/grounded-orchestrator.d.ts +43 -0
  162. package/dist/grounded-orchestrator.d.ts.map +1 -0
  163. package/dist/grounded-orchestrator.js +1445 -0
  164. package/dist/grounded-prompt.d.ts +2 -0
  165. package/dist/grounded-prompt.d.ts.map +1 -0
  166. package/dist/grounded-prompt.js +17 -0
  167. package/dist/grounded-qa-hybrid.d.ts +36 -0
  168. package/dist/grounded-qa-hybrid.d.ts.map +1 -0
  169. package/dist/grounded-qa-hybrid.js +762 -0
  170. package/dist/grounded-qa-multi-source.d.ts +38 -0
  171. package/dist/grounded-qa-multi-source.d.ts.map +1 -0
  172. package/dist/grounded-qa-multi-source.js +461 -0
  173. package/dist/grounded-qa.d.ts +45 -0
  174. package/dist/grounded-qa.d.ts.map +1 -0
  175. package/dist/grounded-qa.js +877 -0
  176. package/dist/grounded-rerank.d.ts +26 -0
  177. package/dist/grounded-rerank.d.ts.map +1 -0
  178. package/dist/grounded-rerank.js +72 -0
  179. package/dist/grounded-turn-registry.d.ts +23 -0
  180. package/dist/grounded-turn-registry.d.ts.map +1 -0
  181. package/dist/grounded-turn-registry.js +102 -0
  182. package/dist/headers.d.ts +3 -0
  183. package/dist/headers.d.ts.map +1 -0
  184. package/dist/headers.js +22 -0
  185. package/dist/host-check.d.ts +3 -0
  186. package/dist/host-check.d.ts.map +1 -0
  187. package/dist/host-check.js +58 -0
  188. package/dist/index.d.ts +26 -0
  189. package/dist/index.d.ts.map +1 -0
  190. package/dist/index.js +33 -0
  191. package/dist/load-csp.d.ts +3 -0
  192. package/dist/load-csp.d.ts.map +1 -0
  193. package/dist/load-csp.js +100 -0
  194. package/dist/local-knowledge-grounded-qa.d.ts +42 -0
  195. package/dist/local-knowledge-grounded-qa.d.ts.map +1 -0
  196. package/dist/local-knowledge-grounded-qa.js +678 -0
  197. package/dist/local-knowledge-handlers.d.ts +24 -0
  198. package/dist/local-knowledge-handlers.d.ts.map +1 -0
  199. package/dist/local-knowledge-handlers.js +1285 -0
  200. package/dist/local-knowledge-indexing-registry.d.ts +13 -0
  201. package/dist/local-knowledge-indexing-registry.d.ts.map +1 -0
  202. package/dist/local-knowledge-indexing-registry.js +53 -0
  203. package/dist/localKnowledgeKeyProvider.d.ts +11 -0
  204. package/dist/localKnowledgeKeyProvider.d.ts.map +1 -0
  205. package/dist/localKnowledgeKeyProvider.js +48 -0
  206. package/dist/memory-audit-event-builders.d.ts +21 -0
  207. package/dist/memory-audit-event-builders.d.ts.map +1 -0
  208. package/dist/memory-audit-event-builders.js +187 -0
  209. package/dist/memory-audit-handler.d.ts +23 -0
  210. package/dist/memory-audit-handler.d.ts.map +1 -0
  211. package/dist/memory-audit-handler.js +191 -0
  212. package/dist/memory-capture-policy.d.ts +10 -0
  213. package/dist/memory-capture-policy.d.ts.map +1 -0
  214. package/dist/memory-capture-policy.js +44 -0
  215. package/dist/memory-consolidation-handlers.d.ts +6 -0
  216. package/dist/memory-consolidation-handlers.d.ts.map +1 -0
  217. package/dist/memory-consolidation-handlers.js +491 -0
  218. package/dist/memory-consolidation-registry.d.ts +47 -0
  219. package/dist/memory-consolidation-registry.d.ts.map +1 -0
  220. package/dist/memory-consolidation-registry.js +106 -0
  221. package/dist/memory-conv-handlers.d.ts +8 -0
  222. package/dist/memory-conv-handlers.d.ts.map +1 -0
  223. package/dist/memory-conv-handlers.js +369 -0
  224. package/dist/memory-conversation-context.d.ts +13 -0
  225. package/dist/memory-conversation-context.d.ts.map +1 -0
  226. package/dist/memory-conversation-context.js +22 -0
  227. package/dist/memory-diagnostics.d.ts +29 -0
  228. package/dist/memory-diagnostics.d.ts.map +1 -0
  229. package/dist/memory-diagnostics.js +122 -0
  230. package/dist/memory-embedding.d.ts +21 -0
  231. package/dist/memory-embedding.d.ts.map +1 -0
  232. package/dist/memory-embedding.js +264 -0
  233. package/dist/memory-handlers.d.ts +19 -0
  234. package/dist/memory-handlers.d.ts.map +1 -0
  235. package/dist/memory-handlers.js +1204 -0
  236. package/dist/memory-maintenance-handlers.d.ts +35 -0
  237. package/dist/memory-maintenance-handlers.d.ts.map +1 -0
  238. package/dist/memory-maintenance-handlers.js +219 -0
  239. package/dist/memory-record-builders.d.ts +4 -0
  240. package/dist/memory-record-builders.d.ts.map +1 -0
  241. package/dist/memory-record-builders.js +19 -0
  242. package/dist/memory-retention.d.ts +31 -0
  243. package/dist/memory-retention.d.ts.map +1 -0
  244. package/dist/memory-retention.js +151 -0
  245. package/dist/memory-retrieval-signals.d.ts +12 -0
  246. package/dist/memory-retrieval-signals.d.ts.map +1 -0
  247. package/dist/memory-retrieval-signals.js +100 -0
  248. package/dist/memory-salience.d.ts +12 -0
  249. package/dist/memory-salience.d.ts.map +1 -0
  250. package/dist/memory-salience.js +154 -0
  251. package/dist/memory-scope-sanitizer.d.ts +6 -0
  252. package/dist/memory-scope-sanitizer.d.ts.map +1 -0
  253. package/dist/memory-scope-sanitizer.js +106 -0
  254. package/dist/memory-target-resolver.d.ts +4 -0
  255. package/dist/memory-target-resolver.d.ts.map +1 -0
  256. package/dist/memory-target-resolver.js +73 -0
  257. package/dist/memory-workflow-port.d.ts +14 -0
  258. package/dist/memory-workflow-port.d.ts.map +1 -0
  259. package/dist/memory-workflow-port.js +186 -0
  260. package/dist/private-json.d.ts +3 -0
  261. package/dist/private-json.d.ts.map +1 -0
  262. package/dist/private-json.js +62 -0
  263. package/dist/promptEnhancer/index.d.ts +3 -0
  264. package/dist/promptEnhancer/index.d.ts.map +1 -0
  265. package/dist/promptEnhancer/index.js +5 -0
  266. package/dist/promptEnhancer/orchestrate.d.ts +2 -0
  267. package/dist/promptEnhancer/orchestrate.d.ts.map +1 -0
  268. package/dist/promptEnhancer/orchestrate.js +5 -0
  269. package/dist/promptEnhancer/routes.d.ts +9 -0
  270. package/dist/promptEnhancer/routes.d.ts.map +1 -0
  271. package/dist/promptEnhancer/routes.js +205 -0
  272. package/dist/qualityIntelligence/capsuleAdapter.d.ts +27 -0
  273. package/dist/qualityIntelligence/capsuleAdapter.d.ts.map +1 -0
  274. package/dist/qualityIntelligence/capsuleAdapter.js +57 -0
  275. package/dist/qualityIntelligence/connectorAuthorization.d.ts +22 -0
  276. package/dist/qualityIntelligence/connectorAuthorization.d.ts.map +1 -0
  277. package/dist/qualityIntelligence/connectorAuthorization.js +35 -0
  278. package/dist/qualityIntelligence/connectorErrors.d.ts +16 -0
  279. package/dist/qualityIntelligence/connectorErrors.d.ts.map +1 -0
  280. package/dist/qualityIntelligence/connectorErrors.js +56 -0
  281. package/dist/qualityIntelligence/connectorRoutes.d.ts +7 -0
  282. package/dist/qualityIntelligence/connectorRoutes.d.ts.map +1 -0
  283. package/dist/qualityIntelligence/connectorRoutes.js +167 -0
  284. package/dist/qualityIntelligence/editRoutes.d.ts +5 -0
  285. package/dist/qualityIntelligence/editRoutes.d.ts.map +1 -0
  286. package/dist/qualityIntelligence/editRoutes.js +293 -0
  287. package/dist/qualityIntelligence/exportAssembly.d.ts +22 -0
  288. package/dist/qualityIntelligence/exportAssembly.d.ts.map +1 -0
  289. package/dist/qualityIntelligence/exportAssembly.js +352 -0
  290. package/dist/qualityIntelligence/exportRoutes.d.ts +5 -0
  291. package/dist/qualityIntelligence/exportRoutes.d.ts.map +1 -0
  292. package/dist/qualityIntelligence/exportRoutes.js +320 -0
  293. package/dist/qualityIntelligence/figma/figmaConcurrency.d.ts +8 -0
  294. package/dist/qualityIntelligence/figma/figmaConcurrency.d.ts.map +1 -0
  295. package/dist/qualityIntelligence/figma/figmaConcurrency.js +34 -0
  296. package/dist/qualityIntelligence/figma/figmaConnector.d.ts +65 -0
  297. package/dist/qualityIntelligence/figma/figmaConnector.d.ts.map +1 -0
  298. package/dist/qualityIntelligence/figma/figmaConnector.js +184 -0
  299. package/dist/qualityIntelligence/figma/figmaConnectorAudit.d.ts +52 -0
  300. package/dist/qualityIntelligence/figma/figmaConnectorAudit.d.ts.map +1 -0
  301. package/dist/qualityIntelligence/figma/figmaConnectorAudit.js +63 -0
  302. package/dist/qualityIntelligence/figma/figmaConnectorErrors.d.ts +31 -0
  303. package/dist/qualityIntelligence/figma/figmaConnectorErrors.d.ts.map +1 -0
  304. package/dist/qualityIntelligence/figma/figmaConnectorErrors.js +220 -0
  305. package/dist/qualityIntelligence/figma/figmaConnectorMetrics.d.ts +44 -0
  306. package/dist/qualityIntelligence/figma/figmaConnectorMetrics.d.ts.map +1 -0
  307. package/dist/qualityIntelligence/figma/figmaConnectorMetrics.js +49 -0
  308. package/dist/qualityIntelligence/figma/figmaConsent.d.ts +39 -0
  309. package/dist/qualityIntelligence/figma/figmaConsent.d.ts.map +1 -0
  310. package/dist/qualityIntelligence/figma/figmaConsent.js +62 -0
  311. package/dist/qualityIntelligence/figma/figmaHttpPort.d.ts +28 -0
  312. package/dist/qualityIntelligence/figma/figmaHttpPort.d.ts.map +1 -0
  313. package/dist/qualityIntelligence/figma/figmaHttpPort.js +70 -0
  314. package/dist/qualityIntelligence/figma/figmaObservedActions.d.ts +49 -0
  315. package/dist/qualityIntelligence/figma/figmaObservedActions.d.ts.map +1 -0
  316. package/dist/qualityIntelligence/figma/figmaObservedActions.js +89 -0
  317. package/dist/qualityIntelligence/figma/figmaReadiness.d.ts +32 -0
  318. package/dist/qualityIntelligence/figma/figmaReadiness.d.ts.map +1 -0
  319. package/dist/qualityIntelligence/figma/figmaReadiness.js +67 -0
  320. package/dist/qualityIntelligence/figma/figmaRenderPort.d.ts +29 -0
  321. package/dist/qualityIntelligence/figma/figmaRenderPort.d.ts.map +1 -0
  322. package/dist/qualityIntelligence/figma/figmaRenderPort.js +93 -0
  323. package/dist/qualityIntelligence/figma/figmaResnapshot.d.ts +28 -0
  324. package/dist/qualityIntelligence/figma/figmaResnapshot.d.ts.map +1 -0
  325. package/dist/qualityIntelligence/figma/figmaResnapshot.js +38 -0
  326. package/dist/qualityIntelligence/figma/figmaRetry.d.ts +31 -0
  327. package/dist/qualityIntelligence/figma/figmaRetry.d.ts.map +1 -0
  328. package/dist/qualityIntelligence/figma/figmaRetry.js +62 -0
  329. package/dist/qualityIntelligence/figma/figmaScopeRef.d.ts +9 -0
  330. package/dist/qualityIntelligence/figma/figmaScopeRef.d.ts.map +1 -0
  331. package/dist/qualityIntelligence/figma/figmaScopeRef.js +18 -0
  332. package/dist/qualityIntelligence/figma/figmaScopedPagination.d.ts +86 -0
  333. package/dist/qualityIntelligence/figma/figmaScopedPagination.d.ts.map +1 -0
  334. package/dist/qualityIntelligence/figma/figmaScopedPagination.js +308 -0
  335. package/dist/qualityIntelligence/figma/figmaSnapshotBuilder.d.ts +31 -0
  336. package/dist/qualityIntelligence/figma/figmaSnapshotBuilder.d.ts.map +1 -0
  337. package/dist/qualityIntelligence/figma/figmaSnapshotBuilder.js +314 -0
  338. package/dist/qualityIntelligence/figma/figmaSnapshotHash.d.ts +18 -0
  339. package/dist/qualityIntelligence/figma/figmaSnapshotHash.d.ts.map +1 -0
  340. package/dist/qualityIntelligence/figma/figmaSnapshotHash.js +63 -0
  341. package/dist/qualityIntelligence/figma/figmaSnapshotTypes.d.ts +65 -0
  342. package/dist/qualityIntelligence/figma/figmaSnapshotTypes.d.ts.map +1 -0
  343. package/dist/qualityIntelligence/figma/figmaSnapshotTypes.js +13 -0
  344. package/dist/qualityIntelligence/figma/figmaTokenSource.d.ts +9 -0
  345. package/dist/qualityIntelligence/figma/figmaTokenSource.d.ts.map +1 -0
  346. package/dist/qualityIntelligence/figma/figmaTokenSource.js +61 -0
  347. package/dist/qualityIntelligence/figma/figmaTokenStore.d.ts +19 -0
  348. package/dist/qualityIntelligence/figma/figmaTokenStore.d.ts.map +1 -0
  349. package/dist/qualityIntelligence/figma/figmaTokenStore.js +156 -0
  350. package/dist/qualityIntelligence/figma/figmaUrl.d.ts +6 -0
  351. package/dist/qualityIntelligence/figma/figmaUrl.d.ts.map +1 -0
  352. package/dist/qualityIntelligence/figma/figmaUrl.js +36 -0
  353. package/dist/qualityIntelligence/figma/index.d.ts +20 -0
  354. package/dist/qualityIntelligence/figma/index.d.ts.map +1 -0
  355. package/dist/qualityIntelligence/figma/index.js +26 -0
  356. package/dist/qualityIntelligence/figmaCodegenRoutes.d.ts +28 -0
  357. package/dist/qualityIntelligence/figmaCodegenRoutes.d.ts.map +1 -0
  358. package/dist/qualityIntelligence/figmaCodegenRoutes.js +165 -0
  359. package/dist/qualityIntelligence/figmaSnapshotAdapter.d.ts +55 -0
  360. package/dist/qualityIntelligence/figmaSnapshotAdapter.d.ts.map +1 -0
  361. package/dist/qualityIntelligence/figmaSnapshotAdapter.js +219 -0
  362. package/dist/qualityIntelligence/figmaSnapshotOrchestration.d.ts +64 -0
  363. package/dist/qualityIntelligence/figmaSnapshotOrchestration.d.ts.map +1 -0
  364. package/dist/qualityIntelligence/figmaSnapshotOrchestration.js +203 -0
  365. package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts +112 -0
  366. package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts.map +1 -0
  367. package/dist/qualityIntelligence/figmaSnapshotRoutes.js +1063 -0
  368. package/dist/qualityIntelligence/figmaSnapshotScreenIds.d.ts +19 -0
  369. package/dist/qualityIntelligence/figmaSnapshotScreenIds.d.ts.map +1 -0
  370. package/dist/qualityIntelligence/figmaSnapshotScreenIds.js +75 -0
  371. package/dist/qualityIntelligence/generationPort.d.ts +15 -0
  372. package/dist/qualityIntelligence/generationPort.d.ts.map +1 -0
  373. package/dist/qualityIntelligence/generationPort.js +185 -0
  374. package/dist/qualityIntelligence/handoffErrors.d.ts +9 -0
  375. package/dist/qualityIntelligence/handoffErrors.d.ts.map +1 -0
  376. package/dist/qualityIntelligence/handoffErrors.js +21 -0
  377. package/dist/qualityIntelligence/handoffRoutes.d.ts +15 -0
  378. package/dist/qualityIntelligence/handoffRoutes.d.ts.map +1 -0
  379. package/dist/qualityIntelligence/handoffRoutes.js +341 -0
  380. package/dist/qualityIntelligence/index.d.ts +17 -0
  381. package/dist/qualityIntelligence/index.d.ts.map +1 -0
  382. package/dist/qualityIntelligence/index.js +36 -0
  383. package/dist/qualityIntelligence/judgePort.d.ts +30 -0
  384. package/dist/qualityIntelligence/judgePort.d.ts.map +1 -0
  385. package/dist/qualityIntelligence/judgePort.js +326 -0
  386. package/dist/qualityIntelligence/modelSelection.d.ts +58 -0
  387. package/dist/qualityIntelligence/modelSelection.d.ts.map +1 -0
  388. package/dist/qualityIntelligence/modelSelection.js +148 -0
  389. package/dist/qualityIntelligence/reCheckRoutes.d.ts +6 -0
  390. package/dist/qualityIntelligence/reCheckRoutes.d.ts.map +1 -0
  391. package/dist/qualityIntelligence/reCheckRoutes.js +1157 -0
  392. package/dist/qualityIntelligence/retentionEnforcement.d.ts +13 -0
  393. package/dist/qualityIntelligence/retentionEnforcement.d.ts.map +1 -0
  394. package/dist/qualityIntelligence/retentionEnforcement.js +47 -0
  395. package/dist/qualityIntelligence/retentionRoutes.d.ts +8 -0
  396. package/dist/qualityIntelligence/retentionRoutes.d.ts.map +1 -0
  397. package/dist/qualityIntelligence/retentionRoutes.js +74 -0
  398. package/dist/qualityIntelligence/reviewRoutes.d.ts +5 -0
  399. package/dist/qualityIntelligence/reviewRoutes.d.ts.map +1 -0
  400. package/dist/qualityIntelligence/reviewRoutes.js +145 -0
  401. package/dist/qualityIntelligence/reviewStore.d.ts +75 -0
  402. package/dist/qualityIntelligence/reviewStore.d.ts.map +1 -0
  403. package/dist/qualityIntelligence/reviewStore.js +170 -0
  404. package/dist/qualityIntelligence/runExecution.d.ts +36 -0
  405. package/dist/qualityIntelligence/runExecution.d.ts.map +1 -0
  406. package/dist/qualityIntelligence/runExecution.js +180 -0
  407. package/dist/qualityIntelligence/runIngestion.d.ts +70 -0
  408. package/dist/qualityIntelligence/runIngestion.d.ts.map +1 -0
  409. package/dist/qualityIntelligence/runIngestion.js +1235 -0
  410. package/dist/qualityIntelligence/runRegistry.d.ts +31 -0
  411. package/dist/qualityIntelligence/runRegistry.d.ts.map +1 -0
  412. package/dist/qualityIntelligence/runRegistry.js +66 -0
  413. package/dist/qualityIntelligence/runRoutes.d.ts +16 -0
  414. package/dist/qualityIntelligence/runRoutes.d.ts.map +1 -0
  415. package/dist/qualityIntelligence/runRoutes.js +357 -0
  416. package/dist/qualityIntelligence/traceabilityRoutes.d.ts +5 -0
  417. package/dist/qualityIntelligence/traceabilityRoutes.d.ts.map +1 -0
  418. package/dist/qualityIntelligence/traceabilityRoutes.js +173 -0
  419. package/dist/qualityIntelligence/uiRoutes.d.ts +7 -0
  420. package/dist/qualityIntelligence/uiRoutes.d.ts.map +1 -0
  421. package/dist/qualityIntelligence/uiRoutes.js +336 -0
  422. package/dist/read-handlers.d.ts +9 -0
  423. package/dist/read-handlers.d.ts.map +1 -0
  424. package/dist/read-handlers.js +265 -0
  425. package/dist/relationship-handlers.d.ts +191 -0
  426. package/dist/relationship-handlers.d.ts.map +1 -0
  427. package/dist/relationship-handlers.js +0 -0
  428. package/dist/routes.d.ts +37 -0
  429. package/dist/routes.d.ts.map +1 -0
  430. package/dist/routes.js +507 -0
  431. package/dist/run-engine.d.ts +25 -0
  432. package/dist/run-engine.d.ts.map +1 -0
  433. package/dist/run-engine.js +385 -0
  434. package/dist/run-handlers.d.ts +9 -0
  435. package/dist/run-handlers.d.ts.map +1 -0
  436. package/dist/run-handlers.js +465 -0
  437. package/dist/run-request.d.ts +17 -0
  438. package/dist/run-request.d.ts.map +1 -0
  439. package/dist/run-request.js +219 -0
  440. package/dist/runs.d.ts +47 -0
  441. package/dist/runs.d.ts.map +1 -0
  442. package/dist/runs.js +100 -0
  443. package/dist/server.d.ts +13 -0
  444. package/dist/server.d.ts.map +1 -0
  445. package/dist/server.js +152 -0
  446. package/dist/sink.d.ts +28 -0
  447. package/dist/sink.d.ts.map +1 -0
  448. package/dist/sink.js +80 -0
  449. package/dist/sse-write.d.ts +9 -0
  450. package/dist/sse-write.d.ts.map +1 -0
  451. package/dist/sse-write.js +26 -0
  452. package/dist/sse.d.ts +8 -0
  453. package/dist/sse.d.ts.map +1 -0
  454. package/dist/sse.js +27 -0
  455. package/dist/static.d.ts +5 -0
  456. package/dist/static.d.ts.map +1 -0
  457. package/dist/static.js +76 -0
  458. package/dist/store/chats.d.ts +17 -0
  459. package/dist/store/chats.d.ts.map +1 -0
  460. package/dist/store/chats.js +624 -0
  461. package/dist/store/db.d.ts +11 -0
  462. package/dist/store/db.d.ts.map +1 -0
  463. package/dist/store/db.js +203 -0
  464. package/dist/store/errors.d.ts +13 -0
  465. package/dist/store/errors.d.ts.map +1 -0
  466. package/dist/store/errors.js +30 -0
  467. package/dist/store/index.d.ts +7 -0
  468. package/dist/store/index.d.ts.map +1 -0
  469. package/dist/store/index.js +6 -0
  470. package/dist/store/messages.d.ts +8 -0
  471. package/dist/store/messages.d.ts.map +1 -0
  472. package/dist/store/messages.js +149 -0
  473. package/dist/store/paths.d.ts +5 -0
  474. package/dist/store/paths.d.ts.map +1 -0
  475. package/dist/store/paths.js +84 -0
  476. package/dist/store/projects.d.ts +8 -0
  477. package/dist/store/projects.d.ts.map +1 -0
  478. package/dist/store/projects.js +59 -0
  479. package/dist/store/relationship-audit.d.ts +42 -0
  480. package/dist/store/relationship-audit.d.ts.map +1 -0
  481. package/dist/store/relationship-audit.js +155 -0
  482. package/dist/store/relationships.d.ts +191 -0
  483. package/dist/store/relationships.d.ts.map +1 -0
  484. package/dist/store/relationships.js +724 -0
  485. package/dist/store/schema.d.ts +4 -0
  486. package/dist/store/schema.d.ts.map +1 -0
  487. package/dist/store/schema.js +220 -0
  488. package/dist/store/types.d.ts +29 -0
  489. package/dist/store/types.d.ts.map +1 -0
  490. package/dist/store/types.js +8 -0
  491. package/dist/store/validation.d.ts +7 -0
  492. package/dist/store/validation.d.ts.map +1 -0
  493. package/dist/store/validation.js +117 -0
  494. package/dist/store-handlers.d.ts +17 -0
  495. package/dist/store-handlers.d.ts.map +1 -0
  496. package/dist/store-handlers.js +872 -0
  497. package/dist/terminal-errors.d.ts +22 -0
  498. package/dist/terminal-errors.d.ts.map +1 -0
  499. package/dist/terminal-errors.js +45 -0
  500. package/dist/terminal-evidence.d.ts +21 -0
  501. package/dist/terminal-evidence.d.ts.map +1 -0
  502. package/dist/terminal-evidence.js +65 -0
  503. package/dist/terminal-routes.d.ts +10 -0
  504. package/dist/terminal-routes.d.ts.map +1 -0
  505. package/dist/terminal-routes.js +219 -0
  506. package/dist/terminal.d.ts +68 -0
  507. package/dist/terminal.d.ts.map +1 -0
  508. package/dist/terminal.js +855 -0
  509. package/package.json +52 -0
@@ -0,0 +1,877 @@
1
+ // BFF route POST /api/chats/messages/grounded (Issue #185 / Epic #177). Composes the
2
+ // orchestrator's pure pipeline with the UiStore so a single HTTP round trip persists both
3
+ // the user question and the assistant answer alongside a redacted citation projection.
4
+ // All path validation runs in the composed layers; this module only validates wire-shape
5
+ // inputs (chatId + content) and enforces that the chat carries a connected scope.
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import { realpathSync } from "node:fs";
8
+ import { basename } from "node:path";
9
+ import { CancelledError, ContextOverflowError, GatewayError, findCapability, findConfiguredCapability, resolveCostClass, } from "@oscharko-dev/keiko-model-gateway";
10
+ import { persistConnectedContextEvidence } from "@oscharko-dev/keiko-evidence";
11
+ import { redact } from "@oscharko-dev/keiko-security";
12
+ import { RepoSearchInvalidQueryError, RepoSearchInvalidRangeError, RepoSearchUnsupportedFileError, } from "@oscharko-dev/keiko-workspace";
13
+ import { CONNECTED_CONTEXT_SCHEMA_VERSION, validateConnectedContextPack, } from "@oscharko-dev/keiko-contracts/connected-context";
14
+ import { buildGroundedAnswerContextPackSummary, } from "@oscharko-dev/keiko-contracts/bff-wire";
15
+ import { stripUnsafeFormatChars } from "@oscharko-dev/keiko-contracts/text-safety";
16
+ import { errorBody } from "./routes.js";
17
+ import { currentGatewayConfig, currentGroundingLimits, currentRedactionSecrets } from "./deps.js";
18
+ import { ClarificationNeededError, clarificationUserMessage, runGroundedExploration, } from "./grounded-orchestrator.js";
19
+ import { microIndexForGroundedScope } from "./grounded-context-index.js";
20
+ import { pathIsDenied } from "./files-deny.js";
21
+ import { handleLocalKnowledgeGroundedAsk } from "./local-knowledge-grounded-qa.js";
22
+ import { buildConnectedScopes, createMultiSourceAnswerer, defaultRetriever, runMultiSourceAsk, } from "./grounded-qa-multi-source.js";
23
+ import { buildLocalKnowledgeScopes, runHybridGroundedAsk, } from "./grounded-qa-hybrid.js";
24
+ import { GROUNDED_SYSTEM_PROMPT } from "./grounded-prompt.js";
25
+ import { rememberGroundedTurn } from "./grounded-turn-registry.js";
26
+ import { assertUsableAssistantContent } from "./assistant-response.js";
27
+ // ─── Body parsing (mirrors store-handlers' bounded reader) ────────────────────
28
+ const MAX_BODY_BYTES = 128_000;
29
+ const MAX_CONTENT_CHARS = 16_000;
30
+ class BodyTooLargeError extends Error {
31
+ constructor() {
32
+ super("body too large");
33
+ this.name = "BodyTooLargeError";
34
+ }
35
+ }
36
+ function readBody(req) {
37
+ return new Promise((resolve, reject) => {
38
+ const chunks = [];
39
+ let total = 0;
40
+ let capped = false;
41
+ req.on("data", (chunk) => {
42
+ total += chunk.length;
43
+ if (total > MAX_BODY_BYTES) {
44
+ if (!capped) {
45
+ capped = true;
46
+ chunks.length = 0;
47
+ reject(new BodyTooLargeError());
48
+ req.resume();
49
+ }
50
+ return;
51
+ }
52
+ chunks.push(chunk);
53
+ });
54
+ req.on("end", () => {
55
+ if (!capped)
56
+ resolve(Buffer.concat(chunks).toString("utf8"));
57
+ });
58
+ req.on("error", reject);
59
+ });
60
+ }
61
+ export function badRequest(message) {
62
+ return { status: 400, body: errorBody("BAD_REQUEST", message) };
63
+ }
64
+ export function clarificationRequest(message) {
65
+ return { status: 400, body: errorBody("CLARIFICATION_NEEDED", message) };
66
+ }
67
+ function notFound(message) {
68
+ return { status: 404, body: errorBody("NOT_FOUND", message) };
69
+ }
70
+ function payloadTooLarge() {
71
+ return {
72
+ status: 413,
73
+ body: errorBody("PAYLOAD_TOO_LARGE", "Request body exceeds the size limit."),
74
+ };
75
+ }
76
+ export function internalError(message) {
77
+ return { status: 500, body: errorBody("INTERNAL", message) };
78
+ }
79
+ // Issue #154 (GAP-B) — the dynamic `error.message` of a GatewayError may echo the provider base
80
+ // URL, an `Authorization: Bearer …` header, or an `api-key: …` value back from the provider's
81
+ // response. It is scrubbed through the SAME boundary as the desktop chat path
82
+ // (redact + currentRedactionSecrets) before crossing the browser wire. The static `error.code`
83
+ // enum and the fixed cancellation string carry no caller data and stay verbatim. Reading the LIVE
84
+ // secrets via currentRedactionSecrets(deps) (not the startup snapshot) scrubs apiKey/baseUrl values
85
+ // added through PATCH /api/gateway/config after process start (Epic #177).
86
+ function gatewayErrorResult(error, deps) {
87
+ if (error instanceof CancelledError) {
88
+ return { status: 499, body: errorBody(error.code, "Grounded request was cancelled.") };
89
+ }
90
+ const status = error.code === "GATEWAY_AUTHENTICATION" ? 401 : error.retryable ? 503 : 502;
91
+ const message = redact(error.message, currentRedactionSecrets(deps));
92
+ return { status, body: errorBody(error.code, message) };
93
+ }
94
+ export function mappedGatewayError(error, deps) {
95
+ return error instanceof GatewayError ? gatewayErrorResult(error, deps) : undefined;
96
+ }
97
+ export function mappedWorkspaceError(error) {
98
+ if (error instanceof RepoSearchInvalidQueryError ||
99
+ error instanceof RepoSearchInvalidRangeError ||
100
+ error instanceof RepoSearchUnsupportedFileError) {
101
+ return badRequest(error.message);
102
+ }
103
+ return undefined;
104
+ }
105
+ export function isValidGroundedPack(pack) {
106
+ try {
107
+ return validateConnectedContextPack(pack).ok;
108
+ }
109
+ catch {
110
+ return false;
111
+ }
112
+ }
113
+ function parseJsonObject(raw) {
114
+ let parsed;
115
+ try {
116
+ parsed = raw.length === 0 ? {} : JSON.parse(raw);
117
+ }
118
+ catch {
119
+ return { kind: "err", result: badRequest("Request body is not valid JSON.") };
120
+ }
121
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
122
+ return { kind: "err", result: badRequest("Request body must be a JSON object.") };
123
+ }
124
+ return { kind: "ok", value: parsed };
125
+ }
126
+ function parseBody(raw) {
127
+ const objResult = parseJsonObject(raw);
128
+ if (objResult.kind === "err")
129
+ return objResult;
130
+ const obj = objResult.value;
131
+ const chatId = typeof obj.chatId === "string" ? obj.chatId : "";
132
+ const content = typeof obj.content === "string" ? obj.content.trim() : "";
133
+ let modelId;
134
+ if ("modelId" in obj) {
135
+ if (typeof obj.modelId !== "string" || obj.modelId.trim().length === 0) {
136
+ return {
137
+ kind: "err",
138
+ result: badRequest('Field "modelId" must be a non-empty string when provided.'),
139
+ };
140
+ }
141
+ modelId = obj.modelId.trim();
142
+ }
143
+ if (chatId.length === 0) {
144
+ return { kind: "err", result: badRequest('Field "chatId" is required.') };
145
+ }
146
+ if (content.length === 0 || content.length > MAX_CONTENT_CHARS) {
147
+ return {
148
+ kind: "err",
149
+ result: badRequest(`Field "content" must be between 1 and ${String(MAX_CONTENT_CHARS)} characters.`),
150
+ };
151
+ }
152
+ return { kind: "ok", value: { chatId, content, modelId } };
153
+ }
154
+ // ─── Scope / query construction ───────────────────────────────────────────────
155
+ // SHA-256(chatId + connectedAtMs) truncated to 16 hex chars — deterministic across calls so
156
+ // the assembler's pack stableId is stable for a given chat-scope binding. The scopeId is
157
+ // observable only inside the BFF; no client trust is placed on the value.
158
+ function deriveScopeId(chat) {
159
+ if (chat.connectedScope === undefined) {
160
+ return `chat-${chat.id}`;
161
+ }
162
+ const hash = createHash("sha256")
163
+ .update(`${chat.id}|${String(chat.connectedScope.connectedAtMs)}|${chat.connectedScope.root ?? ""}`)
164
+ .digest("hex");
165
+ return `cs-${hash.slice(0, 16)}`;
166
+ }
167
+ // Epic #532 — per-source scope id for the multi-source path. The index makes ids distinct even
168
+ // when two connected scopes share a root and connectedAtMs (e.g. the same folder added twice with
169
+ // differing relativePaths). The single-source path keeps `deriveScopeId(chat)` (index-free) so its
170
+ // scopeId — which the microIndex key and audit evidence derive from — stays byte-identical (AC5).
171
+ export function deriveScopeIdFrom(chat, cs, index) {
172
+ const hash = createHash("sha256")
173
+ .update(`${chat.id}|${String(cs.connectedAtMs)}|${cs.root ?? ""}|${String(index)}`)
174
+ .digest("hex");
175
+ return `cs-${hash.slice(0, 16)}`;
176
+ }
177
+ // Builds ONE SelectedScope from a connected scope. `scopeId` is supplied by the caller so the
178
+ // single path can pass the legacy `deriveScopeId(chat)` value while the multi path passes a
179
+ // per-source id; everything else is identical between the two paths.
180
+ export function buildSelectedScopeFrom(chat, cs, scopeId) {
181
+ return {
182
+ schemaVersion: CONNECTED_CONTEXT_SCHEMA_VERSION,
183
+ scopeId,
184
+ // Epic #532 — a connected folder may live outside the chat's project. When the scope carries
185
+ // its own validated root, ground against that folder; otherwise fall back to the chat project.
186
+ workspaceRoot: cs.root ?? chat.projectPath,
187
+ kind: cs.kind,
188
+ relativePaths: cs.relativePaths,
189
+ conversationId: chat.id,
190
+ connectedAtMs: cs.connectedAtMs,
191
+ // This scope was built from a user-connected folder/files (Files↔Chat edge or scope pill), so
192
+ // the planner may accept plain natural-language questions without a file/symbol anchor.
193
+ explicitConnection: true,
194
+ };
195
+ }
196
+ function buildSelectedScope(chat) {
197
+ const cs = chat.connectedScope;
198
+ if (cs === undefined)
199
+ return undefined;
200
+ return buildSelectedScopeFrom(chat, cs, deriveScopeId(chat));
201
+ }
202
+ function canonicalGroundedRoot(rootInput, deps) {
203
+ if (pathIsDenied(rootInput)) {
204
+ return badRequest("Connected scope is excluded from Keiko's safe read surface.");
205
+ }
206
+ let realRoot;
207
+ try {
208
+ realRoot = realpathSync(rootInput);
209
+ }
210
+ catch {
211
+ return badRequest("Connected scope root is not accessible.");
212
+ }
213
+ if (pathIsDenied(realRoot)) {
214
+ return badRequest("Connected scope is excluded from Keiko's safe read surface.");
215
+ }
216
+ const redacted = deps.redactor(realRoot);
217
+ if (typeof redacted === "string" && redacted !== realRoot) {
218
+ return badRequest("Connected scope root contains credential-shaped metadata.");
219
+ }
220
+ return realRoot;
221
+ }
222
+ function skippedFolderMessage(result) {
223
+ const body = result.body;
224
+ return body.error?.message ?? "not accessible";
225
+ }
226
+ // Fail-soft canonicalization: inaccessible/denied scopes are collected in `skipped` instead of
227
+ // aborting the entire request. Callers apply the hard-400 only when NO healthy scope remains.
228
+ function canonicalizeGroundedFolderScopes(chat, deps, scopes) {
229
+ const canonical = [];
230
+ const skipped = [];
231
+ for (const scope of scopes) {
232
+ const rootInput = scope.root ?? chat.projectPath;
233
+ const realRoot = canonicalGroundedRoot(rootInput, deps);
234
+ if (typeof realRoot !== "string") {
235
+ const label = scope.root !== undefined ? basename(scope.root) : "project";
236
+ skipped.push({ label, message: skippedFolderMessage(realRoot), reason: realRoot });
237
+ continue;
238
+ }
239
+ canonical.push({ ...scope, root: realRoot });
240
+ }
241
+ return { canonical, skipped };
242
+ }
243
+ function withCanonicalFolderScopes(chat, scopes) {
244
+ if (scopes.length === 0)
245
+ return chat;
246
+ return { ...chat, connectedScopes: scopes, connectedScope: scopes[0] };
247
+ }
248
+ export function buildQuery(content, nowMs) {
249
+ return {
250
+ kind: "natural-language",
251
+ text: content,
252
+ caseSensitive: false,
253
+ maxResults: 50,
254
+ emittedAtMs: nowMs(),
255
+ };
256
+ }
257
+ // ─── Model Gateway answerer ───────────────────────────────────────────────────
258
+ function chatCapability(deps, modelId) {
259
+ const config = currentGatewayConfig(deps);
260
+ return config === undefined ? findCapability(modelId) : findConfiguredCapability(config, modelId);
261
+ }
262
+ function resolveGroundedModelId(deps, chat, requestedModelId) {
263
+ const modelId = requestedModelId ?? chat.selectedModel;
264
+ const capability = chatCapability(deps, modelId);
265
+ if (capability?.kind !== "chat") {
266
+ return {
267
+ status: 400,
268
+ body: errorBody("BAD_REQUEST", "modelId must be a configured chat model id."),
269
+ };
270
+ }
271
+ return modelId;
272
+ }
273
+ function requestAbortSignal(ctx) {
274
+ const controller = new AbortController();
275
+ const abort = () => {
276
+ if (!controller.signal.aborted) {
277
+ controller.abort("grounded request cancelled");
278
+ }
279
+ };
280
+ // The req "aborted" event is deprecated since Node 17 and fires unreliably.
281
+ // The res "close" event is the canonical signal for a disconnected client.
282
+ ctx.res.on("close", () => {
283
+ if (!ctx.res.writableEnded)
284
+ abort();
285
+ });
286
+ return controller.signal;
287
+ }
288
+ export function ensureNotCancelled(signal) {
289
+ if (signal.aborted) {
290
+ throw new CancelledError("grounded request cancelled");
291
+ }
292
+ }
293
+ function formatLineRange(citation) {
294
+ if (citation.lineRange === undefined)
295
+ return citation.scopePath;
296
+ return `${citation.scopePath}:${String(citation.lineRange.startLine)}-${String(citation.lineRange.endLine)}`;
297
+ }
298
+ function redactedString(redactor, value) {
299
+ // GRD-001: strip Trojan-source / invisible format chars BEFORE redaction so a zero-width
300
+ // character cannot split a secret shape past the redactor, and so reordered/hidden text
301
+ // never reaches the prompt or the browser-rendered wire.
302
+ const safe = stripUnsafeFormatChars(value);
303
+ const redacted = redactor(safe);
304
+ return typeof redacted === "string" ? redacted : safe;
305
+ }
306
+ export function promptSafeExcerptText(value) {
307
+ return value.split("```").join("` ` `");
308
+ }
309
+ const APPROX_BYTES_PER_TOKEN = 4;
310
+ export function promptByteLength(messages) {
311
+ return Buffer.byteLength(messages.map((message) => message.content).join("\n"), "utf8");
312
+ }
313
+ export function modelInputPromptByteLimit(modelInputTokensMax) {
314
+ return Math.max(0, Math.floor(modelInputTokensMax) * APPROX_BYTES_PER_TOKEN);
315
+ }
316
+ function clampUtf8Bytes(value, maxBytes) {
317
+ if (maxBytes <= 0)
318
+ return "";
319
+ if (Buffer.byteLength(value, "utf8") <= maxBytes)
320
+ return value;
321
+ let low = 0;
322
+ let high = value.length;
323
+ while (low < high) {
324
+ const mid = Math.ceil((low + high) / 2);
325
+ if (Buffer.byteLength(value.slice(0, mid), "utf8") <= maxBytes) {
326
+ low = mid;
327
+ }
328
+ else {
329
+ high = mid - 1;
330
+ }
331
+ }
332
+ return value.slice(0, low);
333
+ }
334
+ function packExcerptCount(pack) {
335
+ return pack.files.reduce((count, file) => count + file.excerpts.length, 0);
336
+ }
337
+ export function withPromptExcerptByteLimit(pack, maxExcerptBytes) {
338
+ return {
339
+ ...pack,
340
+ files: pack.files.map((file) => ({
341
+ ...file,
342
+ excerpts: file.excerpts.map((excerpt) => ({
343
+ ...excerpt,
344
+ content: clampUtf8Bytes(excerpt.content, maxExcerptBytes),
345
+ })),
346
+ })),
347
+ };
348
+ }
349
+ function promptBudgetedMessages(question, pack, redactor, build) {
350
+ const limit = modelInputPromptByteLimit(pack.budget.modelInputTokensMax);
351
+ let messages = build(question, pack, redactor);
352
+ if (limit === 0 || promptByteLength(messages) <= limit)
353
+ return messages;
354
+ const excerptCount = packExcerptCount(pack);
355
+ if (excerptCount === 0)
356
+ return messages;
357
+ const emptyPack = withPromptExcerptByteLimit(pack, 0);
358
+ const emptyMessages = build(question, emptyPack, redactor);
359
+ const overheadBytes = promptByteLength(emptyMessages);
360
+ // When overhead alone (system prompt + question + framing) exceeds the limit, no amount of
361
+ // excerpt trimming can bring the prompt within budget. Throw instead of sending an over-limit
362
+ // prompt to the provider which would result in an opaque 400 context-window error.
363
+ if (overheadBytes > limit) {
364
+ throw new ContextOverflowError(`Grounded prompt overhead (${String(overheadBytes)} bytes) exceeds model input limit (${String(limit)} bytes).`);
365
+ }
366
+ let maxExcerptBytes = Math.max(0, Math.floor((limit - overheadBytes) / excerptCount));
367
+ while (maxExcerptBytes >= 0) {
368
+ messages = build(question, withPromptExcerptByteLimit(pack, maxExcerptBytes), redactor);
369
+ if (promptByteLength(messages) <= limit || maxExcerptBytes === 0) {
370
+ return messages;
371
+ }
372
+ maxExcerptBytes = Math.max(0, Math.floor(maxExcerptBytes * 0.8));
373
+ }
374
+ return emptyMessages;
375
+ }
376
+ export function packBudgetSummary(pack) {
377
+ const { usage, budget } = pack;
378
+ return [
379
+ `search calls ${String(usage.searchCalls)}/${String(budget.searchCallsMax)}`,
380
+ `files read ${String(usage.filesRead)}/${String(budget.filesReadMax)}`,
381
+ `excerpt bytes ${String(usage.excerptBytes)}/${String(budget.excerptBytesMax)}`,
382
+ `model input tokens ${String(usage.modelInputTokens)}/${String(budget.modelInputTokensMax)}`,
383
+ `model output tokens ${String(usage.modelOutputTokens)}/${String(budget.modelOutputTokensMax)}`,
384
+ `rerank calls ${String(usage.rerankCalls)}/${String(budget.rerankCallsMax)}`,
385
+ `elapsed ${String(usage.elapsedMs)}/${String(budget.elapsedMsMax)} ms`,
386
+ ].join("; ");
387
+ }
388
+ // Bounded document extraction (Issue #1285): a citation derived from a `document-extract` atom
389
+ // refers to a connected DOCX/XLSX/PDF rather than a code/text file. The format token is derived
390
+ // from the (already-redacted) scopePath suffix so prompt framing and browser citations can label
391
+ // document evidence distinctly without re-deriving it in the UI.
392
+ function documentFormatForAtom(atom) {
393
+ if (atom.provenance.kind !== "document-extract") {
394
+ return undefined;
395
+ }
396
+ const path = atom.scopePath.toLowerCase();
397
+ if (path.endsWith(".docx")) {
398
+ return "docx";
399
+ }
400
+ if (path.endsWith(".xlsx")) {
401
+ return "xlsx";
402
+ }
403
+ if (path.endsWith(".pdf")) {
404
+ return "pdf";
405
+ }
406
+ return undefined;
407
+ }
408
+ export function evidenceLines(pack, redactor) {
409
+ const lines = [];
410
+ for (const file of pack.files) {
411
+ lines.push(`File: ${redactedString(redactor, file.scopePath)}`);
412
+ if (file.excerpts.length === 0) {
413
+ lines.push("- No excerpt content was available for this selected file.");
414
+ continue;
415
+ }
416
+ for (const excerpt of file.excerpts) {
417
+ const citation = formatLineRange({
418
+ scopePath: excerpt.atom.scopePath,
419
+ lineRange: excerpt.atom.lineRange,
420
+ score: excerpt.atom.score,
421
+ stableId: excerpt.atom.stableId,
422
+ });
423
+ const documentFormat = documentFormatForAtom(excerpt.atom);
424
+ const label = documentFormat === undefined
425
+ ? "Evidence"
426
+ : `Document evidence (${documentFormat.toUpperCase()}, extracted text)`;
427
+ lines.push(`- ${label} ${redactedString(redactor, citation)} (score ${excerpt.atom.score.toFixed(2)}):`);
428
+ lines.push("```");
429
+ lines.push(promptSafeExcerptText(redactedString(redactor, excerpt.content)));
430
+ lines.push("```");
431
+ }
432
+ }
433
+ if (lines.length === 0) {
434
+ lines.push("No evidence excerpts were selected for this question.");
435
+ }
436
+ return lines;
437
+ }
438
+ export function uncertaintyLines(pack, redactor) {
439
+ if (pack.uncertainty.length === 0)
440
+ return ["None."];
441
+ return pack.uncertainty.map((marker) => `- ${marker.kind}: ${redactedString(redactor, marker.claim)}`);
442
+ }
443
+ // The grounded system message is shared verbatim by the single-source and multi-source (#532)
444
+ // paths so both apply the identical untrusted-evidence + citation + no-secret guardrails. The
445
+ // single-source wire output must stay byte-identical (AC5), so this literal must not change.
446
+ // GROUNDED_SYSTEM_PROMPT now lives in the dependency-free ./grounded-prompt.js leaf (re-exported
447
+ // here for back-compat) so the hybrid path can interpolate it without a circular-import TDZ.
448
+ export { GROUNDED_SYSTEM_PROMPT };
449
+ function buildRawGroundedGatewayMessages(question, pack, redactor) {
450
+ const safeQuestion = redactedString(redactor, question);
451
+ const userContent = [
452
+ "User question:",
453
+ safeQuestion,
454
+ "",
455
+ "Connected repository context pack:",
456
+ `- schemaVersion: ${pack.schemaVersion}`,
457
+ `- stableId: ${redactedString(redactor, pack.stableId)}`,
458
+ `- scope kind: ${pack.scope.kind}`,
459
+ `- query kind: ${pack.query.kind}`,
460
+ `- budget/usage: ${packBudgetSummary(pack)}`,
461
+ `- omitted evidence atoms: ${String(pack.omitted.length)}`,
462
+ "",
463
+ "Repository evidence excerpts:",
464
+ ...evidenceLines(pack, redactor),
465
+ "",
466
+ "Known uncertainty from retrieval:",
467
+ ...uncertaintyLines(pack, redactor),
468
+ ].join("\n");
469
+ return [
470
+ { role: "system", content: GROUNDED_SYSTEM_PROMPT },
471
+ { role: "user", content: userContent },
472
+ ];
473
+ }
474
+ export function buildGroundedGatewayMessages(question, pack, redactor) {
475
+ return promptBudgetedMessages(question, pack, redactor, buildRawGroundedGatewayMessages);
476
+ }
477
+ function createGatewayAnswerer(model, modelId, redactor, signal) {
478
+ return {
479
+ answer: async (question, pack) => {
480
+ ensureNotCancelled(signal);
481
+ const response = await model.call({
482
+ modelId,
483
+ messages: buildGroundedGatewayMessages(question, pack, redactor),
484
+ stream: false,
485
+ }, signal);
486
+ const content = response.content.trim();
487
+ assertUsableAssistantContent(content, modelId);
488
+ return {
489
+ content,
490
+ usage: {
491
+ promptTokens: response.usage.promptTokens,
492
+ completionTokens: response.usage.completionTokens,
493
+ },
494
+ };
495
+ },
496
+ };
497
+ }
498
+ function defaultRunner(deps, modelId, signal) {
499
+ const model = deps.modelPortFactory(modelId);
500
+ if (model === undefined) {
501
+ return { status: 400, body: errorBody("NO_MODEL", "No model provider is configured.") };
502
+ }
503
+ return (input) => {
504
+ const nowMs = Date.now;
505
+ return runGroundedExploration(input, {
506
+ answerer: createGatewayAnswerer(model, modelId, deps.redactor, signal),
507
+ nowMs,
508
+ signal,
509
+ microIndex: microIndexForGroundedScope(input.scope, nowMs),
510
+ });
511
+ };
512
+ }
513
+ // ─── Citation projection ──────────────────────────────────────────────────────
514
+ export function redactString(redactor, value) {
515
+ // GRD-001: strip Trojan-source / invisible format chars before redaction (see redactedString).
516
+ return redactor(stripUnsafeFormatChars(value));
517
+ }
518
+ export function buildCitations(pack, redactor) {
519
+ const citations = [];
520
+ for (const file of pack.files) {
521
+ for (const excerpt of file.excerpts) {
522
+ const documentFormat = documentFormatForAtom(excerpt.atom);
523
+ citations.push({
524
+ scopePath: redactString(redactor, excerpt.atom.scopePath),
525
+ lineRange: excerpt.atom.lineRange,
526
+ score: excerpt.atom.score,
527
+ stableId: redactString(redactor, excerpt.atom.stableId),
528
+ ...(documentFormat === undefined ? {} : { documentFormat }),
529
+ });
530
+ }
531
+ }
532
+ citations.sort((a, b) => b.score - a.score);
533
+ return citations;
534
+ }
535
+ export function buildUncertainty(pack, redactor) {
536
+ // uncertainty.claim is the one wire-visible string sourced from the in-process pack that
537
+ // can carry user-controlled text (e.g., excerpt fragments paraphrased into a confidence
538
+ // marker). Production packs SHOULD be upstream-redacted (per ADR-0019), but the BFF still
539
+ // applies the live-payload redactor as defense in depth so secret-shaped strings never
540
+ // reach the browser even when the pack assembler skips its own redaction step.
541
+ return pack.uncertainty.map((u) => ({
542
+ kind: u.kind,
543
+ claim: redactor(u.claim),
544
+ }));
545
+ }
546
+ // ─── Lookup helpers ───────────────────────────────────────────────────────────
547
+ // Epic #177 audit: the grounded-ask hot path scanned every project's chat list per request
548
+ // (O(projects × chats)). The chat id is unique across projects, so `UiStore.findChatById` is a
549
+ // single-row SELECT. This helper is kept (instead of inlining the store call) so callers can
550
+ // continue to depend on the deps surface rather than the store directly.
551
+ function findChatById(deps, chatId) {
552
+ return deps.store.findChatById(chatId);
553
+ }
554
+ // Atomic insert via the existing createMessages batch (wraps BEGIN/COMMIT) so a transient
555
+ // failure on the assistant insert rolls back the user insert. Returns both rows.
556
+ export function persistGroundedExchange(deps, chatId, userContent, assistantContent) {
557
+ const now = Date.now();
558
+ const base = {
559
+ chatId,
560
+ timestamp: now,
561
+ runId: undefined,
562
+ workflowId: undefined,
563
+ workflowStatus: undefined,
564
+ shortResult: undefined,
565
+ taskType: undefined,
566
+ };
567
+ const [user, assistant] = deps.store.createMessages([
568
+ { ...base, role: "user", content: userContent },
569
+ { ...base, role: "assistant", content: assistantContent },
570
+ ]);
571
+ if (user === undefined || assistant === undefined) {
572
+ throw new Error("createMessages returned fewer rows than expected");
573
+ }
574
+ return [user, assistant];
575
+ }
576
+ function persistGroundedAuditEvidence(workerCtx, output, citationCount) {
577
+ const finishedAt = Date.now();
578
+ const startedAt = Math.max(0, finishedAt - output.elapsedMs);
579
+ const runId = `grounded-${randomUUID()}`;
580
+ persistConnectedContextEvidence({
581
+ runId,
582
+ modelId: workerCtx.modelId,
583
+ // Epic #532 audit (L1): record the root that was ACTUALLY searched. For a connected external
584
+ // folder scope.workspaceRoot is cs.root, not chat.projectPath — the evidence ledger must name
585
+ // the real grounding root so the audit trail is honest about which tree produced the answer.
586
+ workspaceRoot: workerCtx.scope.workspaceRoot,
587
+ chatId: workerCtx.chat.id,
588
+ plan: output.plan,
589
+ pack: output.pack,
590
+ citationCount,
591
+ elapsedMs: output.elapsedMs,
592
+ startedAt,
593
+ finishedAt,
594
+ }, {
595
+ store: workerCtx.deps.evidenceStore,
596
+ env: workerCtx.deps.env,
597
+ // Epic #177 audit: read the LIVE gateway-derived secrets list so apiKey/baseUrl values
598
+ // added via the runtime PATCH /api/gateway/config path are scrubbed by the evidence
599
+ // persister. `deps.redactionSecrets` is the startup snapshot frozen by buildUiHandlerDeps.
600
+ additionalSecrets: currentRedactionSecrets(workerCtx.deps),
601
+ costClassResolver: resolveCostClass,
602
+ });
603
+ return runId;
604
+ }
605
+ async function runAsk(workerCtx) {
606
+ const { chat, content, deps } = workerCtx;
607
+ const query = buildQuery(content, () => Date.now());
608
+ const output = await runGroundedRunner(workerCtx, query);
609
+ if (isRouteResult(output))
610
+ return output;
611
+ if (!isValidGroundedPack(output.pack)) {
612
+ return internalError("Grounded answer context pack failed validation.");
613
+ }
614
+ const cancelResult = ensureRouteNotCancelled(workerCtx.signal, deps);
615
+ if (cancelResult !== undefined)
616
+ return cancelResult;
617
+ const userContent = redactString(deps.redactor, content);
618
+ const assistantContent = redactString(deps.redactor, output.assistantContent);
619
+ const citations = buildCitations(output.pack, deps.redactor);
620
+ const evidenceRunId = persistGroundedAuditEvidence(workerCtx, output, citations.length);
621
+ const [userMessage, assistantMessage] = persistGroundedExchange(deps, chat.id, userContent, assistantContent);
622
+ const contextPack = buildGroundedAnswerContextPackSummary(output.pack, citations.length, output.elapsedMs);
623
+ const answer = {
624
+ groundingKind: "connected-context",
625
+ userMessageId: userMessage.id,
626
+ assistantMessageId: assistantMessage.id,
627
+ evidenceRunId,
628
+ content: assistantContent,
629
+ citations,
630
+ uncertainty: buildUncertainty(output.pack, deps.redactor),
631
+ omittedCount: output.pack.omitted.length,
632
+ elapsedMs: output.elapsedMs,
633
+ contextPack,
634
+ };
635
+ rememberGroundedTurn({
636
+ assistantMessageId: assistantMessage.id,
637
+ chatId: chat.id,
638
+ workspaceRoot: output.pack.scope.workspaceRoot,
639
+ evidenceRunId,
640
+ packs: [output.pack],
641
+ });
642
+ return { status: 200, body: answer };
643
+ }
644
+ function isRouteResult(value) {
645
+ return typeof value === "object" && value !== null && "status" in value;
646
+ }
647
+ function ensureRouteNotCancelled(signal, deps) {
648
+ try {
649
+ ensureNotCancelled(signal);
650
+ return undefined;
651
+ }
652
+ catch (error) {
653
+ const gatewayResult = mappedGatewayError(error, deps);
654
+ if (gatewayResult !== undefined)
655
+ return gatewayResult;
656
+ throw error;
657
+ }
658
+ }
659
+ async function runGroundedRunner(workerCtx, query) {
660
+ const { scope, runner } = workerCtx;
661
+ try {
662
+ ensureNotCancelled(workerCtx.signal);
663
+ // Epic #532 — ground against the scope's own root (a folder that may live outside the chat's
664
+ // project), not the chat projectPath. buildSelectedScope set scope.workspaceRoot = cs.root ??
665
+ // chat.projectPath, so a connected external folder resolves correctly.
666
+ const output = await runner({ scope, query, workspaceRoot: scope.workspaceRoot });
667
+ ensureNotCancelled(workerCtx.signal);
668
+ return output;
669
+ }
670
+ catch (error) {
671
+ if (error instanceof ClarificationNeededError) {
672
+ return clarificationRequest(clarificationUserMessage(error));
673
+ }
674
+ const workspaceResult = mappedWorkspaceError(error);
675
+ if (workspaceResult !== undefined)
676
+ return workspaceResult;
677
+ const gatewayResult = mappedGatewayError(error, workerCtx.deps);
678
+ if (gatewayResult !== undefined)
679
+ return gatewayResult;
680
+ throw error;
681
+ }
682
+ }
683
+ async function prepareGroundedAsk(ctx, deps) {
684
+ const signal = requestAbortSignal(ctx);
685
+ let raw;
686
+ try {
687
+ raw = await readBody(ctx.req);
688
+ }
689
+ catch (error) {
690
+ if (error instanceof BodyTooLargeError)
691
+ return payloadTooLarge();
692
+ throw error;
693
+ }
694
+ const parsed = parseBody(raw);
695
+ if (parsed.kind === "err")
696
+ return parsed.result;
697
+ const chat = findChatById(deps, parsed.value.chatId);
698
+ if (chat === undefined)
699
+ return notFound("Chat not found.");
700
+ return { chat, input: parsed.value, signal };
701
+ }
702
+ function resolveGroundedRunner(deps, chat, requestedModelId, signal, runner) {
703
+ if (runner !== undefined) {
704
+ return {
705
+ modelId: requestedModelId ?? chat.selectedModel,
706
+ runner,
707
+ };
708
+ }
709
+ const modelId = resolveGroundedModelId(deps, chat, requestedModelId);
710
+ if (typeof modelId !== "string")
711
+ return modelId;
712
+ const builtRunner = defaultRunner(deps, modelId, signal);
713
+ if (typeof builtRunner !== "function")
714
+ return builtRunner;
715
+ return { modelId, runner: builtRunner };
716
+ }
717
+ function resolveMultiSourceSeam(deps, modelId, signal, override) {
718
+ if (override !== undefined)
719
+ return override;
720
+ const model = deps.modelPortFactory(modelId);
721
+ if (model === undefined) {
722
+ return { status: 400, body: errorBody("NO_MODEL", "No model provider is configured.") };
723
+ }
724
+ return {
725
+ retriever: defaultRetriever(signal),
726
+ answerer: createMultiSourceAnswerer(model, modelId, deps.redactor, signal),
727
+ };
728
+ }
729
+ async function dispatchMultiSourceAsk(args, deps, scopes, skippedFolders, seamOverride) {
730
+ const { chat, input, signal } = args;
731
+ // An injected seam (tests) bypasses model-capability resolution exactly as the single-source path
732
+ // does for an injected runner: there is no real model port to validate against. Production (no
733
+ // override) resolves the chat-model guardrails once, shared with the single path.
734
+ const modelId = seamOverride !== undefined
735
+ ? (input.modelId ?? chat.selectedModel)
736
+ : resolveGroundedModelId(deps, chat, input.modelId);
737
+ if (typeof modelId !== "string")
738
+ return modelId;
739
+ const seam = resolveMultiSourceSeam(deps, modelId, signal, seamOverride);
740
+ if ("status" in seam)
741
+ return seam;
742
+ return runMultiSourceAsk({
743
+ chat,
744
+ scopes,
745
+ content: input.content,
746
+ modelId,
747
+ deps,
748
+ retriever: seam.retriever,
749
+ answerer: seam.answerer,
750
+ signal,
751
+ preSkipped: skippedFolders.map((s) => ({ label: s.label, message: s.message })),
752
+ });
753
+ }
754
+ // Epic #532 — builds the single-source SelectedScope from the canonical list when the legacy
755
+ // `connectedScope` field is absent. Uses index 0's per-source id; this branch never applies to a
756
+ // legacy chat (which carries `connectedScope`), so the byte-identical legacy path is untouched.
757
+ function singleScopeFromList(chat, scopes) {
758
+ const cs = scopes[0];
759
+ if (cs === undefined)
760
+ return undefined;
761
+ return buildSelectedScopeFrom(chat, cs, deriveScopeIdFrom(chat, cs, 0));
762
+ }
763
+ // Epic #532 — the folder-only branch (0 handled by caller; 1 → single-source runner unless
764
+ // there are pre-skipped folders, in which case multi-source carries the skip notice; 2+ → the
765
+ // multi-source merge). Extracted so handleGroundedAsk stays the thin count-based dispatcher.
766
+ // Release 0.2.0 — ask-path defense-in-depth (mirror of the hybrid path's capSourcesToLimits):
767
+ // a stored over-cap chat (legacy rows, or an operator who later lowered the limits) must not
768
+ // fan out unboundedly at ask time. The first `cap` folders (connection order) stay live; the
769
+ // rest surface as source-skipped notices (basename label only — no path leak).
770
+ function capFolderScopesForAsk(deps, scopes, skippedFolders) {
771
+ const cap = currentGroundingLimits(deps).maxConnectedSources;
772
+ if (scopes.length <= cap)
773
+ return { scopes, skipped: skippedFolders };
774
+ const overCap = scopes.slice(cap).map((scope) => {
775
+ const label = scope.root !== undefined ? basename(scope.root) : "project";
776
+ const message = "skipped: over the connected-source limit";
777
+ return { label, message, reason: badRequest(`Source "${label}" ${message}.`) };
778
+ });
779
+ return { scopes: scopes.slice(0, cap), skipped: [...skippedFolders, ...overCap] };
780
+ }
781
+ async function dispatchFolderAsk(prepared, deps, allScopes, allSkippedFolders, runner, multiSource) {
782
+ const { chat, input, signal } = prepared;
783
+ const { scopes, skipped: skippedFolders } = capFolderScopesForAsk(deps, allScopes, allSkippedFolders);
784
+ // 2+ healthy folders or 1 healthy + some skipped → multi-source (carries skip-notice).
785
+ if (scopes.length >= 2 || (scopes.length === 1 && skippedFolders.length > 0)) {
786
+ return dispatchMultiSourceAsk(prepared, deps, scopes, skippedFolders, multiSource);
787
+ }
788
+ const scope = buildSelectedScope(chat) ?? singleScopeFromList(chat, scopes);
789
+ if (scope === undefined) {
790
+ return badRequest("Chat has no connected scope.");
791
+ }
792
+ // Epic #177 audit (GAP-B) — mirror the PATCH-route deny-list check for the grounded-ask hot
793
+ // path. The PATCH route validates via validateFallbackProjectRoot before persisting a scope;
794
+ // a chat whose projectPath was created before the deny-list was added (or via the test store)
795
+ // could otherwise reach the orchestrator with a credential-dir workspaceRoot. Reject before
796
+ // calling the runner so no filesystem access occurs against a denied path.
797
+ if (pathIsDenied(scope.workspaceRoot)) {
798
+ return badRequest("Connected scope is excluded from Keiko's safe read surface.");
799
+ }
800
+ const resolved = resolveGroundedRunner(deps, chat, input.modelId, signal, runner);
801
+ if ("status" in resolved)
802
+ return resolved;
803
+ return runAsk({
804
+ chat,
805
+ scope,
806
+ content: input.content,
807
+ modelId: resolved.modelId,
808
+ deps,
809
+ runner: resolved.runner,
810
+ signal,
811
+ });
812
+ }
813
+ function hybridSeamFields(seam) {
814
+ if (seam === undefined)
815
+ return {};
816
+ return {
817
+ ...(seam.folderRetriever !== undefined ? { folderRetriever: seam.folderRetriever } : {}),
818
+ ...(seam.connectorRetrieve !== undefined ? { connectorRetrieve: seam.connectorRetrieve } : {}),
819
+ ...(seam.answer !== undefined ? { answer: seam.answer } : {}),
820
+ };
821
+ }
822
+ async function dispatchHybridAsk(prepared, deps, skippedFolders, seam) {
823
+ const { chat, input, signal } = prepared;
824
+ // An injected answerer (tests) bypasses model-capability resolution exactly as the multi-source
825
+ // path does: there is no real model port to validate. Production resolves the guardrails once.
826
+ const modelId = seam?.answer !== undefined
827
+ ? (input.modelId ?? chat.selectedModel)
828
+ : resolveGroundedModelId(deps, chat, input.modelId);
829
+ if (typeof modelId !== "string")
830
+ return modelId;
831
+ return runHybridGroundedAsk({
832
+ chat,
833
+ content: input.content,
834
+ modelId,
835
+ deps,
836
+ signal,
837
+ preSkippedFolders: skippedFolders.map((s) => ({
838
+ label: s.label,
839
+ reason: "not-accessible",
840
+ message: s.message,
841
+ })),
842
+ ...hybridSeamFields(seam),
843
+ });
844
+ }
845
+ // ─── Public handler ───────────────────────────────────────────────────────────
846
+ export async function handleGroundedAsk(ctx, deps, runner, multiSource, hybrid) {
847
+ const prepared = await prepareGroundedAsk(ctx, deps);
848
+ if ("status" in prepared)
849
+ return prepared;
850
+ const { chat } = prepared;
851
+ // Epic #189 — count-based dispatch over BOTH source kinds. 0+0 → no scope. Connector-free chats
852
+ // keep the EXISTING folder path (#532, byte-identical). A lone connector with no folders keeps the
853
+ // EXISTING single-connector path (#189, byte-identical). Everything else (folders+connector, or
854
+ // 2+ connectors) is the hybrid merge.
855
+ // Fail-soft: inaccessible/denied folders are skipped; only effective (canonical) counts drive
856
+ // dispatch. A chat with ONLY denied/inaccessible sources still returns the original 400 so the
857
+ // user sees a clear rejection (security preserved).
858
+ const folderScopes = buildConnectedScopes(chat);
859
+ const { canonical: canonicalFolderScopes, skipped: skippedFolders } = canonicalizeGroundedFolderScopes(chat, deps, folderScopes);
860
+ const preparedWithCanonicalFolders = {
861
+ ...prepared,
862
+ chat: withCanonicalFolderScopes(chat, canonicalFolderScopes),
863
+ };
864
+ const connectorCount = buildLocalKnowledgeScopes(chat).length;
865
+ const effectiveFolders = canonicalFolderScopes.length;
866
+ if (effectiveFolders === 0 && connectorCount === 0) {
867
+ // Hard-fail: return the first skipped reason (preserves exact 400 message) or the generic guard.
868
+ return skippedFolders[0]?.reason ?? badRequest("Chat has no connected scope.");
869
+ }
870
+ if (connectorCount === 0) {
871
+ return dispatchFolderAsk(preparedWithCanonicalFolders, deps, canonicalFolderScopes, skippedFolders, runner, multiSource);
872
+ }
873
+ if (effectiveFolders === 0 && connectorCount === 1) {
874
+ return handleLocalKnowledgeGroundedAsk(chat, prepared.input, deps, prepared.signal);
875
+ }
876
+ return dispatchHybridAsk(preparedWithCanonicalFolders, deps, skippedFolders, hybrid);
877
+ }