#!/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"] 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): pattern = schema["pattern"] # Sicherstellen dass Pattern ^...$ hat für fullmatch if not (pattern.startswith("^") and pattern.endswith("$")): fullmatch_pattern = f"^{pattern}$" else: fullmatch_pattern = pattern if not re.fullmatch(fullmatch_pattern, data[field]): errors.append( f"❌ Feld '{field}' entspricht nicht dem Pattern: {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": if "values" not in var_schema: errors.append( f"❌ Enum Variable '{var_name}' benötigt 'values' Array" ) elif not var_schema.get("values"): errors.append( f"❌ Enum Variable '{var_name}' hat leeres '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("⚠️ Warnung: 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_block_start = content.find("```") if template_block_start != -1: # Finde den Code-Block nach "## Template" oder "# Template" template_section = content.find("## Template", content.find("Template")) if template_section == -1: template_section = content.find("# Template", content.find("Template")) if template_section > template_block_start: template_block_start = template_section if template_block_start == -1: errors.append("❌ Kein Code-Block für Template gefunden") else: # Prüfe ob Variablen im Template sind template_content = content[template_block_start:] if "{" not in template_content or "}" not in template_content: errors.append( "⚠️ Warnung: Keine Variablen (z.B. {var}) im Template gefunden" ) return len([e for e in errors if e.startswith("❌")]) == 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}"] ALLOWED_DIRS = {"system", "user", "custom", "categories"} def find_templates(directory: Path) -> List[Path]: """Findet alle Template-Dateien in einem Verzeichnis Baum.""" templates = [] for root, dirs, files in os.walk(directory): rel = os.path.relpath(root, directory) if rel != "." and rel.split(os.sep)[0] not in ALLOWED_DIRS: dirs.clear() # Skip nicht-erlaubte Subdirs continue 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") type_group = parser.add_mutually_exclusive_group() type_group.add_argument( "--type-json", action="store_true", help="Nur JSON-Templates validieren" ) type_group.add_argument( "--type-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 args.type_json: templates = [t for t in templates if t.suffix == ".json"] if args.type_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.type_json: templates = [t for t in templates if t.suffix == ".json"] if args.type_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) # Make path relative to base_dir, handling both absolute and relative paths try: rel_path = str(template_path.relative_to(base_dir)) except ValueError: # If template_path is not under base_dir, use absolute path rel_path = str(template_path) 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") if invalid > 0: sys.exit(2) # Validierungsfehler sys.exit(0) if __name__ == "__main__": main()