mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:07:18 +01:00
Some checks failed
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
design_system tests / test (push) Has been cancelled
gatelock pre-commit / pre-commit (push) Has been cancelled
gatelock tests / test (3.10) (push) Has been cancelled
gatelock tests / test (3.11) (push) Has been cancelled
gatelock tests / test (3.12) (push) Has been cancelled
web_ui tests / test (push) Has been cancelled
Markdown across every repo had accumulated a dozen ad-hoc names (refactor_claude_todo.md x23, DESIGN_AUDIT_TODO.md, NEXT_SESSION, HANDOFF, PLAN, SPEC) with nothing distinguishing a live task from a finished record. An audit found docs asserting work was pending that shipped months earlier, and one asserting completion while the work was open. Four namespaces now: README.md, CLAUDE*.md, DOCS*.md, TODO*.md. Task files carry the literal "REMOVE ME AFTER FINISH" and are deleted when the work lands. Enforced bidirectionally -- a TODO without the marker fails, AND any file carrying the marker that is not named TODO*.md fails. Without the reverse rule a task can be renamed into the permanent namespace and quietly live forever, which is the failure being fixed. md_naming/ mirrors the file_length topology and imports its tables (EXCLUDED_DIRS, THIRD_PARTY_REPOS, VENDORED_ANYWHERE, GENERATED_PATTERN) rather than restating them, so the two gates cannot disagree about the same file. 71 tests, 100% branch coverage. stale.py warns on TODOs past 90 days but never fails: no machine can prove a task is finished, so the honest mechanism is a nudge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
86 lines
2.9 KiB
Python
86 lines
2.9 KiB
Python
"""CLI: dry-run or apply the markdown-naming migration.
|
|
|
|
python3 -m md_naming.migrate.run # show what would happen
|
|
python3 -m md_naming.migrate.run --apply # do it
|
|
|
|
Triaged classifications that are not in classify.py are loaded from a JSON
|
|
sidecar, so a re-triage does not require editing code.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
if __package__ in (None, ""): # pragma: no cover - plain-script import path
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
|
|
from md_naming.migrate import apply as apply_mod
|
|
from md_naming.migrate import classify
|
|
from md_naming.migrate.plan import HOME, add_auto, build
|
|
from md_naming.migrate.survey import gate_violations
|
|
|
|
TRIAGE = Path(__file__).parent / "triage.json"
|
|
|
|
|
|
def load_triage() -> tuple[dict[str, str], dict[str, str]]:
|
|
"""Extra task/record classifications from the JSON sidecar."""
|
|
if not TRIAGE.is_file():
|
|
return {}, {}
|
|
data = json.loads(TRIAGE.read_text(encoding="utf-8"))
|
|
return data.get("tasks", {}), data.get("records", {})
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Migrate markdown filenames.")
|
|
parser.add_argument(
|
|
"--apply", action="store_true", help="perform the moves (default: dry run)"
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
extra_tasks, extra_records = load_triage()
|
|
plan = build(extra_tasks, extra_records)
|
|
|
|
# Anything the gate still flags that no table named: rename it
|
|
# mechanically. Computed live from the gate rather than a saved list, so
|
|
# the migration cannot drift from what the gate actually rejects.
|
|
classified = {str(move.old) for move in plan.moves} | {
|
|
str(HOME / rel) for rel in (*classify.PROVEN_DONE, *classify.CLAIMED_DONE)
|
|
}
|
|
remaining = [
|
|
str(path.relative_to(HOME))
|
|
for path in gate_violations()
|
|
if str(path) not in classified
|
|
]
|
|
add_auto(plan, remaining)
|
|
|
|
if plan.unresolved:
|
|
print(f"WARNING: {len(plan.unresolved)} classified path(s) missing:")
|
|
for path in plan.unresolved:
|
|
print(f" {path}")
|
|
|
|
# References first: a half-applied run must never leave the harness
|
|
# config pointing at a file that has already moved.
|
|
for line in apply_mod.repair_references(plan, apply=args.apply):
|
|
print(line)
|
|
|
|
for move in plan.moves:
|
|
print(apply_mod.git_mv(move, apply=args.apply))
|
|
target = move.new if args.apply and move.new.is_file() else move.old
|
|
if move.needs_marker and target.is_file():
|
|
note = apply_mod.ensure_marker(target, apply=args.apply)
|
|
if note:
|
|
print(f" {note}")
|
|
|
|
tasks = sum(1 for m in plan.moves if m.kind == "task")
|
|
records = len(plan.moves) - tasks
|
|
mode = "APPLIED" if args.apply else "DRY RUN"
|
|
print(f"\n{mode}: {len(plan.moves)} moves ({tasks} TODO, {records} DOCS)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|