#!/usr/bin/env python3 """Icon validation + project sync (strategist GATE tool). Strategist runs this while locking the icon inventory (design_spec.md §VI / spec_lock.md `icons`). It does two things, in order of importance: 1. **Validate** each ``lib/name`` against the global icon library (``templates/icons/``). A name that does not resolve to a real file is a hard error — the script exits non-zero listing every miss, so the strategist's "missing icon = re-pick now" GATE actually fires instead of carrying a phantom icon into generation. 2. **Copy** each resolved icon into ``/icons//`` so the project carries its own icon set. This is belt-and-suspenders: ``finalize_svg.py`` and the native ``svg_to_pptx`` converter both resolve project-first with a fallback to the global library (see ``svg_finalize.embed_icons``), so the copy is not strictly required for embedding — but it keeps the project self-contained and validates existence on the spot. Usage: python3 scripts/icon_sync.py [ ...] Example: python3 scripts/icon_sync.py /workspace/ppt生成/deck tabler-outline/brain tabler-outline/cpu Exit code: 0 every icon resolved (and copied) 1 one or more icons missing / unresolved 2 bad arguments """ from __future__ import annotations import shutil import sys from pathlib import Path # Windows GBK console safety (mirrors other ppt scripts). try: # pragma: no cover - platform dependent sys.stdout.reconfigure(encoding="utf-8", errors="replace") sys.stderr.reconfigure(encoding="utf-8", errors="replace") except Exception: # pragma: no cover pass # Reuse the single source of truth for name resolution (handles the chunk/ # alias and the un-prefixed legacy layout) so validation matches what the # embedder will actually load at finalize / export time. sys.path.insert(0, str(Path(__file__).resolve().parent)) from svg_finalize.embed_icons import resolve_icon_path # noqa: E402 GLOBAL_ICONS_DIR = Path(__file__).resolve().parent.parent / "templates" / "icons" def main(argv: list[str] | None = None) -> int: args = list(sys.argv[1:] if argv is None else argv) if len(args) < 2: print("usage: icon_sync.py [ ...]") return 2 project_path = Path(args[0]) names = args[1:] if not project_path.exists(): print(f"[ERROR] project path not found: {project_path}") return 2 project_icons = project_path / "icons" missing: list[str] = [] copied = 0 for name in names: src, _base = resolve_icon_path(name, GLOBAL_ICONS_DIR) if not src.exists(): missing.append(name) print(f"[MISSING] {name} (no file under {GLOBAL_ICONS_DIR})") continue # Mirror into /icons//.svg, preserving the lib dir. rel = src.relative_to(GLOBAL_ICONS_DIR) dst = project_icons / rel dst.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(src, dst) copied += 1 print(f"[OK] {name} -> {dst}") print() print(f"[Summary] {copied} copied, {len(missing)} missing") if missing: print("[GATE] missing icons — re-pick real filenames via " "`ls templates/icons// | grep `, fix spec_lock.md, re-run:") for m in missing: print(f" - {m}") return 1 return 0 if __name__ == "__main__": raise SystemExit(main())