diff --git a/SCOPE.md b/SCOPE.md index b074eca..0a85233 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -15,7 +15,8 @@ adapting, and implementing pricing models across the product lifecycle. See - Generic pricing core under `adaptive_pricing_core/`, including: canonical pricing schema and validation, boundary evaluation, comparable customer LTV, customer-tuning solver, provider-publication primitives, Stripe - mapping, and governance models. + mapping, governance models, and typed customer assurance claims backed by + tenancy-posture minimums. - Generic framework documentation and research (`INTENT.md`, `docs/`, `research/`, `registry/`). - Project-specific proving grounds under `projects//`, with Coulomb @@ -36,6 +37,12 @@ adapting, and implementing pricing models across the product lifecycle. See they still require supplemental operational or contract logic. - Autonomous customer-visible pricing rollouts or irreversible pricing changes without human approval. +- Treating a tier's assurance wording as authorization, placement state, or a + substitute for current service/provider posture evidence. The tier records + the commercial minimum; operational owners declare and enforce what exists. +- Plan-derived tenant guardrail ceilings until `ADAPTIVE-WP-0010` establishes a + jointly accepted contract with `tenant-engine`. No ceiling schema or feed is + implemented yet. - Project-specific deployment material in generic doc paths (use `projects//`). @@ -44,20 +51,35 @@ adapting, and implementing pricing models across the product lifecycle. See - **Phase:** framework plus executable core. The repo is no longer just docs and research around a single MVP; it now contains reusable pricing-core modules and one concrete deployment. -- **Implemented milestones:** `ADAPTIVE-WP-0003` through `ADAPTIVE-WP-0008` +- **Implemented milestones:** `ADAPTIVE-WP-0003` through `ADAPTIVE-WP-0009` are finished. The repo now has canonical pricing models, explainable boundary validation, comparable-customer LTV simulation, customer-tuning, provider - shadow publication, and governance workflows. + shadow publication, governance workflows, and typed tier assurance claims. - **Generic runtime surface:** reusable implementation lives under - `adaptive_pricing_core/`. + `adaptive_pricing_core/`. Assurance claims are optional and default empty; + when present, the schema validates typed `{I,A,E,P,R,V}` minima, the boundary + engine checks wording and coupled floors, and governance requires one-time + approval when a tier definition adds, changes, or removes a claim. - **Deployment surface:** Coulomb Social remains the first proving ground under `projects/coulomb-pricing/observatory/`, including the dashboard/API, data loaders, tuning pilot, Stripe shadow-publication flow, and governed recommendation surfaces. +- **Current commercial posture:** all current Coulomb pricing models remain + silent on isolation, availability, retention, and performance assurances. + The claim machinery is a preparatory guardrail; it does not invent a live + customer promise. +- **Tenancy posture:** root `tenancy.yaml` declares the observatory as a local, + unauthenticated, single-user design-time tool (`I0/A0/E0`, `P/R: n/a`, + `V0`). Exposing it as a shared service is a new scope decision requiring a + fresh posture assessment before deployment. - **Execution boundary:** Stripe support is currently a provider abstraction and local shadow-state publisher, not live Stripe API management. +- **Next proposed seam:** `ADAPTIVE-WP-0010` explores plan-derived tenant + guardrail ceilings, but its interface and ownership contract with + `tenant-engine` are not yet agreed and no implementation has started. - **Coordination:** State Hub integration (`ADAPTIVE-WP-0001`) remains the repo - workflow backbone for orientation, workplan tracking, and brief sync. + workflow backbone for orientation, workplan tracking, generated + `WORK-RECORDS.md`, and brief sync. ## Getting Oriented @@ -67,6 +89,7 @@ adapting, and implementing pricing models across the product lifecycle. See - Customer tuning: `docs/CustomerTuningSolver.md` - Provider publication: `docs/StripePublication.md` - Governance workflow: `docs/GovernanceWorkflows.md` +- Tenancy posture declaration: `tenancy.yaml` - Implementation roadmap: `docs/ImplementationRoadmap.md` - Agent instructions: `AGENTS.md` - Workplans: `workplans/` diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md new file mode 100644 index 0000000..bb23c55 --- /dev/null +++ b/WORK-RECORDS.md @@ -0,0 +1,67 @@ +# Work Records — adaptive-pricing + +> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record +> stage 3). Do not edit by hand — edit the source file/block listed for +> each record and re-run fix-consistency to refresh this index. Archived +> workplans are omitted; closed decisions/intakes/engagements stay listed +> so recently-resolved work is still visible. [auto] + +| Kind | ID | Status | Lane | Source | +| --- | --- | --- | --- | --- | +| workplan | ADAPTIVE-WP-0001 | finished | — | workplans/ADAPTIVE-WP-0001-statehub-bootstrap.md | +| workplan | ADAPTIVE-WP-0003 | finished | — | workplans/ADAPTIVE-WP-0003-canonical-pricing-core-and-schema.md | +| workplan | ADAPTIVE-WP-0004 | finished | — | workplans/ADAPTIVE-WP-0004-boundary-engine-and-explainable-validation.md | +| workplan | ADAPTIVE-WP-0005 | finished | — | workplans/ADAPTIVE-WP-0005-comparable-customer-ltv-and-simulation-upgrade.md | +| workplan | ADAPTIVE-WP-0006 | finished | — | workplans/ADAPTIVE-WP-0006-customer-tuning-solver-prototype.md | +| workplan | ADAPTIVE-WP-0007 | finished | — | workplans/ADAPTIVE-WP-0007-provider-abstraction-and-stripe-publication.md | +| workplan | ADAPTIVE-WP-0008 | finished | — | workplans/ADAPTIVE-WP-0008-governance-and-recommendation-workflows.md | +| workplan | ADAPTIVE-WP-0009 | finished | — | workplans/ADAPTIVE-WP-0009-tenancy-posture-alignment.md | +| workplan | ADAPTIVE-WP-0010 | proposed | — | workplans/ADAPTIVE-WP-0010-plan-derived-guardrail-ceilings.md | +| workplan | ADHOC-2026-07-02 | finished | — | workplans/ADHOC-2026-07-02.md | +| workplan | ADHOC-2026-07-03 | finished | — | workplans/ADHOC-2026-07-03.md | +| task | ADAPTIVE-WP-0001-T01 | done | — | workplans/ADAPTIVE-WP-0001-statehub-bootstrap.md | +| task | ADAPTIVE-WP-0001-T02 | done | — | workplans/ADAPTIVE-WP-0001-statehub-bootstrap.md | +| task | ADAPTIVE-WP-0001-T03 | done | — | workplans/ADAPTIVE-WP-0001-statehub-bootstrap.md | +| task | ADAPTIVE-WP-0003-T01 | done | — | workplans/ADAPTIVE-WP-0003-canonical-pricing-core-and-schema.md | +| task | ADAPTIVE-WP-0003-T02 | done | — | workplans/ADAPTIVE-WP-0003-canonical-pricing-core-and-schema.md | +| task | ADAPTIVE-WP-0003-T03 | done | — | workplans/ADAPTIVE-WP-0003-canonical-pricing-core-and-schema.md | +| task | ADAPTIVE-WP-0003-T04 | done | — | workplans/ADAPTIVE-WP-0003-canonical-pricing-core-and-schema.md | +| task | ADAPTIVE-WP-0003-T05 | done | — | workplans/ADAPTIVE-WP-0003-canonical-pricing-core-and-schema.md | +| task | ADAPTIVE-WP-0004-T01 | done | — | workplans/ADAPTIVE-WP-0004-boundary-engine-and-explainable-validation.md | +| task | ADAPTIVE-WP-0004-T02 | done | — | workplans/ADAPTIVE-WP-0004-boundary-engine-and-explainable-validation.md | +| task | ADAPTIVE-WP-0004-T03 | done | — | workplans/ADAPTIVE-WP-0004-boundary-engine-and-explainable-validation.md | +| task | ADAPTIVE-WP-0004-T04 | done | — | workplans/ADAPTIVE-WP-0004-boundary-engine-and-explainable-validation.md | +| task | ADAPTIVE-WP-0004-T05 | done | — | workplans/ADAPTIVE-WP-0004-boundary-engine-and-explainable-validation.md | +| task | ADAPTIVE-WP-0005-T01 | done | — | workplans/ADAPTIVE-WP-0005-comparable-customer-ltv-and-simulation-upgrade.md | +| task | ADAPTIVE-WP-0005-T02 | done | — | workplans/ADAPTIVE-WP-0005-comparable-customer-ltv-and-simulation-upgrade.md | +| task | ADAPTIVE-WP-0005-T03 | done | — | workplans/ADAPTIVE-WP-0005-comparable-customer-ltv-and-simulation-upgrade.md | +| task | ADAPTIVE-WP-0005-T04 | done | — | workplans/ADAPTIVE-WP-0005-comparable-customer-ltv-and-simulation-upgrade.md | +| task | ADAPTIVE-WP-0005-T05 | done | — | workplans/ADAPTIVE-WP-0005-comparable-customer-ltv-and-simulation-upgrade.md | +| task | ADAPTIVE-WP-0006-T01 | done | — | workplans/ADAPTIVE-WP-0006-customer-tuning-solver-prototype.md | +| task | ADAPTIVE-WP-0006-T02 | done | — | workplans/ADAPTIVE-WP-0006-customer-tuning-solver-prototype.md | +| task | ADAPTIVE-WP-0006-T03 | done | — | workplans/ADAPTIVE-WP-0006-customer-tuning-solver-prototype.md | +| task | ADAPTIVE-WP-0006-T04 | done | — | workplans/ADAPTIVE-WP-0006-customer-tuning-solver-prototype.md | +| task | ADAPTIVE-WP-0006-T05 | done | — | workplans/ADAPTIVE-WP-0006-customer-tuning-solver-prototype.md | +| task | ADAPTIVE-WP-0007-T01 | done | — | workplans/ADAPTIVE-WP-0007-provider-abstraction-and-stripe-publication.md | +| task | ADAPTIVE-WP-0007-T02 | done | — | workplans/ADAPTIVE-WP-0007-provider-abstraction-and-stripe-publication.md | +| task | ADAPTIVE-WP-0007-T03 | done | — | workplans/ADAPTIVE-WP-0007-provider-abstraction-and-stripe-publication.md | +| task | ADAPTIVE-WP-0007-T04 | done | — | workplans/ADAPTIVE-WP-0007-provider-abstraction-and-stripe-publication.md | +| task | ADAPTIVE-WP-0007-T05 | done | — | workplans/ADAPTIVE-WP-0007-provider-abstraction-and-stripe-publication.md | +| task | ADAPTIVE-WP-0008-T01 | done | — | workplans/ADAPTIVE-WP-0008-governance-and-recommendation-workflows.md | +| task | ADAPTIVE-WP-0008-T02 | done | — | workplans/ADAPTIVE-WP-0008-governance-and-recommendation-workflows.md | +| task | ADAPTIVE-WP-0008-T03 | done | — | workplans/ADAPTIVE-WP-0008-governance-and-recommendation-workflows.md | +| task | ADAPTIVE-WP-0008-T04 | done | — | workplans/ADAPTIVE-WP-0008-governance-and-recommendation-workflows.md | +| task | ADAPTIVE-WP-0008-T05 | done | — | workplans/ADAPTIVE-WP-0008-governance-and-recommendation-workflows.md | +| task | ADAPTIVE-WP-0009-T01 | done | — | workplans/ADAPTIVE-WP-0009-tenancy-posture-alignment.md | +| task | ADAPTIVE-WP-0009-T02 | done | — | workplans/ADAPTIVE-WP-0009-tenancy-posture-alignment.md | +| task | ADAPTIVE-WP-0009-T03 | done | — | workplans/ADAPTIVE-WP-0009-tenancy-posture-alignment.md | +| task | ADAPTIVE-WP-0009-T04 | done | — | workplans/ADAPTIVE-WP-0009-tenancy-posture-alignment.md | +| task | ADAPTIVE-WP-0009-T05 | done | — | workplans/ADAPTIVE-WP-0009-tenancy-posture-alignment.md | +| task | ADAPTIVE-WP-0009-T06 | done | — | workplans/ADAPTIVE-WP-0009-tenancy-posture-alignment.md | +| task | ADAPTIVE-WP-0010-T01 | todo | — | workplans/ADAPTIVE-WP-0010-plan-derived-guardrail-ceilings.md | +| task | ADAPTIVE-WP-0010-T02 | wait | — | workplans/ADAPTIVE-WP-0010-plan-derived-guardrail-ceilings.md | +| task | ADAPTIVE-WP-0010-T03 | wait | — | workplans/ADAPTIVE-WP-0010-plan-derived-guardrail-ceilings.md | +| task | ADAPTIVE-WP-0010-T04 | wait | — | workplans/ADAPTIVE-WP-0010-plan-derived-guardrail-ceilings.md | +| task | ADHOC-2026-07-02-T01 | done | — | workplans/ADHOC-2026-07-02.md | +| task | ADHOC-2026-07-02-T02 | done | — | workplans/ADHOC-2026-07-02.md | +| task | ADHOC-2026-07-03-T01 | done | — | workplans/ADHOC-2026-07-03.md | diff --git a/adaptive_pricing_core/__init__.py b/adaptive_pricing_core/__init__.py index 8119742..f184f6e 100644 --- a/adaptive_pricing_core/__init__.py +++ b/adaptive_pricing_core/__init__.py @@ -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", diff --git a/adaptive_pricing_core/boundary_engine.py b/adaptive_pricing_core/boundary_engine.py index e9e9869..82ffd0c 100644 --- a/adaptive_pricing_core/boundary_engine.py +++ b/adaptive_pricing_core/boundary_engine.py @@ -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), diff --git a/adaptive_pricing_core/governance.py b/adaptive_pricing_core/governance.py index cc7f12b..dd6e2a8 100644 --- a/adaptive_pricing_core/governance.py +++ b/adaptive_pricing_core/governance.py @@ -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"), diff --git a/adaptive_pricing_core/pricing_models.py b/adaptive_pricing_core/pricing_models.py index f1acf6c..1e80af0 100644 --- a/adaptive_pricing_core/pricing_models.py +++ b/adaptive_pricing_core/pricing_models.py @@ -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 ''}'" + 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 diff --git a/docs/GovernanceWorkflows.md b/docs/GovernanceWorkflows.md index d67a342..994fc5e 100644 --- a/docs/GovernanceWorkflows.md +++ b/docs/GovernanceWorkflows.md @@ -1,6 +1,6 @@ # Governance Workflows -Status: MVP for `ADAPTIVE-WP-0008`. +Status: MVP for `ADAPTIVE-WP-0008` and `ADAPTIVE-WP-0009`. ## Purpose @@ -14,6 +14,7 @@ The repository now exposes: - a customer-facing safe-tuning contract surface - pricing health checks - provider-publication audit and revision surfaces +- one-time approval for new or changed tier assurance definitions ## Core And Adapter Layers @@ -42,6 +43,19 @@ The policy model covers: For Coulomb, the current policy keeps customer-visible tuning disabled and requires approval for candidate rollouts and approximate Stripe mappings. +## Tier Assurance Definition Gate + +`assess_tier_definition_assurance()` compares a proposed pricing model with +its previously approved definition. Adding, changing, or removing an +`assurance_claims` entry returns `approval_required`, with the claim's evidence +reference attached to the blocking requirement. An unchanged definition +returns `proceed`. + +This gate runs when a tier definition changes. It is deliberately not a +campaign, publication, or per-customer approval: the approved tier records the +commercial wording and typed tenancy-posture minimum once, while service and +provider owners remain authoritative for current operational posture. + ## Recommendation Workflow Recommendations now include: diff --git a/docs/PricingModelSchema.md b/docs/PricingModelSchema.md index 4916ccd..8f11871 100644 --- a/docs/PricingModelSchema.md +++ b/docs/PricingModelSchema.md @@ -23,6 +23,7 @@ Each pricing model contains: - explicit charge components - commitments - tunable parameters +- optional customer assurance claims with typed tenancy-posture minimums - eligibility and provider hints - free-form metadata for deployment-specific details @@ -73,6 +74,25 @@ tunable_parameters: max_value: decimal | null options: [] +# Optional. Absence means the tier makes no assurance claim. +assurance_claims: + - id: string + kind: isolation | availability | retention | performance + customer_wording: string + minimum_levels: + I: 0..3 + A: 0..4 + E: 0..4 + P: 0..4 + R: 0..4 + V: 0..4 + delivering_service: string + evidence_ref: string + maximum_erasure_horizon_days: integer | null + provider_contract_ref: string | null + resource_governor_ref: string | null + erasure_mechanism: row-deletion | key-destruction | null + eligibility: - string @@ -102,6 +122,18 @@ Current runtime validation enforces: - tunable parameter keys are unique - `customer_tunable` parameters declare bounds or enumerated options - commitment ids are unique +- assurance claim ids are unique and each claim names a supported kind, + customer wording, typed in-range minimums, delivering service, and evidence +- retention horizons are positive integers and erasure mechanisms use the + canonical vocabulary + +`assurance_claims` records the internal minimum that supports a customer-facing +promise; it does not expose the framework ladder on a price page. The boundary +engine checks coupled floors and wording, including E4/P3, retention horizons, +resource-governed performance, and availability failure scope. Adding, +changing, or removing a claim is assessed once at tier definition by +`assess_tier_definition_assurance()`. An unchanged claim is not a per-campaign +approval gate. ## Transitional Compatibility diff --git a/projects/coulomb-pricing/tests/test_boundary_engine.py b/projects/coulomb-pricing/tests/test_boundary_engine.py index 1174b67..e34e408 100644 --- a/projects/coulomb-pricing/tests/test_boundary_engine.py +++ b/projects/coulomb-pricing/tests/test_boundary_engine.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import replace from decimal import Decimal from pathlib import Path @@ -9,6 +10,7 @@ from adaptive_pricing_core.boundary_engine import ( PricingConfiguration, validate_pricing_configuration, ) +from adaptive_pricing_core.pricing_models import AssuranceClaim from observatory.load import load_pricing_models DATA_DIR = Path(__file__).resolve().parent.parent / "data" @@ -18,6 +20,106 @@ def _model(model_id: str): return next(item for item in load_pricing_models(DATA_DIR) if item.id == model_id) +def _configuration_with_claim(claim: AssuranceClaim) -> PricingConfiguration: + return PricingConfiguration( + model=replace(_model("flat-899-eur-monthly"), assurance_claims=(claim,)), + segment="coulomb-social-members", + allocated_fixed_cost=Decimal("1.00"), + payment_fee_rate_pct=Decimal("5"), + ) + + +def _claim(**changes) -> AssuranceClaim: + defaults = { + "id": "isolation-tier", + "kind": "isolation", + "customer_wording": "Other tenants cannot reach your data.", + "minimum_levels": {"E": 4, "P": 3}, + "delivering_service": "tenant-engine", + "evidence_ref": "evidence/isolation-review.json", + } + defaults.update(changes) + return AssuranceClaim(**defaults) + + +def test_valid_assurance_claim_passes_with_explainable_result() -> None: + result = validate_pricing_configuration( + _configuration_with_claim(_claim()), + BoundaryPolicy(), + ) + + assurance = next(item for item in result.constraints if item.id == "assurance-claims") + assert assurance.status == "pass" + assert assurance.details["claim_ids"] == ["isolation-tier"] + + +def test_isolation_wording_requires_e4_and_coupled_p3() -> None: + result = validate_pricing_configuration( + _configuration_with_claim(_claim(minimum_levels={"E": 3, "P": 2})), + BoundaryPolicy(), + ) + + assurance = next(item for item in result.constraints if item.id == "assurance-claims") + assert assurance.status == "fail" + assert "E §11.4" in assurance.reason + + +def test_retention_claim_requires_bounded_shared_horizon() -> None: + result = validate_pricing_configuration( + _configuration_with_claim( + _claim( + id="retention-tier", + kind="retention", + customer_wording="Deleted data is gone.", + minimum_levels={"R": 3, "P": 1}, + ) + ), + BoundaryPolicy(), + ) + + assurance = next(item for item in result.constraints if item.id == "assurance-claims") + assert assurance.status == "fail" + assert "R §4.5.4/§11.1" in assurance.reason + assert "P §4.5.4" in assurance.reason + assert "R §11.4" in assurance.reason + + +def test_performance_claim_requires_p2_or_resource_governor() -> None: + result = validate_pricing_configuration( + _configuration_with_claim( + _claim( + id="performance-tier", + kind="performance", + customer_wording="Predictable performance under neighbour load.", + minimum_levels={"P": 1}, + ) + ), + BoundaryPolicy(), + ) + + assurance = next(item for item in result.constraints if item.id == "assurance-claims") + assert assurance.status == "fail" + assert "P §8.3.1/§11.6" in assurance.reason + + +def test_availability_wording_requires_matching_failure_scope() -> None: + result = validate_pricing_configuration( + _configuration_with_claim( + _claim( + id="availability-tier", + kind="availability", + customer_wording="High availability survives regional loss.", + minimum_levels={"V": 3}, + ) + ), + BoundaryPolicy(), + ) + + assurance = next(item for item in result.constraints if item.id == "assurance-claims") + assert assurance.status == "fail" + assert "V §11.4" in assurance.reason + + def test_commitment_backed_discount_is_accepted_when_economics_stay_strong() -> None: result = validate_pricing_configuration( PricingConfiguration( diff --git a/projects/coulomb-pricing/tests/test_governance.py b/projects/coulomb-pricing/tests/test_governance.py index d5830c4..bdf44ae 100644 --- a/projects/coulomb-pricing/tests/test_governance.py +++ b/projects/coulomb-pricing/tests/test_governance.py @@ -1,8 +1,12 @@ from __future__ import annotations +from dataclasses import replace from pathlib import Path +from adaptive_pricing_core.governance import assess_tier_definition_assurance +from adaptive_pricing_core.pricing_models import AssuranceClaim from observatory.api import build_dashboard_payload +from observatory.load import load_pricing_models DATA_DIR = Path(__file__).resolve().parent.parent / "data" @@ -46,3 +50,30 @@ def test_recommendations_include_governed_execution_gate() -> None: assert execution_gate["confidence"] == "0.88" assert execution_gate["risks"] assert execution_gate["supporting_observations"] + + +def test_changed_assurance_claim_requires_one_time_definition_approval() -> None: + previous = next( + item for item in load_pricing_models(DATA_DIR) if item.id == "flat-899-eur-monthly" + ) + current = replace( + previous, + assurance_claims=( + AssuranceClaim( + id="availability-tier", + kind="availability", + customer_wording="Survives a zone loss.", + minimum_levels={"V": 3}, + delivering_service="railiance-platform", + evidence_ref="evidence/zone-failover.json", + ), + ), + ) + + assessment = assess_tier_definition_assurance(current, previous) + unchanged = assess_tier_definition_assurance(current, current) + + assert assessment.decision == "approval_required" + assert assessment.approvals[0].evidence_refs == ("evidence/zone-failover.json",) + assert unchanged.decision == "proceed" + assert unchanged.approvals == () diff --git a/projects/coulomb-pricing/tests/test_pricing_model_schema.py b/projects/coulomb-pricing/tests/test_pricing_model_schema.py index f499590..3340250 100644 --- a/projects/coulomb-pricing/tests/test_pricing_model_schema.py +++ b/projects/coulomb-pricing/tests/test_pricing_model_schema.py @@ -1,9 +1,16 @@ from __future__ import annotations +import json +from dataclasses import replace from decimal import Decimal from pathlib import Path -from adaptive_pricing_core.pricing_models import validate_pricing_catalog +from adaptive_pricing_core.pricing_models import ( + AssuranceClaim, + load_pricing_models as load_core_pricing_models, + validate_pricing_catalog, + validate_pricing_model, +) from observatory.load import load_pricing_models DATA_DIR = Path(__file__).resolve().parent.parent / "data" @@ -33,3 +40,55 @@ def test_flat_model_still_exposes_access_fee_compatibility_fields() -> None: assert model.access_fee_amount == Decimal("8.99") assert model.access_fee_cadence == "monthly" assert len(model.charge_components) == 1 + + +def test_assurance_claim_schema_is_optional_and_typed() -> None: + model = next( + item for item in load_pricing_models(DATA_DIR) if item.id == "flat-899-eur-monthly" + ) + assert model.assurance_claims == () + + invalid = replace( + model, + assurance_claims=( + AssuranceClaim( + id="retention", + kind="retention", + customer_wording="Deleted data is gone.", + minimum_levels={"R": True, "P": 1}, + delivering_service="", + evidence_ref="", + maximum_erasure_horizon_days=0, + erasure_mechanism="magic", + ), + ), + ) + issues = validate_pricing_model(invalid) + + assert any("invalid R level" in issue for issue in issues) + assert any("requires delivering_service" in issue for issue in issues) + assert any("requires evidence_ref" in issue for issue in issues) + assert any("invalid maximum erasure horizon" in issue for issue in issues) + assert any("unsupported erasure_mechanism" in issue for issue in issues) + + +def test_assurance_claim_loads_from_json_catalog(tmp_path: Path) -> None: + catalog = json.loads((DATA_DIR / "pricing-models.json").read_text(encoding="utf-8")) + catalog["models"][0]["assurance_claims"] = [ + { + "id": "availability-tier", + "kind": "availability", + "customer_wording": "Survives a zone loss.", + "minimum_levels": {"V": "3"}, + "delivering_service": "railiance-platform", + "evidence_ref": "evidence/zone-failover.json", + } + ] + catalog_path = tmp_path / "pricing-models.json" + catalog_path.write_text(json.dumps(catalog), encoding="utf-8") + + models = load_core_pricing_models(catalog_path) + claim = models[0].assurance_claims[0] + + assert claim.id == "availability-tier" + assert claim.minimum_levels == {"V": 3} diff --git a/tenancy.yaml b/tenancy.yaml new file mode 100644 index 0000000..fc787a2 --- /dev/null +++ b/tenancy.yaml @@ -0,0 +1,36 @@ +# Canonical declaration: NetKingdom Tenancy Posture v0.1 draft-8. +schema_version: "0.1" +framework: netkingdom-tenancy-posture +service: coulomb-observatory +role: design-time-pricing-analysis + +tenancy: + current: { I: 0, A: 0, E: 0, P: "n/a", R: "n/a", V: 0 } + target: { I: 0, A: 0, E: 0, P: "n/a", R: "n/a", V: 0 } + reviewed: "2026-08-17" + review_due: "2027-02-17" + service_class: interactive + permanent: [I, A, E, P, R, V] + reason: + I: >- + The observatory's local HTTP surface is unauthenticated and single-user + by design. It is not a production tenant service. + A: >- + No authorization boundary exists because the local surface is not + exposed or supported as a shared service. + E: >- + No runtime tenant boundary exists. Project JSON is local proving-ground + data and must not be mistaken for a multi-tenant datastore. + P: >- + No runtime datastore exists; repository JSON is source material, not a + provisioned substrate. + R: >- + No runtime data is retained. Repository history follows source-control + governance rather than a service erasure policy. + V: >- + No availability position exists for a local design-time tool. + gap: + I: >- + Permanent only while the server remains local and single-user. Any + proposal to expose it creates a new service and requires reassessment + before deployment. diff --git a/workplans/ADAPTIVE-WP-0010-plan-derived-guardrail-ceilings.md b/workplans/ADAPTIVE-WP-0010-plan-derived-guardrail-ceilings.md index c96785b..30ed8b8 100644 --- a/workplans/ADAPTIVE-WP-0010-plan-derived-guardrail-ceilings.md +++ b/workplans/ADAPTIVE-WP-0010-plan-derived-guardrail-ceilings.md @@ -9,6 +9,7 @@ owner: codex topic_slug: helix-forge created: "2026-08-18" updated: "2026-08-18" +state_hub_workstream_id: "38613963-4455-4e0c-849f-65fdda68f572" --- # Plan-derived guardrail spend ceilings @@ -67,6 +68,7 @@ T01 produces a shape both repos accept. id: ADAPTIVE-WP-0010-T01 status: todo priority: high +state_hub_task_id: "7109ccf7-1267-44b6-9f20-38eff7bb7dd2" ``` Agree the shape of the feed jointly with `tenant-engine`: what @@ -92,6 +94,7 @@ Record the outcome as a decision, since it binds two repos. id: ADAPTIVE-WP-0010-T02 status: wait priority: medium +state_hub_task_id: "7da0b8f5-93a7-4048-900d-8fa39eb0de96" ``` Blocked on T01. @@ -110,6 +113,7 @@ if the field is named or shaped like an allowance, it will be read as one. id: ADAPTIVE-WP-0010-T03 status: wait priority: medium +state_hub_task_id: "82788fbd-e0e7-4bb0-94ca-405176e74809" ``` Blocked on T02. @@ -127,6 +131,7 @@ Boundary-engine rules, explainable in the existing style: id: ADAPTIVE-WP-0010-T04 status: wait priority: medium +state_hub_task_id: "c9220137-baed-4ba1-929c-449ba1a754b9" ``` Blocked on T02.