# ruff: noqa # mypy: ignore-errors """Customer Mission #2 — the company arrives with a PROFILE, not a journey. Mission #1 (Maschinenbauer) ended on one open seam: „Scope → Journey" — a consultant had to hand-pick „ISO 27001 → CRA". This mission deliberately STRESSES that seam with a multi-certified company (the ETO-archetype: many certs at once) and asks the reframe question: is the start a *certification* you map to a journey, or a *Company Capability Profile* you compute a delta from? Finding (proven below with the real engines): there is NO per-certificate journey. The start is the whole profile; certifications are OBSERVATIONS that feed it; more evidence = smaller delta (12 → 9). The „journey" is not selected — it is the delta `(Company Profile, Target)`. That shrinks Mission #1's jump: the seam is not a journey-matcher, it is profile-intake + target-pick. Synthetic multi-certified company (NO real names). Runs the REAL engines end-to-end. Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/mission_multicert_cra.py Not product code; not imported by the app. Non-runtime -> no deploy. """ from __future__ import annotations import os import yaml from compliance.company import ( CompanyContext, Certification, CapabilityMappingEntry, build_company_profile, ) from compliance.reasoning.enums import Confidence from compliance.transition_reasoning import ( TransitionContext, TransitionGoal, TargetRequirement, assess_transition, CoverageStatus, ) from compliance.optimization import roadmap_from_delta, select_within_budget OUT = [] RAT = [] # (question, answer) — the per-mission selection rationale the user asked to record def w(s=""): OUT.append(s) def rationale(question, answer): RAT.append((question, answer)) _HERE = os.path.dirname(__file__) _K = os.path.join(_HERE, "..", "knowledge") CP = yaml.safe_load(open(os.path.join(_K, "transition_patterns", "transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml"), encoding="utf-8")) w('# Customer Mission #2 — „Wir haben SCHON viel. Was fehlt UNS noch für die CRA?"') w("") w('_Zweite Mission, bewusst ANDERS als #1: nicht „ein Zertifikat → ein Ziel", sondern ein hoch-zertifiziertes Unternehmen, das mit einem ganzen Profil ankommt. Test der einzigen offenen Naht aus Mission #1 (Scope → Journey). Synthetischer Kunde, keine echten Namen._') w("") w("## Der Kunde (synthetisch, hoch-zertifiziert)") w("> **ISO 9001** · **ISO 27001** · **ISO 14001** · **TISAX** · **CE-Prozess** · **PSIRT** · vernetzte Maschinen · Export EU") w('> **Eine Frage:** „Wir sind schon in vielem zertifiziert — was genau fehlt UNS noch, um CRA-konform zu sein?"') w("") # ── 0. Company Capability Profile — DER Startzustand (nicht ein Zertifikat) ── # Each certification is an OBSERVATION yielding probable capabilities. The profile is their AGGREGATE. # Evidence is TARGET-RELATIVE: ISO 14001 is in the profile but irrelevant to the CRA (honest). infosec = [a["capability"] for a in CP["likely_covered"]] CERT_OBS = { "ISO27001": (infosec, Confidence.MEDIUM, "Informationssicherheit (ISMS)"), "TISAX": (infosec, Confidence.MEDIUM, "Automotive-ISMS — verstärkt dieselben Infosec-Fähigkeiten"), "PSIRT": (["coordinated_vulnerability_disclosure", "exploited_vuln_and_incident_reporting"], Confidence.HIGH, "Product-Security-Incident-Response — deckt ZWEI CRA-Delta-Fähigkeiten"), "ISO9001": (["ce_conformity_assessment_and_technical_documentation"], Confidence.MEDIUM, "QM-Dokumentendisziplin → CE-/Technische-Doku-Fähigkeit"), "ISO14001": (["environmental_management_documentation"], Confidence.MEDIUM, "Umweltmanagement — im Profil, aber für die CRA NICHT relevant"), } cmap = {k: CapabilityMappingEntry(capability_ids=v[0], confidence=v[1]) for k, v in CERT_OBS.items()} ctx = CompanyContext(company_id="mc", certifications=[Certification(certification_id=k) for k in CERT_OBS]) profile = build_company_profile(ctx, cmap) w("## 0. Company Capability Profile — der eigentliche Startzustand") w("> Das Unternehmen bringt **kein Zertifikat als Startpunkt**, sondern ein **Profil**. Jedes Zertifikat ist eine *Beobachtung*, die wahrscheinliche Fähigkeiten beisteuert; der Startzustand ist ihre **Aggregation**.") w("") w("| Zertifikat (Beobachtung) | steuert Fähigkeiten bei | Vertrauen |") w("|---|---|---|") for k, (caps, conf, note) in CERT_OBS.items(): w("| **%s** — %s | %s | %s |" % (k, note, ", ".join("`%s`" % c for c in caps), conf.value)) w("") w("→ **Evidence ist zielrelativ:** ISO 14001 liegt im Profil, hilft der CRA aber **nicht**; PSIRT (oft übersehen) deckt **zwei** CRA-kritische Delta-Fähigkeiten. Genau deshalb darf man **nicht ein Zertifikat** zur Journey machen — das ganze Profil zählt.") w("") # ── 1. Ziel (Intent) — was wollen SIE erreichen? ────────────────────────── # The selection input is NOT a certificate. It is (Company Profile, Target). Intent = „CRA-konform werden". TARGET = "CRA" w("## 1. Ziel _(Intent)_ — was wollen Sie erreichen?") w('- Intent: **„CRA-konform werden"** → Ziel-Profil = **CRA** (die von der CRA geforderten Fähigkeiten).') w("- Auswahl-Eingabe ist damit **(Company Profile, Ziel)** — **kein** Zertifikat, das auf eine Journey gemappt wird.") w("") # ── 2. Capability Delta — Profil → Ziel (das ist „die Journey") ──────────── reqs = [TargetRequirement(capability_id=a["capability"]) for a in CP["likely_covered"]] reqs += [TargetRequirement(capability_id=d["capability"], question_intent=d.get("needed_information", "verify_existence")) for d in CP["delta_requirements"]] assess = assess_transition(TransitionContext(company_id="mc", target=TransitionGoal(target_id=TARGET)), reqs, profile) missing = sorted({c.capability_id for c in assess.coverage if c.status == CoverageStatus.MISSING}) # counterfactual: a single-certificate company (ISO 27001 only) — to show evidence shrinks the delta. single_prof = build_company_profile( CompanyContext(company_id="s", certifications=[Certification(certification_id="ISO27001")]), {"ISO27001": CapabilityMappingEntry(capability_ids=infosec, confidence=Confidence.MEDIUM)}) single_missing = sorted({c.capability_id for c in assess_transition(TransitionContext(company_id="s", target=TransitionGoal(target_id=TARGET)), reqs, single_prof).coverage if c.status == CoverageStatus.MISSING}) closed_by_evidence = sorted(set(single_missing) - set(missing)) w('## 2. Capability Delta — Profil → CRA _(das IST die „Journey")_') w("> %s" % assess.summary.headline) w("- **Delta dieses Profils:** %d fehlende Fähigkeiten." % len(missing)) w("- **Gegenprobe (nur ISO 27001):** %d fehlende Fähigkeiten." % len(single_missing)) w("- **Mehr Evidence → kleineres Delta:** die zusätzliche Zertifizierung schließt **%d** Fähigkeiten *vorab*: %s." % ( len(closed_by_evidence), ", ".join("`%s`" % c for c in closed_by_evidence))) w("") w("→ Es wurde **keine Journey ausgewählt**. Das Delta `(Profil, Ziel)` IST die Journey — berechnet, nicht gewählt. Die Journey ist nur die *Erklärung* dieses Deltas.") w("") # ── 3. Roadmap — was zuerst? (gleicher Engine wie #1) ───────────────────── delta_t = {d["capability"]: d.get("covers_targets", []) for d in CP["delta_requirements"]} opt = roadmap_from_delta(assess, delta_t) bud = select_within_budget({m.capability_id: m.covers for m in opt.ranked_measures}, 5) w("## 3. Roadmap — was zuerst? _(gleicher Hebel-Engine wie Mission #1)_") w("> %s" % opt.headline) w("- Top-5 nach Hebel schließen **%d von %d** offenen Anforderungen (%d%%)." % ( bud.requirements_closed, bud.total_requirements, int(round(bud.coverage_ratio * 100)))) w("") # ── Selektions-Rationale (die vom User geforderten 5 Fragen) ─────────────── rationale("Welche Journey wurde gewählt?", "**Keine per-Zertifikat-Journey.** Die Journey ist `Company Capability Profile → CRA`. Gewählt wurde nur das **Ziel** (CRA); der Startzustand wurde aus ALLEN Zertifikaten aggregiert.") rationale("Warum?", 'Bei 6 Zertifikaten würde „ISO 27001 → CRA" die Evidence aus PSIRT/ISO 9001 wegwerfen (= 3 Fähigkeiten, die sonst fälschlich als Delta erschienen). Nur das **ganze Profil** ergibt das korrekte Delta.') rationale("Welche Informationen waren für die Auswahl entscheidend?", "(a) das **Ziel/Intent** (CRA) und (b) die **vollständige Zertifikatsliste** als Profil — **nicht** ein einzelnes Zertifikat. Welche Evidence hilft, ist **zielrelativ** (ISO 14001 irrelevant, PSIRT hoch-relevant).") rationale("Musste das Journey-Modell erweitert werden?", "**Konzeptionell ja, strukturell nein.** `from → to` bleibt; aber `from` ist ein **Company Capability Profile** (Multi-Cert-Aggregat), kein Zertifikat. Die Engines (`build_company_profile` + `assess_transition`) tun das BEREITS — es war ein Benenn-/Framing-Fehler, kein fehlender Code.") rationale("Musste ein neuer Selektionsparameter eingeführt werden?", "**Ja — und er VEREINFACHT.** Eingabe ist `(Company Profile, Ziel)`, nicht `(Zertifikat, Ziel)`. Die Zertifikate kollabieren ins Profil → **keine 2^N Cert-Kombinationen**, nur Profil→Ziel. Der Selektor wird damit kleiner, nicht größer.") w("## Selektions-Rationale — die 5 Fragen (pro Mission zu dokumentieren)") w("") for q, a in RAT: w("**%s** " % q) w(a) w("") # ── Was diese Mission über Mission #1 verändert ─────────────────────────── w("## Was Mission #2 an Mission #1 verändert") w("") w('- Mission #1 nannte **Scope → Journey** einen Sprung („kein Selektor `certs × targets → journeys`"). Mission #2 zeigt: **diese Naht schrumpft.** Es gibt keinen Journey-Matcher zu bauen — die Journey ist das **berechnete Delta** `(Profil, Ziel)`. Was real fehlt, ist nur **Profil-Intake + Ziel-Wahl**, nicht eine Journey-Auswahl-Engine.') w('- Bestätigt den Reframe: **Es gibt keine „ISO 27001 → CRA"-Transition — nur „Company Capability Profile → CRA".** Zertifikate sind **Beobachtungen/Evidence**, kein Journey-Startpunkt.') w("- **Beobachtung für den (noch nicht gebauten) Selektor:** Eingabe = `(Company Profile, Ziel)`. Diversität über weitere Missionen muss zeigen, ob auch **Produktprofil** und **Intent-Klasse** als Parameter nötig werden — erst dann kanonisieren ([[rule-of-three-canonicalization]]).") w("") print("\n".join(OUT))