From 1997a0d21c7303e1f84ae0344d2754abca29911f Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 26 Jun 2026 23:00:51 -0400 Subject: [PATCH] chore(scripts): add organize_reports.py; date MCP_BUGFIX report organize_reports.py moves loose files in docs/reports/ into week folders named --
(Monday of the file's week). Old weeks only; current week's files stay put. Non-recursive: subdirectories like code_path_audit/ and license_cve_audit/ are skipped. Dry-run by default; --apply to move. MCP_BUGFIX.md had no date in the filename; renamed to MCP_BUGFIX_20260306.md so the organizer's filename-date heuristic picks it up correctly. --- .../{MCP_BUGFIX.md => MCP_BUGFIX_20260306.md} | 0 scripts/organize_reports.py | 158 ++++++++++++++++++ 2 files changed, 158 insertions(+) rename docs/reports/{MCP_BUGFIX.md => MCP_BUGFIX_20260306.md} (100%) create mode 100644 scripts/organize_reports.py diff --git a/docs/reports/MCP_BUGFIX.md b/docs/reports/MCP_BUGFIX_20260306.md similarity index 100% rename from docs/reports/MCP_BUGFIX.md rename to docs/reports/MCP_BUGFIX_20260306.md diff --git a/scripts/organize_reports.py b/scripts/organize_reports.py new file mode 100644 index 00000000..b089766c --- /dev/null +++ b/scripts/organize_reports.py @@ -0,0 +1,158 @@ +"""Organize docs/reports/ files into week folders named --
. + +Scheme: ShareX-style -- where resolves to the day-of-month +(zero-padded) of the week's Monday. Example: files from the week of Mon +June 22, 2026 land in docs/reports/2026-06-22/. + +Only moves files from OLD weeks; the current week's files stay in place +so in-flight work isn't buried. + +Date resolution per file: + 1. Parse an 8-digit YYYYMMDD or YYYY-MM-DD substring from the filename + (most reports embed one, e.g. TRACK_COMPLETION_..._20260627.md). + 2. Fall back to the file's mtime. NOTE: git checkout resets mtime, so + undated files may be mis-classified as 'current week' and left in place. + +Idempotent: subdirectories (existing week folders) are skipped; files +already at their computed destination are skipped. + +Non-recursive: only immediate children of the reports dir are scanned. +Subdirectories (existing week folders, category folders like +code_path_audit/ + license_cve_audit/) are never entered; their contents +are left untouched. Only loose .md (or other) files in the immediate +reports dir are candidates for moving. + +Usage: + python scripts/organize_reports.py # dry-run; print plan + python scripts/organize_reports.py --apply # actually move files + python scripts/organize_reports.py --json # dry-run; JSON output + python scripts/organize_reports.py --dir docs/reports +""" +from __future__ import annotations +import argparse +import json +import re +import shutil +import sys +from datetime import date, timedelta +from pathlib import Path + +_DATE_REGEX = re.compile(r"(\d{4})[-_]?(\d{2})[-_]?(\d{2})") + + +def _week_monday(d: date) -> date: + return d - timedelta(days=d.weekday()) + + +def _parse_filename_date(name: str) -> date | None: + m = _DATE_REGEX.search(name) + if not m: + return None + y, mo, dy = int(m.group(1)), int(m.group(2)), int(m.group(3)) + if not (2000 <= y < 2100 and 1 <= mo <= 12 and 1 <= dy <= 31): + return None + try: + return date(y, mo, dy) + except ValueError: + return None + + +def _file_week_monday(path: Path) -> date | None: + fd = _parse_filename_date(path.name) + if fd is not None: + return _week_monday(fd) + try: + mt = path.stat().st_mtime + except OSError: + return None + return _week_monday(date.fromtimestamp(mt)) + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Organize docs/reports/ files into week folders named --
(Monday of the file's week). Old weeks only; current week's files stay in place." + ) + parser.add_argument("--apply", action="store_true", help="Actually move files (default: dry-run)") + parser.add_argument("--json", action="store_true", help="Emit JSON instead of human-readable output") + parser.add_argument("--dir", default="docs/reports", help="Reports directory (default: docs/reports)") + args = parser.parse_args() + + reports = Path(args.dir) + if not reports.is_dir(): + print(f"error: {reports} is not a directory", file=sys.stderr) + return 1 + + current_monday = _week_monday(date.today()) + moves: list[dict] = [] + skipped_current_week: list[str] = [] + skipped_no_date: list[str] = [] + skipped_already_in_place: list[str] = [] + + for entry in sorted(reports.iterdir(), key=lambda e: e.name): + if entry.is_dir(): + continue + if not entry.is_file(): + continue + fm = _file_week_monday(entry) + if fm is None: + skipped_no_date.append(entry.name) + continue + if fm >= current_monday: + skipped_current_week.append(entry.name) + continue + dest_dir = reports / fm.isoformat() + dest = dest_dir / entry.name + if dest.exists(): + skipped_already_in_place.append(entry.name) + continue + moves.append({"src": str(entry), "dest": str(dest), "week": fm.isoformat()}) + + if args.json: + print(json.dumps({ + "current_week_monday": current_monday.isoformat(), + "reports_dir": str(reports), + "moves": moves, + "skipped_current_week": skipped_current_week, + "skipped_no_date": skipped_no_date, + "skipped_already_in_place": skipped_already_in_place, + }, indent=2)) + return 0 + + print(f"Current week Monday: {current_monday.isoformat()} (files from this week stay put)") + print(f"Reports dir: {reports}") + print() + if not moves: + print("Nothing to move.") + else: + tag = "MOVE" if args.apply else "DRY " + print(f"{tag}: {len(moves)} file(s)") + for mv in moves: + print(f" {tag} {mv['src']} -> {mv['dest']}") + + if skipped_current_week: + print(f"\nSkipped (current week): {len(skipped_current_week)}") + for n in skipped_current_week: + print(f" - {n}") + if skipped_already_in_place: + print(f"\nSkipped (already in destination): {len(skipped_already_in_place)}") + for n in skipped_already_in_place: + print(f" - {n}") + if skipped_no_date: + print(f"\nSkipped (no parseable date): {len(skipped_no_date)}") + for n in skipped_no_date: + print(f" - {n}") + + if args.apply and moves: + for mv in moves: + dest_path = Path(mv["dest"]) + dest_path.parent.mkdir(parents=True, exist_ok=True) + shutil.move(mv["src"], mv["dest"]) + print(f"\nApplied: moved {len(moves)} file(s).") + elif moves: + print("\nDry run. Pass --apply to move.") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file