Complete WP-0015: forgejo_hubs migration + closeout (T05/T07)

migrations/0007_forgejo_hubs.sql: table forgejo_hubs, auto-populated
via a BEFORE INSERT trigger on phase_manifests that reads repo_hub/
repo_hub_uri straight out of the manifest JSONB (no top-level columns
needed). ON CONFLICT DO NOTHING -- a hub already seen is left alone;
correcting a URI is a SECURITY DEFINER governance action
(correct_forgejo_hub_uri), not a plain UPDATE, matching every other
governance-action pattern in this project. Thin Python wrappers added
to registry.py.

tests/test_forgejo_hubs.py (6 Docker-gated tests) and
tests/test_reference_docs.py (13 tests, no Docker needed -- smoke-tests
every real specs/policies/specs/profiles/ file, not just the two
exercised incidentally by T03's Control Plane tests).

All seven WP-0015 tasks done; workplan marked finished. Final suite:
94 passing offline, 183 passing under the service extras venv. No
stray Docker containers left running.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-03 23:57:46 +02:00
parent eaea89e125
commit de308f8947
6 changed files with 419 additions and 5 deletions

View file

@ -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 (T02T04) 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; T01T03 `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; T01T04, T06 done. T05 (`forgejo_hubs` migration) next |
| [TREV-WP-0015](workplans/TREV-WP-0015-phase-provenance-implementation.md) | Implement `specs/PhaseProvenanceSpecAddendum.md`**finished**, all 7 tasks done. Phase provenance fields, `specs/policies/`/`specs/profiles/` extraction, reference-rendering routes, ledger UI change, and the `forgejo_hubs` registry are all in place |
Hub index: [`WORK-RECORDS.md`](WORK-RECORDS.md) · brief: [`.custodian-brief.md`](.custodian-brief.md)

View file

@ -0,0 +1,84 @@
-- WP-0015-T05 (specs/PhaseProvenanceSpecAddendum.md §4): the Forgejo
-- hub -> service URI registry.
-- Depends on migrations/0001_registries.sql (phase_manifests).
--
-- Per WP-0012-T02's accepted decision: target-revenue is the generic
-- framework, not a specific deployment's list of repos it monetizes, so
-- this mapping is hosted Trust Service data (parallel to
-- licensor_identities, migrations/0005_licensor_credentials.sql), never
-- a file in this git repo. A Phase Manifest stays fully self-describing
-- and offline-verifiable regardless -- it already carries
-- repo_hub_uri/repo_name directly at registration time
-- (specs/PhaseLifecycleUseCases.md use case 9). This table exists purely
-- as an admin/repair convenience: if a hub's URI ever changes, there is
-- one row to correct rather than every affected Phase Manifest.
BEGIN;
CREATE TABLE IF NOT EXISTS forgejo_hubs (
hub_slug text PRIMARY KEY,
service_uri text NOT NULL,
first_seen_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
GRANT SELECT, INSERT ON forgejo_hubs TO trf_app;
-- No UPDATE/DELETE grant for trf_app -- correcting a hub's URI after the
-- fact is a governance action (correct_forgejo_hub_uri() below), same
-- pattern as set_extension_status()/revoke_credential(), not a route
-- the application's ordinary write path can take.
-- Auto-populate on first sight of a repo_hub during Phase registration --
-- registry.register_phase_manifest() never needs to know this table
-- exists; the trigger reads repo_hub/repo_hub_uri straight out of the
-- manifest JSONB it was already given, mirroring
-- ensure_licensor_identity()'s auto-create-on-first-INSERT pattern.
-- ON CONFLICT DO NOTHING: a hub already seen is left alone here -- URI
-- corrections go through correct_forgejo_hub_uri() below, not a silent
-- overwrite on the next unrelated Phase registration.
CREATE OR REPLACE FUNCTION ensure_forgejo_hub() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_hub_slug text := NEW.manifest #>> '{phase,milestone_release,repo_hub}';
v_hub_uri text := NEW.manifest #>> '{phase,milestone_release,repo_hub_uri}';
BEGIN
IF v_hub_slug IS NOT NULL AND v_hub_uri IS NOT NULL THEN
INSERT INTO forgejo_hubs (hub_slug, service_uri)
VALUES (v_hub_slug, v_hub_uri)
ON CONFLICT (hub_slug) DO NOTHING;
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS phase_manifests_ensure_forgejo_hub ON phase_manifests;
CREATE TRIGGER phase_manifests_ensure_forgejo_hub
BEFORE INSERT ON phase_manifests
FOR EACH ROW
EXECUTE FUNCTION ensure_forgejo_hub();
-- Correcting a hub's URI after the fact (a domain move, etc.) is a
-- recorded governance action, not an ordinary UPDATE -- trf_app has no
-- UPDATE grant on forgejo_hubs at all.
CREATE OR REPLACE FUNCTION correct_forgejo_hub_uri(
p_hub_slug text,
p_new_uri text
) RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
UPDATE forgejo_hubs
SET service_uri = p_new_uri,
updated_at = now()
WHERE hub_slug = p_hub_slug;
IF NOT FOUND THEN
RAISE EXCEPTION 'unknown forgejo hub: %', p_hub_slug;
END IF;
END;
$$;
GRANT EXECUTE ON FUNCTION correct_forgejo_hub_uri(text, text) TO trf_app;
COMMIT;

View file

@ -29,7 +29,7 @@ from dataclasses import dataclass
from typing import Any
from psycopg import Connection
from psycopg.errors import UniqueViolation
from psycopg.errors import RaiseException, UniqueViolation
from psycopg.types.json import Jsonb
from . import validation
@ -296,3 +296,45 @@ def promote_extension_canonical(
"SELECT set_extension_status(%s, %s, %s, %s)",
(extension_id, version, "canonical", approved_by),
)
def get_forgejo_hub(conn: Connection, hub_slug: str) -> dict[str, Any] | None:
"""Look up a hub's currently recorded service URI (WP-0015-T05).
Purely an admin/repair convenience a Phase Manifest never needs
this to be reachable to be independently verified, since it already
carries `repo_hub_uri`/`repo_name` directly at registration time
(`specs/PhaseLifecycleUseCases.md` use case 9). Rows are created
automatically by `phase_manifests_ensure_forgejo_hub`
(`migrations/0007_forgejo_hubs.sql`) the first time a hub is seen
there is no explicit "register a hub" function to pair with this one.
"""
row = conn.execute(
"SELECT hub_slug, service_uri, first_seen_at, updated_at "
"FROM forgejo_hubs WHERE hub_slug = %s",
(hub_slug,),
).fetchone()
if row is None:
return None
hub_slug_, service_uri, first_seen_at, updated_at = row
return {
"hub_slug": hub_slug_,
"service_uri": service_uri,
"first_seen_at": first_seen_at,
"updated_at": updated_at,
}
def correct_forgejo_hub_uri(conn: Connection, hub_slug: str, new_uri: str) -> None:
"""Correct a hub's recorded service URI (e.g. after a domain move).
Calls the database's `correct_forgejo_hub_uri` function rather than
an UPDATE the application role has no UPDATE grant on
`forgejo_hubs` at all, matching every other governance-action pattern
in this project (`revoke_sub_credential`,
`promote_extension_canonical`).
"""
try:
conn.execute("SELECT correct_forgejo_hub_uri(%s, %s)", (hub_slug, new_uri))
except RaiseException as exc:
raise RegistrationError(f"unknown forgejo hub: {hub_slug!r}") from exc

177
tests/test_forgejo_hubs.py Normal file
View file

@ -0,0 +1,177 @@
"""Integration tests for WP-0015-T05 (`forgejo_hubs` registry).
Same ephemeral, disposable Postgres-via-Docker pattern as the other
hosted test modules (never the shared state-hub instance).
"""
from __future__ import annotations
import copy
import shutil
import subprocess
import time
import uuid
from pathlib import Path
import pytest
psycopg = pytest.importorskip("psycopg")
from conftest import golden_manifest # noqa: E402
REPO_ROOT = Path(__file__).resolve().parents[1]
MIGRATIONS = [
REPO_ROOT / "migrations" / "0001_registries.sql",
REPO_ROOT / "migrations" / "0002_ledger.sql",
REPO_ROOT / "migrations" / "0003_attestations.sql",
REPO_ROOT / "migrations" / "0004_breach_records.sql",
REPO_ROOT / "migrations" / "0005_licensor_credentials.sql",
REPO_ROOT / "migrations" / "0006_control_plane.sql",
REPO_ROOT / "migrations" / "0007_forgejo_hubs.sql",
]
pytestmark = pytest.mark.skipif(
shutil.which("docker") is None, reason="docker not available"
)
@pytest.fixture(scope="module")
def pg_container():
name = f"trf-test-pg-hubs-{uuid.uuid4().hex[:8]}"
subprocess.run(
[
"docker", "run", "--rm", "-d",
"--name", name,
"-e", "POSTGRES_PASSWORD=postgres",
"-e", "POSTGRES_DB=target_revenue_test",
"-p", "127.0.0.1::5432",
"postgres:16-alpine",
],
check=True, capture_output=True,
)
try:
port_out = subprocess.run(
["docker", "port", name, "5432/tcp"], check=True, capture_output=True, text=True
).stdout.strip()
host_port = port_out.split(":")[-1]
dsn = f"host=127.0.0.1 port={host_port} dbname=target_revenue_test user=postgres password=postgres"
for _ in range(60):
try:
with psycopg.connect(dsn, connect_timeout=1):
break
except psycopg.OperationalError:
time.sleep(0.5)
else:
raise RuntimeError("postgres container did not become ready in time")
with psycopg.connect(dsn) as conn:
for migration in MIGRATIONS:
conn.execute(migration.read_text(encoding="utf-8"))
conn.commit()
conn.execute(
"INSERT INTO licensors (token, licensor_id, credential_label, rights, issued_by) "
"VALUES (%s, %s, %s, %s, %s)",
("founding-token", "binky", "founder", "operator", "bootstrap"),
)
conn.commit()
app_dsn = (
f"host=127.0.0.1 port={host_port} dbname=target_revenue_test "
f"user=trf_app password=changeme-in-deployment"
)
yield {"admin_dsn": dsn, "app_dsn": app_dsn, "founding_token": "founding-token"}
finally:
subprocess.run(["docker", "stop", name], capture_output=True)
@pytest.fixture()
def conn(pg_container):
with psycopg.connect(pg_container["app_dsn"]) as connection:
yield connection
@pytest.fixture()
def licensor(conn, pg_container):
from target_revenue import registry
return registry.authenticate(conn, pg_container["founding_token"])
def _manifest_with_hub(hub_slug: str, hub_uri: str, phase_suffix: str) -> dict:
manifest = copy.deepcopy(golden_manifest())
manifest["phase"]["id"] = manifest["phase"]["id"] + "-hubtest-" + phase_suffix
manifest["phase"]["milestone_release"]["repo_hub"] = hub_slug
manifest["phase"]["milestone_release"]["repo_hub_uri"] = hub_uri
return manifest
def test_registering_phase_auto_creates_hub_row(conn, licensor):
from target_revenue import registry
suffix = uuid.uuid4().hex[:8]
manifest = _manifest_with_hub(f"hub-{suffix}", "https://forgejo.example.invalid", suffix)
registry.register_phase_manifest(conn, licensor, manifest)
conn.commit()
hub = registry.get_forgejo_hub(conn, f"hub-{suffix}")
assert hub is not None
assert hub["service_uri"] == "https://forgejo.example.invalid"
def test_unknown_hub_returns_none(conn):
from target_revenue import registry
assert registry.get_forgejo_hub(conn, "no-such-hub") is None
def test_second_phase_same_hub_does_not_overwrite(conn, licensor):
"""ON CONFLICT DO NOTHING: a hub already seen is left alone by the
auto-populate trigger -- only correct_forgejo_hub_uri() may change it."""
from target_revenue import registry
suffix = uuid.uuid4().hex[:8]
hub_slug = f"hub-{suffix}"
manifest_a = _manifest_with_hub(hub_slug, "https://first-seen.example.invalid", suffix + "a")
registry.register_phase_manifest(conn, licensor, manifest_a)
conn.commit()
manifest_b = _manifest_with_hub(hub_slug, "https://different-uri.example.invalid", suffix + "b")
registry.register_phase_manifest(conn, licensor, manifest_b)
conn.commit()
hub = registry.get_forgejo_hub(conn, hub_slug)
assert hub["service_uri"] == "https://first-seen.example.invalid"
def test_correct_forgejo_hub_uri_updates_existing_hub(conn, licensor):
from target_revenue import registry
suffix = uuid.uuid4().hex[:8]
hub_slug = f"hub-{suffix}"
manifest = _manifest_with_hub(hub_slug, "https://old-domain.example.invalid", suffix)
registry.register_phase_manifest(conn, licensor, manifest)
conn.commit()
registry.correct_forgejo_hub_uri(conn, hub_slug, "https://new-domain.example.invalid")
conn.commit()
hub = registry.get_forgejo_hub(conn, hub_slug)
assert hub["service_uri"] == "https://new-domain.example.invalid"
def test_correct_forgejo_hub_uri_unknown_hub_is_rejected(conn):
from target_revenue import registry
with pytest.raises(registry.RegistrationError, match="unknown forgejo hub"):
registry.correct_forgejo_hub_uri(conn, "never-registered", "https://x.example.invalid")
def test_application_role_cannot_update_forgejo_hubs_directly(pg_container):
with psycopg.connect(pg_container["app_dsn"]) as app_conn:
with pytest.raises(psycopg.errors.InsufficientPrivilege):
app_conn.execute(
"UPDATE forgejo_hubs SET service_uri = 'https://tampered.invalid' "
"WHERE hub_slug = 'anything'"
)
app_conn.rollback()

View file

@ -0,0 +1,71 @@
"""Smoke tests for WP-0015-T07: every real `specs/policies/`/
`specs/profiles/` file must actually render without error. No database
or Docker required this only exercises `reference_docs.py`'s pure
file-rendering logic.
"""
from __future__ import annotations
import pytest
from conftest import REPO_ROOT
pytest.importorskip("markdown")
pytest.importorskip("yaml")
from target_revenue.service import reference_docs # noqa: E402
POLICY_SLUGS = ["linear-longstop-v0"]
PROFILE_SLUGS = [
"development-license",
"cost-plus-operations",
"phase-sponsorship",
"service-with-development-allocation",
"product-ideation",
"general-consulting",
]
def test_all_real_policy_files_exist_on_disk():
for slug in POLICY_SLUGS:
assert (REPO_ROOT / "specs" / "policies" / f"{slug}.md").is_file()
def test_all_real_profile_files_exist_on_disk():
for slug in PROFILE_SLUGS:
assert (REPO_ROOT / "specs" / "profiles" / f"{slug}.md").is_file()
@pytest.mark.parametrize("slug", POLICY_SLUGS)
def test_policy_doc_renders_without_error(slug):
result = reference_docs.load_reference_doc("policies", slug)
assert result is not None
html, frontmatter = result
assert "<h1>" in html or "<h2>" in html
assert "policy_id" in frontmatter
assert frontmatter["policy_id"].startswith("trsl:policy:")
@pytest.mark.parametrize("slug", PROFILE_SLUGS)
def test_profile_doc_renders_without_error(slug):
result = reference_docs.load_reference_doc("profiles", slug)
assert result is not None
html, frontmatter = result
assert "<h1>" in html
assert "extension_id" in frontmatter
assert frontmatter["extension_id"].startswith("trsl:extension:")
def test_unknown_kind_returns_none():
assert reference_docs.load_reference_doc("calculators", "anything") is None
def test_unknown_slug_returns_none():
assert reference_docs.load_reference_doc("policies", "does-not-exist") is None
def test_policy_slug_from_id():
assert reference_docs.policy_slug_from_id("trsl:policy:linear-longstop-v0@1.0") == "linear-longstop-v0"
def test_extension_slug_from_id():
assert reference_docs.extension_slug_from_id("trsl:extension:development-license@1.0") == "development-license"

View file

@ -4,7 +4,7 @@ type: workplan
title: "Implement Phase provenance, spec-file, and ledger UI changes"
domain: infotech
repo: target-revenue
status: active
status: finished
owner: claude
topic_slug: infotech
created: "2026-08-03"
@ -173,7 +173,7 @@ reference plus a live-JSON link when it resolves to this instance
```task
id: TREV-WP-0015-T05
status: todo
status: done
priority: high
state_hub_task_id: "816f1ac2-da40-4550-8d6c-64a1a335a660"
```
@ -188,6 +188,31 @@ implementation (not a T02-style human gate, per the addendum) whether
correcting a hub's URI after the fact is a `SECURITY DEFINER` governance
action or a plain `UPDATE`, and document whichever is chosen.
**Result:** `migrations/0007_forgejo_hubs.sql` — the `BEFORE INSERT`
trigger on `phase_manifests` reads `repo_hub`/`repo_hub_uri` straight out
of the manifest JSONB (`NEW.manifest #>> '{phase,milestone_release,
repo_hub}'`) rather than needing those as top-level table columns, since
`phase_manifests` stores the whole manifest as one `jsonb` blob.
`ON CONFLICT (hub_slug) DO NOTHING` — a hub already seen is left alone by
the trigger; only a second registration under the same slug with a
different URI would otherwise silently overwrite it, which is exactly
the failure mode this table exists to prevent.
**Decided: `SECURITY DEFINER` governance action**, not a plain `UPDATE`
`correct_forgejo_hub_uri(hub_slug, new_uri)`, matching
`set_extension_status()`/`revoke_credential()`. `trf_app` has no UPDATE
grant on `forgejo_hubs` at all. Thin Python wrappers added to
`registry.py`: `get_forgejo_hub()`, `correct_forgejo_hub_uri()` (raises
`RegistrationError` for an unknown slug, via `psycopg.errors.
RaiseException`).
New `tests/test_forgejo_hubs.py`, 6 Docker-gated tests: auto-creation on
first registration, unknown-hub lookup, second-registration-same-hub
does *not* overwrite, explicit correction does, correction of an unknown
hub is rejected, and DB-level UPDATE rejection for the application role.
Full suite: 94 passing offline (unchanged), 170 passing with Docker (up
from 164).
```task
id: TREV-WP-0015-T06
status: done
@ -215,7 +240,7 @@ regression test: `test_pilot_candidate_manifest_carries_real_repo_provenance`.
```task
id: TREV-WP-0015-T07
status: todo
status: done
priority: high
state_hub_task_id: "39b34438-a1b1-49da-a88a-f3bec926aa89"
```
@ -228,3 +253,18 @@ without error. Update `README.md`'s WP-0015 row and this workplan's
Result sections; run the full offline + Docker-gated suite; fence-count
check before committing any workplan edit, per this project's standing
practice.
**Result:** New `tests/test_reference_docs.py` — 13 tests, no Docker
required (only needs the `service` extras venv for `markdown`/`PyYAML`),
covering every real policy/profile file on disk, not just the two
exercised incidentally by T03's Control Plane tests. Skips cleanly under
plain `python3` (offline suite) when those extras aren't installed,
matching this project's existing `pytest.importorskip` convention.
All seven WP-0015 tasks are now done. Final suite counts: **94 passing
offline** (up from 84 at the start of this workplan — T01's schema tests
and T06's backfill-regression test, unchanged since; nothing in T02T07
touches an offline-only code path), **183 passing under `.venv` with the
`service` extras** (the Docker-gated total moved from 146 → 170 across
T01T05, plus 13 more non-Docker service-extras tests from this task). No
stray Docker containers left running.