#!/usr/bin/env python3 """ PPT Master - Project Specification Helpers Scaffold and validate the Markdown planning artifacts used by project_manager.py. The module keeps schema parsing and deterministic scaffold rendering independent from the broader project-management command surface. Usage: Import validate_project_artifacts() or scaffold_project_artifact(). Examples: from project_management.project_specs import validate_markdown_schema Dependencies: None (only uses the standard library and local project modules) """ from __future__ import annotations import json import math import re from pathlib import Path from typing import Mapping from .paths import ( SCAFFOLD_DIR, SCHEMA_DIR, SCRIPTS_DIR, SKILL_DIR, ) try: from project_utils import ( CANVAS_FORMATS, get_project_info as get_project_info_common, validate_communication_trace, ) except ImportError: import sys tools_dir = SCRIPTS_DIR if str(tools_dir) not in sys.path: sys.path.insert(0, str(tools_dir)) from project_utils import ( # type: ignore CANVAS_FORMATS, get_project_info as get_project_info_common, validate_communication_trace, ) try: from visualization_catalog import ( LEGACY_STRUCTURE_INTENT_KIND, VISUALIZATION_SVG_KIND, VisualizationCatalogError, resolve_visualization_reference, ) except ImportError: import sys if str(SCRIPTS_DIR) not in sys.path: sys.path.insert(0, str(SCRIPTS_DIR)) from visualization_catalog import ( # type: ignore LEGACY_STRUCTURE_INTENT_KIND, VISUALIZATION_SVG_KIND, VisualizationCatalogError, resolve_visualization_reference, ) TOOLS_DIR = SCRIPTS_DIR _CUSTOM_REFERENCE_CATALOGS = ( ("mode", "mode", "mode_references", SKILL_DIR / "references" / "modes"), ( "visual_style", "visual_style", "visual_style_references", SKILL_DIR / "references" / "visual-styles", ), ( "colors", "image_rendering", "image_rendering_references", SKILL_DIR / "references" / "image-renderings", ), ) _MARKDOWN_H2_RE = re.compile(r"^##[ \t]+(.+?)[ \t]*$", re.MULTILINE) _MARKDOWN_SUBHEADING_RE = re.compile(r"^#{3,6}[ \t]+(.+?)[ \t]*$", re.MULTILINE) _MARKDOWN_DATA_LINE_RE = re.compile( r"^[ \t]*-[ \t]+(?:\*\*)?([^:\n*]+?)(?:\*\*)?[ \t]*:[ \t]*(.*)$", re.MULTILINE, ) _IMAGE_PATH_SUFFIXES = frozenset( { ".bmp", ".emf", ".gif", ".jpeg", ".jpg", ".png", ".svg", ".tif", ".tiff", ".webp", ".wmf", } ) _IMAGE_ACQUISITION_SOURCES = frozenset( {"ai", "web", "user", "formula", "placeholder", "slice"} ) _IMAGE_CROP_POLICIES = frozenset({"adaptive", "no-crop"}) _LEGACY_IMAGE_METADATA_KEYS = frozenset( { "image_rendering", "image_rendering_behavior", "image_rendering_references", } ) _LEGACY_SPEC_LOCK_FORBIDDEN = frozenset({"Mixing icon libraries"}) _SCAFFOLD_TOKEN_RE = re.compile(r"\{\{[A-Z_]+\}\}") _SCHEMA_MARKER_RE = re.compile( r"^$", re.IGNORECASE, ) def _normalize_schema_value(value: str) -> str: """Normalize a Markdown scalar before enum, pattern, and catalog checks.""" normalized = value.strip() if ( len(normalized) >= 2 and normalized[0] == normalized[-1] and normalized[0] in "'\"`" ): return normalized[1:-1].strip() return normalized def _extract_schema_marker(text: str) -> tuple[str | None, str | None]: """Read the optional version marker from the first non-empty line.""" first_line = next((line.strip() for line in text.splitlines() if line.strip()), "") if not first_line.startswith("