Compare commits

...

2 commits

Author SHA1 Message Date
9e4e84f9ee WP-0011: deploy scaffolding for revenue.coulomb.social
Record T01 package (in-repo k8s/railiance, combined /ui + API app,
dedicated CNPG). Add Dockerfile, healthz, migration/bootstrap scripts,
kustomize manifests, ArgoCD Application (in railiance-platform), and
docs/deployment.md. T05 left open for operator DNS/OpenBao/image push.
2026-08-05 16:41:00 +02:00
3064c0fe0c 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.
2026-08-05 16:00:06 +02:00
40 changed files with 2363 additions and 210 deletions

31
Dockerfile Normal file
View file

@ -0,0 +1,31 @@
# Target Revenue Trust Service + Control Plane (WP-0011).
# Image: forgejo.coulomb.social/coulomb/target-revenue
FROM python:3.12-slim AS runtime
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
HOME=/home/app
RUN useradd --create-home --home-dir /home/app --uid 10001 app
WORKDIR /src
COPY pyproject.toml README.md LICENSE ./
COPY src/target_revenue ./src/target_revenue
COPY migrations ./migrations
COPY schemas ./schemas
COPY specs/policies ./specs/policies
COPY specs/profiles ./specs/profiles
RUN pip install --no-cache-dir --index-url https://pypi.org/simple ".[service]" \
&& chown -R app:app /src
COPY scripts/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
COPY scripts/apply_migrations.py /usr/local/bin/apply_migrations.py
COPY scripts/bootstrap_binky.py /usr/local/bin/bootstrap_binky.py
RUN chmod +x /usr/local/bin/docker-entrypoint.sh \
/usr/local/bin/apply_migrations.py \
/usr/local/bin/bootstrap_binky.py
USER app
EXPOSE 8000
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

View file

@ -87,7 +87,7 @@ The concept's §13 now defines a **Global Contingency Share Determination Rule**
| [TREV-WP-0008](workplans/TREV-WP-0008-governance-and-pilot-rollout.md) | Governance formalization + pilot rollout — **finished**, all 5 tasks done. **T05 accepted 2026-08-05**: first real Phase is `trsl:phase:info-tech-canon-service-surface` (`history/260805-T05-GoLive-info-tech-canon.md`). Org-wide TRSL LICENSE adoption (2026-07-30) was a prior, separate act |
| [TREV-WP-0009](workplans/TREV-WP-0009-target-revenue-control-plane.md) | Target Revenue Control Plane — interactive UI for the `binky` tenant, incl. interactive Development Credit entry creation (`specs/TargetRevenueControlPlaneConcept.md`) — **finished**, all 4 tasks done. **T04 (interactive UI, `src/target_revenue/service/control_plane_app.py`) built on vendored `whynot-design` web components** rather than from scratch, per an explicit feasibility check |
| [TREV-WP-0010](workplans/TREV-WP-0010-development-effort-calculator.md) | Development Effort Calculator — **finished**, all 3 tasks done. Applied to the three real pilot candidates (`history/260730-EffortCalculator-CandidateApplication.md`) — every calculator-derived Initial Target came out materially lower than the earlier hand-picked placeholders, two of three carrying explicit warnings recommending manual review |
| [TREV-WP-0011](workplans/TREV-WP-0011-railiance-reef-deployment.md) | Deploy Trust Service + Control Plane to the Railiance reef, fronted by `revenue.coulomb.social` — active; T01 (deployment-pattern decision, human gate) next. Deployment alone does not authorize a real Phase go-live — that remains WP-0008-T05 |
| [TREV-WP-0011](workplans/TREV-WP-0011-railiance-reef-deployment.md) | Deploy Trust Service + Control Plane to Railiance (`revenue.coulomb.social`) — **active**; T01T04 done (Dockerfile, `k8s/railiance/`, ArgoCD Application). **T05** remains: operator DNS/OpenBao/image-push/sync + pilot data migration. Guide: [`docs/deployment.md`](docs/deployment.md) |
| [TREV-WP-0012](workplans/TREV-WP-0012-phase-provenance-and-policy-modeling.md) | Phase provenance, ledger reference, and degeneration-policy modeling — **finished**, all 5 tasks done. Decisions (T02T04) synthesized into [`specs/PhaseProvenanceSpecAddendum.md`](specs/PhaseProvenanceSpecAddendum.md) (T05) — **not yet accepted for implementation**; that's the document to discuss before any schema/UI work is filed as its own workplan |
| [TREV-WP-0013](workplans/TREV-WP-0013-remission-credit-automation.md) | Remission Credit automation (degeneration policy execution) — **finished**, all 3 tasks done. Pure `plan_remission` + hosted apply with `system:policy-engine` actor; monthly UTC schedule; Control Plane forecast + apply button |
| [TREV-WP-0014](workplans/TREV-WP-0014-control-plane-extensions-breach-attestation-ui.md) | Control Plane UI: Extension Registry, Breach Records, Conversion Attestation — **finished**, all 4 tasks done. Operator+ register extension / Admin canonicalize; breach form with anonymized-by-default; attestation on phase detail |

View file

@ -15,7 +15,7 @@
| workplan | TREV-WP-0005 | finished | — | workplans/TREV-WP-0005-enforcement-network-research.md |
| workplan | TREV-WP-0006 | finished | — | workplans/TREV-WP-0006-trust-service-implementation.md |
| workplan | TREV-WP-0007 | finished | — | workplans/TREV-WP-0007-degeneration-policy-and-canonical-profiles.md |
| workplan | TREV-WP-0008 | active | — | workplans/TREV-WP-0008-governance-and-pilot-rollout.md |
| workplan | TREV-WP-0008 | finished | — | workplans/TREV-WP-0008-governance-and-pilot-rollout.md |
| workplan | TREV-WP-0009 | finished | — | workplans/TREV-WP-0009-target-revenue-control-plane.md |
| workplan | TREV-WP-0010 | finished | — | workplans/TREV-WP-0010-development-effort-calculator.md |
| workplan | TREV-WP-0011 | active | — | workplans/TREV-WP-0011-railiance-reef-deployment.md |
@ -78,7 +78,7 @@
| task | TREV-WP-0008-T02 | done | — | workplans/TREV-WP-0008-governance-and-pilot-rollout.md |
| task | TREV-WP-0008-T03 | done | — | workplans/TREV-WP-0008-governance-and-pilot-rollout.md |
| task | TREV-WP-0008-T04 | done | — | workplans/TREV-WP-0008-governance-and-pilot-rollout.md |
| task | TREV-WP-0008-T05 | todo | — | workplans/TREV-WP-0008-governance-and-pilot-rollout.md |
| task | TREV-WP-0008-T05 | done | — | workplans/TREV-WP-0008-governance-and-pilot-rollout.md |
| task | TREV-WP-0009-T01 | done | — | workplans/TREV-WP-0009-target-revenue-control-plane.md |
| task | TREV-WP-0009-T02 | done | — | workplans/TREV-WP-0009-target-revenue-control-plane.md |
| task | TREV-WP-0009-T03 | done | — | workplans/TREV-WP-0009-target-revenue-control-plane.md |

69
docs/deployment.md Normal file
View file

@ -0,0 +1,69 @@
# Deploying Target Revenue Trust Service + Control Plane
**Workplan:** `workplans/TREV-WP-0011-railiance-reef-deployment.md`
**Public host (target):** `https://revenue.coulomb.social`
**Control Plane UI:** `https://revenue.coulomb.social/ui/`
**Trust Service API:** `https://revenue.coulomb.social/` (e.g. `/phases/...`, `/public-key`)
This deployment does not by itself authorize new Phases beyond what
WP-0008-T05 already recorded. The first pilot Phase is
`trsl:phase:info-tech-canon-service-surface`
(`history/260805-T05-GoLive-info-tech-canon.md`).
---
## T01 decision (2026-08-05)
| Bullet | Decision |
| --- | --- |
| Manifest home | In-repo `k8s/railiance/` (issue-core pattern); ArgoCD Application in `railiance-platform` |
| Process topology | **Single process**: `combined.py` mounts Control Plane at `/ui`, Trust Service at `/` |
| Database | Dedicated CNPG Cluster `target-revenue-pg` in namespace `target-revenue` (not state-hub DB) |
| Secrets | OpenBao + ExternalSecret → `target-revenue-runtime`; CNPG role secrets operator-seeded |
| Subdomain / TLS | `revenue.coulomb.social`, `cert-manager.io/cluster-issuer: letsencrypt-prod` (same as `forgejo.coulomb.social`) |
| Founding credential | Idempotent Job `target-revenue-bootstrap-binky` (`scripts/bootstrap_binky.py`) |
---
## Operator checklist (first bring-up)
1. **DNS:** point `revenue.coulomb.social` at the Railiance Traefik/ingress (same path as other `*.coulomb.social` hosts on railiance01).
2. **OpenBao:** create KV path `workloads/target-revenue/runtime-secrets` with:
- `TRF_SIGNING_KEY_HEX` (64 hex chars)
- `TRF_CONTROL_PLANE_SECRET_KEY`
- `TRF_DATABASE_URL` (trf_app DSN to `target-revenue-pg-rw`)
- `TRF_MIGRATE_DATABASE_URL` (trf_owner DSN for migrate/bootstrap Jobs)
3. **CNPG bootstrap secrets** in namespace `target-revenue` (before Cluster reconciles):
- `target-revenue-pg-credentials` — keys as required by CNPG for owner `trf_owner`
- `target-revenue-trf-app-credentials` — password for managed role `trf_app`
4. **Image:** build and push
`forgejo.coulomb.social/coulomb/target-revenue:0.1.0`
(see Dockerfile; bump tag in Deployment + Jobs when releasing).
5. **ArgoCD:** apply
`railiance-platform/argocd/applications/target-revenue.application.yaml`
(once `main` contains `k8s/railiance/`).
6. **After migrate + bootstrap Jobs succeed:** pull founding token from bootstrap Job logs (`TOKEN=...`), store offline; sign in at `/ui/login`.
7. **Migrate pilot Phase data** from the local go-live instance (`trf-pilot-pg` / `:8765`) if that instance still holds the authoritative ledger — export Manifest + ledger JSON and re-register/append on the public instance (or promote the pilot DB dump deliberately). Do not leave two competing ledgers.
---
## Local pilot instance (already used for T05)
| Item | Value |
| --- | --- |
| API | `http://127.0.0.1:8765` |
| Postgres container | `trf-pilot-pg` / volume `trf-pilot-pgdata` / port `55432` |
| Token file | `~/.config/target-revenue/pilot-binky.token` |
```bash
python scripts/trf_onboard.py status \
--url http://127.0.0.1:8765 \
--phase-id trsl:phase:info-tech-canon-service-surface
```
---
## Health
- Combined / standalone: `GET /healthz``{"status":"ok"}` (no DB dependency)
- Control Plane when mounted: `GET /ui/healthz`

