diff --git a/.console/log.md b/.console/log.md index 2f9f02561..84a2b9755 100644 --- a/.console/log.md +++ b/.console/log.md @@ -1,3 +1,12 @@ +## 2026-06-13 — fix(spec-hygiene): active.json projects only active campaigns (campaign GC) + +_rebuild_active_projection wrote every campaign — incl. complete/cancelled — to state/campaigns/ +active.json, which OperatorConsole's campaign pane reads. Terminal campaigns are history (their +record lives in Plane) but accumulated forever in the projection (observed: 11 records, 10 terminal, +0 truly active — cluttering the pane). Fix: skip non-active campaigns when rebuilding the projection. ++1 test. (A one-time prune of active.json doesn't stick — it's rebuilt each spec-hygiene cycle — so +the projection filter IS the durable prune.) + ## 2026-06-13 — fix(reviewer): gate merge on the full required-check set (guard D) #272 + Guard C close "merge on red/incomplete/no-checks CI", but a hole remained: a required check diff --git a/src/operations_center/entrypoints/spec_hygiene/main.py b/src/operations_center/entrypoints/spec_hygiene/main.py index 50e2e870a..2f86f5e10 100644 --- a/src/operations_center/entrypoints/spec_hygiene/main.py +++ b/src/operations_center/entrypoints/spec_hygiene/main.py @@ -147,6 +147,14 @@ def _rebuild_active_projection( else: status = "active" + # active.json is the ACTIVE projection (OperatorConsole's campaign pane + # reads it). Terminal campaigns (complete/cancelled) are history — their + # record lives in Plane — so they are not projected here. Without this the + # projection accumulates every finished campaign indefinitely, cluttering + # the status pane (observed: 11 records, 10 terminal, only 0 truly active). + if status != "active": + continue + # Pull slug + spec_file + created_at from the parent issue when we can. parent = next( (i for i in issues if str(i.get("name", "")).startswith("[Campaign]")), diff --git a/tests/maintenance/test_spec_hygiene_task.py b/tests/maintenance/test_spec_hygiene_task.py index 1082661a7..f2c1556c8 100644 --- a/tests/maintenance/test_spec_hygiene_task.py +++ b/tests/maintenance/test_spec_hygiene_task.py @@ -98,3 +98,31 @@ def _boom(settings, client): # noqa: ARG001 result = task.run_once(MaintenanceContext(cycle_id="c", now=datetime.now(timezone.utc))) assert result.status == "failed" assert result.error == "plane down" + + +def test_rebuild_active_projection_excludes_terminal(tmp_path): + """active.json projects only active campaigns; terminal ones (complete/cancelled) + are history (in Plane) and must not accumulate in the pane projection.""" + from operations_center.spec_author.state import CampaignStateManager + + mgr = CampaignStateManager(state_path=tmp_path / "active.json") + + def issue(cid: str, state: str) -> dict: + return { + "name": f"[Campaign] {cid}", + "labels": [{"name": "source: spec-campaign"}, {"name": f"campaign-id: {cid}"}], + "state": {"name": state}, + } + + spec_hygiene_main._rebuild_active_projection( + mgr, + [ + issue("active1", "in progress"), # → active + issue("cancelled1", "cancelled"), # → cancelled (excluded) + issue("done1", "done"), # → complete (excluded) + ], + ) + + saved = mgr.load() + assert [c.slug for c in saved.campaigns] == ["active1"] + assert all(c.status == "active" for c in saved.campaigns)