feat: normalize exclusions format

This commit is contained in:
2026-05-18 02:33:24 +00:00
parent d18c4a4a8e
commit b1ed236720
3 changed files with 41 additions and 4 deletions
+20 -2
View File
@@ -37,10 +37,22 @@ function readJSONArray(fullPath, label) {
function normalizeExclusions(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;
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) {
if (!Number.isFinite(mtimeMs)) return 'unknown';
return new Date(mtimeMs).toISOString();
@@ -278,14 +290,20 @@ export function loadExclusions(workspace, repoState = null) {
try {
const stat = fs.statSync(fullPath);
const data = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
rawCount = Array.isArray(data) ? data.length : Array.isArray(data?.excluded_findings) ? data.excluded_findings.length : 0;
exclusions = dedupeExclusions(normalizeExclusions(data).map((exclusion, index) => normalizeExclusionEntry(exclusion, index)));
const sourceFormat = detectExclusionSource(data);
const normalizedSource = normalizeExclusions(data);
rawCount = normalizedSource.length;
exclusions = dedupeExclusions(normalizedSource.map((exclusion, index) => normalizeExclusionEntry(exclusion, index)));
const branch = repoState?.branch || 'detached';
const shortSha = repoState?.shortSha || repoState?.headSha || 'unknown';
const commitTime = repoState?.commitTime || 'unknown';
line(`讀取排除問題檔案: ${fullPath}`);
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}`);
if (sourceFormat !== 'array') {
writeCanonicalExclusions(fullPath, normalizedSource);
line(`排除問題格式已修正為頂層陣列: source=${sourceFormat} -> array`);
}
} catch (e) {
warn(`讀取排除問題失敗: ${e.message},視為空: ${fullPath}`);
exclusions = [];
+19
View File
@@ -41,6 +41,25 @@ describe('findings exclusions', () => {
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', () => {
const findings = [
{ location: 'entrypoint.sh:180', role: 'Maya', suggestion: 'keep' },