Skip to content

Commit 995ae5b

Browse files
authored
Merge pull request #2 from prithvee07/claude/contribution-docs-review-sy001c
ci: add doc-count-check workflow to catch stale skill/command counts
2 parents bbc2074 + ba4290f commit 995ae5b

2 files changed

Lines changed: 190 additions & 0 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: doc-count-check
2+
3+
# Keeps skill/command counts in docs honest against what's actually on disk.
4+
# Catches stale numbers (e.g. "71 SKILL.md files" after the bundle grew to 82)
5+
# before they merge. No external dependencies — stdlib Python only.
6+
7+
on:
8+
pull_request:
9+
paths:
10+
- "skills/**"
11+
- "commands/**"
12+
- "README.md"
13+
- "SECURITY.md"
14+
- "USAGE.md"
15+
- "INSTALL.md"
16+
- "docs/skills.md"
17+
- "docs/credits.md"
18+
- "docs/architecture.md"
19+
- "scripts/check_doc_counts.py"
20+
- ".github/workflows/doc-count-check.yml"
21+
push:
22+
branches: [main]
23+
paths:
24+
- "skills/**"
25+
- "commands/**"
26+
- "scripts/check_doc_counts.py"
27+
workflow_dispatch:
28+
29+
permissions:
30+
contents: read
31+
32+
jobs:
33+
check:
34+
name: Check doc counts
35+
runs-on: ubuntu-latest
36+
steps:
37+
- uses: actions/checkout@v4
38+
39+
- uses: actions/setup-python@v5
40+
with:
41+
python-version: "3.12"
42+
43+
- name: Verify skill/command counts in docs match disk
44+
run: python3 scripts/check_doc_counts.py

