← Files DesignlyARCHIVED FILE

skills/creative-director/scripts/generate_links.py

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

↓ Download file

# /// script
# requires-python = ">=3.11"
# dependencies = ["pyyaml"]
# ///

import re
import sys
from pathlib import Path

import yaml

CARDS_DIR = Path(__file__).parent.parent / "references" / "legendary-campaigns" / "cards"
MOC_PATTERN_PATH = Path(__file__).parent.parent / "references" / "legendary-campaigns" / "MOC-pattern.md"


def parse_frontmatter(text: str) -> dict | None:
    if not text.startswith("---"):
        return None
    end = text.find("\n---", 3)
    if end == -1:
        return None
    yaml_block = text[3:end].strip()
    try:
        return yaml.safe_load(yaml_block) or {}
    except yaml.YAMLError:
        return None


def build_related_section(card: dict, all_cards: list[dict]) -> str:
    lines: list[str] = []

    patterns = card.get("pattern", [])
    if not patterns:
        return "## Related\n"

    first_pattern = str(patterns[0]).lower()
    lines.append(f"- Pattern: [[../MOC-pattern.md#{first_pattern}]]")

    siblings = [
        c for c in all_cards
        if c["id"] != card["id"]
        and patterns[0] in c.get("pattern", [])
    ]
    siblings.sort(key=lambda c: c.get("quality_score", 0), reverse=True)
    for sibling in siblings[:2]:
        lines.append(f"- Sibling: [[{sibling['id']}.md|{sibling['title']}]]")

    if card.get("emotion_tier") == 3:
        current_emotions = set(card.get("emotion", []))
        candidates = [
            c for c in all_cards
            if c["id"] != card["id"]
            and c.get("emotion_tier") == 3
            and set(c.get("emotion", [])) & current_emotions
        ]
        candidates.sort(key=lambda c: c.get("quality_score", 0), reverse=True)
        if candidates:
            top = candidates[0]
            lines.append(f"- Emotion match: [[{top['id']}.md|{top['title']}]]")

    return "## Related\n" + "\n".join(lines) + "\n"


def update_card(path: Path, related_text: str) -> bool:
    original = path.read_text(encoding="utf-8")

    # Find ## Related section and replace up to next ## heading or EOF
    pattern = re.compile(r"^## Related\b.*?(?=^## |\Z)", re.MULTILINE | re.DOTALL)
    match = pattern.search(original)

    if match:
        updated = original[: match.start()] + related_text + original[match.end():]
    else:
        # Append to end, ensuring single trailing newline before section
        updated = original.rstrip("\n") + "\n\n" + related_text

    path.write_text(updated, encoding="utf-8")
    return True


def main() -> None:
    if not CARDS_DIR.exists():
        print(f"Cards directory not found: {CARDS_DIR}", file=sys.stderr)
        sys.exit(1)

    md_files = sorted(CARDS_DIR.glob("*.md"))

    all_cards: list[dict] = []
    skipped: list[str] = []
    errors: list[str] = []

    for path in md_files:
        text = path.read_text(encoding="utf-8")
        fm = parse_frontmatter(text)
        if fm is None:
            print(f"WARNING: no frontmatter — skipping {path.name}", file=sys.stderr)
            skipped.append(path.name)
            continue

        card_id = fm.get("id") or path.stem
        title = fm.get("title") or path.stem
        patterns = fm.get("pattern") or []

        if not patterns:
            print(f"WARNING: empty pattern field — skipping {path.name}", file=sys.stderr)
            skipped.append(path.name)
            continue

        all_cards.append({
            "id": str(card_id),
            "title": str(title),
            "pattern": patterns if isinstance(patterns, list) else [patterns],
            "emotion": fm.get("emotion") or [],
            "emotion_tier": fm.get("emotion_tier"),
            "quality_score": fm.get("quality_score") or 0,
            "_path": path,
        })

    updated_count = 0
    for card in all_cards:
        path: Path = card["_path"]
        try:
            related_text = build_related_section(card, all_cards)
            update_card(path, related_text)
            updated_count += 1
        except Exception as exc:
            print(f"ERROR: {path.name} — {exc}", file=sys.stderr)
            errors.append(path.name)

    print(f"Updated {updated_count} cards ({len(skipped)} skipped, {len(errors)} errors)")


if __name__ == "__main__":
    main()

SHA-256: 2b09eb805fbb72de19aa6bfbad58f4c3523da12883e7f7a10b6af9f3fe1c1814