#!/usr/bin/env python3 """Strip machine-specific filesystem paths from anything destined for publication. The provenance information in these reports is worth publishing; the author's directory layout is not. This replaces concrete paths with neutral placeholders while leaving checkpoint *names* intact, since those are the actual provenance identifiers and are already public in config.json. python scripts/sanitize_paths.py --root . --check python scripts/sanitize_paths.py --root . --apply """ from __future__ import annotations import argparse import re import sys from pathlib import Path # Ordered: longest / most specific first, so a broad rule cannot eat a narrow one. REPLACEMENTS: list[tuple[re.Pattern[str], str]] = [ # Windows form, both single and JSON-escaped backslashes. ( re.compile(r"E:\\{1,2}New folder \(4\)\\{1,2}wraithfast-9b\\{1,2}wraith-finetune"), "", ), (re.compile(r"E:\\{1,2}New folder \(4\)"), ""), # POSIX / WSL form. (re.compile(r"/mnt/e/New folder \(4\)/wraithfast-9b/wraith-finetune"), ""), (re.compile(r"/mnt/e/New folder \(4\)"), ""), (re.compile(r"/mnt/c/piko9b-weights"), ""), # Home directories and Windows user profiles, whoever they belong to. (re.compile(r"/home/[A-Za-z0-9_.-]+"), ""), (re.compile(r"C:\\{1,2}Users\\{1,2}[A-Za-z0-9_.-]+"), ""), ] # Leftover bare drive/folder references that survive the rules above. RESIDUAL = re.compile(r"New folder \(4\)|/mnt/[a-z]/|C:\\Users|/home/[a-z]") SUFFIXES = {".md", ".json", ".py", ".yaml", ".yml", ".txt", ".cff", ".toml", ".jinja"} SKIP_DIRS = {".git", "__pycache__", ".ruff_cache", ".pytest_cache", ".venv"} def iter_files(root: Path): for path in sorted(root.rglob("*")): if not path.is_file() or path.suffix not in SUFFIXES: continue if any(part in SKIP_DIRS for part in path.parts): continue # Never rewrite this script's own rules. if path.name == "sanitize_paths.py": continue yield path def sanitize(text: str) -> tuple[str, int]: total = 0 for pattern, replacement in REPLACEMENTS: text, count = pattern.subn(replacement, text) total += count return text, total def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--root", type=Path, default=Path(".")) parser.add_argument("--apply", action="store_true", help="Rewrite files in place.") parser.add_argument("--check", action="store_true", help="Exit 1 if anything remains.") args = parser.parse_args() changed: list[tuple[Path, int]] = [] residual: list[str] = [] for path in iter_files(args.root): original = path.read_text(encoding="utf-8", errors="replace") cleaned, count = sanitize(original) if count: changed.append((path.relative_to(args.root), count)) if args.apply: path.write_text(cleaned, encoding="utf-8") final = cleaned if args.apply or count else original for line_number, line in enumerate(final.splitlines(), start=1): if RESIDUAL.search(line): residual.append( f"{path.relative_to(args.root)}:{line_number}: {line.strip()[:110]}" ) verb = "sanitised" if args.apply else "would sanitise" for path, count in changed: print(f" {verb} {path} ({count} occurrence(s))") print(f"\n{len(changed)} file(s) {verb}, {sum(c for _, c in changed)} replacement(s)") if residual: print(f"\n{len(residual)} residual reference(s) needing manual review:") for entry in residual[:40]: print(f" {entry}") if args.check and residual: sys.exit(1) if __name__ == "__main__": main()