feat: add institutional governance and recovery contracts

This commit is contained in:
2026-08-01 17:46:54 +02:00
parent b65b48832b
commit 7192d32e65
61 changed files with 12539 additions and 168 deletions
+816
View File
@@ -0,0 +1,816 @@
from __future__ import annotations
from dataclasses import replace
from datetime import UTC, datetime, timedelta
import unittest
from govoplan_core.core.automation import ActionExecutionRequest, AutomationInvocation
from govoplan_core.core.events import PlatformEvent
from govoplan_core.core.external_references import ExternalObjectReference
from govoplan_core.core.institutional import (
ActorRepresentationReference,
DecisionEffectReference,
EvidenceReference,
ExternalSourceReference,
FormDefinition,
FormFieldDefinition,
FormalDecision,
GovernedContextEnvelope,
InformationGovernanceReference,
InstitutionalContextError,
InstitutionalReference,
LegalBasisReference,
MandateDefinition,
MandateResolution,
MandateResolutionRequest,
PartyRepresentation,
PartySubjectReference,
ProcedureParty,
ServiceBinding,
ServiceAvailabilityRequirement,
ServiceDefinition,
ServiceLaunchRequest,
ServiceLaunchResult,
TemporalRevision,
derive_service_restriction,
resolve_mandate_candidates,
revise_procedure_party,
revise_formal_decision,
revise_mandate_definition,
revoke_party_representation,
)
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
def _reference(
kind: str,
object_id: str,
*,
tenant_id: str = "tenant-1",
) -> InstitutionalReference:
owners = {
"institution": "tenancy",
"organization_unit": "organizations",
"function": "organizations",
"function_assignment": "idm",
"mandate": "cases",
"jurisdiction": "cases",
"service": "portal",
"form": "forms",
"form_submission": "forms_runtime",
"case": "cases",
"party": "cases",
"decision": "committee",
"work_item": "workflow_engine",
"workflow": "workflow_engine",
"approval": "workflow_engine",
"record": "audit",
}
return InstitutionalReference(
kind=kind,
owner_module=owners[kind],
object_id=object_id,
tenant_id=tenant_id,
version="3",
valid_at=NOW,
label=f"Protected label for {object_id}",
)
def _context() -> GovernedContextEnvelope:
external = ExternalObjectReference(
system="gis.example",
object_type="administrative_area",
object_id="area-7",
maturity="read",
authority_mode="external_mirror",
canonical_url="https://gis.example/areas/7",
observed_at=NOW,
)
return GovernedContextEnvelope(
tenant_id="tenant-1",
temporal=TemporalRevision(
revision="7",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1),
recorded_at=NOW,
change_reason="Case state changed.",
),
actor=ActorRepresentationReference(
tenant_id="tenant-1",
account_id="account-1",
identity_id="identity-1",
represented_function_ref=_reference("function", "function-1"),
function_assignment_ref=_reference(
"function_assignment", "assignment-1"
),
mandate_ref=_reference("mandate", "mandate-1"),
),
institution_ref=_reference("institution", "institution-1"),
organization_unit_ref=_reference("organization_unit", "unit-1"),
function_ref=_reference("function", "function-1"),
mandate_ref=_reference("mandate", "mandate-1"),
jurisdiction_refs=(_reference("jurisdiction", "jurisdiction-1"),),
service_ref=_reference("service", "service-1"),
case_ref=_reference("case", "case-1"),
party_refs=(_reference("party", "party-1"),),
work_item_ref=_reference("work_item", "work-item-1"),
workflow_ref=_reference("workflow", "workflow-1"),
approval_refs=(_reference("approval", "approval-1"),),
decision_ref=_reference("decision", "decision-1"),
record_refs=(_reference("record", "record-1"),),
legal_bases=(
LegalBasisReference(
kind="law",
authority="Example legislature",
reference="law-1/section-3",
version="2026-01",
effective_from=NOW - timedelta(days=30),
title="Protected legal title",
inspection_url="https://law.example/section-3",
),
),
evidence=(
EvidenceReference(
kind="document",
owner_module="files",
evidence_id="file-1",
tenant_id="tenant-1",
version="4",
checksum="sha256:abcd",
captured_at=NOW,
inspection_url="https://govoplan.example/files/file-1",
),
),
information_governance=InformationGovernanceReference(
classification="confidential",
purposes=("case decision",),
legal_basis_refs=("law-1/section-3",),
retention_policy_ref="records:case-default",
disclosure_state="restricted",
),
external_sources=(
ExternalSourceReference(
reference=external,
authority_mode="external_mirror",
maturity="read",
freshness_state="current",
health_state="ok",
),
),
)
class InstitutionalContractTests(unittest.TestCase):
def test_context_round_trip_preserves_versioned_references(self) -> None:
context = _context()
restored = GovernedContextEnvelope.from_mapping(
context.to_dict(include_inspection=True)
)
self.assertEqual(context.tenant_id, restored.tenant_id)
self.assertEqual("mandate-1", restored.mandate_ref.object_id)
self.assertEqual(
"external_mirror", restored.external_sources[0].authority_mode
)
self.assertEqual("workflow-1", restored.workflow_ref.object_id)
self.assertEqual("approval-1", restored.approval_refs[0].object_id)
self.assertEqual("record-1", restored.record_refs[0].object_id)
self.assertTrue(restored.temporal.effective_at(NOW))
def test_cross_tenant_reference_is_rejected(self) -> None:
with self.assertRaisesRegex(InstitutionalContextError, "different tenant"):
GovernedContextEnvelope(
tenant_id="tenant-1",
temporal=TemporalRevision(revision="1", recorded_at=NOW),
case_ref=_reference("case", "case-2", tenant_id="tenant-2"),
)
def test_default_serialization_redacts_labels_and_inspection_links(self) -> None:
payload = _context().to_dict()
self.assertIsNone(payload["case_ref"]["label"])
self.assertIsNone(payload["legal_bases"][0]["title"])
self.assertIsNone(payload["legal_bases"][0]["inspection_url"])
self.assertIsNone(payload["evidence"][0]["inspection_url"])
def test_event_and_action_envelopes_carry_reference_context(self) -> None:
context = _context()
event = PlatformEvent(
type="committee.decision.recorded",
module_id="committee",
institutional_context=context,
)
action = ActionExecutionRequest(
tenant_id="tenant-1",
action_key="committee.decision.publish",
input={"decision_ref": "decision-1"},
idempotency_key="decision-1:publish:1",
invocation=AutomationInvocation(kind="workflow"),
institutional_context=context,
)
self.assertEqual(
"decision-1",
event.to_dict()["institutional_context"]["decision_ref"]["object_id"],
)
self.assertIs(context, action.institutional_context)
def test_formal_decision_redacts_reasoning_but_keeps_evidence_refs(self) -> None:
context = _context()
decision = FormalDecision(
reference=_reference("decision", "decision-1"),
temporal=context.temporal,
decision_type="permit",
subject_refs=(_reference("case", "case-1"),),
state="decided",
authority_context=context,
fact_evidence=context.evidence,
legal_bases=context.legal_bases,
operative_result="Permit granted.",
reasoning="Protected reasoning.",
)
redacted = decision.to_dict()
protected = decision.to_dict(include_protected=True)
self.assertIsNone(redacted["reasoning"])
self.assertEqual("Protected reasoning.", protected["reasoning"])
self.assertEqual(
"file-1", redacted["fact_evidence"][0]["evidence_id"]
)
restored = FormalDecision.from_mapping(protected)
self.assertEqual("Protected reasoning.", restored.reasoning)
self.assertEqual("decision-1", restored.reference.object_id)
def test_semantic_provider_dtos_round_trip_without_domain_tables(self) -> None:
context = _context()
mandate = MandateDefinition(
reference=_reference("mandate", "mandate-1"),
temporal=context.temporal,
task_types=("committee.formal_decision",),
authority_types=("permit",),
organization_unit_refs=(
_reference("organization_unit", "unit-1"),
),
function_refs=(_reference("function", "function-1"),),
jurisdiction_refs=context.jurisdiction_refs,
legal_bases=context.legal_bases,
evidence=context.evidence,
)
resolution = MandateResolution(
competent=True,
mandates=(mandate,),
explanation="Authority matched.",
evidence=context.evidence,
)
service = ServiceDefinition(
reference=_reference("service", "service-1"),
key="permit.service",
temporal=context.temporal,
title="Permit service",
audience=("residents",),
legal_bases=context.legal_bases,
channels=("portal", "mail"),
responsible_organization_ref=_reference(
"organization_unit", "unit-1"
),
responsible_function_ref=_reference("function", "function-1"),
mandate_ref=_reference("mandate", "mandate-1"),
jurisdiction_refs=context.jurisdiction_refs,
bindings=(ServiceBinding("workflow", "workflow:permit"),),
publication_state="published",
)
represented = _reference("party", "party-applicant")
representative = _reference("party", "party-agent")
party = ProcedureParty(
reference=represented,
procedure_ref=_reference("case", "case-1"),
role="applicant",
subject=PartySubjectReference(
kind="identity",
provider="identity",
subject_id="identity-1",
tenant_id="tenant-1",
version="2",
),
temporal=context.temporal,
preferred_channels=("portal",),
permitted_channels=("portal", "mail"),
delivery_recipient=True,
representations=(
PartyRepresentation(
representative_party_ref=representative,
represented_party_ref=represented,
power_ref="power-1",
permitted_actions=("submit", "receive"),
temporal=context.temporal,
evidence=context.evidence,
),
),
contact_snapshot_refs=("addresses:snapshot-1",),
evidence=context.evidence,
)
restored_resolution = MandateResolution.from_mapping(
resolution.to_dict(include_inspection=True)
)
restored_service = ServiceDefinition.from_mapping(service.to_dict())
restored_party = ProcedureParty.from_mapping(
party.to_dict(include_inspection=True)
)
self.assertTrue(restored_resolution.competent)
self.assertEqual(
"committee.formal_decision",
restored_resolution.mandates[0].task_types[0],
)
self.assertEqual("workflow:permit", restored_service.bindings[0].reference)
self.assertEqual("identity-1", restored_party.subject.subject_id)
self.assertEqual(
"party-agent",
restored_party.representations[0].representative_party_ref.object_id,
)
def test_invalid_semantic_states_are_rejected(self) -> None:
with self.assertRaisesRegex(InstitutionalContextError, "disclosure state"):
InformationGovernanceReference(
classification="internal",
purposes=("test",),
disclosure_state="invalid", # type: ignore[arg-type]
)
def test_service_launch_contract_requires_exact_same_tenant_targets(self) -> None:
service_ref = _reference("service", "service-1")
binding = ServiceBinding("case", "permit")
request = ServiceLaunchRequest(
service_ref=service_ref,
binding=binding,
idempotency_key="launch-1",
requested_at=NOW,
parameters={"title": "Permit application"},
)
result = ServiceLaunchResult(
service_ref=service_ref,
binding=binding,
state="started",
target_ref=_reference("case", "case-1"),
href="/cases/case-1",
metadata={"case_number": "PERMIT-1"},
)
self.assertEqual("launch-1", request.idempotency_key)
self.assertEqual("case-1", result.to_dict()["target_ref"]["object_id"])
with self.assertRaisesRegex(InstitutionalContextError, "another tenant"):
ServiceLaunchResult(
service_ref=service_ref,
binding=binding,
state="started",
target_ref=_reference("case", "case-2", tenant_id="tenant-2"),
metadata={},
)
def test_form_definition_contract_round_trips_exact_schema(self) -> None:
definition = FormDefinition(
reference=_reference("form", "permit-application"),
key="permit-application",
temporal=TemporalRevision(
revision="3",
recorded_at=NOW,
change_reason="Add delivery preference.",
),
title="Permit application",
description="Apply for a permit.",
fields=(
FormFieldDefinition(
key="email",
label="Email address",
value_type="email",
required=True,
constraints={"max_length": 255},
),
FormFieldDefinition(
key="delivery",
label="Delivery",
value_type="choice",
options=("portal", "mail"),
),
),
max_attachments=3,
signature_requirement="optional",
policy_refs=("policy:permit-intake",),
handoff_kinds=("case",),
)
restored = FormDefinition.from_mapping(definition.to_dict())
self.assertEqual("3", restored.reference.version)
self.assertEqual("email", restored.fields[0].key)
self.assertEqual(("portal", "mail"), restored.fields[1].options)
with self.assertRaisesRegex(InstitutionalContextError, "unique"):
replace(restored, fields=(restored.fields[0], restored.fields[0]))
def test_service_launch_redirect_rejects_credentialed_urls(self) -> None:
with self.assertRaisesRegex(InstitutionalContextError, "credential-free"):
ServiceLaunchResult(
service_ref=_reference("service", "service-1"),
binding=ServiceBinding("external", "external-entry"),
state="redirect",
href="https://user:secret@example.test/start",
metadata={},
)
with self.assertRaisesRegex(InstitutionalContextError, "must be a boolean"):
ServiceBinding.from_mapping(
{
"kind": "workflow",
"reference": "workflow:permit",
"required": "false",
}
)
def test_service_derivation_can_only_tighten_parent_constraints(self) -> None:
parent = ServiceDefinition(
reference=_reference("service", "permit-template"),
key="permit.apply",
temporal=TemporalRevision(
revision="7",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=10),
recorded_at=NOW - timedelta(days=2),
),
title="Permit service",
audience=("resident", "business"),
prerequisites=("registered-address",),
required_evidence_types=("application",),
channels=("portal", "mail"),
bindings=(ServiceBinding("case", "permit-case"),),
availability_requirements=(
ServiceAvailabilityRequirement("policy", "permit-access"),
),
publication_state="published",
)
temporal = TemporalRevision(
revision="8",
valid_from=NOW,
valid_to=NOW + timedelta(days=5),
recorded_at=NOW,
change_reason="Tenant specialization.",
)
reference = replace(
_reference("service", "permit-tenant"),
version="8",
)
derived = derive_service_restriction(
parent,
reference=reference,
temporal=temporal,
audience=("resident",),
channels=("portal",),
add_prerequisites=("tenant-residency",),
add_required_evidence_types=("identity",),
add_bindings=(ServiceBinding("workflow", "permit-review"),),
)
self.assertEqual(parent.reference, derived.derived_from_ref)
self.assertEqual(("resident",), derived.audience)
self.assertEqual(("portal",), derived.channels)
self.assertEqual(
("registered-address", "tenant-residency"),
derived.prerequisites,
)
self.assertIn(ServiceBinding("case", "permit-case"), derived.bindings)
self.assertEqual(
parent.availability_requirements,
derived.availability_requirements,
)
self.assertEqual(
parent.availability_requirements,
ServiceDefinition.from_mapping(
derived.to_dict()
).availability_requirements,
)
with self.assertRaisesRegex(InstitutionalContextError, "widen.*audience"):
derive_service_restriction(
parent,
reference=reference,
temporal=temporal,
audience=("resident", "visitor"),
)
with self.assertRaisesRegex(InstitutionalContextError, "beyond"):
derive_service_restriction(
parent,
reference=reference,
temporal=replace(temporal, valid_to=None),
)
with self.assertRaisesRegex(
InstitutionalContextError,
"publication state",
):
derive_service_restriction(
replace(parent, publication_state="suspended"),
reference=reference,
temporal=temporal,
publication_state="published",
)
def test_mandate_resolution_is_effective_scoped_and_deterministic(self) -> None:
context = _context()
mandate = MandateDefinition(
reference=_reference("mandate", "mandate-1"),
temporal=TemporalRevision(
revision="1",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(days=1),
recorded_at=NOW - timedelta(days=2),
),
task_types=("committee.formal_decision",),
authority_types=("permit",),
organization_unit_refs=(
_reference("organization_unit", "unit-1"),
),
function_refs=(_reference("function", "function-1"),),
jurisdiction_refs=context.jurisdiction_refs,
subject_types=("permit",),
evidence=context.evidence,
)
request = MandateResolutionRequest(
tenant_id="tenant-1",
effective_at=NOW,
task_type="committee.formal_decision",
authority_type="permit",
organization_unit_ref=replace(
_reference("organization_unit", "unit-1"), version="9"
),
function_ref=replace(
_reference("function", "function-1"), version="9"
),
jurisdiction_refs=tuple(
replace(item, version="9") for item in context.jurisdiction_refs
),
subject_type="permit",
)
resolution = resolve_mandate_candidates(
request,
(
replace(
mandate,
reference=_reference("mandate", "retired"),
status="retired",
),
mandate,
),
)
self.assertTrue(resolution.competent)
self.assertEqual("mandate-1", resolution.mandates[0].reference.object_id)
self.assertEqual("file-1", resolution.evidence[0].evidence_id)
ambiguous = resolve_mandate_candidates(
request,
(
mandate,
replace(
mandate,
reference=_reference("mandate", "mandate-2"),
),
),
)
self.assertFalse(ambiguous.competent)
self.assertEqual(2, len(ambiguous.conflict_refs))
missing_jurisdiction = resolve_mandate_candidates(
replace(request, jurisdiction_refs=()),
(mandate,),
)
self.assertFalse(missing_jurisdiction.competent)
def test_mandate_resolution_rejects_cross_tenant_candidates(self) -> None:
request = MandateResolutionRequest(
tenant_id="tenant-1",
effective_at=NOW,
task_type="committee.formal_decision",
authority_type="permit",
)
candidate = MandateDefinition(
reference=_reference("mandate", "mandate-2", tenant_id="tenant-2"),
temporal=TemporalRevision(revision="1", recorded_at=NOW),
task_types=("committee.formal_decision",),
authority_types=("permit",),
)
with self.assertRaisesRegex(InstitutionalContextError, "cross tenants"):
resolve_mandate_candidates(request, (candidate,))
def test_mandate_lifecycle_revisions_are_linked_by_stable_identity(self) -> None:
current = MandateDefinition(
reference=_reference("mandate", "mandate-1"),
temporal=TemporalRevision(revision="1", recorded_at=NOW),
task_types=("committee.formal_decision",),
authority_types=("permit",),
status="active",
)
temporal = TemporalRevision(
revision="2",
recorded_at=NOW + timedelta(minutes=1),
change_reason="Authority temporarily suspended.",
)
suspended = revise_mandate_definition(
current,
expected_revision="1",
temporal=temporal,
status="suspended",
suspension_reason="Delegation is under review.",
)
self.assertEqual("active", current.status)
self.assertEqual("suspended", suspended.status)
self.assertEqual("2", suspended.reference.version)
self.assertEqual(current.reference.object_id, suspended.reference.object_id)
with self.assertRaisesRegex(InstitutionalContextError, "stale"):
revise_mandate_definition(
current,
expected_revision="0",
temporal=temporal,
status="suspended",
suspension_reason="Review.",
)
with self.assertRaisesRegex(InstitutionalContextError, "not allowed"):
revise_mandate_definition(
suspended,
expected_revision="2",
temporal=replace(
temporal,
revision="3",
change_reason="Invalid reset.",
),
status="draft",
)
def test_formal_decision_revisions_are_linked_and_occ_guarded(self) -> None:
context = _context()
reference = replace(
_reference("decision", "decision-1"),
version=context.temporal.revision,
)
context = replace(context, decision_ref=reference)
current = FormalDecision(
reference=reference,
temporal=context.temporal,
decision_type="permit",
subject_refs=(_reference("case", "case-1"),),
state="decided",
authority_context=context,
fact_evidence=context.evidence,
legal_bases=context.legal_bases,
operative_result="Permit granted.",
reasoning="Protected reasoning.",
requested_effects=(
DecisionEffectReference("notify", "requested"),
),
)
with self.assertRaisesRegex(
InstitutionalContextError,
"service account",
):
replace(current, assurance_level="automated_under_mandate")
revision = TemporalRevision(
revision="8",
valid_from=NOW + timedelta(minutes=1),
recorded_at=NOW + timedelta(minutes=1),
change_reason="Decision entered into force.",
)
effective = revise_formal_decision(
current,
expected_revision="7",
temporal=revision,
state="effective",
)
self.assertEqual("decided", current.state)
self.assertEqual("8", effective.reference.version)
self.assertEqual("7", effective.supersedes_ref.version)
self.assertEqual(
effective.reference,
effective.authority_context.decision_ref,
)
self.assertEqual(current.requested_effects, effective.requested_effects)
with self.assertRaisesRegex(InstitutionalContextError, "stale"):
revise_formal_decision(
current,
expected_revision="6",
temporal=revision,
state="effective",
)
with self.assertRaisesRegex(InstitutionalContextError, "not allowed"):
revise_formal_decision(
effective,
expected_revision="8",
temporal=replace(
revision,
revision="9",
change_reason="Invalid regression.",
),
state="proposed",
)
corrected = revise_formal_decision(
effective,
expected_revision="8",
temporal=replace(
revision,
revision="9",
recorded_at=NOW + timedelta(minutes=2),
change_reason="Corrected operative wording.",
),
state="corrected",
operative_result="Permit granted subject to condition A.",
)
self.assertEqual(effective.reference, corrected.correction_of_ref)
self.assertEqual(effective.reference, corrected.supersedes_ref)
def test_party_corrections_and_representation_revocation_are_versioned(self) -> None:
context = _context()
represented = _reference("party", "party-applicant")
representative = _reference("party", "party-agent")
representation = PartyRepresentation(
representative_party_ref=representative,
represented_party_ref=represented,
power_ref="power-1",
permitted_actions=("submit", "receive"),
temporal=TemporalRevision(
revision="1",
valid_from=NOW - timedelta(days=1),
recorded_at=NOW - timedelta(days=1),
),
evidence=context.evidence,
)
current = ProcedureParty(
reference=represented,
procedure_ref=_reference("case", "case-1"),
role="applicant",
subject=PartySubjectReference(
kind="identity",
provider="identity",
subject_id="identity-1",
tenant_id="tenant-1",
version="2",
),
temporal=TemporalRevision(revision="1", recorded_at=NOW),
permitted_channels=("mail", "postbox"),
preferred_channels=("mail",),
delivery_recipient=True,
representations=(representation,),
contact_snapshot_refs=("addresses:snapshot-1",),
evidence=context.evidence,
)
revision = TemporalRevision(
revision="2",
valid_from=NOW,
recorded_at=NOW + timedelta(minutes=1),
change_reason="Correct preferred delivery channel.",
)
corrected = revise_procedure_party(
current,
expected_revision="1",
temporal=revision,
preferred_channels=("postbox",),
)
revoked_power = revoke_party_representation(
representation,
expected_revision="1",
temporal=TemporalRevision(
revision="2",
valid_from=NOW - timedelta(days=1),
valid_to=NOW + timedelta(minutes=2),
recorded_at=NOW + timedelta(minutes=2),
change_reason="Power was revoked.",
),
revoked_at=NOW + timedelta(minutes=2),
)
self.assertEqual(("mail",), current.preferred_channels)
self.assertEqual(("postbox",), corrected.preferred_channels)
self.assertEqual("2", corrected.reference.version)
self.assertIsNone(representation.revoked_at)
self.assertEqual(NOW + timedelta(minutes=2), revoked_power.revoked_at)
with self.assertRaisesRegex(InstitutionalContextError, "stale"):
revise_procedure_party(
current,
expected_revision="0",
temporal=revision,
)
if __name__ == "__main__":
unittest.main()