All files / src issue-verify.service.ts

92.04% Statements 81/88
83.6% Branches 51/61
90.9% Functions 10/11
91.95% Lines 80/87

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308                                                1x 1x   1x                                           17x 17x                             17x 1x     16x 3x         16x 16x             16x 16x 16x 1x     1x 1x       15x 1x     1x 1x     14x 14x 1x     1x           1x     13x 13x       16x     13x 3x       13x 3x         16x     13x 13x     16x 13x 13x                       16x 16x 16x 3x     16x                           13x   13x 13x                         13x 11x 10x 1x 1x       11x 9x         9x 4x 1x   4x 5x 3x 1x     2x 1x       9x   2x     2x 1x 1x 1x     1x   2x                       13x 13x 13x     13x                                                       13x 13x 1x     13x                                       13x      
import {
  LlmProxyService,
  type LLMMode,
  type VerboseLevel,
  shouldLog,
  type LlmJsonPutSchema,
  LlmJsonPut,
  parallel,
} from "@spaceflow/core";
import {
  ReviewIssue,
  ReviewSpec,
  ReviewRule,
  ReviewSpecService,
  FileContentsMap,
  FileContentLine,
} from "./review-spec";
 
interface VerifyResult {
  fixed: boolean;
  valid: boolean;
  reason: string;
}
 
const TRUE = "true";
const FALSE = "false";
 
const VERIFY_SCHEMA: LlmJsonPutSchema = {
  type: "object",
  properties: {
    fixed: {
      type: "boolean",
      description: "问题是否已被修复",
    },
    valid: {
      type: "boolean",
      description: "问题是否有效,有效的条件就是你需要看看代码是否符合规范",
    },
    reason: {
      type: "string",
      description: "判断依据,说明为什么认为问题已修复或仍存在",
    },
  },
  required: ["fixed", "valid", "reason"],
  additionalProperties: false,
};
 
export class IssueVerifyService {
  constructor(
    protected readonly llmProxyService: LlmProxyService,
    protected readonly reviewSpecService: ReviewSpecService,
  ) {}
 
  /**
   * 验证历史 issues 是否已被修复
   * 按并发数批量验证,每批并行调用 LLM
   */
  async verifyIssueFixes(
    existingIssues: ReviewIssue[],
    fileContents: FileContentsMap,
    specs: ReviewSpec[],
    llmMode: LLMMode,
    verbose?: VerboseLevel,
    concurrency: number = 10,
  ): Promise<ReviewIssue[]> {
    if (existingIssues.length === 0) {
      return [];
    }
 
    if (shouldLog(verbose, 1)) {
      console.log(
        `\n🔍 开始验证 ${existingIssues.length} 个历史问题是否已修复 (并发: ${concurrency})...`,
      );
    }
 
    const verifiedIssues: ReviewIssue[] = [];
    const llmJsonPut = new LlmJsonPut<VerifyResult>(VERIFY_SCHEMA);
 
    // 预处理:分离已修复和需要验证的 issues
    const toVerify: {
      issue: ReviewIssue;
      fileContent: FileContentLine[];
      ruleInfo: { rule: ReviewRule; spec: ReviewSpec } | null;
    }[] = [];
    for (const issue of existingIssues) {
      if (issue.fixed) {
        Iif (shouldLog(verbose, 1)) {
          console.log(`   ⏭️  跳过已修复: ${issue.file}:${issue.line} (${issue.ruleId})`);
        }
        verifiedIssues.push(issue);
        continue;
      }
 
      // valid === 'false' 的问题跳过复查(已确认无效的问题无需再次验证)
      if (issue.valid === FALSE) {
        Iif (shouldLog(verbose, 1)) {
          console.log(`   ⏭️  跳过无效问题: ${issue.file}:${issue.line} (${issue.ruleId})`);
        }
        verifiedIssues.push(issue);
        continue;
      }
 
      const fileContent = fileContents.get(issue.file);
      if (fileContent === undefined) {
        Iif (shouldLog(verbose, 1)) {
          console.log(`   ✅ 文件已删除: ${issue.file}:${issue.line} (${issue.ruleId})`);
        }
        verifiedIssues.push({
          ...issue,
          fixed: new Date().toISOString(),
          valid: FALSE,
          reason: "文件已删除",
        });
        continue;
      }
 
      const ruleInfo = this.reviewSpecService.findRuleById(issue.ruleId, specs);
      toVerify.push({ issue, fileContent, ruleInfo });
    }
 
    // 使用 parallel 库并行处理
    const executor = parallel({
      concurrency,
      onTaskStart: (taskId) => {
        if (shouldLog(verbose, 1)) {
          console.log(`   🔎 验证: ${taskId}`);
        }
      },
      onTaskComplete: (taskId, success) => {
        if (shouldLog(verbose, 1)) {
          console.log(`   ${success ? "✅" : "❌"} 完成: ${taskId}`);
        }
      },
    });
 
    const results = await executor.map(
      toVerify,
      async ({ issue, fileContent, ruleInfo }) =>
        this.verifySingleIssue(issue, fileContent, ruleInfo, llmMode, llmJsonPut, verbose),
      ({ issue }) => `${issue.file}:${issue.line}`,
    );
 
    for (const result of results) {
      if (result.success && result.result) {
        verifiedIssues.push(result.result);
      } else E{
        // 失败时保留原始 issue
        const originalItem = toVerify.find(
          (item) => `${item.issue.file}:${item.issue.line}` === result.id,
        );
        if (originalItem) {
          verifiedIssues.push(originalItem.issue);
        }
      }
    }
 
    const fixedCount = verifiedIssues.filter((i) => i.fixed).length;
    const unfixedCount = verifiedIssues.length - fixedCount;
    if (shouldLog(verbose, 1)) {
      console.log(`\n📊 验证完成: ${fixedCount} 个已修复, ${unfixedCount} 个未修复`);
    }
 
    return verifiedIssues;
  }
 
