98d616d82b
The learning point is not the hypothesis, it is the QUESTION — and confirmed/refuted is too coarse.
"partial, only critical suppliers" or "certified but not lived" are not "wrong", they are valuable
knowledge. So the chain is Hypothesis -> Question -> Observation -> (Review) -> Hypothesis, and the
observation model must be defined cleanly before any store/API (else thousands of too-coarse
observations get migrated later).
compliance/onboarding/observations.py:
- ObservationType: confirmed / partial / refuted / not_applicable / unknown (richer than binary).
- Observation: {hypothesis_id, capability, question, answer (free text), observation_type,
scope_note ("only critical suppliers"), evidence_uploaded, reviewed, reviewed_by}.
- empirical_distribution() -> a DISTRIBUTION (confirmed 61 / partial 31 / refuted 8), not one %.
- empirical_confidence() -> (confirmed + 0.5*partial) / (confirmed+partial+refuted); n.a./unknown
excluded; None until calibrated.
- REVIEW GATE: only reviewed observations calibrate — a raw answer never changes a hypothesis (no
learning from outliers).
Refactor: the hypothesis is now PURE curated knowledge — the binary observations counter and any
confidence are removed from CapabilityHypothesis and the YAML; confidence is COMPUTED from the separate
reviewed observation stream. Pure, mypy --strict clean. Persistence/aggregation/calibration are 59b/c/d.
Non-runtime -> no deploy. 12 tests pass, check-loc 0.
85 lines
4.4 KiB
Python
85 lines
4.4 KiB
Python
"""Certification Capability Hypotheses — capability-centric library + empirical confidence.
|
|
|
|
Pins the reuse design (one capability, many supporting certs -> ~40-60 hypotheses, not ~300), the
|
|
automatic multi-certification merge, the empirical (computed) confidence loop, and the Welt-1 guarantee
|
|
that capabilities NO cert suggests (SBOM, signed updates, CVD) are never inferred -> they stay in the
|
|
delta and get asked. Then the Advisor consumes the resolved library end-to-end.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
import yaml
|
|
|
|
from compliance.onboarding import (
|
|
CapabilityHypothesis,
|
|
Observation,
|
|
ObservationType,
|
|
OnboardingInput,
|
|
advisor_start,
|
|
empirical_confidence,
|
|
empirical_distribution,
|
|
inferred_hypotheses,
|
|
resolve_for_certifications,
|
|
)
|
|
from compliance.transition_reasoning import TargetRequirement
|
|
|
|
_DIR = os.path.dirname(__file__)
|
|
_LIB = [CapabilityHypothesis(**h) for h in yaml.safe_load(
|
|
open(os.path.join(_DIR, "..", "knowledge", "certification_hypotheses", "hypotheses.yaml"), encoding="utf-8"))["hypotheses"]]
|
|
|
|
|
|
def test_library_is_capability_centric_and_reuses_certs():
|
|
# the shared core is small (reuse, not 30-per-cert) and document control is supported by many certs
|
|
doc = next(h for h in _LIB if h.capability == "document_and_change_control")
|
|
assert len(doc.supported_by) >= 4
|
|
assert len(_LIB) <= 60 # whole library, not ~300
|
|
|
|
|
|
def test_multi_certification_merges_automatically():
|
|
# a company with ISO9001 + ISO14001 + TISAX gets the UNION of their hypotheses, deduped
|
|
merged = inferred_hypotheses(["ISO9001", "ISO14001", "TISAX"], _LIB)
|
|
caps = {h.capability for h in merged}
|
|
assert "document_and_change_control" in caps # ISO9001 + TISAX
|
|
assert "information_security_management" in caps # TISAX
|
|
assert "environmental_management_documentation" in caps # ISO14001
|
|
# SBOM / signed updates are suggested by NO certificate -> never inferred
|
|
assert "sbom_creation" not in caps and "secure_signed_update_distribution" not in caps
|
|
|
|
|
|
def test_observations_are_richer_than_binary_and_review_gated():
|
|
# the learning unit is the QUESTION; an answer can be partial with a scope note, not just yes/no
|
|
raw = [Observation(hypothesis_id="HYP-supplier", observation_type=ObservationType.CONFIRMED)]
|
|
assert empirical_confidence(raw) is None # unreviewed -> does NOT calibrate (review gate)
|
|
obs = [
|
|
Observation(hypothesis_id="HYP-supplier", observation_type=ObservationType.CONFIRMED, reviewed=True),
|
|
Observation(hypothesis_id="HYP-supplier", observation_type=ObservationType.PARTIAL,
|
|
scope_note="nur kritische Lieferanten", reviewed=True),
|
|
Observation(hypothesis_id="HYP-supplier", observation_type=ObservationType.REFUTED, reviewed=True),
|
|
Observation(hypothesis_id="HYP-supplier", observation_type=ObservationType.NOT_APPLICABLE, reviewed=True),
|
|
]
|
|
dist = empirical_distribution(obs) # a DISTRIBUTION, not a single percentage
|
|
assert dist["confirmed"] == 1 and dist["partial"] == 1 and dist["refuted"] == 1 and dist["not_applicable"] == 1
|
|
# confidence = (confirmed + 0.5*partial) / (confirmed+partial+refuted); n.a. excluded from the base
|
|
assert empirical_confidence(obs) == 0.5
|
|
|
|
|
|
def test_resolve_adapts_to_advisor_input():
|
|
res = resolve_for_certifications(["ISO27001", "ISO9001"], _LIB)
|
|
assert "incident_management" in res["ISO27001"]
|
|
assert "document_and_change_control" in res["ISO9001"]
|
|
|
|
|
|
def test_advisor_consumes_the_library_end_to_end():
|
|
cra = yaml.safe_load(open(os.path.join(_DIR, "..", "knowledge", "transition_patterns",
|
|
"transition_pattern_iso27001_to_cra_maschinenvo_v1.yaml"), encoding="utf-8"))
|
|
req = [TargetRequirement(capability_id=a["capability"]) for a in cra["likely_covered"]]
|
|
req += [TargetRequirement(capability_id=d["capability"], expected_evidence=d.get("expected_evidence", []))
|
|
for d in cra["delta_requirements"]]
|
|
inp = OnboardingInput(company="x", certifications=["ISO27001", "TISAX", "ISO9001", "ISO14001"], target=["CRA"])
|
|
hyp = resolve_for_certifications(inp.certifications, _LIB) # library -> advisor input
|
|
res = advisor_start(inp, hyp, req, target_id="CRA", corpus_status={"CRA": "validated"})
|
|
assert res.inferred_assumptions and res.next_best_questions
|
|
assert any(r.certification == "ISO14001" for r in res.rejected_assumptions) # not relevant to CRA
|