get_charset_collate(); $prefix = $wpdb->prefix; $sql = []; $sql[] = "CREATE TABLE {$prefix}aiforya_sm_schemas ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, post_id BIGINT UNSIGNED NOT NULL, post_type VARCHAR(20) NOT NULL, schema_type VARCHAR(100) NOT NULL, schema_json LONGTEXT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'active', source VARCHAR(20) NOT NULL DEFAULT 'auto_detected', richness_score TINYINT UNSIGNED DEFAULT 0, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), KEY post_id (post_id), KEY schema_type (schema_type), KEY status (status) ) {$charset};"; $sql[] = "CREATE TABLE {$prefix}aiforya_sm_detected_types ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, post_id BIGINT UNSIGNED NOT NULL, detected_type VARCHAR(100) NOT NULL, confidence_score FLOAT(3,2) DEFAULT 0, model_used VARCHAR(100) DEFAULT NULL, detected_at DATETIME NOT NULL, PRIMARY KEY (id), KEY post_id (post_id) ) {$charset};"; $sql[] = "CREATE TABLE {$prefix}aiforya_sm_validation_log ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, schema_id BIGINT UNSIGNED NOT NULL, post_id BIGINT UNSIGNED NOT NULL, validation_status VARCHAR(20) NOT NULL, validation_response TEXT, checked_at DATETIME NOT NULL, PRIMARY KEY (id), KEY schema_id (schema_id), KEY post_id (post_id) ) {$charset};"; $sql[] = "CREATE TABLE {$prefix}aiforya_sm_detection_logs ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, post_id BIGINT UNSIGNED NOT NULL, detected_type VARCHAR(100) NOT NULL, confidence FLOAT NULL, ai_provider VARCHAR(40) DEFAULT '', prompt_used LONGTEXT NULL, raw_response LONGTEXT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY post_id (post_id) ) {$charset};"; // Proactive schema prediction — AI recommendations based on the site's niche. $sql[] = "CREATE TABLE {$prefix}aiforya_sm_predictions ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, schema_type VARCHAR(100) NOT NULL, rationale TEXT, confidence TINYINT UNSIGNED DEFAULT 0, candidate_post_ids LONGTEXT, source VARCHAR(40) DEFAULT 'sonnet_niche', status VARCHAR(20) NOT NULL DEFAULT 'active', created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), KEY schema_type (schema_type), KEY status (status) ) {$charset};"; // F11 Schema A/B Testing — UNIQUE absolu marché 2026. $sql[] = "CREATE TABLE {$prefix}aiforya_sm_ab_tests ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, post_id BIGINT UNSIGNED NOT NULL, schema_type VARCHAR(100) NOT NULL, variant_a_strategy VARCHAR(40) NOT NULL DEFAULT 'minimal', variant_b_strategy VARCHAR(40) NOT NULL DEFAULT 'enriched', variant_a_json LONGTEXT NOT NULL, variant_b_json LONGTEXT NOT NULL, impressions_a INT UNSIGNED DEFAULT 0, impressions_b INT UNSIGNED DEFAULT 0, clicks_a INT UNSIGNED DEFAULT 0, clicks_b INT UNSIGNED DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'running', winner CHAR(1) DEFAULT NULL, started_at DATETIME NOT NULL, ended_at DATETIME DEFAULT NULL, PRIMARY KEY (id), KEY post_id (post_id), KEY status (status) ) {$charset};"; // F02 SERP Gap Analysis — cache des scans pour éviter de re-fetch SerpAPI à chaque ouverture. $sql[] = "CREATE TABLE {$prefix}aiforya_sm_serp_scans ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, post_id BIGINT UNSIGNED NOT NULL, keyword VARCHAR(255) NOT NULL, competitors LONGTEXT, gaps LONGTEXT, suggestions LONGTEXT, scanned_at DATETIME NOT NULL, PRIMARY KEY (id), KEY post_id (post_id), KEY scanned_at (scanned_at) ) {$charset};"; // F19 Health Score 360° — snapshots quotidiens du score global (pour la tendance 12 mois). $sql[] = "CREATE TABLE {$prefix}aiforya_sm_health_history ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, snapshot_date DATE NOT NULL, score TINYINT UNSIGNED DEFAULT 0, coverage TINYINT UNSIGNED DEFAULT 0, quality TINYINT UNSIGNED DEFAULT 0, diversity TINYINT UNSIGNED DEFAULT 0, validation TINYINT UNSIGNED DEFAULT 0, gsc_errors INT UNSIGNED DEFAULT 0, breakdown LONGTEXT, created_at DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY snapshot_date (snapshot_date) ) {$charset};"; // F09 Auto-Repair GSC — log des erreurs Search Console + fix appliqués. $sql[] = "CREATE TABLE {$prefix}aiforya_sm_gsc_errors ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, post_id BIGINT UNSIGNED NOT NULL, error_type VARCHAR(80) NOT NULL, error_message TEXT, severity VARCHAR(20) DEFAULT 'warning', fix_applied LONGTEXT, status VARCHAR(20) DEFAULT 'open', detected_at DATETIME NOT NULL, fixed_at DATETIME DEFAULT NULL, PRIMARY KEY (id), KEY post_id (post_id), KEY status (status), KEY error_type (error_type) ) {$charset};"; // F17 Auto-Migration depuis concurrents — log des imports réalisés. $sql[] = "CREATE TABLE {$prefix}aiforya_sm_migration_log ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, source_plugin VARCHAR(40) NOT NULL, posts_imported INT UNSIGNED DEFAULT 0, posts_failed INT UNSIGNED DEFAULT 0, status VARCHAR(20) DEFAULT 'pending', details LONGTEXT, started_at DATETIME NOT NULL, completed_at DATETIME DEFAULT NULL, PRIMARY KEY (id), KEY source_plugin (source_plugin) ) {$charset};"; // F10 Knowledge Graph — entités extraites + sameAs auto. $sql[] = "CREATE TABLE {$prefix}aiforya_sm_entities ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, type VARCHAR(40) NOT NULL, post_id BIGINT UNSIGNED DEFAULT 0, sameAs LONGTEXT, properties LONGTEXT, mentions_count INT UNSIGNED DEFAULT 1, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), KEY type (type), KEY post_id (post_id), KEY name (name(120)) ) {$charset};"; // Multi-schema per post : extension de la table principale (ajout colonne is_primary) // — gère les schémas multiples par post (ex: Recipe + VideoObject + Article). // dbDelta gère ALTER automatiquement si la colonne n'existe pas. foreach ( $sql as $statement ) { dbDelta( $statement ); } // Migration manuelle pour schemas table : ajout slot et is_primary pour multi-schema. $schemas_table = "{$prefix}aiforya_sm_schemas"; $cols = $wpdb->get_col( "SHOW COLUMNS FROM {$schemas_table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared if ( is_array( $cols ) ) { if ( ! in_array( 'slot', $cols, true ) ) { $wpdb->query( "ALTER TABLE {$schemas_table} ADD COLUMN slot VARCHAR(40) NOT NULL DEFAULT 'primary' AFTER schema_type" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared } if ( ! in_array( 'is_primary', $cols, true ) ) { $wpdb->query( "ALTER TABLE {$schemas_table} ADD COLUMN is_primary TINYINT(1) NOT NULL DEFAULT 1 AFTER slot" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared } } } private static function set_default_options(): void { /* * ⛔ CORRECTION DU 13/08/2026 — à l'activation, et dans TOUTES les variantes, * cette ligne écrivait deux réglages liés à l'IA : le nom d'un fournisseur, en * dur, et un tableau destiné à recevoir des jetons d'accès. * * Or le readme de la version .org affirme qu'aucun jeton ni service externe * n'est requis — et c'est exact : aucun chemin d'IA n'aboutit dans cette * édition, et aucun appel réseau n'existe (mesuré : 0 dans dist-org, 2 dans * dist-premium). Ces deux réglages contredisaient donc notre * propre déclaration, et un relecteur qui aurait cherché le nom du fournisseur * aurait eu raison de nous interroger. Les réglages d'IA n'existent que là où * l'IA existe. * * ⚠ CE COMMENTAIRE NE RECOPIE PAS LES VALEURS SUPPRIMÉES, ET C'EST VOLONTAIRE : * une annulation qui cite verbatim ce qu'elle annule laisse la chaîne * extractable par une simple recherche — le ⛔ est un caractère, pas une * négation. Première version de ce commentaire : elle les citait, et elles * ressortaient toutes les cinq du paquet reconstruit. * * MESURE qui autorise le retrait pur et simple plutôt qu'un garde-fou : AUCUN * code ne lit ces deux clés — ni dans dist-org, ni dans dist-premium. Les quatre * lecteurs de `aiforya_sm_settings` (ajax, db, frontend, onglet Réglages) lisent * l'option entière et n'en touchent aucune. Les retirer ne casse donc rien, et * un garde-fou nommant une classe inexistante aurait été un faux repère de plus. */ add_option( 'aiforya_sm_settings', ['auto_inject_enabled' => true, 'cache_ttl_minutes' => 1440] ); add_option( 'aiforya_sm_type_mappings', ['post' => 'Article', 'page' => 'WebPage', 'product' => 'Product'] ); } private static function schedule_cron_events(): void { if ( ! wp_next_scheduled( 'aiforya_sm_process_new_posts' ) ) { wp_schedule_event( time() + DAY_IN_SECONDS, 'every_5_minutes', 'aiforya_sm_process_new_posts' ); } if ( ! wp_next_scheduled( 'aiforya_sm_weekly_validation_refresh' ) ) { wp_schedule_event( time() + DAY_IN_SECONDS, 'weekly', 'aiforya_sm_weekly_validation_refresh' ); } // Les trois tâches gardées par un class_exists() ci-dessous appartiennent à des // fonctionnalités que la variante gratuite ne contient pas : leurs classes sont // retirées à la construction, mais ces programmations, elles, ne l'étaient pas. // Toute installation gratuite plantait donc des tâches qui se déclenchaient à vie // sans écouteur. On interroge la classe qui porte le hook : ce fichier reste juste // dans les deux arbres, et une fonctionnalité retirée demain emportera sa tâche. // (Les classes sont chargées par les require_once du fichier principal, qui // s'exécutent avant register_activation_hook — donc la réponse est fiable ici.) // Prédictions proactives hebdomadaires. if ( class_exists( 'Aiforya_Schema_Markup_Trends' ) && ! wp_next_scheduled( 'aiforya_sm_predict_schemas' ) ) { wp_schedule_event( time() + 2 * DAY_IN_SECONDS, 'weekly', 'aiforya_sm_predict_schemas' ); } // Désignation automatique du gagnant A/B après 14 jours. if ( class_exists( 'Aiforya_Schema_Markup_AB_Test' ) && ! wp_next_scheduled( 'aiforya_sm_ab_auto_pick_winner' ) ) { wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', 'aiforya_sm_ab_auto_pick_winner' ); } // F19 — snapshot Health Score quotidien (tendance 12 mois). if ( ! wp_next_scheduled( 'aiforya_sm_health_snapshot' ) ) { wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', 'aiforya_sm_health_snapshot' ); } /* * Auto-Repair GSC quotidien — CONDITIONNÉ le 13/08/2026. * Il était planifié SANS CONDITION, alors que la ligne juste en dessous * conditionne la récupération des erreurs à `Aiforya_Schema_Markup_GSC`. Or * cette classe n'existe dans AUCUNE variante (mesuré : 0 fichier la déclare, * ni dans dist-org ni dans dist-premium). Rien n'alimentait donc jamais la * table, et un cron quotidien tournait sur chaque site installé pour parcourir * zéro ligne. On aligne les deux gardes sur le même marqueur. */ if ( class_exists( 'Aiforya_Schema_Markup_GSC' ) && ! wp_next_scheduled( 'aiforya_sm_gsc_auto_repair' ) ) { wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', 'aiforya_sm_gsc_auto_repair' ); } // Récupération des erreurs réelles via OAuth Search Console (avant que la // réparation automatique ne tourne). if ( class_exists( 'Aiforya_Schema_Markup_GSC' ) && ! wp_next_scheduled( 'aiforya_sm_gsc_fetch_real_errors' ) ) { wp_schedule_event( time() + 6 * HOUR_IN_SECONDS, 'daily', 'aiforya_sm_gsc_fetch_real_errors' ); } } private static function clear_cron_events(): void { wp_clear_scheduled_hook( 'aiforya_sm_process_new_posts' ); wp_clear_scheduled_hook( 'aiforya_sm_weekly_validation_refresh' ); wp_clear_scheduled_hook( 'aiforya_sm_predict_schemas' ); wp_clear_scheduled_hook( 'aiforya_sm_ab_auto_pick_winner' ); wp_clear_scheduled_hook( 'aiforya_sm_health_snapshot' ); wp_clear_scheduled_hook( 'aiforya_sm_gsc_auto_repair' ); wp_clear_scheduled_hook( 'aiforya_sm_gsc_fetch_real_errors' ); } } endif;