diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100755 index 0000000..b6c9e1c --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,373 @@ +# Peptide Search AI — Architecture + +## Overview + +Peptide Search AI is a WordPress plugin that transforms sites into intelligent, self-growing peptide databases. Visitors search for peptides using an AJAX search form—if a result exists, it displays instantly; if not, the plugin automatically validates the peptide name via lightweight AI, creates a placeholder post, and queues comprehensive content generation in the background via WP-Cron. The generated entry includes molecular data, mechanism of action, research applications, safety profiles, and PubChem cross-reference enrichment. All functionality is non-blocking, respects rate limits, and supports multiple search instances on a single page. + +## File Tree + +``` +peptide-search-ai/ +├── peptide-search-ai.php Main plugin file; defines constants, hooks init/admin_init +├── uninstall.php Cleans up all peptide posts, meta, options, transients +├── readme.txt Plugin directory listing (WordPress.org metadata) +├── composer.json PHP dependencies; PSR-4 autoloader for includes/ +├── .phpcs.xml.dist WordPress Coding Standards (WPCS) configuration +├── .gitignore Excludes vendor, node_modules, IDE files +│ +├── includes/ +│ ├── class-psa-config.php Configuration constants (rate limits, timeouts, tokens, TTL) +│ ├── class-psa-error.php [Deprecated] Standardized error response structure +│ ├── class-psa-post-type.php Custom Post Type (CPT) registration; meta field definitions +│ ├── class-psa-search.php AJAX search, REST API route, shortcode rendering, cache mgmt +│ ├── class-psa-ai-generator.php AI calls (validation + generation); OpenRouter integration +│ ├── class-psa-pubchem.php PubChem PUG REST API integration for molecular enrichment +│ ├── class-psa-admin.php Settings page; API key, model, auto-publish configuration +│ └── class-psa-template.php Single peptide page rendering; quick-facts tables, badges +│ +├── assets/ +│ ├── js/ +│ │ └── peptide-search.js Frontend search, AJAX handler, result rendering (jQuery) +│ └── css/ +│ └── peptide-search.css Scoped styles for search UI, results, pending states, badges +│ +└── .github/workflows/ + ├── ci.yml PHP lint, PHPCS, JS syntax checks on PR/push + └── deploy.yml Pre-deploy validation, orphan deploy branch sync, SSH deploy +``` + +## Data Flow + +```mermaid +graph TD + A["User searches peptide name
(AJAX via search form)"] + B{"Peptide
exists?"} + C["Display published results
(cached, links to single pages)"] + D{"Valid
peptide?"} + E["Show 'not recognized'
message"] + F["Create pending draft post
(placeholder)"] + G["Schedule background generation
(WP-Cron)"] + H["Show 'currently being added'
message"] + I["AI generates full content
(OpenRouter API)"] + J["Enrich with PubChem
(molecular data)"] + K{"Auto-publish
enabled?"} + L["Publish post"] + M["Keep as draft"] + N["Create KB article
(optional)"] + O["User can revisit
and see results"] + + A -->|psa_search AJAX| B + B -->|Yes| C + B -->|No, rate check| D + D -->|Invalid| E + D -->|Valid| F + F --> G + G --> H + H --> O + G -->|WP-Cron fires
psa_generate_peptide_background| I + I --> J + J --> K + K -->|Yes| L + K -->|No| M + L --> N + M --> N + N --> O + + style A fill:#e3f2fd + style C fill:#c8e6c9 + style E fill:#ffcdd2 + style H fill:#fff9c4 + style L fill:#c8e6c9 + style O fill:#c8e6c9 +``` + +### REST API Flow + +```mermaid +graph LR + A["GET /wp-json/peptides/v1/search?q=peptide"] -->|Rate limited
20/hour| B["Search published posts"] + B -->|Found| C["Return JSON results"] + B -->|Not found| D["Return empty results
(no auto-generation)"] + + style A fill:#e3f2fd + style C fill:#c8e6c9 + style D fill:#ffcdd2 +``` + +### Background Generation Flow + +```mermaid +graph TD + A["WP-Cron fires
psa_generate_peptide_background"] --> B["Fetch post details
(must be draft + pending)"] + B --> C["Build generation prompt"] + C --> D["Call OpenRouter API
(full generation model)"] + D -->|Success| E["Parse JSON response"] + D -->|Failure| F["Log error, mark failed"] + E --> G["Attempt PubChem lookup"] + G -->|Found| H["Merge PubChem data
(prioritize for MW, formula)"] + G -->|Not found| I["Use AI data only"] + H --> J["Update post content
+ all meta fields"] + I --> J + J --> K{"Auto-publish?"} + K -->|Yes| L["Publish post"] + K -->|No| M["Keep as draft"] + L --> N["Create KB article"] + M --> N + N --> O["Fire psa_after_generation hook"] + O --> P["Cleanup: delete tracking meta"] + + style A fill:#fff3e0 + style J fill:#c8e6c9 + style P fill:#c8e6c9 + style F fill:#ffcdd2 +``` + +## External API Integrations + +### OpenRouter (AI Generation & Validation) + +**Purpose:** Access hundreds of language models (Gemini, Llama, Mistral, DeepSeek, Qwen, etc.) for peptide validation and full content generation. + +**Code Locations:** +- **Validation:** `includes/class-psa-ai-generator.php` → `PSA_AI_Generator::validate_peptide_name()` (lines 32–90) + - Uses fast, cheap validation model (default: `google/gemini-2.0-flash-001`) + - Prompt: `PSA_AI_Generator::build_validation_prompt()` (validates peptide name only) + - Transient cache: 24 hours to avoid repeated API calls + +- **Full Generation:** `includes/class-psa-ai-generator.php` → `PSA_AI_Generator::generate_peptide_content()` (lines 196–220) + - Uses capable generation model (default: `google/gemini-2.5-flash`) + - Prompt: `PSA_AI_Generator::build_generation_prompt()` (full JSON structure) + - Max tokens: 4096 + - Called via WP-Cron (non-blocking) + +- **API Call:** `includes/class-psa-ai-generator.php` → `PSA_AI_Generator::call_openrouter()` (private static method) + - Endpoint: `https://openrouter.ai/api/v1/chat/completions` + - Requires API key: database `psa_settings['api_key']` or constant `PSA_OPENROUTER_KEY` + - Retry logic: 3 retries with exponential backoff (5s base delay) + +**Configuration:** `Settings > Peptide Search AI` in WordPress admin +- API Key field (or via `PSA_OPENROUTER_KEY` in wp-config.php for security) +- Generation Model input (leave blank for default) +- Validation Model input (leave blank for default) + +### PubChem PUG REST API + +**Purpose:** Verify molecular data (weight, formula, SMILES, InChI) for generated peptides to ensure scientific accuracy. + +**Code Location:** `includes/class-psa-pubchem.php` + +- **Lookup Entry:** `PSA_PubChem::lookup()` (lines 23–74) + - Checks if enabled via setting `use_pubchem` + - Caches results: not found (12h), success (7d), errors (1h) + +- **CID Lookup:** `PSA_PubChem::get_cid_by_name()` (lines 82–107) + - Endpoint: `https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/{name}/cids/JSON` + - Returns PubChem Compound ID + +- **Properties Fetch:** `PSA_PubChem::get_properties()` (lines 115–151) + - Endpoint: `https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/{cid}/property/[list]/JSON` + - Extracts: molecular formula, weight, IUPAC name, SMILES, InChI + +**Integration:** In `PSA_AI_Generator::background_generate()` (lines 143–152) +- Calls PubChem after AI generation completes +- Merges results in `save_peptide_meta()` (lines 263–268): PubChem molecular data takes precedence +- Stores PubChem CID in post meta (`psa_pubchem_cid`) +- Sets source badge to `'pubchem'` if data found, `'ai-generated'` otherwise + +**Configuration:** `Settings > Peptide Search AI` → "PubChem Enrichment" checkbox + +## Key Architectural Decisions + +### 1. Custom Post Type + Post Meta vs. Custom Tables + +**Decision:** Use CPT (`post_type='peptide'`) with post meta fields (`psa_*`). + +**Rationale:** +- **WordPress integration:** Leverage native post, user, revision, and permission systems. +- **Revisions & history:** Posts automatically support edit history; rollback is trivial. +- **Ecosystem:** Works with WP REST API, WP-CLI, backup/migration plugins without custom code. +- **Scalability:** Post meta queries are indexed and optimized; joins across postmeta are standard WP practice. +- **Admin UX:** Native post editor, meta boxes, publish workflow familiar to non-technical users. + +### 2. WP-Cron for Background Generation + +**Decision:** Use `wp_schedule_single_event()` and `spawn_cron()` for non-blocking generation. + +**Rationale:** +- **Non-blocking:** User gets "pending" response immediately; server doesn't wait for AI API calls. +- **Reliability:** WP-Cron integrates with WordPress; admin can reschedule failed tasks via hooks. +- **Cost control:** AI API calls happen in background; can rate-limit globally via daily cap (50/day). +- **Simplicity:** No external job queue infrastructure (Redis, RabbitMQ) required. + +**Trade-off:** WP-Cron triggers only when site receives traffic; if silent periods occur, delayed execution possible. + +### 3. OpenRouter as AI Provider + +**Decision:** Use OpenRouter API instead of single model (e.g., OpenAI only) or self-hosted LLM. + +**Rationale:** +- **Model flexibility:** Users can choose cheap/fast models for validation (Gemini Flash, Llama Scout) or high-quality models for generation (Gemini, Llama, DeepSeek). +- **Cost optimization:** Separate validation model (cheaper) from generation model (better quality). +- **No vendor lock-in:** Switch models without code changes. +- **Reduced CORS/auth complexity:** Single API key, consistent endpoint. + +**Trade-off:** Depends on third-party SaaS; requires API key management and cost monitoring. + +### 4. Orphan Deploy Branch + +**Decision:** CI/CD creates an orphan `deploy` branch containing only production-ready files (assets, includes, main plugin file, readme.txt). + +**Rationale:** +- **Clean deployment:** Development files (CI scripts, docs, tests) don't ship to production. +- **Hostinger deployment:** SSH-based git pull simplicity; can reset to `origin/deploy` without merge conflicts. +- **Security:** `.env`, `.github/workflows`, `composer.json` stay in dev; never push secrets. +- **Staging safety:** Deploy branch is immutable until main passes all CI checks. + +**Implementation:** `.github/workflows/deploy.yml` (lines 110–145) checks out main, removes all files, then re-adds only whitelisted files, commits, and pushes to deploy branch. + +### 5. Transient Caching + +**Decision:** Use WordPress transients for validation results, search cache, rate limits, and PubChem lookups. + +**Rationale:** +- **Simple API:** `get_transient()`, `set_transient()` with TTL is trivial compared to cache frameworks. +- **No schema:** Transients auto-expire; no cleanup scripts required. +- **Respects site config:** Uses WordPress-configured cache backend (database, Redis, memcached, etc.). +- **Rate limit tracking:** Each client IP stored as transient with 1-hour TTL per PSA_Config. + +**TTLs in use:** +- Validation cache: 24 hours (avoid repeated AI calls for same peptide name) +- Search cache: 6 hours (invalidated on post save) +- PubChem cache: 7 days for hits, 12 hours for misses +- Rate limit counters: 1 hour per request window + +## Data Storage + +### Post Meta Fields (PSA_Post_Type::META_FIELDS) + +All stored on peptide posts as post meta: + +| Key | Label | Type | Source | +|-----|-------|------|--------| +| `psa_sequence` | Amino Acid Sequence | Text | AI | +| `psa_molecular_weight` | Molecular Weight | Text | PubChem (preferred) or AI | +| `psa_molecular_formula` | Molecular Formula | Text | PubChem (preferred) or AI | +| `psa_aliases` | Aliases / Synonyms | Text | AI | +| `psa_mechanism` | Mechanism of Action | Text | AI | +| `psa_research_apps` | Research Applications | Text | AI | +| `psa_safety_profile` | Safety & Side Effects | Text | AI | +| `psa_dosage_info` | Dosage Information | Text | AI | +| `psa_references` | References & Citations | Text | AI | +| `psa_source` | Data Source (badge) | Enum: `ai-generated\|pubchem\|manual\|pending\|failed` | System | +| `psa_pubchem_cid` | PubChem Compound ID | Integer | PubChem | +| `psa_generation_started` | Generation start timestamp | Unix timestamp | System (temporary, deleted on completion) | +| `psa_generation_attempts` | Retry counter | Integer | System (temporary) | +| `psa_generation_error` | Error message (if generation failed) | Text | System (temporary) | +| `psa_generation_completed` | Generation completion timestamp | Unix timestamp | System | + +### Options (Plugin Settings) + +Stored in `wp_options` table: + +```php +$psa_settings = array( + 'api_key' => 'sk-...', // OpenRouter API key (or use PSA_OPENROUTER_KEY constant) + 'ai_model' => 'google/gemini-2.5-flash', // Full generation model + 'validation_model' => 'google/gemini-2.0-flash-001', // Lightweight validation model + 'auto_publish' => 'draft|publish', // Auto-publish strategy + 'use_pubchem' => '1|0', // Enable PubChem enrichment +); +``` + +## Frontend Architecture + +### JavaScript (jQuery-based) + +**File:** `assets/js/peptide-search.js` (214 lines) + +- **Multiple instances:** Each `[peptide_search]` shortcode gets a unique instance scoped to `.psa-search-wrap` container. +- **AJAX handler:** `initSearchInstance()` (lines 20–204) manages form submission, input events, result rendering. +- **Debouncing:** Input changes debounced 400ms to avoid excessive AJAX calls. +- **Response handling:** + - `status: 'found'` → render results with title, molecular data, source badge, excerpt, link to single page + - `status: 'pending'` → show "currently being added" message + - `status: 'invalid'` → show "peptide not recognized" message + - `status: 'rate_limited'` → show rate limit error +- **Error handling:** Network, timeout, server errors caught and displayed to user. + +### CSS (BEM-inspired class structure) + +**File:** `assets/css/peptide-search.css` (388 lines) + +- **Prefix:** All classes prefixed `.psa-` (Peptide Search AI) to avoid conflicts. +- **Search form:** `.psa-search-input-wrap`, `.psa-search-input`, `.psa-search-btn` +- **Results:** `.psa-result-item`, `.psa-result-title`, `.psa-result-meta`, `.psa-result-badge` +- **States:** `.psa-checking`, `.psa-pending`, `.psa-invalid`, `.psa-error` +- **Single page:** `.psa-peptide-data`, `.psa-quick-facts`, `.psa-facts-table`, `.psa-source-badge` +- **Responsive:** Mobile breakpoint at 600px for touch-friendly search and collapsed table layout. +- **Colors:** Blue (`#2563eb`) for primary actions, green for verified, yellow for AI-generated, blue for manual. + +### Shortcode Rendering + +**Location:** `PSA_Search::render_search_form()` (lines 117–162 in class-psa-search.php) + +- Instance counter ensures unique `data-psa-instance` attributes on multiple shortcodes. +- Form structure: input + submit button in flex container. +- Result containers pre-rendered (hidden) for AJAX population. +- ARIA attributes for accessibility: `role="search"`, `aria-live="polite"`, `aria-busy` toggle. + +### Asset Loading + +**Location:** `psa_maybe_enqueue_assets()` in main plugin file (lines 63–86) + +- Conditional: load only on: + - Front page (hero search) + - KB page (page ID 73 or slug `peptide-database`) + - Posts/pages containing `[peptide_search]` shortcode + - Single peptide pages (`is_singular('peptide')`) +- Script localization: `psaAjax` object with `ajaxurl` and nonce injected to frontend. + +## Security Considerations + +- **CSRF:** All AJAX calls include nonce verification (`psa_search_nonce`). +- **Rate limiting:** IP-based transient tracking; AJAX (10/hour), REST API (20/hour). +- **API key:** Stored in options table (fallback) or wp-config.php constant `PSA_OPENROUTER_KEY` (preferred). +- **Sanitization:** All user inputs sanitized via `sanitize_text_field()`, `wp_unslash()`, `wp_kses_post()`. +- **Peptide name validation:** Pre-AI-call character validation blocks obvious injection attempts. +- **SQL injection:** All database queries use `wpdb->prepare()` with placeholders. +- **Output escaping:** HTML output escaped via `esc_html()`, `esc_attr()`, `esc_url()`. + +## Performance Optimizations + +- **Post caching:** `_prime_post_caches()` and `update_meta_cache()` batch-load posts and meta (lines 458–459 in class-psa-search.php). +- **Transient caching:** Search results cached 6 hours; invalidated on post save. +- **Conditional asset loading:** CSS/JS only loaded on relevant pages. +- **Lazy PubChem:** Optional, can be disabled via setting. +- **Daily generation cap:** 50/day to limit API costs. +- **Separate validation model:** Fast/cheap model for upfront validation to reduce full-generation calls. + +## Testing + +**CI Pipeline:** `.github/workflows/ci.yml` +- PHP 7.4, 8.1, 8.3 syntax checks +- WordPress Coding Standards (PHPCS) validation +- JavaScript syntax checks (Node.js) + +**Manual Testing Checklist:** +- Search for existing peptide → results appear +- Search for new peptide (valid) → "pending" message → check back → content generated +- Search for invalid peptide → "not recognized" message +- Test rate limits (10/hour AJAX, 20/hour REST API) +- Verify PubChem data merges correctly +- Test auto-publish on/off +- Verify shortcode works multiple times on same page +- Test on mobile (600px breakpoint) + +## Deployment + +See `.github/workflows/deploy.yml`: +1. Push to main triggers CI tests. +2. If tests pass, validate deployment readiness (required files, no sensitive data). +3. Sync clean files to orphan `deploy` branch. +4. SSH to Hostinger, git pull `deploy` branch into plugin directory. +5. Verify deployment (check version, required files exist). diff --git a/CONVENTIONS.md b/CONVENTIONS.md new file mode 100755 index 0000000..9cd1f14 --- /dev/null +++ b/CONVENTIONS.md @@ -0,0 +1,704 @@ +# Peptide Search AI — Code Conventions + +## Step-by-Step Guides + +### Adding a New AI Provider + +If you want to support a different AI service (e.g., Claude API, LLaMA Cloud, Azure OpenAI) instead of or alongside OpenRouter: + +1. **Create a new provider class:** `includes/class-psa-[provider]-ai.php` + ```php + PSA_Claude_AI::generate_peptide_content( $name ), + 'openrouter' => PSA_AI_Generator::generate_peptide_content( $name ), + default => new WP_Error( 'unknown_provider', "Provider '$provider' not supported" ), + }; + ``` + +5. **Update settings sanitizer:** In `PSA_Admin::sanitize_settings()`, add provider field: + ```php + 'ai_provider' => sanitize_text_field( $input['ai_provider'] ?? 'openrouter' ), + ``` + +6. **Document settings:** Update `readme.txt` with new provider option. + +### Adding a New Setting + +To add a new admin configurable setting (e.g., "Prompt language", "Max retries"): + +1. **Define constant in PSA_Config:** `includes/class-psa-config.php` + ```php + const MY_NEW_SETTING_DEFAULT = 'some_value'; + ``` + +2. **Add to settings sanitizer:** `includes/class-psa-admin.php` + ```php + public static function sanitize_settings( $input ) { + return array( + // ... existing fields + 'my_new_setting' => sanitize_text_field( $input['my_new_setting'] ?? '' ), + ); + } + ``` + +3. **Register settings field:** In `PSA_Admin::register_settings()` + ```php + add_settings_field( + 'my_new_setting', + __( 'My New Setting', 'peptide-search-ai' ), + array( __CLASS__, 'render_my_new_setting_field' ), + 'peptide-search-ai', + 'psa_behavior_section' // or 'psa_ai_section' + ); + ``` + +4. **Create field renderer:** Add method to `PSA_Admin` class + ```php + public static function render_my_new_setting_field() { + $value = self::get_setting( 'my_new_setting', '' ); + ?> + +

+ 'IUPAC Name', + 'psa_clinical_data' => 'Clinical Trials', + ); + ``` + +2. **Update meta box renderer:** In `PSA_Post_Type::render_meta_box()` + ```php + $textarea_fields = array( + // ... existing fields + 'psa_clinical_data', // Add to textarea list if long text + ); + ``` + +3. **Populate during generation:** In `PSA_AI_Generator::save_peptide_meta()` + ```php + private static function save_peptide_meta( $post_id, $ai_data, $pubchem_data ) { + $meta = array( + // ... existing fields + 'psa_iupac_name' => $ai_data['iupac_name'] ?? '', + 'psa_clinical_data' => $ai_data['clinical_trials'] ?? '', + ); + } + ``` + +4. **Include in generation prompt:** Update `PSA_AI_Generator::build_generation_prompt()` to request the field in AI response: + ```php + $prompt = "... Return JSON with: { name, sequence, iupac_name, clinical_trials, ... }"; + ``` + +5. **Display on single page:** In `PSA_Template::append_peptide_data()` + ```php + $detail_fields = array( + 'psa_iupac_name' => 'IUPAC Name', + 'psa_clinical_data' => 'Clinical Trials', + // ... existing fields + ); + ``` + +6. **Optionally export via REST:** Update `PSA_Search::format_peptide()` to include in REST response: + ```php + return array( + // ... existing fields + 'iupac_name' => get_post_meta( $post_id, 'psa_iupac_name', true ), + 'clinical_data' => get_post_meta( $post_id, 'psa_clinical_data', true ), + ); + ``` + +### Adding a New Peptide Category + +To organize peptides into categories (e.g., "Hormones", "Growth Factors", "Neuropeptides"): + +1. **Register custom taxonomy:** In main `peptide-search-ai.php` or new helper file + ```php + function psa_register_peptide_category_taxonomy() { + register_taxonomy( + 'peptide_category', + 'peptide', + array( + 'label' => 'Peptide Categories', + 'hierarchical' => true, + 'show_in_rest' => true, + 'rewrite' => array( 'slug' => 'peptide-category' ), + ) + ); + } + add_action( 'init', 'psa_register_peptide_category_taxonomy' ); + ``` + +2. **Add category field to AI generation prompt:** Update `PSA_AI_Generator::build_generation_prompt()` + ```php + $prompt = "Determine the category of {$peptide_name}. + Return JSON including: { ..., category: 'hormone'|'growth_factor'|'neuropeptide'|'other' }"; + ``` + +3. **Assign category during generation:** In `PSA_AI_Generator::background_generate()` + ```php + if ( ! empty( $result['category'] ) ) { + wp_set_object_terms( $post_id, $result['category'], 'peptide_category' ); + } + ``` + +4. **Display category on single page:** In `PSA_Template::append_peptide_data()` + ```php + $categories = wp_get_object_terms( $post_id, 'peptide_category' ); + if ( ! is_wp_error( $categories ) && ! empty( $categories ) ) { + echo '

Category: ' . esc_html( $categories[0]->name ) . '

'; + } + ``` + +5. **Filter search by category:** Extend `PSA_Search::search_peptides()` to accept optional `category` parameter: + ```php + public static function search_peptides( $query, $category = null ) { + // ... existing search logic + if ( $category ) { + // JOIN with taxonomy terms + } + } + ``` + +6. **Allow REST API filtering:** Update REST route in `PSA_Search::register_rest_routes()` + ```php + 'args' => array( + 'q' => array( ... ), + 'category' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + ``` + +## Naming Conventions + +### CSS Classes (`.psa-*`) + +All CSS classes use lowercase with hyphens, prefixed `.psa-`: + +```css +/* Search form */ +.psa-search-wrap /* Container for entire search UI */ +.psa-search-form /* Form element */ +.psa-search-input-wrap /* Flex container for input + button */ +.psa-search-input /* Text input field */ +.psa-search-btn /* Submit button */ +.psa-search-icon /* Icon inside button */ + +/* Results */ +.psa-search-results /* Results container */ +.psa-result-count /* "N peptides found" text */ +.psa-result-item /* Single result link */ +.psa-result-title /* Result title heading */ +.psa-result-meta /* Metadata row (MW, formula, etc.) */ +.psa-result-excerpt /* Preview text */ +.psa-result-badge /* Source badge on result */ + +/* States */ +.psa-checking /* "Searching..." loading state */ +.psa-checking-inner /* Inner content of checking state */ +.psa-spinner /* Animated spinner */ +.psa-pending /* "Currently being added" message */ +.psa-pending-inner /* Inner content of pending state */ +.psa-pending-icon /* Icon inside pending state */ +.psa-invalid /* "Not found" message */ +.psa-invalid-inner /* Inner content of invalid state */ +.psa-invalid-text /* Text inside invalid state */ +.psa-error /* Error message */ + +/* Single page */ +.psa-peptide-data /* Container for all peptide data on single page */ +.psa-quick-facts /* Quick facts box */ +.psa-facts-table /* Table inside quick facts */ +.psa-sequence /* Amino acid sequence code block */ +.psa-section /* Detail section (Mechanism, Safety, etc.) */ +.psa-section-content /* Rich text content inside section */ +.psa-source-badge /* Source attribution badge at bottom */ + +/* Badge variants */ +.psa-badge-ai /* AI-generated badge (yellow) */ +.psa-badge-verified /* PubChem verified badge (green) */ +.psa-badge-manual /* Manually curated badge (blue) */ + +/* KB page override */ +.psa-kb-search-wrap /* Search container on KB page */ +``` + +**BEM-inspired structure:** +- Block: `.psa-search-wrap` (independent component) +- Element: `.psa-search-input` (part of block, `__` replaces with `-`) +- Modifier: `.psa-badge-ai` (variant of `.psa-badge`, `-` separates type) + +### PHP Naming (`psa_*`, `PSA_*`) + +#### Functions (Global, Snake Case) + +```php +// Plugin-level utility functions +psa_init() /* Main init hook callback */ +psa_admin_init() /* Admin init hook callback */ +psa_activate() /* Plugin activation hook */ +psa_deactivate() /* Plugin deactivation hook */ +psa_maybe_enqueue_assets() /* Conditional asset loading */ +psa_enqueue_frontend_assets() /* Shared asset enqueueing */ +psa_replace_kb_search() /* KB page search injection */ +psa_is_kb_page() /* Check if current page is KB */ +psa_get_client_ip() /* Get visitor IP for rate limiting */ +psa_get_admin_user_id() /* Get first admin user ID */ +``` + +#### Classes (PascalCase, PSA_ Prefix) + +```php +PSA_Config /* Configuration constants */ +PSA_Error /* Error handling [deprecated] */ +PSA_Post_Type /* CPT registration & meta */ +PSA_Search /* Search, AJAX, REST API, shortcode */ +PSA_AI_Generator /* OpenRouter integration */ +PSA_PubChem /* PubChem API integration */ +PSA_Admin /* Settings pages & fields */ +PSA_Template /* Single page rendering */ +``` + +#### Class Methods (camelCase) + +```php +// Static utility methods (all methods in Peptide Search AI are static) +PSA_Search::init() +PSA_Search::render_search_form() +PSA_Search::ajax_search() +PSA_Search::rest_search() +PSA_Search::search_peptides() +PSA_Search::format_peptide() + +// Public methods: descriptive action verbs +PSA_AI_Generator::validate_peptide_name() +PSA_AI_Generator::generate_peptide_content() +PSA_AI_Generator::background_generate() +PSA_PubChem::lookup() + +// Private methods: prefixed with underscore +PSA_Search::find_pending_peptide() +PSA_AI_Generator::parse_ai_response() +PSA_AI_Generator::get_settings() +``` + +#### Post Meta Keys (`psa_*` prefix) + +All lowercase, hyphen-separated (converted to underscore in database): + +```php +psa_sequence /* Amino acid sequence */ +psa_molecular_weight /* Molecular weight (Da) */ +psa_molecular_formula /* Molecular formula (e.g., C12H20N2O3) */ +psa_aliases /* Alternative names */ +psa_mechanism /* Mechanism of action */ +psa_research_apps /* Research applications */ +psa_safety_profile /* Safety & side effects */ +psa_dosage_info /* Dosage information */ +psa_references /* References & citations */ +psa_source /* Source: ai-generated, pubchem, manual, pending, failed */ +psa_pubchem_cid /* PubChem Compound ID */ + +/* Tracking fields (temporary, deleted on completion) */ +psa_generation_started /* Unix timestamp when generation began */ +psa_generation_attempts /* Retry counter */ +psa_generation_error /* Error message if generation failed */ +psa_generation_completed /* Unix timestamp when generation finished */ +``` + +#### Options Keys (`psa_*` prefix) + +Stored in `wp_options`: + +```php +psa_settings /* Serialized array of all plugin settings */ +``` + +#### Constants (`PSA_*` uppercase) + +```php +/* Core plugin constants (defined in main file) */ +PSA_VERSION /* Plugin version (e.g., '4.0.0') */ +PSA_PLUGIN_DIR /* Absolute filesystem path to plugin directory */ +PSA_PLUGIN_URL /* Absolute URL to plugin directory */ +PSA_PLUGIN_FILE /* Absolute path to main plugin file */ + +/* Security constant (optional, in wp-config.php) */ +PSA_OPENROUTER_KEY /* OpenRouter API key (preferred over database) */ + +/* Configuration constants (PSA_Config class) */ +PSA_Config::MIN_QUERY_LENGTH /* Minimum search query length */ +PSA_Config::MAX_QUERY_LENGTH /* Maximum search query length */ +PSA_Config::AJAX_RATE_LIMIT /* Requests per IP per hour (AJAX) */ +PSA_Config::REST_RATE_LIMIT /* Requests per IP per hour (REST) */ +PSA_Config::DAILY_GENERATION_CAP /* Max new peptides per day */ +PSA_Config::PENDING_TIMEOUT /* Timeout for "pending" status (seconds) */ +PSA_Config::VALIDATION_MAX_TOKENS /* Max tokens for validation AI call */ +PSA_Config::GENERATION_MAX_TOKENS /* Max tokens for full generation */ +``` + +### Hooks and Filters + +#### Action Hooks (Format: `psa_{before|after}_{action}`) + +Executed at key lifecycle points. Plugins can hook with `add_action()`: + +```php +/* WordPress integration hooks */ +init /* Standard WordPress init hook */ +admin_init /* Standard WordPress admin init */ +wp_enqueue_scripts /* Standard WordPress asset loading */ +wp_footer /* Standard WordPress footer hook */ +add_meta_boxes /* Standard WordPress meta box hook */ +save_post_peptide /* Standard WordPress post save hook */ +wp_ajax_psa_search /* AJAX handler for search */ +wp_ajax_nopriv_psa_search /* AJAX handler (unauthenticated) */ +rest_api_init /* REST API route registration */ + +/* Plugin-specific actions */ +psa_peptide_created /* Fired when placeholder post is created */ +do_action( 'psa_peptide_created', $post_id, $peptide_name ); + +psa_before_generation /* Fired before AI generation starts */ +do_action( 'psa_before_generation', $post_id, $peptide_name ); + +psa_after_generation /* Fired after generation completes */ +do_action( 'psa_after_generation', $post_id, $result, $peptide_name ); +``` + +#### Filter Hooks (Format: `psa_{noun}_{context}`) + +Allow plugins to modify data. Plugins can hook with `add_filter()`: + +```php +/* Search & retrieval */ +psa_search_results /* Modify search results before return */ +apply_filters( 'psa_search_results', $response, $query ); + +/* AI & validation */ +psa_validation_prompt /* Modify validation prompt before API call */ +apply_filters( 'psa_validation_prompt', $prompt, $peptide_name ); + +psa_generation_prompt /* Modify generation prompt before API call */ +apply_filters( 'psa_generation_prompt', $prompt, $peptide_name ); + +psa_ai_settings /* Modify settings before use in AI code */ +apply_filters( 'psa_ai_settings', $settings ); + +/* Data storage */ +psa_peptide_meta /* Modify meta fields before saving */ +apply_filters( 'psa_peptide_meta', $meta, $post_id, $ai_data, $pubchem_data ); + +psa_template_data /* Modify template data before single-page rendering */ +apply_filters( 'psa_template_data', $data, $post_id ); +``` + +## Error Handling Patterns + +### Returning WP_Error + +Use `new WP_Error( $code, $message )` for recoverable failures: + +```php +public static function validate_peptide_name( $name ) { + $options = self::get_settings(); + $api_key = $options['api_key']; + + if ( empty( $api_key ) ) { + return new WP_Error( 'no_api_key', 'OpenRouter API key is not configured.' ); + } + + // ... call API ... + + if ( is_wp_error( $response ) ) { + return $response; // Propagate error up + } + + // Success: return array + return array( 'is_valid' => true, 'canonical_name' => $canonical_name ); +} +``` + +### Checking for WP_Error + +Before using a result, check if it's an error: + +```php +$validation = PSA_AI_Generator::validate_peptide_name( $query ); + +if ( is_wp_error( $validation ) ) { + error_log( 'PSA: Validation error: ' . $validation->get_error_message() ); + wp_send_json_success( + array( 'status' => 'invalid', 'message' => 'Could not verify this peptide name.' ) + ); + return; +} + +// Safe to access $validation as array +if ( empty( $validation['is_valid'] ) ) { + // ... handle invalid result +} +``` + +### Logging Errors + +Use PHP's `error_log()` for debugging; logs to wp-content/debug.log if `WP_DEBUG_LOG` is true: + +```php +error_log( 'PSA: Generated content for "' . $peptide_name . '" (post ' . $post_id . ')' ); +error_log( 'PSA: Generation FAILED: ' . $error_message ); +``` + +### User-Facing Errors + +For AJAX/REST responses, return JSON errors via `wp_send_json_error()` or `wp_send_json_success()` with error status: + +```php +// AJAX error response +wp_send_json_error( 'Search query is too long.' ); + +// REST error response +return new WP_REST_Response( + array( 'error' => 'Rate limit exceeded.' ), + 429 +); + +// AJAX success but with error status +wp_send_json_success( + array( + 'status' => 'invalid', + 'message' => 'This does not appear to be a recognized peptide.', + ) +); +``` + +## Code Style Rules + +### PHP Formatting (WordPress Coding Standards) + +1. **Indentation:** Tabs (not spaces). Set editor to use tabs. + +2. **Yoda Conditions:** Place constants/variables on left side of comparison: + ```php + // Good + if ( 'pending' === $source ) { ... } + if ( true === $is_valid ) { ... } + + // Avoid + if ( $source === 'pending' ) { ... } + ``` + +3. **String Escaping:** Always escape output to the browser: + ```php + // Good + echo esc_html( $peptide_name ); + echo esc_attr( $class_name ); + echo wp_kses_post( $post_content ); + echo esc_url( $permalink ); + + // Avoid + echo $peptide_name; + ``` + +4. **Sanitization:** Always sanitize user input: + ```php + // Good + $query = sanitize_text_field( wp_unslash( $_GET['q'] ?? '' ) ); + $content = wp_kses_post( wp_unslash( $_POST['content'] ?? '' ) ); + + // Avoid + $query = $_GET['q']; + ``` + +5. **Spacing:** + - No spaces inside parentheses: `if ( $condition )` not `if( $condition )` + - Space after keywords: `if`, `foreach`, `function`, `return` + - Space around operators: `$a = $b + $c` not `$a=$b+$c` + +6. **Line length:** Keep lines under 120 characters (configure in `.phpcs.xml.dist`). + +7. **Function documentation:** Use PHPDoc for all functions/methods: + ```php + /** + * Brief description of what this does. + * + * Longer description if needed. Can span multiple lines. + * + * @param string $name The peptide name to validate. + * @param int $post_id Optional post ID (default 0). + * @return array|WP_Error Result array or error. + */ + public static function validate_peptide_name( $name, $post_id = 0 ) { + // ... + } + ``` + +8. **No shorthand PHP tags:** Use `` to prevent accidental whitespace. + +### JavaScript Formatting + +1. **Quote style:** Use single quotes for strings (consistent with WordPress JS standards). + +2. **Semicolons:** Always end statements with `;` + +3. **Indentation:** 4 spaces (or match project setting). + +4. **Immediately Invoked Function Expression (IIFE):** Wrap in `(function ($) { ... })(jQuery)` to avoid global scope pollution. + +5. **Comment blocks:** + ```javascript + /** + * Render search results. + */ + function renderResults(data) { + // ... implementation + } + ``` + +6. **jQuery document-ready:** Use `$(document).ready()` only for initialization, not for every event handler. + +7. **Error handling:** Always handle AJAX errors: + ```javascript + $.ajax({ + success: function (response) { ... }, + error: function (jqXHR, textStatus, errorThrown) { ... } + }); + ``` + +### CSS Formatting + +1. **Class naming:** Lowercase, hyphens, `.psa-` prefix (see CSS Naming Conventions section). + +2. **Property order:** Use logical grouping: + ```css + .psa-button { + /* Layout */ + display: inline-block; + width: 100%; + + /* Spacing */ + padding: 10px; + margin: 5px 0; + + /* Colors */ + background: #2563eb; + color: #fff; + + /* Typography */ + font-size: 16px; + font-weight: 600; + + /* Transitions */ + transition: background 0.2s; + } + ``` + +3. **Media queries:** Place at end of component or in separate section. + +4. **No IDs:** Use classes only (except for single shortcode instances managed by JS). + +5. **Scoped to plugin:** Never style global elements; always scope via `.psa-` classes. + +### Composer & Dependencies + +1. **composer.json:** Declared dependencies in `require` and `require-dev`. + ```json + "require": { + "php": ">=7.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.0 || ^10.0", + "squizlabs/php_codesniffer": "^3.7", + "wp-coding-standards/wpcs": "^3.0" + } + ``` + +2. **PSR-4 Autoload:** Classes in `includes/` can be autoloaded via `PSA\` namespace (not currently used, but available). + +3. **PHPCS:** Run `composer run lint` before committing: + ```bash + composer install + composer run lint + composer run lint:fix # Auto-fix formatting issues + ``` + +## Summary Table + +| Aspect | Pattern | Example | +|--------|---------|---------| +| **CSS Classes** | `.psa-{name}` | `.psa-search-wrap`, `.psa-badge-verified` | +| **PHP Functions** | `psa_{action}()` | `psa_init()`, `psa_enqueue_frontend_assets()` | +| **PHP Classes** | `PSA_{Name}` | `PSA_Search`, `PSA_AI_Generator` | +| **Class Methods** | `method_name()` | `validate_peptide_name()`, `render_search_form()` | +| **Post Meta** | `psa_{field}` | `psa_sequence`, `psa_molecular_weight` | +| **Constants** | `PSA_{NAME}` | `PSA_VERSION`, `PSA_CONFIG::MAX_TOKENS` | +| **Action Hooks** | `psa_{event}` | `psa_peptide_created`, `psa_after_generation` | +| **Filter Hooks** | `psa_{noun}_{context}` | `psa_search_results`, `psa_peptide_meta` | +| **Indentation** | Tabs | 1 tab = indentation level | +| **Line Length** | <120 chars | Configure in `.phpcs.xml.dist` | +| **Escaping** | `esc_*()` always | `esc_html()`, `esc_attr()`, `wp_kses_post()` | +| **Sanitizing** | `sanitize_*()` always | `sanitize_text_field()`, `wp_unslash()` |