← Files MightShapeARCHIVED FILE

scripts/validate_package.py

12.9 KB · Sep 30, 2026 · 23:14 UTC

↓ Download file

#!/usr/bin/env python3
"""Repository-level MightShape release validator.

This complements, rather than replaces, the official plugin and skill validators.
It checks product invariants that generic package validation cannot know.
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any

from dc_core import DesignCouncilError, json_output, load_json


COUNCIL_IDS = [
    "maya-chen", "leo-martinez", "priya-rao", "marcus-brooks", "elena-rossi",
    "theo-bennett", "samira-okafor", "jack-sullivan", "mei-tanaka", "rafael-alvarez",
]
EXPECTED_SCRIPTS = {
    "dc.py", "dc_core.py", "project_state.py", "select_methods.py", "cluster_ideas.py", "score_pov.py",
    "score_build_gate.py", "check_evidence.py", "allocate_council.py", "session_summary.py", "create_study.py",
    "create_persona.py", "validate_reality_packet.py", "detect_leading_questions.py", "compare_participants.py",
    "synthesize_inquiry.py", "validate_package.py", "sealed_round.py",
    "participant_sources.py", "disclosure_guard.py",
    "render_visual.py",
}
EXPECTED_SCHEMAS = {
    "project-state.schema.json", "evidence.schema.json", "assumption.schema.json", "insight.schema.json",
    "pov.schema.json", "idea.schema.json", "prototype.schema.json", "experiment.schema.json",
    "council-response.schema.json", "human-model.schema.json", "inquiry-study.schema.json",
    "reality-packet.schema.json", "synthetic-persona.schema.json", "interview-guide.schema.json",
    "participant.schema.json", "transcript.schema.json", "research-finding.schema.json", "reality-check.schema.json",
    "participant-source.schema.json", "internal-study.schema.json", "external-study-packet.schema.json",
    "project-exposure.schema.json", "disclosure-review.schema.json", "conflict-policy.schema.json",
    "experience-signal.schema.json", "participant-profile.schema.json", "participant-verification-status.schema.json",
    "research-session-type.schema.json", "exchange-recruitment-request.schema.json",
    "exchange-credit-ledger.schema.json", "learning-signal.schema.json",
    "ip-exposure-assessment.schema.json", "demand-signal-event.schema.json",
    "visual-artifact.schema.json",
    "participation-session.schema.json",
    "team-channel-binding.schema.json",
    "team-workshop-session.schema.json",
}
EXPECTED_TEMPLATES = {
    "design-brief.md", "research-plan.md", "interview-guide.md", "reality-packet.md", "pov-card.md",
    "hmw-card.md", "prototype-card.md", "experiment-card.md", "decision-record.md",
}
SOURCE_FAMILIES = {"public_design_practice", "supplemental_design_practice", "mightshape_original"}
SEMVER_RE = re.compile(
    r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"
    r"(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?"
    r"(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$"
)


def _is_semver(value: object) -> bool:
    match = SEMVER_RE.fullmatch(str(value))
    if not match:
        return False
    prerelease = match.group(4)
    if prerelease:
        for identifier in prerelease.split("."):
            if identifier.isdigit() and len(identifier) > 1 and identifier.startswith("0"):
                return False
    return True


def _check(condition: bool, code: str, message: str, errors: list[dict[str, str]]) -> None:
    if not condition:
        errors.append({"code": code, "message": message})


def validate_package(root: str | Path) -> dict[str, Any]:
    root = Path(root).resolve()
    errors: list[dict[str, str]] = []
    warnings: list[dict[str, str]] = []
    checks: list[dict[str, Any]] = []

    def report(name: str, before: int, details: Any = None) -> None:
        checks.append({"name": name, "passes": len(errors) == before, "details": details})

    before = len(errors)
    for relative in (".codex-plugin/plugin.json", "README.md", "LICENSE", "AGENTS.md", "skills/mightshape/SKILL.md"):
        _check((root / relative).is_file(), "MISSING_FILE", relative, errors)
    report("required repository files", before)

    before = len(errors)
    manifest_path = root / ".codex-plugin" / "plugin.json"
    if manifest_path.exists():
        try:
            manifest = load_json(manifest_path)
            _check(manifest.get("name") == "mightshape", "MANIFEST_NAME", "plugin name must be mightshape", errors)
            _check(
                _is_semver(manifest.get("version", "")),
                "MANIFEST_VERSION",
                "plugin version must be valid SemVer (for example 1.0.0 or 0.9.0-beta.1)",
                errors,
            )
            skill_path = root / str(manifest.get("skills", "")).lstrip("./")
            _check(skill_path.is_dir(), "MANIFEST_SKILLS", "manifest skills path does not exist", errors)
            interface = manifest.get("interface", {})
            for key in ("composerIcon", "logo"):
                value = interface.get(key)
                _check(bool(value) and (root / str(value).lstrip("./")).is_file(), "MANIFEST_ASSET", f"interface.{key} target is missing", errors)
            _check(interface.get("brandColor") == "#6457FF", "BRAND_COLOR", "expected MightShape brand color #6457FF", errors)
        except DesignCouncilError as exc:
            errors.append({"code": "MANIFEST_JSON", "message": str(exc)})
    report("plugin manifest", before)

    skill_root = root / "skills" / "mightshape"
    before = len(errors)
    skill_path = skill_root / "SKILL.md"
    if skill_path.exists():
        skill = skill_path.read_text(encoding="utf-8")
        _check(skill.startswith("---\n"), "SKILL_FRONTMATTER", "SKILL.md needs YAML frontmatter", errors)
        _check("name: design-think" in skill, "SKILL_NAME", "primary skill name must be design-think", errors)
        _check("description:" in skill.split("---", 2)[1], "SKILL_DESCRIPTION", "skill description is missing", errors)
        _check(len(skill.splitlines()) <= 500, "SKILL_TOO_LONG", "SKILL.md exceeds 500 lines; preserve progressive disclosure", errors)
        linked = re.findall(r"\]\((references/[^)#]+)\)", skill)
        for relative in linked:
            _check((skill_root / relative).is_file(), "BROKEN_SKILL_LINK", relative, errors)
        for invariant in ("sealed", "SOLUTION BLACKOUT", "Build Gate", "evidence firewall", "Minority Report"):
            _check(invariant.lower() in skill.lower(), "SKILL_INVARIANT", f"SKILL.md does not route {invariant}", errors)
    report("skill metadata and progressive disclosure", before)

    before = len(errors)
    schema_root = skill_root / "schemas"
    found_schemas = {path.name for path in schema_root.glob("*.json")}
    for missing in sorted(EXPECTED_SCHEMAS - found_schemas):
        errors.append({"code": "MISSING_SCHEMA", "message": missing})
    for path in sorted(schema_root.glob("*.json")):
        try:
            schema = load_json(path)
            try:
                import jsonschema  # type: ignore
                jsonschema.validators.validator_for(schema).check_schema(schema)
            except ImportError:
                if schema.get("type") != "object":
                    warnings.append({"code": "SCHEMA_BASIC_ONLY", "message": f"jsonschema unavailable; minimally checked {path.name}"})
        except Exception as exc:
            errors.append({"code": "INVALID_SCHEMA", "message": f"{path.name}: {exc}"})
    report("JSON Schemas", before, {"count": len(found_schemas)})

    before = len(errors)
    script_root = skill_root / "scripts"
    found_scripts = {path.name for path in script_root.glob("*.py")}
    for missing in sorted(EXPECTED_SCRIPTS - found_scripts):
        errors.append({"code": "MISSING_SCRIPT", "message": missing})
    for path in sorted(script_root.glob("*.py")):
        try:
            compile(path.read_text(encoding="utf-8"), str(path), "exec")
        except SyntaxError as exc:
            errors.append({"code": "PYTHON_SYNTAX", "message": f"{path.name}:{exc.lineno}: {exc.msg}"})
    report("deterministic scripts", before, {"count": len(found_scripts)})

    before = len(errors)
    reference_root = skill_root / "references"
    _check(
        (reference_root / "participatory-workshops.md").is_file(),
        "MISSING_REFERENCE",
        "participatory-workshops.md",
        errors,
    )
    for member_id in COUNCIL_IDS:
        path = reference_root / f"council-{member_id}.md"
        _check(path.is_file(), "MISSING_PROFILE", member_id, errors)
        if not path.is_file():
            continue
        text = path.read_text(encoding="utf-8")
        normalized_text = text.lower().replace("-", " ")
        word_count = len(re.findall(r"\b[\w'-]+\b", text))
        _check(word_count >= 900, "SHALLOW_PROFILE", f"{member_id} has only {word_count} words", errors)
        for concept in ("Present life", "Professional reality", "Worldview", "Communication", "Contradiction", "Knowledge bound", "project memory"):
            _check(concept.lower() in normalized_text, "PROFILE_SECTION", f"{member_id} lacks {concept}", errors)
        _check("I don't know" in text or "I don’t know" in text, "KNOWLEDGE_BOUNDARY", f"{member_id} lacks an explicit 'I don't know' behavior", errors)
    report("ten deep Human Models", before, {"count": len(list(reference_root.glob("council-*.md"))) - 2})

    before = len(errors)
    registry_path = reference_root / "method-registry.json"
    if registry_path.exists():
        try:
            registry = load_json(registry_path)
            methods = registry.get("methods", [])
            ids = [item.get("id") for item in methods]
            _check(len(methods) >= 60, "METHOD_COVERAGE", f"registry contains only {len(methods)} methods", errors)
            _check(len(ids) == len(set(ids)), "METHOD_IDS", "method IDs must be unique", errors)
            families = {item.get("source_family") for item in methods}
            _check(families <= SOURCE_FAMILIES, "SOURCE_FAMILY", f"unknown source families: {sorted(families - SOURCE_FAMILIES)}", errors)
            _check(SOURCE_FAMILIES <= families, "SOURCE_FAMILY_COVERAGE", "all three source families must be represented", errors)
            for item in methods:
                for field in ("id", "name", "modes", "purpose", "use_when", "avoid_when", "requires", "outputs", "council", "effort", "source_family"):
                    _check(field in item, "METHOD_FIELD", f"{item.get('id', '?')} missing {field}", errors)
        except DesignCouncilError as exc:
            errors.append({"code": "METHOD_REGISTRY_JSON", "message": str(exc)})
    else:
        errors.append({"code": "MISSING_METHOD_REGISTRY", "message": str(registry_path)})
    report("method registry and attribution", before)

    before = len(errors)
    template_root = skill_root / "assets" / "templates"
    found_templates = {path.name for path in template_root.glob("*.md")} if template_root.exists() else set()
    for missing in sorted(EXPECTED_TEMPLATES - found_templates):
        errors.append({"code": "MISSING_TEMPLATE", "message": missing})
    report("facilitation templates", before, {"count": len(found_templates)})

    before = len(errors)
    app = root / "interview-app"
    for relative in ("package.json", ".openai/hosting.json", "README.md"):
        _check((app / relative).is_file(), "INTERVIEW_APP", f"optional hosted companion missing {relative}", errors)
    if (app / "package.json").exists():
        try:
            package = load_json(app / "package.json")
            _check(bool(package.get("scripts", {}).get("test")), "INTERVIEW_TEST", "interview app has no test script", errors)
            _check(bool(package.get("scripts", {}).get("build")), "INTERVIEW_BUILD", "interview app has no build script", errors)
        except DesignCouncilError as exc:
            errors.append({"code": "INTERVIEW_PACKAGE_JSON", "message": str(exc)})
    report("optional hosted interview companion", before)

    before = len(errors)
    _check((root / "evals" / "cases").is_dir(), "EVALS", "behavioral eval cases directory is missing", errors)
    _check((root / "tests").is_dir(), "TESTS", "unit tests directory is missing", errors)
    report("test and eval harness", before)

    return {
        "valid": not errors,
        "package_root": str(root),
        "checks": checks,
        "errors": errors,
        "warnings": warnings,
        "summary": {"checks": len(checks), "passed": sum(item["passes"] for item in checks), "failed": sum(not item["passes"] for item in checks), "errors": len(errors), "warnings": len(warnings)},
        "note": (
            "Also run Codex's bundled plugin/skill authoring validators and the current "
            "OpenAI submission-portal validator before release."
        ),
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Validate the complete MightShape package")
    parser.add_argument("root", nargs="?", default=".")
    args = parser.parse_args()
    try:
        result = validate_package(args.root)
        json_output(result)
        return 0 if result["valid"] else 1
    except DesignCouncilError as exc:
        print(f"MightShape error: {exc}", file=sys.stderr)
        return 2


if __name__ == "__main__":
    raise SystemExit(main())

SHA-256: 86aa91f92901a0656c56b860c9cbf86bc9baf0dbbae28370202aac33340360b9