feat(rci): Regulatory Change Intelligence foundation (delta over the stored map)
RCI/Delta as a read-/reasoning layer ON TOP of the product-first pipeline. Answers "what changes relative to my existing Regulatory Map?" — NOT "what does the new law say in general". No UI, no ingestion (newsletter/mailbox), no RAG, no new regulations/controls, no legal evaluation outside the stored map. - 4 core objects (compliance/rci/schemas.py): ComplianceBaseline (snapshot of profile + map + registry obligations + required/present evidence), RegulatoryChange (simulated/provided INPUT), ObligationDelta (delta_type NEW|CHANGED|REMOVED| ALREADY_COVERED|NEEDS_REVIEW|NOT_APPLICABLE), ChangeImpactSummary. delta_type is a THIRD vocabulary, disjoint from ClaimCoverage (Welt 1) and ComplianceStatus (Welt 2). - create_baseline() snapshots the existing pipeline once; assess_change() computes deltas deterministically against the snapshot (no re-evaluation). - 12 tests = the 5 acceptance questions (affects product? new/changed? already covered by evidence? needs human review? not relevant?) + repeal/uncertain-reg/ missing-evidence/boundary. Existing pipeline tests stay green; mypy clean; LOC ok. - App/reasoning types only — no compliance-meta-model classes (freeze v1.0 untouched). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
"""Tests for Regulatory Change Intelligence (RCI).
|
||||
|
||||
Acceptance: a simulated RegulatoryChange against a stored ComplianceBaseline can
|
||||
answer: (1) does it affect this product? (2) which obligations are new/changed?
|
||||
(3) which are likely already covered by existing evidence? (4) what must a human
|
||||
review? (5) what is not relevant?
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from compliance.profile.canonical import (
|
||||
CanonicalLifecyclePhase,
|
||||
CanonicalProductRegulatoryProfile,
|
||||
CanonicalProductType,
|
||||
EconomicOperatorRole,
|
||||
)
|
||||
from compliance.rci import (
|
||||
ChangeType,
|
||||
DeltaType,
|
||||
RegulatoryChange,
|
||||
assess_change,
|
||||
create_baseline,
|
||||
)
|
||||
|
||||
PROFILE = CanonicalProductRegulatoryProfile(
|
||||
name="Industriespülmaschine",
|
||||
product_type=CanonicalProductType.MACHINERY,
|
||||
markets=["EU", "DE"],
|
||||
economic_operator_role=EconomicOperatorRole.MANUFACTURER,
|
||||
lifecycle_phase=CanonicalLifecyclePhase.PLACING_ON_MARKET,
|
||||
is_machine=True,
|
||||
is_component=False,
|
||||
has_software_updates=True,
|
||||
has_embedded_software=True,
|
||||
has_remote_access=True,
|
||||
technologies=["cloud", "ota_updates"],
|
||||
)
|
||||
|
||||
# Evidence the customer already has, per obligation.
|
||||
EVIDENCE = {"provide_security_updates": ["policy", "ticket"], "sbom_creation": ["sbom"]}
|
||||
|
||||
BASELINE = create_baseline(PROFILE, EVIDENCE, baseline_id="b1")
|
||||
|
||||
|
||||
def _change(obs, regs=("CRA",), ctype=ChangeType.AMENDMENT, cid="c"):
|
||||
return RegulatoryChange(
|
||||
change_id=cid, affected_regulations=list(regs), affected_obligations=list(obs), change_type=ctype
|
||||
)
|
||||
|
||||
|
||||
def _by_id(assessment):
|
||||
return {d.obligation_id: d for d in assessment.deltas}
|
||||
|
||||
|
||||
# Baseline snapshots the registry-linked obligations from the frozen map.
|
||||
def test_baseline_snapshots_registry_obligations():
|
||||
assert "sbom_creation" in BASELINE.applicable_obligations
|
||||
assert "provide_security_updates" in BASELINE.applicable_obligations
|
||||
assert BASELINE.regulatory_map_snapshot.scope_resolved is True
|
||||
|
||||
|
||||
# 1 + 2. affects the product + flags a NEW obligation.
|
||||
def test_affects_product_and_new_obligation():
|
||||
a = assess_change(BASELINE, _change(["cra_new_requirement_xyz"], cid="c1"))
|
||||
assert a.affects_product is True
|
||||
assert _by_id(a)["cra_new_requirement_xyz"].delta_type == DeltaType.NEW
|
||||
|
||||
|
||||
# 2. an existing obligation amended -> CHANGED.
|
||||
def test_existing_obligation_changed():
|
||||
a = assess_change(BASELINE, _change(["sbom_creation"], cid="c2"))
|
||||
assert _by_id(a)["sbom_creation"].delta_type == DeltaType.CHANGED
|
||||
|
||||
|
||||
# 3. existing obligation with evidence + guidance update -> ALREADY_COVERED.
|
||||
def test_already_covered_by_evidence():
|
||||
a = assess_change(BASELINE, _change(["provide_security_updates"], ctype=ChangeType.GUIDANCE_UPDATE, cid="c3"))
|
||||
assert _by_id(a)["provide_security_updates"].delta_type == DeltaType.ALREADY_COVERED
|
||||
assert a.summary.already_covered == ["provide_security_updates"]
|
||||
|
||||
|
||||
# 4. what a human must review (existing obligation without evidence).
|
||||
def test_needs_review():
|
||||
a = assess_change(BASELINE, _change(["vuln_handling_process"], ctype=ChangeType.GUIDANCE_UPDATE, cid="c4"))
|
||||
assert _by_id(a)["vuln_handling_process"].delta_type == DeltaType.NEEDS_REVIEW
|
||||
assert "vuln_handling_process" in a.summary.needs_review
|
||||
assert "vuln_handling_process" in a.summary.what_matters_for_this_product
|
||||
|
||||
|
||||
# 5. a change to a regulation NOT in the map -> not relevant.
|
||||
def test_not_relevant_offmap_regulation():
|
||||
a = assess_change(BASELINE, _change(["psd2_strong_customer_auth"], regs=["PSD2"], ctype=ChangeType.NEW_REGULATION, cid="c5"))
|
||||
assert a.affects_product is False
|
||||
assert _by_id(a)["psd2_strong_customer_auth"].delta_type == DeltaType.NOT_APPLICABLE
|
||||
assert a.summary.not_relevant == ["psd2_strong_customer_auth"]
|
||||
|
||||
|
||||
# repeal removes an existing obligation.
|
||||
def test_repeal_removes_existing():
|
||||
a = assess_change(BASELINE, _change(["sbom_creation"], ctype=ChangeType.REPEAL, cid="c6"))
|
||||
assert _by_id(a)["sbom_creation"].delta_type == DeltaType.REMOVED
|
||||
|
||||
|
||||
# missing evidence is computed against the obligation's required evidence.
|
||||
def test_missing_evidence_on_changed():
|
||||
a = assess_change(BASELINE, _change(["sbom_creation"], cid="c7")) # requires sbom+repo_scan, has sbom
|
||||
d = _by_id(a)["sbom_creation"]
|
||||
assert "sbom" in d.affected_evidence
|
||||
assert "repo_scan" in d.missing_evidence
|
||||
|
||||
|
||||
# a change to an UNCERTAIN regulation -> needs review (resolve applicability first).
|
||||
def test_uncertain_regulation_needs_review():
|
||||
a = assess_change(BASELINE, _change(["red_cyber_req"], regs=["RED"], cid="c8"))
|
||||
assert a.affects_product is True # RED is in the map's uncertain bucket
|
||||
assert _by_id(a)["red_cyber_req"].delta_type == DeltaType.NEEDS_REVIEW
|
||||
|
||||
|
||||
# RCI answers "vs my map", not "what does the law say" — and works only on the snapshot.
|
||||
def test_works_against_stored_map_no_reevaluation():
|
||||
# a change with no affected_obligations still resolves affects_product from the map
|
||||
a = assess_change(BASELINE, RegulatoryChange(change_id="c9", affected_regulations=["CRA"], affected_obligations=[], change_type=ChangeType.AMENDMENT))
|
||||
assert a.affects_product is True
|
||||
assert a.deltas == []
|
||||
|
||||
|
||||
# delta_type is a THIRD vocabulary, disjoint from ClaimCoverage (Welt 1).
|
||||
def test_delta_vocabulary_distinct_from_claimcoverage():
|
||||
from compliance.reasoning.enums import ClaimCoverage
|
||||
|
||||
assert {d.value for d in DeltaType}.isdisjoint({c.value for c in ClaimCoverage})
|
||||
|
||||
|
||||
# the management summary aggregates the five buckets coherently.
|
||||
def test_summary_buckets():
|
||||
a = assess_change(
|
||||
BASELINE,
|
||||
RegulatoryChange(
|
||||
change_id="c10",
|
||||
affected_regulations=["CRA"],
|
||||
affected_obligations=["cra_new_one", "sbom_creation", "provide_security_updates"],
|
||||
change_type=ChangeType.AMENDMENT,
|
||||
),
|
||||
)
|
||||
s = a.summary
|
||||
assert "cra_new_one" in s.what_matters_for_this_product # NEW
|
||||
assert "sbom_creation" in s.needs_review # CHANGED -> review
|
||||
assert s.what_changed # non-empty management line
|
||||
Reference in New Issue
Block a user