← Files AkinatorARCHIVED FILE

scripts/render_routers.py

9.54 KB · Oct 3, 2026 · 06:33 UTC

↓ Download file

#!/usr/bin/env python3
"""Render every AI entry-point file from one canonical contract.

Every AI tool reads a different entry file. Each one maintained by hand becomes a
separate version of the truth, and an agent reading the stale one acts
confidently and wrongly - the most expensive failure available, because
confidence suppresses checking.

Eleven routers are only safe because they are generated. This is
rules/07-codex-pack-is-generated.md applied at scale, and
rules/04-routers-stay-thin-and-synced.md made mechanical.

Source: context/router-contract.md
Output: eleven routers, listed in ADAPTERS below.

Each adapter differs only in filename, format and its tool-specific block - the
path its skills live at, and how a user invokes one. The shared facts come from
the contract and cannot differ, which is the whole point.

Deterministic: sorted iteration, no clock, no absolute paths.

Usage:
    python scripts/render_routers.py            # dry run, report what differs
    python scripts/render_routers.py --write    # write every router
    python scripts/render_routers.py --check    # exit 1 if any router is drifted

Exit codes:
    0  every router matches the contract (or --write succeeded)
    1  drift detected
    2  the renderer could not run
"""

from __future__ import annotations

import argparse
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path

GENERATOR = "scripts/render_routers.py"
CONTRACT = "context/router-contract.md"

# Sections lifted verbatim from the contract, in the order routers present them.
SHARED_SECTIONS = (
    "Start here",
    "Before you change anything",
    "Running this repo",
    "Layout",
)


@dataclass(frozen=True)
class Adapter:
    """One AI tool's entry-point file."""

    path: str
    tool: str
    # Lines emitted under the tool-specific marker. Empty means the router
    # carries only shared facts.
    specifics: tuple[str, ...] = ()
    # Cursor wants frontmatter; everything else is plain markdown.
    frontmatter: tuple[str, ...] = ()
    heading: str = "# Akinator"


CODEX_SPECIFICS = (
    "- Akinator is ONE skill, `akinator`, read from `.agents/skills/` - repository",
    "  scope, walking up from the working directory to the repository root, then",
    "  `$HOME/.agents/skills`. `$akinator` is its only entry; its stations are",
    "  reference files inside it, never separate skills.",
    "- Always on: every prompt enters the standing contract first. Codex has no",
    "  SessionStart hook, so the installer puts the contract in `AGENTS.md`.",
    "- Install with `install.sh` (or `install.ps1` on Windows).",
    "- The Codex plugin manifest is `.codex-plugin/plugin.json`. See",
    "  `docs/compatibility.md`.",
)

CLAUDE_SPECIFICS = (
    "- The one skill loads from `skills/everything/`, the review lenses from",
    "  `agents/`, and the SessionStart hook from `hooks/hooks.json`.",
    "- Akinator is always-on from SessionStart; normal prompts require no command.",
    "- The skill is also the only command: `/akinator:everything`. There is no",
    "  `commands/` directory, so nothing else appears in the `/` menu.",
    "- Install with `install.sh` / `install.ps1`, or `claude plugin marketplace add`",
    "  then `claude plugin install akinator@akinator`.",
)

GENERIC_SPECIFICS = (
    "- Akinator is always-on for normal repository prompts; no slash command is required.",
    "- This file is one of several AI entry points in this repository. They are",
    "  all rendered from `context/router-contract.md` and state the same facts.",
    "- If your tool reads skills from a directory, point it at `.agents/skills/`",
    "  - Akinator's one skill, generated from `skills/everything/`. They cannot",
    "  diverge: a drift check fails the build if they do.",
)

ADAPTERS: tuple[Adapter, ...] = (
    Adapter("CLAUDE.md", "Claude Code", CLAUDE_SPECIFICS),
    Adapter("AGENTS.md", "Codex and the common fallback", CODEX_SPECIFICS),
    Adapter("CODEX.md", "Codex", CODEX_SPECIFICS),
    Adapter("GEMINI.md", "Gemini", GENERIC_SPECIFICS),
    Adapter("GLM.md", "GLM", GENERIC_SPECIFICS),
    Adapter("KIMI.md", "Kimi", GENERIC_SPECIFICS),
    Adapter("QWEN.md", "Qwen", GENERIC_SPECIFICS),
    Adapter("DEEPSEEK.md", "DeepSeek", GENERIC_SPECIFICS),
    Adapter("MISTRAL.md", "Mistral", GENERIC_SPECIFICS),
    Adapter(
        ".cursor/rules/akinator.mdc",
        "Cursor",
        GENERIC_SPECIFICS,
        frontmatter=(
            "---",
            "description: Akinator - the knowledge layer ships with the code",
            "alwaysApply: true",
            "---",
        ),
    ),
    Adapter(".github/copilot-instructions.md", "GitHub Copilot", GENERIC_SPECIFICS),
)


