diff --git a/src/index.js b/src/index.js index 90ab897..b4c86d0 100644 --- a/src/index.js +++ b/src/index.js @@ -42,9 +42,9 @@ const BOT_COMMIT_PREFIX = 'chore: update ai-review findings [ai-review-bot]'; * @param {Array} params.excluded - 被裁決為誤報而排除的問題清單。 * @returns {string} findings JSON 檔相對於 repo 根目錄的路徑(例如 `.gitea/ai-review/findings/xxx.json`)。 * @remarks - * 使用情境:`main()` 步驟 7 於防守方裁決、`review.sortFindings(kept)` 排序後呼叫本函式保存結果, + * 使用情境:`main()` 步驟 8 於防守方裁決、`review.sortFindings(kept)` 排序後呼叫本函式保存結果, * 再將回傳的相對路徑交給 `commitFindings` commit 並 push 回 PR 來源分支; - * 另在步驟 3 判定無可審查變更時,也會以空清單保存一份空 findings 後以 success 收場。 + * 另在步驟 4 判定無可審查變更時,也會以空清單保存一份空 findings 後以 success 收場。 * 本函式無 try/catch,檔案系統錯誤會往上拋出,由 `main().catch` 以 exit code 1 收場。 */ function saveFindings({ cwd, ctx, tool, kept, excluded }) { @@ -61,7 +61,7 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) { }; fs.writeFileSync(findingsPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); const relativePath = path.relative(cwd, findingsPath); - log('步驟7', 'INF', `findings 已保存:${relativePath}(保留 ${kept.length} 條、排除 ${excluded.length} 條)。`); + log('步驟8', 'INF', `findings 已保存:${relativePath}(保留 ${kept.length} 條、排除 ${excluded.length} 條)。`); return relativePath; } @@ -88,7 +88,7 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) { * @remarks * 使用情境:`main()` 於流程尾端依 `severe.length === 0 ? 'success' : 'failure'` 決定 result、 * 依模式組出 filesToCommit(一般模式:findings 檔+有變更時的 exclusions.json; - * 建問題模式:只有 exclusions.json)後呼叫本函式;另在步驟 3 判定無可審查變更且非建問題模式時, + * 建問題模式:只有 exclusions.json)後呼叫本函式;另在步驟 4 判定無可審查變更且非建問題模式時, * 也會以 result: 'success' 提交空 findings。 * 注意 commit 訊息與模組常數 `BOT_COMMIT_PREFIX` 耦合,修改前綴會使步驟 1 的快速回報失效。 */ @@ -119,13 +119,13 @@ function commitFindings({ cwd, ctx, files, result }) { * * 流程概要: * 1. 快速回報 — 最新 commit 若為 ai-review-bot 的結果 commit([success]/[failure]),直接回報 0/1 不重審; - * 2. 偵測 AI 工具(antigravity/codex/claude)並留言; - * 3. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings, + * 2. 將 PR 既有舊留言標記為解決(跳過本回合留言); + * 3. 偵測 AI 工具(antigravity/codex/claude)並留言; + * 4. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings, * 一般模式 commit success、建問題模式略過 commit,回傳 0); - * 4–5. 攻擊方登場留言、每位攻擊方一個 sub agent 並行找問題; - * 6–7. 防守方登場留言、裁決誤報後排序並保存 findings JSON, + * 5–6. 攻擊方登場留言、每位攻擊方一個 sub agent 並行找問題; + * 7–8. 防守方登場留言、裁決誤報後排序並保存 findings JSON, * 並以 appendExclusions 把誤判/重複問題回寫 .gitea/ai-review/exclusions.json; - * 8. 將 PR 既有舊留言標記為解決(跳過本回合留言); * 9. 嚴重問題逐條掛在程式碼行上留言; * 10. 警告+建議彙整為單一表格留言; * 建問題模式(input: create-issue):保留問題另建 issue(createIssueWithFindings)逐條留言明細; @@ -167,7 +167,7 @@ async function main() { return 1; } - // 本回合發出的一般留言 id:步驟 8 標註過時時要跳過這些。 + // 本回合發出的一般留言 id:步驟 2 標註過時時要跳過這些。 const currentRunCommentIds = new Set(); /** * 建立本回合 PR 一般留言並記錄留言 id,供後續舊留言處理排除。 @@ -185,13 +185,18 @@ async function main() { return created; }; - // ── 步驟 2:偵測 AI agent 工具並留言 ────────────────────────────────── + // ── 步驟 2:將 PR 既有留言標記為解決(本回合留言除外)─────────────────── + // 早於偵測工具與所有本回合留言:先把上一回合的 bot 留言標為過時; + // 此時本回合尚未發出任何留言(currentRunCommentIds 為空),之後發出的留言自然不受影響。 + await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); + + // ── 步驟 3:偵測 AI agent 工具並留言 ────────────────────────────────── const tool = agents.detectTool(); if (!tool) { - log('步驟2', 'ERR', '找不到可用的 AI 工具(antigravity/codex/claude)。'); + log('步驟3', 'ERR', '找不到可用的 AI 工具(antigravity/codex/claude)。'); return 1; } - log('步驟2', 'INF', `選用工具:${tool.name}(${tool.version})。`); + log('步驟3', 'INF', `選用工具:${tool.name}(${tool.version})。`); const runLink = `${ctx.serverUrl}/${ctx.repository}/actions/runs/${ctx.runId}`; await postComment( templates.toolComment({ @@ -204,13 +209,13 @@ async function main() { }), ); - // ── 步驟 3:讀取 .reviewignore、整理 git diff 並留言 ─────────────────── + // ── 步驟 4:讀取 .reviewignore、整理 git diff 並留言 ─────────────────── const ignores = review.loadReviewIgnore(cwd); const base = gitrepo.resolveMergeBase(cwd, ctx.baseRef); const allFiles = gitrepo.changedFiles(cwd, base); const files = allFiles.filter((file) => !review.isIgnored(file, ignores)); const ignoredCount = allFiles.length - files.length; - log('步驟3', 'INF', `變更檔案 ${allFiles.length} 個,套用 .reviewignore 後送審 ${files.length} 個(排除 ${ignoredCount} 個)。`); + log('步驟4', 'INF', `變更檔案 ${allFiles.length} 個,套用 .reviewignore 後送審 ${files.length} 個(排除 ${ignoredCount} 個)。`); if (files.length === 0) { // 沒有可審查的變更:留言說明、保存空 findings、以 success 收場。 @@ -229,20 +234,20 @@ async function main() { await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows }); await postComment(templates.diffComment(diffRows, ignoredCount)); - // ── 步驟 4:攻擊方角色登場留言 ───────────────────────────────────────── + // ── 步驟 5:攻擊方角色登場留言 ───────────────────────────────────────── const roles = loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles')); const attackers = attackersOf(roles); const defenders = defendersOf(roles); - log('步驟4', 'INF', `攻擊方 ${attackers.length} 位、防守方 ${defenders.length} 位。`); + log('步驟5', 'INF', `攻擊方 ${attackers.length} 位、防守方 ${defenders.length} 位。`); await postComment(templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers })); - // ── 步驟 5:每個攻擊方一個 sub agent 並行分析,合併問題列表 ──────────── + // ── 步驟 6:每個攻擊方一個 sub agent 並行分析,合併問題列表 ──────────── const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows }); - // ── 步驟 6:防守方角色登場留言 ───────────────────────────────────────── + // ── 步驟 7:防守方角色登場留言 ───────────────────────────────────────── await postComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders })); - // ── 步驟 7:防守方裁決 → 排除 → 排序 → 保存 findings ────────────────── + // ── 步驟 8:防守方裁決 → 排除 → 排序 → 保存 findings ────────────────── const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings }); review.sortFindings(kept); const relativePath = saveFindings({ cwd, ctx, tool, kept, excluded }); @@ -250,13 +255,10 @@ async function main() { // 誤判/重複的問題附加到 exclusions.json(之後與審查結果一起 commit)。 const exclusionsChanged = review.appendExclusions({ cwd, excluded, prNumber: ctx.prNumber }); - // ── 步驟 7(分組):依嚴重等級分組(嚴重/警告+建議),組內已依檔案與行數排序 ─ + // ── 步驟 8(分組):依嚴重等級分組(嚴重/警告+建議),組內已依檔案與行數排序 ─ const severe = kept.filter((finding) => finding.severity === '嚴重'); const others = kept.filter((finding) => finding.severity !== '嚴重'); - log('步驟7', 'INF', `分組結果:嚴重 ${severe.length} 條、警告+建議 ${others.length} 條。`); - - // ── 步驟 8:將 PR 既有留言標記為解決(本回合留言除外)─────────────────── - await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); + log('步驟8', 'INF', `分組結果:嚴重 ${severe.length} 條、警告+建議 ${others.length} 條。`); // ── 步驟 9:嚴重問題逐條掛在程式碼行上留言(開發者可回覆)────────────── if (severe.length > 0) {