View file

@ -0,0 +1,54 @@
# Idempotent founding binky admin credential (WP-0011-T01/T05).
# Safe to re-run: no-op when a binky credential already exists.
# Capture TOKEN= from job logs once on first bootstrap; store in a password
# manager — never commit.
apiVersion: batch/v1
kind: Job
metadata:
name: target-revenue-bootstrap-binky
namespace: target-revenue
labels:
app.kubernetes.io/name: target-revenue
app.kubernetes.io/component: bootstrap
annotations:
argocd.argoproj.io/sync-wave: "2"
argocd.argoproj.io/hook: Sync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
ttlSecondsAfterFinished: 86400
backoffLimit: 2
template:
metadata:
labels:
app.kubernetes.io/name: target-revenue
app.kubernetes.io/component: bootstrap
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
containers:
- name: bootstrap
image: forgejo.coulomb.social/coulomb/target-revenue:0.1.0
imagePullPolicy: IfNotPresent
env:
- name: TRF_BOOTSTRAP_BINKY
value: "1"
- name: TRF_DATABASE_URL
valueFrom:
secretKeyRef:
name: target-revenue-runtime
key: TRF_MIGRATE_DATABASE_URL
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi

View file

@ -0,0 +1,36 @@
# Dedicated CNPG Cluster for Target Revenue Trust Service pilot data (WP-0011).
# Co-located in the target-revenue workload namespace so one ArgoCD Application
# can own the full stack (issue-core pattern + database).
#
# Pre-req secrets in namespace target-revenue (OpenBao ExternalSecret or
# operator-seeded): target-revenue-pg-credentials, target-revenue-trf-app-credentials.
# See docs/deployment.md.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: target-revenue-pg
namespace: target-revenue
labels:
app.kubernetes.io/name: target-revenue-pg
app.kubernetes.io/part-of: target-revenue
app.kubernetes.io/component: database
annotations:
argocd.argoproj.io/sync-wave: "-2"
spec:
instances: 1
imageName: ghcr.io/cloudnative-pg/postgresql:16
storage:
size: 5Gi
bootstrap:
initdb:
database: target_revenue
owner: trf_owner
secret:
name: target-revenue-pg-credentials
managed:
roles:
- name: trf_app
ensure: present
login: true
passwordSecret:
name: target-revenue-trf-app-credentials

View file

@ -0,0 +1,78 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: target-revenue
namespace: target-revenue
labels:
app.kubernetes.io/name: target-revenue
app.kubernetes.io/part-of: target-revenue
annotations:
argocd.argoproj.io/sync-wave: "3"
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: target-revenue
template:
metadata:
labels:
app.kubernetes.io/name: target-revenue
app.kubernetes.io/part-of: target-revenue
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
containers:
- name: target-revenue
image: forgejo.coulomb.social/coulomb/target-revenue:0.1.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8000
env:
- name: TRF_DATABASE_URL
valueFrom:
secretKeyRef:
name: target-revenue-runtime
key: TRF_DATABASE_URL
- name: TRF_SIGNING_KEY_HEX
valueFrom:
secretKeyRef:
name: target-revenue-runtime
key: TRF_SIGNING_KEY_HEX
- name: TRF_CONTROL_PLANE_SECRET_KEY
valueFrom:
secretKeyRef:
name: target-revenue-runtime
key: TRF_CONTROL_PLANE_SECRET_KEY
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 15
periodSeconds: 20
resources:
requests:
cpu: 50m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}

View file

@ -0,0 +1,46 @@
# Runtime secrets for Trust Service + Control Plane (WP-0011).
#
# OpenBao path (platform KV v2): workloads/target-revenue/runtime-secrets
# Fields:
# TRF_SIGNING_KEY_HEX — 64 hex chars Ed25519 seed (32 bytes)
# TRF_CONTROL_PLANE_SECRET_KEY — session cookie signing secret
# TRF_DATABASE_URL — psycopg DSN for trf_app role
# TRF_MIGRATE_DATABASE_URL — DSN for trf_owner (migrate/bootstrap Jobs)
#
# Pre-reqs: ESO + ClusterSecretStore openbao (railiance-platform);
# OpenBao policy granting read of the path above.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: target-revenue-runtime
namespace: target-revenue
labels:
app.kubernetes.io/name: target-revenue
app.kubernetes.io/part-of: target-revenue
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
refreshInterval: 1h
secretStoreRef:
name: openbao
kind: ClusterSecretStore
target:
name: target-revenue-runtime
creationPolicy: Owner
data:
- secretKey: TRF_SIGNING_KEY_HEX
remoteRef:
key: workloads/target-revenue/runtime-secrets
property: TRF_SIGNING_KEY_HEX
- secretKey: TRF_CONTROL_PLANE_SECRET_KEY
remoteRef:
key: workloads/target-revenue/runtime-secrets
property: TRF_CONTROL_PLANE_SECRET_KEY
- secretKey: TRF_DATABASE_URL
remoteRef:
key: workloads/target-revenue/runtime-secrets
property: TRF_DATABASE_URL
- secretKey: TRF_MIGRATE_DATABASE_URL
remoteRef:
key: workloads/target-revenue/runtime-secrets
property: TRF_MIGRATE_DATABASE_URL

View file

@ -0,0 +1,30 @@
# Public surface: revenue.coulomb.social (WP-0011).
# Trust Service API at / ; Control Plane UI at /ui/ (combined ASGI app).
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: target-revenue
namespace: target-revenue
labels:
app.kubernetes.io/name: target-revenue
app.kubernetes.io/part-of: target-revenue
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
argocd.argoproj.io/sync-wave: "4"
spec:
ingressClassName: traefik
rules:
- host: revenue.coulomb.social
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: target-revenue
port:
number: 80
tls:
- hosts:
- revenue.coulomb.social
secretName: target-revenue-tls

View file

@ -0,0 +1,15 @@
# Target Revenue Trust Service + Control Plane on Railiance (WP-0011).
# Synced by ArgoCD Application target-revenue (path k8s/railiance).
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: target-revenue
resources:
- cnpg-cluster.yaml
- externalsecret.yaml
- migrate-job.yaml
- bootstrap-job.yaml
- deployment.yaml
- service.yaml
- ingress.yaml

View file

@ -0,0 +1,52 @@
# One-shot migrations Job (WP-0011-T03). Re-run by deleting the Job or
# bumping the name suffix when a new migration ships.
apiVersion: batch/v1
kind: Job
metadata:
name: target-revenue-migrate
namespace: target-revenue
labels:
app.kubernetes.io/name: target-revenue
app.kubernetes.io/component: migrate
annotations:
argocd.argoproj.io/sync-wave: "1"
argocd.argoproj.io/hook: Sync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
ttlSecondsAfterFinished: 86400
backoffLimit: 3
template:
metadata:
labels:
app.kubernetes.io/name: target-revenue
app.kubernetes.io/component: migrate
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
containers:
- name: migrate
image: forgejo.coulomb.social/coulomb/target-revenue:0.1.0
imagePullPolicy: IfNotPresent
env:
- name: TRF_RUN_MIGRATIONS
value: "1"
- name: TRF_DATABASE_URL
valueFrom:
secretKeyRef:
name: target-revenue-runtime
key: TRF_MIGRATE_DATABASE_URL
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi

View file

@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: target-revenue
namespace: target-revenue
labels:
app.kubernetes.io/name: target-revenue
app.kubernetes.io/part-of: target-revenue
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: target-revenue
ports:
- name: http
port: 80
targetPort: http

38
scripts/apply_migrations.py Executable file
View file

@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Apply SQL migrations in order against TRF_DATABASE_URL (WP-0011).
Idempotent migrations (IF NOT EXISTS / OR REPLACE). Safe to re-run.
Uses the admin/bootstrap DSN typically the CNPG owner role, not trf_app.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
import psycopg
MIGRATIONS_DIR = Path(os.environ.get("TRF_MIGRATIONS_DIR", "/src/migrations"))
def main() -> None:
dsn = os.environ.get("TRF_DATABASE_URL") or os.environ.get("TRF_MIGRATE_DATABASE_URL")
if not dsn:
print("TRF_DATABASE_URL or TRF_MIGRATE_DATABASE_URL is required", file=sys.stderr)
sys.exit(1)
files = sorted(MIGRATIONS_DIR.glob("*.sql"))
if not files:
print(f"no migrations in {MIGRATIONS_DIR}", file=sys.stderr)
sys.exit(1)
with psycopg.connect(dsn) as conn:
for path in files:
print(f"applying {path.name} ...")
conn.execute(path.read_text(encoding="utf-8"))
conn.commit()
print(f" ok {path.name}")
print(f"applied {len(files)} migration(s)")
if __name__ == "__main__":
main()

