feat(ai-code-review): 建問題模式導向 issue,並整併流程調整、文件與 CI #6

Merged
admin merged 79 commits from ai-review-resolve/develop-20260717-185330 into develop 2026-07-21 09:39:48 +00:00
Showing only changes of commit d7cecc8fa8 - Show all commits
@@ -0,0 +1,601 @@
{
"generatedAt": "2026/07/20 18:31:07",
"commitSha": "0c29daa01434d6b6fa1b20ce81c513c75b1b1c6a",
"prNumber": null,
"tool": {
"name": "code-review-resolve",
"version": "0.0.9",
"model": "(工具預設)"
},
"findings": [
{
"id": "F001",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "警告",
"file": "src/index.js",
"startLine": 119,
"endLine": 139,
"problem": "流程步驟編號被當成跨模組識別值,散落在 `index.js`、各 library 的日誌與 JSDoc、README 流程圖及功能表。這次僅因插入並延後一步,就必須同步修改大量 `步驟2`~`步驟8` 字串,而且實際執行順序已變成 1、3~8、2、9~10;未來再調整流程時非常容易讓文件、日誌與程式碼脫節。",
"suggestion": "以穩定的語意階段名稱取代硬編碼序號,例如 `TOOL_DETECTION`、`COLLECT_DIFF`、`RESOLVE_OLD_COMMENTS`,由單一常數表集中決定顯示名稱;README 的流程順序則從同一份定義產生,或至少不要在各函式文件重複紀錄易變的數字。",
"suggestedCode": "```\nconst PHASE = Object.freeze({\n FAST_RESULT: '快速回報',\n DETECT_TOOL: '偵測 AI 工具',\n COLLECT_DIFF: '整理變更',\n RESOLVE_OLD: '處理舊留言',\n PUBLISH_FINDINGS: '發布審查結果',\n});\n\nlog(PHASE.DETECT_TOOL, 'INF', `選用工具:${tool.name}。`);\n```",
"sourceIssue": 11
},
{
"id": "F002",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "警告",
"file": "readme.md",
"startLine": 59,
"endLine": 130,
"problem": "功能索引把分支名稱與原始碼行號硬編碼在數十個連結中;本次僅因程式碼增行,就必須人工把大量 `#L...` 全面更新,已直接顯示這份文件存在高同步成本。之後任一檔案前段增刪程式碼,都會讓這些連結再次漂移,而且指向會持續變動的 `develop` 分支,使舊版 README 與實際連結內容無法穩定對應。",
"suggestion": "不要手動維護原始碼行號。若只需導覽,連到檔案並由右欄既有章節錨點提供函式級定位;若必須精確指向定義,應由 AST/文件產生工具在 CI 自動建立索引,並連到固定 commit SHA 或版本 tag。至少增加連結檢查,避免半年後整張功能表悄悄失準。",
"suggestedCode": "```\n| 功能名稱 | 功能描述 |\n| --- | --- |\n| [gitrepo.resolveMergeBase](src/lib/gitrepo.js) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) |\n```",
"sourceIssue": 12
},
{
"id": "F003",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 13,
"endLine": 82,
"problem": "`agentFailureDetail()` 同時負責解析程序失敗、決定 debug 政策、讀取全域環境變數、截斷輸出及遮罩機密。尤其直接讀取 `process.env.ACTIONS_STEP_DEBUG` 形成隱藏相依,測試不同輸出政策時必須修改程序全域狀態;日後若其他呼叫端需要不同診斷層級,也只能繼續往這個函式堆條件。",
"suggestion": "把診斷政策改成明確參數,並將「錯誤中繼資料整理」與「輸出片段清理」拆成小函式;在 `main` 或 context 載入階段解析環境設定後注入。這能讓各種 exit code、signal、空輸出與 verbose 模式以純輸入輸出直接測試。",
"suggestedCode": "```\nfunction agentFailureDetail(res, { includeStdout = false, inputLimit = 2000, outputLimit = 500 } = {}) {\n const parts = failureMetadata(res && res.error);\n appendSanitizedOutput(parts, 'stderr', res && res.stderr, inputLimit, outputLimit);\n if (includeStdout) {\n appendSanitizedOutput(parts, 'stdout', res && res.output, inputLimit, outputLimit);\n }\n return parts.length ? parts.join('') : 'AI CLI 執行失敗(無診斷輸出)';\n}\n```",
"sourceIssue": 12
},
{
"id": "F004",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 42,
"endLine": 61,
"problem": "`agentFailureDetail` 的文件自相矛盾:開頭宣稱「原始輸出預設隱藏」,後文卻明言預設附上經遮罩的 `stderr` 與 `stdout` 片段;另提到 `ACTIONS_STEP_DEBUG`,函式內卻沒有相應分支。註解與實作各唱各的調,維護者無法從文件判斷實際日誌行為。",
"suggestion": "統一文件敘述為實際行為:預設輸出經遮罩且限長的診斷片段;若目前並未依 `ACTIONS_STEP_DEBUG` 改變輸出,就移除該段說明,或待真正實作開關後再補上。",
"suggestedCode": "",
"sourceIssue": 13
},
{
"id": "F005",
"reviewer": "Rogue",
"focus": "",
"badge": "⚡",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 216,
"endLine": 218,
"problem": "在 `ensureIssueCreated` 函式中,使用 `for...of` 迴圈搭配 `await` 來逐條對 Gitea API 發送留言請求。由於網路請求存在延遲(每次 RTT 約 100-300ms),在迴圈內阻塞式等待會導致整體執行時間隨留言數量線性增加,浪費大量 CPU 週期與網路連線資源。",
"suggestion": "可以將 `issueBuffer` 中的所有情境留言合併為單一 Markdown 留言發送,僅需一次 API 呼叫;或者使用 `Promise.all` 將這些無相依性的留言請求並行化發送,大幅降低總延遲。",
"suggestedCode": "```\nif (issueBuffer.length > 0) {\n const combinedBody = issueBuffer.join('\\n\\n---\\n\\n');\n await gitea.createCommentOnIssue(ctx, issue.number, combinedBody);\n }\n issueBuffer.length = 0;\n```",
"sourceIssue": 14
},
{
"id": "F006",
"reviewer": "Rogue",
"focus": "",
"badge": "⚡",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 134,
"endLine": 134,
"problem": "在檢查是否為淺層 repository (shallow repository) 時,呼叫了外部子行程執行 `git rev-parse --is-shallow-repository`。建立與啟動 OS 子行程是非常昂貴的操作,會白白浪費數十毫秒的 CPU 週期與系統資源。",
"suggestion": "Git 在淺層 clone 時會在 `.git` 目錄下建立一個 `shallow` 檔案。我們可以使用 Node.js 內建的 `fs.existsSync` 進行本地檔案檢查,不需啟動額外的 Git 子行程,執行速度可快上百倍。",
"suggestedCode": "```\nconst fs = require('fs');\n// ...\nif (fs.existsSync(path.join(cwd, '.git', 'shallow'))) {\n strategies.push(['unshallow', 'fetch', '--no-tags', '--unshallow', 'origin']);\n}\n```",
"sourceIssue": 14
},
{
"id": "F007",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 293,
"endLine": 305,
"problem": "為了防止 Git 推送失敗時在例外訊息中回顯包含 Token 與遠端 URL 的命令列參數,pushWithCredential 的 catch 區塊直接拋出一個固定的 Error('推送審查結果 commit 失敗...')。但這樣一來,它完全吞掉了原始的錯誤(例如 non-fast-forward 非快轉、分支保護規則阻擋、或連線逾時),六個月後的維護者在 CI log 中看到此錯誤時,完全無從判斷失敗的原因。",
"suggestion": "建議在保留安全遮罩的前提下,保留原始 exception 的排錯線索。例如可以檢查並安全地過濾 err.message 或 err.stderr 中所有的敏感字串(如 Token/URL),然後將其作為新錯誤的 cause 屬性或附加訊息傳遞下去。",
"suggestedCode": "```\n} catch (err) {\n // 過濾敏感資訊後保留錯誤細節\n const safeMessage = err.message ? redactSecrets(err.message) : '未知錯誤';\n const error = new Error(`推送審查結果 commit 失敗(${safeMessage})。`);\n error.cause = err;\n throw error;\n }\n```",
"sourceIssue": 14
},
{
"id": "F008",
"reviewer": "Assassin",
"focus": "",
"badge": "🗡️",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 299,
"endLine": 299,
"problem": "在 `pushWithCredential` 中,環境變數 `GIT_CONFIG_KEY_0` 被動態拼接為 `http.${remoteUrl}.extraheader`。如果 `remoteUrl` 來自外部或未經嚴格驗證的 context,且 URL 中包含特殊字元(如點號、路徑分隔符號或引號等),可能會導致 Git 配置解析錯誤,或在特定平台下引發 Git 配置參數注入風險。",
"suggestion": "由於該執行程序僅為一次性的 `git push` 操作,可直接將該憑證應用於所有 HTTP 請求,將 `GIT_CONFIG_KEY_0` 設定為靜態的 `http.extraheader`,以避免動態拼接 URL 所帶來的注入風險。",
"suggestedCode": "```\nGIT_CONFIG_KEY_0: 'http.extraheader',\n```",
"sourceIssue": 14
},
{
"id": "F009",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/index.js",
"startLine": 308,
"endLine": 368,
"problem": "新增或修改的步驟分隔註解(如步驟 8 分組、步驟 2 延後、步驟 9、步驟 10、及建問題模式收束)其尾隨的水平分隔線(─)長度不一或僅存單一字元,破壞了專案既有程式碼中整齊劃一的長分隔線視覺排版,視覺上顯得雜亂、走調。",
"suggestion": "補足尾隨的水平線 ─,使其與鄰近步驟分隔註解的長度(約 70~80 字元寬度)與視覺風格保持一致,維持排版的美觀。",
"suggestedCode": "```\n// ── 步驟 8(分組):依嚴重等級分組(嚴重/警告+建議),組內已依檔案與行數排序 ────────────────\n```",
"sourceIssue": 14
},
{
"id": "F010",
"reviewer": "Rogue",
"focus": "",
"badge": "⚡",
"severity": "建議",
"file": "src/index.js",
"startLine": 366,
"endLine": 382,
"problem": "在建問題模式收束時,先 `await gitea.createIssueComment` 再 `await gitea.addIssueDependency`,這兩個 Gitea API 呼叫是獨立且無資料相依性的,卻以序列(Sequential)方式執行,白白浪費了一次網路往返(RTT)的等待時間。",
"suggestion": "使用 `Promise.all` 同時發起這兩個請求,並行處理以減少整體 execution 的等待時間。",
"suggestedCode": "```\nconst commentPromise = gitea.createIssueComment(\n ctx,\n templates.issueLinkComment({\n issueNumber: issue.number,\n issueUrl: issue.html_url,\n severeCount: severe.length,\n otherCount: others.length,\n })\n );\n const dependencyPromise = gitea.addIssueDependency(ctx, ctx.prNumber, issue.number)\n .then(() => log('建問題', 'INF', `已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number}。`))\n .catch((err) => log('建問題', 'WRN', `設定 PR 相依失敗:${err.message}。`));\n \n await Promise.all([commentPromise, dependencyPromise]);\n```",
"sourceIssue": 14
},
{
"id": "F011",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 33,
"endLine": 34,
"problem": "在 redactSecrets 函式中,針對 authorization 以及其他憑證關鍵字(如 token、secret、password 等)的敏感資訊遮蔽,分別使用了兩條結構極為相似的正規表示式進行替換。這造成了重複的替換邏輯與額外的處理開銷,程式碼的旋律顯得不夠俐落。",
"suggestion": "建議將這兩條正規表示式合併為單一表達式,消除重複的 replace 呼叫,使程式碼更加簡潔優雅且提升運行效率。",
"suggestedCode": "```\n.replace(/((?:authorization|api[_-]?key|token|password|secret|bearer)\\s*[:=]\\s*)\\S+/gi, '$1***')\n```",
"sourceIssue": 14
},
{
"id": "F012",
"reviewer": "Rogue",
"focus": "",
"badge": "⚡",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 48,
"endLine": 53,
"problem": "在 `agentFailureDetail` 之中,進行 stderr 與 stdout 的遮罩處理時,是先截斷至 2,000 字元,然後執行多次複雜的 `redactSecrets` 正規表示式替換,最後再截斷至 500 字元輸出。這會造成 1,500 字元的複雜 regex 運算結果在下一步被直接丟棄,白白浪費了 CPU 進行字串比對與取代的週期。",
"suggestion": "應在呼叫 `redactSecrets` 之前,就先將字串截斷至目標長度(500 字元),再進行遮罩,可大幅減少 regex 運算負擔。",
"suggestedCode": "```\nconst stderr = redactSecrets(String((res && res.stderr) || '').slice(0, 500));\n if (stderr) parts.push(`stderr${stderr}`);\n const stdout = redactSecrets(String((res && res.output) || '').slice(0, 500));\n if (stdout) parts.push(`stdout${stdout}`);\n```",
"sourceIssue": 14
},
{
"id": "F013",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/templates.js",
"startLine": 335,
"endLine": 337,
"problem": "issueFindingComment 函式的 @remarks 文件註解中,說明其使用情境為『建問題模式下 review.postSevereToIssue 把每條嚴重 finding... 作為問題明細的追蹤紀錄』。然而實際上,非嚴重的警告與建議(others)也會透過 review.postOthersToIssue 呼叫此模板進行發布,導致文件描述不夠完整。",
"suggestion": "修正 @remarks 的使用情境說明,將 postOthersToIssue 亦併入描述中,使 JSDoc 文件能如實且精準地反映實際程式碼的呼叫情境。",
"suggestedCode": "```\n* 使用情境:建問題模式下 `review.postSevereToIssue` 與 `review.postOthersToIssue`src/lib/review.js)把保留的各級問題明細\\n * 以本函式產生留言內容、經 `gitea.createCommentOnIssue` 發布到追蹤 issue 上,\\n * 作為問題明細的追蹤紀錄。\n```",
"sourceIssue": 14
},
{
"id": "F014",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "readme.md",
"startLine": 56,
"endLine": 132,
"problem": "README 的功能表大量手動維護 `src/branch/develop/...#Lxx` 深連結,這次光是分支與行號就改了整排。這類文件會隨任何插入註解、重排函式、換預設分支而失準,未來維護者必須在改程式時同步更新一大段文件,維護成本偏高。",
"suggestion": "改成不依賴行號的相對連結,或用文件產生腳本從原始碼 JSDoc 自動產出這張表。若仍要指向 Gitea,建議至少移除 `#Lxx`,或集中定義分支名稱,避免每次改分支都要全表搜尋替換。",
"suggestedCode": "",
"sourceIssue": 15
},
{
"id": "F015",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/gitea.js",
"startLine": 190,
"endLine": 197,
"problem": "`dependency` 作為參數名太薄,與 `issueNumber` 並列時看不出它也是 issue 編號。讀到 `addIssueDependency(ctx, ctx.prNumber, issue.number)` 時,語意要靠上下文補拍子。",
"suggestion": "改用更完整的名稱,例如 `dependencyIssueNumber` 或 `blockingIssueNumber`,讓「誰被誰阻擋」在簽名裡就清楚成形。",
"suggestedCode": "```\nfunction addIssueDependency(ctx, issueNumber, dependencyIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/dependencies`, {\n index: dependencyIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```",
"sourceIssue": 15
},
{
"id": "F016",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 28,
"endLine": 37,
"problem": "這段註解的旋律前後打架:開頭說「原始輸出預設隱藏」,後文卻說失敗時預設附上遮罩後的 stderr/stdout 片段。讀者才剛建立心智模型,下一拍就被改調。",
"suggestion": "請讓摘要句與實際行為一致,明確說明「原始輸出不直接輸出,但會輸出遮罩與截斷後的診斷片段」。",
"suggestedCode": "```\n* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,並附上遮罩、去控制字元且截斷後的 stderr/stdout 片段。\n```",
"sourceIssue": 15
},
{
"id": "F017",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/templates.js",
"startLine": 334,
"endLine": 341,
"problem": "`issueFindingComment` 的註解只提到 `review.postSevereToIssue`,但主流程也以 `postOthersToIssue` 發布警告與建議。文件把共用模板寫成嚴重問題專用,讀起來像少了一個聲部。",
"suggestion": "把 remarks 改成同時涵蓋嚴重、警告與建議的 issue 留言產生器,避免維護者誤以為它只服務嚴重 finding。",
"suggestedCode": "",
"sourceIssue": 15
},
{
"id": "F018",
"reviewer": "Assassin",
"focus": "",
"badge": "🗡️",
"severity": "嚴重",
"file": "src/lib/gitrepo.js",
"startLine": 102,
"endLine": 149,
"problem": "攻擊者可以透過提交惡意 Pull Request,將 `baseRef`PR 目標分支)命名為包含路徑穿越(Path Traversal)的字串,例如 `../../hooks/pre-push`。由於 `resolveMergeBase` 直接將 `baseRef` 拼接至 `git fetch` 的 Refspec 參數中(例如 `+refs/heads/${baseRef}:refs/remotes/origin/${baseRef}`),這將導致 `git fetch` 寫入至 `.git/refs/remotes/origin/../../hooks/pre-push`(即 `.git/hooks/pre-push`)。這會覆寫或建立 Git Hook,並在後續執行 git 操作時自動觸發該惡意 Hook,從而造成遠端程式碼執行(RCE)。同樣地,`commitAndPushFindings` 函數中的 `headRef` 也存在類似的拼接風險。",
"suggestion": "在將 `baseRef` 與 `headRef` 傳入 git 指令之前,應進行嚴格的合法性檢查。建議使用正則表達式限制分支名稱僅能包含安全的字元(如英數字、斜線、底線、連字號、句點),且絕對不得含有 `..` 或以 `-` 開頭,必要時亦可使用 `git check-ref-format` 命令先行驗證該分支名稱是否安全。",
"suggestedCode": "```\nfunction resolveMergeBase(cwd, baseRef) {\n // 嚴格的分支名稱白名單檢查,防止路徑穿越與參數注入\n const safeBranchRegex = /^(?!-)(?!.*?\\.\\.)[a-zA-Z0-9/_.-]+$/;\n if (!safeBranchRegex.test(baseRef)) {\n throw new Error(`偵測到不合法的分支名稱: ${baseRef}`);\n }\n const remoteBase = `origin/${baseRef}`;\n```",
"sourceIssue": 16
},
{
"id": "F019",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 13,
"endLine": 72,
"problem": "`redactSecrets()` 與 `agentFailureDetail()` 是低階日誌診斷/遮罩邏輯,現在放在 `review.js` 這個負責 diff 整理與審查決策的模組頂端。這會讓 `review.js` 的職責繼續膨脹:未來若其他模組也要安全輸出 CLI 錯誤,只能複製這段或反向依賴 review 模組,邊界會越來越不清楚。",
"suggestion": "把這兩個函式搬到專門的工具模組,例如 `src/lib/diagnostics.js` 或 `src/lib/log-redaction.js`,並由 `review.js` 引入。這樣遮罩規則可集中測試與重用,`review.js` 也能維持在「審查流程資料處理」的邊界內。",
"suggestedCode": "",
"sourceIssue": 17
},
{
"id": "F020",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 41,
"endLine": 45,
"problem": "這段註解的旋律前後走調:摘要先說「原始輸出預設隱藏」,下一段卻說失敗時「預設附上 stderr 與 stdout」。讀者還沒進函式本體,文件本身就已經互相拉扯。",
"suggestion": "請讓摘要與實作同拍,直接說明會輸出經遮罩與限長的診斷片段;若真的要隱藏原始輸出,也應同步改實作。",
"suggestedCode": "```\n* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,並附上經遮罩與限長的 stderrstdout 片段。\n```",
"sourceIssue": 17
},
{
"id": "F021",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/templates.js",
"startLine": 334,
"endLine": 338,
"problem": "`issueFindingComment` 的文件只唱「嚴重 finding」,但新版流程也讓警告與建議逐條發到 issue。函式名稱是通用的,註解卻把用途寫窄,後續讀者會誤以為它只服務嚴重問題。",
"suggestion": "把註解改成涵蓋所有 finding 等級,讓文件與函式名稱、呼叫情境保持一致。",
"suggestedCode": "```\n* 使用情境:建問題模式下,`review.postSevereToIssue` 與 `review.postOthersToIssue`\n * 會把各等級 finding 以本函式產生留言內容、經 `gitea.createCommentOnIssue`\n * 發布到追蹤 issue 上,作為問題明細的追蹤紀錄。\n```",
"sourceIssue": 17
},
{
"id": "F022",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "警告",
"file": "readme.md",
"startLine": 76,
"endLine": 132,
"problem": "README 內大量函式清單同時硬編分支名稱與行號錨點,這次 diff 已經整批從 `master` 改成 `develop` 並同步調整行號。這類文件和原始碼結構高度重複,後續只要插入幾行程式,文件連結就會失準,維護者必須靠人工記得同步整張表。",
"suggestion": "改成不含行號的穩定檔案連結,或把這份 API/功能表改由 JSDoc/腳本產生。若一定要保留行號,建議把產生流程寫入 npm script,避免每次程式碼位移都人工批次修改 README。",
"suggestedCode": "",
"sourceIssue": 18
},
{
"id": "F023",
"reviewer": "Mage",
"focus": "",
"badge": "🔮",
"severity": "警告",
"file": "src/index.js",
"startLine": 374,
"endLine": 387,
"problem": "建問題模式下只要有任何保留 finding 就會建立 issue,且後續一律把 PR 設為相依於該 issue;但收尾結果仍是 `severe.length === 0 ? 'success' : 'failure'`。最小情境:攻擊方只產生 1 條「建議」,`severe.length` 為 0action commit `[success]`,但 PR 被 issue dependency 擋住無法合併。這讓「success=可通過」與「非嚴重問題也阻擋合併」兩個語義互相矛盾。",
"suggestion": "明確對齊語義:若只有嚴重問題才應阻擋合併,則只在 `severe.length > 0` 時建立 dependency;若所有保留問題都要阻擋合併,則 result/exit code 不應只看嚴重問題。",
"suggestedCode": "```\nif (severe.length > 0) {\n try {\n await gitea.addIssueDependency(ctx, ctx.prNumber, issue.number);\n log('建問題', 'INF', `已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number}。`);\n } catch (err) {\n log('建問題', 'WRN', `設定 PR 相依失敗(可能未啟用「問題相依」功能):${err.message}。`);\n }\n}\n```",
"sourceIssue": 18
},
{
"id": "F024",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/index.js",
"startLine": 179,
"endLine": 181,
"problem": "`issueBuffer` 的旋律太含糊:讀者會以為裡面放的是 issue,實際上暫存的是尚未送出的留言 body。命名沒有把資料形狀唱清楚。",
"suggestion": "改成能描述內容與用途的名稱,例如 `pendingIssueCommentBodies`,並同步調整註解與迴圈變數。",
"suggestedCode": "```\nconst pendingIssueCommentBodies = [];\nlet issue = null;\n```",
"sourceIssue": 18
},
{
"id": "F025",
"reviewer": "Assassin",
"focus": "",
"badge": "🗡️",
"severity": "警告",
"file": "action.yml",
"startLine": 21,
"endLine": 24,
"problem": "這裡建議呼叫端傳入「能觸發 CI 的 PAT」作為 action token。攻擊者最愛這種長效、可推送、可觸發 workflow 的憑證:只要此 action 在不受信任 PR 上執行,或 PR 能影響 action/workflow 執行內容,惡意變更就可能讀取 `INPUT_TOKEN`、推送結果 commit、再藉由可觸發 CI 的身分製造後續執行鏈。自動 token 原本不觸發 CI 是一道防線,這個建議等於要求使用者把防線拆掉。",
"suggestion": "不要泛稱建議使用可觸發 CI 的 PAT。文件與介面應明確要求最小權限、repo 限定、短效或可輪替 token,並禁止在 fork/不受信任 PR context 暴露 PAT。更穩的設計是分離 API 留言 token 與 push token,且只有在明確受信任事件或受保護分支才允許 push token 存在;否則拒絕 commit/push,只做留言或 artifact。",
"suggestedCode": "",
"sourceIssue": 19
},
{
"id": "F026",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "readme.md",
"startLine": 59,
"endLine": 132,
"problem": "README 的功能列表手動維護了大量 `src/branch/develop/...#Lxx` 深連結與行號。這次 PR 已經一次改動數十個 branch/line anchor,代表文件和原始碼行號高度耦合;下一次只要插入幾行程式,文件就會悄悄過期,維護者很難知道哪些連結還準。",
"suggestion": "避免在手寫 README 綁定行號,改連到函式所在檔案或穩定章節錨點;若必須保留行號,請把這段改成產生式文件,讓 CI 或腳本從原始碼/JSDoc 重新生成,減少人工同步成本。",
"suggestedCode": "",
"sourceIssue": 19
},
{
"id": "F027",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/index.js",
"startLine": 181,
"endLine": 185,
"problem": "`issue` 這個變數名太素,像樂譜上只寫「音符」卻不說是哪一聲部。此處承載的是建問題模式建立出的追蹤 issue,後面還會與 PR issue 編號、Gitea issue API 參數交錯出現,名稱過泛會讓閱讀節奏變濁。",
"suggestion": "改成能表明角色的名稱,例如 `trackingIssue`。對應的 `ensureIssueCreated` 也可改為 `ensureTrackingIssueCreated`,讓閉包狀態與用途一眼對上。",
"suggestedCode": "```\nconst issueBuffer = [];\nlet trackingIssue = null;\n\nconst postComment = async (body) => {\n if (ctx.createIssue) {\n if (trackingIssue) return gitea.createCommentOnIssue(ctx, trackingIssue.number, body);\n issueBuffer.push(body);\n return null;\n }\n const created = await gitea.createIssueComment(ctx, body);\n currentRunCommentIds.add(created.id);\n return created;\n};\n```",
"sourceIssue": 19
},
{
"id": "F028",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 14,
"endLine": 75,
"problem": "`review.js` 這次新增 `redactSecrets()` 與 `agentFailureDetail()`,但這兩個函式處理的是 AI CLI 執行失敗診斷與機密遮罩,責任更接近 `agents.js` 或共用 log/sanitize 工具。現在審查結果整理模組同時負責 diff、裁決、issue 發文與 CLI 診斷格式,模組邊界越來越鬆;之後其他地方若也要記錄 agent 失敗,很容易複製一份遮罩邏輯或反向依賴 `review.js`。",
"suggestion": "將這兩個函式移到 `src/lib/agents.js`(例如匯出 `formatAgentFailure()`),或新增 `src/lib/sanitize.js`/`src/lib/diagnostics.js`。`review.js` 只消費格式化後的錯誤摘要,避免讓審查編排模組承擔 CLI 診斷細節。",
"suggestedCode": "",
"sourceIssue": 19
},
{
"id": "F029",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/templates.js",
"startLine": 388,
"endLine": 405,
"problem": "`issueLinkComment` 產生的是 PR 上唯一的建問題模式回貼留言,但名稱少了 PR 的聲部;同檔已有 `issueBody`、`issueFindingComment`,乍看會以為這也是 issue 內留言模板,命名層次不夠分明。",
"suggestion": "改名為 `prIssueLinkComment` 或 `trackingIssueLinkComment`,讓模板的投遞位置與用途直接寫在名稱裡,避免與 issue 內文、issue finding 留言混成一團。",
"suggestedCode": "```\nfunction prIssueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) {\n return `${MARK}\n## 🔍 AI Code Review|已建立追蹤問題\n\n本次審查結果已彙整到 issue [#${issueNumber}](${issueUrl})(🔴 嚴重 ${severeCount} 條、🟠🔵 警告+建議 ${otherCount} 條),請至該問題追蹤與討論。`;\n}\n```",
"sourceIssue": 19
},
{
"id": "F030",
"reviewer": "Rogue",
"focus": "",
"badge": "⚡",
"severity": "警告",
"file": "src/index.js",
"startLine": 369,
"endLine": 370,
"problem": "建問題模式把所有警告/建議改成逐條發 issue 留言,這裡會把 `others.length` 放大成 N 次遠端 POST;正常模式同一批資料只產生 1 則彙整表格留言。只要 AI 回出數十條警告,CI 時間就會被 API round-trip 線性吃掉,還更容易撞上 Gitea rate limit 或暫時性網路延遲。",
"suggestion": "警告/建議維持批次彙整成單一留言;只有嚴重問題需要逐條追蹤時再拆開。若產品需求一定要逐條回覆,至少在 `postOthersToIssue` 內用有上限的並行池,不要一筆等一筆。",
"suggestedCode": "```\nif (others.length > 0) {\n if (ctx.createIssue) {\n await gitea.createCommentOnIssue(ctx, issue.number, templates.othersComment(others));\n log('步驟10', 'INF', `警告+建議表格留言已發布到 issue${others.length} 條)。`);\n } else {\n await postComment(templates.othersComment(others));\n log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`);\n }\n}\n```",
"sourceIssue": 20
},
{
"id": "F031",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "readme.md",
"startLine": 56,
"endLine": 132,
"problem": "README 的功能表大量硬編遠端分支名稱與行號,這次只是從 `master` 改成 `develop` 並同步行號,但這種文件很容易在下一次函式移動、預設分支更名或重排時再次整批失準。未來維護者會被迫反覆做低價值的連結校正,文件也可能在沒人注意時指到錯誤位置。",
"suggestion": "若 README 是 repo 內文件,優先改成相對路徑連結,並避免固定行號;若必須保留行號,建議用產生腳本統一輸出這張表,讓分支名與行號只從單一來源計算。",
"suggestedCode": "",
"sourceIssue": 21
},
{
"id": "F032",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/index.js",
"startLine": 183,
"endLine": 224,
"problem": "`issue` 與 `issueBuffer` 的命名過於泛泛;在 Gitea 裡 PR 也是 issue,追蹤問題也是 issue,單靠 `issue` 這個名字無法唱出它究竟是哪一個聲部。",
"suggestion": "建議改成更具語義的名稱,例如 `trackingIssue`、`trackingIssueCommentBuffer`,讓讀者不用回讀 create-issue 模式的整段脈絡。",
"suggestedCode": "```\nlet trackingIssue = null;\nconst trackingIssueCommentBuffer = [];\n```",
"sourceIssue": 21
},
{
"id": "F033",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/gitea.js",
"startLine": 190,
"endLine": 194,
"problem": "`addIssueDependency(ctx, issueNumber, dependency)` 的兩個參數名稱太相似,且 `dependency` 少了 issue 語義。這支 API 的方向性本來就容易讀錯,命名再模糊就像兩個音符共用同一個名字。",
"suggestion": "建議把參數改成能表達方向的名稱,例如 `blockedIssueNumber` 與 `blockingIssueNumber`,呼叫端也會更清楚是誰被誰擋住。",
"suggestedCode": "```\nfunction addIssueDependency(ctx, blockedIssueNumber, blockingIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${blockedIssueNumber}/dependencies`, {\n index: blockingIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```",
"sourceIssue": 21
},
{
"id": "F034",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 36,
"endLine": 43,
"problem": "`agentFailureDetail` 的 JSDoc 先說「原始輸出預設隱藏」,下一段又說「預設附上 stderr 與 stdout 片段」。同一段說明前後轉調,讀者會搞不清楚失敗診斷到底會不會輸出 CLI 內容。",
"suggestion": "請統一描述:若設計是輸出已遮罩片段,就刪掉「預設隱藏」;若設計是隱藏原始輸出,就把後段改成條件式說明。",
"suggestedCode": "",
"sourceIssue": 21
},
{
"id": "F035",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/templates.js",
"startLine": 334,
"endLine": 338,
"problem": "`issueFindingComment` 是通用 finding 留言模板,但更新後的說明只寫 `review.postSevereToIssue` 與「嚴重 finding」。然而主流程也將警告/建議逐條發到 issue,這段文件把模板唱窄了,和實際用途不一致。",
"suggestion": "把 remarks 改成涵蓋嚴重、警告與建議的通用 issue finding 留言,避免日後維護者誤以為此模板只能用於嚴重問題。",
"suggestedCode": "",
"sourceIssue": 21
},
{
"id": "F036",
"reviewer": "Rogue",
"focus": "",
"badge": "⚡",
"severity": "警告",
"file": "src/index.js",
"startLine": 305,
"endLine": 305,
"problem": "攻擊方沒有找出任何 finding 時,這裡還是照樣啟動防守方 `runDefenders`。空陣列沒有東西可裁決,卻會多跑一輪 AI CLI/sub agent、讀 exclusions/history、組 prompt;每個乾淨 PR 都被偷走 1 次防守方呼叫的 CPU、等待時間與 token。",
"suggestion": "在 `findings.length === 0` 時直接略過防守方裁決,令 `kept/excluded` 都是空陣列,直接進入保存結果與收尾。這不是微優化,是整輪 AI 呼叫直接歸零。",
"suggestedCode": "```\nconst { kept, excluded } = findings.length === 0\n ? { kept: [], excluded: [] }\n : await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });\n```",
"sourceIssue": 22
},
{
"id": "F037",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/gitrepo.js",
"startLine": 310,
"endLine": 310,
"problem": "`pushWithCredential` 的參數名叫 `secret`,但同一檔其他區段與呼叫端都稱它為 `token`;同一個旋律忽然換調,讀者需要多花心力確認這是不是另一種憑證。",
"suggestion": "沿用既有命名,把 `secret` 改成 `token`,並同步調整 JSDoc 與 `Buffer.from` 內的引用。",
"suggestedCode": "```\nfunction pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl) {\n const basic = Buffer.from(`ai-review-bot:${token}`).toString('base64');\n // ...\n}\n```",
"sourceIssue": 22
},
{
"id": "F038",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 31,
"endLine": 46,
"problem": "`agentFailureDetail` 的註解前後語意不一致:開頭寫「原始輸出預設隱藏」,但後段又說預設會附上遮罩後的 stderr/stdout;最後還提到用 `ACTIONS_STEP_DEBUG=true` 取得原始輸出,但程式碼沒有任何 debug flag 分支。這種文件與實作脫節,會讓未來維護者誤判 CI log 會暴露多少診斷內容。",
"suggestion": "把註解改成符合目前實作:預設輸出限長且遮罩後的 stderr/stdout;若要支援 debug 模式,再補實作分支。不要在註解承諾程式沒有做的行為。",
"suggestedCode": "",
"sourceIssue": 22
},
{
"id": "F039",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/templates.js",
"startLine": 337,
"endLine": 340,
"problem": "`issueFindingComment` 是一般 finding 留言模板,但註解只寫 `review.postSevereToIssue` 的嚴重問題用途;和主流程中「警告+建議也逐條發到 issue」的描述不一致,註解像只唱了半段副歌。",
"suggestion": "把 remarks 改成涵蓋嚴重、警告與建議的共用用途,避免後續維護者誤以為此模板只服務嚴重問題。",
"suggestedCode": "",
"sourceIssue": 22
},
{
"id": "F040",
"reviewer": "Assassin",
"focus": "",
"badge": "🗡️",
"severity": "警告",
"file": "action.yml",
"startLine": 19,
"endLine": 22,
"problem": "這段新增說明鼓勵呼叫端傳入「能觸發 CI 的 PAT」。攻擊者最喜歡這種長效、高權限、可觸發 workflow 的憑證:若 action 跑在不可信 PR、AI CLI 被 prompt injection 誘導讀環境變數,或同 repo PR 可改動本 action 程式碼,就可能把 PAT 外送或濫用成寫入 repo/觸發 CI 的跳板。",
"suggestion": "不要把長效 PAT 當建議預設。改用最小權限、短效的 GitHub AppGitea App token,並明確禁止在不可信 fork PR 傳入可寫 token。若目標只是回報檢查結果,優先用 status/check API 寫結果,不要靠 PAT push 再觸發下一輪 CI。",
"suggestedCode": "```\ndescription: 'Gitea API tokenPR/issue 留言與 push findings 用;請使用最小權限、短效 token,勿在不可信 PR 傳入長效 PAT'\n```",
"sourceIssue": 23
},
{
"id": "F041",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "readme.md",
"startLine": 58,
"endLine": 131,
"problem": "README 的功能表把分支名稱與行號大量硬編在外部 URL 裡,這次已經需要整批 `master` 改 `develop` 並同步多個 `#Lxx`。這類文件會隨任何程式碼插行、函式移動或預設分支變更而失準,維護成本會線性累積,最後讀者點到的文件比沒有文件更誤導。",
"suggestion": "改用 repo 相對連結、不固定行號,或把這段功能表改由腳本從 JSDoc 自動產生。若需要連到特定實作,優先連到檔案或錨點,避免每次重排程式碼都要同步更新幾十個行號。",
"suggestedCode": "```\n| log.taipeiNow | [src/lib/log.js](src/lib/log.js) | 取得台北時區 yyyy/MM/dd HH:mm:ss 時間字串 |\n| review.runAttackers | [src/lib/review.js](src/lib/review.js) | 攻擊方 sub agent 並行找問題並合併列表 |\n```",
"sourceIssue": 23
},
{
"id": "F042",
"reviewer": "Bard",
"focus": "",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 31,
"endLine": 40,
"problem": "這段註解的旋律前後失和:開頭寫「原始輸出預設隱藏」,下一句卻說預設附上 stderr 與 stdout 片段。讀者尚未進入程式碼,就已被兩個互相拉扯的描述絆住。",
"suggestion": "請讓文件只唱一個調性:若目前設計是預設輸出遮罩後的診斷片段,就刪掉「原始輸出預設隱藏」或改成「原始輸出會先遮罩與截斷」。",
"suggestedCode": "",
"sourceIssue": 23
}
],
"excluded": []
}