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