claude-skills/scripts/derive_counters.py
Claude bf33d32051
fix: third-pass review items — counter logic, FDA example annotation, audit/ policy
- derive_counters.py: python_tools condition simplified to the equivalent
  parts[0] != 'scripts' (reviewer M1); dead root_scripts variable removed;
  --check still passes with identical values
- fda-consultant-specialist quick-start: 820.30 example annotated as a legacy
  checklist key mapping to ISO 13485 §7.3 (reviewer m3 — note: switching the
  example to '--section 7.3' as suggested would break; the checker's CLI keys
  are intentionally the legacy 820.x checklist indices, documented in --help)
- CLAUDE.md: audit/ directory documented as an intentional public audit
  record, distinct from the gitignored AUDIT_REPORT.md (reviewer m2)

Reviewer m1 (agents/CLAUDE.md 'engineering-team/' link) is a false positive:
agents/engineering-team/ exists as an agents subfolder containing exactly the
two linked files; check_paths.py confirms 0 unresolvable references.

https://claude.ai/code/session_019AJddAL1NADWMXsy1qNPQF
2026-06-11 15:11:00 +00:00

238 lines
8.1 KiB
Python

#!/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. 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):
# "<N> production-ready skills" (or "production-ready Claude Code skills")
# "skills across <D> domains"
# "<T> Python automation tools" or "<T> Python tools"
# "<R> reference guides"
# "<P> 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
def run_check(root: Path, derived: dict) -> int:
sources = []
readme = root / "README.md"
if readme.is_file():
sources.append(("README.md", readme.read_text(encoding="utf-8")))
claude_md = root / "CLAUDE.md"
if claude_md.is_file():
text = claude_md.read_text(encoding="utf-8")
# Restrict to the "Current Scope" line so history sections don't trip the gate.
scope_lines = [ln for ln in text.splitlines() if ln.startswith("**Current Scope:**")]
sources.append(("CLAUDE.md (Current Scope line)", "\n".join(scope_lines)))
marketplace = root / ".claude-plugin" / "marketplace.json"
if marketplace.is_file():
try:
data = json.loads(marketplace.read_text(encoding="utf-8"))
desc = data.get("metadata", {}).get("description", "")
sources.append(("marketplace.json metadata.description", desc))
except (json.JSONDecodeError, OSError) as exc:
print(f"FAIL: cannot parse marketplace.json: {exc}")
return 1
mismatches = []
for label, text in sources:
claims = extract_claims(text)
if not claims:
mismatches.append(f"{label}: no recognizable counter claims found")
continue
for key, claimed in claims.items():
actual = derived[key]
if claimed != actual:
mismatches.append(
f"{label}: claims {key}={claimed}, derived {key}={actual}"
)
if mismatches:
print("COUNTER CHECK FAILED — headline claims disagree with derived values:")
for line in mismatches:
print(f" - {line}")
print("\nRun `python3 scripts/derive_counters.py` for the ground-truth table.")
return 1
print("Counter check passed: README.md, CLAUDE.md, marketplace.json match derived values.")
return 0
def main() -> int:
parser = argparse.ArgumentParser(
description="Derive repository headline counters from the canonical tree."
)
parser.add_argument("--json", action="store_true", help="emit counters as JSON")
parser.add_argument(
"--check",
action="store_true",
help="exit 1 if README.md / CLAUDE.md / marketplace.json claims drift from derived values",
)
args = parser.parse_args()
derived = derive(REPO_ROOT)
domain_list = derived.pop("_domain_list")
if args.json:
out = dict(derived)
out["domain_list"] = domain_list
print(json.dumps(out, indent=2))
else:
width = max(len(k) for k in derived)
print("Derived repository counters (canonical tree)")
print("-" * 46)
for key, value in derived.items():
print(f"{key.ljust(width)} {value}")
print("-" * 46)
print("domains: " + ", ".join(domain_list))
if args.check:
derived["_domain_list"] = domain_list
return run_check(REPO_ROOT, derived)
return 0
if __name__ == "__main__":
sys.exit(main())