This commit is contained in:
@@ -356,7 +356,7 @@ def consume_first_admin_credential(
|
||||
tenant = Tenant(
|
||||
slug=clean_tenant_slug,
|
||||
name=clean_tenant_name,
|
||||
default_locale="en",
|
||||
default_locale="de",
|
||||
settings={},
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Literal, Mapping, cast
|
||||
|
||||
|
||||
InformationGovernanceAdoption = Literal[
|
||||
"not_applicable",
|
||||
"contract_only",
|
||||
"partial",
|
||||
"enforced",
|
||||
]
|
||||
|
||||
INFORMATION_GOVERNANCE_ADOPTION_ORDER: tuple[InformationGovernanceAdoption, ...] = (
|
||||
"not_applicable",
|
||||
"contract_only",
|
||||
"partial",
|
||||
"enforced",
|
||||
)
|
||||
|
||||
|
||||
class InformationGovernanceDeclarationError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class InformationGovernanceDimension:
|
||||
"""Truthful module-level adoption claim for one cross-cutting dimension."""
|
||||
|
||||
adoption: InformationGovernanceAdoption = "contract_only"
|
||||
object_types: tuple[str, ...] = ()
|
||||
evidence: tuple[str, ...] = ()
|
||||
limitation: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.adoption not in INFORMATION_GOVERNANCE_ADOPTION_ORDER:
|
||||
raise InformationGovernanceDeclarationError(
|
||||
f"Unsupported information-governance adoption: {self.adoption!r}."
|
||||
)
|
||||
for field_name in ("object_types", "evidence"):
|
||||
values = getattr(self, field_name)
|
||||
if len(values) != len(set(values)) or any(not item.strip() for item in values):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
f"Information-governance {field_name.replace('_', ' ')} must "
|
||||
"contain unique non-empty values."
|
||||
)
|
||||
if self.adoption == "enforced" and not self.evidence:
|
||||
raise InformationGovernanceDeclarationError(
|
||||
"An enforced information-governance dimension requires evidence."
|
||||
)
|
||||
if self.adoption in {"partial", "enforced"} and not self.object_types:
|
||||
raise InformationGovernanceDeclarationError(
|
||||
"Partial and enforced information-governance dimensions must "
|
||||
"name their covered object types."
|
||||
)
|
||||
if self.adoption == "not_applicable" and self.object_types:
|
||||
raise InformationGovernanceDeclarationError(
|
||||
"A non-applicable information-governance dimension cannot declare object types."
|
||||
)
|
||||
if self.adoption in {"contract_only", "partial"} and not str(
|
||||
self.limitation or ""
|
||||
).strip():
|
||||
raise InformationGovernanceDeclarationError(
|
||||
"Contract-only and partial adoption must state the current limitation."
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"adoption": self.adoption,
|
||||
"object_types": list(self.object_types),
|
||||
"evidence": list(self.evidence),
|
||||
"limitation": self.limitation,
|
||||
}
|
||||
|
||||
|
||||
def _contract_only_dimension() -> InformationGovernanceDimension:
|
||||
return InformationGovernanceDimension(
|
||||
adoption="contract_only",
|
||||
limitation=(
|
||||
"The platform contract applies, but module-specific adoption evidence "
|
||||
"has not been declared."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ModuleInformationGovernance:
|
||||
"""Cross-cutting data-use requirements and honest adoption evidence."""
|
||||
|
||||
temporal_browsing: InformationGovernanceDimension = field(
|
||||
default_factory=_contract_only_dimension
|
||||
)
|
||||
purpose_aware_access: InformationGovernanceDimension = field(
|
||||
default_factory=_contract_only_dimension
|
||||
)
|
||||
retention: InformationGovernanceDimension = field(
|
||||
default_factory=_contract_only_dimension
|
||||
)
|
||||
institutional_context: InformationGovernanceDimension = field(
|
||||
default_factory=_contract_only_dimension
|
||||
)
|
||||
current_authorization_for_historical_reads: bool = True
|
||||
contract_version: str = "1"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.contract_version != "1":
|
||||
raise InformationGovernanceDeclarationError(
|
||||
"Unsupported module information-governance contract version."
|
||||
)
|
||||
if not self.current_authorization_for_historical_reads:
|
||||
raise InformationGovernanceDeclarationError(
|
||||
"Historical reads must always use current authorization."
|
||||
)
|
||||
for name, dimension in self.dimensions.items():
|
||||
if not isinstance(dimension, InformationGovernanceDimension):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
f"Information-governance dimension {name!r} has an invalid value."
|
||||
)
|
||||
|
||||
@property
|
||||
def dimensions(self) -> Mapping[str, InformationGovernanceDimension]:
|
||||
return {
|
||||
"temporal_browsing": self.temporal_browsing,
|
||||
"purpose_aware_access": self.purpose_aware_access,
|
||||
"retention": self.retention,
|
||||
"institutional_context": self.institutional_context,
|
||||
}
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"contract_version": self.contract_version,
|
||||
"current_authorization_for_historical_reads": (
|
||||
self.current_authorization_for_historical_reads
|
||||
),
|
||||
"dimensions": {
|
||||
name: dimension.to_dict()
|
||||
for name, dimension in self.dimensions.items()
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def information_governance_from_mapping(
|
||||
value: Mapping[str, object],
|
||||
) -> ModuleInformationGovernance:
|
||||
raw_dimensions = value.get("dimensions")
|
||||
if not isinstance(raw_dimensions, Mapping):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
"Information-governance dimensions must be an object."
|
||||
)
|
||||
|
||||
def dimension(name: str) -> InformationGovernanceDimension:
|
||||
raw_dimension = raw_dimensions.get(name)
|
||||
if not isinstance(raw_dimension, Mapping):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
f"Information-governance dimension {name!r} must be an object."
|
||||
)
|
||||
|
||||
def text_tuple(field_name: str) -> tuple[str, ...]:
|
||||
raw_values = raw_dimension.get(field_name, ())
|
||||
if not isinstance(raw_values, (list, tuple)):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
f"Information-governance {name}.{field_name} must be a list."
|
||||
)
|
||||
if any(not isinstance(item, str) for item in raw_values):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
f"Information-governance {name}.{field_name} must contain strings."
|
||||
)
|
||||
return tuple(raw_values)
|
||||
|
||||
raw_limitation = raw_dimension.get("limitation")
|
||||
raw_adoption = raw_dimension.get("adoption") or "contract_only"
|
||||
if not isinstance(raw_adoption, str):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
f"Information-governance {name}.adoption must be a string."
|
||||
)
|
||||
if raw_limitation is not None and not isinstance(raw_limitation, str):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
f"Information-governance {name}.limitation must be a string."
|
||||
)
|
||||
return InformationGovernanceDimension(
|
||||
adoption=cast(
|
||||
InformationGovernanceAdoption,
|
||||
raw_adoption,
|
||||
),
|
||||
object_types=text_tuple("object_types"),
|
||||
evidence=text_tuple("evidence"),
|
||||
limitation=raw_limitation,
|
||||
)
|
||||
|
||||
current_authorization = value.get(
|
||||
"current_authorization_for_historical_reads",
|
||||
True,
|
||||
)
|
||||
if not isinstance(current_authorization, bool):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
"current_authorization_for_historical_reads must be boolean."
|
||||
)
|
||||
return ModuleInformationGovernance(
|
||||
contract_version=str(value.get("contract_version") or "1"),
|
||||
current_authorization_for_historical_reads=current_authorization,
|
||||
temporal_browsing=dimension("temporal_browsing"),
|
||||
purpose_aware_access=dimension("purpose_aware_access"),
|
||||
retention=dimension("retention"),
|
||||
institutional_context=dimension("institutional_context"),
|
||||
)
|
||||
|
||||
|
||||
def information_governance_maturity_issues(
|
||||
declaration: ModuleInformationGovernance,
|
||||
*,
|
||||
maturity: str | None,
|
||||
) -> tuple[str, ...]:
|
||||
if maturity not in {"reference_ready", "supported", "lts"}:
|
||||
return ()
|
||||
incomplete = [
|
||||
name
|
||||
for name, dimension in declaration.dimensions.items()
|
||||
if dimension.adoption not in {"not_applicable", "enforced"}
|
||||
]
|
||||
if not incomplete:
|
||||
return ()
|
||||
return (
|
||||
f"Maturity {maturity!r} requires enforced or explicitly non-applicable "
|
||||
"information governance for: " + ", ".join(incomplete),
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"INFORMATION_GOVERNANCE_ADOPTION_ORDER",
|
||||
"InformationGovernanceAdoption",
|
||||
"InformationGovernanceDeclarationError",
|
||||
"InformationGovernanceDimension",
|
||||
"ModuleInformationGovernance",
|
||||
"information_governance_from_mapping",
|
||||
"information_governance_maturity_issues",
|
||||
]
|
||||
@@ -17,6 +17,10 @@ from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||
|
||||
from govoplan_core.core.versioning import format_version_range, version_range_is_valid, version_satisfies_range
|
||||
from govoplan_core.core.information_governance import (
|
||||
information_governance_from_mapping,
|
||||
information_governance_maturity_issues,
|
||||
)
|
||||
from govoplan_core.core.provider_governance import (
|
||||
external_provider_from_mapping,
|
||||
module_architecture_from_mapping,
|
||||
@@ -630,6 +634,7 @@ def _normalize_catalog_item(value: Any) -> dict[str, object]:
|
||||
"tags": _string_list(value.get("tags")),
|
||||
}
|
||||
raw_architecture = value.get("architecture")
|
||||
architecture_maturity: str | None = None
|
||||
if raw_architecture is not None:
|
||||
if not isinstance(raw_architecture, Mapping):
|
||||
raise ValueError(
|
||||
@@ -647,6 +652,27 @@ def _normalize_catalog_item(value: Any) -> dict[str, object]:
|
||||
+ "; ".join(issues)
|
||||
)
|
||||
item["architecture"] = architecture.to_dict()
|
||||
architecture_maturity = architecture.maturity
|
||||
raw_information_governance = value.get("information_governance")
|
||||
if raw_information_governance is not None:
|
||||
if not isinstance(raw_information_governance, Mapping):
|
||||
raise ValueError(
|
||||
"Module package catalog information_governance for "
|
||||
f"{module_id!r} must be an object."
|
||||
)
|
||||
information_governance = information_governance_from_mapping(
|
||||
raw_information_governance
|
||||
)
|
||||
governance_issues = information_governance_maturity_issues(
|
||||
information_governance,
|
||||
maturity=architecture_maturity,
|
||||
)
|
||||
if governance_issues:
|
||||
raise ValueError(
|
||||
"Module package catalog information_governance for "
|
||||
f"{module_id!r} is invalid: " + "; ".join(governance_issues)
|
||||
)
|
||||
item["information_governance"] = information_governance.to_dict()
|
||||
raw_providers = value.get("external_providers")
|
||||
if raw_providers is not None:
|
||||
if not isinstance(raw_providers, list):
|
||||
|
||||
@@ -4,6 +4,7 @@ from collections.abc import Callable, Iterable, Mapping, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Literal, Protocol, TYPE_CHECKING
|
||||
|
||||
from govoplan_core.core.information_governance import ModuleInformationGovernance
|
||||
from govoplan_core.core.ownership import OwnershipProviderRegistration
|
||||
from govoplan_core.core.provider_governance import (
|
||||
ExternalProviderDeclaration,
|
||||
@@ -446,6 +447,9 @@ class ModuleManifest:
|
||||
...,
|
||||
] = ()
|
||||
architecture: ModuleArchitectureDeclaration | None = None
|
||||
information_governance: ModuleInformationGovernance = field(
|
||||
default_factory=ModuleInformationGovernance
|
||||
)
|
||||
external_providers: tuple[ExternalProviderDeclaration, ...] = ()
|
||||
external_provider_state_providers: tuple[
|
||||
ExternalProviderStateProviderRegistration,
|
||||
|
||||
@@ -32,6 +32,9 @@ from govoplan_core.core.module_entitlements import (
|
||||
current_tenant_execution_context,
|
||||
tenant_execution_scope,
|
||||
)
|
||||
from govoplan_core.core.information_governance import (
|
||||
information_governance_maturity_issues,
|
||||
)
|
||||
from govoplan_core.core.ownership import (
|
||||
OwnershipProviderRegistration,
|
||||
ResourceOwnershipProvider,
|
||||
@@ -788,6 +791,13 @@ def _validate_architecture_declarations(manifest: ModuleManifest) -> None:
|
||||
raise RegistryError(
|
||||
f"Module {manifest.id!r} architecture declaration: {issue}"
|
||||
)
|
||||
for issue in information_governance_maturity_issues(
|
||||
manifest.information_governance,
|
||||
maturity=architecture.maturity if architecture is not None else None,
|
||||
):
|
||||
raise RegistryError(
|
||||
f"Module {manifest.id!r} information-governance declaration: {issue}"
|
||||
)
|
||||
|
||||
provider_ids: set[str] = set()
|
||||
declared_capabilities = {
|
||||
|
||||
Reference in New Issue
Block a user