Skip to content

Commit ecd4258

Browse files
Merge pull request #2 from bjcoombs/add-ci-guardrails
Add documentation guardrails CI
2 parents 55761c5 + a248f2e commit ecd4258

17 files changed

Lines changed: 455 additions & 19 deletions

.github/workflows/docs.yml

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
name: Documentation Guardrails
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- '**.md'
8+
- '.markdownlint-cli2.jsonc'
9+
- 'package.json'
10+
- '.github/workflows/docs.yml'
11+
pull_request:
12+
branches: [main]
13+
paths:
14+
- '**.md'
15+
- '.markdownlint-cli2.jsonc'
16+
- 'package.json'
17+
- '.github/workflows/docs.yml'
18+
19+
permissions:
20+
contents: read
21+
22+
concurrency:
23+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
24+
cancel-in-progress: true
25+
26+
jobs:
27+
markdown-lint:
28+
name: Markdown Lint
29+
runs-on: ubuntu-latest
30+
steps:
31+
- uses: actions/checkout@v4
32+
33+
- uses: actions/setup-node@v4
34+
with:
35+
node-version: '20'
36+
cache: 'npm'
37+
38+
- run: npm ci
39+
40+
- name: Run markdownlint
41+
run: npm run lint:md
42+
43+
claude-md-line-limit:
44+
name: CLAUDE.md Line Limit
45+
runs-on: ubuntu-latest
46+
steps:
47+
- uses: actions/checkout@v4
48+
49+
- name: Check CLAUDE.md is under 150 lines
50+
run: |
51+
lines=$(wc -l < CLAUDE.md)
52+
echo "CLAUDE.md: $lines lines"
53+
if [ "$lines" -gt 150 ]; then
54+
echo "::error::CLAUDE.md is $lines lines (limit: 150). Move content to linked docs."
55+
exit 1
56+
fi
57+
echo "Within limit."
58+
59+
link-integrity:
60+
name: Link Integrity
61+
runs-on: ubuntu-latest
62+
steps:
63+
- uses: actions/checkout@v4
64+
65+
- name: Check internal markdown links resolve
66+
run: |
67+
errors=0
68+
69+
# Find all markdown links in docs/
70+
while IFS= read -r file; do
71+
dir=$(dirname "$file")
72+
73+
# Extract markdown links: [text](path.md) and [text](path.md#anchor)
74+
grep -oP '\[.*?\]\(\K[^)]+\.md(?:#[^)]*)?(?=\))' "$file" 2>/dev/null | while IFS= read -r link; do
75+
# Strip anchor
76+
target="${link%%#*}"
77+
78+
# Skip URLs
79+
case "$target" in
80+
http://*|https://*) continue ;;
81+
esac
82+
83+
# Resolve relative path
84+
resolved="$dir/$target"
85+
if [ ! -f "$resolved" ]; then
86+
echo "::error file=$file::Broken link: $link -> $resolved (file not found)"
87+
# Write to a temp file since we're in a subshell
88+
echo "1" >> /tmp/link_errors
89+
fi
90+
done
91+
done < <(find . -name '*.md' -not -path './node_modules/*' -not -path './.git/*')
92+
93+
if [ -f /tmp/link_errors ]; then
94+
count=$(wc -l < /tmp/link_errors)
95+
echo "::error::$count broken link(s) found"
96+
exit 1
97+
fi
98+
echo "All internal links resolve."
99+
100+
doc-reachability:
101+
name: Doc Reachability from CLAUDE.md
102+
runs-on: ubuntu-latest
103+
steps:
104+
- uses: actions/checkout@v4
105+
106+
- name: Check all docs are reachable from CLAUDE.md
107+
run: |
108+
# Collect all markdown files in docs/
109+
all_docs=$(find docs -name '*.md' | sort)
110+
111+
# Collect all files referenced from CLAUDE.md (@ syntax and markdown links)
112+
referenced=$(grep -oP '(?:@|]\()\.?/?docs/[^)\s]+\.md' CLAUDE.md | sed 's/^@//' | sed 's/^](//' | sed 's/^(//' | sort -u)
113+
114+
orphans=0
115+
for doc in $all_docs; do
116+
# Normalize path (strip leading ./)
117+
doc_clean="${doc#./}"
118+
if ! echo "$referenced" | grep -qF "$doc_clean"; then
119+
echo "::warning file=$doc_clean::Orphaned doc: $doc_clean is not referenced from CLAUDE.md"
120+
orphans=$((orphans + 1))
121+
fi
122+
done
123+
124+
if [ "$orphans" -gt 0 ]; then
125+
echo "$orphans orphaned doc(s) found. All docs should be reachable from CLAUDE.md."
126+
echo "This is a warning, not a failure - add links to CLAUDE.md or delete orphaned files."
127+
fi
128+
echo "Reachability check complete."
129+
130+
writing-conventions:
131+
name: Writing Conventions
132+
runs-on: ubuntu-latest
133+
steps:
134+
- uses: actions/checkout@v4
135+
136+
- name: Check for filler words in docs
137+
run: |
138+
# CLAUDE.md writing rule: "No filler (basically, just, simply)"
139+
warnings=0
140+
141+
while IFS= read -r file; do
142+
# Skip code blocks by filtering out lines between ``` markers
143+
# Check for filler words (case-insensitive, whole words only)
144+
results=$(sed '/^```/,/^```/d' "$file" | grep -inP '\b(basically|simply)\b' || true)
145+
if [ -n "$results" ]; then
146+
while IFS= read -r match; do
147+
echo "::warning file=$file::Filler word found: $match"
148+
warnings=$((warnings + 1))
149+
done <<< "$results"
150+
fi
151+
done < <(find docs -name '*.md')
152+
153+
if [ "$warnings" -gt 0 ]; then
154+
echo "$warnings filler word(s) found. Per CLAUDE.md: 'No filler (basically, just, simply)'"
155+
echo "This is a warning - review and remove where possible."
156+
fi
157+
echo "Writing conventions check complete."

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
package-lock.json

