| 1 | #!/usr/bin/env python3 |
| 2 | """PPT Master SVG quality-check CLI implementation. |
| 3 | |
| 4 | Parses the legacy command-line contract and delegates validation to the checker. |
| 5 | |
| 6 | Usage: |
| 7 | python3 scripts/svg_quality_checker.py <svg_file_or_project> [options] |
| 8 | |
| 9 | Examples: |
| 10 | python3 scripts/svg_quality_checker.py projects/demo --stage final --json |
| 11 | |
| 12 | Dependencies: |
| 13 | Standard library plus local PPT Master validation modules. |
| 14 | """ |
| 15 | |
| 16 | import sys |
| 17 | from pathlib import Path |
| 18 | |
| 19 | from attribution_guard import require_skill_integrity |
| 20 | from slide_roster import discover_slide_svgs |
| 21 | |
| 22 | from .checker import SVGQualityChecker |
| 23 | |
| 24 | |
| 25 | def _first_page_target(target: str) -> str: |
| 26 | """Resolve a project/directory target to its first authored SVG page.""" |
| 27 | path = Path(target) |
| 28 | if path.is_file(): |
| 29 | return str(path) |
| 30 | svg_root = path / "svg_output" if (path / "svg_output").is_dir() else path |
| 31 | svg_files = discover_slide_svgs(svg_root) if svg_root.is_dir() else [] |
| 32 | return str(svg_files[0]) if svg_files else target |
| 33 | |
| 34 | |
| 35 | def _default_json_report_path( |
| 36 | checker: SVGQualityChecker, |
| 37 | target: str, |
| 38 | stage: str, |
| 39 | ) -> Path: |
| 40 | """Choose a stage-specific report path without overwriting the final gate.""" |
| 41 | target_path = Path(target) |
| 42 | project_path = checker._resolve_project_path(target_path) |
| 43 | report_name = ( |
| 44 | "svg_quality_report.json" |
| 45 | if stage == "final" |
| 46 | else "svg_quality_first_page_report.json" |
| 47 | ) |
| 48 | if ( |
| 49 | (project_path / "svg_output").is_dir() |
| 50 | or (project_path / "design_spec.md").is_file() |
| 51 | ): |
| 52 | return project_path / "validation" / report_name |
| 53 | base = target_path if target_path.is_dir() else target_path.parent |
| 54 | return base / report_name |
| 55 | |
| 56 | |
| 57 | def print_usage() -> None: |
| 58 | """Print CLI usage information.""" |
| 59 | print("PPT Master - SVG Quality Check Tool\n") |
| 60 | print("Usage:") |
| 61 | print(" python3 scripts/svg_quality_checker.py <svg_file>") |
| 62 | print(" python3 scripts/svg_quality_checker.py <directory>") |
| 63 | print(" python3 scripts/svg_quality_checker.py <workspace>/templates --template-mode") |
| 64 | print(" python3 scripts/svg_quality_checker.py --all examples") |
| 65 | print("\nExamples:") |
| 66 | print(" python3 scripts/svg_quality_checker.py examples/project/svg_output/slide_01.svg") |
| 67 | print(" python3 scripts/svg_quality_checker.py examples/project/svg_output") |
| 68 | print(" python3 scripts/svg_quality_checker.py examples/project") |
| 69 | print(" python3 scripts/svg_quality_checker.py templates/layouts/presentation_core/templates --template-mode") |
| 70 | print(" python3 scripts/svg_quality_checker.py templates/decks/中国电信/templates --template-mode") |
| 71 | print("\nOptions:") |
| 72 | print(" --format <ppt169|ppt43|...> Expected canvas format") |
| 73 | print(" --stage <first-page|final> first-page checks only the first authored SVG") |
| 74 | print(" with a partial structure roster; final (default)") |
| 75 | print(" requires the complete declared page roster.") |
| 76 | print(" --json Write a machine-readable quality report") |
| 77 | print(" --json-output <path> Override the JSON report path") |
| 78 | print(" --quick-generate Validate lockless flat Quick Generate SVGs;") |
| 79 | print(" ignore design_spec.md and spec_lock.md.") |
| 80 | print(" --template-mode Validate a template workspace's templates/ directory:") |
| 81 | print(" Brand/Style validate their portable workspace contracts;") |
| 82 | print(" Layout/Deck glob *.svg directly, skip spec_lock checks,") |
| 83 | print(" enforce roster consistency, and emit placeholder hints.") |
| 84 | print(" native_structure_mode: structured also enables complete") |
| 85 | print(" per-file and cross-page structure validation. Legacy") |
| 86 | print(" native_structure_mode: template fails and must be") |
| 87 | print(" re-created through create-template before validation.") |
| 88 | print(" Warnings are advisory: they require no modification and do not affect exit status;") |
| 89 | print(" only errors make the command exit with status 1.") |
| 90 | |
| 91 | |
| 92 | def main() -> None: |
| 93 | """Run the CLI entry point.""" |
| 94 | require_skill_integrity() |
| 95 | if len(sys.argv) < 2: |
| 96 | print_usage() |
| 97 | sys.exit(0) |
| 98 | |
| 99 | if sys.argv[1] in {"-h", "--help", "help"}: |
| 100 | print_usage() |
| 101 | sys.exit(0) |
| 102 | |
| 103 | if sys.argv[1].startswith("--") and sys.argv[1] not in {"--all"}: |
| 104 | print(f"[ERROR] Missing target before option: {sys.argv[1]}") |
| 105 | print_usage() |
| 106 | sys.exit(1) |
| 107 | |
| 108 | template_mode = "--template-mode" in sys.argv |
| 109 | quick_generate = "--quick-generate" in sys.argv |
| 110 | if template_mode and quick_generate: |
| 111 | print("[ERROR] --template-mode cannot be combined with --quick-generate") |
| 112 | sys.exit(1) |
| 113 | checker = SVGQualityChecker( |
| 114 | template_mode=template_mode, |
| 115 | quick_generate=quick_generate, |
| 116 | ) |
| 117 | |
| 118 | target = sys.argv[1] |
| 119 | expected_format = None |
| 120 | stage = "final" |
| 121 | |
| 122 | if "--format" in sys.argv: |
| 123 | idx = sys.argv.index("--format") |
| 124 | if idx + 1 < len(sys.argv): |
| 125 | expected_format = sys.argv[idx + 1] |
| 126 | if "--stage" in sys.argv: |
| 127 | idx = sys.argv.index("--stage") |
| 128 | if idx + 1 >= len(sys.argv): |
| 129 | print("[ERROR] --stage requires first-page or final") |
| 130 | sys.exit(1) |
| 131 | stage = sys.argv[idx + 1] |
| 132 | if stage not in {"first-page", "final"}: |
| 133 | print(f"[ERROR] Unsupported quality-check stage: {stage}") |
| 134 | sys.exit(1) |
| 135 | |
| 136 | if target == "--all": |
| 137 | if quick_generate: |
| 138 | print("[ERROR] --quick-generate does not support --all") |
| 139 | sys.exit(1) |
| 140 | if stage != "final": |
| 141 | print("[ERROR] --stage first-page does not support --all") |
| 142 | sys.exit(1) |
| 143 | base_dir = sys.argv[2] if len(sys.argv) > 2 else "examples" |
| 144 | from project_utils import find_all_projects |
| 145 | |
| 146 | projects = find_all_projects(base_dir) |
| 147 | |
| 148 | for project in projects: |
| 149 | print(f"\n{'=' * 80}") |
| 150 | print(f"Checking project: {project.name}") |
| 151 | print("=" * 80) |
| 152 | checker.check_directory(str(project)) |
| 153 | else: |
| 154 | check_target = _first_page_target(target) if stage == "first-page" else target |
| 155 | checker.check_directory(check_target, expected_format) |
| 156 | |
| 157 | if stage == "final" and Path(target).is_dir(): |
| 158 | if checker._has_incomplete_page_roster: |
| 159 | print( |
| 160 | "[TIP] This final-stage run found an incomplete page roster. " |
| 161 | "During serial authoring, use --stage first-page for the first-page " |
| 162 | "gate; keep --stage final for the complete deck." |
| 163 | ) |
| 164 | |
| 165 | checker.print_summary() |
| 166 | |
| 167 | if "--export" in sys.argv: |
| 168 | output_file = "svg_quality_report.txt" |
| 169 | if "--output" in sys.argv: |
| 170 | idx = sys.argv.index("--output") |
| 171 | if idx + 1 < len(sys.argv): |
| 172 | output_file = sys.argv[idx + 1] |
| 173 | checker.export_report(output_file) |
| 174 | |
| 175 | if "--json" in sys.argv or "--json-output" in sys.argv: |
| 176 | if "--json-output" in sys.argv: |
| 177 | idx = sys.argv.index("--json-output") |
| 178 | if idx + 1 >= len(sys.argv): |
| 179 | print("[ERROR] --json-output requires a path") |
| 180 | sys.exit(1) |
| 181 | json_output = Path(sys.argv[idx + 1]) |
| 182 | else: |
| 183 | json_output = _default_json_report_path(checker, target, stage) |
| 184 | checker.export_json_report( |
| 185 | str(json_output), |
| 186 | target=target, |
| 187 | stage=stage, |
| 188 | ) |
| 189 | |
| 190 | if checker.summary["errors"] > 0: |
| 191 | sys.exit(1) |
| 192 | else: |
| 193 | sys.exit(0) |
| 194 |