feat: 導入 AI 程式碼審查 action 並修正進入點與參數接線 #1

Merged
admin merged 30 commits from ai-review-resolve/develop-20260702-160700 into develop 2026-07-03 10:04:33 +00:00
Showing only changes of commit fc5baf32db - Show all commits
+266
View File
@@ -0,0 +1,266 @@
[
{
"level": "critical",
"role": "Assassin",
"location": "src/config.js:7",
"problem": "這裡把 `NODE_TLS_REJECT_UNAUTHORIZED` 全域設為 `0`,等於讓整個 Node 程序放棄 TLS 憑證驗證。攻擊者只要能站到 runner 與 GiteaLLM/任何 HTTPS API 之間,就能用偽造憑證攔截或竄改 diff、review 結果、token 驗證流程,甚至偷走 Authorization header。",
"suggestion": "移除全域停用 TLS 的設定。若內部自簽 CA 是必要情境,請改用可設定的 CA bundle(例如 `NODE_EXTRA_CA_CERTS`)或僅對明確允許的內部 host 使用專用 agent,且預設必須啟用憑證驗證。",
"is_new": true
},
{
"level": "critical",
"role": "Assassin",
"location": "src/config.js:53",
"problem": "這個 helper 直接建立 `rejectUnauthorized: false` 的 HTTPS agent,後續 Gitea API 與 preflight 都會用它。攻擊者若能進行中間人攻擊,就能假冒 Gitea 回傳惡意 diff、偽造 comment/review API 回應,或攔截寫入用 token。",
"suggestion": "不要提供預設不驗證憑證的 agent。改成預設安全驗證;若真的要支援自簽憑證,請要求使用者明確提供信任的 CA 憑證,或以白名單 host 加上明確 opt-in 的設定限制風險。",
"is_new": true
},
{
"level": "critical",
"role": "Assassin",
"location": "src/resolve.js:253",
"problem": "這裡會把 PR 上所有未解決的 review comment ID 全部送去 resolve,而不是只處理 AI Review bot 自己建立的 thread。攻擊者只要開 PR 觸發這個 action,就可能讓 bot 關閉人類審查者留下的安全疑慮或阻擋性對話,繞過人工審查流程。",
"suggestion": "只 resolve 可證明由本 bot 建立且格式符合預期的 comment,例如檢查作者、固定 marker、review body 簽章或 botFinding 解析結果;人類留言與未知格式留言不得自動關閉。",
"is_new": true
},
{
"level": "critical",
"role": "Mage",
"location": "src/findings.js:471",
"problem": "當排除條目有 location 或 role 時,這裡直接把文字比對結果短路成 true。最小重現:exclusions.json 只有 `{ \"location\": \"app/a.js:10\", \"original_finding\": \"誤報 A\" }`,新的 finding 是 `app/a.js:99` 且 suggestion 完全不同,仍會因同檔案而被排除,導致真問題被靜默丟掉。",
"suggestion": "不要用 `exPath || ex.role ? true : textMatches` 跳過文字比對;應至少要求位置精確匹配到同一行,或在同檔/同角色時仍必須通過 `textMatches`,例如 `return locationMatches && roleMatches && textMatches`,並明確定義 suggestion 空白時才是萬用規則。",
"is_new": true
},
{
"level": "warning",
"role": "Assassin",
"location": "src/findings.js:14",
"problem": "這裡把未信任的 Git diff 直接送進 LLM。攻擊者可以在新增程式碼或註解中塞入提示詞注入內容,例如要求模型忽略安全問題、回傳空陣列或偽造低風險 findings,藉此讓自動安全審查失明。",
"suggestion": "在分析 prompt 中明確標示 diff 是不可信資料,要求模型忽略 diff 內任何指令;同時加入結構化封裝、輸出 schema 驗證與必要的規則式安全檢查,避免完全依賴可被 prompt injection 操控的 LLM 判斷。",
"is_new": true
},
{
"level": "warning",
"role": "Bard",
"location": "src/main.js:2",
"problem": "這一行 import 把大量設定常數擠成長長一串,讀起來像沒有換氣的樂句,與後續同檔案多個長 import 一起讓檔案開頭難以掃描。",
"suggestion": "將多項具名 import 改成多行排列,並依來源模組分組維持一致節奏,例如每個匯入項目獨立一行。",
"is_new": true
},
{
"level": "warning",
"role": "Bard",
"location": "src/findings.js:4",
"problem": "這行把四個 prompt/role helper 壓在同一行,與檔案中龐大的流程函式相比,開頭的依賴清單先失了拍,降低可讀性。",
"suggestion": "改為多行具名 import,讓每個 helper 名稱清楚露出,並與其他長 import 採相同格式。",
"is_new": true
},
{
"level": "warning",
"role": "Bard",
"location": "src/gitea.js:2",
"problem": "Gitea 設定匯入一口氣列出八個名稱,行寬過長,讓讀者難以快速分辨這個模組真正依賴哪些環境值。",
"suggestion": "將具名 import 拆成多行,必要時依 token、repo/PR、TLS helper 等語意排序。",
"is_new": true
},
{
"level": "warning",
"role": "Bard",
"location": "src/comments.js:11",
"problem": "大量私有輔助函式都配上篇幅很長的 JSDoc,許多內容只是重述程式碼表面行為,註解的聲量蓋過了旋律本身。",
"suggestion": "保留公開 API 或非直覺決策的文件即可;私有小函式改用簡短註解,或讓函式命名本身說明用途。",
"is_new": true
},
{
"level": "warning",
"role": "Bard",
"location": "src/main.js:18",
"problem": "main() 前的 JSDoc 幾乎把整條 pipeline 逐步重寫一次,和函式內 Step 註解重複,維護時很容易變成兩份會走調的文件。",
"suggestion": "縮短為高階摘要與退出規則;Step 細節留在程式碼附近,避免文件與實作雙重維護。",
"is_new": true
},
{
"level": "warning",
"role": "Leo",
"location": "src/main.js:36",
"problem": "`main()` 把前置驗證、bot commit 判斷、對話收斂、角色分析、合併去重、排除、發布、JSON 驗證、commit/push 與 gate 全部塞在同一個 190 行左右的函式裡,且中間散落多個 `process.exit()`。六個月後要改其中任一步驟時,很難隔離副作用,也不容易針對單一階段寫單元測試。",
"suggestion": "將每個 Step 拆成可注入相依、回傳明確結果的函式,例如 `runAnalysisStep()`、`runFilteringStep()`、`runPublishStep()`;最外層再統一把結果轉成 exit code,讓流程控制與業務邏輯分離。",
"is_new": true
},
{
"level": "warning",
"role": "Leo",
"location": "src/findings.js:381",
"problem": "`loadExclusions()` 同時負責讀檔、解析多種格式、正規化、去重、記錄 repo 狀態、改寫原檔、鏡像寫入與建立 AI prompt 摘要。這個函式的職責過多,之後只要調整 exclusions 格式或同步策略,就很容易牽動不相關行為。",
"suggestion": "拆成 `readExclusionsFile()`、`normalizeExclusionsData()`、`canonicalizeExclusionsFile()`、`logExclusionMetadata()` 等小函式,讓讀取、轉換、寫回與診斷各自可測。",
"is_new": true
},
{
"level": "warning",
"role": "Mage",
"location": "src/findings.js:466",
"problem": "這裡優先使用 `ex.textKey`,但 `textKey` 是由 `toKeyText()` 產生的無分隔且未轉小寫文字,而 findingText 是 `normalizeText()` 產生的小寫、以空白分隔文字。最小重現:排除文字 `Update tests` 會變成 `Updatetests`finding suggestion 會變成 `update tests`,兩邊互相 `includes` 都不成立,導致純文字排除規則失效。",
"suggestion": "排除條目與 finding 應使用同一套正規化函式比對;例如改存並使用 `normalizeText(ex.text || ex.suggestion || ex.title || '')`,或讓 finding 也轉成同樣的 compact/lowercase key。",
"is_new": true
},
{
"level": "warning",
"role": "Mage",
"location": "src/main.js:221",
"problem": "若 Step6 的 `cloneRepo()` 失敗,`repoDir` 會是 undefined,但這裡仍呼叫 `commitAndPush(WORKSPACE, repoDir || WORKSPACE, ...)`。最小重現:遠端 clone 因分支不存在或網路錯誤失敗後,流程降級繼續,最後 Step10 會在 `/workspace` 這個非 git repo 執行 `git config/status/commit`,錯誤只被 `commitAndPush` 吞掉;findings/exclusions 已發布但不會被持久化到 PR 分支,下一輪會遺失記憶狀態。",
"suggestion": "Step10 應在 `repoDir` 不存在時明確跳過 commit/push 並標記持久化失敗,或讓 clone 失敗成為會終止流程的錯誤;不要把 WORKSPACE 當成 repoDir fallback。",
"is_new": true
},
{
"level": "warning",
"role": "Mage",
"location": "src/resolve.js:92",
"problem": "對話分組行號只讀 `position` 或 `original_position`,但新增留言發布時使用的是 `new_position`。若 Gitea 回傳 review comment 只帶 `new_position`,最小重現:同一檔案第 10 行與第 20 行兩則未解決 bot comment 都沒有 `position`,兩者會被合併成 `file|0`,只解析第一個 finding,後續 resolved/open/false_positive 判斷會套錯問題。",
"suggestion": "分組行號應納入 `new_position`,例如 `Number(c?.position) || Number(c?.new_position) || Number(c?.original_position) || 0`,並針對缺行號的 comment 避免把同檔不同對話合併成同一組。",
"is_new": true
},
{
"level": "warning",
"role": "Mage",
"location": "src/findings.js:336",
"problem": "AI 去重回傳只要是非空陣列就被接受,沒有檢查是否比原始 findings 更多。最小重現:原本 3 筆 findings,LLM 異常回傳 20 筆或加入不存在的 location,這裡會直接採用並進入發布與失敗判定,導致憑空產生問題或讓 workflow 誤失敗。",
"suggestion": "去重結果應驗證每筆都能對應回原始 finding,且數量不得大於輸入;無法對應或數量異常時應降級回原始 findings,或只保留 `origMap` 命中的項目。",
"is_new": true
},
{
"level": "warning",
"role": "Maya",
"location": "src/main.js:118",
"problem": "Step5 的角色分析與流程分支是整個 action 的核心,但目前測試沒有覆蓋 main orchestrator:例如所有角色分析都失敗時應 exit 1、部分角色失敗時仍繼續、diff 為空時 exit 0、critical finding 最後應讓 workflow 失敗。這些行為沒有被驗證,等於 pipeline 成敗判斷還沒通過試煉。",
"suggestion": "補上 main 流程層級測試,透過 mock getPRDiff、loadRoles、analyzeWithRole、postFindingsReview、process.exit 等相依,至少覆蓋:diff 空、全部分析失敗、部分分析失敗但繼續、產生 critical 後 exit 1、無 critical 後正常通過。",
"is_new": true
},
{
"level": "warning",
"role": "Maya",
"location": "src/main.js:166",
"problem": "Step7 會把 reconcile.excludedFindings 追加到 exclusions,接著再載入並套用排除規則,但目前缺少整合測試驗證「誤報對話 → 寫入 exclusions → 後續 findings 被排除」這條關鍵路徑。若 append/load/apply 任一環節接錯 workspace 或 mirror,單元測試不一定會抓到。",
"suggestion": "補一個接近流程層級的測試,mock reconcileConversations 回傳 excludedFindings,準備一筆會被排除的新 finding,驗證 appendExclusions 寫入的檔案被 loadExclusions 讀到,且最後 save/post 的 filtered findings 不含該誤報。",
"is_new": true
},
{
"level": "warning",
"role": "Maya",
"location": "src/findings.js:416",
"problem": "applyExclusions 的核心比對支援「只有文字、沒有路徑/角色」的排除規則,但現有測試多半靠相同檔案路徑命中,沒有驗證純文字排除、空文字排除、大小寫/標點差異等邊界。這條排除規則的最脆弱分支還沒被測到。",
"suggestion": "補上 applyExclusions 的邊界測試:只有 suggestion/title 文字沒有 location 的 exclusion 應如何比對;空文字 exclusion 不應意外排除全部;標點、空白、大小寫正規化後相同的文字應依預期排除。",
"is_new": true
},
{
"level": "warning",
"role": "Maya",
"location": "src/gitea.js:95",
"problem": "shouldSkipBotCommit 目前只看到命中 bot marker 的測試,缺少「commit API 失敗、分支查詢失敗、sha/branch 都沒有 marker」時應回 false 的失敗與保守路徑驗證。這是避免 workflow 誤跳過審查的關鍵判斷,不能只測快樂路徑。",
"suggestion": "新增測試讓 getCommitMessageBySha / getBranchHeadCommitMessage 對應的 axios 呼叫拋錯或回一般 commit message,斷言 shouldSkipBotCommit 回 false,且不會把查詢失敗誤判成 bot commit。",
"is_new": true
},
{
"level": "warning",
"role": "Maya",
"location": "src/llm.js:91",
"problem": "runAssistantCLI 有 timeout 與 maxBuffer 兩條重要失敗路徑,但目前測試只覆蓋 CLI 非零退出,沒有驗證逾時會 kill 子程序並拒絕、輸出超過限制會中止且不產生未處理的重複 reject。這些是 CI 上最常見的失敗情境。",
"suggestion": "新增 llm 測試:用假的 CLI sleep 超過 AI_ASSISTANT_TIMEOUT_MS,斷言錯誤訊息包含逾時;再用大量 stdout/stderr 超過 AI_ASSISTANT_MAX_BUFFER,斷言錯誤訊息正確且測試過程沒有 unhandled rejection。",
"is_new": true
},
{
"level": "warning",
"role": "Rogue",
"location": "src/main.js:132",
"problem": "這裡把每個角色的 LLM 分析逐一 await,6 個角色就把總耗時堆成約 6 倍單次模型延遲;這些分析彼此獨立,CPU 沒偷到時間,反而把整條 pipeline 卡在序列網路/CLI 呼叫上。",
"suggestion": "改用 Promise.allSettled 平行執行 roles.map(role => analyzeWithRole(role, diff)),再彙整 fulfilled 結果與 warning;保留 fulfilledAnalyses 的判斷即可。",
"is_new": true
},
{
"level": "warning",
"role": "Rogue",
"location": "src/gitea.js:239",
"problem": "這裡逐一 await 每個 review 的 commentsPR review 一多就變成 N 次遠端呼叫的線性延遲累加;例如 30 個 review 就是 30 個 round-trip 排隊等,時間都被網路空轉偷走。",
"suggestion": "把 reviews.map(review => getPullReviewComments(review.id).catch(...)) 丟進 Promise.all 或 Promise.allSettled 平行抓取,再 flat 結果;單筆失敗仍可記 warn 後略過。",
"is_new": true
},
{
"level": "warning",
"role": "Rogue",
"location": "src/findings.js:430",
"problem": "applyExclusions 在 findings × exclusions 的巢狀比對裡,每遇到一條 exclusion 就重算同一個 finding 的 normalizeTextF 筆 finding、E 條 exclusion 會做最多 F×E 次正規化與正則替換,這是很明顯的 CPU 浪費。",
"suggestion": "先把 findings 預處理成含 fPath、normalizedFindingText 的陣列,exclusions 也先補齊 normalizedExclusionText,再做比對;同一筆文字只正規化一次。",
"is_new": true
},
{
"level": "info",
"role": "Bard",
"location": "src/findings.js:138",
"problem": "註解中留下「不確定」這種未定案語氣,像樂譜上的猶豫記號;讀者無法判斷這是刻意設計、待辦事項,還是審查遺留。",
"suggestion": "若是刻意差異,改寫成明確理由;若待確認,改成可追蹤的 TODO 並標明決策者或議題。",
"is_new": true
},
{
"level": "info",
"role": "Bard",
"location": "src/git.js:226",
"problem": "_sourceRoot` 的參數文件寫著「不確定,待確認」,讓公開函式簽名帶著未完成的旁白,破壞 API 文件的一致與可信度。",
"suggestion": "若參數已不使用,移除它;若為相容性保留,明確寫成 deprecated/compatibility note,不要留下模糊語句。",
"is_new": true
},
{
"level": "info",
"role": "Bard",
"location": "src/config.js:4",
"problem": "註解說「需要內部服務相容時才使用 getInsecureHttpsAgent()」,下一行卻在模組載入時全域設定 TLS 環境變數,文件與程式碼唱了不同旋律。",
"suggestion": "讓註解忠實描述目前行為,或把全域設定移到明確命名的初始化函式;至少避免文件暗示這是選擇性使用。",
"is_new": true
},
{
"level": "info",
"role": "Leo",
"location": "src/json.js:13",
"problem": "`stripCodeFence()` 與 `src/llm.js` 內的 `stripOuterFence()` 幾乎是同一個功能,未來如果要支援更多 fence 格式或修 bug,兩邊需要同步修改,容易產生行為漂移。",
"suggestion": "抽成共用的 JSON/text utility,例如 `src/text.js` 或 `src/json.js` 匯出單一 fence 清理函式,讓 LLM JSON 解析與 JSON repair 共用同一套邏輯。",
"is_new": true
},
{
"level": "info",
"role": "Leo",
"location": "src/findings.js:104",
"problem": "文字正規化邏輯分散在 `normalizeText()`、`toKeyText()`,而 `src/resolve.js` 也有另一套 `normalizeKey()`。這些函式對大小寫、標點與空白的處理不完全一致,長期會讓 finding 去重、排除與對話收斂出現難追的差異。",
"suggestion": "建立單一 normalization 模組,明確定義 `normalizeForDisplayMatch`、`normalizeForSignature` 等用途,再讓 findings、resolve、exclusions 共用,並補上跨模組測試鎖定語意。",
"is_new": true
},
{
"level": "info",
"role": "Leo",
"location": "src/roles.js:7",
"problem": "`ROLES_DIR` 用 `fileURLToPath(import.meta.url)` 直接接 `..` 來推目錄,雖然目前可運作,但語意上把檔案路徑當目錄路徑處理,未來搬檔或重構時不直覺。",
"suggestion": "先用 `path.dirname(fileURLToPath(import.meta.url))` 取得目前模組目錄,再組 `prompts/roles`,讓路徑意圖清楚且不依賴 `..` 抵銷檔名的技巧。",
"is_new": true
},
{
"level": "info",
"role": "Maya",
"location": "src/comments.js:25",
"problem": "Markdown 表格列直接嵌入 role、location、suggestion,但測試沒有覆蓋 suggestion 含 `|`、換行或 Markdown 特殊字元時的輸出。這不是要求現在一定要改格式,而是目前缺少案例確認表格在真實 LLM 輸出下不會被破壞。",
"suggestion": "補一個 comment body 格式測試,輸入 suggestion 含 pipe、換行與粗體符號,斷言輸出的 Markdown 結構符合預期;若目前行為會破表,應先定義轉義或替換規則再測。",
"is_new": true
},
{
"level": "info",
"role": "Rogue",
"location": "src/comments.js:126",
"problem": "countBy 用 filter(predicate).length 只為了計數卻配置中間陣列;formatFindingsStats/formatFindingsStatsLine 每列又重複掃多次,雖然 findings 通常不大,但這是在白白丟記憶體與掃描週期。",
"suggestion": "改成單趟 reduce 統計 new/old × level 的計數表,或讓 countBy 用 for-of 累加數字、不建立 filter 結果陣列。",
"is_new": true
},
{
"level": "info",
"role": "Rogue",
"location": "src/findings.js:382",
"problem": "loadExclusions 前面已經 normalizeExclusionEntry + dedupeExclusions,這裡又呼叫 buildExclusionContext(exclusions) 重新 normalize、dedupe、group 一輪,只為了 log groups 數;排除規則多時會多跑一趟 O(e log e) 的整理成本。",
"suggestion": "讓 buildExclusionContext 可接受已正規化/已去重的 exclusions,或直接在 loadExclusions 重用現有 exclusions 進行 group 統計,避免重複正規化與排序。",
"is_new": true
}
]