drift.py
python
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
1 day ago
| 1 | """Drift detection D1–D3 (§3).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from tools.governance_hygiene.parse import normalize_status, parse_handover_github_main_sha, parse_queue_rows, pr_matches_row |
| 6 | from tools.governance_hygiene.types import DriftReport, DriftState, MergedPullRequest, VerifiedReads |
| 7 | |
| 8 | |
| 9 | def detect_drift( |
| 10 | reads: VerifiedReads, |
| 11 | handover_text: str, |
| 12 | roadmap_text: str, |
| 13 | ) -> DriftReport: |
| 14 | """Compare verified reads to doc claims; never infer VCS state from docs alone.""" |
| 15 | d1 = _detect_d1(reads, handover_text) |
| 16 | d2 = _detect_d2(reads) |
| 17 | d3 = _detect_d3(reads, roadmap_text) |
| 18 | details: dict[str, str] = {} |
| 19 | if d1 == "drifted": |
| 20 | claimed = parse_handover_github_main_sha(handover_text) |
| 21 | details["d1"] = f"claimed={claimed} actual={reads.r1_github_main_sha}" |
| 22 | if d2 == "drifted": |
| 23 | details["d2"] = f"anchor={reads.r2_anchor_sha} canonical={reads.r3_canonical_main_sha}" |
| 24 | if d3 == "drifted": |
| 25 | details["d3"] = "queue status mismatch vs merged PRs" |
| 26 | return DriftReport( |
| 27 | d1_handover_vs_git=d1, |
| 28 | d2_anchor_vs_canonical=d2, |
| 29 | d3_queue_vs_merged=d3, |
| 30 | details=details, |
| 31 | ) |
| 32 | |
| 33 | |
| 34 | def _detect_d1(reads: VerifiedReads, handover_text: str) -> DriftState: |
| 35 | if reads.regime == "muse-only": |
| 36 | return "aligned" |
| 37 | if reads.r1_github_main_sha is None: |
| 38 | return "unreadable" |
| 39 | claimed = parse_handover_github_main_sha(handover_text) |
| 40 | if claimed is None: |
| 41 | return "drifted" |
| 42 | return "aligned" if claimed == reads.r1_github_main_sha.lower() else "drifted" |
| 43 | |
| 44 | |
| 45 | def _detect_d2(reads: VerifiedReads) -> DriftState: |
| 46 | if reads.regime == "git-only": |
| 47 | if reads.r3_canonical_main_sha is None: |
| 48 | reads_r3 = reads.r1_github_main_sha |
| 49 | else: |
| 50 | reads_r3 = reads.r3_canonical_main_sha |
| 51 | if reads_r3 is None: |
| 52 | return "unreadable" |
| 53 | return "aligned" if reads.r2_anchor_sha.lower() == reads_r3.lower() else "drifted" |
| 54 | |
| 55 | if reads.r3_canonical_main_sha is None: |
| 56 | return "unreadable" |
| 57 | return ( |
| 58 | "aligned" |
| 59 | if reads.r2_anchor_sha.lower() == reads.r3_canonical_main_sha.lower() |
| 60 | else "drifted" |
| 61 | ) |
| 62 | |
| 63 | |
| 64 | def _detect_d3(reads: VerifiedReads, roadmap_text: str) -> DriftState: |
| 65 | if reads.regime == "muse-only": |
| 66 | return "aligned" |
| 67 | |
| 68 | rows = parse_queue_rows(roadmap_text) |
| 69 | merged = list(reads.r4_merged_prs) |
| 70 | |
| 71 | for pr in merged: |
| 72 | matching = [row for row in rows if pr_matches_row(pr.title, row)] |
| 73 | if not matching: |
| 74 | continue |
| 75 | for row in matching: |
| 76 | status = normalize_status(row.status) |
| 77 | if status not in {"DONE", "MERGED"}: |
| 78 | return "drifted" |
| 79 | |
| 80 | for row in rows: |
| 81 | status = normalize_status(row.status) |
| 82 | if status != "MERGED": |
| 83 | continue |
| 84 | if not any(pr_matches_row(pr.title, row) for pr in merged): |
| 85 | return "drifted" |
| 86 | |
| 87 | return "aligned" |
| 88 | |
| 89 | |
| 90 | def merged_prs_missing_from_done(handover_text: str, merged: tuple[MergedPullRequest, ...]) -> list[MergedPullRequest]: |
| 91 | """Return merged PRs not yet mentioned in the handover done-recently block.""" |
| 92 | missing: list[MergedPullRequest] = [] |
| 93 | for pr in merged: |
| 94 | if f"PR #{pr.number}" in handover_text or pr.merge_commit_sha[:7] in handover_text: |
| 95 | continue |
| 96 | if pr.title in handover_text: |
| 97 | continue |
| 98 | missing.append(pr) |
| 99 | return missing |
File History
2 commits
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
1 day ago
sha256:4671b7f787ddbe63ced31c895b688c77ab495653b65a730b423329f26b3c1439
feat: K1-P1 complete — agent provenance, build-verification…
Sonnet 4.6
patch
53 days ago