feat: add typed tier assurance guardrails
This commit is contained in:
parent
3d0f614f49
commit
c65a2f1ff9
13 changed files with 727 additions and 7 deletions
33
SCOPE.md
33
SCOPE.md
|
|
@ -15,7 +15,8 @@ adapting, and implementing pricing models across the product lifecycle. See
|
||||||
- Generic pricing core under `adaptive_pricing_core/`, including:
|
- Generic pricing core under `adaptive_pricing_core/`, including:
|
||||||
canonical pricing schema and validation, boundary evaluation, comparable
|
canonical pricing schema and validation, boundary evaluation, comparable
|
||||||
customer LTV, customer-tuning solver, provider-publication primitives, Stripe
|
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/`,
|
- Generic framework documentation and research (`INTENT.md`, `docs/`,
|
||||||
`research/`, `registry/`).
|
`research/`, `registry/`).
|
||||||
- Project-specific proving grounds under `projects/<slug>/`, with Coulomb
|
- Project-specific proving grounds under `projects/<slug>/`, with Coulomb
|
||||||
|
|
@ -36,6 +37,12 @@ adapting, and implementing pricing models across the product lifecycle. See
|
||||||
they still require supplemental operational or contract logic.
|
they still require supplemental operational or contract logic.
|
||||||
- Autonomous customer-visible pricing rollouts or irreversible pricing changes
|
- Autonomous customer-visible pricing rollouts or irreversible pricing changes
|
||||||
without human approval.
|
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
|
- Project-specific deployment material in generic doc paths (use
|
||||||
`projects/<slug>/`).
|
`projects/<slug>/`).
|
||||||
|
|
||||||
|
|
@ -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
|
- **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
|
research around a single MVP; it now contains reusable pricing-core modules
|
||||||
and one concrete deployment.
|
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
|
are finished. The repo now has canonical pricing models, explainable boundary
|
||||||
validation, comparable-customer LTV simulation, customer-tuning, provider
|
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
|
- **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
|
- **Deployment surface:** Coulomb Social remains the first proving ground under
|
||||||
`projects/coulomb-pricing/observatory/`, including the dashboard/API, data
|
`projects/coulomb-pricing/observatory/`, including the dashboard/API, data
|
||||||
loaders, tuning pilot, Stripe shadow-publication flow, and governed
|
loaders, tuning pilot, Stripe shadow-publication flow, and governed
|
||||||
recommendation surfaces.
|
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
|
- **Execution boundary:** Stripe support is currently a provider abstraction and
|
||||||
local shadow-state publisher, not live Stripe API management.
|
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
|
- **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
|
## Getting Oriented
|
||||||
|
|
||||||
|
|
@ -67,6 +89,7 @@ adapting, and implementing pricing models across the product lifecycle. See
|
||||||
- Customer tuning: `docs/CustomerTuningSolver.md`
|
- Customer tuning: `docs/CustomerTuningSolver.md`
|
||||||
- Provider publication: `docs/StripePublication.md`
|
- Provider publication: `docs/StripePublication.md`
|
||||||
- Governance workflow: `docs/GovernanceWorkflows.md`
|
- Governance workflow: `docs/GovernanceWorkflows.md`
|
||||||
|
- Tenancy posture declaration: `tenancy.yaml`
|
||||||
- Implementation roadmap: `docs/ImplementationRoadmap.md`
|
- Implementation roadmap: `docs/ImplementationRoadmap.md`
|
||||||
- Agent instructions: `AGENTS.md`
|
- Agent instructions: `AGENTS.md`
|
||||||
- Workplans: `workplans/`
|
- Workplans: `workplans/`
|
||||||
|
|
|
||||||
67
WORK-RECORDS.md
Normal file
67
WORK-RECORDS.md
Normal file
|
|
@ -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 |
|
||||||
|
|
@ -21,6 +21,7 @@ from .comparable_ltv import (
|
||||||
select_reference_estimate,
|
select_reference_estimate,
|
||||||
)
|
)
|
||||||
from .pricing_models import (
|
from .pricing_models import (
|
||||||
|
AssuranceClaim,
|
||||||
ChargeComponent,
|
ChargeComponent,
|
||||||
Commitment,
|
Commitment,
|
||||||
PricingModel,
|
PricingModel,
|
||||||
|
|
@ -33,6 +34,7 @@ from .pricing_models import (
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BoundaryPolicy",
|
"BoundaryPolicy",
|
||||||
|
"AssuranceClaim",
|
||||||
"ChargeComponent",
|
"ChargeComponent",
|
||||||
"ComparableCustomerProfile",
|
"ComparableCustomerProfile",
|
||||||
"ComparableLTVEstimate",
|
"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, ...]:
|
def default_constraints() -> tuple[BoundaryConstraint, ...]:
|
||||||
return (
|
return (
|
||||||
|
BoundaryConstraint("assurance-claims", "Tier assurance claims", "hard", _assurance_claims),
|
||||||
BoundaryConstraint("segment-eligibility", "Segment eligibility", "hard", _segment_eligibility),
|
BoundaryConstraint("segment-eligibility", "Segment eligibility", "hard", _segment_eligibility),
|
||||||
BoundaryConstraint("usage-variance-limit", "Usage variance limit", "hard", _usage_variance_limit),
|
BoundaryConstraint("usage-variance-limit", "Usage variance limit", "hard", _usage_variance_limit),
|
||||||
BoundaryConstraint("payment-fee-limit", "Payment fee limit", "hard", _payment_fee_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 decimal import Decimal
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from .pricing_models import PricingModel
|
||||||
|
|
||||||
GovernanceDecision = Literal["proceed", "approval_required", "blocked"]
|
GovernanceDecision = Literal["proceed", "approval_required", "blocked"]
|
||||||
RecommendationType = Literal["research", "simulation", "model_change", "execution"]
|
RecommendationType = Literal["research", "simulation", "model_change", "execution"]
|
||||||
RecommendationPriority = Literal["high", "medium", "low"]
|
RecommendationPriority = Literal["high", "medium", "low"]
|
||||||
|
|
@ -46,6 +48,7 @@ class ApprovalRequirement:
|
||||||
approver_role: str
|
approver_role: str
|
||||||
reason: str
|
reason: str
|
||||||
blocking: bool = True
|
blocking: bool = True
|
||||||
|
evidence_refs: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
@ -138,6 +141,95 @@ class SafeTuningContract:
|
||||||
notes: tuple[str, ...] = ()
|
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:
|
def governance_policy_from_dict(raw: dict[str, Any]) -> GovernancePolicy:
|
||||||
return GovernancePolicy(
|
return GovernancePolicy(
|
||||||
policy_id=raw.get("policy_id", "default-governance-policy"),
|
policy_id=raw.get("policy_id", "default-governance-policy"),
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from pathlib import Path
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
PricingModelStatus = Literal["active", "candidate", "retired"]
|
PricingModelStatus = Literal["active", "candidate", "retired"]
|
||||||
|
AssuranceClaimKind = Literal["isolation", "availability", "retention", "performance"]
|
||||||
ChargeComponentKind = Literal[
|
ChargeComponentKind = Literal[
|
||||||
"access",
|
"access",
|
||||||
"setup",
|
"setup",
|
||||||
|
|
@ -40,6 +41,9 @@ _ALLOWED_PARAMETER_CLASSES = {
|
||||||
"constrained",
|
"constrained",
|
||||||
"provider",
|
"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:
|
def _money(value: str | int | float | Decimal | None) -> Decimal | None:
|
||||||
|
|
@ -88,6 +92,20 @@ class TunableParameter:
|
||||||
options: tuple[str, ...] = ()
|
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)
|
@dataclass(frozen=True)
|
||||||
class PricingModel:
|
class PricingModel:
|
||||||
id: str
|
id: str
|
||||||
|
|
@ -104,6 +122,7 @@ class PricingModel:
|
||||||
charge_components: tuple[ChargeComponent, ...] = ()
|
charge_components: tuple[ChargeComponent, ...] = ()
|
||||||
commitments: tuple[Commitment, ...] = ()
|
commitments: tuple[Commitment, ...] = ()
|
||||||
tunable_parameters: tuple[TunableParameter, ...] = ()
|
tunable_parameters: tuple[TunableParameter, ...] = ()
|
||||||
|
assurance_claims: tuple[AssuranceClaim, ...] = ()
|
||||||
eligibility: tuple[str, ...] = ()
|
eligibility: tuple[str, ...] = ()
|
||||||
provider_hints: dict[str, Any] = field(default_factory=dict)
|
provider_hints: dict[str, Any] = field(default_factory=dict)
|
||||||
metadata: 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]]:
|
def _legacy_charge_components(raw: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
components: 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(
|
tunable_parameters=tuple(
|
||||||
_parse_tunable_parameter(item) for item in raw.get("tunable_parameters", [])
|
_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", [])),
|
eligibility=tuple(str(item) for item in raw.get("eligibility", [])),
|
||||||
provider_hints=_tuple_dict(raw.get("provider_hints")),
|
provider_hints=_tuple_dict(raw.get("provider_hints")),
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
|
|
@ -320,4 +372,46 @@ def validate_pricing_model(model: PricingModel) -> list[str]:
|
||||||
if len(commitment_ids) != len(set(commitment_ids)):
|
if len(commitment_ids) != len(set(commitment_ids)):
|
||||||
issues.append("commitment ids must be unique")
|
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
|
return issues
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Governance Workflows
|
# Governance Workflows
|
||||||
|
|
||||||
Status: MVP for `ADAPTIVE-WP-0008`.
|
Status: MVP for `ADAPTIVE-WP-0008` and `ADAPTIVE-WP-0009`.
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
|
|
@ -14,6 +14,7 @@ The repository now exposes:
|
||||||
- a customer-facing safe-tuning contract surface
|
- a customer-facing safe-tuning contract surface
|
||||||
- pricing health checks
|
- pricing health checks
|
||||||
- provider-publication audit and revision surfaces
|
- provider-publication audit and revision surfaces
|
||||||
|
- one-time approval for new or changed tier assurance definitions
|
||||||
|
|
||||||
## Core And Adapter Layers
|
## Core And Adapter Layers
|
||||||
|
|
||||||
|
|
@ -42,6 +43,19 @@ The policy model covers:
|
||||||
For Coulomb, the current policy keeps customer-visible tuning disabled and
|
For Coulomb, the current policy keeps customer-visible tuning disabled and
|
||||||
requires approval for candidate rollouts and approximate Stripe mappings.
|
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
|
## Recommendation Workflow
|
||||||
|
|
||||||
Recommendations now include:
|
Recommendations now include:
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ Each pricing model contains:
|
||||||
- explicit charge components
|
- explicit charge components
|
||||||
- commitments
|
- commitments
|
||||||
- tunable parameters
|
- tunable parameters
|
||||||
|
- optional customer assurance claims with typed tenancy-posture minimums
|
||||||
- eligibility and provider hints
|
- eligibility and provider hints
|
||||||
- free-form metadata for deployment-specific details
|
- free-form metadata for deployment-specific details
|
||||||
|
|
||||||
|
|
@ -73,6 +74,25 @@ tunable_parameters:
|
||||||
max_value: decimal | null
|
max_value: decimal | null
|
||||||
options: []
|
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:
|
eligibility:
|
||||||
- string
|
- string
|
||||||
|
|
||||||
|
|
@ -102,6 +122,18 @@ Current runtime validation enforces:
|
||||||
- tunable parameter keys are unique
|
- tunable parameter keys are unique
|
||||||
- `customer_tunable` parameters declare bounds or enumerated options
|
- `customer_tunable` parameters declare bounds or enumerated options
|
||||||
- commitment ids are unique
|
- 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
|
## Transitional Compatibility
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import replace
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -9,6 +10,7 @@ from adaptive_pricing_core.boundary_engine import (
|
||||||
PricingConfiguration,
|
PricingConfiguration,
|
||||||
validate_pricing_configuration,
|
validate_pricing_configuration,
|
||||||
)
|
)
|
||||||
|
from adaptive_pricing_core.pricing_models import AssuranceClaim
|
||||||
from observatory.load import load_pricing_models
|
from observatory.load import load_pricing_models
|
||||||
|
|
||||||
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
|
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)
|
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:
|
def test_commitment_backed_discount_is_accepted_when_economics_stay_strong() -> None:
|
||||||
result = validate_pricing_configuration(
|
result = validate_pricing_configuration(
|
||||||
PricingConfiguration(
|
PricingConfiguration(
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import replace
|
||||||
from pathlib import Path
|
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.api import build_dashboard_payload
|
||||||
|
from observatory.load import load_pricing_models
|
||||||
|
|
||||||
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
|
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["confidence"] == "0.88"
|
||||||
assert execution_gate["risks"]
|
assert execution_gate["risks"]
|
||||||
assert execution_gate["supporting_observations"]
|
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 == ()
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import replace
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from pathlib import Path
|
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
|
from observatory.load import load_pricing_models
|
||||||
|
|
||||||
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
|
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_amount == Decimal("8.99")
|
||||||
assert model.access_fee_cadence == "monthly"
|
assert model.access_fee_cadence == "monthly"
|
||||||
assert len(model.charge_components) == 1
|
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}
|
||||||
|
|
|
||||||
36
tenancy.yaml
Normal file
36
tenancy.yaml
Normal file
|
|
@ -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.
|
||||||
|
|
@ -9,6 +9,7 @@ owner: codex
|
||||||
topic_slug: helix-forge
|
topic_slug: helix-forge
|
||||||
created: "2026-08-18"
|
created: "2026-08-18"
|
||||||
updated: "2026-08-18"
|
updated: "2026-08-18"
|
||||||
|
state_hub_workstream_id: "38613963-4455-4e0c-849f-65fdda68f572"
|
||||||
---
|
---
|
||||||
|
|
||||||
# Plan-derived guardrail spend ceilings
|
# Plan-derived guardrail spend ceilings
|
||||||
|
|
@ -67,6 +68,7 @@ T01 produces a shape both repos accept.
|
||||||
id: ADAPTIVE-WP-0010-T01
|
id: ADAPTIVE-WP-0010-T01
|
||||||
status: todo
|
status: todo
|
||||||
priority: high
|
priority: high
|
||||||
|
state_hub_task_id: "7109ccf7-1267-44b6-9f20-38eff7bb7dd2"
|
||||||
```
|
```
|
||||||
|
|
||||||
Agree the shape of the feed jointly with `tenant-engine`: what
|
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
|
id: ADAPTIVE-WP-0010-T02
|
||||||
status: wait
|
status: wait
|
||||||
priority: medium
|
priority: medium
|
||||||
|
state_hub_task_id: "7da0b8f5-93a7-4048-900d-8fa39eb0de96"
|
||||||
```
|
```
|
||||||
|
|
||||||
Blocked on T01.
|
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
|
id: ADAPTIVE-WP-0010-T03
|
||||||
status: wait
|
status: wait
|
||||||
priority: medium
|
priority: medium
|
||||||
|
state_hub_task_id: "82788fbd-e0e7-4bb0-94ca-405176e74809"
|
||||||
```
|
```
|
||||||
|
|
||||||
Blocked on T02.
|
Blocked on T02.
|
||||||
|
|
@ -127,6 +131,7 @@ Boundary-engine rules, explainable in the existing style:
|
||||||
id: ADAPTIVE-WP-0010-T04
|
id: ADAPTIVE-WP-0010-T04
|
||||||
status: wait
|
status: wait
|
||||||
priority: medium
|
priority: medium
|
||||||
|
state_hub_task_id: "c9220137-baed-4ba1-929c-449ba1a754b9"
|
||||||
```
|
```
|
||||||
|
|
||||||
Blocked on T02.
|
Blocked on T02.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue