#!/usr/bin/env python3 """derive_counters.py — single source of truth for repository headline counters. Walks the canonical tree (excluding sync copies, docs site, audit workspace, and VCS/CI internals) and derives the headline numbers that README.md, CLAUDE.md, and .claude-plugin/marketplace.json claim: skills count of SKILL.md files plugins_on_disk count of **/.claude-plugin/plugin.json manifests plugins_registered entries in .claude-plugin/marketplace.json `plugins` array python_tools .py files in the canonical tree, excluding repo-root scripts/ (i.e. automation tools shipped inside skill/plugin folders) references .md files under any references/ directory agents .md files under any agents/ directory (excl. CLAUDE.md/README.md) commands .md files under any commands/ directory (excl. CLAUDE.md/README.md) domains top-level folders containing at least one SKILL.md Modes: (default) print a human-readable table --json print the derived counters as JSON --check exit 1 listing mismatches if the headline counters claimed in README.md, root CLAUDE.md ("Current Scope" line), and marketplace.json metadata.description disagree with derived values. Also validates the README "Skills Overview" per-domain table: every domain row's count must equal the SKILL.md count in its linked folder, and every on-disk domain must have a row. CI gate G3. Stdlib only. No writes ever. """ import argparse import json import re import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent # Top-level directories excluded from the canonical tree. EXCLUDED_TOP_LEVEL = { ".git", ".codex", ".codex-plugin", ".gemini", ".hermes", ".vibe", "docs", "audit", "node_modules", ".github", ".claude", } DOC_FILENAMES = {"CLAUDE.md", "README.md"} def canonical_walk(root: Path): """Yield every file in the canonical tree (excluded top-level dirs pruned).""" stack = [root] while stack: current = stack.pop() try: entries = sorted(current.iterdir()) except OSError: continue for entry in entries: if entry.is_symlink(): continue if entry.is_dir(): if current == root and entry.name in EXCLUDED_TOP_LEVEL: continue stack.append(entry) elif entry.is_file(): yield entry def derive(root: Path) -> dict: counters = { "skills": 0, "plugins_on_disk": 0, "plugins_registered": 0, "python_tools": 0, "references": 0, "agents": 0, "commands": 0, "domains": 0, } domains = set() for path in canonical_walk(root): rel = path.relative_to(root) parts = rel.parts name = path.name if name == "SKILL.md": counters["skills"] += 1 if len(parts) > 1: domains.add(parts[0]) elif name == "plugin.json" and path.parent.name == ".claude-plugin": counters["plugins_on_disk"] += 1 elif path.suffix == ".py": # Skill/plugin automation tools: every .py except repo-root scripts/. if parts[0] != "scripts": counters["python_tools"] += 1 elif path.suffix == ".md": if "references" in parts[:-1]: counters["references"] += 1 elif "agents" in parts[:-1] and name not in DOC_FILENAMES: counters["agents"] += 1 elif "commands" in parts[:-1] and name not in DOC_FILENAMES: counters["commands"] += 1 counters["domains"] = len(domains) counters["_domain_list"] = sorted(domains) marketplace = root / ".claude-plugin" / "marketplace.json" if marketplace.is_file(): try: data = json.loads(marketplace.read_text(encoding="utf-8")) counters["plugins_registered"] = len(data.get("plugins", [])) except (json.JSONDecodeError, OSError): counters["plugins_registered"] = -1 return counters # --------------------------------------------------------------------------- # --check: parse the standardized claim patterns in the three headline files. # # Standardized claim patterns (keep docs in lockstep with these): # " production-ready skills" (or "production-ready Claude Code skills") # "skills across domains" # " Python automation tools" or " Python tools" # " reference guides" # "

marketplace plugins" # --------------------------------------------------------------------------- CLAIM_PATTERNS = { "skills": re.compile(r"(\d+)\s+production-ready (?:Claude Code )?skills"), "domains": re.compile(r"skills across\s+(\d+)\s+domains"), "python_tools": re.compile(r"(\d+)\s+Python (?:automation )?tools"), "references": re.compile(r"(\d+)\s+reference guides"), "plugins_registered": re.compile(r"(\d+)\s+marketplace plugins"), } def extract_claims(text: str) -> dict: """Return {counter_name: first claimed int} for every pattern found in text.""" claims = {} for key, pattern in CLAIM_PATTERNS.items(): match = pattern.search(text) if match: claims[key] = int(match.group(1)) return claims # --------------------------------------------------------------------------- # Per-domain table validation: the README "Skills Overview" table has one row # per domain, each ending in a `[/](/)` link. Every row's count # must equal the SKILL.md count in its folder, and every on-disk domain must # have a row. This catches per-domain drift that the headline aggregates miss. # --------------------------------------------------------------------------- DOMAIN_ROW_RE = re.compile(r"^\|\s*\*\*") DOMAIN_LINK_RE = re.compile(r"\]\(([^)]+?)/?\)") def derive_per_domain(root: Path) -> dict: """Return {top_level_domain: SKILL.md count}.""" counts = {} for path in canonical_walk(root): if path.name == "SKILL.md": rel = path.relative_to(root) if len(rel.parts) > 1: counts[rel.parts[0]] = counts.get(rel.parts[0], 0) + 1 return counts def parse_domain_table(root: Path, readme_text: str) -> dict: """Parse README domain rows -> {domain_folder: claimed_count}. Only rows whose trailing link resolves to a real top-level directory are treated as domain rows, so other bold-first-cell tables (install matrices, the skills-vs-agents table) are ignored. """ table = {} for line in readme_text.splitlines(): if not DOMAIN_ROW_RE.match(line): continue cells = [c.strip() for c in line.strip().strip("|").split("|")] if len(cells) < 3: continue count = next((int(c) for c in cells[1:] if c.isdigit()), None) link = DOMAIN_LINK_RE.search(cells[-1]) if count is None or not link: continue folder = link.group(1).strip().rstrip("/").split("/")[0] if folder and (root / folder).is_dir(): table[folder] = count return table def check_domain_table(root: Path) -> list: """Return mismatch strings between README domain rows and on-disk SKILL.md counts.""" readme = root / "README.md" if not readme.is_file(): return [] per_domain = derive_per_domain(root) table = parse_domain_table(root, readme.read_text(encoding="utf-8")) problems = [] for folder, claimed in sorted(table.items()): actual = per_domain.get(folder, 0) if claimed != actual: problems.append( f"README domain table: '{folder}' row claims {claimed}, disk has {actual} skills" ) for domain, actual in sorted(per_domain.items()): if domain not in table: problems.append( f"README domain table: domain '{domain}' ({actual} skills) has no row" ) return problems # README shields.io badges of the form ![Skills](.../Skills-355-brightgreen...). # Validated separately from prose CLAIM_PATTERNS: badges are a fixed # `