feat(ms-ai-architect): Enhet A2 — Layer B baseline-adjudikering: innholds-basert allowlist (75 funn adjudikert, TDD) + 4 korpus-defekter fikset (2 homoglyph-ord, 2 U+00AD-filer); korpus 389/389 exit 0 [skip-docs]

This commit is contained in:
Kjell Tore Guttormsen 2026-07-18 10:31:06 +02:00
commit af6c31c4c1
11 changed files with 1048 additions and 15 deletions

View file

@ -125,21 +125,51 @@ export function disposition(finding, tier) {
return 'block';
}
const RANK = { block: 2, warn: 1, clean: 0 };
const RANK = { block: 2, warn: 1, clean: 0, allow: 0 };
/**
* Does an adjudicated allowlist entry cover this finding? (Enhet A2, brief §8.)
* CONTENT-based, not line-number-based: the entry pins the exact TRIMMED content of
* the adjudicated line, so a line that merely moves stays green while a line whose
* content changes RESURFACES as WARN/BLOCK (fail-safe re-adjudication required).
* All four axes must match: class, evidence, provenance tier, trimmed line content.
* (Path scoping happens in the caller scanPaths which filters entries per file.)
* @param {{class: string, evidence: string}} finding
* @param {string} tier the finding's computed provenance tier
* @param {string} trimmedLine trimmed content of the finding's line
* @param {Array<object>} allow allowlist entries scoped to this file
* @returns {boolean}
*/
export function isAllowlisted(finding, tier, trimmedLine, allow) {
return (allow ?? []).some(
(e) =>
e.class === finding.class &&
e.evidence === finding.evidence &&
e.tier === tier &&
typeof e.match === 'string' &&
e.match === trimmedLine,
);
}
/**
* Classify a batch of raw findings against the file content. Pure + sync.
* @param {string} content the full candidate file content (for code-fence + Source tiering)
* @param {Array<object>} rawFindings [{class, subtype?, severity, line, evidence}]
* @param {{sourceUrl?: string}} [opts] sourceUrl overrides the in-file **Source:** header
* @param {{sourceUrl?: string, allow?: Array<object>}} [opts] sourceUrl overrides the in-file
* **Source:** header; allow = adjudicated allowlist entries ALREADY scoped to this file
* @returns {{disposition: 'block'|'warn'|'clean', findings: Array<object>}}
*/
export function classifyFindings(content, rawFindings, opts = {}) {
const ranges = findFencedCodeRanges(content);
const lines = String(content ?? '').split('\n');
const sourceUrl = opts.sourceUrl ?? parseSourceHeader(content) ?? null;
const findings = (rawFindings ?? []).map((f) => {
const tier = provenanceTier({ line: f.line, ranges, sourceUrl });
return { ...f, tier, disposition: disposition(f, tier) };
const trimmedLine = (lines[f.line - 1] ?? '').trim();
const disp = isAllowlisted(f, tier, trimmedLine, opts.allow)
? 'allow'
: disposition(f, tier);
return { ...f, tier, disposition: disp };
});
let worst = 'clean';
for (const f of findings) {