surface.py python
236 lines 7.4 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 9 hours ago
1 """Active-slice Mode B verification evidence surface (§LT.5.2)."""
2
3 from __future__ import annotations
4
5 import re
6 from dataclasses import dataclass
7 from pathlib import Path
8
9 from adapters.config import OverseerConfig
10 from cli.paths import confine_path
11 from tools.governance_gates.scan import (
12 BUILD_VERIFICATION_PASS_RE,
13 PHASE_DOC_RE,
14 ROADMAP_ROW_RE,
15 _is_auto_model,
16 _normalize_phase_id,
17 scan_governance_gates,
18 )
19 from tools.honesty.ledger import read_ledger_entries
20 from tools.honesty.validate import find_matching_verification_evidence
21
22 AUTO_B_STEP_RE = re.compile(r"\bb\b$", re.IGNORECASE)
23
24
25 @dataclass(frozen=True)
26 class VerificationEvidenceGateReport:
27 """Result of the active-slice verification evidence probe."""
28
29 skipped: bool
30 ok: bool
31 mode: str | None = None
32 matched: bool | None = None
33 phase_id: str | None = None
34 message: str | None = None
35 token: str | None = None
36
37
38 def build_verification_evidence_gate(
39 config: OverseerConfig,
40 repo_root: Path,
41 *,
42 handover_text: str | None = None,
43 roadmap_text: str | None = None,
44 ) -> VerificationEvidenceGateReport:
45 """Run active-slice Mode B when honesty warn/require is enabled."""
46 require = config.honesty.require_verification_evidence
47 if not config.honesty.enabled or require not in {"warn", "require"}:
48 return VerificationEvidenceGateReport(skipped=True, ok=True)
49
50 docs_root = config.repo.root_relative_docs
51 handover_path = repo_root / docs_root / config.docs.handover
52 roadmap_path = repo_root / docs_root / config.docs.roadmap
53 handover = handover_text if handover_text is not None else _read_text(handover_path)
54 roadmap = roadmap_text if roadmap_text is not None else _read_text(roadmap_path)
55
56 gate_scan = scan_governance_gates(
57 config,
58 repo_root,
59 handover_text=handover,
60 roadmap_text=roadmap,
61 )
62 if not gate_scan.active_phases:
63 return VerificationEvidenceGateReport(skipped=True, ok=True)
64
65 phase_id = _select_active_auto_phase(roadmap, gate_scan.active_phases)
66 if phase_id is None:
67 return VerificationEvidenceGateReport(skipped=True, ok=True)
68
69 if not _should_run_mode_b(phase_id, handover, roadmap):
70 return VerificationEvidenceGateReport(skipped=True, ok=True)
71
72 frozen_spec = _frozen_spec_for_phase(roadmap, phase_id)
73 matched = _ledger_match(config, repo_root, phase_id, frozen_spec)
74
75 if matched:
76 return VerificationEvidenceGateReport(
77 skipped=False,
78 ok=True,
79 mode=require,
80 matched=True,
81 phase_id=phase_id,
82 )
83
84 if require == "require":
85 return VerificationEvidenceGateReport(
86 skipped=False,
87 ok=False,
88 mode=require,
89 matched=False,
90 phase_id=phase_id,
91 message="missing verification_evidence ledger entry for active Auto slice",
92 token="missing_verification_evidence",
93 )
94
95 return VerificationEvidenceGateReport(
96 skipped=False,
97 ok=True,
98 mode="warn",
99 matched=False,
100 phase_id=phase_id,
101 message="warning: no matching verification_evidence entry for active Auto slice",
102 )
103
104
105 def verification_evidence_gate_payload(report: VerificationEvidenceGateReport) -> dict | None:
106 if report.skipped:
107 return None
108 payload: dict = {
109 "ok": report.ok,
110 "mode": report.mode,
111 "matched": report.matched,
112 }
113 if report.phase_id:
114 payload["phase_id"] = report.phase_id
115 if report.token:
116 payload["token"] = report.token
117 return payload
118
119
120 def format_verification_evidence_gate_line(report: VerificationEvidenceGateReport) -> str | None:
121 if report.skipped or report.message is None:
122 return None
123 return f"verification_evidence_gate: {report.message}"
124
125
126 def _read_text(path: Path) -> str | None:
127 if not path.is_file():
128 return None
129 return path.read_text(encoding="utf-8")
130
131
132 def _select_active_auto_phase(roadmap: str | None, active: tuple[str, ...]) -> str | None:
133 if roadmap is None:
134 return None
135 for phase_id in active:
136 row = _roadmap_row(roadmap, phase_id)
137 if row is None:
138 continue
139 model = row.group("model")
140 if _is_active_auto_model(model, phase_id):
141 return phase_id
142 return None
143
144
145 def _is_active_auto_model(model: str, phase_id: str) -> bool:
146 if not _is_auto_model(model):
147 return False
148 lowered = model.lower()
149 if "thinking" in lowered and "auto" in lowered:
150 # Thinking → Auto split: only the Auto half ({step}b) is in scope.
151 return bool(AUTO_B_STEP_RE.search(phase_id.replace(" ", "")))
152 return True
153
154
155 def _roadmap_row(roadmap: str, phase_id: str):
156 normalized = _normalize_phase_id(phase_id)
157 tokens = [token for token in re.split(r"[\s/]+", normalized.lower()) if token]
158 for match in ROADMAP_ROW_RE.finditer(roadmap):
159 phase_label = _normalize_phase_id(match.group("phase"))
160 if phase_label == normalized:
161 return match
162 phase_lower = phase_label.lower()
163 if tokens and all(token in phase_lower for token in tokens):
164 return match
165 return None
166
167
168 def _roadmap_status(roadmap: str, phase_id: str) -> str | None:
169 row = _roadmap_row(roadmap, phase_id)
170 if row is None:
171 return None
172 return row.group("status").strip().upper()
173
174
175 def _claims_done_or_bv_pass(corpus: str, phase_id: str) -> bool:
176 if _roadmap_status(corpus, phase_id) == "DONE":
177 return True
178 if not BUILD_VERIFICATION_PASS_RE.search(corpus):
179 return False
180 phase_tokens = [token for token in re.split(r"[\s/]+", phase_id.lower()) if token]
181 if not phase_tokens:
182 return False
183 window = 400
184 for match in BUILD_VERIFICATION_PASS_RE.finditer(corpus):
185 start = max(0, match.start() - window)
186 end = min(len(corpus), match.end() + window)
187 snippet = corpus[start:end].lower()
188 if any(token in snippet for token in phase_tokens):
189 return True
190 return False
191
192
193 def _should_run_mode_b(phase_id: str, handover: str | None, roadmap: str | None) -> bool:
194 corpus = (handover or "") + "\n" + (roadmap or "")
195 status = _roadmap_status(roadmap or "", phase_id)
196 if status in {"TODO", "WIP"} and not _claims_done_or_bv_pass(corpus, phase_id):
197 return False
198 return _claims_done_or_bv_pass(corpus, phase_id)
199
200
201 def _frozen_spec_for_phase(roadmap: str | None, phase_id: str) -> str | None:
202 if roadmap is None:
203 return None
204 row = _roadmap_row(roadmap, phase_id)
205 if row is None:
206 return None
207 matches = PHASE_DOC_RE.findall(row.group("deliverable"))
208 if len(matches) != 1:
209 return None
210 return matches[0]
211
212
213 def _ledger_match(
214 config: OverseerConfig,
215 repo_root: Path,
216 phase_id: str,
217 frozen_spec: str | None,
218 ) -> bool:
219 ledger_rel = config.honesty.ledger
220 if ledger_rel is None or not ledger_rel.strip():
221 return False
222 try:
223 ledger_path = confine_path(repo_root, ledger_rel)
224 except Exception:
225 return False
226 if not ledger_path.is_file() or ledger_path.stat().st_size == 0:
227 return False
228 try:
229 entries = read_ledger_entries(ledger_path)
230 except (ValueError, OSError):
231 return False
232 return find_matching_verification_evidence(
233 entries,
234 phase_id=phase_id,
235 frozen_spec=frozen_spec,
236 ) is not None
File History 1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 9 hours ago