21 KiB
Code Analysis Core Source Selection Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Make code analysis send a compact, curated set of core Java, Vue, SQL, mapper, and config files to the AI instead of a broad source dump.
Architecture: Keep the implementation inside the existing ProjectCodeAnalysisService boundary. The service still reads the preview source tree, but it now scores candidates, truncates each expanded file to a small per-file budget, assembles a selected-file manifest, records skipped low-value or budget-excluded files, and caps expanded source at 60KB. AiGenerateServiceImpl only needs prompt wording changes so the AI treats the source as a curated subset.
Tech Stack: Java 8, Spring Boot 2.5, JUnit 4, Mockito, Maven.
Current status: Code-analysis scoped implementation and focused tests are complete. The full
AiGenerateServiceImplTest class is currently blocked by unrelated database-generation failures
in the existing working tree, so the implementation has not been staged or committed.
Task 1: Add Failing Tests For Core Selection
Files:
-
Modify:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/ProjectCodeAnalysisServiceTest.java -
Test:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/ProjectCodeAnalysisServiceTest.java -
Step 1: Add the missing assertion import
Add this import with the other static JUnit imports:
import static org.junit.Assert.assertFalse;
- Step 2: Write a failing test for scoring and compact budget
Add this test after shouldCapTotalPromptContextAndWarnWhenBudgetExceeded():
@Test
public void shouldSelectCoreFilesBeforeLowValueFilesAndKeepContextCompact()
{
when(frontProjectPreviewService.getStructure(eq(1L), eq(10L), eq("backend")))
.thenReturn(nodes(
node("backend:UserController.java", "UserController.java",
"src/main/java/com/example/user/UserController.java", "file"),
node("backend:UserServiceImpl.java", "UserServiceImpl.java",
"src/main/java/com/example/user/UserServiceImpl.java", "file"),
node("backend:UserMapper.java", "UserMapper.java",
"src/main/java/com/example/user/UserMapper.java", "file"),
node("backend:UserMapper.xml", "UserMapper.xml",
"src/main/resources/mapper/UserMapper.xml", "file"),
node("backend:User.java", "User.java",
"src/main/java/com/example/domain/User.java", "file"),
node("backend:AuditUtil.java", "AuditUtil.java",
"src/main/java/com/example/common/AuditUtil.java", "file"),
node("backend:UserServiceTest.java", "UserServiceTest.java",
"src/test/java/com/example/user/UserServiceTest.java", "file")));
when(frontProjectPreviewService.getStructure(eq(1L), eq(10L), eq("frontend")))
.thenReturn(nodes(
node("frontend:index.vue", "index.vue", "src/views/user/index.vue", "file"),
node("frontend:user.js", "user.js", "src/api/user.js", "file"),
node("frontend:router.js", "router.js", "src/router/index.js", "file"),
node("frontend:index.scss", "index.scss", "src/styles/index.scss", "file")));
when(frontProjectPreviewService.getStructure(eq(1L), eq(10L), eq("admin_frontend")))
.thenReturn(Collections.<Map<String, Object>>emptyList());
when(frontProjectPreviewService.getStructure(eq(1L), eq(10L), eq("sql")))
.thenReturn(nodes(node("sql:user.sql", "user.sql", "user.sql", "file")));
when(frontProjectPreviewService.getFileContent(eq(1L), eq(10L), any(PreviewFileRequest.class)))
.thenAnswer(invocation ->
{
PreviewFileRequest request = invocation.getArgument(2);
return content("// " + request.getCategory() + "\n" + repeat("core source line\n", 400));
});
CodeAnalysisSourceSummary summary = service.buildSourceSummary(1L, 10L);
String context = summary.getPromptContext();
assertTrue(context.length() <= 65 * 1024);
assertTrue(context.contains("Selected core source files:"));
assertTrue(context.contains("UserController.java"));
assertTrue(context.contains("UserServiceImpl.java"));
assertTrue(context.contains("UserMapper.xml"));
assertTrue(context.contains("user.sql"));
assertTrue(context.contains("src/views/user/index.vue"));
assertFalse(context.contains("AuditUtil.java\n//"));
assertFalse(context.contains("index.scss\n//"));
assertTrue(summary.getWarnings().stream().anyMatch(value -> value.contains("60KB")));
assertTrue(summary.getSkippedFiles().stream().anyMatch(file -> file.getPath().contains("index.scss")));
}
- Step 3: Update the existing budget test expectation
In shouldCapTotalPromptContextAndWarnWhenBudgetExceeded(), replace the final assertions with:
assertTrue(summary.getPromptContext().length() <= 65 * 1024);
assertTrue(summary.getWarnings().stream().anyMatch(value -> value.contains("60KB")));
- Step 4: Update the existing large-file test expectation
In shouldTrimLargeFilesAndKeepSummaryUsable(), replace the final assertions with:
assertEquals(Integer.valueOf(1), summary.getAnalyzedFileCount());
assertTrue(summary.getIncludedFiles().get(0).getContent().length() <= 21 * 1024);
assertTrue(summary.getWarnings().stream().anyMatch(value -> value.contains("20KB")));
assertTrue(summary.getEstimatedTokens().intValue() > 0);
- Step 5: Run the focused test and verify it fails
Run:
mvn -pl ruoyi-generator -Dtest=ProjectCodeAnalysisServiceTest test
Expected: FAIL. The new test should fail because the current service still uses a 120KB broad prompt and does not emit the selected-core manifest or 60KB warning.
Task 2: Implement Deterministic Core File Selection
Files:
-
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/ProjectCodeAnalysisService.java -
Test:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/ProjectCodeAnalysisServiceTest.java -
Step 1: Replace the budget constants
Replace:
private static final int LARGE_FILE_LIMIT = 100 * 1024;
private static final int TOTAL_CONTEXT_LIMIT = 120 * 1024;
with:
private static final int PER_FILE_CONTEXT_LIMIT = 20 * 1024;
private static final int TOTAL_CONTEXT_LIMIT = 60 * 1024;
private static final int SKIPPED_MANIFEST_LIMIT = 40;
- Step 2: Truncate each candidate file to the per-file budget
In collectFile, replace the large-file truncation block:
if (content.length() > LARGE_FILE_LIMIT)
{
content = content.substring(0, LARGE_FILE_LIMIT)
+ "\n\n[文件超过 100KB,代码解读仅保留前 100KB 内容]";
truncated = true;
summary.getWarnings().add(path + " 超过 100KB,已裁剪后参与分析");
}
with:
if (content.length() > PER_FILE_CONTEXT_LIMIT)
{
content = content.substring(0, PER_FILE_CONTEXT_LIMIT)
+ "\n\n[文件超过 20KB,代码解读仅保留前 20KB 核心内容]";
truncated = true;
summary.getWarnings().add(path + " 超过 20KB,已裁剪后参与核心代码分析");
}
- Step 3: Replace
finishSummarywith selected-file assembly
Replace the whole finishSummary(CodeAnalysisSourceSummary summary) method with:
private void finishSummary(CodeAnalysisSourceSummary summary)
{
boolean totalTruncated = false;
List<CodeAnalysisSourceSummary.SourceFile> candidates = new ArrayList<CodeAnalysisSourceSummary.SourceFile>(
summary.getIncludedFiles());
Collections.sort(candidates, new Comparator<CodeAnalysisSourceSummary.SourceFile>()
{
@Override
public int compare(CodeAnalysisSourceSummary.SourceFile left, CodeAnalysisSourceSummary.SourceFile right)
{
int priorityCompare = Integer.valueOf(filePriority(left)).compareTo(Integer.valueOf(filePriority(right)));
if (priorityCompare != 0)
{
return priorityCompare;
}
return StringUtils.defaultString(left == null ? null : left.getPath())
.compareTo(StringUtils.defaultString(right == null ? null : right.getPath()));
}
});
List<CodeAnalysisSourceSummary.SourceFile> selected = new ArrayList<CodeAnalysisSourceSummary.SourceFile>();
StringBuilder source = new StringBuilder();
for (CodeAnalysisSourceSummary.SourceFile file : candidates)
{
if (isLowValueFile(file))
{
addSkipped(summary, file.getTemplateType(), file.getPath(), "非核心代码文件");
continue;
}
String block = sourceBlock(file);
if (source.length() + block.length() > TOTAL_CONTEXT_LIMIT)
{
addSkipped(summary, file.getTemplateType(), file.getPath(), "核心源码预算已满,未展开内容");
totalTruncated = true;
continue;
}
selected.add(file);
source.append(block);
}
if (selected.isEmpty() && !candidates.isEmpty())
{
CodeAnalysisSourceSummary.SourceFile fallback = candidates.get(0);
String block = sourceBlock(fallback);
if (block.length() > TOTAL_CONTEXT_LIMIT)
{
block = block.substring(0, TOTAL_CONTEXT_LIMIT)
+ "\n\n[核心源码预算已满,仅保留首个可分析文件的前 60KB 内容]";
fallback.setTruncated(Boolean.TRUE);
totalTruncated = true;
}
selected.add(fallback);
source.append(block);
}
if (totalTruncated)
{
summary.getWarnings().add("核心源码上下文超过 60KB,已优先保留 SQL、Controller、Service、Mapper、实体和核心页面");
}
String promptContext = buildPromptContext(selected, summary.getSkippedFiles(), source);
summary.setIncludedFiles(selected);
summary.setAnalyzedFileCount(selected.size());
summary.setSkippedFileCount(summary.getSkippedFiles().size());
summary.setCharCount(promptContext.length());
summary.setEstimatedTokens(Math.max(0, (promptContext.length() + 3) / 4));
summary.setPromptContext(promptContext.trim());
}
- Step 4: Add helper methods for manifest and low-value filtering
Add these methods below finishSummary:
private String buildPromptContext(List<CodeAnalysisSourceSummary.SourceFile> selected,
List<CodeAnalysisSourceSummary.SkippedFile> skippedFiles, StringBuilder source)
{
StringBuilder prompt = new StringBuilder();
prompt.append("Selected core source files:\n");
for (CodeAnalysisSourceSummary.SourceFile file : selected)
{
prompt.append("- ").append(StringUtils.defaultString(file.getTemplateType()))
.append(" / ").append(StringUtils.defaultString(file.getPath()))
.append(" (").append(fileRole(file)).append(file.getTruncated() ? ", truncated" : ", expanded")
.append(")\n");
}
if (!skippedFiles.isEmpty())
{
prompt.append("\nSkipped or summarized files:\n");
int count = 0;
for (CodeAnalysisSourceSummary.SkippedFile file : skippedFiles)
{
if (count >= SKIPPED_MANIFEST_LIMIT)
{
prompt.append("- ... ").append(skippedFiles.size() - count).append(" more files omitted from manifest\n");
break;
}
prompt.append("- ").append(StringUtils.defaultString(file.getTemplateType()))
.append(" / ").append(StringUtils.defaultString(file.getPath()))
.append(" (").append(StringUtils.defaultString(file.getReason())).append(")\n");
count++;
}
}
prompt.append(source);
return prompt.toString();
}
private String sourceBlock(CodeAnalysisSourceSummary.SourceFile file)
{
return "\n\n## " + StringUtils.defaultString(file.getTemplateType()) + " / "
+ StringUtils.defaultString(file.getPath()) + "\n"
+ StringUtils.defaultString(file.getContent());
}
private boolean isLowValueFile(CodeAnalysisSourceSummary.SourceFile file)
{
String path = StringUtils.defaultString(file == null ? null : file.getPath()).toLowerCase(Locale.ROOT);
String name = fileName(path);
return path.contains("/test/") || name.endsWith("test.java") || name.contains(".test.")
|| path.contains("/tests/") || path.contains("/demo/") || path.contains("/mockups/")
|| path.contains("/audits/") || path.endsWith(".scss") || path.endsWith(".css");
}
private String fileRole(CodeAnalysisSourceSummary.SourceFile file)
{
String path = StringUtils.defaultString(file == null ? null : file.getPath()).toLowerCase(Locale.ROOT);
String type = StringUtils.defaultString(file == null ? null : file.getTemplateType()).toLowerCase(Locale.ROOT);
if ("sql".equals(type) || path.endsWith(".sql"))
{
return "table schema";
}
if (path.contains("controller"))
{
return "controller";
}
if (path.contains("service"))
{
return "service";
}
if (path.contains("mapper"))
{
return "mapper";
}
if (path.contains("/domain/") || path.contains("/entity/") || path.contains("/model/")
|| path.contains("/dto/"))
{
return "domain model";
}
if (path.contains("/views/") || path.endsWith(".vue"))
{
return "vue page";
}
if (path.contains("/api/"))
{
return "frontend api";
}
if (path.contains("router"))
{
return "router";
}
return "supporting file";
}
- Step 5: Replace
filePriorityscoring
Replace the existing filePriority method with:
private int filePriority(CodeAnalysisSourceSummary.SourceFile file)
{
String path = StringUtils.defaultString(file == null ? null : file.getPath()).toLowerCase(Locale.ROOT);
String type = StringUtils.defaultString(file == null ? null : file.getTemplateType()).toLowerCase(Locale.ROOT);
if ("sql".equals(type) || path.endsWith(".sql"))
{
return 10;
}
if (path.contains("controller"))
{
return 20;
}
if (path.contains("serviceimpl") || path.contains("service"))
{
return 30;
}
if (path.contains("mapper"))
{
return 40;
}
if (path.contains("/domain/") || path.contains("/entity/") || path.contains("/model/")
|| path.contains("/dto/"))
{
return 50;
}
if (path.contains("/views/") || path.endsWith(".vue"))
{
return 60;
}
if (path.contains("/api/") || path.contains("router"))
{
return 70;
}
if (path.endsWith("application.yml") || path.endsWith("application.yaml")
|| path.endsWith("pom.xml") || path.endsWith("package.json"))
{
return 80;
}
if (path.endsWith(".java") || path.endsWith(".xml") || path.endsWith(".js") || path.endsWith(".ts"))
{
return 100;
}
return 200;
}
- Step 6: Run the focused tests
Run:
mvn -pl ruoyi-generator -Dtest=ProjectCodeAnalysisServiceTest test
Expected: PASS. All ProjectCodeAnalysisServiceTest tests pass, including the new core selection test.
Task 3: Make AI Prompt Explicit About Curated Source
Files:
-
Modify:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java -
Test:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java -
Step 1: Write failing assertions for prompt wording
In generateCodeAnalysisPromptRequiresFixedSectionsAndSourceGrounding(), after:
assertTrue(prompt.contains("不要编造未出现的模块"));
add:
assertTrue(prompt.contains("精选核心源码"));
assertTrue(prompt.contains("只能依据已展开源码和文件清单"));
assertTrue(prompt.contains("无法从精选源码确认"));
- Step 2: Run the focused prompt test and verify it fails
Run:
mvn -pl ruoyi-generator -Dtest=AiGenerateServiceImplTest#generateCodeAnalysisPromptRequiresFixedSectionsAndSourceGrounding test
Expected: FAIL because the current prompt does not contain the curated-source wording.
- Step 3: Update
buildCodeAnalysisPromptwording
In AiGenerateServiceImpl.buildCodeAnalysisPrompt, replace the first prompt sentence:
prompt.append("你是资深 Java/Vue 毕设项目代码解读助手。请只依据下方源码快照和项目元信息进行解读,不要编造未出现的模块、接口、表或流程。\n");
with:
prompt.append("你是资深 Java/Vue 毕设项目代码解读助手。下方源码快照是系统精选核心源码子集,请只依据已展开源码和文件清单进行解读,不要编造未出现的模块、接口、表或流程;如果细节无法从精选源码确认,请明确写出无法从精选源码确认。\n");
Replace the source snapshot intro:
prompt.append("源码快照如下,每个文件以模板类型和路径标识。请引用真实类名、方法名、组件名、表名或字段名作为依据。\n");
with:
prompt.append("精选核心源码如下,每个文件以模板类型和路径标识,并包含已选文件与跳过文件清单。请引用真实类名、方法名、组件名、表名或字段名作为依据,不要根据未展开文件推断实现细节。\n");
- Step 4: Run the focused prompt test
Run:
mvn -pl ruoyi-generator -Dtest=AiGenerateServiceImplTest#generateCodeAnalysisPromptRequiresFixedSectionsAndSourceGrounding test
Expected: PASS.
- Step 5: Run the two affected test classes
Run:
mvn -pl ruoyi-generator -Dtest=ProjectCodeAnalysisServiceTest,AiGenerateServiceImplTest test
Expected: PASS. Both affected test classes pass.
Task 4: Final Verification And Commit
Files:
-
Verify:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/ProjectCodeAnalysisService.java -
Verify:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java -
Verify:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/ProjectCodeAnalysisServiceTest.java -
Verify:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java -
Step 1: Check the final diff
Run:
git diff -- ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/ProjectCodeAnalysisService.java ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/ProjectCodeAnalysisServiceTest.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java
Expected: Diff only contains the core source selection tests, service selection logic, and prompt wording changes.
- Step 2: Run verification
Run:
mvn -pl ruoyi-generator -Dtest=ProjectCodeAnalysisServiceTest,AiGenerateServiceImplTest test
Expected: PASS.
- Step 3: Stage only the implementation files
Run:
git add -- ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/ProjectCodeAnalysisService.java ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/ProjectCodeAnalysisServiceTest.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java docs/superpowers/plans/2026-07-07-code-analysis-core-selection.md
- Step 4: Commit
Run:
git commit -m "feat: select core source for code analysis"
Expected: Commit succeeds and includes only the implementation files and plan file.