Maturity from run history, pack revision digests, attribute sets, projects and templates (RBT-WP-0008)
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Work Records / validate (push) Has been cancelled

Also fixes run lists and scores mixing runs of different packs in one repository.

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 12:36:34 +02:00
parent 2f60d47746
commit 0203bf553c
19 changed files with 1311 additions and 33 deletions

View file

@ -11,7 +11,7 @@ from datetime import date, datetime, timezone
from pathlib import Path
from .legacy import repo_root
from .pack import PLACEHOLDER, SECRET_RE, Pack, render
from .pack import PLACEHOLDER, SECRET_RE, Pack, digest, render
STATES = ("todo", "running", "done", "failed", "skipped")
VERIFY_TIMEOUT = 60
@ -22,7 +22,7 @@ class RunError(Exception):
def _now() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds")
return datetime.now(timezone.utc).isoformat(timespec="microseconds") # sortable even within one second
def _clean(value: str, what: str) -> str:
@ -69,12 +69,23 @@ def _writable(run: Run) -> None:
raise RunError(f"run is finished (outcome: {run.data['outcome']}); start a new run")
def create(pack: Pack, operator: str, params: dict[str, str] | None = None) -> Run:
ENVIRONMENTS = ("rehearsal", "staging", "production")
def _context(ctx: dict | None) -> dict:
"""Where a run applies: project id, target and environment class. Non-secret; all optional."""
ctx = dict(ctx or {})
if ctx.get("environment") and ctx["environment"] not in ENVIRONMENTS:
raise RunError(f"environment must be one of {', '.join(ENVIRONMENTS)}")
return {k: _clean(str(v), f"context {k}") for k, v in ctx.items() if v and k in ("project", "target", "environment")}
def create(pack: Pack, operator: str, params: dict[str, str] | None = None, context: dict | None = None) -> Run:
if not pack.ok or pack.data.get("engine") != "native":
raise RunError("only valid native packs can be run")
if pack.spec == "runbook-pack/v0.2":
from . import run2
return run2.create(pack, operator, params)
return run2.create(pack, operator, params, context=context)
if pack.spec != "runbook-pack/v0.1":
raise RunError(f"{pack.spec} packs cannot be run")
operator = _clean(operator.strip(), "operator")
@ -102,6 +113,8 @@ def create(pack: Pack, operator: str, params: dict[str, str] | None = None) -> R
"created": _now(),
"params": resolved,
"steps": {s["id"]: {"state": "todo"} for s in pack.data["steps"]},
"pack_digest": digest(pack),
"context": _context(context),
}
run = Run(pack, data)
run.save()
@ -116,8 +129,18 @@ def load(pack: Pack, run_id: str) -> Run:
def list_runs(pack: Pack) -> list[str]:
"""Run ids of this pack. All packs of a repository share one run folder, so filter by pack id."""
d = runs_dir(pack)
return sorted(p.stem for p in d.glob("*.json")) if d.exists() else []
if not d.exists():
return []
out = []
for f in sorted(d.glob("*.json")):
try:
if json.loads(f.read_text()).get("pack_id") == pack.id:
out.append(f.stem)
except (OSError, ValueError):
continue
return out
def _order(run: Run, step_id: str) -> None: