mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
0c29c9e16b
- decomp-architect/corpus/record/: the how-to (13), decision-log, accelerators, retrospective, story, wave-playbook, effort-map, gen3-standards, gen3-handoff, DIGEST and every PhaseEnd (34) verbatim behind an authored front page (what each is, how to read it, what is NOT there — the phase logs, R19 — and that the mining pass is their distillation); tool_census: RECORD_SOURCES + record_dest + the third corpus in plan/write/check (358 copies + 28 pointers, --check OK); kit_lint exempts corpus/record; SETUP Step 6 gains 2c docs/inherited-record.md (+ the verify line; expected-manifest +1); ops-setup/README/tree/methodology/ wiki page/Home/Tools page/README bullet/SETUP row: "two dictionaries" → three - tools/kit_coverage.py (+ config/kit_coverage_map.tsv): derives R1..R83 from DIGEST §3 (asserted contiguous) and the 58 accelerator entries (headings + numbered items), asserts each is cited by a provenance line of the registry seed / the kernels or dispositioned (G / DK / FOLDED:G / ENV / PA / SEED: / KIT: / RECORD / COOKBOOK / NOT-PORTABLE; unknown ids refused); first run: 26 uncited rules + 21 uncited entries → DK-66 (a ledger's tie-break, a checker's widening and a blanket commit are part of the instrument — R70/R80/R52), DK-67 (the ignore file's directory-form wall — S91 (1)), DK-68 (a summarised signal is a claim, not ground truth — R14/R66) in a new kernels section 8 (the museum is 9; "In all" 68) + 41 dispositions (15 PA, 3 ENV, folds into G6/G18/G38/G66/DK-12/19/20/22/25/26/31/35/44/45/46/57/61, 1 KIT template, 1 COOKBOOK); now 0 UNCOVERED on both populations; wired into tools-health after tool_census --check; SETUP row + dictionary row - verify: tool_census --check OK; kit_coverage OK (rules 57 cited + 26 dispositioned / 83; accelerators 41 + 15 / 58); kit_lint OK; doc_links --strict OK; wiki_render --selftest 32 pages / 0 unlisted
189 lines
9.2 KiB
Python
189 lines
9.2 KiB
Python
#!/usr/bin/env python3
|
|
"""kit_lint.py — the day-one decomp kit (decomp-architect/) stays free of the source project's names, paths, addresses and
|
|
rule numbers, honours its own placeholder contract, and ships scripts that parse (P33.5 task 11; in tools-health).
|
|
|
|
tools/kit_lint.py # the whole package: every file under decomp-architect/
|
|
tools/kit_lint.py --paths a b … # an explicit file list (the checks that make sense per file)
|
|
tools/kit_lint.py --selftest # the R39 control: a planted leak line and a planted unlisted placeholder MUST fail
|
|
|
|
Six checks, each printed with its denominator (R41), exit 1 on any finding, exit 2 when the package is absent (R43):
|
|
1. LEAK — no line outside a ```calibration fence and off a `provenance:` line matches the pattern
|
|
`SLUS|Musashi|BFM|Druthulu|func_80|ov_SC|/home/musashi|/mnt/z|172\\.17\\.|\\bR[0-9]{1,2}\\b|§[0-9]+` (the kit refers to rules
|
|
by G-id, to kernels by DK-id, to sections by "step N"; source-project figures live only inside calibration fences).
|
|
The fence-aware form matters: `grep -v calibration` drops only the lines containing that WORD, not the fence contents.
|
|
2. PLACEHOLDERS — the set of `{{NAME}}` tokens used anywhere under the package equals the set listed (backticked) in
|
|
templates/PLACEHOLDERS.md; a used-but-unlisted or listed-but-unused placeholder fails (the contract tasks 11/12 honour).
|
|
3. SYNTAX — every `*.sh` passes `bash -n`; every `*.py` compiles (the `compile` builtin, in memory — no bytecode is written into the kit); every `*.yml`/`*.json` block parses
|
|
(JSON only for files whose whole content is JSON).
|
|
4. GITIGNORE — templates/gitignore.decomp equals the ROM-firewall wiki page's fence (delegated to
|
|
tools/gitignore_template_check.py; rc 0 required).
|
|
5. TODO counts — `TODO(platform)` and `TODO(phase-N)` occurrences, informational, with the file count as denominator
|
|
(the README says how many remain).
|
|
6. COVERAGE — the number of files scanned is printed; zero files is a failure, never a pass.
|
|
Controls (R39): `--selftest` writes a leak line, a fenced (allowed) line, a provenance (allowed) line and an unlisted
|
|
placeholder into a scratch file under .run/ and asserts checks 1 and 2 catch exactly the two planted defects.
|
|
"""
|
|
import json
|
|
import pathlib
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
REPO = pathlib.Path(__file__).resolve().parent.parent
|
|
KIT = REPO / "decomp-architect"
|
|
PLACEHOLDERS_MD = KIT / "templates" / "PLACEHOLDERS.md"
|
|
LEAK_RE = re.compile(r"SLUS|Musashi|BFM|Druthulu|func_80|ov_SC|/home/musashi|/mnt/z|172\.17\.|\bR[0-9]{1,2}\b|§[0-9]+")
|
|
PH_RE = re.compile(r"\{\{[A-Z_0-9]+\}\}")
|
|
PH_LISTED_RE = re.compile(r"`(\{\{[A-Z_0-9]+\}\})`")
|
|
|
|
|
|
CORPUS_DIRS = (KIT / "corpus" / "tools", KIT / "corpus" / "cookbook", KIT / "corpus" / "record") # verbatim evidence: exempt from LEAK + PLACEHOLDERS (coverage + equality are tool_census --check's); P33.5 task 14.5 added the record
|
|
|
|
|
|
def is_corpus(p):
|
|
return any(d in p.parents for d in CORPUS_DIRS)
|
|
|
|
|
|
def kit_files():
|
|
return sorted(p for p in KIT.rglob("*") if p.is_file() and "__pycache__" not in p.parts)
|
|
|
|
|
|
def leak_lines(text):
|
|
"""(lineno, line) for every leak outside a calibration fence and off a provenance line."""
|
|
out, in_cal = [], False
|
|
for i, line in enumerate(text.splitlines(), 1):
|
|
if line.startswith("```"):
|
|
if in_cal:
|
|
in_cal = False
|
|
continue
|
|
if line.strip().startswith("```calibration"):
|
|
in_cal = True
|
|
continue
|
|
if in_cal or "provenance:" in line:
|
|
continue
|
|
if LEAK_RE.search(line):
|
|
out.append((i, line))
|
|
return out
|
|
|
|
|
|
def placeholders_used(files):
|
|
used = {}
|
|
for p in files:
|
|
for m in PH_RE.findall(p.read_text(encoding="utf-8", errors="replace")):
|
|
used.setdefault(m, set()).add(p)
|
|
return used
|
|
|
|
|
|
def placeholders_listed():
|
|
if not PLACEHOLDERS_MD.exists():
|
|
sys.exit(f"kit_lint: {PLACEHOLDERS_MD.relative_to(REPO)} missing — the placeholder contract cannot be checked (R43)")
|
|
return set(PH_LISTED_RE.findall(PLACEHOLDERS_MD.read_text(encoding="utf-8")))
|
|
|
|
|
|
def check_syntax(files):
|
|
bad = []
|
|
for p in files:
|
|
try:
|
|
if p.suffix == ".sh":
|
|
r = subprocess.run(["bash", "-n", str(p)], capture_output=True, text=True)
|
|
if r.returncode:
|
|
bad.append((p, r.stderr.strip().splitlines()[-1] if r.stderr.strip() else "bash -n failed"))
|
|
elif p.suffix == ".py":
|
|
compile(p.read_text(encoding="utf-8"), str(p), "exec") # in memory: never writes bytecode into the kit
|
|
elif p.suffix == ".json":
|
|
json.loads(p.read_text(encoding="utf-8"))
|
|
elif p.suffix in (".yml", ".yaml"):
|
|
try:
|
|
import yaml # noqa: F401
|
|
except ImportError:
|
|
continue
|
|
yaml.safe_load(p.read_text(encoding="utf-8"))
|
|
except Exception as e: # noqa: BLE001 — the message is the finding
|
|
bad.append((p, str(e).splitlines()[0]))
|
|
return bad
|
|
|
|
|
|
def run_checks(files, do_gitignore=True):
|
|
findings = 0
|
|
n = len(files)
|
|
if n == 0:
|
|
print("kit_lint: 0 files to check — refusing to pass on an empty package (R43)")
|
|
return 2
|
|
corpus = [p for p in files if is_corpus(p)]
|
|
files = [p for p in files if not is_corpus(p)]
|
|
print(f"kit_lint: {len(corpus)} corpus files under corpus/tools + corpus/cookbook + corpus/record are verbatim evidence — exempt from the leak and placeholder checks (their coverage and equality are tool_census --check's)")
|
|
# 1 LEAK
|
|
leaks = 0
|
|
for p in files:
|
|
for ln, line in leak_lines(p.read_text(encoding="utf-8", errors="replace")):
|
|
leaks += 1
|
|
print(f" LEAK {p.relative_to(REPO)}:{ln}: {line.strip()[:110]}")
|
|
print(f"kit_lint: leak check — {leaks} finding(s) over {n} files (calibration fences + provenance lines exempt)")
|
|
findings += leaks
|
|
# 2 PLACEHOLDERS
|
|
used = placeholders_used(files)
|
|
listed = placeholders_listed()
|
|
extra, unused = sorted(set(used) - listed), sorted(listed - set(used))
|
|
for ph in extra:
|
|
print(f" PLACEHOLDER used but not listed in PLACEHOLDERS.md: {ph} ({', '.join(str(q.relative_to(REPO)) for q in sorted(used[ph])[:3])})")
|
|
for ph in unused:
|
|
print(f" PLACEHOLDER listed but used nowhere: {ph}")
|
|
print(f"kit_lint: placeholders — used {len(used)}, listed {len(listed)}, unlisted {len(extra)}, unused {len(unused)}")
|
|
findings += len(extra) + len(unused)
|
|
# 3 SYNTAX (the corpus copies included — they are real scripts)
|
|
bad = check_syntax(files + corpus)
|
|
for p, why in bad:
|
|
print(f" SYNTAX {p.relative_to(REPO)}: {why}")
|
|
n_scripts = sum(1 for p in files + corpus if p.suffix in (".sh", ".py", ".json", ".yml", ".yaml"))
|
|
print(f"kit_lint: syntax — {len(bad)} failure(s) over {n_scripts} script/config files")
|
|
findings += len(bad)
|
|
# 4 GITIGNORE
|
|
if do_gitignore:
|
|
r = subprocess.run([sys.executable, str(REPO / "tools" / "gitignore_template_check.py")], capture_output=True, text=True)
|
|
print(f"kit_lint: gitignore template — {(r.stdout.strip().splitlines() or ['(no output)'])[-1]}")
|
|
if r.returncode:
|
|
findings += 1
|
|
# 5 TODO counts (informational)
|
|
text = "\n".join(p.read_text(encoding="utf-8", errors="replace") for p in files)
|
|
print(f"kit_lint: TODO(platform) {text.count('TODO(platform)')}, TODO(phase-N) {len(re.findall(r'TODO\(phase-\d+\)', text))} over {n} files")
|
|
return 1 if findings else 0
|
|
|
|
|
|
def selftest():
|
|
scratch = REPO / ".run" / "kit_lint_selftest"
|
|
scratch.mkdir(parents=True, exist_ok=True)
|
|
f = scratch / "planted.md"
|
|
f.write_text(
|
|
"# planted control\n"
|
|
"a leak line that names the source project: BFM and its rule R32\n" # MUST be caught
|
|
"```calibration\nsource project: BFM 218/218, R22 after every batch\n```\n" # allowed
|
|
"provenance: BFM R32 (Phase 26)\n" # allowed
|
|
"an unlisted placeholder {{NOT_IN_THE_CONTRACT}}\n", # MUST be caught
|
|
encoding="utf-8")
|
|
leaks = leak_lines(f.read_text(encoding="utf-8"))
|
|
used = placeholders_used([f])
|
|
extra = set(used) - placeholders_listed()
|
|
ok = len(leaks) == 1 and leaks[0][0] == 2 and extra == {"{{NOT_IN_THE_CONTRACT}}"}
|
|
print(f"kit_lint --selftest: leak lines caught {len(leaks)} (expected 1, at line 2: {leaks[0][0] if leaks else '-'}), "
|
|
f"unlisted placeholders caught {sorted(extra)} (expected ['{{{{NOT_IN_THE_CONTRACT}}}}']) → {'OK' if ok else 'FAIL'}")
|
|
f.unlink()
|
|
return 0 if ok else 1
|
|
|
|
|
|
def main(argv):
|
|
if "--selftest" in argv:
|
|
return selftest()
|
|
if not KIT.is_dir():
|
|
print(f"kit_lint: {KIT.relative_to(REPO)} does not exist — nothing to lint (R43)")
|
|
return 2
|
|
if "--paths" in argv:
|
|
files = [pathlib.Path(a).resolve() for a in argv[argv.index("--paths") + 1:]]
|
|
return run_checks(files, do_gitignore=False)
|
|
rc = run_checks(kit_files())
|
|
print("kit_lint: OK" if rc == 0 else f"kit_lint: FAIL (rc {rc})")
|
|
return rc
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|