feat(rts): extend Reference Transition Scenarios to multi-regulation (CRA + MaschinenVO)
Roadmap item 2: the RTS now pin MaschinenVO + convergence Expected Outcomes, so the convergence USP is a living regression, not just a one-off section. - RTS-003 (machine + ISMS, networked): full multi-regulation archetype — maschinenvo expected_delta + convergence expected_multi_target (links TP-ISO27001-CRA-MaschinenVO-v1). Generator runs the convergence pattern through RS-005: 4/4 machine-safety delta MISSING + 4/4 expected multi-target caps converge. PASS. - RTS-001 (component): MaschinenVO modeled as `uncertain` (a pure component is usually not a machine; deciding question is_safety_component) — engine must never assert it applies. Honest, parallel to the Data-Act handling. - RTS-002 (machine, QMS-only): MaschinenVO `applies` (is_machine) but LOW convergence — no ISMS means the cyber side is entirely delta, so few caps are shared. The honest contrast that the convergence USP rewards companies who already run an ISMS. - generator: per-RTS maschinenvo/convergence Soll-Ist checks; convergence pattern run once and reused. Data Act stays `uncertain` everywhere, never asserted. All 3 RTS PASS. 18 tests (transition+company), mypy --strict clean, check-loc 0. Non-runtime (knowledge + reference harness) -> no deploy (ADR-001). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -295,6 +295,27 @@ for _pf in sorted(os.listdir(_pat_d2)):
|
||||
with open(os.path.join(_pat_d2, _pf), encoding="utf-8") as _h:
|
||||
_pp = yaml.safe_load(_h)
|
||||
_pat_by_id[_pp["id"]] = _pp
|
||||
# Convergence pattern (multi-target) — run once through RS-005; reused by RTS-003 + the convergence section.
|
||||
_CONV_ID = "TP-ISO27001-CRA-MaschinenVO-v1"
|
||||
_MV_IDS = {"MaschinenVO", "MachineryRegulation", "Maschinenverordnung"}
|
||||
CP = _pat_by_id.get(_CONV_ID)
|
||||
_conv = None
|
||||
_cp_missing = set() # type: ignore[var-annotated]
|
||||
_all_t = [] # type: ignore[var-annotated]
|
||||
_delta_t = {} # type: ignore[var-annotated]
|
||||
if CP:
|
||||
_all_t = sorted({t for it in CP["likely_covered"] + CP["delta_requirements"] for t in it.get("covers_targets", [])})
|
||||
_delta_t = {d["capability"]: d.get("covers_targets", []) for d in CP["delta_requirements"]}
|
||||
_conv = regulatory_convergence(_delta_t, _all_t)
|
||||
_cp_have = [a["capability"] for a in CP["likely_covered"]]
|
||||
_cp_map = {"ISO27001": CapabilityMappingEntry(capability_ids=_cp_have, confidence=Confidence.MEDIUM)}
|
||||
_cp_prof = build_company_profile(
|
||||
CompanyContext(company_id="conv", certifications=[Certification(certification_id="ISO27001")]), _cp_map)
|
||||
_cp_reqs = [TargetRequirement(capability_id=a["capability"]) for a in CP["likely_covered"]]
|
||||
_cp_reqs += [TargetRequirement(capability_id=d["capability"], question_intent=d.get("needed_information", "verify_existence"))
|
||||
for d in CP["delta_requirements"]]
|
||||
_cp_a = assess_transition(TransitionContext(company_id="conv", target=TransitionGoal(target_id="CRA+MaschinenVO")), _cp_reqs, _cp_prof)
|
||||
_cp_missing = {c.capability_id for c in _cp_a.coverage if c.status == CoverageStatus.MISSING}
|
||||
_rts_rows: List[Row] = []
|
||||
for _rf in sorted(f for f in os.listdir(_rts_dir) if f.startswith("RTS-") and f.endswith(".yaml")):
|
||||
with open(os.path.join(_rts_dir, _rf), encoding="utf-8") as _f:
|
||||
@@ -334,9 +355,38 @@ for _rf in sorted(f for f in os.listdir(_rts_dir) if f.startswith("RTS-") and f.
|
||||
w("- Expected Delta erfüllt: **%s** (%d/%d Soll-Delta in der Ist-Lücke)" % ("ja" if _delta_ok else "NEIN", len(_exp_delta & _actual_missing), len(_exp_delta)))
|
||||
w("- Expected likely_covered erfüllt: **%s**" % ("ja" if _cov_ok else "NEIN"))
|
||||
w("- Data Act: Engine sagt **%s** (Soll: uncertain; nie asserted) → %s" % (_da, "ok" if _da_ok else "FEHLER (asserted!)"))
|
||||
# MaschinenVO — multi-regulation target. uncertain (component) vs applies (machine); convergence only with an ISMS.
|
||||
_mv = RTS["expected_outcome"].get("maschinenvo")
|
||||
_mv_ok = True
|
||||
_mv_tag = "—"
|
||||
if _mv and _mv.get("expectation") == "uncertain":
|
||||
_mv_ok = not (_appl & _MV_IDS) # a component: never wrongly asserted as in-scope
|
||||
_mv_tag = "uncertain(ok)" if _mv_ok else "uncertain(FEHLER:asserted)"
|
||||
w("- MaschinenVO: Soll **uncertain** (Komponente, deciding: is_safety_component) → Engine asserted nicht: %s" % ("ok" if _mv_ok else "FEHLER"))
|
||||
elif _mv: # expectation: applies (is_machine: true)
|
||||
_mv_exp = set(_mv.get("expected_delta_at_least", []))
|
||||
if _mv.get("convergence_pattern") == _CONV_ID and CP:
|
||||
_mv_ok = _mv_exp <= _cp_missing
|
||||
_mv_tag = "applies %d/%d Safety-Delta" % (len(_mv_exp & _cp_missing), len(_mv_exp))
|
||||
w("- MaschinenVO **gilt** (is_machine): %d/%d Safety-Delta in der Ist-Lücke (Convergence-Pattern) → %s" % (len(_mv_exp & _cp_missing), len(_mv_exp), "ok" if _mv_ok else "NEIN"))
|
||||
else:
|
||||
_mv_ok = bool(_mv_exp)
|
||||
_mv_tag = "applies (geringe Konvergenz, kein ISMS)"
|
||||
w("- MaschinenVO **gilt** (is_machine): Safety-Delta %s — **geringe Konvergenz ohne ISMS** (RS-004 reg-map-Gate offen)" % (", ".join(sorted(_mv_exp)) or "—"))
|
||||
# Convergence — the USP: capabilities covering CRA AND MaschinenVO at once.
|
||||
_cv = RTS["expected_outcome"].get("convergence")
|
||||
_cv_ok = True
|
||||
if _cv and _conv:
|
||||
_cv_exp = set(_cv.get("expected_multi_target_at_least", []))
|
||||
_cv_hit = _cv_exp & set(_conv.multi_target_capabilities)
|
||||
_cv_ok = _cv_exp <= set(_conv.multi_target_capabilities)
|
||||
w("- Konvergenz CRA∩MaschinenVO: %d/%d erwartete Multi-Target-Caps → %s (%s)" % (len(_cv_hit), len(_cv_exp), "ok" if _cv_ok else "NEIN", _conv.headline))
|
||||
_ok = _ok and _mv_ok and _cv_ok
|
||||
w("")
|
||||
_rts_rows.append(("%s (%s→CRA)" % (RTS["id"], _src), "PASS" if _ok else "PARTIAL",
|
||||
"%d/%d Delta-Soll · likely_covered %s · DataAct=%s" % (len(_exp_delta & _actual_missing), len(_exp_delta), "ok" if _cov_ok else "NEIN", _da)))
|
||||
_rts_rows.append(("%s (%s→CRA%s)" % (RTS["id"], _src, "+MaschVO" if _mv else ""), "PASS" if _ok else "PARTIAL",
|
||||
"%d/%d Delta-Soll · likely_covered %s · DataAct=%s · MaschVO=%s%s" % (
|
||||
len(_exp_delta & _actual_missing), len(_exp_delta), "ok" if _cov_ok else "NEIN", _da, _mv_tag,
|
||||
" · Konvergenz ok" if (_cv and _cv_ok) else "")))
|
||||
coverage_table(_rts_rows)
|
||||
|
||||
# ── Regulatory Convergence — CRA + MaschinenVO (the multi-regulation USP) ───
|
||||
@@ -344,12 +394,7 @@ w("## Regulatory Convergence — CRA + MaschinenVO (Cross-Regulation Capability
|
||||
w("")
|
||||
w("_Der USP: welche Capability deckt MEHRERE Regelwerke gleichzeitig? (Convergence Pattern, RTS-003-Archetyp.)_")
|
||||
w("")
|
||||
_cp_path = os.path.join(_pat_dir, "transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml")
|
||||
with open(_cp_path, encoding="utf-8") as _f:
|
||||
CP = yaml.safe_load(_f)
|
||||
_all_t = sorted({t for it in CP["likely_covered"] + CP["delta_requirements"] for t in it.get("covers_targets", [])})
|
||||
_delta_t = {d["capability"]: d.get("covers_targets", []) for d in CP["delta_requirements"]}
|
||||
_conv = regulatory_convergence(_delta_t, _all_t)
|
||||
assert _conv is not None and CP is not None # precomputed before the RTS loop (convergence pattern on disk)
|
||||
w("**Cross-Regulation Capability Mapping (Delta):** %s" % _conv.headline)
|
||||
w("")
|
||||
w("**Konvergenz — diese neuen Maßnahmen decken BEIDE Regelwerke gleichzeitig:**")
|
||||
|
||||
Reference in New Issue
Block a user