feat(projection): derive a repository's projection from the forge
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 23s

Implements ADR-012 decisions 1 and 2 (STATE-WP-0083 T01, T02 partial). Central
clones the default branch from Forgejo and derives its own projection: 69
workplans and 459 tasks from the-custodian at d5013ae, identical across runs,
with the commit recorded as provenance.

Identifiers are derived in the ADR-007 namespace and verified against live
records, so a forge-derived projection and a preliminary overlay agree on
identity without reconciliation.

The diff first matched hub records by UUID and was badly wrong: most hub records
carry pre-ADR-007 random identifiers, so nearly everything appeared
simultaneously missing and stale, and a reset built on it would have destroyed
and recreated the entire projection. It now matches canonical record id, falling
back to the backing file. whitehat-security — bootstrapped straight from files —
now reports clean, which is the control.

Task-level comparison is deliberately not trusted: hub tasks carry no canonical
record id, only a title, so matching is by title. Recorded as T06; T03 is
limited to workplans until it lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
This commit is contained in:
tegwick 2026-08-25 23:34:25 +02:00
parent 6390b7bead
commit fd0d0d537b
3 changed files with 550 additions and 2 deletions

View file

@ -0,0 +1,98 @@
"""Deriving a projection from the forge (STATE-WP-0083-T01).
The properties that matter are identity and determinism: a forge-derived
projection must compute the same record identities as repo-manager, and the same
commit must always yield the same projection. Without both, the reset in T03
cannot be verified against anything.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from api.services import forge_projection as fp
def test_identity_matches_the_fleet_derivation():
"""Same namespace as ADR-007, so overlay and forge agree on identity."""
assert fp.derived_record_uuid("CUST-WP-0067") == "16249302-2767-55df-aec0-d92c2751c225"
assert fp.derived_record_uuid("CUST-WP-0067-T01") == "f3608db4-20a5-58fb-a965-885eb14858af"
def _repo(tmp_path: Path) -> Path:
root = tmp_path / "demo"
(root / "workplans" / "archived").mkdir(parents=True)
(root / "workplans" / "DEMO-WP-0001-a.md").write_text(
"---\nid: DEMO-WP-0001\ntype: workplan\ntitle: \"First\"\nstatus: active\n---\n\n"
"## Do the thing\n\n```task\nid: DEMO-WP-0001-T01\nstatus: todo\npriority: high\n```\n\n"
"## Do the other\n\n```task\nid: DEMO-WP-0001-T02\nstatus: done\npriority: low\n```\n",
encoding="utf-8",
)
(root / "workplans" / "archived" / "260101-DEMO-WP-0002-b.md").write_text(
"---\nid: DEMO-WP-0002\ntype: workplan\ntitle: \"Second\"\nstatus: finished\n---\n\n# b\n",
encoding="utf-8",
)
(root / "workplans" / "NOTES.md").write_text(
"---\nid: NOT-A-WORKPLAN\ntype: note\n---\n\n# not a workplan\n", encoding="utf-8"
)
return root
def test_derives_workplans_tasks_and_archived_flag(tmp_path):
p = fp.derive_from_checkout(_repo(tmp_path), "demo", "abc123")
assert [w.record_id for w in p.workplans] == ["DEMO-WP-0001", "DEMO-WP-0002"]
first, second = p.workplans
assert first.status == "active" and first.archived is False
assert second.archived is True
assert p.task_count == 2
assert [t.record_id for t in first.tasks] == ["DEMO-WP-0001-T01", "DEMO-WP-0001-T02"]
def test_ignores_files_that_are_not_workplans(tmp_path):
"""Selection is by `type: workplan`; anything else is not this hub's business."""
p = fp.derive_from_checkout(_repo(tmp_path), "demo", "abc123")
assert all(w.record_id != "NOT-A-WORKPLAN" for w in p.workplans)
def test_task_titles_fall_back_to_the_preceding_heading(tmp_path):
p = fp.derive_from_checkout(_repo(tmp_path), "demo", "abc123")
titles = [t.title for t in p.workplans[0].tasks]
assert titles == ["Do the thing", "Do the other"]
def test_identifiers_are_derived_not_read_from_the_file(tmp_path):
"""A forge projection must not inherit whatever id a file happens to carry."""
root = _repo(tmp_path)
f = root / "workplans" / "DEMO-WP-0001-a.md"
f.write_text(
f.read_text(encoding="utf-8").replace(
"status: active",
'status: active\nstate_hub_workstream_id: "00000000-0000-4000-8000-000000000000"',
),
encoding="utf-8",
)
p = fp.derive_from_checkout(root, "demo", "abc123")
assert p.workplans[0].uuid == fp.derived_record_uuid("DEMO-WP-0001")
assert p.workplans[0].uuid != "00000000-0000-4000-8000-000000000000"
def test_same_input_yields_identical_output(tmp_path):
root = _repo(tmp_path)
assert fp.derive_from_checkout(root, "demo", "abc").to_dict() == \
fp.derive_from_checkout(root, "demo", "abc").to_dict()
def test_missing_workplans_directory_is_empty_not_an_error(tmp_path):
(tmp_path / "bare").mkdir()
p = fp.derive_from_checkout(tmp_path / "bare", "bare", "abc")
assert p.workplans == [] and p.commit == "abc"
def test_clone_failure_is_reported_not_swallowed(monkeypatch):
def boom(*a, **k):
raise fp.ForgeDeriveError("repository not found")
monkeypatch.setattr(fp, "_run_git", boom)
with pytest.raises(fp.ForgeDeriveError, match="not found"):
fp.derive_from_forge("nope")