feat(ms-ai-architect): renderer B.3 review adopt page-header + kanban (Keep/Review/Remove) + suppressed-panel

- parseFindings utvidet med status-felt-deteksjon og buckets-mapping {keep, review, remove, suppressed}
- Eksplisitt status vinner; severity-fallback (kritisk/høy → review, medium/lav → keep)
- Norsk og engelsk status-vokabular støttet (suppress/waive/akseptert, behold/keep, tilsyn/review, fjern/remove)
- renderReview wrapper renderPageShell med eyebrow=REVIEW; bytter findings-listen til E1 kanban-board (3 kolonner Keep/Review/Remove)
- E6 SUPPRESSED-panel som collapsible details for waived/akseptert items
- KeyStats utvidet med KEEP/REVIEW/REMOVE-stats
- review.md fixture utvidet med Status-kolonne (1 remove, 4 review, 2 keep, 2 suppressed)

Pluss test-utvidelser:
- Seksjon 25c: SC8 per-renderer verdict-pill assert for Sub-batch B (renderSecurity, renderRos, renderReview)
- Seksjon 25d: Step 11 must_contain — top-risks + suppressed >=1 treff
- Test-suite gar fra 178 -> 183 PASS

[skip-docs]
This commit is contained in:
Kjell Tore Guttormsen 2026-05-04 06:35:38 +02:00
commit 50f0629baf
3 changed files with 124 additions and 14 deletions

View file

