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:
parent
b7be3d3512
commit
dfc1d90c28
6 changed files with 287 additions and 3 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue