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.
- package/dist/acp/adapter.d.ts +91 -0
- package/dist/acp/adapter.d.ts.map +1 -0
- package/dist/acp/adapter.js +315 -0
- package/dist/acp/adapter.js.map +1 -0
- package/dist/acp/index.d.ts +4 -0
- package/dist/acp/index.d.ts.map +1 -0
- package/dist/acp/index.js +2 -0
- package/dist/acp/index.js.map +1 -0
- package/dist/acp/types.d.ts +142 -0
- package/dist/acp/types.d.ts.map +1 -0
- package/dist/acp/types.js +13 -0
- package/dist/acp/types.js.map +1 -0
- package/dist/adversarial-qa.d.ts +60 -0
- package/dist/adversarial-qa.d.ts.map +1 -0
- package/dist/adversarial-qa.js +85 -0
- package/dist/adversarial-qa.js.map +1 -0
- package/dist/agents/audit.d.ts +18 -0
- package/dist/agents/audit.d.ts.map +1 -0
- package/dist/agents/audit.js +78 -0
- package/dist/agents/audit.js.map +1 -0
- package/dist/agents/consensus.d.ts +1 -1
- package/dist/agents/consensus.d.ts.map +1 -1
- package/dist/agents/consensus.js +10 -8
- package/dist/agents/consensus.js.map +1 -1
- package/dist/agents/diagnostic.d.ts.map +1 -1
- package/dist/agents/diagnostic.js +22 -20
- package/dist/agents/diagnostic.js.map +1 -1
- package/dist/agents/fan-out-lenses.d.ts +41 -0
- package/dist/agents/fan-out-lenses.d.ts.map +1 -1
- package/dist/agents/fan-out-lenses.js +117 -3
- package/dist/agents/fan-out-lenses.js.map +1 -1
- package/dist/agents/prompts.d.ts +12 -0
- package/dist/agents/prompts.d.ts.map +1 -1
- package/dist/agents/prompts.js +31 -0
- package/dist/agents/prompts.js.map +1 -1
- package/dist/agents/simple.d.ts +1 -1
- package/dist/agents/simple.d.ts.map +1 -1
- package/dist/agents/simple.js +10 -6
- package/dist/agents/simple.js.map +1 -1
- package/dist/agents/workflow.d.ts +1 -1
- package/dist/agents/workflow.d.ts.map +1 -1
- package/dist/agents/workflow.js +9 -8
- package/dist/agents/workflow.js.map +1 -1
- package/dist/aisvs.d.ts +44 -0
- package/dist/aisvs.d.ts.map +1 -0
- package/dist/aisvs.js +189 -0
- package/dist/aisvs.js.map +1 -0
- package/dist/checklist/context.d.ts.map +1 -1
- package/dist/checklist/context.js +2 -8
- package/dist/checklist/context.js.map +1 -1
- package/dist/checklist/defaults.d.ts.map +1 -1
- package/dist/checklist/defaults.js.map +1 -1
- package/dist/checklist/scorer.d.ts.map +1 -1
- package/dist/checklist/scorer.js +105 -12
- package/dist/checklist/scorer.js.map +1 -1
- package/dist/code-intel/client.d.ts +30 -0
- package/dist/code-intel/client.d.ts.map +1 -0
- package/dist/code-intel/client.js +91 -0
- package/dist/code-intel/client.js.map +1 -0
- package/dist/code-intel/context.d.ts +21 -0
- package/dist/code-intel/context.d.ts.map +1 -0
- package/dist/code-intel/context.js +72 -0
- package/dist/code-intel/context.js.map +1 -0
- package/dist/code-intel/index.d.ts +10 -0
- package/dist/code-intel/index.d.ts.map +1 -0
- package/dist/code-intel/index.js +11 -0
- package/dist/code-intel/index.js.map +1 -0
- package/dist/code-intel/types.d.ts +63 -0
- package/dist/code-intel/types.d.ts.map +1 -0
- package/dist/code-intel/types.js +9 -0
- package/dist/code-intel/types.js.map +1 -0
- package/dist/compress/index.d.ts +55 -0
- package/dist/compress/index.d.ts.map +1 -0
- package/dist/compress/index.js +166 -0
- package/dist/compress/index.js.map +1 -0
- package/dist/cost-footer.d.ts +38 -0
- package/dist/cost-footer.d.ts.map +1 -0
- package/dist/cost-footer.js +95 -0
- package/dist/cost-footer.js.map +1 -0
- package/dist/critique/critique.d.ts +40 -0
- package/dist/critique/critique.d.ts.map +1 -0
- package/dist/critique/critique.js +194 -0
- package/dist/critique/critique.js.map +1 -0
- package/dist/critique/cross-model.d.ts +123 -0
- package/dist/critique/cross-model.d.ts.map +1 -0
- package/dist/critique/cross-model.js +267 -0
- package/dist/critique/cross-model.js.map +1 -0
- package/dist/critique/index.d.ts +8 -0
- package/dist/critique/index.d.ts.map +1 -0
- package/dist/critique/index.js +6 -0
- package/dist/critique/index.js.map +1 -0
- package/dist/critique/prompts.d.ts +11 -0
- package/dist/critique/prompts.d.ts.map +1 -0
- package/dist/critique/prompts.js +66 -0
- package/dist/critique/prompts.js.map +1 -0
- package/dist/critique/types.d.ts +84 -0
- package/dist/critique/types.d.ts.map +1 -0
- package/dist/critique/types.js +13 -0
- package/dist/critique/types.js.map +1 -0
- package/dist/doc-validation/index.d.ts +9 -0
- package/dist/doc-validation/index.d.ts.map +1 -0
- package/dist/doc-validation/index.js +9 -0
- package/dist/doc-validation/index.js.map +1 -0
- package/dist/doc-validation/scanner.d.ts +40 -0
- package/dist/doc-validation/scanner.d.ts.map +1 -0
- package/dist/doc-validation/scanner.js +163 -0
- package/dist/doc-validation/scanner.js.map +1 -0
- package/dist/doc-validation/types.d.ts +27 -0
- package/dist/doc-validation/types.d.ts.map +1 -0
- package/dist/doc-validation/types.js +8 -0
- package/dist/doc-validation/types.js.map +1 -0
- package/dist/embed.d.ts +27 -0
- package/dist/embed.d.ts.map +1 -0
- package/dist/embed.js +47 -0
- package/dist/embed.js.map +1 -0
- package/dist/enhance/enhance.d.ts.map +1 -1
- package/dist/enhance/enhance.js +7 -25
- package/dist/enhance/enhance.js.map +1 -1
- package/dist/enhance/types.d.ts +5 -0
- package/dist/enhance/types.d.ts.map +1 -1
- package/dist/exploitability/analyzer.d.ts +42 -0
- package/dist/exploitability/analyzer.d.ts.map +1 -1
- package/dist/exploitability/analyzer.js +225 -0
- package/dist/exploitability/analyzer.js.map +1 -1
- package/dist/exploitability/index.d.ts +3 -2
- package/dist/exploitability/index.d.ts.map +1 -1
- package/dist/exploitability/index.js +1 -2
- package/dist/exploitability/index.js.map +1 -1
- package/dist/exploitability/types.d.ts +27 -0
- package/dist/exploitability/types.d.ts.map +1 -1
- package/dist/fetch-fix.d.ts +60 -0
- package/dist/fetch-fix.d.ts.map +1 -0
- package/dist/fetch-fix.js +137 -0
- package/dist/fetch-fix.js.map +1 -0
- package/dist/flood/index.d.ts +34 -0
- package/dist/flood/index.d.ts.map +1 -0
- package/dist/flood/index.js +67 -0
- package/dist/flood/index.js.map +1 -0
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +6 -1
- package/dist/format.js.map +1 -1
- package/dist/graph/blast-radius.js +2 -2
- package/dist/graph/blast-radius.js.map +1 -1
- package/dist/graph/call-chain.d.ts +36 -0
- package/dist/graph/call-chain.d.ts.map +1 -0
- package/dist/graph/call-chain.js +291 -0
- package/dist/graph/call-chain.js.map +1 -0
- package/dist/graph/index.d.ts +4 -0
- package/dist/graph/index.d.ts.map +1 -1
- package/dist/graph/index.js +2 -0
- package/dist/graph/index.js.map +1 -1
- package/dist/graph/reverse-deps.d.ts +37 -0
- package/dist/graph/reverse-deps.d.ts.map +1 -0
- package/dist/graph/reverse-deps.js +136 -0
- package/dist/graph/reverse-deps.js.map +1 -0
- package/dist/index.d.ts +50 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -7
- package/dist/index.js.map +1 -1
- package/dist/injection-corpus.d.ts +41 -0
- package/dist/injection-corpus.d.ts.map +1 -0
- package/dist/injection-corpus.js +181 -0
- package/dist/injection-corpus.js.map +1 -0
- package/dist/latent-comms.d.ts +31 -0
- package/dist/latent-comms.d.ts.map +1 -0
- package/dist/latent-comms.js +139 -0
- package/dist/latent-comms.js.map +1 -0
- package/dist/memory/pageindex/chunker.d.ts +33 -0
- package/dist/memory/pageindex/chunker.d.ts.map +1 -0
- package/dist/memory/pageindex/chunker.js +112 -0
- package/dist/memory/pageindex/chunker.js.map +1 -0
- package/dist/memory/pageindex/example.d.ts +22 -0
- package/dist/memory/pageindex/example.d.ts.map +1 -0
- package/dist/memory/pageindex/example.js +94 -0
- package/dist/memory/pageindex/example.js.map +1 -0
- package/dist/memory/pageindex/index.d.ts +15 -0
- package/dist/memory/pageindex/index.d.ts.map +1 -0
- package/dist/memory/pageindex/index.js +17 -0
- package/dist/memory/pageindex/index.js.map +1 -0
- package/dist/memory/pageindex/service.d.ts +53 -0
- package/dist/memory/pageindex/service.d.ts.map +1 -0
- package/dist/memory/pageindex/service.js +229 -0
- package/dist/memory/pageindex/service.js.map +1 -0
- package/dist/memory/pageindex/types.d.ts +67 -0
- package/dist/memory/pageindex/types.d.ts.map +1 -0
- package/dist/memory/pageindex/types.js +14 -0
- package/dist/memory/pageindex/types.js.map +1 -0
- package/dist/memory/persist.d.ts.map +1 -1
- package/dist/memory/persist.js +6 -2
- package/dist/memory/persist.js.map +1 -1
- package/dist/memory/sqlite.d.ts +69 -2
- package/dist/memory/sqlite.d.ts.map +1 -1
- package/dist/memory/sqlite.js +312 -5
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/memory/taxonomy.d.ts +34 -0
- package/dist/memory/taxonomy.d.ts.map +1 -0
- package/dist/memory/taxonomy.js +189 -0
- package/dist/memory/taxonomy.js.map +1 -0
- package/dist/memory/versioning.d.ts.map +1 -1
- package/dist/memory/versioning.js.map +1 -1
- package/dist/negative.d.ts +23 -0
- package/dist/negative.d.ts.map +1 -0
- package/dist/negative.js +40 -0
- package/dist/negative.js.map +1 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +455 -46
- package/dist/pipeline.js.map +1 -1
- package/dist/prompt-intel.d.ts +39 -0
- package/dist/prompt-intel.d.ts.map +1 -0
- package/dist/prompt-intel.js +148 -0
- package/dist/prompt-intel.js.map +1 -0
- package/dist/providers/cli-bridge.d.ts +4 -0
- package/dist/providers/cli-bridge.d.ts.map +1 -1
- package/dist/providers/cli-bridge.js +4 -0
- package/dist/providers/cli-bridge.js.map +1 -1
- package/dist/providers/generate-fn.d.ts +3 -15
- package/dist/providers/generate-fn.d.ts.map +1 -1
- package/dist/providers/generate-fn.js +3 -30
- package/dist/providers/generate-fn.js.map +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/ollama.d.ts +15 -0
- package/dist/providers/ollama.d.ts.map +1 -0
- package/dist/providers/ollama.js +30 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/ranking/index.d.ts +9 -0
- package/dist/ranking/index.d.ts.map +1 -0
- package/dist/ranking/index.js +82 -0
- package/dist/ranking/index.js.map +1 -0
- package/dist/recursive/circuit-breaker.d.ts +36 -0
- package/dist/recursive/circuit-breaker.d.ts.map +1 -0
- package/dist/recursive/circuit-breaker.js +62 -0
- package/dist/recursive/circuit-breaker.js.map +1 -0
- package/dist/recursive/index.d.ts +4 -0
- package/dist/recursive/index.d.ts.map +1 -1
- package/dist/recursive/index.js +18 -1
- package/dist/recursive/index.js.map +1 -1
- package/dist/recursive/types.d.ts +2 -0
- package/dist/recursive/types.d.ts.map +1 -1
- package/dist/recursive/types.js +1 -0
- package/dist/recursive/types.js.map +1 -1
- package/dist/scope/diff-mapper.js.map +1 -1
- package/dist/scope/entity-diff.d.ts +58 -0
- package/dist/scope/entity-diff.d.ts.map +1 -0
- package/dist/scope/entity-diff.js +224 -0
- package/dist/scope/entity-diff.js.map +1 -0
- package/dist/scope/extractor.d.ts.map +1 -1
- package/dist/scope/extractor.js.map +1 -1
- package/dist/scope/index.d.ts +3 -1
- package/dist/scope/index.d.ts.map +1 -1
- package/dist/scope/index.js +3 -0
- package/dist/scope/index.js.map +1 -1
- package/dist/scope/parser.d.ts +1 -1
- package/dist/scope/parser.d.ts.map +1 -1
- package/dist/scope/parser.js.map +1 -1
- package/dist/scope/types.d.ts +32 -0
- package/dist/scope/types.d.ts.map +1 -1
- package/dist/scope/types.js +7 -1
- package/dist/scope/types.js.map +1 -1
- package/dist/search/index.d.ts +11 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +10 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/indexer.d.ts +67 -0
- package/dist/search/indexer.d.ts.map +1 -0
- package/dist/search/indexer.js +196 -0
- package/dist/search/indexer.js.map +1 -0
- package/dist/search/searcher.d.ts +34 -0
- package/dist/search/searcher.d.ts.map +1 -0
- package/dist/search/searcher.js +101 -0
- package/dist/search/searcher.js.map +1 -0
- package/dist/search/types.d.ts +81 -0
- package/dist/search/types.d.ts.map +1 -0
- package/dist/search/types.js +8 -0
- package/dist/search/types.js.map +1 -0
- package/dist/self-improve/index.d.ts +53 -0
- package/dist/self-improve/index.d.ts.map +1 -0
- package/dist/self-improve/index.js +136 -0
- package/dist/self-improve/index.js.map +1 -0
- package/dist/semantic-diff/index.d.ts +31 -0
- package/dist/semantic-diff/index.d.ts.map +1 -0
- package/dist/semantic-diff/index.js +215 -0
- package/dist/semantic-diff/index.js.map +1 -0
- package/dist/testing/index.d.ts +67 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +76 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/vitest-helpers.d.ts +26 -0
- package/dist/testing/vitest-helpers.d.ts.map +1 -0
- package/dist/testing/vitest-helpers.js +37 -0
- package/dist/testing/vitest-helpers.js.map +1 -0
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/remote-query.d.ts +40 -0
- package/dist/tools/remote-query.d.ts.map +1 -0
- package/dist/tools/remote-query.js +71 -0
- package/dist/tools/remote-query.js.map +1 -0
- package/dist/tracing/index.d.ts +39 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +70 -0
- package/dist/tracing/index.js.map +1 -0
- package/dist/trajectory.d.ts +65 -0
- package/dist/trajectory.d.ts.map +1 -0
- package/dist/trajectory.js +126 -0
- package/dist/trajectory.js.map +1 -0
- package/dist/trust/index.d.ts +34 -0
- package/dist/trust/index.d.ts.map +1 -0
- package/dist/trust/index.js +78 -0
- package/dist/trust/index.js.map +1 -0
- package/dist/types.d.ts +144 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -11
- package/dist/types.js.map +1 -1
- 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 {
|
|
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
|
-
//
|
|
67
|
-
if (
|
|
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:
|
|
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
|
-
//
|
|
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,
|
|
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(
|
|
321
|
-
if (effectiveMode !==
|
|
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,
|
|
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
|
-
* -
|
|
625
|
-
* -
|
|
626
|
-
* -
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
//
|
|
685
|
-
|
|
686
|
-
|
|
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
|
|
692
|
-
* For CLI bridge and
|
|
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
|
*/
|