文件预览

gate_check_content.py

查看 Deckcraft 技能包中的文件内容。

文件内容

scripts/gate_check_content.py

#!/usr/bin/env python3
"""
DeckCraft v5 — S3 Content Gate Check
Validates content.json format before rendering.
Catches API format errors that mental review misses.

Usage: python3 gate_check_content.py <content.json_path> <project_dir>
"""
import sys, os, json


def check_content(content_path, project_dir):
    with open(content_path) as f:
        content = json.load(f)

    fail_items = []
    pass_items = []

    pages = content.get("pages", [])

    if len(pages) == 0:
        fail_items.append({
            "check": "has_pages",
            "detail": "content.json has no pages"
        })

    supported_types = {
        "cover", "closing", "toc", "section", "section_divider",
        "content", "content_with_icon",
        "two-col", "vs_compare",
        "table", "stat_cards",
        "chart_bar", "chart_pie", "chart_line", "chart_gauge",
        "timeline", "process_flow", "matrix_2x2",
        "quote", "image_full", "image_split",
        "kpi_dashboard", "team_grid", "checklist",
        "summary", "image"
    }

    for i, page in enumerate(pages):
        idx = page.get("idx", i + 1)

        # 1. Type check
        ptype = page.get("type", "")
        if ptype not in supported_types:
            fail_items.append({
                "check": "valid_type", "slide": idx,
                "detail": f"Unknown type '{ptype}'. Supported: {', '.join(sorted(supported_types))}"
            })
        else:
            pass_items.append({"check": "valid_type", "slide": idx, "detail": f"type={ptype}"})

        # 2. Title check (all pages except cover should have title)
        title = page.get("title", "")
        if not title and ptype != "cover":
            fail_items.append({
                "check": "has_title", "slide": idx,
                "detail": "Missing title"
            })
        elif title:
            if len(title) > 50:
                warnings_item = {"check": "title_length", "slide": idx,
                                 "detail": f"Title '{title[:30]}...' is {len(title)} chars (>50)"}
                # Warning, not failure
                pass_items.append(warnings_item)
            else:
                pass_items.append({"check": "has_title", "slide": idx, "detail": f"OK ({len(title)} chars)"})

        # 3. Key point check (content pages should have key_point)
        key_point = page.get("key_point", "")
        if ptype not in ("cover", "section") and not key_point:
            fail_items.append({
                "check": "has_key_point", "slide": idx,
                "detail": f"Content slide missing key_point (full sentence with insight)"
            })
        elif key_point and len(key_point) < 10:
            fail_items.append({
                "check": "key_point_quality", "slide": idx,
                "detail": f"key_point too short ({len(key_point)} chars): '{key_point}'"
            })

        # 4. Content bullets check
        bullets = page.get("content", [])
        if isinstance(bullets, list) and ptype in ("content", "summary"):
            if len(bullets) > 6:
                fail_items.append({
                    "check": "bullet_count", "slide": idx,
                    "detail": f"{len(bullets)} bullets (max 6)"
                })
            for bi, bullet in enumerate(bullets):
                if isinstance(bullet, str) and len(bullet) > 100:
                    fail_items.append({
                        "check": "bullet_length", "slide": idx,
                        "detail": f"Bullet {bi+1} is {len(bullet)} chars (max 100): '{bullet[:40]}...'"
                    })

        # 5. Table pages need headers and rows
        if ptype == "table":
            headers = page.get("headers", [])
            rows = page.get("rows", [])
            if not headers:
                fail_items.append({
                    "check": "table_headers", "slide": idx,
                    "detail": "Table page missing 'headers'"
                })
            if not rows:
                fail_items.append({
                    "check": "table_rows", "slide": idx,
                    "detail": "Table page missing 'rows'"
                })

        # 6. Two-col pages need left/right content
        if ptype == "two-col":
            if not page.get("left_title") or not page.get("right_title"):
                fail_items.append({
                    "check": "two_col_titles", "slide": idx,
                    "detail": "Two-col page needs left_title and right_title"
                })

        # 7. Image pages should reference an image
        if ptype == "image":
            img = page.get("image", "")
            if not img:
                fail_items.append({
                    "check": "image_source", "slide": idx,
                    "detail": "Image page missing 'image' field"
                })

        # 8. Cover should have title at minimum
        if ptype == "cover" and not title:
            fail_items.append({
                "check": "cover_title", "slide": idx,
                "detail": "Cover page missing title"
            })

    # Global check: cover exists
    cover_exists = any(p.get("type") == "cover" for p in pages)
    if not cover_exists:
        fail_items.append({
            "check": "has_cover",
            "detail": "No cover page found"
        })
    else:
        pass_items.append({"check": "has_cover", "detail": "OK"})

    # Two-col overuse check
    two_col_count = sum(1 for p in pages if p.get("type") == "two-col")
    if two_col_count > 2:
        fail_items.append({
            "check": "two_col_overuse",
            "detail": f"{two_col_count} two-col pages (max 2 recommended)"
        })

    passed = len(fail_items) == 0
    result = {
        "passed": passed,
        "verdict": "PASS — ready for rendering" if passed else "FAIL — fix items before rendering",
        "total_slides": len(pages),
        "fail_count": len(fail_items),
        "pass_count": len(pass_items),
        "fail_items": fail_items,
        "pass_items": pass_items
    }

    os.makedirs(project_dir, exist_ok=True)
    out_path = os.path.join(project_dir, "gate_content.json")
    with open(out_path, "w") as f:
        json.dump(result, f, indent=2, ensure_ascii=False)

    return result


def main():
    if len(sys.argv) < 3:
        print("Usage: python3 gate_check_content.py <content.json_path> <project_dir>")
        print("Output: <project_dir>/gate_content.json")
        sys.exit(1)

    content_path = sys.argv[1]
    project_dir = sys.argv[2]

    if not os.path.exists(content_path):
        result = {
            "passed": False,
            "verdict": "FAIL — content.json not found",
            "fail_items": [{"check": "file_not_found", "detail": content_path}],
            "pass_items": []
        }
        os.makedirs(project_dir, exist_ok=True)
        with open(os.path.join(project_dir, "gate_content.json"), "w") as f:
            json.dump(result, f, indent=2, ensure_ascii=False)
        print(json.dumps(result, indent=2))
        sys.exit(1)

    result = check_content(content_path, project_dir)

    print(json.dumps(result, indent=2, ensure_ascii=False))

    if result["passed"]:
        print(f"\n✅ CONTENT GATE PASSED — {result['total_slides']} slides, {result['pass_count']} checks OK")
    else:
        print(f"\n❌ CONTENT GATE FAILED — {result['fail_count']} issue(s):")
        for item in result["fail_items"]:
            slide_info = f"Slide {item['slide']}" if "slide" in item else "Global"
            print(f"   {slide_info}: [{item['check']}] {item['detail']}")

    sys.exit(0 if result["passed"] else 1)


if __name__ == "__main__":
    main()