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
+24 -33
View File
@@ -26,27 +26,6 @@ export async function analyzeWithRole(role, diff) {
return valid;
}
/**
* 讀取 JSON 陣列檔案;檔案不存在、讀取失敗或內容非陣列時,皆視為空並回傳 []。
*
* @param {string} fullPath - 欲讀取的 JSON 檔案完整路徑。
* @param {string} label - 用於警告訊息中識別此次讀取對象的標籤文字(例如「舊 findings 」)。
* @returns {Array<object>} 解析出的陣列;任何失敗情況皆回傳空陣列 []。
*/
function readJSONArray(fullPath, label) {
if (!fs.existsSync(fullPath)) {
warn(`${label}檔案不存在,視為空`);
return [];
}
try {
const data = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
return Array.isArray(data) ? data : [];
} catch (e) {
warn(`讀取${label}失敗: ${e.message},視為空`);
return [];
}
}
/**
* 將排除設定(頂層陣列、{ exclusions: [] } 或 { excluded_findings: [] })正規化為條目陣列。
*
@@ -305,7 +284,8 @@ function buildExclusionContext(exclusions) {
/**
* 讀取舊 findings(來源分支 cloned repoDir 下 FINDINGS_PATH 指向的檔案),
* 每筆項目一律標記 is_new: false(代表非本次新產生),並記錄檔案大小/修改時間等診斷日誌。
* 同時相容舊版頂層陣列與新版 wrapper 物件;每筆項目一律標記 is_new: false
*(代表非本次新產生),並記錄檔案大小/修改時間等診斷日誌。
* 檔案不存在或讀取失敗時視為空陣列,不拋例外。
*
* @param {string} workspace - 來源分支 clone 出的工作目錄根路徑,FINDINGS_PATH 會相對此路徑解析。
@@ -313,11 +293,20 @@ function buildExclusionContext(exclusions) {
*/
export function loadOldFindings(workspace) {
const fullPath = path.join(workspace, FINDINGS_PATH);
const old = readJSONArray(fullPath, '舊 findings ').map(f => ({ ...f, is_new: false }));
let old = [];
if (fs.existsSync(fullPath)) {
const stat = fs.statSync(fullPath);
line(`讀取舊 findings 檔案: ${fullPath}`);
line(`舊 findings 檔案資訊: bytes=${stat.size} mtime=${formatFileTime(stat.mtimeMs)} path=${path.relative(workspace, fullPath) || fullPath}`);
try {
const stat = fs.statSync(fullPath);
const data = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
const sourceFormat = Array.isArray(data) ? 'array' : (data && Array.isArray(data.findings) ? 'wrapper' : 'unknown');
const rawFindings = Array.isArray(data) ? data : (data && Array.isArray(data.findings) ? data.findings : []);
old = rawFindings.map(f => ({ ...f, is_new: false }));
line(`讀取舊 findings 檔案: ${fullPath}`);
line(`舊 findings 檔案資訊: bytes=${stat.size} mtime=${formatFileTime(stat.mtimeMs)} source=${sourceFormat} path=${path.relative(workspace, fullPath) || fullPath}`);
} catch (e) {
warn(`讀取舊 findings 失敗: ${e.message},視為空: ${fullPath}`);
old = [];
}
} else {
warn(`舊 findings 檔案不存在: ${fullPath}`);
}
@@ -326,7 +315,7 @@ export function loadOldFindings(workspace) {
}
/**
* 合併新舊 findings:以 (role + location + suggestion 前 50 字) 組成的字串為 key,
* 合併新舊 findings:以 (role + location + problem + suggestion 前 50 字) 組成的字串為 key,
* 過濾掉 newFindings 中與 oldFindings(或 newFindings 自身先出現的項目)key 相同的重複項。
* oldFindings 本身不會互相去重(視為既有基準),回傳陣列為 [...oldFindings, ...去重後的 newFindings]。
*
@@ -335,7 +324,7 @@ export function loadOldFindings(workspace) {
* @returns {Array<object>} 合併後的 findings 陣列,不修改傳入的兩個陣列本身。
*/
export function mergeFindings(oldFindings, newFindings) {
const key = f => `${f.role}|${f.location}|${String(f.suggestion).slice(0, 50)}`;
const key = f => `${f.role}|${f.location}|${String(f.problem || '')}|${String(f.suggestion || '').slice(0, 50)}`;
const seen = new Set(oldFindings.map(key));
const deduped = newFindings.filter(f => {
if (seen.has(key(f))) return false;
@@ -348,15 +337,17 @@ export function mergeFindings(oldFindings, newFindings) {
}
/**
* 依等級排序(critical > warning > info),回傳新陣列,不修改傳入的 findings。
* 依等級排序(critical > warning > info,未知等級排最後),回傳新陣列,不修改傳入的 findings。
*
* @param {Array<object>} findings - 欲排序的 findings 陣列(各筆需含 level 欄位)。
* @returns {Array<object>} 依 critical/warning/info 順序排序後的新陣列。
* @remarks level 不在 ['critical','warning','info'] 中的項目,因 indexOf 回傳 -1,
* 會被排到 critical 之前(最前面)而非最後面;此邊界行為是否為預期設計,需人工確認。
* @returns {Array<object>} 依 critical/warning/info 順序排序後的新陣列;未知等級會排在最後。
*/
export function sortByLevel(findings) {
return [...findings].sort((a, b) => LEVELS.indexOf(a.level) - LEVELS.indexOf(b.level));
const rank = (level) => {
const index = LEVELS.indexOf(level);
return index === -1 ? LEVELS.length : index;
};
return [...findings].sort((a, b) => rank(a.level) - rank(b.level));
}
/**