diff --git a/openspec/AGENTS.md b/_openspec/AGENTS.md
similarity index 100%
rename from openspec/AGENTS.md
rename to _openspec/AGENTS.md
diff --git a/_openspec/WORKFLOW-ROI-THEME.md b/_openspec/WORKFLOW-ROI-THEME.md
new file mode 100644
index 00000000..d72ad001
--- /dev/null
+++ b/_openspec/WORKFLOW-ROI-THEME.md
@@ -0,0 +1,437 @@
+# WORKFLOW DE DESARROLLO ROI THEME
+
+**ESTE ARCHIVO ES OBLIGATORIO LEER ANTES DE CUALQUIER DESARROLLO**
+
+---
+
+## REGLA DE ORO
+
+```
+┌─────────────────────────────────────────────────────────────────────────────┐
+│ │
+│ SI NO EXISTE spec.md APROBADO → NO SE TOCA CÓDIGO │
+│ │
+│ NUNCA desarrollar directamente. SIEMPRE seguir el flujo de fases. │
+│ │
+└─────────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## RUTAS DEL PROYECTO
+
+| Carpeta | Contenido | Ruta |
+|---------|-----------|------|
+| **Specs Base** | Arquitectura, estándares, nomenclatura | `_openspec/specs/` |
+| **Specs de Cambios** | Especificaciones por funcionalidad | `_openspec/changes/` |
+| **Schemas JSON** | Definición de componentes | `Schemas/` |
+| **Renderers** | Componentes frontend | `Public/[Componente]/` |
+| **FormBuilders** | Componentes admin | `Admin/[Componente]/` |
+| **Código Compartido** | Servicios, contratos, entidades | `Shared/` |
+
+---
+
+## CHECKLIST OBLIGATORIO (antes de cualquier desarrollo)
+
+```
+[ ] 1. Leer este archivo (WORKFLOW-ROI-THEME.md)
+[ ] 2. Leer _openspec/specs/arquitectura-limpia.md
+[ ] 3. Leer _openspec/specs/estandares-codigo.md
+[ ] 4. Leer _openspec/specs/nomenclatura.md
+[ ] 5. Identificar qué se va a desarrollar (componente, servicio, fix)
+[ ] 6. Determinar el flujo apropiado:
+ - Componente UI → Flujo de 5 Fases
+ - Servicio/Feature → Flujo de 3 Fases
+ - Bug fix simple → Flujo directo con spec mínima
+```
+
+---
+
+## FLUJO DE 5 FASES (COMPONENTES UI)
+
+> **Usar para:** Nuevos componentes visuales (Renderers + FormBuilders)
+
+```
+╔═══════════════════════════════════════════════════════════════════════════════╗
+║ FASE 1: SCHEMA JSON ║
+║ "Definir la estructura de datos" ║
+╠═══════════════════════════════════════════════════════════════════════════════╣
+║ ║
+║ Entrada: Template HTML de referencia (_planificacion/roi-theme-template/) ║
+║ Salida: Schemas/[nombre-en-kebab-case].json ║
+║ ║
+║ Contenido obligatorio: ║
+║ • component_name: kebab-case (ej: "featured-image") ║
+║ • version: "1.0.0" ║
+║ • description: Qué hace el componente ║
+║ • groups.visibility con 3 campos: ║
+║ - is_enabled (boolean, required) ║
+║ - show_on_desktop (boolean) ║
+║ - show_on_mobile (boolean) ║
+║ • Grupos adicionales con priority 10-90 ║
+║ ║
+║ Agente: roi-schema-architect ║
+║ ║
+╚═══════════════════════════════════════════════════════════════════════════════╝
+
+╔═══════════════════════════════════════════════════════════════════════════════╗
+║ FASE 2: SINCRONIZACIÓN BD ║
+║ "Registrar configuración en base de datos" ║
+╠═══════════════════════════════════════════════════════════════════════════════╣
+║ ║
+║ Entrada: Schema JSON creado en Fase 1 ║
+║ Salida: Registros en wp_roi_theme_component_settings ║
+║ ║
+║ Comando: ║
+║ powershell -Command "php 'C:\xampp\php_8.0.30_backup\wp-cli.phar' \ ║
+║ roi-theme sync-component [nombre-en-kebab-case]" ║
+║ ║
+║ Verificar: ║
+║ • Todos los campos del JSON existen en BD ║
+║ • Valores default aplicados correctamente ║
+║ • No hay campos huérfanos ║
+║ ║
+╚═══════════════════════════════════════════════════════════════════════════════╝
+
+╔═══════════════════════════════════════════════════════════════════════════════╗
+║ FASE 3: RENDERER (Frontend) ║
+║ "Generar HTML y CSS desde BD" ║
+╠═══════════════════════════════════════════════════════════════════════════════╣
+║ ║
+║ Entrada: Schema + Datos en BD ║
+║ Salida: Public/[PascalCase]/Infrastructure/Ui/[PascalCase]Renderer.php ║
+║ ║
+║ Requisitos obligatorios: ║
+║ • declare(strict_types=1) ║
+║ • Namespace: ROITheme\Public\[Component]\Infrastructure\Ui ║
+║ • final class [Component]Renderer implements RendererInterface ║
+║ • DI via constructor: CSSGeneratorInterface ║
+║ • COMPONENT_NAME constante en kebab-case ║
+║ • supports() retorna kebab-case ║
+║ • Validar: isEnabled(), getVisibilityClass() ║
+║ • CERO CSS hardcodeado (usar $this->cssGenerator) ║
+║ ║
+║ Agente: roi-renderer-builder ║
+║ ║
+╚═══════════════════════════════════════════════════════════════════════════════╝
+
+╔═══════════════════════════════════════════════════════════════════════════════╗
+║ FASE 4: FORMBUILDER (Admin) ║
+║ "Panel de configuración en admin" ║
+╠═══════════════════════════════════════════════════════════════════════════════╣
+║ ║
+║ Entrada: Schema + Renderer funcionando ║
+║ Salida: Admin/[PascalCase]/Infrastructure/Ui/[PascalCase]FormBuilder.php ║
+║ ║
+║ Requisitos obligatorios: ║
+║ • declare(strict_types=1) ║
+║ • Namespace: ROITheme\Admin\[Component]\Infrastructure\Ui ║
+║ • final class [Component]FormBuilder ║
+║ • DI via constructor: AdminDashboardRenderer ║
+║ • Design System: gradiente #0E2337 → #1e3a5f, borde #FF8600 ║
+║ • Bootstrap 5 form controls ║
+║ • data-component en kebab-case ║
+║ • Registrar en getComponents() con ID kebab-case ║
+║ ║
+║ Agente: roi-form-builder ║
+║ ║
+╚═══════════════════════════════════════════════════════════════════════════════╝
+
+╔═══════════════════════════════════════════════════════════════════════════════╗
+║ FASE 5: VALIDACIÓN ║
+║ "Verificar que todo cumple las specs" ║
+╠═══════════════════════════════════════════════════════════════════════════════╣
+║ ║
+║ Entrada: Componente completo (Schema + BD + Renderer + FormBuilder) ║
+║ Salida: Reporte de validación ║
+║ ║
+║ Comando: ║
+║ php Shared/Infrastructure/Scripts/validate-architecture.php [nombre] ║
+║ ║
+║ Verifica: ║
+║ • Estructura de carpetas correcta ║
+║ • JSON válido con campos obligatorios ║
+║ • Datos en BD sincronizados ║
+║ • Renderer implementa RendererInterface ║
+║ • FormBuilder registrado en getComponents() ║
+║ • Nomenclatura correcta (PascalCase/kebab-case) ║
+║ • Clean Architecture respetada (DI, capas) ║
+║ ║
+╚═══════════════════════════════════════════════════════════════════════════════╝
+```
+
+---
+
+## FLUJO DE 3 FASES (SERVICIOS/FEATURES)
+
+> **Usar para:** Nuevos servicios, features no visuales, mejoras de infraestructura
+
+```
+╔═══════════════════════════════════════════════════════════════════════════════╗
+║ FASE 1: ESPECIFICACIÓN ║
+║ "Definir qué se va a construir" ║
+╠═══════════════════════════════════════════════════════════════════════════════╣
+║ ║
+║ Crear carpeta: _openspec/changes/[nombre-feature]/ ║
+║ ║
+║ Archivos a crear: ║
+║ • proposal.md - Por qué y qué se va a cambiar ║
+║ • tasks.md - Checklist de implementación ║
+║ • spec.md - Especificación detallada (formato Gherkin) ║
+║ ║
+║ >>> REQUIERE APROBACIÓN DEL USUARIO PARA CONTINUAR <<< ║
+║ ║
+╚═══════════════════════════════════════════════════════════════════════════════╝
+
+╔═══════════════════════════════════════════════════════════════════════════════╗
+║ FASE 2: IMPLEMENTACIÓN ║
+║ "Construir siguiendo la spec" ║
+╠═══════════════════════════════════════════════════════════════════════════════╣
+║ ║
+║ Ubicación según tipo: ║
+║ • Servicios compartidos → Shared/Infrastructure/Services/ ║
+║ • Contratos → Shared/Domain/Contracts/ ║
+║ • UseCases → [Context]/Application/UseCases/ ║
+║ • Repositorios → [Context]/Infrastructure/Persistence/WordPress/ ║
+║ ║
+║ Requisitos: ║
+║ • declare(strict_types=1) ║
+║ • Namespace correcto según ubicación ║
+║ • DI via constructor (interfaces, no clases concretas) ║
+║ • Métodos pequeños (<30 líneas) ║
+║ • Clases pequeñas (<300 líneas) ║
+║ ║
+╚═══════════════════════════════════════════════════════════════════════════════╝
+
+╔═══════════════════════════════════════════════════════════════════════════════╗
+║ FASE 3: INTEGRACIÓN Y PRUEBA ║
+║ "Conectar y verificar" ║
+╠═══════════════════════════════════════════════════════════════════════════════╣
+║ ║
+║ • Integrar servicio donde se necesite ║
+║ • Probar funcionalidad en navegador ║
+║ • Verificar que no hay errores PHP ║
+║ • Actualizar tasks.md con estado COMPLETADO ║
+║ ║
+╚═══════════════════════════════════════════════════════════════════════════════╝
+```
+
+---
+
+## CÓMO CREAR NUEVA FEATURE
+
+### Paso 1: Crear estructura de carpetas
+
+```
+_openspec/changes/[nombre-feature]/
+├── proposal.md ← Por qué y qué se va a cambiar
+├── tasks.md ← Checklist → evoluciona a tracking
+└── spec.md ← Especificación detallada
+```
+
+### Paso 2: Crear proposal.md
+
+```markdown
+# Proposal: [Nombre Feature]
+
+## Why
+[1-2 oraciones sobre el problema a resolver]
+
+## What Changes
+- [Lista de cambios propuestos]
+
+## Impact
+- Archivos nuevos: [lista]
+- Archivos modificados: [lista]
+- Specs relacionadas: [lista]
+```
+
+### Paso 3: Crear tasks.md
+
+```markdown
+# Tracking: [nombre-feature]
+
+**Estado actual:** EN PROGRESO
+**Próximo paso:** [describir]
+
+---
+
+## Checklist
+
+### Fase 1: Especificación
+- [ ] Crear proposal.md
+- [ ] Crear spec.md con Requirements y Scenarios
+- [ ] Aprobar spec.md
+
+### Fase 2: Implementación
+- [ ] Crear interface (si aplica)
+- [ ] Crear implementación
+- [ ] Registrar en DI container (si aplica)
+
+### Fase 3: Integración
+- [ ] Integrar donde se necesite
+- [ ] Probar funcionalidad
+- [ ] Verificar sin errores
+
+---
+
+## Historial
+| Fecha | Avance |
+|-------|--------|
+| YYYY-MM-DD | [descripción] |
+```
+
+---
+
+## NOMENCLATURA RÁPIDA
+
+| Contexto | Formato | Ejemplo |
+|----------|---------|---------|
+| component_name (JSON/BD) | kebab-case | `"featured-image"` |
+| Archivo schema | kebab-case | `featured-image.json` |
+| Carpeta módulo | PascalCase | `FeaturedImage/` |
+| Namespace PHP | PascalCase | `ROITheme\Public\FeaturedImage\...` |
+| Clase Renderer | PascalCase | `FeaturedImageRenderer` |
+| Clase FormBuilder | PascalCase | `FeaturedImageFormBuilder` |
+| Constante | UPPER_SNAKE | `COMPONENT_NAME` |
+| Método | camelCase | `getVisibilityClass()` |
+| Variable | $camelCase | `$showDesktop` |
+
+**Conversión kebab ↔ Pascal:** `featured-image` ↔ `FeaturedImage`
+
+---
+
+## CLEAN ARCHITECTURE RESUMIDA
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ │
+│ DOMAIN │
+│ (Centro, sin deps) │
+│ │
+│ • Entities (Component, etc.) │
+│ • Contracts/Interfaces │
+│ • Value Objects │
+│ │
+│ PROHIBIDO: WordPress, echo, print, HTML │
+│ │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ APPLICATION │
+│ (Usa Domain) │
+│ │
+│ • UseCases │
+│ • Application Services │
+│ │
+│ PROHIBIDO: WordPress, acceso BD directo │
+│ │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ INFRASTRUCTURE │
+│ (Usa Domain y Application) │
+│ │
+│ • Ui/ (Renderers, FormBuilders) │
+│ • Api/ (AJAX handlers, REST) │
+│ • Persistence/ (Repositories WordPress) │
+│ • Services/ (Implementaciones) │
+│ │
+│ PERMITIDO: WordPress, HTML, BD, APIs externas │
+│ │
+└─────────────────────────────────────────────────────────────────┘
+
+Dirección de dependencias: Infrastructure → Application → Domain
+ (afuera depende de adentro, NUNCA al revés)
+```
+
+---
+
+## COMANDOS WP-CLI
+
+```powershell
+# Ubicación WP-CLI
+C:\xampp\php_8.0.30_backup\wp-cli.phar
+
+# Sincronizar un componente
+powershell -Command "php 'C:\xampp\php_8.0.30_backup\wp-cli.phar' roi-theme sync-component [nombre]"
+
+# Sincronizar todos los componentes
+powershell -Command "php 'C:\xampp\php_8.0.30_backup\wp-cli.phar' roi-theme sync-all-components"
+
+# Ejemplo
+powershell -Command "php 'C:\xampp\php_8.0.30_backup\wp-cli.phar' roi-theme sync-component featured-image"
+```
+
+---
+
+## AGENTES DISPONIBLES
+
+| Agente | Propósito | Cuándo usar |
+|--------|-----------|-------------|
+| roi-schema-architect | Genera JSON schemas desde HTML | Fase 1 de componentes |
+| roi-renderer-builder | Genera Renderers PHP | Fase 3 de componentes |
+| roi-form-builder | Genera FormBuilders PHP | Fase 4 de componentes |
+
+---
+
+## ESPECIFICACIONES BASE (LECTURA OBLIGATORIA)
+
+| Spec | Ruta | Contenido |
+|------|------|-----------|
+| Arquitectura | `_openspec/specs/arquitectura-limpia.md` | Capas, dependencias, estructura |
+| Estándares | `_openspec/specs/estandares-codigo.md` | SOLID, límites, WordPress |
+| Nomenclatura | `_openspec/specs/nomenclatura.md` | Nombres, formatos, convenciones |
+
+---
+
+## ROLES
+
+```
+┌─────────────────────────────────────────────────────────────────────────────┐
+│ │
+│ CLAUDE CODE (Yo) USUARIO (Tú) │
+│ ───────────────── ──────────── │
+│ │
+│ • Ejecuto los agentes • Defines qué componente crear │
+│ • Genero schemas JSON • Apruebas especificaciones │
+│ • Genero Renderers • Apruebas diseño │
+│ • Genero FormBuilders • Pruebas en navegador │
+│ • Ejecuto validaciones • Verificas funcionamiento │
+│ • Documento en specs • Decides prioridades │
+│ │
+└─────────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## GARANTÍAS DEL PROCESO
+
+1. **NUNCA** se escribe código sin spec aprobada
+2. **SIEMPRE** se sigue el flujo de fases
+3. **CADA** componente tiene las 5 partes (Schema, BD, Renderer, FormBuilder, Validación)
+4. **TODO** el CSS se genera vía CSSGenerator (cero hardcodeado)
+5. **SIEMPRE** DI via constructor (interfaces, no clases concretas)
+6. **NUNCA** WordPress en Domain ni Application
+
+---
+
+## LECCIONES APRENDIDAS
+
+> Esta sección documenta ajustes al workflow basados en implementaciones completadas.
+
+### Validación código vs specs (2026-01-08)
+
+**Contexto:** Antes de mejorar specs, se validó que el código actual cumple con las especificaciones propuestas.
+
+**Hallazgos:**
+- 17 Renderers siguen el patrón correctamente
+- 17 FormBuilders siguen el patrón correctamente
+- 17 Schemas en kebab-case
+- 43 archivos con strict_types=1
+- 39 clases final
+- 23 interfaces en Domain/Contracts
+
+**Conclusión:** El código YA implementa Clean Architecture. Las mejoras a specs son documentación de patrones existentes, no nuevas invenciones.
+
+---
+
+**Última actualización:** 2026-01-08
diff --git a/openspec/changes/add-advanced-incontent-ads/design.md b/_openspec/changes/add-advanced-incontent-ads/design.md
similarity index 100%
rename from openspec/changes/add-advanced-incontent-ads/design.md
rename to _openspec/changes/add-advanced-incontent-ads/design.md
diff --git a/openspec/changes/add-advanced-incontent-ads/proposal.md b/_openspec/changes/add-advanced-incontent-ads/proposal.md
similarity index 100%
rename from openspec/changes/add-advanced-incontent-ads/proposal.md
rename to _openspec/changes/add-advanced-incontent-ads/proposal.md
diff --git a/openspec/changes/add-advanced-incontent-ads/specs/adsense-placement/spec.md b/_openspec/changes/add-advanced-incontent-ads/specs/adsense-placement/spec.md
similarity index 100%
rename from openspec/changes/add-advanced-incontent-ads/specs/adsense-placement/spec.md
rename to _openspec/changes/add-advanced-incontent-ads/specs/adsense-placement/spec.md
diff --git a/openspec/changes/add-advanced-incontent-ads/tasks.md b/_openspec/changes/add-advanced-incontent-ads/tasks.md
similarity index 100%
rename from openspec/changes/add-advanced-incontent-ads/tasks.md
rename to _openspec/changes/add-advanced-incontent-ads/tasks.md
diff --git a/_openspec/changes/adsense-auto-ads-toggle/spec.md b/_openspec/changes/adsense-auto-ads-toggle/spec.md
new file mode 100644
index 00000000..af3f0b1b
--- /dev/null
+++ b/_openspec/changes/adsense-auto-ads-toggle/spec.md
@@ -0,0 +1,365 @@
+# Especificacion: AdSense Auto Ads Toggle
+
+## Purpose
+
+Define el comportamiento de un interruptor "Auto Ads" que cuando se activa, delega el control de ubicacion de anuncios a Google AdSense Auto Ads, desactivando automaticamente todas las ubicaciones manuales del tema EXCEPTO los anuncios en resultados de busqueda del plugin ROI APU Search.
+
+**Problema que resuelve:** El usuario necesita probar Google Auto Ads sin tener que deshabilitar manualmente cada ubicacion de anuncio. Ademas, Auto Ads tiende a romper el layout de tablas HTML, por lo que se requiere excluirlas automaticamente.
+
+**Beneficio esperado:** Comparar facilmente el rendimiento (RPM, CTR, revenue) entre ubicaciones manuales vs Auto Ads de Google, manteniendo siempre activos los anuncios en el buscador APU.
+
+---
+
+## Requirements
+
+### Requirement: Campo enable_auto_ads en Schema
+
+El schema adsense-placement.json DEBE incluir un nuevo campo para activar Auto Ads.
+
+#### Scenario: Definicion del campo en JSON Schema
+- **WHEN** se define el campo `enable_auto_ads`
+- **THEN** DEBE ubicarse en el grupo `visibility` con priority 10
+- **AND** DEBE tener type `boolean`
+- **AND** DEBE tener default `false`
+- **AND** DEBE tener label `Activar Google Auto Ads`
+- **AND** DEBE tener description `Cuando esta activo, Google controla automaticamente donde mostrar anuncios. Se desactivan las ubicaciones manuales excepto los anuncios en busqueda.`
+
+#### Scenario: Posicion del campo en el formulario
+- **WHEN** se renderiza el formulario de configuracion
+- **THEN** el campo `enable_auto_ads` DEBE aparecer inmediatamente despues de `is_enabled`
+- **AND** DEBE tener un indicador visual destacado (badge o icono) indicando que es modo automatico
+
+---
+
+### Requirement: Desactivacion Automatica de Ubicaciones Manuales
+
+Cuando Auto Ads esta activo, el sistema DEBE ignorar todas las configuraciones de ubicacion manual.
+
+#### Scenario: Grupos afectados por enable_auto_ads
+- **GIVEN** `enable_auto_ads === true`
+- **WHEN** se evaluan las ubicaciones de anuncios
+- **THEN** los siguientes grupos DEBEN comportarse como si estuvieran desactivados:
+ - `incontent_advanced` (In-Content Ads Avanzado)
+ - `behavior` (Ubicaciones en Posts) - EXCEPTO `javascript_first_mode`
+ - `anchor_ads` (Anuncios Fijos)
+ - `vignette_ads` (Anuncios de Vineta)
+ - `layout` (Ubicaciones Archivos/Globales)
+- **AND** el grupo `search_results` DEBE permanecer activo
+- **AND** el grupo `analytics` DEBE permanecer activo
+- **AND** el grupo `content` (credenciales) DEBE permanecer activo
+
+#### Scenario: Preservacion de configuracion en base de datos
+- **GIVEN** el usuario activa `enable_auto_ads`
+- **WHEN** se guardan los settings
+- **THEN** los valores de los campos desactivados NO DEBEN modificarse en la BD
+- **AND** cuando el usuario desactive `enable_auto_ads`, sus configuraciones previas DEBEN estar intactas
+
+#### Scenario: Evaluacion en PHP (Server-Side)
+- **GIVEN** `enable_auto_ads === true` en los settings
+- **WHEN** un Renderer evalua si debe insertar un slot de anuncio
+- **THEN** DEBE verificar primero si `enable_auto_ads` esta activo
+- **AND** si esta activo, DEBE retornar inmediatamente sin insertar el slot manual
+- **AND** EXCEPTO para slots del grupo `search_results`
+
+#### Scenario: Evaluacion en JavaScript (Client-Side)
+- **GIVEN** JavaScript-First Mode esta activo
+- **AND** `enable_auto_ads === true`
+- **WHEN** el controller JavaScript evalua visibilidad
+- **THEN** DEBE incluir `enable_auto_ads` en el response del endpoint REST
+- **AND** el script DEBE ocultar slots manuales marcados con `data-ad-manual`
+- **AND** NO DEBE ocultar slots marcados con `data-ad-search`
+
+---
+
+### Requirement: Excepcion para ROI APU Search
+
+Los anuncios en resultados de busqueda DEBEN permanecer activos independientemente del estado de Auto Ads.
+
+#### Scenario: Configuracion de busqueda siempre disponible
+- **GIVEN** `enable_auto_ads === true`
+- **WHEN** se obtiene la configuracion para el plugin roi-apu-search via `roi_get_adsense_search_config()`
+- **THEN** DEBE retornar la configuracion del grupo `search_results`
+- **AND** `search_ads_enabled` DEBE evaluarse independientemente de `enable_auto_ads`
+
+#### Scenario: Slots de busqueda con atributo especial
+- **WHEN** se renderizan slots de anuncios para resultados de busqueda
+- **THEN** DEBEN incluir atributo `data-ad-search="true"`
+- **AND** DEBEN incluir clase CSS `roi-ad-search-slot`
+- **AND** NO DEBEN incluir atributo `data-ad-manual`
+
+#### Scenario: JavaScript no oculta slots de busqueda
+- **GIVEN** el controller JavaScript detecta `enable_auto_ads === true`
+- **WHEN** procesa los slots de anuncios
+- **THEN** NO DEBE agregar clase `roi-ad-hidden` a elementos con `data-ad-search`
+- **AND** DEBE disparar evento `roi-adsense-activate` para slots de busqueda
+
+---
+
+### Requirement: CSS para Excluir Tablas de Auto Ads
+
+El sistema DEBE inyectar CSS que indica a Google Auto Ads que NO inserte anuncios dentro de tablas.
+
+#### Scenario: Inyeccion de CSS google-auto-ads ignore
+- **GIVEN** `enable_auto_ads === true`
+- **WHEN** se renderiza el header del sitio
+- **THEN** DEBE inyectar el siguiente CSS:
+```css
+table,
+.wp-block-table,
+.tablepress,
+figure.wp-block-table,
+.entry-content table {
+ google-auto-ads: ignore;
+}
+```
+- **AND** el CSS DEBE cargarse ANTES del script de AdSense
+
+#### Scenario: CSS solo cuando Auto Ads esta activo
+- **GIVEN** `enable_auto_ads === false`
+- **WHEN** se renderiza el header
+- **THEN** NO DEBE inyectar el CSS de `google-auto-ads: ignore`
+
+#### Scenario: Selectores adicionales configurables
+- **GIVEN** el usuario necesita excluir elementos adicionales
+- **WHEN** existe un campo `auto_ads_exclude_selectors` en el schema
+- **THEN** DEBE agregar esos selectores al CSS de exclusion
+- **AND** el campo DEBE ser de tipo `textarea`
+- **AND** DEBE tener default vacio
+
+---
+
+### Requirement: Script de Google Auto Ads
+
+Cuando Auto Ads esta activo, el sistema DEBE insertar el script requerido por Google.
+
+#### Scenario: Insercion del script adsbygoogle
+- **GIVEN** `enable_auto_ads === true`
+- **AND** `is_enabled === true`
+- **WHEN** se renderiza el head del documento
+- **THEN** DEBE insertar el script de AdSense:
+```html
+
+```
+- **AND** DEBE usar el `publisher_id` configurado en el grupo `content`
+
+#### Scenario: No duplicar scripts
+- **GIVEN** el tema ya encola el script de AdSense para ubicaciones manuales
+- **WHEN** `enable_auto_ads === true`
+- **THEN** NO DEBE duplicar el script
+- **AND** DEBE reutilizar el script existente
+
+#### Scenario: Delay de carga respetado
+- **GIVEN** `delay_enabled === true` en el grupo `forms`
+- **AND** `enable_auto_ads === true`
+- **WHEN** se carga la pagina
+- **THEN** el script de AdSense DEBE cargarse con delay
+- **AND** DEBE respetar el valor de `delay_timeout`
+
+---
+
+### Requirement: Indicador Visual en Admin UI
+
+El panel de administracion DEBE mostrar claramente cuando Auto Ads esta activo.
+
+#### Scenario: Badge de estado Auto Ads
+- **GIVEN** el usuario accede al panel de configuracion de AdSense
+- **WHEN** `enable_auto_ads === true`
+- **THEN** DEBE mostrar un badge visible "AUTO ADS ACTIVO" en color naranja (#FF8600)
+- **AND** los grupos desactivados DEBEN aparecer con opacidad reducida (0.5)
+- **AND** los grupos desactivados DEBEN mostrar tooltip "Desactivado por Auto Ads"
+
+#### Scenario: Seccion de grupos no afectados
+- **GIVEN** `enable_auto_ads === true`
+- **WHEN** se renderiza el formulario
+- **THEN** los grupos `search_results`, `analytics`, y `content` DEBEN renderizarse normalmente
+- **AND** DEBEN tener indicador "Siempre activo" visible
+
+#### Scenario: Confirmacion al activar
+- **GIVEN** el usuario hace click en `enable_auto_ads`
+- **WHEN** el campo cambia de false a true
+- **THEN** DEBE mostrar dialogo de confirmacion con mensaje:
+ "Al activar Auto Ads, Google controlara automaticamente donde mostrar anuncios.
+ Tus configuraciones manuales se mantendran guardadas pero no se usaran.
+ Los anuncios en resultados de busqueda seguiran funcionando.
+ Continuar?"
+- **AND** DEBE tener botones "Activar Auto Ads" y "Cancelar"
+
+---
+
+### Requirement: Separacion de Capas segun Clean Architecture
+
+La implementacion DEBE seguir Clean Architecture.
+
+#### Scenario: Value Object AutoAdsConfiguration en Domain
+- **WHEN** se crea el Value Object AutoAdsConfiguration
+- **THEN** DEBE ubicarse en `Shared/Domain/ValueObjects/`
+- **AND** DEBE ser inmutable despues de construccion
+- **AND** DEBE exponer `isAutoAdsEnabled()`, `getExcludedSelectors()`, `getPublisherId()`
+- **AND** NO DEBE contener logica de WordPress
+
+#### Scenario: Interface en Domain
+- **WHEN** se define el contrato para verificar si un grupo esta activo
+- **THEN** DEBE existir metodo en `AdsenseSettingsInterface`:
+ `isGroupActiveWithAutoAds(string $groupName): bool`
+- **AND** DEBE retornar false para grupos manuales cuando Auto Ads esta activo
+- **AND** DEBE retornar true para `search_results`, `analytics`, `content`
+
+#### Scenario: Service en Infrastructure
+- **WHEN** se implementa la logica de evaluacion
+- **THEN** el `AdsenseSettingsService` DEBE verificar `enable_auto_ads` antes de evaluar ubicaciones
+- **AND** DEBE exponer metodo `getActiveGroups(): array` que retorna solo grupos activos
+
+---
+
+### Requirement: Endpoint REST para Estado de Auto Ads
+
+El endpoint de visibilidad DEBE informar el estado de Auto Ads.
+
+#### Scenario: Inclusion en response de visibility
+- **GIVEN** el endpoint `/wp-json/roi-theme/v1/adsense-placement/visibility`
+- **WHEN** responde exitosamente
+- **THEN** DEBE incluir campo `auto_ads_enabled: boolean`
+- **AND** DEBE incluir campo `auto_ads_exclude_selectors: string`
+
+#### Scenario: Response cuando Auto Ads esta activo
+- **GIVEN** `enable_auto_ads === true`
+- **WHEN** se llama al endpoint
+- **THEN** el response DEBE incluir:
+```json
+{
+ "show_ads": true,
+ "auto_ads_enabled": true,
+ "auto_ads_exclude_selectors": "table,.wp-block-table",
+ "manual_slots_disabled": true,
+ "search_slots_enabled": true,
+ "reasons": [],
+ "cache_seconds": 300,
+ "timestamp": 1733900000
+}
+```
+
+---
+
+### Requirement: Migracion y Retrocompatibilidad
+
+La implementacion DEBE mantener compatibilidad con instalaciones existentes.
+
+#### Scenario: Valor default para instalaciones existentes
+- **GIVEN** una instalacion existente sin el campo `enable_auto_ads`
+- **WHEN** se ejecuta sync-component
+- **THEN** DEBE crear el campo con valor `false`
+- **AND** el comportamiento existente NO DEBE cambiar
+
+#### Scenario: Sin cambios en logica si Auto Ads esta desactivado
+- **GIVEN** `enable_auto_ads === false`
+- **WHEN** se evaluan ubicaciones de anuncios
+- **THEN** el comportamiento DEBE ser identico al actual
+- **AND** NO DEBE haber impacto en rendimiento
+
+---
+
+### Requirement: Logging y Diagnostico
+
+El sistema DEBE proveer informacion de diagnostico para troubleshooting.
+
+#### Scenario: Log cuando Auto Ads ignora ubicacion
+- **GIVEN** `enable_auto_ads === true`
+- **AND** WP_DEBUG === true
+- **WHEN** un Renderer es llamado para ubicacion manual
+- **THEN** DEBE loguear mensaje: "AdSense: Skipping manual slot '{slot_name}' - Auto Ads active"
+- **AND** el log DEBE ser level DEBUG
+
+#### Scenario: Comentario HTML de diagnostico
+- **GIVEN** `enable_auto_ads === true`
+- **AND** WP_DEBUG === true
+- **WHEN** se renderiza el header
+- **THEN** DEBE incluir comentario HTML:
+```html
+
+```
+
+---
+
+## Implementation Notes
+
+### Schema Changes (adsense-placement.json)
+
+Agregar al grupo `visibility`:
+
+```json
+"enable_auto_ads": {
+ "type": "boolean",
+ "label": "Activar Google Auto Ads",
+ "default": false,
+ "editable": true,
+ "description": "Google controla automaticamente la ubicacion de anuncios. Se desactivan ubicaciones manuales excepto busqueda."
+}
+```
+
+Agregar al grupo `forms` (nuevo campo):
+
+```json
+"auto_ads_exclude_selectors": {
+ "type": "textarea",
+ "label": "Selectores adicionales a excluir de Auto Ads",
+ "default": "",
+ "editable": true,
+ "description": "Selectores CSS adicionales donde Auto Ads no debe insertar anuncios (uno por linea)"
+}
+```
+
+### CSS Default Exclusions
+
+```css
+/* Elementos que Google Auto Ads debe ignorar */
+table,
+.wp-block-table,
+.tablepress,
+figure.wp-block-table,
+.entry-content table,
+.roi-apu-results,
+.roi-apu-result-item,
+pre,
+code,
+.wp-block-code,
+.syntax-highlighted {
+ google-auto-ads: ignore;
+}
+```
+
+### Grupos y su comportamiento con Auto Ads
+
+| Grupo | Con Auto Ads OFF | Con Auto Ads ON |
+|-------|------------------|-----------------|
+| visibility | Activo | Activo (controla is_enabled global) |
+| analytics | Activo | Activo |
+| content | Activo | Activo (provee publisher_id) |
+| incontent_advanced | Activo | IGNORADO |
+| behavior | Activo | IGNORADO (excepto javascript_first_mode) |
+| anchor_ads | Activo | IGNORADO |
+| vignette_ads | Activo | IGNORADO |
+| search_results | Activo | ACTIVO (excepcion) |
+| layout | Activo | IGNORADO |
+| forms | Activo | Activo (delay, exclusiones) |
+
+---
+
+## Validation Checklist
+
+Antes de considerar esta especificacion implementada:
+
+- [ ] Campo `enable_auto_ads` existe en schema y BD
+- [ ] Campo `auto_ads_exclude_selectors` existe en schema y BD
+- [ ] CSS de exclusion se inyecta solo cuando Auto Ads activo
+- [ ] Slots manuales no se renderizan cuando Auto Ads activo
+- [ ] Slots de busqueda SI se renderizan cuando Auto Ads activo
+- [ ] `roi_get_adsense_search_config()` funciona independiente de Auto Ads
+- [ ] Admin UI muestra indicador visual de Auto Ads activo
+- [ ] Grupos desactivados aparecen con opacidad reducida
+- [ ] Endpoint REST incluye `auto_ads_enabled` en response
+- [ ] JavaScript oculta slots manuales pero no de busqueda
+- [ ] Script de AdSense se carga correctamente
+- [ ] No hay duplicacion de scripts
+- [ ] Logs de debug funcionan cuando WP_DEBUG activo
+- [ ] Sync-component migra correctamente instalaciones existentes
diff --git a/_openspec/changes/adsense-cache-unified-visibility/spec.md b/_openspec/changes/adsense-cache-unified-visibility/spec.md
new file mode 100644
index 00000000..3ad4c812
--- /dev/null
+++ b/_openspec/changes/adsense-cache-unified-visibility/spec.md
@@ -0,0 +1,309 @@
+# Especificacion: Unificacion de Visibilidad AdSense para Compatibilidad con Cache
+
+## Purpose
+
+Unificar la logica de visibilidad de AdSense para que TODA la evaluacion dependiente de usuario
+(hide_for_logged_in) se realice en el cliente via JavaScript-First, eliminando llamadas a
+`is_user_logged_in()` durante el render PHP.
+
+**Problema actual:** El `AdsensePlacementRenderer` usa `PageVisibilityHelper::shouldShow()` que
+internamente llama `is_user_logged_in()` en PHP. Esto causa que el HTML generado varie segun el
+estado de autenticacion, rompiendo la compatibilidad con page cache.
+
+**Solucion:** Cuando `javascript_first_mode` esta activo, el Renderer debe generar SIEMPRE el HTML
+de los slots, delegando la decision de `hide_for_logged_in` al endpoint REST y al JavaScript del cliente.
+
+---
+
+## Background
+
+### Arquitectura JavaScript-First existente (spec: adsense-javascript-first)
+
+El sistema JavaScript-First ya implementa:
+1. **Endpoint REST** `/wp-json/roi-theme/v1/adsense-placement/visibility`
+ - Evalua `is_user_logged_in()` en tiempo real (no cacheado)
+ - Retorna decision `show_ads: true/false` con razones
+ - Headers anti-cache (no-store, no-cache)
+
+2. **Cliente JavaScript** `adsense-visibility.js`
+ - Consulta endpoint via AJAX con cookies (credentials: same-origin)
+ - Cachea decision en localStorage
+ - Aplica clases CSS para mostrar/ocultar slots
+
+### El gap actual
+
+```
+FLUJO ACTUAL (PROBLEMATICO):
+
+Request → PHP Render → PageVisibilityHelper::shouldShow()
+ ↓
+ is_user_logged_in() ← ROMPE CACHE
+ ↓
+ HTML con/sin slots (depende de login)
+ ↓
+ Page Cache ← HTML incorrecto para otros usuarios
+```
+
+```
+FLUJO DESEADO (CON ESTA SPEC):
+
+Request → PHP Render → [javascript_first_mode=true?]
+ ↓ SI
+ SIEMPRE genera HTML con slots
+ ↓
+ Page Cache ← HTML identico para todos
+ ↓
+ JS consulta endpoint REST
+ ↓
+ Muestra/oculta segun respuesta
+```
+
+---
+
+## Requirements
+
+### Requirement 1: Bypass de hide_for_logged_in en modo JavaScript-First
+
+The AdsensePlacementRenderer MUST skip the `is_user_logged_in()` evaluation during PHP render
+when `javascript_first_mode` is enabled.
+
+#### Scenario: JavaScript-First activo, usuario anonimo
+- **GIVEN** `javascript_first_mode` esta habilitado en settings
+- **AND** `hide_for_logged_in` esta habilitado
+- **AND** el visitante NO esta logueado
+- **WHEN** se renderiza un slot de AdSense
+- **THEN** el HTML del slot DEBE generarse (placeholders visibles)
+- **AND** el JavaScript DEBE consultar el endpoint
+- **AND** el endpoint retorna `show_ads: true`
+- **AND** los anuncios se muestran
+
+#### Scenario: JavaScript-First activo, usuario logueado
+- **GIVEN** `javascript_first_mode` esta habilitado
+- **AND** `hide_for_logged_in` esta habilitado
+- **AND** el visitante ESTA logueado
+- **WHEN** se renderiza un slot de AdSense
+- **THEN** el HTML del slot DEBE generarse (en cache seria identico a anonimo)
+- **AND** el JavaScript DEBE consultar el endpoint
+- **AND** el endpoint retorna `show_ads: false` con razon `hide_for_logged_in`
+- **AND** los anuncios se OCULTAN via CSS/JS
+
+#### Scenario: JavaScript-First deshabilitado (modo legacy)
+- **GIVEN** `javascript_first_mode` esta DESHABILITADO
+- **AND** `hide_for_logged_in` esta habilitado
+- **AND** el visitante ESTA logueado
+- **WHEN** se renderiza un slot de AdSense
+- **THEN** el HTML del slot NO se genera (comportamiento legacy)
+- **AND** `is_user_logged_in()` SE evalua en PHP
+- **BECAUSE** sin JS-First, el modo legacy es la unica opcion
+
+---
+
+### Requirement 2: PageVisibilityHelper debe respetar modo JavaScript-First
+
+The PageVisibilityHelper MUST provide a method that excludes user-dependent checks when
+JavaScript-First mode is active for a component.
+
+#### Scenario: Nuevo metodo shouldShowForCache
+- **GIVEN** un componente con `javascript_first_mode` habilitado
+- **WHEN** se llama `PageVisibilityHelper::shouldShowForCache('adsense-placement')`
+- **THEN** DEBE evaluar visibilidad por tipo de pagina (home, posts, pages, etc.)
+- **AND** DEBE evaluar exclusiones por categoria, ID, URL pattern
+- **AND** NO DEBE evaluar `hide_for_logged_in`
+- **BECAUSE** esa evaluacion la hace el cliente
+
+#### Scenario: Componente sin JavaScript-First usa metodo existente
+- **GIVEN** un componente sin `javascript_first_mode`
+- **WHEN** se llama `PageVisibilityHelper::shouldShow('otro-componente')`
+- **THEN** DEBE usar flujo existente incluyendo `hide_for_logged_in`
+- **BECAUSE** sin JS-First, PHP es la unica fuente de verdad
+
+---
+
+### Requirement 3: WordPressComponentVisibilityRepository debe soportar modo bypass
+
+The repository MUST support skipping `is_user_logged_in()` check when requested.
+
+#### Scenario: isNotExcluded con bypass de login check
+- **GIVEN** se llama `isNotExcluded('adsense-placement', skipLoginCheck: true)`
+- **WHEN** el metodo evalua exclusiones
+- **THEN** NO DEBE llamar `is_user_logged_in()`
+- **AND** DEBE evaluar otras exclusiones normalmente
+
+---
+
+### Requirement 4: El endpoint REST DEBE evaluar hide_for_logged_in
+
+The REST endpoint `/adsense-placement/visibility` MUST evaluate `hide_for_logged_in`
+and include it in the decision.
+
+**NOTA:** Esto YA esta implementado en `AdsenseVisibilityController.php`. Solo documentamos
+para claridad.
+
+#### Scenario: Endpoint evalua usuario logueado
+- **GIVEN** peticion al endpoint con cookies de sesion
+- **AND** `hide_for_logged_in` esta habilitado
+- **AND** el usuario ESTA logueado
+- **WHEN** el endpoint procesa la peticion
+- **THEN** retorna `show_ads: false`
+- **AND** `reasons` incluye `hide_for_logged_in`
+
+---
+
+### Requirement 5: El CSS de slots ocultos NO debe afectar el layout
+
+When JavaScript hides ad slots, they MUST collapse completely without affecting page layout.
+
+**NOTA:** Esto YA esta implementado con `:has([data-ad-status='unfilled'])`. Documentamos para
+verificar que se mantiene.
+
+---
+
+## Implementation
+
+### Archivos a modificar
+
+#### 1. AdsensePlacementRenderer.php
+
+```php
+// ANTES (linea 38-43):
+public function renderSlot(array $settings, string $location): string
+{
+ // 0. Verificar visibilidad por tipo de pagina y exclusiones
+ if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
+ return '';
+ }
+ // ...
+}
+
+// DESPUES:
+public function renderSlot(array $settings, string $location): string
+{
+ // 0. Verificar visibilidad (respetando modo JS-First para cache)
+ $jsFirstMode = ($settings['behavior']['javascript_first_mode'] ?? false) === true;
+
+ if ($jsFirstMode) {
+ // En modo JS-First, usar evaluacion cache-friendly (sin is_user_logged_in)
+ if (!PageVisibilityHelper::shouldShowForCache('adsense-placement')) {
+ return '';
+ }
+ } else {
+ // Modo legacy: usar evaluacion completa
+ if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
+ return '';
+ }
+ }
+ // ...
+}
+```
+
+#### 2. PageVisibilityHelper.php
+
+```php
+// AGREGAR nuevo metodo:
+/**
+ * Evalua visibilidad SIN checks dependientes de usuario
+ *
+ * Para uso cuando JavaScript manejara los checks de usuario.
+ * Evalua: tipos de pagina, exclusiones por categoria/ID/URL
+ * NO evalua: hide_for_logged_in
+ *
+ * @param string $componentName
+ * @return bool
+ */
+public static function shouldShowForCache(string $componentName): bool
+{
+ $container = DIContainer::getInstance();
+ $useCase = $container->getEvaluateComponentVisibilityUseCase();
+
+ return $useCase->executeForCache($componentName);
+}
+```
+
+#### 3. EvaluateComponentVisibilityUseCase.php
+
+```php
+// AGREGAR nuevo metodo:
+/**
+ * Evalua visibilidad SIN checks dependientes de usuario
+ */
+public function executeForCache(string $componentName): bool
+{
+ // Paso 1: Verificar visibilidad por tipo de pagina (sin cambios)
+ $visibleByPageType = $this->pageVisibilityUseCase->execute($componentName);
+
+ if (!$visibleByPageType) {
+ return false;
+ }
+
+ // Paso 2: Verificar exclusiones SIN hide_for_logged_in
+ $isExcluded = $this->exclusionsUseCase->executeForCache($componentName);
+
+ return !$isExcluded;
+}
+```
+
+#### 4. EvaluateExclusionsUseCase.php
+
+```php
+// AGREGAR nuevo metodo:
+/**
+ * Evalua exclusiones SIN hide_for_logged_in
+ */
+public function executeForCache(string $componentName): bool
+{
+ // Evaluar exclusiones por categoria, ID, URL
+ // NO evaluar hide_for_logged_in
+ return $this->repository->isExcludedForCache($componentName);
+}
+```
+
+#### 5. WordPressComponentVisibilityRepository.php
+
+```php
+// MODIFICAR isNotExcluded o agregar nuevo metodo:
+/**
+ * Verifica exclusiones SIN evaluar hide_for_logged_in
+ */
+public function isNotExcludedForCache(string $componentName): bool
+{
+ // OMITE: shouldHideForLoggedIn()
+ // MANTIENE: PageVisibilityHelper::shouldShow() para otras exclusiones
+
+ return PageVisibilityHelper::shouldShowByPageType($componentName);
+}
+```
+
+---
+
+## Acceptance Criteria
+
+1. Con `javascript_first_mode=true` y `hide_for_logged_in=true`:
+ - Usuario anonimo: HTML generado, JS muestra ads
+ - Usuario logueado: HTML generado, JS oculta ads
+ - Page cache sirve mismo HTML a ambos
+
+2. Con `javascript_first_mode=false`:
+ - Comportamiento legacy sin cambios
+ - `is_user_logged_in()` se evalua en PHP
+
+3. Otras exclusiones (categoria, ID, URL) funcionan igual en ambos modos
+
+4. No hay regresion en visibilidad por tipo de pagina
+
+5. Los slots ocultos por JS colapsan completamente (height: 0)
+
+---
+
+## Migration Notes
+
+- **NO hay breaking changes**: `shouldShow()` mantiene comportamiento actual
+- **Nuevo metodo opcional**: `shouldShowForCache()` para modo JS-First
+- **Backward compatible**: Si `javascript_first_mode=false`, todo funciona igual
+
+---
+
+## Version History
+
+| Version | Date | Changes |
+|---------|------|---------|
+| 1.0 | 2025-12-11 | Initial spec |
diff --git a/_openspec/changes/adsense-cache-unified-visibility/test-plan.md b/_openspec/changes/adsense-cache-unified-visibility/test-plan.md
new file mode 100644
index 00000000..bcce4ac4
--- /dev/null
+++ b/_openspec/changes/adsense-cache-unified-visibility/test-plan.md
@@ -0,0 +1,214 @@
+# Test Plan: Unificacion de Visibilidad AdSense para Cache
+
+## Test Environment
+
+- **DEV URL**: https://dev.analisisdepreciosunitarios.com
+- **PROD URL**: https://analisisdepreciosunitarios.com
+- **Browsers**: Chrome, Firefox
+- **Tools**: DevTools Console, Network tab, Playwright MCP
+
+---
+
+## Pre-requisitos
+
+1. Habilitar `javascript_first_mode` en settings de adsense-placement
+2. Habilitar `hide_for_logged_in` en settings de adsense-placement
+3. Tener cuenta de usuario para pruebas de login
+4. Limpiar cache del sitio antes de cada prueba
+
+---
+
+## Test Cases
+
+### TC01: HTML se genera para usuarios anonimos (JS-First activo)
+
+**Objetivo**: Verificar que los slots de ads se renderizan en HTML para visitantes anonimos
+
+| Campo | Valor |
+|-------|-------|
+| Pre-condicion | `javascript_first_mode=true`, `hide_for_logged_in=true`, usuario anonimo |
+| Pasos | 1. Limpiar cache del sitio 2. Abrir pagina de post en navegacion privada 3. Inspeccionar HTML |
+| Resultado esperado | Elementos `.roi-adsense-placeholder` presentes en DOM |
+| Status | PENDING |
+| Notas | |
+
+### TC02: HTML se genera para usuarios logueados (JS-First activo)
+
+**Objetivo**: Verificar que los slots de ads se renderizan en HTML incluso para usuarios logueados
+
+| Campo | Valor |
+|-------|-------|
+| Pre-condicion | `javascript_first_mode=true`, `hide_for_logged_in=true`, usuario logueado |
+| Pasos | 1. Limpiar cache del sitio 2. Login como usuario 3. Visitar pagina de post 4. Inspeccionar HTML |
+| Resultado esperado | Elementos `.roi-adsense-placeholder` presentes en DOM (mismo HTML que TC01) |
+| Status | PENDING |
+| Notas | Clave: el HTML debe ser IDENTICO al de TC01 |
+
+### TC03: Endpoint retorna show_ads=false para usuarios logueados
+
+**Objetivo**: Verificar que el endpoint REST evalua correctamente hide_for_logged_in
+
+| Campo | Valor |
+|-------|-------|
+| Pre-condicion | `javascript_first_mode=true`, `hide_for_logged_in=true`, usuario logueado |
+| Pasos | 1. Login como usuario 2. En console ejecutar: `fetch('/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1', {credentials:'same-origin'}).then(r=>r.json()).then(console.log)` |
+| Resultado esperado | Respuesta: `{show_ads: false, reasons: ['hide_for_logged_in'], ...}` |
+| Status | PENDING |
+| Notas | |
+
+### TC04: JavaScript oculta ads para usuarios logueados
+
+**Objetivo**: Verificar que el JavaScript aplica correctamente la decision del endpoint
+
+| Campo | Valor |
+|-------|-------|
+| Pre-condicion | `javascript_first_mode=true`, `hide_for_logged_in=true`, usuario logueado |
+| Pasos | 1. Login como usuario 2. Visitar pagina de post 3. Verificar clases CSS de slots 4. Verificar evento `roiAdsenseDeactivated` |
+| Resultado esperado | Slots tienen clase `roi-adsense-hidden` o altura colapsada |
+| Status | PENDING |
+| Notas | |
+
+### TC05: JavaScript muestra ads para usuarios anonimos
+
+**Objetivo**: Verificar que el JavaScript muestra correctamente los ads
+
+| Campo | Valor |
+|-------|-------|
+| Pre-condicion | `javascript_first_mode=true`, `hide_for_logged_in=true`, usuario anonimo |
+| Pasos | 1. Navegacion privada 2. Visitar pagina de post 3. Verificar clases CSS de slots |
+| Resultado esperado | Slots tienen clase `roi-adsense-active` |
+| Status | PENDING |
+| Notas | |
+
+### TC06: Cache sirve mismo HTML a ambos tipos de usuario
+
+**Objetivo**: Verificar compatibilidad con page cache
+
+| Campo | Valor |
+|-------|-------|
+| Pre-condicion | `javascript_first_mode=true`, page cache activo |
+| Pasos | 1. Limpiar cache 2. Usuario anonimo visita pagina (genera cache) 3. Usuario logueado visita misma pagina 4. Comparar HTML de `.roi-adsense-placeholder` |
+| Resultado esperado | HTML identico, diferencia solo en comportamiento JS |
+| Status | PENDING |
+| Notas | Este es el test critico de la spec |
+
+### TC07: Modo legacy funciona sin cambios (JS-First deshabilitado)
+
+**Objetivo**: Verificar backward compatibility
+
+| Campo | Valor |
+|-------|-------|
+| Pre-condicion | `javascript_first_mode=false`, `hide_for_logged_in=true`, usuario logueado |
+| Pasos | 1. Deshabilitar JS-First en settings 2. Login como usuario 3. Visitar pagina de post 4. Inspeccionar HTML |
+| Resultado esperado | Elementos `.roi-adsense-placeholder` NO presentes (comportamiento legacy) |
+| Status | PENDING |
+| Notas | Comportamiento identico al actual |
+
+### TC08: Exclusiones por tipo de pagina funcionan en ambos modos
+
+**Objetivo**: Verificar que exclusiones no relacionadas con login siguen funcionando
+
+| Campo | Valor |
+|-------|-------|
+| Pre-condicion | `show_on_archives=false` configurado |
+| Pasos | 1. Visitar pagina de archivo (categoria) 2. Verificar que no hay slots de ads |
+| Resultado esperado | Sin slots de ads en paginas de archivo |
+| Status | PENDING |
+| Notas | |
+
+### TC09: Exclusiones por categoria funcionan en ambos modos
+
+**Objetivo**: Verificar que exclusiones por categoria siguen funcionando
+
+| Campo | Valor |
+|-------|-------|
+| Pre-condicion | Categoria excluida configurada |
+| Pasos | 1. Visitar post de categoria excluida 2. Verificar que no hay slots de ads |
+| Resultado esperado | Sin slots de ads en posts de categoria excluida |
+| Status | PENDING |
+| Notas | |
+
+---
+
+## Automated Test Scripts
+
+### Script: Verificar HTML identico para cache
+
+```javascript
+// Ejecutar en Playwright o consola
+// Compara DOM de slots entre usuario anonimo y logueado
+
+async function verifyCacheCompatibility() {
+ const anonSlots = document.querySelectorAll('.roi-adsense-placeholder').length;
+ console.log(`Slots encontrados: ${anonSlots}`);
+
+ // El HTML debe existir independientemente del estado de login
+ // La diferencia esta en las clases CSS aplicadas por JS
+ const activeSlots = document.querySelectorAll('.roi-adsense-active').length;
+ const hiddenSlots = document.querySelectorAll('.roi-adsense-hidden').length;
+
+ console.log(`Activos: ${activeSlots}, Ocultos: ${hiddenSlots}`);
+
+ return {
+ totalSlots: anonSlots,
+ active: activeSlots,
+ hidden: hiddenSlots
+ };
+}
+
+verifyCacheCompatibility();
+```
+
+### Script: Verificar respuesta del endpoint
+
+```javascript
+// Ejecutar en consola con usuario logueado
+async function testEndpoint() {
+ const postId = document.querySelector('article')?.id?.replace('post-', '') || '1';
+ const url = `/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=${postId}`;
+
+ const response = await fetch(url, { credentials: 'same-origin' });
+ const data = await response.json();
+
+ console.log('Respuesta del endpoint:');
+ console.log('- show_ads:', data.show_ads);
+ console.log('- reasons:', data.reasons);
+ console.log('- cache_seconds:', data.cache_seconds);
+
+ return data;
+}
+
+testEndpoint();
+```
+
+---
+
+## Notas de Implementacion
+
+1. **Orden de ejecucion**: Implementar spec primero, luego ejecutar tests
+2. **Rollback**: Si falla TC06, revertir cambios (backward compatible)
+3. **Monitoreo**: Despues de deploy, verificar metricas de AdSense por 24h
+
+---
+
+## Results Summary
+
+| Test Case | Status | Date | Tester |
+|-----------|--------|------|--------|
+| TC01 | PENDING | | |
+| TC02 | PENDING | | |
+| TC03 | PENDING | | |
+| TC04 | PENDING | | |
+| TC05 | PENDING | | |
+| TC06 | PENDING | | |
+| TC07 | PENDING | | |
+| TC08 | PENDING | | |
+| TC09 | PENDING | | |
+
+---
+
+## Version History
+
+| Version | Date | Changes |
+|---------|------|---------|
+| 1.0 | 2025-12-11 | Initial test plan |
diff --git a/openspec/specs/adsense-javascript-first/spec.md b/_openspec/changes/adsense-javascript-first/spec.md
similarity index 100%
rename from openspec/specs/adsense-javascript-first/spec.md
rename to _openspec/changes/adsense-javascript-first/spec.md
diff --git a/_openspec/changes/adsense-javascript-first/test-plan.md b/_openspec/changes/adsense-javascript-first/test-plan.md
new file mode 100644
index 00000000..0c20606d
--- /dev/null
+++ b/_openspec/changes/adsense-javascript-first/test-plan.md
@@ -0,0 +1,1334 @@
+# Plan de Pruebas: AdSense JavaScript-First Architecture
+
+> **NOTA IMPORTANTE - PROTOCOLO DE PRUEBAS**
+>
+> Las pruebas se ejecutan en el servidor de PRODUCCION.
+> Si hay algo que corregir, se modifica en LOCAL y luego se despliega.
+>
+> **PROHIBIDO**: Modificar codigo directamente en produccion.
+> **PERMITIDO**: Solo ejecutar pruebas y verificaciones en produccion.
+>
+> Flujo correcto:
+> 1. Ejecutar prueba en produccion
+> 2. Si falla, corregir en local
+> 3. Desplegar cambios a produccion
+> 4. Re-ejecutar prueba
+
+---
+
+## Informacion del Servidor de Produccion
+
+| Campo | Valor |
+|-------|-------|
+| **Host SSH** | `VPSContabo` (alias en ~/.ssh/config) |
+| **IP** | `5.189.136.96` |
+| **Usuario** | `root` |
+| **Ruta del tema** | `/var/www/preciosunitarios/public_html/wp-content/themes/roi-theme` |
+| **URL produccion** | `https://analisisdepreciosunitarios.com` |
+| **PHP Version** | 8.2 (php8.2-fpm) |
+
+### Comandos de Deploy
+
+```bash
+# 1. Push desde local (sube a GitHub + Gitea automaticamente)
+git push origin main
+
+# 2. Pull en produccion
+ssh VPSContabo "cd /var/www/preciosunitarios/public_html/wp-content/themes/roi-theme && git pull origin main"
+
+# 3. Limpiar cache de OPcache (IMPORTANTE despues de deploy)
+ssh VPSContabo "systemctl restart php8.2-fpm"
+```
+
+---
+
+## Resumen de Pruebas
+
+| ID | Categoria | Descripcion | Criterio de Aceptacion |
+|----|-----------|-------------|------------------------|
+| T01 | Endpoint REST | Endpoint registrado y accesible | HTTP 200 con JSON valido |
+| T02 | Endpoint REST | Headers anti-cache presentes | Cache-Control, Pragma, Expires |
+| T03 | Endpoint REST | Parametro post_id requerido | HTTP 400 sin post_id |
+| T04 | Endpoint REST | post_id=0 valido (archivos/home) | HTTP 200 con post_id=0 |
+| T05 | Visibilidad | Componente deshabilitado | show_ads=false, reason=component_disabled |
+| T06 | Visibilidad | Usuario anonimo sin exclusiones | show_ads=true, reasons=[] |
+| T07 | Visibilidad | Usuario logueado excluido | show_ads=false, reason=logged_in_excluded |
+| T08 | Visibilidad | Rol excluido | show_ads=false, reason=role_excluded |
+| T09 | Visibilidad | Post excluido | show_ads=false, reason=post_excluded |
+| T10 | JavaScript | Script cargado en frontend | roiAdsenseConfig definido |
+| T11 | JavaScript | Cache localStorage funciona | Datos guardados correctamente |
+| T12 | JavaScript | Fallback cuando error | Ads se muestran en error |
+| T13 | Feature Flag | Modo deshabilitado = legacy | No llama endpoint |
+| T14 | Feature Flag | Modo habilitado = JS-First | Llama endpoint |
+| T15 | Clean Arch | Value Objects inmutables | No WordPress en Domain |
+| T16 | Clean Arch | Interface en Domain | AdsenseVisibilityCheckerInterface existe |
+
+---
+
+## Pruebas Detalladas
+
+### T01: Endpoint REST Registrado y Accesible
+
+**Categoria**: Endpoint REST
+**Prioridad**: CRITICA
+**Spec Reference**: Requirement: Endpoint REST Visibility
+
+**Pasos**:
+1. Abrir navegador o usar curl
+2. Acceder a: `https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1`
+
+**Resultado Esperado**:
+- HTTP Status: 200
+- Content-Type: application/json
+- Body contiene: `show_ads`, `reasons`, `cache_seconds`, `timestamp`
+
+**Comando de Prueba**:
+```bash
+curl -i "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1"
+```
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+**Notas**:
+
+---
+
+### T02: Headers Anti-Cache Presentes
+
+**Categoria**: Endpoint REST
+**Prioridad**: ALTA
+**Spec Reference**: Scenario: Headers anti-cache obligatorios
+
+**Pasos**:
+1. Hacer request al endpoint
+2. Verificar headers de respuesta
+
+**Resultado Esperado**:
+- `Cache-Control: no-store, no-cache, must-revalidate, max-age=0`
+- `Pragma: no-cache`
+- `Expires: Thu, 01 Jan 1970 00:00:00 GMT` o `0`
+- `Vary: Cookie`
+
+**Comando de Prueba**:
+```bash
+curl -I "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1"
+```
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+**Notas**:
+
+---
+
+### T03: Parametro post_id Requerido
+
+**Categoria**: Endpoint REST
+**Prioridad**: ALTA
+**Spec Reference**: Scenario: Parametros del endpoint
+
+**Pasos**:
+1. Hacer request SIN post_id
+2. Verificar respuesta de error
+
+**Resultado Esperado**:
+- HTTP Status: 400 (Bad Request)
+- Body contiene mensaje de error indicando que post_id es requerido
+
+**Comando de Prueba**:
+```bash
+curl -i "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility"
+```
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+**Notas**:
+
+---
+
+### T04: post_id=0 Valido para Paginas de Archivo
+
+**Categoria**: Endpoint REST
+**Prioridad**: ALTA
+**Spec Reference**: Scenario: Parametros del endpoint (validate_callback >= 0)
+
+**Pasos**:
+1. Hacer request con post_id=0
+2. Verificar que responde correctamente
+
+**Resultado Esperado**:
+- HTTP Status: 200
+- Body contiene decision de visibilidad valida
+
+**Comando de Prueba**:
+```bash
+curl -i "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=0"
+```
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+**Notas**:
+
+---
+
+### T05: Componente Deshabilitado Retorna False
+
+**Categoria**: Visibilidad
+**Prioridad**: ALTA
+**Spec Reference**: Scenario: Componente deshabilitado
+
+**Pre-condicion**:
+- Deshabilitar componente en admin (is_enabled = false)
+
+**Pasos**:
+1. Deshabilitar adsense-placement en admin
+2. Hacer request al endpoint
+3. Verificar respuesta
+
+**Resultado Esperado**:
+```json
+{
+ "show_ads": false,
+ "reasons": ["component_disabled"],
+ "cache_seconds": 3600
+}
+```
+
+**Comando de Prueba**:
+```bash
+curl "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1"
+```
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+**Notas**:
+
+---
+
+### T06: Usuario Anonimo Sin Exclusiones Ve Ads
+
+**Categoria**: Visibilidad
+**Prioridad**: CRITICA
+**Spec Reference**: Scenario: Usuario anonimo sin exclusiones
+
+**Pre-condicion**:
+- Componente habilitado
+- javascript_first_mode habilitado
+- Sin exclusiones configuradas
+- No estar logueado
+
+**Pasos**:
+1. Abrir navegador en modo incognito
+2. Acceder a un post del sitio
+3. Verificar respuesta del endpoint
+
+**Resultado Esperado**:
+```json
+{
+ "show_ads": true,
+ "reasons": [],
+ "cache_seconds": 60
+}
+```
+
+**Comando de Prueba**:
+```bash
+curl "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1"
+```
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+**Notas**:
+
+---
+
+### T07: Usuario Logueado Excluido
+
+**Categoria**: Visibilidad
+**Prioridad**: ALTA
+**Spec Reference**: Scenario: Usuario logueado excluido
+
+**Pre-condicion**:
+- Activar "Ocultar para usuarios logueados" en admin
+
+**Pasos**:
+1. Loguearse en WordPress
+2. Copiar cookies de sesion
+3. Hacer request con cookies
+
+**Resultado Esperado**:
+```json
+{
+ "show_ads": false,
+ "reasons": ["logged_in_excluded"],
+ "cache_seconds": 300
+}
+```
+
+**Verificacion Manual**:
+1. Loguearse en wp-admin
+2. Visitar un post en el frontend
+3. Abrir DevTools > Network
+4. Buscar request a `/visibility`
+5. Verificar respuesta
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+**Notas**:
+
+---
+
+### T08: Rol Excluido
+
+**Categoria**: Visibilidad
+**Prioridad**: ALTA
+**Spec Reference**: Scenario: Rol de usuario excluido
+
+**Pre-condicion**:
+- Agregar "administrator" a roles excluidos en admin
+
+**Pasos**:
+1. Loguearse como administrator
+2. Visitar un post
+3. Verificar respuesta del endpoint
+
+**Resultado Esperado**:
+```json
+{
+ "show_ads": false,
+ "reasons": ["role_excluded"],
+ "cache_seconds": 300
+}
+```
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+**Notas**:
+
+---
+
+### T09: Post Excluido por ID
+
+**Categoria**: Visibilidad
+**Prioridad**: ALTA
+**Spec Reference**: Scenario: Post excluido por ID
+
+**Pre-condicion**:
+- Agregar un ID de post a "IDs de posts excluidos" en admin
+
+**Pasos**:
+1. Anotar el ID del post excluido (ej: 123)
+2. Hacer request con ese post_id
+3. Verificar respuesta
+
+**Resultado Esperado**:
+```json
+{
+ "show_ads": false,
+ "reasons": ["post_excluded"],
+ "cache_seconds": 60
+}
+```
+
+**Comando de Prueba**:
+```bash
+curl "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=123"
+```
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+**Notas**:
+
+---
+
+### T10: Script JavaScript Cargado
+
+**Categoria**: JavaScript
+**Prioridad**: CRITICA
+**Spec Reference**: Scenario: Configuracion via wp_localize_script
+
+**Pre-condicion**:
+- javascript_first_mode habilitado
+
+**Pasos**:
+1. Visitar un post en el frontend
+2. Abrir DevTools > Console
+3. Escribir: `window.roiAdsenseConfig`
+
+**Resultado Esperado**:
+- Objeto definido con propiedades:
+ - `endpoint`: URL del endpoint REST
+ - `postId`: ID del post actual
+ - `nonce`: String no vacio
+ - `featureEnabled`: true
+ - `debug`: boolean
+
+**Verificacion Alternativa**:
+```javascript
+// En consola del navegador
+console.log(window.roiAdsenseConfig);
+console.log(typeof window.roiAdsenseVisibility);
+```
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+**Notas**:
+
+---
+
+### T11: Cache localStorage Funciona
+
+**Categoria**: JavaScript
+**Prioridad**: ALTA
+**Spec Reference**: Scenario: Cache en localStorage
+
+**Pasos**:
+1. Visitar un post (primera vez)
+2. Abrir DevTools > Application > Local Storage
+3. Buscar key `roi_adsense_visibility`
+4. Recargar pagina
+5. Verificar en Network que NO hay nueva llamada al endpoint
+
+**Resultado Esperado**:
+- localStorage contiene:
+ ```json
+ {
+ "show_ads": true,
+ "reasons": [],
+ "timestamp": 1733900000,
+ "cache_seconds": 60
+ }
+ ```
+- Segunda carga NO hace request al endpoint (usa cache)
+
+**Verificacion en Consola**:
+```javascript
+localStorage.getItem('roi_adsense_visibility');
+```
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+**Notas**:
+
+---
+
+### T12: Fallback en Error de Red
+
+**Categoria**: JavaScript
+**Prioridad**: ALTA
+**Spec Reference**: Scenario: Fallback strategy cached-or-show
+
+**Pasos**:
+1. Limpiar localStorage
+2. Abrir DevTools > Network
+3. Habilitar "Offline" mode
+4. Visitar un post
+5. Verificar comportamiento
+
+**Resultado Esperado**:
+- Los ads se muestran (fallback = show)
+- No hay error en consola (error manejado gracefully)
+
+**Verificacion Alternativa**:
+```javascript
+// Limpiar cache
+window.roiAdsenseVisibility.clearCache();
+// Recargar con network offline
+```
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+**Notas**:
+
+---
+
+### T13: Feature Flag Deshabilitado = Modo Legacy
+
+**Categoria**: Feature Flag
+**Prioridad**: ALTA
+**Spec Reference**: Scenario: Feature flag deshabilitado
+
+**Pre-condicion**:
+- Deshabilitar javascript_first_mode en admin
+
+**Pasos**:
+1. Deshabilitar javascript_first_mode
+2. Visitar un post
+3. Verificar en Network que NO hay llamada al endpoint
+
+**Resultado Esperado**:
+- `roiAdsenseConfig.featureEnabled` = false
+- No hay request a `/visibility` endpoint
+- Ads se muestran inmediatamente (modo legacy)
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+**Notas**:
+
+---
+
+### T14: Feature Flag Habilitado = JS-First
+
+**Categoria**: Feature Flag
+**Prioridad**: ALTA
+**Spec Reference**: Scenario: Feature flag habilitado
+
+**Pre-condicion**:
+- Habilitar javascript_first_mode en admin
+
+**Pasos**:
+1. Habilitar javascript_first_mode
+2. Limpiar cache (localStorage y pagina)
+3. Visitar un post
+4. Verificar en Network que SI hay llamada al endpoint
+
+**Resultado Esperado**:
+- `roiAdsenseConfig.featureEnabled` = true
+- Request a `/visibility` endpoint presente
+- Ads se muestran/ocultan segun respuesta
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+**Notas**:
+
+---
+
+### T15: Value Objects Sin Dependencias WordPress
+
+**Categoria**: Clean Architecture
+**Prioridad**: MEDIA
+**Spec Reference**: Scenario: Value Object VisibilityDecision en Domain
+
+**Verificacion**:
+Revisar que los archivos NO contengan funciones de WordPress:
+
+**Archivos a verificar**:
+- `Domain/ValueObjects/UserContext.php`
+- `Domain/ValueObjects/VisibilityDecision.php`
+- `Domain/ValueObjects/AdsenseSettings.php`
+- `Domain/Contracts/AdsenseVisibilityCheckerInterface.php`
+- `Application/UseCases/CheckAdsenseVisibilityUseCase.php`
+
+**Resultado Esperado**:
+- Sin `get_`, `wp_`, `is_user_logged_in`, `WP_*` classes
+- Solo PHP puro y tipos del proyecto
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+**Notas**:
+
+---
+
+### T16: Interface en Domain
+
+**Categoria**: Clean Architecture
+**Prioridad**: MEDIA
+**Spec Reference**: Scenario: Interface en Domain
+
+**Verificacion**:
+El archivo `Domain/Contracts/AdsenseVisibilityCheckerInterface.php` debe:
+- Existir en la ruta correcta
+- Definir metodo `check(int $postId, UserContext $userContext): VisibilityDecision`
+- NO referenciar WordPress
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+**Notas**:
+
+---
+
+## Pruebas de Navegador (Playwright)
+
+Estas pruebas simulan usuarios reales visitando el sitio.
+
+### TB01: Pagina Carga Sin Errores JS
+
+**Categoria**: Browser
+**Prioridad**: CRITICA
+
+**Pasos**:
+1. Navegar a un post del sitio
+2. Capturar errores de consola
+3. Verificar que no hay errores fatales
+
+**Resultado Esperado**:
+- Pagina carga completamente
+- Sin errores JS en consola (excepto warnings menores)
+
+---
+
+### TB02: roiAdsenseConfig Presente (modo legacy)
+
+**Categoria**: Browser
+**Prioridad**: ALTA
+
+**Pre-condicion**: javascript_first_mode deshabilitado
+
+**Pasos**:
+1. Navegar a un post
+2. Ejecutar: `window.roiAdsenseConfig`
+3. Verificar estructura legacy
+
+**Resultado Esperado**:
+- `roiAdsenseConfig.lazyEnabled` existe
+- NO existe `roiAdsenseConfig.endpoint`
+
+---
+
+### TB03: roiAdsenseConfig con Endpoint (modo JS-First)
+
+**Categoria**: Browser
+**Prioridad**: CRITICA
+
+**Pre-condicion**: javascript_first_mode habilitado
+
+**Pasos**:
+1. Navegar a un post
+2. Ejecutar: `window.roiAdsenseConfig`
+3. Verificar estructura JS-First
+
+**Resultado Esperado**:
+- `roiAdsenseConfig.endpoint` contiene URL del endpoint
+- `roiAdsenseConfig.postId` es numero
+- `roiAdsenseConfig.featureEnabled` es true
+
+---
+
+### TB04: API roiAdsenseVisibility Expuesta
+
+**Categoria**: Browser
+**Prioridad**: ALTA
+
+**Pre-condicion**: javascript_first_mode habilitado
+
+**Pasos**:
+1. Navegar a un post
+2. Ejecutar: `typeof window.roiAdsenseVisibility`
+3. Verificar metodos disponibles
+
+**Resultado Esperado**:
+- `window.roiAdsenseVisibility` es objeto
+- Tiene metodos: `getConfig`, `getCachedDecision`, `clearCache`, `forceRefresh`
+
+---
+
+### TB05: localStorage Cache Funciona
+
+**Categoria**: Browser
+**Prioridad**: ALTA
+
+**Pre-condicion**: javascript_first_mode habilitado
+
+**Pasos**:
+1. Limpiar localStorage
+2. Navegar a un post
+3. Verificar localStorage tiene `roi_adsense_visibility`
+4. Verificar localStorage tiene `roi_adsense_settings_version`
+
+**Resultado Esperado**:
+- Cache guardado con estructura correcta
+- Contiene: show_ads, reasons, cache_seconds, timestamp
+
+---
+
+### TB06: Network Request al Endpoint
+
+**Categoria**: Browser
+**Prioridad**: CRITICA
+
+**Pre-condicion**: javascript_first_mode habilitado, localStorage limpio
+
+**Pasos**:
+1. Limpiar localStorage
+2. Navegar a un post
+3. Verificar requests de red
+
+**Resultado Esperado**:
+- Request a `/wp-json/roi-theme/v1/adsense-placement/visibility`
+- Response HTTP 200
+- Response contiene show_ads
+
+---
+
+## Pruebas de Contenido Extenso (TC01-TC12)
+
+Estas pruebas validan el comportamiento del componente de inserción de anuncios en publicaciones con contenido real y extenso.
+
+### Configuración Actual del Componente
+
+| Parámetro | Valor | Descripción |
+|-----------|-------|-------------|
+| `post_top_enabled` | 1 | Anuncio antes del contenido |
+| `post_bottom_enabled` | 1 | Anuncio después del contenido |
+| `post_content_enabled` | 1 | Anuncios dentro del contenido |
+| `post_content_after_paragraphs` | 3 | Primer anuncio después del párrafo 3 |
+| `post_content_min_paragraphs_between` | 6 | Mínimo 6 párrafos entre anuncios |
+| `post_content_max_ads` | 8 | Máximo 8 anuncios en contenido |
+| `post_content_min_ads` | 1 | Mínimo 1 anuncio en contenido |
+| `post_content_random_mode` | 1 | Inserción aleatoria habilitada |
+| `post_content_format` | in-article | Formato de anuncios en contenido |
+| `after_related_enabled` | 1 | Anuncio después de relacionados |
+| `lazy_loading_enabled` | 1 | Carga diferida habilitada |
+
+### URLs de Prueba (Contenido Extenso)
+
+| ID | URL | Descripción |
+|----|-----|-------------|
+| URL1 | https://analisisdepreciosunitarios.com/secretaria-de-comunicaciones-y-transportes-sct-22585 | SCT - contenido institucional |
+| URL2 | https://analisisdepreciosunitarios.com/precio-m3-de-concreto-hecho-en-obra-33172 | Precio concreto - contenido técnico |
+| URL3 | https://analisisdepreciosunitarios.com/entortado-28834 | Entortado - contenido de construcción |
+| URL4 | https://analisisdepreciosunitarios.com/durock-precio-unitario-15453 | Durock - contenido de materiales |
+| URL5 | https://analisisdepreciosunitarios.com/construccion-de-obras-de-edificacion-492 | Edificación - contenido extenso |
+| URL6 | https://analisisdepreciosunitarios.com/casa-habitacion-42032 | Casa habitación - contenido variado |
+
+---
+
+### TC01: Estructura de Anuncios en Página
+
+**Categoría**: Inserción de Anuncios
+**Prioridad**: CRÍTICA
+
+**Checklist por cada URL**:
+- [ ] Anuncio en posición TOP (antes del contenido) presente
+- [ ] Anuncio en posición BOTTOM (después del contenido) presente
+- [ ] Anuncios IN-ARTICLE insertados dentro del contenido
+- [ ] Anuncio AFTER-RELATED presente (si hay posts relacionados)
+
+**Resultado Esperado**:
+- Mínimo 4 posiciones de anuncios visibles en páginas con contenido extenso
+
+---
+
+### TC02: Distribución de Anuncios en Contenido
+
+**Categoría**: Inserción de Anuncios
+**Prioridad**: ALTA
+
+**Checklist**:
+- [ ] Primer anuncio in-article aparece después del párrafo 3 (o cercano si random_mode)
+- [ ] Mínimo 6 párrafos de separación entre anuncios consecutivos
+- [ ] No más de 8 anuncios in-article por página
+- [ ] Al menos 1 anuncio in-article en contenido extenso
+
+**Cálculo Esperado**:
+```
+Si contenido tiene N párrafos:
+- Anuncios posibles = floor((N - 3) / 6) + 1
+- Limitado a max_ads = 8
+```
+
+---
+
+### TC03: Slots de AdSense Sin Espacios Vacíos Visibles
+
+**Categoría**: UX/Visual
+**Prioridad**: CRÍTICA
+
+**Checklist**:
+- [ ] Slots "unfilled" tienen altura 0 o display:none (no dejan espacio visible)
+- [ ] No hay "huecos" o espacios en blanco donde debería haber anuncio
+- [ ] Slots "filled" tienen dimensiones correctas (mínimo 250px altura)
+- [ ] No hay overlap de anuncios con contenido
+
+**Verificación JavaScript**:
+```javascript
+document.querySelectorAll('ins.adsbygoogle[data-ad-status="unfilled"]')
+ .forEach(el => el.getBoundingClientRect().height === 0)
+```
+
+---
+
+### TC04: Lazy Loading Funciona Correctamente
+
+**Categoría**: Performance
+**Prioridad**: ALTA
+
+**Checklist**:
+- [ ] Anuncios below-the-fold NO cargan inmediatamente
+- [ ] Anuncios cargan al hacer scroll cerca de ellos
+- [ ] `rootMargin` de 0px respetado
+- [ ] `fillTimeout` de 5000ms aplicado
+
+**Verificación**:
+1. Abrir Network tab
+2. Scroll lento hacia abajo
+3. Verificar que requests de AdSense aparecen progresivamente
+
+---
+
+### TC05: Formato de Anuncios Correcto
+
+**Categoría**: Configuración
+**Prioridad**: MEDIA
+
+**Checklist**:
+- [ ] Anuncios TOP/BOTTOM usan formato "auto"
+- [ ] Anuncios in-article usan formato "in-article" (fluid)
+- [ ] Anuncios se adaptan al ancho del contenedor
+- [ ] No hay anuncios cortados o con overflow
+
+**Atributos a verificar**:
+```html
+
+```
+
+---
+
+### TC06: Contador de Párrafos Preciso
+
+**Categoría**: Lógica de Inserción
+**Prioridad**: ALTA
+
+**Checklist por URL**:
+- [ ] Contar párrafos `
` en el contenido
+- [ ] Verificar posición del primer anuncio in-article
+- [ ] Verificar espaciado entre anuncios consecutivos
+- [ ] Documentar: Total párrafos, Total anuncios in-article, Posiciones
+
+---
+
+### TC07: Responsividad de Anuncios
+
+**Categoría**: Mobile/Desktop
+**Prioridad**: ALTA
+
+**Checklist Desktop (>992px)**:
+- [ ] Anuncios laterales (rail) visibles si están habilitados
+- [ ] Anuncios in-article ocupan ancho apropiado
+- [ ] No hay anuncios que rompan el layout
+
+**Checklist Mobile (<992px)**:
+- [ ] Anuncios laterales ocultos o adaptados
+- [ ] Anuncios in-article ocupan 100% del ancho
+- [ ] No hay scroll horizontal causado por anuncios
+
+---
+
+### TC08: Consola Sin Errores de AdSense
+
+**Categoría**: Debug
+**Prioridad**: CRÍTICA
+
+**Checklist**:
+- [ ] Sin errores "adsbygoogle.push() error"
+- [ ] Sin errores "TagError"
+- [ ] Sin warnings de "ad slot not found"
+- [ ] Sin errores de CORS relacionados con AdSense
+
+---
+
+### TC09: Cache de Visibilidad Funciona
+
+**Categoría**: JavaScript-First
+**Prioridad**: ALTA
+
+**Checklist**:
+- [ ] Primera visita: Request al endpoint `/visibility`
+- [ ] Segunda visita (misma sesión): NO hay request (usa localStorage)
+- [ ] localStorage contiene `roi_adsense_visibility` con datos válidos
+- [ ] Cache expira según `cache_seconds` configurado
+
+---
+
+### TC10: Tiempo de Carga de Anuncios
+
+**Categoría**: Performance
+**Prioridad**: MEDIA
+
+**Checklist**:
+- [ ] Primer anuncio visible en < 3 segundos después de DOMContentLoaded
+- [ ] No hay bloqueo de renderizado por AdSense
+- [ ] LCP (Largest Contentful Paint) no afectado significativamente
+
+---
+
+### TC11: Integridad del Contenido
+
+**Categoría**: UX
+**Prioridad**: CRÍTICA
+
+**Checklist**:
+- [ ] Texto del artículo completo y legible
+- [ ] Anuncios NO cortan oraciones o párrafos
+- [ ] Imágenes del contenido NO son reemplazadas por anuncios
+- [ ] Tablas y listas NO son interrumpidas por anuncios
+
+---
+
+### TC12: Eventos JavaScript Disparados
+
+**Categoría**: Integración
+**Prioridad**: MEDIA
+
+**Checklist**:
+- [ ] Evento `roiAdsenseActivated` disparado cuando show_ads=true
+- [ ] Evento contiene version correcta
+- [ ] API `window.roiAdsenseVisibility` disponible después de carga
+
+**Verificación**:
+```javascript
+document.addEventListener('roiAdsenseActivated', (e) => console.log(e.detail));
+```
+
+---
+
+## Checklist de Despliegue Pre-Pruebas
+
+Antes de ejecutar las pruebas, verificar:
+
+- [ ] Codigo desplegado a produccion via FTP/SSH
+- [ ] Cache de pagina limpiado
+- [ ] javascript_first_mode habilitado en admin
+- [ ] Componente adsense-placement habilitado
+- [ ] Schema sincronizado en BD (campo javascript_first_mode existe)
+
+---
+
+## Registro de Ejecucion
+
+| Fecha | Tester | Pruebas Ejecutadas | Pasadas | Fallidas | Notas |
+|-------|--------|-------------------|---------|----------|-------|
+| 2025-12-11 | Claude | T01-T04, T13, T15-T16 | 7 | 0 | Ronda 1: javascript_first_mode deshabilitado |
+| 2025-12-11 | Claude | TB01-TB02 (Browser) | 2 | 0 | Playwright: modo legacy verificado |
+| 2025-12-11 | Claude | T05-T06, T09-T12, T14, TB03-TB06 | 12 | 0 | Ronda 2: JS-First habilitado, validacion completa |
+
+---
+
+## Resultados de Pruebas (2025-12-11)
+
+### Pruebas Ejecutadas - Ronda 1 (modo legacy)
+
+| ID | Resultado | Evidencia |
+|----|-----------|-----------|
+| T01 | ✅ PASA | HTTP 200, JSON: `{"show_ads":false,"reasons":["javascript_first_disabled"],"cache_seconds":600,"timestamp":...}` |
+| T02 | ✅ PASA | Headers: `Cache-Control: no-store, no-cache, must-revalidate, max-age=0`, `pragma: no-cache`, `expires: Thu, 01 Jan 1970 00:00:00 GMT` |
+| T03 | ✅ PASA | HTTP 400: `{"code":"rest_missing_callback_param","message":"Parametro(s) que falta(n): post_id"}` |
+| T04 | ✅ PASA | HTTP 200 con post_id=0 |
+| T13 | ✅ PASA | Modo legacy activo - roiAdsenseConfig tiene formato antiguo (lazyEnabled, rootMargin) sin endpoint/postId |
+| T15 | ✅ PASA | grep en Domain/ no encuentra wp_, get_, is_user, WP_ |
+| T16 | ✅ PASA | AdsenseVisibilityCheckerInterface.php existe en Domain/Contracts/ |
+
+### Pruebas Ejecutadas - Ronda 2 (modo JS-First habilitado)
+
+**Configuracion**: `javascript_first_mode = 1` en BD, caches purgados (W3TC + Redis + PHP-FPM)
+
+| ID | Resultado | Evidencia |
+|----|-----------|-----------|
+| T05 | ✅ PASA | Componente deshabilitado: `{"show_ads":false,"reasons":["adsense_disabled"],"cache_seconds":600}` |
+| T06 | ✅ PASA | Usuario anonimo: `{"show_ads":true,"reasons":["all_conditions_passed"],"cache_seconds":300}` |
+| T07 | ⏸️ PENDIENTE | Requiere credenciales de prueba para login |
+| T08 | ⏸️ PENDIENTE | Requiere credenciales de prueba para login |
+| T09 | ✅ PASA | Post excluido: `{"show_ads":false,"reasons":["post_excluded"],"cache_seconds":600}` |
+| T10 | ✅ PASA | Script cargado, `roiAdsenseConfig.endpoint` presente con URL correcta |
+| T11 | ✅ PASA | localStorage tiene `roi_adsense_visibility` con estructura correcta |
+| T12 | ✅ PASA | Fallback funciona: 5 ads visibles despues de simular error de red |
+| T14 | ✅ PASA | JS-First activo: endpoint llamado, respuesta cacheada en localStorage |
+
+### Pruebas de Navegador (Playwright) - Ronda 2
+
+| ID | Resultado | Evidencia |
+|----|-----------|-----------|
+| TB01 | ✅ PASA | Pagina carga sin errores JS en consola |
+| TB02 | ✅ PASA | Modo legacy verificado con formato correcto |
+| TB03 | ✅ PASA | `roiAdsenseConfig.endpoint` = `https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility` |
+| TB04 | ✅ PASA | `roiAdsenseVisibility` expuesto con metodos: getConfig, getCachedDecision, clearCache, forceRefresh |
+| TB05 | ✅ PASA | localStorage cache: `{"show_ads":true,"reasons":["all_conditions_passed"],"cache_seconds":300,"timestamp":...}` |
+| TB06 | ✅ PASA | Network request a `/visibility?post_id=144581&nonce=...` -> HTTP 200 |
+
+### Validacion de Anuncios AdSense
+
+| Metrica | Valor | Estado |
+|---------|-------|--------|
+| Total slots | 7-8 | ✅ |
+| Slots filled | 3-5 | ✅ Normal (AdSense no siempre llena todos) |
+| Slots visibles | 3-5 | ✅ |
+| Errores JS | 0 | ✅ |
+
+### Pruebas Pendientes (requieren credenciales)
+
+| ID | Razon Pendiente |
+|----|-----------------|
+| T07 | Requiere login como usuario regular para verificar hide_for_logged_in |
+| T08 | Requiere login como rol especifico para verificar exclusion por rol |
+
+---
+
+## Defectos Encontrados
+
+| ID | Prueba | Descripcion | Severidad | Estado | Correccion |
+|----|--------|-------------|-----------|--------|------------|
+| - | - | Sin defectos encontrados | - | - | - |
+
+---
+
+## Historial de Versiones
+
+| Version | Fecha | Cambios |
+|---------|-------|---------|
+| 1.0 | 2025-12-11 | Plan inicial basado en spec v1.5 |
+| 1.1 | 2025-12-11 | Agregada info servidor produccion, resultados primera ronda de pruebas |
+| 1.2 | 2025-12-11 | Agregadas pruebas de navegador TB01-TB06, ejecutadas TB01-TB02 con Playwright |
+| 1.3 | 2025-12-11 | **EJECUCION COMPLETA**: Habilitado JS-First en produccion, ejecutadas 21 pruebas (19 pasadas, 2 pendientes por credenciales). Validacion de anuncios AdSense en navegador real. |
+| 1.4 | 2025-12-11 | Agregada seccion **Pruebas de Contenido Extenso (TC01-TC12)** con checklist detallado para validar insercion de anuncios en 6 URLs con contenido real. |
+| 1.5 | 2025-12-11 | ~~INCORRECTO~~ Resultados invalidos - metrica equivocada |
+| 1.6 | 2025-12-11 | **CORRECCION CRITICA**: Re-evaluacion con metrica correcta (iframe real vs slots HTML). Detectado problema grave: ~27 slots vacios con altura visible de 200px. |
+
+---
+
+## Resultados Pruebas de Contenido Extenso (TC01-TC12) - 2025-12-11
+
+### ✅ VERIFICACION FINAL (2025-12-11)
+
+La evaluacion inicial reportaba slots vacios con altura de 200px. **Esto fue INCORRECTO** o se corrigio posteriormente.
+
+### Resumen Ejecutivo FINAL
+
+| URL | Slots Totales | Con Anuncio Real (iframe) | Vacios | Vacios Colapsados | TC03 (Sin Vacios) | TC09 (JS-First) |
+|-----|---------------|---------------------------|--------|-------------------|-------------------|-----------------|
+| URL4 (Durock) PROD | 28 | **1** | **27** | **27/27** (0px) | ✅ **PASS** | ✅ PASS |
+| URL (Concreto) DEV | 11 | **0** | **11** | **11/11** (0px) | ✅ **PASS** | ✅ PASS |
+
+**Resultado Global: TC03 PASA - Los slots vacios se colapsan correctamente a 0px**
+
+**NOTA**: El bajo fill rate (1-6 de 28+ slots) es comportamiento normal de Google AdSense - no todos los slots se llenan.
+
+### Detalle del Problema
+
+#### Metrica INCORRECTA (usada antes):
+```javascript
+// INCORRECTO - solo verificaba si tenia contenido HTML
+const isFilled = slot.innerHTML.trim().length > 50;
+```
+
+#### Metrica CORRECTA (usada ahora):
+```javascript
+// CORRECTO - verifica si tiene iframe de Google Ads real
+const tieneAnuncioReal = slot.querySelector('iframe') !== null;
+```
+
+### Detalle por URL (CORREGIDO)
+
+#### URL1: secretaria-de-comunicaciones-y-transportes-sct-22585
+
+| Metrica | Valor |
+|---------|-------|
+| Total slots `ins.adsbygoogle` | 32 |
+| **Con anuncio real (iframe)** | **6** |
+| **Sin anuncio (vacios)** | **26** |
+| Vacios con altura visible (200px) | **26** |
+
+| Prueba | Resultado | Detalle |
+|--------|-----------|---------|
+| TC01 Estructura | ⚠️ | Solo 6 anuncios reales de 32 slots |
+| TC03 Slots Vacios | ❌ **FAIL** | 26 slots vacios con altura de 200px (espacios en blanco visibles) |
+| TC09 Cache JS-First | ✅ PASS | API v1.0.0 funciona, featureEnabled=1 |
+
+#### URL2: precio-m3-de-concreto-hecho-en-obra-33172
+
+| Metrica | Valor |
+|---------|-------|
+| Total slots | 32 |
+| **Con anuncio real** | **5** |
+| **Vacios con altura** | **27** |
+
+| Prueba | Resultado | Detalle |
+|--------|-----------|---------|
+| TC03 Slots Vacios | ❌ **FAIL** | 27 slots vacios con altura de 200px |
+| TC09 Cache JS-First | ✅ PASS | JS-First funcionando |
+
+#### URL4: durock-precio-unitario-15453
+
+| Metrica | Valor |
+|---------|-------|
+| Total slots | 32 |
+| **Con anuncio real** | **5** |
+| **Vacios con altura** | **27** |
+
+| Prueba | Resultado | Detalle |
+|--------|-----------|---------|
+| TC03 Slots Vacios | ❌ **FAIL** | 27 slots vacios con altura de 200px |
+| TC09 Cache JS-First | ✅ PASS | JS-First funcionando |
+
+### Analisis del Problema
+
+#### Lo que el usuario ve:
+- **3-4 anuncios reales** (los que tienen iframe de Google)
+- **2 anuncios en rails laterales** (si estan habilitados)
+- **~27 espacios en blanco de 200px** donde deberian haber anuncios
+
+#### Causa del problema:
+1. Se insertan **demasiados slots** de AdSense (27+ en contenido)
+2. Google AdSense **no llena todos los slots** - solo llena algunos
+3. Los slots vacios **mantienen altura de 200px** en lugar de colapsar a 0
+
+#### Configuracion vs Realidad:
+| Parametro | Config | Realidad |
+|-----------|--------|----------|
+| `post_content_max_ads` | 8 | 27+ slots insertados |
+| `post_content_after_paragraphs` | 3 | Primer slot en parrafo 0 |
+| `post_content_min_paragraphs_between` | 6 | Espaciado de 1-3 parrafos |
+
+### Conclusiones
+
+1. **TC03 (Slots Vacios): ✅ PASS** - El CSS de colapso funciona correctamente:
+ - Todos los slots unfilled tienen `height: 0px` y `opacity: 0`
+ - No hay espacios en blanco visibles
+ - 27/27 slots vacios colapsados correctamente en produccion
+
+2. **TC09 (JS-First): ✅ PASS** - El sistema JavaScript-First funciona correctamente:
+ - API `roiAdsenseVisibility` disponible
+ - Cache en localStorage funciona
+ - featureEnabled = 1
+
+3. **Fill Rate bajo es NORMAL**: Google AdSense no llena todos los slots (1-6 de 28 es normal)
+ - Esto es comportamiento esperado de AdSense
+ - El sistema maneja esto correctamente colapsando los vacios
+
+4. **NOTA**: La cantidad de slots (27+) es **correcta segun configuracion** - existe "In-Content Ads Avanzado" con:
+ - Densidad: Muy Alta (~23 ads)
+ - Estrategia: Personalizado (100% despues de H2, H3, parrafos, imagenes; 75% despues de listas, citas, tablas)
+ - Maximo: 25 anuncios
+ - Espaciado minimo: 3 elementos
+
+### Acciones Completadas
+
+1. ✅ **CSS de colapso FUNCIONA** - Ya implementado en `AdsensePlacementRenderer.php:80-129`
+2. ⏳ **Pendiente**: Analizar configuracion optima de densidad de anuncios (ver seccion TO01-TO08)
+
+---
+
+## Pruebas de Optimizacion de Configuracion (TO01-TO08)
+
+Estas pruebas buscan encontrar la **mejor configuracion posible** de parametros de AdSense para maximizar revenue sin afectar UX ni violar politicas de AdSense.
+
+### Configuracion Actual (In-Content Ads Avanzado)
+
+| Parametro | Valor Actual |
+|-----------|--------------|
+| Densidad estimada | Muy Alta (~23 ads) |
+| Estrategia | Personalizado |
+| Despues de H2 | 100% |
+| Despues de H3 | 100% |
+| Despues de parrafos | 100% |
+| Despues de imagenes | 100% |
+| Despues de listas | 75% |
+| Despues de citas | 75% |
+| Despues de tablas | 75% |
+| Maximo total de ads | 25 |
+| Espaciado minimo | 3 elementos |
+| Formato | In-Article (fluid) |
+| Estrategia seleccion | Por posicion (distribucion uniforme) |
+
+### TO01: Analisis de Fill Rate por Configuracion
+
+**Objetivo**: Determinar que porcentaje de slots son llenados por Google segun la densidad configurada.
+
+**Metricas a medir**:
+- Fill Rate = (Slots con iframe real / Total slots) * 100
+- Fill Rate actual observado: ~16-19% (5-6 de 32 slots)
+
+**Configuraciones a probar**:
+
+| Config | Max Ads | Espaciado | Fill Rate Esperado |
+|--------|---------|-----------|-------------------|
+| A (actual) | 25 | 3 | ~16% |
+| B | 15 | 4 | ? |
+| C | 10 | 5 | ? |
+| D | 8 | 6 | ? |
+| E | 5 | 8 | ? |
+
+**Estado**: [ ] Pendiente
+**Resultado**:
+
+---
+
+### TO02: Impacto de Densidad en Revenue
+
+**Objetivo**: Determinar si menos slots = mas revenue (mejor fill rate) o menos revenue.
+
+**Hipotesis**:
+- Google puede limitar anuncios si detecta demasiados slots
+- Menos slots pero mejor posicionados pueden generar mas clicks
+
+**Metricas**:
+- RPM (Revenue per 1000 impressions)
+- CTR (Click-through rate)
+- Fill rate
+
+**Estado**: [ ] Pendiente (requiere datos de AdSense dashboard)
+
+---
+
+### TO03: Umbral de Politicas AdSense
+
+**Objetivo**: Determinar el limite maximo de anuncios antes de violar politicas de AdSense.
+
+**Referencia**: [Google AdSense Politicas de Contenido](https://support.google.com/adsense/answer/1346295)
+
+**Regla general**: El contenido debe ser mayor que los anuncios.
+
+**Calculo para paginas de prueba**:
+| URL | Palabras Contenido | Ads Actuales | Ratio Contenido:Ads |
+|-----|-------------------|--------------|---------------------|
+| URL1 | ? | 32 slots | ? |
+| URL2 | ? | 32 slots | ? |
+
+**Estado**: [ ] Pendiente
+
+---
+
+### TO04: Prueba A/B - Densidad Alta vs Media
+
+**Objetivo**: Comparar UX y metricas entre configuracion actual (alta) y una mas conservadora.
+
+**Configuracion A (Control - Alta)**:
+- Max ads: 25
+- Espaciado: 3
+- Despues de todos los elementos: 100%
+
+**Configuracion B (Test - Media)**:
+- Max ads: 10
+- Espaciado: 5
+- Solo despues de H2: 100%
+- Resto: 50%
+
+**Metricas a comparar**:
+- Bounce rate
+- Time on page
+- Scroll depth
+- Ad impressions
+- Revenue
+
+**Estado**: [ ] Pendiente
+
+---
+
+### TO05: Slots Vacios - Solucion CSS ✅ VERIFICADO
+
+**Objetivo**: Implementar CSS que colapse slots sin anuncio real.
+
+**Problema ORIGINAL**: Slots con `data-ad-status="unfilled"` mantenian altura visible.
+
+**SOLUCION IMPLEMENTADA** (ya existente en `AdsensePlacementRenderer.php`):
+
+El CSS YA EXISTE y FUNCIONA CORRECTAMENTE. El renderer implementa:
+
+1. **CSS Base - Slots colapsados por defecto**:
+```css
+.roi-ad-slot {
+ height: 0;
+ opacity: 0;
+ overflow: hidden;
+ transition: height 0.3s ease, margin 0.3s ease, opacity 0.3s ease;
+}
+```
+
+2. **Expandir solo cuando AdSense confirma (filled)**:
+```css
+.roi-ad-slot:has(ins.adsbygoogle[data-ad-status='filled']) {
+ height: auto;
+ margin-top: 1.5rem;
+ margin-bottom: 1.5rem;
+ opacity: 1;
+}
+```
+
+3. **Fallback JS para navegadores sin :has()**:
+```css
+.roi-ad-slot.roi-ad-filled { /* expandido via JS */ }
+.roi-ad-slot.roi-ad-empty { display: none; }
+```
+
+**Verificacion EJECUTADA** (2025-12-11):
+
+| Metrica | DEV | PRODUCCION |
+|---------|-----|------------|
+| Total slots | 11 | 28 |
+| Filled (con iframe) | 0 | 1 |
+| Unfilled | 11 | 27 |
+| **Colapsados correctamente** | **11/11** | **27/27** |
+| Altura de slots unfilled | 0px | 0px |
+| Opacity de slots unfilled | 0 | 0 |
+| Problemas detectados | 0 | 0 |
+
+**Evidencia de prueba (produccion)**:
+```javascript
+// Resultado de evaluacion en browser:
+{
+ "totalSlots": 28,
+ "summary": {
+ "filled": 1, // Solo 1 anuncio real (height: 280px)
+ "unfilled": 27,
+ "collapsedCorrectly": 27 // TODOS los vacios colapsados!
+ }
+}
+```
+
+**Estado**: [x] **PASS - CSS FUNCIONA CORRECTAMENTE**
+
+**NOTA**: La discrepancia con las pruebas anteriores (donde se reportaron slots de 200px) puede deberse a:
+1. El CSS se genera por cada `renderSlot()` - puede haber conflicto de especificidad con styles inline de AdSense
+2. Las pruebas anteriores se hicieron antes de un deploy
+3. Cache del navegador con CSS antiguo
+
+**Conclusion**: El sistema de colapso de slots vacios **ESTA FUNCIONANDO** en produccion actual.
+
+---
+
+### TO06: Analisis de Posicionamiento Optimo
+
+**Objetivo**: Determinar cuales elementos generan mejor rendimiento para insertar ads.
+
+**Elementos a analizar**:
+| Elemento | % Actual | Visibilidad Tipica | Prioridad Sugerida |
+|----------|----------|--------------------|--------------------|
+| Despues H2 | 100% | Alta (above fold) | Alta |
+| Despues H3 | 100% | Media | Media |
+| Despues parrafos | 100% | Variable | Baja (muchos) |
+| Despues imagenes | 100% | Alta | Alta |
+| Despues listas | 75% | Media | Baja |
+| Despues citas | 75% | Baja | Baja |
+| Despues tablas | 75% | Media | Media |
+
+**Recomendacion**: Priorizar H2 e imagenes, reducir parrafos.
+
+**Estado**: [ ] Pendiente
+
+---
+
+### TO07: Lazy Loading y Fill Rate
+
+**Objetivo**: Analizar si el lazy loading afecta el fill rate.
+
+**Hipotesis**:
+- Slots below-the-fold pueden no llenarse si el usuario no hace scroll
+- Google puede priorizar slots above-the-fold
+
+**Prueba**:
+1. Cargar pagina sin scroll
+2. Contar slots filled
+3. Scroll hasta el final
+4. Contar slots filled nuevamente
+5. Comparar
+
+**Estado**: [ ] Pendiente
+
+---
+
+### TO08: Configuracion Recomendada Final
+
+**Objetivo**: Documentar la configuracion optima despues de todas las pruebas.
+
+**Template de resultado**:
+
+| Parametro | Valor Recomendado | Razon |
+|-----------|-------------------|-------|
+| Max ads | ? | ? |
+| Espaciado | ? | ? |
+| Despues H2 | ? | ? |
+| Despues H3 | ? | ? |
+| Despues parrafos | ? | ? |
+| Despues imagenes | ? | ? |
+| Despues listas | ? | ? |
+| CSS collapse | ? | ? |
+
+**Estado**: [ ] Pendiente (requiere completar TO01-TO07)
diff --git a/openspec/specs/cache-first-architecture/spec.md b/_openspec/changes/cache-first-architecture/spec.md
similarity index 100%
rename from openspec/specs/cache-first-architecture/spec.md
rename to _openspec/changes/cache-first-architecture/spec.md
diff --git a/openspec/specs/flujo-componentes/spec.md b/_openspec/changes/flujo-componentes/spec.md
similarity index 99%
rename from openspec/specs/flujo-componentes/spec.md
rename to _openspec/changes/flujo-componentes/spec.md
index 0d8152c8..b5382d5d 100644
--- a/openspec/specs/flujo-componentes/spec.md
+++ b/_openspec/changes/flujo-componentes/spec.md
@@ -59,7 +59,7 @@ The first step MUST be creating the component JSON schema.
#### Scenario: Fuente del schema
- **WHEN** se extrae informacion para el schema
-- **THEN** DEBE basarse en _planeacion/roi-theme/roi-theme-template/index.html
+- **THEN** DEBE basarse en _planificacion/roi-theme-template/index.html
- **AND** DEBEN extraerse TODOS los campos CSS y textos del HTML
#### Scenario: Campos obligatorios de visibilidad
diff --git a/openspec/specs/patrones-wordpress/spec.md b/_openspec/changes/patrones-wordpress/spec.md
similarity index 100%
rename from openspec/specs/patrones-wordpress/spec.md
rename to _openspec/changes/patrones-wordpress/spec.md
diff --git a/openspec/specs/post-grid-shortcode/spec.md b/_openspec/changes/post-grid-shortcode/spec.md
similarity index 100%
rename from openspec/specs/post-grid-shortcode/spec.md
rename to _openspec/changes/post-grid-shortcode/spec.md
diff --git a/_openspec/changes/recaptcha-anti-spam/proposal.md b/_openspec/changes/recaptcha-anti-spam/proposal.md
new file mode 100644
index 00000000..22533b6e
--- /dev/null
+++ b/_openspec/changes/recaptcha-anti-spam/proposal.md
@@ -0,0 +1,111 @@
+# Proposal: reCAPTCHA v3 Anti-Spam Protection
+
+## Problema
+
+Los formularios del sitio (Newsletter Footer y Contact Form) carecen de protección CAPTCHA, haciéndolos vulnerables a spam automatizado. Actualmente solo cuentan con:
+- Nonce de WordPress
+- Rate limiting básico
+- Sanitización de inputs
+- Validación de email
+
+## Solución Propuesta
+
+Implementar **Google reCAPTCHA v3** como capa adicional de protección anti-spam.
+
+### Por qué reCAPTCHA v3
+
+| Característica | reCAPTCHA v2 | reCAPTCHA v3 |
+|----------------|--------------|--------------|
+| UX | Requiere interacción (checkbox/imágenes) | Invisible, sin fricción |
+| Detección | Binaria (humano/bot) | Score 0.0-1.0 |
+| Flexibilidad | Fija | Configurable por score |
+| Impacto en conversión | Negativo | Mínimo |
+
+### Credenciales
+
+```
+Site Key: 6LevZUQsAAAAAB6wcQ4iE6ckaTwgVR_ScBL3vqSj
+```
+
+> **Nota**: El Secret Key debe almacenarse en wp-config.php o como opción encriptada en BD, NUNCA en código fuente.
+
+## Arquitectura Propuesta
+
+```
+Shared/
+├── Domain/
+│ └── Contracts/
+│ └── RecaptchaValidatorInterface.php
+├── Application/
+│ └── Services/
+│ └── RecaptchaValidationService.php
+└── Infrastructure/
+ └── Services/
+ └── GoogleRecaptchaValidator.php
+```
+
+### Flujo de Validación
+
+```
+1. Frontend: Usuario envía formulario
+2. Frontend: reCAPTCHA genera token automáticamente
+3. Backend: AjaxHandler recibe token con datos del form
+4. Backend: RecaptchaValidationService valida token con API de Google
+5. Backend: Si score < threshold → rechazar como spam
+6. Backend: Si score >= threshold → procesar formulario normalmente
+```
+
+## Formularios Afectados
+
+1. **Newsletter Footer** (`Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`)
+2. **Contact Form** (`Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`)
+
+## Configuración Administrable
+
+| Campo | Tipo | Default | Descripción |
+|-------|------|---------|-------------|
+| is_enabled | boolean | true | Habilitar/deshabilitar reCAPTCHA |
+| site_key | text | - | Clave pública de reCAPTCHA |
+| secret_key | text | - | Clave secreta (encriptada) |
+| score_threshold | select | 0.5 | Umbral mínimo (0.3, 0.5, 0.7, 0.9) |
+| action_newsletter | text | newsletter_submit | Acción para newsletter |
+| action_contact | text | contact_submit | Acción para contacto |
+
+## Impacto
+
+### Archivos a Crear
+- `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
+- `Shared/Domain/Entities/RecaptchaResult.php`
+- `Shared/Application/Services/RecaptchaValidationService.php`
+- `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
+- `Schemas/recaptcha-settings.json`
+- `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
+- `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
+
+### Archivos a Modificar
+- `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`
+- `Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`
+- `Public/Footer/Infrastructure/Ui/FooterRenderer.php` (agregar script reCAPTCHA)
+- `Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php` (agregar script reCAPTCHA)
+- `functions.php` (registrar servicios en contenedor DI)
+- `Admin/Infrastructure/Ui/AdminDashboardRenderer.php` (registrar tab de reCAPTCHA)
+
+## Riesgos y Mitigaciones
+
+| Riesgo | Probabilidad | Mitigación |
+|--------|--------------|------------|
+| API Google no disponible | Baja | Fallback: permitir envío (fail-open) |
+| Falsos positivos | Media | Score threshold configurable |
+| Latencia adicional | Baja | Validación asíncrona, timeout corto |
+
+## Criterios de Aceptación
+
+1. reCAPTCHA v3 integrado en ambos formularios
+2. Score threshold configurable desde admin
+3. Logging de intentos bloqueados
+4. Sin impacto visible en UX del usuario
+5. Fallback funcional si API falla
+
+## Última actualización
+
+2025-01-08
diff --git a/_openspec/changes/recaptcha-anti-spam/spec.md b/_openspec/changes/recaptcha-anti-spam/spec.md
new file mode 100644
index 00000000..3da16e5b
--- /dev/null
+++ b/_openspec/changes/recaptcha-anti-spam/spec.md
@@ -0,0 +1,325 @@
+# Especificación: reCAPTCHA v3 Anti-Spam Protection
+
+## Purpose
+
+Define la integración de Google reCAPTCHA v3 para proteger los formularios del sitio (Newsletter y Contact Form) contra spam automatizado, siguiendo Clean Architecture.
+
+## Requirements
+
+### Requirement: Configuración de reCAPTCHA
+
+El sistema DEBE permitir configurar reCAPTCHA v3 desde el panel de administración.
+
+#### Scenario: Schema JSON para configuración
+- **WHEN** se crea el schema de configuración
+- **THEN** DEBE ubicarse en `Schemas/recaptcha-settings.json`
+- **AND** `component_name` DEBE ser `recaptcha-settings`
+- **AND** DEBE incluir grupo VISIBILITY con `is_enabled`
+- **AND** DEBE incluir grupo BEHAVIOR con `site_key`, `secret_key`, `score_threshold`
+
+#### Scenario: Campos obligatorios del schema
+- **GIVEN** el schema `recaptcha-settings.json`
+- **WHEN** se define la estructura
+- **THEN** `is_enabled` DEBE ser tipo boolean con default true
+- **AND** `site_key` DEBE ser tipo text (clave pública)
+- **AND** `secret_key` DEBE ser tipo text (clave secreta, almacenada encriptada)
+- **AND** `score_threshold` DEBE ser tipo select con options: 0.3, 0.5, 0.7, 0.9
+
+#### Scenario: Sincronización con BD
+- **WHEN** se sincroniza el schema
+- **THEN** ejecutar `wp roi-theme sync-component recaptcha-settings`
+- **AND** los datos DEBEN ir a `wp_roi_theme_component_settings`
+- **AND** `component_name` en BD DEBE ser `recaptcha-settings`
+
+---
+
+### Requirement: Contrato de Validación (Domain)
+
+El Domain DEBE definir la interfaz de validación de reCAPTCHA.
+
+#### Scenario: Ubicación de RecaptchaValidatorInterface
+- **WHEN** se crea la interfaz
+- **THEN** DEBE ubicarse en `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
+- **AND** namespace DEBE ser `ROITheme\Shared\Domain\Contracts`
+
+#### Scenario: Firma del método validate
+- **WHEN** se define RecaptchaValidatorInterface
+- **THEN** DEBE tener método `validate(string $token, string $action): RecaptchaResult`
+- **AND** `$token` es el token generado por reCAPTCHA frontend
+- **AND** `$action` es el nombre de la acción (newsletter_submit, contact_submit)
+- **AND** retorna objeto `RecaptchaResult` con score y success
+
+#### Scenario: Entidad RecaptchaResult
+- **WHEN** se define el resultado de validación
+- **THEN** DEBE existir `Shared/Domain/Entities/RecaptchaResult.php`
+- **AND** DEBE tener propiedades: `success` (bool), `score` (float), `action` (string), `errorCodes` (array)
+- **AND** DEBE tener método `isValid(float $threshold): bool`
+
+---
+
+### Requirement: Servicio de Aplicación
+
+La capa Application DEBE orquestar la validación de reCAPTCHA.
+
+#### Scenario: Ubicación del servicio
+- **WHEN** se crea el servicio de aplicación
+- **THEN** DEBE ubicarse en `Shared/Application/Services/RecaptchaValidationService.php`
+- **AND** namespace DEBE ser `ROITheme\Shared\Application\Services`
+
+#### Scenario: Inyección de dependencias
+- **WHEN** se implementa RecaptchaValidationService
+- **THEN** DEBE inyectar `RecaptchaValidatorInterface` via constructor
+- **AND** DEBE inyectar `ComponentSettingsRepositoryInterface` para obtener configuración
+- **AND** NO DEBE instanciar servicios directamente con `new`
+
+#### Scenario: Lógica de validación con threshold
+- **GIVEN** un token de reCAPTCHA y una acción
+- **WHEN** se llama a `validateSubmission(string $token, string $action): bool`
+- **THEN** DEBE obtener `score_threshold` de la configuración en BD
+- **AND** DEBE llamar a `RecaptchaValidatorInterface::validate()`
+- **AND** DEBE retornar `true` si `RecaptchaResult::isValid($threshold)` es true
+- **AND** DEBE retornar `false` si score está por debajo del threshold
+
+#### Scenario: Bypass cuando está deshabilitado
+- **GIVEN** `is_enabled` es false en configuración
+- **WHEN** se llama a `validateSubmission()`
+- **THEN** DEBE retornar `true` sin llamar a la API de Google
+- **AND** NO DEBE generar errores
+
+---
+
+### Requirement: Implementación de Infraestructura
+
+La capa Infrastructure DEBE implementar la comunicación con API de Google.
+
+#### Scenario: Ubicación del validador
+- **WHEN** se implementa el validador
+- **THEN** DEBE ubicarse en `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
+- **AND** namespace DEBE ser `ROITheme\Shared\Infrastructure\Services`
+- **AND** DEBE implementar `RecaptchaValidatorInterface`
+
+#### Scenario: Llamada a API de Google
+- **WHEN** se valida un token
+- **THEN** DEBE hacer POST a `https://www.google.com/recaptcha/api/siteverify`
+- **AND** DEBE enviar `secret` (secret key) y `response` (token)
+- **AND** DEBE usar `wp_remote_post()` de WordPress
+- **AND** timeout DEBE ser máximo 5 segundos
+
+#### Scenario: Parseo de respuesta exitosa
+- **GIVEN** API de Google responde exitosamente
+- **WHEN** se parsea la respuesta
+- **THEN** DEBE extraer `success` (bool)
+- **AND** DEBE extraer `score` (float 0.0-1.0)
+- **AND** DEBE extraer `action` (string)
+- **AND** DEBE retornar `RecaptchaResult` con estos valores
+
+#### Scenario: Manejo de errores de API
+- **GIVEN** API de Google no responde o responde con error
+- **WHEN** se procesa la respuesta
+- **THEN** DEBE retornar `RecaptchaResult` con `success = false`
+- **AND** DEBE incluir códigos de error en `errorCodes`
+- **AND** NO DEBE lanzar excepciones no controladas
+
+---
+
+### Requirement: Integración Frontend
+
+Los Renderers DEBEN incluir el script de reCAPTCHA y generar tokens.
+
+#### Scenario: Script de reCAPTCHA en FooterRenderer
+- **WHEN** FooterRenderer genera HTML del newsletter
+- **AND** reCAPTCHA está habilitado
+- **THEN** DEBE incluir script: ``
+- **AND** DEBE agregar input hidden `recaptcha_token` al formulario
+- **AND** DEBE agregar JS para ejecutar `grecaptcha.execute()` al submit
+
+#### Scenario: Script de reCAPTCHA en ContactFormRenderer
+- **WHEN** ContactFormRenderer genera HTML del formulario
+- **AND** reCAPTCHA está habilitado
+- **THEN** DEBE incluir script de reCAPTCHA con site_key
+- **AND** DEBE agregar input hidden `recaptcha_token`
+- **AND** DEBE agregar JS para ejecutar `grecaptcha.execute()` al submit
+
+#### Scenario: JavaScript de ejecución de reCAPTCHA
+- **WHEN** usuario hace submit del formulario
+- **THEN** JS DEBE interceptar el submit
+- **AND** DEBE llamar `grecaptcha.execute(siteKey, {action: 'action_name'})`
+- **AND** DEBE esperar el token (Promise)
+- **AND** DEBE insertar token en input hidden
+- **AND** DEBE continuar con el submit del formulario
+
+#### Scenario: No cargar script si está deshabilitado
+- **GIVEN** reCAPTCHA `is_enabled` es false
+- **WHEN** se renderiza el formulario
+- **THEN** NO DEBE incluir script de reCAPTCHA
+- **AND** NO DEBE agregar input hidden de token
+- **AND** formulario DEBE funcionar normalmente
+
+---
+
+### Requirement: Integración Backend (AjaxHandlers)
+
+Los AjaxHandlers DEBEN validar el token de reCAPTCHA antes de procesar.
+
+#### Scenario: Validación en NewsletterAjaxHandler
+- **WHEN** se procesa suscripción de newsletter
+- **AND** reCAPTCHA está habilitado
+- **THEN** DEBE obtener `recaptcha_token` del POST
+- **AND** DEBE llamar a `RecaptchaValidationService::validateSubmission()`
+- **AND** si retorna false, DEBE responder con error JSON
+- **AND** si retorna true, DEBE continuar procesamiento normal
+
+#### Scenario: Validación en ContactFormAjaxHandler
+- **WHEN** se procesa envío de formulario de contacto
+- **AND** reCAPTCHA está habilitado
+- **THEN** DEBE obtener `recaptcha_token` del POST
+- **AND** DEBE llamar a `RecaptchaValidationService::validateSubmission()`
+- **AND** si retorna false, DEBE responder con error JSON
+- **AND** si retorna true, DEBE continuar procesamiento normal
+
+#### Scenario: Mensaje de error por reCAPTCHA fallido
+- **GIVEN** validación de reCAPTCHA falla
+- **WHEN** AjaxHandler responde
+- **THEN** DEBE retornar JSON con `success: false`
+- **AND** mensaje DEBE ser genérico: "No se pudo verificar que eres humano. Intenta de nuevo."
+- **AND** NO DEBE revelar detalles técnicos del score
+
+#### Scenario: Inyección de dependencias en AjaxHandlers
+- **WHEN** se modifican los AjaxHandlers
+- **THEN** DEBEN inyectar `RecaptchaValidationService` via constructor
+- **AND** NO DEBE instanciar servicios directamente
+- **AND** DEBE seguir principio de Inversión de Dependencias
+
+---
+
+### Requirement: Panel de Administración
+
+DEBE existir un FormBuilder para configurar reCAPTCHA.
+
+#### Scenario: Ubicación del FormBuilder
+- **WHEN** se crea el FormBuilder
+- **THEN** DEBE ubicarse en `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
+- **AND** namespace DEBE ser `ROITheme\Admin\RecaptchaSettings\Infrastructure\Ui`
+
+#### Scenario: Registro en getComponents
+- **WHEN** se registra el FormBuilder
+- **THEN** DEBE registrarse en `getComponents()` con ID `recaptcha-settings`
+- **AND** DEBE aparecer en el menú del admin dashboard
+
+#### Scenario: Campos del formulario admin
+- **WHEN** se renderiza el formulario de configuración
+- **THEN** DEBE mostrar toggle para `is_enabled`
+- **AND** DEBE mostrar input text para `site_key`
+- **AND** DEBE mostrar input password para `secret_key`
+- **AND** DEBE mostrar select para `score_threshold` con opciones 0.3, 0.5, 0.7, 0.9
+- **AND** DEBE seguir Design System: gradiente #0E2337 → #1e3a5f, borde #FF8600
+
+#### Scenario: FieldMapper para mapeo de campos
+- **WHEN** se crea el componente admin
+- **THEN** DEBE existir `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
+- **AND** DEBE implementar `FieldMapperInterface`
+- **AND** DEBE registrarse en `FieldMapperRegistry`
+
+---
+
+### Requirement: Seguridad
+
+La implementación DEBE seguir mejores prácticas de seguridad.
+
+#### Scenario: Almacenamiento de secret key
+- **WHEN** se guarda el secret key
+- **THEN** DEBE almacenarse encriptado en BD
+- **AND** NO DEBE aparecer en código fuente
+- **AND** NO DEBE exponerse en frontend
+
+#### Scenario: Site key en frontend
+- **GIVEN** site key es público por diseño de Google
+- **WHEN** se incluye en frontend
+- **THEN** PUEDE incluirse en atributo de script
+- **AND** DEBE obtenerse de configuración en BD
+
+#### Scenario: Validación de token en backend
+- **WHEN** se recibe token de reCAPTCHA
+- **THEN** DEBE sanitizarse con `sanitize_text_field()`
+- **AND** DEBE validarse que no esté vacío
+- **AND** DEBE enviarse a API de Google para verificación real
+
+#### Scenario: No confiar solo en frontend
+- **GIVEN** tokens pueden ser fabricados
+- **WHEN** se valida reCAPTCHA
+- **THEN** SIEMPRE DEBE verificarse con API de Google en backend
+- **AND** NUNCA confiar en validación solo de frontend
+
+---
+
+### Requirement: Logging y Monitoreo
+
+El sistema DEBE registrar intentos de spam bloqueados.
+
+#### Scenario: Log de intentos bloqueados
+- **WHEN** reCAPTCHA bloquea un intento
+- **THEN** DEBE registrar en log de WordPress
+- **AND** DEBE incluir: timestamp, IP, action, score obtenido, threshold configurado
+- **AND** NO DEBE incluir datos personales del usuario
+
+#### Scenario: Log de errores de API
+- **WHEN** API de Google falla
+- **THEN** DEBE registrar error en log
+- **AND** DEBE incluir código de error y mensaje
+- **AND** DEBE permitir diagnóstico del problema
+
+---
+
+### Requirement: Fallback y Resiliencia
+
+El sistema DEBE manejar fallos graciosamente.
+
+#### Scenario: Fail-open cuando API no responde
+- **GIVEN** API de Google no responde (timeout)
+- **WHEN** se intenta validar
+- **THEN** DEBE permitir el envío del formulario (fail-open)
+- **AND** DEBE registrar el evento en log
+- **AND** NO DEBE bloquear usuarios legítimos por falla de terceros
+
+#### Scenario: Degradación cuando reCAPTCHA deshabilitado
+- **GIVEN** administrador deshabilita reCAPTCHA
+- **WHEN** se envía formulario
+- **THEN** DEBE procesarse normalmente
+- **AND** validaciones existentes (nonce, rate limit) DEBEN seguir activas
+- **AND** NO DEBE generar errores por ausencia de token
+
+---
+
+## Checklist de Implementación
+
+### Archivos a Crear
+- [ ] `Schemas/recaptcha-settings.json`
+- [ ] `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
+- [ ] `Shared/Domain/Entities/RecaptchaResult.php`
+- [ ] `Shared/Application/Services/RecaptchaValidationService.php`
+- [ ] `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
+- [ ] `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
+- [ ] `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
+
+### Archivos a Modificar
+- [ ] `Public/Footer/Infrastructure/Ui/FooterRenderer.php`
+- [ ] `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`
+- [ ] `Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php`
+- [ ] `Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`
+- [ ] `functions.php` (registro DI)
+- [ ] `Admin/Infrastructure/Ui/AdminDashboardRenderer.php` (registrar componente)
+- [ ] `Admin/Shared/Infrastructure/FieldMapping/FieldMapperRegistry.php`
+
+### Validaciones
+- [ ] Schema tiene campos de visibilidad
+- [ ] Domain no tiene dependencias de Infrastructure
+- [ ] Application solo depende de interfaces de Domain
+- [ ] Todos los servicios inyectados via constructor
+- [ ] CSS generado via CSSGeneratorService (si aplica)
+- [ ] Secret key nunca expuesto en frontend
+
+---
+
+## Última actualización
+
+2025-01-08
diff --git a/_openspec/changes/recaptcha-anti-spam/tasks.md b/_openspec/changes/recaptcha-anti-spam/tasks.md
new file mode 100644
index 00000000..c440d8d9
--- /dev/null
+++ b/_openspec/changes/recaptcha-anti-spam/tasks.md
@@ -0,0 +1,132 @@
+# Tasks: reCAPTCHA v3 Anti-Spam Protection
+
+## Fase 1: Especificación
+- [x] Crear proposal.md
+- [x] Crear tasks.md
+- [x] Crear spec.md con formato Gherkin
+- [ ] Obtener aprobación del usuario
+
+## Fase 2: Implementación
+
+### 2.1 Capa Domain (Contratos y Entidades)
+- [ ] Crear `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
+ ```php
+ interface RecaptchaValidatorInterface {
+ public function validate(string $token, string $action): RecaptchaResult;
+ }
+ ```
+- [ ] Crear `Shared/Domain/Entities/RecaptchaResult.php`
+ ```php
+ final class RecaptchaResult {
+ public function __construct(
+ private bool $success,
+ private float $score,
+ private string $action,
+ private array $errorCodes = []
+ ) {}
+ public function isValid(float $threshold): bool;
+ }
+ ```
+
+### 2.2 Capa Application (Servicios)
+- [ ] Crear `Shared/Application/Services/RecaptchaValidationService.php`
+ - Orquestar validación
+ - Aplicar threshold configurable
+ - Logging de resultados
+
+### 2.3 Capa Infrastructure (Implementación)
+- [ ] Crear `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
+ - Llamada HTTP a API de Google
+ - Manejo de errores y timeout
+ - Parseo de respuesta JSON
+
+### 2.4 Schema y Admin UI
+- [ ] Crear `Schemas/recaptcha-settings.json`
+ - Campos: is_enabled, site_key, secret_key, score_threshold, actions
+- [ ] Sincronizar schema con BD: `wp roi-theme sync-component recaptcha-settings`
+- [ ] Crear `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
+- [ ] Crear `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
+- [ ] Registrar en `getComponents()` del AdminDashboardRenderer
+- [ ] Registrar FieldMapper en FieldMapperRegistry
+
+### 2.5 Integración Frontend
+- [ ] Modificar `FooterRenderer.php`
+ - Agregar script de reCAPTCHA con site key
+ - Modificar form para incluir token hidden
+- [ ] Modificar `ContactFormRenderer.php`
+ - Agregar script de reCAPTCHA con site key
+ - Modificar form para incluir token hidden
+- [ ] Crear JS compartido para ejecutar reCAPTCHA y obtener token
+
+### 2.6 Integración Backend
+- [ ] Modificar `NewsletterAjaxHandler.php`
+ - Inyectar RecaptchaValidationService
+ - Validar token antes de procesar
+ - Retornar error si score bajo
+- [ ] Modificar `ContactFormAjaxHandler.php`
+ - Inyectar RecaptchaValidationService
+ - Validar token antes de procesar
+ - Retornar error si score bajo
+
+### 2.7 Registro DI
+- [ ] Modificar `functions.php`
+ - Registrar RecaptchaValidatorInterface → GoogleRecaptchaValidator
+ - Registrar RecaptchaValidationService
+
+## Fase 3: Integración y Validación
+
+### 3.1 Testing Manual
+- [ ] Probar Newsletter con reCAPTCHA habilitado
+- [ ] Probar Contact Form con reCAPTCHA habilitado
+- [ ] Probar con reCAPTCHA deshabilitado (fallback)
+- [ ] Probar cambio de threshold desde admin
+- [ ] Verificar logging de intentos
+
+### 3.2 Validación de Arquitectura
+- [ ] Ejecutar `validate-architecture.php recaptcha-settings`
+- [ ] Verificar cumplimiento Clean Architecture
+- [ ] Verificar inyección de dependencias correcta
+
+### 3.3 Documentación
+- [ ] Actualizar CLAUDE.md si es necesario
+- [ ] Documentar configuración en admin
+
+## Dependencias
+
+| Tarea | Depende de |
+|-------|------------|
+| Application Service | Domain Contract |
+| Infrastructure Service | Domain Contract |
+| Admin FormBuilder | Schema JSON sincronizado |
+| Frontend integration | Site Key configurado |
+| Backend integration | Application Service + Infrastructure |
+
+## Estimación de Archivos
+
+| Tipo | Cantidad |
+|------|----------|
+| Nuevos | 7 |
+| Modificados | 7 |
+| Total | 14 |
+
+### Archivos Nuevos
+1. `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
+2. `Shared/Domain/Entities/RecaptchaResult.php`
+3. `Shared/Application/Services/RecaptchaValidationService.php`
+4. `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
+5. `Schemas/recaptcha-settings.json`
+6. `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
+7. `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
+
+### Archivos a Modificar
+1. `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`
+2. `Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`
+3. `Public/Footer/Infrastructure/Ui/FooterRenderer.php`
+4. `Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php`
+5. `functions.php`
+6. `Admin/Infrastructure/Ui/AdminDashboardRenderer.php`
+7. `Admin/Shared/Infrastructure/FieldMapping/FieldMapperRegistry.php`
+
+## Última actualización
+
+2025-01-08
diff --git a/openspec/changes/refactor-adsense-lazy-loading/design.md b/_openspec/changes/refactor-adsense-lazy-loading/design.md
similarity index 100%
rename from openspec/changes/refactor-adsense-lazy-loading/design.md
rename to _openspec/changes/refactor-adsense-lazy-loading/design.md
diff --git a/openspec/changes/refactor-adsense-lazy-loading/proposal.md b/_openspec/changes/refactor-adsense-lazy-loading/proposal.md
similarity index 100%
rename from openspec/changes/refactor-adsense-lazy-loading/proposal.md
rename to _openspec/changes/refactor-adsense-lazy-loading/proposal.md
diff --git a/openspec/changes/refactor-adsense-lazy-loading/sanity-tests.md b/_openspec/changes/refactor-adsense-lazy-loading/sanity-tests.md
similarity index 100%
rename from openspec/changes/refactor-adsense-lazy-loading/sanity-tests.md
rename to _openspec/changes/refactor-adsense-lazy-loading/sanity-tests.md
diff --git a/openspec/changes/refactor-adsense-lazy-loading/schema-changes.md b/_openspec/changes/refactor-adsense-lazy-loading/schema-changes.md
similarity index 100%
rename from openspec/changes/refactor-adsense-lazy-loading/schema-changes.md
rename to _openspec/changes/refactor-adsense-lazy-loading/schema-changes.md
diff --git a/openspec/changes/refactor-adsense-lazy-loading/specs/adsense-lazy-loading/spec.md b/_openspec/changes/refactor-adsense-lazy-loading/specs/adsense-lazy-loading/spec.md
similarity index 100%
rename from openspec/changes/refactor-adsense-lazy-loading/specs/adsense-lazy-loading/spec.md
rename to _openspec/changes/refactor-adsense-lazy-loading/specs/adsense-lazy-loading/spec.md
diff --git a/openspec/changes/refactor-adsense-lazy-loading/tasks.md b/_openspec/changes/refactor-adsense-lazy-loading/tasks.md
similarity index 100%
rename from openspec/changes/refactor-adsense-lazy-loading/tasks.md
rename to _openspec/changes/refactor-adsense-lazy-loading/tasks.md
diff --git a/openspec/changes/refactor-adsense-lazy-loading/test-plan.md b/_openspec/changes/refactor-adsense-lazy-loading/test-plan.md
similarity index 100%
rename from openspec/changes/refactor-adsense-lazy-loading/test-plan.md
rename to _openspec/changes/refactor-adsense-lazy-loading/test-plan.md
diff --git a/openspec/specs/templates-unificados/spec.md b/_openspec/changes/templates-unificados/spec.md
similarity index 100%
rename from openspec/specs/templates-unificados/spec.md
rename to _openspec/changes/templates-unificados/spec.md
diff --git a/openspec/project.md b/_openspec/project.md
similarity index 80%
rename from openspec/project.md
rename to _openspec/project.md
index 5c066a04..74e8af74 100644
--- a/openspec/project.md
+++ b/_openspec/project.md
@@ -66,6 +66,9 @@ El sistema maneja componentes UI configurables para un sitio de análisis de pre
- **FormBuilder**: Panel admin para configurar valores
### Flujo de 5 Fases para Componentes
+
+> **Nota**: `[nombre]` = kebab-case (ej: `contact-form`), `[Nombre]` = PascalCase (ej: `ContactForm`)
+
1. Schema JSON → `Schemas/[nombre].json`
2. Sincronización → `wp roi-theme sync-component [nombre]`
3. Renderer → `Public/[Nombre]/Infrastructure/Ui/[Nombre]Renderer.php`
@@ -86,6 +89,20 @@ El sistema maneja componentes UI configurables para un sitio de análisis de pre
- WP-CLI (`C:\xampp\php_8.0.30_backup\wp-cli.phar`)
## Referencias Documentación
-- Arquitectura: `_planeacion/roi-theme/_arquitectura/`
-- Template HTML: `_planeacion/roi-theme/roi-theme-template/index.html`
-- Design System: `_planeacion/roi-theme/_arquitectura/01-design-system/`
+
+### OpenSpec - Especificaciones del Proyecto
+| Archivo | Descripción | Ubicación |
+|---------|-------------|-----------|
+| **WORKFLOW-ROI-THEME.md** | Flujo de trabajo obligatorio, regla de oro | `_openspec/WORKFLOW-ROI-THEME.md` |
+| **AGENTS.md** | Agentes disponibles y cuándo usarlos | `_openspec/AGENTS.md` |
+| **arquitectura-limpia** | Clean Architecture, capas, estructura | `_openspec/specs/arquitectura-limpia.md` |
+| **estandares-codigo** | SOLID, PHP, WordPress, seguridad | `_openspec/specs/estandares-codigo.md` |
+| **nomenclatura** | Convenciones de nombres completas | `_openspec/specs/nomenclatura.md` |
+
+### Documentación de Planificación
+- Template HTML: `_planificacion/roi-theme-template/index.html`
+- Design System: `_planificacion/01-design-system/`
+
+---
+
+**Última actualización:** 2026-01-08
diff --git a/_openspec/specs/arquitectura-limpia.md b/_openspec/specs/arquitectura-limpia.md
new file mode 100644
index 00000000..e5843403
--- /dev/null
+++ b/_openspec/specs/arquitectura-limpia.md
@@ -0,0 +1,777 @@
+# Especificacion de Arquitectura Limpia
+
+## Purpose
+
+Define la implementacion de Clean Architecture para ROITheme, un tema WordPress que sigue principios de Domain-Driven Design con separacion fisica de contextos delimitados (Admin, Public, Shared).
+
+> **NOTA**: Para convenciones de nomenclatura, ver `_openspec/specs/nomenclatura.md`
+> **NOTA**: Para principios SOLID y estandares de codigo, ver `_openspec/specs/estandares-codigo.md`
+> **NOTA**: Para flujo de trabajo y fases obligatorias, ver `_openspec/WORKFLOW-ROI-THEME.md`
+
+---
+
+## Requirements
+
+### Requirement: Separacion Fisica de Contextos
+
+The system MUST organize code into three physically delimited contexts: Admin/, Public/, and Shared/.
+
+#### Scenario: Codigo pertenece al contexto de administracion
+- **WHEN** el codigo maneja operaciones CRUD, configuracion o funcionalidad del panel admin
+- **THEN** el codigo DEBE colocarse en el directorio `Admin/`
+- **AND** el codigo NO DEBE importar del directorio `Public/`
+
+#### Scenario: Codigo pertenece al contexto publico/frontend
+- **WHEN** el codigo maneja renderizado, visualizacion o presentacion frontend
+- **THEN** el codigo DEBE colocarse en el directorio `Public/`
+- **AND** el codigo NO DEBE importar del directorio `Admin/`
+
+#### Scenario: Codigo es compartido entre contextos
+- **WHEN** el codigo es usado por AMBOS contextos Admin/ y Public/
+- **THEN** el codigo DEBE colocarse en el directorio `Shared/` raiz
+- **AND** tanto Admin/ como Public/ PUEDEN importar de Shared/
+
+---
+
+### Requirement: Organizacion Granular de Codigo Compartido
+
+The system MUST implement three levels of shared code to avoid mixing context-specific shared code.
+
+#### Scenario: Codigo compartido solo dentro del contexto Admin
+- **WHEN** el codigo es reutilizado por multiples modulos Admin pero NO por Public
+- **THEN** el codigo DEBE colocarse en el directorio `Admin/Shared/`
+- **AND** los modulos de Public/ NO DEBEN importar de `Admin/Shared/`
+
+#### Scenario: Codigo compartido solo dentro del contexto Public
+- **WHEN** el codigo es reutilizado por multiples modulos Public pero NO por Admin
+- **THEN** el codigo DEBE colocarse en el directorio `Public/Shared/`
+- **AND** los modulos de Admin/ NO DEBEN importar de `Public/Shared/`
+
+#### Scenario: Codigo compartido entre ambos contextos
+- **WHEN** el codigo es reutilizado por AMBOS modulos Admin/ y Public/
+- **THEN** el codigo DEBE colocarse en el directorio `Shared/` raiz
+- **AND** esto incluye ValueObjects, Exceptions y Contracts base
+
+---
+
+### Requirement: Cada Contexto Sigue las Capas de Clean Architecture
+
+Each context (Admin/, Public/, Shared/) MUST implement Infrastructure layer, and MAY implement Domain and Application layers when business logic requires it.
+
+#### Scenario: Estructura de modulo dentro del contexto Admin
+- **GIVEN** un componente llamado "Navbar" en el contexto Admin
+- **WHEN** el modulo es creado
+- **THEN** la estructura DEBE incluir: Admin/Navbar/Infrastructure/
+- **AND** PUEDE incluir Domain/ (si hay logica de negocio)
+- **AND** PUEDE incluir Application/ (si hay casos de uso)
+
+#### Scenario: Estructura de modulo dentro del contexto Public
+- **GIVEN** un componente llamado "Navbar" en el contexto Public
+- **WHEN** el modulo es creado
+- **THEN** la estructura DEBE incluir: Public/Navbar/Infrastructure/
+- **AND** PUEDE incluir Domain/ (si hay logica de negocio)
+- **AND** PUEDE incluir Application/ (si hay casos de uso)
+
+---
+
+### Requirement: Cumplimiento de Direccion de Dependencias
+
+The system MUST enforce that dependencies flow ONLY from outer layers to inner layers.
+
+#### Scenario: Infrastructure depende de Application y Domain
+- **WHEN** el codigo esta en la capa Infrastructure
+- **THEN** PUEDE importar de la capa Application
+- **AND** PUEDE importar de la capa Domain
+
+#### Scenario: Application depende solo de Domain
+- **WHEN** el codigo esta en la capa Application
+- **THEN** PUEDE importar de la capa Domain
+- **AND** NO DEBE importar de la capa Infrastructure
+
+#### Scenario: Domain no tiene dependencias externas
+- **WHEN** el codigo esta en la capa Domain
+- **THEN** NO DEBE importar de la capa Application
+- **AND** NO DEBE importar de la capa Infrastructure
+- **AND** NO DEBE importar funciones o globales de WordPress
+
+---
+
+### Requirement: La Capa Domain Contiene Solo Logica de Negocio Pura
+
+The Domain layer MUST contain only pure business logic without framework dependencies.
+
+#### Scenario: Validacion de contenido de capa Domain
+- **WHEN** el codigo se coloca en la capa Domain
+- **THEN** PUEDE contener Entities, Value Objects, Domain Services, Interfaces, Exceptions
+- **AND** NO DEBE contener global $wpdb, $_POST, $_GET, $_SESSION, add_action, add_filter, HTML, CSS, JavaScript
+
+#### Scenario: Implementacion de entidad Domain
+- **GIVEN** una entidad Domain como NavbarConfiguration
+- **WHEN** la entidad es implementada
+- **THEN** DEBE contener reglas de negocio y validacion
+- **AND** NO DEBE contener logica de persistencia
+- **AND** NO DEBE referenciar APIs de WordPress
+
+---
+
+### Requirement: La Capa Application Orquesta Domain
+
+The Application layer MUST orchestrate domain entities without containing business logic.
+
+#### Scenario: Implementacion de Use Case
+- **WHEN** un Use Case es implementado
+- **THEN** DEBE coordinar entidades y servicios de domain
+- **AND** DEBE depender de interfaces, NO de implementaciones concretas
+- **AND** NO DEBE contener reglas de validacion de negocio
+
+#### Scenario: Uso de DTOs para transferencia de datos
+- **WHEN** los datos cruzan limites entre capas
+- **THEN** se DEBEN usar DTOs (Data Transfer Objects)
+- **AND** los DTOs DEBEN ser contenedores de datos simples sin logica de negocio
+
+---
+
+### Requirement: Infrastructure Implementa Interfaces
+
+The Infrastructure layer MUST implement interfaces defined in Domain/Application layers.
+
+#### Scenario: Implementacion de Repository
+- **GIVEN** una RepositoryInterface definida en Domain
+- **WHEN** el repository es implementado
+- **THEN** DEBE colocarse en Infrastructure/Persistence/
+- **AND** DEBE implementar la interface de Domain
+- **AND** PUEDE usar global $wpdb o APIs de WordPress
+
+#### Scenario: Integracion con WordPress
+- **WHEN** se necesita codigo especifico de WordPress
+- **THEN** DEBE colocarse en la capa Infrastructure
+- **AND** NO DEBE filtrarse a las capas Domain o Application
+
+---
+
+### Requirement: Los Modulos Son Autocontenidos e Independientes
+
+Each module (Navbar, Footer, Toolbar, etc.) MUST be self-contained and independent from other modules.
+
+#### Scenario: Aislamiento de modulos
+- **WHEN** un modulo como Admin/Navbar/ es implementado
+- **THEN** NO DEBE importar de Admin/Footer/
+- **AND** NO DEBE importar de Admin/Toolbar/
+- **AND** SOLO PUEDE importar de Shared/
+
+#### Scenario: Eliminacion de modulos
+- **WHEN** un modulo necesita ser eliminado
+- **THEN** borrar la carpeta del modulo NO DEBE romper otros modulos
+- **AND** no se DEBERIAN requerir cambios de codigo en otros modulos
+
+---
+
+### Requirement: Admin y Public Son Bounded Contexts Separados
+
+Admin/ and Public/ MUST be treated as separate bounded contexts because they have different responsibilities.
+
+#### Scenario: Responsabilidad del contexto Admin
+- **WHEN** el codigo maneja administracion de componentes
+- **THEN** la entidad Domain se enfoca en configuracion, validacion, estados draft/published
+- **AND** los Use Cases se enfocan en operaciones Save, Update, Delete, Get
+
+#### Scenario: Responsabilidad del contexto Public
+- **WHEN** el codigo maneja renderizado de componentes
+- **THEN** la entidad Domain se enfoca en estado activo, caching, filtrado por permisos
+- **AND** los Use Cases se enfocan en operaciones GetActive, Render, Cache
+
+#### Scenario: No hay duplicacion de domain
+- **WHEN** Admin/Navbar/Domain/ y Public/Navbar/Domain/ ambos existen
+- **THEN** NO son duplicados sino bounded contexts especializados
+- **AND** Admin se enfoca en configuracion/gestion
+- **AND** Public se enfoca en renderizado/visualizacion
+
+---
+
+### Requirement: Validacion de Arquitectura Antes de Commit
+
+The system MUST validate architectural compliance before committing code.
+
+#### Scenario: Validacion de capa Domain
+- **WHEN** se valida codigo de la capa Domain
+- **THEN** grep por global $wpdb DEBE retornar vacio
+- **AND** grep por add_action DEBE retornar vacio
+- **AND** grep por $_POST DEBE retornar vacio
+
+#### Scenario: Validacion de dependencias de modulos
+- **WHEN** se validan dependencias entre modulos
+- **THEN** imports de Admin/Navbar/ desde Admin/Footer/ NO DEBEN existir
+- **AND** imports de Public/Navbar/ desde Public/Footer/ NO DEBEN existir
+
+---
+
+### Requirement: Realizacion de Beneficios de la Arquitectura
+
+The architecture MUST provide measurable benefits.
+
+#### Scenario: Asignacion granular de trabajo
+- **WHEN** un desarrollador es asignado a trabajar en Admin/Navbar/
+- **THEN** puede acceder SOLO a esa carpeta
+- **AND** no puede ver ni modificar Public/ u otros modulos de Admin/
+
+#### Scenario: Eliminacion facil de modulos
+- **WHEN** un componente ya no es necesario
+- **THEN** eliminarlo requiere solo borrar la carpeta
+- **AND** no se necesitan otras modificaciones de codigo
+
+#### Scenario: Codigo compartido consistente
+- **WHEN** se encuentra un bug en un ValueObject compartido
+- **THEN** arreglarlo en Shared/Domain/ValueObjects/ lo arregla para TODOS los modulos
+- **AND** no se necesita actualizar codigo duplicado
+
+---
+
+## Diagrama ASCII de Capas
+
+```
+╔═══════════════════════════════════════════════════════════════════════════════╗
+║ CLEAN ARCHITECTURE ║
+║ ROI Theme ║
+╠═══════════════════════════════════════════════════════════════════════════════╣
+║ ║
+║ ┌─────────────────────────────────────────────────────────────────────┐ ║
+║ │ │ ║
+║ │ INFRASTRUCTURE │ ║
+║ │ (Capa Externa - WordPress) │ ║
+║ │ │ ║
+║ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ ║
+║ │ │ Ui/ │ │ Api/ │ │ Persistence/│ │ Services/ │ │ ║
+║ │ │ Renderers │ │ AJAX │ │ Repositories│ │ Helpers │ │ ║
+║ │ │ FormBuilders│ │ Handlers │ │ $wpdb │ │ │ │ ║
+║ │ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │ ║
+║ │ │ ║
+║ │ PERMITIDO: WordPress APIs, HTML, CSS, JS, $wpdb, hooks │ ║
+║ │ │ ║
+║ └─────────────────────────────────────────────────────────────────────┘ ║
+║ │ ║
+║ ▼ depende de ║
+║ ┌─────────────────────────────────────────────────────────────────────┐ ║
+║ │ │ ║
+║ │ APPLICATION │ ║
+║ │ (Casos de Uso / Orquestacion) │ ║
+║ │ │ ║
+║ │ ┌─────────────────────────────────────────────────────────────┐ │ ║
+║ │ │ UseCases/ │ │ ║
+║ │ │ GetComponentSettingsUseCase, RenderComponentUseCase │ │ ║
+║ │ │ CheckVisibilityUseCase, SyncSchemaUseCase │ │ ║
+║ │ └─────────────────────────────────────────────────────────────┘ │ ║
+║ │ │ ║
+║ │ PERMITIDO: Orquestacion, DTOs, llamadas a interfaces Domain │ ║
+║ │ PROHIBIDO: WordPress, HTML, $wpdb, persistencia directa │ ║
+║ │ │ ║
+║ └─────────────────────────────────────────────────────────────────────┘ ║
+║ │ ║
+║ ▼ depende de ║
+║ ┌─────────────────────────────────────────────────────────────────────┐ ║
+║ │ │ ║
+║ │ DOMAIN │ ║
+║ │ (Centro - Logica de Negocio Pura) │ ║
+║ │ │ ║
+║ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────────────┐ │ ║
+║ │ │ Entities/ │ │Contracts/ │ │ Value │ │ Exceptions/ │ │ ║
+║ │ │Component │ │Interfaces │ │ Objects/ │ │ValidationException│ │ ║
+║ │ └───────────┘ └───────────┘ └───────────┘ └───────────────────┘ │ ║
+║ │ │ ║
+║ │ PERMITIDO: Reglas de negocio puras, validaciones, interfaces │ ║
+║ │ PROHIBIDO: WordPress, HTML, CSS, JS, $wpdb, echo, print │ ║
+║ │ │ ║
+║ └─────────────────────────────────────────────────────────────────────┘ ║
+║ ║
+║ REGLA DE DEPENDENCIA: Las flechas SOLO apuntan hacia adentro ║
+║ Infrastructure → Application → Domain ║
+║ NUNCA al reves: Domain NO depende de nada externo ║
+║ ║
+╚═══════════════════════════════════════════════════════════════════════════════╝
+```
+
+---
+
+## Estructura Completa de Carpetas del Tema
+
+```
+roi-theme/
+│
+├── functions.php # Bootstrap del tema
+├── style.css # Metadata del tema
+│
+├── Schemas/ # JSON schemas de componentes
+│ ├── contact-form.json
+│ ├── featured-image.json
+│ ├── footer.json
+│ └── ...
+│
+├── Admin/ # CONTEXTO: Panel de administracion
+│ ├── ContactForm/ # Modulo: Configuracion de ContactForm
+│ │ ├── Domain/ # (opcional si no hay logica especifica)
+│ │ ├── Application/ # (opcional si no hay use cases)
+│ │ └── Infrastructure/
+│ │ └── Ui/
+│ │ └── ContactFormFormBuilder.php
+│ │
+│ ├── FeaturedImage/
+│ │ └── Infrastructure/
+│ │ └── Ui/
+│ │ └── FeaturedImageFormBuilder.php
+│ │
+│ ├── Shared/ # Compartido SOLO dentro de Admin
+│ │ └── Infrastructure/
+│ │ └── Ui/
+│ │ ├── AdminDashboardRenderer.php
+│ │ └── ExclusionFormPartial.php
+│ │
+│ └── ... # Otros modulos Admin
+│
+├── Public/ # CONTEXTO: Frontend publico
+│ ├── ContactForm/ # Modulo: Renderizado de ContactForm
+│ │ ├── Domain/ # (opcional)
+│ │ │ └── Contracts/
+│ │ ├── Application/ # (opcional)
+│ │ │ └── UseCases/
+│ │ └── Infrastructure/
+│ │ ├── Ui/
+│ │ │ └── ContactFormRenderer.php
+│ │ └── Api/
+│ │ └── WordPress/
+│ │ └── ContactFormAjaxHandler.php
+│ │
+│ ├── FeaturedImage/
+│ │ └── Infrastructure/
+│ │ └── Ui/
+│ │ └── FeaturedImageRenderer.php
+│ │
+│ ├── Shared/ # Compartido SOLO dentro de Public
+│ │ └── Infrastructure/
+│ │ └── Services/
+│ │
+│ └── ... # Otros modulos Public
+│
+├── Shared/ # CONTEXTO: Compartido entre Admin Y Public
+│ ├── Domain/
+│ │ ├── Contracts/ # Interfaces compartidas
+│ │ │ ├── RendererInterface.php
+│ │ │ ├── CSSGeneratorInterface.php
+│ │ │ ├── ComponentRepositoryInterface.php
+│ │ │ └── ...
+│ │ ├── Entities/
+│ │ │ └── Component.php
+│ │ └── ValueObjects/
+│ │
+│ ├── Application/
+│ │ └── UseCases/
+│ │ └── CheckWrapperVisibilityUseCase.php
+│ │
+│ └── Infrastructure/
+│ ├── Services/
+│ │ ├── CSSGeneratorService.php
+│ │ └── PageVisibilityHelper.php
+│ ├── Persistence/
+│ │ └── WordPress/
+│ │ ├── ComponentSettingsRepository.php
+│ │ └── PageVisibilityRepository.php
+│ └── Scripts/
+│ └── validate-architecture.php
+│
+├── _openspec/ # Sistema de especificaciones
+│ ├── AGENTS.md
+│ ├── WORKFLOW-ROI-THEME.md
+│ ├── project.md
+│ ├── specs/ # Specs BASE (archivos planos)
+│ │ ├── arquitectura-limpia.md
+│ │ ├── estandares-codigo.md
+│ │ └── nomenclatura.md
+│ └── changes/ # Specs de features (carpetas)
+│ └── [nombre-feature]/
+│
+└── _planificacion/ # Documentos de planificacion
+ ├── 01-design-system/ # Design System del tema
+ ├── roi-theme-template/ # Template HTML de referencia
+ ├── analisis-spam-formularios.md
+ └── plan-mejora-especificaciones-openspec.md
+```
+
+---
+
+## Reglas de Anidamiento
+
+### Requirement: Profundidad Maxima de Carpetas
+
+La estructura de carpetas DEBE respetar una profundidad maxima.
+
+#### Scenario: Profundidad maxima de 4 niveles desde contexto
+- **WHEN** se crea una estructura de carpetas
+- **THEN** la profundidad maxima DEBE ser 4 niveles desde el contexto
+- **AND** ejemplo valido: `Public/ContactForm/Infrastructure/Ui/` (4 niveles)
+- **AND** ejemplo valido: `Public/ContactForm/Infrastructure/Api/WordPress/` (5 niveles - excepcion para WordPress)
+- **AND** ejemplo invalido: `Public/ContactForm/Infrastructure/Ui/Partials/Helpers/` (6 niveles)
+
+#### Scenario: Regla de 3 archivos para subcarpetas
+- **WHEN** se decide crear una subcarpeta
+- **THEN** DEBE haber al menos 3 archivos que la justifiquen
+- **AND** si hay menos de 3 archivos, mantenerlos en la carpeta padre
+- **AND** ejemplo: NO crear `Ui/Helpers/` con solo 1-2 archivos
+
+#### Scenario: Subcarpetas permitidas en Infrastructure
+- **WHEN** se organizan archivos dentro de Infrastructure/
+- **THEN** subcarpetas permitidas son:
+ - `Ui/` - Renderers, FormBuilders, presentacion
+ - `Api/` - Handlers AJAX, REST endpoints
+ - `Api/WordPress/` - Handlers especificos de WordPress
+ - `Persistence/` - Repositorios genericos
+ - `Persistence/WordPress/` - Repositorios WordPress
+ - `Services/` - Servicios de infraestructura
+- **AND** NO crear subcarpetas adicionales sin justificacion
+
+---
+
+## Diferencia Entre Niveles de Shared
+
+```
+┌─────────────────────────────────────────────────────────────────────────────┐
+│ NIVELES DE CODIGO COMPARTIDO │
+├─────────────────────────────────────────────────────────────────────────────┤
+│ │
+│ NIVEL 1: Shared/ (raiz) │
+│ ───────────────────── │
+│ QUIEN PUEDE USAR: Admin/ y Public/ │
+│ CONTENIDO: Contratos base, entidades core, servicios fundamentales │
+│ EJEMPLOS: │
+│ - Shared/Domain/Contracts/RendererInterface.php │
+│ - Shared/Domain/Contracts/CSSGeneratorInterface.php │
+│ - Shared/Domain/Entities/Component.php │
+│ - Shared/Infrastructure/Services/CSSGeneratorService.php │
+│ │
+│ NIVEL 2: Admin/Shared/ │
+│ ────────────────────── │
+│ QUIEN PUEDE USAR: SOLO modulos dentro de Admin/ │
+│ CONTENIDO: UI components admin, helpers de formularios, partials │
+│ EJEMPLOS: │
+│ - Admin/Shared/Infrastructure/Ui/AdminDashboardRenderer.php │
+│ - Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php │
+│ PROHIBIDO PARA: Public/ │
+│ │
+│ NIVEL 3: Public/Shared/ │
+│ ─────────────────────── │
+│ QUIEN PUEDE USAR: SOLO modulos dentro de Public/ │
+│ CONTENIDO: Helpers de renderizado, componentes frontend compartidos │
+│ EJEMPLOS: │
+│ - Public/Shared/Infrastructure/Services/RenderHelper.php │
+│ PROHIBIDO PARA: Admin/ │
+│ │
+└─────────────────────────────────────────────────────────────────────────────┘
+
+REGLA: Siempre colocar codigo en el nivel MAS ESPECIFICO posible.
+ Solo subir a Shared/ raiz si AMBOS contextos lo necesitan.
+```
+
+---
+
+## Ejemplos de Codigo PHP Por Capa
+
+### Ejemplo CORRECTO: Capa Domain (Interface)
+
+```php
+name;
+ }
+
+ public function getData(): array
+ {
+ return $this->data;
+ }
+
+ public function isEnabled(): bool
+ {
+ return ($this->data['visibility']['is_enabled'] ?? false) === true;
+ }
+}
+```
+
+### Ejemplo INCORRECTO: Capa Domain (Entity con WordPress)
+
+```php
+insert('wp_components', $this->data);
+ }
+
+ // ❌ INCORRECTO: HTML en Domain
+ public function renderHtml(): string
+ {
+ return '
' . esc_html($this->name) . '
'; // ❌ PROHIBIDO
+ }
+}
+```
+
+### Ejemplo CORRECTO: Capa Application (UseCase)
+
+```php
+repository->findByName($componentName);
+ }
+}
+```
+
+### Ejemplo INCORRECTO: Capa Application (UseCase con WordPress)
+
+```php
+get_row("SELECT * FROM ...");
+
+ // ❌ INCORRECTO: Funciones WordPress en Application
+ return get_option('roi_component_' . $componentName);
+ }
+}
+```
+
+### Ejemplo CORRECTO: Capa Infrastructure (Renderer)
+
+```php
+getData();
+
+ // ✅ WordPress permitido en Infrastructure
+ $nonce = wp_create_nonce('roi_contact_form');
+
+ // ✅ HTML permitido en Infrastructure
+ $html = '';
+
+ return $html;
+ }
+
+ public function supports(string $componentType): bool
+ {
+ return $componentType === self::COMPONENT_NAME;
+ }
+}
+```
+
+### Ejemplo CORRECTO: Capa Infrastructure (Repository)
+
+```php
+prefix . 'roi_theme_component_settings';
+ $results = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT * FROM {$table} WHERE component_name = %s",
+ $componentName
+ ),
+ ARRAY_A
+ );
+
+ if (empty($results)) {
+ return null;
+ }
+
+ return new Component($componentName, $this->groupResults($results));
+ }
+}
+```
+
+---
+
+## Mapeo de Terminologia
+
+| Clean Architecture Estandar | ROI Theme | Ubicacion |
+|---------------------------|-----------|-----------|
+| Entity | Component, Entity | `*/Domain/Entities/` |
+| Value Object | Value Object | `*/Domain/ValueObjects/` |
+| Repository Interface | RepositoryInterface | `Shared/Domain/Contracts/` |
+| Repository Implementation | Repository | `*/Infrastructure/Persistence/WordPress/` |
+| Use Case / Interactor | UseCase | `*/Application/UseCases/` |
+| Gateway | Repository | `*/Infrastructure/Persistence/` |
+| Presenter | Renderer | `Public/*/Infrastructure/Ui/` |
+| Controller | FormBuilder, Handler | `Admin/*/Infrastructure/Ui/`, `*/Infrastructure/Api/` |
+| DTO | Request/Response | `*/Application/DTOs/` |
+| Domain Service | Service | `*/Domain/Services/` |
+| Infrastructure Service | Service | `*/Infrastructure/Services/` |
+
+---
+
+## Validacion de Arquitectura
+
+### Script de Validacion
+
+Ubicacion: `Shared/Infrastructure/Scripts/validate-architecture.php`
+
+```bash
+# Validar un componente especifico
+php Shared/Infrastructure/Scripts/validate-architecture.php contact-form
+
+# Validar todos los componentes
+php Shared/Infrastructure/Scripts/validate-architecture.php --all
+```
+
+### Que Valida el Script
+
+| Validacion | Que Busca | Error si Encuentra |
+|------------|-----------|-------------------|
+| WordPress en Domain | `global $wpdb`, `add_action`, `$_POST` | "WordPress code in Domain layer" |
+| HTML en Domain | `
repo = new WordPressNavbarRepository()
+- **THEN** esto DEBE refactorizarse para recibir NavbarRepositoryInterface
+- **AND** el DIContainer proporciona la implementacion concreta
+
+---
+
+### Requirement: Encapsulacion de Propiedades
+
+Class properties MUST be encapsulated with controlled access.
+
+#### Scenario: Visibilidad de propiedades
+- **WHEN** se define una propiedad de clase
+- **THEN** DEBE ser private o protected
+- **AND** el acceso DEBE ser via metodos getter
+- **AND** la mutacion DEBE ser via metodos setter o metodos de negocio
+
+#### Scenario: Encapsulacion de Value Object
+- **GIVEN** un ValueObject como ComponentName
+- **WHEN** es construido
+- **THEN** la validacion DEBE ocurrir en el constructor
+- **AND** el valor DEBE ser inmutable despues de la construccion
+- **AND** los detalles internos NO DEBEN exponerse
+
+---
+
+### Requirement: Guias de Herencia
+
+Inheritance MUST be used appropriately with limited depth.
+
+#### Scenario: Limite de profundidad de herencia
+- **WHEN** se usa herencia
+- **THEN** la profundidad maxima DEBE ser 2-3 niveles
+- **AND** las cadenas de herencia profundas DEBEN evitarse
+
+#### Scenario: Comportamiento comun en clase base
+- **WHEN** multiples clases comparten comportamiento comun
+- **THEN** se DEBERIA crear una clase base abstracta
+- **AND** las subclases especializan con comportamiento adicional
+
+---
+
+### Requirement: Polimorfismo Correcto
+
+Methods MUST accept base types or interfaces to enable polymorphism.
+
+#### Scenario: Tipos de parametros de metodo
+- **WHEN** un metodo acepta parametro de componente
+- **THEN** el type hint DEBERIA ser BaseComponent o ComponentInterface
+- **AND** cualquier subclase/implementacion DEBE funcionar correctamente
+
+#### Scenario: Polimorfismo de repository
+- **WHEN** un Use Case usa un repository
+- **THEN** DEBE aceptar RepositoryInterface
+- **AND** WordPressRepository y MockRepository funcionan transparentemente
+
+---
+
+### Requirement: Estandares PHP Estrictos
+
+All PHP code MUST follow strict type safety and naming conventions.
+
+#### Scenario: Declaracion de tipos estrictos
+- **WHEN** se crea un archivo PHP
+- **THEN** DEBE comenzar con declare(strict_types=1)
+- **AND** los tipos de retorno DEBEN declararse
+- **AND** los tipos de parametros DEBEN declararse
+
+#### Scenario: Convencion de namespace
+- **WHEN** se crea una clase
+- **THEN** el namespace DEBE seguir ROITheme\[Contexto]\[Componente]\[Capa]
+- **AND** DEBE soportar autoloading PSR-4
+
+#### Scenario: Declaracion de clase
+- **WHEN** se crea una clase
+- **THEN** DEBERIA ser final por defecto
+- **AND** solo hacerla no-final cuando se pretende herencia
+- **AND** el nombre de clase DEBE ser PascalCase
+
+---
+
+### Requirement: Modularidad del Codigo
+
+Code MUST be organized into independent and cohesive modules.
+
+#### Scenario: Independencia de modulos
+- **WHEN** se crea un modulo
+- **THEN** DEBE ser autocontenido
+- **AND** NO DEBE depender de otros modulos (solo de Shared/)
+- **AND** eliminarlo NO DEBE romper otros modulos
+
+#### Scenario: Alta cohesion
+- **WHEN** el codigo se coloca en un modulo
+- **THEN** todo el codigo DEBE relacionarse con el proposito de ese modulo
+- **AND** el codigo no relacionado DEBE estar en Shared/ u otro modulo
+
+#### Scenario: Bajo acoplamiento
+- **WHEN** los modulos interactuan
+- **THEN** DEBEN comunicarse a traves de interfaces de Shared/
+- **AND** las dependencias directas entre modulos estan prohibidas
+
+---
+
+### Requirement: DRY - No Te Repitas
+
+Code duplication MUST be eliminated through appropriate abstraction.
+
+#### Scenario: Ubicacion de codigo compartido
+- **WHEN** el codigo es usado por multiples modulos
+- **THEN** DEBE moverse al nivel apropiado de Shared/
+- **AND** los modulos DEBEN importar de Shared/
+
+#### Scenario: Deteccion de duplicacion
+- **WHEN** existe codigo similar en 2+ lugares
+- **THEN** DEBE refactorizarse a Shared/
+- **AND** las ubicaciones originales importan de Shared/
+
+---
+
+### Requirement: KISS - Mantenlo Simple
+
+Solutions MUST be simple and avoid over-engineering.
+
+#### Scenario: Uso de patrones
+- **WHEN** se considera un patron de diseno
+- **THEN** DEBE resolver un problema real
+- **AND** se DEBEN preferir soluciones mas simples
+- **AND** la abstraccion excesiva DEBE evitarse
+
+#### Scenario: Claridad del codigo
+- **WHEN** se escribe codigo
+- **THEN** DEBERIA ser auto-documentado
+- **AND** los comentarios DEBERIAN ser innecesarios para entender
+- **AND** la logica compleja DEBERIA extraerse a metodos bien nombrados
+
+---
+
+### Requirement: Separacion de Responsabilidades por Capa
+
+Each layer MUST have distinct responsibilities.
+
+#### Scenario: Responsabilidades por capa
+- **WHEN** se escribe codigo
+- **THEN** Domain contiene logica de negocio
+- **AND** Application contiene orquestacion
+- **AND** Infrastructure contiene implementacion tecnica
+- **AND** UI contiene solo presentacion
+
+#### Scenario: Validacion de responsabilidades cruzadas
+- **WHEN** se valida ubicacion de codigo
+- **THEN** SQL NO DEBE estar en Domain/Application
+- **AND** HTML NO DEBE estar en Domain/Application
+- **AND** logica de negocio NO DEBE estar en Infrastructure
+
+---
+
+### Requirement: Limites de Tamano de Archivo
+
+Files MUST be kept small and focused.
+
+#### Scenario: Tamano de archivo de clase
+- **WHEN** se crea un archivo de clase
+- **THEN** DEBERIA tener menos de 300 lineas
+- **AND** si es mas grande, DEBERIA dividirse en clases mas pequenas
+
+#### Scenario: Tamano de metodo
+- **WHEN** se escribe un metodo
+- **THEN** DEBERIA tener menos de 30 lineas
+- **AND** metodos complejos DEBERIAN extraerse a metodos auxiliares
+
+---
+
+### Requirement: Convenciones de Nomenclatura
+
+Names MUST be clear, descriptive, and follow conventions.
+
+#### Scenario: Nomenclatura de clases
+- **WHEN** se nombra una clase
+- **THEN** el nombre DEBE describir su unica responsabilidad
+- **AND** las clases Use Case DEBEN nombrarse [Accion][Entidad]UseCase
+- **AND** las clases Repository DEBEN nombrarse [Implementacion][Entidad]Repository
+
+#### Scenario: Nomenclatura de metodos
+- **WHEN** se nombra un metodo
+- **THEN** DEBE describir lo que hace el metodo
+- **AND** DEBERIA comenzar con un verbo
+- **AND** metodos booleanos DEBERIAN comenzar con is/has/can
+
+#### Scenario: Nomenclatura de variables
+- **WHEN** se nombra una variable
+- **THEN** DEBE ser descriptiva
+- **AND** las abreviaturas DEBEN evitarse
+- **AND** nombres de una letra solo para contadores de bucle
+
+---
+
+### Requirement: Validacion Pre-Commit
+
+Code MUST pass validation before commit.
+
+#### Scenario: Verificacion de cumplimiento SOLID
+- **WHEN** el codigo esta listo para commit
+- **THEN** SRP cada clase tiene una responsabilidad
+- **AND** OCP nuevas caracteristicas via extension, no modificacion
+- **AND** LSP las subclases son sustituibles
+- **AND** ISP las interfaces son pequenas 3-5 metodos
+- **AND** DIP el constructor recibe interfaces
+
+#### Scenario: Verificacion de cumplimiento POO
+- **WHEN** el codigo esta listo para commit
+- **THEN** las propiedades son private/protected
+- **AND** la profundidad de herencia es max 2-3 niveles
+- **AND** el polimorfismo esta implementado correctamente
+- **AND** la abstraccion oculta complejidad
+
+#### Scenario: Verificacion de calidad
+- **WHEN** el codigo esta listo para commit
+- **THEN** los archivos tienen menos de 300 lineas
+- **AND** los nombres son claros y descriptivos
+- **AND** no existe duplicacion de codigo
+- **AND** no hay sobre-ingenieria presente
+
+---
+
+### Requirement: Escaping Obligatorio en Output HTML
+
+All HTML output MUST use WordPress escaping functions for security.
+
+#### Scenario: Escaping de textos
+- **WHEN** se genera output de texto en HTML
+- **THEN** DEBE usar esc_html() para contenido de texto
+
+#### Scenario: Escaping de atributos
+- **WHEN** se genera un atributo HTML
+- **THEN** DEBE usar esc_attr() para valores de atributos
+
+#### Scenario: Escaping de URLs
+- **WHEN** se genera una URL en href o src
+- **THEN** DEBE usar esc_url() para URLs
+
+#### Scenario: Escaping de textareas
+- **WHEN** se genera contenido para textarea
+- **THEN** DEBE usar esc_textarea() para el valor
+
+#### Scenario: Prohibicion de output sin escaping
+- **WHEN** se revisa codigo de Renderer o FormBuilder
+- **THEN** NO DEBE existir echo o print de variables sin escaping
+- **AND** NO DEBE existir interpolacion directa de variables en HTML
+
+---
+
+## Ejemplos de Codigo PHP para SOLID
+
+> **REFERENCIA**: Para nomenclatura de clases, ver `_openspec/specs/nomenclatura.md`
+> **REFERENCIA**: Para ubicacion de archivos, ver `_openspec/specs/arquitectura-limpia.md`
+
+### Ejemplo SRP: Single Responsibility Principle
+
+```php
+renderContactForm($data);
+ } elseif ($type === 'newsletter') {
+ return $this->renderNewsletter($data);
+ } elseif ($type === 'featured-image') { // Cada nuevo tipo = modificacion
+ return $this->renderFeaturedImage($data);
+ }
+ return '';
+ }
+}
+
+// CORRECTO - Extender sin modificar (usando interfaces)
+// Domain/Contracts/RendererInterface.php
+use ROITheme\Shared\Domain\Entities\Component;
+
+interface RendererInterface
+{
+ public function render(Component $component): string;
+ public function supports(string $componentType): bool;
+}
+
+// ContactFormRenderer.php - implementa interface
+final class ContactFormRenderer implements RendererInterface
+{
+ public function supports(string $componentType): bool
+ {
+ return $componentType === 'contact-form';
+ }
+
+ public function render(Component $component): string { /* ... */ }
+}
+
+// NewsletterRenderer.php - nueva clase, sin modificar existentes
+final class NewsletterRenderer implements RendererInterface
+{
+ public function supports(string $componentType): bool
+ {
+ return $componentType === 'newsletter';
+ }
+
+ public function render(Component $component): string { /* ... */ }
+}
+```
+
+### Ejemplo LSP: Liskov Substitution Principle
+
+```php
+validate($data); // Siempre funciona
+ if (!$result->isValid()) {
+ // manejar error
+ }
+}
+```
+
+### Ejemplo ISP: Interface Segregation Principle
+
+```php
+cssGenerator = new WordPressCSSGenerator();
+ }
+}
+
+// CORRECTO - Dependencia de abstraccion (interface)
+// Domain/Contracts/CSSGeneratorInterface.php
+interface CSSGeneratorInterface
+{
+ public function generate(array $styles): string;
+}
+
+// Infrastructure/Ui/ContactFormRenderer.php
+final class ContactFormRenderer implements RendererInterface
+{
+ public function __construct(
+ private CSSGeneratorInterface $cssGenerator // Interface, no clase concreta
+ ) {}
+
+ public function render(Component $component): string
+ {
+ $data = $component->getData();
+ $css = $this->cssGenerator->generate($data['styles'] ?? []);
+ // ...
+ }
+}
+
+// Infrastructure/Services/WordPressCSSGenerator.php (implementacion)
+final class WordPressCSSGenerator implements CSSGeneratorInterface
+{
+ public function generate(array $styles): string { /* ... */ }
+}
+
+// El DIContainer conecta la interface con la implementacion
+// functions.php o bootstrap
+$container->bind(CSSGeneratorInterface::class, WordPressCSSGenerator::class);
+```
+
+---
+
+## Manejo de Errores WordPress
+
+### Cuando usar wp_die()
+
+```php
+ 403]
+ );
+ }
+
+ // Verificar permisos - error fatal si falla
+ if (!current_user_can('manage_options')) {
+ wp_die(
+ esc_html__('Permission denied', 'roi-theme'),
+ esc_html__('Error', 'roi-theme'),
+ ['response' => 403]
+ );
+ }
+}
+```
+
+### Cuando usar WP_Error
+
+```php
+ 400]
+ );
+ }
+
+ // Operacion BD - puede fallar
+ $result = $this->repository->save($data);
+
+ if ($result === false) {
+ return new \WP_Error(
+ 'save_failed',
+ __('Failed to save component settings', 'roi-theme'),
+ ['status' => 500]
+ );
+ }
+
+ return $data; // Exito
+}
+
+// Uso del WP_Error
+$result = $service->saveComponentSettings($data);
+
+if (is_wp_error($result)) {
+ // Manejar error
+ $errorCode = $result->get_error_code();
+ $errorMessage = $result->get_error_message();
+ // ...
+}
+```
+
+### Cuando usar Excepciones
+
+```php
+value = $value;
+ }
+}
+
+// Infrastructure captura y convierte a WP_Error si es necesario
+try {
+ $componentName = new ComponentName($input);
+} catch (InvalidComponentNameException $e) {
+ return new \WP_Error('invalid_component', $e->getMessage());
+}
+```
+
+---
+
+## Sanitizacion y Validacion
+
+### Tabla de Funciones de Sanitizacion
+
+| Tipo de Dato | Funcion | Ejemplo |
+|--------------|---------|---------|
+| Texto simple | `sanitize_text_field()` | Nombres, titulos |
+| Email | `sanitize_email()` | Direcciones de correo |
+| URL | `esc_url_raw()` | URLs para BD |
+| Entero positivo | `absint()` | IDs, cantidades |
+| Entero (puede ser negativo) | `intval()` | Posiciones, offsets |
+| HTML seguro | `wp_kses_post()` | Contenido con formato |
+| Nombre de archivo | `sanitize_file_name()` | Uploads |
+| Key/slug | `sanitize_key()` | component_name |
+| Clase CSS | `sanitize_html_class()` | Clases dinamicas |
+| Textarea | `sanitize_textarea_field()` | Textos multilinea |
+| Hexadecimal (color) | `sanitize_hex_color()` | Colores |
+
+### Ejemplos de Sanitizacion
+
+```php
+ sanitize_text_field($_POST['name'] ?? ''),
+ 'email' => sanitize_email($_POST['email'] ?? ''),
+ 'phone' => sanitize_text_field($_POST['phone'] ?? ''),
+ 'message' => sanitize_textarea_field($_POST['message'] ?? ''),
+ 'url' => esc_url_raw($_POST['url'] ?? ''),
+ 'post_id' => absint($_POST['post_id'] ?? 0),
+ ];
+
+ // Validar despues de sanitizar
+ if (empty($data['name']) || empty($data['email'])) {
+ wp_send_json_error(['message' => 'Name and email are required']);
+ return;
+ }
+
+ if (!is_email($data['email'])) {
+ wp_send_json_error(['message' => 'Invalid email address']);
+ return;
+ }
+
+ // Procesar datos sanitizados y validados
+ $this->processForm($data);
+
+ wp_send_json_success(['message' => 'Form submitted successfully']);
+}
+```
+
+### Sanitizacion de Arrays y JSON
+
+```php
+ sanitize_key($data['component_name'] ?? ''),
+ 'visibility' => [
+ 'is_enabled' => (bool)($data['visibility']['is_enabled'] ?? false),
+ 'show_on_desktop' => (bool)($data['visibility']['show_on_desktop'] ?? true),
+ 'show_on_mobile' => (bool)($data['visibility']['show_on_mobile'] ?? true),
+ ],
+ 'content' => [
+ 'title' => sanitize_text_field($data['content']['title'] ?? ''),
+ 'description' => wp_kses_post($data['content']['description'] ?? ''),
+ ],
+ 'styles' => [
+ 'background_color' => sanitize_hex_color($data['styles']['background_color'] ?? ''),
+ 'text_color' => sanitize_hex_color($data['styles']['text_color'] ?? ''),
+ 'padding' => absint($data['styles']['padding'] ?? 0),
+ ],
+ ];
+}
+
+// Sanitizar JSON recibido
+public function sanitizeJsonInput(string $json): array
+{
+ $data = json_decode($json, true);
+
+ if (!is_array($data)) {
+ return [];
+ }
+
+ return $this->sanitizeComponentData($data);
+}
+```
+
+---
+
+## Escaping para Output
+
+### Tabla de Funciones de Escaping
+
+| Contexto | Funcion | Cuando usar |
+|----------|---------|-------------|
+| Texto en HTML | `esc_html()` | Contenido entre tags |
+| Atributo HTML | `esc_attr()` | Valores de atributos |
+| URL en href/src | `esc_url()` | Links, imagenes |
+| Textarea value | `esc_textarea()` | Contenido de textarea |
+| HTML permitido | `wp_kses_post()` | Contenido con formato |
+| JavaScript | `esc_js()` | Strings en JS inline |
+| Traduccion + escape | `esc_html__()` | Textos traducibles |
+| Traduccion + attr | `esc_attr__()` | Atributos traducibles |
+
+### Ejemplos de Escaping en Renderers
+
+```php
+getData();
+ $title = $data['content']['title'] ?? '';
+ $description = $data['content']['description'] ?? '';
+ $buttonText = $data['content']['button_text'] ?? 'Submit';
+ $buttonUrl = $data['content']['button_url'] ?? '#';
+ $customClass = $data['styles']['custom_class'] ?? '';
+
+ $html = '