# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## 📁 File Organization Guidelines **⚠️ IMPORTANT - Temporary Documentation Files**: - **All temporary markdown files** created without explicit user request **MUST** be saved to `/plans/temp/` - Examples: exploration notes, research summaries, draft documentation, intermediate analysis - Permanent documentation (user-requested plans, design docs) goes in `/plans/` - The `/plans/temp/` directory is git-ignored and not tracked in version control ## 🤖 Task Agent Usage Guidelines **IMPORTANT**: Use specialized Task agents liberally for exploration, planning, and research tasks. ### When to Use Task Agents **Explore Agent** (use `subagent_type=Explore`): - Understanding codebase structure and architecture - Finding where features are implemented across multiple files - Exploring error handling patterns, API endpoints, or design patterns - Questions like "How does X work?", "Where is Y handled?", "What's the structure of Z?" - Set thoroughness: `quick` (basic), `medium` (moderate), or `very thorough` (comprehensive) **Plan Agent** (use `subagent_type=Plan`): - Breaking down complex feature implementations - Designing multi-step refactoring approaches - Planning architectural changes or migrations **General-Purpose Agent** (use `subagent_type=general-purpose`): - Multi-step tasks requiring multiple tool invocations - Documentation lookups via WebSearch/WebFetch - Complex searches across many files with multiple rounds ### Documentation Lookup Pattern When encountering unfamiliar libraries, frameworks, or patterns: 1. **Use Task agent with WebSearch/WebFetch** to look up official documentation 2. **Search for**: Rails 8 features, Playwright APIs, Python FastAPI patterns, etc. 3. **Example**: "Look up Rails 8 Turbo Stream documentation to understand real-time updates" ### Examples ```typescript // ❌ Don't: Manually grep/glob through unfamiliar codebase grep -r "webhook" . // ✅ Do: Use Explore agent Task(subagent_type="Explore", prompt="Find all webhook implementations and explain how webhooks work in this Rails+Python microservices architecture", thoroughness="medium") // ❌ Don't: Guess how to implement without research // Implement feature based on assumptions // ✅ Do: Use general-purpose agent for documentation lookup Task(subagent_type="general-purpose", prompt="Research Rails 8 ActionCable best practices for WebSocket broadcasting, then summarize the recommended pattern for real-time updates") ``` ## Project Overview Meme Search is a self-hosted AI-powered meme search engine with a microservices architecture: - **Rails Application** (`meme_search/meme_search_app`) - Main web app on port 3000 - **Python Image-to-Text Service** (`meme_search/image_to_text_generator`) - AI inference service on port 8000 - **PostgreSQL with pgvector** - Database with vector similarity search ## Environment Setup **Tool Versions** (managed via [mise](https://mise.jdx.dev/)): - Ruby: 3.4.2 | Python: 3.12 | Node.js: 20 | PostgreSQL: 17 (Docker) **Quick Setup**: ```bash brew install mise eval "$(mise activate zsh)" # Add to shell config cd /path/to/meme-search && mise trust && mise install mise doctor # Verify setup ``` **Run Commands**: `mise exec -- ` or let mise auto-activate when you `cd` into the project ## Development Commands ### Quick Reference **Unified CI Tests** (from project root): ```bash npm test # All tests (Rails + Python + E2E) ~5-10 min npm run test:ci:skip-e2e # Skip E2E for faster feedback ~3-5 min npm run test:rails # Rails only npm run test:python # Python only ``` **Rails** (`meme_search/meme_search_app`): ```bash ./bin/dev # Start dev server bash run_tests.sh # All Rails tests bin/rails test test/models # Model tests COVERAGE=true bin/rails test # With coverage rubocop app && brakeman # Lint + security scan ``` **Python** (`meme_search/image_to_text_generator`): ```bash bash run_tests.sh # All Python tests (lint + integration + unit) pytest tests/unit/ # Unit tests (88 tests, 81.52% coverage) ruff check app/ # Lint ``` **Docker**: ```bash docker compose up # Production (pre-built images) docker compose -f docker-compose-local-build.yml up # Local build ``` ### Docker E2E Tests **Purpose**: Validate full microservices stack (Rails + Python + PostgreSQL) in production-like containers. For **local validation before major releases**, not CI/CD (10-15 min builds). See `playwright-docker/README.md` for details. ```bash npm run test:e2e:docker # Full test (setup + run + teardown) npm run test:e2e:docker:setup # Build + start services npm run test:e2e:docker:ui # Interactive UI mode ``` **Docker E2E** (6/7 passing): Tests Docker builds, isolated ports (3001, 5433, 8000) **CI E2E** (16/16 passing): Tests local services, fast, runs in GitHub Actions ### Playwright E2E Tests (CI) **16/16 tests passing** (100% migrated from Capybara). Uses Page Object Model pattern. **See `playwright/README.md` for comprehensive docs**. ```bash npm run test:e2e # Run all tests npm run test:e2e:ui # Interactive UI mode (recommended) npm run test:e2e:debug # Step-through debugging npm run test:e2e:report # View last report ``` **Rails-Specific Patterns** (common gotchas): - Turbo Streams: Wait 500ms + `networkidle` after async DOM updates - Dialogs: Target `dialog[data-slideover-target="dialog"]` not wrapper div - Checkboxes: Target nested `input[type="checkbox"]` not container - Debounced inputs: Wait 800ms (300ms debounce + 500ms buffer) - Database: Each test resets via `resetTestDatabase()` helper **Structure**: `playwright/tests/` (16 specs), `playwright/pages/` (Page Objects), `playwright/utils/` (helpers). **Prerequisites**: Rails test server on 3000, PostgreSQL with pgvector, `npx playwright install chromium`. ## Architecture Quick Reference **Rails Models**: `ImageCore` (main meme entity), `ImageEmbedding` (384-dim vectors), `ImagePath`, `TagName`, `ImageTag`, `ImageToText` **Rails Controllers**: `ImageCoresController` (CRUD/search/webhooks), `ImageUploadsController` (drag-and-drop uploads), `Settings::{ImagePaths,TagNames,ImageToTexts}Controller` **Rails Channels**: `ImageDescriptionChannel`, `ImageStatusChannel` (WebSocket real-time updates) **Stimulus Controllers**: `file_upload_controller.js` (drag-and-drop upload UI with preview/validation) **Python FastAPI**: `app/app.py` (/add_job, /check_queue, /remove_job), `app/image_to_text_generator.py` (vision-language models), `app/jobs.py` (background worker), `app/job_queue.py` (SQLite queue), `app/senders.py` (Rails callbacks) **Python Models**: Florence-2-base (default, 250M), Florence-2-large (700M), SmolVLM-256/500, Moondream2 (1.9B), Moondream2-INT8 (1.9B quantized, ~1.5-2GB memory) ## Testing Strategy **Rails** (`test/`): Models (mock HTTP/`$embedding_model`/File), Controllers (mock HTTP/ActionCable), Channels (`assert_broadcasts`), E2E Playwright (16 tests). Coverage: SimpleCov (`COVERAGE=true`). **Python** (`tests/`): Unit (`@patch` for HTTP/models, temp SQLite), Integration (FastAPI test client, mock Rails callbacks). Coverage: pytest-cov (70% threshold, `htmlcov/`). ## Key Mocking Patterns **Rails HTTP**: `Net::HTTP.stub_any_instance(:request, Net::HTTPSuccess.new("1.1", "200", "OK")) { ... }` **Rails Embedding**: Mock `$embedding_model` global variable **Rails File System**: `File.stub(:directory?, true) { Dir.stub(:entries, [...]) { ... } }` **Python HTTP**: `@patch('app.senders.requests.post')` with `mock_response.status_code = 200` **Python Models**: `@patch('model_init.AutoModelForCausalLM.from_pretrained')` to avoid downloads ## CI/CD & Workflows **Rails CI** (`.github/workflows/pro-app-test.yml`): Brakeman, JS audit, RuboCop, Playwright E2E (16 tests, browser caching), unit tests, PostgreSQL+pgvector **Python CI** (`.github/workflows/pro-image-to-text-test.yml`): Ruff linting, integration + unit tests (60% coverage), artifacts **Build**: Manual-only Docker builds via `workflow_dispatch` or local `build_and_push.sh` script. Multi-platform support (AMD64, ARM64) → GitHub Container Registry ## Common Workflows **Image Processing (Directory Scan)**: User adds path → `ImagePath` creates `ImageCore` → Rails calls Python `/add_job` → Python processes → Calls Rails webhooks (`description_receiver`, `status_receiver`) → Rails updates DB + broadcasts WebSocket → `ImageEmbedding` created for vector search **Image Upload (Drag-and-Drop)**: User drags images to `/image_uploads/new` → Browser validates (JPG/PNG/WEBP, <10MB) → POST to `ImageUploadsController#create` → Files saved to `/public/memes/direct-uploads/` → Auto-creates "direct-uploads" `ImagePath` → Triggers scan → Creates `ImageCore` records → User manually generates descriptions **Search**: Keyword (PgSearch `search_any_word`), Vector (embedding → cosine similarity via neighbor gem), Filtering (tags, paths, embeddings) **Real-time**: ActionCable broadcasts (`ImageDescriptionChannel`, `ImageStatusChannel`) stream to all clients. Status: 0=not_started, 1=in_queue, 2=processing, 3=done, 4=removing, 5=failed ## Dev Notes - Mock model inference in CI (memory limits) - PostgreSQL with pgvector required - First model download slow, cached in `models/` - Meme dirs mounted in both services (`/rails/public/memes`, `/app/public/memes`) - **REQUIRED**: `/direct-uploads` mount for drag-and-drop uploads (`./meme_search/direct-uploads/:/rails/public/memes/direct-uploads` and `/app/public/memes/direct-uploads`) - Custom ports via `.env` (APP_PORT, GEN_PORT) ## Image Upload Feature **Location**: `/image_uploads/new` (accessible via "Upload" navigation link) **Features**: - Drag-and-drop interface with file preview - Multiple file upload support - Client-side validation (JPG/PNG/WEBP, max 10MB per file) - Filename sanitization (removes path traversal attempts) - Auto-creates "direct-uploads" ImagePath on first upload - Triggers automatic scan to create ImageCore records - Manual description generation (consistent with existing workflow) **File Storage**: - Host: `./meme_search/direct-uploads/` - Container: `/rails/public/memes/direct-uploads/` and `/app/public/memes/direct-uploads/` - Uses original filenames (sanitized for safety) - Same filename overwrites previous file - Files kept indefinitely (user deletes via standard ImageCore UI) **Testing**: - Rails controller tests: `test/controllers/image_uploads_controller_test.rb` (8 tests) - Playwright E2E: `playwright/tests/image-uploads.spec.ts` (7 tests) - Page Object: `playwright/pages/image-uploads.page.ts`