feat: Journey Matcher — the delta explains the journey (Delta -> Journey, ADR-011)

The sanctioned last architectural building block. Reverses the order: not Goal -> Journey -> Delta
but Goal -> Required -> Delta -> Journey. A Journey is the EXPLANATION of the Capability Delta, not
its cause — so this is a Matcher/Explainer, not a Selector.

New module compliance/journey_matcher/ = the third independent, interchangeable function of the
pipeline, beside Company 2A (Evidence -> Capability) and RS-005 (Capability -> Delta):

  match_journeys(delta, journeys, context) -> ranked, auditable explanation

- Looks ONLY at the Capability Delta — never at certificates, regulation, tenders or the goal.
  Journey signatures are certificate-agnostic capability clusters (Input -> Output pattern).
- score = share of the delta a journey explains (recall over the missing capabilities); journey_only
  documents where a journey reaches beyond the delta so a broad journey is not silently preferred.
- Deliberately dumb + deterministic (pure set overlap; NO ML/embeddings/LLM), fully auditable
  (matched / unexplained / journey_only / context signals); a learning ranker can sit on top later.
- Signatures injected, engine hermetic. mypy --strict clean.

Validated on the real patterns (demo): a CRA+MaschinenVO delta ranks the convergence journey 100%,
"ISO27001 -> CRA" 56% (misses the machine-safety caps), "ISMS -> TISAX" 0%. This resolves the
"Scope -> Journey" jump from Customer Mission #1. Freeze exception explicitly authorised; non-runtime
-> no deploy. 12 tests pass, check-loc 0.
This commit is contained in:
Benjamin Admin
2026-06-28 10:36:43 +02:00
parent 3c6e2a2acc
commit 80bf1993e0
8 changed files with 511 additions and 0 deletions
@@ -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",
]
@@ -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)
@@ -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 = ""
@@ -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.
@@ -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))
@@ -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
@@ -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
@@ -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).