Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
#!/usr/bin/env python3
"""
pcbflow_log — structured logging for PCB Flow automation.
Implements the two things Phase 1 of the reliability roadmap needs:
1. The {ok, err} envelope — every wrapped step returns success-or-error as DATA,
so a failure is never silently swallowed (vulnerability SC-7).
2. The JSONL log schema — one record per attempt, readable by both the AI and
the engineer, written to projects/<board>/.logs/<phase>.jsonl
See reliability/SELF_HEALING.md §1 for the schema design.
Use in a Python automation step:
from pcbflow_log import PhaseLogger
log = PhaseLogger("door-sensor", "04-schematic-generation")
result = log.run("wire block U3 (IMU)", lambda: wire_imu())
if not result["ok"]:
... # the failure is logged; decide recovery
Render a phase log for a human:
python3 tools/pcbflow_log.py render projects/door-sensor/.logs/04-schematic-generation.jsonl
"""
import json, sys, traceback, datetime
from pathlib import Path
def _now():
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
class PhaseLogger:
"""Append-only structured log for one project phase."""
def __init__(self, project, phase, root="projects"):
self.project = project
self.phase = phase
self.dir = Path(root) / project / ".logs"
self.dir.mkdir(parents=True, exist_ok=True)
self.path = self.dir / f"{phase}.jsonl"
def _write(self, rec):
rec = {"ts": _now(), "project": self.project, "phase": self.phase, **rec}
with open(self.path, "a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
return rec
# --- the four record types (SELF_HEALING.md §1) ---
def ok(self, step, **extra):
return self._write({"step": step, "status": "ok", **extra})
def failed(self, step, error_class, message, stack=None, **extra):
return self._write({"step": step, "status": "failed",
"error": {"class": error_class, "message": message, "stack": stack},
**extra})
def recovered(self, step, error_class, attempted, attempts=1, human_message="", **extra):
return self._write({"step": step, "status": "recovered",
"error": {"class": error_class},
"recovery": {"attempted": attempted, "attempts": attempts, "result": "success"},
"human_message": human_message, **extra})
def escalated(self, step, error_class, message, human_message, **extra):
return self._write({"step": step, "status": "escalated",
"error": {"class": error_class, "message": message},
"human_message": human_message, **extra})
def run(self, step, fn, error_class="SC-1"):
"""Execute fn() with the {ok, err} envelope. Never swallows: on success
returns {"ok": True, "v": <result>} and logs ok; on exception returns
{"ok": False, "err": <msg>} and logs the failure with a full stack."""
try:
value = fn()
self.ok(step)
return {"ok": True, "v": value}
except Exception as exc:
self.failed(step, error_class, str(exc), traceback.format_exc())
return {"ok": False, "err": str(exc)}
_SYM = {"ok": "✅", "failed": "❌", "recovered": "♻️ ", "escalated": "⛔"}
def render(path):
"""Print the human-readable mirror of a JSONL phase log."""
p = Path(path)
if not p.exists():
print(f"no log at {path}")
return
for line in p.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
r = json.loads(line)
t = r.get("ts", "")[11:19]
sym = _SYM.get(r.get("status"), "· ")
head = f"{t} {r.get('project','')} · {r.get('phase','')} · {r.get('step','')}"
print(f"{head} {sym}{r.get('status','')}")
msg = r.get("human_message")
if msg:
print(f" → {msg}")
err = r.get("error") or {}
if r.get("status") == "failed" and err.get("message"):
print(f" → [{err.get('class','?')}] {err.get('message')}")
if __name__ == "__main__":
if len(sys.argv) >= 3 and sys.argv[1] == "render":
render(sys.argv[2])
else:
print(__doc__)
7315b96d0This file is pinned to the selected release. Download the original to open it in your design tool.