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:
parent
a8184d5e14
commit
e3940cdc26
4 changed files with 2455 additions and 0 deletions
2120
docs/evidence/2026-10-04-freshness-coverage.json
Normal file
2120
docs/evidence/2026-10-04-freshness-coverage.json
Normal file
File diff suppressed because it is too large
Load diff
60
docs/evidence/2026-10-04-freshness-coverage.md
Normal file
60
docs/evidence/2026-10-04-freshness-coverage.md
Normal 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.
|
||||
251
src/repo_manager/freshness.py
Normal file
251
src/repo_manager/freshness.py
Normal 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
24
tests/test_freshness.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue