+ 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 /