parsers.py python
111 lines 3.4 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago
1 """Document-derived gate parsers (§HGD.7.1).
2
3 Never invents ``DONE`` on garbage input.
4 """
5
6 from __future__ import annotations
7
8 import re
9 from dataclasses import dataclass
10
11 STATUS_TOKENS = frozenset({"TODO", "WIP", "DONE", "BLOCKED"})
12
13 # Build-status table rows: | **Phase** | Auto | **DONE** | ...
14 _BUILD_STATUS_ROW = re.compile(
15 r"^\|\s*\*?\*?([^|*]+?)\*?\*?\s*\|\s*([^|]+?)\s*\|\s*\*?\*?(TODO|WIP|DONE|BLOCKED)\*?\*?\s*\|",
16 re.IGNORECASE | re.MULTILINE,
17 )
18
19 # Status: DONE under a phase heading
20 _INLINE_STATUS = re.compile(
21 r"(?im)^\s*(?:\*\*)?Status(?:\*\*)?\s*:\s*\*?\*?(TODO|WIP|DONE|BLOCKED)\*?\*?",
22 )
23
24 _PENDING_GATES = re.compile(
25 r"(?is)pending[-\s]?gates[^\n]*\n+(.*?)(?=\n## |\n# |\Z)",
26 )
27
28
29 @dataclass(frozen=True)
30 class DocumentDerivedGates:
31 """Parsed document-derived gate view."""
32
33 ok: bool
34 error: str | None
35 phases: list[dict[str, str]]
36 pending_gates_excerpt: str | None
37
38
39 def parse_document_derived_gates(
40 *,
41 roadmap_text: str | None,
42 handover_text: str | None,
43 ) -> DocumentDerivedGates:
44 """Build document-derived gates from ROADMAP and/or HANDOVER text.
45
46 On total parse failure (no usable text or zero recognizable status tokens
47 when text claims build status poorly) returns ``ok: false`` without fabricating DONE.
48 """
49 phases: list[dict[str, str]] = []
50 seen: set[str] = set()
51
52 if roadmap_text:
53 for match in _BUILD_STATUS_ROW.finditer(roadmap_text):
54 phase_id = match.group(1).strip()
55 status = match.group(3).upper()
56 if status not in STATUS_TOKENS:
57 continue
58 if not phase_id or phase_id.lower() in {"phase", "id", "---"}:
59 continue
60 # Skip markdown separator rows
61 if set(phase_id) <= {"-"}:
62 continue
63 key = phase_id.lower()
64 if key in seen:
65 continue
66 seen.add(key)
67 phases.append({"id": phase_id, "status": status})
68
69 pending_excerpt: str | None = None
70 if handover_text:
71 pending_match = _PENDING_GATES.search(handover_text)
72 if pending_match:
73 excerpt = pending_match.group(1).strip()
74 if excerpt:
75 pending_excerpt = excerpt[:500]
76
77 if roadmap_text is None and handover_text is None:
78 return DocumentDerivedGates(
79 ok=False,
80 error="parse_failed",
81 phases=[],
82 pending_gates_excerpt=None,
83 )
84
85 # Garbage-only input with no recognizable phases → ok false, never invent DONE.
86 garbage_roadmap = bool(roadmap_text) and not phases and _looks_like_status_claim(roadmap_text)
87 if garbage_roadmap and not pending_excerpt:
88 return DocumentDerivedGates(
89 ok=False,
90 error="parse_failed",
91 phases=[],
92 pending_gates_excerpt=None,
93 )
94
95 return DocumentDerivedGates(
96 ok=True,
97 error=None,
98 phases=phases,
99 pending_gates_excerpt=pending_excerpt,
100 )
101
102
103 def _looks_like_status_claim(text: str) -> bool:
104 """Heuristic: text mentions Status/DONE tokens but rows failed to parse."""
105 lowered = text.lower()
106 return "status" in lowered or "done" in lowered or "| " in text
107
108
109 def never_invent_done_on_garbage(text: str) -> DocumentDerivedGates:
110 """Unit-test helper: garbage input must not invent DONE."""
111 return parse_document_derived_gates(roadmap_text=text, handover_text=None)
File History 1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago