docs-suggest

  1#!/usr/bin/env bash
  2#
  3# Analyze code changes and suggest documentation updates.
  4#
  5# Usage:
  6#   script/docs-suggest --pr 49177           # Analyze a PR (immediate mode)
  7#   script/docs-suggest --commit abc123      # Analyze a commit
  8#   script/docs-suggest --diff file.patch    # Analyze a diff file
  9#   script/docs-suggest --staged             # Analyze staged changes
 10#
 11# Modes:
 12#   --batch       Append suggestions to batch file for later PR (default for main)
 13#   --immediate   Output suggestions directly (default for cherry-picks)
 14#
 15# Options:
 16#   --dry-run     Show assembled context without calling LLM
 17#   --output FILE Write suggestions to file instead of stdout (immediate mode)
 18#   --verbose     Show detailed progress
 19#   --model NAME  Override default model
 20#   --preview     Add preview callout to suggested docs (auto-detected)
 21#
 22# Batch mode:
 23#   Suggestions are appended to docs/.suggestions/pending.md
 24#   Use script/docs-suggest-publish to create a PR from batched suggestions
 25#
 26# Examples:
 27#   # Analyze a PR to main (batches by default)
 28#   script/docs-suggest --pr 49100
 29#
 30#   # Analyze a cherry-pick PR (immediate by default)
 31#   script/docs-suggest --pr 49152
 32#
 33#   # Force immediate output for testing
 34#   script/docs-suggest --pr 49100 --immediate
 35#
 36#   # Dry run to see context
 37#   script/docs-suggest --pr 49100 --dry-run
 38
 39set -euo pipefail
 40
 41# Defaults
 42MODE=""
 43TARGET=""
 44DRY_RUN=false
 45VERBOSE=false
 46OUTPUT=""
 47MODEL="${DROID_MODEL:-claude-sonnet-4-5-20250929}"
 48OUTPUT_MODE=""  # batch or immediate, auto-detected if not set
 49ADD_PREVIEW_CALLOUT=""  # auto-detected if not set
 50
 51# Colors for output
 52RED='\033[0;31m'
 53GREEN='\033[0;32m'
 54YELLOW='\033[0;33m'
 55BLUE='\033[0;34m'
 56NC='\033[0m' # No Color
 57
 58log() {
 59    if [[ "$VERBOSE" == "true" ]]; then
 60        echo -e "${BLUE}[docs-suggest]${NC} $*" >&2
 61    fi
 62}
 63
 64error() {
 65    echo -e "${RED}Error:${NC} $*" >&2
 66    exit 1
 67}
 68
 69warn() {
 70    echo -e "${YELLOW}Warning:${NC} $*" >&2
 71}
 72
 73# Parse arguments
 74while [[ $# -gt 0 ]]; do
 75    case $1 in
 76        --pr)
 77            MODE="pr"
 78            TARGET="$2"
 79            shift 2
 80            ;;
 81        --commit)
 82            MODE="commit"
 83            TARGET="$2"
 84            shift 2
 85            ;;
 86        --diff)
 87            MODE="diff"
 88            TARGET="$2"
 89            shift 2
 90            ;;
 91        --staged)
 92            MODE="staged"
 93            shift
 94            ;;
 95        --batch)
 96            OUTPUT_MODE="batch"
 97            shift
 98            ;;
 99        --immediate)
