|
10 | 10 |
|
11 | 11 | from __future__ import annotations |
12 | 12 |
|
| 13 | +import json |
13 | 14 | import sys |
14 | 15 | from pathlib import Path |
15 | 16 |
|
16 | | -from . import assemble, brief, gates, leaves, state |
| 17 | +from . import assemble, brief, gates, leaves, signoff, state |
17 | 18 | from .config import Config |
18 | 19 |
|
19 | 20 |
|
@@ -52,9 +53,11 @@ def advance(d: Path, cfg: Config) -> None: |
52 | 53 | assemble.assemble_summary(d, cfg) # pure code → SUMMARY.md §1–8 |
53 | 54 | elif s == state.ITERATE_DO: |
54 | 55 | _say(f"→ {d.name}: iterate-to-Do — clearing downstream, rebuilding…") |
55 | | - _clear_downstream_of_brief(d) # re-run Do against the same brief |
| 56 | + _carry_forward_into_brief(d) # persist the WHY before the clear wipes it |
| 57 | + _clear_downstream_of_brief(d) # re-run Do against the (now annotated) brief |
56 | 58 | elif s == state.ITERATE_PLAN: |
57 | 59 | _say(f"→ {d.name}: iterate-to-Plan — versioning brief…") |
| 60 | + _carry_forward_into_brief(d) # annotate the brief before it is versioned |
58 | 61 | _version_brief_and_clear(d) # preserve brief.vN.md; human re-authors |
59 | 62 | # UNPLANNED / AWAITING_SIGNOFF / COMPLETE: nothing for the driver to do. |
60 | 63 |
|
@@ -97,3 +100,57 @@ def _next_brief_version(d: Path) -> int: |
97 | 100 | existing = [p.stem for p in d.glob("brief.v*.md")] |
98 | 101 | nums = [int(s.split("brief.v")[1]) for s in existing if s.split("brief.v")[1].isdigit()] |
99 | 102 | return (max(nums) + 1) if nums else 1 |
| 103 | + |
| 104 | + |
| 105 | +# ---------------------------------------------------------------------------- |
| 106 | +# Iterate carry-forward — persist the WHY into the one input the next beat reads. |
| 107 | +# ---------------------------------------------------------------------------- |
| 108 | +def _carry_forward_into_brief(d: Path) -> None: |
| 109 | + """Fold the previous iteration's insight into ``brief.md`` BEFORE the clear wipes |
| 110 | + SUMMARY/check-*, so the next attempt isn't blind. On iterate-do the annotated |
| 111 | + brief stays in place (the rebuild reads it); on iterate-plan it is versioned to |
| 112 | + ``brief.vN.md`` with the re-authoring context attached. |
| 113 | +
|
| 114 | + Captures whatever is available — the §9 sign-off rationale AND the failing gates |
| 115 | + (gating *and* advisory, since an iterate is often driven by an advisory red), so |
| 116 | + an iterate with no recorded rationale still carries context. Best-effort: it must |
| 117 | + never break the transition, so any failure is swallowed. |
| 118 | + """ |
| 119 | + brief_path = d / "brief.md" |
| 120 | + if not brief_path.exists(): |
| 121 | + return |
| 122 | + try: |
| 123 | + delta = signoff.iteration_delta(d / "SUMMARY.md") |
| 124 | + fails = _failing_gate_lines(d / "check-gates.json") |
| 125 | + if not delta and not fails: |
| 126 | + return |
| 127 | + n = brief_path.read_text(encoding="utf-8").count("## Iteration ") + 1 |
| 128 | + out = [f"\n## Iteration {n} — carry-forward (from the previous attempt)\n"] |
| 129 | + if delta: |
| 130 | + out.append(f"- Sign-off rationale: {delta}\n") |
| 131 | + for f in fails: |
| 132 | + out.append(f"- Failing gate: {f}\n") |
| 133 | + out.append("- Address the above; do NOT re-attempt the rejected approach " |
| 134 | + "unchanged. Satisfy the brief's Success criterion (the end result).\n") |
| 135 | + with brief_path.open("a", encoding="utf-8") as fh: |
| 136 | + fh.write("".join(out)) |
| 137 | + except Exception: # noqa: BLE001 — carry-forward is advisory; never break the iterate |
| 138 | + pass |
| 139 | + |
| 140 | + |
| 141 | +def _failing_gate_lines(gates_json: Path) -> list[str]: |
| 142 | + """``"check — evidence"`` for each failing row in ``check-gates.json`` — gating AND |
| 143 | + advisory, since an iterate is often driven by an advisory red. Best-effort.""" |
| 144 | + if not gates_json.exists(): |
| 145 | + return [] |
| 146 | + try: |
| 147 | + data = json.loads(gates_json.read_text(encoding="utf-8")) |
| 148 | + except (ValueError, OSError): |
| 149 | + return [] |
| 150 | + out: list[str] = [] |
| 151 | + for r in data.get("rows", []): |
| 152 | + if r.get("result") == "fail": |
| 153 | + ev = r.get("path_line") or r.get("oracle") or "" |
| 154 | + tag = "" if r.get("gating") else " (advisory)" |
| 155 | + out.append(f"{r.get('check', '?')}{tag} — {ev}".strip(" —")) |
| 156 | + return out |
0 commit comments