This file is intended to help AI coding assistants understand the structure, conventions, and configuration of this Ruby on Rails application. The goal is to reduce investigation time and ensure tools can reason effectively about how to program within this app.
See Also: REPO_OVERVIEW.md for complete architecture documentation and test status.
Full Details: See REPO_OVERVIEW.md for complete stack documentation.
Quick Reference: Rails 7.1.5.1 β’ Ruby 3.2.3+ β’ PostgreSQL β’ Tailwind CSS β’ Hotwire/Turbo β’ Devise β’ PgSearch β’ Kaminari
Full Details: See MODEL_RELATIONSHIPS.md for complete association documentation.
Key Conventions:
id (integer), but alumni relationships use buidcreated_at, updated_at on all modelsC-000000000 for CRM integrationdiscard or paranoia gem)Critical Rule: Association name is :alumni (NOT :alumnus)
# β
Correct
EngagementActivity.joins(:alumni)
EngagementActivity.joins(alumni: { degrees: { major: :college } })
# β Wrong
EngagementActivity.joins(:alumnus)
EngagementActivity.joins(alumni: { degrees: :college }) # Skips major
.env or Rails credentials (config/credentials.yml.enc) for secretstest/ directory)Full Details: See
docs/features/for complete feature documentation.
Core Features:
unaccent| Service/Helper | Purpose |
|---|---|
EngagementScoreCalculator |
Level-based scoring with activity caps |
TopEngagedAlumniService |
Alumni ranking with time period filtering |
FiscalYearHelper |
Fiscal year calculations from degree_date |
AlumniFilterService |
Complex filtering for stats/exports |
EngagementStats::* |
Statistics tab services β See copilot-instructions.md |
IMPORTANT: Before building any alumni matching/lookup logic, check if AlumniLookupService already handles your use case. It provides:
NICKNAME_MAPThere are two separate lookup paths with different capabilities:
| Method | What it does | Uses Nickname Variations? |
|---|---|---|
find(email:, buid:, name:) |
Uses preloaded hash index for O(1) lookup | β Only exact names |
find_name_candidates(first, last) |
Searches with generate_name_variations() |
β Expands MaddieβMadeline |
Key insight: The preloaded index only stores exact names. Nickname expansion (via NICKNAME_MAP in app/models/concerns/name_variation.rb) happens at search-time in find_name_candidates().
Common pitfall: If find() returns nil, you may still find a match via find_name_candidates(). When exactly 1 candidate is found via nickname expansion, use it as the match:
alumni = lookup_service.find(name: "Maddie Smith") # Returns nil (no exact match)
candidates = lookup_service.find_name_candidates("Maddie", "Smith") # Finds "Madeline Smith"
# Use single candidate as the match
if alumni.nil? && candidates&.length == 1
alumni = candidates.first
end
Expanding nickname variations during preload (49K alumni Γ NICKNAME_MAP iterations) takes 30+ seconds. Keep preload as exact-match only; expand variations at search-time on the smaller search term set.
# β Bad: Expanding variations during preload (slow!)
Alumni.find_each do |a|
variations = Alumni.generate_name_variations(a.first_name)
variations.each { |v| @index[v] = a }
end
# β
Good: Only expand variations when searching
def find_name_candidates(first_name, last_name)
name_variations = Alumni.generate_name_variations(first_name)
name_variations.each do |variant|
# Search for each variant...
end
end
*_id for foreign keys*_code for enums and classification fieldsbuid = unique ID for alumni recordspref_ = preferred contact method (email, phone, etc.)*_date = always a DATE column, not a stringHash Syntax: Use modern Ruby 2.7+ keyword-style hashes with symbol keys.
# β
Good
includes(degrees: { major: :college }, champion_signups: [], affinities: [])
# β Avoid mixing styles
includes(:degrees => { :major => :college }, :champion_signups, :affinities)
Eager Loading: For has_many associations, use empty arrays to make intent explicit:
# β
Explicit intent - avoids edge cases
Alumni.where(buid: buids)
.includes(degrees: { major: :college }, champion_signups: [], affinities: [])
IMPORTANT: The word βalumniβ is Latin and the same in singular and plural.
Alumni (singular model representing one alumni record)alumni (correct for both singular and plural):alumni (NOT :alumnus)alumni(:fixture_name) (NOT alumnis(:fixture_name))"alumni".pluralize returns "alumni" (configured in config/initializers/inflections.rb)Instance Variable Convention:
@alum = single Alumni record (e.g., in show, update, destroy actions)@alumni = collection of Alumni records (e.g., in index, search results)# β
Correct controller patterns
def show
@alum = Alumni.find_by!(buid: params[:id])
end
def search
@alumni = Alumni.search(query).page(params[:page])
end
# β
Correct fixture accessor in tests
test "should show alumni" do
alum = alumni(:john_doe)
get alumni_url(alum.buid)
assert_response :success
end
Model Association Errors: See MODEL_RELATIONSHIPS.md for detailed error messages and solutions.
Common Issues:
engaged_by_year, % engaged)degree_code) silently break aggregatesPhoto uploads require the native VIPS library for image processing.
Error: LoadError: Could not open library 'vips.42'
Solution:
# macOS
brew install vips
# Linux
apt-get install libvips-dev
Note: Heroku includes VIPS automatically; this is only needed for local development.
Based on lessons learned from actual development issues encountered in this project.
Before implementing any feature, always research existing patterns in the codebase:
# Before adding pagination, check how it's used elsewhere
grep -r "paginate" app/views/
grep -r "page(" app/controllers/
# Before adding CSV exports, see existing patterns
grep -r "respond_to.*csv" app/controllers/
grep -r "CSV.generate" app/
# Before adding routes, see similar route structures
grep -A5 -B5 "resources.*except" config/routes.rb
# Check what's actually installed and configured
grep -i kaminari Gemfile # Pagination gem
grep -i devise Gemfile # Authentication
grep -r "config.active_storage" config/ # File upload config
app/views/alumni/search.html.erb for search/listing patternsapp/views/settings/ for admin interface patternsNever build a complete feature without testing each component individually.
# Add route to config/routes.rb
# Test: rails routes | grep new_feature
# Start with minimal implementation
def new_feature
render plain: "Feature works!"
end
# Test: curl localhost:3000/path/to/feature
<!-- Start with minimal HTML -->
<h1>New Feature</h1>
<p>Basic content</p>
# Test: Visit page in browser
# Route validation
rails routes | grep feature_name
# Controller validation (basic functionality)
echo "MyController.new.respond_to?(:feature_method)" | rails console
# View validation (template compilation)
# Visit the page and check for ActionView errors
# Feature validation (full functionality test)
curl -I localhost:3000/feature/path
grep -r "paginate" app/views/ | head -10<%= paginate @collection %> without themesGemfile and existing usage patternsrails routes | grep similar_namerails routes | grep new_route_namedocs/development/MODEL_RELATIONSHIPS.mdecho "Model.joins(:association).count" | rails consolegrep -r "similar-class" app/views/Before declaring any feature complete:
curl -I localhost:3000/feature/path
# Test in Rails console
echo "YourController.new.your_method" | rails console
Always create annotated tags with comprehensive release notes (not brief one-liners):
git tag -a v1.0.8 -m "Release v1.0.8: Feature Title
Feature Name (Full Feature):
- Phase 1: Description
- Phase 2: Description
- What was added
Search Enhancements:
- New scopes or filters
- API changes
Fixes & Maintenance:
- Bug fixes
- Rake tasks added
- Documentation updates
Pre-deployment:
- Rake tasks to run before/after deploy
- Manual steps needed
733 tests passing"
The tag message appears on the GitHub Releases page and serves as the official changelog.
When something breaks, use these commands to quickly diagnose:
# Check route exists
rails routes | grep problematic_route
# Check controller method exists
echo "YourController.instance_methods.include?(:method_name)" | rails console
# Check model relationships
echo "YourModel.reflect_on_all_associations.map(&:name)" | rails console
# Check database state
echo "YourModel.count" | rails console
# Check gem availability
echo "defined?(Kaminari)" | rails console
# Check recent logs
tail -n 50 log/development.log
When encountering and fixing any issue:
byebug, pry, and rails console all enabled in developmentlog/development.log for controller-level errorstailwind.config.js; no PostCSS unless added manuallyDegree and Activity models with joins on buidEngagementScoreCalculator for scoring logicapp/views/shared/ or app/views/components/Context: App runs on Heroku with memory-constrained dynos. These patterns prevent R14 (memory quota) and H12 (timeout) errors.
| Pattern | Why |
|---|---|
Avoid .to_a on large queries |
Loads entire dataset into memory |
| Batch processing (50 records) | Limits memory per iteration |
| Force filtering for large datasets | Show warning if > 5,000 records unfiltered |
| Limit processing to 1,000 max | Prevents runaway queries |
| 4-hour caching for expensive calcs | Reduces repeated computation |
# config/puma.rb - Memory-optimized settings
workers 1 # Single worker (not auto-detect)
threads 3, 3 # Fixed thread count
# Monitor memory and performance
heroku logs --tail | grep -E "(Allocations|ActiveRecord|Completed)"
# Check memory usage
heroku run rake maintenance:memory_check
# Clean up sessions (run daily via Heroku Scheduler)
heroku run rake maintenance:cleanup_sessions
buid, major_code, college_code)includes() to prevent N+1 queriesREQUIRED: All features and bug fixes must include documentation updates.
docs/features/
docs/development/MODEL_RELATIONSHIPS.md
MODEL_RELATIONSHIPS.mddocs/README.md index for new filesdocs/ folderdocs/README.md updated if new files added