#!/usr/bin/env python """_genplan.py -- shared GENERATION_PLAN.md phase-line parser. Imported by plan_edit.py (cmd_gen, show --gen-phase, grammar), genend_index.py and research_add.py so the `- . | key: value | …` line grammar (templates/GENERATION_PLAN.template.md:8-10) is parsed in one place. Also the deferred-item scan (discussion indexes + newest PhaseEnd) shared by launch.py seeds and plan_edit.py approve. Stdlib only; no imports from `pa/`. """ import os import re PHASE_RE = re.compile(r"^\s*-\s+([0-9]+(?:\.[0-9]+)+)\s+[^|]*\|") def phase_lines(text): """-> the `- . | …` lines of a GENERATION_PLAN.md text, in order.""" out = [] for raw in text.split("\n"): line = raw[:-1] if raw.endswith("\r") else raw if PHASE_RE.match(line): out.append(raw) return out def fields(line): """-> dict(id, name, milestone, scope, depends, status, phase_end) for one phase line.""" body = line[:-1] if line.endswith("\r") else line body = re.sub(r"^\s*-\s+", "", body.strip()) parts = [p.strip() for p in body.split(" | ")] m = re.match(r"^(\S+)\s+(.*)$", parts[0]) if parts else None out = {"id": m.group(1) if m else (parts[0] if parts else ""), "name": m.group(2).strip() if m else "", "milestone": "", "scope": "", "depends": "", "status": "", "phase_end": ""} for p in parts[1:]: if ":" not in p: continue k, v = p.split(":", 1) k = k.strip().lower().replace("-", "_") if k in out: out[k] = v.strip() return out def first_open(text): """-> the id of the first phase line (document order) whose status is `open`, else None.""" for line in phase_lines(text): f = fields(line) if f["status"] == "open": return f["id"] return None # --------------------------------------------------------------------------- # # deferred items (launch.py planner seed, plan_edit.py approve Triage check) # --------------------------------------------------------------------------- # PE_RE = re.compile(r"^PhaseEnd_Phase(.+)\.md$") FROM_T_RE = re.compile(r"^\s*-\s+from\s+T\d+[^:]*:\s*(.*)$") def _read(path): try: with open(path, encoding="utf-8", newline="") as fh: return fh.read() except OSError: return None def index_files(pe_dir): """-> [(path, id column)]: DISCUSSION_INDEX.md (phase | id | …), discussions/INDEX.md (id | …).""" return [(os.path.join(pe_dir, "DISCUSSION_INDEX.md"), 1), (os.path.join(pe_dir, "current", "discussions", "INDEX.md"), 0)] def index_rows(pe_dir): """-> [(path, line no, cells, id column)] for every data row of both discussion indexes.""" out = [] for path, col in index_files(pe_dir): text = _read(path) if text is None: continue for i, raw in enumerate(text.split("\n")): s = raw.rstrip("\r").strip() if not s or s.startswith("#") or s.startswith("