scai 0.1.117 → 0.1.119
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/README.md +88 -503
- package/dist/agents/MainAgent.js +255 -0
- package/dist/agents/contextReviewStep.js +104 -0
- package/dist/agents/finalPlanGenStep.js +123 -0
- package/dist/agents/infoPlanGenStep.js +126 -0
- package/dist/agents/planGeneratorStep.js +118 -0
- package/dist/agents/planResolverStep.js +95 -0
- package/dist/agents/planTargetFilesStep.js +48 -0
- package/dist/agents/preFileSearchCheckStep.js +95 -0
- package/dist/agents/selectRelevantSourcesStep.js +100 -0
- package/dist/agents/semanticAnalysisStep.js +144 -0
- package/dist/agents/structuralAnalysisStep.js +46 -0
- package/dist/agents/transformPlanGenStep.js +107 -0
- package/dist/agents/understandIntentStep.js +72 -0
- package/dist/agents/validationAnalysisStep.js +87 -0
- package/dist/commands/AskCmd.js +47 -116
- package/dist/commands/ChangeLogUpdateCmd.js +11 -5
- package/dist/commands/CommitSuggesterCmd.js +50 -75
- package/dist/commands/DaemonCmd.js +119 -29
- package/dist/commands/IndexCmd.js +41 -24
- package/dist/commands/InspectCmd.js +0 -1
- package/dist/commands/ReadlineSingleton.js +18 -0
- package/dist/commands/ResetDbCmd.js +20 -21
- package/dist/commands/ReviewCmd.js +89 -54
- package/dist/commands/SummaryCmd.js +12 -18
- package/dist/commands/WorkflowCmd.js +41 -0
- package/dist/commands/factory.js +254 -0
- package/dist/config.js +67 -15
- package/dist/constants.js +20 -4
- package/dist/context.js +10 -11
- package/dist/daemon/daemonQueues.js +63 -0
- package/dist/daemon/daemonWorker.js +40 -63
- package/dist/daemon/generateSummaries.js +58 -0
- package/dist/daemon/runFolderCapsuleBatch.js +247 -0
- package/dist/daemon/runIndexingBatch.js +147 -0
- package/dist/daemon/runKgBatch.js +104 -0
- package/dist/db/fileIndex.js +168 -63
- package/dist/db/functionExtractors/extractFromJava.js +210 -6
- package/dist/db/functionExtractors/extractFromJs.js +173 -214
- package/dist/db/functionExtractors/extractFromTs.js +159 -160
- package/dist/db/functionExtractors/index.js +7 -5
- package/dist/db/schema.js +55 -20
- package/dist/db/sqlTemplates.js +50 -19
- package/dist/fileRules/builtins.js +31 -14
- package/dist/fileRules/codeAllowedExtensions.js +4 -0
- package/dist/fileRules/fileExceptions.js +0 -13
- package/dist/fileRules/ignoredExtensions.js +10 -0
- package/dist/index.js +128 -325
- package/dist/lib/generate.js +37 -14
- package/dist/lib/generateFolderCapsules.js +109 -0
- package/dist/lib/spinner.js +12 -5
- package/dist/modelSetup.js +1 -11
- package/dist/pipeline/modules/changeLogModule.js +16 -19
- package/dist/pipeline/modules/chunkManagerModule.js +24 -0
- package/dist/pipeline/modules/cleanupModule.js +95 -91
- package/dist/pipeline/modules/codeTransformModule.js +208 -0
- package/dist/pipeline/modules/commentModule.js +20 -11
- package/dist/pipeline/modules/commitSuggesterModule.js +36 -14
- package/dist/pipeline/modules/contextReviewModule.js +52 -0
- package/dist/pipeline/modules/fileReaderModule.js +72 -0
- package/dist/pipeline/modules/fileSearchModule.js +136 -0
- package/dist/pipeline/modules/finalAnswerModule.js +53 -0
- package/dist/pipeline/modules/gatherInfoModule.js +176 -0
- package/dist/pipeline/modules/generateTestsModule.js +63 -54
- package/dist/pipeline/modules/kgModule.js +26 -11
- package/dist/pipeline/modules/preserveCodeModule.js +91 -49
- package/dist/pipeline/modules/refactorModule.js +19 -7
- package/dist/pipeline/modules/repairTestsModule.js +44 -36
- package/dist/pipeline/modules/reviewModule.js +23 -13
- package/dist/pipeline/modules/summaryModule.js +27 -35
- package/dist/pipeline/modules/writeFileModule.js +86 -0
- package/dist/pipeline/registry/moduleRegistry.js +38 -93
- package/dist/pipeline/runModulePipeline.js +22 -19
- package/dist/scripts/dbcheck.js +143 -228
- package/dist/utils/buildContextualPrompt.js +245 -172
- package/dist/utils/debugContext.js +24 -0
- package/dist/utils/fileTree.js +16 -6
- package/dist/utils/loadRelevantFolderCapsules.js +64 -0
- package/dist/utils/log.js +2 -0
- package/dist/utils/normalizeData.js +23 -0
- package/dist/utils/planActions.js +60 -0
- package/dist/utils/promptBuilderHelper.js +67 -0
- package/dist/utils/promptLogHelper.js +52 -0
- package/dist/utils/sanitizeQuery.js +20 -8
- package/dist/utils/sleep.js +3 -0
- package/dist/utils/splitCodeIntoChunk.js +65 -32
- package/dist/utils/vscode.js +49 -0
- package/dist/workflow/workflowResolver.js +14 -0
- package/dist/workflow/workflowRunner.js +103 -0
- package/package.json +6 -5
- package/dist/agent/agentManager.js +0 -39
- package/dist/agent/workflowManager.js +0 -95
- package/dist/commands/ModulePipelineCmd.js +0 -31
- package/dist/daemon/daemonBatch.js +0 -186
- package/dist/fileRules/scoreFiles.js +0 -71
- package/dist/lib/generateEmbedding.js +0 -22
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { generate } from '../lib/generate.js';
|
|
2
|
+
import { PLAN_ACTIONS } from '../utils/planActions.js';
|
|
3
|
+
import { logInputOutput } from '../utils/promptLogHelper.js';
|
|
4
|
+
import { cleanupModule } from '../pipeline/modules/cleanupModule.js';
|
|
5
|
+
const MAX_STEPS = 100;
|
|
6
|
+
export const planGeneratorStep = {
|
|
7
|
+
name: 'planGenerator',
|
|
8
|
+
description: 'Converts a textual plan into structured JSON steps.',
|
|
9
|
+
requires: ['userQuery', 'intent', 'analysis.routingDecision'],
|
|
10
|
+
produces: ['analysis.planSuggestion.plan'],
|
|
11
|
+
async run(context) {
|
|
12
|
+
var _a, _b;
|
|
13
|
+
context.analysis || (context.analysis = {});
|
|
14
|
+
// We check for missing files detected in the preFileSearchCheckStep
|
|
15
|
+
// If none we filter out fileSearch actions.
|
|
16
|
+
const missingFiles = Array.isArray(context.analysis.focus?.missingFiles)
|
|
17
|
+
? context.analysis.focus.missingFiles
|
|
18
|
+
: [];
|
|
19
|
+
const hasMissingFiles = missingFiles.length > 0;
|
|
20
|
+
const effectiveActions = PLAN_ACTIONS.filter(a => {
|
|
21
|
+
if (!hasMissingFiles && a.action === 'fileSearch') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
});
|
|
26
|
+
const actionsJson = JSON.stringify(effectiveActions, null, 2);
|
|
27
|
+
const intentText = context.analysis?.intent?.normalizedQuery ?? context.initContext?.userQuery;
|
|
28
|
+
const intentCategory = context.analysis?.intent?.intentCategory ?? '';
|
|
29
|
+
const suggestionText = context.analysis?.planSuggestion?.text ?? '';
|
|
30
|
+
const prompt = `
|
|
31
|
+
You are an autonomous coding agent. Convert the following textual suggestion into a structured JSON plan.
|
|
32
|
+
|
|
33
|
+
Intent / task description:
|
|
34
|
+
${intentText}
|
|
35
|
+
|
|
36
|
+
Suggest task type:
|
|
37
|
+
${intentCategory};
|
|
38
|
+
|
|
39
|
+
Suggested plan / guidance:
|
|
40
|
+
${suggestionText}
|
|
41
|
+
|
|
42
|
+
Folder info:
|
|
43
|
+
${context.analysis?.folderCapsulesHuman}
|
|
44
|
+
|
|
45
|
+
Allowed actions and their groups (JSON format):
|
|
46
|
+
${actionsJson}
|
|
47
|
+
|
|
48
|
+
Existing relevant files:
|
|
49
|
+
${JSON.stringify(context.analysis?.focus?.relevantFiles ?? {}, null, 2)}
|
|
50
|
+
|
|
51
|
+
Only implement search if there are missing files:
|
|
52
|
+
${JSON.stringify(context.analysis?.focus?.missingFiles ?? {}, null, 2)}
|
|
53
|
+
|
|
54
|
+
⚡ Phase guidance:
|
|
55
|
+
- Actions are grouped into phases: info, transform, finalize.
|
|
56
|
+
- Respect group boundaries: do all "info" steps first, then "transform", then "finalize".
|
|
57
|
+
- Each step must include: "action", "targetFile" (optional), "description", "metadata"
|
|
58
|
+
|
|
59
|
+
❌ IMPORTANT: Do NOT use "codeTransform" as an analysis step. All analysis tasks are already handled by semantic/structural analysis modules. Only use "codeTransform" if you are generating actual code changes in the transform phase.
|
|
60
|
+
|
|
61
|
+
Rules:
|
|
62
|
+
- Use only the actions listed above.
|
|
63
|
+
- Limit to a maximum of ${MAX_STEPS} steps.
|
|
64
|
+
- Return strictly valid JSON matching:
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
"steps": [
|
|
68
|
+
{ "action": "stepName", "targetFile": "optional/path.ts", "description": "explanation", "metadata": {} }
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
`.trim();
|
|
72
|
+
try {
|
|
73
|
+
console.dir("planGeneratorPrompt: ", prompt);
|
|
74
|
+
const genInput = { query: intentText ?? '', content: prompt };
|
|
75
|
+
const genOutput = await generate(genInput);
|
|
76
|
+
const llmOutput = typeof genOutput.data === 'string'
|
|
77
|
+
? genOutput.data
|
|
78
|
+
: JSON.stringify(genOutput.data ?? '{}');
|
|
79
|
+
const cleaned = await cleanupModule.run({
|
|
80
|
+
query: intentText ?? '',
|
|
81
|
+
content: llmOutput,
|
|
82
|
+
});
|
|
83
|
+
const jsonString = typeof cleaned.content === 'string'
|
|
84
|
+
? cleaned.content
|
|
85
|
+
: JSON.stringify(cleaned.content ?? '{}');
|
|
86
|
+
let plan = JSON.parse(jsonString);
|
|
87
|
+
if (!plan || typeof plan !== 'object' || !Array.isArray(plan.steps)) {
|
|
88
|
+
throw new Error('Invalid plan structure');
|
|
89
|
+
}
|
|
90
|
+
if (plan.steps.length > MAX_STEPS) {
|
|
91
|
+
console.warn(`⚠️ Truncating plan steps: got ${plan.steps.length}, limiting to ${MAX_STEPS}`);
|
|
92
|
+
plan.steps = plan.steps.slice(0, MAX_STEPS);
|
|
93
|
+
}
|
|
94
|
+
plan.steps = plan.steps.map(step => {
|
|
95
|
+
const actionDef = PLAN_ACTIONS.find(a => a.action === step.action);
|
|
96
|
+
return {
|
|
97
|
+
...step,
|
|
98
|
+
metadata: {
|
|
99
|
+
...step.metadata,
|
|
100
|
+
routingConfidence: context.analysis?.routingDecision?.confidence ?? 0
|
|
101
|
+
},
|
|
102
|
+
groups: actionDef?.groups ?? []
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
context.analysis ?? (context.analysis = {});
|
|
106
|
+
(_a = context.analysis).planSuggestion ?? (_a.planSuggestion = { text: '' });
|
|
107
|
+
context.analysis.planSuggestion.plan = plan;
|
|
108
|
+
logInputOutput('planGenerator', 'output', { generatedPlan: plan });
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
console.warn('⚠️ Failed to parse plan JSON:', err);
|
|
112
|
+
context.analysis ?? (context.analysis = {});
|
|
113
|
+
(_b = context.analysis).planSuggestion ?? (_b.planSuggestion = { text: '' });
|
|
114
|
+
context.analysis.planSuggestion.plan = { steps: [] };
|
|
115
|
+
logInputOutput('planGenerator', 'output', { generatedPlan: { steps: [] } });
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { generate } from '../lib/generate.js';
|
|
2
|
+
import { logInputOutput } from '../utils/promptLogHelper.js';
|
|
3
|
+
function logRoutingDecision(context) {
|
|
4
|
+
logInputOutput('planResolver', 'output', context.analysis?.routingDecision ?? {});
|
|
5
|
+
}
|
|
6
|
+
export const planResolverStep = {
|
|
7
|
+
name: 'planResolver',
|
|
8
|
+
// 🔒 Hard contract
|
|
9
|
+
requires: ['userQuery', 'intent'],
|
|
10
|
+
produces: [
|
|
11
|
+
'analysis.routingDecision',
|
|
12
|
+
'analysis.planSuggestion',
|
|
13
|
+
],
|
|
14
|
+
async run(context) {
|
|
15
|
+
context.analysis || (context.analysis = {});
|
|
16
|
+
const prompt = `
|
|
17
|
+
You are a routing classifier, not a problem solver.
|
|
18
|
+
|
|
19
|
+
User query:
|
|
20
|
+
${context.initContext?.userQuery}
|
|
21
|
+
|
|
22
|
+
Repo section:
|
|
23
|
+
${context.initContext?.repoTree}
|
|
24
|
+
|
|
25
|
+
${context.analysis?.folderCapsulesHuman}
|
|
26
|
+
|
|
27
|
+
Decide ONE of the following:
|
|
28
|
+
|
|
29
|
+
RESOLVED:
|
|
30
|
+
- Only if the user can be answered immediately
|
|
31
|
+
- The answer must be short (1–2 paragraphs)
|
|
32
|
+
- No code blocks
|
|
33
|
+
- No step-by-step instructions
|
|
34
|
+
- No file-level documentation
|
|
35
|
+
|
|
36
|
+
PLAN:
|
|
37
|
+
- If the task requires reading, modifying, generating, or validating code
|
|
38
|
+
- If the answer would be long, structured, or procedural
|
|
39
|
+
- If files are mentioned or implied
|
|
40
|
+
|
|
41
|
+
`.trim();
|
|
42
|
+
try {
|
|
43
|
+
const genInput = {
|
|
44
|
+
query: context.initContext?.userQuery ?? '',
|
|
45
|
+
content: prompt,
|
|
46
|
+
};
|
|
47
|
+
const genOutput = await generate(genInput);
|
|
48
|
+
console.log('PlanResolverOutput: ', genInput.data);
|
|
49
|
+
const raw = String(genOutput.data ?? '').trim();
|
|
50
|
+
logInputOutput('planResolver raw', 'output', raw);
|
|
51
|
+
const isResolved = raw.startsWith('RESOLVED:');
|
|
52
|
+
const isPlan = raw.startsWith('PLAN:');
|
|
53
|
+
const resolvedText = raw.replace(/^RESOLVED:\s*/i, '').trim();
|
|
54
|
+
const incompleteAnswerPatterns = [
|
|
55
|
+
/possible/i, /provide/i, /need/i, /cannot/i, /missing/i,
|
|
56
|
+
/unknown/i, /unspecified/i, /unavailable/i, /undefined/i,
|
|
57
|
+
/incomplete/i, /insufficient/i, /ambiguous/i, /unclear/i,
|
|
58
|
+
/requires/i, /lack/i, /omitted/i,
|
|
59
|
+
];
|
|
60
|
+
const requiresMoreWork = incompleteAnswerPatterns.some(p => p.test(resolvedText));
|
|
61
|
+
if (isResolved && resolvedText && !requiresMoreWork) {
|
|
62
|
+
context.analysis.routingDecision = {
|
|
63
|
+
decision: 'final-answer',
|
|
64
|
+
answer: resolvedText,
|
|
65
|
+
rationale: 'Request can be fully resolved immediately without further steps.',
|
|
66
|
+
confidence: 0.8,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const planText = isPlan
|
|
71
|
+
? raw.replace(/^PLAN:\s*/i, '').trim()
|
|
72
|
+
: resolvedText;
|
|
73
|
+
context.analysis.planSuggestion = {
|
|
74
|
+
text: planText,
|
|
75
|
+
};
|
|
76
|
+
context.analysis.routingDecision = {
|
|
77
|
+
decision: 'needs-plan',
|
|
78
|
+
rationale: isPlan
|
|
79
|
+
? 'Model explicitly indicated planning or execution is required.'
|
|
80
|
+
: 'Request cannot be completed without further steps or execution.',
|
|
81
|
+
confidence: isPlan ? 0.6 : 0.4,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
logRoutingDecision(context);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.warn('⚠️ planResolver failed:', err);
|
|
88
|
+
context.analysis.routingDecision = {
|
|
89
|
+
decision: 'needs-plan',
|
|
90
|
+
rationale: 'planResolver encountered an internal error.',
|
|
91
|
+
confidence: 0.2,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { logInputOutput } from "../utils/promptLogHelper.js";
|
|
2
|
+
export const planTargetFilesStep = {
|
|
3
|
+
name: "planTargetFilesStep",
|
|
4
|
+
description: "Sync files from analysis.focus into plan.targetFiles. Ensures only workingFiles are moved and does not redo intent or risks.",
|
|
5
|
+
groups: ["analysis"],
|
|
6
|
+
run: async (input) => {
|
|
7
|
+
const context = input.context;
|
|
8
|
+
const query = input.query ?? "";
|
|
9
|
+
if (!context) {
|
|
10
|
+
const output = {
|
|
11
|
+
query,
|
|
12
|
+
data: { notes: "[planTargetFilesStep] No context provided; skipping." },
|
|
13
|
+
};
|
|
14
|
+
logInputOutput("planTargetFilesStep", "output", output.data);
|
|
15
|
+
return output;
|
|
16
|
+
}
|
|
17
|
+
// Ensure analysis.focus exists
|
|
18
|
+
const focusFiles = context.analysis?.focus?.relevantFiles ?? [];
|
|
19
|
+
if (!focusFiles.length) {
|
|
20
|
+
const output = {
|
|
21
|
+
query,
|
|
22
|
+
data: { notes: "[planTargetFilesStep] No relevant files in analysis.focus; nothing to move." },
|
|
23
|
+
};
|
|
24
|
+
logInputOutput("planTargetFilesStep", "output", output.data);
|
|
25
|
+
return output;
|
|
26
|
+
}
|
|
27
|
+
// Ensure plan exists
|
|
28
|
+
context.plan || (context.plan = {});
|
|
29
|
+
// Move relevant files to plan.targetFiles, only if they exist in workingFiles
|
|
30
|
+
const workingFilePaths = new Set(context.workingFiles?.map(f => f.path) ?? []);
|
|
31
|
+
const targetFiles = new Set(context.plan.targetFiles ?? []);
|
|
32
|
+
focusFiles.forEach(f => {
|
|
33
|
+
if (workingFilePaths.has(f)) {
|
|
34
|
+
targetFiles.add(f);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
context.plan.targetFiles = Array.from(targetFiles);
|
|
38
|
+
const output = {
|
|
39
|
+
query,
|
|
40
|
+
data: {
|
|
41
|
+
movedFiles: Array.from(targetFiles),
|
|
42
|
+
notes: "Focus files successfully moved to plan.targetFiles.",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
logInputOutput("planTargetFilesStep", "output", output.data);
|
|
46
|
+
return output;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// File: src/modules/preFileSearchCheckStep.ts
|
|
2
|
+
import { generate } from "../lib/generate.js";
|
|
3
|
+
import { logInputOutput } from "../utils/promptLogHelper.js";
|
|
4
|
+
import { cleanupModule } from "../pipeline/modules/cleanupModule.js";
|
|
5
|
+
export async function preFileSearchCheckStep(context) {
|
|
6
|
+
if (!context.analysis)
|
|
7
|
+
context.analysis = {};
|
|
8
|
+
const intent = context.analysis.intent;
|
|
9
|
+
const planSuggestion = context.analysis.planSuggestion?.text ?? "";
|
|
10
|
+
// Step 1: gather known files from initContext only
|
|
11
|
+
const knownFiles = new Set(context.initContext?.relatedFiles ?? []);
|
|
12
|
+
// Step 2: extract file names from normalizedQuery or planSuggestion
|
|
13
|
+
const extractedFiles = extractFilesFromAnalysis(context.analysis);
|
|
14
|
+
// Step 3: populate focus
|
|
15
|
+
context.analysis.focus = context.analysis.focus || { relevantFiles: [] };
|
|
16
|
+
context.analysis.focus.relevantFiles = extractedFiles.filter(f => knownFiles.has(f));
|
|
17
|
+
context.analysis.focus.missingFiles = extractedFiles.filter(f => !knownFiles.has(f));
|
|
18
|
+
context.analysis.focus.rationale = `Pre-check: ${context.analysis.focus.relevantFiles.length} files already available, ${context.analysis.focus.missingFiles.length} missing.`;
|
|
19
|
+
// Optional: LLM call for semantic understanding (assumptions, constraints, risks)
|
|
20
|
+
const prompt = `
|
|
21
|
+
You are an AI meta-agent assisting with context verification. The user intent and plan suggestion are below.
|
|
22
|
+
|
|
23
|
+
User intent:
|
|
24
|
+
${JSON.stringify(intent ?? {}, null, 2)}
|
|
25
|
+
|
|
26
|
+
Plan suggestion:
|
|
27
|
+
${planSuggestion}
|
|
28
|
+
|
|
29
|
+
Known files in context:
|
|
30
|
+
${JSON.stringify([...knownFiles], null, 2)}
|
|
31
|
+
|
|
32
|
+
Task:
|
|
33
|
+
1. Confirm which files are already available to satisfy the intent.
|
|
34
|
+
2. List missing files if any.
|
|
35
|
+
3. Suggest any assumptions, constraints, or risks that may affect execution.
|
|
36
|
+
4. Return STRICT JSON with shape:
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
"relevantFiles": string[],
|
|
40
|
+
"missingFiles": string[],
|
|
41
|
+
"rationale": string,
|
|
42
|
+
"understanding": {
|
|
43
|
+
"assumptions"?: string[],
|
|
44
|
+
"constraints"?: string[],
|
|
45
|
+
"risks"?: string[]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
`.trim();
|
|
49
|
+
const ai = await generate({
|
|
50
|
+
query: context.initContext?.userQuery ?? '',
|
|
51
|
+
content: prompt
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
const cleaned = await cleanupModule.run({
|
|
55
|
+
query: context.initContext?.userQuery ?? '',
|
|
56
|
+
content: ai.data,
|
|
57
|
+
});
|
|
58
|
+
let parsed;
|
|
59
|
+
if (typeof cleaned.data === "object" && cleaned.data !== null) {
|
|
60
|
+
parsed = cleaned.data;
|
|
61
|
+
}
|
|
62
|
+
else if (typeof cleaned.content === "string") {
|
|
63
|
+
parsed = JSON.parse(cleaned.content);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
throw new Error("Cleanup output is neither object nor string");
|
|
67
|
+
}
|
|
68
|
+
context.analysis.focus.relevantFiles =
|
|
69
|
+
parsed.relevantFiles ?? context.analysis.focus.relevantFiles;
|
|
70
|
+
context.analysis.focus.missingFiles =
|
|
71
|
+
parsed.missingFiles ?? context.analysis.focus.missingFiles;
|
|
72
|
+
context.analysis.focus.rationale =
|
|
73
|
+
parsed.rationale ?? context.analysis.focus.rationale;
|
|
74
|
+
if (parsed.understanding) {
|
|
75
|
+
context.analysis.understanding = {
|
|
76
|
+
...context.analysis.understanding,
|
|
77
|
+
...parsed.understanding,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
logInputOutput("preFileSearchCheckStep", "output", parsed);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
console.warn("[preFileSearchCheckStep] Failed to parse AI output, using defaults", err);
|
|
84
|
+
}
|
|
85
|
+
// Simple regex-based extractor
|
|
86
|
+
function extractFilesFromAnalysis(analysis) {
|
|
87
|
+
const sources = [
|
|
88
|
+
analysis?.intent?.normalizedQuery ?? "",
|
|
89
|
+
analysis?.planSuggestion?.text ?? ""
|
|
90
|
+
].join("\n");
|
|
91
|
+
const regex = /\b([\w\-\./]+\.js|[\w\-\./]+\.ts)\b/g;
|
|
92
|
+
const matches = sources.match(regex);
|
|
93
|
+
return matches ? Array.from(new Set(matches.map(m => m.trim()))) : [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// File: src/modules/selectRelevantSourcesStep.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { generate } from "../lib/generate.js";
|
|
5
|
+
import { cleanupModule } from "../pipeline/modules/cleanupModule.js";
|
|
6
|
+
import { logInputOutput } from "../utils/promptLogHelper.js";
|
|
7
|
+
export const selectRelevantSourcesStep = {
|
|
8
|
+
name: "selectRelevantSources",
|
|
9
|
+
description: "Selects the most relevant files for the query from relatedFiles + workingFiles, loads their code, and updates context.workingFiles.",
|
|
10
|
+
groups: ["analysis"],
|
|
11
|
+
run: async (input) => {
|
|
12
|
+
const query = input.query ?? "";
|
|
13
|
+
const context = input.context;
|
|
14
|
+
if (!context) {
|
|
15
|
+
throw new Error("[selectRelevantSources] StructuredContext is required.");
|
|
16
|
+
}
|
|
17
|
+
console.log(chalk.blueBright(`📁 [selectRelevantSources] Selecting relevant files for query "${query}"...`));
|
|
18
|
+
// Merge candidate paths (relatedFiles + existing workingFiles)
|
|
19
|
+
// At this point in-depth context build or searchfiles may have placed files in
|
|
20
|
+
// working files.
|
|
21
|
+
const candidatePaths = [
|
|
22
|
+
...(context.analysis?.focus?.relevantFiles ?? []),
|
|
23
|
+
...(context.initContext?.relatedFiles ?? []),
|
|
24
|
+
...(context.workingFiles?.map(f => f.path) ?? [])
|
|
25
|
+
];
|
|
26
|
+
const uniquePaths = Array.from(new Set(candidatePaths));
|
|
27
|
+
// -----------------------------
|
|
28
|
+
// Prompt the LLM to rank files
|
|
29
|
+
// -----------------------------
|
|
30
|
+
const prompt = `
|
|
31
|
+
You are given:
|
|
32
|
+
|
|
33
|
+
Query:
|
|
34
|
+
"${query}"
|
|
35
|
+
|
|
36
|
+
Candidate file paths:
|
|
37
|
+
${JSON.stringify(uniquePaths, null, 2)}
|
|
38
|
+
|
|
39
|
+
Task:
|
|
40
|
+
- Identify ONLY the files directly you find to be directly relevant to the query.
|
|
41
|
+
- Return ONLY a JSON array of objects.
|
|
42
|
+
- Each object must have at least a "path" field.
|
|
43
|
+
- Optional fields: "summary".
|
|
44
|
+
- Do NOT include explanations.
|
|
45
|
+
`.trim();
|
|
46
|
+
let topFiles = [];
|
|
47
|
+
try {
|
|
48
|
+
const response = await generate({ content: prompt, query: "" });
|
|
49
|
+
const cleaned = await cleanupModule.run({
|
|
50
|
+
query,
|
|
51
|
+
content: response.data,
|
|
52
|
+
});
|
|
53
|
+
const rawFiles = Array.isArray(cleaned.data) ? cleaned.data : [];
|
|
54
|
+
// Load code for selected files
|
|
55
|
+
topFiles = rawFiles
|
|
56
|
+
.map((f) => {
|
|
57
|
+
if (typeof f?.path !== "string")
|
|
58
|
+
return null;
|
|
59
|
+
let code;
|
|
60
|
+
try {
|
|
61
|
+
code = fs.readFileSync(f.path, "utf-8");
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.warn(`⚠️ Failed to read file ${f.path}:`, err.message);
|
|
65
|
+
}
|
|
66
|
+
return { path: f.path, code };
|
|
67
|
+
})
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
console.error("❌ [selectRelevantSources] Model selection failed:", err);
|
|
72
|
+
topFiles = [];
|
|
73
|
+
}
|
|
74
|
+
// -----------------------------
|
|
75
|
+
// Persist authoritative context (append, do not overwrite)
|
|
76
|
+
// -----------------------------
|
|
77
|
+
context.workingFiles = [
|
|
78
|
+
...(context.workingFiles ?? []), // existing working files
|
|
79
|
+
...topFiles, // new top files
|
|
80
|
+
];
|
|
81
|
+
// Deduplicate by path
|
|
82
|
+
const seenPaths = new Set();
|
|
83
|
+
context.workingFiles = context.workingFiles.filter(f => {
|
|
84
|
+
if (!f.path)
|
|
85
|
+
return false; // safety check
|
|
86
|
+
if (seenPaths.has(f.path))
|
|
87
|
+
return false;
|
|
88
|
+
seenPaths.add(f.path);
|
|
89
|
+
return true;
|
|
90
|
+
});
|
|
91
|
+
const output = {
|
|
92
|
+
query,
|
|
93
|
+
data: {
|
|
94
|
+
workingFiles: topFiles.map(f => ({ path: f.path })),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
logInputOutput("selectRelevantSources", "output", output.data);
|
|
98
|
+
return output;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// File: src/modules/semanticAnalysisStep.ts
|
|
2
|
+
import { logInputOutput } from "../utils/promptLogHelper.js";
|
|
3
|
+
import { generate } from "../lib/generate.js";
|
|
4
|
+
import { cleanupModule } from "../pipeline/modules/cleanupModule.js";
|
|
5
|
+
export const semanticAnalysisStep = {
|
|
6
|
+
name: "semanticAnalysis",
|
|
7
|
+
description: "Query-centric semantic analysis: per-file relevance and optional combined insights.",
|
|
8
|
+
groups: ["analysis"],
|
|
9
|
+
run: async (input) => {
|
|
10
|
+
var _a, _b;
|
|
11
|
+
const context = input.context;
|
|
12
|
+
if (!context)
|
|
13
|
+
throw new Error("[semanticAnalysisStep] No context provided");
|
|
14
|
+
const workingFiles = context.workingFiles ?? [];
|
|
15
|
+
if (!workingFiles.length) {
|
|
16
|
+
const notes = "[semanticAnalysisStep] No working files loaded; skipping semantic analysis";
|
|
17
|
+
logInputOutput("semanticAnalysisStep", "output", { notes });
|
|
18
|
+
return { query: input.query, data: { notes }, context };
|
|
19
|
+
}
|
|
20
|
+
context.analysis || (context.analysis = {});
|
|
21
|
+
(_a = context.analysis).fileAnalysis || (_a.fileAnalysis = {});
|
|
22
|
+
(_b = context.analysis).combinedAnalysis || (_b.combinedAnalysis = {});
|
|
23
|
+
// Get the files relevant to this query
|
|
24
|
+
const focusFiles = new Set(context.analysis.focus?.relevantFiles ?? []);
|
|
25
|
+
const filesToAnalyze = workingFiles.filter(f => focusFiles.has(f.path) || focusFiles.size === 0);
|
|
26
|
+
// ----------------------------
|
|
27
|
+
// 1️⃣ Per-file relevance analysis
|
|
28
|
+
// ----------------------------
|
|
29
|
+
for (const file of filesToAnalyze) {
|
|
30
|
+
const filePath = file.path;
|
|
31
|
+
if (context.analysis.fileAnalysis[filePath])
|
|
32
|
+
continue;
|
|
33
|
+
const fileAnalysis = await analyzeFile(file, input.query);
|
|
34
|
+
context.analysis.fileAnalysis[filePath] = fileAnalysis;
|
|
35
|
+
logInputOutput("semanticAnalysisStep - per-file", "output", { file: filePath, analysis: fileAnalysis });
|
|
36
|
+
}
|
|
37
|
+
// ----------------------------
|
|
38
|
+
// 2️⃣ Cross-file combined analysis
|
|
39
|
+
// Only use files marked as relevant
|
|
40
|
+
// Skip if ≤ 2 relevant files
|
|
41
|
+
// ----------------------------
|
|
42
|
+
const relevantFileAnalysis = Object.fromEntries(Object.entries(context.analysis.fileAnalysis)
|
|
43
|
+
.filter(([_, fa]) => fa.intent && fa.intent !== "irrelevant"));
|
|
44
|
+
let combinedAnalysis = { sharedPatterns: [], architectureSummary: "[skipped]", hotspots: [] };
|
|
45
|
+
if (Object.keys(relevantFileAnalysis).length > 2) {
|
|
46
|
+
combinedAnalysis = await analyzeCombined(relevantFileAnalysis, input.query);
|
|
47
|
+
context.analysis.combinedAnalysis = combinedAnalysis;
|
|
48
|
+
logInputOutput("semanticAnalysisStep - combined", "output", combinedAnalysis);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
context.analysis.combinedAnalysis = combinedAnalysis;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
query: input.query,
|
|
55
|
+
data: { notes: "Query-centric semantic analysis completed" },
|
|
56
|
+
context,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
async function analyzeFile(file, query) {
|
|
61
|
+
const prompt = `
|
|
62
|
+
You are given a user query:
|
|
63
|
+
"${query}"
|
|
64
|
+
|
|
65
|
+
Task:
|
|
66
|
+
- Determine if an analysis of this file is relevant to answering the query.
|
|
67
|
+
- If relevant, describe in 1-2 sentences how it contributes to answering the query.
|
|
68
|
+
- If irrelevant, set intent to "irrelevant".
|
|
69
|
+
- Optionally, include any risks that may affect the query's outcome.
|
|
70
|
+
|
|
71
|
+
Return STRICT JSON:
|
|
72
|
+
{
|
|
73
|
+
"intent": string,
|
|
74
|
+
"risks"?: string[]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
File path: ${file.path}
|
|
78
|
+
Code snippet: ${file.code ?? "[no code]"}
|
|
79
|
+
`;
|
|
80
|
+
try {
|
|
81
|
+
const ai = await generate({ query: file.path, content: prompt });
|
|
82
|
+
const cleaned = await cleanupModule.run({ query, content: ai.data });
|
|
83
|
+
let data;
|
|
84
|
+
// Attempt to use structured data if available
|
|
85
|
+
if (typeof cleaned.data === "object" && cleaned.data) {
|
|
86
|
+
data = cleaned.data;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
try {
|
|
90
|
+
data = JSON.parse(String(cleaned.content ?? "{}"));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
console.warn(`[semanticAnalysisStep] Non-JSON output for ${file.path}, defaulting to irrelevant`);
|
|
94
|
+
data = { intent: "irrelevant", risks: [] };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
intent: data.intent ?? "irrelevant",
|
|
99
|
+
risks: Array.isArray(data.risks) ? data.risks : []
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
console.warn(`[semanticAnalysisStep] Failed to analyze file ${file.path}:`, err);
|
|
104
|
+
return { intent: "irrelevant", risks: [] };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/* -----------------------------------------
|
|
108
|
+
Helper: analyzeCombined
|
|
109
|
+
Only for relevant files, concise
|
|
110
|
+
-------------------------------------------- */
|
|
111
|
+
async function analyzeCombined(fileAnalysis, query) {
|
|
112
|
+
const prompt = `
|
|
113
|
+
You are given per-file analysis relevant to the query:
|
|
114
|
+
${JSON.stringify(fileAnalysis, null, 2)}
|
|
115
|
+
|
|
116
|
+
Task:
|
|
117
|
+
- Identify shared patterns across these files in the context of the query
|
|
118
|
+
- Summarize high-level architecture relevant to answering the query
|
|
119
|
+
- List any hotspots or risks that could impact the query's outcome
|
|
120
|
+
|
|
121
|
+
Return STRICT JSON:
|
|
122
|
+
{
|
|
123
|
+
"sharedPatterns"?: string[],
|
|
124
|
+
"architectureSummary"?: string,
|
|
125
|
+
"hotspots"?: string[]
|
|
126
|
+
}
|
|
127
|
+
`;
|
|
128
|
+
try {
|
|
129
|
+
const ai = await generate({ query: "cross-file analysis", content: prompt });
|
|
130
|
+
const cleaned = await cleanupModule.run({ query, content: ai.data });
|
|
131
|
+
const data = typeof cleaned.data === "object" && cleaned.data
|
|
132
|
+
? cleaned.data
|
|
133
|
+
: JSON.parse(String(cleaned.content ?? "{}"));
|
|
134
|
+
return {
|
|
135
|
+
sharedPatterns: data.sharedPatterns ?? [],
|
|
136
|
+
architectureSummary: data.architectureSummary ?? "[unparsed]",
|
|
137
|
+
hotspots: data.hotspots ?? []
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
console.warn("[semanticAnalysisStep] Failed to parse combined analysis:", err);
|
|
142
|
+
return { sharedPatterns: [], architectureSummary: "[unparsed]", hotspots: [] };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { logInputOutput } from "../utils/promptLogHelper.js";
|
|
2
|
+
export const structuralAnalysisStep = {
|
|
3
|
+
name: "structuralAnalysis",
|
|
4
|
+
description: "Derive structural characteristics from workingFiles using existing metadata only. " +
|
|
5
|
+
"Produces a durable, machine-usable summary of file shape and relationships.",
|
|
6
|
+
groups: ["analysis"],
|
|
7
|
+
run: async (input) => {
|
|
8
|
+
const ctx = input.context;
|
|
9
|
+
if (!ctx) {
|
|
10
|
+
throw new Error("[structuralAnalysis] StructuredContext is required but was not provided.");
|
|
11
|
+
}
|
|
12
|
+
const workingFiles = ctx.workingFiles;
|
|
13
|
+
if (!Array.isArray(workingFiles) || workingFiles.length === 0) {
|
|
14
|
+
const output = {
|
|
15
|
+
query: input.query,
|
|
16
|
+
data: {
|
|
17
|
+
notes: "No workingFiles present; structural analysis skipped."
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
logInputOutput("structuralAnalysis", "output", output.data);
|
|
21
|
+
return output;
|
|
22
|
+
}
|
|
23
|
+
const files = workingFiles.map((file) => ({
|
|
24
|
+
path: file.path,
|
|
25
|
+
imports: file.imports ?? [],
|
|
26
|
+
exports: file.exports ?? [],
|
|
27
|
+
functions: file.functions?.length ?? 0,
|
|
28
|
+
classes: file.classes?.length ?? 0,
|
|
29
|
+
}));
|
|
30
|
+
// 🧠 Persist durable structural understanding
|
|
31
|
+
ctx.analysis ?? (ctx.analysis = {});
|
|
32
|
+
ctx.analysis.structure = {
|
|
33
|
+
files,
|
|
34
|
+
};
|
|
35
|
+
const output = {
|
|
36
|
+
query: input.query,
|
|
37
|
+
data: {
|
|
38
|
+
fileCount: files.length,
|
|
39
|
+
files,
|
|
40
|
+
notes: "Structural characteristics derived from loaded workingFiles.",
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
logInputOutput("structuralAnalysis", "output", output.data);
|
|
44
|
+
return output;
|
|
45
|
+
}
|
|
46
|
+
};
|