@ -2831,15 +2831,40 @@
const sevKey = tbl.headers.find(function (h) { return /severity|alvorlighet/i.test(h); });
const locKey = tbl.headers.find(function (h) { return /lokasjon|location/i.test(h); });
const recKey = tbl.headers.find(function (h) { return /anbefaling|recommendation/i.test(h); });
const stKey = tbl.headers.find(function (h) { return /^status$/i.test(h); });
const findings = tbl.rows.map(function (row) {
return {
id: row[idKey] || '',
severity: (row[sevKey] || '').toLowerCase().trim(),
location: row[locKey] || '',
recommendation: row[recKey] || ''
recommendation: row[recKey] || '',
status: stKey ? String(row[stKey] || '').toLowerCase().trim() : ''
};
});
return { ok: true, data: { findings: findings } };
// Bucket-mapping (E1 kanban + E6 suppressed-panel).
// Eksplisitt status-felt vinner. Fallback: severity-basert.
// suppressed/waived/ignored/akseptert → suppressed
// keep/behold/accepted → keep
// review/tilsyn/escalate/eskaler → review
// remove/fjern/reject/avvis/blokker → remove
// severity critical/kritisk/high/høy → review
// severity medium/moderat/low/lav → keep
const bucketOf = function (f) {
const s = f.status || '';
if (/suppress|waive|ignore|akseptert/.test(s)) return 'suppressed';
if (/^keep$|behold|accepted/.test(s)) return 'keep';
if (/^review$|tilsyn|escalat|eskaler/.test(s)) return 'review';
if (/^remove$|fjern|reject|avvis|blokk/.test(s)) return 'remove';
const sev = f.severity || '';
if (/crit|kritisk/.test(sev)) return 'review';
if (/høy|high/.test(sev)) return 'review';
if (/medium|moderat/.test(sev)) return 'keep';
if (/lav|low/.test(sev)) return 'keep';
return 'review';
};
const buckets = { keep: [], review: [], remove: [], suppressed: [] };
findings.forEach(function (f) { buckets[bucketOf(f)].push(f); });
return { ok: true, data: { findings: findings, buckets: buckets } };
}
function parseCostDistribution(md) {
@ -3645,7 +3670,59 @@
}
function renderReview(data, slot) {
slot.innerHTML = renderFindingsBlock(data.findings || [], 'Funn');
const buckets = data.buckets || { keep: [], review: [], remove: [], suppressed: [] };
const cardFor = function (bucket, label) {
const items = buckets[bucket] || [];
const cards = items.length ? items.map(function (it) {
const sev = (it.severity || '').toUpperCase();
const head = it.id ? (it.id + ' — ' + (it.location || '')) : (it.location || '');
const recommendation = it.recommendation ? '<div class="kanban-card__meta">' + escapeHtml(it.recommendation) + '</div>' : '';
const sevTag = sev ? '<div class="kanban-card__meta">Severity: ' + escapeHtml(sev) + '</div>' : '';
return '<div class="kanban-card" data-severity="' + escapeAttr(it.severity || '') + '">' +
'<div class="kanban-card__name">' + escapeHtml(head) + '</div>' +
sevTag +
recommendation +
'</div>';
}).join('') : '<div class="kanban-col__empty">Ingen funn</div>';
return '<div class="kanban-col" data-bucket="' + escapeAttr(bucket) + '">' +
'<div class="kanban-col__head">' +
'<span class="kanban-col__title">' + escapeHtml(label) + '</span>' +
'<span class="kanban-col__count">' + items.length + '</span>' +
'</div>' +
cards +
'</div>';
};
const kanbanHtml = '<div class="kanban-board">' +
cardFor('keep', 'Keep') +
cardFor('review', 'Review') +
cardFor('remove', 'Remove') +
'</div>';
// E6 suppressed-panel for waived/akseptert items (collapsed by default).
const suppressed = buckets.suppressed || [];
const suppressedHtml = suppressed.length ? '<details class="suppressed-panel">' +
'<summary>Undertrykt (' + suppressed.length + ') — godtatt eller waiver registrert</summary>' +
'<div class="suppressed-panel__list">' + suppressed.map(function (it) {
return '<div class="suppressed-panel__item">' +
'<span class="suppressed-panel__id">' + escapeHtml(it.id || '—') + '</span>' +
'<span>' + escapeHtml(it.location || it.recommendation || '') + '</span>' +
'</div>';
}).join('') + '</div>' +
'</details>' : '';
const body = kanbanHtml + suppressedHtml;
// KeyStats: utvid 'findings'-archetype med BUCKET-stats (KEEP/REVIEW/REMOVE).
const baseStats = inferKeyStats(data, 'findings');
const stats = data.keyStats || baseStats.concat([
{ label: 'KEEP', value: (buckets.keep || []).length, modifier: 'low' },
{ label: 'REVIEW', value: (buckets.review || []).length, modifier: (buckets.review || []).length ? 'high' : 'low' },
{ label: 'REMOVE', value: (buckets.remove || []).length, modifier: (buckets.remove || []).length ? 'critical' : 'low' }
]);
slot.innerHTML = renderPageShell({
eyebrow: 'REVIEW',
title: data.title || 'Arkitekturgjennomgang',
lede: data.lede || 'Funn fordelt på Keep / Review / Remove med suppressed-panel for waived items.',
verdict: data.verdict || inferVerdict(data, 'findings'),
keyStats: stats
}, body);
}
// ---- Sub-batch C: Economy (2) ----

View file

