parse.py
python
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