refresh review pipeline
This commit is contained in:
+98
-15
@@ -28,8 +28,11 @@ function findingRow(f) {
|
||||
* 將多筆 findings 組成完整的 Markdown 表格(含表頭與分隔列)。
|
||||
*
|
||||
* @param {Array<object>} findings 審查問題陣列;空陣列時僅輸出表頭與分隔列。每筆物件格式見 {@link findingRow}。
|
||||
* 注意:本參數必須是陣列,傳入 null/undefined 會在 `.map` 呼叫時拋出 TypeError(未防呆,需人工確認是否要補強)。
|
||||
* @returns {string} 完整的 Markdown 表格字串(表頭:等級|審查員|位置|建議)。
|
||||
* @remarks 內部輔助函式,供發布舊問題、新問題(非嚴重)、單筆嚴重問題等 comment 內文使用。
|
||||
* @remarks 內部輔助函式,供 {@link postOldFindingsComment}、{@link postNewNonCriticalComment}、
|
||||
* {@link postNewCriticalComments} 組裝 comment 內文使用。
|
||||
* 使用情境:任何要把一批 findings 呈現成單一 Markdown 表格的地方,先篩好要顯示的子集合再呼叫本函式。
|
||||
*/
|
||||
function buildTable(findings) {
|
||||
const rows = findings.map(findingRow).join('\n');
|
||||
@@ -60,8 +63,16 @@ const bySeverity = (a, b) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析 finding 的 location 取出檔案與行號,供行內 comment 標註使用。
|
||||
* 支援 "file:19" 與 "file:70-82"(取起始行);無行號或含多個檔案(逗號)時回傳 null。
|
||||
* 解析 finding 的 `location` 欄位,取出檔案路徑與(起始)行號,供行內 comment 標註使用。
|
||||
*
|
||||
* @param {string} location finding 的位置字串。支援 `"file:19"`(單行)與 `"file:70-82"`(範圍,僅取起始行 19/70);
|
||||
* 若包含逗號(代表對應多個檔案)、非字串、或無法比對出行號,一律視為無法定位。
|
||||
* @returns {{ file: string, line: number } | null}
|
||||
* 可解析時回傳 `{ file, line }`(line 為正整數起始行);`location` 非字串、含逗號、格式不符、
|
||||
* 或行號非正整數時回傳 `null`。
|
||||
* @remarks 供 {@link toReviewComment} 與 {@link postNewCriticalComments} 判斷 finding 是否能標註到
|
||||
* diff 中的具體檔案行號;回傳 `null` 時呼叫端會降級為一般(非行內)comment。
|
||||
* 使用情境:任何要把 finding 轉成 Gitea 行內 review comment 前,都應先呼叫本函式確認可定位。
|
||||
*/
|
||||
export function parseLocation(location) {
|
||||
if (typeof location !== 'string') return null;
|
||||
@@ -73,7 +84,18 @@ export function parseLocation(location) {
|
||||
return line > 0 ? { file: match[1], line } : null;
|
||||
}
|
||||
|
||||
/** 行內 comment 內容:等級/審查員/建議 */
|
||||
/**
|
||||
* 產生單一 finding 的行內(inline)review comment 內文:等級/審查員/建議三行。
|
||||
*
|
||||
* @param {{ level?: string, role?: string, suggestion?: string }} f 單筆審查問題物件。
|
||||
* `role`、`suggestion` 未定義時會直接輸出 `undefined` 字樣(未做防呆轉換)。
|
||||
* @returns {string} 三行 Markdown 字串,以 `\n` 連接。
|
||||
* @remarks 內部輔助函式,僅供 {@link postNewCriticalComments} 在成功解析出行號({@link parseLocation}
|
||||
* 回傳非 null)時,組裝要標註到具體檔案行號的行內 comment 使用;相較 {@link reviewCommentBody}
|
||||
* 少了「問題」一行,因為行內位置本身已能定位問題所在。
|
||||
* 使用情境:僅用於新(`is_new` 為 truthy)且等級為 `critical` 的 finding,且該 finding 的
|
||||
* `location` 能被解析出具體行號時。
|
||||
*/
|
||||
function inlineCommentBody(f) {
|
||||
return `**等級**:${levelText(f)}\n**審查員**:${f.role}\n**建議**:${f.suggestion}`;
|
||||
}
|
||||
@@ -213,10 +235,28 @@ function toReviewComment(f) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 發布單一 Gitea review:
|
||||
* - summaryFindings 只用來統計本文數字(含新舊問題)
|
||||
* - commentFindings 用來產生 review comments,並依嚴重等級排序;
|
||||
* 只為新問題加上行內標註,舊問題(is_new === false)僅計入統計、不再重複標註檔案與行數
|
||||
* 發布單一 Gitea review:一次性送出「統計摘要 + 逐筆行內 review comment」,並提供多層降級機制。
|
||||
*
|
||||
* @param {Array<object>} findings 本次審查的完整 findings 陣列;當 `deps.summaryFindings` 或
|
||||
* `deps.commentFindings` 未提供時,兩者皆預設使用此參數。
|
||||
* @param {object} [deps={}] 可覆寫的相依注入物件(主要供測試替換,正常情境可省略)。
|
||||
* @param {Function} [deps.postReview=postPullReview] 發布整批 review(含 body 與 comments)的函式。
|
||||
* @param {Function} [deps.postInline=postPullReviewComment] 發布單筆行內 review comment 的函式。
|
||||
* @param {Function} [deps.postIssue=postComment] 發布一般(非 review)comment 的函式,作為最終降級手段。
|
||||
* @param {Array<object>} [deps.summaryFindings=findings] 用於統計本文數字(含新舊問題)的 findings 子集合。
|
||||
* @param {Array<object>} [deps.commentFindings=findings] 用於產生 review comments 的 findings 子集合;
|
||||
* 會先依 {@link bySeverity} 排序,僅新問題(`is_new !== false`)會被轉成行內 comment,
|
||||
* 舊問題只計入統計、不再重複標註檔案與行數。
|
||||
* @param {string} [deps.usageSection=''] 附加在統計表之後的用量/token 統計區塊;空字串時不附加。
|
||||
* @returns {Promise<void>} 無回傳值。
|
||||
* @remarks
|
||||
* 降級順序:① 整批 `postReview`(含 comments)→ 失敗則 ② 僅 body 的 `postReview`
|
||||
* (comments 為空陣列)→ 失敗則 ③ `postIssue(body)`。**注意:③ 未包在 try/catch 中**,
|
||||
* 若 `postIssue` 本身失敗,例外會直接從本函式往外拋出(reject),呼叫端須自行 catch。
|
||||
* 無論走到哪一步,只要走完 ①~③ 中任一步不再往下失敗,後續都會逐筆嘗試 `postInline` 補發
|
||||
* 行內 comment,每筆各自失敗僅記錄 warn 並略過,不影響其他筆。
|
||||
* 使用情境:CI 流程完成一輪 AI Code Review 後,呼叫一次本函式即可把整批結果發布到 Gitea PR;
|
||||
* 單元測試時可透過 `deps` 注入假的 `postReview`/`postInline`/`postIssue` 以驗證各降級分支。
|
||||
*/
|
||||
export async function postFindingsReview(findings, deps = {}) {
|
||||
const {
|
||||
@@ -254,8 +294,19 @@ export async function postFindingsReview(findings, deps = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 寫入 findings.json。
|
||||
* 預設寫到 workspace;若提供 mirrorDir,則同步寫入另一份供 repo commit 使用。
|
||||
* 將 findings 寫入 `findings.json`(同步阻塞 I/O)。
|
||||
*
|
||||
* @param {string} workspace 主要輸出目錄;實際寫入路徑為 `path.join(workspace, FINDINGS_PATH)`。
|
||||
* @param {Array<object>} findings 要寫入的 findings 陣列;會以 `JSON.stringify(findings, null, 2)` 序列化,
|
||||
* 並在檔尾補一個換行字元。
|
||||
* @param {?string} [mirrorDir=null] 額外鏡射輸出目錄(例如供後續 repo commit 使用);
|
||||
* 為 `null`/`undefined`,或與 `workspace` 相同時,只會寫入一份(不重複寫入同一路徑)。
|
||||
* @returns {void} 無回傳值;成功時每個目標各記錄一行 log。
|
||||
* @remarks 每個目標寫入前皆會以 `fs.mkdirSync(..., { recursive: true })` 建立必要的父目錄。
|
||||
* 本函式為同步阻塞呼叫,且**未做例外防護**——`fs.mkdirSync`/`fs.writeFileSync` 拋出的例外
|
||||
* (例如權限不足、磁碟已滿)會直接向呼叫端傳播,需人工確認呼叫端是否需要額外 try/catch。
|
||||
* 使用情境:每輪 AI Code Review 完成、findings 已定案後呼叫一次,將結果落地成 JSON 檔;
|
||||
* 若同時需要寫回 workspace 與 repo 兩個位置,傳入 `mirrorDir` 即可一次呼叫完成兩份寫入。
|
||||
*/
|
||||
export function saveFindings(workspace, findings, mirrorDir = null) {
|
||||
const targets = [workspace];
|
||||
@@ -270,7 +321,16 @@ export function saveFindings(workspace, findings, mirrorDir = null) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 發布所有舊問題 comment(一次發布,依等級排序)
|
||||
* 發布所有舊問題的彙總 comment(一次性發布一則一般 comment,不含行內標註)。
|
||||
*
|
||||
* @param {Array<{ is_new?: boolean, level?: string }>} findings 審查問題陣列;
|
||||
* 本函式以 `!f.is_new` 篩選舊問題——`is_new` 為 `false`、`undefined` 或其他 falsy 值皆視為舊問題
|
||||
* (注意:此判定與 {@link newFindingsOnly} 的 `is_new !== false` 不同,`undefined` 在此處被視為
|
||||
* 「舊」而非「新」,是否為預期設計需人工確認)。
|
||||
* @returns {Promise<void>} 無回傳值;`old.length === 0` 時直接 return,不會呼叫 `postComment`。
|
||||
* @remarks 資料列**未依等級排序**,維持 `findings` 原始輸入順序輸出(與 {@link postFindingsReview}
|
||||
* 內部先用 `bySeverity` 排序的行為不同,請勿假設本函式輸出已排序)。
|
||||
* 使用情境:每輪 AI Code Review 收斂新舊問題後,統一針對「仍未解決的舊問題」發一則彙總說明。
|
||||
*/
|
||||
export async function postOldFindingsComment(findings) {
|
||||
const old = findings.filter(f => !f.is_new);
|
||||
@@ -284,7 +344,17 @@ export async function postOldFindingsComment(findings) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 發布新問題中非 critical 的 comment(一次發布)
|
||||
* 發布新問題中非 critical 等級者的彙總 comment(一次性發布一則一般 comment)。
|
||||
*
|
||||
* @param {Array<{ is_new?: boolean, level?: string }>} findings 審查問題陣列;
|
||||
* 以 `f.is_new && f.level !== 'critical'` 篩選——`is_new` 須為 truthy(例如 `true`)才算新問題,
|
||||
* `undefined`/`false` 皆會被排除(注意:此判定比 {@link newFindingsOnly} 的
|
||||
* `is_new !== false` 更嚴格,兩者對 `undefined` 的處理方向相反,是否為預期設計需人工確認)。
|
||||
* `level !== 'critical'` 涵蓋 `warning`、`info` 及任何非 `'critical'` 的其他值(含未知等級字串)。
|
||||
* @returns {Promise<void>} 無回傳值;`items.length === 0` 時直接 return,不會呼叫 `postComment`。
|
||||
* @remarks 資料列未依等級排序,維持 `findings` 原始輸入順序輸出。
|
||||
* 使用情境:每輪 AI Code Review 中,把「明確標記為新(`is_new === true`)且非嚴重」的問題
|
||||
* 統一彙總成一則 comment 通知,嚴重問題另由 {@link postNewCriticalComments} 逐筆單獨發布。
|
||||
*/
|
||||
export async function postNewNonCriticalComment(findings) {
|
||||
const items = findings.filter(f => f.is_new && f.level !== 'critical');
|
||||
@@ -298,9 +368,22 @@ export async function postNewNonCriticalComment(findings) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每個新 critical 問題各發一個 comment。
|
||||
* 優先用 Gitea 行內 review comment 標註問題檔案與行數(內容為等級/審查員/建議);
|
||||
* 若 location 無法解析出行號,或行內發布失敗(例如該行不在 diff 範圍),則降級為一般 comment。
|
||||
* 針對每個新的 critical 問題各發一個 comment;優先用 Gitea 行內 review comment 標註問題檔案與行數
|
||||
* (內容為等級/審查員/建議),無法定位或行內發布失敗時降級為一般 comment。
|
||||
*
|
||||
* @param {Array<{ is_new?: boolean, level?: string, location?: string, role?: string, suggestion?: string }>} findings
|
||||
* 審查問題陣列;以 `f.is_new && f.level === 'critical'` 篩選——`is_new` 須為 truthy 才算新問題
|
||||
* (與 {@link newFindingsOnly} 的寬鬆判定不同,`undefined` 會被排除,需人工確認是否為預期設計)。
|
||||
* @param {object} [deps={}] 可覆寫的相依注入物件(主要供測試替換)。
|
||||
* @param {Function} [deps.postInline=postPullReviewComment] 發布單筆行內 review comment 的函式。
|
||||
* @param {Function} [deps.postIssue=postComment] 發布一般 comment 的降級函式。
|
||||
* @returns {Promise<void>} 無回傳值;`criticals.length === 0` 時直接 return。
|
||||
* @remarks 每筆 critical finding 依序處理:`location` 能解析出行號且 `postInline` 成功時只發行內
|
||||
* comment;否則(無法解析,或 `postInline` 失敗且已被捕捉記錄 warn)改用 `postIssue` 發一般 comment。
|
||||
* **注意**:`postIssue` 呼叫未包在 try/catch 中,若其拋出例外會中斷整個迴圈,導致排在後面的
|
||||
* critical findings 不會被處理,此為需人工確認的行為,是否要補強視情況而定。
|
||||
* 使用情境:每輪 AI Code Review 中,把「明確標記為新且等級為 critical」的問題逐筆單獨標註到
|
||||
* PR 對應的檔案行號,讓審查者能直接在 diff 上看到問題。
|
||||
*/
|
||||
export async function postNewCriticalComments(findings, deps = {}) {
|
||||
const { postInline = postPullReviewComment, postIssue = postComment } = deps;
|
||||
|
||||
Reference in New Issue
Block a user