# 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: ```java import static org.junit.Assert.assertFalse; ``` - [ ] **Step 2: Write a failing test for scoring and compact budget** Add this test after `shouldCapTotalPromptContextAndWarnWhenBudgetExceeded()`: ```java @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.>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: ```java 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: ```java 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: ```bash 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: ```java private static final int LARGE_FILE_LIMIT = 100 * 1024; private static final int TOTAL_CONTEXT_LIMIT = 120 * 1024; ``` with: ```java 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: ```java 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: ```java 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 `finishSummary` with selected-file assembly** Replace the whole `finishSummary(CodeAnalysisSourceSummary summary)` method with: ```java private void finishSummary(CodeAnalysisSourceSummary summary) { boolean totalTruncated = false; List candidates = new ArrayList( summary.getIncludedFiles()); Collections.sort(candidates, new Comparator() { @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 selected = new ArrayList(); 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`: ```java private String buildPromptContext(List selected, List 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 `filePriority` scoring** Replace the existing `filePriority` method with: ```java 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: ```bash 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: ```java assertTrue(prompt.contains("不要编造未出现的模块")); ``` add: ```java assertTrue(prompt.contains("精选核心源码")); assertTrue(prompt.contains("只能依据已展开源码和文件清单")); assertTrue(prompt.contains("无法从精选源码确认")); ``` - [ ] **Step 2: Run the focused prompt test and verify it fails** Run: ```bash mvn -pl ruoyi-generator -Dtest=AiGenerateServiceImplTest#generateCodeAnalysisPromptRequiresFixedSectionsAndSourceGrounding test ``` Expected: FAIL because the current prompt does not contain the curated-source wording. - [ ] **Step 3: Update `buildCodeAnalysisPrompt` wording** In `AiGenerateServiceImpl.buildCodeAnalysisPrompt`, replace the first prompt sentence: ```java prompt.append("你是资深 Java/Vue 毕设项目代码解读助手。请只依据下方源码快照和项目元信息进行解读,不要编造未出现的模块、接口、表或流程。\n"); ``` with: ```java prompt.append("你是资深 Java/Vue 毕设项目代码解读助手。下方源码快照是系统精选核心源码子集,请只依据已展开源码和文件清单进行解读,不要编造未出现的模块、接口、表或流程;如果细节无法从精选源码确认,请明确写出无法从精选源码确认。\n"); ``` Replace the source snapshot intro: ```java prompt.append("源码快照如下,每个文件以模板类型和路径标识。请引用真实类名、方法名、组件名、表名或字段名作为依据。\n"); ``` with: ```java prompt.append("精选核心源码如下,每个文件以模板类型和路径标识,并包含已选文件与跳过文件清单。请引用真实类名、方法名、组件名、表名或字段名作为依据,不要根据未展开文件推断实现细节。\n"); ``` - [ ] **Step 4: Run the focused prompt test** Run: ```bash mvn -pl ruoyi-generator -Dtest=AiGenerateServiceImplTest#generateCodeAnalysisPromptRequiresFixedSectionsAndSourceGrounding test ``` Expected: PASS. - [ ] **Step 5: Run the two affected test classes** Run: ```bash 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: ```bash 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: ```bash mvn -pl ruoyi-generator -Dtest=ProjectCodeAnalysisServiceTest,AiGenerateServiceImplTest test ``` Expected: PASS. - [ ] **Step 3: Stage only the implementation files** Run: ```bash 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: ```bash git commit -m "feat: select core source for code analysis" ``` Expected: Commit succeeds and includes only the implementation files and plan file.