parse.py python
175 lines 5.7 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 12 hours ago
1 """Parse governance doc claims for drift verification (D1/D3)."""
2
3 from __future__ import annotations
4
5 import re
6
7 from tools.governance_hygiene.types import QueueRow
8
9 SHA_RE = r"[0-9a-fA-F]{7,40}"
10 STATUS_RE = r"\*\*([^*]+)\*\*"
11
12
13 def parse_handover_github_main_sha(text: str) -> str | None:
14 """Extract claimed GitHub ``main`` sha from handover (D1 left-hand side)."""
15 claims: set[str] = set()
16
17 patterns = [
18 rf"GitHub\s+`main`\s*\|\s*`?({SHA_RE})`?",
19 rf"\*\*main HEAD\*\*\s*\|\s*`({SHA_RE})`",
20 rf"\*\*Repo state:\*\*\s*`main`\s+at\s+`({SHA_RE})`",
21 rf"GitHub\s+`main`\s*\|\s*`\s*({SHA_RE})\s*`",
22 ]
23 for pattern in patterns:
24 for match in re.finditer(pattern, text, flags=re.IGNORECASE):
25 claims.add(match.group(1).lower())
26
27 if not claims:
28 return None
29 if len(claims) > 1:
30 return None
31 return next(iter(claims))
32
33
34 def parse_queue_rows(roadmap_text: str) -> list[QueueRow]:
35 """Parse build-queue table rows from roadmap (D3 claims)."""
36 rows: list[QueueRow] = []
37 in_queue = False
38 for line in roadmap_text.splitlines():
39 if line.strip().startswith("## Build queue"):
40 in_queue = True
41 continue
42 if in_queue and line.startswith("## "):
43 break
44 if not in_queue or not line.startswith("|"):
45 continue
46 if "---" in line or "Phase |" in line or "Phase|" in line:
47 continue
48 cells = [cell.strip() for cell in line.strip("|").split("|")]
49 if len(cells) < 4:
50 continue
51 phase_label = cells[0].strip()
52 if not phase_label or phase_label.startswith("<"):
53 continue
54 rows.append(
55 QueueRow(
56 phase_label=phase_label,
57 model=cells[1].strip(),
58 status=cells[2].strip(),
59 deliverable=cells[3].strip(),
60 raw_line=line,
61 )
62 )
63 return rows
64
65
66 def normalize_status(status: str) -> str:
67 """Normalize a queue status cell to uppercase token."""
68 cleaned = status.upper()
69 for token in ("TODO", "WIP", "DONE", "MERGED", "BLOCKED", "NEXT"):
70 if token in cleaned:
71 return token
72 return cleaned
73
74
75 def phase_tokens(phase_label: str) -> list[str]:
76 """Derive searchable tokens from a phase label."""
77 tokens: list[str] = []
78 bold = re.search(r"\*\*([^*]+)\*\*", phase_label)
79 label = bold.group(1) if bold else phase_label
80 tokens.append(label.strip())
81 for part in re.split(r"[\s/—\-]+", label):
82 part = part.strip()
83 if part and part not in tokens:
84 tokens.append(part)
85 return tokens
86
87
88 # Land/PR boilerplate words — never slice-identifying on their own
89 # (live GSW land-b false-positive: PR "GSW land-b docs sync + …" stamped the
90 # unrelated open row "PLS-a Post-land main sync freeze" DONE via bare "land"/"sync").
91 _GENERIC_PHASE_TOKENS = frozenset(
92 {
93 "main",
94 "land",
95 "sync",
96 "post",
97 "docs",
98 "doc",
99 "fix",
100 "freeze",
101 "build",
102 "mirror",
103 "merge",
104 "gate",
105 "kit",
106 "the",
107 "and",
108 "for",
109 "with",
110 "review",
111 # English product words — live ONS land-b false-positive: PR #63
112 # "Contributor prep … visibility checklist" stamped open row
113 # "Public repository visibility flip" DONE via bare "visibility".
114 "public",
115 "repository",
116 "visibility",
117 "flip",
118 "checklist",
119 "guide",
120 "prep",
121 }
122 )
123
124
125 # Slice IDs only: leading run of UPPERCASE/digits, then one or more hyphen
126 # segments (PLS-a, GSW-FIX, GFG-D2-FIX, K13-DOGFOOD). Rejects English
127 # hyphenations like Post-land that appear in titles as "post-land".
128 _COMPOUND_SLICE_RE = re.compile(r"[A-Z][A-Z0-9]{1,}(?:-[A-Za-z0-9]+)+")
129
130
131 def _compound_slice_ids(phase_label: str) -> list[str]:
132 """Hyphenated slice IDs from a phase label (``PLS-a``, ``GSW-FIX``).
133
134 ``phase_tokens`` splits on hyphens, so these must be recovered from the
135 full label. Live closeout dogfood: PR #55 title ``queue PLS post-land``
136 must not stamp open row ``PLS-a`` via the bare prefix ``PLS``, and must
137 not match via the English hyphenation ``Post-land`` / ``post-land``.
138 """
139 bold = re.search(r"\*\*([^*]+)\*\*", phase_label)
140 label = bold.group(1) if bold else phase_label
141 return _COMPOUND_SLICE_RE.findall(label)
142
143
144 def _word_bounded(needle: str, haystack: str) -> bool:
145 return bool(re.search(rf"(?<![0-9a-z]){re.escape(needle)}(?![0-9a-z])", haystack))
146
147
148 def pr_matches_row(pr_title: str, row: QueueRow) -> bool:
149 """Return whether a merged PR title plausibly belongs to a queue row.
150
151 Match evidence, in order:
152 1. Full phase label as a substring.
153 2. When the label has hyphenated slice IDs (``PLS-a``, ``GSW-FIX``), only
154 those compounds may match (word-bounded). Bare prefixes (``PLS``,
155 ``GSW``) are not enough — PR titles that merely *mention* a future
156 slice must not stamp its open queue row DONE.
157 3. Otherwise: word-bounded non-generic tokens of length >= 3.
158 """
159 title_lower = pr_title.lower()
160 tokens = phase_tokens(row.phase_label)
161 if not tokens:
162 return False
163 full_label = tokens[0].lower()
164 if full_label and full_label in title_lower:
165 return True
166 compounds = _compound_slice_ids(row.phase_label)
167 if compounds:
168 return any(_word_bounded(compound.lower(), title_lower) for compound in compounds)
169 for token in tokens[1:]:
170 cleaned = token.lower()
171 if len(cleaned) < 3 or cleaned in _GENERIC_PHASE_TOKENS:
172 continue
173 if _word_bounded(cleaned, title_lower):
174 return True
175 return False
File History 2 commits
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 12 hours ago
sha256:4671b7f787ddbe63ced31c895b688c77ab495653b65a730b423329f26b3c1439 feat: K1-P1 complete — agent provenance, build-verification… Sonnet 4.6 patch 52 days ago