← Files Marketing CouncilARCHIVED FILE

scripts/validate_pack.py

5.14 KB · Oct 2, 2026 · 00:31 UTC

↓ Download file

#!/usr/bin/env python3
"""Validate Marketing Council structure and Agent Skills frontmatter without third-party packages."""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path

NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")


def _frontmatter(path: Path) -> dict:
    text = path.read_text(encoding="utf-8")
    if not text.startswith("---\n"):
        return {}
    parts = text.split("---", 2)
    if len(parts) < 3:
        return {}
    data = {}
    for line in parts[1].strip().splitlines():
        if line.startswith(" ") or ":" not in line:
            continue
        key, value = line.split(":", 1)
        data[key.strip()] = value.strip().strip('"')
    return data


def _openai_interface(path: Path) -> dict:
    text = path.read_text(encoding="utf-8")
    data: dict[str, str] = {}
    for line in text.splitlines():
        stripped = line.strip()
        if stripped.startswith(("display_name:", "short_description:", "default_prompt:")):
            key, value = stripped.split(":", 1)
            data[key] = value.strip().strip('"')
    return data


def validate_pack(root: str | Path) -> dict:
    root = Path(root)
    errors: list[str] = []
    warnings: list[str] = []
    required_dirs = ["skills", "agents", "hooks", "workflows", "references", "neural"]
    for name in required_dirs:
        if not (root / name).is_dir():
            errors.append(f"missing required directory: {name}")

    skill_files = sorted((root / "skills").glob("*/SKILL.md")) if (root / "skills").exists() else []
    if (root / "SKILL.md").exists():
        skill_files = [root / "SKILL.md", *skill_files]
    if not skill_files:
        errors.append("no SKILL.md files found")
    for path in skill_files:
        fm = _frontmatter(path)
        name = fm.get("name", "")
        desc = fm.get("description", "")
        expected_name = root.name if path == root / "SKILL.md" else path.parent.name
        if name != expected_name:
            errors.append(f"{path}: frontmatter name must match directory")
        if not NAME_RE.fullmatch(name):
            errors.append(f"{path}: invalid skill name")
        if not desc:
            errors.append(f"{path}: missing description")
        elif len(desc) > 1024:
            errors.append(f"{path}: description exceeds 1024 characters")
        elif not desc.startswith("Use when"):
            warnings.append(f"{path}: description should start with 'Use when' for discovery")
        extra_fields = set(fm) - {"name", "description"}
        if extra_fields:
            errors.append(f"{path}: unsupported SKILL.md frontmatter fields: {sorted(extra_fields)}")
        if len(path.read_text(encoding="utf-8").splitlines()) > 500:
            warnings.append(f"{path}: over 500 lines; split for progressive disclosure")

        if path != root / "SKILL.md":
            openai_path = path.parent / "agents" / "openai.yaml"
            if not openai_path.is_file():
                errors.append(f"{path.parent.name}: missing agents/openai.yaml")
            else:
                iface = _openai_interface(openai_path)
                short = iface.get("short_description", "")
                prompt = iface.get("default_prompt", "")
                if not iface.get("display_name"):
                    errors.append(f"{path.parent.name}: missing interface.display_name")
                if not 25 <= len(short) <= 64:
                    errors.append(f"{path.parent.name}: interface.short_description must be 25..64 characters")
                if f"${path.parent.name}" not in prompt:
                    errors.append(f"{path.parent.name}: interface.default_prompt must mention ${path.parent.name}")


    graph_path = root / "neural" / "graph.json"
    if graph_path.is_file():
        try:
            graph = json.loads(graph_path.read_text(encoding="utf-8"))
            ids = {node.get("id") for node in graph.get("nodes", [])}
            if len(ids) != len(graph.get("nodes", [])):
                errors.append("neural graph has duplicate node ids")
            for edge in graph.get("edges", []):
                if edge.get("from") not in ids or edge.get("to") not in ids:
                    errors.append(f"neural graph has dangling edge: {edge}")
        except Exception as exc:
            errors.append(f"neural graph unreadable: {exc}")

    corpus = "\n".join(p.read_text(encoding="utf-8", errors="ignore") for p in root.glob("**/*.md"))
    for phrase in ("you are philip kotler", "you are seth godin", "you are steve jobs", "pretend to be philip kotler"):
        if phrase in corpus.lower():
            errors.append(f"celebrity role-play instruction found: {phrase}")

    return {
        "valid": not errors,
        "skill_count": len(skill_files),
        "errors": errors,
        "warnings": warnings,
    }


def main() -> None:
    p = argparse.ArgumentParser(description="Validate a Marketing Council pack.")
    p.add_argument("root", nargs="?", default=Path(__file__).resolve().parents[1], type=Path)
    args = p.parse_args()
    result = validate_pack(args.root)
    print(json.dumps(result, indent=2, sort_keys=True))
    raise SystemExit(0 if result["valid"] else 1)


if __name__ == "__main__":
    main()

SHA-256: 1ef01374a6b93d958182b81b4c30373258e6fef27b3f71a027748bf4d5a50cae