#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Contrôle de cohérence du projet PromptKey (utilisé par la CI Gitea Actions). Vérifie : 1. la présence des fichiers attendus à la racine du dépôt ; 2. le pack de démarrage : 20 prompts, 5 par catégorie (Général, Écriture, Code, Analyse) sous packs/essentiels/ ; 3. chaque prompt : en-tête délimité par ---, contenant au minimum `id` (GUID valide et unique) et `title` ; 4. les noms de fichiers : minuscules, tirets, sans accents ; 5. toute variable {{...}} du corps est soit automatique (selection, clipboard, date, datetime, time, app, cursor), soit déclarée dans le bloc `variables:` de l'en-tête. Usage : python3 scripts/check_pack.py [racine_du_depot] Code de sortie : 0 si tout est conforme, 1 sinon (liste des erreurs). """ import re import sys import uuid from pathlib import Path RACINE = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path(__file__).resolve().parent.parent FICHIERS_ATTENDUS = ["README.md", "CHANGELOG.md", "LICENSE", ".gitignore", "build.ps1"] CATEGORIES = {"Général": 5, "Écriture": 5, "Code": 5, "Analyse": 5} VARIABLES_AUTO = {"selection", "clipboard", "date", "datetime", "time", "app", "cursor"} MOTIF_NOM_FICHIER = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*\.md$") MOTIF_VARIABLE = re.compile(r"\{\{\s*([A-Za-z0-9_]+)\s*\}\}") erreurs = [] def erreur(message): erreurs.append(message) print(f"ERREUR : {message}") # --- 1. Fichiers attendus --------------------------------------------------- for nom in FICHIERS_ATTENDUS: if not (RACINE / nom).is_file(): erreur(f"fichier attendu manquant : {nom}") # --- 2 à 5. Pack de démarrage ------------------------------------------------ pack = RACINE / "packs" / "essentiels" if not pack.is_dir(): erreur(f"dossier du pack introuvable : {pack}") else: ids_vus = {} total = 0 for categorie, attendu in CATEGORIES.items(): dossier = pack / categorie fichiers = sorted(dossier.glob("*.md")) if dossier.is_dir() else [] if len(fichiers) != attendu: erreur(f"catégorie {categorie} : {len(fichiers)} prompt(s), {attendu} attendus") for fichier in fichiers: total += 1 rel = fichier.relative_to(RACINE) if not MOTIF_NOM_FICHIER.match(fichier.name): erreur(f"{rel} : nom de fichier non conforme (minuscules, tirets, sans accents)") texte = fichier.read_text(encoding="utf-8") lignes = texte.splitlines() if not lignes or lignes[0].strip() != "---" or "---" not in lignes[1:]: erreur(f"{rel} : en-tête --- absent ou non fermé") continue fin = lignes[1:].index("---") + 1 entete = lignes[1:fin] corps = "\n".join(lignes[fin + 1:]) champs = {} declarees = set() for ligne in entete: m = re.match(r"^([a-z_]+):\s*(.*)$", ligne) if m: champs[m.group(1)] = m.group(2).strip() m2 = re.match(r"^\s*- name:\s*(\S+)\s*$", ligne) if m2: declarees.add(m2.group(1).lower()) for champ in ("id", "title"): if not champs.get(champ): erreur(f"{rel} : champ obligatoire absent dans l'en-tête : {champ}") guid = champs.get("id", "") try: uuid.UUID(guid) except (ValueError, AttributeError): erreur(f"{rel} : id n'est pas un GUID valide : {guid!r}") if guid in ids_vus: erreur(f"{rel} : id dupliqué (déjà dans {ids_vus[guid]})") ids_vus[guid] = str(rel) for nom_var in MOTIF_VARIABLE.findall(corps): if nom_var.lower() not in VARIABLES_AUTO and nom_var.lower() not in declarees: erreur(f"{rel} : variable {{{{{nom_var}}}}} utilisée mais non déclarée") if total != 20: erreur(f"pack : {total} prompt(s) au total, 20 attendus") else: print(f"Pack : {total} prompts, {len(ids_vus)} id uniques.") if erreurs: print(f"\nÉCHEC : {len(erreurs)} erreur(s).") sys.exit(1) print("OK : fichiers attendus présents, pack cohérent.")