@ -6,17 +6,17 @@ Reviewers: AI-arkitekt, sikkerhetsarkitekt, Datatilsynet
## Funn
| ID | Severity | Lokasjon | Anbefaling |
|----|----------|----------|------------|
| F-01 | critical | Authentication layer | Tilgang til AI-forklaringer mangler attribute-based access control — alle saksbehandler ser alle saker. Implementer ABAC basert på sak-tildeling. |
| F-02 | high | Data pipeline | Treningsdata oppdateres månedlig, men ingen formell drift-deteksjon. Etabler statistisk drift-monitoring i Azure Monitor. |
| F-03 | high | Model serving | Modellen serves fra en enkelt regional endpoint uten failover. Replikér til en sekundær region for RTO < 1t. |
| F-04 | high | Logging | Audit-logg lagres 30 dager — under arkivlovens krav for sak-relevant info. Endre retensjon til 7 år for sak-knyttede oppslag. |
| F-05 | medium | Cost management | Ingen budsjettalarmer på Azure AI Services — prediction-kostnaden kan øke med 4× ved belastnings-topper uten varsel. |
| F-06 | medium | Compliance | FRIA-rapport ikke vedlikeholdt etter modell-endring 2026-03-12. Re-evaluering trengs. |
| F-07 | medium | UX | saksbehandler-grensesnitt viser ikke konfidensgrad tydelig nok — risiko for over-trust på AI-output. |
| F-08 | low | Documentation | README mangler oppdatert arkitekturdiagram (siste fra 2025-11). |
| F-09 | low | Testing | Manglende E2E-test for utenlandske objekt-ID. |
| ID | Severity | Status | Lokasjon | Anbefaling |
|----|----------|--------|----------|------------|
| F-01 | critical | remove | Authentication layer | Tilgang til AI-forklaringer mangler attribute-based access control — alle saksbehandler ser alle saker. Implementer ABAC basert på sak-tildeling. |
| F-02 | high | review | Data pipeline | Treningsdata oppdateres månedlig, men ingen formell drift-deteksjon. Etabler statistisk drift-monitoring i Azure Monitor. |
| F-03 | high | review | Model serving | Modellen serves fra en enkelt regional endpoint uten failover. Replikér til en sekundær region for RTO < 1t. |
| F-04 | high | review | Logging | Audit-logg lagres 30 dager — under arkivlovens krav for sak-relevant info. Endre retensjon til 7 år for sak-knyttede oppslag. |
| F-05 | medium | keep | Cost management | Ingen budsjettalarmer på Azure AI Services — prediction-kostnaden kan øke med 4× ved belastnings-topper uten varsel. |
| F-06 | medium | review | Compliance | FRIA-rapport ikke vedlikeholdt etter modell-endring 2026-03-12. Re-evaluering trengs. |
| F-07 | medium | keep | UX | saksbehandler-grensesnitt viser ikke konfidensgrad tydelig nok — risiko for over-trust på AI-output. |
| F-08 | low | suppressed | Documentation | README mangler oppdatert arkitekturdiagram (siste fra 2025-11). |
| F-09 | low | suppressed | Testing | Manglende E2E-test for utenlandske objekt-ID. |
## Sammendrag

View file

@ -423,6 +423,39 @@ else
fail "residual-pair markup mangler (Step 10 must_contain krever >=1)"
fi
# -------------------------------------------------------
# 25c. SC8 — per-renderer verdict-pill emission for Sub-batch B (R7)
# Hver av de 3 Sub-batch B-rendererene må enten emitte data-verdict direkte
# i sin body, eller invokere renderPageShell (som emitter via helper).
# -------------------------------------------------------
SC8_RENDERERS_B="renderSecurity renderRos renderReview"
for fn in $SC8_RENDERERS_B; do
body=$(awk "/function $fn\(/,/^ \}$/" "$HTML_FILE")
if echo "$body" | grep -qE "verdict[^A-Za-z]*data-verdict\s*=\s*[\"'](go|go-with-conditions|block|approved|failed|allow|warning|n-a)[\"']" \
|| echo "$body" | grep -q "renderPageShell"; then
pass "SC8 verdict-pill: $fn (direkte eller via renderPageShell)"
else
fail "SC8 verdict-pill: $fn mangler både data-verdict og renderPageShell"
fi
done
# -------------------------------------------------------
# 25d. Step 11 must_contain — top-risks + suppressed
# -------------------------------------------------------
toprisks_count=$( { grep -cE "top-risks" "$HTML_FILE" || true; } | tr -d ' ')
if [ "${toprisks_count:-0}" -ge 1 ]; then
pass "top-risks markup til stede ($toprisks_count treff, Step 11 must_contain)"
else
fail "top-risks markup mangler (Step 11 must_contain krever >=1)"
fi
suppressed_count=$( { grep -cE "suppressed" "$HTML_FILE" || true; } | tr -d ' ')
if [ "${suppressed_count:-0}" -ge 1 ]; then
pass "suppressed markup til stede ($suppressed_count treff, Step 11 must_contain)"
else
fail "suppressed markup mangler (Step 11 must_contain krever >=1)"
fi
# -------------------------------------------------------
# 25. Inline-script eneste JS — ingen <script src=> til lokale .js-filer
# -------------------------------------------------------