phase 6.4: operator-decision automation in verify-patch-content
Adds --report action mode to verify-patch-content.py that implements
the AGENTS.md § 'Orphan-Patch Supersession Decision Tree' algorithmically.
For each orphan, the action recommender outputs one of:
- INTEGRATED: fork log has >5 commits matching topic keywords;
the work IS in the fork under a different commit
subject. Safe to archive as superseded.
- FILE-RESTRUCTURED: target file no longer exists in fork (file
rebased past patch's expectations). Safe to archive.
- NO-TARGET-FILE: patch lacks +++ b/ header (non-actionable).
Safe to archive.
- MISSING-UPSTREAM: work NOT in fork; needs reapply or fork rebase.
Algorithm (in suggest_action_for_orphan):
1. Check if patch has no target file (lacks +++ b/) → NO-TARGET-FILE
2. Check if target file no longer exists in fork → FILE-RESTRUCTURED
3. Run 'git log -i --grep <topic-keyword>' on the fork; if >5 commits
match → INTEGRATED
4. Otherwise → MISSING-UPSTREAM
Validated with synthetic test: created a local/patches/relibc/
_test-synthetic-orphan.patch with target=lib/test/strange.rs (which
doesn't exist in fork). The recommender correctly classified it as
FILE-RESTRUCTURED.
Wired into pre-push-checks.sh as check #3a (verify-patch-content-action).
Future new orphans are auto-classified; the operator only needs to
verify the suggested classification matches reality and then archive
to legacy-superseded-2026-07-12/.
This commit is contained in:
@@ -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" \
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
Submodule local/sources/base updated: 71f74b2bd8...ab0da30613
+1
-1
Submodule local/sources/relibc updated: d157c2274e...b28734893b
Reference in New Issue
Block a user