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

@ -274,3 +274,30 @@ def test_duplicate_entry_id_rejected(client, pg_container, registered_phase):
second = client.post(f"/phases/{phase_id}/ledger", json=e, headers=auth_headers(pg_container["token"]))
assert second.status_code == 422
assert "already exists" in second.json()["detail"]
def test_metrics_endpoint_unauthenticated_and_matches_offline_computation(
client, pg_container, registered_phase
):
"""WP-0006-T05: the hosted /metrics response must match what
target_revenue.metrics.compute_metrics computes offline from the same
exported Manifest + Ledger the metrics-layer analogue of T04's
hosted/offline fold-agreement test."""
from target_revenue import metrics as metrics_module
phase_id = registered_phase["phase"]["id"]
e = _entry(phase_id, "trsl:entry:ledgertestmetrics0001", "development-credit", 4000, "2026-08-01T00:00:00Z")
resp = client.post(f"/phases/{phase_id}/ledger", json=e, headers=auth_headers(pg_container["token"]))
assert resp.status_code == 201, resp.text
# No Authorization header at all: metrics are public per FR-9/FR-10.
metrics_resp = client.get(f"/phases/{phase_id}/metrics")
assert metrics_resp.status_code == 200
hosted = metrics_resp.json()
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)
assert hosted == offline
assert hosted["facts"]["cumulative_development_credit"] == 4000