Implement public metrics service (WP-0006-T05)

src/target_revenue/metrics.py: compute_metrics(manifest, entries, as_of)
is pure/deterministic like fold.py, reusing fold.py/conversion.py
unchanged. Returns facts/calculations/forecasts as three explicitly
separated blocks (TrustServicePRD TS-FR-5), covering the mandatory Q9
set plus the recommended velocity/forecast tier - forecasts are always
null rather than populated once a Phase has converted or velocity is
non-positive, so nothing disguises a projection as a fact.

Adds GET /phases/{id}/metrics (unauthenticated per FR-9/FR-10).
tests/test_metrics.py (6 tests) needs no Docker/Postgres and runs
under plain system Python. One new Docker-gated test in
test_ledger_hosting.py proves the hosted /metrics response exactly
matches compute_metrics() run offline against the same export.
This commit is contained in:
tegwick 2026-07-29 21:42:52 +02:00
parent b7be3d3512
commit dfc1d90c28
6 changed files with 287 additions and 3 deletions

View file

@ -18,7 +18,7 @@ from fastapi import Depends, FastAPI, HTTPException, Request
from psycopg import Connection
from psycopg_pool import ConnectionPool
from .. import ledger, registry
from .. import ledger, metrics, registry
from . import keys
app = FastAPI(title="Target Revenue Trust Service — Registries", version="0.1.0")
@ -140,3 +140,16 @@ def read_ledger(
if registry.get_phase_manifest(conn, phase_id) is None:
raise HTTPException(status_code=404, detail="phase not found")
return ledger.get_ledger(conn, phase_id)
@app.get("/phases/{phase_id}/metrics")
def read_metrics(
phase_id: str, conn: Connection = Depends(get_connection)
) -> dict[str, Any]:
"""Public, unauthenticated per FR-9/FR-10 — see `target_revenue.metrics`
for the fact/calculation/forecast labeling this response preserves."""
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)
return metrics.compute_metrics(manifest, entries, metrics.utcnow())