.markdownlint-cli2.jsonc

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// markdownlint-cli2 configuration
2+
// Aligned with CLAUDE.md markdown conventions:
3+
// Line length: Prefer 80-100, hard limit 120
4+
// Headers: # for page title, ## for main sections, ### for subsections
5+
// Code blocks: Specify language
6+
// Lists: - for bullets, numbered for sequences
7+
// No emoji in docs (exception: README.md sparingly)
8+
{
9+
"config": {
10+
"default": true,
11+
12+
// MD013/line-length
13+
// CLAUDE.md says "prefer 80-100, hard limit 120" but existing prose
14+
// exceeds 120 extensively. Disabled for now - enforce on new content
15+
// by ratcheting down over time. Code blocks and headings are checked
16+
// via other rules.
17+
"MD013": false,
18+
19+
// MD024/no-duplicate-heading - allow in different sections
20+
// Pattern docs reuse ### In Practice, ### Problem, ### Solution
21+
"MD024": {
22+
"siblings_only": true
23+
},
24+
25+
// MD033/no-inline-html - allow for README badges and diagrams
26+
"MD033": false,
27+
28+
// MD041/first-line-heading - disabled for files with front matter
29+
"MD041": false,
30+
31+
// MD040/fenced-code-language - code blocks should specify language
32+
// CLAUDE.md requires this but 42 existing blocks lack language tags
33+
// (mostly directory structure diagrams). Disabled for now - enable
34+
// after adding language tags to existing blocks (use `text` for
35+
// directory trees and plain output).
36+
"MD040": false,
37+
38+
// MD036/no-emphasis-as-heading - emphasis used as heading
39+
// Pattern docs use **bold** for inline emphasis that isn't a heading
40+
"MD036": false,
41+
42+
// MD051/link-fragments - false positives with anchor links
43+
"MD051": false,
44+
45+
// MD060/table-column-style - table formatting is cosmetic
46+
"MD060": false
47+
},
48+
49+
"ignores": [
50+
"**/node_modules/**",
51+
"**/.git/**"
52+
]
53+
}

CLAUDE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,24 @@ See @README.md for detailed project overview.
3232
## Writing Rules for This Repository
3333

3434
**When writing docs:**
35+
3536
- Language must be precise. No filler ("basically", "just", "simply").
3637
- No vague requirements ("make it fast", "clean it up").
3738
- Every claim must be specific and verifiable.
3839

3940
**When writing examples:**
41+
4042
- Working code only. No TODOs, no placeholders.
4143
- All examples must run as-is.
4244
- TypeScript and Python versions for each pattern.
4345

4446
**Doc freshness:**
47+
4548
- If you modify code in `examples/`, update the corresponding `docs/` file in the same session.
4649
- Code changes without doc updates are incomplete work.
4750

4851
**Master index requirement:**
52+
4953
- Every doc in this repo must be reachable from CLAUDE.md (this file).
5054
- This is the entry point — agents discover all project docs from here.
5155

@@ -61,18 +65,23 @@ See @README.md for detailed project overview.
6165
## Level Documentation
6266

6367
**Foundation:**
68+
6469
- @docs/L0-foundation.md — Deep modules, progressive disclosure, CLAUDE.md patterns
6570

6671
**Closed Loop Design and Verification:**
72+
6773
- @docs/L1-feedback-loops.md — Context harvesting, stack tests, full-loop assertions, sequential design
6874

6975
**Behavioral Guardrails:**
76+
7077
- @docs/L2-behavioral-guardrails.md — Skills, hooks, constitutional rules
7178

7279
**Optimization:**
80+
7381
- @docs/L3-optimization.md — Token efficiency, smart routing, guardrail middleware
7482

7583
**Standards & Measurement:**
84+
7685
- @docs/L4-standards-measurement.md — Evidence-based claims, drift detection, metrics
7786

7887
## Cross-Cutting Guides

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ docs/references/ # Case study and further reading
7373
## Contributing
7474

7575
This is a living pattern library. Contributions welcome:
76+
7677
- **New patterns** that extend or challenge the existing framework
7778
- **Real-world examples** from different domains (the current examples lean toward ecommerce and trading)
7879
- **Corrections** when a pattern doesn't match your experience — document the exception

0 commit comments

Comments
 (0)