106 lines
3.4 KiB
Python
106 lines
3.4 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Add a terminology grandfather note to archived workplan files (CUST-WP-0055 T07).
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
python tools/add_archive_terminology_note.py --repo the-custodian
|
||
|
|
python tools/add_archive_terminology_note.py --all-repos
|
||
|
|
python tools/add_archive_terminology_note.py --repo the-custodian --dry-run
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import re
|
||
|
|
import sys
|
||
|
|
import urllib.request
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
HOME = Path("/home/worsch")
|
||
|
|
DEFAULT_API_BASE = "http://127.0.0.1:8000"
|
||
|
|
ARCHIVE_DIR = Path("workplans/archived")
|
||
|
|
NOTE_MARKER = "Terminology note:"
|
||
|
|
NOTE_BODY = (
|
||
|
|
"> **Terminology note:** Historical text in this archived workplan may use "
|
||
|
|
'the legacy term "workstream". The fleet term is **workplan** '
|
||
|
|
"(`canon/standards/workplan-terminology-fleet_v0.1.md`).\n"
|
||
|
|
)
|
||
|
|
FRONTMATTER_RE = re.compile(r"^---\n.*?\n---\n", re.DOTALL)
|
||
|
|
|
||
|
|
|
||
|
|
def list_repos(api_base: str) -> list[str]:
|
||
|
|
try:
|
||
|
|
with urllib.request.urlopen(f"{api_base.rstrip('/')}/repos/", timeout=10) as resp:
|
||
|
|
import json
|
||
|
|
|
||
|
|
rows = json.loads(resp.read().decode())
|
||
|
|
return sorted({r["slug"] for r in rows if r.get("slug")})
|
||
|
|
except Exception:
|
||
|
|
return []
|
||
|
|
|
||
|
|
|
||
|
|
def archived_files(repo_root: Path) -> list[Path]:
|
||
|
|
archive = repo_root / ARCHIVE_DIR
|
||
|
|
if not archive.is_dir():
|
||
|
|
return []
|
||
|
|
return sorted(p for p in archive.rglob("*.md") if p.is_file())
|
||
|
|
|
||
|
|
|
||
|
|
def add_note(text: str) -> tuple[str, bool]:
|
||
|
|
if NOTE_MARKER in text:
|
||
|
|
return text, False
|
||
|
|
match = FRONTMATTER_RE.match(text)
|
||
|
|
if not match:
|
||
|
|
return text, False
|
||
|
|
insert_at = match.end()
|
||
|
|
return text[:insert_at] + "\n" + NOTE_BODY + text[insert_at:], True
|
||
|
|
|
||
|
|
|
||
|
|
def process_repo(repo_root: Path, *, dry_run: bool) -> tuple[int, int]:
|
||
|
|
changed = 0
|
||
|
|
scanned = 0
|
||
|
|
for path in archived_files(repo_root):
|
||
|
|
scanned += 1
|
||
|
|
original = path.read_text(encoding="utf-8")
|
||
|
|
updated, did_change = add_note(original)
|
||
|
|
if not did_change:
|
||
|
|
continue
|
||
|
|
changed += 1
|
||
|
|
if dry_run:
|
||
|
|
print(f"would update: {path}")
|
||
|
|
else:
|
||
|
|
path.write_text(updated, encoding="utf-8")
|
||
|
|
print(f"updated: {path}")
|
||
|
|
return scanned, changed
|
||
|
|
|
||
|
|
|
||
|
|
def main(argv: list[str] | None = None) -> int:
|
||
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
||
|
|
parser.add_argument("--repo", action="append", default=[], help="repo slug under ~/")
|
||
|
|
parser.add_argument("--all-repos", action="store_true", help="every registered repo")
|
||
|
|
parser.add_argument("--api-base", default=DEFAULT_API_BASE)
|
||
|
|
parser.add_argument("--dry-run", action="store_true")
|
||
|
|
args = parser.parse_args(argv)
|
||
|
|
|
||
|
|
repos = args.repo or (list_repos(args.api_base) if args.all_repos else [])
|
||
|
|
if not repos:
|
||
|
|
print("No repos selected.", file=sys.stderr)
|
||
|
|
return 1
|
||
|
|
|
||
|
|
total_scanned = 0
|
||
|
|
total_changed = 0
|
||
|
|
for slug in repos:
|
||
|
|
root = HOME / slug
|
||
|
|
if not root.is_dir():
|
||
|
|
print(f"skip missing checkout: {slug}")
|
||
|
|
continue
|
||
|
|
scanned, changed = process_repo(root, dry_run=args.dry_run)
|
||
|
|
total_scanned += scanned
|
||
|
|
total_changed += changed
|
||
|
|
if scanned:
|
||
|
|
print(f"{slug}: {changed}/{scanned} archived files updated")
|
||
|
|
|
||
|
|
print(f"done: {total_changed} files updated ({total_scanned} scanned)")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(main())
|