feat: normalize exclusions format
This commit is contained in:
@@ -21,8 +21,8 @@
|
|||||||
- 已驗收:log 已出現 `AI 去重: 13 -> 11 筆`,且程式具備失敗時保留所有問題的降級處理。
|
- 已驗收:log 已出現 `AI 去重: 13 -> 11 筆`,且程式具備失敗時保留所有問題的降級處理。
|
||||||
|
|
||||||
## 階段五:AI 排除問題過濾
|
## 階段五:AI 排除問題過濾
|
||||||
- 目標:讀取排除問題檔案(`.gitea/ai-review/exclusions.json`)時先去除重複條目、整理成語意群組摘要,再進行規則過濾並呼叫 AI 判斷剩餘問題是否為誤報或不適用,兩層過濾後產生最終問題清單。
|
- 目標:讀取排除問題檔案(`.gitea/ai-review/exclusions.json`)時先去除重複條目、整理成語意群組摘要;若檔案不是頂層陣列格式,需主動修正成正確格式,再進行規則過濾並呼叫 AI 判斷剩餘問題是否為誤報或不適用,兩層過濾後產生最終問題清單。
|
||||||
- 驗收:log 中能看到排除問題檔案讀取成功或不存在的訊息、重複排除條目的整理摘要、規則過濾數量變化,以及「AI 誤報過濾: N -> M 筆」或降級訊息。
|
- 驗收:log 中能看到排除問題檔案讀取成功或不存在的訊息、重複排除條目的整理摘要、格式修正訊息、規則過濾數量變化,以及「AI 誤報過濾: N -> M 筆」或降級訊息。
|
||||||
- 部分驗收:log 已顯示 `讀取排除問題: 50 筆` 與 `排除過濾: 11 -> 0 筆`,但這次未進入 `AI 誤報過濾: N -> M 筆` 的正向路徑。
|
- 部分驗收:log 已顯示 `讀取排除問題: 50 筆` 與 `排除過濾: 11 -> 0 筆`,但這次未進入 `AI 誤報過濾: N -> M 筆` 的正向路徑。
|
||||||
- 可驗收紀錄情境:當 `排除過濾` 後仍保留 1 筆以上 findings 時,log 會出現 `AI 誤報過濾: N -> M 筆`;若 API 額度不足或回傳失敗,則會出現 `AI 誤報過濾失敗(...),降級:保留所有問題`。
|
- 可驗收紀錄情境:當 `排除過濾` 後仍保留 1 筆以上 findings 時,log 會出現 `AI 誤報過濾: N -> M 筆`;若 API 額度不足或回傳失敗,則會出現 `AI 誤報過濾失敗(...),降級:保留所有問題`。
|
||||||
|
|
||||||
|
|||||||
+20
-2
@@ -37,10 +37,22 @@ function readJSONArray(fullPath, label) {
|
|||||||
|
|
||||||
function normalizeExclusions(data) {
|
function normalizeExclusions(data) {
|
||||||
if (Array.isArray(data)) return data;
|
if (Array.isArray(data)) return data;
|
||||||
|
if (data && Array.isArray(data.exclusions)) return data.exclusions;
|
||||||
if (data && Array.isArray(data.excluded_findings)) return data.excluded_findings;
|
if (data && Array.isArray(data.excluded_findings)) return data.excluded_findings;
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectExclusionSource(data) {
|
||||||
|
if (Array.isArray(data)) return 'array';
|
||||||
|
if (data && Array.isArray(data.exclusions)) return 'exclusions';
|
||||||
|
if (data && Array.isArray(data.excluded_findings)) return 'excluded_findings';
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeCanonicalExclusions(fullPath, exclusions) {
|
||||||
|
fs.writeFileSync(fullPath, JSON.stringify(exclusions, null, 2) + '\n', 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
function formatFileTime(mtimeMs) {
|
function formatFileTime(mtimeMs) {
|
||||||
if (!Number.isFinite(mtimeMs)) return 'unknown';
|
if (!Number.isFinite(mtimeMs)) return 'unknown';
|
||||||
return new Date(mtimeMs).toISOString();
|
return new Date(mtimeMs).toISOString();
|
||||||
@@ -278,14 +290,20 @@ export function loadExclusions(workspace, repoState = null) {
|
|||||||
try {
|
try {
|
||||||
const stat = fs.statSync(fullPath);
|
const stat = fs.statSync(fullPath);
|
||||||
const data = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
const data = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
||||||
rawCount = Array.isArray(data) ? data.length : Array.isArray(data?.excluded_findings) ? data.excluded_findings.length : 0;
|
const sourceFormat = detectExclusionSource(data);
|
||||||
exclusions = dedupeExclusions(normalizeExclusions(data).map((exclusion, index) => normalizeExclusionEntry(exclusion, index)));
|
const normalizedSource = normalizeExclusions(data);
|
||||||
|
rawCount = normalizedSource.length;
|
||||||
|
exclusions = dedupeExclusions(normalizedSource.map((exclusion, index) => normalizeExclusionEntry(exclusion, index)));
|
||||||
const branch = repoState?.branch || 'detached';
|
const branch = repoState?.branch || 'detached';
|
||||||
const shortSha = repoState?.shortSha || repoState?.headSha || 'unknown';
|
const shortSha = repoState?.shortSha || repoState?.headSha || 'unknown';
|
||||||
const commitTime = repoState?.commitTime || 'unknown';
|
const commitTime = repoState?.commitTime || 'unknown';
|
||||||
line(`讀取排除問題檔案: ${fullPath}`);
|
line(`讀取排除問題檔案: ${fullPath}`);
|
||||||
line(`來源分支狀態: branch=${branch} commit=${shortSha} commit_time=${commitTime}`);
|
line(`來源分支狀態: branch=${branch} commit=${shortSha} commit_time=${commitTime}`);
|
||||||
line(`檔案資訊: bytes=${stat.size} mtime=${formatFileTime(stat.mtimeMs)} raw=${rawCount} normalized=${exclusions.length} path=${path.relative(workspace, fullPath) || fullPath}`);
|
line(`檔案資訊: bytes=${stat.size} mtime=${formatFileTime(stat.mtimeMs)} raw=${rawCount} normalized=${exclusions.length} path=${path.relative(workspace, fullPath) || fullPath}`);
|
||||||
|
if (sourceFormat !== 'array') {
|
||||||
|
writeCanonicalExclusions(fullPath, normalizedSource);
|
||||||
|
line(`排除問題格式已修正為頂層陣列: source=${sourceFormat} -> array`);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
warn(`讀取排除問題失敗: ${e.message},視為空: ${fullPath}`);
|
warn(`讀取排除問題失敗: ${e.message},視為空: ${fullPath}`);
|
||||||
exclusions = [];
|
exclusions = [];
|
||||||
|
|||||||
@@ -41,6 +41,25 @@ describe('findings exclusions', () => {
|
|||||||
assert.equal(exclusions[0].title, 'fetch_package_versions jq overhead');
|
assert.equal(exclusions[0].title, 'fetch_package_versions jq overhead');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('repairs exclusions wrapper format to a top-level array', () => {
|
||||||
|
const fullPath = path.join(workspace, EXCLUSIONS_PATH);
|
||||||
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
|
fs.writeFileSync(fullPath, JSON.stringify({
|
||||||
|
exclusions: [
|
||||||
|
{ location: 'README.md:12', suggestion: 'keep' },
|
||||||
|
],
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
|
const exclusions = loadExclusions(workspace);
|
||||||
|
const repaired = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
||||||
|
|
||||||
|
assert.equal(exclusions.length, 1);
|
||||||
|
assert.ok(Array.isArray(repaired));
|
||||||
|
assert.equal(repaired[0].location, 'README.md:12');
|
||||||
|
assert.equal(repaired[0].suggestion, 'keep');
|
||||||
|
assert.ok(logs.some(line => line.includes('排除問題格式已修正為頂層陣列: source=exclusions -> array')));
|
||||||
|
});
|
||||||
|
|
||||||
it('applies exclusions loaded from wrapper format', () => {
|
it('applies exclusions loaded from wrapper format', () => {
|
||||||
const findings = [
|
const findings = [
|
||||||
{ location: 'entrypoint.sh:180', role: 'Maya', suggestion: 'keep' },
|
{ location: 'entrypoint.sh:180', role: 'Maya', suggestion: 'keep' },
|
||||||
|
|||||||
Reference in New Issue
Block a user