feat(capability): Master Capability Registry v0 (Phase 2C, Compliance Execution domain)
Third instance of the identity-machine pattern (after Master Controls and Master Obligations). New compliance/capability/ package: MasterCapability with stable MCAP ids, CapabilityCandidate minting, seven typed relation types, a VERSIONED derivation policy, and identity lifecycle (merge/split/deprecate/redirect with provenance). Stored: identities, sources, relationship types, policy versions, lifecycle events, provenance. Derived (never stored): confidence/status via evaluate_relation under a policy version. Hard rule (structurally guarded): a certification alone can never yield CONFIRMED — only CONFIRMS + concrete artifact (or expert) does. Built from the Reasoning session per user directive but this IS the Compliance Execution model (Execution owns Capability) — handed off via the board. Metadata-first: CapabilityRelation is registry metadata, NOT a new meta-model class (freeze v1.0 untouched). No Company-Gap, no real ISO/cert mappings, no UI/RAG, no generic canonicalization engine. 11 tests; mypy --strict clean; LOC ok. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
"""Tests for Master Capability Registry v0 (Phase 2C, Compliance Execution domain).
|
||||
|
||||
Acceptance: a registry mints stable MCAP ids, stores typed relations + versioned
|
||||
policy + lifecycle events + provenance, and DERIVES confidence/status on demand
|
||||
(never stored). Hard rule: a certification alone can never yield CONFIRMED.
|
||||
|
||||
No real ISO/cert mappings here — only synthetic relations (mappings are not part of
|
||||
v0; they are Execution data, injected later).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from compliance.capability import (
|
||||
DEFAULT_POLICY,
|
||||
AssertionStatus,
|
||||
CapabilityCandidate,
|
||||
CapabilityRegistry,
|
||||
CapabilityRelation,
|
||||
Confidence,
|
||||
EvidenceKind,
|
||||
LifecycleEventType,
|
||||
PolicyRule,
|
||||
PolicyVersion,
|
||||
RelationType,
|
||||
assert_no_certification_confirms,
|
||||
deprecate_capability,
|
||||
evaluate_relation,
|
||||
merge_capabilities,
|
||||
mint_capability,
|
||||
resolve,
|
||||
)
|
||||
|
||||
|
||||
def _rel(rt, ek, target="MCAP-00001", src="x"):
|
||||
return CapabilityRelation(
|
||||
relation_id="r", source=src, target_capability_id=target, relationship_type=rt, evidence_kind=ek
|
||||
)
|
||||
|
||||
|
||||
# 1. minting assigns stable, incrementing MCAP ids.
|
||||
def test_mint_stable_mcap_id():
|
||||
reg = CapabilityRegistry()
|
||||
a = mint_capability(reg, CapabilityCandidate(raw_term="Patch Management"))
|
||||
b = mint_capability(reg, CapabilityCandidate(raw_term="Incident Response"))
|
||||
assert a.capability_id == "MCAP-00001"
|
||||
assert b.capability_id == "MCAP-00002"
|
||||
assert reg.capabilities["MCAP-00001"].name == "Patch Management"
|
||||
|
||||
|
||||
# 2. minted capability carries provenance.
|
||||
def test_mint_records_provenance():
|
||||
reg = CapabilityRegistry()
|
||||
cap = mint_capability(reg, CapabilityCandidate(raw_term="Secure Development"))
|
||||
assert cap.provenance.author == "system"
|
||||
assert "Secure Development" in cap.provenance.basis
|
||||
|
||||
|
||||
# 3. all seven relationship types exist.
|
||||
def test_relation_types_complete():
|
||||
assert {t.value for t in RelationType} == {
|
||||
"equivalent", "supports", "requires", "confirms", "broader_than", "narrower_than", "related_to",
|
||||
}
|
||||
|
||||
|
||||
# 4. confidence is COMPUTED from relation_type + evidence + policy_version, not stored.
|
||||
def test_confidence_computed_and_policy_referenced():
|
||||
rel = _rel(RelationType.SUPPORTS, EvidenceKind.CERTIFICATION)
|
||||
a = evaluate_relation(rel, DEFAULT_POLICY)
|
||||
assert a.status == AssertionStatus.INFERRED and a.confidence == Confidence.LOW
|
||||
assert a.policy_version == "capability-policy-v0"
|
||||
# the registry itself stores NO confidence/coverage
|
||||
assert "confidence" not in CapabilityRegistry.model_fields
|
||||
assert "coverage" not in CapabilityRegistry.model_fields
|
||||
|
||||
|
||||
# 4b. a DIFFERENT policy version yields a different result for the SAME relation
|
||||
# -> "why did you say X last year" needs the policy-as-of-then.
|
||||
def test_policy_versioning_changes_outcome():
|
||||
rel = _rel(RelationType.SUPPORTS, EvidenceKind.CERTIFICATION)
|
||||
v0b = PolicyVersion(
|
||||
policy_version="capability-policy-v0b",
|
||||
rules=[PolicyRule(
|
||||
relationship_type=RelationType.SUPPORTS, evidence_kind=EvidenceKind.CERTIFICATION,
|
||||
status=AssertionStatus.INFERRED, confidence=Confidence.MEDIUM,
|
||||
)],
|
||||
)
|
||||
assert evaluate_relation(rel, DEFAULT_POLICY).confidence == Confidence.LOW
|
||||
assert evaluate_relation(rel, v0b).confidence == Confidence.MEDIUM
|
||||
assert evaluate_relation(rel, v0b).policy_version == "capability-policy-v0b"
|
||||
|
||||
|
||||
# 5 + 8. HARD RULE: a certification alone can NEVER be CONFIRMED.
|
||||
def test_certification_never_confirmed():
|
||||
for rt in (RelationType.SUPPORTS, RelationType.EQUIVALENT, RelationType.BROADER_THAN, RelationType.RELATED_TO):
|
||||
a = evaluate_relation(_rel(rt, EvidenceKind.CERTIFICATION), DEFAULT_POLICY)
|
||||
assert a.status != AssertionStatus.CONFIRMED
|
||||
# only a concrete artifact via a CONFIRMS relation reaches CONFIRMED
|
||||
assert evaluate_relation(_rel(RelationType.CONFIRMS, EvidenceKind.ARTIFACT)).status == AssertionStatus.CONFIRMED
|
||||
# the shipped policy structurally satisfies the hard rule
|
||||
assert_no_certification_confirms(DEFAULT_POLICY)
|
||||
|
||||
|
||||
# 8b. a policy that maps a certification to CONFIRMED is rejected.
|
||||
def test_bad_policy_rejected():
|
||||
bad = PolicyVersion(
|
||||
policy_version="bad",
|
||||
rules=[PolicyRule(
|
||||
relationship_type=RelationType.EQUIVALENT, evidence_kind=EvidenceKind.CERTIFICATION,
|
||||
status=AssertionStatus.CONFIRMED, confidence=Confidence.HIGH,
|
||||
)],
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
assert_no_certification_confirms(bad)
|
||||
|
||||
|
||||
# 6. provenance on a curated relation atom.
|
||||
def test_relation_carries_provenance():
|
||||
rel = CapabilityRelation(
|
||||
relation_id="r1", source="certification:ISO27001", target_capability_id="MCAP-00001",
|
||||
relationship_type=RelationType.SUPPORTS, evidence_kind=EvidenceKind.CERTIFICATION,
|
||||
)
|
||||
assert rel.relationship_type == RelationType.SUPPORTS
|
||||
assert hasattr(rel, "provenance")
|
||||
|
||||
|
||||
# 9. merge keeps a redirect: resolve(old) follows it to the new capability.
|
||||
def test_merge_keeps_redirect():
|
||||
reg = CapabilityRegistry()
|
||||
old = mint_capability(reg, CapabilityCandidate(raw_term="Update Management"))
|
||||
new = mint_capability(reg, CapabilityCandidate(raw_term="Software Update Management"))
|
||||
event = merge_capabilities(reg, old.capability_id, new.capability_id)
|
||||
assert event.event_type == LifecycleEventType.MERGE
|
||||
assert reg.capabilities[old.capability_id].redirect_to == new.capability_id
|
||||
# resolve follows the redirect to the canonical capability
|
||||
assert resolve(reg, old.capability_id).capability_id == new.capability_id
|
||||
assert reg.lifecycle_events[-1].from_ids == [old.capability_id]
|
||||
|
||||
|
||||
# 9b. deprecate without a redirect resolves to None (no canonical target).
|
||||
def test_deprecate_without_redirect_resolves_none():
|
||||
reg = CapabilityRegistry()
|
||||
cap = mint_capability(reg, CapabilityCandidate(raw_term="Legacy Capability"))
|
||||
deprecate_capability(reg, cap.capability_id)
|
||||
assert resolve(reg, cap.capability_id) is None
|
||||
assert reg.capabilities[cap.capability_id].state.value == "deprecated"
|
||||
|
||||
|
||||
# requires = an obligation NEEDS a capability (relevance), not possession -> unknown.
|
||||
def test_requires_is_relevance_not_possession():
|
||||
a = evaluate_relation(_rel(RelationType.REQUIRES, EvidenceKind.NONE), DEFAULT_POLICY)
|
||||
assert a.status == AssertionStatus.UNKNOWN
|
||||
Reference in New Issue
Block a user