ghagga-core 2.8.1 → 2.9.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 (316) hide show
  1. package/dist/acp/adapter.d.ts +91 -0
  2. package/dist/acp/adapter.d.ts.map +1 -0
  3. package/dist/acp/adapter.js +315 -0
  4. package/dist/acp/adapter.js.map +1 -0
  5. package/dist/acp/index.d.ts +4 -0
  6. package/dist/acp/index.d.ts.map +1 -0
  7. package/dist/acp/index.js +2 -0
  8. package/dist/acp/index.js.map +1 -0
  9. package/dist/acp/types.d.ts +142 -0
  10. package/dist/acp/types.d.ts.map +1 -0
  11. package/dist/acp/types.js +13 -0
  12. package/dist/acp/types.js.map +1 -0
  13. package/dist/adversarial-qa.d.ts +60 -0
  14. package/dist/adversarial-qa.d.ts.map +1 -0
  15. package/dist/adversarial-qa.js +85 -0
  16. package/dist/adversarial-qa.js.map +1 -0
  17. package/dist/agents/audit.d.ts +18 -0
  18. package/dist/agents/audit.d.ts.map +1 -0
  19. package/dist/agents/audit.js +78 -0
  20. package/dist/agents/audit.js.map +1 -0
  21. package/dist/agents/consensus.d.ts +1 -1
  22. package/dist/agents/consensus.d.ts.map +1 -1
  23. package/dist/agents/consensus.js +10 -8
  24. package/dist/agents/consensus.js.map +1 -1
  25. package/dist/agents/diagnostic.d.ts.map +1 -1
  26. package/dist/agents/diagnostic.js +22 -20
  27. package/dist/agents/diagnostic.js.map +1 -1
  28. package/dist/agents/fan-out-lenses.d.ts +41 -0
  29. package/dist/agents/fan-out-lenses.d.ts.map +1 -1
  30. package/dist/agents/fan-out-lenses.js +117 -3
  31. package/dist/agents/fan-out-lenses.js.map +1 -1
  32. package/dist/agents/prompts.d.ts +12 -0
  33. package/dist/agents/prompts.d.ts.map +1 -1
  34. package/dist/agents/prompts.js +31 -0
  35. package/dist/agents/prompts.js.map +1 -1
  36. package/dist/agents/simple.d.ts +1 -1
  37. package/dist/agents/simple.d.ts.map +1 -1
  38. package/dist/agents/simple.js +10 -6
  39. package/dist/agents/simple.js.map +1 -1
  40. package/dist/agents/workflow.d.ts +1 -1
  41. package/dist/agents/workflow.d.ts.map +1 -1
  42. package/dist/agents/workflow.js +9 -8
  43. package/dist/agents/workflow.js.map +1 -1
  44. package/dist/aisvs.d.ts +44 -0
  45. package/dist/aisvs.d.ts.map +1 -0
  46. package/dist/aisvs.js +189 -0
  47. package/dist/aisvs.js.map +1 -0
  48. package/dist/checklist/context.d.ts.map +1 -1
  49. package/dist/checklist/context.js +2 -8
  50. package/dist/checklist/context.js.map +1 -1
  51. package/dist/checklist/defaults.d.ts.map +1 -1
  52. package/dist/checklist/defaults.js.map +1 -1
  53. package/dist/checklist/scorer.d.ts.map +1 -1
  54. package/dist/checklist/scorer.js +105 -12
  55. package/dist/checklist/scorer.js.map +1 -1
  56. package/dist/code-intel/client.d.ts +30 -0
  57. package/dist/code-intel/client.d.ts.map +1 -0
  58. package/dist/code-intel/client.js +91 -0
  59. package/dist/code-intel/client.js.map +1 -0
  60. package/dist/code-intel/context.d.ts +21 -0
  61. package/dist/code-intel/context.d.ts.map +1 -0
  62. package/dist/code-intel/context.js +72 -0
  63. package/dist/code-intel/context.js.map +1 -0
  64. package/dist/code-intel/index.d.ts +10 -0
  65. package/dist/code-intel/index.d.ts.map +1 -0
  66. package/dist/code-intel/index.js +11 -0
  67. package/dist/code-intel/index.js.map +1 -0
  68. package/dist/code-intel/types.d.ts +63 -0
  69. package/dist/code-intel/types.d.ts.map +1 -0
  70. package/dist/code-intel/types.js +9 -0
  71. package/dist/code-intel/types.js.map +1 -0
  72. package/dist/compress/index.d.ts +55 -0
  73. package/dist/compress/index.d.ts.map +1 -0
  74. package/dist/compress/index.js +166 -0
  75. package/dist/compress/index.js.map +1 -0
  76. package/dist/cost-footer.d.ts +38 -0
  77. package/dist/cost-footer.d.ts.map +1 -0
  78. package/dist/cost-footer.js +95 -0
  79. package/dist/cost-footer.js.map +1 -0
  80. package/dist/critique/critique.d.ts +40 -0
  81. package/dist/critique/critique.d.ts.map +1 -0
  82. package/dist/critique/critique.js +194 -0
  83. package/dist/critique/critique.js.map +1 -0
  84. package/dist/critique/cross-model.d.ts +123 -0
  85. package/dist/critique/cross-model.d.ts.map +1 -0
  86. package/dist/critique/cross-model.js +267 -0
  87. package/dist/critique/cross-model.js.map +1 -0
  88. package/dist/critique/index.d.ts +8 -0
  89. package/dist/critique/index.d.ts.map +1 -0
  90. package/dist/critique/index.js +6 -0
  91. package/dist/critique/index.js.map +1 -0
  92. package/dist/critique/prompts.d.ts +11 -0
  93. package/dist/critique/prompts.d.ts.map +1 -0
  94. package/dist/critique/prompts.js +66 -0
  95. package/dist/critique/prompts.js.map +1 -0
  96. package/dist/critique/types.d.ts +84 -0
  97. package/dist/critique/types.d.ts.map +1 -0
  98. package/dist/critique/types.js +13 -0
  99. package/dist/critique/types.js.map +1 -0
  100. package/dist/doc-validation/index.d.ts +9 -0
  101. package/dist/doc-validation/index.d.ts.map +1 -0
  102. package/dist/doc-validation/index.js +9 -0
  103. package/dist/doc-validation/index.js.map +1 -0
  104. package/dist/doc-validation/scanner.d.ts +40 -0
  105. package/dist/doc-validation/scanner.d.ts.map +1 -0
  106. package/dist/doc-validation/scanner.js +163 -0
  107. package/dist/doc-validation/scanner.js.map +1 -0
  108. package/dist/doc-validation/types.d.ts +27 -0
  109. package/dist/doc-validation/types.d.ts.map +1 -0
  110. package/dist/doc-validation/types.js +8 -0
  111. package/dist/doc-validation/types.js.map +1 -0
  112. package/dist/embed.d.ts +27 -0
  113. package/dist/embed.d.ts.map +1 -0
  114. package/dist/embed.js +47 -0
  115. package/dist/embed.js.map +1 -0
  116. package/dist/enhance/enhance.d.ts.map +1 -1
  117. package/dist/enhance/enhance.js +7 -25
  118. package/dist/enhance/enhance.js.map +1 -1
  119. package/dist/enhance/types.d.ts +5 -0
  120. package/dist/enhance/types.d.ts.map +1 -1
  121. package/dist/exploitability/analyzer.d.ts +42 -0
  122. package/dist/exploitability/analyzer.d.ts.map +1 -1
  123. package/dist/exploitability/analyzer.js +225 -0
  124. package/dist/exploitability/analyzer.js.map +1 -1
  125. package/dist/exploitability/index.d.ts +3 -2
  126. package/dist/exploitability/index.d.ts.map +1 -1
  127. package/dist/exploitability/index.js +1 -2
  128. package/dist/exploitability/index.js.map +1 -1
  129. package/dist/exploitability/types.d.ts +27 -0
  130. package/dist/exploitability/types.d.ts.map +1 -1
  131. package/dist/fetch-fix.d.ts +60 -0
  132. package/dist/fetch-fix.d.ts.map +1 -0
  133. package/dist/fetch-fix.js +137 -0
  134. package/dist/fetch-fix.js.map +1 -0
  135. package/dist/flood/index.d.ts +34 -0
  136. package/dist/flood/index.d.ts.map +1 -0
  137. package/dist/flood/index.js +67 -0
  138. package/dist/flood/index.js.map +1 -0
  139. package/dist/format.d.ts.map +1 -1
  140. package/dist/format.js +6 -1
  141. package/dist/format.js.map +1 -1
  142. package/dist/graph/blast-radius.js +2 -2
  143. package/dist/graph/blast-radius.js.map +1 -1
  144. package/dist/graph/call-chain.d.ts +36 -0
  145. package/dist/graph/call-chain.d.ts.map +1 -0
  146. package/dist/graph/call-chain.js +291 -0
  147. package/dist/graph/call-chain.js.map +1 -0
  148. package/dist/graph/index.d.ts +4 -0
  149. package/dist/graph/index.d.ts.map +1 -1
  150. package/dist/graph/index.js +2 -0
  151. package/dist/graph/index.js.map +1 -1
  152. package/dist/graph/reverse-deps.d.ts +37 -0
  153. package/dist/graph/reverse-deps.d.ts.map +1 -0
  154. package/dist/graph/reverse-deps.js +136 -0
  155. package/dist/graph/reverse-deps.js.map +1 -0
  156. package/dist/index.d.ts +50 -11
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +34 -7
  159. package/dist/index.js.map +1 -1
  160. package/dist/injection-corpus.d.ts +41 -0
  161. package/dist/injection-corpus.d.ts.map +1 -0
  162. package/dist/injection-corpus.js +181 -0
  163. package/dist/injection-corpus.js.map +1 -0
  164. package/dist/latent-comms.d.ts +31 -0
  165. package/dist/latent-comms.d.ts.map +1 -0
  166. package/dist/latent-comms.js +139 -0
  167. package/dist/latent-comms.js.map +1 -0
  168. package/dist/memory/pageindex/chunker.d.ts +33 -0
  169. package/dist/memory/pageindex/chunker.d.ts.map +1 -0
  170. package/dist/memory/pageindex/chunker.js +112 -0
  171. package/dist/memory/pageindex/chunker.js.map +1 -0
  172. package/dist/memory/pageindex/example.d.ts +22 -0
  173. package/dist/memory/pageindex/example.d.ts.map +1 -0
  174. package/dist/memory/pageindex/example.js +94 -0
  175. package/dist/memory/pageindex/example.js.map +1 -0
  176. package/dist/memory/pageindex/index.d.ts +15 -0
  177. package/dist/memory/pageindex/index.d.ts.map +1 -0
  178. package/dist/memory/pageindex/index.js +17 -0
  179. package/dist/memory/pageindex/index.js.map +1 -0
  180. package/dist/memory/pageindex/service.d.ts +53 -0
  181. package/dist/memory/pageindex/service.d.ts.map +1 -0
  182. package/dist/memory/pageindex/service.js +229 -0
  183. package/dist/memory/pageindex/service.js.map +1 -0
  184. package/dist/memory/pageindex/types.d.ts +67 -0
  185. package/dist/memory/pageindex/types.d.ts.map +1 -0
  186. package/dist/memory/pageindex/types.js +14 -0
  187. package/dist/memory/pageindex/types.js.map +1 -0
  188. package/dist/memory/persist.d.ts.map +1 -1
  189. package/dist/memory/persist.js +6 -2
  190. package/dist/memory/persist.js.map +1 -1
  191. package/dist/memory/sqlite.d.ts +69 -2
  192. package/dist/memory/sqlite.d.ts.map +1 -1
  193. package/dist/memory/sqlite.js +312 -5
  194. package/dist/memory/sqlite.js.map +1 -1
  195. package/dist/memory/taxonomy.d.ts +34 -0
  196. package/dist/memory/taxonomy.d.ts.map +1 -0
  197. package/dist/memory/taxonomy.js +189 -0
  198. package/dist/memory/taxonomy.js.map +1 -0
  199. package/dist/memory/versioning.d.ts.map +1 -1
  200. package/dist/memory/versioning.js.map +1 -1
  201. package/dist/negative.d.ts +23 -0
  202. package/dist/negative.d.ts.map +1 -0
  203. package/dist/negative.js +40 -0
  204. package/dist/negative.js.map +1 -0
  205. package/dist/pipeline.d.ts.map +1 -1
  206. package/dist/pipeline.js +455 -46
  207. package/dist/pipeline.js.map +1 -1
  208. package/dist/prompt-intel.d.ts +39 -0
  209. package/dist/prompt-intel.d.ts.map +1 -0
  210. package/dist/prompt-intel.js +148 -0
  211. package/dist/prompt-intel.js.map +1 -0
  212. package/dist/providers/cli-bridge.d.ts +4 -0
  213. package/dist/providers/cli-bridge.d.ts.map +1 -1
  214. package/dist/providers/cli-bridge.js +4 -0
  215. package/dist/providers/cli-bridge.js.map +1 -1
  216. package/dist/providers/generate-fn.d.ts +3 -15
  217. package/dist/providers/generate-fn.d.ts.map +1 -1
  218. package/dist/providers/generate-fn.js +3 -30
  219. package/dist/providers/generate-fn.js.map +1 -1
  220. package/dist/providers/index.d.ts.map +1 -1
  221. package/dist/providers/index.js.map +1 -1
  222. package/dist/providers/ollama.d.ts +15 -0
  223. package/dist/providers/ollama.d.ts.map +1 -0
  224. package/dist/providers/ollama.js +30 -0
  225. package/dist/providers/ollama.js.map +1 -0
  226. package/dist/ranking/index.d.ts +9 -0
  227. package/dist/ranking/index.d.ts.map +1 -0
  228. package/dist/ranking/index.js +82 -0
  229. package/dist/ranking/index.js.map +1 -0
  230. package/dist/recursive/circuit-breaker.d.ts +36 -0
  231. package/dist/recursive/circuit-breaker.d.ts.map +1 -0
  232. package/dist/recursive/circuit-breaker.js +62 -0
  233. package/dist/recursive/circuit-breaker.js.map +1 -0
  234. package/dist/recursive/index.d.ts +4 -0
  235. package/dist/recursive/index.d.ts.map +1 -1
  236. package/dist/recursive/index.js +18 -1
  237. package/dist/recursive/index.js.map +1 -1
  238. package/dist/recursive/types.d.ts +2 -0
  239. package/dist/recursive/types.d.ts.map +1 -1
  240. package/dist/recursive/types.js +1 -0
  241. package/dist/recursive/types.js.map +1 -1
  242. package/dist/scope/diff-mapper.js.map +1 -1
  243. package/dist/scope/entity-diff.d.ts +58 -0
  244. package/dist/scope/entity-diff.d.ts.map +1 -0
  245. package/dist/scope/entity-diff.js +224 -0
  246. package/dist/scope/entity-diff.js.map +1 -0
  247. package/dist/scope/extractor.d.ts.map +1 -1
  248. package/dist/scope/extractor.js.map +1 -1
  249. package/dist/scope/index.d.ts +3 -1
  250. package/dist/scope/index.d.ts.map +1 -1
  251. package/dist/scope/index.js +3 -0
  252. package/dist/scope/index.js.map +1 -1
  253. package/dist/scope/parser.d.ts +1 -1
  254. package/dist/scope/parser.d.ts.map +1 -1
  255. package/dist/scope/parser.js.map +1 -1
  256. package/dist/scope/types.d.ts +32 -0
  257. package/dist/scope/types.d.ts.map +1 -1
  258. package/dist/scope/types.js +7 -1
  259. package/dist/scope/types.js.map +1 -1
  260. package/dist/search/index.d.ts +11 -0
  261. package/dist/search/index.d.ts.map +1 -0
  262. package/dist/search/index.js +10 -0
  263. package/dist/search/index.js.map +1 -0
  264. package/dist/search/indexer.d.ts +67 -0
  265. package/dist/search/indexer.d.ts.map +1 -0
  266. package/dist/search/indexer.js +196 -0
  267. package/dist/search/indexer.js.map +1 -0
  268. package/dist/search/searcher.d.ts +34 -0
  269. package/dist/search/searcher.d.ts.map +1 -0
  270. package/dist/search/searcher.js +101 -0
  271. package/dist/search/searcher.js.map +1 -0
  272. package/dist/search/types.d.ts +81 -0
  273. package/dist/search/types.d.ts.map +1 -0
  274. package/dist/search/types.js +8 -0
  275. package/dist/search/types.js.map +1 -0
  276. package/dist/self-improve/index.d.ts +53 -0
  277. package/dist/self-improve/index.d.ts.map +1 -0
  278. package/dist/self-improve/index.js +136 -0
  279. package/dist/self-improve/index.js.map +1 -0
  280. package/dist/semantic-diff/index.d.ts +31 -0
  281. package/dist/semantic-diff/index.d.ts.map +1 -0
  282. package/dist/semantic-diff/index.js +215 -0
  283. package/dist/semantic-diff/index.js.map +1 -0
  284. package/dist/testing/index.d.ts +67 -0
  285. package/dist/testing/index.d.ts.map +1 -0
  286. package/dist/testing/index.js +76 -0
  287. package/dist/testing/index.js.map +1 -0
  288. package/dist/testing/vitest-helpers.d.ts +26 -0
  289. package/dist/testing/vitest-helpers.d.ts.map +1 -0
  290. package/dist/testing/vitest-helpers.js +37 -0
  291. package/dist/testing/vitest-helpers.js.map +1 -0
  292. package/dist/tools/index.d.ts +3 -1
  293. package/dist/tools/index.d.ts.map +1 -1
  294. package/dist/tools/index.js +2 -1
  295. package/dist/tools/index.js.map +1 -1
  296. package/dist/tools/remote-query.d.ts +40 -0
  297. package/dist/tools/remote-query.d.ts.map +1 -0
  298. package/dist/tools/remote-query.js +71 -0
  299. package/dist/tools/remote-query.js.map +1 -0
  300. package/dist/tracing/index.d.ts +39 -0
  301. package/dist/tracing/index.d.ts.map +1 -0
  302. package/dist/tracing/index.js +70 -0
  303. package/dist/tracing/index.js.map +1 -0
  304. package/dist/trajectory.d.ts +65 -0
  305. package/dist/trajectory.d.ts.map +1 -0
  306. package/dist/trajectory.js +126 -0
  307. package/dist/trajectory.js.map +1 -0
  308. package/dist/trust/index.d.ts +34 -0
  309. package/dist/trust/index.d.ts.map +1 -0
  310. package/dist/trust/index.js +78 -0
  311. package/dist/trust/index.js.map +1 -0
  312. package/dist/types.d.ts +144 -5
  313. package/dist/types.d.ts.map +1 -1
  314. package/dist/types.js +2 -11
  315. package/dist/types.js.map +1 -1
  316. package/package.json +1 -3
