diff --git a/backend-compliance/compliance/journey_matcher/__init__.py b/backend-compliance/compliance/journey_matcher/__init__.py new file mode 100644 index 00000000..ca6c1784 --- /dev/null +++ b/backend-compliance/compliance/journey_matcher/__init__.py @@ -0,0 +1,30 @@ +"""Journey Matcher — the Delta -> Journey function of the Capability Delta Engine. + +The third independent function of the pipeline (after Company 2A `Evidence -> Capability` and RS-005 +`Capability -> Delta`): given ONLY the Capability Delta, rank the known journeys that best EXPLAIN it. +A Journey is an EXPLANATION of the delta, not its cause — order is `Goal -> Required -> Delta -> Journey`. + +Deliberately dumb + deterministic (pure set overlap; no ML/embeddings/LLM), fully auditable, signatures +INJECTED (certificate-agnostic capability clusters). No new corpus, no graph (freeze v1.0). The Matcher +is sanctioned as the last architectural building block; everything after is knowledge work. +""" + +from __future__ import annotations + +from .engine import match_journeys +from .schemas import ( + JourneyMatch, + JourneyMatchReason, + JourneyMatchResult, + JourneySignature, + MatchContext, +) + +__all__ = [ + "match_journeys", + "JourneySignature", + "MatchContext", + "JourneyMatch", + "JourneyMatchReason", + "JourneyMatchResult", +] diff --git a/backend-compliance/compliance/journey_matcher/engine.py b/backend-compliance/compliance/journey_matcher/engine.py new file mode 100644 index 00000000..445dcf2b --- /dev/null +++ b/backend-compliance/compliance/journey_matcher/engine.py @@ -0,0 +1,94 @@ +"""Journey Matcher — the Delta -> Journey function of the Capability Delta Engine. + +Three INDEPENDENT functions now compose the pipeline, each a different problem, all interchangeable: + 1. Evidence -> Capability (Company 2A) + 2. Capability -> Delta (RS-005, transition_reasoning) + 3. Delta -> Journey (THIS module) + +The paradigm shift: a Journey is no longer the CAUSE (Goal -> Journey -> Delta) but the EXPLANATION +(Goal -> Required -> Delta -> Journey). The matcher does NOT look at certifications, regulations, +tenders, OEM specs or the goal — it looks ONLY at the Capability Delta and asks: which known journeys +describe exactly this delta? Output is a ranked, auditable explanation ("Journey A explains 82% of the +delta, because 8 of 10 missing capabilities are identical, same target type, ..."). + +Deliberately DUMB and deterministic: pure set overlap, NO ML, NO embeddings, NO LLM. A learning ranker +can be layered ON TOP later; this core stays auditable. Journey signatures are INJECTED (certificate- +agnostic capability clusters), never loaded here — the engine stays hermetic. No new corpus, no +graph/meta-model class (freeze v1.0). Python 3.9 compatible. + +Honesty: `score` is the share of the DELTA a journey explains (recall over the customer's missing +capabilities), never a "fit" or a compliance verdict. `journey_only` documents where a journey reaches +BEYOND this delta, so a broad journey that explains everything is not silently preferred. +""" + +from __future__ import annotations + +from typing import List, Optional, Sequence + +from .schemas import ( + JourneyMatch, + JourneyMatchReason, + JourneyMatchResult, + JourneySignature, + MatchContext, +) + + +def _context_signals(journey: JourneySignature, context: Optional[MatchContext]) -> List[str]: + """Corroborating reasons only — these are documented, they never change the score.""" + if context is None: + return [] + signals: List[str] = [] + if context.target_type and journey.target_type and context.target_type == journey.target_type: + signals.append("gleiche Zielart") + if context.industry and journey.industry and context.industry == journey.industry: + signals.append("gleiche Branche") + if context.product_type and journey.product_type and context.product_type == journey.product_type: + signals.append("gleicher Produkttyp") + return signals + + +def match_journeys( + delta: Sequence[str], + journeys: Sequence[JourneySignature], + context: Optional[MatchContext] = None, +) -> JourneyMatchResult: + """Rank known journeys by the share of the Capability Delta they EXPLAIN. + + `delta` = the customer's MISSING capabilities (from RS-005). `journeys` = injected, certificate- + agnostic signatures. score = |delta INTERSECT pattern| / |delta|. Ranking is deterministic: + score desc, then context-signal count desc (corroboration only), then journey_id asc. Context + never changes the score — only the documented reasons. Pure; no I/O; computed-not-stored. + """ + delta_set = set(delta) + n = len(delta_set) + matches: List[JourneyMatch] = [] + for j in journeys: + pattern = set(j.capability_pattern) + matched = sorted(delta_set & pattern) + score = (len(matched) / n) if n else 0.0 + signals = _context_signals(j, context) + reason = JourneyMatchReason( + matched_capabilities=matched, + unexplained_delta=sorted(delta_set - pattern), + journey_only=sorted(pattern - delta_set), + context_signals=signals, + ) + matches.append( + JourneyMatch( + journey_id=j.journey_id, + label=j.label, + score=round(score, 2), + explains="%d von %d fehlenden Capabilities" % (len(matched), n), + reason=reason, + ) + ) + matches.sort(key=lambda m: (-m.score, -len(m.reason.context_signals), m.journey_id)) + best = matches[0] if matches and matches[0].score > 0.0 else None + headline = ( + "%d Journeys erklaeren das Delta; beste: %s (%d%% des Deltas)" + % (sum(1 for m in matches if m.score > 0.0), best.label, round(best.score * 100)) + if best + else "Keine bekannte Journey erklaert dieses Delta (neue Journey-Kandidatin)" + ) + return JourneyMatchResult(delta_size=n, matches=matches, best=best, headline=headline) diff --git a/backend-compliance/compliance/journey_matcher/schemas.py b/backend-compliance/compliance/journey_matcher/schemas.py new file mode 100644 index 00000000..83376e97 --- /dev/null +++ b/backend-compliance/compliance/journey_matcher/schemas.py @@ -0,0 +1,66 @@ +"""Schemas for the Journey Matcher — the Delta -> Journey function of the Capability Delta Engine. + +Derived views (computed-not-stored): nothing here is persisted; every match is recomputed from the +input delta + injected journey signatures each call. No new corpus, no graph (freeze v1.0). +Python 3.9 compatible (no `|` unions). +""" + +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class JourneySignature(BaseModel): + """A known journey described ONLY by its capability pattern (Input cluster -> Output cluster). + + Deliberately certificate-/regulation-agnostic: the match uses `capability_pattern` alone. `label` + and the context fields exist for the human-auditable explanation, NEVER for the score. (Today the + signatures are derived from the transition patterns; the IDs like "ISO27001->CRA" are just one way + to describe the clusters — the matcher never reads them.) + """ + + journey_id: str + label: str + capability_pattern: List[str] = Field(default_factory=list) # OUTPUT cluster: the delta this journey is about + assumed_capabilities: List[str] = Field(default_factory=list) # INPUT cluster: typically already present + industry: Optional[str] = None + product_type: Optional[str] = None + target_type: Optional[str] = None # context only: regulation / certification / contract / environmental + + +class MatchContext(BaseModel): + """Optional corroborating context — surfaced as documented reasons, never part of the score.""" + + industry: Optional[str] = None + product_type: Optional[str] = None + target_type: Optional[str] = None + + +class JourneyMatchReason(BaseModel): + """The auditable WHY behind one match — everything a reviewer needs, no opaque score.""" + + matched_capabilities: List[str] = Field(default_factory=list) # delta INTERSECT pattern (what it explains) + unexplained_delta: List[str] = Field(default_factory=list) # delta - pattern (what it does NOT explain) + journey_only: List[str] = Field(default_factory=list) # pattern - delta (journey covers, not needed here) + context_signals: List[str] = Field(default_factory=list) # "gleiche Zielart", "gleiche Branche", ... + + +class JourneyMatch(BaseModel): + """One known journey, ranked by how much of the delta it EXPLAINS (not how well it 'fits').""" + + journey_id: str + label: str + score: float = 0.0 # |delta INTERSECT pattern| / |delta|, 0..1: share of the delta explained + explains: str = "" # "8 von 10 fehlenden Capabilities" + reason: JourneyMatchReason + + +class JourneyMatchResult(BaseModel): + """Ranked known journeys that EXPLAIN a Capability Delta. Journey = explanation, not cause.""" + + delta_size: int = 0 + matches: List[JourneyMatch] = Field(default_factory=list) # ranked desc by score + best: Optional[JourneyMatch] = None + headline: str = "" diff --git a/backend-compliance/reference_scenarios/journey_matcher_demo.md b/backend-compliance/reference_scenarios/journey_matcher_demo.md new file mode 100644 index 00000000..ffaab49e --- /dev/null +++ b/backend-compliance/reference_scenarios/journey_matcher_demo.md @@ -0,0 +1,28 @@ +# Journey Matcher — Delta -> Journey (an echten Pattern validiert) + +_Der Matcher fragt NICHT „welche Journey passt?", sondern „welche bekannten Journeys ERKLÄREN dieses Capability Delta?". Er sieht nur das Delta — keine Zertifikate, kein Regelwerk, kein Ziel. Journey = Erklärung, nicht Ursache. Deterministisch, kein ML/Embedding/LLM. Synthetischer Kunde, keine echten Namen._ + +## Eingang: ein echtes Capability Delta +- Multi-zertifiziertes Unternehmen will **CRA + MaschinenVO** → **9 fehlende Capabilities** (aus RS-005). +- Der Matcher bekommt **nur diese 9 Capabilities** — sonst nichts. + +## Delta -> Journey: Rangliste (Anteil des Deltas, den die Journey erklärt) +> 3 Journeys erklaeren das Delta; beste: ISO27001 -> CRA + MaschinenVO (100% des Deltas) + +| Journey (Capability-Cluster) | erklärt | Anteil | +|---|---|---| +| **ISO27001 -> CRA + MaschinenVO** | 9 von 9 fehlenden Capabilities | 100% | +| **ISO27001 -> CRA** | 5 von 9 fehlenden Capabilities | 56% | +| **ISO9001 -> CRA** | 4 von 9 fehlenden Capabilities | 44% | +| **ISMS -> TISAX** | 0 von 9 fehlenden Capabilities | 0% | + +## Warum „ISO27001 -> CRA + MaschinenVO"? — auditierbar, keine Blackbox +- **Erklärte Capabilities (9):** `machine_safety_risk_assessment`, `mechanical_safety_and_guards`, `operating_instructions_and_safety_information`, `product_cyber_risk_assessment`, `protection_against_corruption_of_safety_functions`, `public_security_advisories` … +- **Nicht erklärt (Rest-Delta):** — (Journey erklärt das GESAMTE Delta) +- **Journey reicht darüber hinaus:** `ce_conformity_assessment_and_technical_documentation`, `coordinated_vulnerability_disclosure`, `exploited_vuln_and_incident_reporting` +- **Kontext-Signale:** gleiche Zielart + +## Der Paradigmenwechsel + +> Reihenfolge ist jetzt **`Goal → Required → Delta → Journey`**, nicht mehr `Goal → Journey → Delta`. Die Journey ist die **Erklärung** des Deltas. Der Matcher ist bewusst **dumm + deterministisch** (reine Mengenüberlappung) und damit auditierbar; ein lernendes Ranking kann später DAVOR gesetzt werden. Drei austauschbare Funktionen: `Evidence→Capability` (Company 2A) · `Capability→Delta` (RS-005) · **`Delta→Journey` (dieser Matcher)**. In keiner kommt „Regulation" als Sonderfall vor — CRA, TISAX, Ausschreibung, OEM-Spec und Umweltziel sind nur verschiedene Quellen des Required State. + diff --git a/backend-compliance/reference_scenarios/journey_matcher_demo.py b/backend-compliance/reference_scenarios/journey_matcher_demo.py new file mode 100644 index 00000000..c9065d72 --- /dev/null +++ b/backend-compliance/reference_scenarios/journey_matcher_demo.py @@ -0,0 +1,108 @@ +# ruff: noqa +# mypy: ignore-errors +"""Journey Matcher demo — Delta -> Journey on the REAL transition patterns. + +Validates the new matcher end-to-end: take a real Capability Delta (a multi-certified company that +wants CRA + MaschinenVO), then rank the KNOWN journeys purely by how much of THAT delta each explains. +The matcher never looks at the certificates, the regulation or the goal — only at the delta. The +journey is the EXPLANATION of the delta, not its cause (order: Goal -> Required -> Delta -> Journey). + +Journey signatures are derived from the transition-pattern YAMLs here (non-core), then injected into the +hermetic engine. Synthetic company (NO real names). Non-runtime -> no deploy. +Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/journey_matcher_demo.py +""" +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.journey_matcher import JourneySignature, MatchContext, match_journeys + +OUT = [] + + +def w(s=""): + OUT.append(s) + + +_K = os.path.join(os.path.dirname(__file__), "..", "knowledge", "transition_patterns") +_PATTERNS = { + "transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml": ("ISO27001 -> CRA + MaschinenVO", "regulation"), + "transition_pattern_iso27001_to_cra_v1.yaml": ("ISO27001 -> CRA", "regulation"), + "transition_pattern_iso9001_to_cra_v1.yaml": ("ISO9001 -> CRA", "regulation"), + "transition_pattern_isms_to_tisax_v1.yaml": ("ISMS -> TISAX", "certification"), +} + + +def _load(name): + return yaml.safe_load(open(os.path.join(_K, name), encoding="utf-8")) + + +# ── Journey library: signatures = capability CLUSTERS (the matcher never reads the IDs) ────── +journeys = [] +for fname, (label, ttype) in _PATTERNS.items(): + p = _load(fname) + journeys.append(JourneySignature( + journey_id=p.get("id", fname), + label=label, + capability_pattern=[d["capability"] for d in p["delta_requirements"]], # OUTPUT cluster + assumed_capabilities=[a["capability"] for a in p["likely_covered"]], # INPUT cluster + target_type=ttype, + )) + +# ── A real Capability Delta: multi-certified company that wants CRA + MaschinenVO ──────────── +CP = _load("transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml") +infosec = [a["capability"] for a in CP["likely_covered"]] +cmap = { + "ISO27001": CapabilityMappingEntry(capability_ids=infosec, confidence=Confidence.MEDIUM), + "PSIRT": CapabilityMappingEntry(capability_ids=["coordinated_vulnerability_disclosure", + "exploited_vuln_and_incident_reporting"], confidence=Confidence.HIGH), + "ISO9001": CapabilityMappingEntry(capability_ids=["ce_conformity_assessment_and_technical_documentation"], + confidence=Confidence.MEDIUM), +} +profile = build_company_profile( + CompanyContext(company_id="d", certifications=[Certification(certification_id=k) for k in cmap]), cmap) +reqs = [TargetRequirement(capability_id=a["capability"]) for a in CP["likely_covered"]] +reqs += [TargetRequirement(capability_id=d["capability"]) for d in CP["delta_requirements"]] +assess = assess_transition(TransitionContext(company_id="d", target=TransitionGoal(target_id="CRA+MaschinenVO")), reqs, profile) +delta = sorted({c.capability_id for c in assess.coverage if c.status == CoverageStatus.MISSING}) + +# ── Delta -> Journey: rank the known journeys that EXPLAIN this delta ──────────────────────── +result = match_journeys(delta, journeys, MatchContext(target_type="regulation")) + +w("# Journey Matcher — Delta -> Journey (an echten Pattern validiert)") +w("") +w('_Der Matcher fragt NICHT „welche Journey passt?", sondern „welche bekannten Journeys ERKLÄREN dieses Capability Delta?". Er sieht nur das Delta — keine Zertifikate, kein Regelwerk, kein Ziel. Journey = Erklärung, nicht Ursache. Deterministisch, kein ML/Embedding/LLM. Synthetischer Kunde, keine echten Namen._') +w("") +w("## Eingang: ein echtes Capability Delta") +w("- Multi-zertifiziertes Unternehmen will **CRA + MaschinenVO** → **%d fehlende Capabilities** (aus RS-005)." % len(delta)) +w("- Der Matcher bekommt **nur diese %d Capabilities** — sonst nichts." % len(delta)) +w("") +w("## Delta -> Journey: Rangliste (Anteil des Deltas, den die Journey erklärt)") +w("> %s" % result.headline) +w("") +w("| Journey (Capability-Cluster) | erklärt | Anteil |") +w("|---|---|---|") +for m in result.matches: + w("| **%s** | %s | %d%% |" % (m.label, m.explains, round(m.score * 100))) +w("") +b = result.best +w('## Warum „%s"? — auditierbar, keine Blackbox' % b.label) +w("- **Erklärte Capabilities (%d):** %s" % (len(b.reason.matched_capabilities), ", ".join("`%s`" % c for c in b.reason.matched_capabilities[:6]) + (" …" if len(b.reason.matched_capabilities) > 6 else ""))) +w("- **Nicht erklärt (Rest-Delta):** %s" % (", ".join("`%s`" % c for c in b.reason.unexplained_delta) or "— (Journey erklärt das GESAMTE Delta)")) +w("- **Journey reicht darüber hinaus:** %s" % (", ".join("`%s`" % c for c in b.reason.journey_only) or "—")) +w("- **Kontext-Signale:** %s" % (", ".join(b.reason.context_signals) or "—")) +w("") +w("## Der Paradigmenwechsel") +w("") +w('> Reihenfolge ist jetzt **`Goal → Required → Delta → Journey`**, nicht mehr `Goal → Journey → Delta`. Die Journey ist die **Erklärung** des Deltas. Der Matcher ist bewusst **dumm + deterministisch** (reine Mengenüberlappung) und damit auditierbar; ein lernendes Ranking kann später DAVOR gesetzt werden. Drei austauschbare Funktionen: `Evidence→Capability` (Company 2A) · `Capability→Delta` (RS-005) · **`Delta→Journey` (dieser Matcher)**. In keiner kommt „Regulation" als Sonderfall vor — CRA, TISAX, Ausschreibung, OEM-Spec und Umweltziel sind nur verschiedene Quellen des Required State.') +w("") + +print("\n".join(OUT)) diff --git a/backend-compliance/tests/test_journey_matcher.py b/backend-compliance/tests/test_journey_matcher.py new file mode 100644 index 00000000..1b7c7504 --- /dev/null +++ b/backend-compliance/tests/test_journey_matcher.py @@ -0,0 +1,80 @@ +"""Unit tests for the Journey Matcher (Delta -> Journey). + +The matcher ranks known journeys by the share of the Capability Delta they EXPLAIN, using ONLY the +delta and injected capability-cluster signatures — deterministic, auditable, no ML. These tests pin +the score semantics (recall over the delta), the ranking order, the audit reasons, and that context +corroborates without ever changing the score. +""" + +from __future__ import annotations + +from compliance.journey_matcher import ( + JourneySignature, + MatchContext, + match_journeys, +) + + +def _sig(jid, pattern, **kw): + return JourneySignature(journey_id=jid, label=jid, capability_pattern=pattern, **kw) + + +def test_score_is_share_of_delta_explained(): + delta = ["a", "b", "c", "d", "e"] + j = _sig("J", ["a", "b", "c", "d"]) # explains 4 of 5 + res = match_journeys(delta, [j]) + assert res.matches[0].score == 0.8 + assert res.matches[0].explains == "4 von 5 fehlenden Capabilities" + + +def test_ranking_orders_by_explanatory_power(): + delta = ["a", "b", "c", "d"] + journeys = [ + _sig("low", ["a"]), # 1/4 + _sig("high", ["a", "b", "c"]), # 3/4 + _sig("mid", ["a", "b"]), # 2/4 + ] + res = match_journeys(delta, journeys) + assert [m.journey_id for m in res.matches] == ["high", "mid", "low"] + assert res.best.journey_id == "high" + + +def test_audit_reason_partitions_the_delta(): + delta = ["a", "b", "c"] + j = _sig("J", ["b", "c", "x", "y"]) # explains b,c; misses a; reaches beyond into x,y + r = match_journeys(delta, [j]).matches[0].reason + assert r.matched_capabilities == ["b", "c"] + assert r.unexplained_delta == ["a"] + assert r.journey_only == ["x", "y"] + + +def test_context_corroborates_but_never_changes_score(): + delta = ["a", "b"] + same = _sig("same", ["a", "b"], target_type="regulation") + other = _sig("other", ["a", "b"], target_type="contract") + ctx = MatchContext(target_type="regulation") + res = match_journeys(delta, [other, same], ctx) + # identical score (1.0) -> tie broken by context-signal count: 'same' first + assert res.matches[0].score == res.matches[1].score == 1.0 + assert res.matches[0].journey_id == "same" + assert "gleiche Zielart" in res.matches[0].reason.context_signals + assert res.matches[1].reason.context_signals == [] + + +def test_deterministic_tiebreak_by_journey_id(): + delta = ["a", "b"] + res = match_journeys(delta, [_sig("zeta", ["a"]), _sig("alpha", ["a"])]) + assert [m.journey_id for m in res.matches] == ["alpha", "zeta"] + + +def test_no_journey_explains_the_delta(): + res = match_journeys(["a", "b"], [_sig("J", ["x", "y"])]) + assert res.best is None + assert res.matches[0].score == 0.0 + assert "neue Journey-Kandidatin" in res.headline + + +def test_empty_delta_yields_no_best(): + res = match_journeys([], [_sig("J", ["a"])]) + assert res.delta_size == 0 + assert res.best is None diff --git a/backend-compliance/tests/test_journey_matcher_demo.py b/backend-compliance/tests/test_journey_matcher_demo.py new file mode 100644 index 00000000..2792baa7 --- /dev/null +++ b/backend-compliance/tests/test_journey_matcher_demo.py @@ -0,0 +1,51 @@ +"""Journey Matcher demo test — Delta -> Journey on the real transition patterns. + +Pins that the matcher, given ONLY a real Capability Delta (a multi-cert company wanting CRA + +MaschinenVO), correctly ranks the known journeys by explanatory power: the convergence journey +explains the whole delta, the CRA-only journey explains the security part but misses the machine- +safety capabilities, and the TISAX journey is irrelevant. End-to-end through the real engines. +""" + +from __future__ import annotations + +import os +import subprocess +import sys + + +def _run(): + root = os.path.join(os.path.dirname(__file__), "..") + r = subprocess.run( + [sys.executable, "reference_scenarios/journey_matcher_demo.py"], + cwd=root, env={**os.environ, "PYTHONPATH": "."}, capture_output=True, text=True, + ) + assert r.returncode == 0, r.stderr + return r.stdout + + +def test_runs_end_to_end(): + out = _run() + assert "Journey Matcher" in out + assert "Goal → Required → Delta → Journey" in out + + +def test_convergence_journey_explains_the_whole_delta(): + out = _run() + assert "**ISO27001 -> CRA + MaschinenVO** | 9 von 9 fehlenden Capabilities | 100% |" in out + + +def test_partial_journey_misses_machine_safety(): + out = _run() + # CRA-only journey explains the security part but not the MaschinenVO capabilities + assert "**ISO27001 -> CRA** | 5 von 9 fehlenden Capabilities | 56% |" in out + + +def test_irrelevant_journey_scores_zero(): + out = _run() + assert "**ISMS -> TISAX** | 0 von 9 fehlenden Capabilities | 0% |" in out + + +def test_match_is_auditable(): + out = _run() + assert "auditierbar, keine Blackbox" in out + assert "Erklärte Capabilities" in out diff --git a/docs-src/architecture/adr/ADR-011-journey-matcher-delta-explains-journey.md b/docs-src/architecture/adr/ADR-011-journey-matcher-delta-explains-journey.md new file mode 100644 index 00000000..3e38ac05 --- /dev/null +++ b/docs-src/architecture/adr/ADR-011-journey-matcher-delta-explains-journey.md @@ -0,0 +1,54 @@ +# ADR-011: Journey Matcher — the delta explains the journey (Delta -> Journey) + +- **Status:** Accepted +- **Datum:** 2026-06-28 +- **Typ:** Architektur-Entscheidung (neues Modul — vom User ausdrücklich freigegeben) +- **Bezug:** [ADR-003](ADR-003-capability-delta-engine-with-renderers.md), [ADR-002](ADR-002-transition-is-data-not-architecture.md), [[transition-reasoning]], [[strategy-requirements-intelligence]], journey-model-spec-v1 + +## Kontext + +Bisher war die **Journey die Ursache**: der Berater wählte vorne eine Journey (`Goal → Journey → Delta`). +Genau das war der eine offene „Sprung" aus Customer Mission #1 (Scope → Journey). Wir haben ihn bewusst +NICHT zu früh gebaut, sondern erst **fünf bewusst unterschiedliche Zielarten** validiert (Mission #1–#5: +Regulation · Certification · Public Tender · OEM-Spec · Umwelt/Material). Damit existiert die Diversität, +um aus **beobachteten Fällen** zu generalisieren statt aus Annahmen — die Voraussetzung, den Selektor +überhaupt sinnvoll zu bauen. + +## Entscheidung + +1. **Umkehrung der Reihenfolge: `Goal → Required → Delta → Journey`.** Die Journey ist die **Erklärung** + des Capability Deltas, nicht seine Ursache. Genannt **Journey Matcher / Explainer**, nicht „Selector". + +2. **Neues Modul `compliance/journey_matcher/` = die dritte Funktion `Delta → Journey`** — neben + Company 2A (`Evidence → Capability`) und RS-005 (`Capability → Delta`). Drei **unabhängige, + austauschbare** Funktionen, drei verschiedene Probleme. + +3. **Der Matcher sieht NUR das Capability Delta** — keine Zertifikate, kein Regelwerk, kein Ziel. + Journey-Signaturen sind **zertifikat-agnostische Capability-Cluster** (`Input Pattern → Output Pattern`); + die IDs wie „ISO27001 → CRA" sind nur eine Beschreibung der Cluster, der Matcher liest sie nie. + **Score = Anteil des Deltas, den eine Journey erklärt** (Recall über die fehlenden Capabilities), nie + ein „Fit" oder ein Konformitätsurteil. `journey_only` dokumentiert, wo eine Journey über das Delta + hinausreicht (eine breite Journey wird nicht still bevorzugt). + +4. **Bewusst dumm + deterministisch:** reine Mengenüberlappung, **kein ML, keine Embeddings, kein LLM**. + Voll auditierbar (matched / unexplained / journey_only / Kontext-Signale). Ein lernendes Ranking kann + später DAVOR gesetzt werden; der deterministische Kern bleibt nachvollziehbar. Kontext (Branche/ + Produkttyp/Zielart) ist nur **dokumentierte Korroboration + Tie-Break**, nie Teil des Scores. + +5. **Signaturen werden INJIZIERT**, nicht im Kern geladen — die Engine bleibt hermetisch (wie RS-005). + +## Konsequenzen + +- **Der „Scope → Journey"-Sprung aus Mission #1 ist aufgelöst:** es gibt keinen Journey-Matcher als + Sonderlogik je Zielart — die Journey ergibt sich aus dem Delta. An echten Pattern validiert: ein + CRA+MaschinenVO-Delta rankt die Konvergenz-Journey 100 %, „ISO27001 → CRA" 56 % (verfehlt die + Maschinensicherheit), „ISMS → TISAX" 0 %. +- **Letzter wirklich architektureller Baustein.** Alles danach ist überwiegend **Wissensarbeit, + Korpusaufbau, Domänenmodellierung** — in der Pipeline `Reality → Evidence → Capability Profile → + Required State → Capability Delta → Journey → Roadmap → Playbooks → Verification` kommt „Regulation" + nicht mehr als Sonderfall vor. +- **Freeze-Ausnahme, bewusst:** der User hat dieses EINE neue Modul ausdrücklich freigegeben. Kein + weiteres Metamodell/Graph. Non-runtime (kein App-Caller) → kein Deploy ([ADR-001](ADR-001-runtime-deploy-policy.md)). +- **Folgearbeit (nicht jetzt):** Journeys als reine `Capability Cluster A → Cluster B` (statt ISO/CRA-IDs); + `Intent → Scope → Journey`-Ebene darüber; lernendes Ranking als Vor-Stufe; `relevance(evidence, target)` + als eigene Berechnung (aus Mission #5).