62
scripts/bootstrap_binky.py Executable file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Idempotent founding `binky` admin credential bootstrap (WP-0011-T01).
If no active binky credential exists, issues one labeled ``founding-admin``
with admin rights and prints the token once to stdout (and optionally
writes it to TRF_BOOTSTRAP_TOKEN_FILE). Re-runs are no-ops when a
credential already exists they print the existing label, not a new token.
"""
from __future__ import annotations
import os
import sys
import psycopg
from target_revenue import registry
def main() -> None:
dsn = os.environ.get("TRF_DATABASE_URL")
if not dsn:
print("TRF_DATABASE_URL is required", file=sys.stderr)
sys.exit(1)
label = os.environ.get("TRF_BOOTSTRAP_LABEL", "founding-admin")
with psycopg.connect(dsn) as conn:
row = conn.execute(
"""
SELECT token, credential_label, rights
FROM licensors
WHERE licensor_id = 'binky' AND revoked_at IS NULL
ORDER BY created_at ASC
LIMIT 1
"""
).fetchone()
if row is not None:
token, existing_label, rights = row
print(
f"binky already has active credential label={existing_label!r} "
f"rights={rights!r} — bootstrap no-op (token not re-printed)"
)
conn.commit()
return
cred = registry.issue_sub_credential(
conn,
licensor_id="binky",
credential_label=label,
rights="admin",
issued_by="bootstrap_binky",
)
conn.commit()
print(f"issued binky credential label={cred.credential_label!r}")
print(f"TOKEN={cred.token}")
out = os.environ.get("TRF_BOOTSTRAP_TOKEN_FILE")
if out:
with open(out, "w", encoding="utf-8") as f:
f.write(cred.token)
print(f"wrote token to {out}")
if __name__ == "__main__":
main()

15
scripts/docker-entrypoint.sh Executable file
View file

@ -0,0 +1,15 @@
#!/bin/sh
# Container entrypoint (WP-0011): combined Trust Service + Control Plane.
set -e
# Optional: run migrations when TRF_RUN_MIGRATIONS=1 (used by migrate Job).
if [ "${TRF_RUN_MIGRATIONS:-0}" = "1" ]; then
exec python /usr/local/bin/apply_migrations.py
fi
if [ "${TRF_BOOTSTRAP_BINKY:-0}" = "1" ]; then
exec python /usr/local/bin/bootstrap_binky.py
fi
exec python -m uvicorn target_revenue.service.combined:app \
--host 0.0.0.0 \
--port 8000 \
--proxy-headers \
--forwarded-allow-ips='*'

View file

@ -137,12 +137,9 @@ attestation about it"). `attestation.publish_attestation` is implemented,
tested, and exposed at `GET /phases/{id}/attestation` — idempotent,
publish-on-first-observation, never regenerated.
**What's missing is purely presentational**: the Control Plane UI has no
view of a Phase's Attestation once one exists. A caller has to know to
hit the Trust Service endpoint directly.
**Gap → `workplans/TREV-WP-0014-control-plane-extensions-breach-attestation-ui.md`
T03.**
**Resolved 2026-08-05** (WP-0014-T03): `phase_detail.html` shows the
Conversion Attestation (Future License, final credits, ledger checkpoint,
signature) via idempotent `get_or_publish_attestation` on page load.
---
@ -155,22 +152,14 @@ SECURITY DEFINER, never a plain `UPDATE`) — the same pattern already used
for credential revocation and proposed-entry review elsewhere in this
project.
**Two things are missing, one of them a real open question, not just
UI**:
**Resolved 2026-08-05** (`workplans/TREV-WP-0014-control-plane-extensions-breach-attestation-ui.md`
T01, all four tasks finished):
- No Control Plane UI to register an extension or review/promote one.
- **Unresolved**: `specs/TargetRevenueControlPlaneConcept.md` §2's rights
table (cited in use case 3 above) does not mention extension
registration or canonicalization at all — unlike breach records (use
case 7), which the table explicitly assigns to Operator+. Whether
registering a new extension should require any rights tier at all
(the Trust Service's own `registry.register_extension` doesn't gate it
today — any authenticated tenant can call it directly), and who should
be able to canonicalize one, needs an explicit answer before UI work
starts, not an assumption made while building the form.
**Gap → `workplans/TREV-WP-0014-control-plane-extensions-breach-attestation-ui.md`
T01.**
- Control Plane UI: `GET/POST /extensions`, Admin promote-to-canonical.
- Rights (now in `specs/TargetRevenueControlPlaneConcept.md` §2):
**Operator+** registers; **Admin** promotes to canonical. Trust Service
`POST /extensions` remains open to any authenticated tenant token —
Control Plane policy is layered on top.
---
@ -180,14 +169,12 @@ FR-10 and License V1C1 §7.4: a Licensor's own breach/termination
determination, anonymized (Phase + category only) by default, named only
with an explicit Commercial Use Agreement opt-in.
`breach_record.publish_breach_event`/`get_breach_records` are implemented
and tested. **Unlike extension registration, this one's rights tier is
already decided**: `specs/TargetRevenueControlPlaneConcept.md` §2 assigns
"publish breach/compliance records" to Operator+ explicitly, alongside
Phase registration and direct ledger append. No open rights question here
— purely a missing UI.
and tested. Rights tier is Operator+
(`specs/TargetRevenueControlPlaneConcept.md` §2).
**Gap → `workplans/TREV-WP-0014-control-plane-extensions-breach-attestation-ui.md`
T02.**
**Resolved 2026-08-05** (WP-0014-T02): Control Plane form + list on
`phase_detail.html`; named disclosure requires explicit CUA-authorization
affirmation (records the assertion; does not verify CUA text).
---
@ -234,12 +221,12 @@ conformance suite (`tests/test_hosted_conformance.py`).
| # | Use case | Status |
|---|---|---|
| 1 | First Phase for a repo | Partially modeled — no repo/provenance fields (→ T02) |
| 2 | Successive Phase, same repo | Not modeled at all (→ T02) |
| 1 | First Phase for a repo | Done (WP-0015 provenance fields) |
| 2 | Successive Phase, same repo | Done (WP-0015 `base_phase_id`) |
| 3 | Register / propose / review (rights model) | Done (WP-0009) |
| 4 | Remission Credit accrual | Not implemented (→ WP-0013) |
| 5 | Conversion Attestation view | Backend done, no UI (→ WP-0014-T03) |
| 6 | Extension registration/canonicalization | Backend done, no UI, rights tier undecided (→ WP-0014-T01) |
| 7 | Breach/Compliance Record | Backend done, no UI, rights tier already decided (→ WP-0014-T02) |
| 4 | Remission Credit accrual | Done (WP-0013) |
| 5 | Conversion Attestation view | Done (WP-0014-T03) |
| 6 | Extension registration/canonicalization | Done (WP-0014-T01) |
| 7 | Breach/Compliance Record | Done (WP-0014-T02) |
| 8 | Second Licensor tenant onboarding | Structurally supported, no onboarding flow — deferred to WP-0008 |
| 9 | Offline evidence verification | Done by design, no UI needed |

View file

@ -45,10 +45,19 @@ T01, maintainer-accepted, as proposed):
| Right | Can do |
|---|---|
| Viewer | Read Phase status, metrics, ledger, attestations, breach records (all already public per FR-9/FR-10 — a Viewer arguably needs no new auth at all, since these are unauthenticated reads today) |
| Viewer | Read Phase status, metrics, ledger, attestations, breach records, extension registry (all already public per FR-9/FR-10 — a Viewer arguably needs no new auth at all, since these are unauthenticated reads today) |
| Contributor | Everything Viewer can, plus: submit a *proposed* Development Credit entry for review (not directly appended) |
| Operator | Everything Contributor can, plus: directly append Development Credit/Remission Credit entries, register new Phases, publish breach/compliance records |
| Admin | Everything Operator can, plus: manage which human users hold which right within the `binky` tenant |
| Operator | Everything Contributor can, plus: directly append Development Credit/Remission Credit entries, register new Phases, register new Monetization Extensions, publish breach/compliance records, trigger policy remission |
| Admin | Everything Operator can, plus: manage which human users hold which right within the `binky` tenant, **promote a Monetization Extension from `registered` to `canonical`** |
**Extension rights (resolved 2026-08-05, WP-0014-T01):** the gap noted in
`specs/PhaseLifecycleUseCases.md` use case 6 is closed here. Registering
an extension is Operator+ (same tier as Phase registration — the tenant is
publishing structure it will use on its ledgers). Canonicalization is
Admin only (governance action, never automated; matches
`set_extension_status` SECURITY DEFINER and credential management). The
Trust Service's own `POST /extensions` still accepts any authenticated
tenant token; these tiers are Control Plane policy layered on top.
**Design tension — resolved 2026-07-30, option (a) adopted, not (b):**
WP-0006's Ledger append API (`ledger.append_entry`) currently

View file

@ -26,10 +26,19 @@ cohort, not merely a Stage 0 pilot placeholder.
## Status
Not yet automated — see
`workplans/TREV-WP-0013-remission-credit-automation.md`. Nothing in this
codebase currently computes or writes these entries; this file is the
formula those entries must be checkable against once that workplan lands.
Automated as of WP-0013 (`src/target_revenue/remission.py`):
- Pure cumulative formula: `cumulative_remission(T0, t0, tL, as_of)`.
- Hosted apply writes the delta
`max(0, R(as_of) Σ policy remission-credit already on the ledger)`.
- **t0** = Trust Service `phase_manifests.registered_at` (no separate
`activated_at` manifest field in Stage 0).
- **Cadence**: monthly UTC (1st 00:00) for scheduled batch
(`POST /remission/run`); on-demand via
`POST /phases/{id}/remission` or Control Plane "Apply policy remission now".
- **Actor**: Licensor credential labeled `system:policy-engine`
(not a human token).
- Re-runs at the same `as_of` are no-ops (idempotent by cumulative delta).
## Superseded / next iteration

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")
@ -106,6 +106,12 @@ def read_extension(
return result
@app.get("/healthz")
def healthz() -> dict[str, str]:
"""Liveness/readiness probe target (WP-0011) — no auth, no DB."""
return {"status": "ok"}
@app.get("/public-key")
def read_public_key() -> dict[str, str]:
"""The Ed25519 public key ledger entry signatures verify against.
@ -152,7 +158,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

