"""Regulatory Optimization — the Roadmap / Management RENDERER of the Capability Delta Engine. GAP analysis and measure-prioritisation are TWO VIEWS OF THE SAME COMPUTATION. The Capability Delta Engine (`compliance/transition_reasoning`, RS-005) computes Required - Known = the Capability Delta once. Renderers read that ONE delta: - Interview Renderer (missing INFORMATION -> questions) = `TransitionQuestionRequest` (built) - Roadmap / Management Renderer (missing CAPABILITIES -> measures by leverage) = THIS module - Evidence Renderer (missing EVIDENCE -> upload requests) = later There is one truth, not a Gap engine and a separate Roadmap engine. A measure (a capability to implement) has *regulatory leverage* = the number of distinct regulatory requirements it closes AT ONCE (e.g. patch management closes a CRA, a MaschinenVO, an IEC 62443 and an ISO 27001 requirement -> leverage 4). The product turns from "you have N obligations" into "of N identified requirements you only need M measures — and these K first". Fully deterministic, computed-not-stored, NO new corpus. `regulatory_leverage`/`select_within_budget` are pure math over `capability -> requirements`; `roadmap_from_delta` binds them to the RS-005 delta (dependency optimization -> transition_reasoning, acyclic; the delta engine stays hermetic). No new graph/meta-model class (freeze v1.0). Python 3.9 compatible. Honesty (Welt-1): the percentages are exact count ratios over the IDENTIFIED requirements from the known patterns — never "% gesetzeskonform". Label outputs as "der identifizierten Anforderungen". """ from __future__ import annotations from typing import Dict, List, Optional from ..transition_reasoning import CoverageStatus, TransitionAssessment from .schemas import BudgetPlan, OptimizationPlan, RankedMeasure def _ranked( capability_requirements: Dict[str, List[str]], in_scope: Optional[List[str]] ) -> List[RankedMeasure]: """Rank measures: leverage desc, then capability_id asc (deterministic). Empty covers dropped.""" scope = ( set(in_scope) if in_scope is not None else {r for reqs in capability_requirements.values() for r in reqs} ) measures: List[RankedMeasure] = [] for cap, reqs in capability_requirements.items(): covers = sorted({r for r in reqs if r in scope}) if not covers: continue # this capability closes nothing in scope -> not a measure here measures.append(RankedMeasure(capability_id=cap, covers=covers, leverage=len(covers))) measures.sort(key=lambda m: (-m.leverage, m.capability_id)) total = sum(m.leverage for m in measures) running = 0 for m in measures: running += m.leverage m.cumulative_requirements = running m.cumulative_coverage = (running / total) if total else 0.0 return measures def regulatory_leverage( capability_requirements: Dict[str, List[str]], in_scope: Optional[List[str]] = None ) -> OptimizationPlan: """Rank measures by regulatory leverage; report the compression (requirements -> measures). `capability_requirements`: measure (capability_id) -> the requirement keys it satisfies. A requirement key is currently a regulation (via `covers_targets`); finer obligation granularity is a future extension. `in_scope`: restrict the requirement keys counted (default: all seen). """ measures = _ranked(capability_requirements, in_scope) scope = sorted( set(in_scope) if in_scope is not None else {r for reqs in capability_requirements.values() for r in reqs} ) total = sum(m.leverage for m in measures) avg = (total / len(measures)) if measures else 0.0 headline = ( "%d identifizierte Anforderungen aus %d Regelwerken -> %d Massnahmen (Ø Hebel %.1f)." % (total, len(scope), len(measures), avg) ) return OptimizationPlan( in_scope_requirements=scope, total_measures=len(measures), total_requirements=total, ranked_measures=measures, headline=headline, ) def select_within_budget( capability_requirements: Dict[str, List[str]], budget: int, in_scope: Optional[List[str]] = None, ) -> BudgetPlan: """The budget answer: with K measures, pick the K highest-leverage ones and report coverage. Because each requirement key is closed by exactly one measure here, greedy-by-leverage is the optimal cover, so ranking == selection. (When requirements become shared across capabilities, this becomes weighted set-cover; the signature is ready for that.) """ measures = _ranked(capability_requirements, in_scope) total = sum(m.leverage for m in measures) k = max(0, budget) selected = measures[:k] closed = selected[-1].cumulative_requirements if selected else 0 ratio = (closed / total) if total else 0.0 headline = ( "Mit den Top-%d Massnahmen (nach regulatorischem Hebel) schliessen Sie %d von %d " "identifizierten Anforderungen (%.0f%%)." % (len(selected), closed, total, ratio * 100) ) return BudgetPlan( budget=budget, selected_capabilities=[m.capability_id for m in selected], requirements_closed=closed, total_requirements=total, coverage_ratio=ratio, headline=headline, ) def roadmap_from_delta( assessment: TransitionAssessment, capability_requirements: Dict[str, List[str]], in_scope: Optional[List[str]] = None, open_statuses: Optional[List[CoverageStatus]] = None, ) -> OptimizationPlan: """Render the Roadmap view FROM a Capability Delta (an RS-005 `TransitionAssessment`). Takes the OPEN capabilities of the delta — MISSING by default — and ranks them by regulatory leverage. This is the same delta the Interview Renderer turns into questions; here it becomes prioritised measures. The binding that makes "one truth, two renderers" real in code. """ statuses = set(open_statuses) if open_statuses is not None else {CoverageStatus.MISSING} open_caps = [c.capability_id for c in assessment.coverage if c.status in statuses] delta_reqs = {cap: capability_requirements.get(cap, []) for cap in open_caps} return regulatory_leverage(delta_reqs, in_scope)