Go-live T05 + WP-0013/0014: first Phase and Control Plane completion
Accept WP-0008-T05 for trsl:phase:info-tech-canon-service-surface (history/260805-T05-GoLive-info-tech-canon.md). Finish WP-0013 remission automation and WP-0014 extension/breach/attestation Control Plane UI. Update SCOPE, README, and pilot-candidate notes for pilot Stage 1.
This commit is contained in:
parent
f56d82f09a
commit
3064c0fe0c
18 changed files with 1676 additions and 72 deletions
367
src/target_revenue/remission.py
Normal file
367
src/target_revenue/remission.py
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
"""Remission Credit calculation and ledger application (WP-0013).
|
||||
|
||||
Implements `trsl:policy:linear-longstop-v0@1.0` from
|
||||
`specs/policies/linear-longstop-v0.md`:
|
||||
|
||||
R(t) = T0 × clamp((t − t0)/(tL − t0), 0, 1)
|
||||
|
||||
Design decisions (WP-0013-T01, recorded 2026-08-05):
|
||||
|
||||
1. **t0 (Phase activation)** is the Trust Service registration timestamp
|
||||
(`phase_manifests.registered_at`). No new manifest field: in Stage 0 a
|
||||
Phase is not active until registered. Pure callers pass `t0` explicitly
|
||||
so offline packages stay free of hosting state.
|
||||
|
||||
2. **Cadence** is monthly UTC by convention for scheduled runs (1st of
|
||||
each month 00:00 UTC, or longstop if sooner). The pure model itself is
|
||||
*cumulative*, not period-keyed: each invocation remits
|
||||
`max(0, R(as_of) − already_recorded_policy_remission)`. Re-running at
|
||||
the same `as_of` is a no-op; catching up after a missed schedule works
|
||||
without double-counting. On-demand apply uses the same delta formula.
|
||||
|
||||
3. **Idempotency** follows from (2). Policy-produced entries are identified
|
||||
by `type == remission-credit` and `extension.id` matching the policy
|
||||
(corrections stay out of the "already remitted" sum so a deliberate
|
||||
`remission-correction` is not silently undone by the next run).
|
||||
|
||||
4. **Actor** is a dedicated Licensor credential labeled
|
||||
`system:policy-engine` (rights: operator). Policy-driven entries are not
|
||||
attributed to a human; `submitted_by_token` is never left null. The
|
||||
credential is auto-issued on first use per Licensor tenant.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
from psycopg import Connection
|
||||
|
||||
from .registry import Licensor
|
||||
|
||||
#: Credential label for the non-human actor that submits policy remissions.
|
||||
POLICY_ENGINE_CREDENTIAL_LABEL = "system:policy-engine"
|
||||
|
||||
#: Dust floor: skip ledger writes smaller than this (currency major units).
|
||||
MIN_REMISSION_AMOUNT = 0.01
|
||||
|
||||
LINEAR_LONGSTOP_V0_POLICY_ID = "trsl:policy:linear-longstop-v0"
|
||||
LINEAR_LONGSTOP_V0_VERSION = "1.0"
|
||||
LINEAR_LONGSTOP_V0_URN = f"{LINEAR_LONGSTOP_V0_POLICY_ID}@{LINEAR_LONGSTOP_V0_VERSION}"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RemissionPlan:
|
||||
"""Result of planning one policy-driven remission at a point in time."""
|
||||
|
||||
policy_id: str
|
||||
policy_version: str
|
||||
cumulative_expected: float
|
||||
already_remitted: float
|
||||
delta: float
|
||||
as_of: datetime
|
||||
t0: datetime
|
||||
longstop_at: datetime
|
||||
initial_target: float
|
||||
|
||||
@property
|
||||
def should_append(self) -> bool:
|
||||
return self.delta >= MIN_REMISSION_AMOUNT
|
||||
|
||||
|
||||
def parse_policy_urn(urn: str) -> tuple[str, str]:
|
||||
"""Split `trsl:policy:slug@version` into (id, version)."""
|
||||
if "@" not in urn:
|
||||
raise ValueError(f"policy URN missing @version: {urn!r}")
|
||||
policy_id, version = urn.rsplit("@", 1)
|
||||
if not policy_id.startswith("trsl:policy:"):
|
||||
raise ValueError(f"not a degeneration policy URN: {urn!r}")
|
||||
return policy_id, version
|
||||
|
||||
|
||||
def _parse_ts(ts: str | datetime) -> datetime:
|
||||
if isinstance(ts, datetime):
|
||||
if ts.tzinfo is None:
|
||||
raise ValueError("timestamps must be timezone-aware")
|
||||
return ts
|
||||
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
||||
if dt.tzinfo is None:
|
||||
raise ValueError("timestamps must be timezone-aware")
|
||||
return dt
|
||||
|
||||
|
||||
def clamp01(x: float) -> float:
|
||||
return 0.0 if x < 0.0 else 1.0 if x > 1.0 else x
|
||||
|
||||
|
||||
def cumulative_remission(
|
||||
initial_target: float,
|
||||
t0: datetime,
|
||||
longstop_at: datetime,
|
||||
as_of: datetime,
|
||||
) -> float:
|
||||
"""R(t) for linear-longstop-v0. Pure; no I/O.
|
||||
|
||||
Before t0 → 0; at/after longstop → full Initial Target; linear in between.
|
||||
"""
|
||||
t0 = _parse_ts(t0)
|
||||
longstop_at = _parse_ts(longstop_at)
|
||||
as_of = _parse_ts(as_of)
|
||||
if initial_target <= 0:
|
||||
raise ValueError("initial_target must be positive")
|
||||
span = (longstop_at - t0).total_seconds()
|
||||
if span <= 0:
|
||||
# Degenerate longstop at or before activation: full remission once as_of >= t0.
|
||||
return float(initial_target) if as_of >= t0 else 0.0
|
||||
progress = clamp01((as_of - t0).total_seconds() / span)
|
||||
return float(initial_target) * progress
|
||||
|
||||
|
||||
def policy_remission_already_recorded(
|
||||
entries: list[dict[str, Any]],
|
||||
policy_id: str = LINEAR_LONGSTOP_V0_POLICY_ID,
|
||||
) -> float:
|
||||
"""Sum of `remission-credit` amounts produced by this policy id.
|
||||
|
||||
Does not include `remission-correction` or
|
||||
`administrative-correction-remission` — those are deliberate human
|
||||
adjustments that must not be auto-undone by the next policy run.
|
||||
"""
|
||||
total = 0.0
|
||||
for entry in entries:
|
||||
if entry.get("type") != "remission-credit":
|
||||
continue
|
||||
ext = entry.get("extension") or {}
|
||||
if ext.get("id") == policy_id:
|
||||
total += float(entry["amount"])
|
||||
return total
|
||||
|
||||
|
||||
def plan_remission(
|
||||
manifest: dict[str, Any],
|
||||
entries: list[dict[str, Any]],
|
||||
t0: datetime,
|
||||
as_of: datetime,
|
||||
) -> RemissionPlan | None:
|
||||
"""Plan the next policy-driven remission for a Phase, or None if N/A.
|
||||
|
||||
Returns None when the Phase's degeneration_policy is not a supported
|
||||
automated policy (today: only linear-longstop-v0). Raises ValueError
|
||||
on missing longstop / bad timestamps.
|
||||
"""
|
||||
policy_urn = manifest["phase"]["degeneration_policy"]
|
||||
try:
|
||||
policy_id, policy_version = parse_policy_urn(policy_urn)
|
||||
except ValueError:
|
||||
return None
|
||||
if policy_id != LINEAR_LONGSTOP_V0_POLICY_ID:
|
||||
return None
|
||||
|
||||
initial = float(manifest["phase"]["initial_target"]["amount"])
|
||||
longstop_at = _parse_ts(manifest["phase"]["longstop_at"])
|
||||
t0_dt = _parse_ts(t0)
|
||||
as_of_dt = _parse_ts(as_of)
|
||||
|
||||
expected = cumulative_remission(initial, t0_dt, longstop_at, as_of_dt)
|
||||
already = policy_remission_already_recorded(entries, policy_id)
|
||||
# Never remit more than still outstanding against Initial Target when
|
||||
# other credits already reduced it — fold clamps Outstanding at 0, but
|
||||
# over-remitting relative to T0 would still inflate Remission Credit
|
||||
# facts. Cap cumulative expected at T0 (formula already does) and delta
|
||||
# at max(0, T0 - already) is implicit. Also do not reverse over-remission
|
||||
# via negative delta (corrections handle that).
|
||||
delta = max(0.0, expected - already)
|
||||
# Round to cents to avoid float dust ledger spam.
|
||||
delta = round(delta, 2)
|
||||
expected = round(expected, 2)
|
||||
|
||||
return RemissionPlan(
|
||||
policy_id=policy_id,
|
||||
policy_version=policy_version,
|
||||
cumulative_expected=expected,
|
||||
already_remitted=round(already, 2),
|
||||
delta=delta,
|
||||
as_of=as_of_dt,
|
||||
t0=t0_dt,
|
||||
longstop_at=longstop_at,
|
||||
initial_target=initial,
|
||||
)
|
||||
|
||||
|
||||
def build_remission_entry_input(
|
||||
phase_id: str,
|
||||
plan: RemissionPlan,
|
||||
currency: str,
|
||||
entry_id: str | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Build a ledger entry input (no previous_entry_hash/signature).
|
||||
|
||||
Returns None when `plan.should_append` is false (idempotent no-op).
|
||||
"""
|
||||
if not plan.should_append:
|
||||
return None
|
||||
if entry_id is None:
|
||||
entry_id = f"trsl:entry:rem{secrets.token_hex(12)}"
|
||||
as_of_iso = plan.as_of.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
return {
|
||||
"id": entry_id,
|
||||
"phase": phase_id,
|
||||
"type": "remission-credit",
|
||||
"amount": plan.delta,
|
||||
"currency": currency,
|
||||
"recognized_at": as_of_iso,
|
||||
"extension": {
|
||||
"id": plan.policy_id,
|
||||
"version": plan.policy_version,
|
||||
},
|
||||
"evidence_reference": (
|
||||
f"{plan.policy_id}@{plan.policy_version}"
|
||||
f"#as_of={as_of_iso}"
|
||||
f"#cumulative={plan.cumulative_expected}"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def next_monthly_boundary(as_of: datetime) -> datetime:
|
||||
"""Next 1st-of-month 00:00 UTC strictly after `as_of`."""
|
||||
as_of = _parse_ts(as_of).astimezone(timezone.utc)
|
||||
if as_of.month == 12:
|
||||
candidate = datetime(as_of.year + 1, 1, 1, tzinfo=timezone.utc)
|
||||
else:
|
||||
candidate = datetime(as_of.year, as_of.month + 1, 1, tzinfo=timezone.utc)
|
||||
# If as_of is exactly on a boundary, still move to the *next* one
|
||||
# (scheduled run at T has already been eligible for that instant).
|
||||
if as_of >= candidate:
|
||||
if candidate.month == 12:
|
||||
candidate = datetime(candidate.year + 1, 1, 1, tzinfo=timezone.utc)
|
||||
else:
|
||||
candidate = datetime(candidate.year, candidate.month + 1, 1, tzinfo=timezone.utc)
|
||||
return candidate
|
||||
|
||||
|
||||
def next_scheduled_remission_at(as_of: datetime, longstop_at: datetime) -> datetime | None:
|
||||
"""Next scheduled recognition instant: min(next monthly UTC, longstop), if still future."""
|
||||
as_of = _parse_ts(as_of)
|
||||
longstop_at = _parse_ts(longstop_at)
|
||||
if as_of >= longstop_at:
|
||||
return None
|
||||
monthly = next_monthly_boundary(as_of)
|
||||
return monthly if monthly < longstop_at else longstop_at
|
||||
|
||||
|
||||
# --- Hosted application path ------------------------------------------------
|
||||
|
||||
|
||||
def get_phase_registered_at(conn: "Connection", phase_id: str) -> datetime | None:
|
||||
row = conn.execute(
|
||||
"SELECT registered_at FROM phase_manifests WHERE phase_id = %s",
|
||||
(phase_id,),
|
||||
).fetchone()
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
def ensure_policy_engine_credential(conn: "Connection", licensor_id: str) -> "Licensor":
|
||||
"""Return the `system:policy-engine` credential for a Licensor, creating it if needed."""
|
||||
from . import registry
|
||||
from .registry import Licensor
|
||||
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT token, licensor_id, credential_label, rights
|
||||
FROM licensors
|
||||
WHERE licensor_id = %s
|
||||
AND credential_label = %s
|
||||
AND revoked_at IS NULL
|
||||
LIMIT 1
|
||||
""",
|
||||
(licensor_id, POLICY_ENGINE_CREDENTIAL_LABEL),
|
||||
).fetchone()
|
||||
if row is not None:
|
||||
token, lid, label, rights = row
|
||||
return Licensor(token=token, licensor_id=lid, credential_label=label, rights=rights)
|
||||
return registry.issue_sub_credential(
|
||||
conn,
|
||||
licensor_id=licensor_id,
|
||||
credential_label=POLICY_ENGINE_CREDENTIAL_LABEL,
|
||||
rights="operator",
|
||||
issued_by="system:remission-automation",
|
||||
)
|
||||
|
||||
|
||||
def apply_remission_for_phase(
|
||||
conn: "Connection",
|
||||
phase_id: str,
|
||||
signing_key: "Ed25519PrivateKey",
|
||||
as_of: datetime | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Compute and append a policy remission entry for one Phase if needed.
|
||||
|
||||
Returns the signed entry if one was written, else None (already current,
|
||||
unsupported policy, or phase missing). Uses the Phase owner's
|
||||
`system:policy-engine` credential for `submitted_by_token`.
|
||||
"""
|
||||
from . import ledger, registry
|
||||
from .registry import RegistrationError
|
||||
|
||||
if as_of is None:
|
||||
as_of = datetime.now(timezone.utc)
|
||||
else:
|
||||
as_of = _parse_ts(as_of)
|
||||
|
||||
manifest = registry.get_phase_manifest(conn, phase_id)
|
||||
if manifest is None:
|
||||
raise RegistrationError(f"phase {phase_id!r} is not registered")
|
||||
|
||||
registered_at = get_phase_registered_at(conn, phase_id)
|
||||
if registered_at is None:
|
||||
raise RegistrationError(f"phase {phase_id!r} has no registered_at")
|
||||
if registered_at.tzinfo is None:
|
||||
registered_at = registered_at.replace(tzinfo=timezone.utc)
|
||||
|
||||
entries = ledger.get_ledger(conn, phase_id)
|
||||
plan = plan_remission(manifest, entries, t0=registered_at, as_of=as_of)
|
||||
if plan is None:
|
||||
raise RegistrationError(
|
||||
f"phase {phase_id!r} degeneration_policy is not an automated policy "
|
||||
f"(supported: {LINEAR_LONGSTOP_V0_URN})"
|
||||
)
|
||||
|
||||
entry_input = build_remission_entry_input(
|
||||
phase_id,
|
||||
plan,
|
||||
currency=manifest["phase"]["initial_target"]["currency"],
|
||||
)
|
||||
if entry_input is None:
|
||||
return None
|
||||
|
||||
licensor_id = conn.execute(
|
||||
"SELECT licensor_id FROM phase_manifests WHERE phase_id = %s",
|
||||
(phase_id,),
|
||||
).fetchone()[0]
|
||||
engine = ensure_policy_engine_credential(conn, licensor_id)
|
||||
return ledger.append_entry(conn, engine, phase_id, entry_input, signing_key)
|
||||
|
||||
|
||||
def apply_remission_for_all_phases(
|
||||
conn: "Connection",
|
||||
signing_key: "Ed25519PrivateKey",
|
||||
as_of: datetime | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Run policy remission for every registered Phase. Returns appended entries."""
|
||||
from .registry import RegistrationError
|
||||
|
||||
rows = conn.execute("SELECT phase_id FROM phase_manifests ORDER BY phase_id").fetchall()
|
||||
written: list[dict[str, Any]] = []
|
||||
for (phase_id,) in rows:
|
||||
try:
|
||||
entry = apply_remission_for_phase(conn, phase_id, signing_key, as_of=as_of)
|
||||
except RegistrationError:
|
||||
# Skip Phases with unsupported policies rather than aborting the batch.
|
||||
continue
|
||||
if entry is not None:
|
||||
written.append(entry)
|
||||
return written
|
||||
Loading…
Add table
Add a link
Reference in a new issue