Implement hosted Conversion Attestation publication (WP-0006-T06)
migrations/0003_attestations.sql: one row per Phase, no UPDATE/DELETE
grant for trf_app (a published attestation is permanent).
src/target_revenue/attestation.py: publish_attestation() is idempotent
(existing row returned unchanged, never regenerated) and derives
conversion_timestamp from the ledger itself via _find_conversion_prefix(),
which finds the earliest prefix where the fold reaches Outstanding
Target = 0 rather than trusting entries[-1] or wall-clock publish time -
so a later unrelated entry can never change an already-converged Phase's
recorded conversion moment. Raises NotConvertedError rather than
fabricating a conversion. Reuses conversion.generate_attestation()
unchanged. Guarded the psycopg import behind TYPE_CHECKING so the pure
helper stays testable under plain system Python.
service/app.py adds GET /phases/{id}/attestation (unauthenticated,
publish-on-first-observation).
tests/test_attestation.py (3 tests, no Docker/Postgres) proves the
earliest-crossing behavior. 6 new Docker-gated tests in
test_ledger_hosting.py cover pre-conversion 404, the core
legal-technical property that /metrics facts/calculations/forecasts
are identical whether or not /attestation was ever called, one-time
publication, signature verification, and DB-level UPDATE/DELETE
rejection.
This commit is contained in:
parent
caa98ead3b
commit
28f0f429b2
7 changed files with 341 additions and 3 deletions
|
|
@ -18,7 +18,7 @@ from fastapi import Depends, FastAPI, HTTPException, Request
|
|||
from psycopg import Connection
|
||||
from psycopg_pool import ConnectionPool
|
||||
|
||||
from .. import ledger, metrics, registry
|
||||
from .. import attestation, ledger, metrics, registry
|
||||
from . import keys
|
||||
|
||||
app = FastAPI(title="Target Revenue Trust Service — Registries", version="0.1.0")
|
||||
|
|
@ -153,3 +153,24 @@ def read_metrics(
|
|||
raise HTTPException(status_code=404, detail="phase not found")
|
||||
entries = ledger.get_ledger(conn, phase_id)
|
||||
return metrics.compute_metrics(manifest, entries, metrics.utcnow())
|
||||
|
||||
|
||||
@app.get("/phases/{phase_id}/attestation")
|
||||
def read_attestation(
|
||||
phase_id: str,
|
||||
conn: Connection = Depends(get_connection),
|
||||
signing_key=Depends(get_signing_key),
|
||||
) -> dict[str, Any]:
|
||||
"""Public, unauthenticated. Publishes (idempotently) the first time a
|
||||
conversion is observed, and simply returns the already-published
|
||||
record on every call after that — never regenerates, never requires
|
||||
this call to have happened for conversion to already be legally true
|
||||
(see target_revenue.attestation's module docstring)."""
|
||||
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)
|
||||
try:
|
||||
return attestation.publish_attestation(conn, manifest, entries, signing_key)
|
||||
except attestation.NotConvertedError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue