Add read-only freshness coverage measurement and 2026-10-04 evidence

For STATE-WP-0094-T05: runner revision, active definition, per-repo Forge
commit, projection sync proxy, queued receipts and freshness window, with
explicit gaps and coverage holes. No writes to hub, cluster or Forge.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 671493@bnt-lap001
Assistant-Session: 60d7b11f-78f9-4731-9367-918511b2762d
This commit is contained in:
tegwick 2026-10-04 12:46:01 +02:00
parent a8184d5e14
commit e3940cdc26
4 changed files with 2455 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,60 @@
# Freshness coverage measurement — 2026-10-04
For STATE-WP-0094-T05 (freshness-coverage half). Raw data:
`docs/evidence/2026-10-04-freshness-coverage.json`, produced read-only by
`uv run python -m repo_manager.freshness --out <file>` at 2026-10-04T10:40Z.
Sources: State Hub REST (GET only) on the primary (`instance_role=primary`,
`railiance01`, schema `a0b1c2d3e4f5`); `git ls-remote` against Forge;
`kubectl get` (read-only); `statehub outbox status`. No workstation checkout
was read (ADR-002). There were no writes to the hub, cluster, Forge or any checkout.
## Required fields and what the sources supply
| T05 field | Result | Source / gap |
| --- | --- | --- |
| Exact runner revision | **No scheduled C-rule runner exists.** Engine source HEAD `a8184d5e14fb` (repo-manager, not deployed: no repo-manager Deployment in the cluster). Adapter image `state-hub:main-5dd3df9`; scheduler image `activity-core@sha256:75e240ac…` | kubectl, git |
| Active definition | **None active.** Only C-rule definition live in `activity-core/actcore-external-activity-definitions`: `state-hub-consistency-sweep.md` (id `7c4e9a12-…`, v1.1, `enabled: false`, `status: paused`, `*/15`), paused by RMGR-WP-0005-T11. It is also the legacy `--all --remote` sweep path, which ADR-002 excludes | kubectl |
| Per-repository source commit | 141/141 active repositories resolved a Forge HEAD | `git ls-remote` |
| Last successful receipt | **Gap.** The hub does not persist projection receipts or expected/derived commits; `rmgr sync` returns the receipt only to its caller. Proxy: newest `backing_synced_at` per repository snapshot. This proxy shows that a sync happened. It does not show that the projection equals Forge HEAD | hub snapshot |
| Failed / queued receipts | Queued: edge outbox 0 pending / 0 conflict. Failed: **gap.** No sync/projection kinds in the mutation meter (1305 entries). `/ops-runs/summary` is unavailable (`ACTIVITY_CORE_URL is not configured`) | statehub outbox, hub |
| Measured freshness window | See below. The window is observed, not an SLA, because no schedule drives it | hub snapshot |
## Coverage numbers (active repositories = 141 of 146)
- Projection sync timestamp present: 136; absent: 5
- Synced within 24 h: 35 (25 %); within 7 d: 120 (85 %); within 30 d: 124
- Age of last projection sync (hours): min 0.2, median 109.2, p90 292.1, max 949.4 (~39.5 d)
- Task dependency rows projected: 524. They were not reconciled by normal receiving; see the State Hub T05 note.
## Explicit coverage holes
Measured now:
1. `access-engine`: snapshot request timed out, so its freshness is unknown.
2. `clay-borg`, `pr-hall-of-helix`, `python-snake`: 0 projected workplans and no sync stamp.
3. `repo-seed`: 2 workplans, no `backing_synced_at`.
4. Older than 7 days (16): `railiance-forge`, `ralph-workplan`, `rapp-issue-core`,
`rapp-sbom-nexus`, `repo-scoping`, `shard-wiki`, `target-revenue`, `tele-mcp`,
`testdrive-jsui`, `timeline-svg` (all ~949 h); `tmux-amq` (940 h);
`whynot-design` (862 h); `rapp-canned-prompts` (625 h); `rein-openweights`,
`resource-control` (292 h); `railiance-enablement` (243 h).
5. All 141: projected commit is unknown, so Forge HEAD equality cannot be proven for any repository.
Historical registrar failures (kept as holes, not counted as covered):
6. RMGR-WP-0005-T11: legacy sweep paused because railiance01 checkouts targeted
the stale `gitea-remote` lineage. Nothing since has proved coverage for the repositories that sweep touched.
7. RMGR-WP-0005 rail-kubernetes (2026-08-22): wrapper reported `failed` before
file writeback. It was fixed in `c90f701`, but there is no later scheduled receipt.
8. RMGR-WP-0019-T03 (wait): Activity receiving refused three empty duplicate
projections; disposition is pending under STATE-WP-0095-T04 (decision `91b7c633-…`).
9. RMGR-WP-0014/0015: unknown aliases, same-parent UUID collisions and active
unbound invalid records still refuse before writes. Any repository that hits these is unsynchronised by design.
## Conclusion
No supported freshness schedule exists. The T05 freshness requirement is not met.
Freshness depends on ad hoc syncs (25 % of repositories within 24 h), and
receipts are not durable. Proposal: RMGR-WP-0020 (bounded Activity Core → Repo
Manager runner). It is not enabled.

