feat(retirement): route SBOM scans through repo-manager
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 25s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
tegwick 2026-08-21 23:15:34 +02:00
parent b9d9ffed5f
commit 81861d816b
3 changed files with 70 additions and 22 deletions

View file

@ -144,12 +144,12 @@ def record_mutation(
detail: dict[str, Any] | None = None,
) -> None:
try:
from datetime import datetime, timezone
from datetime import UTC, datetime
path = meter_path()
path.parent.mkdir(parents=True, exist_ok=True)
row = {
"ts": datetime.now(timezone.utc).isoformat(),
"ts": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
"source": source,
"kind": kind,
"repo_slug": repo_slug,
@ -409,6 +409,31 @@ def rm_update_register_entry(
return result
def rm_scan_sbom(
*,
repo_path: str | Path,
repo_slug: str | None = None,
) -> dict[str, Any]:
"""Derive a versioned SBOM snapshot from repository-owned sources."""
args = ["sbom", "scan", "--path", str(repo_path)]
if repo_slug:
args.extend(["--slug", repo_slug])
code, out, err = run_rmgr(args)
try:
result = json.loads(out.strip() or "{}")
except json.JSONDecodeError:
result = {
"ok": False,
"error": f"rmgr non-json exit={code} stderr={err!r} stdout={out[:500]!r}",
}
if code != 0:
result.setdefault("ok", False)
result.setdefault("error", f"rmgr exit={code} stderr={err!r}")
result["exit_code"] = code
return result
def rm_scaffold(
*,
repo_path: str | Path,

View file

@ -208,16 +208,13 @@ flow and needs a State Hub API restart to take effect. Piloting on
| | |
| --- | --- |
| Flag | `RM_SLICE_REGISTERS` |
| Owner API | **does not exist** — see the readiness correction above |
| Rollback | n/a until there is something to roll back from |
| Owner API | `rmgr register ...`; `rmgr sbom scan|licence-report` |
| Rollback | flag → `off`; retain current State Hub projection |
**Blocked.** repo-manager has no register surface: no SBOM ingest, no repo
goals, no contributions, no technical-debt or extension-point registry. This
slice cannot start until a repo-manager workplan builds one.
Low coupling and low write volume still make this the right slice to prove the
`RM_SLICE_*` pattern **once the owner side exists** — but that is a
repo-manager-side prerequisite, not a State Hub task.
**Owner ready (2026-08-21).** Finished RMGR-WP-0008 supplies the shared six-kind
register spine, governed compatibility adapter, lockfile-derived SBOM and
licence/copyleft report. Historical SBOM rows are cache provenance; rebuild
from current repository sources rather than copying them into authority.
### A4 · Work + repo UI — 22 items
`work-coordination-ui` (14, `replace`) · `repo-ui` (8)
@ -225,11 +222,14 @@ repo-manager-side prerequisite, not a State Hub task.
| | |
| --- | --- |
| Flag | `RM_SLICE_UI` |
| Owner API | owner-side dashboard; hub pages redirect |
| Owner API | hub-core projection/query plus hub/ops UI clients |
| Rollback | remove redirect; hub pages are unchanged behind it |
`replace`, not `move`: the contract changes. Must follow A1A3, since the pages
render what those slices serve.
render what those slices serve. RMGR-ADR-003 corrects the earlier inventory
assignment: Repo Manager owns the repository contracts, not a dashboard. A4's
UI implementation therefore rides with B5 even though these 22 inventory items
remain grouped here for traceability.
### A5 · Topic spine — 8 items
`topic-spine` (8, `replace`)
@ -240,14 +240,17 @@ render what those slices serve.
| Owner API | `rmgr` topic/classification contract |
| Rollback | flag → `off` |
Last in Wave A: it is a `replace` under a changed contract, and A1A4 read it.
The versioned Repo Manager classification contract is published and accepted by
finished HUB-WP-0004. Consumer route switching remains a State Hub cutover
action.
---
## Wave B — hub-core (128 items) · gated on HUB-WP-0004
## Wave B — hub-core (128 items) · HUB-WP-0004 owner gate satisfied
HUB-WP-0004 is `proposed`. **No hub-side adapter work until it is at least
`ready`.** Slices are listed to fix scope and order, not to start.
HUB-WP-0004 finished 2026-08-21 and accepted the project architecture, runtime,
ports, conformance scaffold, and absorption plan. Individual production
cutovers still require their slice evidence and operator gates.
| Slice | Capabilities | Items |
| --- | --- | --- |
@ -435,14 +438,14 @@ discovered live at cutover.
1. ~~Resolve the legacy-meter window discrepancy~~ — done 2026-08-20; E1 and the
evidenced part of E2 are executed.
2. **Raise a `repo-manager` workplan for the register surface** (A3), the UI
surface (A4), and the topic-spine contract (A5). Until that exists, 79 of
Wave A's 165 items have nowhere to go. This is now the critical path for the
whole retirement, ahead of anything State Hub can do to itself.
2. ~~Build the Repo Manager receiving surface for A3/A5 and settle A4
ownership~~ — finished in RMGR-WP-0008. A4 belongs to hub-core projection UI,
not Repo Manager.
3. Cut **A2a** (task status + reconcile, ~6 items) — the only slice in the plan
executable today. First step is operational: create `~/.repo-manager/dual-run.yaml`
and enable writeback/reconcile for one pilot repo, then watch the mutation
meter. A2bA2e need repo-manager parsers and adapters that do not exist.
4. Do not open Wave B until HUB-WP-0004 reaches `ready`; 125 items sit behind it.
4. HUB-WP-0004's owner-readiness gate is satisfied; advance Wave B only through
its per-slice dual-run and operator gates.
5. Do not build register, UI, or topic capability in State Hub to unblock
items 23 — `policies/retirement-freeze.md` makes that inadmissible.

View file

@ -93,3 +93,23 @@ def test_rm_update_register_entry_builds_shared_spine_command(monkeypatch):
assert seen["args"][0:2] == ["register", "put"]
assert "technical-debt" in seen["args"]
assert '{"severity":"high"}' in seen["args"]
def test_rm_scan_sbom_builds_derived_snapshot_command(monkeypatch):
seen = {}
def fake_run(args, *, timeout=120):
seen["args"] = args
return (
0,
'{"schema":"repo-manager.sbom-snapshot.v1","ok":true,"entry_count":3}',
"",
)
monkeypatch.setattr(adapter, "run_rmgr", fake_run)
result = adapter.rm_scan_sbom(repo_path="/repos/demo", repo_slug="demo")
assert result["ok"] is True
assert result["schema"] == "repo-manager.sbom-snapshot.v1"
assert result["exit_code"] == 0
assert seen["args"] == ["sbom", "scan", "--path", "/repos/demo", "--slug", "demo"]