#!/usr/bin/env python3 """ Prompt Template Validator Validiert JSON- und Markdown-Templates gegen definierte Schemas. """ import argparse import json import os import re import sys from pathlib import Path from typing import Dict, List, Optional, Tuple # JSON Schema für JSON-Templates JSON_SCHEMA = { "required": ["name", "template", "variables"], "properties": { "name": {"type": "string", "minLength": 1}, "version": {"type": "string", "pattern": r"^\d+\.\d+$"}, "description": {"type": "string"}, "role": {"type": "string"}, "template": {"type": "string", "minLength": 1}, "variables": { "type": "object", "patternProperties": { r"^.+$": { "type": "object", "properties": { "type": {"type": "string", "enum": ["string", "number", "enum", "boolean"]}, "required": {"type": "boolean"}, "default": {}, "description": {"type": "string"}, "values": {"type": "array"}, }, "required": ["type"], } }, }, "tags": {"type": "array", "items": {"type": "string"}}, "language": {"type": "string", "enum": ["de", "en", "fr", "es", "any"]}, }, } # Muster für Markdown-Templates MD_REQUIRED_SECTIONS = ["Template", "Variablen"] MD_VARIABLE_PATTERN = re.compile(r"\|\s*(\w+)\s*\|\s*(\w+)\s*\|") def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]: """Validiert ein JSON-Template.""" errors = [] try: with open(filepath, 'r', encoding='utf-8') as f: data = json.load(f) except json.JSONDecodeError as e: return False, [f"❌ JSON Syntax Error: {e}"] except Exception as e: return False, [f"❌ Datei kann nicht gelesen werden: {e}"] # Schemas validieren if not isinstance(data, dict): return False, ["❌ Root muss ein Object sein"] # Required Felder for field in JSON_SCHEMA.get("required", []): if field not in data: errors.append(f"❌ Fehlendes Pflichtfeld: '{field}'") # Feld-Typen validieren for field, schema in JSON_SCHEMA.get("properties", {}).items(): if field in data: field_type = schema.get("type") if field_type == "string" and not isinstance(data[field], str): errors.append(f"❌ Feld '{field}' muss ein String sein") elif field_type == "object" and not isinstance(data[field], dict): errors.append(f"❌ Feld '{field}' muss ein Object sein") elif field_type == "array" and not isinstance(data[field], list): errors.append(f"❌ Feld '{field}' muss ein Array sein") # Pattern validieren if "pattern" in schema and isinstance(data[field], str): if not re.match(schema["pattern"], data[field]): errors.append(f"❌ Feld '{field}' entspricht nicht dem Pattern: {schema['pattern']}") # Template prüfen if "template" in data: template = data["template"] if not isinstance(template, str): errors.append("❌ Template muss ein String sein") else: # Variablen im Template prüfen if "variables" in data: template_vars = set(re.findall(r"\{(\w+)\}", template)) defined_vars = set(data["variables"].keys()) undefined_vars = template_vars - defined_vars if undefined_vars: errors.append(f"❌ Undefinierte Variablen im Template: {', '.join(undefined_vars)}") # Variablen validieren if "variables" in data: variables = data["variables"] if not isinstance(variables, dict): errors.append("❌ Variables muss ein Object sein") else: for var_name, var_schema in variables.items(): if not isinstance(var_schema, dict): errors.append(f"❌ Variable '{var_name}' muss ein Object sein") continue if "type" not in var_schema: errors.append(f"❌ Variable '{var_name}' benötigt ein 'type' Feld") if var_schema.get("type") == "enum" and "values" not in var_schema: errors.append(f"❌ Enum Variable '{var_name}' benötigt 'values' Array") return len(errors) == 0, errors def validate_md_template(filepath: Path) -> Tuple[bool, List[str]]: """Validiert ein Markdown-Template.""" errors = [] try: with open(filepath, 'r', encoding='utf-8') as f: content = f.read() except Exception as e: return False, [f"❌ Datei kann nicht gelesen werden: {e}"] # Mindestlänge if len(content.strip()) < 50: errors.append("❌ Template zu kurz (mind. 50 Zeichen)") # Titel prüfen if not content.startswith("# "): errors.append("❌ Fehlender Titel (erwartet: # Titel)") # Pflichtabschnitte prüfen for section in MD_REQUIRED_SECTIONS: if f"## {section}" not in content and f"# {section}" not in content: errors.append(f"❌ Fehlender Abschnitt: {section}") # Variablen-Tabelle prüfen if "Variablen" in content: var_section_start = content.find("## Variablen") if var_section_start == -1: var_section_start = content.find("# Variablen") if var_section_start != -1: var_section = content[var_section_start:var_section_start + 500] if "| Variable |" not in var_section: errors.append("❌ Variablen-Tabelle nicht im korrekten Format") # Template-Block prüfen if "Template" in content: template_start = content.find("```") if template_start == -1: errors.append("❌ Kein Code-Block für Template gefunden") else: # Prüfe ob Variablen im Template sind template_content = content[template_start:] if "{" not in template_content or "}" not in template_content: errors.append("⚠️ Warnung: Keine Variablen (z.B. {var}) im Template gefunden") return len(errors) == 0, errors def validate_template(filepath: Path) -> Tuple[bool, List[str]]: """Validiert ein Template basierend auf der Dateiendung.""" if filepath.suffix.lower() == ".json": return validate_json_template(filepath) elif filepath.suffix.lower() == ".md": return validate_md_template(filepath) else: return False, [f"❌ Unsupported file type: {filepath.suffix}"] def find_templates(directory: Path) -> List[Path]: """Findet alle Template-Dateien in einem Verzeichnis Baum.""" templates = [] for root, _, files in os.walk(directory): for file in files: if file.endswith((".json", ".md")): templates.append(Path(root) / file) return templates def main(): parser = argparse.ArgumentParser( description="Validiert Prompt-Templates", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Beispiele: python validate.py templates/system/code_reviewer.json python validate.py --all python validate.py --json templates/ """ ) parser.add_argument("path", nargs="?", help="Pfad zum Template oder Verzeichnis") parser.add_argument("--all", action="store_true", help="Alle Templates validieren") parser.add_argument("--json", action="store_true", help="Nur JSON-Templates validieren") parser.add_argument("--md", action="store_true", help="Nur Markdown-Templates validieren") args = parser.parse_args() base_dir = Path(__file__).parent.parent if args.all: # Alle Templates finden templates = find_templates(base_dir / "templates") if not args.json: templates += find_templates(base_dir / "categories") if args.json: templates = [t for t in templates if t.suffix == ".json"] if args.md: templates = [t for t in templates if t.suffix == ".md"] elif args.path: path = Path(args.path) if path.is_dir(): templates = find_templates(path) if args.json: templates = [t for t in templates if t.suffix == ".json"] if args.md: templates = [t for t in templates if t.suffix == ".md"] else: templates = [path] else: print("❌ Bitte Pfad angeben oder --all verwenden") print("Beispiel: python validate.py --all") sys.exit(1) if not templates: print("❌ Keine Templates gefunden") sys.exit(1) # Validierung total = len(templates) valid = 0 invalid = 0 print(f"\n{'='*60}") print(f"Validiere {total} Template(s)...\n") for template_path in sorted(templates): is_valid, errors = validate_template(template_path) rel_path = str(template_path.relative_to(base_dir)) if is_valid: print(f"✅ {rel_path}") valid += 1 else: print(f"❌ {rel_path}") for error in errors: print(f" {error}") invalid += 1 print(f"\n{'='*60}") print(f"Ergebnis: {valid} ✅ | {invalid} ❌ | {total} Total") sys.exit(0 if invalid == 0 else 1) if __name__ == "__main__": main()