View file

@ -0,0 +1,251 @@
"""Read-only freshness-coverage measurement (STATE-WP-0094-T05 evidence).
Gathers, from currently supported sources only:
* State Hub REST (GET only): instance identity, ``/repos/``, per-repository
``work-record-projection/snapshot`` (``backing_synced_at``).
* Forge (``git ls-remote``, read-only): per-repository source HEAD commit.
* Kubernetes (``kubectl get``, read-only): runner images and the live
Activity Core definition that would route C-rule runs to Repo Manager.
* Local State Hub edge outbox (``statehub outbox status``): queued receipts.
It never writes to the hub, the cluster, Forge or any checkout, and it does not
read workstation checkouts (ADR-002). Fields that no supported source exposes are
reported under ``gaps`` instead of being inferred.
Usage::
python -m repo_manager.freshness --out docs/evidence/<date>-freshness-coverage.json
"""
from __future__ import annotations
import argparse
import json
import re
import statistics
import subprocess
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
DEFINITIONS_CONFIGMAP = ("activity-core", "actcore-external-activity-definitions")
C_RULE_QUERY = "consistency_sweep_remote_all"
def _get(api: str, path: str, timeout: float = 30) -> Any:
with urllib.request.urlopen(f"{api}{path}", timeout=timeout) as resp:
return json.loads(resp.read())
def _run(cmd: list[str], timeout: float = 30) -> tuple[int, str]:
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=False)
except (OSError, subprocess.TimeoutExpired) as exc:
return 1, str(exc)
return proc.returncode, (proc.stdout if proc.returncode == 0 else proc.stderr).strip()
def _parse_ts(value: str | None) -> datetime | None:
if not value:
return None
return datetime.fromisoformat(value)
def forge_head(remote_url: str | None) -> dict[str, Any]:
if not remote_url:
return {"commit": None, "error": "no remote_url"}
code, out = _run(["git", "ls-remote", remote_url, "HEAD"], timeout=30)
if code != 0 or not out:
return {"commit": None, "error": (out or "empty ls-remote")[-200:]}
return {"commit": out.split()[0], "error": None}
def projection_sync(api: str, slug: str) -> dict[str, Any]:
try:
snap = _get(api, f"/repos/{slug}/work-record-projection/snapshot")
except Exception as exc: # noqa: BLE001 - evidence collector records any failure
return {"error": str(exc)[-200:]}
stamps = [w.get("backing_synced_at") for w in snap.get("workplans", [])]
stamps = [s for s in stamps if s]
return {
"error": None,
"workplans": len(snap.get("workplans", [])),
"tasks": len(snap.get("tasks", [])),
"dependencies": len(snap.get("dependencies", [])),
"last_backing_synced_at": max(stamps) if stamps else None,
}
def live_definitions() -> dict[str, Any]:
ns, name = DEFINITIONS_CONFIGMAP
code, out = _run(["kubectl", "-n", ns, "get", "configmap", name, "-o", "json"])
if code != 0:
return {"error": out[-200:], "c_rule_definitions": []}
data = json.loads(out).get("data", {})
found = []
for key, body in sorted(data.items()):
if C_RULE_QUERY not in body and "repo-manager" not in body.lower():
continue
field = {
f: (m.group(1).strip().strip('"') if m else None)
for f, m in (
("id", re.search(r"^\s*id:\s*(.+)$", body, re.MULTILINE)),
("version", re.search(r"^\s*version:\s*(.+)$", body, re.MULTILINE)),
("enabled", re.search(r"^\s*enabled:\s*(\S+)", body, re.MULTILINE)),
("status", re.search(r"^\s*status:\s*(\S+)", body, re.MULTILINE)),
("cron", re.search(r"cron_expression:\s*\"?([^\"\n]+)", body)),
)
}
found.append({"key": key, **field})
return {"error": None, "configmap": f"{ns}/{name}", "c_rule_definitions": found}
def runner_revisions(repo_root: Path) -> dict[str, Any]:
code, head = _run(["git", "-C", str(repo_root), "rev-parse", "HEAD"])
images = {}
for ns, deploy in (("state-hub", "state-hub"), ("activity-core", "actcore-worker")):
c, img = _run(
[
"kubectl",
"-n",
ns,
"get",
"deploy",
deploy,
"-o",
"jsonpath={.spec.template.spec.containers[0].image}",
]
)
images[f"{ns}/{deploy}"] = img if c == 0 else f"unavailable: {img[-120:]}"
c, rm = _run(["kubectl", "get", "deploy", "-A", "-o", "name"])
rm_deployed = [d for d in rm.splitlines() if "repo-manager" in d] if c == 0 else None
return {
"repo_manager_source_head": head if code == 0 else None,
"repo_manager_cluster_deployments": rm_deployed,
"images": images,
}
def summarize(rows: list[dict[str, Any]], now: datetime) -> dict[str, Any]:
"""Pure summary of per-repository rows (unit-tested)."""
active = [r for r in rows if r.get("status") == "active"]
ages = []
for r in active:
ts = _parse_ts(r.get("last_backing_synced_at"))
r["age_hours"] = round((now - ts).total_seconds() / 3600, 1) if ts else None
if ts:
ages.append(r["age_hours"])
ages.sort()
def within(h: float) -> int:
return sum(1 for a in ages if a <= h)
def pct(p: float) -> float | None:
if not ages:
return None
return ages[min(len(ages) - 1, round(p * (len(ages) - 1)))]
return {
"repos_total": len(rows),
"repos_active": len(active),
"active_with_projection_sync": len(ages),
"active_never_synced_or_no_workplans": len(active) - len(ages),
"active_forge_head_resolved": sum(1 for r in active if r.get("forge_commit")),
"synced_within_24h": within(24),
"synced_within_7d": within(24 * 7),
"synced_within_30d": within(24 * 30),
"age_hours": {
"min": ages[0] if ages else None,
"median": statistics.median(ages) if ages else None,
"p90": pct(0.9),
"max": ages[-1] if ages else None,
},
}
def measure(api: str, repo_root: Path, workers: int = 8) -> dict[str, Any]:
now = datetime.now(UTC)
health = _get(api, "/state/health")
repos = _get(api, "/repos/?limit=1000")
def row(repo: dict[str, Any]) -> dict[str, Any]:
forge = forge_head(repo.get("remote_url")) if repo.get("status") == "active" else {}
proj = projection_sync(api, repo["slug"])
return {
"slug": repo["slug"],
"status": repo.get("status"),
"remote_url": repo.get("remote_url"),
"forge_commit": forge.get("commit"),
"forge_error": forge.get("error"),
"hub_last_state_synced_at": repo.get("last_state_synced_at"),
"last_backing_synced_at": proj.get("last_backing_synced_at"),
"projection_error": proj.get("error"),
"workplans": proj.get("workplans"),
"tasks": proj.get("tasks"),
"dependencies": proj.get("dependencies"),
}
with ThreadPoolExecutor(max_workers=workers) as pool:
rows = sorted(pool.map(row, repos), key=lambda r: r["slug"])
code, outbox = _run(["statehub", "outbox", "status"])
try:
outbox_status = json.loads(outbox) if code == 0 else {"error": outbox[-200:]}
except json.JSONDecodeError:
outbox_status = {"error": outbox[-200:]}
try:
ops_runs = _get(api, "/ops-runs/summary")
except Exception as exc: # noqa: BLE001
ops_runs = {"error": str(exc)[-200:]}
return {
"schema": "repo-manager.freshness-coverage.v1",
"measured_at": now.isoformat().replace("+00:00", "Z"),
"hub": {k: health.get(k) for k in ("status", "instance_role", "instance_label")}
| {"api_base": api, "schema": health.get("schema")},
"runner": runner_revisions(repo_root),
"active_definition": live_definitions(),
"receipts": {
"edge_outbox": outbox_status,
"ops_runs_summary": ops_runs,
},
"summary": summarize(rows, now),
"repositories": rows,
"gaps": [
(
"State Hub does not persist per-repository projection receipts or the "
"derived/expected source commit; `rmgr sync` returns the receipt to its "
"caller only. last_backing_synced_at is the closest supported proxy for "
"'last successful receipt' and proves no commit equality with forge_commit."
),
(
"Failed receipts are not persisted anywhere queryable (mutation meter has "
"no sync/projection kinds; ops-runs summary depends on ACTIVITY_CORE_URL)."
),
(
"No supported scheduled C-rule runner exists; freshness is driven only by "
"manual/agent-initiated syncs, so the window below is observed, not an SLA."
),
],
}
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--api-base", default="http://127.0.0.1:8000")
parser.add_argument("--repo-root", default=str(Path(__file__).resolve().parents[2]))
parser.add_argument("--out", default=None)
args = parser.parse_args(argv)
result = measure(args.api_base.rstrip("/"), Path(args.repo_root))
text = json.dumps(result, indent=2, sort_keys=False) + "\n"
if args.out:
Path(args.out).write_text(text)
print(json.dumps(result["summary"], indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())

24
tests/test_freshness.py Normal file
View file

@ -0,0 +1,24 @@
from datetime import UTC, datetime
from repo_manager.freshness import summarize
def test_summarize_counts_windows_and_holes():
now = datetime(2026, 10, 4, 12, 0, tzinfo=UTC)
rows = [
{"status": "active", "last_backing_synced_at": "2026-10-04T10:00:00Z",
"forge_commit": "a"},
{"status": "active", "last_backing_synced_at": "2026-09-30T12:00:00Z",
"forge_commit": None},
{"status": "active", "last_backing_synced_at": None, "forge_commit": "b"},
{"status": "archived", "last_backing_synced_at": "2026-01-01T00:00:00Z"},
]
s = summarize(rows, now)
assert s["repos_total"] == 4
assert s["repos_active"] == 3
assert s["active_with_projection_sync"] == 2
assert s["active_never_synced_or_no_workplans"] == 1
assert s["active_forge_head_resolved"] == 2
assert s["synced_within_24h"] == 1
assert s["synced_within_7d"] == 2
assert s["age_hours"]["max"] == 96.0