feat: Customer Mission #4 — a second, different contract target (no tender-special-logic)

One contract example (Mission #3's public tender) is not enough to safely generalise: it risks
baking tender-shaped assumptions into the later Scope→Journey selector. This mission runs TWO
deliberately different contract sub-types against the same company through the IDENTICAL engine:

  - public tender   (procurement: pentest report, references, support SLA, SBOM)  -> delta 4
  - private OEM spec (Lastenheft: CSMS, functional safety, SUMS, ASPICE)            -> delta 3

The two deltas are completely DISJOINT (no shared missing capability), proving the contracts are
genuinely different — yet there is no per-contract code: assess_transition treats each as a plain
Required set, exactly like a regulation or a certification. Evidence-Relevance is target-relative
even between two contracts (TISAX worth more to the automotive OEM than to the generic tender).

Conclusion: "Contract" as a requirement source is now covered by >=2 diverse cases, so the later
selector can treat any contract uniformly. Synthetic company + synthetic contracts (NO real names).
Non-runtime -> no deploy. 5 tests pass.
This commit is contained in:
Benjamin Admin
2026-06-28 09:42:31 +02:00
parent b6c400902e
commit b71771e52e
3 changed files with 230 additions and 0 deletions
@@ -0,0 +1,39 @@
# Customer Mission #4 — zwei verschiedene Verträge, eine Engine (kein Contract-Spezialfall)
_Mission #3 zeigte EINEN Vertrag (öffentliche Ausschreibung) durch dieselbe Engine wie Gesetz/Zertifizierung. Ein Beispiel reicht nicht — sonst backen wir Tender-Annahmen in den späteren Selektor. Hier laufen ZWEI bewusst unterschiedliche Vertragsarten gegen dasselbe Unternehmen. Synthetischer Kunde + synthetische Verträge, keine echten Namen._
## Der Kunde (synthetisch) — EIN Profil
> **ISO 9001 · ISO 27001 · ISO 14001 · TISAX · CE-Prozess · PSIRT** · vernetzte Maschinen · Export EU
## 1. Zwei Vertragsarten — dieselbe Engine `Profil Required = Delta`
| Vertrag | Art | geforderte Fähigkeiten | Delta (fehlt) |
|---|---|---|---|
| **Öffentliche Ausschreibung** | public tender | 10 | **4** |
| **OEM-Lastenheft** | private OEM spec | 11 | **3** |
→ Beide Verträge sind nur ein `Required`-Satz; **es gibt keinen Contract-spezifischen Codepfad**`assess_transition` behandelt sie identisch zu Gesetz und Zertifizierung.
## 2. Verschiedene Verträge → verschiedene Deltas (Beweis: keine Tender-Speziallogik nötig)
- **Nur Ausschreibung:** `penetration_test_evidence`, `reference_project_evidence`, `sbom_creation`, `security_sla_and_support_commitment`
- **Nur OEM-Lastenheft:** `aspice_process_capability`, `functional_safety_evidence`, `software_update_management_system`
- **Beiden gemeinsam:** —
→ Die zwei Verträge fordern **strukturell anderes** (Beschaffungsnachweise vs. Automotive-Engineering: CSMS/funktionale Sicherheit/SUMS/ASPICE). Trotzdem **ein** Mechanismus. Genau das brauchten wir vor dem Selektor: zwei diverse Contract-Ziele, kein Sonderpfad.
## 3. Evidence-Relevanz(Vertrag)
| Zertifizierung (Evidence) | → Ausschreibung | → OEM-Lastenheft |
|---|---|---|
| **ISO27001** | hoch (4) | hoch (4) |
| **TISAX** | hoch (4) | hoch (6) |
| **PSIRT** | mittel (1) | mittel (2) |
| **ISO9001** | keine (0) | keine (0) |
| **ISO14001** | keine (0) | keine (0) |
| **CE** | keine (0) | keine (0) |
**TISAX** zählt gegen den **Automotive-OEM** mehr als gegen die generische Ausschreibung (Prototype Protection, CSMS); **ISO 14001** ist gegen beide **keine**. Bestätigt: **Relevanz ist eine Funktion des Ziels** — auch zwischen zwei Verträgen.
## Befund
> **Zwei strukturell verschiedene Verträge, ein Mechanismus, keine Zeile Contract-Spezialcode.** Damit ist „Contract" als Anforderungsquelle abgesichert (≥2 diverse Fälle): der spätere Scope→Journey-Selektor kann **jeden** Vertrag als reinen `Required`-Satz behandeln, ohne Tender-Speziallogik. Nächste sinnvolle Diversität vor dem Selektor: ein Vertrag, der bewusst auf NICHT-Security-Fähigkeiten zielt (z. B. Umwelt-/Materialnachweise), um auch die zielrelative Evidence-Relevanz über Domänen hinweg zu prüfen.
@@ -0,0 +1,134 @@
# ruff: noqa
# mypy: ignore-errors
"""Customer Mission #4 — a SECOND, different contract target (no tender-special-logic).
Mission #3 showed one contract (a public tender) runs through the same engine as a regulation and a
certification. One contract is not enough: with a single example we might accidentally bake
tender-shaped assumptions into the later Scope→Journey selector. So this mission runs TWO deliberately
different contract sub-types against the same company through the same engine:
- a PUBLIC TENDER (öffentliche Ausschreibung — procurement: pentest report, references, SLA)
- a PRIVATE OEM SPEC (Lastenheft — automotive customer: CSMS, functional safety, SUMS, ASPICE)
If both reduce to a plain `Required` set and produce DIFFERENT deltas with the IDENTICAL engine and NO
per-contract code, then „Contract" is not a special case — it is just another requirement source, and
the selector can treat any contract uniformly.
Synthetic company + synthetic contracts (NO real names). Runs the REAL engine. Non-runtime -> no deploy.
Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/mission_second_contract.py
"""
from __future__ import annotations
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,
)
OUT = []
def w(s=""):
OUT.append(s)
# ── Two contracts, each just a set of required capabilities (a contract has no parser — injected) ──
TENDER = [ # public procurement: security baseline + procurement-specific evidence
"information_security_management", "access_control_and_authentication", "incident_management",
"technical_vulnerability_management", "coordinated_vulnerability_disclosure", "sbom_creation",
"supplier_security", "penetration_test_evidence", "reference_project_evidence",
"security_sla_and_support_commitment",
]
OEM_SPEC = [ # private automotive customer Lastenheft: security + automotive-engineering-specific
"information_security_management", "access_control_and_authentication", "incident_management",
"supplier_security", "prototype_protection", "secure_signed_update_distribution",
"dedicated_security_contact", "cybersecurity_management_system", "software_update_management_system",
"functional_safety_evidence", "aspice_process_capability",
]
CONTRACTS = [
("Öffentliche Ausschreibung", "public tender", TENDER),
("OEM-Lastenheft", "private OEM spec", OEM_SPEC),
]
# ── ONE company profile (same multi-certified archetype as Missions #2/#3) ──────────────────
CERT_OBS = {
"ISO27001": ["information_security_management", "incident_management", "access_control_and_authentication",
"technical_vulnerability_management", "security_logging_and_monitoring",
"secure_development_lifecycle", "cybersecurity_management_system"],
"TISAX": ["information_security_management", "access_control_and_authentication", "incident_management",
"supplier_security", "prototype_protection", "cybersecurity_management_system"],
"PSIRT": ["coordinated_vulnerability_disclosure", "dedicated_security_contact",
"secure_signed_update_distribution"],
"ISO9001": ["ce_conformity_assessment_and_technical_documentation"],
"ISO14001": ["environmental_management_documentation"],
"CE": ["ce_conformity_assessment_and_technical_documentation"],
}
cmap = {k: CapabilityMappingEntry(capability_ids=v, confidence=Confidence.MEDIUM) for k, v in CERT_OBS.items()}
profile = build_company_profile(
CompanyContext(company_id="mc4", certifications=[Certification(certification_id=k) for k in CERT_OBS]), cmap)
def _delta(req_caps):
reqs = [TargetRequirement(capability_id=c) for c in req_caps]
a = assess_transition(TransitionContext(company_id="mc4", target=TransitionGoal(target_id="c")), reqs, profile)
return sorted({c.capability_id for c in a.coverage if c.status == CoverageStatus.MISSING})
def _relevance(cert_caps, req_caps):
n = len(set(cert_caps) & set(req_caps))
return n, ("hoch" if n >= 3 else "mittel" if n >= 1 else "keine")
w('# Customer Mission #4 — zwei verschiedene Verträge, eine Engine (kein Contract-Spezialfall)')
w("")
w('_Mission #3 zeigte EINEN Vertrag (öffentliche Ausschreibung) durch dieselbe Engine wie Gesetz/Zertifizierung. Ein Beispiel reicht nicht — sonst backen wir Tender-Annahmen in den späteren Selektor. Hier laufen ZWEI bewusst unterschiedliche Vertragsarten gegen dasselbe Unternehmen. Synthetischer Kunde + synthetische Verträge, keine echten Namen._')
w("")
w("## Der Kunde (synthetisch) — EIN Profil")
w("> **ISO 9001 · ISO 27001 · ISO 14001 · TISAX · CE-Prozess · PSIRT** · vernetzte Maschinen · Export EU")
w("")
# ── 1. Zwei Verträge durch DIESELBE Engine ────────────────────────────────
deltas = {name: _delta(caps) for name, _, caps in CONTRACTS}
w("## 1. Zwei Vertragsarten — dieselbe Engine `Profil Required = Delta`")
w("")
w("| Vertrag | Art | geforderte Fähigkeiten | Delta (fehlt) |")
w("|---|---|---|---|")
for name, kind, caps in CONTRACTS:
w("| **%s** | %s | %d | **%d** |" % (name, kind, len(caps), len(deltas[name])))
w("")
w("→ Beide Verträge sind nur ein `Required`-Satz; **es gibt keinen Contract-spezifischen Codepfad** — `assess_transition` behandelt sie identisch zu Gesetz und Zertifizierung.")
w("")
# ── 2. Die Deltas sind UNTERSCHIEDLICH (also echte verschiedene Verträge) ──
only_t = sorted(set(deltas["Öffentliche Ausschreibung"]) - set(deltas["OEM-Lastenheft"]))
only_o = sorted(set(deltas["OEM-Lastenheft"]) - set(deltas["Öffentliche Ausschreibung"]))
shared = sorted(set(deltas["Öffentliche Ausschreibung"]) & set(deltas["OEM-Lastenheft"]))
w("## 2. Verschiedene Verträge → verschiedene Deltas (Beweis: keine Tender-Speziallogik nötig)")
w("- **Nur Ausschreibung:** %s" % (", ".join("`%s`" % c for c in only_t) or ""))
w("- **Nur OEM-Lastenheft:** %s" % (", ".join("`%s`" % c for c in only_o) or ""))
w("- **Beiden gemeinsam:** %s" % (", ".join("`%s`" % c for c in shared) or ""))
w("")
w("→ Die zwei Verträge fordern **strukturell anderes** (Beschaffungsnachweise vs. Automotive-Engineering: CSMS/funktionale Sicherheit/SUMS/ASPICE). Trotzdem **ein** Mechanismus. Genau das brauchten wir vor dem Selektor: zwei diverse Contract-Ziele, kein Sonderpfad.")
w("")
# ── 3. Evidence-Relevance(Vertrag) — dieselbe Evidence, anderer Wert je Vertrag ──
w("## 3. Evidence-Relevanz(Vertrag)")
w("| Zertifizierung (Evidence) | → Ausschreibung | → OEM-Lastenheft |")
w("|---|---|---|")
for cert, caps in CERT_OBS.items():
nt, lt = _relevance(caps, TENDER)
no, lo = _relevance(caps, OEM_SPEC)
w("| **%s** | %s (%d) | %s (%d) |" % (cert, lt, nt, lo, no))
w("")
w("→ **TISAX** zählt gegen den **Automotive-OEM** mehr als gegen die generische Ausschreibung (Prototype Protection, CSMS); **ISO 14001** ist gegen beide **keine**. Bestätigt: **Relevanz ist eine Funktion des Ziels** — auch zwischen zwei Verträgen.")
w("")
# ── Befund ────────────────────────────────────────────────────────────────
w("## Befund")
w("")
w('> **Zwei strukturell verschiedene Verträge, ein Mechanismus, keine Zeile Contract-Spezialcode.** Damit ist „Contract" als Anforderungsquelle abgesichert (≥2 diverse Fälle): der spätere Scope→Journey-Selektor kann **jeden** Vertrag als reinen `Required`-Satz behandeln, ohne Tender-Speziallogik. Nächste sinnvolle Diversität vor dem Selektor: ein Vertrag, der bewusst auf NICHT-Security-Fähigkeiten zielt (z. B. Umwelt-/Materialnachweise), um auch die zielrelative Evidence-Relevanz über Domänen hinweg zu prüfen.')
w("")
print("\n".join(OUT))
@@ -0,0 +1,57 @@
"""Customer Mission #4 — a second, different contract target (no tender-special-logic).
Pins what Mission #4 guards: TWO structurally different contract sub-types (a public tender and a
private OEM Lastenheft) run through the identical engine and produce DIFFERENT, non-overlapping deltas
with no per-contract code. That is the evidence that the later Scope→Journey selector can treat any
contract as a plain Required set — no tender-shaped special case baked in.
"""
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/mission_second_contract.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 "Customer Mission #4" in out
assert "kein Contract-Spezialfall" in out
def test_two_distinct_contract_types_one_engine():
out = _run()
assert "public tender" in out and "private OEM spec" in out
assert "keinen Contract-spezifischen Codepfad" in out
def test_contracts_produce_different_deltas():
out = _run()
# the two contracts must be genuinely different: their deltas do not overlap
assert "**Beiden gemeinsam:** —" in out
# each carries its own distinctive missing capabilities
assert "penetration_test_evidence" in out # tender-only
assert "functional_safety_evidence" in out # OEM-only
def test_evidence_relevance_differs_between_contracts():
out = _run()
# TISAX is worth more against the automotive OEM spec than the generic tender
assert "**TISAX** | hoch (4) | hoch (6) |" in out
assert "Relevanz ist eine Funktion des Ziels" in out
def test_no_real_company_names():
out = _run().lower()
for name in ["eto", "owis", "winterhalter"]:
assert name not in out