fix(review-resolve): 支援 wrapper findings 並修正輸出格式

This commit is contained in:
2026-08-07 16:41:47 +00:00
parent 605d557455
commit 65dcb52777
8 changed files with 184 additions and 109 deletions
+18 -25
View File
@@ -2,6 +2,7 @@ import fs from 'fs';
import path from 'path';
import { postComment, postPullReviewComment, postPullReview } from './gitea.js';
import { FINDINGS_PATH } from './config.js';
import { buildFindingsWrapper } from './json.js';
import { ok, line, warn } from './log.js';
const LEVEL_EMOJI = { critical: '🔴', warning: '🟡', info: '🔵' };
@@ -35,7 +36,8 @@ function findingRow(f) {
* 使用情境:任何要把一批 findings 呈現成單一 Markdown 表格的地方,先篩好要顯示的子集合再呼叫本函式。
*/
function buildTable(findings) {
const rows = findings.map(findingRow).join('\n');
const list = Array.isArray(findings) ? findings : [];
const rows = list.map(findingRow).join('\n');
return `| 等級 | 審查員 | 位置 | 建議 |\n|------|--------|------|------|\n${rows}`;
}
@@ -97,7 +99,7 @@ export function parseLocation(location) {
* `location` 能被解析出具體行號時。
*/
function inlineCommentBody(f) {
return `**等級**:${levelText(f)}\n**審查員**:${f.role}\n**建議**:${f.suggestion}`;
return `**等級**:${levelText(f)}\n**審查員**:${f?.role || 'AI Review'}\n**建議**:${f?.suggestion || ''}`;
}
/**
@@ -123,9 +125,9 @@ function problemText(f) {
function reviewCommentBody(f) {
return [
`**嚴重等級**:${levelText(f)}`,
`**審查員**:${f.role}`,
`**審查員**:${f?.role || 'AI Review'}`,
`**問題**:${problemText(f)}`,
`**建議**:${f.suggestion}`,
`**建議**:${f?.suggestion || ''}`,
].join('\n');
}
@@ -235,15 +237,10 @@ function toReviewComment(f) {
}
/**
* 發布單一 Gitea review,必要時會先降級成 summary review,再降級成一般 comment。
* 發布單一 Gitea review,必要時降級成 summary review 或一般 comment。
*
* @param {Array<object>} findings 審查 findings。
* @param {object} [deps={}] 可注入的相依物件。
* @param {Function} [deps.postReview=postPullReview] 發布整批 review 的函式。
* @param {Function} [deps.postInline=postPullReviewComment] 發布單筆行內 comment 的函式。
* @param {Function} [deps.postIssue=postComment] 發布一般 comment 的降級函式。
* @param {Array<object>} [deps.summaryFindings=findings] 用於統計的 findings 子集合。
* @param {Array<object>} [deps.commentFindings=findings] 用於建立 review comments 的 findings 子集合。
* @param {string} [deps.usageSection=''] 附加的使用量區塊。
* @returns {Promise<void>} 無回傳值。
*/
export async function postFindingsReview(findings, deps = {}) {
@@ -282,11 +279,11 @@ export async function postFindingsReview(findings, deps = {}) {
}
/**
* 將 findings 寫入 `findings.json`(同步阻塞 I/O)。
* 將 findings 寫入新版 wrapper 格式的 `findings.json`(同步阻塞 I/O)。
*
* @param {string} workspace 主要輸出目錄;實際寫入路徑為 `path.join(workspace, FINDINGS_PATH)`。
* @param {Array<object>} findings 要寫入的 findings 陣列;會以 `JSON.stringify(findings, null, 2)` 序列化,
* 並在檔尾補一個換行字元。
* @param {Array<object>} findings 要寫入的 findings 陣列;會包成包含 `generatedAt`/`commitSha`/
* `prNumber`/`tool`/`findings`/`excluded` 的 wrapper,再以 2 空白縮排 JSON 序列化並補換行。
* @param {?string} [mirrorDir=null] 額外鏡射輸出目錄(例如供後續 repo commit 使用);
* 為 `null`/`undefined`,或與 `workspace` 相同時,只會寫入一份(不重複寫入同一路徑)。
* @returns {void} 無回傳值;成功時每個目標各記錄一行 log。
@@ -297,14 +294,15 @@ export async function postFindingsReview(findings, deps = {}) {
* 若同時需要寫回 workspace 與 repo 兩個位置,傳入 `mirrorDir` 即可一次呼叫完成兩份寫入。
*/
export function saveFindings(workspace, findings, mirrorDir = null) {
const wrapper = buildFindingsWrapper(findings, []);
const targets = [workspace];
if (mirrorDir && mirrorDir !== workspace) targets.push(mirrorDir);
for (const targetDir of targets) {
const fullPath = path.join(targetDir, FINDINGS_PATH);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, JSON.stringify(findings, null, 2) + '\n', 'utf8');
ok(`findings 寫入: ${fullPath} (${findings.length} 筆)`);
fs.writeFileSync(fullPath, JSON.stringify(wrapper, null, 2) + '\n', 'utf8');
ok(`findings 寫入: ${fullPath} (${wrapper.findings.length} 筆)`);
}
}
@@ -312,12 +310,9 @@ export function saveFindings(workspace, findings, mirrorDir = null) {
* 發布所有舊問題的彙總 comment(一次性發布一則一般 comment,不含行內標註)。
*
* @param {Array<{ is_new?: boolean, level?: string }>} findings 審查問題陣列;
* 本函式以 `!f.is_new` 篩選舊問題——`is_new` 為 `false`、`undefined` 或其他 falsy 值皆視為舊問題
* (注意:此判定與 {@link newFindingsOnly} 的 `is_new !== false` 不同,`undefined` 在此處被視為
* 「舊」而非「新」,是否為預期設計需人工確認)。
* 本函式以 `!f.is_new` 篩選舊問題——`is_new` 為 `false`、`undefined` 或其他 falsy 值皆視為舊問題。
* @returns {Promise<void>} 無回傳值;`old.length === 0` 時直接 return,不會呼叫 `postComment`。
* @remarks 資料列**未依等級排序**,維持 `findings` 原始輸入順序輸出(與 {@link postFindingsReview}
* 內部先用 `bySeverity` 排序的行為不同,請勿假設本函式輸出已排序)。
* @remarks 資料列**未依等級排序**,維持 `findings` 原始輸入順序輸出。
* 使用情境:每輪 AI Code Review 收斂新舊問題後,統一針對「仍未解決的舊問題」發一則彙總說明。
*/
export async function postOldFindingsComment(findings) {
@@ -336,8 +331,7 @@ export async function postOldFindingsComment(findings) {
*
* @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` 的處理方向相反,是否為預期設計需人工確認)。
* `undefined`/`false` 皆會被排除。
* `level !== 'critical'` 涵蓋 `warning`、`info` 及任何非 `'critical'` 的其他值(含未知等級字串)。
* @returns {Promise<void>} 無回傳值;`items.length === 0` 時直接 return,不會呼叫 `postComment`。
* @remarks 資料列未依等級排序,維持 `findings` 原始輸入順序輸出。
@@ -360,8 +354,7 @@ export async function postNewNonCriticalComment(findings) {
* (內容為等級/審查員/建議),無法定位或行內發布失敗時降級為一般 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` 會被排除,需人工確認是否為預期設計)。
* 審查問題陣列;以 `f.is_new && f.level === 'critical'` 篩選——`is_new` 須為 truthy 才算新問題。
* @param {object} [deps={}] 可覆寫的相依注入物件(主要供測試替換)。
* @param {Function} [deps.postInline=postPullReviewComment] 發布單筆行內 review comment 的函式。
* @param {Function} [deps.postIssue=postComment] 發布一般 comment 的降級函式。