target-revenue/migrations/0007_forgejo_hubs.sql
tegwick de308f8947 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>
2026-08-03 23:57:46 +02:00

84 lines
3.2 KiB
PL/PgSQL

-- 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;