Skip to main content
This cookbook shows how to automatically update documentation when code changes, ensure docs stay current, and generate API documentation from code.

How it works

Documentation sync hooks can:
  1. Update API docs: Generate documentation from code annotations
  2. Sync code examples: Keep examples in docs matching actual code
  3. Validate doc references: Ensure docs reference correct code paths
  4. Update changelogs: Auto-generate changelog entries
  5. Check doc completeness: Require documentation for new features

Prerequisites

Install documentation tools:
npm install -D typedoc jsdoc documentation
npm install -D @readme/openapi-parser  # For OpenAPI

Basic documentation automation

Auto-generate API docs

Generate API documentation when code changes. Create .factory/hooks/generate-api-docs.sh:
#!/bin/bash
set -e

input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')

# Only process code files
if ! echo "$file_path" | grep -qE '\.(ts|tsx|js|jsx|py|go)$'; then
  exit 0
fi

# Skip test files
if echo "$file_path" | grep -qE '\.(test|spec)\.(ts|tsx|js|jsx)$'; then
  exit 0
fi

cwd=$(echo "$input" | jq -r '.cwd')
cd "$cwd"

echo "📚 Updating API documentation..."

case "$file_path" in
  *.ts|*.tsx)
    # TypeScript - use typedoc
    if command -v typedoc &> /dev/null && [ -f "typedoc.json" ]; then
      echo "Generating TypeScript docs..."
      typedoc --out docs/api src/ 2>&1 || {
        echo "⚠️ Failed to generate docs" >&2
      }
      echo "✓ API docs updated at docs/api"
    fi
    ;;
    
  *.py)
    # Python - use pdoc
    if command -v pdoc &> /dev/null; then
      module_name=$(echo "$file_path" | sed 's|^src/||; s|/|.|g; s|\.py$||')
      echo "Generating Python docs for $module_name..."
      
      pdoc --html --output-dir docs/api "$module_name" --force 2>&1 || {
        echo "⚠️ Failed to generate docs" >&2
      }
      echo "✓ API docs updated"
    fi
    ;;
    
  *.go)
    # Go - use godoc
    if command -v godoc &> /dev/null; then
      echo "Generating Go docs..."
      # Go docs are typically served, not generated
      # But we can create markdown from godoc
      echo "✓ Go docs available via 'godoc -http=:6060'"
    fi
    ;;
esac

exit 0
chmod +x .factory/hooks/generate-api-docs.sh
Add to .factory/settings.json:
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$DROID_PROJECT_DIR\"/.factory/hooks/generate-api-docs.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Update README on project changes

Keep README in sync with project structure. Create .factory/hooks/update-readme.sh:
#!/bin/bash
set -e

input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')

# Only update on package.json or structure changes
if ! echo "$file_path" | grep -qE '(package\.json|README\.md)$'; then
  exit 0
fi

cwd=$(echo "$input" | jq -r '.cwd')
cd "$cwd"

# Don't run if README is the file being edited
if [ "$file_path" = "README.md" ]; then
  exit 0
fi

echo "📝 Checking README..."

# Update package info in README
if [ -f "package.json" ] && [ -f "README.md" ]; then
  name=$(jq -r '.name' package.json)
  version=$(jq -r '.version' package.json)
  description=$(jq -r '.description' package.json)
  
  # Check if version in README matches package.json
  if ! grep -q "Version: $version" README.md; then
    echo "⚠️ README version out of sync with package.json"
    echo "Package version: $version"
    echo "Consider updating README.md or ask me to do it"
  fi
fi

# Check for missing documentation sections
required_sections=("Installation" "Usage" "API" "Contributing")
missing_sections=()

for section in "${required_sections[@]}"; do
  if ! grep -qi "## $section" README.md; then
    missing_sections+=("$section")
  fi
done