scripts/check_doc_counts.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/usr/bin/env python3
2+
"""
3+
check_doc_counts.py — verify skill/command counts in docs match what's on disk.
4+
5+
The bundle's skill/command counts have grown several times (skills: 51 -> 71 ->
6+
82; hunt-* skills: 24 -> 48 -> 57) and each time, at least one doc's hardcoded
7+
number didn't get updated along with the rest (see the 71/48/24 stale-count
8+
fixes). This script computes the real counts from skills/ and commands/ on
9+
disk, then checks every doc location that asserts one of those numbers in
10+
prose, plus two places where a count is derived by summing a table/section
11+
breakdown.
12+
13+
It deliberately does NOT try to reconcile every document's bespoke
14+
sub-categorization (e.g. docs/architecture.md's narrower "enterprise-platform"
15+
grouping vs. docs/skills.md's generated one) — only the three ground-truth
16+
totals: all skills, hunt-* skills, and slash commands.
17+
18+
If a doc's wording changes, update CHECKS to match — a "pattern not found"
19+
error is a prompt to update this script, not necessarily a doc bug.
20+
21+
Exit code 0 = all counts match, 1 = at least one mismatch.
22+
Stdlib only — no pip install needed in CI.
23+
24+
Usage:
25+
python3 scripts/check_doc_counts.py
26+
"""
27+
import os
28+
import re
29+
import sys
30+
31+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
32+
SKILLS_DIR = os.path.join(REPO, "skills")
33+
COMMANDS_DIR = os.path.join(REPO, "commands")
34+
35+
36+
def count_skills():
37+
total = 0
38+
hunt = 0
39+
for d in sorted(os.listdir(SKILLS_DIR)):
40+
if os.path.isfile(os.path.join(SKILLS_DIR, d, "SKILL.md")):
41+
total += 1
42+
if d.startswith("hunt-"):
43+
hunt += 1
44+
return total, hunt
45+
46+
47+
def count_commands():
48+
return len([f for f in os.listdir(COMMANDS_DIR) if f.endswith(".md")])
49+
50+
51+
def read(path):
52+
with open(os.path.join(REPO, path), encoding="utf-8") as fh:
53+
return fh.read()
54+
55+
56+
# (file, regex with one capture group per checked number, key or tuple of keys
57+
# into the `actual` dict, human label for error messages)
58+
CHECKS = [
59+
("README.md", r"\*\*(\d+) skills\*\* · 15 slash commands", "total", "hero line skill count"),
60+
("README.md", r"skills\*\* · (\d+) slash commands ·", "commands", "hero line command count"),
61+
("README.md", r"\| (\d+) skills \+ (\d+) slash commands \|", ("total", "commands"), "install-path comparison table"),
62+
("README.md", r"\*\*(\d+) skills\*\*, auto-loaded", "total", "'What's inside' intro"),
63+
("README.md", r"(\d+) `hunt-\*` skills curated from", "hunt", "'Hunt webapps' bullet"),
64+
("README.md", r"Also ships \*\*(\d+) slash commands\*\*", "commands", "documentation table footer"),
65+
("README.md", r"\(8 of (\d+) skills \+ (\d+) slash commands\)", ("total", "commands"), "vendored-foundation credit"),
66+
("SECURITY.md", r"installing (\d+) `SKILL\.md` files", "total", "supply-chain-trust intro"),
67+
("USAGE.md", r"the (\d+)-skill Claude-BugHunter bundle", "total", "doc intro"),
68+
("USAGE.md", r"copies (\d+) skills \+ (\d+) commands into Claude Code", ("total", "commands"), "quickstart code block"),
69+
("USAGE.md", r"(\d+) `hunt-\*` skills \+ \d+ enterprise-platform skills", "hunt", "phase-3 architecture table row"),
70+
("USAGE.md", r"### Hunt — (\d+) per-class web skills", "hunt", "skill-inventory section header"),
71+
("USAGE.md", r"installs all (\d+) skills, (\d+) commands", ("total", "commands"), "setup-for-someone-new summary"),
72+
("INSTALL.md", r"All (\d+) skills →", "total", "what-gets-installed list"),
73+
("docs/skills.md", r"All \*\*(\d+) skills\*\* in the bundle", "total", "catalog intro"),
74+
("docs/skills.md", r"## Hunt — web app vuln classes \((\d+)\)", "hunt", "catalog Hunt section header"),
75+
("docs/credits.md", r"\| \*\*Total\*\* \| (\d+) skills \+ (\d+) commands \|", ("total", "commands"), "credits breakdown total row"),
76+
("docs/architecture.md", r"(\d+) skills mapped to 6 phases", "total", "doc intro"),
77+
("docs/architecture.md", r"a (\d+)-skill `hunt-\*` sub-stack", "hunt", "doc intro"),
78+
("docs/architecture.md", r"Of (\d+) skills: \d+ original", "total", "source-breakdown sentence"),
79+
("docs/architecture.md", r"\*\*(\d+) `hunt-\*` skills\*\* \| original \+ community", "hunt", "phase-3 detail table"),
80+
]
81+
82+
83+
def check_assertions(actual):
84+
errors = []
85+
for file, pattern, key, label in CHECKS:
86+
text = read(file)
87+
m = re.search(pattern, text)
88+
if not m:
89+
errors.append(
90+
f"{file}: pattern not found for '{label}' ({pattern}) — "
91+
f"doc wording changed, update scripts/check_doc_counts.py"
92+
)
93+
continue
94+
keys = key if isinstance(key, tuple) else (key,)
95+
for i, k in enumerate(keys):
96+
got = int(m.group(i + 1))
97+
want = actual[k]
98+
if got != want:
99+
errors.append(f"{file}: '{label}' says {got} but actual {k} count is {want}")
100+
return errors
101+
102+
103+
def check_readme_table_sum(errors, actual):
104+
"""README's 'What's inside' category table must sum to the total skill count."""
105+
text = read("README.md")
106+
m = re.search(r"\| Category \| # \| Examples \|\n\|[-| ]+\n((?:\|.*\n)+)", text)
107+
if not m:
108+
errors.append("README.md: could not locate 'What's inside' category table to sum")
109+
return
110+
total = 0
111+
for row in m.group(1).strip("\n").split("\n"):
112+
cells = [c.strip() for c in row.strip("|").split("|")]
113+
if len(cells) >= 2 and cells[1].isdigit():
114+
total += int(cells[1])
115+
if total != actual["total"]:
116+
errors.append(f"README.md: 'What's inside' category table sums to {total}, actual total is {actual['total']}")
117+
118+
119+
def check_catalog_section_sum(errors, actual):
120+
"""docs/skills.md's generated section headers ('## Name (N)') must sum to the total."""
121+
text = read("docs/skills.md")
122+
counts = [int(n) for n in re.findall(r"^## .+\((\d+)\)\s*$", text, re.MULTILINE)]
123+
total = sum(counts)
124+
if total != actual["total"]:
125+
errors.append(f"docs/skills.md: section headers sum to {total}, actual total is {actual['total']}")
126+
127+
128+
def main():
129+
total, hunt = count_skills()
130+
commands = count_commands()
131+
actual = {"total": total, "hunt": hunt, "commands": commands}
132+
133+
errors = check_assertions(actual)
134+
check_readme_table_sum(errors, actual)
135+
check_catalog_section_sum(errors, actual)
136+
137+
for e in errors:
138+
print(f"::error:: {e}" if os.environ.get("GITHUB_ACTIONS") else f"ERROR {e}")
139+
140+
print(f"\nGround truth: {total} skills ({hunt} hunt-*), {commands} slash commands.")
141+
print(f"Checked {len(CHECKS)} doc assertions + 2 structural sums: {len(errors)} error(s).")
142+
return 1 if errors else 0
143+
144+
145+
if __name__ == "__main__":
146+
sys.exit(main())

0 commit comments

Comments
 (0)