package/dist/pipeline.js CHANGED
@@ -16,24 +16,34 @@
16
16
  */
17
17
  import { runConsensusReview } from './agents/consensus.js';
18
18
  import { runDiagnosticReview } from './agents/diagnostic.js';
19
- import { runFanOutReview } from './agents/fan-out-lenses.js';
20
- import { buildStackHints } from './agents/prompts.js';
19
+ import { loadLensesFromDir, runFanOutReview } from './agents/fan-out-lenses.js';
20
+ import { buildCodeIntelSection, buildStackHints } from './agents/prompts.js';
21
21
  import { runSimpleReview } from './agents/simple.js';
22
22
  import { runWorkflowReview } from './agents/workflow.js';
23
23
  import { buildChecklistContext, resolveChecklistConfig, scoreFindings } from './checklist/index.js';
24
+ import { buildCodeIntelContext } from './code-intel/context.js';
25
+ import { extractChangedSymbols as extractChangedSymbolsFromDiff, scanDocsForSymbols as scanDocsForSymbolRefs, } from './doc-validation/index.js';
24
26
  import { enhanceFindings, mergeEnhanceResult } from './enhance/index.js';
25
27
  import { serializeFindings } from './enhance/prompt.js';
26
- import { analyzeExploitability } from './exploitability/index.js';
28
+ import { analyzeExploitability, analyzeUsage } from './exploitability/index.js';
29
+ import { detectFlood } from './flood/index.js';
27
30
  import { computeBlastRadius } from './graph/blast-radius.js';