# --------------------------------------------------------------------------
# Contract parsing
# --------------------------------------------------------------------------

HEADING = re.compile(r"^##\s+(.*?)\s*$", re.MULTILINE)


def parse_contract(text: str) -> dict[str, str]:
    """Split the contract into its `##` sections, keyed by title."""
    out: dict[str, str] = {}
    heads = list(HEADING.finditer(text))
    for index, match in enumerate(heads):
        start = match.end()
        end = heads[index + 1].start() if index + 1 < len(heads) else len(text)
        out[match.group(1).strip()] = text[start:end].strip("\n")
    return out


def banner() -> str:
    """No timestamp - a clock in generated output makes every run a diff."""
    return (
        "<!--\n"
        "GENERATED FILE - DO NOT EDIT BY HAND.\n"
        f"Generated by `{GENERATOR}` from `{CONTRACT}`.\n"
        "Edit the contract, then regenerate. Every router is rendered from it,\n"
        "so editing one directly forks the truth and the drift check fails.\n"
        "See `rules/09-routers-are-rendered-from-one-contract.md`.\n"
        "-->\n"
    )


def render(adapter: Adapter, sections: dict[str, str]) -> str:
    missing = [s for s in SHARED_SECTIONS if s not in sections]
    if missing:
        raise SystemExit(
            f"{GENERATOR}: {CONTRACT} is missing required section(s): "
            f"{', '.join(missing)}"
        )

    lines: list[str] = []
    if adapter.frontmatter:
        lines.extend(adapter.frontmatter)
        lines.append("")
    lines.append(banner())
    lines.append(adapter.heading)
    lines.append("")
    lines.append(sections["Identity"].strip())
    lines.append("")

    for title in SHARED_SECTIONS:
        lines.append(f"## {title}")
        lines.append("")
        lines.append(sections[title].strip())
        lines.append("")

    if adapter.specifics:
        # Marked so the coverage check's router-sync invariant can tell an
        # intentional per-tool difference from rot.
        lines.append("<!-- akinator:tool-specific -->")
        lines.append(f"## {adapter.tool}")
        lines.append("")
        lines.extend(adapter.specifics)
        lines.append("")

    return "\n".join(lines)


# --------------------------------------------------------------------------
# Plan, diff, write
# --------------------------------------------------------------------------

def plan(repo: Path) -> dict[str, str]:
    contract = repo / CONTRACT
    if not contract.is_file():
        raise SystemExit(f"{GENERATOR}: no {CONTRACT} under {repo}")
    sections = parse_contract(contract.read_text(encoding="utf-8"))
    return {a.path: render(a, sections) for a in sorted(ADAPTERS, key=lambda a: a.path)}


def diff(repo: Path) -> list[str]:
    """Routers whose content differs from the contract."""
    drifted: list[str] = []
    for rel, content in sorted(plan(repo).items()):
        path = repo / rel
        current = path.read_text(encoding="utf-8") if path.is_file() else None
        if current != content:
            drifted.append(rel)
    return drifted


def write(repo: Path) -> list[str]:
    written: list[str] = []
    for rel, content in sorted(plan(repo).items()):
        path = repo / rel
        path.parent.mkdir(parents=True, exist_ok=True)
        current = path.read_text(encoding="utf-8") if path.is_file() else None
        if current != content:
            path.write_text(content, encoding="utf-8", newline="\n")
            written.append(rel)
    return written


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(prog="render_routers")
    parser.add_argument("root", nargs="?", default=".")
    parser.add_argument("--write", action="store_true")
    parser.add_argument("--check", action="store_true")
    parser.add_argument("--list", action="store_true", dest="do_list")
    args = parser.parse_args(argv)

    repo = Path(args.root).resolve()

    if args.do_list:
        for adapter in sorted(ADAPTERS, key=lambda a: a.path):
            marker = "tool-specific" if adapter.specifics else "shared only"
            print(f"{adapter.path:40} {adapter.tool:32} {marker}")
        print(f"\n{len(ADAPTERS)} routers rendered from {CONTRACT}")
        return 0

    if not (repo / CONTRACT).is_file():
        print(f"no {CONTRACT} under {repo}", file=sys.stderr)
        return 2

    if args.write:
        written = write(repo)
        for rel in written:
            print(f"wrote   {rel}")
        if not written:
            print(f"all {len(ADAPTERS)} routers already match the contract")
        return 0

    drifted = diff(repo)
    for rel in drifted:
        print(f"drifted {rel}")
    if drifted:
        print(
            f"\n{len(drifted)} of {len(ADAPTERS)} router(s) differ from "
            f"{CONTRACT}.\nFix with: python {GENERATOR} --write"
        )
        return 1

    print(f"all {len(ADAPTERS)} routers match the contract.")
    return 0


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

SHA-256: ef3345c4f33dd44595d36569631864c715e0dc2add5137df518f3949ce168bde