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 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 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 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) {
|
const findings = tbl.rows.map(function (row) {
|
||||||
return {
|
return {
|
||||||
id: row[idKey] || '',
|
id: row[idKey] || '',
|
||||||
severity: (row[sevKey] || '').toLowerCase().trim(),
|
severity: (row[sevKey] || '').toLowerCase().trim(),
|
||||||
location: row[locKey] || '',
|
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) {
|
function parseCostDistribution(md) {
|
||||||
|
|
@ -3645,7 +3670,59 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderReview(data, slot) {
|
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) ----
|
// ---- Sub-batch C: Economy (2) ----
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,17 @@ Reviewers: AI-arkitekt, sikkerhetsarkitekt, Datatilsynet
|
||||||
|
|
||||||
## Funn
|
## Funn
|
||||||
|
|
||||||
| ID | Severity | Lokasjon | Anbefaling |
|
| ID | Severity | Status | 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-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 | Data pipeline | Treningsdata oppdateres månedlig, men ingen formell drift-deteksjon. Etabler statistisk drift-monitoring i Azure Monitor. |
|
| 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 | Model serving | Modellen serves fra en enkelt regional endpoint uten failover. Replikér til en sekundær region for RTO < 1t. |
|
| 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 | Logging | Audit-logg lagres 30 dager — under arkivlovens krav for sak-relevant info. Endre retensjon til 7 år for sak-knyttede oppslag. |
|
| 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 | Cost management | Ingen budsjettalarmer på Azure AI Services — prediction-kostnaden kan øke med 4× ved belastnings-topper uten varsel. |
|
| 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 | Compliance | FRIA-rapport ikke vedlikeholdt etter modell-endring 2026-03-12. Re-evaluering trengs. |
|
| F-06 | medium | review | 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-07 | medium | keep | 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-08 | low | suppressed | Documentation | README mangler oppdatert arkitekturdiagram (siste fra 2025-11). |
|
||||||
| F-09 | low | Testing | Manglende E2E-test for utenlandske objekt-ID. |
|
| F-09 | low | suppressed | Testing | Manglende E2E-test for utenlandske objekt-ID. |
|
||||||
|
|
||||||
## Sammendrag
|
## Sammendrag
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -423,6 +423,39 @@ else
|
||||||
fail "residual-pair markup mangler (Step 10 must_contain krever >=1)"
|
fail "residual-pair markup mangler (Step 10 must_contain krever >=1)"
|
||||||
fi
|
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
|
# 25. Inline-script eneste JS — ingen <script src=> til lokale .js-filer
|
||||||
# -------------------------------------------------------
|
# -------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue