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:
tegwick 2026-08-05 16:00:06 +02:00
parent f56d82f09a
commit 3064c0fe0c
18 changed files with 1676 additions and 72 deletions

View file

@ -20,7 +20,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
from . import ledger, registry
from . import attestation, breach_record, ledger, registry, remission
from .registry import Licensor
if TYPE_CHECKING:
@ -168,6 +168,120 @@ def append_development_credit(
return stored
def apply_policy_remission(
conn: "Connection",
licensor: Licensor,
phase_id: str,
signing_key: Any,
) -> dict[str, Any] | None:
"""Operator+ only: run degeneration-policy remission for one Phase
(WP-0013). Ledger attribution uses `system:policy-engine`; the audit
log records which human credential triggered the run."""
_require_right(licensor, "operator")
stored = remission.apply_remission_for_phase(conn, phase_id, signing_key)
if stored is not None:
record_audit_event(
conn, licensor, action="apply_policy_remission", phase_id=phase_id,
trust_service_record_id=stored["id"],
detail={"type": stored["type"], "amount": stored["amount"]},
)
else:
record_audit_event(
conn, licensor, action="apply_policy_remission", phase_id=phase_id,
detail={"status": "up_to_date"},
)
return stored
# --- Extension Registry (WP-0014-T01) ---------------------------------------
#
# Rights decision (recorded 2026-08-05, WP-0014-T01; fills the gap noted in
# specs/PhaseLifecycleUseCases.md use case 6 and concept §2):
# - Register a new extension: Operator+ (same tier as Phase registration —
# the tenant is publishing structure it will use on its ledgers).
# - Promote to canonical: Admin only (governance action, never automated;
# matches credential management and set_extension_status SECURITY DEFINER).
# The Trust Service's own API still accepts any authenticated tenant token for
# register_extension; these gates are Control Plane policy layered on top.
def register_extension(
conn: "Connection", licensor: Licensor, extension: dict[str, Any]
) -> None:
"""Operator+: register a Monetization Extension (starts as `registered`)."""
_require_right(licensor, "operator")
registry.register_extension(conn, licensor, extension)
record_audit_event(
conn, licensor, action="register_extension",
detail={"extension_id": extension["id"], "version": extension["version"]},
)
def promote_extension_canonical(
conn: "Connection", admin: Licensor, extension_id: str, version: str
) -> None:
"""Admin only: promote an extension from `registered` to `canonical`."""
_require_right(admin, "admin")
approved_by = admin.credential_label or admin.token
registry.promote_extension_canonical(conn, extension_id, version, approved_by)
record_audit_event(
conn, admin, action="promote_extension_canonical",
detail={"extension_id": extension_id, "version": version},
)
# --- Breach / Compliance Records (WP-0014-T02) ------------------------------
def publish_breach_event(
conn: "Connection",
licensor: Licensor,
phase_id: str,
event_input: dict[str, Any],
signing_key: Any,
) -> dict[str, Any]:
"""Operator+: publish one breach/compliance lifecycle event (concept §2)."""
_require_right(licensor, "operator")
stored = breach_record.publish_breach_event(
conn, licensor, phase_id, event_input, signing_key
)
record_audit_event(
conn, licensor, action="publish_breach_event", phase_id=phase_id,
trust_service_record_id=stored["id"],
detail={
"case_id": stored["case_id"],
"event_type": stored["event_type"],
"anonymized": stored["anonymized"],
},
)
return stored
# --- Conversion Attestation (WP-0014-T03) -----------------------------------
def get_or_publish_attestation(
conn: "Connection",
phase_id: str,
signing_key: Any,
) -> dict[str, Any] | None:
"""Read-only UI helper: return the published attestation if converted.
Calls `attestation.publish_attestation` which is idempotent and only
writes on first observation of Outstanding Target = 0. Returns None
when the Phase has not converted (does not raise). No rights check
attestations are public facts (FR-9); any signed-in Viewer may see them.
"""
manifest = registry.get_phase_manifest(conn, phase_id)
if manifest is None:
return None
entries = ledger.get_ledger(conn, phase_id)
try:
return attestation.publish_attestation(conn, manifest, entries, signing_key)
except attestation.NotConvertedError:
return None
def propose_ledger_entry(
conn: "Connection", licensor: Licensor, phase_id: str, entry_input: dict[str, Any]
) -> int:

View file

@ -25,6 +25,7 @@ from typing import Any
from . import conversion as conversion_module
from . import fold as fold_module
from . import remission as remission_module
_MATERIAL_PROGRESS_TYPES = frozenset({"development-credit", "remission-credit"})
@ -34,9 +35,17 @@ def _parse(ts: str) -> datetime:
def compute_metrics(
manifest: dict[str, Any], entries: list[dict[str, Any]], as_of: datetime
manifest: dict[str, Any],
entries: list[dict[str, Any]],
as_of: datetime,
activated_at: datetime | None = None,
) -> dict[str, Any]:
"""Compute the full labeled metrics set for one Phase at a point in time."""
"""Compute the full labeled metrics set for one Phase at a point in time.
`activated_at` is optional Phase activation (t0). Hosted callers pass
`phase_manifests.registered_at` (WP-0013). When omitted, policy-based
remission forecasts are left null rather than inventing a t0.
"""
if as_of.tzinfo is None:
raise ValueError("as_of must be timezone-aware")
@ -61,6 +70,7 @@ def compute_metrics(
"future_license": status.future_license,
"last_ledger_entry_id": last_entry["id"] if last_entry else None,
"longstop_at": manifest["phase"].get("longstop_at"),
"activated_at": activated_at.isoformat() if activated_at is not None else None,
}
calculations: dict[str, Any] = {
@ -95,7 +105,12 @@ def compute_metrics(
(as_of - _parse(last_material["recognized_at"])).total_seconds() / 86400.0, 4
)
forecasts: dict[str, Any] = {"projected_conversion_date": None}
forecasts: dict[str, Any] = {
"projected_conversion_date": None,
"remission_if_applied_now": None,
"next_scheduled_remission_at": None,
"next_scheduled_remission_amount": None,
}
velocity = (
(calculations["development_credit_velocity_per_day"] or 0.0)
+ (calculations["remission_credit_velocity_per_day"] or 0.0)
@ -106,6 +121,28 @@ def compute_metrics(
as_of.replace(microsecond=0) + _timedelta_days(days_remaining)
).isoformat()
if activated_at is not None:
plan_now = remission_module.plan_remission(
manifest, entries, t0=activated_at, as_of=as_of
)
if plan_now is not None:
forecasts["remission_if_applied_now"] = (
plan_now.delta if plan_now.should_append else 0.0
)
longstop = _parse(manifest["phase"]["longstop_at"])
next_at = remission_module.next_scheduled_remission_at(as_of, longstop)
if next_at is not None and not status.is_converted:
forecasts["next_scheduled_remission_at"] = next_at.isoformat()
plan_next = remission_module.plan_remission(
manifest, entries, t0=activated_at, as_of=next_at
)
if plan_next is not None:
# Amount that would still be due at the next boundary if
# nothing else is remitted between now and then.
forecasts["next_scheduled_remission_amount"] = (
plan_next.delta if plan_next.should_append else 0.0
)
return {
"phase": manifest["phase"]["id"],
"as_of": as_of.isoformat(),

View file

@ -280,6 +280,35 @@ def get_extension(
return {**contract, "status": status}
def list_extensions(conn: Connection) -> list[dict[str, Any]]:
"""All registered extensions (any Licensor), most recently registered first.
Status is the hosting-layer column (authoritative for canonical/
registered/deprecated), not the contract JSON's own `status` field
which is only what the author submitted.
"""
rows = conn.execute(
"""
SELECT extension_id, version, licensor_id, contract, status, registered_at
FROM extensions
ORDER BY registered_at DESC, extension_id ASC, version ASC
"""
).fetchall()
result = []
for extension_id, version, licensor_id, contract, status, registered_at in rows:
result.append(
{
"extension_id": extension_id,
"version": version,
"licensor_id": licensor_id,
"contract": contract,
"status": status,
"registered_at": registered_at,
}
)
return result
def promote_extension_canonical(
conn: Connection, extension_id: str, version: str, approved_by: str
) -> None:

View 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

View file

@ -18,7 +18,7 @@ from fastapi import Depends, FastAPI, HTTPException, Request
from psycopg import Connection
from psycopg_pool import ConnectionPool
from .. import attestation, breach_record, ledger, metrics, registry
from .. import attestation, breach_record, ledger, metrics, registry, remission
from . import keys
app = FastAPI(title="Target Revenue Trust Service — Registries", version="0.1.0")
@ -152,7 +152,65 @@ def read_metrics(
if manifest is None:
raise HTTPException(status_code=404, detail="phase not found")
entries = ledger.get_ledger(conn, phase_id)
return metrics.compute_metrics(manifest, entries, metrics.utcnow())
activated_at = remission.get_phase_registered_at(conn, phase_id)
return metrics.compute_metrics(
manifest, entries, metrics.utcnow(), activated_at=activated_at
)
@app.post("/phases/{phase_id}/remission", status_code=201)
def apply_phase_remission(
phase_id: str,
licensor: registry.Licensor = Depends(get_licensor),
conn: Connection = Depends(get_connection),
signing_key=Depends(get_signing_key),
) -> dict[str, Any]:
"""On-demand linear-longstop remission apply (WP-0013).
Auth required (Operator+ of the Phase's Licensor). The ledger entry
itself is attributed to `system:policy-engine`, not the calling human
the caller only authorizes the run. Returns 200-shaped body with
`status: up_to_date` and no entry when the Phase is already current
(idempotent).
"""
if not registry.has_right(licensor.rights, "operator"):
raise HTTPException(status_code=403, detail="operator rights required")
manifest = registry.get_phase_manifest(conn, phase_id)
if manifest is None:
raise HTTPException(status_code=404, detail="phase not found")
owner = conn.execute(
"SELECT licensor_id FROM phase_manifests WHERE phase_id = %s", (phase_id,)
).fetchone()[0]
if owner != licensor.licensor_id:
raise HTTPException(status_code=403, detail="not authorized for this Phase")
try:
entry = remission.apply_remission_for_phase(conn, phase_id, signing_key)
except registry.RegistrationError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
if entry is None:
return {"phase_id": phase_id, "status": "up_to_date", "entry": None}
return {"phase_id": phase_id, "status": "appended", "entry": entry}
@app.post("/remission/run", status_code=200)
def run_remission_batch(
licensor: registry.Licensor = Depends(get_licensor),
conn: Connection = Depends(get_connection),
signing_key=Depends(get_signing_key),
) -> dict[str, Any]:
"""Batch scheduled-style run across all Phases (WP-0013).
Intended for a cron job holding an Operator+ credential. Each Phase
is planned independently; unsupported policies are skipped.
"""
if not registry.has_right(licensor.rights, "operator"):
raise HTTPException(status_code=403, detail="operator rights required")
written = remission.apply_remission_for_all_phases(conn, signing_key)
return {
"status": "ok",
"entries_appended": len(written),
"entry_ids": [e["id"] for e in written],
}
@app.get("/phases/{phase_id}/attestation")

View file

@ -25,7 +25,7 @@ from psycopg import Connection
from psycopg_pool import ConnectionPool
from starlette.middleware.sessions import SessionMiddleware
from .. import control_plane, ledger, metrics, registry
from .. import breach_record, control_plane, ledger, metrics, registry
from . import keys, reference_docs
_STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
@ -248,19 +248,56 @@ def phase_detail(
request: Request,
licensor: registry.Licensor = Depends(require_login),
conn: Connection = Depends(get_connection),
signing_key=Depends(get_signing_key),
):
from .. import remission
manifest = registry.get_phase_manifest(conn, phase_id)
if manifest is None:
raise HTTPException(status_code=404, detail="phase not found")
entries = ledger.get_ledger(conn, phase_id)
computed_metrics = metrics.compute_metrics(manifest, entries, metrics.utcnow())
activated_at = remission.get_phase_registered_at(conn, phase_id)
computed_metrics = metrics.compute_metrics(
manifest, entries, metrics.utcnow(), activated_at=activated_at
)
breaches = breach_record.get_breach_records(conn, phase_id)
phase_attestation = control_plane.get_or_publish_attestation(
conn, phase_id, signing_key
)
return templates.TemplateResponse(
request,
"phase_detail.html",
_template_context(request, licensor, manifest=manifest, ledger=entries, metrics=computed_metrics),
_template_context(
request,
licensor,
manifest=manifest,
ledger=entries,
metrics=computed_metrics,
breaches=breaches,
attestation=phase_attestation,
),
)
@app.post("/phases/{phase_id}/remission")
def phase_remission_apply(
phase_id: str,
request: Request,
licensor: registry.Licensor = Depends(require_login),
conn: Connection = Depends(get_connection),
signing_key=Depends(get_signing_key),
):
try:
entry = control_plane.apply_policy_remission(conn, licensor, phase_id, signing_key)
except (control_plane.ControlPlaneError, registry.RegistrationError) as exc:
return _redirect(f"/phases/{phase_id}", request, str(exc), "danger")
if entry is None:
flash = "Remission already up to date — no new entry written."
else:
flash = f"Remission credit {entry['id']} appended ({entry['amount']})."
return _redirect(f"/phases/{phase_id}", request, flash, "success")
@app.post("/phases/{phase_id}/ledger")
def phase_ledger_submit(
phase_id: str,
@ -298,6 +335,133 @@ def phase_ledger_submit(
return _redirect(f"/phases/{phase_id}", request, flash, "success")
@app.post("/phases/{phase_id}/breach")
def phase_breach_publish(
phase_id: str,
request: Request,
licensor: registry.Licensor = Depends(require_login),
conn: Connection = Depends(get_connection),
signing_key=Depends(get_signing_key),
record_id: str = Form(...),
case_id: str = Form(...),
event_type: str = Form(...),
category: str = Form(...),
event_at: str = Form(...),
evidence_reference: str = Form(""),
anonymized: str = Form("true"),
named_entitlement_holder: str = Form(""),
named_disclosure_authorized: str = Form(""),
):
"""Operator+: publish a breach/compliance event (WP-0014-T02)."""
is_anonymized = anonymized.lower() in ("true", "1", "on", "yes")
event_input: dict[str, Any] = {
"id": record_id,
"case_id": case_id,
"event_type": event_type,
"category": category,
"event_at": event_at,
"anonymized": is_anonymized,
}
if evidence_reference:
event_input["evidence_reference"] = evidence_reference
if not is_anonymized:
event_input["named_entitlement_holder"] = named_entitlement_holder
event_input["named_disclosure_authorized_under_cua"] = (
named_disclosure_authorized.lower() in ("true", "1", "on", "yes")
)
try:
stored = control_plane.publish_breach_event(
conn, licensor, phase_id, event_input, signing_key
)
except (control_plane.ControlPlaneError, registry.RegistrationError) as exc:
return _redirect(f"/phases/{phase_id}", request, str(exc), "danger")
return _redirect(
f"/phases/{phase_id}",
request,
f"Breach record {stored['id']} published ({stored['event_type']}).",
"success",
)
# --- Extension Registry (WP-0014-T01) ----------------------------------------
@app.get("/extensions")
def extensions_list(
request: Request,
licensor: registry.Licensor = Depends(require_login),
conn: Connection = Depends(get_connection),
):
extensions = registry.list_extensions(conn)
return templates.TemplateResponse(
request,
"extensions.html",
_template_context(request, licensor, extensions=extensions),
)
@app.post("/extensions")
def extensions_register(
request: Request,
licensor: registry.Licensor = Depends(require_login),
conn: Connection = Depends(get_connection),
extension_id: str = Form(...),
version: str = Form(...),
value_description: str = Form(...),
pricing_method: str = Form(...),
allocation_rule: str = Form(...),
default_rate: str = Form(""),
recognition_event: str = Form(...),
reversal_rule: str = Form(...),
evidence_requirement: str = Form(...),
):
extension: dict[str, Any] = {
"id": extension_id,
"version": version,
"value": {"description": value_description},
"pricing": {"method": pricing_method},
"allocation": {"rule": allocation_rule},
"recognition": {"event": recognition_event},
"reversal": {"rule": reversal_rule},
"evidence": {"requirement": evidence_requirement},
# Author-submitted status is always `registered`; the hosting column
# is authoritative and starts registered regardless (registry.py).
"status": "registered",
}
if default_rate.strip():
extension["allocation"]["default_rate"] = float(default_rate)
try:
control_plane.register_extension(conn, licensor, extension)
except (control_plane.ControlPlaneError, registry.RegistrationError) as exc:
return _redirect("/extensions", request, str(exc), "danger")
return _redirect(
"/extensions",
request,
f"Extension {extension_id}@{version} registered.",
"success",
)
@app.post("/extensions/promote")
def extensions_promote(
request: Request,
licensor: registry.Licensor = Depends(require_login),
conn: Connection = Depends(get_connection),
extension_id: str = Form(...),
version: str = Form(...),
):
try:
control_plane.promote_extension_canonical(conn, licensor, extension_id, version)
except (control_plane.ControlPlaneError, registry.RegistrationError) as exc:
return _redirect("/extensions", request, str(exc), "danger")
return _redirect(
"/extensions",
request,
f"Extension {extension_id}@{version} promoted to canonical.",
"success",
)
# --- Proposals (Operator+) --------------------------------------------------

View file

@ -0,0 +1,87 @@
{% extends "base.html" %}
{% block title %}Extensions — Target Revenue Control Plane{% endblock %}
{% block content %}
<wn-page-header>
<span slot="title">Monetization Extension Registry</span>
</wn-page-header>
<p style="color:#888;font-size:0.9rem;">
Rights: <strong>Operator+</strong> may register; <strong>Admin</strong> may
promote to <code>canonical</code> (governance action, never automated).
See <code>specs/TargetRevenueControlPlaneConcept.md</code> §2.
</p>
{% if extensions %}
<table class="wn-plain">
<thead>
<tr>
<th>id</th>
<th>version</th>
<th>status</th>
<th>licensor</th>
<th></th>
</tr>
</thead>
<tbody>
{% for ext in extensions %}
<tr>
<td><code>{{ ext.extension_id }}</code></td>
<td>{{ ext.version }}</td>
<td><wn-tag>{{ ext.status }}</wn-tag></td>
<td>{{ ext.licensor_id }}</td>
<td>
{% if session_rights == "admin" and ext.status == "registered" %}
<form method="post" action="/extensions/promote" style="display:inline" class="wn-form">
<input type="hidden" name="extension_id" value="{{ ext.extension_id }}">
<input type="hidden" name="version" value="{{ ext.version }}">
<wn-button type="submit" variant="secondary">Promote to canonical</wn-button>
</form>
{% endif %}
</td>
</tr>
<tr>
<td colspan="5" style="color:#666;font-size:0.85rem;padding-top:0;">
{{ ext.contract.value.description }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<wn-empty-state>No extensions registered yet.</wn-empty-state>
{% endif %}
{% if session_rights in ("operator", "admin") %}
<h3>Register a new extension</h3>
<form class="wn-form" method="post" action="/extensions">
<wn-field-row label="Extension id (trsl:extension:...)">
<wn-input name="extension_id" required></wn-input>
</wn-field-row>
<wn-field-row label="Version (e.g. 1.0)">
<wn-input name="version" required></wn-input>
</wn-field-row>
<wn-field-row label="Value description">
<wn-input name="value_description" required></wn-input>
</wn-field-row>
<wn-field-row label="Pricing method">
<wn-input name="pricing_method" required></wn-input>
</wn-field-row>
<wn-field-row label="Allocation rule">
<wn-input name="allocation_rule" required></wn-input>
</wn-field-row>
<wn-field-row label="Default rate (01, optional)">
<wn-input type="number" step="0.01" min="0" max="1" name="default_rate"></wn-input>
</wn-field-row>
<wn-field-row label="Recognition event">
<wn-input name="recognition_event" value="payment-settled" required></wn-input>
</wn-field-row>
<wn-field-row label="Reversal rule">
<wn-input name="reversal_rule" required></wn-input>
</wn-field-row>
<wn-field-row label="Evidence requirement">
<wn-input name="evidence_requirement" required></wn-input>
</wn-field-row>
<wn-button type="submit" variant="primary">Register extension</wn-button>
</form>
{% endif %}
{% endblock %}

View file

@ -31,8 +31,47 @@
&middot; <a href="/reference/policies/{{ policy_slug(manifest.phase.degeneration_policy) }}">view spec</a>
</td>
</tr>
<tr><td>Longstop</td><td>{{ metrics.facts.longstop_at or "—" }}</td></tr>
<tr><td>Activated (t0)</td><td>{{ metrics.facts.activated_at or "—" }}</td></tr>
</table>
<h3>Remission forecast <wn-tag>forecast</wn-tag></h3>
<table class="wn-plain">
<tr>
<td>If applied now</td>
<td>
{% if metrics.forecasts.remission_if_applied_now is not none %}
{{ metrics.forecasts.remission_if_applied_now }} {{ metrics.facts.initial_target_currency }}
{% else %}
{% endif %}
</td>
</tr>
<tr>
<td>Next scheduled (monthly UTC / longstop)</td>
<td>{{ metrics.forecasts.next_scheduled_remission_at or "—" }}</td>
</tr>
<tr>
<td>Amount at next schedule</td>
<td>
{% if metrics.forecasts.next_scheduled_remission_amount is not none %}
{{ metrics.forecasts.next_scheduled_remission_amount }} {{ metrics.facts.initial_target_currency }}
{% else %}
{% endif %}
</td>
</tr>
</table>
{% if session_rights in ("operator", "admin") %}
<form class="wn-form" method="post" action="/phases/{{ manifest.phase.id }}/remission" style="margin-bottom:1.5rem;">
<wn-button type="submit" variant="secondary">Apply policy remission now</wn-button>
<p style="color:#888;font-size:0.85rem;margin-top:0.5rem;">
Writes a <code>remission-credit</code> delta under <code>system:policy-engine</code>
(idempotent — no double-remit if already current).
</p>
</form>
{% endif %}
<h3>Ledger ({{ ledger | length }} entries)</h3>
{% if ledger %}
<table class="wn-plain">
@ -78,4 +117,98 @@
</wn-button>
</form>
{% endif %}
<h3>Conversion Attestation</h3>
{% if attestation %}
<table class="wn-plain">
<tr><td>Phase</td><td>{{ attestation.phase }}</td></tr>
<tr><td>Milestone release</td><td>{{ attestation.milestone_release }}</td></tr>
<tr><td>Converted at</td><td>{{ attestation.conversion_timestamp }}</td></tr>
<tr><td>Future License</td><td><strong>{{ attestation.future_license }}</strong></td></tr>
<tr><td>Final development credit</td><td>{{ attestation.final_development_credit }}</td></tr>
<tr><td>Final remission credit</td><td>{{ attestation.final_remission_credit }}</td></tr>
<tr><td>Outstanding at conversion</td><td>{{ attestation.final_outstanding_target }}</td></tr>
<tr><td>Ledger checkpoint</td><td><code>{{ attestation.ledger_checkpoint }}</code></td></tr>
<tr><td>Signature</td><td><code style="font-size:0.75rem;word-break:break-all;">{{ attestation.signature }}</code></td></tr>
</table>
{% elif metrics.facts.is_converted %}
<p>Phase is converted; attestation will publish on next observation.</p>
{% else %}
<wn-empty-state>Not converted — no attestation yet.</wn-empty-state>
{% endif %}
<h3>Breach / Compliance Records ({{ breaches | length }})</h3>
{% if breaches %}
<table class="wn-plain">
<thead>
<tr>
<th>id</th>
<th>case</th>
<th>type</th>
<th>category</th>
<th>event_at</th>
<th>named?</th>
</tr>
</thead>
<tbody>
{% for b in breaches %}
<tr>
<td>{{ b.id }}</td>
<td>{{ b.case_id }}</td>
<td>{{ b.event_type }}</td>
<td>{{ b.category }}</td>
<td>{{ b.event_at }}</td>
<td>
{% if b.anonymized %}
anonymized
{% else %}
{{ b.named_entitlement_holder }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<wn-empty-state>No breach/compliance records published for this Phase.</wn-empty-state>
{% endif %}
{% if session_rights in ("operator", "admin") %}
<h3>Publish a breach/compliance event</h3>
<p style="color:#888;font-size:0.85rem;">
Anonymized by default (Phase + category only). Named disclosure requires
an affirmative CUA authorization check (License V1C1 §7.4) — this form
records that assertion; it does not verify the CUA text.
</p>
<form class="wn-form" method="post" action="/phases/{{ manifest.phase.id }}/breach">
<wn-field-row label="Record id (unique)">
<wn-input name="record_id" required></wn-input>
</wn-field-row>
<wn-field-row label="Case id">
<wn-input name="case_id" required></wn-input>
</wn-field-row>
<wn-field-row label="Event type (alleged|cured|determined|terminated)">
<wn-input name="event_type" value="alleged" required></wn-input>
</wn-field-row>
<wn-field-row label="Category">
<wn-input name="category" required></wn-input>
</wn-field-row>
<wn-field-row label="Event at (ISO 8601)">
<wn-input name="event_at" required></wn-input>
</wn-field-row>
<wn-field-row label="Evidence reference (optional)">
<wn-input name="evidence_reference"></wn-input>
</wn-field-row>
<wn-field-row label="Anonymized (true/false)">
<wn-input name="anonymized" value="true" required></wn-input>
</wn-field-row>
<wn-field-row label="Named entitlement holder (only if anonymized=false)">
<wn-input name="named_entitlement_holder"></wn-input>
</wn-field-row>
<wn-field-row label="Named disclosure authorized under CUA (true if named)">
<wn-input name="named_disclosure_authorized" value="false"></wn-input>
</wn-field-row>
<wn-button type="submit" variant="secondary">Publish breach record</wn-button>
</form>
{% endif %}
{% endblock %}