diff --git a/.github/styles/config/vocabularies/Docs/accept.txt b/.github/styles/config/vocabularies/Docs/accept.txt index aecd4bca36..43d1be0745 100644 --- a/.github/styles/config/vocabularies/Docs/accept.txt +++ b/.github/styles/config/vocabularies/Docs/accept.txt @@ -1 +1,41 @@ Astro +nf-core +Nextflow +Seqera +SeqeraLabs +Bytesize +Conda +Mamba +Bioconda +Singularity +Apptainer +Docker +GitHub +GitLab +Bitbucket +AWS +Azure +GCP +S3 +Kubernetes +Slurm +PBS +LSF +SGE +TORQUE +HTCondor +Podman +Charliecloud +Shifter +Spack +OpenEBench +MultiQC +FastQC +nf-test +DSL2 +Groovy +CWL +WDL +Snakemake +Prefect +Cromwell diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 2aa08b8477..6245da64b0 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -5,26 +5,36 @@ on: - main pull_request: +permissions: + contents: read + pull-requests: write + checks: write + jobs: pre-commit: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: 20 - run: npm install --only=dev + - name: Install custom mdx2vast for MDX directive vale support + run: npm install -g https://github.com/edmundmiller/mdx2vast.git - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 vale: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Install Vale - run: | - wget https://github.com/errata-ai/vale/releases/download/v3.0.5/vale_3.0.5_Linux_64-bit.tar.gz -O vale.tar.gz - tar -xvzf vale.tar.gz vale - rm vale.tar.gz - - name: Spell check + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - name: Install custom mdx2vast for MDX directive support + run: npm install -g https://github.com/edmundmiller/mdx2vast.git + - name: Verify Vale setup run: | - ./vale sync - ./vale ${{inputs.DOC_SRC}} + echo "Vale config:" && cat .vale.ini + echo "mdx2vast installed:" && which mdx2vast + - uses: errata-ai/vale-action@v2.1.1 + with: + files: sites/main-site/src/content,sites/docs/src/content,sites/configs/src/content,sites/modules-subworkflows/src/content,sites/pipelines/src/content + reporter: github-pr-check + fail_on_error: true + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 53e03b8be6..17f64dfb9e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,3 +9,12 @@ repos: - prettier-plugin-svelte@3.4.0 - prettier-plugin-astro@0.14.1 files: \.(astro|svelte|mdx|md|yml|yaml)$ + - repo: https://github.com/errata-ai/vale + rev: v3.7.1 + hooks: + - id: vale + name: vale sync + pass_filenames: false + args: [sync] + - id: vale + args: [--output=line, --minAlertLevel=error] diff --git a/.vale.ini b/.vale.ini index 7a9265d150..24711070b4 100644 --- a/.vale.ini +++ b/.vale.ini @@ -1,20 +1,87 @@ StylesPath = .github/styles -MinAlertLevel = suggestion +MinAlertLevel = warning -Packages = https://github.com/nf-core/vale/releases/latest/download/nf-core.zip +Packages = MDX, https://github.com/nf-core/vale/releases/latest/download/nf-core.zip Vocab = Docs -[*.md] +# Global settings for all Markdown and MDX files +[*.{md,mdx}] BasedOnStyles = nf-core, Vale Vale.Spelling = NO -[src/content/events/**] +# Blog posts - apply standard rules with relaxed personal pronouns +[sites/main-site/src/content/blog/**/*.{md,mdx}] +BasedOnStyles = nf-core, Vale +Vale.Spelling = NO nf-core.We = NO -nf-core.SeqeraLabs = NO -[src/content/events/2024] -nf-core.SeqeraLabs = YES +# Documentation - stricter rules for formal documentation +[sites/docs/src/content/**/*.{md,mdx}] +BasedOnStyles = nf-core, Vale +Vale.Spelling = NO + + +# Component and pipeline pages +[sites/modules-subworkflows/src/content/**/*.{md,mdx}] +BasedOnStyles = nf-core, Vale +Vale.Spelling = NO + +[sites/pipelines/src/content/**/*.{md,mdx}] +BasedOnStyles = nf-core, Vale +Vale.Spelling = NO -[src/content/events/*/bytesize_*.md] +# Config pages +[sites/configs/src/content/**/*.{md,mdx}] BasedOnStyles = nf-core, Vale Vale.Spelling = NO + +# Ignore API reference documentation (auto-generated) - must be at end +[sites/docs/src/content/api_reference/**/*.md] +BasedOnStyles = + +[sites/docs/src/content/api_reference/**/*.mdx] +BasedOnStyles = + +# Ignore events from previous years only (2024 and before) - must be at end +[sites/main-site/src/content/events/2018/**/*.md] +BasedOnStyles = + +[sites/main-site/src/content/events/2019/**/*.md] +BasedOnStyles = + +[sites/main-site/src/content/events/2020/**/*.md] +BasedOnStyles = + +[sites/main-site/src/content/events/2021/**/*.md] +BasedOnStyles = + +[sites/main-site/src/content/events/2022/**/*.md] +BasedOnStyles = + +[sites/main-site/src/content/events/2023/**/*.md] +BasedOnStyles = + +[sites/main-site/src/content/events/2024/**/*.md] +BasedOnStyles = + +[sites/main-site/src/content/events/2018/**/*.mdx] +BasedOnStyles = + +[sites/main-site/src/content/events/2019/**/*.mdx] +BasedOnStyles = + +[sites/main-site/src/content/events/2020/**/*.mdx] +BasedOnStyles = + +[sites/main-site/src/content/events/2021/**/*.mdx] +BasedOnStyles = + +[sites/main-site/src/content/events/2022/**/*.mdx] +BasedOnStyles = + +[sites/main-site/src/content/events/2023/**/*.mdx] +BasedOnStyles = + +[sites/main-site/src/content/events/2024/**/*.mdx] +BasedOnStyles = + diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..076895e9d0 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,28 @@ +{ + "recommendations": [ + // Documentation and linting + "errata-ai.vale-server", + "davidanson.vscode-markdownlint", + "streetsidesoftware.code-spell-checker", + + // Formatting and Prettier + "esbenp.prettier-vscode", + + // Astro and web development + "astro-build.astro-vscode", + "bradlc.vscode-tailwindcss", + + // YAML and configuration files + "redhat.vscode-yaml", + + // Git and version control + "eamodio.gitlens", + + // Commitizen support + "commitizen.git-conventional-commits", + + // Link checking (if available) + "tchayen.markdown-links" + ], + "unwantedRecommendations": [] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 750775e7fb..40767f3a69 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,48 @@ { "markdown.styles": [ "public/vscode_markdown.css" - ] + ], + + "// Documentation linting and formatting": "", + "vale.valeCLI.config": ".vale.ini", + "vale.valeCLI.path": "vale", + "vale.core.useCLI": true, + + "[markdown]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.rulers": [120], + "editor.wordWrap": "wordWrapColumn", + "editor.wordWrapColumn": 120 + }, + + "// MDX settings": "", + "[mdx]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.rulers": [120], + "editor.wordWrap": "wordWrapColumn", + "editor.wordWrapColumn": 120 + }, + + "// YAML settings": "", + "[yaml]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.insertSpaces": true, + "editor.tabSize": 2 + }, + + "// Astro settings": "", + "[astro]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + + "// File explorer": "", + "files.exclude": { + "**/node_modules": true, + "**/dist": true, + "**/.astro": true + } } diff --git a/hooks/README.md b/hooks/README.md new file mode 100644 index 0000000000..85c61d518d --- /dev/null +++ b/hooks/README.md @@ -0,0 +1,188 @@ +# Claude Code Hooks for nf-core Website + +This directory contains Claude Code hooks that automatically format and lint files when Claude modifies them, providing real-time feedback on prose quality using Python for robust JSON processing and integration. + +## Available Hooks + +### šŸ”§ `post-tool.py` + +**Triggers**: After Claude uses Write, Edit, or MultiEdit tools +**Implementation**: Python 3 with structured JSON input/output +**Actions**: + +- Reads Claude Code's structured JSON input via stdin +- Runs `prettier --write` on modified files for consistent formatting +- Runs `vale` prose linting on markdown/MDX files +- Provides immediate feedback on writing quality to Claude via JSON output +- Respects existing `.vale.ini` configuration +- Returns `additionalContext` to Claude when prose issues are found + +### šŸ“‹ `user-prompt-submit.py` + +**Triggers**: When user submits documentation-related prompts +**Implementation**: Python 3 with smart prompt detection +**Actions**: + +- Reads user prompt from Claude Code's JSON input +- Checks recently modified documentation files (last 10 minutes) +- Runs Vale prose quality checks on recent files +- Only activates for documentation-related prompts (auto-detects keywords) +- Provides context to Claude via JSON output about documentation quality +- Adds guidance on nf-core documentation standards + +### šŸ› ļø `utils.py` + +**Purpose**: Shared Python utilities for all hooks +**Functions**: + +- JSON input/output handling for Claude Code integration +- Tool availability checking (prettier, vale) +- File type filtering and validation +- Robust formatting and linting with error handling +- Recent file detection and processing +- Claude Code hook output formatting + +## Setup Instructions + +### 1. Configure Claude Code Settings + +The hooks are automatically configured in `.claude/settings.local.json`: + +```json +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/hooks/post-tool.py" + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/hooks/user-prompt-submit.py" + } + ] + } + ] + } +} +``` + +### 2. Verify Dependencies + +The hooks work best when these tools are installed: + +- **Python 3** - Hook implementation language (system default) +- **prettier** - Code formatting (`npm install -g prettier`) +- **vale** - Prose linting (`brew install vale`) + +### 3. Test the Setup + +```bash +# Test post-tool hook with proper Claude Code JSON input +echo '{"tool_name":"Write","tool_input":{"file_path":"test.md"}}' | ./hooks/post-tool.py + +# Test user-prompt-submit hook with documentation prompt +echo '{"prompt":"Help me write documentation"}' | ./hooks/user-prompt-submit.py + +# Test user-prompt-submit hook with non-documentation prompt (should exit silently) +echo '{"prompt":"What is the weather?"}' | ./hooks/user-prompt-submit.py +``` + +## How It Works + +### Post-Tool Hook Flow + +1. **Claude modifies files** using Write/Edit/MultiEdit +2. **Hook triggers** automatically after tool execution +3. **Prettier formats** the modified files for consistency +4. **Vale checks prose** quality and terminology +5. **Feedback provided** to Claude about writing quality + +### User-Prompt Hook Flow + +1. **User submits prompt** containing documentation keywords +2. **Hook scans** for recently modified documentation files +3. **Vale checks** prose quality of recent changes +4. **Summary provided** before Claude responds + +### Smart Filtering + +- **File types**: Only processes `.md`, `.mdx`, `.js`, `.ts`, `.astro`, `.yml`, `.yaml` +- **Vale linting**: Only on markdown/MDX files +- **Respects `.vale.ini`**: Ignores API reference docs and old events +- **Performance**: Only processes changed files + +## Benefits + +### For Claude + +- āœ… **Real-time feedback** on prose quality +- āœ… **Learning opportunity** from Vale suggestions +- āœ… **Consistent formatting** via prettier +- āœ… **Terminology awareness** (nf-core specific terms) + +### For Contributors + +- āœ… **Automatic formatting** of Claude-generated content +- āœ… **Prose quality assurance** built into the workflow +- āœ… **Consistent style** across all documentation +- āœ… **No additional setup** required + +### For Project + +- āœ… **Maintains high documentation standards** +- āœ… **Reduces review overhead** for maintainers +- āœ… **Ensures consistent terminology** usage +- āœ… **Professional documentation** quality + +## Example Output + +When Claude modifies a file with prose issues: + +``` +šŸ¤– Claude Code Hook: Post-Tool Formatting & Linting +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +šŸ”§ Tool used: Edit +šŸ“ Processing files: + → docs/new-guide.md +šŸŽØ Formatting docs/new-guide.md with prettier... +āœ… Formatted successfully +šŸ“ Checking prose quality with Vale... +šŸ“Š Vale found prose suggestions: + 7:8 error Use 'nf-core' instead of 'nf_core' + 9:5 error Use 'Bytesize' instead of 'bytesize' + +šŸ’” These suggestions help improve documentation clarity and consistency. +``` + +## Troubleshooting + +### Hook Not Running + +- Verify hooks are configured in Claude Code settings +- Check that hook files are executable (`chmod +x hooks/*.sh`) +- Ensure you're in the correct directory + +### Missing Tools + +- Install prettier: `npm install -g prettier` +- Install Vale: `brew install vale` +- Hooks will gracefully skip missing tools + +### Vale Issues + +- Hooks respect your existing `.vale.ini` configuration +- API reference docs and old events are automatically ignored +- Vale suggestions are informational, not blocking + +This system creates a seamless feedback loop where Claude learns from its writing and continuously improves documentation quality! šŸš€ diff --git a/hooks/post-tool.py b/hooks/post-tool.py new file mode 100755 index 0000000000..c3d78790ab --- /dev/null +++ b/hooks/post-tool.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +""" +Claude Code Post-Tool Hook + +Automatically runs after Claude uses Write, Edit, or MultiEdit tools. +Provides real-time formatting with prettier and prose quality checking with Vale. +""" + +import sys +from pathlib import Path + +# Add hooks directory to path for imports +hooks_dir = Path(__file__).parent +sys.path.insert(0, str(hooks_dir)) + +from utils import ( + read_hook_input, write_json_output, print_header, print_separator, + get_file_paths_from_tool_input, format_file_with_prettier, + lint_file_with_vale, should_process_file, should_lint_with_vale +) + + +def main(): + """Main hook execution.""" + # Read input from Claude Code + input_data = read_hook_input() + + if not input_data: + # No input available - exit silently + sys.exit(0) + + # Extract relevant information + tool_name = input_data.get('tool_name', 'Unknown') + tool_input = input_data.get('tool_input', {}) + tool_response = input_data.get('tool_response', {}) + + # Get file paths to process + file_paths = get_file_paths_from_tool_input(tool_input, tool_name) + + if not file_paths: + # No files to process - exit silently + sys.exit(0) + + # Track processing results + processed_files = [] + formatting_results = [] + vale_results = [] + has_prose_issues = False + + print_header("Post-Tool Formatting & Linting") + print(f"šŸ”§ Tool used: {tool_name}") + print("šŸ“ Processing files:") + + # Process each file + for file_path in file_paths: + print(f" → {file_path}") + + # Skip if file doesn't exist + if not Path(file_path).exists(): + print(f" āš ļø File not found: {file_path}") + continue + + processed_files.append(file_path) + + # Format with prettier + if should_process_file(file_path): + print(f"šŸŽØ Formatting {file_path} with prettier...") + format_result = format_file_with_prettier(file_path) + formatting_results.append({ + 'file': file_path, + 'result': format_result + }) + + if format_result['success']: + print(f"āœ… {format_result['message']}") + else: + print(f"āš ļø {format_result['message']}") + else: + print(f"ā­ļø Skipping formatting (unsupported file type)") + + # Lint with Vale + if should_lint_with_vale(file_path): + print(f"šŸ“ Checking prose quality with Vale...") + vale_result = lint_file_with_vale(file_path) + vale_results.append({ + 'file': file_path, + 'result': vale_result + }) + + if vale_result['success']: + if vale_result['issues']: + has_prose_issues = True + print(f"šŸ“Š Vale found prose suggestions:") + # Indent the Vale output + for line in vale_result['output'].split('\n'): + if line.strip(): + print(f" {line}") + print("") + else: + print(f"āœ… Vale: No prose issues found") + else: + print(f"āš ļø Vale: {vale_result['message']}") + else: + print(f"ā­ļø Skipping Vale check (not a markdown file)") + + print() # Add spacing between files + + # Summary + if processed_files: + print("šŸŽÆ File processing complete!") + + if has_prose_issues: + print() + print("šŸ“š Vale found some prose suggestions above. These help ensure:") + print(" • Consistent terminology (nf-core, Nextflow, etc.)") + print(" • Clear, readable documentation") + print(" • Professional writing style") + print() + print("šŸ’” Consider reviewing and addressing these suggestions for better documentation quality.") + else: + all_vale_passed = all( + not result['result'].get('issues', False) + for result in vale_results + if result['result'].get('success', False) + ) + if all_vale_passed and vale_results: + print("✨ All prose checks passed - excellent writing!") + + # Provide additional context to Claude if there are issues + if has_prose_issues: + additional_context = [] + additional_context.append("Note: The files you just modified have some prose quality suggestions from Vale:") + + for result in vale_results: + if result['result'].get('issues'): + additional_context.append(f"- {result['file']}: Found terminology and style suggestions") + + additional_context.append("These suggestions help maintain consistent nf-core documentation standards.") + + # Return structured output with additional context for Claude + output = { + "hookSpecificOutput": { + "hookEventName": "PostToolUse", + "additionalContext": "\n".join(additional_context) + } + } + write_json_output(output) + else: + # Exit successfully without additional feedback + pass + + else: + print("ā„¹ļø No eligible files found to process") + + print_separator() + print("āœ… Hook execution complete") + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\nāŒ Hook interrupted", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"āŒ Hook error: {str(e)}", file=sys.stderr) + sys.exit(1) \ No newline at end of file diff --git a/hooks/user-prompt-submit.py b/hooks/user-prompt-submit.py new file mode 100755 index 0000000000..0b3296fa6a --- /dev/null +++ b/hooks/user-prompt-submit.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +Claude Code User Prompt Submit Hook + +Runs when user submits a prompt. Provides quick prose quality check +on recently modified documentation and adds relevant context for Claude. +""" + +import sys +from pathlib import Path + +# Add hooks directory to path for imports +hooks_dir = Path(__file__).parent +sys.path.insert(0, str(hooks_dir)) + +from utils import ( + read_hook_input, write_json_output, print_header, print_separator, + get_recent_documentation_files, lint_file_with_vale, + is_documentation_related_prompt +) + + +def main(): + """Main hook execution.""" + # Read input from Claude Code + input_data = read_hook_input() + + if not input_data: + # No input available - exit silently + sys.exit(0) + + # Extract the user prompt + user_prompt = input_data.get('prompt', '') + + if not user_prompt: + # No prompt provided - exit silently + sys.exit(0) + + # Only run if the prompt is related to documentation/writing + if not is_documentation_related_prompt(user_prompt): + # Not a documentation-related prompt, exit silently + sys.exit(0) + + # Get recently modified documentation files + recent_files = get_recent_documentation_files(minutes=10) + + if not recent_files: + # No recent documentation files, but still provide context about documentation intent + additional_context = ( + "The user's prompt appears to be documentation-related. " + "Consider documentation best practices: consistent terminology (nf-core, Nextflow), " + "clear structure, and professional writing style." + ) + + output = { + "hookSpecificOutput": { + "hookEventName": "UserPromptSubmit", + "additionalContext": additional_context + } + } + write_json_output(output) + sys.exit(0) + + print_header("Documentation Quality Check") + print("šŸ” Checking recently modified documentation files for prose quality...") + print() + + issues_found = False + files_checked = 0 + issue_summary = [] + + for file_path in recent_files: + if not Path(file_path).exists(): + continue + + files_checked += 1 + print(f"šŸ“„ Checking: {file_path}") + + # Quick Vale check + vale_result = lint_file_with_vale(file_path) + + if vale_result['success']: + if vale_result['issues']: + issues_found = True + print(" šŸ“ Found prose suggestions:") + # Show the Vale output with proper indentation + for line in vale_result['output'].split('\n'): + if line.strip(): + print(f" {line}") + issue_summary.append(f"• {file_path}: Found prose suggestions") + else: + print(" āœ… Prose quality looks good") + else: + print(f" āš ļø Vale not available: {vale_result['message']}") + + print() + + # Summary for user display + if files_checked == 0: + print("ā„¹ļø No recent documentation files found to check") + elif issues_found: + print("šŸ“Š Summary: Found some prose suggestions in recent documentation.") + print("šŸ’” Consider these suggestions to improve clarity and consistency.") + print("šŸŽÆ Common improvements: terminology consistency, readability, style.") + else: + print("✨ Summary: All recent documentation looks great!") + print("šŸŽ‰ No prose issues found in recently modified files.") + + print_separator() + print("šŸ¤– Ready to help with your documentation request!") + print() + + # Prepare additional context for Claude + context_parts = [] + + context_parts.append("Documentation context:") + + if issues_found: + context_parts.append(f"Recent prose quality check found suggestions in {len([s for s in issue_summary])} files:") + context_parts.extend(issue_summary) + context_parts.append("Please consider nf-core documentation standards: use 'nf-core' (not 'nf_core'), 'Nextflow' (not 'nextflow'), proper capitalization, and clear writing.") + else: + context_parts.append(f"Recent prose quality check: all {files_checked} recently modified documentation files look good.") + + context_parts.append("Focus on maintaining consistent terminology and professional writing style.") + + # Return structured output with additional context + output = { + "hookSpecificOutput": { + "hookEventName": "UserPromptSubmit", + "additionalContext": "\n".join(context_parts) + } + } + write_json_output(output) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\nāŒ Hook interrupted", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"āŒ Hook error: {str(e)}", file=sys.stderr) + sys.exit(1) \ No newline at end of file diff --git a/hooks/utils.py b/hooks/utils.py new file mode 100755 index 0000000000..57365377b2 --- /dev/null +++ b/hooks/utils.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +""" +Utility functions for Claude Code hooks. + +This module provides shared functionality for file processing, formatting, +and prose quality checking across all Claude Code hooks. +""" + +import json +import subprocess +import sys +from pathlib import Path +from typing import Dict, List, Optional, Any +import shutil +import os +import time + + +def check_tool_available(tool: str) -> bool: + """Check if a tool is available in the system PATH.""" + return shutil.which(tool) is not None + + +def should_process_file(file_path: str) -> bool: + """Check if file should be processed based on extension.""" + file_path_obj = Path(file_path) + return file_path_obj.suffix.lower() in { + '.md', '.mdx', '.js', '.ts', '.tsx', '.jsx', + '.astro', '.svelte', '.css', '.scss', '.json', + '.yml', '.yaml' + } + + +def should_lint_with_vale(file_path: str) -> bool: + """Check if file should be linted with Vale.""" + file_path_obj = Path(file_path) + return file_path_obj.suffix.lower() in {'.md', '.mdx'} + + +def format_file_with_prettier(file_path: str) -> Dict[str, Any]: + """ + Format file with prettier if available. + + Returns: + Dict with 'success' bool and 'message' str + """ + if not check_tool_available('prettier'): + return { + 'success': False, + 'message': 'prettier not available' + } + + if not should_process_file(file_path): + return { + 'success': False, + 'message': f'File type not supported: {Path(file_path).suffix}' + } + + if not Path(file_path).exists(): + return { + 'success': False, + 'message': f'File does not exist: {file_path}' + } + + try: + result = subprocess.run( + ['prettier', '--write', '--log-level=error', file_path], + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode == 0: + return { + 'success': True, + 'message': 'Formatted successfully' + } + else: + return { + 'success': False, + 'message': f'Prettier failed: {result.stderr.strip()}' + } + except subprocess.TimeoutExpired: + return { + 'success': False, + 'message': 'Prettier timed out' + } + except Exception as e: + return { + 'success': False, + 'message': f'Prettier error: {str(e)}' + } + + +def lint_file_with_vale(file_path: str) -> Dict[str, Any]: + """ + Lint file with Vale if available. + + Returns: + Dict with 'success' bool, 'issues' bool, 'output' str, and 'message' str + """ + if not check_tool_available('vale'): + return { + 'success': False, + 'issues': False, + 'output': '', + 'message': 'Vale not available' + } + + if not should_lint_with_vale(file_path): + return { + 'success': True, + 'issues': False, + 'output': '', + 'message': f'File type not supported for Vale: {Path(file_path).suffix}' + } + + if not Path(file_path).exists(): + return { + 'success': False, + 'issues': False, + 'output': '', + 'message': f'File does not exist: {file_path}' + } + + try: + result = subprocess.run( + ['vale', '--config=.vale.ini', file_path], + capture_output=True, + text=True, + timeout=30 + ) + + # Vale exit codes: 0 = no issues, >0 = issues found + has_issues = result.returncode > 0 + + return { + 'success': True, + 'issues': has_issues, + 'output': result.stdout.strip() if has_issues else '', + 'message': 'Vale check completed' + } + + except subprocess.TimeoutExpired: + return { + 'success': False, + 'issues': False, + 'output': '', + 'message': 'Vale timed out' + } + except Exception as e: + return { + 'success': False, + 'issues': False, + 'output': '', + 'message': f'Vale error: {str(e)}' + } + + +def get_recent_documentation_files(minutes: int = 10) -> List[str]: + """ + Get recently modified documentation files. + + Args: + minutes: How many minutes back to look for modifications + + Returns: + List of file paths + """ + cutoff_time = time.time() - (minutes * 60) + recent_files = [] + + try: + # Find markdown and MDX files + for pattern in ['**/*.md', '**/*.mdx']: + for file_path in Path('.').glob(pattern): + if not file_path.is_file(): + continue + + # Skip API reference files (ignored in Vale config) + if 'api_reference' in str(file_path): + continue + + # Skip old events (ignored in Vale config) + if any(year in str(file_path) for year in [ + 'events/2018', 'events/2019', 'events/2020', + 'events/2021', 'events/2022', 'events/2023', 'events/2024' + ]): + continue + + # Check if file was modified recently + if file_path.stat().st_mtime > cutoff_time: + recent_files.append(str(file_path)) + + # Limit to 10 most recent files + recent_files.sort(key=lambda f: Path(f).stat().st_mtime, reverse=True) + return recent_files[:10] + + except Exception as e: + print(f"Error finding recent files: {e}", file=sys.stderr) + return [] + + +def is_documentation_related_prompt(prompt: str) -> bool: + """Check if prompt is related to documentation/writing.""" + doc_keywords = [ + 'document', 'write', 'edit', 'readme', 'guide', 'tutorial', + 'prose', 'content', 'markdown', 'mdx', 'vale', 'style', + 'writing', 'docs', 'documentation' + ] + + prompt_lower = prompt.lower() + return any(keyword in prompt_lower for keyword in doc_keywords) + + +def print_header(title: str): + """Print a formatted header.""" + print(f"\nšŸ¤– Claude Code Hook: {title}") + print("━" * 80) + print() + + +def print_separator(): + """Print a separator line.""" + print("\n" + "━" * 80 + "\n") + + +def read_hook_input() -> Dict[str, Any]: + """ + Read and parse JSON input from stdin. + + Returns: + Parsed JSON data from Claude Code + """ + try: + if sys.stdin.isatty(): + # No input available + return {} + + input_data = sys.stdin.read().strip() + if not input_data: + return {} + + return json.loads(input_data) + + except json.JSONDecodeError as e: + print(f"Error: Invalid JSON input: {e}", file=sys.stderr) + return {} + except Exception as e: + print(f"Error reading input: {e}", file=sys.stderr) + return {} + + +def write_json_output(output: Dict[str, Any]): + """Write JSON output to stdout for Claude Code.""" + try: + print(json.dumps(output)) + except Exception as e: + print(f"Error writing JSON output: {e}", file=sys.stderr) + sys.exit(1) + + +def get_file_paths_from_tool_input(tool_input: Dict[str, Any], tool_name: str) -> List[str]: + """ + Extract file paths from tool input based on tool type. + + Args: + tool_input: The tool_input from Claude Code + tool_name: The name of the tool that was used + + Returns: + List of file paths to process + """ + file_paths = [] + + if tool_name in ['Write', 'Edit', 'MultiEdit']: + # Single file operations + if 'file_path' in tool_input: + file_paths.append(tool_input['file_path']) + elif tool_name == 'NotebookEdit': + # Jupyter notebook operations + if 'notebook_path' in tool_input: + file_paths.append(tool_input['notebook_path']) + + # Filter to only existing files + return [fp for fp in file_paths if Path(fp).exists()] \ No newline at end of file diff --git a/test-scoping.md b/test-scoping.md new file mode 100644 index 0000000000..f2ad2f6f29 --- /dev/null +++ b/test-scoping.md @@ -0,0 +1,5 @@ +# Test Variable Scoping Fix + +This file has intentional prose issues: +- nf_core should be nf-core +- bytesize should be Bytesize \ No newline at end of file