scai 0.1.117 → 0.1.118

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 (95) hide show
  1. package/dist/agents/MainAgent.js +255 -0
  2. package/dist/agents/contextReviewStep.js +104 -0
  3. package/dist/agents/finalPlanGenStep.js +123 -0
  4. package/dist/agents/infoPlanGenStep.js +126 -0
  5. package/dist/agents/planGeneratorStep.js +118 -0
  6. package/dist/agents/planResolverStep.js +95 -0
  7. package/dist/agents/planTargetFilesStep.js +48 -0
  8. package/dist/agents/preFileSearchCheckStep.js +95 -0
  9. package/dist/agents/selectRelevantSourcesStep.js +100 -0
  10. package/dist/agents/semanticAnalysisStep.js +144 -0
  11. package/dist/agents/structuralAnalysisStep.js +46 -0
  12. package/dist/agents/transformPlanGenStep.js +107 -0
  13. package/dist/agents/understandIntentStep.js +72 -0
  14. package/dist/agents/validationAnalysisStep.js +87 -0
  15. package/dist/commands/AskCmd.js +47 -116
  16. package/dist/commands/ChangeLogUpdateCmd.js +11 -5
  17. package/dist/commands/CommitSuggesterCmd.js +50 -75
  18. package/dist/commands/DaemonCmd.js +119 -29
  19. package/dist/commands/IndexCmd.js +41 -24
  20. package/dist/commands/InspectCmd.js +0 -1
  21. package/dist/commands/ReadlineSingleton.js +18 -0
  22. package/dist/commands/ResetDbCmd.js +20 -21
  23. package/dist/commands/ReviewCmd.js +89 -54
  24. package/dist/commands/SummaryCmd.js +12 -18
  25. package/dist/commands/WorkflowCmd.js +41 -0
  26. package/dist/commands/factory.js +254 -0
  27. package/dist/config.js +67 -15
  28. package/dist/constants.js +20 -4
  29. package/dist/context.js +10 -11
  30. package/dist/daemon/daemonQueues.js +63 -0
  31. package/dist/daemon/daemonWorker.js +40 -63
  32. package/dist/daemon/generateSummaries.js +58 -0
  33. package/dist/daemon/runFolderCapsuleBatch.js +247 -0
  34. package/dist/daemon/runIndexingBatch.js +147 -0
  35. package/dist/daemon/runKgBatch.js +104 -0
  36. package/dist/db/fileIndex.js +168 -63
  37. package/dist/db/functionExtractors/extractFromJava.js +210 -6
  38. package/dist/db/functionExtractors/extractFromJs.js +173 -214
  39. package/dist/db/functionExtractors/extractFromTs.js +159 -160
  40. package/dist/db/functionExtractors/index.js +7 -5
  41. package/dist/db/schema.js +55 -20
  42. package/dist/db/sqlTemplates.js +50 -19
  43. package/dist/fileRules/builtins.js +31 -14
  44. package/dist/fileRules/codeAllowedExtensions.js +4 -0
  45. package/dist/fileRules/fileExceptions.js +0 -13
  46. package/dist/fileRules/ignoredExtensions.js +10 -0
  47. package/dist/index.js +128 -325
  48. package/dist/lib/generate.js +37 -14
  49. package/dist/lib/generateFolderCapsules.js +109 -0
  50. package/dist/lib/spinner.js +12 -5
  51. package/dist/modelSetup.js +0 -10
  52. package/dist/pipeline/modules/changeLogModule.js +16 -19
  53. package/dist/pipeline/modules/chunkManagerModule.js +24 -0
  54. package/dist/pipeline/modules/cleanupModule.js +96 -91
  55. package/dist/pipeline/modules/codeTransformModule.js +208 -0
  56. package/dist/pipeline/modules/commentModule.js +20 -11
  57. package/dist/pipeline/modules/commitSuggesterModule.js +36 -14
  58. package/dist/pipeline/modules/contextReviewModule.js +52 -0
  59. package/dist/pipeline/modules/fileReaderModule.js +72 -0
  60. package/dist/pipeline/modules/fileSearchModule.js +136 -0
  61. package/dist/pipeline/modules/finalAnswerModule.js +53 -0
  62. package/dist/pipeline/modules/gatherInfoModule.js +176 -0
  63. package/dist/pipeline/modules/generateTestsModule.js +63 -54
  64. package/dist/pipeline/modules/kgModule.js +26 -11
  65. package/dist/pipeline/modules/preserveCodeModule.js +91 -49
  66. package/dist/pipeline/modules/refactorModule.js +19 -7
  67. package/dist/pipeline/modules/repairTestsModule.js +44 -36
  68. package/dist/pipeline/modules/reviewModule.js +23 -13
  69. package/dist/pipeline/modules/summaryModule.js +27 -35
  70. package/dist/pipeline/modules/writeFileModule.js +86 -0
  71. package/dist/pipeline/registry/moduleRegistry.js +38 -93
  72. package/dist/pipeline/runModulePipeline.js +22 -19
  73. package/dist/scripts/dbcheck.js +143 -228
  74. package/dist/utils/buildContextualPrompt.js +245 -172
  75. package/dist/utils/debugContext.js +24 -0
  76. package/dist/utils/fileTree.js +16 -6
  77. package/dist/utils/loadRelevantFolderCapsules.js +64 -0
  78. package/dist/utils/log.js +2 -0
  79. package/dist/utils/normalizeData.js +23 -0
  80. package/dist/utils/planActions.js +60 -0
  81. package/dist/utils/promptBuilderHelper.js +67 -0
  82. package/dist/utils/promptLogHelper.js +52 -0
  83. package/dist/utils/sanitizeQuery.js +20 -8
  84. package/dist/utils/sleep.js +3 -0
  85. package/dist/utils/splitCodeIntoChunk.js +65 -32
  86. package/dist/utils/vscode.js +49 -0
  87. package/dist/workflow/workflowResolver.js +14 -0
  88. package/dist/workflow/workflowRunner.js +103 -0
  89. package/package.json +6 -5
  90. package/dist/agent/agentManager.js +0 -39
  91. package/dist/agent/workflowManager.js +0 -95
  92. package/dist/commands/ModulePipelineCmd.js +0 -31
  93. package/dist/daemon/daemonBatch.js +0 -186
  94. package/dist/fileRules/scoreFiles.js +0 -71
  95. 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
+ };