if [ ${#missing_sections[@]} -gt 0 ]; then
  echo "⚠️ README missing sections:"
  printf '  - %s\n' "${missing_sections[@]}"
fi

exit 0
chmod +x .factory/hooks/update-readme.sh

Sync code examples in documentation

Ensure code examples in docs match actual code: Create .factory/hooks/sync-doc-examples.py:
#!/usr/bin/env python3
"""
Sync code examples in markdown docs with actual source code.
"""
import json
import sys
import re
import os

def extract_code_snippets(doc_file):
    """Extract code snippets from markdown file."""
    with open(doc_file, 'r') as f:
        content = f.read()
    
    # Find code blocks with source file annotations
    # Format: ```typescript
    # // From: src/components/Button.tsx
    pattern = r'```(\w+)\n// From: (.*?)\n(.*?)\n```'
    snippets = re.findall(pattern, content, re.DOTALL)
    
    return snippets

def verify_snippet_matches_source(language, source_file, snippet):
    """Check if snippet exists in source file."""
    if not os.path.exists(source_file):
        return False, f"Source file not found: {source_file}"
    
    with open(source_file, 'r') as f:
        source_content = f.read()
    
    # Normalize whitespace for comparison
    normalized_snippet = ' '.join(snippet.split())
    normalized_source = ' '.join(source_content.split())
    
    if normalized_snippet in normalized_source:
        return True, "Snippet matches source"
    else:
        return False, "Snippet does not match source code"

def main():
    input_data = json.load(sys.stdin)
    file_path = input_data.get('tool_input', {}).get('file_path', '')
    
    # Check both code files and doc files
    if file_path.endswith(('.md', '.mdx')):
        # Doc file changed - verify all examples
        print(f"📖 Verifying code examples in {file_path}...")
        
        snippets = extract_code_snippets(file_path)
        issues = []
        
        for lang, source, snippet in snippets:
            matches, message = verify_snippet_matches_source(lang, source, snippet)
            if not matches:
                issues.append(f"{source}: {message}")
        
        if issues:
            print("⚠️ Some code examples may be outdated:", file=sys.stderr)
            for issue in issues:
                print(f"  - {issue}", file=sys.stderr)
            print("\nConsider updating the examples in the documentation.", file=sys.stderr)
        else:
            print("✓ All code examples are in sync")
    
    elif file_path.endswith(('.ts', '.tsx', '.js', '.jsx', '.py')):
        # Code file changed - check if it's referenced in docs
        print(f"Checking if {file_path} is referenced in documentation...")
        
        # Find docs that reference this file
        doc_files = []
        for root, dirs, files in os.walk('docs'):
            for file in files:
                if file.endswith(('.md', '.mdx')):
                    doc_path = os.path.join(root, file)
                    with open(doc_path, 'r') as f:
                        if file_path in f.read():
                            doc_files.append(doc_path)
        
        if doc_files:
            print(f"ℹ️ File is referenced in {len(doc_files)} documentation file(s):")
            for doc in doc_files:
                print(f"  - {doc}")
            print("\nConsider updating these docs if the API changed.")
    
    sys.exit(0)

if __name__ == '__main__':
    try:
        main()
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(0)
chmod +x .factory/hooks/sync-doc-examples.py

Advanced documentation automation

OpenAPI/Swagger spec validation

Ensure API documentation matches implementation: Create .factory/hooks/validate-openapi.sh:
#!/bin/bash
set -e

input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')

# Check if this is an API route file
if ! echo "$file_path" | grep -qE 'routes/.*\.(ts|js)$'; then
  exit 0
fi

cwd=$(echo "$input" | jq -r '.cwd')
cd "$cwd"

# Check if OpenAPI spec exists
if [ ! -f "openapi.yaml" ] && [ ! -f "swagger.yaml" ]; then
  exit 0
fi

spec_file="openapi.yaml"
[ -f "swagger.yaml" ] && spec_file="swagger.yaml"

echo "🔍 Validating OpenAPI spec..."

# Extract route definitions from code
# This is simplified - you'd need a more sophisticated parser
routes=$(grep -E "router\.(get|post|put|delete|patch)" "$file_path" | \
  sed -E "s/.*router\.([a-z]+)\(['\"](.*?)['\"].*/\1 \2/" || true)

if [ -n "$routes" ]; then
  echo "Routes in $file_path:"
  echo "$routes" | sed 's/^/  /'
  
  # Check if routes are documented in OpenAPI spec
  while IFS= read -r route; do
    method=$(echo "$route" | awk '{print $1}')
    path=$(echo "$route" | awk '{print $2}')
    
    if ! grep -q "$path" "$spec_file"; then
      echo "⚠️ Route not documented in $spec_file: $method $path"
      echo "Consider adding this endpoint to the API documentation."
    fi
  done <<< "$routes"
fi

# Validate spec syntax
if command -v swagger-cli &> /dev/null; then
  if swagger-cli validate "$spec_file" 2>&1; then
    echo "✓ OpenAPI spec is valid"
  else
    echo "❌ OpenAPI spec has errors" >&2
  fi
fi

exit 0
chmod +x .factory/hooks/validate-openapi.sh

JSDoc/TSDoc enforcement

Require documentation comments for public APIs: Create .factory/hooks/enforce-jsdoc.sh:
#!/bin/bash

input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')
content=$(echo "$input" | jq -r '.tool_input.content // ""')

# Only check TypeScript/JavaScript files
if ! echo "$file_path" | grep -qE '\.(ts|tsx|js|jsx)$'; then
  exit 0
fi

# Skip test files
if echo "$file_path" | grep -qE '\.(test|spec)\.(ts|tsx|js|jsx)$'; then
  exit 0
fi

# Check for exported functions/classes without JSDoc
missing_docs=()

# Find exported functions without JSDoc
while IFS= read -r line; do
  # Check if line is an export
  if echo "$line" | grep -qE '^export (function|class|const|interface|type)'; then
    # Get the name
    name=$(echo "$line" | sed -E 's/^export (function|class|const|interface|type) ([a-zA-Z0-9_]+).*/\2/')
    
    # Check if there's a JSDoc comment before it
    # This is simplified - you'd want a proper parser
    if ! echo "$content" | grep -B1 "^export.*$name" | grep -qE '^\s*\*'; then
      missing_docs+=("$name")
    fi
  fi
done <<< "$content"

if [ ${#missing_docs[@]} -gt 0 ]; then
  echo "⚠️ Exported items missing documentation:" >&2
  printf '  - %s\n' "${missing_docs[@]}" >&2
  echo "" >&2
  echo "Please add JSDoc comments for public APIs:" >&2
  echo "/**" >&2
  echo " * Description of what this does" >&2
  echo " * @param paramName - Parameter description" >&2
  echo " * @returns Return value description" >&2
  echo " */" >&2
  
  # Warning only, don't block
  # Change to exit 2 to enforce documentation
fi

exit 0
chmod +x .factory/hooks/enforce-jsdoc.sh

Generate changelog from commits

Automatically build changelog from git history: Create .factory/hooks/generate-changelog.sh:
#!/bin/bash
set -e

input=$(cat)
hook_event=$(echo "$input" | jq -r '.hook_event_name')

# Only run on Stop (after work is complete)
if [ "$hook_event" != "Stop" ]; then
  exit 0
fi

cwd=$(echo "$input" | jq -r '.cwd')
cd "$cwd"

# Check if there are new commits since last changelog update
if [ ! -f "CHANGELOG.md" ]; then
  exit 0
fi

# Get last version in changelog
last_version=$(grep -m1 "## \[" CHANGELOG.md | sed -E 's/.*\[([0-9.]+)\].*/\1/')

if [ -z "$last_version" ]; then
  exit 0
fi

# Get commits since last version tag
if git rev-parse "v$last_version" &>/dev/null; then
  new_commits=$(git log "v$last_version..HEAD" --oneline)
  
  if [ -n "$new_commits" ]; then
    echo "📝 New commits since v$last_version"
    echo ""
    echo "Consider updating CHANGELOG.md with:"
    echo ""
    
    # Group commits by type
    echo "### Features"
    git log "v$last_version..HEAD" --oneline | grep "^[a-f0-9]* feat" | sed 's/^[a-f0-9]* feat[:(]/- /' || true
    echo ""
    
    echo "### Bug Fixes"
    git log "v$last_version..HEAD" --oneline | grep "^[a-f0-9]* fix" | sed 's/^[a-f0-9]* fix[:(]/- /' || true
    echo ""
  fi
fi

exit 0
chmod +x .factory/hooks/generate-changelog.sh

Docs coverage report

Track which code has documentation: Create .factory/hooks/docs-coverage.py:
#!/usr/bin/env python3
"""
Calculate documentation coverage for the codebase.
"""
import os
import re
import sys
import json

def count_functions(file_path):
    """Count functions in a file."""
    with open(file_path, 'r') as f:
        content = f.read()
    
    # Count function definitions (simplified)
    if file_path.endswith('.py'):
        functions = re.findall(r'^def \w+\(', content, re.MULTILINE)
    elif file_path.endswith(('.ts', '.tsx', '.js', '.jsx')):
        functions = re.findall(r'(^export function \w+|^function \w+|const \w+ = \(.*\) =>)', 
                              content, re.MULTILINE)
    else:
        return 0
    
    return len(functions)

def count_documented_functions(file_path):
    """Count functions with documentation."""
    with open(file_path, 'r') as f:
        content = f.read()
    
    if file_path.endswith('.py'):
        # Look for docstrings
        pattern = r'def \w+\(.*?\):\s*"""'
    elif file_path.endswith(('.ts', '.tsx', '.js', '.jsx')):
        # Look for JSDoc comments
        pattern = r'/\*\*.*?\*/\s*(export )?function \w+'
    else:
        return 0
    
    documented = re.findall(pattern, content, re.DOTALL)
    return len(documented)

def main():
    # Calculate documentation coverage for src/
    src_dir = 'src'
    
    if not os.path.exists(src_dir):
        sys.exit(0)
    
    total_functions = 0
    documented_functions = 0
    
    for root, dirs, files in os.walk(src_dir):
        # Skip test files
        dirs[:] = [d for d in dirs if d not in ['__tests__', 'test', 'tests']]
        
        for file in files:
            if file.endswith(('.py', '.ts', '.tsx', '.js', '.jsx')):
                file_path = os.path.join(root, file)
                total_functions += count_functions(file_path)
                documented_functions += count_documented_functions(file_path)
    
    if total_functions > 0:
        coverage = (documented_functions / total_functions) * 100
        
        print(f"\n📊 Documentation Coverage Report")
        print(f"Documented functions: {documented_functions}/{total_functions}")
        print(f"Coverage: {coverage:.1f}%")
        
        if coverage < 60:
            print("\n⚠️ Documentation coverage is low")
            print("Consider adding documentation to public APIs")
        elif coverage < 80:
            print("\n✓ Documentation coverage is good, but could be better")
        else:
            print("\n✓ Excellent documentation coverage!")

if __name__ == '__main__':
    try:
        main()
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
    
    sys.exit(0)
chmod +x .factory/hooks/docs-coverage.py
Run on session end:
{
  "hooks": {
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$DROID_PROJECT_DIR\"/.factory/hooks/docs-coverage.py",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

Best practices

1

Automate but don't auto-commit

Generate docs but let users review before committing:
# Generate docs
typedoc --out docs/api src/

# Alert but don't commit
echo "✓ API docs regenerated at docs/api"
echo "Review changes: git diff docs/"
2

Keep doc examples testable

Extract examples from actual tests:
# Extract example from test file
sed -n '/Example:/,/End example/p' Button.test.tsx > docs/examples/button.md
3

Version documentation

Tag docs with version info:
# API Reference

> Version: 2.1.0  
> Last updated: 2024-01-15
4

Link docs to code

Include source file references:
## Button Component

Source: [`src/components/Button.tsx`](../src/components/Button.tsx)
5

Use doc generation tools

Don’t manually maintain API docs:
# TypeScript
typedoc --out docs/api src/

# Python
pdoc --html --output-dir docs/api src/

# Go
godoc -http=:6060

Troubleshooting

Problem: Can’t generate documentation Solution: Check tool configuration:
# Verify tool installation
which typedoc

# Check config file
cat typedoc.json

# Test manually
typedoc --version
typedoc --help
Problem: Doc examples don’t match code Solution: Extract examples from tests:
// In test file
/* DOC_EXAMPLE_START: basic-usage */
const result = myFunction(input);
expect(result).toBe(expected);
/* DOC_EXAMPLE_END */
Then extract programmatically:
sed -n '/DOC_EXAMPLE_START/,/DOC_EXAMPLE_END/p' test.ts
Problem: Doc generation takes too long Solution: Build incrementally:
# Only rebuild changed files
typedoc --incremental src/

# Or skip in hooks, run manually
if [ "$SKIP_DOC_BUILD" = "true" ]; then
  exit 0
fi

See also