Documents the PR-gated review process enforced at the agent layer. Free-plan workaround since branch protection requires GitHub Pro.
26 KiB
Executable File
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:
-
Create a new provider class:
includes/class-psa-[provider]-ai.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 } } -
Update admin settings: In
class-psa-admin.php, add new field to select provider:add_settings_field( 'ai_provider', __( 'AI Provider', 'peptide-search-ai' ), array( __CLASS__, 'render_provider_field' ), 'peptide-search-ai', 'psa_ai_section' ); -
Update PSA_AI_Generator: Modify
get_settings()and both validation/generation methods to use the selected provider: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 ); } -
Add fallback logic: If provider fails, optionally retry with OpenRouter as fallback:
$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" ), }; -
Update settings sanitizer: In
PSA_Admin::sanitize_settings(), add provider field:'ai_provider' => sanitize_text_field( $input['ai_provider'] ?? 'openrouter' ), -
Document settings: Update
readme.txtwith new provider option.
Adding a New Setting
To add a new admin configurable setting (e.g., "Prompt language", "Max retries"):
-
Define constant in PSA_Config:
includes/class-psa-config.phpconst MY_NEW_SETTING_DEFAULT = 'some_value'; -
Add to settings sanitizer:
includes/class-psa-admin.phppublic static function sanitize_settings( $input ) { return array( // ... existing fields 'my_new_setting' => sanitize_text_field( $input['my_new_setting'] ?? '' ), ); } -
Register settings field: In
PSA_Admin::register_settings()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' ); -
Create field renderer: Add method to
PSA_Adminclasspublic 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 } -
Use in your code: Retrieve via
PSA_AI_Generator::get_settings()(or create similar getter if needed):$options = self::get_settings(); $my_setting = $options['my_new_setting'] ?? 'default'; -
Allow filtering: Apply
psa_ai_settingsfilter so extensions can modify:$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"):
-
Define in PSA_Post_Type::META_FIELDS:
includes/class-psa-post-type.phpconst META_FIELDS = array( // ... existing fields 'psa_iupac_name' => 'IUPAC Name', 'psa_clinical_data' => 'Clinical Trials', ); -
Update meta box renderer: In
PSA_Post_Type::render_meta_box()$textarea_fields = array( // ... existing fields 'psa_clinical_data', // Add to textarea list if long text ); -
Populate during generation: In
PSA_AI_Generator::save_peptide_meta()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'] ?? '', ); } -
Include in generation prompt: Update
PSA_AI_Generator::build_generation_prompt()to request the field in AI response:$prompt = "... Return JSON with: { name, sequence, iupac_name, clinical_trials, ... }"; -
Display on single page: In
PSA_Template::append_peptide_data()$detail_fields = array( 'psa_iupac_name' => 'IUPAC Name', 'psa_clinical_data' => 'Clinical Trials', // ... existing fields ); -
Optionally export via REST: Update
PSA_Search::format_peptide()to include in REST response: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"):
-
Register custom taxonomy: In main
peptide-search-ai.phpor new helper filefunction 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' ); -
Add category field to AI generation prompt: Update
PSA_AI_Generator::build_generation_prompt()$prompt = "Determine the category of {$peptide_name}. Return JSON including: { ..., category: 'hormone'|'growth_factor'|'neuropeptide'|'other' }"; -
Assign category during generation: In
PSA_AI_Generator::background_generate()if ( ! empty( $result['category'] ) ) { wp_set_object_terms( $post_id, $result['category'], 'peptide_category' ); } -
Display category on single page: In
PSA_Template::append_peptide_data()$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>'; } -
Filter search by category: Extend
PSA_Search::search_peptides()to accept optionalcategoryparameter:public static function search_peptides( $query, $category = null ) { // ... existing search logic if ( $category ) { // JOIN with taxonomy terms } } -
Allow REST API filtering: Update REST route in
PSA_Search::register_rest_routes()'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-:
/* 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)
// 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)
PSA_Config /* Configuration constants */
PSA_Post_Type /* CPT registration & meta */
PSA_Search /* Search, AJAX, REST API, shortcode */
PSA_AI_Generator /* AI pipeline orchestrator */
PSA_OpenRouter /* OpenRouter API communication layer */
PSA_KB_Builder /* Echo Knowledge Base article builder */
PSA_PubChem /* PubChem API integration */
PSA_Cost_Tracker /* API usage tracking & budget enforcement */
PSA_Encryption /* AES-256-CBC API key encryption */
PSA_Admin /* Settings pages & fields */
PSA_Template /* Single page rendering */
Class Methods (camelCase)
// 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):
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:
psa_settings /* Serialized array of all plugin settings */
Constants (PSA_* uppercase)
/* 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():
/* 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():
/* 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:
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:
$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:
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:
// 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)
-
Indentation: Tabs (not spaces). Set editor to use tabs.
-
Yoda Conditions: Place constants/variables on left side of comparison:
// Good if ( 'pending' === $source ) { ... } if ( true === $is_valid ) { ... } // Avoid if ( $source === 'pending' ) { ... } -
String Escaping: Always escape output to the browser:
// 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; -
Sanitization: Always sanitize user input:
// Good $query = sanitize_text_field( wp_unslash( $_GET['q'] ?? '' ) ); $content = wp_kses_post( wp_unslash( $_POST['content'] ?? '' ) ); // Avoid $query = $_GET['q']; -
Spacing:
- No spaces inside parentheses:
if ( $condition )notif( $condition ) - Space after keywords:
if,foreach,function,return - Space around operators:
$a = $b + $cnot$a=$b+$c
- No spaces inside parentheses:
-
Line length: Keep lines under 120 characters (configure in
.phpcs.xml.dist). -
Function documentation: Use PHPDoc for all functions/methods:
/** * 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 ) { // ... } -
No shorthand PHP tags: Use
<?phpnot<? -
No closing PHP tag: At end of PHP-only files, omit
?>to prevent accidental whitespace.
JavaScript Formatting
-
Quote style: Use single quotes for strings (consistent with WordPress JS standards).
-
Semicolons: Always end statements with
; -
Indentation: 4 spaces (or match project setting).
-
Immediately Invoked Function Expression (IIFE): Wrap in
(function ($) { ... })(jQuery)to avoid global scope pollution. -
Comment blocks:
/** * Render search results. */ function renderResults(data) { // ... implementation } -
jQuery document-ready: Use
$(document).ready()only for initialization, not for every event handler. -
Error handling: Always handle AJAX errors:
$.ajax({ success: function (response) { ... }, error: function (jqXHR, textStatus, errorThrown) { ... } });
CSS Formatting
-
Class naming: Lowercase, hyphens,
.psa-prefix (see CSS Naming Conventions section). -
Property order: Use logical grouping:
.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; } -
Media queries: Place at end of component or in separate section.
-
No IDs: Use classes only (except for single shortcode instances managed by JS).
-
Scoped to plugin: Never style global elements; always scope via
.psa-classes.
Composer & Dependencies
-
composer.json: Declared dependencies in
requireandrequire-dev."require": { "php": ">=7.4" }, "require-dev": { "phpunit/phpunit": "^9.0 || ^10.0", "squizlabs/php_codesniffer": "^3.7", "wp-coding-standards/wpcs": "^3.0" } -
PSR-4 Autoload: Classes in
includes/can be autoloaded viaPSA\namespace (not currently used, but available). -
PHPCS: Run
composer run lintbefore committing: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() |
Git Workflow — PR-Gated, Soft-Enforced
This repo is private on GitHub's free plan, which does not support branch protection or rulesets. The review gate is enforced at the agent layer, not server-side. Every agent and human contributor follows these rules; the .github/workflows/main-push-audit.yml tripwire opens an audit issue on any direct push to main that did not come from a merged PR.
Rules
-
Never push to
maindirectly. Every change lands via a pull request that the maintainer merges. No self-merging. No force-pushing tomain. -
Branch naming:
claude/<scope>-<YYYYMMDD>for agent-authored work,fix/<scope>orfeat/<scope>for human-authored. The scope is a 1–3 word kebab-case description. -
Commit trailer: every commit authored by an agent must end with an
Agent-Session:trailer so commits can be correlated back to the conversation that produced them, even though all commits share thepeptiderepobot identity.feat: add per-request cost tracking Agent-Session: cowork-2026-04-14-cost-audit -
PR description template: every PR description covers
- What changed (one paragraph)
- Why (motivation or incident link)
- Risk flags (schema changes, API contract changes, cost impact, compatibility)
- Test plan (what was run locally, what to smoke-test after merge)
-
Emergency push exception: if a situation genuinely requires pushing to
mainwithout a PR (site down, CI broken, one-line hotfix), surface it in the chat before doing it. Every emergency push gets a follow-up PR that commits the same changes through the normal flow so git stays the source of truth. The tripwire will open an audit issue — close it with a comment explaining why.
Opening a PR from an agent session
The gh CLI is not installed in the Cowork sandbox. Use curl with the PAT from the workspace .env.credentials:
GH_TOKEN=$(grep "^GITHUB_PAT=" "$WORKSPACE/.env.credentials" | cut -d= -f2)
git push -u origin HEAD
curl -s -X POST -H "Authorization: token $GH_TOKEN" \
-H "Content-Type: application/json" \
"https://api.github.com/repos/peptiderepo/<repo>/pulls" \
-d "$(jq -cn --arg title "<title>" --arg head "<branch>" --arg body "<body>" \
'{title:$title, head:$head, base:"main", body:$body}')"
When this changes
If the peptiderepo GitHub account is ever upgraded to Pro (or the repo goes public), replace this soft-enforcement section with a note pointing to the real branch protection rules in repo settings.