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, detail: dict[str, Any] | None = None,
) -> None: ) -> None:
try: try:
from datetime import datetime, timezone from datetime import UTC, datetime
path = meter_path() path = meter_path()
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
row = { row = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
"source": source, "source": source,
"kind": kind, "kind": kind,
"repo_slug": repo_slug, "repo_slug": repo_slug,
@ -409,6 +409,31 @@ def rm_update_register_entry(
return result 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( def rm_scaffold(
*, *,
repo_path: str | Path, 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` | | Flag | `RM_SLICE_REGISTERS` |
| Owner API | **does not exist** — see the readiness correction above | | Owner API | `rmgr register ...`; `rmgr sbom scan|licence-report` |
| Rollback | n/a until there is something to roll back from | | Rollback | flag → `off`; retain current State Hub projection |
**Blocked.** repo-manager has no register surface: no SBOM ingest, no repo **Owner ready (2026-08-21).** Finished RMGR-WP-0008 supplies the shared six-kind
goals, no contributions, no technical-debt or extension-point registry. This register spine, governed compatibility adapter, lockfile-derived SBOM and
slice cannot start until a repo-manager workplan builds one. licence/copyleft report. Historical SBOM rows are cache provenance; rebuild
from current repository sources rather than copying them into authority.
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.
### A4 · Work + repo UI — 22 items ### A4 · Work + repo UI — 22 items
`work-coordination-ui` (14, `replace`) · `repo-ui` (8) `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` | | 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 | | Rollback | remove redirect; hub pages are unchanged behind it |
`replace`, not `move`: the contract changes. Must follow A1A3, since the pages `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 ### A5 · Topic spine — 8 items
`topic-spine` (8, `replace`) `topic-spine` (8, `replace`)
@ -240,14 +240,17 @@ render what those slices serve.
| Owner API | `rmgr` topic/classification contract | | Owner API | `rmgr` topic/classification contract |
| Rollback | flag → `off` | | 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 HUB-WP-0004 finished 2026-08-21 and accepted the project architecture, runtime,
`ready`.** Slices are listed to fix scope and order, not to start. ports, conformance scaffold, and absorption plan. Individual production
cutovers still require their slice evidence and operator gates.
| Slice | Capabilities | Items | | 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 1. ~~Resolve the legacy-meter window discrepancy~~ — done 2026-08-20; E1 and the
evidenced part of E2 are executed. evidenced part of E2 are executed.
2. **Raise a `repo-manager` workplan for the register surface** (A3), the UI 2. ~~Build the Repo Manager receiving surface for A3/A5 and settle A4
surface (A4), and the topic-spine contract (A5). Until that exists, 79 of ownership~~ — finished in RMGR-WP-0008. A4 belongs to hub-core projection UI,
Wave A's 165 items have nowhere to go. This is now the critical path for the not Repo Manager.
whole retirement, ahead of anything State Hub can do to itself.
3. Cut **A2a** (task status + reconcile, ~6 items) — the only slice in the plan 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` executable today. First step is operational: create `~/.repo-manager/dual-run.yaml`
and enable writeback/reconcile for one pilot repo, then watch the mutation and enable writeback/reconcile for one pilot repo, then watch the mutation
meter. A2bA2e need repo-manager parsers and adapters that do not exist. 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 5. Do not build register, UI, or topic capability in State Hub to unblock
items 23 — `policies/retirement-freeze.md` makes that inadmissible. 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 seen["args"][0:2] == ["register", "put"]
assert "technical-debt" in seen["args"] assert "technical-debt" in seen["args"]
assert '{"severity":"high"}' 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"]