Something went wrong. Try again.
fork of gpui
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621#!/usr/bin/env bash## Analyze code changes and suggest documentation updates.## Usage:# script/docs-suggest --pr 49177 # Analyze a PR (immediate mode)# script/docs-suggest --commit abc123 # Analyze a commit# script/docs-suggest --diff file.patch # Analyze a diff file# script/docs-suggest --staged # Analyze staged changes## Modes:# --batch Append suggestions to batch file for later PR (default for main)# --immediate Output suggestions directly (default for cherry-picks)## Options:# --dry-run Show assembled context without calling LLM# --output FILE Write suggestions to file instead of stdout (immediate mode)# --verbose Show detailed progress# --model NAME Override default model# --preview Add preview callout to suggested docs (auto-detected)## Batch mode:# Suggestions are appended to docs/.suggestions/pending.md# Use script/docs-suggest-publish to create a PR from batched suggestions## Examples:# # Analyze a PR to main (batches by default)# script/docs-suggest --pr 49100## # Analyze a cherry-pick PR (immediate by default)# script/docs-suggest --pr 49152## # Force immediate output for testing# script/docs-suggest --pr 49100 --immediate## # Dry run to see context# script/docs-suggest --pr 49100 --dry-run
set -euo pipefail
# DefaultsMODE=""TARGET=""DRY_RUN=falseVERBOSE=falseOUTPUT=""MODEL="${DROID_MODEL:-claude-sonnet-4-5-20250929}"OUTPUT_MODE="" # batch or immediate, auto-detected if not setADD_PREVIEW_CALLOUT="" # auto-detected if not set
# Colors for outputRED='\033[0;31m'GREEN='\033[0;32m'YELLOW='\033[0;33m'BLUE='\033[0;34m'NC='\033[0m' # No Color
log() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${BLUE}[docs-suggest]${NC} $*" >&2 fi}
error() { echo -e "${RED}Error:${NC} $*" >&2 exit 1}
warn() { echo -e "${YELLOW}Warning:${NC} $*" >&2}
# Parse argumentswhile [[ $# -gt 0 ]]; do case $1 in --pr) MODE="pr" TARGET="$2" shift 2 ;; --commit) MODE="commit" TARGET="$2" shift 2 ;; --diff) MODE="diff" TARGET="$2" shift 2 ;; --staged) MODE="staged" shift ;; --batch) OUTPUT_MODE="batch" shift ;; --immediate) OUTPUT_MODE="immediate" shift ;; --preview) ADD_PREVIEW_CALLOUT="true" shift ;; --no-preview) ADD_PREVIEW_CALLOUT="false" shift ;; --dry-run) DRY_RUN=true shift ;; --verbose) VERBOSE=true shift ;; --output) OUTPUT="$2" shift 2 ;; --model) MODEL="$2" shift 2 ;; -h|--help) head -42 "$0" | tail -40 exit 0 ;; *) error "Unknown option: $1" ;; esacdone
# Validate modeif [[ -z "$MODE" ]]; then error "Must specify one of: --pr, --commit, --diff, or --staged"fi
# Get repo rootREPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"cd "$REPO_ROOT"
# Batch file locationBATCH_DIR="$REPO_ROOT/docs/.suggestions"BATCH_FILE="$BATCH_DIR/pending.md"
# Temp directory for context assemblyTMPDIR=$(mktemp -d)trap 'rm -rf "$TMPDIR"' EXIT
log "Mode: $MODE, Target: ${TARGET:-staged changes}"log "Temp dir: $TMPDIR"
# ============================================================================# Step 1: Get the diff and detect context# ============================================================================
get_diff() { case $MODE in pr) if ! command -v gh &> /dev/null; then error "gh CLI required for --pr mode. Install: https://cli.github.com" fi log "Fetching PR #$TARGET info..." # Get PR metadata for auto-detection PR_JSON=$(gh pr view "$TARGET" --json baseRefName,title,number) PR_BASE=$(echo "$PR_JSON" | grep -o '"baseRefName":"[^"]*"' | cut -d'"' -f4) PR_TITLE=$(echo "$PR_JSON" | grep -o '"title":"[^"]*"' | cut -d'"' -f4) PR_NUMBER=$(echo "$PR_JSON" | grep -o '"number":[0-9]*' | cut -d':' -f2) log "PR #$PR_NUMBER: $PR_TITLE (base: $PR_BASE)" # Auto-detect output mode based on target branch if [[ -z "$OUTPUT_MODE" ]]; then if [[ "$PR_BASE" == "main" ]]; then OUTPUT_MODE="batch" log "Auto-detected: batch mode (PR targets main)" else OUTPUT_MODE="immediate" log "Auto-detected: immediate mode (PR targets $PR_BASE)" fi fi # Auto-detect preview callout if [[ -z "$ADD_PREVIEW_CALLOUT" ]]; then if [[ "$PR_BASE" == "main" ]]; then ADD_PREVIEW_CALLOUT="true" log "Auto-detected: will add preview callout (new feature going to main)" else # Cherry-pick to release branch - check if it's preview or stable ADD_PREVIEW_CALLOUT="false" log "Auto-detected: no preview callout (cherry-pick)" fi fi # Store metadata for batch mode echo "$PR_NUMBER" > "$TMPDIR/pr_number" echo "$PR_TITLE" > "$TMPDIR/pr_title" echo "$PR_BASE" > "$TMPDIR/pr_base" log "Fetching PR #$TARGET diff..." gh pr diff "$TARGET" > "$TMPDIR/changes.diff" gh pr diff "$TARGET" --name-only > "$TMPDIR/changed_files.txt" ;; commit) log "Getting commit $TARGET diff..." git show "$TARGET" --format="" > "$TMPDIR/changes.diff" git show "$TARGET" --format="" --name-only > "$TMPDIR/changed_files.txt" # Default to immediate for commits OUTPUT_MODE="${OUTPUT_MODE:-immediate}" ADD_PREVIEW_CALLOUT="${ADD_PREVIEW_CALLOUT:-false}" ;; diff) if [[ ! -f "$TARGET" ]]; then error "Diff file not found: $TARGET" fi log "Using provided diff file..." cp "$TARGET" "$TMPDIR/changes.diff" grep -E '^\+\+\+ b/' "$TARGET" | sed 's|^+++ b/||' > "$TMPDIR/changed_files.txt" || true OUTPUT_MODE="${OUTPUT_MODE:-immediate}" ADD_PREVIEW_CALLOUT="${ADD_PREVIEW_CALLOUT:-false}" ;; staged) log "Getting staged changes..." git diff --cached > "$TMPDIR/changes.diff" git diff --cached --name-only > "$TMPDIR/changed_files.txt" OUTPUT_MODE="${OUTPUT_MODE:-immediate}" ADD_PREVIEW_CALLOUT="${ADD_PREVIEW_CALLOUT:-false}" ;; esac
if [[ ! -s "$TMPDIR/changes.diff" ]]; then error "No changes found" fi
log "Found $(wc -l < "$TMPDIR/changed_files.txt" | tr -d ' ') changed files" log "Output mode: $OUTPUT_MODE, Preview callout: $ADD_PREVIEW_CALLOUT"}
# ============================================================================# Step 2: Filter to relevant changes# ============================================================================
filter_changes() { log "Filtering to documentation-relevant changes..." # Keep only source code changes (not tests, not CI, not docs themselves) grep -E '^crates/.*\.rs$' "$TMPDIR/changed_files.txt" | \ grep -v '_test\.rs$' | \ grep -v '/tests/' | \ grep -v '/test_' > "$TMPDIR/source_files.txt" || true # Also track if settings/keybindings changed grep -E '(settings|keymap|actions)' "$TMPDIR/changed_files.txt" > "$TMPDIR/config_files.txt" || true local source_count=$(wc -l < "$TMPDIR/source_files.txt" | tr -d ' ') local config_count=$(wc -l < "$TMPDIR/config_files.txt" | tr -d ' ') log "Relevant files: $source_count source, $config_count config" if [[ "$source_count" -eq 0 && "$config_count" -eq 0 ]]; then echo "No documentation-relevant changes detected (only tests, CI, or docs modified)." exit 0 fi}
# ============================================================================# Step 3: Assemble context# ============================================================================
assemble_context() { log "Assembling context..." # Start the prompt cat > "$TMPDIR/prompt.md" << 'PROMPT_HEADER'# Documentation Suggestion Request
You are analyzing code changes to determine if documentation updates are needed.
Before analysis, read and follow these rule files:- `.rules`- `docs/.rules`
## Your Task
1. Analyze the diff below for user-facing changes2. Determine if any documentation updates are warranted3. If yes, provide specific, actionable suggestions4. If no, explain why no updates are needed
## Guidelines
PROMPT_HEADER
# Add conventions log "Adding documentation conventions..." cat >> "$TMPDIR/prompt.md" << 'CONVENTIONS'### Documentation Conventions
- **Voice**: Second person ("you"), present tense, direct and concise- **No hedging**: Avoid "simply", "just", "easily"- **Settings pattern**: Show Settings Editor UI first, then JSON as alternative- **Keybindings**: Use `{#kb action::Name}` syntax, not hardcoded keys- **Terminology**: "folder" not "directory", "project" not "workspace", "Settings Editor" not "settings UI"- **SEO keyword targeting**: For each docs page you suggest updating, choose one primary keyword/intent phrase using the page's user intent- **SEO metadata**: Every updated/new docs page should include frontmatter with `title` and `description` (single-line `key: value` entries)- **Metadata quality**: Titles should clearly state page intent (~50-60 chars), descriptions should summarize the reader outcome (~140-160 chars)- **Keyword usage**: Use the primary keyword naturally in frontmatter and in page body at least once; never keyword-stuff- **SEO structure**: Keep exactly one H1 and preserve logical H1→H2→H3 hierarchy- **Internal links minimum**: Non-reference pages should include at least 3 useful internal docs links; reference pages can include fewer when extra links would be noise- **Marketing links**: For main feature pages, include a relevant `zed.dev` marketing link alongside docs links
### Brand Voice Rubric (Required)
For suggested doc text, apply the brand rubric scoring exactly and only pass textthat scores 4+ on every criterion:
| Criterion || -------------------- || Technical Grounding || Natural Syntax || Quiet Confidence || Developer Respect || Information Priority || Specificity || Voice Consistency || Earned Claims |
Pass threshold: all criteria 4+ (minimum 32/40 total).
Also reject suggestions containing obvious taboo phrasing (hype, emotionalmanipulation, or marketing-style superlatives).
For every docs file you suggest changing, treat the entire file as in scope forbrand review (not only the edited section). Include any additional full-filevoice fixes needed to reach passing rubric scores.
### What Requires Documentation
- New user-facing features or commands- Changed keybindings or default behaviors- New or modified settings- Deprecated or removed functionality
### What Does NOT Require Documentation
- Internal refactoring without behavioral changes- Performance optimizations (unless user-visible)- Bug fixes that restore documented behavior- Test changes, CI changesCONVENTIONS
# Add preview callout instructions if needed if [[ "$ADD_PREVIEW_CALLOUT" == "true" ]]; then # Get current preview version for modification callouts local preview_version 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") preview_version="${preview_version#v}" # Remove leading 'v' cat >> "$TMPDIR/prompt.md" << PREVIEW_INSTRUCTIONS
### Preview Release Callouts
This change is going into Zed Preview first. Use the appropriate callout based on the type of change:
#### For NEW/ADDITIVE features (new commands, new settings, new UI elements):
\`\`\`markdown> **Preview:** This feature is available in Zed Preview. It will be included in the next Stable release.\`\`\`
#### For BEHAVIOR MODIFICATIONS (changed defaults, altered behavior of existing features):
\`\`\`markdown> **Changed in Preview (v${preview_version}).** See [release notes](/releases#${preview_version}).\`\`\`
**Guidelines:**- Use the "Preview" callout for entirely new features or sections- Use the "Changed in Preview" callout when modifying documentation of existing behavior- Place callouts immediately after the section heading, before any content- Both callout types will be stripped when the feature ships to StablePREVIEW_INSTRUCTIONS fi
echo "" >> "$TMPDIR/prompt.md" echo "## Changed Files" >> "$TMPDIR/prompt.md" echo "" >> "$TMPDIR/prompt.md" echo '```' >> "$TMPDIR/prompt.md" cat "$TMPDIR/changed_files.txt" >> "$TMPDIR/prompt.md" echo '```' >> "$TMPDIR/prompt.md" echo "" >> "$TMPDIR/prompt.md" # Add the diff (truncated if huge) echo "## Code Diff" >> "$TMPDIR/prompt.md" echo "" >> "$TMPDIR/prompt.md" local diff_lines=$(wc -l < "$TMPDIR/changes.diff" | tr -d ' ') if [[ "$diff_lines" -gt 2000 ]]; then warn "Diff is large ($diff_lines lines), truncating to 2000 lines" echo '```diff' >> "$TMPDIR/prompt.md" head -2000 "$TMPDIR/changes.diff" >> "$TMPDIR/prompt.md" echo "" >> "$TMPDIR/prompt.md" echo "[... truncated, $((diff_lines - 2000)) more lines ...]" >> "$TMPDIR/prompt.md" echo '```' >> "$TMPDIR/prompt.md" else echo '```diff' >> "$TMPDIR/prompt.md" cat "$TMPDIR/changes.diff" >> "$TMPDIR/prompt.md" echo '```' >> "$TMPDIR/prompt.md" fi # Add output format instructions cat >> "$TMPDIR/prompt.md" << 'OUTPUT_FORMAT'
## Output Format
Respond with ONE of these formats:
### If documentation updates ARE needed:
```markdown## Documentation Suggestions
### Summary[1-2 sentence summary of what changed and why docs need updating]
### Suggested Changes
#### 1. [docs/src/path/to/file.md]- **Section**: [existing section to update, or "New section"]- **Change**: [Add/Update/Remove]- **Target keyword**: [single keyword/intent phrase for this page]- **Frontmatter**: ```yaml --- title: ... description: ... --- ```- **Links**: [List at least 3 internal docs links for non-reference pages; if this is a main feature page, include one relevant `zed.dev` marketing link]- **Suggestion**: [Specific text or description of what to add/change]- **Full-file brand pass**: [Required: yes. Note any additional voice edits elsewhere in the same file needed to pass rubric across the entire file.]- **Brand voice scorecard**:
| Criterion | Score | Notes | | -------------------- | ----- | ----- | | Technical Grounding | /5 | | | Natural Syntax | /5 | | | Quiet Confidence | /5 | | | Developer Respect | /5 | | | Information Priority | /5 | | | Specificity | /5 | | | Voice Consistency | /5 | | | Earned Claims | /5 | | | **TOTAL** | /40 | |
Pass threshold: all criteria 4+.
#### 2. [docs/src/another/file.md]...
### Notes for Reviewer[Any context or uncertainty worth flagging]```
### If NO documentation updates are needed:
```markdown## No Documentation Updates Needed
**Reason**: [Brief explanation - e.g., "Internal refactoring only", "Test changes", "Bug fix restoring existing behavior"]
**Changes reviewed**:- [Brief summary of what the code changes do]- [Why they don't affect user-facing documentation]```
Be conservative. Only suggest documentation changes when there's a clear user-facing impact.OUTPUT_FORMAT
log "Context assembled: $(wc -l < "$TMPDIR/prompt.md" | tr -d ' ') lines"}
# ============================================================================# Step 4: Run the analysis# ============================================================================
run_analysis() { if [[ "$DRY_RUN" == "true" ]]; then echo -e "${GREEN}=== DRY RUN: Assembled Context ===${NC}" echo "" echo "Output mode: $OUTPUT_MODE" echo "Preview callout: $ADD_PREVIEW_CALLOUT" if [[ "$OUTPUT_MODE" == "batch" ]]; then echo "Batch file: $BATCH_FILE" fi echo "" cat "$TMPDIR/prompt.md" echo "" echo -e "${GREEN}=== End Context ===${NC}" echo "" echo "To run for real, remove --dry-run flag" return fi # Check for droid CLI if ! command -v droid &> /dev/null; then error "droid CLI required. Install from: https://app.factory.ai/cli" fi log "Running analysis with model: $MODEL" # Run the LLM local suggestions suggestions=$(droid exec -m "$MODEL" -f "$TMPDIR/prompt.md") # Handle output based on mode if [[ "$OUTPUT_MODE" == "batch" ]]; then append_to_batch "$suggestions" else output_immediate "$suggestions" fi}
# ============================================================================# Output handlers# ============================================================================
append_to_batch() { local suggestions="$1" # Check if suggestions indicate no updates needed if echo "$suggestions" | grep -q "No Documentation Updates Needed"; then log "No documentation updates needed, skipping batch" echo "$suggestions" return fi # Create batch directory if needed mkdir -p "$BATCH_DIR" # Get PR info if available local pr_number="" local pr_title="" if [[ -f "$TMPDIR/pr_number" ]]; then pr_number=$(cat "$TMPDIR/pr_number") pr_title=$(cat "$TMPDIR/pr_title") fi # Initialize batch file if it doesn't exist if [[ ! -f "$BATCH_FILE" ]]; then cat > "$BATCH_FILE" << 'BATCH_HEADER'# Pending Documentation Suggestions
This file contains batched documentation suggestions for the next Preview release.Run `script/docs-suggest-publish` to create a PR from these suggestions.
---
BATCH_HEADER fi # Append suggestions with metadata { echo "" echo "## PR #$pr_number: $pr_title" echo "" echo "_Added: $(date -u +%Y-%m-%dT%H:%M:%SZ)_" echo "" echo "$suggestions" echo "" echo "---" } >> "$BATCH_FILE" echo -e "${GREEN}Suggestions batched to:${NC} $BATCH_FILE" echo "" echo "Batched suggestions for PR #$pr_number" echo "Run 'script/docs-suggest-publish' when ready to create the docs PR."}
output_immediate() { local suggestions="$1" if [[ -n "$OUTPUT" ]]; then echo "$suggestions" > "$OUTPUT" echo "Suggestions written to: $OUTPUT" else echo "$suggestions" fi}
# ============================================================================# Main# ============================================================================
main() { get_diff filter_changes assemble_context run_analysis}
main