  /**
   * 验证单个 issue 是否已修复
   */
  protected async verifySingleIssue(
    issue: ReviewIssue,
    fileContent: FileContentLine[],
    ruleInfo: { rule: ReviewRule; spec: ReviewSpec } | null,
    llmMode: LLMMode,
    llmJsonPut: LlmJsonPut<VerifyResult>,
    verbose?: VerboseLevel,
  ): Promise<ReviewIssue> {
    const verifyPrompt = this.buildVerifyPrompt(issue, fileContent, ruleInfo);
 
    try {
      const stream = this.llmProxyService.chatStream(
        [
          { role: "system", content: verifyPrompt.systemPrompt },
          { role: "user", content: verifyPrompt.userPrompt },
        ],
        {
          adapter: llmMode,
          jsonSchema: llmJsonPut,
          verbose,
        },
      );
 
      let result: VerifyResult | undefined;
      for await (const event of stream) {
        if (event.type === "result") {
          result = event.response.structuredOutput as VerifyResult | undefined;
        } else Eif (event.type === "error") {
          console.error(`      ❌ 验证失败: ${event.message}`);
        }
      }
 
      if (result) {
        const updatedIssue: ReviewIssue = {
          ...issue,
          valid: issue.fixed || issue.resolved ? TRUE : result.valid ? TRUE : FALSE,
        };
 
        if (result.fixed) {
          if (shouldLog(verbose, 1)) {
            console.log(`      ✅ 已修复: ${result.reason}`);
          }
          updatedIssue.fixed = new Date().toISOString();
        } else if (!result.valid) {
          if (shouldLog(verbose, 1)) {
            console.log(`      ❌ 无效问题: ${result.reason}`);
          }
        } else {
          if (shouldLog(verbose, 1)) {
            console.log(`      ⚠️  未修复: ${result.reason}`);
          }
        }
 
        return updatedIssue;
      } else {
        return issue;
      }
    } catch (error) {
      if (error instanceof Error) {
        console.error(`      ❌ 验证出错: ${error.message}`);
        Eif (error.stack) {
          console.error(`      堆栈信息:\n${error.stack}`);
        }
      } else {
        console.error(`      ❌ 验证出错: ${String(error)}`);
      }
      return issue;
    }
  }
 
  /**
   * 构建验证单个 issue 是否已修复的 prompt
   */
  protected buildVerifyPrompt(
    issue: ReviewIssue,
    fileContent: FileContentLine[],
    ruleInfo: { rule: ReviewRule; spec: ReviewSpec } | null,
  ): { systemPrompt: string; userPrompt: string } {
    const padWidth = String(fileContent.length).length;
    const linesWithNumbers = fileContent
      .map(([, line], index) => `${String(index + 1).padStart(padWidth)}| ${line}`)
      .join("\n");
 
    const systemPrompt = `你是一个代码审查专家。你的任务是判断之前发现的一个代码问题:
1. 是否有效(是否真的违反了规则)
2. 是否已经被修复
 
请仔细分析当前的代码内容。
 
## 输出要求
- valid: 布尔值,true 表示问题有效(代码确实违反了规则),false 表示问题无效(误报)
- fixed: 布尔值,true 表示问题已经被修复,false 表示问题仍然存在
- reason: 判断依据
 
## 判断标准
 
### valid 判断
- 根据规则 ID 和问题描述,判断代码是否真的违反了该规则
- 如果问题描述与实际代码不符,valid 为 false
- 如果规则不适用于该代码场景,valid 为 false
 
### fixed 判断
- 只有当问题所在的代码已被修改,且修改后的代码不再违反规则时,fixed 才为 true
- 如果问题所在的代码仍然存在且仍违反规则,fixed 必须为 false
- 如果代码行号发生变化但问题本质仍存在,fixed 必须为 false
 
## 重要提醒
- valid=false 时,fixed 的值无意义(无效问题无需修复)
- 请确保 valid 和 fixed 的值与 reason 的描述一致!`;
 
    // 构建规则定义部分
    let ruleSection = "";
    if (ruleInfo) {
      ruleSection = this.reviewSpecService.buildSpecsSection([ruleInfo.spec]);
    }
 
    const userPrompt = `## 规则定义
 
${ruleSection}
 
## 之前发现的问题
 
- **文件**: ${issue.file}
- **行号**: ${issue.line}
- **规则**: ${issue.ruleId} (来自 ${issue.specFile})
- **问题描述**: ${issue.reason}
${issue.suggestion ? `- **原建议**: ${issue.suggestion}` : ""}
 
## 当前文件内容
 
\`\`\`
${linesWithNumbers}
\`\`\`
 
请判断这个问题是否有效,以及是否已经被修复。`;
 
    return { systemPrompt, userPrompt };
  }
}