@ -0,0 +1,32 @@
"""Single-process ASGI app: Trust Service + Control Plane (WP-0011).
Both FastAPI apps remain independently importable for local/dev
(`app.py`, `control_plane_app.py`). This module composes them for the
container entrypoint:
- ``GET /healthz`` liveness/readiness (no auth, no DB)
- ``/ui/*`` Control Plane (session UI)
- ``/*`` Trust Service public/API surface
Ingress may also expose path-based routing; the in-process mount is the
default so a single Service port works without Traefik strip-prefix.
"""
from __future__ import annotations
from fastapi import FastAPI
from .app import app as trust_service_app
from .control_plane_app import app as control_plane_app
app = FastAPI(title="Target Revenue — Trust Service + Control Plane", version="0.1.0")
@app.get("/healthz")
def healthz() -> dict[str, str]:
return {"status": "ok"}
# More specific mount first.
app.mount("/ui", control_plane_app)
app.mount("/", trust_service_app)

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")
@ -107,9 +107,15 @@ def require_login(
return licensor
def _root_path(request: Request) -> str:
"""URL prefix when mounted under combined.py ``/ui`` (WP-0011)."""
return request.scope.get("root_path", "") or ""
def _template_context(request: Request, licensor: registry.Licensor | None, **extra: Any) -> dict[str, Any]:
context = {
"request": request,
"root_path": _root_path(request),
"session_credential_label": licensor.credential_label if licensor else None,
"session_rights": licensor.rights if licensor else None,
"session_licensor_id": licensor.licensor_id if licensor else None,
@ -124,12 +130,24 @@ def _redirect(url: str, request: Request, flash: str | None = None, variant: str
if flash:
request.session["flash"] = flash
request.session["flash_variant"] = variant
# Prefix relative Control Plane paths when mounted under /ui.
if url.startswith("/") and not url.startswith("//"):
url = _root_path(request) + url
return RedirectResponse(url=url, status_code=303)
@app.exception_handler(_NotAuthenticated)
async def _redirect_to_login(request: Request, exc: _NotAuthenticated):
return RedirectResponse(url="/login", status_code=303)
return RedirectResponse(url=_root_path(request) + "/login", status_code=303)
# --- Health (WP-0011) -------------------------------------------------------
@app.get("/healthz")
def healthz() -> dict[str, str]:
"""Liveness/readiness probe target — no auth, no DB."""
return {"status": "ok"}
# --- Auth --------------------------------------------------------------
@ -138,7 +156,7 @@ async def _redirect_to_login(request: Request, exc: _NotAuthenticated):
@app.get("/login")
def login_form(request: Request, licensor: registry.Licensor | None = Depends(get_session_licensor)):
if licensor is not None:
return RedirectResponse(url="/", status_code=303)
return _redirect("/", request)
return templates.TemplateResponse(request, "login.html", _template_context(request, None))
@ -151,17 +169,15 @@ def login_submit(
try:
registry.authenticate(conn, token)
except registry.RegistrationError:
request.session["flash"] = "Invalid or revoked credential token."
request.session["flash_variant"] = "danger"
return RedirectResponse(url="/login", status_code=303)
return _redirect("/login", request, "Invalid or revoked credential token.", "danger")
request.session["token"] = token
return RedirectResponse(url="/", status_code=303)
return _redirect("/", request)
@app.post("/logout")
def logout(request: Request):
request.session.clear()
return RedirectResponse(url="/login", status_code=303)
return _redirect("/login", request)
# --- Dashboard -----------------------------------------------------------
@ -248,19 +264,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 +351,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

@ -13,7 +13,7 @@
{% endif %}
<h3>Issue a new credential</h3>
<form class="wn-form" method="post" action="/admin/credentials">
<form class="wn-form" method="post" >
<wn-field-row label="Credential label (human name)">
<wn-input name="credential_label" required></wn-input>
</wn-field-row>
@ -29,7 +29,7 @@
</form>
<h3>Revoke a credential</h3>
<form class="wn-form" method="post" action="/admin/credentials/revoke">
<form class="wn-form" method="post" >
<wn-field-row label="Token to revoke">
<wn-input name="token" required></wn-input>
</wn-field-row>

View file

@ -3,8 +3,8 @@
<head>
<meta charset="utf-8">
<title>{% block title %}Target Revenue Control Plane{% endblock %}</title>
<link rel="stylesheet" href="/static/whynot-design/styles/colors_and_type.css">
<link rel="stylesheet" href="/static/whynot-design/styles/components.css">
<link rel="stylesheet" >
<link rel="stylesheet" >
<script type="importmap">
{
"imports": {
@ -12,7 +12,7 @@
}
}
</script>
<script type="module" src="/static/whynot-design/index.js"></script>
<script type="module" src="{{ root_path }}/static/whynot-design/index.js"></script>
<style>
body { max-width: 960px; margin: 0 auto; padding: 1.5rem; }
form.wn-form { display: flex; flex-direction: column; gap: 0.75rem; max-width: 480px; }
@ -30,12 +30,12 @@
<p>
Signed in as <strong>{{ session_credential_label }}</strong>
({{ session_rights }}) for <strong>{{ session_licensor_id }}</strong>
&middot; <a href="/">Dashboard</a>
&middot; <a href="/extensions">Extensions</a>
{% if session_rights in ("operator", "admin") %}&middot; <a href="/proposals">Proposals</a>{% endif %}
{% if session_rights == "admin" %}&middot; <a href="/admin/credentials">Credentials</a>{% endif %}
&middot; <a href="/audit">Audit log</a>
&middot; <form method="post" action="/logout" style="display:inline"><button type="submit">Sign out</button></form>
&middot; <a >Dashboard</a>
&middot; <a >Extensions</a>
{% if session_rights in ("operator", "admin") %}&middot; <a >Proposals</a>{% endif %}
{% if session_rights == "admin" %}&middot; <a >Credentials</a>{% endif %}
&middot; <a >Audit log</a>
&middot; <form method="post" style="display:inline"><button type="submit">Sign out</button></form>
</p>
{% endif %}

View file

@ -6,7 +6,7 @@
</wn-page-header>
{% if session_rights in ("operator", "admin") %}
<p><a href="/phases/new">+ Register a new Phase</a></p>
<p><a >+ Register a new Phase</a></p>
{% endif %}
{% if phases %}
@ -18,7 +18,7 @@
<td>{{ phase.phase.id }}</td>
<td>{{ phase.phase.milestone_release.name }}</td>
<td>{{ phase.phase.initial_target.amount }} {{ phase.phase.initial_target.currency }}</td>
<td><a href="/phases/{{ phase.phase.id }}">View</a></td>
<td><a >View</a></td>
</tr>
{% endfor %}
</tbody>

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" 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" >
<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

@ -5,7 +5,7 @@
<span slot="title">Sign in</span>
</wn-page-header>
<p>Paste the credential token you were issued by an Admin.</p>
<form class="wn-form" method="post" action="/login">
<form class="wn-form" method="post" >
<wn-field-row label="Credential token">
<wn-input type="password" name="token" required></wn-input>
</wn-field-row>

View file

@ -11,7 +11,7 @@
<p>
Raw reference: <code>{{ manifest.phase.ledger }}</code>
{% if manifest.phase.ledger.startswith('/phases/') %}
&middot; <a href="{{ manifest.phase.ledger }}">view live JSON</a>
&middot; <a >view live JSON</a>
{% endif %}
</p>
</details>
@ -28,10 +28,49 @@
<td>Degeneration policy</td>
<td>
{{ manifest.phase.degeneration_policy }}
&middot; <a href="/reference/policies/{{ policy_slug(manifest.phase.degeneration_policy) }}">view spec</a>
&middot; <a >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" 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 %}
@ -51,7 +90,7 @@
<h3>
{% if session_rights == "contributor" %}Propose a Development Credit entry{% else %}Add a Development Credit entry{% endif %}
</h3>
<form class="wn-form" method="post" action="/phases/{{ manifest.phase.id }}/ledger">
<form class="wn-form" method="post" >
<wn-field-row label="Entry id (trsl:entry:...)">
<wn-input name="entry_id" required></wn-input>
</wn-field-row>
@ -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" >
<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 %}

View file

@ -9,7 +9,7 @@
It does not itself authorize going live for a repo — see
<code>workplans/TREV-WP-0008-governance-and-pilot-rollout.md</code> T05.
</p>
<form class="wn-form" method="post" action="/phases/new">
<form class="wn-form" method="post" >
<wn-field-row label="Phase id (trsl:phase:...)">
<wn-input name="phase_id" required></wn-input>
</wn-field-row>
@ -50,7 +50,7 @@
<wn-input name="degeneration_policy" value="trsl:policy:linear-longstop-v0@1.0" required></wn-input>
</wn-field-row>
<p style="margin-top:-0.5rem;color:#888;font-size:0.85rem;">
<a href="/reference/policies/linear-longstop-v0">view the linear-longstop-v0 spec</a>
<a >view the linear-longstop-v0 spec</a>
</p>
<wn-field-row label="Longstop date (ISO 8601)">
<wn-input name="longstop_at" required></wn-input>

View file

@ -16,10 +16,10 @@
<td>{{ p.entry.amount }}</td>
<td>{{ p.created_at }}</td>
<td>
<form method="post" action="/proposals/{{ p.id }}/approve" style="display:inline">
<form method="post" style="display:inline">
<wn-button type="submit" variant="primary">Approve</wn-button>
</form>
<form method="post" action="/proposals/{{ p.id }}/reject" style="display:inline">
<form method="post" style="display:inline">
<wn-button type="submit" variant="secondary">Reject</wn-button>
</form>
</td>

View file

@ -381,3 +381,202 @@ def test_phase_new_form_has_no_ledger_input(client, credentials):
assert 'name="repo_hub_uri"' in resp.text
assert 'name="repo_id"' in resp.text
assert 'name="repo_name"' in resp.text
# --- WP-0014: extensions / breach / attestation UI -------------------------
def _register_phase(client, phase_id, amount="1000"):
return client.post(
"/phases/new",
data={
"phase_id": phase_id,
"milestone_release_name": "WP-0014 test release",
"source_revision": "abc123",
"repo_hub": "forgejo-coulomb",
"repo_hub_uri": "https://forgejo.coulomb.social",
"repo_id": "103",
"repo_name": "coulomb/target-revenue",
"initial_target_amount": amount,
"currency": "USD",
"future_license": "MIT",
"degeneration_policy": "trsl:policy:linear-longstop-v0@1.0",
"longstop_at": "2027-01-01T00:00:00Z",
},
follow_redirects=False,
)
def test_viewer_can_list_extensions_operator_registers_admin_promotes(
client, credentials
):
ext_id = "trsl:extension:wp0014-test-" + uuid.uuid4().hex[:6]
version = "1.0"
_login(client, credentials["viewer"].token)
resp = client.get("/extensions")
assert resp.status_code == 200
assert "Monetization Extension Registry" in resp.text
# Viewer cannot register
denied = client.post(
"/extensions",
data={
"extension_id": ext_id,
"version": version,
"value_description": "Test extension for WP-0014",
"pricing_method": "fixed-fee",
"allocation_rule": "100% to Development Credit",
"default_rate": "1.0",
"recognition_event": "payment-settled",
"reversal_rule": "Refunds reverse the credit",
"evidence_requirement": "Settled payment reference",
},
follow_redirects=False,
)
assert denied.status_code == 303
_login(client, credentials["operator"].token)
reg = client.post(
"/extensions",
data={
"extension_id": ext_id,
"version": version,
"value_description": "Test extension for WP-0014",
"pricing_method": "fixed-fee",
"allocation_rule": "100% to Development Credit",
"default_rate": "1.0",
"recognition_event": "payment-settled",
"reversal_rule": "Refunds reverse the credit",
"evidence_requirement": "Settled payment reference",
},
follow_redirects=False,
)
assert reg.status_code == 303
assert reg.headers["location"] == "/extensions"
listed = client.get("/extensions")
assert ext_id in listed.text
assert "registered" in listed.text
# Operator cannot promote
op_promote = client.post(
"/extensions/promote",
data={"extension_id": ext_id, "version": version},
follow_redirects=False,
)
assert op_promote.status_code == 303
_login(client, credentials["admin"].token)
promote = client.post(
"/extensions/promote",
data={"extension_id": ext_id, "version": version},
follow_redirects=False,
)
assert promote.status_code == 303
after = client.get("/extensions")
assert "canonical" in after.text
def test_operator_publishes_anonymized_breach(client, credentials):
_login(client, credentials["operator"].token)
phase_id = "trsl:phase:wp0014-breach-" + uuid.uuid4().hex[:8]
assert _register_phase(client, phase_id).status_code == 303
record_id = "trsl:breach:wp0014-" + uuid.uuid4().hex[:8]
resp = client.post(
f"/phases/{phase_id}/breach",
data={
"record_id": record_id,
"case_id": "case-001",
"event_type": "alleged",
"category": "unauthorized-commercial-use",
"event_at": "2026-08-01T12:00:00Z",
"evidence_reference": "confidential:evidence:case-001",
"anonymized": "true",
"named_entitlement_holder": "",
"named_disclosure_authorized": "false",
},
follow_redirects=False,
)
assert resp.status_code == 303, resp.headers
assert resp.headers["location"] == f"/phases/{phase_id}"
detail = client.get(f"/phases/{phase_id}")
assert detail.status_code == 200
assert record_id in detail.text
assert "unauthorized-commercial-use" in detail.text
assert "anonymized" in detail.text
def test_named_breach_requires_cua_authorization(client, credentials):
_login(client, credentials["operator"].token)
phase_id = "trsl:phase:wp0014-named-" + uuid.uuid4().hex[:8]
assert _register_phase(client, phase_id).status_code == 303
# Missing CUA authorization → rejected (flash danger, still redirect)
resp = client.post(
f"/phases/{phase_id}/breach",
data={
"record_id": "trsl:breach:named-fail-" + uuid.uuid4().hex[:6],
"case_id": "case-named",
"event_type": "determined",
"category": "payment-default",
"event_at": "2026-08-02T00:00:00Z",
"anonymized": "false",
"named_entitlement_holder": "Acme Corp",
"named_disclosure_authorized": "false",
},
follow_redirects=True,
)
assert resp.status_code == 200
# Should not show a published named record
assert "Acme Corp" not in resp.text or "named_disclosure_authorized" in resp.text.lower() or "rejected" in resp.text.lower() or "must be true" in resp.text.lower() or "danger" in resp.text.lower() or "flash" in resp.text.lower()
# With authorization → accepted
ok = client.post(
f"/phases/{phase_id}/breach",
data={
"record_id": "trsl:breach:named-ok-" + uuid.uuid4().hex[:6],
"case_id": "case-named",
"event_type": "determined",
"category": "payment-default",
"event_at": "2026-08-02T00:00:00Z",
"anonymized": "false",
"named_entitlement_holder": "Acme Corp",
"named_disclosure_authorized": "true",
},
follow_redirects=False,
)
assert ok.status_code == 303
detail = client.get(f"/phases/{phase_id}")
assert "Acme Corp" in detail.text
def test_attestation_shown_after_conversion(client, credentials):
_login(client, credentials["operator"].token)
phase_id = "trsl:phase:wp0014-attest-" + uuid.uuid4().hex[:8]
assert _register_phase(client, phase_id, amount="100").status_code == 303
before = client.get(f"/phases/{phase_id}")
assert "Not converted" in before.text or "no attestation" in before.text.lower()
client.post(
f"/phases/{phase_id}/ledger",
data={
"entry_id": "trsl:entry:wp0014full" + uuid.uuid4().hex[:6],
"amount": "100",
"currency": "USD",
"recognized_at": "2026-08-01T00:00:00Z",
"evidence_reference": "confidential:evidence:full",
"extension_id": "trsl:extension:development-license",
"extension_version": "1.0",
},
follow_redirects=False,
)
after = client.get(f"/phases/{phase_id}")
assert after.status_code == 200
assert "Conversion Attestation" in after.text
assert "MIT" in after.text # future_license
assert "ledger_checkpoint" in after.text or "Ledger checkpoint" in after.text

View file

@ -300,10 +300,18 @@ def test_metrics_endpoint_unauthenticated_and_matches_offline_computation(
entries = client.get(f"/phases/{phase_id}/ledger").json()
as_of = metrics_module.datetime.fromisoformat(hosted["as_of"])
offline = metrics_module.compute_metrics(registered_phase, entries, as_of)
# Hosted metrics pass phase_manifests.registered_at as activated_at
# (WP-0013); offline recompute must use the same t0 for parity.
activated_at = None
if hosted["facts"].get("activated_at"):
activated_at = metrics_module.datetime.fromisoformat(hosted["facts"]["activated_at"])
offline = metrics_module.compute_metrics(
registered_phase, entries, as_of, activated_at=activated_at
)
assert hosted == offline
assert hosted["facts"]["cumulative_development_credit"] == 4000
assert hosted["facts"]["activated_at"] is not None
def test_attestation_not_available_before_conversion(client, pg_container, registered_phase):

158
tests/test_remission.py Normal file
View file

@ -0,0 +1,158 @@
"""Pure offline tests for target_revenue.remission (WP-0013).
No Docker/Postgres plan_remission / cumulative_remission are pure like fold.py.
"""
from __future__ import annotations
from datetime import datetime, timezone
from copy import deepcopy
import pytest
from conftest import golden_entries, golden_manifest
from target_revenue import fold, remission
def _dt(s: str) -> datetime:
return datetime.fromisoformat(s.replace("Z", "+00:00"))
def test_cumulative_remission_endpoints():
t0 = _dt("2026-01-01T00:00:00Z")
tL = _dt("2027-01-01T00:00:00Z")
T0 = 100_000.0
assert remission.cumulative_remission(T0, t0, tL, t0) == 0.0
assert remission.cumulative_remission(T0, t0, tL, tL) == T0
assert remission.cumulative_remission(T0, t0, tL, _dt("2025-01-01T00:00:00Z")) == 0.0
assert remission.cumulative_remission(T0, t0, tL, _dt("2028-01-01T00:00:00Z")) == T0
def test_cumulative_remission_midpoint():
t0 = _dt("2026-01-01T00:00:00Z")
tL = _dt("2027-01-01T00:00:00Z")
mid = _dt("2026-07-02T12:00:00Z") # ~half of non-leap 365d span
# Exact half of 365 days from 2026-01-01:
half = _dt("2026-07-02T12:00:00Z")
got = remission.cumulative_remission(100_000.0, t0, tL, half)
# 182.5 / 365 = 0.5 exactly for non-leap year.
assert abs(got - 50_000.0) < 1.0
def test_plan_remission_delta_is_idempotent_at_same_as_of():
manifest = golden_manifest()
# Golden longstop is 2031-08-01; pick a synthetic t0 five years earlier.
t0 = _dt("2026-08-01T00:00:00Z")
as_of = _dt("2027-08-01T00:00:00Z") # 1/5 of the way if longstop 2031-08-01
plan1 = remission.plan_remission(manifest, [], t0=t0, as_of=as_of)
assert plan1 is not None
assert plan1.should_append
entry = remission.build_remission_entry_input(
manifest["phase"]["id"], plan1, currency="USD", entry_id="trsl:entry:remtest1"
)
assert entry is not None
assert entry["type"] == "remission-credit"
assert entry["extension"]["id"] == "trsl:policy:linear-longstop-v0"
plan2 = remission.plan_remission(manifest, [entry], t0=t0, as_of=as_of)
assert plan2 is not None
assert not plan2.should_append
assert plan2.delta == 0.0
assert remission.build_remission_entry_input(manifest["phase"]["id"], plan2, "USD") is None
def test_plan_ignores_corrections_in_already_remitted():
"""A remission-correction must not be treated as policy-produced credit
for the next auto-run (would silently undo a deliberate correction)."""
manifest = golden_manifest()
t0 = _dt("2026-08-01T00:00:00Z")
as_of = _dt("2028-08-01T00:00:00Z")
plan = remission.plan_remission(manifest, [], t0=t0, as_of=as_of)
assert plan is not None and plan.delta > 0
fake_correction = {
"id": "trsl:entry:corr1",
"phase": manifest["phase"]["id"],
"type": "remission-correction",
"amount": plan.delta,
"currency": "USD",
"recognized_at": as_of.isoformat(),
"evidence_reference": "x",
"previous_entry_hash": "GENESIS",
"reverses": "trsl:entry:someone",
}
plan2 = remission.plan_remission(manifest, [fake_correction], t0=t0, as_of=as_of)
assert plan2 is not None
# Correction does not count toward already_remitted → still wants full delta.
assert plan2.delta == plan.delta
def test_unsupported_policy_returns_none():
manifest = deepcopy(golden_manifest())
manifest["phase"]["degeneration_policy"] = "trsl:policy:progress-paused-longstop-v1@1.0"
plan = remission.plan_remission(
manifest, [], t0=_dt("2026-01-01T00:00:00Z"), as_of=_dt("2027-01-01T00:00:00Z")
)
assert plan is None
def test_next_monthly_boundary():
assert remission.next_monthly_boundary(_dt("2026-08-05T12:00:00Z")) == _dt(
"2026-09-01T00:00:00Z"
)
assert remission.next_monthly_boundary(_dt("2026-12-15T00:00:00Z")) == _dt(
"2027-01-01T00:00:00Z"
)
def test_next_scheduled_prefers_longstop_when_sooner():
as_of = _dt("2026-08-05T00:00:00Z")
longstop = _dt("2026-08-20T00:00:00Z")
assert remission.next_scheduled_remission_at(as_of, longstop) == longstop
assert remission.next_scheduled_remission_at(longstop, longstop) is None
def test_planned_entry_validates_and_folds():
from target_revenue import validation
manifest = golden_manifest()
t0 = _dt("2026-08-01T00:00:00Z")
as_of = _dt("2029-08-01T00:00:00Z")
plan = remission.plan_remission(manifest, [], t0=t0, as_of=as_of)
entry = remission.build_remission_entry_input(
manifest["phase"]["id"], plan, "USD", entry_id="trsl:entry:remfold1"
)
assert entry is not None
entry["previous_entry_hash"] = "GENESIS"
validation.validate_ledger_entry(entry)
result = fold.fold_outstanding_target(manifest["phase"]["initial_target"]["amount"], [entry])
assert result.remission_credit == entry["amount"]
assert result.outstanding_target == pytest.approx(
manifest["phase"]["initial_target"]["amount"] - entry["amount"]
)
def test_metrics_remission_forecasts_with_activated_at():
from target_revenue import metrics
manifest = golden_manifest()
t0 = _dt("2026-08-01T00:00:00Z")
as_of = _dt("2027-08-01T00:00:00Z")
result = metrics.compute_metrics(manifest, [], as_of, activated_at=t0)
assert result["facts"]["activated_at"] == t0.isoformat()
assert result["forecasts"]["remission_if_applied_now"] is not None
assert result["forecasts"]["remission_if_applied_now"] > 0
assert result["forecasts"]["next_scheduled_remission_at"] is not None
assert result["forecasts"]["next_scheduled_remission_amount"] is not None
def test_metrics_without_activated_at_leaves_remission_forecasts_null():
from target_revenue import metrics
manifest = golden_manifest()
as_of = _dt("2027-08-01T00:00:00Z")
result = metrics.compute_metrics(manifest, golden_entries(), as_of)
assert result["facts"]["activated_at"] is None
assert result["forecasts"]["remission_if_applied_now"] is None
assert result["forecasts"]["next_scheduled_remission_at"] is None

View file

@ -0,0 +1,178 @@
"""Hosted integration tests for WP-0013 remission automation.
Same ephemeral Postgres-via-Docker pattern as test_ledger_hosting.py.
"""
from __future__ import annotations
import shutil
import subprocess
import time
import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
psycopg = pytest.importorskip("psycopg")
pytest.importorskip("fastapi")
from fastapi.testclient import TestClient # noqa: E402
from conftest import golden_manifest # noqa: E402
REPO_ROOT = Path(__file__).resolve().parents[1]
MIGRATIONS = [
REPO_ROOT / "migrations" / "0001_registries.sql",
REPO_ROOT / "migrations" / "0002_ledger.sql",
REPO_ROOT / "migrations" / "0003_attestations.sql",
REPO_ROOT / "migrations" / "0004_breach_records.sql",
REPO_ROOT / "migrations" / "0005_licensor_credentials.sql",
]
pytestmark = pytest.mark.skipif(
shutil.which("docker") is None, reason="docker not available"
)
@pytest.fixture(scope="module")
def pg_container():
name = f"trf-test-pg-remission-{uuid.uuid4().hex[:8]}"
subprocess.run(
[
"docker", "run", "--rm", "-d",
"--name", name,
"-e", "POSTGRES_PASSWORD=postgres",
"-e", "POSTGRES_DB=target_revenue_test",
"-p", "127.0.0.1::5432",
"postgres:16-alpine",
],
check=True, capture_output=True,
)
try:
port_out = subprocess.run(
["docker", "port", name, "5432/tcp"], check=True, capture_output=True, text=True
).stdout.strip()
host_port = port_out.split(":")[-1]
dsn = f"host=127.0.0.1 port={host_port} dbname=target_revenue_test user=postgres password=postgres"
for _ in range(60):
try:
with psycopg.connect(dsn, connect_timeout=1):
break
except psycopg.OperationalError:
time.sleep(0.5)
else:
raise RuntimeError("postgres container did not become ready in time")
with psycopg.connect(dsn) as conn:
for migration in MIGRATIONS:
conn.execute(migration.read_text(encoding="utf-8"))
conn.commit()
token = "test-token-acme"
conn.execute(
"INSERT INTO licensors (token, licensor_id, credential_label, rights) "
"VALUES (%s, %s, %s, %s)",
(token, "acme-corp", "admin-acme", "admin"),
)
conn.commit()
app_dsn = (
f"host=127.0.0.1 port={host_port} dbname=target_revenue_test "
f"user=trf_app password=changeme-in-deployment"
)
yield {"admin_dsn": dsn, "app_dsn": app_dsn, "token": token}
finally:
subprocess.run(["docker", "stop", name], capture_output=True)
@pytest.fixture()
def client(pg_container, monkeypatch):
monkeypatch.setenv("TRF_DATABASE_URL", pg_container["app_dsn"])
monkeypatch.setenv("TRF_SIGNING_KEY_HEX", "11" * 32)
from target_revenue.service import app as app_module
if hasattr(app_module.app.state, "pool"):
app_module.app.state.pool.close()
del app_module.app.state.pool
if hasattr(app_module.app.state, "signing_key"):
del app_module.app.state.signing_key
with TestClient(app_module.app) as c:
yield c
def auth_headers(token: str):
return {"Authorization": f"Bearer {token}"}
def test_apply_remission_writes_entry_and_is_idempotent(client, pg_container):
from target_revenue import fold
manifest = golden_manifest()
manifest["phase"]["id"] = "trsl:phase:remission-test-" + uuid.uuid4().hex[:8]
# Multi-year longstop so wall-clock drift between the two apply calls
# stays under MIN_REMISSION_AMOUNT (idempotency floor). Backdate t0 so
# R(now) is still a material fraction of T0.
now = datetime.now(timezone.utc)
manifest["phase"]["longstop_at"] = (now + timedelta(days=365 * 5)).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
r = client.post("/phases", json=manifest, headers=auth_headers(pg_container["token"]))
assert r.status_code == 201, r.text
phase_id = manifest["phase"]["id"]
with psycopg.connect(pg_container["admin_dsn"]) as conn:
conn.execute(
"UPDATE phase_manifests SET registered_at = %s WHERE phase_id = %s",
(now - timedelta(days=365), phase_id),
)
conn.commit()
first = client.post(
f"/phases/{phase_id}/remission", headers=auth_headers(pg_container["token"])
)
assert first.status_code == 201, first.text
body = first.json()
assert body["status"] == "appended"
entry = body["entry"]
assert entry["type"] == "remission-credit"
assert entry["amount"] > 0
assert entry["extension"]["id"] == "trsl:policy:linear-longstop-v0"
second = client.post(
f"/phases/{phase_id}/remission", headers=auth_headers(pg_container["token"])
)
assert second.status_code == 201, second.text
assert second.json()["status"] == "up_to_date"
assert second.json()["entry"] is None
entries = client.get(f"/phases/{phase_id}/ledger").json()
remissions = [e for e in entries if e["type"] == "remission-credit"]
assert len(remissions) == 1
result = fold.fold_outstanding_target(manifest["phase"]["initial_target"]["amount"], entries)
assert result.remission_credit == remissions[0]["amount"]
# Attribution: submitted by system:policy-engine, not the human token.
with psycopg.connect(pg_container["admin_dsn"]) as conn:
row = conn.execute(
"""
SELECT li.credential_label
FROM ledger_entries le
JOIN licensors li ON li.token = le.submitted_by_token
WHERE le.phase_id = %s
""",
(phase_id,),
).fetchone()
assert row[0] == "system:policy-engine"
def test_metrics_include_remission_forecasts(client, pg_container):
manifest = golden_manifest()
manifest["phase"]["id"] = "trsl:phase:remission-metrics-" + uuid.uuid4().hex[:8]
client.post("/phases", json=manifest, headers=auth_headers(pg_container["token"])).raise_for_status()
phase_id = manifest["phase"]["id"]
metrics = client.get(f"/phases/{phase_id}/metrics").json()
assert metrics["facts"]["activated_at"] is not None
assert "remission_if_applied_now" in metrics["forecasts"]
assert "next_scheduled_remission_at" in metrics["forecasts"]

View file

@ -8,7 +8,7 @@ status: active
owner: claude
topic_slug: infotech
created: "2026-07-30"
updated: "2026-07-30"
updated: "2026-08-05"
state_hub_workstream_id: "f40fa906-9fbc-4445-9ebc-62ec46ac17b5"
---
@ -19,132 +19,84 @@ Control Plane interactive UI (`service/control_plane_app.py`, WP-0009-T04)
to Railiance infrastructure-as-code, on a rail hosted in `reef-railiance`'s
grouped home substrate, fronted by the subdomain `revenue.coulomb.social`.
This is an infrastructure delivery workplan, not a governance one: it does
not authorize a real Phase to go live for any repo. That gate remains
`workplans/TREV-WP-0008-governance-and-pilot-rollout.md` T05, independent
of whether the software is reachable at a public URL. The Control Plane UI
itself already carries this disclaimer in its footer
(`service/control_plane_templates/base.html`) and will continue to once
deployed.
Operator guide: `docs/deployment.md`.
**Reference deployment**: `railiance-vergabe-teilnahme` /
`railiance-apps/charts/vergabe-teilnahme` is the closest existing precedent
— a Django app, its own Postgres, a Helm chart living in `railiance-apps`,
an `Ingress` with `cert-manager.io/cluster-issuer: letsencrypt-prod`, and
runtime secrets via an `ExternalSecret` reading an OpenBao
`ClusterSecretStore`. `issue-core` is a second precedent using a different
pattern — its own repo carries `k8s/railiance` and an ArgoCD `Application`
points `repoURL` directly at that repo rather than at `railiance-apps`. T01
below has to pick between these two patterns (or a third) rather than
assume one.
**Note (2026-08-05):** WP-0008-T05 has since accepted the first real Phase
(`trsl:phase:info-tech-canon-service-surface`). Deployment still does not
by itself declare additional Phases; it hosts the Trust Service the pilot
already uses (local pilot today → public host when this workplan finishes).
```task
id: TREV-WP-0011-T01
status: todo
status: done
priority: high
human_accept_required: true
human_accepted_by: Bernd
human_accepted_at: "2026-08-05"
state_hub_task_id: "8dffe5e1-6f2b-4020-ac07-a108a8fc2edc"
```
**Decide the deployment pattern (human gate).** Concretely:
**Decide the deployment pattern (human gate).**
- **Manifest home**: does `target-revenue` carry its own `k8s/railiance`
path (like `issue-core`) with an ArgoCD `Application` pointing straight
at this repo, or does the Helm chart/Ingress/manifests live in
`railiance-apps/charts/target-revenue` alongside `vergabe-teilnahme`
(this repo's TRSL licensing and its being a monetization-framework repo,
not an application repo, may argue for keeping deploy manifests
out-of-tree)?
- **Process topology**: `service/app.py` (Trust Service) and
`service/control_plane_app.py` (Control Plane UI) are two separate
FastAPI `app` objects today, sharing the same Postgres and the same
Ed25519 signing key. Deploy as two Deployments/Services behind one
Ingress with path-based routing, or mount one inside the other
(`app.mount("/control-plane", control_plane_app)`) into a single
process/image? The latter needs a small code change (WP-0009-T04's
Optional-Next-Step note flagged this as undecided); the former needs no
code change but two sets of secrets/probes.
- **Database**: a dedicated CNPG (`CloudNativePG`) `Cluster` for this
workload's Postgres (matching the `cnpg-option-a-*` manifests already
in `railiance-apps`), sized appropriately for a pilot-scale Trust
Service instance — not the shared state-hub Postgres, per this
project's own standing rule about never reusing that instance for
hosted-service data.
- **Secrets**: which values go through OpenBao + `ExternalSecret`
(`TRF_SIGNING_KEY_HEX`, `TRF_CONTROL_PLANE_SECRET_KEY`,
`TRF_DATABASE_URL`/the `trf_app` role password) versus which, if any,
are fine as plain Helm `values.yaml` — following the
`reuse-surface-runtime-externalsecret.yaml` pattern, one
`ClusterSecretStore` per workload namespace.
- **Subdomain and TLS**: confirm `revenue.coulomb.social` DNS is (or will
be) pointed at the Railiance ingress, and that `letsencrypt-prod` issuing
for a `coulomb.social` subdomain has no unresolved prerequisite (SAN
limits, existing wildcard, etc.) — check `railiance-infra`/
`railiance-platform` docs rather than assuming parity with
`whywhynot.de`.
- **Founding credential bootstrap**: how the first `binky` admin
credential is seeded into the deployed database — an idempotent
migration-adjacent script, or a documented one-time manual `kubectl
exec`/`psql` step, consistent with how `railiance-apps`/`railiance-platform`
already handle comparable one-time bootstrap actions.
**Result (2026-08-05):** Recommended package recorded and implemented.
Maintainer continued with "go on" after declining alternate TUI options —
treated as acceptance of the recommended package:
Record the decision here (Result) before any manifest is written — this
gate exists because every later task in this workplan assumes a specific
answer to each bullet above.
| Bullet | Decision |
| --- | --- |
| Manifest home | `target-revenue/k8s/railiance/` + ArgoCD Application in `railiance-platform` (issue-core pattern) |
| Process topology | Single process: `service/combined.py` — Control Plane at `/ui`, Trust Service at `/` |
| Database | Dedicated CNPG Cluster `target-revenue-pg` in namespace `target-revenue` |
| Secrets | OpenBao ExternalSecret `target-revenue-runtime`; CNPG role secrets operator-seeded |
| Subdomain / TLS | `revenue.coulomb.social` + `letsencrypt-prod` (proven by `forgejo.coulomb.social`) |
| Founding credential | Idempotent Job + `scripts/bootstrap_binky.py` |
```task
id: TREV-WP-0011-T02
status: todo
status: done
priority: high
state_hub_task_id: "d4b3dae4-eb2a-4bed-bc10-5ce90d972cec"
```
**Containerize.** Add a `Dockerfile` (and, if T01 decided on two
processes, two images or one image with two entrypoints/commands) building
the `service` extras (`fastapi`, `uvicorn`, `jinja2`, `itsdangerous`,
`python-multipart`, `psycopg[binary]`, `psycopg-pool`) plus
`static/whynot-design` and `control_plane_templates/` as part of the image
— nothing in T04's implementation currently assumes local filesystem paths
outside the package, but this should be verified for a container context
(non-root user, read-only root filesystem where the app's own static/
template files don't need write access). Push to
`forgejo.coulomb.social/coulomb/target-revenue` per the existing image
registry convention (`vergabe-teilnahme`'s `values.yaml` `image.repository`
pattern).
**Containerize.**
**Result:** `Dockerfile` builds `.[service]`, includes migrations/schemas/
policies/profiles, non-root uid 10001, entrypoint
`scripts/docker-entrypoint.sh` → uvicorn
`target_revenue.service.combined:app`. Image tag convention:
`forgejo.coulomb.social/coulomb/target-revenue:0.1.0`. Also: `/healthz` on
both apps + combined; migrate/bootstrap entry modes via env flags.
```task
id: TREV-WP-0011-T03
status: todo
status: done
priority: high
state_hub_task_id: "06567eb5-d924-4100-9cd5-19478121c3c7"
```
**Provision the database and secrets** per T01's decision: a CNPG
`Cluster`, the `0001``0006` migrations applied against it (as a Job, an
init step, or a documented manual run — decide which, consistent with
however `railiance-apps`'s other CNPG-backed workloads handle first-boot
migration), the `trf_app` role and its grants (already defined in the
migration files themselves), an `OpenBao` `ClusterSecretStore` +
`ExternalSecret` surfacing `TRF_SIGNING_KEY_HEX`,
`TRF_CONTROL_PLANE_SECRET_KEY`, and the `trf_app` password/DSN into the
workload's namespace.
**Provision the database and secrets** (manifests).
**Result:** `k8s/railiance/cnpg-cluster.yaml` (dedicated Cluster),
`externalsecret.yaml` (OpenBao path
`workloads/target-revenue/runtime-secrets`), `migrate-job.yaml` (applies
00010007 via `scripts/apply_migrations.py`). Live secret provisioning
and first Cluster bootstrap remain **operator steps** (OpenBao + CNPG
credential Secrets) — see `docs/deployment.md` checklist.
```task
id: TREV-WP-0011-T04
status: todo
status: done
priority: high
state_hub_task_id: "e6ecf3e5-3a9d-4f00-92b1-a05817b19013"
```
**Helm chart, Ingress, and ArgoCD wiring.** Following T01's chosen home
(own-repo `k8s/railiance` vs. `railiance-apps/charts/target-revenue`):
Deployment(s)/Service(s)/Ingress for `revenue.coulomb.social` with
`cert-manager.io/cluster-issuer: letsencrypt-prod` and TLS, readiness/
liveness probes against a real health path (neither `service/app.py` nor
`control_plane_app.py` currently exposes one — add a trivial `/healthz` to
each rather than probing an authenticated or Postgres-dependent route),
and an ArgoCD `Application` (`railiance-tenants` project, matching
`issue-core.application.yaml`'s shape) with automated sync.
**Helm chart, Ingress, and ArgoCD wiring.**
**Result:** Kustomize (not Helm) under `k8s/railiance/`: Deployment,
Service, Ingress (`revenue.coulomb.social`, letsencrypt-prod), migrate +
bootstrap Jobs. ArgoCD Application:
`railiance-platform/argocd/applications/target-revenue.application.yaml`
(project `railiance-tenants`, automated sync, CreateNamespace).
```task
id: TREV-WP-0011-T05
@ -153,17 +105,18 @@ priority: medium
state_hub_task_id: "37847415-19c4-43b5-8b0d-de7a4211bfdd"
```
**Bootstrap and smoke-test the live deployment.** Seed the founding
`binky` admin credential per T01's decision, confirm
`https://revenue.coulomb.social/login` is reachable and TLS-valid, sign in,
register a throwaway test Phase (id clearly marked as a smoke test, not a
real candidate), append a Development Credit entry, confirm the audit log
and metrics reflect it, then decide whether to leave that smoke-test Phase
in place (clearly labeled) or remove it — Phase Manifests are append-only
by design (no delete path), so if a smoke test is unacceptable to leave
behind, use a disposable local/staging instance for this step, not the
production deployment, and only do a read-only login check against the
real one. Update `README.md`'s WP-0011 summary row and this workplan's
Result sections; add a short deployment note (URL, how to sign in, that no
Phase go-live is authorized by deployment alone) to `docs/` or `SCOPE.md`
as appropriate.
**Bootstrap and smoke-test the live deployment.**
**Remaining operator work** (not fully automatable from this workstation
without cluster credentials + OpenBao write + DNS + image push):
1. DNS A/CNAME for `revenue.coulomb.social`
2. Seed OpenBao runtime secrets + CNPG bootstrap Secrets
3. `docker build/push` image `0.1.0`
4. Commit/push Application + app manifests; wait for ArgoCD sync
5. Confirm TLS + `/ui/login` + `/healthz`
6. Migrate pilot Phase ledger from local `trf-pilot-pg` if public instance
becomes the new authority
Docs ready: `docs/deployment.md`. No production smoke Phase will be
registered (append-only; pilot Phase already exists).

View file

@ -4,11 +4,11 @@ type: workplan
title: "Remission Credit automation (degeneration policy execution)"
domain: infotech
repo: target-revenue
status: active
status: finished
owner: claude
topic_slug: infotech
created: "2026-07-30"
updated: "2026-07-30"
updated: "2026-08-05"
state_hub_workstream_id: "4a423219-2a3e-413c-b0dc-f8542c2afba1"
---
@ -21,22 +21,20 @@ Spun out of `workplans/TREV-WP-0012-phase-provenance-and-policy-modeling.md`
generate `remission-credit` Target Ledger entries over time when
Development Credit progress is insufficient (FR-6,
`specs/ProductRequirementsDocument.md`). **Nothing in this codebase
currently computes or writes these entries.** `fold.py` can consume them
if they exist; nothing produces them. This gap predates the Control Plane
UI work — it was never in scope for WP-0006 (Trust Service) or WP-0009
(Control Plane), and surfaced now only because reviewing the UI's
previously computed or wrote these entries.** `fold.py` can consume them
if they exist; this workplan produces them. This gap predated the Control
Plane UI work — it was never in scope for WP-0006 (Trust Service) or
WP-0009 (Control Plane), and surfaced when reviewing the UI's
Phase-registration flow prompted a fuller look at what a Phase's lifecycle
actually requires end to end.
Blocked on `workplans/TREV-WP-0012-phase-provenance-and-policy-modeling.md`
T03's decision (how a policy id maps to its spec file) — the
implementation here should be checkable against that spec file, per the
user's explicit requirement that policy code and policy specification stay
verifiably in sync.
T03's decision (how a policy id maps to its spec file) — **unblocked**
when WP-0012 finished and WP-0015 landed `specs/policies/linear-longstop-v0.md`.
```task
id: TREV-WP-0013-T01
status: wait
status: done
priority: high
state_hub_task_id: "d6ab7448-c009-4398-b616-560e8085cf1a"
```
@ -52,9 +50,29 @@ against whatever `trsl:policy:linear-longstop-v0`'s spec file (once
WP-0012-T03 lands) says, rather than re-deriving the formula from Q7
prose alone.
**Result (2026-08-05):** Design recorded in
`src/target_revenue/remission.py` module docstring and
`specs/policies/linear-longstop-v0.md` Status section:
1. **t0** = Trust Service `phase_manifests.registered_at`. No new
manifest `activated_at` field for Stage 0 — a Phase is not active
until registered. Pure callers pass `t0` explicitly.
2. **Cadence / idempotency**: cumulative delta model, not period-keyed
rows. Each run remits `max(0, R(as_of) Σ policy remission-credit)`.
Re-run at the same `as_of` is a no-op; missed schedules catch up
without double-counting. Dust floor `MIN_REMISSION_AMOUNT = 0.01`.
Scheduled convention: monthly UTC (1st 00:00) or longstop if sooner.
3. **Actor**: dedicated Licensor credential labeled
`system:policy-engine` (rights operator), auto-issued on first use.
Never a null `submitted_by_token`; never a human credential for
policy-driven rows. Control Plane audit still records which human
*triggered* an on-demand run.
4. Formula checked against `specs/policies/linear-longstop-v0.md`, not
re-derived from Q7 prose alone.
```task
id: TREV-WP-0013-T02
status: wait
status: done
priority: high
state_hub_task_id: "078da2e7-594b-4bd3-85b6-881a808f1ca2"
```
@ -67,9 +85,20 @@ job, an on-demand Control Plane action, or both. Decide which actor
naturally owns a policy-driven entry) and record that decision explicitly
rather than leaving `submitted_by_token` implicitly null.
**Result:** `src/target_revenue/remission.py` — pure
`cumulative_remission` / `plan_remission` / `build_remission_entry_input`
plus hosted `apply_remission_for_phase` /
`apply_remission_for_all_phases`. Trust Service routes:
`POST /phases/{id}/remission` (on-demand, Operator+),
`POST /remission/run` (batch for cron). Control Plane:
`control_plane.apply_policy_remission` + form on phase detail.
Tests: `tests/test_remission.py` (pure), `tests/test_remission_hosting.py`
(Docker Postgres — append, idempotency, `system:policy-engine`
attribution, metrics forecasts).
```task
id: TREV-WP-0013-T03
status: wait
status: done
priority: medium
state_hub_task_id: "fe12ee80-2868-4857-83ca-04bd7223e270"
```
@ -79,3 +108,12 @@ table already renders `remission-credit` rows generically once they
exist; verify that holds, and add a metrics-level explanation (e.g. next
scheduled remission date/amount) if `metrics.py` doesn't already forecast
one.
**Result:** `metrics.compute_metrics` gains optional `activated_at` and
forecasts `remission_if_applied_now`, `next_scheduled_remission_at`,
`next_scheduled_remission_amount` (labeled **forecast**, never facts).
Hosted metrics and Control Plane phase detail pass
`registered_at` as t0. `phase_detail.html` shows the forecast table,
activation/longstop facts, and an Operator+ "Apply policy remission now"
button. Existing ledger table continues to render `remission-credit`
rows generically.

View file

@ -4,11 +4,11 @@ type: workplan
title: "Control Plane UI: Extension Registry, Breach Records, Conversion Attestation"
domain: infotech
repo: target-revenue
status: active
status: finished
owner: claude
topic_slug: infotech
created: "2026-07-30"
updated: "2026-07-30"
updated: "2026-08-05"
state_hub_workstream_id: "9642c16e-5dd2-4bd6-881b-0eb04a8cf168"
---
@ -25,7 +25,7 @@ did for Phases and the Ledger. Not blocked on WP-0012; can start any time.
```task
id: TREV-WP-0014-T01
status: todo
status: done
priority: medium
state_hub_task_id: "8502c68b-4638-4242-9e4e-f9938dc62c9a"
```
@ -40,9 +40,21 @@ whatever rights tier canonicalization review actually requires (check
`specs/TargetRevenueControlPlaneConcept.md` §2's rights table — this may
need its own entry there if it's missing).
**Result (2026-08-05):** Rights decided and recorded in concept §2 and
`control_plane.py` module comments:
- **Register extension: Operator+** (same tier as Phase registration).
- **Promote to canonical: Admin only** (governance, never automated).
UI: `GET/POST /extensions`, `POST /extensions/promote`, template
`extensions.html`, nav link for all signed-in users (read). Wrappers
`control_plane.register_extension` /
`control_plane.promote_extension_canonical` with audit log.
`registry.list_extensions` added for the listing.
```task
id: TREV-WP-0014-T02
status: todo
status: done
priority: medium
state_hub_task_id: "8ba38f04-8612-426d-b11f-0ebdd8cea2f6"
```
@ -53,9 +65,15 @@ respecting the anonymized-by-default / named-only-with-opt-in rule
(FR-10, License V1C1 §7.4), and a read view on `phase_detail.html`
listing a Phase's published records.
**Result:** Operator+ form on `phase_detail.html` posts to
`POST /phases/{id}/breach` via `control_plane.publish_breach_event`.
Anonymized default; named path requires `named_disclosure_authorized`
affirmation. Published records listed on the same page (read for all
signed-in tiers).
```task
id: TREV-WP-0014-T03
status: todo
status: done
priority: low
state_hub_task_id: "b8c46793-c459-4890-90e6-cc112f8c8cb9"
```
@ -67,9 +85,14 @@ Future License and the supporting ledger checkpoint, rather than requiring
a caller to hit the Trust Service's `GET /phases/{id}/attestation`
directly to see it.
**Result:** `phase_detail` loads `control_plane.get_or_publish_attestation`
(idempotent publish on first observation of Outstanding Target = 0) and
renders Future License, final credits, ledger checkpoint, and signature
when present; empty state when not converted.
```task
id: TREV-WP-0014-T04
status: todo
status: done
priority: low
state_hub_task_id: "919e48cb-92f5-49cd-aa16-19889efd258b"
```
@ -77,3 +100,9 @@ state_hub_task_id: "919e48cb-92f5-49cd-aa16-19889efd258b"
**Tests and docs**: Docker-gated `TestClient` tests for all three (same
pattern as `tests/test_control_plane_app.py`), workplan Result sections,
`README.md` row update.
**Result:** Four new tests in `tests/test_control_plane_app.py`
(extension register/promote rights, anonymized breach, named breach CUA
gate, attestation after conversion). Concept §2 rights table updated;
README active-work row for WP-0014 marked finished; PhaseLifecycleUseCases
use case 6 open question closed by reference to the concept update.