31
+ import { buildCallChainFromDiff } from './graph/call-chain.js';
32
+ import { buildReverseDependencyMap, findDependents } from './graph/reverse-deps.js';
28
33
  import { isGraphStale } from './graph/schema.js';
29
34
  import { persistReviewObservations } from './memory/persist.js';
30
35
  import { searchMemoryForContext } from './memory/search.js';
36
+ import { SqliteMemoryStorage } from './memory/sqlite.js';
37
+ import { formatNegativeExamplesPrompt } from './negative.js';
31
38
  import { resolveCredentialEnvVar } from './providers/cli-bridge.js';
32
- import { createAISDKGenerateFn, createCLIBridgeGenerateFn, createGatewayGenerateFn, } from './providers/generate-fn.js';
39
+ import { createCLIBridgeGenerateFn, createGatewayGenerateFn, createOllamaGenerateFn, } from './providers/generate-fn.js';
40
+ import { rankFindings } from './ranking/index.js';
33
41
  import { recursiveReview } from './recursive/index.js';
42
+ import { deriveRules, formatRulesForPrompt, loadFeedback } from './self-improve/index.js';
34
43
  import { initializeDefaultTools } from './tools/plugins/index.js';
35
44
  import { toolRegistry } from './tools/registry.js';
36
45
  import { formatStaticAnalysisContext, isToolRegistryEnabled, runStaticAnalysis, } from './tools/runner.js';
46
+ import { computeAuthorTrustScore, getReviewModeForTier } from './trust/index.js';
37
47
  import { buildProgressiveContext } from './utils/context-levels.js';
38
48
  import { filterDiffFiles, parseDiffFiles, truncateDiff } from './utils/diff.js';
39
49
  import { detectStacks } from './utils/stack-detect.js';
