docs: add ARCHITECTURE.md and CONVENTIONS.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
peptiderepo
2026-04-11 18:54:22 +07:00
parent 8e8d72e683
commit 54f9719516
2 changed files with 1077 additions and 0 deletions

373
ARCHITECTURE.md Executable file
View File

@@ -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<br/>(AJAX via search form)"]
B{"Peptide<br/>exists?"}
C["Display published results<br/>(cached, links to single pages)"]
D{"Valid<br/>peptide?"}
E["Show 'not recognized'<br/>message"]
F["Create pending draft post<br/>(placeholder)"]
G["Schedule background generation<br/>(WP-Cron)"]
H["Show 'currently being added'<br/>message"]
I["AI generates full content<br/>(OpenRouter API)"]
J["Enrich with PubChem<br/>(molecular data)"]
K{"Auto-publish<br/>enabled?"}
L["Publish post"]
M["Keep as draft"]
N["Create KB article<br/>(optional)"]
O["User can revisit<br/>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<br/>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<br/>20/hour| B["Search published posts"]
B -->|Found| C["Return JSON results"]
B -->|Not found| D["Return empty results<br/>(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<br/>psa_generate_peptide_background"] --> B["Fetch post details<br/>(must be draft + pending)"]
B --> C["Build generation prompt"]
C --> D["Call OpenRouter API<br/>(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<br/>(prioritize for MW, formula)"]
G -->|Not found| I["Use AI data only"]
H --> J["Update post content<br/>+ 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).

704
CONVENTIONS.md Executable file
View File

@@ -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
<?php
if ( ! defined( 'ABSPATH' ) ) { exit; }
class PSA_[Provider]_AI {
const API_URL = 'https://...';
public static function validate_peptide_name( $name ) {
// Similar signature to PSA_AI_Generator::validate_peptide_name()
// Return: array { is_valid: bool, canonical_name: string, reason: string }
}
public static function generate_peptide_content( $peptide_name ) {
// Similar signature to PSA_AI_Generator::generate_peptide_content()
// Return: array { name, overview, sequence, ... } or WP_Error
}
}
```
2. **Update admin settings:** In `class-psa-admin.php`, add new field to select provider:
```php
add_settings_field(
'ai_provider',
__( 'AI Provider', 'peptide-search-ai' ),
array( __CLASS__, 'render_provider_field' ),
'peptide-search-ai',
'psa_ai_section'
);
```
3. **Update PSA_AI_Generator:** Modify `get_settings()` and both validation/generation methods to use the selected provider:
```php
private static function get_settings() {
$settings = wp_parse_args( get_option( 'psa_settings', array() ), $defaults );
$provider = $settings['ai_provider'] ?? 'openrouter';
return apply_filters( 'psa_ai_settings', $settings, $provider );
}
```
4. **Add fallback logic:** If provider fails, optionally retry with OpenRouter as fallback:
```php
$response = match( $provider ) {
'claude' => 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', '' );
?>
<input type="text" name="psa_settings[my_new_setting]" value="<?php echo esc_attr( $value ); ?>" />
<p class="description"><?php esc_html_e( 'Description here.', 'peptide-search-ai' ); ?></p>
<?php
}
```
5. **Use in your code:** Retrieve via `PSA_AI_Generator::get_settings()` (or create similar getter if needed):
```php
$options = self::get_settings();
$my_setting = $options['my_new_setting'] ?? 'default';
```
6. **Allow filtering:** Apply `psa_ai_settings` filter so extensions can modify:
```php
$settings = apply_filters( 'psa_ai_settings', $settings );
```
### Adding a New Meta Field
To store additional data on peptide posts (e.g., "IUPAC Name", "Clinical Trials Info"):
1. **Define in PSA_Post_Type::META_FIELDS:** `includes/class-psa-post-type.php`
```php
const META_FIELDS = array(
// ... existing fields
'psa_iupac_name' => '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 '<p>Category: ' . esc_html( $categories[0]->name ) . '</p>';
}
```
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 `<?php` not `<?`
9. **No closing PHP tag:** At end of PHP-only files, omit `?>` 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()` |