#!/usr/bin/env python3
"""Check: the roles your agent actually loads point at the style page.

    style-pointed <directory>

How a reply is written lives in one page, `formwork/style.md`. It is not
copied into twenty eight role files. The generator writes a pointer to it into
every file it produces, and this check is the reason that pointer can be
trusted.

CATCHES  The pointer quietly leaving. Delete the two lines in the generator
         that write it, run the generator again, and every generated role
         loses the pointer at once. `generated-current` compares what is on
         disk against what the source produces, so both halves change together
         and it sees nothing wrong. The gate stayed green while the style page
         became a document nothing led to.

         Also a project that has `formwork/style.md` and a generated tree that
         predates it, which is the same silence for a duller reason.

WHAT IT CANNOT DO

Tell whether the page is read, or followed. Nothing can. It checks that the
road exists, not that anybody drove down it.

Notice roles that were **deleted** rather than fixed. Remove the generated
files and this check goes green, because it has nothing left to look at. To
catch that it would have to know which roles ought to exist, which means
carrying a second copy of the generator, and a checker built out of the thing
it checks is not a check. `generated-current` covers the opposite case, a
generated file with no source behind it.

Exit status:
    0   every generated role points at the page, or there is nothing to check
    1   at least one does not, and it is named
    2   the check could not run
"""
import os
import sys

PAGE = os.path.join("formwork", "style.md")
# The whole path, not "style.md". A role whose only mention was the word
# "lifestyle.md" in an unrelated description satisfied the old substring test.
NEEDLE = "formwork/style.md"

# What marks a file as one the generator wrote. Without this, a README sitting
# in .claude/agents was treated as a role and failed, with a printed remedy
# that could never fix it, because the generator does not own that file.
GENERATED = "GENERATED FROM"

# Where each runtime's generated roles live. The same list the generator
# writes to, and guard-wired reads from.
TREES = [
    (os.path.join(".claude", "agents"), ".md"),
    (os.path.join(".gemini", "agents"), ".md"),
    (os.path.join(".codex", "agents"), ".toml"),
]

SKIP_DIRS = {".git", "node_modules", "__pycache__", ".venv"}


# Paths the runner has told this check to stay out of. The kit's own fixtures
# are wrong on purpose. Resolved against the directory being checked, not
# against wherever the person happened to be standing: a relative exclude used
# to mean two different places depending on the caller.
def excluded_paths(root):
    out = []
    for p in os.environ.get("FORMWORK_EXCLUDE", "").split(os.pathsep):
        if not p:
            continue
        out.append(os.path.realpath(p if os.path.isabs(p)
                                    else os.path.join(root, p)))
    return out


def is_excluded(path, excludes):
    a = os.path.realpath(path)
    return any(a == e or a.startswith(e + os.sep) for e in excludes)


def generated(root):
    """Every generated role file, with the runtime folder it came from.

    Walked, not listed. Claude Code loads agents from subdirectories, and a
    flat listing reported "nothing generated here yet" with an un-pointed role
    sitting one folder down. A leading dot was skipped for the same reason and
    is now read like anything else.
    """
    out = []
    excludes = excluded_paths(root)
    for rel, ending in TREES:
        d = os.path.join(root, rel)
        if not os.path.isdir(d) or is_excluded(d, excludes):
            continue
        for base, dirs, files in os.walk(d):
            dirs.sort()
            for fn in sorted(files):
                full = os.path.join(base, fn)
                if not fn.endswith(ending) or is_excluded(full, excludes):
                    continue
                out.append((os.path.relpath(full, root), full))
    return out


def main(argv):
    args = [a for a in argv[1:] if not a.startswith("--")]
    if not args:
        print("usage: style-pointed <directory>", file=sys.stderr)
        return 2
    root = args[0]
    if not os.path.isdir(root):
        print("ERROR: not a directory: %s" % root, file=sys.stderr)
        return 2

    if not os.path.isfile(os.path.join(root, PAGE)):
        print("no %s here, nothing to point at" % PAGE)
        return 0

    files = generated(root)
    if not files:
        print("nothing generated here yet, nothing to check")
        return 0

    missing, looked = [], 0
    for rel, full in files:
        try:
            text = open(full, encoding="utf-8", errors="replace").read()
        except OSError as e:
            print("ERROR: cannot read %s: %s" % (rel, e), file=sys.stderr)
            return 2
        # Only files this kit generated. Anything else in that folder is
        # somebody's own, and running the generator would never change it.
        if GENERATED not in text:
            continue
        looked += 1
        # A generated role is allowed to say it in any wording. What is not
        # allowed is never naming the page at all.
        if NEEDLE not in text:
            missing.append(rel)

    if not looked:
        print("no generated role files found, nothing to check")
        return 0

    if missing:
        print("%d of %d generated role(s) do not name %s"
              % (len(missing), looked, PAGE))
        for rel in missing[:10]:
            # A newline in a filename used to break the list into more lines
            # than there were findings.
            print("  %s" % rel.replace("\n", "\\n"))
        if len(missing) > 10:
            print("  and %d more" % (len(missing) - 10))
        print("  Run formwork roles. If the generator stopped writing the "
              "pointer, that is the bug.")
        return 1

    print("%d generated role(s), every one names %s" % (looked, PAGE))
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))
