Active user attribute set, latest activities, guarded changes to used values, artefact links, durations and averages; irreversible showcase step
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s
Work Records / validate (push) Has been cancelled

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

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 295952@bnt-lap001
Assistant-Session: e93f64ad-516c-46eb-9666-aad8d300c477
This commit is contained in:
tegwick 2026-09-29 15:50:56 +02:00
parent 6716cee921
commit 8d62c1fbe8
22 changed files with 1025 additions and 35 deletions

View file

@ -389,17 +389,67 @@ def effective_outcome(run: Run, now: datetime | None = None, stale_days: int = S
return "abandoned" if (now - last).days >= stale_days else "stalled"
def _ts(s: str) -> datetime:
return datetime.fromisoformat(s)
def fmt_duration(seconds: float | None) -> str:
"""Human duration: '45 s', '3 min 20 s', '1 h 5 min', '2 d 3 h'."""
if seconds is None:
return "-"
s = int(round(seconds))
if s < 60:
return f"{s} s"
if s < 3600:
return f"{s // 60} min {s % 60} s" if s % 60 else f"{s // 60} min"
if s < 86400:
return f"{s // 3600} h {(s % 3600) // 60} min" if (s % 3600) // 60 else f"{s // 3600} h"
return f"{s // 86400} d {(s % 86400) // 3600} h" if (s % 86400) // 3600 else f"{s // 86400} d"
def run_times(run: Run, now: datetime | None = None) -> dict:
"""Start, finish and durations of one run.
elapsed: start to finish (to now while unfinished); working: the time steps were actually in progress;
step_seconds: per step, from start to finish.
"""
now = now or datetime.now(timezone.utc)
start = _ts(run.data["created"])
fin = run.data.get("finished")
end = _ts(fin) if fin else now
steps = {}
for sid, st in run.data["steps"].items():
if st.get("started") and st.get("finished"):
steps[sid] = max(0.0, (_ts(st["finished"]) - _ts(st["started"])).total_seconds())
return {"started": run.data["created"], "finished": fin, "elapsed": max(0.0, (end - start).total_seconds()),
"finished_run": bool(fin), "working": sum(steps.values()), "step_seconds": steps}
def scorecard(pack: Pack, now: datetime | None = None, stale_days: int = STALE_DAYS) -> dict:
"""How often a runbook was tried and how it went, derived from its run records."""
"""How often a runbook was tried and how it went, derived from its run records.
avg_seconds / median_seconds / fastest_seconds are the elapsed times of *succeeded* runs only:
unfinished and failed runs say nothing about how long a working run takes.
"""
card = {"tried": 0, "succeeded": 0, "failed": 0, "abandoned": 0, "stalled": 0,
"feedback": {"good": 0, "neutral": 0, "poor": 0}}
durations: list[float] = []
for rid in list_runs(pack):
r = load(pack, rid)
card["tried"] += 1
card[effective_outcome(r, now, stale_days)] += 1
outcome = effective_outcome(r, now, stale_days)
card[outcome] += 1
if outcome == "succeeded" and r.data.get("finished"):
durations.append(run_times(r)["elapsed"])
fb = latest_feedback(r)
if fb:
card["feedback"][fb["rating"]] += 1
durations.sort()
card["avg_seconds"] = sum(durations) / len(durations) if durations else None
card["median_seconds"] = (durations[len(durations) // 2] if len(durations) % 2 else
(durations[len(durations) // 2 - 1] + durations[len(durations) // 2]) / 2) if durations else None
card["fastest_seconds"] = durations[0] if durations else None
card["timed_runs"] = len(durations)
return card