Files
RedBear-OS/local/scripts/verify-patch-content.py
T
vasilito 687b2650be 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/.
2026-07-12 10:43:15 +03:00

254 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
# verify-patch-content.sh (Python implementation) — Enforce patch-presence
# invariant from local/AGENTS.md § "Fork Supremacy Policy":
# "Each patch in local/patches/<comp>/ MUST be reflected in fork HEAD."
#
# Usage:
# local/scripts/verify-patch-content.sh # Audit all forks
# local/scripts/verify-patch-content.sh --strict # Exit 1 if orphans
# local/scripts/verify-patch-content.sh base kernel # Audit specific forks
#
# Exit codes:
# 0 — All patches preserved in fork HEAD
# 1 — At least one orphaned patch detected (only in --strict mode)
#
# This is the missing check that the Phase 1.0 audit revealed:
# no part of the build system checked that patches listed in
# local/patches/<comp>/ had actually been absorbed into the
# local/sources/<comp>/ fork's working tree before now.
import argparse
import os
import re
import subprocess
import sys
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
COMPONENTS = {
'base': 'local/sources/base',
'bootloader': 'local/sources/bootloader',
'installer': 'local/sources/installer',
'kernel': 'local/sources/kernel',
'libredox': 'local/sources/libredox',
'redoxfs': 'local/sources/redoxfs',
'relibc': 'local/sources/relibc',
'syscall': 'local/sources/syscall',
'userutils': 'local/sources/userutils',
}
def extract_target_file(text):
"""Get the file path that the patch modifies."""
m_plus = re.search(r'^\+\+\+ b/(\S+)', text, re.MULTILINE)
if m_plus and m_plus.group(1) != '/dev/null':
return m_plus.group(1)
m_minus = re.search(r'^--- a/(\S+)', text, re.MULTILINE)
if m_minus and m_minus.group(1) != '/dev/null':
return m_minus.group(1)
return None
def extract_added_lines(text):
"""Get the substantive added lines from a unified diff (ignoring headers)."""
lines = []
for line in text.split('\n'):
if line.startswith('+++') or line.startswith('---') or line.startswith('@@'):
continue
if line.startswith('+') and not line.startswith('+++'):
content = line[1:].strip()
# Skip trivial additions (timestamps, empty)
if content and not re.match(r'^\d{4}-\d{2}-\d{2}', content):
lines.append(content[:60])
return lines
def audit_component(component, fork_root):
"""Audit a single component's patch preservation status."""
patches_dir = os.path.join(REPO_ROOT, 'local/patches', component)
if not os.path.isdir(patches_dir):
return None # component has no patches dir — skip
if not os.path.isdir(fork_root):
return None
total = 0
preserved = 0
orphaned = []
for fn in sorted(os.listdir(patches_dir)):
if not fn.endswith('.patch'):
continue
total += 1
ppath = os.path.join(patches_dir, fn)
with open(ppath, 'r', errors='ignore') as f:
text = f.read()
target = extract_target_file(text)
if not target:
orphaned.append((fn, '?', 'no-target-file'))
continue
target_path = os.path.join(fork_root, target)
if not os.path.exists(target_path):
orphaned.append((fn, target, 'target-missing-in-fork'))
continue
added = extract_added_lines(text)
if not added:
orphaned.append((fn, target, 'patch-has-no-added-lines'))
continue
try:
with open(target_path, 'r', errors='ignore') as f:
target_text = f.read()
except Exception:
orphaned.append((fn, target, 'cannot-read-target'))
continue
matched = sum(1 for line in added[:5] if line in target_text)
if matched >= 1:
preserved += 1
else:
orphaned.append((fn, target, 'no-added-line-matches'))
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', '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()
total_patches = 0
total_preserved = 0
total_orphaned = 0
failed_components = []
orphans_by_comp = {} # comp -> [orphan_tuples]; for operator-decision
detail_lines = []
print('=' * 70)
print(' Red Bear OS — Patch Preservation Audit')
print('=' * 70)
print(f' Source of truth: local/patches/<comp>/*')
print(f' Fork target: local/sources/<comp>')
print(f' Strict mode: {args.strict}')
print('=' * 70)
for comp in args.components:
if comp not in COMPONENTS:
print(f' SKIP: unknown component {comp}', file=sys.stderr)
continue
fork_rel = COMPONENTS[comp]
fork_path = os.path.join(REPO_ROOT, fork_rel)
if not os.path.isdir(fork_path):
if not args.quiet:
print(f' {comp:12s}: SKIP (fork not at {fork_rel})')
continue
result = audit_component(comp, fork_path)
if result is None:
if not args.quiet:
print(f' {comp:12s}: SKIP (no patches dir)')
continue
total, preserved, orphaned = result
total_patches += total
total_preserved += preserved
total_orphaned += len(orphaned)
pct = 100 * preserved / total if total > 0 else 0
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]:
detail_lines.append(f' - {fn}: {target} ({reason})')
if len(orphaned) > 50:
detail_lines.append(f' ... +{len(orphaned)-50} more')
if orphaned:
failed_components.append(comp)
print('=' * 70)
print(f' TOTAL: {total_patches} patches, {total_preserved} preserved, {total_orphaned} orphaned')
print('=' * 70)
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
print(' ACTION REQUIRED: orphaned patches must be applied to fork HEAD')
print(' See: local/docs/PATCH-PRESERVATION-AUDIT-2026-07-12.md')
print(' Helper: re-run local/scripts/apply-patches.sh and re-absorb')
if args.strict:
return 1
return 0
if __name__ == '__main__':
sys.exit(main())