opencode-skills-collection 3.0.46 → 3.0.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled-skills/.antigravity-install-manifest.json +15 -1
- package/bundled-skills/2slides-ppt-generator/SKILL.md +1 -1
- package/bundled-skills/2slides-ppt-generator/scripts/create_pdf_slides.py +2 -1
- package/bundled-skills/2slides-ppt-generator/scripts/generate_narration.py +2 -1
- package/bundled-skills/2slides-ppt-generator/scripts/generate_slides.py +13 -7
- package/bundled-skills/accint-solve/SKILL.md +205 -0
- package/bundled-skills/android-cli/SKILL.md +239 -0
- package/bundled-skills/android-cli/references/interact.md +83 -0
- package/bundled-skills/android-cli/references/journeys.md +105 -0
- package/bundled-skills/android-dev/references/hybrid.md +7 -4
- package/bundled-skills/android-dev/references/react-native.md +5 -2
- package/bundled-skills/atlas-contract/SKILL.md +4 -4
- package/bundled-skills/atlas-ledger/SKILL.md +10 -7
- package/bundled-skills/bun-development/SKILL.md +1 -1
- package/bundled-skills/cloud-penetration-testing/SKILL.md +1 -1
- package/bundled-skills/codebase-to-wordpress-converter/SKILL.md +1 -0
- package/bundled-skills/codex-fable5/SKILL.md +154 -0
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +4 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/dos-verify-done-claims/SKILL.md +173 -0
- package/bundled-skills/ecl-harness-engineer/LICENSE +21 -0
- package/bundled-skills/ecl-harness-engineer/SKILL.md +714 -0
- package/bundled-skills/ecl-harness-engineer/agents/analyzer.md +119 -0
- package/bundled-skills/ecl-harness-engineer/agents/auditor.md +212 -0
- package/bundled-skills/ecl-harness-engineer/agents/creator-config.md +343 -0
- package/bundled-skills/ecl-harness-engineer/agents/creator-docs.md +201 -0
- package/bundled-skills/ecl-harness-engineer/agents/creator-linters.md +123 -0
- package/bundled-skills/ecl-harness-engineer/references/adapters/adapter-schema.md +204 -0
- package/bundled-skills/ecl-harness-engineer/references/adapters/generic.md +156 -0
- package/bundled-skills/ecl-harness-engineer/references/adapters/go.md +212 -0
- package/bundled-skills/ecl-harness-engineer/references/adapters/java.md +205 -0
- package/bundled-skills/ecl-harness-engineer/references/adapters/python.md +225 -0
- package/bundled-skills/ecl-harness-engineer/references/adapters/rust.md +220 -0
- package/bundled-skills/ecl-harness-engineer/references/adapters/typescript.md +245 -0
- package/bundled-skills/ecl-harness-engineer/references/architecture-diagrams.md +420 -0
- package/bundled-skills/ecl-harness-engineer/references/audit-templates.md +649 -0
- package/bundled-skills/ecl-harness-engineer/references/capability-registry.md +485 -0
- package/bundled-skills/ecl-harness-engineer/references/darwin-eval-prompts.md +373 -0
- package/bundled-skills/ecl-harness-engineer/references/documentation-templates.md +741 -0
- package/bundled-skills/ecl-harness-engineer/references/durability-patterns.md +423 -0
- package/bundled-skills/ecl-harness-engineer/references/ecl-harness.md +1431 -0
- package/bundled-skills/ecl-harness-engineer/references/environment-config-guide.md +534 -0
- package/bundled-skills/ecl-harness-engineer/references/environment-detection-guide.md +751 -0
- package/bundled-skills/ecl-harness-engineer/references/eval-templates.md +377 -0
- package/bundled-skills/ecl-harness-engineer/references/gc-templates.md +798 -0
- package/bundled-skills/ecl-harness-engineer/references/greenfield-templates.md +1385 -0
- package/bundled-skills/ecl-harness-engineer/references/linter-templates.md +448 -0
- package/bundled-skills/ecl-harness-engineer/references/observability-templates.md +315 -0
- package/bundled-skills/efficient-web-research/SKILL.md +320 -0
- package/bundled-skills/environment-setup-guide/SKILL.md +2 -2
- package/bundled-skills/evolution/SKILL.md +1 -1
- package/bundled-skills/gitops-workflow/SKILL.md +1 -1
- package/bundled-skills/linkerd-patterns/SKILL.md +1 -1
- package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package-lock.json +504 -1317
- package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package.json +2 -2
- package/bundled-skills/lovable-cleanup/SKILL.md +416 -0
- package/bundled-skills/monopoly/SKILL.md +397 -0
- package/bundled-skills/monopoly/patterns/SKILL.md +331 -0
- package/bundled-skills/monopoly/scale-benchmarks/SKILL.md +174 -0
- package/bundled-skills/monopoly/security-checklist/SKILL.md +69 -0
- package/bundled-skills/monopoly/tech-matrix/SKILL.md +268 -0
- package/bundled-skills/pagespeed-enhancer/SKILL.md +579 -0
- package/bundled-skills/polis-protocol/SKILL.md +6 -3
- package/bundled-skills/sharp-coder/SKILL.md +131 -0
- package/bundled-skills/unship/SKILL.md +11 -5
- package/bundled-skills/uv-package-manager/resources/implementation-playbook.md +1 -1
- package/bundled-skills/varlock/SKILL.md +2 -2
- package/package.json +1 -1
- package/skills_index.json +314 -4
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
# Garbage Collection Templates
|
|
2
|
+
|
|
3
|
+
> "Documentation entropy compounds. A single outdated comment erodes trust in all docs."
|
|
4
|
+
|
|
5
|
+
Templates for automated documentation hygiene — detecting stale docs, broken links, interface drift,
|
|
6
|
+
and other forms of documentation rot before they erode agent trust.
|
|
7
|
+
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
- [Why Garbage Collection](#why-garbage-collection)
|
|
11
|
+
- [Stale Doc Detector](#1-stale-doc-detector)
|
|
12
|
+
- [Broken Link Checker](#2-broken-link-checker)
|
|
13
|
+
- [Interface Drift Detector](#3-interface-drift-detector)
|
|
14
|
+
- [Unified GC Runner](#4-unified-gc-runner)
|
|
15
|
+
- [CI Integration](#5-ci-integration)
|
|
16
|
+
- [Scheduling Guide](#6-scheduling-guide)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Why Garbage Collection
|
|
21
|
+
|
|
22
|
+
Every outdated doc is a trap for agents. When an agent reads stale architecture docs, it makes
|
|
23
|
+
decisions based on fiction — then validation catches the mismatch 30 tool calls later, wasting
|
|
24
|
+
tokens and time. Proactive garbage collection prevents this by catching documentation rot
|
|
25
|
+
at the source.
|
|
26
|
+
|
|
27
|
+
| Problem | Impact on Agents | Detection Method |
|
|
28
|
+
|---------|-----------------|------------------|
|
|
29
|
+
| Stale doc (code changed, doc didn't) | Makes decisions on outdated architecture | Compare timestamps |
|
|
30
|
+
| Broken link (target moved/deleted) | Dead-end navigation, wasted context | Crawl internal links |
|
|
31
|
+
| Interface drift (doc says X, code does Y) | Writes code against wrong interface | Parse code vs doc |
|
|
32
|
+
| Orphan doc (no code references it) | Clutters context, wastes tokens | Reverse reference check |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 1. Stale Doc Detector
|
|
37
|
+
|
|
38
|
+
Compares documentation timestamps against the code they describe. If the code changed significantly
|
|
39
|
+
after the doc was last updated, the doc is potentially stale.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
#!/usr/bin/env python3
|
|
43
|
+
"""
|
|
44
|
+
scripts/gc-stale-docs.py
|
|
45
|
+
|
|
46
|
+
Detects documentation that may be stale relative to the code it describes.
|
|
47
|
+
|
|
48
|
+
Usage:
|
|
49
|
+
python3 scripts/gc-stale-docs.py .
|
|
50
|
+
python3 scripts/gc-stale-docs.py . --json
|
|
51
|
+
python3 scripts/gc-stale-docs.py . --threshold 30 # days
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
import argparse
|
|
55
|
+
import json
|
|
56
|
+
import os
|
|
57
|
+
import re
|
|
58
|
+
import subprocess
|
|
59
|
+
import sys
|
|
60
|
+
from datetime import datetime, timedelta
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
from typing import Dict, List, Optional, Tuple
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_git_last_modified(filepath: str) -> Optional[datetime]:
|
|
66
|
+
"""Get the last git commit date for a file."""
|
|
67
|
+
try:
|
|
68
|
+
result = subprocess.run(
|
|
69
|
+
["git", "log", "-1", "--format=%aI", "--", filepath],
|
|
70
|
+
capture_output=True, text=True, timeout=10
|
|
71
|
+
)
|
|
72
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
73
|
+
return datetime.fromisoformat(result.stdout.strip())
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def extract_code_references(doc_path: str) -> List[str]:
|
|
80
|
+
"""Extract file paths referenced in a markdown document.
|
|
81
|
+
|
|
82
|
+
Looks for patterns like:
|
|
83
|
+
- `internal/core/service.go`
|
|
84
|
+
- `internal/core/service.go:25-48`
|
|
85
|
+
- [link text](../internal/core/service.go)
|
|
86
|
+
- Sources: [`internal/types/token.go:10-15`]()
|
|
87
|
+
"""
|
|
88
|
+
references = []
|
|
89
|
+
try:
|
|
90
|
+
content = Path(doc_path).read_text()
|
|
91
|
+
except Exception:
|
|
92
|
+
return references
|
|
93
|
+
|
|
94
|
+
# Match backtick-quoted file paths
|
|
95
|
+
backtick_pattern = r'`([a-zA-Z0-9_./\-]+\.(go|ts|tsx|js|jsx|py|rs))(?::\d+(?:-\d+)?)?`'
|
|
96
|
+
references.extend(m[0] for m in re.findall(backtick_pattern, content))
|
|
97
|
+
|
|
98
|
+
# Match markdown link targets
|
|
99
|
+
link_pattern = r'\]\((?:\.\.?/)*([a-zA-Z0-9_./\-]+\.(go|ts|tsx|js|jsx|py|rs))(?::\d+(?:-\d+)?)?\)'
|
|
100
|
+
references.extend(m[0] for m in re.findall(link_pattern, content))
|
|
101
|
+
|
|
102
|
+
# Deduplicate, strip line numbers
|
|
103
|
+
cleaned = set()
|
|
104
|
+
for ref in references:
|
|
105
|
+
clean = re.sub(r':\d+(-\d+)?$', '', ref)
|
|
106
|
+
cleaned.add(clean)
|
|
107
|
+
return list(cleaned)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def find_doc_to_code_mapping(project_root: Path) -> Dict[str, List[str]]:
|
|
111
|
+
"""Map each doc file to the code files it references."""
|
|
112
|
+
mapping = {}
|
|
113
|
+
doc_dirs = ["docs", "docs/design-docs", "docs/references"]
|
|
114
|
+
doc_files = []
|
|
115
|
+
|
|
116
|
+
for d in doc_dirs:
|
|
117
|
+
doc_dir = project_root / d
|
|
118
|
+
if doc_dir.exists():
|
|
119
|
+
for f in doc_dir.glob("*.md"):
|
|
120
|
+
doc_files.append(f)
|
|
121
|
+
|
|
122
|
+
# Also check AGENTS.md
|
|
123
|
+
agents_md = project_root / "AGENTS.md"
|
|
124
|
+
if agents_md.exists():
|
|
125
|
+
doc_files.append(agents_md)
|
|
126
|
+
|
|
127
|
+
for doc_file in doc_files:
|
|
128
|
+
refs = extract_code_references(str(doc_file))
|
|
129
|
+
if refs:
|
|
130
|
+
rel_doc = str(doc_file.relative_to(project_root))
|
|
131
|
+
mapping[rel_doc] = refs
|
|
132
|
+
|
|
133
|
+
return mapping
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def check_staleness(
|
|
137
|
+
project_root: Path,
|
|
138
|
+
threshold_days: int = 14
|
|
139
|
+
) -> List[Dict]:
|
|
140
|
+
"""Check all docs for staleness relative to referenced code."""
|
|
141
|
+
mapping = find_doc_to_code_mapping(project_root)
|
|
142
|
+
threshold = timedelta(days=threshold_days)
|
|
143
|
+
findings = []
|
|
144
|
+
|
|
145
|
+
for doc_path, code_refs in mapping.items():
|
|
146
|
+
doc_modified = get_git_last_modified(doc_path)
|
|
147
|
+
if not doc_modified:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
stale_refs = []
|
|
151
|
+
for code_ref in code_refs:
|
|
152
|
+
code_path = str(project_root / code_ref)
|
|
153
|
+
if not os.path.exists(code_path):
|
|
154
|
+
stale_refs.append({
|
|
155
|
+
"file": code_ref,
|
|
156
|
+
"reason": "file_deleted",
|
|
157
|
+
"detail": f"Referenced file no longer exists"
|
|
158
|
+
})
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
code_modified = get_git_last_modified(code_ref)
|
|
162
|
+
if code_modified and code_modified > doc_modified + threshold:
|
|
163
|
+
days_behind = (code_modified - doc_modified).days
|
|
164
|
+
stale_refs.append({
|
|
165
|
+
"file": code_ref,
|
|
166
|
+
"reason": "code_newer",
|
|
167
|
+
"detail": f"Code updated {days_behind} days after doc",
|
|
168
|
+
"code_date": code_modified.isoformat(),
|
|
169
|
+
"doc_date": doc_modified.isoformat()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
if stale_refs:
|
|
173
|
+
findings.append({
|
|
174
|
+
"doc": doc_path,
|
|
175
|
+
"doc_last_modified": doc_modified.isoformat(),
|
|
176
|
+
"stale_references": stale_refs,
|
|
177
|
+
"severity": "high" if any(r["reason"] == "file_deleted" for r in stale_refs) else "medium"
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
return findings
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def main():
|
|
184
|
+
parser = argparse.ArgumentParser(description="Detect stale documentation")
|
|
185
|
+
parser.add_argument("project_root", help="Project root directory")
|
|
186
|
+
parser.add_argument("--threshold", type=int, default=14,
|
|
187
|
+
help="Days threshold for staleness (default: 14)")
|
|
188
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
189
|
+
args = parser.parse_args()
|
|
190
|
+
|
|
191
|
+
project_root = Path(args.project_root).resolve()
|
|
192
|
+
findings = check_staleness(project_root, args.threshold)
|
|
193
|
+
|
|
194
|
+
if args.json:
|
|
195
|
+
print(json.dumps({"stale_docs": findings, "threshold_days": args.threshold}, indent=2))
|
|
196
|
+
else:
|
|
197
|
+
if not findings:
|
|
198
|
+
print("✓ No stale documentation detected")
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
print(f"⚠ Found {len(findings)} potentially stale document(s):\n")
|
|
202
|
+
for f in findings:
|
|
203
|
+
severity_icon = "🔴" if f["severity"] == "high" else "🟡"
|
|
204
|
+
print(f"{severity_icon} {f['doc']} (last updated: {f['doc_last_modified'][:10]})")
|
|
205
|
+
for ref in f["stale_references"]:
|
|
206
|
+
if ref["reason"] == "file_deleted":
|
|
207
|
+
print(f" ✗ {ref['file']} — DELETED (reference is broken)")
|
|
208
|
+
else:
|
|
209
|
+
print(f" ✗ {ref['file']} — code updated {ref['detail']}")
|
|
210
|
+
print()
|
|
211
|
+
|
|
212
|
+
sys.exit(1 if findings else 0)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
main()
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## 2. Broken Link Checker
|
|
222
|
+
|
|
223
|
+
Validates all internal markdown links (to other docs, to code files, to anchors) are still valid.
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
#!/usr/bin/env python3
|
|
227
|
+
"""
|
|
228
|
+
scripts/gc-broken-links.py
|
|
229
|
+
|
|
230
|
+
Checks all markdown files for broken internal links.
|
|
231
|
+
|
|
232
|
+
Usage:
|
|
233
|
+
python3 scripts/gc-broken-links.py .
|
|
234
|
+
python3 scripts/gc-broken-links.py . --json
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
import argparse
|
|
238
|
+
import json
|
|
239
|
+
import re
|
|
240
|
+
import sys
|
|
241
|
+
from pathlib import Path
|
|
242
|
+
from typing import Dict, List
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def find_markdown_files(root: Path) -> List[Path]:
|
|
246
|
+
"""Find all markdown files in the project."""
|
|
247
|
+
md_files = []
|
|
248
|
+
for pattern in ["*.md", "docs/**/*.md"]:
|
|
249
|
+
md_files.extend(root.glob(pattern))
|
|
250
|
+
return sorted(set(md_files))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def extract_links(md_path: Path) -> List[Dict]:
|
|
254
|
+
"""Extract all internal links from a markdown file."""
|
|
255
|
+
content = md_path.read_text()
|
|
256
|
+
links = []
|
|
257
|
+
|
|
258
|
+
# [text](target) — skip external URLs
|
|
259
|
+
link_pattern = r'\[([^\]]*)\]\(([^)]+)\)'
|
|
260
|
+
for match in re.finditer(link_pattern, content):
|
|
261
|
+
target = match.group(2)
|
|
262
|
+
if target.startswith(("http://", "https://", "mailto:")):
|
|
263
|
+
continue
|
|
264
|
+
line_num = content[:match.start()].count('\n') + 1
|
|
265
|
+
links.append({
|
|
266
|
+
"text": match.group(1),
|
|
267
|
+
"target": target,
|
|
268
|
+
"line": line_num
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
return links
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def resolve_link(md_path: Path, target: str, project_root: Path) -> Dict:
|
|
275
|
+
"""Check if a link target is valid. Returns status dict."""
|
|
276
|
+
# Split anchor from path
|
|
277
|
+
if "#" in target:
|
|
278
|
+
path_part, anchor = target.rsplit("#", 1)
|
|
279
|
+
else:
|
|
280
|
+
path_part, anchor = target, None
|
|
281
|
+
|
|
282
|
+
if not path_part:
|
|
283
|
+
# Pure anchor link (#section) — check current file
|
|
284
|
+
if anchor:
|
|
285
|
+
return check_anchor(md_path, anchor)
|
|
286
|
+
return {"valid": True}
|
|
287
|
+
|
|
288
|
+
# Resolve relative path from the markdown file's directory
|
|
289
|
+
resolved = (md_path.parent / path_part).resolve()
|
|
290
|
+
|
|
291
|
+
if not resolved.exists():
|
|
292
|
+
return {
|
|
293
|
+
"valid": False,
|
|
294
|
+
"reason": "file_not_found",
|
|
295
|
+
"resolved_path": str(resolved.relative_to(project_root))
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if anchor and resolved.suffix == ".md":
|
|
299
|
+
return check_anchor(resolved, anchor)
|
|
300
|
+
|
|
301
|
+
return {"valid": True}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def check_anchor(md_path: Path, anchor: str) -> Dict:
|
|
305
|
+
"""Check if an anchor exists in a markdown file."""
|
|
306
|
+
try:
|
|
307
|
+
content = md_path.read_text()
|
|
308
|
+
except Exception:
|
|
309
|
+
return {"valid": False, "reason": "cannot_read_file"}
|
|
310
|
+
|
|
311
|
+
# Generate anchors from headings (GitHub-style)
|
|
312
|
+
headings = re.findall(r'^#{1,6}\s+(.+)$', content, re.MULTILINE)
|
|
313
|
+
anchors = set()
|
|
314
|
+
for h in headings:
|
|
315
|
+
slug = re.sub(r'[^\w\s-]', '', h.lower())
|
|
316
|
+
slug = re.sub(r'[\s]+', '-', slug).strip('-')
|
|
317
|
+
anchors.add(slug)
|
|
318
|
+
|
|
319
|
+
if anchor.lower() in anchors:
|
|
320
|
+
return {"valid": True}
|
|
321
|
+
return {
|
|
322
|
+
"valid": False,
|
|
323
|
+
"reason": "anchor_not_found",
|
|
324
|
+
"anchor": anchor,
|
|
325
|
+
"available_anchors": sorted(anchors)[:10]
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def check_all_links(project_root: Path) -> List[Dict]:
|
|
330
|
+
"""Check all internal links in all markdown files."""
|
|
331
|
+
findings = []
|
|
332
|
+
md_files = find_markdown_files(project_root)
|
|
333
|
+
|
|
334
|
+
for md_file in md_files:
|
|
335
|
+
links = extract_links(md_file)
|
|
336
|
+
broken = []
|
|
337
|
+
for link in links:
|
|
338
|
+
result = resolve_link(md_file, link["target"], project_root)
|
|
339
|
+
if not result["valid"]:
|
|
340
|
+
broken.append({**link, **result})
|
|
341
|
+
|
|
342
|
+
if broken:
|
|
343
|
+
findings.append({
|
|
344
|
+
"file": str(md_file.relative_to(project_root)),
|
|
345
|
+
"broken_links": broken
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
return findings
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def main():
|
|
352
|
+
parser = argparse.ArgumentParser(description="Check for broken internal links")
|
|
353
|
+
parser.add_argument("project_root", help="Project root directory")
|
|
354
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
355
|
+
args = parser.parse_args()
|
|
356
|
+
|
|
357
|
+
project_root = Path(args.project_root).resolve()
|
|
358
|
+
findings = check_all_links(project_root)
|
|
359
|
+
|
|
360
|
+
if args.json:
|
|
361
|
+
print(json.dumps({"broken_links": findings}, indent=2))
|
|
362
|
+
else:
|
|
363
|
+
if not findings:
|
|
364
|
+
print("✓ No broken internal links found")
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
total = sum(len(f["broken_links"]) for f in findings)
|
|
368
|
+
print(f"✗ Found {total} broken link(s) in {len(findings)} file(s):\n")
|
|
369
|
+
for f in findings:
|
|
370
|
+
print(f" {f['file']}:")
|
|
371
|
+
for link in f["broken_links"]:
|
|
372
|
+
print(f" Line {link['line']}: [{link['text']}]({link['target']})")
|
|
373
|
+
print(f" → {link['reason']}")
|
|
374
|
+
print()
|
|
375
|
+
|
|
376
|
+
sys.exit(1 if findings else 0)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
if __name__ == "__main__":
|
|
380
|
+
main()
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## 3. Interface Drift Detector
|
|
386
|
+
|
|
387
|
+
Detects when documented interfaces no longer match the actual code — the most insidious form
|
|
388
|
+
of documentation rot because the doc looks legitimate but leads agents astray.
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
#!/usr/bin/env python3
|
|
392
|
+
"""
|
|
393
|
+
scripts/gc-interface-drift.py
|
|
394
|
+
|
|
395
|
+
Detects drift between documented interfaces and actual code definitions.
|
|
396
|
+
|
|
397
|
+
Usage:
|
|
398
|
+
python3 scripts/gc-interface-drift.py .
|
|
399
|
+
python3 scripts/gc-interface-drift.py . --json
|
|
400
|
+
python3 scripts/gc-interface-drift.py . --doc docs/ARCHITECTURE.md
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
import argparse
|
|
404
|
+
import json
|
|
405
|
+
import re
|
|
406
|
+
import subprocess
|
|
407
|
+
import sys
|
|
408
|
+
from pathlib import Path
|
|
409
|
+
from typing import Dict, List, Optional, Set
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def extract_documented_interfaces(doc_path: Path) -> List[Dict]:
|
|
413
|
+
"""Extract interface/type names mentioned in documentation."""
|
|
414
|
+
content = doc_path.read_text()
|
|
415
|
+
interfaces = []
|
|
416
|
+
|
|
417
|
+
# Match backtick-quoted type names with file references
|
|
418
|
+
# Pattern: `TypeName` ... `file.go:line`
|
|
419
|
+
type_pattern = r'`([A-Z][a-zA-Z0-9]+(?:Interface|Service|Provider|Handler|Repository|Store|Manager|Client)?)`'
|
|
420
|
+
for match in re.finditer(type_pattern, content):
|
|
421
|
+
name = match.group(1)
|
|
422
|
+
line_num = content[:match.start()].count('\n') + 1
|
|
423
|
+
interfaces.append({
|
|
424
|
+
"name": name,
|
|
425
|
+
"doc_file": str(doc_path),
|
|
426
|
+
"doc_line": line_num
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
return interfaces
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def find_type_in_code_go(project_root: Path, type_name: str) -> Optional[Dict]:
|
|
433
|
+
"""Find a Go type/interface definition in code."""
|
|
434
|
+
try:
|
|
435
|
+
result = subprocess.run(
|
|
436
|
+
["grep", "-rn", f"type {type_name} ", "--include=*.go",
|
|
437
|
+
str(project_root)],
|
|
438
|
+
capture_output=True, text=True, timeout=10
|
|
439
|
+
)
|
|
440
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
441
|
+
first_match = result.stdout.strip().split('\n')[0]
|
|
442
|
+
parts = first_match.split(':', 2)
|
|
443
|
+
if len(parts) >= 2:
|
|
444
|
+
return {
|
|
445
|
+
"file": str(Path(parts[0]).relative_to(project_root)),
|
|
446
|
+
"line": int(parts[1]),
|
|
447
|
+
"definition": parts[2].strip() if len(parts) > 2 else ""
|
|
448
|
+
}
|
|
449
|
+
except Exception:
|
|
450
|
+
pass
|
|
451
|
+
return None
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def find_type_in_code_ts(project_root: Path, type_name: str) -> Optional[Dict]:
|
|
455
|
+
"""Find a TypeScript interface/class/type definition in code."""
|
|
456
|
+
try:
|
|
457
|
+
patterns = [
|
|
458
|
+
f"(export )?interface {type_name}",
|
|
459
|
+
f"(export )?class {type_name}",
|
|
460
|
+
f"(export )?type {type_name}"
|
|
461
|
+
]
|
|
462
|
+
for pattern in patterns:
|
|
463
|
+
result = subprocess.run(
|
|
464
|
+
["grep", "-rn", "-E", pattern, "--include=*.ts", "--include=*.tsx",
|
|
465
|
+
str(project_root)],
|
|
466
|
+
capture_output=True, text=True, timeout=10
|
|
467
|
+
)
|
|
468
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
469
|
+
first_match = result.stdout.strip().split('\n')[0]
|
|
470
|
+
parts = first_match.split(':', 2)
|
|
471
|
+
if len(parts) >= 2:
|
|
472
|
+
return {
|
|
473
|
+
"file": str(Path(parts[0]).relative_to(project_root)),
|
|
474
|
+
"line": int(parts[1]),
|
|
475
|
+
"definition": parts[2].strip() if len(parts) > 2 else ""
|
|
476
|
+
}
|
|
477
|
+
except Exception:
|
|
478
|
+
pass
|
|
479
|
+
return None
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def find_type_in_code_py(project_root: Path, type_name: str) -> Optional[Dict]:
|
|
483
|
+
"""Find a Python class definition in code."""
|
|
484
|
+
try:
|
|
485
|
+
result = subprocess.run(
|
|
486
|
+
["grep", "-rn", f"class {type_name}", "--include=*.py",
|
|
487
|
+
str(project_root)],
|
|
488
|
+
capture_output=True, text=True, timeout=10
|
|
489
|
+
)
|
|
490
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
491
|
+
first_match = result.stdout.strip().split('\n')[0]
|
|
492
|
+
parts = first_match.split(':', 2)
|
|
493
|
+
if len(parts) >= 2:
|
|
494
|
+
return {
|
|
495
|
+
"file": str(Path(parts[0]).relative_to(project_root)),
|
|
496
|
+
"line": int(parts[1]),
|
|
497
|
+
"definition": parts[2].strip() if len(parts) > 2 else ""
|
|
498
|
+
}
|
|
499
|
+
except Exception:
|
|
500
|
+
pass
|
|
501
|
+
return None
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def detect_project_lang(project_root: Path) -> str:
|
|
505
|
+
"""Auto-detect project language."""
|
|
506
|
+
if (project_root / "go.mod").exists():
|
|
507
|
+
return "go"
|
|
508
|
+
if (project_root / "package.json").exists():
|
|
509
|
+
return "ts"
|
|
510
|
+
if (project_root / "pyproject.toml").exists() or (project_root / "requirements.txt").exists():
|
|
511
|
+
return "py"
|
|
512
|
+
return "go" # default
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def check_interface_drift(project_root: Path, doc_path: Optional[str] = None) -> List[Dict]:
|
|
516
|
+
"""Check for interface drift between docs and code."""
|
|
517
|
+
lang = detect_project_lang(project_root)
|
|
518
|
+
finder = {"go": find_type_in_code_go, "ts": find_type_in_code_ts, "py": find_type_in_code_py}[lang]
|
|
519
|
+
|
|
520
|
+
# Collect docs to check
|
|
521
|
+
if doc_path:
|
|
522
|
+
doc_files = [project_root / doc_path]
|
|
523
|
+
else:
|
|
524
|
+
doc_files = []
|
|
525
|
+
for pattern in ["docs/*.md", "docs/**/*.md", "AGENTS.md"]:
|
|
526
|
+
doc_files.extend(project_root.glob(pattern))
|
|
527
|
+
|
|
528
|
+
findings = []
|
|
529
|
+
seen_types: Set[str] = set()
|
|
530
|
+
|
|
531
|
+
for df in doc_files:
|
|
532
|
+
if not df.exists():
|
|
533
|
+
continue
|
|
534
|
+
documented = extract_documented_interfaces(df)
|
|
535
|
+
|
|
536
|
+
for iface in documented:
|
|
537
|
+
if iface["name"] in seen_types:
|
|
538
|
+
continue
|
|
539
|
+
seen_types.add(iface["name"])
|
|
540
|
+
|
|
541
|
+
code_loc = finder(project_root, iface["name"])
|
|
542
|
+
if not code_loc:
|
|
543
|
+
findings.append({
|
|
544
|
+
"type_name": iface["name"],
|
|
545
|
+
"documented_in": str(df.relative_to(project_root)),
|
|
546
|
+
"doc_line": iface["doc_line"],
|
|
547
|
+
"status": "not_found_in_code",
|
|
548
|
+
"severity": "high",
|
|
549
|
+
"suggestion": f"Type '{iface['name']}' referenced in docs but not found in code. "
|
|
550
|
+
f"It may have been renamed, deleted, or the doc is outdated."
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
return findings
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def main():
|
|
557
|
+
parser = argparse.ArgumentParser(description="Detect interface drift between docs and code")
|
|
558
|
+
parser.add_argument("project_root", help="Project root directory")
|
|
559
|
+
parser.add_argument("--doc", help="Check a specific doc file only")
|
|
560
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
561
|
+
args = parser.parse_args()
|
|
562
|
+
|
|
563
|
+
project_root = Path(args.project_root).resolve()
|
|
564
|
+
findings = check_interface_drift(project_root, args.doc)
|
|
565
|
+
|
|
566
|
+
if args.json:
|
|
567
|
+
print(json.dumps({"interface_drift": findings}, indent=2))
|
|
568
|
+
else:
|
|
569
|
+
if not findings:
|
|
570
|
+
print("✓ No interface drift detected")
|
|
571
|
+
return
|
|
572
|
+
|
|
573
|
+
print(f"⚠ Found {len(findings)} potential interface drift issue(s):\n")
|
|
574
|
+
for f in findings:
|
|
575
|
+
icon = "🔴" if f["severity"] == "high" else "🟡"
|
|
576
|
+
print(f"{icon} {f['type_name']}")
|
|
577
|
+
print(f" Documented in: {f['documented_in']}:{f['doc_line']}")
|
|
578
|
+
print(f" Status: {f['status']}")
|
|
579
|
+
print(f" → {f['suggestion']}")
|
|
580
|
+
print()
|
|
581
|
+
|
|
582
|
+
sys.exit(1 if findings else 0)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
if __name__ == "__main__":
|
|
586
|
+
main()
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
## 4. Unified GC Runner
|
|
592
|
+
|
|
593
|
+
Runs all garbage collection checks in one pass. Produces a combined report.
|
|
594
|
+
|
|
595
|
+
```python
|
|
596
|
+
#!/usr/bin/env python3
|
|
597
|
+
"""
|
|
598
|
+
scripts/gc-docs.py
|
|
599
|
+
|
|
600
|
+
Unified documentation garbage collection runner.
|
|
601
|
+
Runs stale doc detection, broken link checking, and interface drift detection.
|
|
602
|
+
|
|
603
|
+
Usage:
|
|
604
|
+
python3 scripts/gc-docs.py .
|
|
605
|
+
python3 scripts/gc-docs.py . --json
|
|
606
|
+
python3 scripts/gc-docs.py . --checks stale,links # selective checks
|
|
607
|
+
"""
|
|
608
|
+
|
|
609
|
+
import argparse
|
|
610
|
+
import json
|
|
611
|
+
import sys
|
|
612
|
+
from pathlib import Path
|
|
613
|
+
from datetime import datetime
|
|
614
|
+
|
|
615
|
+
# Import the individual checkers (when used as a unified runner,
|
|
616
|
+
# these would be in the same scripts/ directory)
|
|
617
|
+
# For template purposes, showing the integration pattern:
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def run_gc(project_root: Path, checks: list, threshold_days: int = 14) -> dict:
|
|
621
|
+
"""Run selected garbage collection checks."""
|
|
622
|
+
report = {
|
|
623
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
624
|
+
"project_root": str(project_root),
|
|
625
|
+
"checks_run": checks,
|
|
626
|
+
"results": {},
|
|
627
|
+
"summary": {"total_issues": 0, "high": 0, "medium": 0, "low": 0}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if "stale" in checks:
|
|
631
|
+
# Run stale doc detection
|
|
632
|
+
from gc_stale_docs import check_staleness
|
|
633
|
+
stale = check_staleness(project_root, threshold_days)
|
|
634
|
+
report["results"]["stale_docs"] = stale
|
|
635
|
+
for item in stale:
|
|
636
|
+
report["summary"]["total_issues"] += 1
|
|
637
|
+
report["summary"][item.get("severity", "medium")] += 1
|
|
638
|
+
|
|
639
|
+
if "links" in checks:
|
|
640
|
+
# Run broken link detection
|
|
641
|
+
from gc_broken_links import check_all_links
|
|
642
|
+
broken = check_all_links(project_root)
|
|
643
|
+
report["results"]["broken_links"] = broken
|
|
644
|
+
for item in broken:
|
|
645
|
+
count = len(item.get("broken_links", []))
|
|
646
|
+
report["summary"]["total_issues"] += count
|
|
647
|
+
report["summary"]["high"] += count # broken links are always high severity
|
|
648
|
+
|
|
649
|
+
if "drift" in checks:
|
|
650
|
+
# Run interface drift detection
|
|
651
|
+
from gc_interface_drift import check_interface_drift
|
|
652
|
+
drift = check_interface_drift(project_root)
|
|
653
|
+
report["results"]["interface_drift"] = drift
|
|
654
|
+
for item in drift:
|
|
655
|
+
report["summary"]["total_issues"] += 1
|
|
656
|
+
report["summary"][item.get("severity", "medium")] += 1
|
|
657
|
+
|
|
658
|
+
return report
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def main():
|
|
662
|
+
parser = argparse.ArgumentParser(description="Documentation garbage collection")
|
|
663
|
+
parser.add_argument("project_root", help="Project root directory")
|
|
664
|
+
parser.add_argument("--checks", default="stale,links,drift",
|
|
665
|
+
help="Comma-separated checks to run (default: all)")
|
|
666
|
+
parser.add_argument("--threshold", type=int, default=14,
|
|
667
|
+
help="Staleness threshold in days")
|
|
668
|
+
parser.add_argument("--json", action="store_true")
|
|
669
|
+
parser.add_argument("-o", "--output", help="Write report to file")
|
|
670
|
+
args = parser.parse_args()
|
|
671
|
+
|
|
672
|
+
project_root = Path(args.project_root).resolve()
|
|
673
|
+
checks = [c.strip() for c in args.checks.split(",")]
|
|
674
|
+
report = run_gc(project_root, checks, args.threshold)
|
|
675
|
+
|
|
676
|
+
output = json.dumps(report, indent=2) if args.json else format_report(report)
|
|
677
|
+
|
|
678
|
+
if args.output:
|
|
679
|
+
Path(args.output).write_text(output)
|
|
680
|
+
print(f"Report written to {args.output}")
|
|
681
|
+
else:
|
|
682
|
+
print(output)
|
|
683
|
+
|
|
684
|
+
sys.exit(1 if report["summary"]["total_issues"] > 0 else 0)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def format_report(report: dict) -> str:
|
|
688
|
+
"""Human-readable report format."""
|
|
689
|
+
lines = []
|
|
690
|
+
s = report["summary"]
|
|
691
|
+
total = s["total_issues"]
|
|
692
|
+
|
|
693
|
+
if total == 0:
|
|
694
|
+
return "✓ Documentation is clean — no issues found"
|
|
695
|
+
|
|
696
|
+
lines.append(f"Documentation GC Report — {report['timestamp'][:10]}")
|
|
697
|
+
lines.append(f"{'=' * 50}")
|
|
698
|
+
lines.append(f"Total issues: {total} (🔴 {s['high']} high, 🟡 {s['medium']} medium, 🟢 {s['low']} low)")
|
|
699
|
+
lines.append("")
|
|
700
|
+
|
|
701
|
+
for check_name, results in report["results"].items():
|
|
702
|
+
if results:
|
|
703
|
+
lines.append(f"## {check_name.replace('_', ' ').title()}")
|
|
704
|
+
lines.append(f" {len(results)} issue(s) found")
|
|
705
|
+
lines.append("")
|
|
706
|
+
|
|
707
|
+
return "\n".join(lines)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
if __name__ == "__main__":
|
|
711
|
+
main()
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
---
|
|
715
|
+
|
|
716
|
+
## 5. CI Integration
|
|
717
|
+
|
|
718
|
+
### GitHub Actions
|
|
719
|
+
|
|
720
|
+
```yaml
|
|
721
|
+
# .github/workflows/doc-gc.yml
|
|
722
|
+
name: Documentation GC
|
|
723
|
+
on:
|
|
724
|
+
push:
|
|
725
|
+
branches: [main]
|
|
726
|
+
paths: ['docs/**', 'AGENTS.md', '*.md']
|
|
727
|
+
schedule:
|
|
728
|
+
- cron: '0 9 * * 1' # Weekly Monday 9am
|
|
729
|
+
|
|
730
|
+
jobs:
|
|
731
|
+
doc-gc:
|
|
732
|
+
runs-on: ubuntu-latest
|
|
733
|
+
steps:
|
|
734
|
+
- uses: actions/checkout@v4
|
|
735
|
+
with:
|
|
736
|
+
fetch-depth: 0 # Need full history for git log dates
|
|
737
|
+
|
|
738
|
+
- uses: actions/setup-python@v5
|
|
739
|
+
with:
|
|
740
|
+
python-version: '3.11'
|
|
741
|
+
|
|
742
|
+
- name: Run documentation GC
|
|
743
|
+
run: python3 scripts/gc-docs.py . --json -o gc-report.json
|
|
744
|
+
|
|
745
|
+
- name: Upload report
|
|
746
|
+
if: failure()
|
|
747
|
+
uses: actions/upload-artifact@v4
|
|
748
|
+
with:
|
|
749
|
+
name: doc-gc-report
|
|
750
|
+
path: gc-report.json
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
### Makefile Target
|
|
754
|
+
|
|
755
|
+
```makefile
|
|
756
|
+
.PHONY: gc-docs
|
|
757
|
+
gc-docs:
|
|
758
|
+
@echo "Running documentation garbage collection..."
|
|
759
|
+
@python3 scripts/gc-docs.py . || true
|
|
760
|
+
@echo ""
|
|
761
|
+
@echo "To fix: review stale docs and update or delete them"
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
---
|
|
765
|
+
|
|
766
|
+
## 6. Scheduling Guide
|
|
767
|
+
|
|
768
|
+
| Check | Frequency | Trigger | Rationale |
|
|
769
|
+
|-------|-----------|---------|-----------|
|
|
770
|
+
| Broken links | Per-commit (CI) | Push to main | Catches renames/deletes immediately |
|
|
771
|
+
| Stale docs | Weekly | Scheduled CI | Balances signal vs noise |
|
|
772
|
+
| Interface drift | Per-PR | PR check | Prevents drift from entering main |
|
|
773
|
+
| Full GC | Weekly + on-demand | Manual or scheduled | Comprehensive health check |
|
|
774
|
+
|
|
775
|
+
### Running from Harness Executor
|
|
776
|
+
|
|
777
|
+
After completing a task that creates or modifies code, the executor can run a quick GC check:
|
|
778
|
+
|
|
779
|
+
```bash
|
|
780
|
+
# Quick post-task check — just broken links on changed docs
|
|
781
|
+
python3 scripts/gc-broken-links.py . --json | python3 -c "
|
|
782
|
+
import sys, json
|
|
783
|
+
data = json.load(sys.stdin)
|
|
784
|
+
if data['broken_links']:
|
|
785
|
+
print('⚠ Task may have introduced broken doc links — review before committing')
|
|
786
|
+
"
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
### Running from ECL Harness Engineer (Improve Mode)
|
|
790
|
+
|
|
791
|
+
During a harness audit, run the full GC suite to assess documentation health:
|
|
792
|
+
|
|
793
|
+
```bash
|
|
794
|
+
python3 scripts/gc-docs.py . --json -o harness/trace/gc-report.json
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
The GC report feeds into the audit score (Documentation dimension) and helps prioritize
|
|
798
|
+
which docs to fix first.
|