Files
yidaima/RuoYi-Vue/docs/superpowers/plans/2026-07-07-code-analysis-core-selection.md

21 KiB
Raw Permalink Blame History

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 finishSummary with 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 filePriority scoring

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 buildCodeAnalysisPrompt wording

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.