diff --git a/.claude/skills/rubycritic/SKILL.md b/.claude/skills/rubycritic/SKILL.md
index d69ce27..1c30472 100644
--- a/.claude/skills/rubycritic/SKILL.md
+++ b/.claude/skills/rubycritic/SKILL.md
@@ -3,72 +3,156 @@ name: rubycritic
description: Integrate RubyCritic to analyze Ruby code quality and maintain high standards throughout development. Use when working on Ruby projects to check code smells, complexity, and duplication. Triggers include creating/editing Ruby files, refactoring code, reviewing code quality, or when user requests code analysis or quality checks.
---
-# RubyCritic Code Quality Integration
+
+Maintain high code quality standards in Ruby projects by integrating RubyCritic analysis into the development workflow. Automatically detect code smells, complexity issues, and duplication, providing actionable guidance for improvements.
+
-This skill integrates RubyCritic to maintain high code quality standards in Ruby projects during Claude Code sessions.
-
-## Quick Start
-
-When working on Ruby code, periodically run RubyCritic to check code quality:
+
+Run quality check on Ruby files:
```bash
scripts/check_quality.sh [path/to/ruby/files]
```
-If no path is provided, it analyzes the current directory.
+If no path is provided, analyzes the current directory. The script automatically installs RubyCritic if missing.
-## Workflow Integration
+**Immediate feedback**:
-### When to Run RubyCritic
+- Overall score (0-100)
+- File ratings (A-F)
+- Specific code smells detected
+
-Run quality checks:
+
+
+**Proactive analysis**: Run RubyCritic automatically after significant code changes:
- After creating new Ruby files or classes
-- After significant refactoring
+- After implementing complex methods (>10 lines)
+- After refactoring existing code
+- Before marking tasks as complete
- Before committing code
-- When user explicitly requests code quality analysis
-- After implementing complex methods or logic
-### Interpreting Results
+**Integration pattern**:
-RubyCritic provides:
+1. Make code changes
+2. Run `scripts/check_quality.sh [changed_files]`
+3. Review output for issues
+4. Address critical smells (if any)
+5. Re-run to verify improvements
+6. Proceed with next task
-- **Overall Score**: Project-wide quality rating (0-100)
-- **File Ratings**: A-F letter grades per file
-- **Code Smells**: Specific issues detected by Reek
-- **Complexity**: Flog scores indicating method complexity
-- **Duplication**: Flay scores showing code duplication
+**When to skip**: Simple variable renames, comment changes, or minor formatting adjustments don't require quality checks.
+
-### Quality Thresholds
+
+**Overall Score**:
-Aim for:
+- 95+ (excellent) - Maintain this standard
+- 90-94 (good) - Minor improvements possible
+- 80-89 (acceptable) - Consider refactoring
+- Below 80 - Prioritize improvements
-- **Overall Score**: 95+ (excellent), 90+ (good), 80+ (acceptable)
-- **File Ratings**: A or B ratings for all files
-- **No Critical Smells**: Address any high-priority issues immediately
+**File Ratings**:
-### Responding to Issues
+- A/B - Acceptable quality
+- C - Needs attention
+- D/F - Requires refactoring
-When RubyCritic identifies problems:
+**Issue Types**:
-1. **Review the console output** for specific issues
-2. **Prioritize critical smells** (complexity, duplication, unclear naming)
-3. **Refactor incrementally** - fix issues one at a time
-4. **Re-run analysis** after each fix to verify improvement
-5. **Explain changes** to the user if quality improves significantly
+- **Code Smells** (Reek) - Design and readability issues
+- **Complexity** (Flog) - Overly complex methods
+- **Duplication** (Flay) - Repeated code patterns
+
-## Installation Handling
+
+**Priority order**:
-The check_quality.sh script automatically:
+1. **Critical smells** - Long parameter lists, high complexity, feature envy
+2. **Duplication** - Extract shared methods or modules
+3. **Minor smells** - Unused parameters, duplicate method calls
+4. **Style issues** - Naming, organization
-- Detects if RubyCritic is installed
-- Installs it if missing (with user awareness)
-- Uses bundler if Gemfile is present
-- Falls back to system gem installation
+**Incremental fixing**:
+
+- Fix one issue at a time
+- Run analysis after each fix
+- Verify score improves
+- Explain significant improvements to user
+
+**When scores drop**:
+
+- Identify which file/method caused the drop
+- Review recent changes in that area
+- Fix immediately before continuing
+- Don't accumulate technical debt
+
+
+
+**Common errors and solutions**:
+
+**"RubyCritic not found"**: Script auto-installs, but if it fails:
+
+- Check Ruby is installed: `ruby --version`
+- Manually install: `gem install rubycritic`
+- Or add to Gemfile: `gem 'rubycritic', require: false`
+
+**"No files to analyze"**: Verify path contains `.rb` files
+
+- Check path is correct
+- Use explicit path: `scripts/check_quality.sh app/models`
+
+**"Bundler error"**: Gemfile.lock conflict
+
+- Run `bundle install` first
+- Or use system gem: `gem install rubycritic && rubycritic [path]`
+
+**Analysis hangs**: Large codebase
-## Configuration
+- Analyze specific directories instead of entire project
+- Use `--no-browser` flag to skip HTML generation
+- Consider `.rubycritic.yml` to exclude paths
-RubyCritic respects `.rubycritic.yml` if present in the project. For custom configuration, create this file with options like:
+For additional error scenarios, see [references/error-handling.md](references/error-handling.md)
+
+
+
+
+**Pre-commit quality checks**: Automatically run RubyCritic before commits:
+
+```bash
+# .git/hooks/pre-commit
+#!/bin/bash
+# Get staged Ruby files
+RUBY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.rb$')
+
+if [ -n "$RUBY_FILES" ]; then
+ echo "Running RubyCritic on staged files..."
+ scripts/check_quality.sh $RUBY_FILES
+
+ if [ $? -ne 0 ]; then
+ echo "Quality check failed. Fix issues or use --no-verify to skip."
+ exit 1
+ fi
+fi
+```
+
+**CI integration**: Add to GitHub Actions, GitLab CI, or other CI systems:
+
+```yaml
+# .github/workflows/quality.yml
+- name: Run RubyCritic
+ run: |
+ gem install rubycritic
+ rubycritic --format json --minimum-score 90
+```
+
+For complete git hooks setup and CI examples, see [references/git-hooks.md](references/git-hooks.md)
+
+
+
+**Basic configuration** (`.rubycritic.yml`):
```yaml
minimum_score: 95
@@ -80,21 +164,102 @@ paths:
no_browser: true
```
-## Output Formats
+**Common options**:
+
+- `minimum_score`: Fail if score below this threshold
+- `formats`: Output formats (console, html, json)
+- `paths`: Directories to analyze
+- `no_browser`: Don't auto-open HTML report
+
+For advanced configuration and custom thresholds, see [references/configuration.md](references/configuration.md)
+
+
+
+**Quick quality check during development**:
+
+```bash
+# Check recently modified files
+scripts/check_quality.sh $(git diff --name-only | grep '\.rb$')
+```
+
+**Generate detailed HTML report**:
+
+```bash
+bundle exec rubycritic --format html app/
+# Opens browser with detailed analysis
+```
+
+**Compare with main branch** (CI mode):
+
+```bash
+rubycritic --mode-ci --branch main app/
+# Shows only changes from main branch
+```
+
+**Check specific file types**:
+
+```bash
+scripts/check_quality.sh app/models/*.rb
+scripts/check_quality.sh app/services/**/*.rb
+```
+
+
-The script uses console format by default for inline feedback. For detailed reports:
+
+For detailed examples of common code smells and how to fix them, see [references/code_smells.md](references/code_smells.md)
+
+**Quick reference**:
+
+- **Control Parameter** - Replace boolean params with separate methods
+- **Feature Envy** - Move method to the class it uses most
+- **Long Parameter List** - Use parameter objects or hashes
+- **High Complexity** - Extract methods, use early returns
+- **Duplication** - Extract to shared methods or modules
+
+
+
+**Automatic installation**: The `check_quality.sh` script handles installation automatically:
+
+- Detects if RubyCritic is installed
+- Uses bundler if Gemfile present
+- Falls back to system gem installation
+- Adds to Gemfile development group if needed
+
+**Manual installation**:
+
+With Bundler:
+
+```ruby
+# Gemfile
+group :development do
+ gem 'rubycritic', require: false
+end
+```
+
+System-wide:
+
+```bash
+gem install rubycritic
+```
-- HTML report: `rubycritic --format html [paths]`
-- JSON output: `rubycritic --format json [paths]`
+
-## Best Practices
+
+RubyCritic is successfully integrated when:
-1. **Run early and often** - Catch issues before they multiply
-2. **Address issues immediately** - Don't let technical debt accumulate
-3. **Explain to users** - When fixing quality issues, briefly explain what was improved
-4. **Set baselines** - On new projects, establish quality standards early
-5. **CI mode** - For comparing branches: `--mode-ci --branch main`
+- Quality checks run automatically after significant code changes
+- Overall score maintained at 90+ (or project-defined threshold)
+- Critical code smells addressed immediately
+- Quality improvements explained to user when significant
+- No quality regressions introduced by changes
+- Files maintain A or B ratings
+
-## Bundled Resources
+
+**Detailed references**:
-- **scripts/check_quality.sh**: Automated quality check with installation handling
+- [references/code_smells.md](references/code_smells.md) - Common smells and fixes with examples
+- [references/configuration.md](references/configuration.md) - Advanced RubyCritic configuration
+- [references/git-hooks.md](references/git-hooks.md) - Pre-commit hooks and CI integration
+- [references/error-handling.md](references/error-handling.md) - Troubleshooting common errors
+
diff --git a/.claude/skills/rubycritic/references/configuration.md b/.claude/skills/rubycritic/references/configuration.md
new file mode 100644
index 0000000..99f7616
--- /dev/null
+++ b/.claude/skills/rubycritic/references/configuration.md
@@ -0,0 +1,396 @@
+# RubyCritic Configuration Reference
+
+This guide covers advanced RubyCritic configuration options for customizing analysis behavior, output formats, and quality thresholds.
+
+## Configuration File
+
+Create `.rubycritic.yml` in your project root:
+
+```yaml
+# .rubycritic.yml
+
+# Minimum acceptable score (0-100)
+minimum_score: 95
+
+# Output formats (can specify multiple)
+formats:
+ - console # Terminal output
+ - html # HTML report in tmp/rubycritic
+ - json # JSON output for CI/tooling
+
+# Paths to analyze
+paths:
+ - 'app/'
+ - 'lib/'
+ - 'spec/'
+
+# Paths to exclude
+exclude_paths:
+ - 'db/migrate/**/*'
+ - 'config/**/*'
+ - 'vendor/**/*'
+
+# Don't auto-open browser for HTML reports
+no_browser: true
+
+# Suppress output (useful for CI)
+suppress_ratings: false
+
+# CI mode options
+mode: default # or 'ci' for CI mode
+
+# Branch to compare against (CI mode)
+branch: main
+
+# Deduplicate similar smells
+deduplicate_symlinks: true
+```
+
+## Common Configuration Patterns
+
+### Strict Quality Standards
+
+For new projects or teams prioritizing code quality:
+
+```yaml
+minimum_score: 95
+formats:
+ - console
+ - html
+paths:
+ - 'app/'
+ - 'lib/'
+no_browser: true
+suppress_ratings: false
+```
+
+### CI/CD Integration
+
+For continuous integration environments:
+
+```yaml
+minimum_score: 90
+formats:
+ - json
+ - console
+mode: ci
+branch: main
+no_browser: true
+suppress_ratings: true
+exclude_paths:
+ - 'db/migrate/**/*'
+ - 'spec/**/*'
+```
+
+### Legacy Codebase
+
+For existing projects with technical debt:
+
+```yaml
+minimum_score: 70 # Lower threshold
+formats:
+ - html
+ - console
+paths:
+ - 'app/models'
+ - 'app/services'
+exclude_paths:
+ - 'app/controllers/**/*' # Exclude problematic areas temporarily
+ - 'lib/legacy/**/*'
+no_browser: false # Open reports for review
+```
+
+### Development Mode
+
+For active development with fast feedback:
+
+```yaml
+minimum_score: 85
+formats:
+ - console
+no_browser: true
+suppress_ratings: false
+deduplicate_symlinks: true
+```
+
+## Command-Line Options
+
+Override configuration file with CLI options:
+
+```bash
+# Set minimum score
+rubycritic --minimum-score 90 app/
+
+# Specify format
+rubycritic --format html app/
+
+# CI mode
+rubycritic --mode-ci --branch main app/
+
+# Suppress browser opening
+rubycritic --no-browser app/
+
+# Multiple formats
+rubycritic --format console --format json app/
+
+# Custom paths
+rubycritic app/models app/services
+
+# Help
+rubycritic --help
+```
+
+## Output Formats
+
+### Console Format
+
+Terminal-friendly output with immediate feedback:
+
+```bash
+rubycritic --format console app/
+```
+
+Output includes:
+- Overall score
+- File-by-file ratings
+- List of code smells
+- Complexity metrics
+
+### HTML Format
+
+Detailed browser-based report:
+
+```bash
+rubycritic --format html app/
+```
+
+Features:
+- Interactive file browser
+- Visual complexity graphs
+- Clickable code smells
+- Historical trends (if run multiple times)
+- Saved to `tmp/rubycritic/index.html`
+
+### JSON Format
+
+Machine-readable output for tooling integration:
+
+```bash
+rubycritic --format json app/ > quality_report.json
+```
+
+Use cases:
+- CI/CD pipeline parsing
+- Custom reporting tools
+- Quality metrics tracking
+- Integration with dashboards
+
+## CI Mode
+
+Compare changes against a base branch:
+
+```bash
+rubycritic --mode-ci --branch main app/
+```
+
+**Benefits**:
+- Only analyzes changed files
+- Shows quality delta
+- Faster on large codebases
+- Focuses on new issues
+
+**Configuration**:
+```yaml
+mode: ci
+branch: main
+minimum_score: 90
+```
+
+## Score Calculation
+
+RubyCritic calculates scores based on:
+
+1. **Reek** - Code smell detection (40% weight)
+2. **Flog** - Complexity analysis (30% weight)
+3. **Flay** - Duplication detection (30% weight)
+
+### Score Ranges
+
+- **90-100**: Excellent - exemplary code quality
+- **80-89**: Good - minor improvements recommended
+- **70-79**: Fair - some technical debt present
+- **60-69**: Poor - significant refactoring needed
+- **0-59**: Critical - major quality issues
+
+### Custom Thresholds
+
+Set thresholds based on project maturity:
+
+**New projects**: 95+
+**Active development**: 90+
+**Established projects**: 85+
+**Legacy codebases**: 70+ (with improvement plan)
+
+## Excluding Paths
+
+### Temporary Exclusions
+
+Exclude paths during active development:
+
+```yaml
+exclude_paths:
+ - 'app/controllers/legacy_controller.rb'
+ - 'lib/deprecated/**/*'
+```
+
+### Permanent Exclusions
+
+Exclude paths that shouldn't be analyzed:
+
+```yaml
+exclude_paths:
+ - 'db/migrate/**/*' # Migrations
+ - 'db/schema.rb' # Auto-generated
+ - 'config/**/*' # Configuration
+ - 'vendor/**/*' # Third-party code
+ - 'bin/**/*' # Scripts
+ - 'spec/fixtures/**/*' # Test fixtures
+ - 'spec/support/shared/**/*' # Test helpers
+```
+
+## Integration with Other Tools
+
+### RuboCop Integration
+
+RubyCritic complements RuboCop:
+
+- **RuboCop**: Style and syntax enforcement
+- **RubyCritic**: Complexity and design analysis
+
+Run both:
+```bash
+rubocop app/ && rubycritic app/
+```
+
+### SimpleCov Integration
+
+Combine with test coverage:
+
+```bash
+# Run tests with coverage
+bundle exec rspec
+
+# Then analyze code quality
+rubycritic app/
+```
+
+### CI Pipeline Integration
+
+```yaml
+# .github/workflows/quality.yml
+name: Code Quality
+
+on: [push, pull_request]
+
+jobs:
+ quality:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2
+ bundler-cache: true
+
+ - name: Run RubyCritic
+ run: |
+ gem install rubycritic
+ rubycritic --format json --minimum-score 90 app/ lib/
+
+ - name: Upload Report
+ uses: actions/upload-artifact@v2
+ with:
+ name: rubycritic-report
+ path: tmp/rubycritic/
+```
+
+## Performance Optimization
+
+For large codebases:
+
+### Analyze Specific Directories
+
+```bash
+# Only analyze changed areas
+rubycritic app/models app/services
+```
+
+### Use CI Mode
+
+```bash
+# Only analyze changed files
+rubycritic --mode-ci --branch main
+```
+
+### Exclude Non-Critical Paths
+
+```yaml
+paths:
+ - 'app/models'
+ - 'app/services'
+ - 'lib/core'
+# Exclude specs, migrations, config
+```
+
+### Disable Browser Opening
+
+```yaml
+no_browser: true
+```
+
+## Troubleshooting Configuration
+
+### Configuration Not Loading
+
+Verify file location and syntax:
+```bash
+# Check if file exists
+ls -la .rubycritic.yml
+
+# Validate YAML syntax
+ruby -e "require 'yaml'; YAML.load_file('.rubycritic.yml')"
+```
+
+### Scores Too Low
+
+Adjust thresholds based on codebase maturity:
+```yaml
+minimum_score: 80 # More lenient
+```
+
+### Too Many Exclusions
+
+Review exclusions periodically:
+```bash
+# List excluded paths
+grep -A 10 "exclude_paths:" .rubycritic.yml
+```
+
+### Performance Issues
+
+Reduce analysis scope:
+```yaml
+paths:
+ - 'app/models' # Start small
+ - 'app/services'
+```
+
+## Best Practices
+
+1. **Version control**: Commit `.rubycritic.yml` to repository
+2. **Team alignment**: Agree on minimum score thresholds
+3. **Gradual improvement**: Start with lower threshold, increase over time
+4. **Focused analysis**: Analyze specific paths for faster feedback
+5. **CI integration**: Run on every PR to prevent regressions
+6. **Historical tracking**: Keep HTML reports for trend analysis
+7. **Regular review**: Adjust configuration as project evolves
diff --git a/.claude/skills/rubycritic/references/error-handling.md b/.claude/skills/rubycritic/references/error-handling.md
new file mode 100644
index 0000000..d79f043
--- /dev/null
+++ b/.claude/skills/rubycritic/references/error-handling.md
@@ -0,0 +1,653 @@
+# RubyCritic Error Handling and Troubleshooting
+
+This guide covers common errors, edge cases, and troubleshooting strategies when using RubyCritic.
+
+## Installation Errors
+
+### "RubyCritic not found"
+
+**Symptom**: Command `rubycritic` not recognized
+
+**Causes**:
+- RubyCritic not installed
+- Not in PATH (when using Bundler)
+- Wrong Ruby version active
+
+**Solutions**:
+
+```bash
+# Check if installed
+which rubycritic
+
+# Install system-wide
+gem install rubycritic
+
+# Or add to Gemfile
+# Gemfile
+group :development do
+ gem 'rubycritic', require: false
+end
+
+# Then install
+bundle install
+
+# Use with Bundler
+bundle exec rubycritic app/
+```
+
+### "Gem::InstallError: You don't have write permissions"
+
+**Symptom**: Permission denied when installing gem
+
+**Solutions**:
+
+```bash
+# Option 1: Use bundler (recommended)
+bundle install
+
+# Option 2: Install to user directory
+gem install --user-install rubycritic
+
+# Option 3: Use rbenv/rvm (recommended for development)
+rbenv install 3.2.0
+rbenv global 3.2.0
+gem install rubycritic
+```
+
+### "Bundler::GemNotFound: Could not find gem 'rubycritic'"
+
+**Symptom**: Gem not in Gemfile.lock
+
+**Solutions**:
+
+```bash
+# Add to Gemfile
+echo "gem 'rubycritic', require: false, group: :development" >> Gemfile
+
+# Update bundle
+bundle install
+
+# Verify installation
+bundle exec rubycritic --version
+```
+
+### "LoadError: cannot load such file"
+
+**Symptom**: Missing dependencies for RubyCritic
+
+**Solutions**:
+
+```bash
+# Update bundler
+bundle update
+
+# Re-install RubyCritic
+gem uninstall rubycritic
+gem install rubycritic
+
+# Check Ruby version compatibility
+ruby --version # Should be 2.7+
+```
+
+## Analysis Errors
+
+### "No files to critique"
+
+**Symptom**: RubyCritic finds no Ruby files to analyze
+
+**Causes**:
+- Wrong path specified
+- No `.rb` files in path
+- Path excluded in configuration
+
+**Solutions**:
+
+```bash
+# Verify path contains Ruby files
+ls -la app/*.rb
+
+# Check explicit file extension
+rubycritic app/**/*.rb
+
+# Verify configuration exclusions
+cat .rubycritic.yml | grep -A 5 exclude_paths
+
+# Use absolute paths
+rubycritic $(pwd)/app/
+```
+
+### "Analysis timed out"
+
+**Symptom**: RubyCritic hangs or takes extremely long
+
+**Causes**:
+- Very large codebase
+- Infinite loops in analyzed code
+- Resource constraints
+
+**Solutions**:
+
+```bash
+# Analyze smaller directories
+rubycritic app/models/
+
+# Use CI mode for faster analysis
+rubycritic --mode-ci --branch main app/
+
+# Increase timeout (if available in your version)
+# Or split into multiple runs
+rubycritic app/models/
+rubycritic app/services/
+rubycritic app/controllers/
+
+# Check for problematic files
+# Analyze one directory at a time to identify issues
+```
+
+### "Invalid multibyte char (UTF-8)"
+
+**Symptom**: Encoding errors when analyzing files
+
+**Causes**:
+- Files with invalid UTF-8 encoding
+- Mixed encodings in codebase
+
+**Solutions**:
+
+```bash
+# Find files with encoding issues
+find . -name "*.rb" -exec file {} \; | grep -v "UTF-8"
+
+# Fix encoding in problematic files
+# Add magic comment at top of file:
+# frozen_string_literal: true
+# encoding: UTF-8
+
+# Or convert file encoding
+iconv -f ISO-8859-1 -t UTF-8 problematic_file.rb -o fixed_file.rb
+```
+
+### "SyntaxError: unexpected token"
+
+**Symptom**: RubyCritic fails on valid Ruby syntax
+
+**Causes**:
+- Unsupported Ruby version features
+- RubyCritic version too old
+- Actually invalid syntax in code
+
+**Solutions**:
+
+```bash
+# Check RubyCritic version
+rubycritic --version
+
+# Update RubyCritic
+gem update rubycritic
+
+# Verify syntax is actually valid
+ruby -c app/models/user.rb
+
+# Check Ruby version compatibility
+ruby --version
+```
+
+## Configuration Errors
+
+### "Invalid YAML in .rubycritic.yml"
+
+**Symptom**: Configuration file not parsed correctly
+
+**Solutions**:
+
+```bash
+# Validate YAML syntax
+ruby -e "require 'yaml'; YAML.load_file('.rubycritic.yml')"
+
+# Common YAML issues:
+# - Wrong indentation (use spaces, not tabs)
+# - Missing quotes around special characters
+# - Incorrect list syntax
+
+# Example of common error:
+# ❌ Wrong
+paths:
+- app/ # Missing space after hyphen
+
+# ✅ Correct
+paths:
+ - 'app/'
+```
+
+### "Unrecognized option"
+
+**Symptom**: Command-line option not recognized
+
+**Causes**:
+- Typo in option name
+- Option not available in RubyCritic version
+- Incorrect option format
+
+**Solutions**:
+
+```bash
+# Check available options
+rubycritic --help
+
+# Verify correct format
+rubycritic --minimum-score 90 # Correct
+rubycritic --minimum_score 90 # Wrong (underscore)
+
+# Check version supports option
+rubycritic --version
+```
+
+### "Configuration file not found"
+
+**Symptom**: RubyCritic doesn't load `.rubycritic.yml`
+
+**Solutions**:
+
+```bash
+# Verify file exists in project root
+ls -la .rubycritic.yml
+
+# Check file permissions
+chmod 644 .rubycritic.yml
+
+# Use absolute path in config
+pwd # Get current directory
+# Ensure running RubyCritic from project root
+
+# Debug: Run with explicit paths
+rubycritic --format console app/
+```
+
+## Output Errors
+
+### "Permission denied writing to tmp/"
+
+**Symptom**: Cannot write HTML report
+
+**Solutions**:
+
+```bash
+# Check tmp directory permissions
+ls -la tmp/
+
+# Create directory if missing
+mkdir -p tmp/rubycritic
+
+# Fix permissions
+chmod 755 tmp/
+
+# Or use different output directory
+rubycritic --path ./reports app/
+```
+
+### "Browser failed to open"
+
+**Symptom**: HTML report generated but browser doesn't open
+
+**Solutions**:
+
+```bash
+# Use --no-browser flag
+rubycritic --no-browser app/
+
+# Manually open report
+open tmp/rubycritic/index.html # macOS
+xdg-open tmp/rubycritic/index.html # Linux
+
+# Add to configuration
+# .rubycritic.yml
+no_browser: true
+```
+
+### "JSON output malformed"
+
+**Symptom**: JSON format produces invalid JSON
+
+**Solutions**:
+
+```bash
+# Validate JSON output
+rubycritic --format json app/ | jq .
+
+# If jq not installed
+rubycritic --format json app/ > output.json
+ruby -e "require 'json'; JSON.parse(File.read('output.json'))"
+
+# Use console format for debugging
+rubycritic --format console app/
+```
+
+## Score Calculation Issues
+
+### "Score unexpectedly low"
+
+**Symptom**: Quality score lower than expected
+
+**Investigation**:
+
+```bash
+# Get detailed breakdown
+rubycritic --format html app/
+# Open HTML report to see which files/metrics are low
+
+# Check individual analyzers
+# Look for:
+# - High Flog scores (complexity)
+# - Many Reek smells
+# - Flay duplications
+
+# Focus on worst files first
+rubycritic --format console app/ | grep "F:"
+```
+
+**Common causes**:
+- Long methods (>10 lines)
+- High cyclomatic complexity
+- Many parameters (>3)
+- Duplicate code
+- Feature envy (using other classes' methods)
+
+### "Score changes between runs"
+
+**Symptom**: Inconsistent scores for same code
+
+**Causes**:
+- Different file sets analyzed
+- Configuration changes
+- RubyCritic version differences
+
+**Solutions**:
+
+```bash
+# Use consistent paths
+rubycritic app/ lib/ # Always specify same paths
+
+# Lock configuration
+# Commit .rubycritic.yml to version control
+
+# Use same version
+# Add to Gemfile with version constraint
+gem 'rubycritic', '~> 4.7', require: false
+```
+
+### "Cannot determine score from output"
+
+**Symptom**: Parsing tools can't extract score
+
+**Solutions**:
+
+```bash
+# Use JSON format for machine parsing
+rubycritic --format json app/ > quality.json
+
+# Parse JSON for score
+ruby -e "
+ require 'json'
+ data = JSON.parse(File.read('quality.json'))
+ puts data['score']
+"
+
+# Or use grep with console format
+rubycritic --format console app/ | grep -oP 'Score: \K\d+'
+```
+
+## Integration Errors
+
+### "Git hook not executing"
+
+**Symptom**: Pre-commit/pre-push hook not running
+
+**Solutions**:
+
+```bash
+# Check if hook exists
+ls -la .git/hooks/pre-commit
+
+# Verify executable permissions
+chmod +x .git/hooks/pre-commit
+
+# Test hook manually
+.git/hooks/pre-commit
+
+# Check shebang line
+head -1 .git/hooks/pre-commit
+# Should be: #!/bin/bash
+
+# Verify git config allows hooks
+git config --get core.hooksPath
+```
+
+### "CI build failing but local passes"
+
+**Symptom**: Quality check passes locally but fails in CI
+
+**Causes**:
+- Different RubyCritic versions
+- Different file sets analyzed
+- Configuration not in version control
+
+**Solutions**:
+
+```bash
+# Pin RubyCritic version in Gemfile
+gem 'rubycritic', '~> 4.7.0', require: false
+
+# Ensure configuration is committed
+git add .rubycritic.yml
+git commit -m "Add RubyCritic config"
+
+# Use same command locally and in CI
+bundle exec rubycritic app/ lib/
+
+# Debug: Compare versions
+rubycritic --version # Local
+# vs CI output
+
+# Check for ignored files in .gitignore
+# That might exist locally but not in CI
+```
+
+### "Bundler can't find rubycritic in CI"
+
+**Symptom**: CI can't install or run RubyCritic
+
+**Solutions**:
+
+```yaml
+# Ensure proper setup in CI
+steps:
+ - name: Install dependencies
+ run: bundle install
+
+ - name: Run RubyCritic
+ run: bundle exec rubycritic app/
+
+# Or install separately
+steps:
+ - name: Install RubyCritic
+ run: gem install rubycritic
+
+ - name: Run analysis
+ run: rubycritic app/
+```
+
+## Performance Issues
+
+### "Analysis takes too long"
+
+**Symptom**: RubyCritic runs for minutes/hours
+
+**Solutions**:
+
+```bash
+# Profile which directories are slow
+time rubycritic app/models/
+time rubycritic app/services/
+time rubycritic app/controllers/
+
+# Exclude large/generated files
+# .rubycritic.yml
+exclude_paths:
+ - 'app/assets/**/*'
+ - 'db/**/*'
+ - 'spec/**/*'
+
+# Use CI mode on branches
+rubycritic --mode-ci --branch main app/
+
+# Analyze only changed files in git hook
+git diff --cached --name-only | grep '\.rb$' | xargs rubycritic
+```
+
+### "Running out of memory"
+
+**Symptom**: RubyCritic crashes with memory errors
+
+**Solutions**:
+
+```bash
+# Increase Ruby memory limit
+RUBY_GC_HEAP_GROWTH_FACTOR=1.1 rubycritic app/
+
+# Analyze in smaller batches
+for dir in app/*/; do
+ rubycritic "$dir"
+done
+
+# Reduce scope
+# Only analyze app/ not spec/
+rubycritic app/
+
+# Use CI mode (more efficient)
+rubycritic --mode-ci --branch main app/
+```
+
+## Debugging Strategies
+
+### Enable Verbose Output
+
+```bash
+# Run with verbose mode (if available)
+rubycritic --verbose app/
+
+# Check RubyCritic logs
+# Location varies by OS
+cat tmp/rubycritic.log
+```
+
+### Isolate Problematic Files
+
+```bash
+# Binary search approach
+# Split files in half, find which half has issues
+
+# Test individual files
+for file in app/models/*.rb; do
+ echo "Analyzing $file"
+ rubycritic "$file" || echo "Failed on $file"
+done
+```
+
+### Check Dependencies
+
+```bash
+# Verify all RubyCritic dependencies
+bundle exec gem dependency rubycritic
+
+# Update dependencies
+bundle update rubycritic
+
+# Clean and reinstall
+gem uninstall rubycritic
+bundle install
+```
+
+### Compare with Fresh Environment
+
+```bash
+# Create new gemset/environment
+# Test if issue persists
+
+# Docker test
+docker run -it ruby:3.2 bash
+gem install rubycritic
+# Test analysis
+```
+
+## Getting Help
+
+### Gather Debug Information
+
+When reporting issues:
+
+```bash
+# Collect version info
+ruby --version
+rubycritic --version
+bundle --version
+
+# Configuration
+cat .rubycritic.yml
+
+# Command used
+echo "rubycritic app/"
+
+# Error output
+rubycritic app/ 2>&1 | tee error.log
+
+# System info
+uname -a
+```
+
+### Resources
+
+- **GitHub Issues**: https://github.com/whitesmith/rubycritic/issues
+- **Documentation**: https://github.com/whitesmith/rubycritic
+- **Stack Overflow**: Tag `rubycritic`
+
+## Prevention Best Practices
+
+1. **Pin versions**: Lock RubyCritic version in Gemfile
+2. **Test configuration**: Validate `.rubycritic.yml` syntax
+3. **Incremental adoption**: Start with small directories
+4. **Monitor performance**: Track analysis time
+5. **Document exceptions**: Comment why files are excluded
+6. **Version control config**: Commit configuration files
+7. **CI validation**: Test hooks in CI environment
+8. **Regular updates**: Keep RubyCritic updated
+9. **Team training**: Document common issues
+10. **Fallback plans**: Have manual quality review process
+
+## Quick Reference
+
+### Emergency Fixes
+
+```bash
+# Skip quality check temporarily
+git commit --no-verify
+
+# Force run with basic settings
+rubycritic --format console --no-browser app/
+
+# Minimal analysis
+rubycritic app/models/user.rb
+
+# Reset configuration
+rm .rubycritic.yml
+rubycritic app/
+```
+
+### Health Check
+
+```bash
+# Verify setup is working
+rubycritic --version && echo "✓ Installed"
+[ -f .rubycritic.yml ] && echo "✓ Configured"
+ruby -c .git/hooks/pre-commit && echo "✓ Hook valid"
+bundle exec rubycritic --help && echo "✓ Bundler OK"
+```
diff --git a/.claude/skills/rubycritic/references/git-hooks.md b/.claude/skills/rubycritic/references/git-hooks.md
new file mode 100644
index 0000000..fb49657
--- /dev/null
+++ b/.claude/skills/rubycritic/references/git-hooks.md
@@ -0,0 +1,618 @@
+# Git Hooks and CI Integration
+
+This guide covers integrating RubyCritic into git workflows and continuous integration pipelines to maintain code quality automatically.
+
+## Pre-Commit Hook
+
+Automatically run RubyCritic on staged files before commits.
+
+### Basic Pre-Commit Hook
+
+Create `.git/hooks/pre-commit`:
+
+```bash
+#!/bin/bash
+
+# Get staged Ruby files
+RUBY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.rb$')
+
+if [ -z "$RUBY_FILES" ]; then
+ # No Ruby files staged, skip check
+ exit 0
+fi
+
+echo "Running RubyCritic on staged files..."
+echo "$RUBY_FILES"
+echo ""
+
+# Run RubyCritic on staged files
+if [ -f "scripts/check_quality.sh" ]; then
+ scripts/check_quality.sh $RUBY_FILES
+else
+ bundle exec rubycritic --format console --no-browser $RUBY_FILES
+fi
+
+RESULT=$?
+
+if [ $RESULT -ne 0 ]; then
+ echo ""
+ echo "❌ Quality check failed!"
+ echo "Fix the issues above or use 'git commit --no-verify' to skip this check."
+ exit 1
+fi
+
+echo "✅ Quality check passed!"
+exit 0
+```
+
+Make it executable:
+```bash
+chmod +x .git/hooks/pre-commit
+```
+
+### Pre-Commit Hook with Threshold
+
+Only fail on severe quality issues:
+
+```bash
+#!/bin/bash
+
+RUBY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.rb$')
+
+if [ -z "$RUBY_FILES" ]; then
+ exit 0
+fi
+
+echo "Running RubyCritic on staged files..."
+
+# Run and capture output
+OUTPUT=$(bundle exec rubycritic --format console --no-browser $RUBY_FILES 2>&1)
+echo "$OUTPUT"
+
+# Extract score from output
+SCORE=$(echo "$OUTPUT" | grep -oP 'Score: \K\d+' | head -1)
+
+if [ -z "$SCORE" ]; then
+ echo "⚠️ Could not determine quality score"
+ exit 0 # Don't block commit if we can't get score
+fi
+
+MINIMUM_SCORE=85
+
+if [ "$SCORE" -lt "$MINIMUM_SCORE" ]; then
+ echo ""
+ echo "❌ Quality score $SCORE is below minimum $MINIMUM_SCORE"
+ echo "Please improve code quality or use --no-verify to skip."
+ exit 1
+fi
+
+echo "✅ Quality score: $SCORE (minimum: $MINIMUM_SCORE)"
+exit 0
+```
+
+### Pre-Commit Hook with Selective Analysis
+
+Only analyze files in critical directories:
+
+```bash
+#!/bin/bash
+
+RUBY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.rb$')
+
+if [ -z "$RUBY_FILES" ]; then
+ exit 0
+fi
+
+# Filter for critical paths only
+CRITICAL_FILES=$(echo "$RUBY_FILES" | grep -E '^(app/models|app/services|lib)/')
+
+if [ -z "$CRITICAL_FILES" ]; then
+ echo "No critical files changed, skipping quality check"
+ exit 0
+fi
+
+echo "Running RubyCritic on critical files:"
+echo "$CRITICAL_FILES"
+echo ""
+
+bundle exec rubycritic --format console --no-browser $CRITICAL_FILES
+
+if [ $? -ne 0 ]; then
+ echo "❌ Quality check failed!"
+ exit 1
+fi
+
+echo "✅ Quality check passed!"
+exit 0
+```
+
+## Pre-Push Hook
+
+Run comprehensive analysis before pushing to remote:
+
+Create `.git/hooks/pre-push`:
+
+```bash
+#!/bin/bash
+
+echo "Running comprehensive quality check before push..."
+echo ""
+
+# Get all commits being pushed
+while read local_ref local_sha remote_ref remote_sha
+do
+ if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
+ # Branch is being deleted, skip
+ continue
+ fi
+
+ if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
+ # New branch, compare with main
+ RANGE="main..$local_sha"
+ else
+ # Existing branch, compare with remote
+ RANGE="$remote_sha..$local_sha"
+ fi
+
+ # Get changed Ruby files
+ RUBY_FILES=$(git diff --name-only $RANGE | grep '\.rb$')
+
+ if [ -n "$RUBY_FILES" ]; then
+ echo "Analyzing changed files:"
+ echo "$RUBY_FILES"
+ echo ""
+
+ bundle exec rubycritic --format console --no-browser $RUBY_FILES
+
+ if [ $? -ne 0 ]; then
+ echo ""
+ echo "❌ Quality check failed!"
+ echo "Fix issues or use 'git push --no-verify' to skip."
+ exit 1
+ fi
+ fi
+done
+
+echo "✅ All quality checks passed!"
+exit 0
+```
+
+Make it executable:
+```bash
+chmod +x .git/hooks/pre-push
+```
+
+## Commit Message Hook
+
+Add quality score to commit message automatically:
+
+Create `.git/hooks/prepare-commit-msg`:
+
+```bash
+#!/bin/bash
+
+COMMIT_MSG_FILE=$1
+COMMIT_SOURCE=$2
+
+# Skip if amending or using a message from another source
+if [ "$COMMIT_SOURCE" = "message" ] || [ "$COMMIT_SOURCE" = "merge" ]; then
+ exit 0
+fi
+
+# Get staged Ruby files
+RUBY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.rb$')
+
+if [ -z "$RUBY_FILES" ]; then
+ exit 0
+fi
+
+# Run RubyCritic and capture score
+OUTPUT=$(bundle exec rubycritic --format console --no-browser $RUBY_FILES 2>&1)
+SCORE=$(echo "$OUTPUT" | grep -oP 'Score: \K\d+' | head -1)
+
+if [ -n "$SCORE" ]; then
+ # Append quality score to commit message
+ echo "" >> "$COMMIT_MSG_FILE"
+ echo "Code Quality Score: $SCORE" >> "$COMMIT_MSG_FILE"
+fi
+
+exit 0
+```
+
+Make it executable:
+```bash
+chmod +x .git/hooks/prepare-commit-msg
+```
+
+## GitHub Actions Integration
+
+### Basic Workflow
+
+Create `.github/workflows/code-quality.yml`:
+
+```yaml
+name: Code Quality
+
+on:
+ push:
+ branches: [ main, develop ]
+ pull_request:
+ branches: [ main, develop ]
+
+jobs:
+ rubycritic:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0 # Full history for CI mode
+
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2
+ bundler-cache: true
+
+ - name: Install RubyCritic
+ run: gem install rubycritic
+
+ - name: Run RubyCritic
+ run: |
+ rubycritic --format console --format json \
+ --minimum-score 90 \
+ --no-browser \
+ app/ lib/
+
+ - name: Upload Report
+ if: always()
+ uses: actions/upload-artifact@v3
+ with:
+ name: rubycritic-report
+ path: tmp/rubycritic/
+ retention-days: 30
+```
+
+### PR-Focused Workflow
+
+Only analyze changed files in pull requests:
+
+```yaml
+name: PR Quality Check
+
+on:
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ quality-check:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2
+ bundler-cache: true
+
+ - name: Install RubyCritic
+ run: gem install rubycritic
+
+ - name: Get changed files
+ id: changed-files
+ uses: tj-actions/changed-files@v35
+ with:
+ files: |
+ **/*.rb
+
+ - name: Run RubyCritic on changed files
+ if: steps.changed-files.outputs.any_changed == 'true'
+ run: |
+ echo "Changed Ruby files:"
+ echo "${{ steps.changed-files.outputs.all_changed_files }}"
+
+ rubycritic --format console \
+ --minimum-score 90 \
+ --no-browser \
+ ${{ steps.changed-files.outputs.all_changed_files }}
+
+ - name: Comment PR with Results
+ if: failure()
+ uses: actions/github-script@v6
+ with:
+ script: |
+ github.rest.issues.createComment({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: '⚠️ Code quality check failed. Please review the RubyCritic output above.'
+ })
+```
+
+### Workflow with Coverage and Quality
+
+Combine with SimpleCov for comprehensive analysis:
+
+```yaml
+name: Tests and Quality
+
+on: [push, pull_request]
+
+jobs:
+ test-and-quality:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2
+ bundler-cache: true
+
+ - name: Run tests with coverage
+ run: bundle exec rspec
+ env:
+ COVERAGE: true
+
+ - name: Install RubyCritic
+ run: gem install rubycritic
+
+ - name: Run RubyCritic
+ run: |
+ rubycritic --format console --format html \
+ --minimum-score 90 \
+ app/ lib/
+
+ - name: Upload Coverage Report
+ uses: actions/upload-artifact@v3
+ with:
+ name: coverage-report
+ path: coverage/
+
+ - name: Upload Quality Report
+ uses: actions/upload-artifact@v3
+ with:
+ name: quality-report
+ path: tmp/rubycritic/
+```
+
+## GitLab CI Integration
+
+Create `.gitlab-ci.yml`:
+
+```yaml
+stages:
+ - test
+ - quality
+
+quality:
+ stage: quality
+ image: ruby:3.2
+ before_script:
+ - gem install rubycritic
+ script:
+ - rubycritic --format console --format json --minimum-score 90 app/ lib/
+ artifacts:
+ paths:
+ - tmp/rubycritic/
+ expire_in: 1 week
+ only:
+ - merge_requests
+ - main
+```
+
+### GitLab CI with Changed Files Only
+
+```yaml
+quality:mr:
+ stage: quality
+ image: ruby:3.2
+ before_script:
+ - gem install rubycritic
+ script:
+ - |
+ git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
+ CHANGED_FILES=$(git diff --name-only origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD | grep '\.rb$' || true)
+
+ if [ -n "$CHANGED_FILES" ]; then
+ echo "Analyzing changed files:"
+ echo "$CHANGED_FILES"
+ rubycritic --format console --minimum-score 90 $CHANGED_FILES
+ else
+ echo "No Ruby files changed"
+ fi
+ only:
+ - merge_requests
+```
+
+## CircleCI Integration
+
+Create `.circleci/config.yml`:
+
+```yaml
+version: 2.1
+
+jobs:
+ quality:
+ docker:
+ - image: cimg/ruby:3.2
+ steps:
+ - checkout
+ - run:
+ name: Install dependencies
+ command: bundle install
+ - run:
+ name: Install RubyCritic
+ command: gem install rubycritic
+ - run:
+ name: Run RubyCritic
+ command: |
+ rubycritic --format console --format json \
+ --minimum-score 90 \
+ --no-browser \
+ app/ lib/
+ - store_artifacts:
+ path: tmp/rubycritic
+ destination: quality-report
+
+workflows:
+ version: 2
+ build-and-quality:
+ jobs:
+ - quality
+```
+
+## Shared Git Hooks Setup
+
+Use a shared hooks directory for team consistency:
+
+### Setup Script
+
+Create `bin/setup-git-hooks`:
+
+```bash
+#!/bin/bash
+
+HOOKS_DIR=".git-hooks"
+GIT_HOOKS_DIR=".git/hooks"
+
+# Create symlinks for all hooks
+for hook in "$HOOKS_DIR"/*; do
+ hook_name=$(basename "$hook")
+ ln -sf "../../$HOOKS_DIR/$hook_name" "$GIT_HOOKS_DIR/$hook_name"
+ chmod +x "$HOOKS_DIR/$hook_name"
+ echo "Installed $hook_name hook"
+done
+
+echo "✅ Git hooks installed successfully!"
+```
+
+Make executable:
+```bash
+chmod +x bin/setup-git-hooks
+```
+
+### Team Workflow
+
+1. Create `.git-hooks/` directory (tracked in git)
+2. Add hooks to `.git-hooks/`
+3. Run `bin/setup-git-hooks` during project setup
+4. Document in README:
+
+```markdown
+## Setup
+
+1. Clone repository
+2. Run `bin/setup-git-hooks` to install quality checks
+3. Install dependencies: `bundle install`
+```
+
+## Bypassing Hooks
+
+When necessary to bypass quality checks:
+
+```bash
+# Skip pre-commit hook
+git commit --no-verify -m "Message"
+
+# Skip pre-push hook
+git push --no-verify
+```
+
+**Use sparingly** - only when:
+- Emergency hotfixes
+- Work-in-progress commits
+- Non-code changes (docs, config)
+
+## Best Practices
+
+1. **Fast feedback**: Use pre-commit for quick checks
+2. **Comprehensive analysis**: Use pre-push for thorough checks
+3. **CI as gatekeeper**: Always run in CI, even if hooks are skipped
+4. **Team adoption**: Make hooks easy to install (`bin/setup-git-hooks`)
+5. **Flexible thresholds**: Different scores for different branches
+6. **Clear messaging**: Explain why checks fail and how to fix
+7. **Escape hatch**: Document when `--no-verify` is acceptable
+8. **Artifact storage**: Keep reports for historical analysis
+9. **PR comments**: Auto-comment on PRs with quality issues
+10. **Regular review**: Adjust thresholds as codebase improves
+
+## Troubleshooting
+
+### Hooks Not Running
+
+```bash
+# Check if hooks are executable
+ls -la .git/hooks/pre-commit
+
+# Make executable
+chmod +x .git/hooks/pre-commit
+
+# Verify hook content
+cat .git/hooks/pre-commit
+```
+
+### Hooks Running on Non-Ruby Commits
+
+Add file type check:
+```bash
+RUBY_FILES=$(git diff --cached --name-only | grep '\.rb$')
+if [ -z "$RUBY_FILES" ]; then
+ exit 0 # No Ruby files, skip
+fi
+```
+
+### CI Failing but Local Hooks Pass
+
+Ensure same RubyCritic version:
+```yaml
+# In CI, use Bundler version
+- run: bundle exec rubycritic ...
+
+# Locally, also use Bundler
+bundle exec rubycritic ...
+```
+
+### Performance Issues in Hooks
+
+Analyze only changed files:
+```bash
+# Instead of analyzing entire app/
+RUBY_FILES=$(git diff --cached --name-only | grep '\.rb$')
+rubycritic $RUBY_FILES
+```
+
+## Example Repository Setup
+
+Complete setup for a Rails application:
+
+```
+project/
+├── .git-hooks/
+│ ├── pre-commit # Quality check on staged files
+│ └── pre-push # Comprehensive analysis
+├── .github/
+│ └── workflows/
+│ └── quality.yml # CI quality checks
+├── .rubycritic.yml # Configuration
+├── bin/
+│ └── setup-git-hooks # Installation script
+└── scripts/
+ └── check_quality.sh # Shared quality check script
+```
+
+This provides:
+- Local pre-commit checks (fast)
+- Pre-push comprehensive analysis
+- CI validation
+- Team-wide consistency
+- Easy onboarding
diff --git a/.claude/skills/rubycritic/scripts/check_quality.sh b/.claude/skills/rubycritic/scripts/check_quality.sh
index f0280b5..3a86543 100755
--- a/.claude/skills/rubycritic/scripts/check_quality.sh
+++ b/.claude/skills/rubycritic/scripts/check_quality.sh
@@ -22,31 +22,31 @@ check_rubycritic_installed() {
if command_exists rubycritic; then
return 0
fi
-
+
# Check if it's available via bundler
if [ -f "Gemfile" ] && command_exists bundle; then
if bundle exec rubycritic --version >/dev/null 2>&1; then
return 0
fi
fi
-
+
return 1
}
# Function to install RubyCritic
install_rubycritic() {
echo -e "${YELLOW}RubyCritic is not installed. Installing now...${NC}"
-
+
# Check if we're in a bundler project
if [ -f "Gemfile" ]; then
echo -e "${BLUE}Detected Gemfile. Checking if rubycritic is in Gemfile...${NC}"
-
+
if grep -q "rubycritic" Gemfile; then
echo -e "${BLUE}RubyCritic found in Gemfile. Running bundle install...${NC}"
bundle install
else
echo -e "${YELLOW}RubyCritic not in Gemfile. Adding it to development group...${NC}"
-
+
# Add to Gemfile
if grep -q "group :development do" Gemfile; then
# Insert after the development group line
@@ -58,14 +58,14 @@ install_rubycritic() {
echo " gem 'rubycritic', require: false" >> Gemfile
echo "end" >> Gemfile
fi
-
+
bundle install
fi
else
echo -e "${BLUE}No Gemfile found. Installing RubyCritic as a system gem...${NC}"
gem install rubycritic
fi
-
+
echo -e "${GREEN}✓ RubyCritic installed successfully!${NC}"
echo ""
}
@@ -73,25 +73,25 @@ install_rubycritic() {
# Function to run RubyCritic
run_rubycritic() {
local target_path="${1:-.}"
-
+
echo -e "${BLUE}Running RubyCritic analysis on: ${target_path}${NC}"
echo ""
-
+
# Determine how to run RubyCritic
local cmd="rubycritic"
if [ -f "Gemfile" ] && command_exists bundle; then
cmd="bundle exec rubycritic"
fi
-
+
# Run RubyCritic with console format and no browser
# Use --no-browser to prevent opening HTML report
# Use --format console for immediate feedback
$cmd --format console --no-browser "$target_path"
-
+
local exit_code=$?
-
+
echo ""
-
+
if [ $exit_code -eq 0 ]; then
echo -e "${GREEN}✓ Quality check complete!${NC}"
echo -e "${BLUE}Tip: For detailed HTML report, run: $cmd --format html $target_path${NC}"
@@ -99,7 +99,7 @@ run_rubycritic() {
echo -e "${YELLOW}⚠ Quality check completed with warnings${NC}"
echo -e "${BLUE}Review the output above for issues to address${NC}"
fi
-
+
return $exit_code
}
@@ -109,19 +109,19 @@ main() {
echo -e "${BLUE} RubyCritic Code Quality Analyzer${NC}"
echo -e "${BLUE}═══════════════════════════════════════════════${NC}"
echo ""
-
+
# Check for Ruby
if ! command_exists ruby; then
echo -e "${RED}Error: Ruby is not installed${NC}"
echo "Please install Ruby before running this script"
exit 1
fi
-
+
# Check and install RubyCritic if needed
if ! check_rubycritic_installed; then
install_rubycritic
fi
-
+
# Run the analysis
run_rubycritic "$@"
}
diff --git a/.github/workflows/rubycritic.yml b/.github/workflows/rubycritic.yml
new file mode 100644
index 0000000..1a92df9
--- /dev/null
+++ b/.github/workflows/rubycritic.yml
@@ -0,0 +1,262 @@
+name: RubyCritic Code Quality
+
+on:
+ pull_request:
+ branches: [ main ]
+ paths:
+ - 'pdf_converter/**/*.rb'
+ - 'pdf_converter/Gemfile'
+ - 'pdf_converter/Gemfile.lock'
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ rubycritic:
+ runs-on: ubuntu-latest
+
+ defaults:
+ run:
+ working-directory: pdf_converter
+
+ steps:
+ - name: Checkout PR branch
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Full history for accurate comparison
+
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: '3.4'
+ bundler-cache: true
+ working-directory: pdf_converter
+
+ - name: Run RubyCritic on PR branch
+ run: |
+ echo "=== Running RubyCritic ==="
+
+ # Run RubyCritic in CI mode to compare against base branch
+ set +e # Don't exit on error, but capture it
+ bundle exec rubycritic \
+ --mode-ci \
+ --branch origin/${{ github.base_ref }} \
+ --format json \
+ --format console \
+ --no-browser \
+ app/ lib/
+
+ RC_EXIT_CODE=$?
+ echo "RubyCritic exit code: $RC_EXIT_CODE"
+ set -e
+
+ echo "=== Checking for report files ==="
+ echo "Contents of tmp directory:"
+ ls -la tmp/ || echo "tmp directory does not exist"
+
+ echo "Contents of tmp/rubycritic directory:"
+ ls -la tmp/rubycritic/ || echo "tmp/rubycritic directory does not exist"
+
+ # Save the report for analysis (copy to workspace root for github-script access)
+ if [ -f tmp/rubycritic/report.json ]; then
+ echo "=== Found report.json, copying to workspace root ==="
+ cp tmp/rubycritic/report.json ../rubycritic_report.json
+ echo "Report copied successfully"
+ echo "Report score:"
+ cat tmp/rubycritic/report.json | jq '.score' || echo "Could not parse score with jq"
+ else
+ echo "WARNING: CI mode report not found. Running full analysis..."
+ echo "This might happen when CI mode has no changed files to analyze."
+
+ # Fallback: Run without CI mode to get a full report
+ bundle exec rubycritic \
+ --format json \
+ --format console \
+ --no-browser \
+ app/ lib/ || echo "RubyCritic failed"
+
+ echo "Contents of tmp/rubycritic after full run:"
+ ls -la tmp/rubycritic/ || echo "Still no tmp/rubycritic directory"
+
+ if [ -f tmp/rubycritic/report.json ]; then
+ echo "=== Found report.json after full analysis ==="
+ cp tmp/rubycritic/report.json ../rubycritic_report.json
+ echo "Report copied successfully"
+ else
+ echo "ERROR: Still no report found. Skipping quality analysis."
+ echo "Creating empty marker file to prevent errors in next step"
+ echo '{"score": 0, "analyzed_modules": [], "error": "No report generated"}' > ../rubycritic_report.json
+ fi
+ fi
+
+ - name: Analyze results and comment on PR
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+ const path = require('path');
+
+ // Read RubyCritic JSON report
+ let report;
+ try {
+ const reportPath = path.join(process.env.GITHUB_WORKSPACE, 'rubycritic_report.json');
+ console.log('Looking for report at:', reportPath);
+
+ if (!fs.existsSync(reportPath)) {
+ console.log('No RubyCritic report found at:', reportPath);
+ console.log('Workspace contents:', fs.readdirSync(process.env.GITHUB_WORKSPACE));
+ return;
+ }
+
+ const reportContent = fs.readFileSync(reportPath, 'utf8');
+ report = JSON.parse(reportContent);
+ console.log('Report loaded successfully. Score:', report.score);
+ } catch (error) {
+ console.error('Error reading report:', error);
+ return;
+ }
+
+ // Check for error in report
+ if (report.error) {
+ console.log('Report generation error:', report.error);
+ const errorComment = `## 🔍 RubyCritic Code Quality Report\n\n` +
+ `⚠️ **Unable to generate quality report**\n\n` +
+ `RubyCritic could not analyze the code. This might happen when:\n` +
+ `- No Ruby files were changed in this PR\n` +
+ `- RubyCritic encountered an error during analysis\n\n` +
+ `Please check the [workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.`;
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: errorComment
+ });
+ return;
+ }
+
+ // Extract metrics from report
+ const score = report.score || 0;
+ const analyzedFiles = report.analysed_modules || [];
+
+ // Calculate quality breakdown
+ const filesByGrade = {};
+ analyzedFiles.forEach(file => {
+ const grade = file.rating || 'N/A';
+ filesByGrade[grade] = (filesByGrade[grade] || 0) + 1;
+ });
+
+ // Find files with issues (grade C or lower)
+ const filesWithIssues = analyzedFiles
+ .filter(file => ['C', 'D', 'F'].includes(file.rating))
+ .sort((a, b) => (a.score || 0) - (b.score || 0))
+ .slice(0, 10); // Top 10 worst files
+
+ // Build comment body
+ let commentBody = `## 🔍 RubyCritic Code Quality Report\n\n`;
+
+ // Score summary with emoji
+ let scoreEmoji = '✅';
+ let scoreStatus = 'Excellent';
+ if (score < 60) {
+ scoreEmoji = '🔴';
+ scoreStatus = 'Critical';
+ } else if (score < 70) {
+ scoreEmoji = '🟠';
+ scoreStatus = 'Poor';
+ } else if (score < 80) {
+ scoreEmoji = '🟡';
+ scoreStatus = 'Fair';
+ } else if (score < 90) {
+ scoreEmoji = '🟢';
+ scoreStatus = 'Good';
+ }
+
+ commentBody += `### Overall Score: ${scoreEmoji} **${score.toFixed(1)}/100** (${scoreStatus})\n\n`;
+
+ // Quality distribution
+ commentBody += `### Quality Distribution\n\n`;
+ commentBody += `| Grade | Files | Description |\n`;
+ commentBody += `|-------|-------|-------------|\n`;
+ commentBody += `| A | ${filesByGrade['A'] || 0} | Excellent quality |\n`;
+ commentBody += `| B | ${filesByGrade['B'] || 0} | Good quality |\n`;
+ commentBody += `| C | ${filesByGrade['C'] || 0} | Fair quality |\n`;
+ commentBody += `| D | ${filesByGrade['D'] || 0} | Poor quality |\n`;
+ commentBody += `| F | ${filesByGrade['F'] || 0} | Critical issues |\n\n`;
+
+ // Files needing attention
+ if (filesWithIssues.length > 0) {
+ commentBody += `### 📋 Files Needing Attention\n\n`;
+ commentBody += `| File | Score | Grade | Issues |\n`;
+ commentBody += `|------|-------|-------|--------|\n`;
+
+ filesWithIssues.forEach(file => {
+ const fileName = file.path.replace('pdf_converter/', '');
+ const fileScore = (file.score || 0).toFixed(1);
+ const grade = file.rating || 'N/A';
+ const smellsCount = (file.smells || []).length;
+ commentBody += `| \`${fileName}\` | ${fileScore} | ${grade} | ${smellsCount} |\n`;
+ });
+ commentBody += `\n`;
+ } else {
+ commentBody += `### ✨ Great Job!\n\nAll analyzed files have good quality ratings (B or better).\n\n`;
+ }
+
+ // Recommendations
+ commentBody += `### 💡 Recommendations\n\n`;
+ if (score >= 90) {
+ commentBody += `- ✅ Code quality is excellent! Keep up the good work.\n`;
+ } else if (score >= 80) {
+ commentBody += `- 🔹 Consider refactoring files with grade C or lower\n`;
+ commentBody += `- 🔹 Focus on reducing complexity in lower-scored files\n`;
+ } else if (score >= 70) {
+ commentBody += `- ⚠️ Several files need refactoring\n`;
+ commentBody += `- ⚠️ Review code smells and complexity issues\n`;
+ commentBody += `- ⚠️ Consider breaking down large classes/methods\n`;
+ } else {
+ commentBody += `- 🔴 Significant refactoring needed\n`;
+ commentBody += `- 🔴 Review all files with grade D or F\n`;
+ commentBody += `- 🔴 Focus on reducing complexity and code smells\n`;
+ commentBody += `- 🔴 Consider pair programming or code review sessions\n`;
+ }
+
+ commentBody += `\n---\n`;
+ commentBody += `📊 Full report available in the [RubyCritic HTML output](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})\n`;
+
+ // Find existing comment
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ });
+
+ const existingComment = comments.find(comment =>
+ comment.user.login === 'github-actions[bot]' &&
+ comment.body.includes('RubyCritic Code Quality Report')
+ );
+
+ // Post or update comment
+ if (existingComment) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: existingComment.id,
+ body: commentBody
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: commentBody
+ });
+ }
+
+ - name: Upload RubyCritic HTML Report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: rubycritic-report
+ path: pdf_converter/tmp/rubycritic/
+ retention-days: 30
diff --git a/pdf_converter/.rubocop.yml b/pdf_converter/.rubocop.yml
index 5456688..8dd8c4e 100644
--- a/pdf_converter/.rubocop.yml
+++ b/pdf_converter/.rubocop.yml
@@ -48,4 +48,8 @@ Lint/UnusedMethodArgument:
# Disable FetchEnvVar for ENV variable checks in specs and app code
Style/FetchEnvVar:
- Enabled: false
\ No newline at end of file
+ Enabled: false
+
+# Allow "error" as exception variable name (more descriptive than "e")
+Naming/RescuedExceptionsVariableName:
+ PreferredName: error
\ No newline at end of file
diff --git a/pdf_converter/app.rb b/pdf_converter/app.rb
index 857051a..15e9011 100644
--- a/pdf_converter/app.rb
+++ b/pdf_converter/app.rb
@@ -44,44 +44,27 @@ def process_pdf_conversion(request_body, start_time, response_builder)
puts "Authentication successful for unique_id: #{unique_id}"
# Download PDF
- download_result = PdfDownloader.new.download(request_body['source'])
- return handle_failure(download_result, response_builder, 'PDF download', output_dir) unless download_result[:success]
-
- pdf_content = download_result[:content]
- puts "PDF downloaded successfully, size: #{pdf_content.bytesize} bytes"
+ pdf_content = download_pdf(request_body['source'], response_builder, output_dir)
+ return pdf_content if pdf_content.is_a?(Hash) && pdf_content[:statusCode]
# Convert PDF to images
- conversion_result = PdfConverter.new.convert_to_images(
- pdf_content: pdf_content,
- output_dir: output_dir,
- unique_id: unique_id,
- dpi: ENV['CONVERSION_DPI']&.to_i || 300
- )
- unless conversion_result[:success]
- return handle_failure(conversion_result, response_builder, 'PDF conversion',
- output_dir)
- end
-
- images = conversion_result[:images]
- page_count = images.size
- puts "PDF converted successfully: #{page_count} pages"
+ conversion_result = convert_pdf_to_images(pdf_content, output_dir, unique_id, response_builder)
+ return conversion_result if conversion_result.is_a?(Hash) && conversion_result[:statusCode]
# Upload images as zip file
- upload_result = ImageUploader.new.upload_images_from_files(request_body['destination'], images, unique_id)
- return handle_failure(upload_result, response_builder, 'Zip upload', output_dir) unless upload_result[:success]
-
- zip_url = upload_result[:zip_url]
- puts "Zip file uploaded successfully: #{zip_url}"
+ zip_url = upload_images_as_zip(request_body['destination'], conversion_result[:images], unique_id, response_builder,
+ output_dir)
+ return zip_url if zip_url.is_a?(Hash) && zip_url[:statusCode]
# Send webhook notification
- notify_webhook(request_body['webhook'], unique_id, zip_url, page_count, start_time)
+ notify_webhook(request_body['webhook'], unique_id, zip_url, conversion_result[:images].size, start_time)
# Clean up and return success
FileUtils.rm_rf(output_dir)
response_builder.success_response(
unique_id: unique_id,
zip_url: zip_url,
- page_count: page_count,
+ page_count: conversion_result[:images].size,
metadata: conversion_result[:metadata]
)
end
@@ -141,6 +124,61 @@ def send_webhook(webhook_url, unique_id, zip_url, page_count, start_time)
# Don't fail the request if webhook fails, just log it
end
+# Downloads PDF from source URL
+#
+# @param source_url [String] Source URL for PDF
+# @param response_builder [ResponseBuilder] Response builder instance
+# @param output_dir [String] Output directory for cleanup on failure
+# @return [String, Hash] PDF content or error response
+def download_pdf(source_url, response_builder, output_dir)
+ download_result = PdfDownloader.new.download(source_url)
+ return handle_failure(download_result, response_builder, 'PDF download', output_dir) unless download_result[:success]
+
+ pdf_content = download_result[:content]
+ puts "PDF downloaded successfully, size: #{pdf_content.bytesize} bytes"
+ pdf_content
+end
+
+# Converts PDF content to images
+#
+# @param pdf_content [String] PDF file content
+# @param output_dir [String] Output directory for images
+# @param unique_id [String] Unique identifier
+# @param response_builder [ResponseBuilder] Response builder instance
+# @return [Hash] Conversion result or error response
+def convert_pdf_to_images(pdf_content, output_dir, unique_id, response_builder)
+ conversion_result = PdfConverter.new.convert_to_images(
+ pdf_content: pdf_content,
+ output_dir: output_dir,
+ unique_id: unique_id,
+ dpi: ENV['CONVERSION_DPI']&.to_i || 300
+ )
+ unless conversion_result[:success]
+ return handle_failure(conversion_result, response_builder, 'PDF conversion',
+ output_dir)
+ end
+
+ puts "PDF converted successfully: #{conversion_result[:images].size} pages"
+ conversion_result
+end
+
+# Uploads converted images as a zip file
+#
+# @param destination_url [String] Destination URL for zip file
+# @param images [Array] Array of image paths
+# @param unique_id [String] Unique identifier
+# @param response_builder [ResponseBuilder] Response builder instance
+# @param output_dir [String] Output directory for cleanup on failure
+# @return [String, Hash] Zip URL or error response
+def upload_images_as_zip(destination_url, images, unique_id, response_builder, output_dir)
+ upload_result = ImageUploader.new.upload_images_from_files(destination_url, images, unique_id)
+ return handle_failure(upload_result, response_builder, 'Zip upload', output_dir) unless upload_result[:success]
+
+ zip_url = upload_result[:zip_url]
+ puts "Zip file uploaded successfully: #{zip_url}"
+ zip_url
+end
+
def authenticate_request(event)
# Initialize authenticator (cached after first initialization in Lambda)
@authenticator ||= JwtAuthenticator.new(ENV['JWT_SECRET_NAME'] || 'pdf-converter/jwt-secret')
@@ -150,12 +188,12 @@ def authenticate_request(event)
# Authenticate the request
@authenticator.authenticate(headers)
-rescue JwtAuthenticator::AuthenticationError => e
+rescue JwtAuthenticator::AuthenticationError => error
# Handle secrets manager errors
- puts "ERROR: Authentication service error: #{e.message}"
+ puts "ERROR: Authentication service error: #{error.message}"
{ authenticated: false, error: 'Authentication service unavailable' }
-rescue StandardError => e
+rescue StandardError => error
# Handle any other unexpected errors
- puts "ERROR: Unexpected authentication error: #{e.message}"
+ puts "ERROR: Unexpected authentication error: #{error.message}"
{ authenticated: false, error: 'Authentication service error' }
end
diff --git a/pdf_converter/app/image_uploader.rb b/pdf_converter/app/image_uploader.rb
index cbccf82..dd46d47 100644
--- a/pdf_converter/app/image_uploader.rb
+++ b/pdf_converter/app/image_uploader.rb
@@ -42,13 +42,13 @@ def upload(url, content, content_type = 'image/png')
etag: etag,
size: content.bytesize
}
- rescue ArgumentError => e
- error_result(e.message)
+ rescue ArgumentError => error
+ error_result(error.message)
rescue URI::InvalidURIError
error_result('Invalid URL format')
- rescue StandardError => e
+ rescue StandardError => error
# Provide better error message for 403 errors
- error_message = e.message
+ error_message = error.message
if error_message.include?('403')
error_result('Access denied - URL may be expired or invalid')
else
@@ -62,32 +62,11 @@ def upload(url, content, content_type = 'image/png')
# @param content_type [String] The content type for all images
# @return [Array] Array of result hashes for each upload
def upload_batch(urls, images, content_type = 'image/png')
- raise ArgumentError, 'Number of URLs must match number of images' unless urls.size == images.size
-
+ validate_batch_inputs(urls, images)
log_info("Starting batch upload of #{urls.size} images")
- results = []
- Async do
- barrier = Async::Barrier.new
- semaphore = Async::Semaphore.new(THREAD_POOL_SIZE, parent: barrier)
-
- urls.zip(images).each_with_index do |(url, content), index|
- semaphore.async do
- result = upload(url, content, content_type)
- result[:index] = index
- results << result
- end
- end
-
- # Wait for all uploads to complete
- barrier.wait
- end
-
- # Sort results by index to maintain order
- results.sort_by! { |result| result[:index] }
-
- successful = results.count { |result| result[:success] }
- log_info("Batch upload completed: #{successful}/#{results.size} successful")
+ results = perform_concurrent_uploads(urls, images, content_type)
+ log_batch_completion(results)
results
end
@@ -115,10 +94,10 @@ def upload_images_from_files(destination_url, image_paths, unique_id)
zip_url: UrlUtils.strip_query_params([destination_url]).first,
etag: upload_result[:etag]
}
- rescue StandardError => e
+ rescue StandardError => error
{
success: false,
- error: "Zip upload error: #{e.message}"
+ error: "Zip upload error: #{error.message}"
}
end
@@ -129,6 +108,37 @@ def validate_inputs(url, content)
raise ArgumentError, 'Content cannot be nil or empty' if content.nil? || content.empty?
end
+ def validate_batch_inputs(urls, images)
+ raise ArgumentError, 'Number of URLs must match number of images' unless urls.size == images.size
+ end
+
+ def perform_concurrent_uploads(urls, images, content_type)
+ results = []
+
+ Async do
+ barrier = Async::Barrier.new
+ semaphore = Async::Semaphore.new(THREAD_POOL_SIZE, parent: barrier)
+
+ urls.zip(images).each_with_index do |(url, content), index|
+ semaphore.async do
+ result = upload(url, content, content_type)
+ result[:index] = index
+ results << result
+ end
+ end
+
+ barrier.wait
+ end
+
+ results.sort_by! { |result| result[:index] }
+ results
+ end
+
+ def log_batch_completion(results)
+ successful = results.count { |result| result[:success] }
+ log_info("Batch upload completed: #{successful}/#{results.size} successful")
+ end
+
# Uploads content with retry logic for transient failures
# @param uri [URI] The URI to upload to
# @param content [String] The content to upload
@@ -138,8 +148,8 @@ def upload_with_retry(uri, content, content_type)
RetryHandler.with_retry(logger: @logger) do
perform_upload(uri, content, content_type)
end
- rescue RetryHandler::RetryError => e
- raise StandardError, e.message
+ rescue RetryHandler::RetryError => error
+ raise StandardError, error.message
end
def perform_upload(uri, content, content_type)
diff --git a/pdf_converter/app/jwt_authenticator.rb b/pdf_converter/app/jwt_authenticator.rb
index d3c0a29..829103a 100644
--- a/pdf_converter/app/jwt_authenticator.rb
+++ b/pdf_converter/app/jwt_authenticator.rb
@@ -76,8 +76,8 @@ def validate_token(token)
{ valid: false, error: 'Invalid signature' }
rescue JWT::DecodeError
{ valid: false, error: 'Malformed token' }
- rescue StandardError => e
- { valid: false, error: "Token validation error: #{e.message}" }
+ rescue StandardError => error
+ { valid: false, error: "Token validation error: #{error.message}" }
end
end
@@ -100,10 +100,10 @@ def retrieve_secret
log_debug('Successfully retrieved JWT secret from Secrets Manager')
rescue Aws::SecretsManager::Errors::ResourceNotFoundException
handle_secret_error("Secret '#{@secret_name}' not found")
- rescue Aws::SecretsManager::Errors::ServiceError => e
- handle_secret_error("AWS service error - #{e.message}")
- rescue StandardError => e
- handle_secret_error(e.message)
+ rescue Aws::SecretsManager::Errors::ServiceError => error
+ handle_secret_error("AWS service error - #{error.message}")
+ rescue StandardError => error
+ handle_secret_error(error.message)
end
# Builds the AWS Secrets Manager client configuration
diff --git a/pdf_converter/app/pdf_converter.rb b/pdf_converter/app/pdf_converter.rb
index b0effdb..0a0d35a 100644
--- a/pdf_converter/app/pdf_converter.rb
+++ b/pdf_converter/app/pdf_converter.rb
@@ -41,11 +41,12 @@ def convert_to_images(pdf_content:, output_dir:, unique_id:, **options)
return validation_error if validation_error
# Convert all pages to images
- images = convert_all_pages(temp_pdf_path, page_count, output_dir, unique_id, conversion_dpi)
+ context = { output_dir: output_dir, unique_id: unique_id, dpi: conversion_dpi }
+ images = convert_all_pages(temp_pdf_path, page_count, context)
success_result(images, page_count, conversion_dpi)
- rescue StandardError => e
- error_result("PDF conversion failed: #{e.message}")
+ rescue StandardError => error
+ error_result("PDF conversion failed: #{error.message}")
ensure
cleanup_temp_file(temp_pdf)
end
@@ -62,8 +63,8 @@ def get_page_count(pdf_content)
temp_pdf.close
temp_pdf.unlink
count
- rescue StandardError => e
- log_error("Failed to get page count: #{e.message}")
+ rescue StandardError => error
+ log_error("Failed to get page count: #{error.message}")
0
end
@@ -82,16 +83,14 @@ def validate_page_count(page_count)
# Converts all pages of a PDF to PNG images
# @param pdf_path [String] Path to the temporary PDF file
# @param page_count [Integer] Total number of pages
- # @param output_dir [String] Directory to save images
- # @param unique_id [String] Unique identifier for naming
- # @param dpi [Integer] DPI for conversion
+ # @param context [Hash] Conversion context with :output_dir, :unique_id, :dpi
# @return [Array] Array of image file paths
- def convert_all_pages(pdf_path, page_count, output_dir, unique_id, dpi)
+ def convert_all_pages(pdf_path, page_count, context)
images = []
- log_info("Starting conversion of #{page_count} pages at #{dpi} DPI")
+ log_info("Starting conversion of #{page_count} pages at #{context[:dpi]} DPI")
(0...page_count).each do |page_index|
- image_path = convert_page(pdf_path, page_index, output_dir, unique_id, dpi)
+ image_path = convert_page(pdf_path, page_index, context)
images << image_path
page_number = page_index + 1
log_info("Converted page #{page_number}/#{page_count}")
@@ -149,26 +148,26 @@ def get_page_count_from_file(pdf_path)
# Each page is loaded vertically, so total height / page height = page count
first_page = Vips::Image.pdfload(pdf_path, n: 1, dpi: 1)
(image.height / first_page.height).to_i
- rescue StandardError => e
- log_error("Failed to load PDF for page count: #{e.message}")
+ rescue StandardError => error
+ log_error("Failed to load PDF for page count: #{error.message}")
0
end
- def convert_page(pdf_path, page_index, output_dir, unique_id, dpi)
+ def convert_page(pdf_path, page_index, context)
# Page number for filename (1-indexed)
page_number = page_index + 1
- output_filename = "#{unique_id}_page_#{page_number}.png"
- output_path = File.join(output_dir, output_filename)
+ output_filename = "#{context[:unique_id]}_page_#{page_number}.png"
+ output_path = File.join(context[:output_dir], output_filename)
# Load specific page from PDF
- image = Vips::Image.pdfload(pdf_path, page: page_index, n: 1, dpi: dpi)
+ image = Vips::Image.pdfload(pdf_path, page: page_index, n: 1, dpi: context[:dpi])
# Convert to PNG with compression
image.pngsave(output_path, compression: @compression)
output_path
- rescue StandardError => e
- log_error("Failed to convert page #{page_number}: #{e.message}")
+ rescue StandardError => error
+ log_error("Failed to convert page #{page_number}: #{error.message}")
raise
end
diff --git a/pdf_converter/app/pdf_downloader.rb b/pdf_converter/app/pdf_downloader.rb
index 3c6987f..98d1125 100644
--- a/pdf_converter/app/pdf_downloader.rb
+++ b/pdf_converter/app/pdf_downloader.rb
@@ -41,8 +41,8 @@ def download(url)
}
rescue URI::InvalidURIError
error_result('Invalid URL format')
- rescue StandardError => e
- error_result("Download failed: #{e.message}")
+ rescue StandardError => error
+ error_result("Download failed: #{error.message}")
end
# Validates that the content is a valid PDF
@@ -64,8 +64,8 @@ def download_with_retry(uri)
RetryHandler.with_retry(logger: @logger) do
fetch_with_redirects(uri)
end
- rescue RetryHandler::RetryError => e
- raise StandardError, e.message
+ rescue RetryHandler::RetryError => error
+ raise StandardError, error.message
end
def validate_url(url)
diff --git a/pdf_converter/app/webhook_notifier.rb b/pdf_converter/app/webhook_notifier.rb
index 6703a43..73bd7ca 100644
--- a/pdf_converter/app/webhook_notifier.rb
+++ b/pdf_converter/app/webhook_notifier.rb
@@ -39,8 +39,8 @@ def notify(webhook_url:, unique_id:, status:, images:, page_count:, processing_t
error_msg = "Webhook returned HTTP #{response.code}: #{response.body}"
{ error: error_msg }
end
- rescue StandardError => e
- { error: "Webhook error: #{e.message}" }
+ rescue StandardError => error
+ { error: "Webhook error: #{error.message}" }
end
private
diff --git a/pdf_converter/lib/retry_handler.rb b/pdf_converter/lib/retry_handler.rb
index 398254a..f4f8282 100644
--- a/pdf_converter/lib/retry_handler.rb
+++ b/pdf_converter/lib/retry_handler.rb
@@ -44,20 +44,20 @@ def self.with_retry(max_attempts: DEFAULT_MAX_ATTEMPTS, delay_base: DEFAULT_RETR
while attempt <= max_attempts
begin
return yield(attempt)
- rescue *NON_RETRYABLE_EXCEPTIONS => e
+ rescue *NON_RETRYABLE_EXCEPTIONS => error
# Don't retry non-retryable errors, fail immediately
- raise e
- rescue StandardError => e
- last_error = e
+ raise error
+ rescue StandardError => error
+ last_error = error
# Check if we should retry this error
- raise e unless retryable_error?(e)
+ raise error unless retryable_error?(error)
# Check if we have attempts remaining
- raise RetryError, "#{e.message} after #{max_attempts} attempts" if attempt >= max_attempts
+ raise RetryError, "#{error.message} after #{max_attempts} attempts" if attempt >= max_attempts
# Log the retry attempt
- log_retry(logger, attempt, e.message)
+ log_retry(logger, attempt, error.message)
# Wait before retrying with exponential backoff
wait_before_retry(attempt, delay_base)
diff --git a/pdf_converter/spec/app/pdf_converter_spec.rb b/pdf_converter/spec/app/pdf_converter_spec.rb
index a97d8cb..b38025a 100644
--- a/pdf_converter/spec/app/pdf_converter_spec.rb
+++ b/pdf_converter/spec/app/pdf_converter_spec.rb
@@ -463,14 +463,16 @@ def height
context 'with single page' do
it 'converts one page' do
temp_pdf = Tempfile.new(['test', '.pdf'])
- images = converter.send(:convert_all_pages, temp_pdf.path, 1, output_dir, unique_id, 300)
+ context = { output_dir: output_dir, unique_id: unique_id, dpi: 300 }
+ images = converter.send(:convert_all_pages, temp_pdf.path, 1, context)
expect(images.size).to eq(1)
temp_pdf.close!
end
it 'does not trigger garbage collection' do
temp_pdf = Tempfile.new(['test', '.pdf'])
- converter.send(:convert_all_pages, temp_pdf.path, 1, output_dir, unique_id, 300)
+ context = { output_dir: output_dir, unique_id: unique_id, dpi: 300 }
+ converter.send(:convert_all_pages, temp_pdf.path, 1, context)
expect(GC).not_to have_received(:start)
temp_pdf.close!
end
@@ -479,7 +481,8 @@ def height
context 'with 10 pages' do
it 'triggers garbage collection after 10th page' do
temp_pdf = Tempfile.new(['test', '.pdf'])
- converter.send(:convert_all_pages, temp_pdf.path, 10, output_dir, unique_id, 300)
+ context = { output_dir: output_dir, unique_id: unique_id, dpi: 300 }
+ converter.send(:convert_all_pages, temp_pdf.path, 10, context)
expect(GC).to have_received(:start).once
temp_pdf.close!
end
@@ -488,7 +491,8 @@ def height
context 'with 25 pages' do
it 'triggers garbage collection twice' do
temp_pdf = Tempfile.new(['test', '.pdf'])
- converter.send(:convert_all_pages, temp_pdf.path, 25, output_dir, unique_id, 300)
+ context = { output_dir: output_dir, unique_id: unique_id, dpi: 300 }
+ converter.send(:convert_all_pages, temp_pdf.path, 25, context)
expect(GC).to have_received(:start).twice
temp_pdf.close!
end
@@ -600,24 +604,28 @@ def height
context 'with successful conversion' do
it 'returns output path' do
- path = converter.send(:convert_page, temp_pdf.path, 0, output_dir, unique_id, 300)
+ context = { output_dir: output_dir, unique_id: unique_id, dpi: 300 }
+ path = converter.send(:convert_page, temp_pdf.path, 0, context)
expect(path).to include(output_dir)
expect(path).to end_with('.png')
end
it 'uses 1-indexed page numbers in filename' do
- path = converter.send(:convert_page, temp_pdf.path, 0, output_dir, unique_id, 300)
+ context = { output_dir: output_dir, unique_id: unique_id, dpi: 300 }
+ path = converter.send(:convert_page, temp_pdf.path, 0, context)
expect(path).to include('page_1.png')
end
it 'loads correct page from PDF' do
- converter.send(:convert_page, temp_pdf.path, 2, output_dir, unique_id, 300)
+ context = { output_dir: output_dir, unique_id: unique_id, dpi: 300 }
+ converter.send(:convert_page, temp_pdf.path, 2, context)
expect(Vips::Image).to have_received(:pdfload)
.with(temp_pdf.path, page: 2, n: 1, dpi: 300)
end
it 'saves with configured compression' do
- converter.send(:convert_page, temp_pdf.path, 0, output_dir, unique_id, 300)
+ context = { output_dir: output_dir, unique_id: unique_id, dpi: 300 }
+ converter.send(:convert_page, temp_pdf.path, 0, context)
expect(vips_image).to have_received(:pngsave)
.with(anything, compression: 6)
end