From 4791e548217f803fbe0c5882e52b2a75381b18b9 Mon Sep 17 00:00:00 2001 From: peptiderepo Date: Mon, 20 Apr 2026 02:34:13 +0700 Subject: [PATCH] v0.6.2: deterministic peptide auto-linker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Peptide_Linker class scans generated HTML and wraps every mention of a known peptide in a hyperlink to its /peptides/ database page. Runs as post-processing in Publisher — no LLM involvement, 100% reliable. Handles case-insensitive matching, hyphen/space variants, skips existing tags. Gracefully no-ops if PR Core is not active. Removed prompt-based peptide linking from Content_Prompts (unreliable). Co-Authored-By: Claude Opus 4.6 --- ARCHITECTURE.md | 1 + CHANGELOG.md | 17 +++ includes/core/class-content-prompts.php | 45 +------ includes/core/class-peptide-linker.php | 149 ++++++++++++++++++++++++ includes/core/class-publisher.php | 9 +- prautoblogger.php | 4 +- 6 files changed, 179 insertions(+), 46 deletions(-) create mode 100644 includes/core/class-peptide-linker.php diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e843624..3e695be 100755 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -134,6 +134,7 @@ prautoblogger/ │ │ ├── class-semantic-dedup.php # Embedding-based cosine similarity dedup with keyword fallback │ │ ├── class-content-generator.php# Writer agent — manages the generation pipeline │ │ ├── class-content-prompts.php # Builds all LLM prompts for content generation (system, stage, linking rules) +│ │ ├── class-peptide-linker.php # Deterministic post-processor: hyperlinks peptide mentions to /peptides/ pages │ │ ├── class-chief-editor.php # Editor agent — LLM-powered editorial review │ │ ├── class-publisher.php # Creates WordPress posts from approved content │ │ ├── class-post-assembler.php # Post-creation helpers: taxonomy, log linking, images, sanitization diff --git a/CHANGELOG.md b/CHANGELOG.md index a10d14f..e94ca96 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to PRAutoBlogger will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project uses [Semantic Versioning](https://semver.org/). +## [0.6.2] — 2026-04-20 + +### Added +- **Deterministic peptide auto-linker.** New `Peptide_Linker` class scans + generated HTML and wraps every mention of a known peptide in a hyperlink + to its `/peptides/{slug}/` database page. Runs as a post-processing step + in Publisher before `wp_insert_post` — no LLM involvement, 100% reliable. + Handles case-insensitive matching, hyphen/space variants (e.g., "BPC-157" + and "BPC 157"), and skips text already inside `` tags. Gracefully + no-ops if PR Core is not active. + +### Changed +- **Removed prompt-based peptide linking.** The system prompt no longer asks + the LLM to link peptides (unreliable). Peptide links are now injected + deterministically by `Peptide_Linker` after content generation. The prompt + still provides article links and the "never fabricate URLs" rule. + ## [0.6.1] — 2026-04-20 ### Added diff --git a/includes/core/class-content-prompts.php b/includes/core/class-content-prompts.php index 096699d..ba7a04a 100644 --- a/includes/core/class-content-prompts.php +++ b/includes/core/class-content-prompts.php @@ -167,13 +167,10 @@ class PRAutoBlogger_Content_Prompts { $rules = "--- LINKING RULES ---\n"; $rules .= "NEVER fabricate or invent URLs. You do NOT have access to external sources.\n"; $rules .= "Only use the internal links listed below when linking within the article.\n"; - $rules .= "If no listed page is relevant to a section, do not insert a link.\n"; - $rules .= "IMPORTANT: Whenever ANY peptide name from the peptide database list below\n"; - $rules .= "is mentioned in the article, ALWAYS link the FIRST mention to its database page.\n\n"; + $rules .= "If no listed article is relevant to a section, do not insert a link.\n"; + $rules .= "Do NOT add links for peptide names — those are injected automatically after generation.\n\n"; $rules .= self::build_article_links(); - $rules .= "\n"; - $rules .= self::build_peptide_links(); $rules .= "--- END LINKING RULES ---"; @@ -211,42 +208,4 @@ class PRAutoBlogger_Content_Prompts { return implode( "\n", $lines ) . "\n"; } - /** - * Build a reference list of all published peptide database pages. - * - * Every peptide mentioned in an article must link to its database page - * on first mention. This gives the model the real URLs to use. - * - * @return string Formatted peptide link list, or empty if CPT unavailable. - */ - private static function build_peptide_links(): string { - // PR Core may not be active — guard against missing post type. - if ( ! post_type_exists( 'pr_peptide' ) ) { - return ''; - } - - $peptides = get_posts( [ - 'post_type' => 'pr_peptide', - 'post_status' => 'publish', - 'posts_per_page' => 200, - 'orderby' => 'title', - 'order' => 'ASC', - 'fields' => 'ids', - ] ); - - if ( empty( $peptides ) ) { - return ''; - } - - $lines = [ "Peptide database pages — ALWAYS link the first mention of each peptide:\n" ]; - foreach ( $peptides as $peptide_id ) { - $title = get_the_title( $peptide_id ); - $url = get_permalink( $peptide_id ); - if ( $title && $url ) { - $lines[] = "- {$title}: {$url}"; - } - } - - return implode( "\n", $lines ) . "\n"; - } } diff --git a/includes/core/class-peptide-linker.php b/includes/core/class-peptide-linker.php new file mode 100644 index 0000000..c45a4ab --- /dev/null +++ b/includes/core/class-peptide-linker.php @@ -0,0 +1,149 @@ + tag pointing to its /peptides/ page. + * Skips text that is already inside an existing tag to avoid nested links. + * + * Triggered by: PRAutoBlogger_Publisher::create_post() (before wp_insert_post). + * Dependencies: PR Core plugin (pr_peptide CPT). Gracefully no-ops if PR Core is inactive. + * + * @see core/class-publisher.php — Calls inject_links() on post_content. + * @see peptide-repo-core/includes/cpt/class-pr-core-peptide-cpt.php — Peptide CPT. + */ +class PRAutoBlogger_Peptide_Linker { + + /** + * Inject peptide hyperlinks into HTML content. + * + * Every occurrence of a known peptide name is wrapped in a link to its + * database page, except when the text is already inside an tag. + * + * @param string $html Article HTML content. + * @return string HTML with peptide names hyperlinked. + */ + public static function inject_links( string $html ): string { + if ( ! post_type_exists( 'pr_peptide' ) ) { + return $html; + } + + $link_map = self::build_link_map(); + if ( empty( $link_map ) ) { + return $html; + } + + // Build a single regex alternation of all peptide names, longest first + // so "BPC-157" matches before a hypothetical "BPC" would. + $names = array_keys( $link_map ); + usort( $names, static function ( string $a, string $b ): int { + return strlen( $b ) - strlen( $a ); + } ); + + $escaped = array_map( static function ( string $name ): string { + return preg_quote( $name, '/' ); + }, $names ); + + // Word-boundary match, case-insensitive. + $pattern = '/\b(' . implode( '|', $escaped ) . ')\b/iu'; + + // Split HTML into segments: inside … tags vs. outside. + // Only replace in segments that are NOT inside an anchor tag. + return self::replace_outside_links( $html, $pattern, $link_map ); + } + + /** + * Build a map of peptide display names → permalink URLs. + * + * Includes the canonical title and common aliases (e.g., "BPC-157" and + * "BPC 157") so both hyphenated and space-separated forms get linked. + * + * @return array Name → URL pairs, case-preserved keys. + */ + private static function build_link_map(): array { + $peptides = get_posts( [ + 'post_type' => 'pr_peptide', + 'post_status' => 'publish', + 'posts_per_page' => 200, + 'orderby' => 'title', + 'order' => 'ASC', + 'fields' => 'ids', + ] ); + + $map = []; + foreach ( $peptides as $peptide_id ) { + $title = get_the_title( $peptide_id ); + $url = get_permalink( $peptide_id ); + if ( ! $title || ! $url ) { + continue; + } + + // Canonical name (e.g., "BPC-157"). + $map[ $title ] = $url; + + // If the name contains hyphens, also match the space-separated + // variant (e.g., "BPC 157" → same URL as "BPC-157"). + if ( strpos( $title, '-' ) !== false ) { + $space_variant = str_replace( '-', ' ', $title ); + $map[ $space_variant ] = $url; + } + } + + return $map; + } + + /** + * Replace peptide names with links, but only outside existing tags. + * + * Splits HTML into chunks by … boundaries. Replacement only + * runs on chunks that are outside anchors, preventing nested tags. + * + * @param string $html Full HTML content. + * @param string $pattern Regex pattern matching peptide names. + * @param array $link_map Name → URL map (case-preserved keys). + * @return string HTML with replacements applied. + */ + private static function replace_outside_links( string $html, string $pattern, array $link_map ): string { + // Split on anchor tags, keeping the delimiters. + // This regex captures full … blocks as array elements. + $parts = preg_split( '/(]*>.*?<\/a>)/is', $html, -1, PREG_SPLIT_DELIM_CAPTURE ); + + if ( false === $parts ) { + return $html; + } + + // Build a lowercase → URL lookup for case-insensitive matching. + $lower_map = []; + foreach ( $link_map as $name => $url ) { + $lower_map[ mb_strtolower( $name ) ] = $url; + } + + $result = ''; + foreach ( $parts as $part ) { + // If this chunk is an anchor tag, pass it through untouched. + if ( preg_match( '/^' . esc_html( $matched_text ) . ''; + }, + $part + ); + } + + return $result; + } +} diff --git a/includes/core/class-publisher.php b/includes/core/class-publisher.php index 772c2b6..879f812 100755 --- a/includes/core/class-publisher.php +++ b/includes/core/class-publisher.php @@ -12,6 +12,7 @@ declare(strict_types=1); * WordPress wp_insert_post(), post meta API. * * @see core/class-post-assembler.php — Post-creation helpers (taxonomy, images, logs). + * @see core/class-peptide-linker.php — Injects peptide database hyperlinks into content. * @see core/class-chief-editor.php — Produces the editorial review we consume. * @see core/class-content-generator.php — Produces the content we publish. * @see ARCHITECTURE.md — Data flow step 6. @@ -74,9 +75,15 @@ class PRAutoBlogger_Publisher { string $post_status, ?string $run_id = null ): int { + // Clean LLM artifacts, then inject peptide hyperlinks deterministically + // before the content enters WordPress. Peptide linker no-ops gracefully + // if PR Core is not active. + $clean_content = PRAutoBlogger_Post_Assembler::sanitize_llm_content( $content ); + $linked_content = PRAutoBlogger_Peptide_Linker::inject_links( $clean_content ); + $post_data = [ 'post_title' => sanitize_text_field( $idea->get_suggested_title() ), - 'post_content' => wp_kses_post( PRAutoBlogger_Post_Assembler::sanitize_llm_content( $content ) ), + 'post_content' => wp_kses_post( $linked_content ), 'post_status' => $post_status, 'post_type' => 'post', 'post_author' => PRAutoBlogger_Post_Assembler::get_default_author_id(), diff --git a/prautoblogger.php b/prautoblogger.php index d66adee..542020d 100644 --- a/prautoblogger.php +++ b/prautoblogger.php @@ -8,7 +8,7 @@ declare(strict_types=1); * Plugin Name: PRAutoBlogger * Plugin URI: https://peptiderepo.com/prautoblogger * Description: Monitors social media for trending topics, generates SEO-friendly blog posts using AI, and publishes them on a daily schedule with full cost tracking and self-improvement metrics. - * Version: 0.6.1 + * Version: 0.6.2 * Requires at least: 6.0 * Requires PHP: 7.4 * Author: PeptideRepo @@ -35,7 +35,7 @@ if ( ! defined( 'ABSPATH' ) ) { | and limits without magic strings. */ -define( 'PRAUTOBLOGGER_VERSION', '0.6.1' ); +define( 'PRAUTOBLOGGER_VERSION', '0.6.2' ); define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' ); define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ ); define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );