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.
Files changed (78) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +15 -1
  2. package/bundled-skills/2slides-ppt-generator/SKILL.md +1 -1
  3. package/bundled-skills/2slides-ppt-generator/scripts/create_pdf_slides.py +2 -1
  4. package/bundled-skills/2slides-ppt-generator/scripts/generate_narration.py +2 -1
  5. package/bundled-skills/2slides-ppt-generator/scripts/generate_slides.py +13 -7
  6. package/bundled-skills/accint-solve/SKILL.md +205 -0
  7. package/bundled-skills/android-cli/SKILL.md +239 -0
  8. package/bundled-skills/android-cli/references/interact.md +83 -0
  9. package/bundled-skills/android-cli/references/journeys.md +105 -0
  10. package/bundled-skills/android-dev/references/hybrid.md +7 -4
  11. package/bundled-skills/android-dev/references/react-native.md +5 -2
  12. package/bundled-skills/atlas-contract/SKILL.md +4 -4
  13. package/bundled-skills/atlas-ledger/SKILL.md +10 -7
  14. package/bundled-skills/bun-development/SKILL.md +1 -1
  15. package/bundled-skills/cloud-penetration-testing/SKILL.md +1 -1
  16. package/bundled-skills/codebase-to-wordpress-converter/SKILL.md +1 -0
  17. package/bundled-skills/codex-fable5/SKILL.md +154 -0
  18. package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
  19. package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
  20. package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
  21. package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
  22. package/bundled-skills/docs/users/bundles.md +1 -1
  23. package/bundled-skills/docs/users/claude-code-skills.md +1 -1
  24. package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
  25. package/bundled-skills/docs/users/getting-started.md +1 -1
  26. package/bundled-skills/docs/users/kiro-integration.md +1 -1
  27. package/bundled-skills/docs/users/usage.md +4 -4
  28. package/bundled-skills/docs/users/visual-guide.md +4 -4
  29. package/bundled-skills/dos-verify-done-claims/SKILL.md +173 -0
  30. package/bundled-skills/ecl-harness-engineer/LICENSE +21 -0
  31. package/bundled-skills/ecl-harness-engineer/SKILL.md +714 -0
  32. package/bundled-skills/ecl-harness-engineer/agents/analyzer.md +119 -0
  33. package/bundled-skills/ecl-harness-engineer/agents/auditor.md +212 -0
  34. package/bundled-skills/ecl-harness-engineer/agents/creator-config.md +343 -0
  35. package/bundled-skills/ecl-harness-engineer/agents/creator-docs.md +201 -0
  36. package/bundled-skills/ecl-harness-engineer/agents/creator-linters.md +123 -0
  37. package/bundled-skills/ecl-harness-engineer/references/adapters/adapter-schema.md +204 -0
  38. package/bundled-skills/ecl-harness-engineer/references/adapters/generic.md +156 -0
  39. package/bundled-skills/ecl-harness-engineer/references/adapters/go.md +212 -0
  40. package/bundled-skills/ecl-harness-engineer/references/adapters/java.md +205 -0
  41. package/bundled-skills/ecl-harness-engineer/references/adapters/python.md +225 -0
  42. package/bundled-skills/ecl-harness-engineer/references/adapters/rust.md +220 -0
  43. package/bundled-skills/ecl-harness-engineer/references/adapters/typescript.md +245 -0
  44. package/bundled-skills/ecl-harness-engineer/references/architecture-diagrams.md +420 -0
  45. package/bundled-skills/ecl-harness-engineer/references/audit-templates.md +649 -0
  46. package/bundled-skills/ecl-harness-engineer/references/capability-registry.md +485 -0
  47. package/bundled-skills/ecl-harness-engineer/references/darwin-eval-prompts.md +373 -0
  48. package/bundled-skills/ecl-harness-engineer/references/documentation-templates.md +741 -0
  49. package/bundled-skills/ecl-harness-engineer/references/durability-patterns.md +423 -0
  50. package/bundled-skills/ecl-harness-engineer/references/ecl-harness.md +1431 -0
  51. package/bundled-skills/ecl-harness-engineer/references/environment-config-guide.md +534 -0
  52. package/bundled-skills/ecl-harness-engineer/references/environment-detection-guide.md +751 -0
  53. package/bundled-skills/ecl-harness-engineer/references/eval-templates.md +377 -0
  54. package/bundled-skills/ecl-harness-engineer/references/gc-templates.md +798 -0
  55. package/bundled-skills/ecl-harness-engineer/references/greenfield-templates.md +1385 -0
  56. package/bundled-skills/ecl-harness-engineer/references/linter-templates.md +448 -0
  57. package/bundled-skills/ecl-harness-engineer/references/observability-templates.md +315 -0
  58. package/bundled-skills/efficient-web-research/SKILL.md +320 -0
  59. package/bundled-skills/environment-setup-guide/SKILL.md +2 -2
  60. package/bundled-skills/evolution/SKILL.md +1 -1
  61. package/bundled-skills/gitops-workflow/SKILL.md +1 -1
  62. package/bundled-skills/linkerd-patterns/SKILL.md +1 -1
  63. package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package-lock.json +504 -1317
  64. package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package.json +2 -2
  65. package/bundled-skills/lovable-cleanup/SKILL.md +416 -0
  66. package/bundled-skills/monopoly/SKILL.md +397 -0
  67. package/bundled-skills/monopoly/patterns/SKILL.md +331 -0
  68. package/bundled-skills/monopoly/scale-benchmarks/SKILL.md +174 -0
  69. package/bundled-skills/monopoly/security-checklist/SKILL.md +69 -0
  70. package/bundled-skills/monopoly/tech-matrix/SKILL.md +268 -0
  71. package/bundled-skills/pagespeed-enhancer/SKILL.md +579 -0
  72. package/bundled-skills/polis-protocol/SKILL.md +6 -3
  73. package/bundled-skills/sharp-coder/SKILL.md +131 -0
  74. package/bundled-skills/unship/SKILL.md +11 -5
  75. package/bundled-skills/uv-package-manager/resources/implementation-playbook.md +1 -1
  76. package/bundled-skills/varlock/SKILL.md +2 -2
  77. package/package.json +1 -1
  78. 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.