# -*- coding: utf-8 -*-
"""
tidy.py — file placement and debris enforcement.
OWNER: 01_System/tidy.py
REVIEW: whenever 01_System/portable/FILE_PLACEMENT_MAP.md changes.
GOV-F3.9 create the folder, never work around it -> --ensure
GOV-F3.10 no debris; sweep to 06_Archive/_debris/<date>/ with a hash manifest, never delete -> --sweep
GOV-F3.11 no links to live files outside the project
GOV-F3.12 a mechanical check fails on any of the above, and has been proven able to fail
Usage:
python 01_System/tidy.py --report list every violation, exit 1 if any
python 01_System/tidy.py --ensure create missing mandated folders
python 01_System/tidy.py --sweep archive debris with a manifest (nothing deleted)
"""
import os, re, sys, json, hashlib, datetime, shutil
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
MANDATED_FOLDERS = [
"00_Handover", "01_System", "01_System/checkers", "01_System/portable",
"01_System/portable/templates/business_plan",
"02_Work", "02_Work/_scratch", "02_Work/diagrams", "02_Work/scratch",
"03_Registers", "04_Inputs", "05_Outputs",
"06_Archive", "06_Archive/_versions", "06_Archive/_superseded", "06_Archive/_debris",
"07_Improvements", "08_Reusable_Knowledge", "09_Help_Hub",
]
ROOT_ALLOWED = {"README.md", "Dashboard.html"}
# GOV-F3.8: "A file that fits no row is a question for Zaid, never a new folder of your own
# invention." These top-level folders exist on Zaid's disk inside this project's root and were not
# created by this project. They are NOT quietly legitimised — each one is named here with the
# question it raises, the question is carried as an open action, and the tidy report prints it
# every run so it cannot fade into the background. Nothing here is moved: one of them is a folder
# separately connected to this session and moving it would break something outside this project.
FOREIGN_TOP = {
"00_Governance framework":
"Holds the v3.9 Standard (already filed in 05_Outputs) and WEBAPP_REFERENCE, which belongs "
"to the AI Project Governance project and is separately connected to this session. "
"ACT-014 asks Zaid where it should live. Not moved.",
"Project":
"Empty directory left by an earlier session. Not removed: this mount forbids deletion.",
"Projects":
"Empty directory left by an earlier session. Not removed: this mount forbids deletion.",
}
DEBRIS_PATTERNS = [
(r"__pycache__$", "python bytecode cache"),
(r"\.pyc$", "python bytecode"),
(r"\.(tmp|bak|orig)$", "temporary or backup copy"),
(r"~$", "editor backup"),
(r"\.(lock|lck)$", "lock file"),
(r"\(\d+\)\.[A-Za-z0-9]+$", "cloud-sync duplicate copy"),
(r"-conflict", "cloud-sync conflict copy"),
(r"(?i)\bcopy\b.*\.[A-Za-z0-9]+$", "duplicate copy"),
]
SECRET_PATTERNS = [
(r"^\.env$", ".env secret store"),
(r"\.(key|pem|p12|pfx)$", "key material"),
(r"^id_rsa", "private key"),
]
LINK_PATTERNS = [(r"\.(lnk|url|webloc)$", "shortcut to a file outside the project")]
# Folders the map knows about, as top-level names
KNOWN_TOP = {f.split("/")[0] for f in MANDATED_FOLDERS}
def walk(skip_archive=True):
for dp, dns, fns in os.walk(ROOT):
rel = os.path.relpath(dp, ROOT)
if rel == ".":
rel = ""
if skip_archive and rel.startswith("06_Archive"):
dns[:] = []
continue
yield rel, dns, fns
def report():
findings = []
foreign = []
# 1. mandated folders present
for f in MANDATED_FOLDERS:
if not os.path.isdir(os.path.join(ROOT, f)):
findings.append(("MISSING FOLDER", f, "mandated by the placement map; run --ensure"))
# 2. only README.md and Dashboard.html at the root
for name in sorted(os.listdir(ROOT)):
p = os.path.join(ROOT, name)
if os.path.isfile(p) and name not in ROOT_ALLOWED and not name.startswith("."):
findings.append(("LOOSE ROOT FILE", name, "only README.md and Dashboard.html may sit at the root"))
if os.path.isdir(p) and name not in KNOWN_TOP and not name.startswith("."):
if name in FOREIGN_TOP:
foreign.append((name, FOREIGN_TOP[name]))
else:
findings.append(("UNKNOWN FOLDER", name,
"the placement map does not know this folder; it is a question for "
"Zaid (GOV-F3.8), not a new rule"))
# 3. debris, secrets and links anywhere outside the archive
for rel, dns, fns in walk():
for d in list(dns):
for pat, why in DEBRIS_PATTERNS:
if re.search(pat, d):
findings.append(("DEBRIS", os.path.join(rel, d), why))
for fn in fns:
path = os.path.join(rel, fn) if rel else fn
for pat, why in DEBRIS_PATTERNS:
if re.search(pat, fn):
findings.append(("DEBRIS", path, why)); break
for pat, why in SECRET_PATTERNS:
if re.search(pat, fn):
findings.append(("SECRET STORE", path, why + " — must not exist in the project (INV-5)"))
for pat, why in LINK_PATTERNS:
if re.search(pat, fn):
findings.append(("LINK", path, why))
full = os.path.join(ROOT, path)
if os.path.islink(full):
findings.append(("SYMLINK", path, "symbolic link to a file outside the project (GOV-F3.11)"))
report.foreign = foreign
# 4. a live-named instruction file inside the archive would be read as current
arch = os.path.join(ROOT, "06_Archive")
if os.path.isdir(arch):
for dp, dns, fns in os.walk(arch):
for fn in fns:
if re.match(r"(?i)^(CLAUDE|AGENTS?|INSTRUCTIONS?|GEMINI)\.md$", fn):
findings.append(("ARCHIVED INSTRUCTION FILE",
os.path.relpath(os.path.join(dp, fn), ROOT),
"tools read nested instruction files as current; rename to *.archived"))
return findings
def ensure():
made = []
for f in MANDATED_FOLDERS:
p = os.path.join(ROOT, f)
if not os.path.isdir(p):
os.makedirs(p, exist_ok=True)
made.append(f)
return made
def sweep():
"""Archive debris with a hash manifest. Nothing is deleted (GOV-D1.7, GOV-F3.10)."""
findings = [f for f in report() if f[0] in ("DEBRIS", "LINK", "SYMLINK")]
if not findings:
return None, []
stamp = datetime.date.today().isoformat()
dest = os.path.join(ROOT, "06_Archive", "_debris", stamp)
os.makedirs(dest, exist_ok=True)
manifest, moved = [], []
for kind, path, why in findings:
src = os.path.join(ROOT, path)
if not os.path.exists(src):
continue
flat = path.replace(os.sep, "__")
dst = os.path.join(dest, flat)
if os.path.isdir(src):
# Zip-then-delete fails on a mount that forbids unlink — which is exactly the mount
# this project lives on when it is on Zaid's computer, so the sweep failed there and
# the debris check stayed red through every run. Moving the directory is a rename, and
# a rename is permitted where a delete is not. The archive is the moved directory
# itself, which is a better record than a zip of it anyway.
try:
shutil.make_archive(dst, "zip", src)
shutil.rmtree(src)
dst = dst + ".zip"
except (PermissionError, OSError):
if os.path.exists(dst):
dst = dst + "_" + datetime.datetime.now().strftime("%H%M%S")
shutil.move(src, dst)
else:
shutil.move(src, dst)
if os.path.isdir(dst):
h = hashlib.sha256()
for dp2, _dn2, fn2 in os.walk(dst):
for f2 in sorted(fn2):
h.update(open(os.path.join(dp2, f2), "rb").read())
digest = h.hexdigest()
else:
digest = hashlib.sha256(open(dst, "rb").read()).hexdigest()
manifest.append({"original_path": path, "archived_as": os.path.basename(dst),
"reason": why, "sha256": digest})
moved.append(path)
with open(os.path.join(dest, "MANIFEST.json"), "w", encoding="utf-8") as f:
json.dump({"swept": stamp, "root": "06_Archive/_debris/" + stamp,
"note": "Nothing was deleted. Every entry carries the SHA-256 of the archived copy "
"so a rollback can be verified (GOV-F3.10).",
"files": manifest}, f, indent=2)
return dest, moved
def main():
args = set(sys.argv[1:]) or {"--report"}
if "--ensure" in args:
made = ensure()
print("created %d missing folder(s)%s" % (len(made), (": " + ", ".join(made)) if made else ""))
if "--sweep" in args:
dest, moved = sweep()
if dest:
print("swept %d item(s) to %s with a hash manifest" % (len(moved), os.path.relpath(dest, ROOT)))
else:
print("nothing to sweep")
findings = report()
if "--report" in args or not (args - {"--report"}):
if not findings:
print("TIDY PASS — placement map satisfied: no loose root files, no debris, no secret stores, "
"no links to live files, no unknown folders, no missing mandated folders.")
else:
print("TIDY FAIL — %d finding(s):" % len(findings))
for kind, path, why in findings:
print(" %-24s %-52s %s" % (kind, path[:52], why))
for name, why in getattr(report, "foreign", []):
print(" NOTE foreign top-level folder %-26s %s" % (name, why))
sys.exit(1 if findings else 0)
if __name__ == "__main__":
main()