@@ -63,8 +73,20 @@ function validateInput(input) {
63
73
  if (input.provider === 'gateway') {
64
74
  return;
65
75
  }
66
- // Single provider mode (CLI/Action backward compat)
67
- if (!input.apiKey) {
76
+ // Ollama mode: no API key required (local instance)
77
+ if (input.provider === 'ollama') {
78
+ return;
79
+ }
80
+ // Single provider mode — must be one of the 3 supported providers
81
+ if (input.provider) {
82
+ const supported = ['gateway', 'cli-bridge', 'ollama'];
83
+ if (!supported.includes(input.provider)) {
84
+ throw new Error(`Provider '${input.provider}' is no longer supported directly. ` +
85
+ `Set provider: 'gateway' and configure credentials in mcp-llm-bridge. ` +
86
+ `See docs/configuration.md#gateway-mode-mcp-llm-bridge`);
87
+ }
88
+ }
89
+ if (!input.apiKey && input.provider !== 'cli-bridge' && input.provider !== 'ollama') {
68
90
  throw new Error('Review input must include an API key');
69
91
  }
70
92
  if (!input.provider) {
@@ -89,6 +111,8 @@ function validateInput(input) {
89
111
  export async function reviewPipeline(input) {
90
112
  const startTime = Date.now();
91
113
  const emit = input.onProgress ?? (() => { });
114
+ // Track steps that failed but were gracefully degraded
115
+ const failedSteps = [];
92
116
  // Resolve whether AI review is enabled
93
117
  const aiEnabled = resolveAiEnabled(input);
94
118
  // ── Step 1: Validate ───────────────────────────────────────
@@ -116,6 +140,30 @@ export async function reviewPipeline(input) {
116
140
  message: `Parsed ${allFiles.length} files from diff, ${filteredFiles.length} after filtering (${blocked.length} blocked, ${redacted.length} redacted)`,
117
141
  detail: filteredFiles.map((f) => ` ${f.path}`).join('\n'),
118
142
  });
143
+ // ── Step 2.1: Flood / spam detection ──────────────────────
144
+ // Runs before any expensive operation (static analysis, LLM).
145
+ const linesChanged = allFiles.reduce((sum, f) => sum + f.additions + f.deletions, 0);
146
+ const floodResult = detectFlood({
147
+ authorLogin: input.author ?? '',
148
+ prTitle: input.context?.commitMessages[0] ?? '',
149
+ prBody: input.context?.commitMessages.slice(1).join('\n') ?? null,
150
+ linesChanged,
151
+ recentPrCount: undefined,
152
+ });
153
+ if (floodResult.isFlood) {
154
+ emit({
155
+ step: 'flood-detection',
156
+ message: `Flood detected: ${floodResult.recommendation}`,
157
+ detail: floodResult.signals.map((s) => ` [${s.type}] ${s.detail}`).join('\n'),
158
+ });
159
+ }
160
+ if (floodResult.recommendation === 'skip') {
161
+ const skipped = createSkippedResult(input, startTime);
162
+ return {
163
+ ...skipped,
164
+ summary: 'Flood detection: PR skipped (bot author or spam signal).',
165
+ };
166
+ }
119
167
  // If all files were filtered out, skip the review
120
168
  if (filteredFiles.length === 0) {
121
169
  return createSkippedResult(input, startTime);
@@ -190,6 +238,10 @@ export async function reviewPipeline(input) {
190
238
  }
191
239
  catch (error) {
192
240
  console.warn('[ghagga] Blast-radius failed (degrading gracefully):', error);
241
+ failedSteps.push({
242
+ step: 'blast-radius',
243
+ error: error instanceof Error ? error.message : String(error),
244
+ });
193
245
  emit({ step: 'blast-radius', message: 'Blast radius: skipped (error loading graph)' });
194
246
  blastRadiusMetadata = {
195
247
  enabled: true,
@@ -200,6 +252,54 @@ export async function reviewPipeline(input) {
200
252
  };
201
253
  }
202
254
  }
255
+ // ── Step 2.6: Call-chain + reverse-deps (optional, runs when blast-radius enabled) ──
256
+ let callChainContext = '';
257
+ if (input.settings.enableBlastRadius) {
258
+ try {
259
+ if (input.fileReader) {
260
+ const fileContentsMap = new Map();
261
+ for (const fp of fileList) {
262
+ try {
263
+ const content = await input.fileReader(fp);
264
+ if (content)
265
+ fileContentsMap.set(fp, content);
266
+ }
267
+ catch {
268
+ // non-fatal — skip unreadable files
269
+ }
270
+ }
271
+ if (fileContentsMap.size > 0) {
272
+ const callChain = buildCallChainFromDiff(filteredDiff, fileContentsMap);
273
+ if (callChain.affectedSymbols.length > 0) {
274
+ const affectedFiles = [...new Set(callChain.affectedSymbols.map((s) => s.filePath))];
275
+ callChainContext = `\n## Call-Chain Impact\n${callChain.affectedSymbols.length} symbol(s) across ${affectedFiles.length} file(s) may be affected by these changes (depth: ${callChain.depth}).\n`;
276
+ emit({
277
+ step: 'call-chain',
278
+ message: `Call-chain: ${callChain.changedSymbols.length} changed symbol(s), ${callChain.affectedSymbols.length} affected symbol(s)`,
279
+ });
280
+ }
281
+ const reverseDepMap = buildReverseDependencyMap([...fileContentsMap.keys()], fileContentsMap);
282
+ const highRiskFiles = [];
283
+ for (const fp of fileList) {
284
+ const result = findDependents(fp, reverseDepMap, 2);
285
+ if (result.transitiveCount >= 3) {
286
+ highRiskFiles.push(`${fp} (${result.transitiveCount} dependents)`);
287
+ }
288
+ }
289
+ if (highRiskFiles.length > 0) {
290
+ callChainContext += `\n## High-Risk Files (many dependents)\nThese changed files have many transitive dependents — review carefully:\n${highRiskFiles.map((f) => `- ${f}`).join('\n')}\n`;
291
+ emit({
292
+ step: 'reverse-deps',
293
+ message: `Reverse deps: ${highRiskFiles.length} high-risk file(s) detected`,
294
+ });
295
+ }
296
+ }
297
+ }
298
+ }
299
+ catch (error) {
300
+ console.warn('[ghagga] Call-chain/reverse-deps failed (degrading gracefully):', error instanceof Error ? error.message : String(error));
301
+ }
302
+ }
203
303
  // ── Step 3: Detect tech stacks ─────────────────────────────
204
304
  const stacks = detectStacks(fileList);
205
305
  const stackHints = buildStackHints(stacks);
@@ -225,33 +325,104 @@ export async function reviewPipeline(input) {
225
325
  ? 'Using precomputed static analysis from runner...'
226
326
  : 'Running static analysis & memory search...',
227
327
  });
228
- const [staticResult, rawMemoryContext] = await Promise.all([
328
+ const [staticResult, rawMemoryContext, codeIntelResults] = await Promise.all([
229
329
  input.precomputedStaticAnalysis
230
330
  ? Promise.resolve(input.precomputedStaticAnalysis)
231
- : runStaticAnalysisSafe(fileList, input),
232
- aiEnabled ? searchMemorySafe(input, fileList) : Promise.resolve(null),
331
+ : runStaticAnalysisSafe(fileList, input, failedSteps),
332
+ aiEnabled ? searchMemorySafe(input, fileList, failedSteps) : Promise.resolve(null),
333
+ queryCodeIntelSafe(input, fileList, emit, failedSteps),
233
334
  ]);
335
+ // ── Step 5.0: Negative examples (optional) ────────────────────
336
+ // Load dismissed findings for the files in this diff and prepend them
337
+ // to the memory context so agents suppress known false positives.
338
+ let negativeExamplesPrompt = '';
339
+ if (input.features?.negativeExamples !== false &&
340
+ input.memoryStorage instanceof SqliteMemoryStorage) {
341
+ try {
342
+ const allNegativeExamples = fileList.flatMap((filePath) => input.memoryStorage.getNegativeExamplesForFile(filePath));
343
+ // De-duplicate by findingHash
344
+ const seen = new Set();
345
+ const uniqueExamples = allNegativeExamples.filter((e) => {
346
+ if (seen.has(e.findingHash))
347
+ return false;
348
+ seen.add(e.findingHash);
349
+ return true;
350
+ });
351
+ negativeExamplesPrompt = formatNegativeExamplesPrompt(uniqueExamples);
352
+ if (negativeExamplesPrompt) {
353
+ emit({
354
+ step: 'negative-examples',
355
+ message: `Loaded ${uniqueExamples.length} dismissed finding(s) — injecting suppression context`,
356
+ });
357
+ }
358
+ }
359
+ catch (error) {
360
+ // Non-fatal — degraded gracefully
361
+ console.warn('[ghagga] Negative examples load failed (degrading gracefully):', error instanceof Error ? error.message : String(error));
362
+ }
363
+ }
364
+ // ── Step 5.0a: Self-improve rules (optional) ─────────────────
365
+ let selfImproveRulesPrompt = '';
366
+ if (input.settings.selfImprovePath) {
367
+ try {
368
+ const feedback = await loadFeedback(input.settings.selfImprovePath);
369
+ if (feedback.length > 0) {
370
+ const rules = deriveRules(feedback);
371
+ selfImproveRulesPrompt = formatRulesForPrompt(rules);
372
+ if (selfImproveRulesPrompt) {
373
+ emit({
374
+ step: 'self-improve',
375
+ message: `Self-improve: loaded ${feedback.length} feedback record(s), derived ${rules.length} rule(s)`,
376
+ });
377
+ }
378
+ }
379
+ }
380
+ catch (error) {
381
+ console.warn('[ghagga] Self-improve rules load failed (degrading gracefully):', error instanceof Error ? error.message : String(error));
382
+ }
383
+ }
234
384
  // Build full (L2) context first, then choose fidelity level based on budget
235
385
  const fullStaticContext = formatStaticAnalysisContext(staticResult);
386
+ // Prepend self-improve rules + negative examples to memory context
387
+ const rawMemoryContextWithNegatives = [selfImproveRulesPrompt, negativeExamplesPrompt, rawMemoryContext].filter(Boolean).join('\n') ||
388
+ null;
236
389
  const progressiveContext = buildProgressiveContext({
237
390
  staticResult,
238
- memoryContext: rawMemoryContext,
391
+ memoryContext: rawMemoryContextWithNegatives,
239
392
  stackHints,
240
393
  contextBudget,
241
394
  fullStaticContext,
242
395
  });
243
396
  const staticContext = progressiveContext.staticContext;
244
397
  const memoryContext = progressiveContext.memoryContext;
398
+ // ── Step 5.1b: Build code intelligence context (optional) ───
399
+ const codeIntelContext = codeIntelResults.length > 0
400
+ ? buildCodeIntelSection(buildCodeIntelContext(codeIntelResults, input.settings.codeIntelMaxTokens))
401
+ : '';
402
+ let codeIntelMetadata;
403
+ if (input.settings.enableCodeIntel) {
404
+ codeIntelMetadata = {
405
+ enabled: true,
406
+ providerAvailable: !!input.codeIntelProvider,
407
+ filesQueried: fileList.length,
408
+ filesWithData: codeIntelResults.filter((r) => r.callers.length > 0 || r.callees.length > 0 || r.imports.length > 0).length,
409
+ queryDurationMs: 0, // Timing captured in queryCodeIntelSafe
410
+ };
411
+ }
245
412
  {
246
413
  const toolsSummary = Object.entries(staticResult)
247
414
  .map(([name, result]) => ` ${name}: ${result.status} (${result.findings.length} findings)`)
248
415
  .join('\n');
249
416
  const levelDetail = ` context levels: static=${progressiveContext.staticLevel}, memory=${progressiveContext.memoryLevel}`;
417
+ const codeIntelDetail = codeIntelContext
418
+ ? `\n code-intel: ${codeIntelResults.length} file(s) with structural data`
419
+ : '\n code-intel: disabled or unavailable';
250
420
  emit({
251
421
  step: 'static-results',
252
422
  message: `Static analysis complete (context: static=${progressiveContext.staticLevel}, memory=${progressiveContext.memoryLevel})`,
253
423
  detail: toolsSummary +
254
424
  (rawMemoryContext ? '\n memory: loaded' : '\n memory: disabled') +
425
+ codeIntelDetail +
255
426
  '\n' +
256
427
  levelDetail,
257
428
  });
@@ -269,6 +440,11 @@ export async function reviewPipeline(input) {
269
440
  }
270
441
  }
271
442
  // ── Step 5.5: AI Enhance (optional) ─────────────────────────
443
+ // Resolve active provider early — needed by enhance block below and by Step 6
444
+ const activeProvider = input.providerChain?.[0]?.provider ?? input.provider ?? 'gateway';
445
+ const isCliBridge = activeProvider === 'cli-bridge';
446
+ const isGateway = activeProvider === 'gateway';
447
+ const isOllama = activeProvider === 'ollama';
272
448
  let enhancedStaticFindings;
273
449
  let enhanceMetadata;
274
450
  if (input.enhance) {
@@ -281,17 +457,23 @@ export async function reviewPipeline(input) {
281
457
  emit({ step: 'static-analysis', message: 'Enhancing findings with AI...' });
282
458
  try {
283
459
  const primary = resolvePrimaryProvider(input);
460
+ const enhanceGenerateFn = resolveGenerateTextFns(input, isCliBridge, isGateway, isOllama)[0];
284
461
  const serialized = serializeFindings(allStaticFindings);
285
462
  const { result: eResult, metadata: eMeta } = await enhanceFindings({
286
463
  findings: serialized,
287
464
  provider: primary.provider,
288
465
  model: primary.model,
289
466
  apiKey: primary.apiKey,
467
+ generateFn: enhanceGenerateFn,
290
468
  });
291
469
  enhancedStaticFindings = mergeEnhanceResult(allStaticFindings, eResult);
292
470
  enhanceMetadata = eMeta;
293
471
  }
294
- catch {
472
+ catch (enhanceError) {
473
+ failedSteps.push({
474
+ step: 'ai-enhance',
475
+ error: enhanceError instanceof Error ? enhanceError.message : String(enhanceError),
476
+ });
295
477
  emit({
296
478
  step: 'static-analysis',
297
479
  message: 'AI enhance failed — continuing without enhancement',
@@ -299,29 +481,62 @@ export async function reviewPipeline(input) {
299
481
  }
300
482
  }
301
483
  }
484
+ // ── Step 5.6: Author trust scoring (optional) ──────────────
485
+ // When features.authorTrust is enabled and input.author is set, compute a
486
+ // trust score from git history and potentially override the review mode.
487
+ let trustOverrideMode;
488
+ if (input.features?.authorTrust && input.author) {
489
+ try {
490
+ const author = input.author;
491
+ const sqliteStorage = input.memoryStorage instanceof SqliteMemoryStorage ? input.memoryStorage : null;
492
+ // Check for a cached (fresh) score — recompute if older than 1 day
493
+ const ONE_DAY_MS = 24 * 60 * 60 * 1000;
494
+ let trustScore = sqliteStorage?.getTrustScore(author) ?? null;
495
+ const isStale = !trustScore || Date.now() - trustScore.lastUpdated.getTime() > ONE_DAY_MS;
496
+ if (isStale) {
497
+ trustScore = await computeAuthorTrustScore(author, { cwd: process.cwd() });
498
+ sqliteStorage?.upsertTrustScore(trustScore);
499
+ }
500
+ if (!trustScore) {
501
+ throw new Error('Trust score unavailable');
502
+ }
503
+ const recommendedMode = getReviewModeForTier(trustScore.tier, input.mode);
504
+ if (recommendedMode !== input.mode) {
505
+ trustOverrideMode = recommendedMode;
506
+ }
507
+ emit({
508
+ step: 'author-trust',
509
+ message: `[trust] author=${author} score=${trustScore.score} tier=${trustScore.tier} → mode=${recommendedMode}`,
510
+ });
511
+ }
512
+ catch (error) {
513
+ console.warn('[ghagga] Author trust scoring failed (non-fatal):', error instanceof Error ? error.message : String(error));
514
+ failedSteps.push({
515
+ step: 'author-trust',
516
+ error: error instanceof Error ? error.message : String(error),
517
+ });
518
+ }
519
+ }
520
+ // Effective input mode — may be overridden by trust scoring
521
+ const resolvedInputMode = trustOverrideMode ?? input.mode;
302
522
  // ── Step 6: Execute agent mode (or skip if AI disabled) ────
303
523
  let result;
304
- // Check if CLI bridge should be used (intercept before normal provider flow)
305
- const isCliBridge = input.provider === 'cli-bridge' ||
306
- input.providerChain?.[0]?.provider === 'cli-bridge';
307
- // Check if LLM Gateway should be used (intercept before normal provider flow)
308
- const isGateway = input.provider === 'gateway' ||
309
- input.providerChain?.[0]?.provider === 'gateway';
524
+ // activeProvider / isCliBridge / isGateway / isOllama are resolved above (Step 5.5)
310
525
  if (!aiEnabled) {
311
526
  // Static-only mode: no LLM calls
312
527
  emit({ step: 'agent-start', message: 'AI review disabled — returning static analysis only' });
313
- result = createStaticOnlyResult(staticResult, input.mode, startTime);
528
+ result = createStaticOnlyResult(staticResult, resolvedInputMode, startTime);
314
529
  }
315
530
  else {
316
531
  // ── Unified dispatch: all backends, all modes ──────────────
317
532
  // Step 1: Build GenerateTextFn(s) for the detected backend
318
- const generateFns = resolveGenerateTextFns(input, isCliBridge, isGateway);
533
+ const generateFns = resolveGenerateTextFns(input, isCliBridge, isGateway, isOllama);
319
534
  // Step 2: Resolve effective mode (diagnostic → simple for non-SDK backends)
320
- const effectiveMode = resolveEffectiveMode(input.mode, isCliBridge, isGateway);
321
- if (effectiveMode !== input.mode) {
535
+ const effectiveMode = resolveEffectiveMode(resolvedInputMode, isCliBridge, isGateway, isOllama);
536
+ if (effectiveMode !== resolvedInputMode) {
322
537
  emit({
323
538
  step: 'mode-fallback',
324
- message: `Diagnostic mode not supported with ${isCliBridge ? 'CLI bridge' : 'gateway'} — falling back to simple mode`,
539
+ message: `Diagnostic mode not supported with ${isCliBridge ? 'CLI bridge' : isOllama ? 'Ollama' : 'gateway'} — falling back to simple mode`,
325
540
  });
326
541
  }
327
542
  // Resolve primary provider for progress messages and metadata
@@ -330,6 +545,8 @@ export async function reviewPipeline(input) {
330
545
  step: 'agent-start',
331
546
  message: `Running ${effectiveMode} agent with ${primary.provider}/${primary.model}...`,
332
547
  });
548
+ // Combine stack hints, code intelligence context, and call-chain context for agent prompts
549
+ const combinedStackHints = stackHints + codeIntelContext + callChainContext;
333
550
  try {
334
551
  switch (effectiveMode) {
335
552
  case 'simple':
@@ -337,7 +554,7 @@ export async function reviewPipeline(input) {
337
554
  diff: truncatedDiff,
338
555
  staticContext,
339
556
  memoryContext,
340
- stackHints,
557
+ stackHints: combinedStackHints,
341
558
  checklistContext,
342
559
  reviewLevel: input.settings.reviewLevel,
343
560
  onProgress: input.onProgress,
@@ -353,11 +570,13 @@ export async function reviewPipeline(input) {
353
570
  diff: truncatedDiff,
354
571
  staticContext,
355
572
  memoryContext,
356
- stackHints,
573
+ stackHints: combinedStackHints,
357
574
  checklistContext,
358
575
  reviewLevel: input.settings.reviewLevel,
359
576
  onProgress: input.onProgress,
360
577
  generateFns,
578
+ concurrency: input.settings?.reviewConcurrency,
579
+ delayMs: input.settings?.reviewDelayMs,
361
580
  // Backward compat fields (not used when generateFns is provided)
362
581
  provider: primary.provider ?? 'cli-bridge',
363
582
  model: primary.model ?? 'auto',
@@ -371,11 +590,13 @@ export async function reviewPipeline(input) {
371
590
  models: buildConsensusModels(input.providerChain, primary),
372
591
  staticContext,
373
592
  memoryContext,
374
- stackHints,
593
+ stackHints: combinedStackHints,
375
594
  checklistContext,
376
595
  reviewLevel: input.settings.reviewLevel,
377
596
  onProgress: input.onProgress,
378
597
  generateFns,
598
+ concurrency: input.settings?.reviewConcurrency,
599
+ delayMs: input.settings?.reviewDelayMs,
379
600
  });
380
601
  break;
381
602
  case 'diagnostic':
@@ -387,13 +608,17 @@ export async function reviewPipeline(input) {
387
608
  apiKey: primary.apiKey,
388
609
  staticContext,
389
610
  memoryContext,
390
- stackHints,
611
+ stackHints: combinedStackHints,
391
612
  checklistContext,
392
613
  reviewLevel: input.settings.reviewLevel,
393
614
  onProgress: input.onProgress,
394
615
  });
395
616
  break;
396
617
  case 'fan-out':
618
+ // Load custom lenses from directory (if configured)
619
+ if (input.settings.lensDir) {
620
+ await loadLensesFromDir(input.settings.lensDir, input.onProgress);
621
+ }
397
622
  result = await runFanOutReview({
398
623
  diff: truncatedDiff,
399
624
  provider: primary.provider ?? 'cli-bridge',
@@ -401,11 +626,13 @@ export async function reviewPipeline(input) {
401
626
  apiKey: primary.apiKey ?? '',
402
627
  staticContext,
403
628
  memoryContext,
404
- stackHints,
629
+ stackHints: combinedStackHints,
405
630
  checklistContext,
406
631
  reviewLevel: input.settings.reviewLevel,
407
632
  onProgress: input.onProgress,
408
633
  generateFns,
634
+ // Forward lens selection from settings (CLI flags > config > defaults)
635
+ ...(input.settings.lenses ? { lenses: input.settings.lenses } : {}),
409
636
  });
410
637
  break;
411
638
  default: {
@@ -417,8 +644,12 @@ export async function reviewPipeline(input) {
417
644
  catch (error) {
418
645
  // Agent failed — return static results with NEEDS_HUMAN_REVIEW
419
646
  console.warn('[ghagga] AI review failed, returning static analysis only:', error instanceof Error ? error.message : String(error));
647
+ failedSteps.push({
648
+ step: 'ai-review',
649
+ error: error instanceof Error ? error.message : String(error),
650
+ });
420
651
  emit({ step: 'agent-failed', message: 'AI review failed — returning static analysis only' });
421
- result = createStaticOnlyResult(staticResult, input.mode, startTime);
652
+ result = createStaticOnlyResult(staticResult, resolvedInputMode, startTime);
422
653
  result.status = 'NEEDS_HUMAN_REVIEW';
423
654
  result.summary = `AI review failed (${error instanceof Error ? error.message : 'unknown error'}). Static analysis results are shown below.`;
424
655
  }
@@ -460,6 +691,10 @@ export async function reviewPipeline(input) {
460
691
  if (blastRadiusMetadata) {
461
692
  result.metadata.blastRadius = blastRadiusMetadata;
462
693
  }
694
+ // Add code intelligence metadata (if applicable)
695
+ if (codeIntelMetadata) {
696
+ result.codeIntelMetadata = codeIntelMetadata;
697
+ }
463
698
  // ── Step 7.4: Exploitability analysis (optional) ────────────
464
699
  if (input.settings.enableBlastRadius && result.findings.length > 0) {
465
700
  const trivyCveCount = result.findings.filter((f) => f.source === 'trivy' && f.category === 'dependency-vulnerability').length;
@@ -486,9 +721,35 @@ export async function reviewPipeline(input) {
486
721
  step: 'exploitability',
487
722
  message: `Exploitability analysis complete: ${exploitable} exploitable, ${potential} potentially, ${notExploitable} not exploitable`,
488
723
  });
724
+ // Function-level usage analysis (requires fileReader)
725
+ if (input.fileReader) {
726
+ emit({
727
+ step: 'usage-analysis',
728
+ message: 'Analyzing function-level usage of vulnerable packages...',
729
+ });
730
+ await analyzeUsage(result.findings, exploitGraph, input.fileReader);
731
+ const usageLabels = result.findings
732
+ .filter((f) => f.usageLabel)
733
+ .reduce((acc, f) => {
734
+ const key = f.usageLabel ?? 'unknown';
735
+ acc[key] = (acc[key] ?? 0) + 1;
736
+ return acc;
737
+ }, {});
738
+ const inUse = usageLabels['in-use'] ?? 0;
739
+ const importedNotCalled = usageLabels['imported-not-called'] ?? 0;
740
+ const notInUse = usageLabels['not-in-use'] ?? 0;
741
+ emit({
742
+ step: 'usage-analysis',
743
+ message: `Usage analysis complete: ${inUse} in-use, ${importedNotCalled} imported-not-called, ${notInUse} not-in-use`,
744
+ });
745
+ }
489
746
  }
490
747
  catch (error) {
491
748
  console.warn('[ghagga] Exploitability analysis failed (non-fatal):', error instanceof Error ? error.message : String(error));
749
+ failedSteps.push({
750
+ step: 'exploitability',
751
+ error: error instanceof Error ? error.message : String(error),
752
+ });
492
753
  emit({
493
754
  step: 'exploitability',
494
755
  message: 'Exploitability analysis failed — continuing without',
@@ -508,7 +769,7 @@ export async function reviewPipeline(input) {
508
769
  if (input.settings.enableRecursiveReview && aiEnabled && result.findings.length > 0) {
509
770
  emit({ step: 'recursive-review', message: 'Running recursive review on suggested fixes...' });
510
771
  try {
511
- const generateFns = resolveGenerateTextFns(input, isCliBridge, isGateway);
772
+ const generateFns = resolveGenerateTextFns(input, isCliBridge, isGateway, isOllama);
512
773
  const report = await recursiveReview({
513
774
  originalDiff: truncatedDiff,
514
775
  findings: result.findings,
@@ -538,15 +799,97 @@ export async function reviewPipeline(input) {
538
799
  }
539
800
  catch (error) {
540
801
  console.warn('[ghagga] Recursive review failed (non-fatal):', error instanceof Error ? error.message : String(error));
802
+ failedSteps.push({
803
+ step: 'recursive-review',
804
+ error: error instanceof Error ? error.message : String(error),
805
+ });
541
806
  emit({ step: 'recursive-review', message: 'Recursive review failed — continuing without' });
542
807
  }
543
808
  }
809
+ // ── Step 7.7: Code-doc validation (optional) ───────────────────
810
+ if (input.settings.enableDocValidation && filteredFiles.length > 0) {
811
+ try {
812
+ const changedSymbols = extractChangedSymbolsFromDiff(filteredDiff);
813
+ if (changedSymbols.length > 0) {
814
+ emit({
815
+ step: 'doc-validation',
816
+ message: `Scanning docs for ${changedSymbols.length} changed symbol(s)...`,
817
+ });
818
+ const docResult = scanDocsForSymbolRefs(changedSymbols, allFiles, fileList);
819
+ result.docValidation = docResult;
820
+ if (docResult.staleReferences.length > 0) {
821
+ // Convert stale references to findings
822
+ for (const ref of docResult.staleReferences) {
823
+ result.findings.push({
824
+ severity: 'low',
825
+ category: 'documentation',
826
+ file: ref.file,
827
+ line: ref.line,
828
+ message: `Documentation references \`${ref.symbol}\` which was changed in this PR but this doc was not updated.`,
829
+ suggestion: `Review and update the reference to \`${ref.symbol}\` in this file.`,
830
+ source: 'doc-validation',
831
+ });
832
+ }
833
+ emit({
834
+ step: 'doc-validation',
835
+ message: `Doc validation: ${docResult.staleReferences.length} stale reference(s) found in ${docResult.docsScanned} doc(s)`,
836
+ });
837
+ }
838
+ else {
839
+ emit({
840
+ step: 'doc-validation',
841
+ message: `Doc validation: no stale references (${docResult.docsScanned} docs scanned)`,
842
+ });
843
+ }
844
+ }
845
+ }
846
+ catch (error) {
847
+ console.warn('[ghagga] Doc validation failed (non-fatal):', error instanceof Error ? error.message : String(error));
848
+ failedSteps.push({
849
+ step: 'doc-validation',
850
+ error: error instanceof Error ? error.message : String(error),
851
+ });
852
+ emit({ step: 'doc-validation', message: 'Doc validation failed — continuing without' });
853
+ }
854
+ }
855
+ // ── Step 7.8: Semantic ranking of findings (optional) ─────────
856
+ const semanticRankingEnabled = input.features?.semanticRanking !== false && !!input.embeddingProvider;
857
+ if (semanticRankingEnabled && result.findings.length > 1) {
858
+ emit({ step: 'semantic-ranking', message: 'Reranking findings by semantic relevance...' });
859
+ try {
860
+ result.findings = await rankFindings(result.findings, input.embeddingProvider);
861
+ emit({
862
+ step: 'semantic-ranking',
863
+ message: `Semantic ranking complete (${result.findings.length} findings reranked)`,
864
+ });
865
+ }
866
+ catch (error) {
867
+ console.warn('[ghagga] Semantic ranking failed (non-fatal):', error instanceof Error ? error.message : String(error));
868
+ failedSteps.push({
869
+ step: 'semantic-ranking',
870
+ error: error instanceof Error ? error.message : String(error),
871
+ });
872
+ emit({ step: 'semantic-ranking', message: 'Semantic ranking failed — continuing without' });
873
+ }
874
+ }
544
875
  // ── Step 8: Persist to memory (awaited for SQLite correctness) ──
545
876
  if (input.settings.enableMemory && input.memoryStorage && input.context) {
546
877
  await persistReviewObservations(input.memoryStorage, input.context.repoFullName, input.context.prNumber, result).catch((error) => {
547
878
  console.warn('[ghagga] Memory persist failed (non-fatal):', error instanceof Error ? error.message : String(error));
879
+ failedSteps.push({
880
+ step: 'memory-persist',
881
+ error: error instanceof Error ? error.message : String(error),
882
+ });
548
883
  });
549
884
  }
885
+ // ── Step 9: Attach failed steps and mark as PARTIAL ─────────
886
+ if (failedSteps.length > 0) {
887
+ result.failedSteps = failedSteps;
888
+ // Only downgrade to PARTIAL if the review otherwise appeared successful
889
+ if (result.status === 'PASSED') {
890
+ result.status = 'PARTIAL';
891
+ }
892
+ }
550
893
  return result;
551
894
  }
552
895
  // ─── Provider Resolution ────────────────────────────────────────
@@ -621,11 +964,14 @@ function resolvePrimaryModel(input) {
621
964
  /**
622
965
  * Create the appropriate GenerateTextFn(s) based on the provider type.
623
966
  *
624
- * - CLI Bridge: single fn wrapping generateViaCLI
625
- * - Gateway: single fn wrapping generateViaGateway
626
- * - AI SDK: one fn per provider chain entry (for round-robin distribution)
967
+ * - cli-bridge: single fn wrapping generateViaCLI
968
+ * - gateway: one fn per gateway chain entry (for round-robin distribution)
969
+ * - ollama: single fn wrapping local Ollama OpenAI-compatible API
970
+ *
971
+ * Providers that are no longer supported directly (anthropic, openai, etc.)
972
+ * throw a migration error pointing users to gateway mode.
627
973
  */
628
- function resolveGenerateTextFns(input, isCliBridge, isGateway) {
974
+ function resolveGenerateTextFns(input, isCliBridge, isGateway, isOllama) {
629
975
  if (isCliBridge) {
630
976
  // Resolve CLI bridge options from provider chain or flat input fields
631
977
  const cliBridgeEntry = input.providerChain?.[0];
@@ -676,23 +1022,25 @@ function resolveGenerateTextFns(input, isCliBridge, isGateway) {
676
1022
  }),
677
1023
  ];
678
1024
  }
679
- // AI SDK: one function per chain entry (for round-robin distribution)
680
- const chain = input.providerChain && input.providerChain.length > 0 ? input.providerChain : null;
681
- if (chain) {
682
- return chain.map((entry) => createAISDKGenerateFn(entry.provider, entry.model, entry.apiKey));
1025
+ if (isOllama) {
1026
+ const model = input.model && input.model !== 'auto' ? input.model : 'llama3';
1027
+ return [createOllamaGenerateFn(model, input.ollamaBaseURL)];
683
1028
  }
684
- // Single provider from flat fields (backward compat)
685
- const primary = resolvePrimaryProvider(input);
686
- return [createAISDKGenerateFn(primary.provider, primary.model, primary.apiKey)];
1029
+ // Legacy provider migration guard should never reach here with the narrowed type,
1030
+ // but protects against runtime strings from older configs.
1031
+ const legacyProvider = input.providerChain?.[0]?.provider ?? input.provider ?? 'unknown';
1032
+ throw new Error(`Provider '${legacyProvider}' is no longer supported directly. ` +
1033
+ `Set provider: 'gateway' and configure credentials in mcp-llm-bridge. ` +
1034
+ `See docs/configuration.md#gateway-mode-mcp-llm-bridge`);
687
1035
  }
688
1036
  /**
689
1037
  * Resolve the effective review mode.
690
1038
  *
691
- * Diagnostic mode only works with AI SDK (it needs direct model access).
692
- * For CLI bridge and gateway, fall back to simple mode.
1039
+ * Diagnostic mode requires direct model access (not available in non-SDK backends).
1040
+ * For CLI bridge, gateway, and Ollama, fall back to simple mode.
693
1041
  */
694
- function resolveEffectiveMode(mode, isCliBridge, isGateway) {
695
- if (mode === 'diagnostic' && (isCliBridge || isGateway)) {
1042
+ function resolveEffectiveMode(mode, isCliBridge, isGateway, isOllama) {
1043
+ if (mode === 'diagnostic' && (isCliBridge || isGateway || isOllama)) {
696
1044
  return 'simple';
697
1045
  }
698
1046
  // Fan-out works with all backends (uses generateFns like workflow/consensus)
@@ -703,7 +1051,7 @@ function resolveEffectiveMode(mode, isCliBridge, isGateway) {
703
1051
  * Run static analysis with graceful degradation.
704
1052
  * Returns a result with all tools skipped if anything goes wrong.
705
1053
  */
706
- async function runStaticAnalysisSafe(fileList, input) {
1054
+ async function runStaticAnalysisSafe(fileList, input, failedSteps) {
707
1055
  try {
708
1056
  // Build a file map for static analysis (paths only, content from diff)
709
1057
  const files = new Map();
@@ -721,6 +1069,10 @@ async function runStaticAnalysisSafe(fileList, input) {
721
1069
  }
722
1070
  catch (error) {
723
1071
  console.warn('[ghagga] Static analysis failed (degrading gracefully):', error instanceof Error ? error.message : String(error));
1072
+ failedSteps.push({
1073
+ step: 'static-analysis',
1074
+ error: error instanceof Error ? error.message : String(error),
1075
+ });
724
1076
  const errorResult = {
725
1077
  status: 'error',
726
1078
  findings: [],
@@ -738,7 +1090,7 @@ async function runStaticAnalysisSafe(fileList, input) {
738
1090
  * Search memory with graceful degradation.
739
1091
  * Returns null if memory is disabled or unavailable.
740
1092
  */
741
- async function searchMemorySafe(input, fileList) {
1093
+ async function searchMemorySafe(input, fileList, failedSteps) {
742
1094
  if (!input.settings.enableMemory || !input.memoryStorage || !input.context) {
743
1095
  return null;
744
1096
  }
@@ -747,9 +1099,66 @@ async function searchMemorySafe(input, fileList) {
747
1099
  }
748
1100
  catch (error) {
749
1101
  console.warn('[ghagga] Memory search failed (degrading gracefully):', error instanceof Error ? error.message : String(error));
1102
+ failedSteps.push({
1103
+ step: 'memory-search',
1104
+ error: error instanceof Error ? error.message : String(error),
1105
+ });
750
1106
  return null;
751
1107
  }
752
1108
  }
1109
+ /**
1110
+ * Query the code intelligence provider for structural context.
1111
+ * Returns an empty array when disabled, unavailable, or on error.
1112
+ */
1113
+ async function queryCodeIntelSafe(input, fileList, emit, failedSteps) {
1114
+ if (!input.settings.enableCodeIntel || !input.codeIntelProvider) {
1115
+ return [];
1116
+ }
1117
+ const startTime = Date.now();
1118
+ emit({
1119
+ step: 'code-intel',
1120
+ message: `Querying code intelligence for ${fileList.length} file(s)...`,
1121
+ });
1122
+ try {
1123
+ const results = [];
1124
+ const provider = input.codeIntelProvider;
1125
+ // Query each changed file for structural data (parallel)
1126
+ const queries = fileList.map(async (file) => {
1127
+ const [imports, exports] = await Promise.all([
1128
+ provider.getFileImports(file),
1129
+ provider.getFileExports(file),
1130
+ ]);
1131
+ // Query callers/callees for each exported symbol
1132
+ const callerResults = await Promise.all(exports.slice(0, 10).map((sym) => provider.getCallers(sym, file)));
1133
+ const calleeResults = await Promise.all(exports.slice(0, 10).map((sym) => provider.getCallees(sym, file)));
1134
+ const callers = callerResults.flat();
1135
+ const callees = calleeResults.flat();
1136
+ return { file, callers, callees, imports, exports };
1137
+ });
1138
+ const settled = await Promise.allSettled(queries);
1139
+ for (const outcome of settled) {
1140
+ if (outcome.status === 'fulfilled') {
1141
+ results.push(outcome.value);
1142
+ }
1143
+ }
1144
+ const durationMs = Date.now() - startTime;
1145
+ const withData = results.filter((r) => r.callers.length > 0 || r.callees.length > 0 || r.imports.length > 0).length;
1146
+ emit({
1147
+ step: 'code-intel',
1148
+ message: `Code intelligence: ${withData}/${results.length} files with structural data (${durationMs}ms)`,
1149
+ });
1150
+ return results;
1151
+ }
1152
+ catch (error) {
1153
+ console.warn('[ghagga] Code intelligence query failed (degrading gracefully):', error instanceof Error ? error.message : String(error));
1154
+ failedSteps.push({
1155
+ step: 'code-intel',
1156
+ error: error instanceof Error ? error.message : String(error),
1157
+ });
1158
+ emit({ step: 'code-intel', message: 'Code intelligence: failed — continuing without' });
1159
+ return [];
1160
+ }
1161
+ }
753
1162
  /**
754
1163
  * Create a SKIPPED result when all files are filtered out.
755
1164
  */