fix: persist repaired exclusions

This commit is contained in:
2026-05-18 02:40:53 +00:00
parent 915e9cc2da
commit b8294d5ca7
3 changed files with 29 additions and 2 deletions
+6 -1
View File
@@ -272,7 +272,7 @@ export async function deduplicateWithAI(findings) {
/** /**
* 讀取排除問題檔案(從來源分支的 cloned repoDir 中的 EXCLUSIONS_PATH * 讀取排除問題檔案(從來源分支的 cloned repoDir 中的 EXCLUSIONS_PATH
*/ */
export function loadExclusions(workspace, repoState = null) { export function loadExclusions(workspace, repoState = null, mirrorWorkspace = null) {
const fullPath = path.join(workspace, EXCLUSIONS_PATH); const fullPath = path.join(workspace, EXCLUSIONS_PATH);
if (!fs.existsSync(fullPath)) { if (!fs.existsSync(fullPath)) {
warn(`排除問題檔案不存在,視為空: ${fullPath}`); warn(`排除問題檔案不存在,視為空: ${fullPath}`);
@@ -302,6 +302,11 @@ export function loadExclusions(workspace, repoState = null) {
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') { if (sourceFormat !== 'array') {
writeCanonicalExclusions(fullPath, normalizedSource); writeCanonicalExclusions(fullPath, normalizedSource);
if (mirrorWorkspace && path.resolve(mirrorWorkspace) !== path.resolve(workspace)) {
const mirrorPath = path.join(mirrorWorkspace, EXCLUSIONS_PATH);
fs.mkdirSync(path.dirname(mirrorPath), { recursive: true });
writeCanonicalExclusions(mirrorPath, normalizedSource);
}
line(`排除問題格式已修正為頂層陣列: source=${sourceFormat} -> array`); line(`排除問題格式已修正為頂層陣列: source=${sourceFormat} -> array`);
} }
} catch (e) { } catch (e) {
+22
View File
@@ -60,6 +60,28 @@ describe('findings exclusions', () => {
assert.ok(logs.some(line => line.includes('排除問題格式已修正為頂層陣列: source=exclusions -> array'))); assert.ok(logs.some(line => line.includes('排除問題格式已修正為頂層陣列: source=exclusions -> array')));
}); });
it('mirrors repaired exclusions into the workspace root when requested', () => {
const repoRoot = path.join(workspace, 'repo');
const mirrorRoot = path.join(workspace, 'workspace');
const repoFullPath = path.join(repoRoot, EXCLUSIONS_PATH);
const mirrorFullPath = path.join(mirrorRoot, EXCLUSIONS_PATH);
fs.mkdirSync(path.dirname(repoFullPath), { recursive: true });
fs.mkdirSync(path.dirname(mirrorFullPath), { recursive: true });
fs.writeFileSync(repoFullPath, JSON.stringify({
exclusions: [
{ location: 'README.md:12', suggestion: 'keep' },
],
}, null, 2));
const exclusions = loadExclusions(repoRoot, null, mirrorRoot);
const mirror = JSON.parse(fs.readFileSync(mirrorFullPath, 'utf8'));
assert.equal(exclusions.length, 1);
assert.ok(Array.isArray(mirror));
assert.equal(mirror[0].location, 'README.md:12');
assert.equal(mirror[0].suggestion, 'keep');
});
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' },
+1 -1
View File
@@ -95,7 +95,7 @@ async function main() {
ok(`Step3 去重完成: ${mergedFindings.length} -> ${sorted.length} 筆 (critical=${sorted.filter(f=>f.level==='critical').length} warning=${sorted.filter(f=>f.level==='warning').length} info=${sorted.filter(f=>f.level==='info').length})`); ok(`Step3 去重完成: ${mergedFindings.length} -> ${sorted.length} 筆 (critical=${sorted.filter(f=>f.level==='critical').length} warning=${sorted.filter(f=>f.level==='warning').length} info=${sorted.filter(f=>f.level==='info').length})`);
step('Step4', 'AI 排除問題過濾'); step('Step4', 'AI 排除問題過濾');
const exclusions = loadExclusions(repoDir || WORKSPACE, repoState); const exclusions = loadExclusions(repoDir || WORKSPACE, repoState, WORKSPACE);
const ruleFiltered = applyExclusions(sorted, exclusions); const ruleFiltered = applyExclusions(sorted, exclusions);
const filtered = await filterFalsePositivesWithAI(ruleFiltered, exclusions); const filtered = await filterFalsePositivesWithAI(ruleFiltered, exclusions);
ok(`Step4 完成: findings total=${filtered.length}`); ok(`Step4 完成: findings total=${filtered.length}`);