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