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:
parent
20717102aa
commit
50f0629baf
3 changed files with 124 additions and 14 deletions
|
|
@ -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) ----
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# -------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue