Merge pull request 'feat: Regulatory Completeness Engine (auditable coverage, not confidence)' (#21) from feat/regulatory-completeness into main

This commit is contained in:
pilotadmin
2026-06-27 14:17:07 +02:00
8 changed files with 389 additions and 3 deletions
@@ -0,0 +1,24 @@
"""Regulatory Completeness — auditable knowledge coverage, not confidence.
An internal quality machine: for an assessment it reports identified vs assessed regulations and
justifies every open or excluded domain (corpus gap -> future_corpus; applicability uncertain ->
query_required). The metric is counts, never a single percentage. The product never claims full
coverage — it makes its own knowledge state transparent and auditable. Deterministic, no LLM, no
new corpus/meta-model class (freeze v1.0).
"""
from __future__ import annotations
from .engine import assess_completeness
from .schemas import (
Assumption, CompletenessReport, CorpusStatus, DomainCoverage, Exclusion,
)
__all__ = [
"assess_completeness",
"CompletenessReport",
"CorpusStatus",
"DomainCoverage",
"Exclusion",
"Assumption",
]
@@ -0,0 +1,89 @@
"""Regulatory Completeness Engine — measure auditable knowledge coverage for an assessment.
Separates what we IDENTIFIED (triggered regulations) from what we ASSESSED (validated corpus AND
determined applicability), and justifies every gap. Two kinds of „open":
- corpus gap — no validated corpus yet (e.g. Environmental) -> future_corpus
- applicability open — corpus exists but applicability is uncertain (Data Act) -> query_required
The metric is COUNTS, never a single percentage. The audit statement says plainly „wir bewerteten M
von N Domänen; K sind nicht im validierten Korpus und wurden bewusst nicht bewertet".
Deterministic, computed-not-stored, no LLM, no new corpus/meta-model class (freeze v1.0). Python 3.9.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from .schemas import (
Assumption, CompletenessReport, CorpusStatus, DomainCoverage, Exclusion,
)
_VALID = {s.value for s in CorpusStatus}
def _status(corpus_status: Dict[str, str], reg: str) -> CorpusStatus:
raw = corpus_status.get(reg, "unknown")
return CorpusStatus(raw) if raw in _VALID else CorpusStatus.UNKNOWN
def assess_completeness(
identified_regulations: List[str],
corpus_status: Dict[str, str],
uncertain: Optional[List[Dict[str, Any]]] = None,
assumptions: Optional[List[Dict[str, Any]]] = None,
assessed_obligations: int = 0,
) -> CompletenessReport:
"""Build the auditable coverage report.
`identified_regulations`: triggered/identified for this product. `corpus_status`: regulation ->
one of validated/draft/unsupported/unknown (curated/injected corpus registry). `uncertain`:
applicability-uncertain regulations [{regulation, deciding_question, reason}]. `assumptions`:
[{key, value, note}]. `assessed_obligations`: count from Execution (injected, default 0).
"""
ids = sorted(set(identified_regulations))
unc = uncertain or []
unc_subjects = {str(u.get("regulation") or u.get("subject")) for u in unc if (u.get("regulation") or u.get("subject"))}
coverage = [DomainCoverage(regulation=r, status=_status(corpus_status, r)) for r in ids]
assessed = [r for r in ids if _status(corpus_status, r) == CorpusStatus.VALIDATED and r not in unc_subjects]
open_regs = [r for r in ids if r not in assessed]
open_corpora = [r for r in ids if _status(corpus_status, r) in (CorpusStatus.UNSUPPORTED, CorpusStatus.UNKNOWN)]
exclusions: List[Exclusion] = []
for u in unc:
subj = str(u.get("regulation") or u.get("subject") or "")
if not subj:
continue
exclusions.append(Exclusion(
subject=subj, reason=str(u.get("reason", "Anwendbarkeit unsicher")),
deciding_question=str(u.get("deciding_question", "")), resolution="query_required"))
for r in open_regs:
if r in unc_subjects:
continue
st = _status(corpus_status, r)
if st == CorpusStatus.DRAFT:
exclusions.append(Exclusion(subject=r, reason="Korpus in Bearbeitung (draft)", resolution="in_review"))
else:
exclusions.append(Exclusion(subject=r, reason="nicht im validierten Korpus", resolution="future_corpus"))
covered_subjects = {e.subject for e in exclusions}
justification = (not open_regs) or set(open_regs) <= covered_subjects
assumptions_m = [Assumption(key=str(a.get("key", "")), value=str(a.get("value", "")), note=str(a.get("note", ""))) for a in (assumptions or [])]
summary = "Identifiziert %d · bewertet %d · offen %d · Unsicherheiten %d · Begründung %s" % (
len(ids), len(assessed), len(open_regs), len(unc), "ja" if justification else "nein")
if open_regs:
audit = (
"Für dieses Produkt konnten wir %d von %d identifizierten regulatorischen Domänen vollständig "
"bewerten. %d weitere %s noch nicht Bestandteil des validierten Korpus bzw. anwendungsunsicher "
"und wurden deshalb bewusst nicht bewertet." % (
len(assessed), len(ids), len(open_regs), "ist" if len(open_regs) == 1 else "sind"))
else:
audit = "Für dieses Produkt konnten wir alle %d identifizierten regulatorischen Domänen vollständig bewerten." % len(ids)
return CompletenessReport(
identified_regulations=ids, assessed_regulations=assessed, open_regulations=open_regs,
open_corpora=open_corpora, coverage=coverage, assumptions=assumptions_m, exclusions=exclusions,
uncertainties_count=len(unc), assessed_obligations=assessed_obligations,
justification_present=justification, completeness_summary=summary, audit_statement=audit,
)
@@ -0,0 +1,62 @@
"""Schemas for the Regulatory Completeness Engine — auditable knowledge-coverage, not confidence.
For an assessment it answers „wie sicher sind wir, dass diese Antwort VOLLSTÄNDIG ist?" by separating
IDENTIFIED regulations from ASSESSED ones (those in the validated corpus) and listing every open or
excluded domain WITH a reason. The metric is counts, never a single „87%". This is an internal quality
machine: the product never claims full coverage — it makes its own knowledge state transparent.
Deterministic, computed-not-stored, no new meta-model class (freeze v1.0). Python 3.9 compatible.
"""
from __future__ import annotations
from enum import Enum
from typing import List
from pydantic import BaseModel, Field
class CorpusStatus(str, Enum):
"""The maturity of our knowledge corpus for a regulation/domain."""
VALIDATED = "validated" # we can fully assess this
DRAFT = "draft" # partial / under review
UNSUPPORTED = "unsupported" # triggered but no corpus yet
UNKNOWN = "unknown" # not in our registry at all
class DomainCoverage(BaseModel):
regulation: str
status: CorpusStatus = CorpusStatus.UNKNOWN
note: str = ""
class Exclusion(BaseModel):
"""A domain/regulation DELIBERATELY not assessed — always with a reason (the heart of the engine)."""
subject: str
reason: str
deciding_question: str = "" # what would resolve it (if a query)
resolution: str = "future_corpus" # query_required | future_corpus | not_applicable
class Assumption(BaseModel):
key: str
value: str = ""
note: str = ""
class CompletenessReport(BaseModel):
"""The auditable coverage report for one assessment — counts + justification, NO single percentage."""
identified_regulations: List[str] = Field(default_factory=list)
assessed_regulations: List[str] = Field(default_factory=list) # in the validated corpus
open_regulations: List[str] = Field(default_factory=list) # identified but not validated
open_corpora: List[str] = Field(default_factory=list) # missing domains worth building
coverage: List[DomainCoverage] = Field(default_factory=list)
assumptions: List[Assumption] = Field(default_factory=list)
exclusions: List[Exclusion] = Field(default_factory=list)
uncertainties_count: int = 0
assessed_obligations: int = 0 # injected (Execution-owned)
justification_present: bool = False
completeness_summary: str = "" # "Identifiziert N · bewertet M · offen K · ..."
audit_statement: str = "" # the honest narrative sentence
@@ -104,3 +104,38 @@ def knowledge_intake_section(base_dir) -> None:
("Impact-Triage (HIGH/LOW/NONE/new_domain)", "PASS", "3 Beispiel-Dokumente korrekt eingeordnet"),
("Regelwerk-ID-Normalisierung", "TODO", "CRA vs Cyber Resilience Act vereinheitlichen"),
])
def completeness_section() -> None:
"""Render the Regulatory Completeness section (kept here so generate.py stays under the LOC budget)."""
from compliance.completeness import assess_completeness
rep = assess_completeness(
identified_regulations=["CRA", "MaschinenVO", "EMV", "Environmental", "DataAct"],
corpus_status={"CRA": "validated", "MaschinenVO": "validated", "EMV": "unsupported",
"Environmental": "unsupported", "DataAct": "validated"},
uncertain=[{"regulation": "DataAct", "deciding_question": "generates_usage_data", "reason": "generates_usage_data = unbekannt"}],
assumptions=[{"key": "Funkmodul", "value": "nein"}, {"key": "personenbezogene Nutzungsdaten", "value": "nein"}],
assessed_obligations=128)
w("## Regulatory Completeness — was wir bewerten konnten, und was bewusst nicht")
w("")
w('_Interne Qualitätsmaschine (KEIN Confidence-Score): trennt IDENTIFIZIERT von BEWERTET und begründet jede Lücke. Keine Prozentzahl — auditierbar und ehrlich: „Wir zeigen auch, was wir noch nicht wissen und warum."_')
w("")
w("**%s**" % rep.completeness_summary)
w("")
w("> %s" % rep.audit_statement)
w("")
w("- **Bewertet:** %s (%d Pflichten)" % (", ".join(rep.assessed_regulations), rep.assessed_obligations))
w("- **Offen (jeweils begründet):**")
for e in rep.exclusions:
dq = (" → Rückfrage: `%s`" % e.deciding_question) if e.deciding_question else ""
w(" - `%s` — %s `[%s]`%s" % (e.subject, e.reason, e.resolution, dq))
w("- **Annahmen:** %s" % ", ".join("%s=%s" % (a.key, a.value) for a in rep.assumptions))
w("")
w("_Sobald der Umwelt-Korpus (ISO 14001 etc.) landet, kippt `Environmental` automatisch von offen auf bewertet — die Completeness Engine dokumentiert den Fortschritt je Domäne._")
w("")
coverage_table([
("Regulatory Completeness (auditierbar)", "PASS", rep.completeness_summary),
("Begründete Ausschlüsse (Korpus/Anwendbarkeit)", "PASS", "%d Ausschlüsse, alle mit Grund" % len(rep.exclusions)),
("Fortschritts-Doku je Domäne", "PASS", "Environmental offen→validated bei Korpus-Landung"),
])
@@ -46,7 +46,7 @@ import yaml
from _helpers import ( # noqa: E402 (script-dir module; keeps generate.py under the LOC budget)
OUT, ROLLUP, Row, w, coverage_table, reg_map_block, unsupported_block, interp_status,
knowledge_intake_section,
knowledge_intake_section, completeness_section,
)
ISO_MAP = {"ISO27001": CapabilityMappingEntry(
@@ -465,6 +465,7 @@ coverage_table([
])
knowledge_intake_section(os.path.dirname(__file__)) # Knowledge Intake (impact triage) — kept in _helpers for LOC
completeness_section() # Regulatory Completeness — kept in _helpers for LOC
# ── Epics + roll-up ───────────────────────────────────────────────────────
w("## Gaps → Epics (Backlog — nur erfasst, NICHT implementiert)")
@@ -340,6 +340,31 @@ _So entsteht bei jedem neuen Dokument eine Impact-Analyse statt „200 Seiten PD
| Impact-Triage (HIGH/LOW/NONE/new_domain) | **PASS** | 3 Beispiel-Dokumente korrekt eingeordnet |
| Regelwerk-ID-Normalisierung | **TODO** | CRA vs Cyber Resilience Act vereinheitlichen |
## Regulatory Completeness — was wir bewerten konnten, und was bewusst nicht
_Interne Qualitätsmaschine (KEIN Confidence-Score): trennt IDENTIFIZIERT von BEWERTET und begründet jede Lücke. Keine Prozentzahl — auditierbar und ehrlich: „Wir zeigen auch, was wir noch nicht wissen und warum."_
**Identifiziert 5 · bewertet 2 · offen 3 · Unsicherheiten 1 · Begründung ja**
> Für dieses Produkt konnten wir 2 von 5 identifizierten regulatorischen Domänen vollständig bewerten. 3 weitere sind noch nicht Bestandteil des validierten Korpus bzw. anwendungsunsicher und wurden deshalb bewusst nicht bewertet.
- **Bewertet:** CRA, MaschinenVO (128 Pflichten)
- **Offen (jeweils begründet):**
- `DataAct` — generates_usage_data = unbekannt `[query_required]` → Rückfrage: `generates_usage_data`
- `EMV` — nicht im validierten Korpus `[future_corpus]`
- `Environmental` — nicht im validierten Korpus `[future_corpus]`
- **Annahmen:** Funkmodul=nein, personenbezogene Nutzungsdaten=nein
_Sobald der Umwelt-Korpus (ISO 14001 etc.) landet, kippt `Environmental` automatisch von offen auf bewertet — die Completeness Engine dokumentiert den Fortschritt je Domäne._
**Architecture Coverage**
| Layer | Status | Hinweis |
|---|---|---|
| Regulatory Completeness (auditierbar) | **PASS** | Identifiziert 5 · bewertet 2 · offen 3 · Unsicherheiten 1 · Begründung ja |
| Begründete Ausschlüsse (Korpus/Anwendbarkeit) | **PASS** | 3 Ausschlüsse, alle mit Grund |
| Fortschritts-Doku je Domäne | **PASS** | Environmental offen→validated bei Korpus-Landung |
## Gaps → Epics (Backlog — nur erfasst, NICHT implementiert)
| Epic | Titel | schliesst Coverage-Luecke |
@@ -351,6 +376,6 @@ _So entsteht bei jedem neuen Dokument eine Impact-Analyse statt „200 Seiten PD
## Suite-Status (Roll-up)
- Coverage-Zellen gesamt: **41**
- PASS: **30** · PARTIAL: 3 · UNSUPPORTED: 1 · TODO: 6 · N/A: 1 · NEEDS_FACTS: 0
- Coverage-Zellen gesamt: **44**
- PASS: **33** · PARTIAL: 3 · UNSUPPORTED: 1 · TODO: 6 · N/A: 1 · NEEDS_FACTS: 0
- Fortschritt = PASS-Anteil steigt, wenn Epics RS-001…004 landen (objektiver Maßstab, kein LOC).
@@ -0,0 +1,97 @@
"""Tests for the Regulatory Completeness Engine — auditable coverage, not confidence.
Acceptance: separate identified from assessed regulations; justify every gap (corpus gap ->
future_corpus, applicability uncertain -> query_required with a deciding question); report counts
(never a single percentage); emit an honest audit statement. The product shows what it does NOT
know and why.
"""
from __future__ import annotations
from compliance.completeness import CompletenessReport, CorpusStatus, assess_completeness
IDENTIFIED = ["CRA", "MaschinenVO", "EMV", "Environmental", "DataAct"]
CORPUS = {"CRA": "validated", "MaschinenVO": "validated", "EMV": "validated",
"Environmental": "unsupported", "DataAct": "validated"}
UNCERTAIN = [{"regulation": "DataAct", "deciding_question": "generates_usage_data", "reason": "generates_usage_data unbekannt"}]
def _report():
return assess_completeness(IDENTIFIED, CORPUS, uncertain=UNCERTAIN,
assumptions=[{"key": "funkmodul", "value": "nein"}], assessed_obligations=128)
def test_assessed_excludes_uncertain_even_if_corpus_validated():
r = _report()
# DataAct has a validated corpus but uncertain applicability -> NOT assessed
assert r.assessed_regulations == ["CRA", "EMV", "MaschinenVO"]
assert "DataAct" in r.open_regulations and "Environmental" in r.open_regulations
def test_corpus_gap_vs_applicability_exclusion():
r = _report()
by = {e.subject: e for e in r.exclusions}
assert by["DataAct"].resolution == "query_required" and by["DataAct"].deciding_question == "generates_usage_data"
assert by["Environmental"].resolution == "future_corpus"
def test_open_corpora_is_unsupported_only():
r = _report()
# DataAct corpus is validated (only applicability is open) -> NOT an open corpus
assert r.open_corpora == ["Environmental"]
def test_justification_present_when_every_gap_has_a_reason():
r = _report()
assert r.justification_present is True
open_subjects = {e.subject for e in r.exclusions}
assert set(r.open_regulations) <= open_subjects
def test_counts_summary_has_no_percentage():
r = _report()
assert "%" not in r.completeness_summary and "%" not in r.audit_statement
assert "Identifiziert 5" in r.completeness_summary and "bewertet 3" in r.completeness_summary
def test_audit_statement_is_honest():
r = _report()
assert "3 von 5" in r.audit_statement and "bewusst nicht bewertet" in r.audit_statement
def test_draft_corpus_is_in_review_exclusion():
r = assess_completeness(["CRA", "IEC62443"], {"CRA": "validated", "IEC62443": "draft"})
by = {e.subject: e for e in r.exclusions}
assert by["IEC62443"].resolution == "in_review"
assert "IEC62443" in r.open_regulations and "IEC62443" not in r.open_corpora # draft != missing corpus
def test_all_assessed_no_open():
r = assess_completeness(["CRA", "MaschinenVO"], {"CRA": "validated", "MaschinenVO": "validated"})
assert r.open_regulations == [] and r.exclusions == []
assert r.justification_present is True
assert "alle 2" in r.audit_statement
def test_coverage_status_mapped():
r = _report()
cov = {c.regulation: c.status for c in r.coverage}
assert cov["CRA"] == CorpusStatus.VALIDATED and cov["Environmental"] == CorpusStatus.UNSUPPORTED
def test_assumptions_and_obligations_carried():
r = _report()
assert r.assessed_obligations == 128
assert [a.key for a in r.assumptions] == ["funkmodul"]
def test_unknown_regulation_defaults_to_open_corpus():
r = assess_completeness(["CRA", "REACH"], {"CRA": "validated"}) # REACH not in registry -> unknown
assert "REACH" in r.open_corpora and r.assessed_regulations == ["CRA"]
def test_deterministic_and_type():
r1 = _report()
r2 = _report()
assert r1.model_dump() == r2.model_dump()
assert isinstance(r1, CompletenessReport)
@@ -0,0 +1,53 @@
# ADR-007: Regulatory Completeness — auditable knowledge coverage, not confidence
- **Status:** Accepted
- **Datum:** 2026-06-27
- **Typ:** Architektur-Entscheidung
- **Bezug:** [ADR-006](ADR-006-knowledge-intake.md), [ADR-003](ADR-003-capability-delta-engine-with-renderers.md), Architektur-Freeze v1.0, [[transition-reasoning]], [[reasoning-vs-compliance-boundary]]
## Kontext
Die Engine beantwortet inzwischen *Was gilt? · Was fehlt? · Wie umsetzen?*. Es fehlt eine
übergeordnete Fähigkeit: **„Wie sicher sind wir, dass diese Antwort VOLLSTÄNDIG ist?"** Das ist
NICHT Confidence (Vertrauen in eine einzelne Aussage), sondern Abdeckung (welche Teile des Problems
haben wir überhaupt bewertet).
Der Übergang von Feature- zu Produktentwicklung verlangt, dass der Kunde — gerade in regulierten
Branchen — sehen kann, was die Plattform NICHT weiß und warum sie dazu keine Aussage trifft.
## Entscheidung
1. **Eine Regulatory Completeness Engine** (`compliance/completeness`, interne Qualitätsmaschine,
non-runtime) trennt für eine Beratung **IDENTIFIZIERT** (getriggerte Regelwerke) von **BEWERTET**
(validierter Korpus UND geklärte Anwendbarkeit) und **begründet jede Lücke**.
2. **Zwei Arten „offen", je mit Begründung:**
- **Korpus-Lücke** — kein validierter Korpus (z. B. Environmental) → `future_corpus`.
- **Anwendbarkeits-Unsicherheit** — Korpus vorhanden, aber Anwendbarkeit unklar (Data Act,
`generates_usage_data` unbekannt) → `query_required` mit deciding question.
3. **Die Metrik sind ZÄHLUNGEN, keine einzelne Prozentzahl.** Nicht „87 %", sondern
`Identifiziert N · bewertet M · offen K · Unsicherheiten U · Begründung ja`. Plus ein ehrlicher
**Audit-Satz**: „Wir bewerteten M von N Domänen; K sind nicht im validierten Korpus / anwendungs-
unsicher und wurden bewusst nicht bewertet."
4. **Annahmen und begründete Ausschlüsse sind explizit** (z. B. „kein Funkmodul",
„keine personenbezogenen Nutzungsdaten").
## Konsequenzen
- **Auditierbar + ehrlich:** das System behauptet KEINE Vollständigkeit, es macht den eigenen
Wissensstand transparent. Direkte Fortsetzung der Welt-1-Disziplin ([[reasoning-vs-compliance-boundary]])
auf Produktebene.
- **Fortschritt je Domäne wird automatisch dokumentiert:** landet der Umwelt-Korpus (ISO 14001),
kippt `Environmental` von `unsupported`/offen auf `validated`/bewertet — die Engine zeigt, WARUM
sich die Antwort verändert hat.
- **Verkaufsargument:** „Wir sagen Ihnen nicht nur, was wir wissen — wir zeigen transparent, was wir
noch nicht wissen und warum wir dazu keine Aussage treffen." Transparenz = Vertrauen.
- **Freeze-konform:** kein neues Metamodell, kein Graph, kein neuer Corpus. `compliance/completeness`
ist eine reine, deterministische Aggregation (computed-not-stored); Corpus-Status + Obligation-Zahl
werden injiziert (Execution-/Kuratoren-owned).
- **Phasen-Reihenfolge:** **A Wissensfabrik** (Intake ✓ / Draft ✓ / Review) → **A½ Regulatory
Completeness** (diese ADR) → **B neue Domänen** (ISO 14001 / REACH / CLP / IATF 16949 / IEC 62443).
Completeness VOR den Domänen, damit jeder Domänen-Zuwachs sofort messbar wird.
- Diese ADR ist non-runtime → kein Deploy (siehe [ADR-001](ADR-001-runtime-deploy-policy.md)).