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
287## Your Task
288
2891. Analyze the diff below for user-facing changes
2902. Determine if any documentation updates are warranted
2913. If yes, provide specific, actionable suggestions
2924. If no, explain why no updates are needed
293
294## Guidelines
295
296PROMPT_HEADER
297
298    # Add conventions
299    log "Adding documentation conventions..."
300    cat >> "$TMPDIR/prompt.md" << 'CONVENTIONS'
301### Documentation Conventions
302
303- **Voice**: Second person ("you"), present tense, direct and concise
304- **No hedging**: Avoid "simply", "just", "easily"
305- **Settings pattern**: Show Settings Editor UI first, then JSON as alternative
306- **Keybindings**: Use `{#kb action::Name}` syntax, not hardcoded keys
307- **Terminology**: "folder" not "directory", "project" not "workspace", "Settings Editor" not "settings UI"
308- **SEO keyword targeting**: For each docs page you suggest updating, choose one
309  primary keyword/intent phrase using the page's user intent
310- **SEO metadata**: Every updated/new docs page should include frontmatter with
311  `title` and `description` (single-line `key: value` entries)
312- **Metadata quality**: Titles should clearly state page intent (~50-60 chars),
313  descriptions should summarize the reader outcome (~140-160 chars)
314- **Keyword usage**: Use the primary keyword naturally in frontmatter and in page
315  body at least once; never keyword-stuff
316- **SEO structure**: Keep exactly one H1 and preserve logical H1→H2→H3
317  hierarchy
318- **Internal links minimum**: Non-reference pages should include at least 3
319  useful internal docs links; reference pages can include fewer when extra links
320  would be noise
321- **Marketing links**: For main feature pages, include a relevant `zed.dev`
322  marketing link alongside docs links
323
324### Brand Voice Rubric (Required)
325
326For suggested doc text, apply the brand rubric scoring exactly and only pass text
327that scores 4+ on every criterion:
328
329| Criterion            |
330| -------------------- |
331| Technical Grounding  |
332| Natural Syntax       |
333| Quiet Confidence     |
334| Developer Respect    |
335| Information Priority |
336| Specificity          |
337| Voice Consistency    |
338| Earned Claims        |
339
340Pass threshold: all criteria 4+ (minimum 32/40 total).
341
342Also reject suggestions containing obvious taboo phrasing (hype, emotional
343manipulation, or marketing-style superlatives).
344
345For every docs file you suggest changing, treat the entire file as in scope for
346brand review (not only the edited section). Include any additional full-file
347voice fixes needed to reach passing rubric scores.
348
349### What Requires Documentation
350
351- New user-facing features or commands
352- Changed keybindings or default behaviors
353- New or modified settings
354- Deprecated or removed functionality
355
356### What Does NOT Require Documentation
357
358- Internal refactoring without behavioral changes
359- Performance optimizations (unless user-visible)
360- Bug fixes that restore documented behavior
361- Test changes, CI changes
362CONVENTIONS
363
364    # Add preview callout instructions if needed
365    if [[ "$ADD_PREVIEW_CALLOUT" == "true" ]]; then
366        # Get current preview version for modification callouts
367        local preview_version
368        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")
369        preview_version="${preview_version#v}"  # Remove leading 'v'
370        
371        cat >> "$TMPDIR/prompt.md" << PREVIEW_INSTRUCTIONS
372
373### Preview Release Callouts
374
375This change is going into Zed Preview first. Use the appropriate callout based on the type of change:
376
377#### For NEW/ADDITIVE features (new commands, new settings, new UI elements):
378
379\`\`\`markdown
380> **Preview:** This feature is available in Zed Preview. It will be included in the next Stable release.
381\`\`\`
382
383#### For BEHAVIOR MODIFICATIONS (changed defaults, altered behavior of existing features):
384
385\`\`\`markdown
386> **Changed in Preview (v${preview_version}).** See [release notes](/releases#${preview_version}).
387\`\`\`
388
389**Guidelines:**
390- Use the "Preview" callout for entirely new features or sections
391- Use the "Changed in Preview" callout when modifying documentation of existing behavior
392- Place callouts immediately after the section heading, before any content
393- Both callout types will be stripped when the feature ships to Stable
394PREVIEW_INSTRUCTIONS
395    fi
396
397    echo "" >> "$TMPDIR/prompt.md"
398    echo "## Changed Files" >> "$TMPDIR/prompt.md"
399    echo "" >> "$TMPDIR/prompt.md"
400    echo '```' >> "$TMPDIR/prompt.md"
401    cat "$TMPDIR/changed_files.txt" >> "$TMPDIR/prompt.md"
402    echo '```' >> "$TMPDIR/prompt.md"
403    echo "" >> "$TMPDIR/prompt.md"
404    
405    # Add the diff (truncated if huge)
406    echo "## Code Diff" >> "$TMPDIR/prompt.md"
407    echo "" >> "$TMPDIR/prompt.md"
408    
409    local diff_lines=$(wc -l < "$TMPDIR/changes.diff" | tr -d ' ')
410    if [[ "$diff_lines" -gt 2000 ]]; then
411        warn "Diff is large ($diff_lines lines), truncating to 2000 lines"
412        echo '```diff' >> "$TMPDIR/prompt.md"
413        head -2000 "$TMPDIR/changes.diff" >> "$TMPDIR/prompt.md"
414        echo "" >> "$TMPDIR/prompt.md"
415        echo "[... truncated, $((diff_lines - 2000)) more lines ...]" >> "$TMPDIR/prompt.md"
416        echo '```' >> "$TMPDIR/prompt.md"
417    else
418        echo '```diff' >> "$TMPDIR/prompt.md"
419        cat "$TMPDIR/changes.diff" >> "$TMPDIR/prompt.md"
420        echo '```' >> "$TMPDIR/prompt.md"
421    fi
422    
423    # Add output format instructions
424    cat >> "$TMPDIR/prompt.md" << 'OUTPUT_FORMAT'
425
426## Output Format
427
428Respond with ONE of these formats:
429
430### If documentation updates ARE needed:
431
432```markdown
433## Documentation Suggestions
434
435### Summary
436[1-2 sentence summary of what changed and why docs need updating]
437
438### Suggested Changes
439
440#### 1. [docs/src/path/to/file.md]
441- **Section**: [existing section to update, or "New section"]
442- **Change**: [Add/Update/Remove]
443- **Target keyword**: [single keyword/intent phrase for this page]
444- **Frontmatter**:
445  ```yaml
446  ---
447  title: ...
448  description: ...
449  ---
450  ```
451- **Links**: [List at least 3 internal docs links for non-reference pages; if
452  this is a main feature page, include one relevant `zed.dev` marketing link]
453- **Suggestion**: [Specific text or description of what to add/change]
454- **Full-file brand pass**: [Required: yes. Note any additional voice edits
455  elsewhere in the same file needed to pass rubric across the entire file.]
456- **Brand voice scorecard**:
457
458  | Criterion            | Score | Notes |
459  | -------------------- | ----- | ----- |
460  | Technical Grounding  | /5    |       |
461  | Natural Syntax       | /5    |       |
462  | Quiet Confidence     | /5    |       |
463  | Developer Respect    | /5    |       |
464  | Information Priority | /5    |       |
465  | Specificity          | /5    |       |
466  | Voice Consistency    | /5    |       |
467  | Earned Claims        | /5    |       |
468  | **TOTAL**            | /40   |       |
469
470  Pass threshold: all criteria 4+.
471
472#### 2. [docs/src/another/file.md]
473...
474
475### Notes for Reviewer
476[Any context or uncertainty worth flagging]
477```
478
479### If NO documentation updates are needed:
480
481```markdown
482## No Documentation Updates Needed
483
484**Reason**: [Brief explanation - e.g., "Internal refactoring only", "Test changes", "Bug fix restoring existing behavior"]
485
486**Changes reviewed**:
487- [Brief summary of what the code changes do]
488- [Why they don't affect user-facing documentation]
489```
490
491Be conservative. Only suggest documentation changes when there's a clear user-facing impact.
492OUTPUT_FORMAT
493
494    log "Context assembled: $(wc -l < "$TMPDIR/prompt.md" | tr -d ' ') lines"
495}
496
497# ============================================================================
498# Step 4: Run the analysis
499# ============================================================================
500
501run_analysis() {
502    if [[ "$DRY_RUN" == "true" ]]; then
503        echo -e "${GREEN}=== DRY RUN: Assembled Context ===${NC}"
504        echo ""
505        echo "Output mode: $OUTPUT_MODE"
506        echo "Preview callout: $ADD_PREVIEW_CALLOUT"
507        if [[ "$OUTPUT_MODE" == "batch" ]]; then
508            echo "Batch file: $BATCH_FILE"
509        fi
510        echo ""
511        cat "$TMPDIR/prompt.md"
512        echo ""
513        echo -e "${GREEN}=== End Context ===${NC}"
514        echo ""
515        echo "To run for real, remove --dry-run flag"
516        return
517    fi
518    
519    # Check for droid CLI
520    if ! command -v droid &> /dev/null; then
521        error "droid CLI required. Install from: https://app.factory.ai/cli"
522    fi
523    
524    log "Running analysis with model: $MODEL"
525    
526    # Run the LLM
527    local suggestions
528    suggestions=$(droid exec -m "$MODEL" -f "$TMPDIR/prompt.md")
529    
530    # Handle output based on mode
531    if [[ "$OUTPUT_MODE" == "batch" ]]; then
532        append_to_batch "$suggestions"
533    else
534        output_immediate "$suggestions"
535    fi
536}
537
538# ============================================================================
539# Output handlers
540# ============================================================================
541
542append_to_batch() {
543    local suggestions="$1"
544    
545    # Check if suggestions indicate no updates needed
546    if echo "$suggestions" | grep -q "No Documentation Updates Needed"; then
547        log "No documentation updates needed, skipping batch"
548        echo "$suggestions"
549        return
550    fi
551    
552    # Create batch directory if needed
553    mkdir -p "$BATCH_DIR"
554    
555    # Get PR info if available
556    local pr_number=""
557    local pr_title=""
558    if [[ -f "$TMPDIR/pr_number" ]]; then
559        pr_number=$(cat "$TMPDIR/pr_number")
560        pr_title=$(cat "$TMPDIR/pr_title")
561    fi
562    
563    # Initialize batch file if it doesn't exist
564    if [[ ! -f "$BATCH_FILE" ]]; then
565        cat > "$BATCH_FILE" << 'BATCH_HEADER'
566# Pending Documentation Suggestions
567
568This file contains batched documentation suggestions for the next Preview release.
569Run `script/docs-suggest-publish` to create a PR from these suggestions.
570
571---
572
573BATCH_HEADER
574    fi
575    
576    # Append suggestions with metadata
577    {
578        echo ""
579        echo "## PR #$pr_number: $pr_title"
580        echo ""
581        echo "_Added: $(date -u +%Y-%m-%dT%H:%M:%SZ)_"
582        echo ""
583        echo "$suggestions"
584        echo ""
585        echo "---"
586    } >> "$BATCH_FILE"
587    
588    echo -e "${GREEN}Suggestions batched to:${NC} $BATCH_FILE"
589    echo ""
590    echo "Batched suggestions for PR #$pr_number"
591    echo "Run 'script/docs-suggest-publish' when ready to create the docs PR."
592}
593
594output_immediate() {
595    local suggestions="$1"
596    
597    if [[ -n "$OUTPUT" ]]; then
598        echo "$suggestions" > "$OUTPUT"
599        echo "Suggestions written to: $OUTPUT"
600    else
601        echo "$suggestions"
602    fi
603}
604
605# ============================================================================
606# Main
607# ============================================================================
608
609main() {
610    get_diff
611    filter_changes
612    assemble_context
613    run_analysis
614}
615
616main