diff --git a/src/index.js b/src/index.js index b4c86d0..72d6237 100644 --- a/src/index.js +++ b/src/index.js @@ -117,9 +117,9 @@ function commitFindings({ cwd, ctx, files, result }) { /** * AI code review 主流程:依固定 10 步驟執行多角色審查,回傳 process exit code。 * - * 流程概要: + * 流程概要(步驟 2~10 描述一般模式;建問題模式差異見末段): * 1. 快速回報 — 最新 commit 若為 ai-review-bot 的結果 commit([success]/[failure]),直接回報 0/1 不重審; - * 2. 將 PR 既有舊留言標記為解決(跳過本回合留言); + * 2. 將 PR 既有舊留言標記為解決(跳過本回合留言;建問題模式不執行此步); * 3. 偵測 AI 工具(antigravity/codex/claude)並留言; * 4. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings, * 一般模式 commit success、建問題模式略過 commit,回傳 0); @@ -128,7 +128,10 @@ function commitFindings({ cwd, ctx, files, result }) { * 並以 appendExclusions 把誤判/重複問題回寫 .gitea/ai-review/exclusions.json; * 9. 嚴重問題逐條掛在程式碼行上留言; * 10. 警告+建議彙整為單一表格留言; - * 建問題模式(input: create-issue):保留問題另建 issue(createIssueWithFindings)逐條留言明細; + * 建問題模式(input: create-issue):不執行步驟 2、不觸碰 PR 既有留言;步驟 3~10 的所有留言 + * 改發到追蹤 issue(工具/diff/角色留言先暫存,確定有保留問題後才建立 issue 並一次寫入, + * 嚴重問題與警告+建議亦發到該 issue);無保留問題則不建 issue、僅在 PR 留審查通過提示; + * 收束時依保留問題補掛 issue 標籤,並在 PR 回貼 issue 連結形成雙向關聯; * 收尾:組 filesToCommit —— 一般模式 commit findings 檔(+有變更的 exclusions.json)、 * 建問題模式只 commit exclusions.json、無檔案可 commit 時略過; * commit 訊息帶結果標記(success=無嚴重問題、failure=有嚴重問題)。 @@ -167,28 +170,60 @@ async function main() { return 1; } - // 本回合發出的一般留言 id:步驟 2 標註過時時要跳過這些。 + // 本回合(一般模式)發出的 PR 留言 id:resolveOldComments 標註過時時要跳過這些。 const currentRunCommentIds = new Set(); + // 建問題模式:issue 於「確定有保留問題」後才建立;在那之前的情境留言(工具/diff/角色) + // 先暫存於 issueBuffer,建立 issue 後一次寫入。 + const issueBuffer = []; + let issue = null; /** - * 建立本回合 PR 一般留言並記錄留言 id,供後續舊留言處理排除。 + * 發布一則審查留言。依模式決定去向: + * - 一般模式:發到 PR,並記錄留言 id 供 `resolveOldComments` 排除。 + * - 建問題模式:issue 已建立時發到 issue;尚未建立時先暫存到 `issueBuffer`。 * - * @param {string} body 要發布到 PR 的 Markdown 留言內容。 - * @returns {Promise} Gitea API 建立的留言物件;至少預期包含 `id`。 + * @param {string} body 要發布的 Markdown 留言內容。 + * @returns {Promise} 一般模式、或建問題模式且 issue 已建立時回傳 Gitea 留言物件; + * 建問題模式尚未建立 issue 而先暫存時回傳 null。 * @remarks * 使用情境:只在 `main()` 內部使用,處理工具資訊、diff 摘要、角色登場與 - * 警告/建議彙整等一般留言。若 Gitea API 失敗,例外會往上拋出並由 - * 主流程頂層 catch 收斂。 + * 警告/建議彙整等留言。若 Gitea API 失敗,例外會往上拋出並由主流程頂層 catch 收斂。 */ const postComment = async (body) => { + if (ctx.createIssue) { + if (issue) return gitea.createCommentOnIssue(ctx, issue.number, body); + issueBuffer.push(body); + return null; + } const created = await gitea.createIssueComment(ctx, body); currentRunCommentIds.add(created.id); return created; }; + /** + * 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,先不掛標籤), + * 並把 `issueBuffer` 內暫存的情境留言依序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。 + * 僅於「確定有保留問題」時呼叫一次。 + * + * @returns {Promise} 無回傳值;結果反映在閉包變數 `issue` 與 issue 留言。 + */ + const ensureIssueCreated = async () => { + issue = await gitea.createIssue(ctx, { + title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`, + body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }), + labels: [], + }); + log('建問題', 'INF', `已建立追蹤 issue #${issue.number},寫入 ${issueBuffer.length} 則情境留言。`); + for (const body of issueBuffer) { + await gitea.createCommentOnIssue(ctx, issue.number, body); + } + issueBuffer.length = 0; + }; // ── 步驟 2:將 PR 既有留言標記為解決(本回合留言除外)─────────────────── - // 早於偵測工具與所有本回合留言:先把上一回合的 bot 留言標為過時; - // 此時本回合尚未發出任何留言(currentRunCommentIds 為空),之後發出的留言自然不受影響。 - await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); + // 僅一般模式執行;建問題模式不觸碰 PR 既有留言(審查內容改發到 issue)。 + // 早於偵測工具與所有本回合留言:先把上一回合的 bot 留言標為過時(此時尚無本回合留言)。 + if (!ctx.createIssue) { + await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); + } // ── 步驟 3:偵測 AI agent 工具並留言 ────────────────────────────────── const tool = agents.detectTool(); @@ -218,8 +253,13 @@ async function main() { log('步驟4', 'INF', `變更檔案 ${allFiles.length} 個,套用 .reviewignore 後送審 ${files.length} 個(排除 ${ignoredCount} 個)。`); if (files.length === 0) { - // 沒有可審查的變更:留言說明、保存空 findings、以 success 收場。 - await postComment(templates.nothingToReviewComment(ignoredCount)); + // 沒有可審查的變更:告知、保存空 findings、以 success 收場。 + // 建問題模式無問題可追蹤 → 不建 issue,直接在 PR 告知(暫存的情境留言捨棄)。 + if (ctx.createIssue) { + await gitea.createIssueComment(ctx, templates.nothingToReviewComment(ignoredCount)); + } else { + await postComment(templates.nothingToReviewComment(ignoredCount)); + } const relativePath = saveFindings({ cwd, ctx, tool, kept: [], excluded: [] }); if (ctx.createIssue) { // 建問題模式下 findings 不進版控,且 exclusions.json 無變更 → 沒東西可提交。 @@ -260,24 +300,59 @@ async function main() { const others = kept.filter((finding) => finding.severity !== '嚴重'); log('步驟8', 'INF', `分組結果:嚴重 ${severe.length} 條、警告+建議 ${others.length} 條。`); - // ── 步驟 9:嚴重問題逐條掛在程式碼行上留言(開發者可回覆)────────────── - if (severe.length > 0) { - await review.postSevereComments({ ctx, gitea, severe, cwd }); + // ── 建問題模式:確定有保留問題才建立 issue,並把暫存的情境留言一次寫入; + // 無保留問題則不建 issue,改在 PR 留一則審查通過提示。 ────────────────── + if (ctx.createIssue) { + if (kept.length > 0) { + await ensureIssueCreated(); + } else { + log('建問題', 'INF', '沒有保留的問題,略過建立 issue,於 PR 留審查通過提示。'); + await gitea.createIssueComment(ctx, templates.noFindingsComment()); + } } - // ── 步驟 10:警告+建議彙整為單一表格留言 ────────────────────────────── + // ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式發到 issue)─ + if (severe.length > 0) { + if (ctx.createIssue) { + await review.postSevereToIssue({ ctx, gitea, issueNumber: issue.number, severe }); + } else { + await review.postSevereComments({ ctx, gitea, severe, cwd }); + } + } + + // ── 步驟 10:警告+建議彙整為單一表格留言(去向由 postComment 依模式決定)── if (others.length > 0) { await postComment(templates.othersComment(others)); log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`); } - // ── 建問題模式(input: create-issue):另建 issue 逐條留言問題明細 ────── - if (ctx.createIssue) { - if (kept.length > 0) { - await review.createIssueWithFindings({ ctx, gitea, tool, model: ctx.model, cwd, findings: kept }); - } else { - log('建問題', 'INF', '沒有保留的問題,略過建立 issue。'); + // ── 建問題模式收束:依保留問題補掛 issue 標籤,並在 PR 回貼 issue 連結(雙向關聯)─ + if (ctx.createIssue && issue) { + try { + const labels = await gitea.listLabels(ctx); + const labelIds = await review.selectLabels({ + tool, + model: ctx.model, + cwd, + labels, + prTitle: ctx.prTitle, + prBody: ctx.prBody, + findings: kept, + }); + await gitea.addLabelsToIssue(ctx, issue.number, labelIds); + } catch (err) { + log('建問題', 'WRN', `補掛標籤失敗(${err.message}),issue 不掛標籤。`); } + await gitea.createIssueComment( + ctx, + templates.issueLinkComment({ + issueNumber: issue.number, + issueUrl: issue.html_url, + severeCount: severe.length, + otherCount: others.length, + }), + ); + log('建問題', 'INF', `issue #${issue.number} 已寫入審查內容,並在 PR 回貼連結。`); } // ── 收尾:commit 並 push(success=無嚴重問題、failure=有嚴重問題)─────── diff --git a/src/lib/gitea.js b/src/lib/gitea.js index ef71722..91d0c29 100644 --- a/src/lib/gitea.js +++ b/src/lib/gitea.js @@ -99,8 +99,8 @@ function whoAmI(ctx) { * @returns {Promise} 建立成功的留言物件(含 `id`、`body`、`user` 等欄位, * 依 Gitea API 回應而定)。 * @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:建問題模式(input: create-issue)下, - * `createIssueWithFindings` 建立 issue 後,逐條把 finding 明細留言到該 issue; + * @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 建立 issue 後, + * 把工具/diff/角色情境留言與 `review.postSevereToIssue` 的嚴重問題明細留言到該 issue; * 另外 `createIssueComment` 也委派本函式對 `ctx.prNumber` 留言。 */ function createCommentOnIssue(ctx, issueNumber, body) { @@ -134,9 +134,9 @@ function createIssueComment(ctx, body) { * @returns {Promise} 標籤物件陣列(每筆含 `id`、`name`、`color` 等欄位, * 依 Gitea API 回應而定);存取庫無標籤時為空陣列。 * @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:建問題模式(input: create-issue)下, - * `createIssueWithFindings` 先以本函式取得可用標籤,再交給 `selectLabels` - * 讓 AI 從中挑選適合掛在新 issue 上的標籤子集合。 + * @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 收束時 + * 先以本函式取得可用標籤,再交給 `review.selectLabels` 讓 AI 挑出適合的標籤子集合, + * 最後以 `addLabelsToIssue` 補掛到追蹤 issue 上。 */ function listLabels(ctx) { return listAll(ctx, `/repos/${ctx.owner}/${ctx.repo}/labels`); @@ -156,10 +156,9 @@ function listLabels(ctx) { * @returns {Promise} 建立成功的 issue 物件(含 `number`、`title`、 * `html_url` 等欄位,依 Gitea API 回應而定)。 * @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:建問題模式(input: create-issue)下, - * `createIssueWithFindings` 以 PR 標題/描述為 issue 標題與本文、 - * 配上 `selectLabels` 挑出的標籤 id,呼叫本函式建立追蹤問題的 issue, - * 再逐條把 finding 明細留言到該 issue。 + * @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 的 `ensureIssueCreated` + * 以 PR 標題/描述為 issue 標題與本文(先不掛標籤)呼叫本函式建立追蹤問題的 issue, + * 之後再把審查內容留言到該 issue、並以 `addLabelsToIssue` 補掛標籤。 */ function createIssue(ctx, { title, body, labels }) { return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues`, { @@ -169,6 +168,27 @@ function createIssue(ctx, { title, body, labels }) { }); } +/** + * 為既有 issue 追加標籤(不影響既有標籤)。 + * 對應 endpoint:`POST /repos/{owner}/{repo}/issues/{issueNumber}/labels`。 + * `labels` 為空陣列時不呼叫 API、直接回傳 `null`(省一次無意義請求)。 + * + * @param {object} ctx - 執行環境 context。必要欄位:`apiBase`、`token`、 + * `owner`(repo 擁有者)、`repo`(repo 名稱)。 + * @param {number|string} issueNumber - 目標 issue 編號。 + * @param {number[]} labels - 要追加的標籤 id 陣列。 + * @returns {Promise} 追加後該 issue 的標籤陣列(依 Gitea API 回應而定); + * `labels` 為空時回傳 `null`(未發出請求)。 + * @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 + * @remarks 使用情境:建問題模式(input: create-issue)下先以空標籤建立 issue、 + * 待防守方裁決得到保留問題後,再以 `selectLabels` 挑出的標籤 id 呼叫本函式補掛, + * 讓標籤挑選能參考最終的問題清單。 + */ +function addLabelsToIssue(ctx, issueNumber, labels) { + if (!labels || labels.length === 0) return Promise.resolve(null); + return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/labels`, { labels }); +} + /** * 列出 PR 上的全部一般留言(自動分頁撈取,每頁 50 筆直到取完)。 * 對應 endpoint:`GET /repos/{owner}/{repo}/issues/{prNumber}/comments`。 @@ -303,6 +323,7 @@ module.exports = { createCommentOnIssue, listLabels, createIssue, + addLabelsToIssue, listIssueComments, editIssueComment, createReview, diff --git a/src/lib/review.js b/src/lib/review.js index cf504f8..73302b5 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -578,30 +578,6 @@ function sortFindings(findings) { ); } -/** - * 建問題模式的就地排序:依檔案路徑、再依嚴重等級(嚴重→警告→建議)、再依起始行遞增。 - * - * 與 {@link sortFindings}(嚴重度優先)不同,本排序以檔案路徑為第一鍵, - * 讓 issue 上逐條留言的問題「同檔集中」,便於開發者逐檔處理。 - * 嚴重等級權重取自 `templates.SEVERITY_ORDER`;未知等級排最後。 - * 注意:直接修改傳入陣列(in-place),無回傳值。 - * - * @param {Array<{file: string, severity: string, startLine: number}>} findings - 要排序的 finding 陣列(通常為保留問題 `kept` 的複本)。 - * @returns {void} 無回傳值;排序結果反映在傳入陣列本身。 - * @remarks - * 使用情境:建問題模式(input: create-issue)下,{@link createIssueWithFindings} - * 先以 `[...findings]` 複製保留問題(不動原陣列的嚴重度排序), - * 再對複本呼叫本函式,依「檔案→嚴重度→行號」的順序逐條留言到新 issue。 - */ -function sortFindingsForIssue(findings) { - findings.sort( - (a, b) => - a.file.localeCompare(b.file) || - (templates.SEVERITY_ORDER[a.severity] ?? 9) - (templates.SEVERITY_ORDER[b.severity] ?? 9) || - a.startLine - b.startLine, - ); -} - /** * 建問題模式:以 AI 依 PR 標題/描述與問題列表摘要, * 從存取庫可用標籤中挑選適合掛在追蹤 issue 上的標籤子集合。 @@ -620,9 +596,9 @@ function sortFindingsForIssue(findings) { * @param {Array} params.findings - 保留的問題列表;每條取 severity/focus/file 與截斷 120 字的 problem 作為挑選依據。 * @returns {Promise} 挑中的標籤 id 陣列(可用標籤的子集合);無適合標籤或任何失敗時為空陣列。 * @remarks - * 使用情境:建問題模式(input: create-issue)下,{@link createIssueWithFindings} - * 先呼叫 `gitea.listLabels` 取得可用標籤,再以本函式取得標籤 id 子集合, - * 傳給 `gitea.createIssue` 讓新 issue 自動掛上合適標籤。 + * 使用情境:建問題模式(input: create-issue)下,`main()`(src/index.js) + * 在建立追蹤 issue 後先呼叫 `gitea.listLabels` 取得可用標籤,再以本函式依保留問題 + * 挑出標籤 id 子集合,交給 `gitea.addLabelsToIssue` 補掛到 issue 上。 */ async function selectLabels({ tool, model, cwd, labels, prTitle, prBody, findings }) { if (labels.length === 0) return []; @@ -670,61 +646,30 @@ ${JSON.stringify(brief)} } /** - * 建問題模式:把保留的審查問題建成存取庫的追蹤 issue 並逐條留言明細。 + * 建問題模式:把嚴重 findings 逐條以一般留言發到追蹤 issue。 * - * 流程:AI 挑標籤(`listLabels` + {@link selectLabels},失敗不掛標籤) - * → 建立 issue(標題=PR 標題、本文=PR 描述加追溯資訊;失敗記 ERR 並回傳 null 不阻斷主流程) - * → 複製 findings 依「檔案路徑→嚴重等級→起始行」排序({@link sortFindingsForIssue}) - * → 逐條以 `templates.issueFindingComment` 留言到 issue。 + * issue 無法把留言掛在程式碼行上(沒有 diff 定位),故改以 + * {@link templates.issueFindingComment} 在內文標明位置逐條發布—— + * 等同一般模式 PR 步驟 9 的嚴重問題,改以 issue 留言呈現。 + * findings 由呼叫端事先以 {@link sortFindings} 排序(嚴重度→檔案→行號),本函式不再排序。 * * @param {Object} params - 解構參數。 - * @param {Object} params.ctx - 執行環境 context(`loadContext()` 回傳);使用 `prNumber`、`prTitle`、`prBody` 及 Gitea API 認證欄位。 - * @param {Object} params.gitea - Gitea API 模組(src/lib/gitea.js);以參數注入便於測試替換,使用 `listLabels`、`createIssue`、`createCommentOnIssue`。 - * @param {Object} params.tool - `detectTool()` 偵測到的 AI CLI 工具描述物件(挑標籤用)。 - * @param {string} params.model - 指定 AI 模型名稱;空字串=工具預設。 - * @param {string} params.cwd - agent 的工作目錄(repo 根目錄)。 - * @param {Array} params.findings - 要寫進 issue 的問題列表(通常為防守方裁決後保留的 `kept`);本函式以複本排序,不改動原陣列順序。 - * @returns {Promise} 建立成功的 Gitea issue 物件(含 `number` 等欄位);建立 issue 失敗時為 null。 - * @throws {Error} 逐條留言(`createCommentOnIssue`)失敗時未攔截、向上拋出;列標籤與建 issue 的失敗則已於函式內降級處理。 + * @param {Object} params.ctx - 執行環境 context(`loadContext()` 回傳);供 Gitea API 認證。 + * @param {Object} params.gitea - Gitea API 模組(src/lib/gitea.js);以參數注入便於測試替換,使用 `createCommentOnIssue`。 + * @param {number} params.issueNumber - 目標追蹤 issue 的編號。 + * @param {Array} params.severe - severity 為「嚴重」的 finding 列表(已排序;呼叫端保證非空)。 + * @returns {Promise} 無回傳值;結果反映在 issue 留言與日誌。 + * @throws {Error} 逐條留言(`createCommentOnIssue`)失敗時未攔截、向上拋出,由主流程頂層 catch 收斂。 * @remarks - * 使用情境:`main()`(src/index.js)在步驟 10 之後、收尾之前, - * 於 `ctx.createIssue` 為 true 且 `kept.length > 0` 時呼叫本函式; - * 此模式下問題明細已保存在 issue 留言,收尾只 commit exclusions.json、findings 檔不進版控。 + * 使用情境:建問題模式(input: create-issue)下,`main()`(src/index.js)於防守方裁決後 + * 建立追蹤 issue、寫入情境留言,再以本函式把嚴重問題逐條留言到該 issue; + * 警告+建議則以 {@link templates.othersComment} 彙整成單一表格另發到同一 issue。 */ -async function createIssueWithFindings({ ctx, gitea, tool, model, cwd, findings }) { - let labelIds = []; - try { - const labels = await gitea.listLabels(ctx); - labelIds = await selectLabels({ - tool, - model, - cwd, - labels, - prTitle: ctx.prTitle, - prBody: ctx.prBody, - findings, - }); - } catch (err) { - log('建問題', 'WRN', `取得存取庫標籤失敗(${err.message}),issue 不掛標籤。`); +async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) { + for (const finding of severe) { + await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding)); } - let issue; - try { - issue = await gitea.createIssue(ctx, { - title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`, - body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }), - labels: labelIds, - }); - } catch (err) { - log('建問題', 'ERR', `建立 issue 失敗:${err.message}。`); - return null; - } - const sorted = [...findings]; - sortFindingsForIssue(sorted); - for (const finding of sorted) { - await gitea.createCommentOnIssue(ctx, issue.number, templates.issueFindingComment(finding)); - } - log('建問題', 'INF', `issue #${issue.number} 已建立並逐條留言 ${sorted.length} 條問題。`); - return issue; + log('步驟9', 'INF', `已將 ${severe.length} 條嚴重問題留言到 issue #${issueNumber}。`); } /** @@ -882,9 +827,8 @@ module.exports = { runDefenders, sortFindings, appendExclusions, - sortFindingsForIssue, selectLabels, - createIssueWithFindings, + postSevereToIssue, resolveOldComments, postSevereComments, }; diff --git a/src/lib/templates.js b/src/lib/templates.js index 6219b0f..ef364e7 100644 --- a/src/lib/templates.js +++ b/src/lib/templates.js @@ -302,7 +302,7 @@ function othersComment(findings) { * @param {string} [params.prBody] - PR 描述原文;nullish 或 trim 後為空時輸出佔位文字。 * @returns {string} 完整 issue 本文 Markdown 字串(含 MARK 隱藏標記)。 * @remarks - * 使用情境:建問題模式下 `createIssueWithFindings`(src/lib/review.js)建立 issue 時, + * 使用情境:建問題模式下 `main()`(src/index.js)的 `ensureIssueCreated` 建立 issue 時, * 以「標題=PR 標題、本文=本函式輸出」呼叫 `gitea.createIssue`, * 讓 issue 讀者能從本文回溯到觸發審查的 PR,再從下方留言逐條查看問題明細。 */ @@ -334,9 +334,9 @@ ${body || '(PR 無描述)'} * @param {string} [finding.suggestedCode] - 建議寫法程式碼;有值才輸出「建議寫法」區塊。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks - * 使用情境:建問題模式下 `createIssueWithFindings`(src/lib/review.js)建立 issue 後, - * 把保留的 findings 依「檔案路徑→嚴重等級→起始行」排序,逐條以本函式產生留言內容、 - * 經 `gitea.createCommentOnIssue` 發布到新 issue 上,作為問題明細的追蹤紀錄。 + * 使用情境:建問題模式下 `review.postSevereToIssue`(src/lib/review.js)把每條嚴重 finding + * 以本函式產生留言內容、經 `gitea.createCommentOnIssue` 發布到追蹤 issue 上, + * 作為問題明細的追蹤紀錄。 */ function issueFindingComment(finding) { const emoji = SEVERITY_EMOJI[finding.severity] || '🔵'; @@ -383,6 +383,48 @@ function nothingToReviewComment(ignoredCount) { 本次 PR 套用 \`.reviewignore\` 後**沒有可審查的變更**${ignoredCount > 0 ? `(${ignoredCount} 個檔案被排除)` : ''},視為審查通過。`; } +/** + * 產生建問題模式(input: create-issue)下,回貼到「PR」的追蹤問題連結留言。 + * + * 建問題模式把審查內容全部發到 issue、不留在 PR;本留言是 PR 上唯一的一則審查留言, + * 提供 issue 連結與問題數量統計,讓 PR 讀者一眼看到「本次審查結果在哪個 issue」。 + * + * @param {Object} params - 解構參數。 + * @param {number} params.issueNumber - 追蹤問題的 issue 編號;內插為 Markdown 連結文字。 + * @param {string} params.issueUrl - 追蹤問題的 issue 網址;作為 Markdown 連結目標。 + * @param {number} params.severeCount - 嚴重問題條數,顯示在統計。 + * @param {number} params.otherCount - 警告+建議問題條數,顯示在統計。 + * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 + * @remarks + * 使用情境:建問題模式下 `main()`(src/index.js)在 issue 建立並寫入全部審查內容後, + * 以本函式對 PR 留一則連結留言,達成「問題關聯回 PR」;issue 內文另以 + * {@link issueBody} 反向引用 `PR #N`,形成雙向交叉連結。 + */ +function issueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) { + return `${MARK} +## 🔍 AI Code Review|已建立追蹤問題 + +本次審查結果已彙整到 issue [#${issueNumber}](${issueUrl})(🔴 嚴重 ${severeCount} 條、🟠🔵 警告+建議 ${otherCount} 條),請至該問題追蹤與討論。`; +} + +/** + * 產生「已審查但無需保留問題」時,回貼到「PR」的審查通過留言。 + * + * 建問題模式下,若防守方裁決後沒有任何保留問題(不值得為此開 issue), + * 以本留言在 PR 上告知審查通過,取代原本會發到 issue 的問題內容。 + * + * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 + * @remarks + * 使用情境:建問題模式下 `main()`(src/index.js)於防守方裁決後 + * `kept.length === 0` 時呼叫;此情況不建立 issue,僅在 PR 留下本則通過提示。 + */ +function noFindingsComment() { + return `${MARK} +## ✅ AI Code Review|審查通過 + +本次 AI Code Review 未發現需保留的問題,視為審查通過。`; +} + module.exports = { MARK, OUTDATED_PREFIX, @@ -397,4 +439,6 @@ module.exports = { issueBody, issueFindingComment, nothingToReviewComment, + issueLinkComment, + noFindingsComment, };