feat: add typed tier assurance guardrails
This commit is contained in:
parent
3d0f614f49
commit
c65a2f1ff9
13 changed files with 727 additions and 7 deletions
|
|
@ -21,6 +21,7 @@ from .comparable_ltv import (
|
|||
select_reference_estimate,
|
||||
)
|
||||
from .pricing_models import (
|
||||
AssuranceClaim,
|
||||
ChargeComponent,
|
||||
Commitment,
|
||||
PricingModel,
|
||||
|
|
@ -33,6 +34,7 @@ from .pricing_models import (
|
|||
|
||||
__all__ = [
|
||||
"BoundaryPolicy",
|
||||
"AssuranceClaim",
|
||||
"ChargeComponent",
|
||||
"ComparableCustomerProfile",
|
||||
"ComparableLTVEstimate",
|
||||
|
|
|
|||
|
|
@ -852,8 +852,171 @@ def _commitment_backed_concession(
|
|||
)
|
||||
|
||||
|
||||
def _assurance_claims(
|
||||
configuration: PricingConfiguration,
|
||||
_policy: BoundaryPolicy,
|
||||
_metrics: PricingMetrics,
|
||||
_baseline: PricingMetrics,
|
||||
) -> ConstraintResult:
|
||||
claims = configuration.model.assurance_claims
|
||||
if not claims:
|
||||
return ConstraintResult(
|
||||
id="assurance-claims",
|
||||
title="Tier assurance claims",
|
||||
severity="hard",
|
||||
status="pass",
|
||||
summary="The tier makes no customer assurance claim.",
|
||||
reason="Silent tiers are unaffected by Tenancy Posture §11.",
|
||||
actual_value=0,
|
||||
threshold_value=0,
|
||||
unit="claims",
|
||||
)
|
||||
|
||||
violations: list[dict[str, str]] = []
|
||||
|
||||
def fail(claim_id: str, axis: str, section: str, reason: str) -> None:
|
||||
violations.append(
|
||||
{"claim_id": claim_id, "axis": axis, "section": section, "reason": reason}
|
||||
)
|
||||
|
||||
for claim in claims:
|
||||
levels = claim.minimum_levels
|
||||
wording = " ".join(claim.customer_wording.lower().split())
|
||||
if not levels:
|
||||
fail(claim.id, "all", "§11.1/§11.3", "claim has no typed minimum levels")
|
||||
if not claim.delivering_service.strip():
|
||||
fail(claim.id, "all", "§11.3", "delivering service is not named")
|
||||
if not claim.evidence_ref.strip():
|
||||
fail(claim.id, "all", "§11.3", "evidence reference is missing")
|
||||
|
||||
required_axis = {
|
||||
"isolation": "E",
|
||||
"availability": "V",
|
||||
"retention": "R",
|
||||
"performance": "P",
|
||||
}.get(claim.kind)
|
||||
if required_axis and required_axis not in levels:
|
||||
fail(
|
||||
claim.id,
|
||||
required_axis,
|
||||
"§11.1/§11.3",
|
||||
f"{claim.kind} claim does not record a {required_axis} minimum",
|
||||
)
|
||||
|
||||
if claim.kind == "isolation":
|
||||
if "cannot reach" in wording and levels.get("E", -1) < 4:
|
||||
fail(
|
||||
claim.id,
|
||||
"E",
|
||||
"§11.4",
|
||||
"'cannot reach' wording requires E4",
|
||||
)
|
||||
if levels.get("E", -1) >= 4 and levels.get("P", -1) < 3:
|
||||
fail(claim.id, "P", "§3.2", "E4 is reachable only at P3 or above")
|
||||
|
||||
if claim.kind == "retention":
|
||||
horizon = claim.maximum_erasure_horizon_days
|
||||
if horizon is None:
|
||||
fail(
|
||||
claim.id,
|
||||
"R",
|
||||
"§4.5.4/§11.1",
|
||||
"retention claim has no maximum erasure horizon",
|
||||
)
|
||||
if levels.get("P", -1) < 2 and not claim.provider_contract_ref:
|
||||
fail(
|
||||
claim.id,
|
||||
"P",
|
||||
"§4.5.4",
|
||||
"retention promise below P2 needs a provider contract that bounds the shared horizon",
|
||||
)
|
||||
deleted_is_gone = "deleted" in wording and (
|
||||
" is gone" in wording or " are gone" in wording or "permanently removed" in wording
|
||||
)
|
||||
horizon_disclosed = horizon is not None and str(horizon) in wording and "day" in wording
|
||||
if deleted_is_gone and levels.get("R", -1) < 4 and not horizon_disclosed:
|
||||
fail(
|
||||
claim.id,
|
||||
"R",
|
||||
"§11.4",
|
||||
"'deleted data is gone' requires R4 or the erasure horizon in customer wording",
|
||||
)
|
||||
regulatory_language = any(
|
||||
phrase in wording
|
||||
for phrase in ("regulator approved", "regulator-endorsed", "article 17 compliant")
|
||||
)
|
||||
if (
|
||||
levels.get("R", -1) >= 4
|
||||
and claim.erasure_mechanism == "key-destruction"
|
||||
and regulatory_language
|
||||
):
|
||||
fail(
|
||||
claim.id,
|
||||
"R/E",
|
||||
"§4.5/§11.4",
|
||||
"key-destruction wording may not imply regulatory endorsement",
|
||||
)
|
||||
|
||||
if claim.kind == "performance" and levels.get("P", -1) < 2:
|
||||
if not claim.resource_governor_ref:
|
||||
fail(
|
||||
claim.id,
|
||||
"P",
|
||||
"§8.3.1/§11.6",
|
||||
"performance-differentiated tier requires P2 or an enforceable resource governor",
|
||||
)
|
||||
|
||||
if claim.kind == "availability":
|
||||
if "zone" in wording and levels.get("V", -1) < 3:
|
||||
fail(claim.id, "V", "§11.4", "zone-loss wording requires V3")
|
||||
if ("region" in wording or "regional loss" in wording) and levels.get("V", -1) < 4:
|
||||
fail(claim.id, "V", "§11.4", "regional-loss wording requires V4")
|
||||
if "high availability" in wording and not any(
|
||||
term in wording for term in ("restart", "node", "zone", "region")
|
||||
):
|
||||
fail(
|
||||
claim.id,
|
||||
"V",
|
||||
"§11.4",
|
||||
"'high availability' must name the failure it survives",
|
||||
)
|
||||
|
||||
if violations:
|
||||
axes = ", ".join(sorted({item["axis"] for item in violations}))
|
||||
return ConstraintResult(
|
||||
id="assurance-claims",
|
||||
title="Tier assurance claims",
|
||||
severity="hard",
|
||||
status="fail",
|
||||
summary=f"Assurance claims violate typed minima on axis/axes {axes}.",
|
||||
reason="; ".join(
|
||||
f"{item['claim_id']} [{item['axis']} {item['section']}]: {item['reason']}"
|
||||
for item in violations
|
||||
),
|
||||
actual_value=len(violations),
|
||||
threshold_value=0,
|
||||
unit="violations",
|
||||
details={"violations": violations},
|
||||
suggested_action="Raise the typed minima, narrow the customer wording, or attach the required provider control.",
|
||||
)
|
||||
|
||||
return ConstraintResult(
|
||||
id="assurance-claims",
|
||||
title="Tier assurance claims",
|
||||
severity="hard",
|
||||
status="pass",
|
||||
summary=f"All {len(claims)} assurance claim(s) map to explicit framework minima.",
|
||||
reason="Customer wording, delivering service, evidence and coupled axis floors are recorded.",
|
||||
actual_value=len(claims),
|
||||
threshold_value=len(claims),
|
||||
unit="claims",
|
||||
details={"claim_ids": [claim.id for claim in claims]},
|
||||
)
|
||||
|
||||
|
||||
def default_constraints() -> tuple[BoundaryConstraint, ...]:
|
||||
return (
|
||||
BoundaryConstraint("assurance-claims", "Tier assurance claims", "hard", _assurance_claims),
|
||||
BoundaryConstraint("segment-eligibility", "Segment eligibility", "hard", _segment_eligibility),
|
||||
BoundaryConstraint("usage-variance-limit", "Usage variance limit", "hard", _usage_variance_limit),
|
||||
BoundaryConstraint("payment-fee-limit", "Payment fee limit", "hard", _payment_fee_limit),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ from dataclasses import dataclass, field
|
|||
from decimal import Decimal
|
||||
from typing import Any, Literal
|
||||
|
||||
from .pricing_models import PricingModel
|
||||
|
||||
GovernanceDecision = Literal["proceed", "approval_required", "blocked"]
|
||||
RecommendationType = Literal["research", "simulation", "model_change", "execution"]
|
||||
RecommendationPriority = Literal["high", "medium", "low"]
|
||||
|
|
@ -46,6 +48,7 @@ class ApprovalRequirement:
|
|||
approver_role: str
|
||||
reason: str
|
||||
blocking: bool = True
|
||||
evidence_refs: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -138,6 +141,95 @@ class SafeTuningContract:
|
|||
notes: tuple[str, ...] = ()
|
||||
|
||||
|
||||
def assurance_claim_approval_requirements(
|
||||
model: PricingModel,
|
||||
previous_model: PricingModel | None,
|
||||
*,
|
||||
approver_role: str = "operator",
|
||||
) -> tuple[ApprovalRequirement, ...]:
|
||||
"""Return one-time approval gates for assurance changes at tier definition.
|
||||
|
||||
Callers must pass the previously approved definition. Publication and
|
||||
campaign workflows must not call this for an unchanged model: Tenancy
|
||||
Posture section 11.3 records the approval once when a tier is defined.
|
||||
"""
|
||||
|
||||
current = {claim.id: claim for claim in model.assurance_claims}
|
||||
previous = {
|
||||
claim.id: claim
|
||||
for claim in (previous_model.assurance_claims if previous_model is not None else ())
|
||||
}
|
||||
changed_ids = sorted(
|
||||
claim_id
|
||||
for claim_id in current.keys() | previous.keys()
|
||||
if current.get(claim_id) != previous.get(claim_id)
|
||||
)
|
||||
|
||||
requirements: list[ApprovalRequirement] = []
|
||||
for claim_id in changed_ids:
|
||||
claim = current.get(claim_id)
|
||||
prior_claim = previous.get(claim_id)
|
||||
evidence_ref = (claim or prior_claim).evidence_ref
|
||||
change = "removed" if claim is None else "added" if prior_claim is None else "changed"
|
||||
requirements.append(
|
||||
ApprovalRequirement(
|
||||
id=f"assurance-claim-{claim_id}-approval",
|
||||
title=f"Assurance claim '{claim_id}' approval",
|
||||
approver_role=approver_role,
|
||||
reason=(
|
||||
f"Tier '{model.id}' {change} customer assurance claim '{claim_id}'. "
|
||||
f"Review its typed minimums and evidence before the tier definition is accepted."
|
||||
),
|
||||
evidence_refs=(evidence_ref,),
|
||||
)
|
||||
)
|
||||
return tuple(requirements)
|
||||
|
||||
|
||||
def assess_tier_definition_assurance(
|
||||
model: PricingModel,
|
||||
previous_model: PricingModel | None,
|
||||
*,
|
||||
approver_role: str = "operator",
|
||||
) -> GovernanceAssessment:
|
||||
approvals = assurance_claim_approval_requirements(
|
||||
model,
|
||||
previous_model,
|
||||
approver_role=approver_role,
|
||||
)
|
||||
if approvals:
|
||||
return GovernanceAssessment(
|
||||
decision="approval_required",
|
||||
summary="Approval required for a changed customer assurance claim.",
|
||||
approvals=approvals,
|
||||
risks=(
|
||||
GovernanceRisk(
|
||||
id="unapproved-assurance-change",
|
||||
severity="high",
|
||||
summary="The tier definition changes a customer-visible assurance obligation.",
|
||||
mitigation="Validate the typed minima, inspect the attached evidence, and record human approval once for this tier revision.",
|
||||
),
|
||||
),
|
||||
supporting_observations=tuple(
|
||||
SupportingObservation(
|
||||
id=f"{approval.id}-evidence",
|
||||
title="Assurance evidence",
|
||||
summary="Evidence attached to the tier-definition approval gate.",
|
||||
source_ref=approval.evidence_refs[0],
|
||||
)
|
||||
for approval in approvals
|
||||
),
|
||||
notes=("Do not reuse this definition-time gate as a per-campaign approval.",),
|
||||
)
|
||||
return GovernanceAssessment(
|
||||
decision="proceed",
|
||||
summary="No assurance claim changed since the approved tier definition.",
|
||||
approvals=(),
|
||||
risks=(),
|
||||
supporting_observations=(),
|
||||
)
|
||||
|
||||
|
||||
def governance_policy_from_dict(raw: dict[str, Any]) -> GovernancePolicy:
|
||||
return GovernancePolicy(
|
||||
policy_id=raw.get("policy_id", "default-governance-policy"),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from pathlib import Path
|
|||
from typing import Any, Literal
|
||||
|
||||
PricingModelStatus = Literal["active", "candidate", "retired"]
|
||||
AssuranceClaimKind = Literal["isolation", "availability", "retention", "performance"]
|
||||
ChargeComponentKind = Literal[
|
||||
"access",
|
||||
"setup",
|
||||
|
|
@ -40,6 +41,9 @@ _ALLOWED_PARAMETER_CLASSES = {
|
|||
"constrained",
|
||||
"provider",
|
||||
}
|
||||
_ALLOWED_ASSURANCE_KINDS = {"isolation", "availability", "retention", "performance"}
|
||||
_AXIS_MAXIMUMS = {"I": 3, "A": 4, "E": 4, "P": 4, "R": 4, "V": 4}
|
||||
_ALLOWED_ERASURE_MECHANISMS = {"row-deletion", "key-destruction"}
|
||||
|
||||
|
||||
def _money(value: str | int | float | Decimal | None) -> Decimal | None:
|
||||
|
|
@ -88,6 +92,20 @@ class TunableParameter:
|
|||
options: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AssuranceClaim:
|
||||
id: str
|
||||
kind: AssuranceClaimKind | str
|
||||
customer_wording: str
|
||||
minimum_levels: dict[str, int]
|
||||
delivering_service: str
|
||||
evidence_ref: str
|
||||
maximum_erasure_horizon_days: int | None = None
|
||||
provider_contract_ref: str | None = None
|
||||
resource_governor_ref: str | None = None
|
||||
erasure_mechanism: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PricingModel:
|
||||
id: str
|
||||
|
|
@ -104,6 +122,7 @@ class PricingModel:
|
|||
charge_components: tuple[ChargeComponent, ...] = ()
|
||||
commitments: tuple[Commitment, ...] = ()
|
||||
tunable_parameters: tuple[TunableParameter, ...] = ()
|
||||
assurance_claims: tuple[AssuranceClaim, ...] = ()
|
||||
eligibility: tuple[str, ...] = ()
|
||||
provider_hints: dict[str, Any] = field(default_factory=dict)
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
|
@ -152,6 +171,36 @@ def _parse_tunable_parameter(raw: dict[str, Any]) -> TunableParameter:
|
|||
)
|
||||
|
||||
|
||||
def _parse_assurance_claim(raw: dict[str, Any]) -> AssuranceClaim:
|
||||
def integer_or_raw(value: Any) -> Any:
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return value
|
||||
return value
|
||||
|
||||
return AssuranceClaim(
|
||||
id=str(raw.get("id", "")),
|
||||
kind=str(raw.get("kind", "")),
|
||||
customer_wording=str(raw.get("customer_wording", "")),
|
||||
minimum_levels={
|
||||
str(axis): integer_or_raw(level)
|
||||
for axis, level in raw.get("minimum_levels", {}).items()
|
||||
},
|
||||
delivering_service=str(raw.get("delivering_service", "")),
|
||||
evidence_ref=str(raw.get("evidence_ref", "")),
|
||||
maximum_erasure_horizon_days=(
|
||||
integer_or_raw(raw["maximum_erasure_horizon_days"])
|
||||
if raw.get("maximum_erasure_horizon_days") is not None
|
||||
else None
|
||||
),
|
||||
provider_contract_ref=raw.get("provider_contract_ref"),
|
||||
resource_governor_ref=raw.get("resource_governor_ref"),
|
||||
erasure_mechanism=raw.get("erasure_mechanism"),
|
||||
)
|
||||
|
||||
|
||||
def _legacy_charge_components(raw: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
components: list[dict[str, Any]] = [
|
||||
{
|
||||
|
|
@ -233,6 +282,9 @@ def _parse_pricing_model(raw: dict[str, Any]) -> PricingModel:
|
|||
tunable_parameters=tuple(
|
||||
_parse_tunable_parameter(item) for item in raw.get("tunable_parameters", [])
|
||||
),
|
||||
assurance_claims=tuple(
|
||||
_parse_assurance_claim(item) for item in raw.get("assurance_claims", [])
|
||||
),
|
||||
eligibility=tuple(str(item) for item in raw.get("eligibility", [])),
|
||||
provider_hints=_tuple_dict(raw.get("provider_hints")),
|
||||
metadata=metadata,
|
||||
|
|
@ -320,4 +372,46 @@ def validate_pricing_model(model: PricingModel) -> list[str]:
|
|||
if len(commitment_ids) != len(set(commitment_ids)):
|
||||
issues.append("commitment ids must be unique")
|
||||
|
||||
claim_ids = [claim.id for claim in model.assurance_claims]
|
||||
if len(claim_ids) != len(set(claim_ids)):
|
||||
issues.append("assurance claim ids must be unique")
|
||||
for claim in model.assurance_claims:
|
||||
prefix = f"assurance claim '{claim.id or '<missing>'}'"
|
||||
if not claim.id.strip():
|
||||
issues.append("assurance claim id is required")
|
||||
if claim.kind not in _ALLOWED_ASSURANCE_KINDS:
|
||||
issues.append(f"{prefix} has unsupported kind '{claim.kind}'")
|
||||
for field_name, value in (
|
||||
("customer_wording", claim.customer_wording),
|
||||
("delivering_service", claim.delivering_service),
|
||||
("evidence_ref", claim.evidence_ref),
|
||||
):
|
||||
if not value.strip():
|
||||
issues.append(f"{prefix} requires {field_name}")
|
||||
if not claim.minimum_levels:
|
||||
issues.append(f"{prefix} requires minimum_levels")
|
||||
for axis, level in claim.minimum_levels.items():
|
||||
if axis not in _AXIS_MAXIMUMS:
|
||||
issues.append(f"{prefix} has unknown axis '{axis}'")
|
||||
elif (
|
||||
isinstance(level, bool)
|
||||
or not isinstance(level, int)
|
||||
or not 0 <= level <= _AXIS_MAXIMUMS[axis]
|
||||
):
|
||||
issues.append(f"{prefix} has invalid {axis} level {level!r}")
|
||||
if (
|
||||
claim.maximum_erasure_horizon_days is not None
|
||||
and (
|
||||
isinstance(claim.maximum_erasure_horizon_days, bool)
|
||||
or not isinstance(claim.maximum_erasure_horizon_days, int)
|
||||
or claim.maximum_erasure_horizon_days < 1
|
||||
)
|
||||
):
|
||||
issues.append(f"{prefix} has an invalid maximum erasure horizon")
|
||||
if (
|
||||
claim.erasure_mechanism is not None
|
||||
and claim.erasure_mechanism not in _ALLOWED_ERASURE_MECHANISMS
|
||||
):
|
||||
issues.append(f"{prefix} has unsupported erasure_mechanism")
|
||||
|
||||
return issues
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue