feat(budget): count the skill listing budget with make budget

scripts/budget.py holds the listing limits (1500 words in total, 40 per skill) and the one way to count a description, prints the total and the longest descriptions, reads the staged versions with --index, and exits 1 over budget. make budget [INDEX=1] runs it; test_lock.py takes its limits and counting from the script instead of its own copy. It replaces the one-liner that was run by hand a dozen times on 2026-11-03.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-03 17:57:08 +01:00
1 parent 66e21dcd5c
commit fc7d238d06
3 files changed
+99 -11

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Count every first-party skill description against the listing budget.
Args:
--repo: skills repository root.
--index: count the staged versions in the git index instead of the files.
--top: how many of the longest descriptions to list.
Exit codes:
0 within budget, 1 over budget, 2 usage error.
"""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
LISTING_WORDS = 1500
DESCRIPTION_WORDS = 40
def words(skill_md: str) -> int:
frontmatter = skill_md.split("---", 2)[1]
return len(frontmatter.split("description:", 1)[1].split()) - 1
def git(repo: Path, *args: str) -> str:
result = subprocess.run(
["git", *args], cwd=repo, capture_output=True, text=True, check=False
)
if result.returncode != 0:
print(f"budget: {result.stderr.strip()}", file=sys.stderr)
sys.exit(2)
return result.stdout
def counts(repo: Path, index: bool) -> dict[str, int]:
if index:
paths = git(repo, "ls-files", "skills/*/SKILL.md").split()
return {Path(p).parent.name: words(git(repo, "show", f":{p}")) for p in paths}
return {
p.parent.name: words(p.read_text(encoding="utf-8"))
for p in sorted((repo / "skills").glob("*/SKILL.md"))
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--repo", required=True)
parser.add_argument("--index", action="store_true")
parser.add_argument("--top", type=int, required=True)
args = parser.parse_args()
repo = Path(args.repo)
if not (repo / "skills").is_dir():
print(f"budget: {repo} has no skills/ directory", file=sys.stderr)
return 2
found = counts(repo, args.index)
total = sum(found.values())
over = sorted(name for name, n in found.items() if n > DESCRIPTION_WORDS)
print(f"total {total}/{LISTING_WORDS} words in {len(found)} skills")
for name, n in sorted(found.items(), key=lambda item: -item[1])[: args.top]:
print(f"{n}\t{name}")
for name in over:
print(f"over {DESCRIPTION_WORDS}: {name}")
return 1 if total > LISTING_WORDS or over else 0
if __name__ == "__main__":
sys.exit(main())