diff --git a/local/scripts/pre-push-checks.sh b/local/scripts/pre-push-checks.sh index a5a091e10f..57bdd951f8 100755 --- a/local/scripts/pre-push-checks.sh +++ b/local/scripts/pre-push-checks.sh @@ -71,6 +71,12 @@ run_check "verify-patch-content" \ "python3 local/scripts/verify-patch-content.py" \ "No orphan patches in local/patches/" +# 3a. verify-patch-content --report action (Phase 6.4: decision-tree +# auto-suggestions for any orphans the operator must triage) +run_check "verify-patch-content-action" \ + "python3 local/scripts/verify-patch-content.py --report action" \ + "Orphan-patch decision-tree (INTEGRATED/SUPERSEDED/etc.)" + # 4. verify-collision-detection run_check "verify-collision-detection" \ "python3 local/scripts/verify-collision-detection.py" \ diff --git a/local/scripts/verify-patch-content.py b/local/scripts/verify-patch-content.py index 0f299f0642..004bdf9ce0 100755 --- a/local/scripts/verify-patch-content.py +++ b/local/scripts/verify-patch-content.py @@ -106,12 +106,54 @@ def audit_component(component, fork_root): return total, preserved, orphaned +def suggest_action_for_orphan(patch_name, target_path, comp): + """For an orphan patch, suggest an action by inspecting the fork + log for related work. Implements the SUPERSEDED / INTEGRATED + decision tree from AGENTS.md § 'Orphan-Patch Supersession Decision + Tree' so the operator doesn't have to do this manually. + + Returns: 'INTEGRATED', 'FILE-RESTRUCTURED', 'NO-TARGET-FILE', or + 'MISSING-UPSTREAM' depending on what the fork log + file + system show. + """ + pname = patch_name.replace('.patch', '') + fork_dir = f'local/sources/{comp}' + if not os.path.isdir(fork_dir): + return 'MISSING-UPSTREAM' + # 1. No target file: patch lacks +++ b/ header (non-actionable) + if not target_path or target_path == '?': + return 'NO-TARGET-FILE' + full_target = os.path.join(fork_dir, target_path) + if not os.path.exists(full_target): + return 'FILE-RESTRUCTURED' + # 2. Fork log: are there commits about this topic? + keywords = re.findall(r'[A-Za-z]+', pname.lower()) + stop = {'a','an','the','in','of','on','to','for','with','and','or','is', + 'p0','p1','p2','p3','p4','p5','p6','p7','p8','p9','p10','p11', + 'patch','the','for'} + keywords = [k for k in keywords if len(k) > 3 and k not in stop][:6] + try: + result = subprocess.run( + ['git', '-C', fork_dir, 'log', '--all', '--oneline', + '-i', '--grep', keywords[0], '-n', '50'], + capture_output=True, text=True, timeout=15 + ) + n_commits = len(result.stdout.strip().splitlines()) if result.stdout.strip() else 0 + if n_commits > 5: + return 'INTEGRATED' + except Exception: + pass + return 'MISSING-UPSTREAM' + + def main(): ap = argparse.ArgumentParser() ap.add_argument('components', nargs='*', default=list(COMPONENTS.keys())) ap.add_argument('--strict', action='store_true', help='Exit non-zero if any orphaned patch detected') - ap.add_argument('--report', choices=['summary', 'detail'], default='summary') + ap.add_argument('--report', choices=['summary', 'detail', 'action'], default='summary', + help='Summary: per-component totals. Detail: per-orphan listing. ' + 'Action: per-orphan SUPERSEDED/INTEGRATED decision suggestions.') ap.add_argument('--quiet', '-q', action='store_true') args = ap.parse_args() @@ -119,6 +161,7 @@ def main(): total_preserved = 0 total_orphaned = 0 failed_components = [] + orphans_by_comp = {} # comp -> [orphan_tuples]; for operator-decision detail_lines = [] print('=' * 70) @@ -152,6 +195,10 @@ def main(): status = 'OK ' if not orphaned else 'WARN' line = f' [{status}] {comp:10s}: preserved={preserved}/{total} ({pct:.0f}%) orphaned={len(orphaned)}' print(line) + if orphaned: + failed_components.append(comp) + # per-comp orphan list for operator-decision support + orphans_by_comp[comp] = orphaned if orphaned and args.report == 'detail': detail_lines.append(f'\n {comp} orphaned patches:') for fn, target, reason in orphaned[:50]: @@ -168,6 +215,28 @@ def main(): if detail_lines: print('\n'.join(detail_lines)) + # Operator-decision support: for each orphan, suggest an action + # (INTEGRATED / FILE-RESTRUCTURED / NO-TARGET-FILE / MISSING-UPSTREAM) + # Implements AGENTS.md § 'Orphan-Patch Supersession Decision Tree' + if args.report == 'action' and total_orphaned > 0: + print() + print('=' * 70) + print(' Operator-decision suggestions (per AGENTS.md decision tree)') + print('=' * 70) + for comp, comp_orphans in orphans_by_comp.items(): + for orphan_info in comp_orphans: + fn, target, _reason = orphan_info + action = suggest_action_for_orphan(fn, target, comp) + print(f' {comp}/{fn} -> {action}') + if action == 'INTEGRATED': + print(' (work IS in fork under different commit; safe to archive as superseded)') + elif action == 'FILE-RESTRUCTURED': + print(' (target file no longer in fork; safe to archive as superseded)') + elif action == 'NO-TARGET-FILE': + print(' (patch lacks +++ b/ header; non-actionable; safe to archive)') + else: # MISSING-UPSTREAM + print(' (work NOT in fork; needs reapply or fork rebase)') + if total_orphaned == 0: print(' ALL FORKS: patches preserved in fork HEAD') return 0 diff --git a/local/sources/base b/local/sources/base index 71f74b2bd8..ab0da30613 160000 --- a/local/sources/base +++ b/local/sources/base @@ -1 +1 @@ -Subproject commit 71f74b2bd85f31b379f4e4b4062d23856ed025a1 +Subproject commit ab0da30613f23214de34222d23134310db9e60f9 diff --git a/local/sources/relibc b/local/sources/relibc index d157c2274e..b28734893b 160000 --- a/local/sources/relibc +++ b/local/sources/relibc @@ -1 +1 @@ -Subproject commit d157c2274e7e2fe42231a4b92c69f2eec3fb9de2 +Subproject commit b28734893b0c15d1c7b6ea849f67c7cb94f5fade