100            OUTPUT_MODE="immediate"
101            shift
102            ;;
103        --preview)
104            ADD_PREVIEW_CALLOUT="true"
105            shift
106            ;;
107        --no-preview)
108            ADD_PREVIEW_CALLOUT="false"
109            shift
110            ;;
111        --dry-run)
112            DRY_RUN=true
113            shift
114            ;;
115        --verbose)
116            VERBOSE=true
117            shift
118            ;;
119        --output)
120            OUTPUT="$2"
121            shift 2
122            ;;
123        --model)
124            MODEL="$2"
125            shift 2
126            ;;
127        -h|--help)
128            head -42 "$0" | tail -40
129            exit 0
130            ;;
131        *)
132            error "Unknown option: $1"
133            ;;
134    esac
135done
136
137# Validate mode
138if [[ -z "$MODE" ]]; then
139    error "Must specify one of: --pr, --commit, --diff, or --staged"
140fi
141
142# Get repo root
143REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
144cd "$REPO_ROOT"
145
146# Batch file location
147BATCH_DIR="$REPO_ROOT/docs/.suggestions"
148BATCH_FILE="$BATCH_DIR/pending.md"
149
150# Temp directory for context assembly
151TMPDIR=$(mktemp -d)
152trap 'rm -rf "$TMPDIR"' EXIT
153
154log "Mode: $MODE, Target: ${TARGET:-staged changes}"
155log "Temp dir: $TMPDIR"
156
157# ============================================================================
158# Step 1: Get the diff and detect context
159# ============================================================================
160
161get_diff() {
162    case $MODE in
163        pr)
164            if ! command -v gh &> /dev/null; then
165                error "gh CLI required for --pr mode. Install: https://cli.github.com"
166            fi
167            log "Fetching PR #$TARGET info..."
168            
169            # Get PR metadata for auto-detection
170            PR_JSON=$(gh pr view "$TARGET" --json baseRefName,title,number)
171            PR_BASE=$(echo "$PR_JSON" | grep -o '"baseRefName":"[^"]*"' | cut -d'"' -f4)
172            PR_TITLE=$(echo "$PR_JSON" | grep -o '"title":"[^"]*"' | cut -d'"' -f4)
173            PR_NUMBER=$(echo "$PR_JSON" | grep -o '"number":[0-9]*' | cut -d':' -f2)
174            
175            log "PR #$PR_NUMBER: $PR_TITLE (base: $PR_BASE)"
176            
177            # Auto-detect output mode based on target branch
178            if [[ -z "$OUTPUT_MODE" ]]; then
179                if [[ "$PR_BASE" == "main" ]]; then
180                    OUTPUT_MODE="batch"
181                    log "Auto-detected: batch mode (PR targets main)"
182                else
183                    OUTPUT_MODE="immediate"
184                    log "Auto-detected: immediate mode (PR targets $PR_BASE)"
185                fi
186            fi
187            
188            # Auto-detect preview callout
189            if [[ -z "$ADD_PREVIEW_CALLOUT" ]]; then
190                if [[ "$PR_BASE" == "main" ]]; then
191                    ADD_PREVIEW_CALLOUT="true"
192                    log "Auto-detected: will add preview callout (new feature going to main)"
193                else
194                    # Cherry-pick to release branch - check if it's preview or stable
195                    ADD_PREVIEW_CALLOUT="false"
196                    log "Auto-detected: no preview callout (cherry-pick)"
197                fi
198            fi
199            
200            # Store metadata for batch mode
201            echo "$PR_NUMBER" > "$TMPDIR/pr_number"
202            echo "$PR_TITLE" > "$TMPDIR/pr_title"
203            echo "$PR_BASE" > "$TMPDIR/pr_base"
204            
205            log "Fetching PR #$TARGET diff..."
206            gh pr diff "$TARGET" > "$TMPDIR/changes.diff"
207            gh pr diff "$TARGET" --name-only > "$TMPDIR/changed_files.txt"
208            ;;
209        commit)
210            log "Getting commit $TARGET diff..."
211            git show "$TARGET" --format="" > "$TMPDIR/changes.diff"
212            git show "$TARGET" --format="" --name-only > "$TMPDIR/changed_files.txt"
213            
214            # Default to immediate for commits
215            OUTPUT_MODE="${OUTPUT_MODE:-immediate}"
216            ADD_PREVIEW_CALLOUT="${ADD_PREVIEW_CALLOUT:-false}"
217            ;;
218        diff)
219            if [[ ! -f "$TARGET" ]]; then
220                error "Diff file not found: $TARGET"
221            fi
222            log "Using provided diff file..."
223            cp "$TARGET" "$TMPDIR/changes.diff"
224            grep -E '^\+\+\+ b/' "$TARGET" | sed 's|^+++ b/||' > "$TMPDIR/changed_files.txt" || true
225            
226            OUTPUT_MODE="${OUTPUT_MODE:-immediate}"
227            ADD_PREVIEW_CALLOUT="${ADD_PREVIEW_CALLOUT:-false}"
228            ;;
229        staged)
230            log "Getting staged changes..."
231            git diff --cached > "$TMPDIR/changes.diff"
232            git diff --cached --name-only > "$TMPDIR/changed_files.txt"
233            
234            OUTPUT_MODE="${OUTPUT_MODE:-immediate}"
235            ADD_PREVIEW_CALLOUT="${ADD_PREVIEW_CALLOUT:-false}"
236            ;;
237    esac
238
239    if [[ ! -s "$TMPDIR/changes.diff" ]]; then
240        error "No changes found"
241    fi
242
243    log "Found $(wc -l < "$TMPDIR/changed_files.txt" | tr -d ' ') changed files"
244    log "Output mode: $OUTPUT_MODE, Preview callout: $ADD_PREVIEW_CALLOUT"
245}
246
247# ============================================================================
248# Step 2: Filter to relevant changes
249# ============================================================================
250
251filter_changes() {
252    log "Filtering to documentation-relevant changes..."
253    
254    # Keep only source code changes (not tests, not CI, not docs themselves)
255    grep -E '^crates/.*\.rs$' "$TMPDIR/changed_files.txt" | \
256        grep -v '_test\.rs$' | \
257        grep -v '/tests/' | \
258        grep -v '/test_' > "$TMPDIR/source_files.txt" || true
259    
260    # Also track if settings/keybindings changed
261    grep -E '(settings|keymap|actions)' "$TMPDIR/changed_files.txt" > "$TMPDIR/config_files.txt" || true
262    
263    local source_count=$(wc -l < "$TMPDIR/source_files.txt" | tr -d ' ')
264    local config_count=$(wc -l < "$TMPDIR/config_files.txt" | tr -d ' ')
265    
266    log "Relevant files: $source_count source, $config_count config"
267    
268    if [[ "$source_count" -eq 0 && "$config_count" -eq 0 ]]; then
269        echo "No documentation-relevant changes detected (only tests, CI, or docs modified)."
270        exit 0
271    fi
272}
273
274# ============================================================================
275# Step 3: Assemble context
276# ============================================================================
277
278assemble_context() {
279    log "Assembling context..."
280    
281    # Start the prompt
282    cat > "$TMPDIR/prompt.md" << 'PROMPT_HEADER'
283# Documentation Suggestion Request
284
285You are analyzing code changes to determine if documentation updates are needed.
286
287Before analysis, read and follow these rule files:
288- `.rules`
289- `docs/.rules`
290
291## Your Task
292
2931. Analyze the diff below for user-facing changes
2942. Determine if any documentation updates are warranted
2953. If yes, provide specific, actionable suggestions
2964. If no, explain why no updates are needed
297
298## Guidelines
299
300PROMPT_HEADER
301
302    # Add conventions
303    log "Adding documentation conventions..."
304    cat >> "$TMPDIR/prompt.md" << 'CONVENTIONS'
305### Documentation Conventions
306
307- **Voice**: Second person ("you"), present tense, direct and concise
308- **No hedging**: Avoid "simply", "just", "easily"
309- **Settings pattern**: Show Settings Editor UI first, then JSON as alternative
310- **Keybindings**: Use `{#kb action::Name}` syntax, not hardcoded keys
311- **Terminology**: "folder" not "directory", "project" not "workspace", "Settings Editor" not "settings UI"
312- **SEO keyword targeting**: For each docs page you suggest updating, choose one
313  primary keyword/intent phrase using the page's user intent
314- **SEO metadata**: Every updated/new docs page should include frontmatter with
315  `title` and `description` (single-line `key: value` entries)
316- **Metadata quality**: Titles should clearly state page intent (~50-60 chars),
317  descriptions should summarize the reader outcome (~140-160 chars)
318- **Keyword usage**: Use the primary keyword naturally in frontmatter and in page
319  body at least once; never keyword-stuff
320- **SEO structure**: Keep exactly one H1 and preserve logical H1→H2→H3
321  hierarchy
322- **Internal links minimum**: Non-reference pages should include at least 3
323  useful internal docs links; reference pages can include fewer when extra links
324  would be noise
325- **Marketing links**: For main feature pages, include a relevant `zed.dev`
326  marketing link alongside docs links
327
328### Brand Voice Rubric (Required)
329
330For suggested doc text, apply the brand rubric scoring exactly and only pass text
331that scores 4+ on every criterion:
332
333| Criterion            |
334| -------------------- |
335| Technical Grounding  |
336| Natural Syntax       |
337| Quiet Confidence     |
338| Developer Respect    |
339| Information Priority |
340| Specificity          |
341| Voice Consistency    |
342| Earned Claims        |
343
344Pass threshold: all criteria 4+ (minimum 32/40 total).
345
346Also reject suggestions containing obvious taboo phrasing (hype, emotional
347manipulation, or marketing-style superlatives).
348
349For every docs file you suggest changing, treat the entire file as in scope for
350brand review (not only the edited section). Include any additional full-file
351voice fixes needed to reach passing rubric scores.
352
353### What Requires Documentation
354
355- New user-facing features or commands
356- Changed keybindings or default behaviors
357- New or modified settings
358- Deprecated or removed functionality
359
360### What Does NOT Require Documentation
361
362- Internal refactoring without behavioral changes
363- Performance optimizations (unless user-visible)
364- Bug fixes that restore documented behavior
365- Test changes, CI changes
366CONVENTIONS
367
368    # Add preview callout instructions if needed
369    if [[ "$ADD_PREVIEW_CALLOUT" == "true" ]]; then
370        # Get current preview version for modification callouts
371        local preview_version
372        preview_version=$(gh release list --limit 5 2>/dev/null | grep -E '\-pre\s' | head -1 | grep -oE 'v[0-9]+\.[0-9]+' | head -1 || echo "v0.XXX")
373        preview_version="${preview_version#v}"  # Remove leading 'v'
374        
375        cat >> "$TMPDIR/prompt.md" << PREVIEW_INSTRUCTIONS
376
377### Preview Release Callouts
378
379This change is going into Zed Preview first. Use the appropriate callout based on the type of change:
380
381#### For NEW/ADDITIVE features (new commands, new settings, new UI elements):
382
383\`\`\`markdown
384> **Preview:** This feature is available in Zed Preview. It will be included in the next Stable release.
385\`\`\`
386
387#### For BEHAVIOR MODIFICATIONS (changed defaults, altered behavior of existing features):
388
389\`\`\`markdown
390> **Changed in Preview (v${preview_version}).** See [release notes](/releases#${preview_version}).
391\`\`\`
392
393**Guidelines:**
394- Use the "Preview" callout for entirely new features or sections
395- Use the "Changed in Preview" callout when modifying documentation of existing behavior
396- Place callouts immediately after the section heading, before any content
397- Both callout types will be stripped when the feature ships to Stable
398PREVIEW_INSTRUCTIONS
399    fi
400
401    echo "" >> "$TMPDIR/prompt.md"
402    echo "## Changed Files" >> "$TMPDIR/prompt.md"
403    echo "" >> "$TMPDIR/prompt.md"
404    echo '```' >> "$TMPDIR/prompt.md"
405    cat "$TMPDIR/changed_files.txt" >> "$TMPDIR/prompt.md"
406    echo '```' >> "$TMPDIR/prompt.md"
407    echo "" >> "$TMPDIR/prompt.md"
408    
409    # Add the diff (truncated if huge)
410    echo "## Code Diff" >> "$TMPDIR/prompt.md"
411    echo "" >> "$TMPDIR/prompt.md"
412    
413    local diff_lines=$(wc -l < "$TMPDIR/changes.diff" | tr -d ' ')
414    if [[ "$diff_lines" -gt 2000 ]]; then
415        warn "Diff is large ($diff_lines lines), truncating to 2000 lines"
416        echo '```diff' >> "$TMPDIR/prompt.md"
417        head -2000 "$TMPDIR/changes.diff" >> "$TMPDIR/prompt.md"
418        echo "" >> "$TMPDIR/prompt.md"
419        echo "[... truncated, $((diff_lines - 2000)) more lines ...]" >> "$TMPDIR/prompt.md"
420        echo '```' >> "$TMPDIR/prompt.md"
421    else
422        echo '```diff' >> "$TMPDIR/prompt.md"
423        cat "$TMPDIR/changes.diff" >> "$TMPDIR/prompt.md"
424        echo '```' >> "$TMPDIR/prompt.md"
425    fi
426    
427    # Add output format instructions
428    cat >> "$TMPDIR/prompt.md" << 'OUTPUT_FORMAT'
429
430## Output Format
431
432Respond with ONE of these formats:
433
434### If documentation updates ARE needed:
435
436```markdown
437## Documentation Suggestions
438
439### Summary
440[1-2 sentence summary of what changed and why docs need updating]
441
442### Suggested Changes
443
444#### 1. [docs/src/path/to/file.md]
445- **Section**: [existing section to update, or "New section"]
446- **Change**: [Add/Update/Remove]
447- **Target keyword**: [single keyword/intent phrase for this page]
448- **Frontmatter**:
449  ```yaml
450  ---
451  title: ...
452  description: ...
453  ---
454  ```
455- **Links**: [List at least 3 internal docs links for non-reference pages; if
456  this is a main feature page, include one relevant `zed.dev` marketing link]
457- **Suggestion**: [Specific text or description of what to add/change]
458- **Full-file brand pass**: [Required: yes. Note any additional voice edits
459  elsewhere in the same file needed to pass rubric across the entire file.]
460- **Brand voice scorecard**:
461
462  | Criterion            | Score | Notes |
463  | -------------------- | ----- | ----- |
464  | Technical Grounding  | /5    |       |
465  | Natural Syntax       | /5    |       |
466  | Quiet Confidence     | /5    |       |
467  | Developer Respect    | /5    |       |
468  | Information Priority | /5    |       |
469  | Specificity          | /5    |       |
470  | Voice Consistency    | /5    |       |
471  | Earned Claims        | /5    |       |
472  | **TOTAL**            | /40   |       |
473
474  Pass threshold: all criteria 4+.
475
476#### 2. [docs/src/another/file.md]
477...
478
479### Notes for Reviewer
480[Any context or uncertainty worth flagging]
481```
482
483### If NO documentation updates are needed:
484
485```markdown
486## No Documentation Updates Needed
487
488**Reason**: [Brief explanation - e.g., "Internal refactoring only", "Test changes", "Bug fix restoring existing behavior"]
489
490**Changes reviewed**:
491- [Brief summary of what the code changes do]
492- [Why they don't affect user-facing documentation]
493```
494
495Be conservative. Only suggest documentation changes when there's a clear user-facing impact.
496OUTPUT_FORMAT
497
498    log "Context assembled: $(wc -l < "$TMPDIR/prompt.md" | tr -d ' ') lines"
499}
500
501# ============================================================================
502# Step 4: Run the analysis
503# ============================================================================
504
505run_analysis() {
506    if [[ "$DRY_RUN" == "true" ]]; then
507        echo -e "${GREEN}=== DRY RUN: Assembled Context ===${NC}"
508        echo ""
509        echo "Output mode: $OUTPUT_MODE"
510        echo "Preview callout: $ADD_PREVIEW_CALLOUT"
511        if [[ "$OUTPUT_MODE" == "batch" ]]; then
512            echo "Batch file: $BATCH_FILE"
513        fi
514        echo ""
515        cat "$TMPDIR/prompt.md"
516        echo ""
517        echo -e "${GREEN}=== End Context ===${NC}"
518        echo ""
519        echo "To run for real, remove --dry-run flag"
520        return
521    fi
522    
523    # Check for droid CLI
524    if ! command -v droid &> /dev/null; then
525        error "droid CLI required. Install from: https://app.factory.ai/cli"
526    fi
527    
528    log "Running analysis with model: $MODEL"
529    
530    # Run the LLM
531    local suggestions
532    suggestions=$(droid exec -m "$MODEL" -f "$TMPDIR/prompt.md")
533    
534    # Handle output based on mode
535    if [[ "$OUTPUT_MODE" == "batch" ]]; then
536        append_to_batch "$suggestions"
537    else
538        output_immediate "$suggestions"
539    fi
540}
541
542# ============================================================================
543# Output handlers
544# ============================================================================
545
546append_to_batch() {
547    local suggestions="$1"
548    
549    # Check if suggestions indicate no updates needed
550    if echo "$suggestions" | grep -q "No Documentation Updates Needed"; then
551        log "No documentation updates needed, skipping batch"
552        echo "$suggestions"
553        return
554    fi
555    
556    # Create batch directory if needed
557    mkdir -p "$BATCH_DIR"
558    
559    # Get PR info if available
560    local pr_number=""
561    local pr_title=""
562    if [[ -f "$TMPDIR/pr_number" ]]; then
563        pr_number=$(cat "$TMPDIR/pr_number")
564        pr_title=$(cat "$TMPDIR/pr_title")
565    fi
566    
567    # Initialize batch file if it doesn't exist
568    if [[ ! -f "$BATCH_FILE" ]]; then
569        cat > "$BATCH_FILE" << 'BATCH_HEADER'
570# Pending Documentation Suggestions
571
572This file contains batched documentation suggestions for the next Preview release.
573Run `script/docs-suggest-publish` to create a PR from these suggestions.
574
575---
576
577BATCH_HEADER
578    fi
579    
580    # Append suggestions with metadata
581    {
582        echo ""
583        echo "## PR #$pr_number: $pr_title"
584        echo ""
585        echo "_Added: $(date -u +%Y-%m-%dT%H:%M:%SZ)_"
586        echo ""
587        echo "$suggestions"
588        echo ""
589        echo "---"
590    } >> "$BATCH_FILE"
591    
592    echo -e "${GREEN}Suggestions batched to:${NC} $BATCH_FILE"
593    echo ""
594    echo "Batched suggestions for PR #$pr_number"
595    echo "Run 'script/docs-suggest-publish' when ready to create the docs PR."
596}
597
598output_immediate() {
599    local suggestions="$1"
600    
601    if [[ -n "$OUTPUT" ]]; then
602        echo "$suggestions" > "$OUTPUT"
603        echo "Suggestions written to: $OUTPUT"
604    else
605        echo "$suggestions"
606    fi
607}
608
609# ============================================================================
610# Main
611# ============================================================================
612
613main() {
614    get_diff
615    filter_changes
616    assemble_context
617    run_analysis
618}
619
620main