v0.2 walk-through UI: guided flow, data view, overview, modes, overrides, typed phrase; retry confirmation; visual pass
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:
parent
f8ce4717a8
commit
f070999aa2
12 changed files with 1285 additions and 53 deletions
103
rtut/run2.py
103
rtut/run2.py
|
|
@ -370,11 +370,19 @@ def fail(run: Run, step_id: str, note: str = "") -> None:
|
|||
run.save()
|
||||
|
||||
|
||||
def retry(run: Run, step_id: str) -> None:
|
||||
def retry(run: Run, step_id: str, acknowledge_partial: bool = False) -> None:
|
||||
"""Retry a failed step. Unless it is declared idempotent, the operator must confirm that the earlier
|
||||
attempt did not partly apply (a repeated non-idempotent step can double its effect)."""
|
||||
_writable(run)
|
||||
st = run.data["steps"][step_id]
|
||||
if st["state"] != "failed":
|
||||
raise RunError(f"step {step_id} has not failed")
|
||||
idem = step_def(run, step_id).get("idempotent") or flow(run).steps[step_id].get("idempotent")
|
||||
if idem != "idempotent":
|
||||
if not acknowledge_partial:
|
||||
raise RunError(f"step {step_id} is {idem or 'not declared idempotent'}: confirm that the earlier attempt did not "
|
||||
"partly apply before retrying it")
|
||||
st.setdefault("retries", []).append({"at": _now(), "acknowledged_partial": True})
|
||||
for dp in flow(run).steps[step_id].get("produces", []):
|
||||
rec = run.data["datapoints"].get(dp)
|
||||
if rec and rec["state"] == "err" and rec.get("error", "").startswith("producing step"):
|
||||
|
|
@ -544,3 +552,96 @@ def _auto_confirm_gate(run: Run, gid: str, now: datetime) -> dict:
|
|||
run.save()
|
||||
return out
|
||||
return {"action": "waiting", "item": gid, "detail": "gate did not pass yet"}
|
||||
|
||||
|
||||
# ---- views for renderers: overview, datapoint table with dimensions, artefacts, mode switch -----------------------------
|
||||
|
||||
def switch_mode(run: Run, mode: str) -> None:
|
||||
"""Take over (auto-confirm to manual) or hand over (manual to auto-confirm). The change is recorded."""
|
||||
_writable(run)
|
||||
if mode not in ("manual", "auto-confirm"):
|
||||
raise RunError("mode must be manual or auto-confirm")
|
||||
if mode == run.data["mode"]:
|
||||
raise RunError(f"the run is already in {mode} mode")
|
||||
run.data.setdefault("mode_history", []).append({"from": run.data["mode"], "to": mode, "at": _now(), "who": run.data["operator"]})
|
||||
run.data["mode"] = mode
|
||||
if mode == "auto-confirm":
|
||||
run.data["auto_confirm"].pop("stopped", None)
|
||||
run.save()
|
||||
|
||||
|
||||
def overview(run: Run) -> dict:
|
||||
"""Progress per stage, datapoint states, deviations: the state of the run at a glance."""
|
||||
fl = flow(run)
|
||||
stages = []
|
||||
|
||||
def walk(items: list[dict], depth: int) -> None:
|
||||
for st in items:
|
||||
steps = fl.members(st["id"], include_gates=False)
|
||||
gates = [g for g in fl.members(st["id"]) if fl.kind(g) == "gate"]
|
||||
sstates = [run.data["steps"][s]["state"] for s in steps]
|
||||
stages.append({
|
||||
"id": st["id"], "title": st["title"], "goal": st["goal"], "depth": depth,
|
||||
"steps_total": len(steps), "steps_done": sum(x in ("done", "skipped") for x in sstates),
|
||||
"steps_pushed": sstates.count("pushed"), "steps_failed": sstates.count("failed"),
|
||||
"steps_running": sstates.count("running"),
|
||||
"gates": [{"id": g, "title": fl.gates[g]["title"], "state": run.data["gates"][g]["state"]} for g in gates],
|
||||
"complete": is_complete(run, st["id"]),
|
||||
"unmet": [] if is_complete(run, st["id"]) else unmet(run, st["id"]),
|
||||
})
|
||||
walk(st.get("stages", []), depth + 1)
|
||||
|
||||
walk(fl.data["stages"], 0)
|
||||
dps: dict[str, int] = {}
|
||||
for v in run.data["datapoints"].values():
|
||||
dps[v["state"]] = dps.get(v["state"], 0) + 1
|
||||
items = [i for i in fl.order]
|
||||
done = sum(1 for i in items if is_complete(run, i) and not (fl.kind(i) == "step" and run.data["steps"][i]["state"] == "pushed")
|
||||
and not (fl.kind(i) == "gate" and run.data["gates"][i]["state"] == "pushed"))
|
||||
pushed = sum(1 for i in items if (fl.kind(i) == "step" and run.data["steps"][i]["state"] == "pushed")
|
||||
or (fl.kind(i) == "gate" and run.data["gates"][i]["state"] == "pushed"))
|
||||
return {"stages": stages, "datapoints": dps, "deviations": list(run.data["deviations"]),
|
||||
"progress": {"done": done, "pushed": pushed, "total": len(items)}, "mode": run.data["mode"],
|
||||
"mode_history": run.data.get("mode_history", []), "stopped": run.data["auto_confirm"].get("stopped")}
|
||||
|
||||
|
||||
def dimension_keys(run: Run) -> list[str]:
|
||||
keys: set[str] = set()
|
||||
for d in flow(run).datapoints.values():
|
||||
keys |= set((d.get("dimensions") or {}).keys())
|
||||
return sorted(keys)
|
||||
|
||||
|
||||
def datapoint_rows(run: Run, group: str | None = None, flt: str | None = None) -> dict[str, list[dict]]:
|
||||
"""Datapoints with their state, optionally filtered by 'dimension:value' and grouped by a dimension.
|
||||
|
||||
Returns {group label: [rows]}; without grouping there is one group, ''.
|
||||
"""
|
||||
fl = flow(run)
|
||||
fkey, _, fval = (flt or "").partition(":")
|
||||
groups: dict[str, list[dict]] = {}
|
||||
for d in fl.datapoints.values():
|
||||
dims = d.get("dimensions") or {}
|
||||
if flt and dims.get(fkey) != fval:
|
||||
continue
|
||||
rec = run.data["datapoints"][d["id"]]
|
||||
row = {"id": d["id"], "label": d["label"], "type": d["type"], "values": d.get("values"), "state": rec["state"],
|
||||
"value": rec.get("value"), "how": rec.get("how"), "error": rec.get("error"), "dimensions": dims,
|
||||
"ask_in": d.get("ask_in"), "produced_by": d.get("produced_by"), "confirmed_by": d.get("confirmed_by"),
|
||||
"editable": not d.get("produced_by")}
|
||||
groups.setdefault(dims.get(group, "(none)") if group else "", []).append(row)
|
||||
return dict(sorted(groups.items()))
|
||||
|
||||
|
||||
def artefact_rows(run: Run) -> list[dict]:
|
||||
"""What the run produced or will produce, and where it lives (a reference, never a secret)."""
|
||||
fl = flow(run)
|
||||
out = []
|
||||
for a in run.pack.data.get("artefacts", []):
|
||||
dp = run.data["datapoints"].get(a["location"]["datapoint"], {})
|
||||
base = dp.get("value") if dp.get("state") in ("set", "ok") else None
|
||||
loc = (base.rstrip("/") + "/" + a["location"]["path"]) if base and a["location"].get("path") else base
|
||||
out.append({"id": a["id"], "title": a["title"], "location": loc, "produced_by": a["produced_by"],
|
||||
"produced_state": run.data["steps"][a["produced_by"]]["state"] if a["produced_by"] in run.data["steps"] else "?",
|
||||
"lives_until": a.get("lives_until")})
|
||||
return out
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue