From d894599647955795541574ab7c7f32153698a4c8 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 3 Aug 2026 23:45:59 +0200 Subject: [PATCH] Implement WP-0015-T03: Control Plane reference-rendering routes New service/reference_docs.py renders specs/policies/*.md and specs/profiles/*.md read-only at request time via a small markdown library (added markdown + PyYAML to the service extras) -- not a static-build pipeline, matching the WP-0012-T03 decision to skip state-hub's heavier Observable Framework pattern. One parameterized route, GET /reference/{kind}/{slug}, covers both addendum URL shapes. Discovered phase_detail.html's Status table never displayed the degeneration_policy id at all -- added that row (with the reference link) rather than wiring a link with nothing to attach it to. phase_new.html gets a plain link next to the field. Deliberately did not wire extension-id links into the UI in this task -- extension ids don't appear anywhere in the Control Plane today (that's WP-0014's gap, not this one's to expand). 6 new Docker-gated tests. Full suite: 94 passing offline, 164 passing with Docker (up from 158). Co-Authored-By: Claude Sonnet 5 --- README.md | 2 +- pyproject.toml | 2 + .../service/control_plane_app.py | 32 +++++++++- .../control_plane_templates/phase_detail.html | 7 +++ .../control_plane_templates/phase_new.html | 3 + .../control_plane_templates/reference.html | 15 +++++ src/target_revenue/service/reference_docs.py | 58 +++++++++++++++++++ tests/test_control_plane_app.py | 58 +++++++++++++++++++ ...WP-0015-phase-provenance-implementation.md | 35 ++++++++++- 9 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 src/target_revenue/service/control_plane_templates/reference.html create mode 100644 src/target_revenue/service/reference_docs.py diff --git a/README.md b/README.md index 24f9c38..964e2ce 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ The concept's §13 now defines a **Global Contingency Share Determination Rule** | [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 (T02–T04) 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) — active; T01–T03 `wait` on WP-0012-T03's policy-spec-file decision. Nothing currently computes or writes `remission-credit` ledger entries | | [TREV-WP-0014](workplans/TREV-WP-0014-control-plane-extensions-breach-attestation-ui.md) | Control Plane UI: Extension Registry, Breach Records, Conversion Attestation — active; T01 next. Backend for all three already exists (WP-0006); UI-only work, not blocked on WP-0012 | -| [TREV-WP-0015](workplans/TREV-WP-0015-phase-provenance-implementation.md) | Implement `specs/PhaseProvenanceSpecAddendum.md` — active; T01, T02, T04, T06 done. T03 (Control Plane reference-rendering routes) next | +| [TREV-WP-0015](workplans/TREV-WP-0015-phase-provenance-implementation.md) | Implement `specs/PhaseProvenanceSpecAddendum.md` — active; T01–T04, T06 done. T05 (`forgejo_hubs` migration) next | Hub index: [`WORK-RECORDS.md`](WORK-RECORDS.md) · brief: [`.custodian-brief.md`](.custodian-brief.md) diff --git a/pyproject.toml b/pyproject.toml index f66d396..5333cc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,8 @@ service = [ "jinja2>=3.1", "itsdangerous>=2.1", "python-multipart>=0.0.9", + "markdown>=3.6", + "PyYAML>=6.0", ] service-dev = [ "target-revenue[service,dev]", diff --git a/src/target_revenue/service/control_plane_app.py b/src/target_revenue/service/control_plane_app.py index d566c20..2358883 100644 --- a/src/target_revenue/service/control_plane_app.py +++ b/src/target_revenue/service/control_plane_app.py @@ -26,7 +26,7 @@ from psycopg_pool import ConnectionPool from starlette.middleware.sessions import SessionMiddleware from .. import control_plane, ledger, metrics, registry -from . import keys +from . import keys, reference_docs _STATIC_DIR = os.path.join(os.path.dirname(__file__), "static") _TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), "control_plane_templates") @@ -37,6 +37,8 @@ _SECRET_KEY_ENV = "TRF_CONTROL_PLANE_SECRET_KEY" app = FastAPI(title="Target Revenue Control Plane", version="0.1.0") app.mount("/static", StaticFiles(directory=_STATIC_DIR), name="static") templates = Jinja2Templates(directory=_TEMPLATES_DIR) +templates.env.globals["policy_slug"] = reference_docs.policy_slug_from_id +templates.env.globals["extension_slug"] = reference_docs.extension_slug_from_id _secret_key = os.environ.get(_SECRET_KEY_ENV) if not _secret_key: @@ -408,3 +410,31 @@ def audit_log( ): log = control_plane.get_audit_log(conn) return templates.TemplateResponse(request, "audit.html", _template_context(request, licensor, log=log)) + + +# --- Reference docs (specs/policies/, specs/profiles/) ---------------------- + + +@app.get("/reference/{kind}/{slug}") +def reference_doc( + kind: str, + slug: str, + request: Request, + licensor: registry.Licensor = Depends(require_login), +): + """Read-only rendering of a `specs/policies/`/`specs/profiles/` + markdown file (WP-0015-T03) — never editable from here.""" + result = reference_docs.load_reference_doc(kind, slug) + if result is None: + raise HTTPException(status_code=404, detail="reference document not found") + doc_html, frontmatter = result + return templates.TemplateResponse( + request, + "reference.html", + _template_context( + request, licensor, + doc_html=doc_html, + doc_title=frontmatter.get("title", slug), + source_path=f"specs/{kind}/{slug}.md", + ), + ) diff --git a/src/target_revenue/service/control_plane_templates/phase_detail.html b/src/target_revenue/service/control_plane_templates/phase_detail.html index 104d20f..becbc53 100644 --- a/src/target_revenue/service/control_plane_templates/phase_detail.html +++ b/src/target_revenue/service/control_plane_templates/phase_detail.html @@ -24,6 +24,13 @@ Outstanding Target{{ metrics.facts.outstanding_target }} Target satisfaction{{ metrics.calculations.target_satisfaction_percentage }}% Converted{{ metrics.facts.is_converted }} + + Degeneration policy + + {{ manifest.phase.degeneration_policy }} + · view spec + +

Ledger ({{ ledger | length }} entries)

diff --git a/src/target_revenue/service/control_plane_templates/phase_new.html b/src/target_revenue/service/control_plane_templates/phase_new.html index 9ac0faa..06ead52 100644 --- a/src/target_revenue/service/control_plane_templates/phase_new.html +++ b/src/target_revenue/service/control_plane_templates/phase_new.html @@ -49,6 +49,9 @@ +

+ view the linear-longstop-v0 spec +

diff --git a/src/target_revenue/service/control_plane_templates/reference.html b/src/target_revenue/service/control_plane_templates/reference.html new file mode 100644 index 0000000..bfdf0da --- /dev/null +++ b/src/target_revenue/service/control_plane_templates/reference.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% block title %}{{ doc_title }} — Target Revenue Control Plane{% endblock %} +{% block content %} + + {{ doc_title }} + +

+ Read-only reference, rendered from {{ source_path }}. See + that file's own git history for the full revision trail — this view + cannot be edited. +

+
+{{ doc_html | safe }} +
+{% endblock %} diff --git a/src/target_revenue/service/reference_docs.py b/src/target_revenue/service/reference_docs.py new file mode 100644 index 0000000..d8feeb4 --- /dev/null +++ b/src/target_revenue/service/reference_docs.py @@ -0,0 +1,58 @@ +"""Read-only rendering of `specs/policies/`/`specs/profiles/` markdown +files for the Control Plane (WP-0015-T03). + +Server-side markdown -> HTML at request time, deliberately not a static +build pipeline (unlike state-hub's Observable Framework Reference +section, confirmed materially heavier during WP-0012-T03) -- this repo's +existing lightweight FastAPI+Jinja2 stack needs nothing more than a +small markdown library for six profile pages and one policy page. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import markdown +import yaml + +_SPECS_DIR = Path(__file__).resolve().parents[3] / "specs" + +# kind -> subdirectory name under specs/. Only these two exist today +# (WP-0015-T02); a third kind (e.g. "calculators") can be added here if +# a future workplan gives calculators their own reference route. +REFERENCE_KINDS = frozenset({"policies", "profiles"}) + + +def load_reference_doc(kind: str, slug: str) -> tuple[str, dict[str, Any]] | None: + """Return (rendered_html, frontmatter) for one spec file, or None if + `kind` is unknown or no matching file exists. Read-only: there is no + corresponding write path anywhere in this module.""" + if kind not in REFERENCE_KINDS: + return None + path = _SPECS_DIR / kind / f"{slug}.md" + if not path.is_file(): + return None + + raw = path.read_text(encoding="utf-8") + frontmatter: dict[str, Any] = {} + if raw.startswith("---\n"): + end = raw.find("\n---\n", 4) + if end != -1: + frontmatter = yaml.safe_load(raw[4:end]) or {} + raw = raw[end + 5 :] + + html = markdown.markdown(raw) + return html, frontmatter + + +def policy_slug_from_id(policy_id: str) -> str: + """`trsl:policy:linear-longstop-v0@1.0` -> `linear-longstop-v0`.""" + name = policy_id.split(":")[-1] + return name.split("@")[0] + + +def extension_slug_from_id(extension_id: str) -> str: + """`trsl:extension:development-license@1.0` -> `development-license`.""" + name = extension_id.split(":")[-1] + return name.split("@")[0] diff --git a/tests/test_control_plane_app.py b/tests/test_control_plane_app.py index 37a4920..76b4476 100644 --- a/tests/test_control_plane_app.py +++ b/tests/test_control_plane_app.py @@ -296,6 +296,64 @@ def test_audit_log_visible_to_signed_in_user(client, credentials): assert resp.status_code == 200 +def test_reference_policy_doc_renders(client, credentials): + _login(client, credentials["viewer"].token) + resp = client.get("/reference/policies/linear-longstop-v0") + assert resp.status_code == 200 + assert "Linear Longstop v0" in resp.text + assert "clamp" in resp.text + + +def test_reference_profile_doc_renders(client, credentials): + _login(client, credentials["viewer"].token) + resp = client.get("/reference/profiles/development-license") + assert resp.status_code == 200 + assert "Development License" in resp.text + assert "Commercial Entitlement" in resp.text + + +def test_reference_unknown_slug_is_404(client, credentials): + _login(client, credentials["viewer"].token) + resp = client.get("/reference/policies/does-not-exist") + assert resp.status_code == 404 + + +def test_reference_unknown_kind_is_404(client, credentials): + _login(client, credentials["viewer"].token) + resp = client.get("/reference/calculators/development-effort-calculator-candidate-a") + assert resp.status_code == 404 + + +def test_reference_requires_login(client): + resp = client.get("/reference/policies/linear-longstop-v0", follow_redirects=False) + assert resp.status_code == 303 + assert resp.headers["location"] == "/login" + + +def test_phase_detail_links_to_policy_reference(client, credentials): + _login(client, credentials["operator"].token) + phase_id = "trsl:phase:cpapp-refcheck-" + uuid.uuid4().hex[:8] + client.post( + "/phases/new", + data={ + "phase_id": phase_id, + "milestone_release_name": "CP UI reference-link check", + "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": "1000", + "currency": "USD", + "future_license": "MIT", + "degeneration_policy": "trsl:policy:linear-longstop-v0@1.0", + "longstop_at": "2027-01-01T00:00:00Z", + }, + ) + detail = client.get(f"/phases/{phase_id}") + assert "/reference/policies/linear-longstop-v0" in detail.text + + def test_form_bridge_script_present(client): """whynot-design's wn-input/wn-select/wn-button are not form-associated custom elements — their real /