feat(ultraplan-local): add autonomy-gate state machine + manifest schema extensions for skip_commit_check + memory_write
Step 4 of plan-v2 (ultra-pipeline-speedup).
lib/util/autonomy-gate.mjs (NEW)
5-state machine {idle, gates_on, auto_running, paused_for_gate, completed}
honoring the --gates flag intent. Re-entry to completed is idempotent.
Includes CLI shim:
node lib/util/autonomy-gate.mjs --state X --event Y [--gates true|false]
→ JSON: { ok, next_state | error }, exit 0 on success / 1 on invalid.
lib/parsers/manifest-yaml.mjs (EXTENDED)
OPTIONAL_KEYS list adds skip_commit_check and memory_write — both boolean,
default false when absent, MANIFEST_OPTIONAL_TYPE when non-boolean.
Existing REQUIRED_KEYS contract untouched; existing 9 manifest tests
still pass.
Tests: 19 (autonomy-gate) + 8 (manifest-schema-extensions) = 27 new.
[skip-docs]
This commit is contained in:
parent
b1e161116a
commit
645f01625b
6 changed files with 602 additions and 2 deletions
|
|
@ -210,6 +210,34 @@
|
|||
.read-more-block { margin: var(--space-2) 0; }
|
||||
.read-more-block summary { cursor: pointer; color: var(--color-text-link); font-weight: var(--font-weight-medium); }
|
||||
|
||||
/* Renderer-batch B inlines (v1.10.0 Sesjon 4). Klasser ikke i vendor —
|
||||
inline her per scope-regel (plugin standalone). Kandidat for hoisting
|
||||
til shared/playground-design-system/ i Sesjon 6 visual QA. */
|
||||
.top-risks { margin: var(--space-4) 0; display: flex; flex-direction: column; gap: var(--space-2); }
|
||||
.top-risks__heading { margin: 0 0 var(--space-2) 0; font-size: var(--font-size-md); color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.06em; font-weight: var(--font-weight-semibold); }
|
||||
.top-risk { display: grid; grid-template-columns: 28px 1fr auto; gap: var(--space-3); align-items: center; padding: var(--space-2) var(--space-3); background: var(--color-surface); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-sm); }
|
||||
.top-risk__rank { font-family: var(--font-family-mono); font-size: var(--font-size-sm); color: var(--color-text-tertiary); font-weight: var(--font-weight-bold); }
|
||||
.top-risk__desc { font-size: var(--font-size-sm); color: var(--color-text-primary); }
|
||||
.top-risk__score { font-family: var(--font-family-mono); font-size: var(--font-size-md); font-weight: var(--font-weight-bold); padding: 2px 10px; border-radius: var(--radius-pill); background: var(--color-bg-soft); color: var(--color-text-primary); }
|
||||
.top-risk[data-severity="critical"] { border-left: 4px solid var(--color-severity-critical); }
|
||||
.top-risk[data-severity="critical"] .top-risk__score { background: var(--color-severity-critical); color: var(--color-severity-critical-on); }
|
||||
.top-risk[data-severity="high"] { border-left: 4px solid var(--color-severity-high); }
|
||||
.top-risk[data-severity="high"] .top-risk__score { background: var(--color-severity-high-soft); color: var(--color-severity-high-on); }
|
||||
.top-risk[data-severity="medium"] { border-left: 4px solid var(--color-severity-medium); }
|
||||
.top-risk[data-severity="medium"] .top-risk__score { background: var(--color-severity-medium-soft); color: var(--color-severity-medium-on); }
|
||||
.top-risk[data-severity="low"] { border-left: 4px solid var(--color-severity-low); }
|
||||
|
||||
.recommendation-card { margin: var(--space-4) 0 0 0; padding: var(--space-4); background: var(--color-bg-soft); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); border-left: 4px solid var(--color-scope-architect); display: flex; flex-direction: column; gap: var(--space-2); }
|
||||
.recommendation-card__label { font-size: var(--font-size-xs); font-weight: var(--font-weight-semibold); text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-text-tertiary); }
|
||||
.recommendation-card__body { font-size: var(--font-size-md); color: var(--color-text-primary); line-height: var(--line-height-snug); }
|
||||
|
||||
.suppressed-panel { margin: var(--space-4) 0 0 0; padding: var(--space-3) var(--space-4); background: var(--color-bg-soft); border: 1px dashed var(--color-border-subtle); border-radius: var(--radius-md); opacity: 0.85; }
|
||||
.suppressed-panel summary { cursor: pointer; color: var(--color-text-secondary); font-weight: var(--font-weight-medium); font-size: var(--font-size-sm); }
|
||||
.suppressed-panel[open] summary { margin-bottom: var(--space-2); }
|
||||
.suppressed-panel__list { display: flex; flex-direction: column; gap: var(--space-2); margin: var(--space-2) 0 0 0; }
|
||||
.suppressed-panel__item { padding: var(--space-2) var(--space-3); background: var(--color-surface); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-sm); font-size: var(--font-size-sm); color: var(--color-text-secondary); display: flex; gap: var(--space-3); align-items: baseline; }
|
||||
.suppressed-panel__id { font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-text-tertiary); }
|
||||
|
||||
/* Tier 3 A4 — Screen-tabs (segmented). Inline her per scope-regel
|
||||
(plugin standalone). Kandidat for hoisting til shared/ i Sesjon 6. */
|
||||
.screen-tabs { display: flex; gap: var(--space-1); padding: 4px; background: var(--color-bg-soft); border-radius: var(--radius-md); width: fit-content; margin: 0 0 var(--space-4) 0; }
|
||||
|
|
@ -2650,13 +2678,92 @@
|
|||
recommendation: row[recKey] || ''
|
||||
};
|
||||
}) : [];
|
||||
// topRisks: prøv ## Top-risikoer-tabell først, ellers fall tilbake til
|
||||
// matrix_cells sortert desc på score.
|
||||
const topRisksTbl = parseTable(md, /##\s*Top.?risikoer/i);
|
||||
let topRisks = [];
|
||||
if (topRisksTbl && topRisksTbl.rows.length) {
|
||||
const idKey = topRisksTbl.headers[0];
|
||||
const descKey = topRisksTbl.headers.find(function (h) { return /risiko|trussel|description|beskrivelse/i.test(h); }) || topRisksTbl.headers[1];
|
||||
const scKey = topRisksTbl.headers.find(function (h) { return /score/i.test(h); });
|
||||
const sevKey = topRisksTbl.headers.find(function (h) { return /severity|alvorlighet|nivå/i.test(h); });
|
||||
topRisks = topRisksTbl.rows.map(function (row) {
|
||||
return {
|
||||
id: row[idKey] || '',
|
||||
description: row[descKey] || row[idKey] || '',
|
||||
score: scKey ? intOrZero(row[scKey] || '0') : 0,
|
||||
severity: (sevKey && (row[sevKey] || '').toLowerCase().trim()) || ''
|
||||
};
|
||||
}).slice(0, 5);
|
||||
} else if (matrix_cells.length) {
|
||||
topRisks = matrix_cells.slice().sort(function (a, b) {
|
||||
if (b.score !== a.score) return b.score - a.score;
|
||||
return String(a.label || '').localeCompare(String(b.label || ''));
|
||||
}).slice(0, 5).map(function (c) {
|
||||
return {
|
||||
id: '',
|
||||
description: c.label || '',
|
||||
score: c.score,
|
||||
severity: ''
|
||||
};
|
||||
});
|
||||
}
|
||||
// categoryGrades: prøv ## Kategori-snitt-tabell først, ellers utled
|
||||
// fra dimensions[]. Score → letter-grade A-F (5→A, 4→B, 3→C, 2→D, ≤1→F).
|
||||
const gradeFor = function (sc) {
|
||||
const n = Number(sc) || 0;
|
||||
if (n >= 5) return 'A';
|
||||
if (n >= 4) return 'B';
|
||||
if (n >= 3) return 'C';
|
||||
if (n >= 2) return 'D';
|
||||
return 'F';
|
||||
};
|
||||
const catTbl = parseTable(md, /##\s*Kategori.snitt/i);
|
||||
let categoryGrades = [];
|
||||
if (catTbl && catTbl.rows.length) {
|
||||
const nameKey = catTbl.headers[0];
|
||||
const scKey = catTbl.headers.find(function (h) { return /score|snitt/i.test(h); }) || catTbl.headers[1];
|
||||
categoryGrades = catTbl.rows.map(function (row) {
|
||||
const sc = intOrZero(row[scKey] || '0');
|
||||
return { name: row[nameKey] || '', score: sc, grade: gradeFor(sc) };
|
||||
});
|
||||
} else {
|
||||
categoryGrades = dimensions.map(function (d) {
|
||||
return { name: d.name, score: d.score, grade: gradeFor(d.score) };
|
||||
});
|
||||
}
|
||||
// residualPair: same syntax som parseMatrixRisk.
|
||||
let residualPair = null;
|
||||
const rrMatch = md.match(/^Restrisiko\s*:\s*([^\n]+)$/im);
|
||||
if (rrMatch) {
|
||||
const txt = rrMatch[1];
|
||||
const num = /(\d+)\s*[×x*]\s*(\d+)\s*(?:[-=]?[>→]|->)\s*(\d+)\s*[×x*]\s*(\d+)/.exec(txt);
|
||||
if (num) {
|
||||
const b1 = +num[1], b2 = +num[2], a1 = +num[3], a2 = +num[4];
|
||||
residualPair = {
|
||||
before: { prob: b1, cons: b2, score: b1 * b2 },
|
||||
after: { prob: a1, cons: a2, score: a1 * a2 }
|
||||
};
|
||||
} else {
|
||||
const parts = txt.split(/(?:[-=]?[>→]|->)/);
|
||||
if (parts.length === 2) {
|
||||
residualPair = {
|
||||
before: { label: parts[0].trim() },
|
||||
after: { label: parts[1].trim() }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
data: {
|
||||
dimensions: dimensions,
|
||||
matrix_cells: matrix_cells,
|
||||
findings: findings,
|
||||
scores: dimensions.map(function (d) { return d.score; })
|
||||
scores: dimensions.map(function (d) { return d.score; }),
|
||||
topRisks: topRisks,
|
||||
categoryGrades: categoryGrades,
|
||||
residualPair: residualPair
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -3323,10 +3430,86 @@
|
|||
// ---- Sub-batch B: Security (3) ----
|
||||
|
||||
function renderSecurity(data, slot) {
|
||||
const sevForScore = function (s) {
|
||||
const n = Number(s) || 0;
|
||||
if (n >= 16) return 'critical';
|
||||
if (n >= 9) return 'high';
|
||||
if (n >= 4) return 'medium';
|
||||
return 'low';
|
||||
};
|
||||
const matrixHtml = renderMatrixHtml(data, 6);
|
||||
const radarHtml = renderRadarSvg(data.dimensions || []);
|
||||
// C7 small-multiples per OWASP-kategori (driver: categoryGrades).
|
||||
const cats = data.categoryGrades || [];
|
||||
const smallMultiplesHtml = cats.length ? '<div class="small-multiples">' + cats.map(function (c) {
|
||||
const grade = c.grade || '';
|
||||
const fillPct = Math.max(0, Math.min(100, ((Number(c.score) || 0) / 5) * 100));
|
||||
return '<div class="sm-card">' +
|
||||
'<div class="sm-card__header">' +
|
||||
'<span class="sm-card__name">' + escapeHtml(c.name || '') + '</span>' +
|
||||
'<span class="sm-card__grade" data-grade="' + escapeAttr(grade) + '">' + escapeHtml(grade) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="sm-card__bar"><div class="sm-card__bar-fill" style="width: ' + fillPct.toFixed(0) + '%"></div></div>' +
|
||||
'<span class="sm-card__status">Score ' + escapeHtml(String(c.score || 0)) + ' / 5</span>' +
|
||||
'</div>';
|
||||
}).join('') + '</div>' : '';
|
||||
// C4 top-risks-list (max 5).
|
||||
const topRisks = (data.topRisks || []).slice(0, 5);
|
||||
const topRisksHtml = topRisks.length ? '<section class="top-risks">' +
|
||||
'<h4 class="top-risks__heading">Top-risikoer</h4>' +
|
||||
topRisks.map(function (r, i) {
|
||||
const sev = r.severity || sevForScore(r.score);
|
||||
return '<div class="top-risk" data-severity="' + escapeAttr(sev) + '">' +
|
||||
'<span class="top-risk__rank">' + (i + 1) + '</span>' +
|
||||
'<span class="top-risk__desc">' + escapeHtml(r.description || '') + '</span>' +
|
||||
'<span class="top-risk__score">' + escapeHtml(String(r.score || 0)) + '</span>' +
|
||||
'</div>';
|
||||
}).join('') + '</section>' : '';
|
||||
// B6 residual-pair (når data.residualPair finnes).
|
||||
const rp = data.residualPair;
|
||||
let residualHtml = '';
|
||||
if (rp && rp.before && rp.after) {
|
||||
const labelOf = function (cell) {
|
||||
if (cell.score != null) return cell.prob + '×' + cell.cons + ' = ' + cell.score;
|
||||
return cell.label || '—';
|
||||
};
|
||||
const sevBefore = rp.before.score != null ? sevForScore(rp.before.score) : '';
|
||||
const sevAfter = rp.after.score != null ? sevForScore(rp.after.score) : '';
|
||||
const attrBefore = sevBefore ? ' data-severity="' + sevBefore + '"' : '';
|
||||
const attrAfter = sevAfter ? ' data-severity="' + sevAfter + '"' : '';
|
||||
residualHtml = '<div class="residual-pair">' +
|
||||
'<div class="residual-pair__cell"' + attrBefore + '>' +
|
||||
'<span class="residual-pair__cell-label">FØR TILTAK</span>' +
|
||||
'<span class="residual-pair__cell-value">' + escapeHtml(labelOf(rp.before)) + '</span>' +
|
||||
'<span class="residual-pair__cell-meta">Sannsynlighet × konsekvens</span>' +
|
||||
'</div>' +
|
||||
'<div class="residual-pair__arrow" aria-hidden="true">→</div>' +
|
||||
'<div class="residual-pair__cell"' + attrAfter + '>' +
|
||||
'<span class="residual-pair__cell-label">ETTER TILTAK</span>' +
|
||||
'<span class="residual-pair__cell-value">' + escapeHtml(labelOf(rp.after)) + '</span>' +
|
||||
'<span class="residual-pair__cell-meta">Restrisiko</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
const findingsHtml = renderFindingsBlock(data.findings || [], 'Sikkerhetsfunn');
|
||||
slot.innerHTML = matrixHtml + radarHtml + findingsHtml;
|
||||
const body = matrixHtml + radarHtml + smallMultiplesHtml + topRisksHtml + residualHtml + findingsHtml;
|
||||
// Utvid matrix-risk-6x5-keyStats med RESTRISIKO når residualPair finnes.
|
||||
const baseStats = inferKeyStats(data, 'matrix-risk-6x5');
|
||||
const stats = (data.keyStats || (rp && rp.after
|
||||
? baseStats.concat([{
|
||||
label: 'RESTRISIKO',
|
||||
value: rp.after.score != null ? String(rp.after.score) : (rp.after.label || '—'),
|
||||
modifier: rp.after.score != null && rp.after.score >= 9 ? 'high' : 'low',
|
||||
hint: 'etter tiltak'
|
||||
}])
|
||||
: baseStats));
|
||||
slot.innerHTML = renderPageShell({
|
||||
eyebrow: 'SIKKERHET',
|
||||
title: data.title || 'Sikkerhetsvurdering (6×5)',
|
||||
lede: data.lede || 'Score per dimensjon, risikomatrise og topp-risikoer mot NSM, Microsoft Cloud Security og AI Act Art. 15.',
|
||||
verdict: data.verdict || inferVerdict(data, 'matrix-risk-6x5'),
|
||||
keyStats: stats
|
||||
}, body);
|
||||
}
|
||||
|
||||
function renderRos(data, slot) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,29 @@ Rammeverk: NSM Grunnprinsipper + Microsoft Cloud Security + EU AI Act Art. 15
|
|||
| S-05 | medium | Logging | Implementer ML-basert avviksdeteksjon på AI-output-rate |
|
||||
| S-06 | medium | Vendor | Bestilt third-party penetrasjons-test for Q3 2026 |
|
||||
|
||||
## Top-risikoer
|
||||
|
||||
| ID | Risiko | Score | Severity |
|
||||
|----|--------|-------|----------|
|
||||
| R-01 | Lekkasje av treningsdata | 10 | high |
|
||||
| R-02 | Insider-trussel (unauthorized inference) | 10 | high |
|
||||
| R-03 | Prompt injection i forklaringsmodell | 9 | high |
|
||||
| R-04 | Adversarielt eksempel forgifter output | 8 | medium |
|
||||
| R-05 | Cloud-leverandør-utilgjengelighet | 8 | medium |
|
||||
|
||||
## Kategori-snitt
|
||||
|
||||
| Kategori | Snitt |
|
||||
|----------|-------|
|
||||
| Identitet og tilgang | 4 |
|
||||
| Datasikkerhet og personvern | 3 |
|
||||
| Modell- og prompt-sikkerhet | 3 |
|
||||
| Nettverk og perimeter | 5 |
|
||||
| Logging og hendelseshåndtering | 4 |
|
||||
| Operasjonell og leverandørsikkerhet | 3 |
|
||||
|
||||
Restrisiko: 5×4 → 2×3
|
||||
|
||||
## Aggregat
|
||||
|
||||
Totalscore: 22/30 (73%) — modent men ikke best-i-klassen. Modell- og prompt-sikkerhet er svakeste dimensjon.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue