diff --git a/app/main.js b/app/main.js index 42e9fed..d1d2b19 100644 --- a/app/main.js +++ b/app/main.js @@ -46,10 +46,12 @@ async function main() { } step('Step2', 'PR 對話收斂'); - let reconcile = { resolvedFindings: [], carriedFindings: [], resolvedCount: 0, unresolvedCount: 0 }; + let reconcile = { resolvedFindings: [], carriedFindings: [], resolvedCount: 0, duplicateCount: 0, unresolvedCount: 0 }; try { - reconcile = await reconcileConversations(); - ok(`Step2 完成: resolved=${reconcile.resolvedCount} unresolved=${reconcile.unresolvedCount} 加回=${reconcile.carriedFindings.length}`); + // 載入來源分支既有的舊問題,供對話收斂判斷「未修復但已存在於舊問題」的情況 + const oldFindingsForReconcile = loadOldFindings(WORKSPACE); + reconcile = await reconcileConversations({ oldFindings: oldFindingsForReconcile }); + ok(`Step2 完成: resolved=${reconcile.resolvedCount} duplicate=${reconcile.duplicateCount} 加回=${reconcile.carriedFindings.length}`); } catch (e) { warn(`Step2 對話收斂失敗(繼續執行): ${e.message}`); } diff --git a/app/resolve.js b/app/resolve.js index 1ddac94..409fc31 100644 --- a/app/resolve.js +++ b/app/resolve.js @@ -134,11 +134,12 @@ function isSafeRepoPath(p) { } /** - * 對話收斂主流程: - * 1. 取得 PR 所有行內 review comment,收斂成對話,跳過已 resolve 的; - * 2. 取每個對話所在檔案的最新內容,請 AI 判斷問題是否已解決; - * 3. 已解決者呼叫 Gitea resolve API 解決對話,並記錄其 finding(供移除舊問題); - * 4. 未解決且可解析為 bot finding 者,收集為「加回問題列表」清單。 + * 對話收斂主流程:取得 PR 所有行內 review comment、收斂成對話、跳過已 resolve 的, + * 取最新程式碼請 AI 判斷後,對每個待判斷對話做下列處置: + * 1. 程式碼已修復(AI 判定已解決)→ 解決對話,並記錄其 finding 供從舊問題移除; + * 2. 未修復但已存在於舊問題(以檔案+建議簽章比對)→ 解決對話(已被追蹤,不重複加回); + * 3. 未修復且不在舊問題 → 不解決對話,將其 finding 加入舊問題集合(carriedFindings)。 + * deps.oldFindings 提供來源分支既有的舊問題清單以供第 2 步比對。 * 任一外部呼叫失敗都降級處理(保守視為未解決),不中斷整體 pipeline。 */ export async function reconcileConversations(deps = {}) { @@ -147,6 +148,7 @@ export async function reconcileConversations(deps = {}) { resolveComment = resolvePullReviewComment, getFileContent = getFileContentAtRef, judge = judgeConversationsResolved, + oldFindings = [], } = deps; let comments; @@ -196,39 +198,63 @@ export async function reconcileConversations(deps = {}) { verdicts = items.map(it => ({ idx: it.idx, resolved: false })); } const resolvedSet = new Set(verdicts.filter(v => v.resolved).map(v => v.idx)); + const oldSigs = new Set((oldFindings || []).map(findingSig)); - const resolvedFindings = []; - const carriedFindings = []; + // 分類每個待判斷對話: + // - 'resolved':程式碼已修復 → 解決對話,並從舊問題移除 + // - 'duplicate':未修復但已存在於舊問題 → 解決對話(不重複加回) + // - 'carry':未修復且不在舊問題 → 加入舊問題集合(不解決對話) + const dispositions = open.map((c, i) => { + if (resolvedSet.has(i)) return 'resolved'; + const sig = c.botFinding ? findingSig(c.botFinding) : null; + if (sig && oldSigs.has(sig)) return 'duplicate'; + return 'carry'; + }); - // 並行 resolve 所有 AI 判定已解決的對話(allSettled:個別失敗不中斷其他) + // 並行對 resolved 與 duplicate 的對話呼叫 resolve API(allSettled:個別失敗不中斷其他) const resolveTargets = open .map((c, i) => ({ c, i })) - .filter(({ i }) => resolvedSet.has(i)); + .filter(({ i }) => dispositions[i] === 'resolved' || dispositions[i] === 'duplicate'); const settled = await Promise.allSettled( resolveTargets.map(({ c }) => resolveComment(c.commentIds[0])), ); const resolveOutcome = new Map(); resolveTargets.forEach(({ i }, j) => resolveOutcome.set(i, settled[j])); + const resolvedFindings = []; + const carriedFindings = []; let resolvedCount = 0; + let duplicateCount = 0; for (let i = 0; i < open.length; i++) { const c = open[i]; - const outcome = resolveOutcome.get(i); - if (outcome?.status === 'fulfilled') { - resolvedCount += 1; - if (c.botFinding) resolvedFindings.push({ ...c.botFinding, is_new: false }); - ok(`對話已解決並 resolve: ${c.path}:${c.line}`); + const disp = dispositions[i]; + + if (disp === 'carry') { + pushCarried(carriedFindings, c); continue; } - if (outcome?.status === 'rejected') { - warn(`resolve 對話失敗(保留為未解決): ${c.path}:${c.line} error=${outcome.reason?.message}`); + + const outcome = resolveOutcome.get(i); + if (outcome?.status === 'fulfilled') { + if (disp === 'resolved') { + resolvedCount += 1; + if (c.botFinding) resolvedFindings.push({ ...c.botFinding, is_new: false }); + ok(`對話已解決並 resolve(程式碼已修復): ${c.path}:${c.line}`); + } else { + duplicateCount += 1; + ok(`對話已解決並 resolve(已存在於舊問題): ${c.path}:${c.line}`); + } + continue; } - pushCarried(carriedFindings, c); + + warn(`resolve 對話失敗: ${c.path}:${c.line} error=${outcome?.reason?.message}`); + // resolved 但無法關閉對話時,保守加回舊問題避免遺漏;duplicate 本就在舊問題中,無須加回 + if (disp === 'resolved') pushCarried(carriedFindings, c); } const unresolvedCount = open.length - resolvedCount; - ok(`對話收斂完成: resolved=${resolvedCount} unresolved=${unresolvedCount} 加回 findings=${carriedFindings.length}`); - return { resolvedFindings, carriedFindings, resolvedCount, unresolvedCount }; + ok(`對話收斂完成: 已修復 resolved=${resolvedCount} 已存在舊問題 duplicate=${duplicateCount} 加入舊問題 carried=${carriedFindings.length}`); + return { resolvedFindings, carriedFindings, resolvedCount, duplicateCount, unresolvedCount }; } function fileOf(location) {