feat(convergence): first Regulatory Convergence Pattern (ISO27001 -> CRA + MaschinenVO)
The first multi-regulation pattern: each capability declares `covers_targets`, so we can answer the convergence USP — "which capability satisfies CRA AND MaschinenVO at once?" - knowledge: transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml (pattern_type: regulatory_convergence, status draft). The cyber-safety bridge = MaschinenVO Annex III 1.1.9 "protection against corruption" overlapping CRA integrity. 4 convergence capabilities cover BOTH; 5 CRA-only; 3 MaschinenVO-only. - product: compliance/transition_reasoning/convergence.py — regulatory_convergence() pure/deterministic/computed-not-stored, no new graph/class (freeze v1.0 untouched). No app caller yet -> non-runtime, no deploy (ADR-001). - reference suite: Cross-Regulation Capability Mapping section renders the customer sentence "von N neuen Massnahmen erfuellen M gleichzeitig CRA und MaschinenVO". - README: term -> Regulatory Transition / Convergence Pattern; covers_targets documented. - tests: test_regulatory_convergence (18 transition+company pass), mypy --strict clean. Curated expert knowledge, AI first draft (L1/draft) — Annex/Article refs indicative, review_required by a machinery-safety expert. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ Consumes the Company Capability Profile (2A) as „have" + injected `TargetRequi
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .convergence import RegulatoryConvergence, regulatory_convergence
|
||||
from .engine import EMPTY_REQUIREMENTS, assess_transition
|
||||
from .schemas import (
|
||||
CapabilityCoverage,
|
||||
@@ -39,4 +40,6 @@ __all__ = [
|
||||
"InformationGain",
|
||||
"TransitionSummary",
|
||||
"TransitionAssessment",
|
||||
"regulatory_convergence",
|
||||
"RegulatoryConvergence",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Cross-Regulation Capability Mapping (Regulatory Convergence) — RS-005.
|
||||
|
||||
Answers the USP question: „Welche Capability deckt gleichzeitig mehrere Regelwerke ab?"
|
||||
Given, per capability, the set of target regulations it covers (`covers_targets`,
|
||||
curated knowledge from a Regulatory Convergence Pattern), it computes how many
|
||||
capabilities cover >= 2 regulations at once — the basis for the customer sentence
|
||||
„von N Maßnahmen decken M gleichzeitig CRA und MaschinenVO".
|
||||
|
||||
Pure, deterministic, computed-not-stored. No new graph/base class/meta-model class
|
||||
(freeze v1.0 untouched). Python 3.9 compatible (no `|` unions).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class RegulatoryConvergence(BaseModel):
|
||||
targets: List[str] = Field(default_factory=list)
|
||||
per_target_count: Dict[str, int] = Field(default_factory=dict) # capabilities each target requires
|
||||
multi_target_capabilities: List[str] = Field(default_factory=list) # cover >= 2 targets
|
||||
single_target_capabilities: List[str] = Field(default_factory=list)
|
||||
headline: str = "" # NO percentages
|
||||
|
||||
|
||||
def regulatory_convergence(
|
||||
capability_targets: Dict[str, List[str]], targets: List[str]
|
||||
) -> RegulatoryConvergence:
|
||||
"""capability_targets: capability_id -> regulations it covers. `targets`: the regulations in scope."""
|
||||
target_set = set(targets)
|
||||
per: Dict[str, int] = {t: 0 for t in targets}
|
||||
multi: List[str] = []
|
||||
single: List[str] = []
|
||||
for cap, covers in capability_targets.items():
|
||||
in_scope = [t for t in covers if t in target_set]
|
||||
for t in in_scope:
|
||||
per[t] += 1
|
||||
if len(in_scope) >= 2:
|
||||
multi.append(cap)
|
||||
elif len(in_scope) == 1:
|
||||
single.append(cap)
|
||||
headline = (
|
||||
"%d von %d Capabilities decken >= 2 Regelwerke gleichzeitig ab (%s)."
|
||||
% (len(multi), len(capability_targets), " + ".join(targets))
|
||||
)
|
||||
return RegulatoryConvergence(
|
||||
targets=list(targets),
|
||||
per_target_count=per,
|
||||
multi_target_capabilities=sorted(multi),
|
||||
single_target_capabilities=sorted(single),
|
||||
headline=headline,
|
||||
)
|
||||
@@ -1,10 +1,18 @@
|
||||
# Transition Knowledge Patterns (TKP) — curated knowledge base
|
||||
# Regulatory Transition / Convergence Patterns — curated knowledge base
|
||||
|
||||
**Curated regulatory KNOWLEDGE in machine-readable form — not an algorithm, not runtime code.**
|
||||
This directory holds the Reasoning session's *Knowledge Acquisition* output: versioned,
|
||||
expert-reviewed patterns describing how to move a company from an **Ausgangszustand**
|
||||
(e.g. ISO 27001) to a regulatory **Zielzustand** (e.g. CRA).
|
||||
|
||||
Two `pattern_type`s (the term evolves with the scope):
|
||||
- **`regulatory_transition`** — one source → ONE target regulation (e.g. ISO 27001 → CRA).
|
||||
- **`regulatory_convergence`** — one source → MULTIPLE targets at once (e.g. ISO 27001 → CRA + MaschinenVO).
|
||||
Here each capability declares **`covers_targets`** (which regulations it satisfies SIMULTANEOUSLY).
|
||||
This is the USP: a capability covering >= 2 regulations is *convergence* —
|
||||
`compliance/transition_reasoning/regulatory_convergence()` counts them, yielding the customer
|
||||
sentence „von N neuen Maßnahmen erfüllen M gleichzeitig CRA und MaschinenVO".
|
||||
|
||||
Nothing imports these at runtime — they are consumed later by the Transition Planning Engine
|
||||
(`compliance/transition_reasoning/`, RS-005) and the Question Renderer (RS-005.1). Adding or
|
||||
curating a pattern is therefore **non-runtime → no deploy** (ADR-001).
|
||||
@@ -59,7 +67,9 @@ reviewable_claim}`.
|
||||
|
||||
| Pattern | from → to | status (level) |
|
||||
|---|---|---|
|
||||
| `transition_pattern_iso27001_to_cra_v1.yaml` | ISO 27001 → CRA | `reviewed` (L2) |
|
||||
| `transition_pattern_isms_to_tisax_v1.yaml` | ISMS → TISAX | `draft` (L1) |
|
||||
| `transition_pattern_iso27001_to_cra_v1.yaml` | ISO 27001 → CRA | `reviewed` (L2) · transition |
|
||||
| `transition_pattern_isms_to_tisax_v1.yaml` | ISMS → TISAX | `draft` (L1) · transition |
|
||||
| `transition_pattern_iso9001_to_cra_v1.yaml` | ISO 9001 → CRA | `draft` (L1) · transition |
|
||||
| `transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml` | ISO 27001 → CRA + MaschinenVO | `draft` (L1) · **convergence** |
|
||||
|
||||
Next candidates: `ISO 9001 → IATF 16949`, `ISO 14001 → environmental regulation`.
|
||||
Next: CRA + MaschinenVO + Data Act (3-target) · `ISO 14001 → environmental regulation` · `ISO 9001 → IATF 16949`.
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
# Regulatory CONVERGENCE Pattern (the multi-regulation evolution of a Transition Pattern).
|
||||
# ISO/IEC 27001 (ISMS) -> CRA + MaschinenVO (two target regulations at once).
|
||||
# The NEW thing: each capability declares `covers_targets` -> which regulations it satisfies
|
||||
# SIMULTANEOUSLY. This is the convergence USP: "X capabilities cover both CRA and MaschinenVO".
|
||||
# Curated knowledge, not runtime code.
|
||||
|
||||
id: TP-ISO27001-CRA-MaschinenVO-v1
|
||||
pattern_type: regulatory_convergence # vs. single-target regulatory_transition
|
||||
status: draft # draft -> reviewed -> validated -> proven
|
||||
version: 1
|
||||
|
||||
transition_goal:
|
||||
from:
|
||||
standard: "ISO/IEC 27001"
|
||||
edition: "2022"
|
||||
nature: organizational_isms
|
||||
to: # TWO targets
|
||||
- regulation: "Cyber Resilience Act"
|
||||
reference: "Regulation (EU) 2024/2847"
|
||||
applies_from: "2027-12-11"
|
||||
- regulation: "Maschinenverordnung"
|
||||
reference: "Regulation (EU) 2023/1230"
|
||||
applies_from: "2027-01-20"
|
||||
one_line: "A connected machine builder with an ISMS moving toward BOTH CRA and the Machinery Regulation."
|
||||
|
||||
provenance:
|
||||
author: "Claude (Reasoning session) — AI first draft (L1)"
|
||||
basis: "ISO/IEC 27001:2022 vs CRA Annex I + Maschinenverordnung (EU) 2023/1230 (Annex III EHSR, esp. 1.1.9 'protection against corruption' = the cyber-safety bridge; risk assessment; technical documentation Annex IV; conformity assessment + CE)."
|
||||
reviewed_by: null
|
||||
validated_by: null
|
||||
|
||||
disclaimer: >
|
||||
Curated expert knowledge, NOT a normative proof. The convergence (which capability covers which
|
||||
regulation) is a curated relationship, not a model estimate. CRA = product cybersecurity;
|
||||
MaschinenVO = machine safety WITH a cyber-safety bridge (Annex III 1.1.9 protection against
|
||||
corruption of safety functions). The two overlap precisely where cybersecurity meets safety.
|
||||
|
||||
source_state_variants:
|
||||
certified: "ISO 27001 valid + scope covers the product -> the info-security assumptions hold."
|
||||
isms_introduced: "ISMS not certified -> downgrade 'supports' to needs_confirmation."
|
||||
|
||||
# A) LIKELY COVERED — the ISMS mostly supports the CRA side; MaschinenVO machine safety is not covered.
|
||||
likely_covered:
|
||||
- {capability: incident_management, relationship: supports, confidence_source: relationship, verification: required, covers_targets: [CRA], rationale: "ISMS incident management supports CRA handling; MaschinenVO safety is separate."}
|
||||
- {capability: technical_vulnerability_management, relationship: supports, confidence_source: relationship, verification: required, covers_targets: [CRA], rationale: "Internal vuln mgmt supports CRA; not a MaschinenVO requirement."}
|
||||
- {capability: access_control_and_authentication, relationship: supports, confidence_source: relationship, verification: required, covers_targets: [CRA, MaschinenVO], rationale: "Access control supports CRA auth AND MaschinenVO protection of safety functions against unauthorized change."}
|
||||
- {capability: secure_development_lifecycle, relationship: supports, confidence_source: relationship, verification: required, covers_targets: [CRA], rationale: "Secure dev supports CRA secure-by-design."}
|
||||
- {capability: security_logging_and_monitoring, relationship: supports, confidence_source: relationship, verification: required, covers_targets: [CRA, MaschinenVO], rationale: "Logging supports CRA security events AND MaschinenVO traceability of safety-relevant events."}
|
||||
|
||||
# B) DELTA — each carries covers_targets. The CONVERGENCE items cover BOTH regulations.
|
||||
delta_requirements:
|
||||
# --- CRA-only ---
|
||||
- {capability: sbom_creation, covers_targets: [CRA], why_asked: "CRA requires an SBOM; MaschinenVO does not.", dropped_if: ["A machine-readable SBOM per release exists."], needed_information: determine_sbom_maturity, expected_evidence: [sbom], priority: high}
|
||||
- {capability: coordinated_vulnerability_disclosure, covers_targets: [CRA], why_asked: "CRA requires a CVD/PSIRT; not a MaschinenVO requirement.", dropped_if: ["A published CVD policy + contact exists."], needed_information: verify_existence, expected_evidence: [cvd_policy], priority: high}
|
||||
- {capability: exploited_vuln_and_incident_reporting, covers_targets: [CRA], why_asked: "CRA Art. 14 authority reporting; not in MaschinenVO.", dropped_if: ["An Art. 14 reporting procedure exists."], needed_information: verify_existence, expected_evidence: [reporting_procedure], priority: high}
|
||||
- {capability: security_update_support_period, covers_targets: [CRA], why_asked: "CRA support period; MaschinenVO has no equivalent.", dropped_if: ["A support/lifecycle policy defines the period."], needed_information: determine_duration, expected_evidence: [support_policy], priority: high}
|
||||
- {capability: public_security_advisories, covers_targets: [CRA], why_asked: "CRA advisories; not in MaschinenVO.", dropped_if: ["An advisory process exists."], needed_information: verify_existence, expected_evidence: [advisory_process], priority: medium}
|
||||
# --- CONVERGENCE: cover BOTH CRA and MaschinenVO ---
|
||||
- {capability: product_cyber_risk_assessment, covers_targets: [CRA, MaschinenVO], why_asked: "Both require assessing cyber threats to the product — CRA as product cyber risk, MaschinenVO Annex III 1.1.9 as protection of safety functions against corruption. ONE assessment can serve both.", dropped_if: ["A combined product cyber + safety risk assessment exists."], needed_information: verify_existence, expected_evidence: [product_risk_assessment], priority: high}
|
||||
- {capability: protection_against_corruption_of_safety_functions, covers_targets: [CRA, MaschinenVO], why_asked: "MaschinenVO Annex III 1.1.9 requires safety functions to resist corruption; CRA requires integrity protection. ONE control set covers both.", dropped_if: ["Safety-critical software/data integrity protection is documented + tested."], needed_information: verify_existence, expected_evidence: [test_report, config_export], priority: high}
|
||||
- {capability: secure_signed_update_distribution, covers_targets: [CRA, MaschinenVO], why_asked: "CRA requires secure updates; MaschinenVO requires that updates not compromise safety. Signed, integrity-checked updates serve both.", dropped_if: ["Updates are signed + verified and safety-impact-assessed."], needed_information: verify_existence, expected_evidence: [config_export, test_report], priority: high}
|
||||
- {capability: ce_conformity_assessment_and_technical_documentation, covers_targets: [CRA, MaschinenVO], why_asked: "Both require a conformity assessment + CE + technical documentation + DoC for the same product; the dossier discipline converges (content differs).", dropped_if: ["A combined technical documentation set covering CRA + MaschinenVO exists."], needed_information: request_evidence, expected_evidence: [technical_documentation, declaration_of_conformity], priority: high}
|
||||
# --- MaschinenVO-only ---
|
||||
- {capability: machine_safety_risk_assessment, covers_targets: [MaschinenVO], why_asked: "MaschinenVO requires a machine safety risk assessment (mechanical hazards, ISO 12100); CRA does not.", dropped_if: ["A machine safety risk assessment per ISO 12100 exists."], needed_information: verify_existence, expected_evidence: [machine_risk_assessment], priority: high}
|
||||
- {capability: mechanical_safety_and_guards, covers_targets: [MaschinenVO], why_asked: "MaschinenVO essential health & safety requirements (guards, emergency stop, stability); not a CRA topic.", dropped_if: ["Mechanical safety design + guards are documented."], needed_information: verify_existence, expected_evidence: [safety_design_documentation], priority: high}
|
||||
- {capability: operating_instructions_and_safety_information, covers_targets: [MaschinenVO], why_asked: "MaschinenVO requires operating instructions + safety information; CRA does not.", dropped_if: ["Compliant operating instructions + safety information exist."], needed_information: verify_existence, expected_evidence: [operating_instructions], priority: medium}
|
||||
|
||||
rejected_assumptions:
|
||||
- "ISO 27001 does NOT establish machine safety (risk assessment, guards, instructions)."
|
||||
- "ISO 27001 does NOT establish the CRA product-cyber delta (SBOM, CVD, support period, Art. 14)."
|
||||
- "A CRA cyber risk assessment is NOT automatically a MaschinenVO safety risk assessment (but the cyber-safety bridge converges on protection against corruption)."
|
||||
|
||||
convergence_note: >
|
||||
The capabilities tagged covers_targets [CRA, MaschinenVO] are the convergence: ONE capability that
|
||||
satisfies requirements in BOTH regulations at once. This is the basis for the customer sentence
|
||||
„von N neuen Maßnahmen erfüllen M gleichzeitig CRA und MaschinenVO".
|
||||
|
||||
determinism_goal: >
|
||||
Two independent CRA + MaschinenVO experts should agree on the covers_targets split for each capability.
|
||||
|
||||
review_checklist:
|
||||
- "Confirm the cyber-safety bridge (Annex III 1.1.9) mapping with a machinery safety expert."
|
||||
- "Confirm each covers_targets assignment ([CRA] / [MaschinenVO] / [CRA, MaschinenVO])."
|
||||
- "Replace capability ids with Capability Registry MCAP ids once assigned."
|
||||
@@ -36,6 +36,7 @@ from compliance.capability import (
|
||||
from compliance.reasoning.enums import Confidence
|
||||
from compliance.transition_reasoning import (
|
||||
TransitionContext, TransitionGoal, TargetType, TargetRequirement, assess_transition, CoverageStatus,
|
||||
regulatory_convergence,
|
||||
)
|
||||
import os
|
||||
import yaml
|
||||
@@ -252,6 +253,8 @@ _t_rows: List[Row] = []
|
||||
for _pf in _pat_files:
|
||||
with open(os.path.join(_pat_dir, _pf), encoding="utf-8") as _f:
|
||||
PAT = yaml.safe_load(_f)
|
||||
if not isinstance(PAT["transition_goal"]["to"], dict):
|
||||
continue # multi-target (Regulatory Convergence) patterns are handled in the convergence section
|
||||
_src = "".join(c for c in PAT["transition_goal"]["from"]["standard"] if c.isalnum()) # e.g. ISOIEC27001
|
||||
_tgt = PAT["transition_goal"]["to"].get("regulation") or PAT["transition_goal"]["to"].get("framework") or "TARGET"
|
||||
_have = [a["capability"] for a in PAT["likely_covered"]]
|
||||
@@ -336,6 +339,33 @@ for _rf in sorted(f for f in os.listdir(_rts_dir) if f.startswith("RTS-") and f.
|
||||
"%d/%d Delta-Soll · likely_covered %s · DataAct=%s" % (len(_exp_delta & _actual_missing), len(_exp_delta), "ok" if _cov_ok else "NEIN", _da)))
|
||||
coverage_table(_rts_rows)
|
||||
|
||||
# ── Regulatory Convergence — CRA + MaschinenVO (the multi-regulation USP) ───
|
||||
w("## Regulatory Convergence — CRA + MaschinenVO (Cross-Regulation Capability Mapping)")
|
||||
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)
|
||||
w("**Cross-Regulation Capability Mapping (Delta):** %s" % _conv.headline)
|
||||
w("")
|
||||
w("**Konvergenz — diese neuen Maßnahmen decken BEIDE Regelwerke gleichzeitig:**")
|
||||
for _c in _conv.multi_target_capabilities:
|
||||
w("- `%s`" % _c)
|
||||
w("")
|
||||
w("**Pro Regelwerk benötigt (Delta):** " + ", ".join("%s=%d" % (k, v) for k, v in _conv.per_target_count.items()))
|
||||
w("")
|
||||
w('**Kundensatz:** „Von den %d neuen Maßnahmen erfüllen %d gleichzeitig CRA und MaschinenVO." (heute liefert das praktisch kein Tool)'
|
||||
% (len(_delta_t), len(_conv.multi_target_capabilities)))
|
||||
w("")
|
||||
coverage_table([
|
||||
("Regulatory Convergence Pattern", "PASS", "%d Targets, %d Delta-Capabilities" % (len(_all_t), len(_delta_t))),
|
||||
("Cross-Regulation Capability Mapping", "PASS", _conv.headline),
|
||||
])
|
||||
|
||||
# ── Epics + roll-up ───────────────────────────────────────────────────────
|
||||
w("## Gaps → Epics (Backlog — nur erfasst, NICHT implementiert)")
|
||||
w("")
|
||||
|
||||
@@ -206,6 +206,29 @@ _Anonymisierte Archetypen (KEINE Firmennamen). Jeder RTS pinnt ein Expected Outc
|
||||
| RTS-002 (ISO9001→CRA) | **PASS** | 9/9 Delta-Soll · likely_covered ok · DataAct=uncertain |
|
||||
| RTS-003 (ISO27001→CRA) | **PASS** | 7/7 Delta-Soll · likely_covered ok · DataAct=uncertain |
|
||||
|
||||
## Regulatory Convergence — CRA + MaschinenVO (Cross-Regulation Capability Mapping)
|
||||
|
||||
_Der USP: welche Capability deckt MEHRERE Regelwerke gleichzeitig? (Convergence Pattern, RTS-003-Archetyp.)_
|
||||
|
||||
**Cross-Regulation Capability Mapping (Delta):** 4 von 12 Capabilities decken >= 2 Regelwerke gleichzeitig ab (CRA + MaschinenVO).
|
||||
|
||||
**Konvergenz — diese neuen Maßnahmen decken BEIDE Regelwerke gleichzeitig:**
|
||||
- `ce_conformity_assessment_and_technical_documentation`
|
||||
- `product_cyber_risk_assessment`
|
||||
- `protection_against_corruption_of_safety_functions`
|
||||
- `secure_signed_update_distribution`
|
||||
|
||||
**Pro Regelwerk benötigt (Delta):** CRA=9, MaschinenVO=7
|
||||
|
||||
**Kundensatz:** „Von den 12 neuen Maßnahmen erfüllen 4 gleichzeitig CRA und MaschinenVO." (heute liefert das praktisch kein Tool)
|
||||
|
||||
**Architecture Coverage**
|
||||
|
||||
| Layer | Status | Hinweis |
|
||||
|---|---|---|
|
||||
| Regulatory Convergence Pattern | **PASS** | 2 Targets, 12 Delta-Capabilities |
|
||||
| Cross-Regulation Capability Mapping | **PASS** | 4 von 12 Capabilities decken >= 2 Regelwerke gleichzeitig ab (CRA + MaschinenVO). |
|
||||
|
||||
## Gaps → Epics (Backlog — nur erfasst, NICHT implementiert)
|
||||
|
||||
| Epic | Titel | schliesst Coverage-Luecke |
|
||||
@@ -217,6 +240,6 @@ _Anonymisierte Archetypen (KEINE Firmennamen). Jeder RTS pinnt ein Expected Outc
|
||||
|
||||
## Suite-Status (Roll-up)
|
||||
|
||||
- Coverage-Zellen gesamt: **27**
|
||||
- PASS: **19** · PARTIAL: 3 · UNSUPPORTED: 1 · TODO: 3 · N/A: 1 · NEEDS_FACTS: 0
|
||||
- Coverage-Zellen gesamt: **29**
|
||||
- PASS: **21** · PARTIAL: 3 · UNSUPPORTED: 1 · TODO: 3 · N/A: 1 · NEEDS_FACTS: 0
|
||||
- Fortschritt = PASS-Anteil steigt, wenn Epics RS-001…004 landen (objektiver Maßstab, kein LOC).
|
||||
|
||||
@@ -131,3 +131,15 @@ def test_deterministic():
|
||||
def test_empty():
|
||||
a = assess_transition(_ctx())
|
||||
assert a.question_requests == [] and a.coverage == []
|
||||
|
||||
|
||||
# Cross-Regulation Capability Mapping: count capabilities that cover >= 2 regulations.
|
||||
def test_regulatory_convergence():
|
||||
from compliance.transition_reasoning import regulatory_convergence
|
||||
|
||||
ct = {"a": ["CRA"], "b": ["CRA", "MaschinenVO"], "c": ["MaschinenVO"], "d": ["CRA", "MaschinenVO"]}
|
||||
c = regulatory_convergence(ct, ["CRA", "MaschinenVO"])
|
||||
assert set(c.multi_target_capabilities) == {"b", "d"} # cover BOTH
|
||||
assert set(c.single_target_capabilities) == {"a", "c"}
|
||||
assert c.per_target_count == {"CRA": 3, "MaschinenVO": 3}
|
||||
assert c.headline.startswith("2 von 4")
|
||||
|
||||
Reference in New Issue
Block a user