#!/usr/bin/env python3 """Fail if decomposition docs regress hybrid vocabulary (gig vs non-combat skill). Catches phrases like "one combat skill" or bare "combat skill" that imply a combat SkillDef. Allows "non-combat skill(s)" and "anti-combat …" when the hyphen prefix is immediately before ``combat``. Run in CI (PR gate) and locally after editing ``docs/decomposition/**``. """ from __future__ import annotations import re import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent DECOMP_ROOT = REPO_ROOT / "docs/decomposition" # Standalone phrases that almost always indicate a doc bug vs game-design hybrid model. LITERAL_BANS = [ "one combat skill", "one combat skills", ] # Match "combat skill" / "combat skills" unless prefixed by "non-" or "anti-" (hyphenated). COMBAT_SKILL = re.compile(r"\bcombat skills?\b", re.IGNORECASE) def line_has_bad_combat_skill(line: str) -> bool: for m in COMBAT_SKILL.finditer(line): start = m.start() if start >= 4 and line[start - 4 : start].lower() == "non-": continue if start >= 5 and line[start - 5 : start].lower() == "anti-": continue return True return False def check_file(path: Path) -> list[str]: errors: list[str] = [] text = path.read_text(encoding="utf-8") lower = text.lower() for banned in LITERAL_BANS: if banned in lower: errors.append(f"{path.relative_to(REPO_ROOT)}: contains forbidden phrase {banned!r}") for i, line in enumerate(text.splitlines(), start=1): if line_has_bad_combat_skill(line): errors.append(f"{path.relative_to(REPO_ROOT)}:{i}: suspicious 'combat skill' (use gig or 'non-combat skill'): {line.strip()[:200]}") return errors def main() -> int: if not DECOMP_ROOT.is_dir(): print(f"error: missing {DECOMP_ROOT}", file=sys.stderr) return 1 all_errors: list[str] = [] for path in sorted(DECOMP_ROOT.rglob("*.md")): all_errors.extend(check_file(path)) if all_errors: for msg in all_errors: print(msg, file=sys.stderr) print(f"decomposition language check failed ({len(all_errors)} issue(s))", file=sys.stderr) return 1 print("decomposition language OK: hybrid vocabulary (no stray combat SkillDef phrasing)") return 0 if __name__ == "__main__": raise SystemExit(main())