target-revenue/workplans/TREV-WP-0015-phase-provenance-implementation.md
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

270 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: TREV-WP-0015
type: workplan
title: "Implement Phase provenance, spec-file, and ledger UI changes"
domain: infotech
repo: target-revenue
status: finished
owner: claude
topic_slug: infotech
created: "2026-08-03"
updated: "2026-08-03"
state_hub_workstream_id: "fe1bedd3-dadc-41cc-a669-92623743e620"
---
# Implement Phase provenance, spec-file, and ledger UI changes
Implements `specs/PhaseProvenanceSpecAddendum.md`, accepted 2026-08-03,
which itself synthesizes the decisions from
`workplans/TREV-WP-0012-phase-provenance-and-policy-modeling.md` T02T04.
Task numbering below matches the addendum's §6 suggested breakdown
1:1 — each task cites its addendum section as the authoritative source of
what to build, rather than re-deriving field shapes or rationale here.
```task
id: TREV-WP-0015-T01
status: done
priority: high
state_hub_task_id: "cdca0ef5-a332-40e2-b165-10ffa238f413"
```
**Schema change** (addendum §1): add `repo_hub`, `repo_hub_uri`,
`repo_id`, `repo_name` as required properties of
`phase.milestone_release`, and `phase.base_phase_id` as an optional
property of `phase`, to `schemas/phase_manifest.schema.json`. Update
`validation.py` if the new pattern/format constraints need anything
beyond what JSON Schema's own `pattern`/`format` keywords already
enforce (they shouldn't). Add offline tests: a manifest missing any of
the four new required fields is rejected; one with `base_phase_id`
absent validates (first Phase); one with it present and correctly
patterned validates (successive Phase).
**Result:** Schema updated exactly as specified; `validation.py` needed
no changes (`pattern`/`format`/`minimum` already sufficient). Added six
tests to `tests/test_manifest_validation.py` covering all four new
required fields and both `base_phase_id` states plus a malformed one.
Updating `examples/phase-001/manifest.json` (the fixture 13 other test
files depend on) turned out to be necessary just to keep the offline
suite green, not optional — done inline with placeholder-but-plausible
values (`repo_hub: "example-hub"`, synthetic id/name), since it's a
fictional fixture, not a real repo.
**Discovered scope gap, fixed inline rather than left broken**: the
addendum's task breakdown didn't call out that the Control Plane's
existing Phase registration form (`phase_new.html`,
`control_plane_app.py`) would stop working the moment this schema
change landed, since nothing collected the four new required fields.
Fixed as part of this task rather than leaving registration broken
between tasks — this also completes T04's registration-form half (drop
`ledger` input, auto-compute it) and adds the `base_phase_id` field,
since all three changes touch the same form/route. `phase_detail.html`'s
ledger-reference drill-down (T04's other half) is done too. Full suite:
94 passing offline (up from 84 — six new schema tests, three new
provenance-backfill tests, two new Control Plane form tests), 158
passing with Docker (up from 146).
```task
id: TREV-WP-0015-T02
status: done
priority: high
state_hub_task_id: "8b5ab6ea-fa80-4e08-8733-2faad810962e"
```
**Spec-file extraction** (addendum §2): create `specs/policies/
linear-longstop-v0.md` (extracted from `specs/
OpenQuestions-WorkingDefaults.md` Q7's prose, with `policy_id`/`title`
frontmatter); create the six `specs/profiles/*.md` files (extracted from
`specs/CanonicalMonetizationProfiles.md` §1§6, with `extension_id`/
`title` frontmatter), leaving §7 (cross-profile summary) and §8
(non-goals) in place as the overview document. Add `calculator_id`/
`revision`/`title` frontmatter to `specs/
DevelopmentEffortCalculatorConcept.md` (no move). Update
`OpenQuestions-WorkingDefaults.md` Q7 to point at the new file rather
than contain the policy's content directly.
**Result:** `specs/policies/linear-longstop-v0.md` and all six
`specs/profiles/*.md` files created exactly as scoped, each with the
decided frontmatter shape. `CanonicalMonetizationProfiles.md` §1§6
replaced with a linked list pointing at the new files; §7 (cross-profile
summary) and §8 (non-goals) untouched. `OpenQuestions-WorkingDefaults.md`
Q7 now records only the adoption decision, pointing at the policy file
for content. `DevelopmentEffortCalculatorConcept.md` got its
`calculator_id`/`revision`/`title` frontmatter, no move (already
conformed). Checked for stale cross-references before editing — nothing
in the repo links to the removed `CanonicalMonetizationProfiles.md`
section anchors (`grep` confirmed), so no other file needed updating.
Offline suite unaffected (no code reads these doc files' content) —
94 passing, unchanged.
```task
id: TREV-WP-0015-T03
status: done
priority: medium
state_hub_task_id: "681002d5-582b-4ddd-8b1f-dc25ce5b0e5a"
```
**Control Plane reference-rendering routes** (addendum §2): add
`GET /reference/policies/{slug}` and `GET /reference/profiles/{slug}` to
`service/control_plane_app.py`, rendering the corresponding
`specs/policies/`/`specs/profiles/` markdown file to HTML server-side at
request time (a small Python markdown library — confirm one is already
an acceptable addition to the `service` extras in `pyproject.toml`, or
add it). Read-only, no edit capability. Link to these routes from
wherever a policy id or extension id already appears in
`phase_detail.html`/`phase_new.html`.
**Result:** New module `service/reference_docs.py` (`load_reference_doc`,
`policy_slug_from_id`, `extension_slug_from_id`), kept separate from
`control_plane_app.py` per this project's pattern of separating HTTP
framing from logic. One parameterized route,
`GET /reference/{kind}/{slug}` (`kind` restricted to `policies`/
`profiles`), satisfies both addendum URL shapes without two near-identical
route functions. Added `markdown>=3.6` and `PyYAML>=6.0` to the `service`
extras (frontmatter parsing).
**Discovered along the way**: `phase_detail.html`'s Status table never
displayed `manifest.phase.degeneration_policy` at all — there was no
existing spot to attach a link to. Added the row (with the reference
link) as part of wiring this in, rather than leaving the policy id
invisible on the one page where a human would want to check it.
`phase_new.html` gets a plain link next to the field (only one policy
exists today, so a static link is sufficient; revisit if a second policy
is ever offered as a choice there).
**Deliberately not done in this task**: extension ids don't currently
appear anywhere in the Control Plane UI at all (Phase registration
doesn't collect `extensions`, and the ledger-entry form's `extension_id`
is free-typed, not selected from a registered list) — that's WP-0014's
gap (Extension Registry has no UI yet), not something to silently expand
here. `extension_slug_from_id`/the `profiles` reference route exist and
are tested directly; wiring an actual extension-id link into the UI
waits for WP-0014.
6 new Docker-gated tests (`test_reference_policy_doc_renders`,
`_profile_doc_renders`, `_unknown_slug_is_404`, `_unknown_kind_is_404`,
`_requires_login`, `test_phase_detail_links_to_policy_reference`). Full
suite: 94 passing offline (unchanged, this task touches no offline
code path), 164 passing with Docker (up from 158).
```task
id: TREV-WP-0015-T04
status: done
priority: medium
state_hub_task_id: "af660317-e732-4ad7-bd4a-86ada8602ec7"
```
**Ledger UI change** (addendum §3): drop the `ledger` input from
`phase_new.html`; have the registration route auto-compute
`phase.ledger` as the canonical `/phases/{phase_id}/ledger` reference
before validation/persistence. Add a small "ledger reference"
link/disclosure to `phase_detail.html` showing the raw reference plus a
live-data link when it resolves to this same instance. The existing
Ledger entries table is untouched.
**Result:** Done as part of T01's fix (same form/route touched by both
tasks — see T01's Result). `control_plane_app.py`'s registration route
sets `phase["ledger"] = f"/phases/{phase_id}/ledger"` before validation;
`phase_new.html` no longer has a `ledger` input at all.
`phase_detail.html` gained a `<details>` disclosure showing the raw
reference plus a live-JSON link when it resolves to this instance
(`manifest.phase.ledger.startswith('/phases/')`). Regression test:
`test_phase_new_form_has_no_ledger_input`.
```task
id: TREV-WP-0015-T05
status: done
priority: high
state_hub_task_id: "816f1ac2-da40-4550-8d6c-64a1a335a660"
```
**`forgejo_hubs` migration** (addendum §4): new migration
`migrations/0007_forgejo_hubs.sql`, table `forgejo_hubs (hub_slug
PRIMARY KEY, service_uri, first_seen_at, updated_at)`, auto-populated on
first sight of a `repo_hub` during Phase registration (mirroring
`ensure_licensor_identity`'s auto-create-on-first-INSERT trigger
pattern in `migrations/0005_licensor_credentials.sql`). Decide during
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
priority: medium
state_hub_task_id: "719aa6db-93d8-4eaa-ac1b-e7ee4c00bd89"
```
**Backfill the three example manifests** (addendum §5):
`examples/pilot-candidates/{net-kingdom-local-identity,
railiance-vergabe-teilnahme, info-tech-canon-service-surface}/manifest.json`
each get `repo_hub`/`repo_hub_uri`/`repo_id`/`repo_name` from their real
Forgejo data (`GET /api/v1/repos/{owner}/{repo}`, same call already
confirmed working for `target-revenue` itself during WP-0012-T02), no
`base_phase_id` (all three are first Phases), and `ledger` set to match
what the Control Plane would now auto-compute.
**Result:** Confirmed live against `forgejo.coulomb.social` (all three
repos are `coulomb/*`, same org as `target-revenue`): `net-kingdom` id
`67`, `vergabe-teilnahme` id `62` (the actual application repo per
`history/260730-EffortCalculator-CandidateApplication.md` §3, not the
`railiance-apps` deployment repo), `info-tech-canon` id `47`. All three
manifests backfilled with real, not placeholder, ids; `ledger` set to
`/phases/{phase_id}/ledger` matching §3's auto-compute convention. New
regression test: `test_pilot_candidate_manifest_carries_real_repo_provenance`.
```task
id: TREV-WP-0015-T07
status: done
priority: high
state_hub_task_id: "39b34438-a1b1-49da-a88a-f3bec926aa89"
```
**Tests, docs, and closeout**: offline schema/validation tests (T01),
Docker-gated `TestClient` tests for T03's new routes and T04/T05's
changes (extending `tests/test_control_plane_app.py`), a rendering smoke
test that each `specs/policies/`/`specs/profiles/` file actually renders
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.