<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: David Bernardo</title>
    <description>The latest articles on DEV Community by David Bernardo (@davidbernardo).</description>
    <link>https://dev.to/davidbernardo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4126877%2Fcdca9f2a-1640-4a31-8be8-8b5afc3d210f.jpg</url>
      <title>DEV Community: David Bernardo</title>
      <link>https://dev.to/davidbernardo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/davidbernardo"/>
    <language>en</language>
    <item>
      <title>Spec-Driven Development (SDD) con IA: Dejando atrás el 'Vibe Coding' para construir software multiplataforma de grado profesional</title>
      <dc:creator>David Bernardo</dc:creator>
      <pubDate>Mon, 21 Sep 2026 21:11:20 +0000</pubDate>
      <link>https://dev.to/davidbernardo/spec-driven-development-sdd-con-ia-dejando-atras-el-vibe-coding-para-construir-software-nd4</link>
      <guid>https://dev.to/davidbernardo/spec-driven-development-sdd-con-ia-dejando-atras-el-vibe-coding-para-construir-software-nd4</guid>
      <description>&lt;p&gt;En los últimos meses, el término &lt;em&gt;"Vibe Coding"&lt;/em&gt; se ha popularizado en redes y comunidades técnicas: sentarte frente al editor, lanzar prompts vagos a un modelo de lenguaje de última generación, ver cómo el código aparece mágicamente y ajustar a prueba y error hasta que "parezca funcionar".&lt;/p&gt;

&lt;p&gt;Para un script de fin de semana o un prototipo desechable, esto puede sentirse como magia. Pero cuando intentas construir &lt;strong&gt;software de producción&lt;/strong&gt;, con concurrencia real, arquitecturas multiplataforma, contratos estrictos y rendimiento de ultra-baja latencia, el "Vibe Coding" se estrella contra la realidad:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;La muerte por mil parches:&lt;/strong&gt; Pides un botón, la IA rompe el estado. Pides arreglar el estado, rompe el ciclo de vida. Pides arreglar el ciclo de vida, introduce fugas de memoria.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La ilusión de avance:&lt;/strong&gt; El código crece exponencialmente, pero la comprensibilidad del sistema cae en picada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Imposibilidad de auditoría:&lt;/strong&gt; Ningún desarrollador humano —ni ningún otro agente de IA en el futuro— puede entender por qué se tomaron ciertas decisiones, cuáles eran los supuestos de diseño o cómo refactorizar sin derrumbar el castillo de naipes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Existe una alternativa profesional, rigurosa y escalable: &lt;strong&gt;Spec-Driven Development (SDD)&lt;/strong&gt; asistido por Inteligencia Artificial.&lt;/p&gt;

&lt;p&gt;En este artículo, desglosaremos la metodología exacta, la arquitectura y los &lt;strong&gt;prompts operativos&lt;/strong&gt; que utilizamos para construir &lt;strong&gt;Virtual Studio Companion&lt;/strong&gt;: una suite multiplataforma con &lt;strong&gt;Kotlin Multiplatform (KMP)&lt;/strong&gt; y &lt;strong&gt;Compose Multiplatform&lt;/strong&gt; que transforma un dispositivo móvil Android en una cámara de transmisión de ultra-baja latencia para &lt;strong&gt;OBS Studio&lt;/strong&gt; en Windows, integrando monitor de video nativo con Skia, código QR ISO/IEC 18004, telemetría bidireccional y cero dependencias de Java en el cliente final.&lt;/p&gt;

&lt;p&gt;Todo el código y sus especificaciones están disponibles en el repositorio de código abierto:&lt;br&gt;&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://github.com/dhbernardo/virtual-studio-companion" rel="noopener noreferrer"&gt;GitHub: dhbernardo/virtual-studio-companion (Tag v1.0.1)&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  1. ¿Qué es Spec-Driven Development (SDD)?
&lt;/h2&gt;

&lt;p&gt;En el desarrollo tradicional, el código fuente suele ser la única fuente de verdad ("el código es la documentación"). Con la llegada de los agentes de IA, esta premisa es peligrosa: los modelos operan sobre ventanas de contexto limitadas y tienden a optimizar soluciones locales a corto plazo sin considerar la integridad global.&lt;/p&gt;

&lt;p&gt;En &lt;strong&gt;Spec-Driven Development (SDD)&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Las especificaciones son documentos vivos y la ÚNICA fuente de verdad (&lt;em&gt;Single Source of Truth&lt;/em&gt;). Ninguna línea de código de producción se escribe o modifica si no está expresamente modelada en su especificación.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A["📜 spec.md&amp;lt;br/&amp;gt;(Requisitos EARS &amp;amp; Criterios BDD)"] --&amp;gt; B["📐 plan.md&amp;lt;br/&amp;gt;(Arquitectura &amp;amp; Diagramas)"]
    B --&amp;gt; C["✅ tasks.md&amp;lt;br/&amp;gt;(Tareas Atómicas 'Hecho cuando:')"]
    C --&amp;gt; D["🧪 TDD Estricto&amp;lt;br/&amp;gt;(Test Primero -&amp;gt; Código -&amp;gt; Build)"]
    D --&amp;gt; E["📦 Conventional Commit&amp;lt;br/&amp;gt;(Trazabilidad 1:1)"]&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  La regla de oro ante defectos y restricciones imprevistas
&lt;/h3&gt;

&lt;p&gt;Cuando durante las pruebas de integración descubrimos una restricción de plataforma (por ejemplo, la política Cleartext de Android o una condición de carrera de concurrencia en sockets), &lt;strong&gt;NO creamos tareas sueltas ni parches desordenados&lt;/strong&gt; (e.g. &lt;code&gt;T09: arreglar socket&lt;/code&gt;, &lt;code&gt;T10: parche de botones&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Crear tareas sueltas convierte el repositorio en un historial caótico de parches y debilita la especificación principal. &lt;strong&gt;Lo canónico en SDD es refinar las especificaciones y tareas existentes&lt;/strong&gt; en su contexto original (&lt;code&gt;spec.md&lt;/code&gt;, &lt;code&gt;plan.md&lt;/code&gt;, &lt;code&gt;tasks.md&lt;/code&gt;). De este modo, cualquier ingeniero o agente que audite el sistema meses después encontrará la especificación exacta, autocontenida y fiel a la realidad.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. El Marco de Gobernanza: &lt;code&gt;docs/constitution.md&lt;/code&gt; y &lt;code&gt;AGENTS.md&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Para que un agente de IA trabaje con autonomía de alto nivel sin descarriarse, necesita barreras innegociables. En nuestro proyecto, esto se implementó a través de dos archivos fundamentales:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. La Constitución del Proyecto (&lt;code&gt;docs/constitution.md&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Establece las &lt;strong&gt;8 leyes fundamentales&lt;/strong&gt; que ningún prompt ni decisión técnica puede violar:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Principio&lt;/th&gt;
&lt;th&gt;Regla Innegociable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1. Núcleo Puro (&lt;code&gt;commonMain&lt;/code&gt;)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Prohibido importar &lt;code&gt;android.*&lt;/code&gt;, &lt;code&gt;java.*&lt;/code&gt; o librerías de plataforma en el código compartido. Todo se resuelve mediante Puertos e Interfaces.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2. Cero Java en el Cliente&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;El ejecutable de Windows se empaqueta con &lt;code&gt;jpackage&lt;/code&gt; y un micro-runtime Temurin (&lt;code&gt;jlink&lt;/code&gt;). El usuario final no necesita Java.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. Licenciamiento Permisivo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Prohibidas librerías con copyleft restrictivo (GPLv3). Solo Apache 2.0, MIT, BSD.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4. Resiliencia de Red&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nunca asumir que mDNS funciona (por routers con &lt;em&gt;AP Isolation&lt;/em&gt;). El código QR es el mecanismo primario incondicional.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;5. Paridad de Temas&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Soporte estricto para modo oscuro (&lt;code&gt;StudioDarkColors&lt;/code&gt;) y modo claro (&lt;code&gt;StudioLightColors&lt;/code&gt;), detectando el sistema (&lt;code&gt;isSystemInDarkTheme()&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;6. SDD y TDD Primero&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No se implementa código sin prueba unitaria previa que valide el comportamiento esperado.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;7. CI/CD sin Fricción&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pipelines automatizados en GitHub Actions para Linux (&lt;code&gt;app-debug.apk&lt;/code&gt;) y Windows (&lt;code&gt;.exe&lt;/code&gt; / &lt;code&gt;.msi&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;8. Cero Hardcoded Strings&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Prohibido escribir texto literal en UI o composables. El 100% de textos proviene de &lt;code&gt;Res.string.*&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  2. El Contexto del Agente (&lt;code&gt;AGENTS.md&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Define la topología del monorepo, los comandos frecuentes (&lt;code&gt;./gradlew allTests&lt;/code&gt;, &lt;code&gt;./gradlew :desktopApp:desktopTest&lt;/code&gt;), el flujo de trabajo SDD y la prohibición expresa de improvisar código.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. La Anatomía de una Especificación SDD (&lt;code&gt;specs/&amp;lt;feature&amp;gt;/&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Cada módulo o funcionalidad del sistema vive dentro de &lt;code&gt;specs/&lt;/code&gt; y se descompone en tres artefactos complementarios:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;specs/003-desktop-windows-host/
├── spec.md      # Requisitos del Negocio y Criterios de Aceptación
├── plan.md      # Diseño Técnico, Diagramas y Decisiones Arquitectónicas
└── tasks.md     # Checklist de Tareas Incrementales y Verificables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A. &lt;code&gt;spec.md&lt;/code&gt;: Requisitos EARS y Criterios BDD
&lt;/h3&gt;

&lt;p&gt;Los requisitos se redactan usando la sintaxis &lt;strong&gt;EARS&lt;/strong&gt; (&lt;em&gt;Easy Approach to Requirements Syntax&lt;/em&gt;) y criterios de aceptación &lt;strong&gt;BDD&lt;/strong&gt; (&lt;em&gt;Dado-Cuando-Entonces&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### RF-003: Integración Automática, Control Simétrico y Resiliencia con OBS Studio&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Tipo:**&lt;/span&gt; Event-driven
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Definición:**&lt;/span&gt; CUANDO el Host detecte que OBS Studio está en ejecución (o al pulsar "Conectar OBS"), el sistema DEBE conectarse al puerto 4455 implementando IObsConnector, resolver el desafío SHA256 y crear/actualizar la fuente Browser Source "Virtual Studio Camera".
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Criterio de Aceptación:**&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="gs"&gt;**DADO QUE**&lt;/span&gt; OBS Studio está conectado activamente
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="gs"&gt;**CUANDO**&lt;/span&gt; el usuario pulsa "Desconectar OBS" en el dashboard
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="gs"&gt;**ENTONCES**&lt;/span&gt; el Host debe cerrar limpiamente el socket con OBS, transitar a DISCONNECTED y cancelar reintentos, preservando intacta la transmisión móvil activa y el monitor de retorno Skia.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  B. &lt;code&gt;plan.md&lt;/code&gt;: Diagramas de Secuencia y Arquitectura
&lt;/h3&gt;

&lt;p&gt;Incluye diagramas Mermaid que modelan el flujo de estados y la secuencia de comunicación entre el streamer, el Host Ktor, la app Android y OBS Studio:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    autonumber
    actor Streamer as Streamer (Windows)
    participant Host as Desktop Host (Compose + Ktor)
    participant Phone as Android App
    participant OBS as OBS Studio (:4455)

    Host-&amp;gt;&amp;gt;Host: Enlaza Ktor (8080-8090) &amp;amp; Genera QR con TTL (120s)
    Phone-&amp;gt;&amp;gt;Host: Escanea QR y envía HANDSHAKE_INIT(token)
    Host-&amp;gt;&amp;gt;Phone: HANDSHAKE_ACK (Sesión validada)
    Host-&amp;gt;&amp;gt;OBS: CreateInput / SetInputSettings ("browser_source")
    Phone-&amp;gt;&amp;gt;Host: Transmite tramas JPEG binarias (/ws/stream)
    Host-&amp;gt;&amp;gt;Host: Decodifica con Skia (Image.makeFromEncoded)
    Host-&amp;gt;&amp;gt;Streamer: Renderiza Monitor de Retorno 16:9 en Compose Desktop
    Host-&amp;gt;&amp;gt;OBS: Sirve stream multipart (/stream/mjpeg)&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  C. &lt;code&gt;tasks.md&lt;/code&gt;: La Condición &lt;em&gt;"Hecho cuando:"&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;Cada tarea atómica define una condición estricta e incontrovertible. Un checkbox &lt;code&gt;[x]&lt;/code&gt; no se marca por intuición; solo se marca si la condición se cumple y la terminal lo certifica:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; [x] &lt;span class="gs"&gt;**T04: Implementación de `ObsWebSocketAdapter` implementando `IObsConnector`**&lt;/span&gt;  
  &lt;span class="ge"&gt;*Requisitos cubiertos:*&lt;/span&gt; &lt;span class="sb"&gt;`RF-003`&lt;/span&gt;, &lt;span class="sb"&gt;`RNF-003`&lt;/span&gt;  
  &lt;span class="ge"&gt;*Hecho cuando:*&lt;/span&gt; El adaptador resuelva el handshake autenticado en &lt;span class="sb"&gt;`ws://localhost:4455`&lt;/span&gt;, implemente &lt;span class="sb"&gt;`KtorObsTransport`&lt;/span&gt; con buffer de repetición (&lt;span class="sb"&gt;`replay = 1`&lt;/span&gt;) previniendo pérdida del frame &lt;span class="sb"&gt;`op: 0`&lt;/span&gt;, soporte desconexión explícita (&lt;span class="sb"&gt;`disconnect()`&lt;/span&gt;) sin bucles en &lt;span class="sb"&gt;`finally`&lt;/span&gt;, configure de forma idempotente la fuente Browser Source &lt;span class="sb"&gt;`Virtual Studio Camera`&lt;/span&gt;, y apruebe las pruebas de &lt;span class="sb"&gt;`T03`&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. El Kit de Prompts Operativos (Copia y Pega)
&lt;/h2&gt;

&lt;p&gt;Aquí radica el secreto para que una IA no degrade el proyecto. Diseñamos &lt;strong&gt;cuatro prompts clave&lt;/strong&gt; para cada momento del ciclo de vida del software:&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt 1: Ejecución Secuencial Completa (Batch SDD)
&lt;/h3&gt;

&lt;p&gt;Utilízalo cuando la especificación, el plan y las tareas estén completamente definidos y desees que el agente ejecute con rigor senior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Actúa como un desarrollador senior y ejecuta de forma autónoma y secuencial TODAS las tareas pendientes de &lt;span class="sb"&gt;`[NUMERO-NOMBRE-SPEC]/tasks.md`&lt;/span&gt;.

Antes de comenzar:
&lt;span class="p"&gt;1.&lt;/span&gt; Lee &lt;span class="sb"&gt;`docs/constitution.md`&lt;/span&gt; y &lt;span class="sb"&gt;`AGENTS.md`&lt;/span&gt; (respetando todas las restricciones: cero strings en crudo, licencias permisivas, independencia de &lt;span class="sb"&gt;`shared/commonMain`&lt;/span&gt;).
&lt;span class="p"&gt;2.&lt;/span&gt; Lee &lt;span class="sb"&gt;`[NUMERO-NOMBRE-SPEC]/plan.md`&lt;/span&gt; y &lt;span class="sb"&gt;`[NUMERO-NOMBRE-SPEC]/spec.md`&lt;/span&gt;.

Para CADA tarea pendiente en &lt;span class="sb"&gt;`tasks.md`&lt;/span&gt;, ejecuta en bucle estricto:
&lt;span class="p"&gt;1.&lt;/span&gt; IDENTIFICACIÓN:
&lt;span class="p"&gt;   -&lt;/span&gt; Identifica los requisitos (RF/RNF) y la condición "Hecho cuando:".
&lt;span class="p"&gt;   -&lt;/span&gt; Clasifica la tarea:
     a) ¿Es Lógica de Dominio, Protocolo, Caso de Uso o ViewModel? -&amp;gt; APLICA TDD ESTRICTO (Paso 2A).
     b) ¿Es Infraestructura de Build, CI/CD, Tokens o Recursos? -&amp;gt; APLICA VERIFICACIÓN DE BUILD (Paso 2B).
&lt;span class="p"&gt;
2.&lt;/span&gt; EJECUCIÓN SEGÚN EL TIPO:
&lt;span class="p"&gt;   -&lt;/span&gt; [Paso 2A - TDD]:
&lt;span class="p"&gt;     1.&lt;/span&gt; Escribe primero las pruebas unitarias pertinentes (&lt;span class="sb"&gt;`*Test.kt`&lt;/span&gt;) cubriendo casos normales, límites y de error.
&lt;span class="p"&gt;     2.&lt;/span&gt; Implementa el código mínimo para hacer pasar los tests.
&lt;span class="p"&gt;     3.&lt;/span&gt; Ejecuta el comando de test correspondiente (ej. &lt;span class="sb"&gt;`./gradlew :shared:allTests`&lt;/span&gt; o &lt;span class="sb"&gt;`./gradlew :desktopApp:desktopTest`&lt;/span&gt;).
&lt;span class="p"&gt;   -&lt;/span&gt; [Paso 2B - Infraestructura / Configuración]:
&lt;span class="p"&gt;     1.&lt;/span&gt; Implementa los archivos de configuración, scripts de build o recursos.
&lt;span class="p"&gt;     2.&lt;/span&gt; Ejecuta el comando de validación o compilación (ej. &lt;span class="sb"&gt;`./gradlew assembleDebug`&lt;/span&gt; o linters).
&lt;span class="p"&gt;
3.&lt;/span&gt; EVIDENCIA EN RESPUESTA:
&lt;span class="p"&gt;   -&lt;/span&gt; Muestra el extracto de la terminal que certifique el éxito (&lt;span class="sb"&gt;`BUILD SUCCESSFUL`&lt;/span&gt; o &lt;span class="sb"&gt;`Tests PASSED`&lt;/span&gt;).
&lt;span class="p"&gt;
4.&lt;/span&gt; ACTUALIZACIÓN DE TASKS:
&lt;span class="p"&gt;   -&lt;/span&gt; Marca con &lt;span class="sb"&gt;`[x]`&lt;/span&gt; el checkbox en &lt;span class="sb"&gt;`tasks.md`&lt;/span&gt; ÚNICAMENTE si se cumple la condición "Hecho cuando:".
&lt;span class="p"&gt;
5.&lt;/span&gt; GIT COMMIT:
&lt;span class="p"&gt;   -&lt;/span&gt; Crea un commit con formato Conventional Commits en inglés (según &lt;span class="sb"&gt;`git-conventions`&lt;/span&gt;).

AL FINALIZAR TODAS LAS TAREAS DEL SPEC:
&lt;span class="p"&gt;-&lt;/span&gt; Presenta la Matriz de Trazabilidad de Requisitos.
&lt;span class="p"&gt;-&lt;/span&gt; Confirma que no se vulneró ningún Principio de &lt;span class="sb"&gt;`docs/constitution.md`&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Prompt 2: Ejecución Tarea por Tarea (Paso a Paso Supervisado)
&lt;/h3&gt;

&lt;p&gt;Ideal para desarrolladores que prefieren un control minucioso antes de cada avance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Actúa como un desarrollador senior.
Lee &lt;span class="sb"&gt;`docs/constitution.md`&lt;/span&gt;, &lt;span class="sb"&gt;`AGENTS.md`&lt;/span&gt;, y el trío &lt;span class="sb"&gt;`specs/[MODULO]/spec.md`&lt;/span&gt;, &lt;span class="sb"&gt;`plan.md`&lt;/span&gt; y &lt;span class="sb"&gt;`tasks.md`&lt;/span&gt;.

Ejecuta EXCLUSIVAMENTE la tarea [ID_TAREA] de &lt;span class="sb"&gt;`tasks.md`&lt;/span&gt;.
&lt;span class="p"&gt;1.&lt;/span&gt; Aplica TDD estricto si es lógica/UI: escribe el test unitario primero, hazlo fallar, implementa el código mínimo para que pase.
&lt;span class="p"&gt;2.&lt;/span&gt; Ejecuta el comando de verificación en terminal correspondiente.
&lt;span class="p"&gt;3.&lt;/span&gt; Cumplida la condición "Hecho cuando:", marca &lt;span class="sb"&gt;`[x]`&lt;/span&gt; en &lt;span class="sb"&gt;`tasks.md`&lt;/span&gt;.
&lt;span class="p"&gt;4.&lt;/span&gt; Crea el commit atómico en inglés siguiendo Conventional Commits.
&lt;span class="p"&gt;5.&lt;/span&gt; DETENTE y espera mi confirmación antes de proceder con la siguiente tarea.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Prompt 3: El Diagnóstico Crítico ("Cero Parches Prematuros")
&lt;/h3&gt;

&lt;p&gt;Cuando ocurre un error durante las pruebas manuales o automatizadas, &lt;strong&gt;nunca le pidas a la IA que lo arregle de inmediato&lt;/strong&gt;. Primero aíslala para que analice la causa raíz:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;[Pega aquí los logs de terminal, trazas de excepción, comportamiento anómalo observado o contexto de la prueba realizada]

Identifica los problemas, no plantees soluciones todavía. Solicita si requieres más contexto, trazas o mayor detalle de lo que está ocurriendo.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;¿Por qué este prompt es vital?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Si le dices a la IA &lt;em&gt;"tengo este error, arréglalo"&lt;/em&gt;, asumirá una solución apresurada que casi siempre consiste en parchotear el código local, rompiendo contratos arquitectónicos. Al forzarla a limitarse a diagnosticar, la IA analiza el flujo de llamadas, identifica condiciones de carrera o inconsistencias de estado, y tú como desarrollador validas la hipótesis técnica antes de que toque un solo archivo.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Prompt 4: El Refinamiento SDD Canónico (Sin Tareas Sueltas)
&lt;/h3&gt;

&lt;p&gt;Una vez identificado y consensuado el problema, utiliza este prompt para actualizar la especificación:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Crear un plan para refinar las especificaciones y tareas existentes, planes, evitar crear tareas sueltas, ya que esto convierte el proyecto en un historial caótico de parches y debilita la especificación principal (rompiendo las reglas de SDD).
De esta manera, cualquier desarrollador o agente que en el futuro vuelva a implementar o auditar el sistema tendrá la especificación exacta y autocontenida.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Caso de Estudio Real: Virtual Studio Companion
&lt;/h2&gt;

&lt;p&gt;Para ilustrar el poder de esta metodología, veamos cómo resolvimos &lt;strong&gt;cuatro desafíos de alta complejidad&lt;/strong&gt; durante el desarrollo de nuestro sistema.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué hace la aplicación?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Móvil Android:&lt;/strong&gt; Captura video a 1080p con &lt;strong&gt;CameraX&lt;/strong&gt;, comprime cuadros a JPEG aplicando la matriz de rotación del sensor en un subproceso dedicado (&lt;code&gt;CameraX-Worker&lt;/code&gt;), calcula FPS/bitrate reales en una ventana deslizante de 1000 ms, y transmite tramas binarias por WebSocket hacia el Host. Escanea códigos QR con &lt;strong&gt;Google ML Kit&lt;/strong&gt; acelerado por hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desktop Windows (Host):&lt;/strong&gt; Servidor local embebido con &lt;strong&gt;Ktor&lt;/strong&gt; (fallback automático en puertos &lt;code&gt;8080-8090&lt;/code&gt;). Genera códigos QR conforme a &lt;strong&gt;ISO/IEC 18004&lt;/strong&gt; con &lt;strong&gt;ZXing Core&lt;/strong&gt;. Decodifica las tramas de video en tiempo real mediante &lt;strong&gt;Skia&lt;/strong&gt; (&lt;code&gt;org.jetbrains.skia.Image.makeFromEncoded&lt;/code&gt;) para renderizar un &lt;strong&gt;Monitor de Retorno de Video Nativo&lt;/strong&gt; en Compose Desktop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integración con OBS Studio:&lt;/strong&gt; Sirve un stream multipart HTTP continuo en &lt;code&gt;/stream/mjpeg&lt;/code&gt; y una página web responsive en &lt;code&gt;/stream/preview&lt;/code&gt;. Se comunica vía WebSocket con &lt;strong&gt;OBS Studio v5&lt;/strong&gt;, resolviendo desafíos criptográficos SHA256 para inyectar automáticamente una fuente &lt;em&gt;Browser Source&lt;/em&gt; en la escena activa.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sincronización Bidireccional:&lt;/strong&gt; Control remoto de zoom y linterna en ambas direcciones sin bucles de rebote (&lt;em&gt;echo loops&lt;/em&gt;), junto a telemetría viva de batería, estado térmico y latencia RTT (Ping/Pong).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fprkntg9l49ptnl5wf5q9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fprkntg9l49ptnl5wf5q9.png" alt="Dashboard de Transmisión del Host con Monitor Skia" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Caso 1: La Política Cleartext y el Visor Perforado en Android
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;El problema:&lt;/strong&gt; Al intentar conectar por primera vez el teléfono al Host por Wi-Fi, Android abortaba con &lt;code&gt;UnknownServiceException: CLEARTEXT communication not permitted&lt;/code&gt;. Además, el visor de escaneo de QR renderizaba un recuadro negro sólido sobre la cámara.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El enfoque "Vibe Coding" habría hecho:&lt;/strong&gt; Agregar un flag suelto en el manifest y poner hacks en la vista.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cómo se resolvió en SDD:&lt;/strong&gt;

&lt;ol&gt;
&lt;li&gt;Se refinó &lt;code&gt;RF-001&lt;/code&gt; en &lt;code&gt;specs/004-android-mobile-app/spec.md&lt;/code&gt; para abarcar el soporte de contingencia manual (ingreso de IP y token) y la política de red local como requisito no funcional formal (&lt;strong&gt;&lt;code&gt;RNF-005&lt;/code&gt;&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Se implementó &lt;code&gt;network_security_config.xml&lt;/code&gt; habilitando tráfico local en redes privadas.&lt;/li&gt;
&lt;li&gt;En &lt;code&gt;ScannerOverlay&lt;/code&gt;, se descubrió que &lt;code&gt;BlendMode.Clear&lt;/code&gt; perforaba las capas de Compose hasta el fondo negro nativo. Se refinó la tarea canónica &lt;code&gt;T08&lt;/code&gt; para aplicar &lt;code&gt;Modifier.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)&lt;/code&gt;, logrando un recorte translúcido al 50% con nitidez cristalina.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpqcxi2z2d3a9bnxhnb69.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpqcxi2z2d3a9bnxhnb69.png" alt="Dashboard de Transmisión del Host con Monitor Skia" width="800" height="1778"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Caso 2: El Rebote Reactivo en el Arranque de Sesión
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;El problema:&lt;/strong&gt; Al enfocar el código QR, la app móvil detectaba el token, abría la pantalla de streaming y de inmediato rebotaba hacia la pantalla de escaneo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El diagnóstico con Prompt 3:&lt;/strong&gt;
La pantalla &lt;code&gt;CameraScreen&lt;/code&gt; observaba &lt;code&gt;uiState.connectionState&lt;/code&gt;. Como el ViewModel nacía por defecto en &lt;code&gt;Disconnected&lt;/code&gt; y la corrutina de conexión demoraba unos milisegundos en arrancar, el observador interpretaba el estado inicial como una desconexión y llamaba a &lt;code&gt;onDisconnect()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El refinamiento con Prompt 4:&lt;/strong&gt;
Se actualizaron las tareas canónicas &lt;code&gt;T06&lt;/code&gt; y &lt;code&gt;T07&lt;/code&gt; de &lt;code&gt;004-android-mobile-app&lt;/code&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CameraStreamViewModel&lt;/code&gt; ahora acepta &lt;code&gt;initialConfig: PairingConfig?&lt;/code&gt; y nace de inmediato en estado &lt;code&gt;ConnectionState.Pairing(config)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Se incorporó un guardián reactivo (&lt;code&gt;hasActiveSession&lt;/code&gt;) que solo permite ejecutar &lt;code&gt;onDisconnect()&lt;/code&gt; si la sesión estuvo previamente en &lt;code&gt;Connected&lt;/code&gt; o &lt;code&gt;Streaming&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwhx9fo4xbe2ztdf9it3f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwhx9fo4xbe2ztdf9it3f.png" alt="HUD de Estudio en Android con Controles Nítidos" width="800" height="1778"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Caso 3: La Condición de Carrera en el WebSocket de OBS (&lt;code&gt;replay = 1&lt;/code&gt;)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;El problema:&lt;/strong&gt; Si el usuario conectaba OBS antes de emparejar el móvil, funcionaba perfecto. Pero si emparejaba el móvil primero y luego pulsaba "Conectar OBS", el Host caía invariablemente en &lt;code&gt;OBS Error&lt;/code&gt; y &lt;code&gt;Conectando...&lt;/code&gt;, a pesar de que el stream de video seguía activo en OBS.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;El diagnóstico con Prompt 3:&lt;/strong&gt;  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;En &lt;code&gt;KtorObsTransport&lt;/code&gt;, el flujo de mensajes entrantes estaba configurado como:
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt; &lt;span class="c1"&gt;// ❌ ANTES: replay = 0 por defecto&lt;/span&gt;
 &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;_incoming&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableSharedFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;extraBufferCapacity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Cuando el móvil ya está transmitiendo video, el despachador de hilos de la CPU se encuentra bajo carga procesando tramas binarias y decodificando imágenes Skia.&lt;/li&gt;
&lt;li&gt;Al pulsar "Conectar OBS", el socket se abría y OBS Studio respondía de inmediato con el frame inicial &lt;code&gt;op: 0 (Hello)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Pero el recolector de corrutinas (&lt;code&gt;transport.incoming.collect&lt;/code&gt;) tardaba unos pocos milisegundos en suscribirse. Al tener &lt;code&gt;replay = 0&lt;/code&gt;, el mensaje &lt;code&gt;op: 0&lt;/code&gt; se descartaba.&lt;/li&gt;
&lt;li&gt;El adaptador se quedaba esperando un saludo que nunca llegaría, alcanzaba el timeout de 4 segundos y caía en error.&lt;/li&gt;
&lt;li&gt;En las pruebas unitarias pasaba porque el mock &lt;code&gt;FakeObsTransport&lt;/code&gt; tenía por casualidad &lt;code&gt;replay = 1&lt;/code&gt; configurado.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;El refinamiento con Prompt 4:&lt;/strong&gt;
Se refinaron &lt;code&gt;RF-003&lt;/code&gt; y las tareas &lt;code&gt;T03&lt;/code&gt; y &lt;code&gt;T04&lt;/code&gt; en &lt;code&gt;specs/003-desktop-windows-host/&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// ✅ DESPUÉS: replay = 1 garantizado y suscripción atómica previa&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;_incoming&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableSharedFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;replay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extraBufferCapacity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Además, la suscripción al canal se realiza antes de abrir el socket de red y bajo protección de un &lt;code&gt;Mutex&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3n62ukzx5k98bqzz3qos.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3n62ukzx5k98bqzz3qos.png" alt="Configuración Idempotente de Browser Source en OBS Studio" width="800" height="993"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Caso 4: Desconexión Simétrica e Independiente de OBS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;El problema:&lt;/strong&gt; Cuando OBS se conectaba, el botón "Conectar OBS" desaparecía de la interfaz y no había forma de desconectarlo. El único botón visible era "Desconectar", pero ese botón cerraba la sesión del móvil y devolvía la app al código QR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El refinamiento SDD:&lt;/strong&gt;
Se actualizaron &lt;code&gt;RF-003&lt;/code&gt;, &lt;code&gt;RF-004&lt;/code&gt;, &lt;code&gt;T05&lt;/code&gt; y &lt;code&gt;T06&lt;/code&gt;:

&lt;ul&gt;
&lt;li&gt;Se implementó &lt;code&gt;DesktopHostViewModel.disconnectObs()&lt;/code&gt; desacoplado de &lt;code&gt;disconnectSession()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Se diseñaron botones contextuales en Compose Desktop: si está conectado, muestra &lt;code&gt;Desconectar OBS&lt;/code&gt; (&lt;code&gt;Res.string.desktop_action_disconnect_obs&lt;/code&gt;); si está desconectado, muestra &lt;code&gt;Conectar OBS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;El botón rojo general queda reservado para la sesión del teléfono.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdawzniypoenzkge3cuql.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdawzniypoenzkge3cuql.png" alt="Pantalla de Emparejamiento QR con Token Legible y Estado OBS" width="800" height="745"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  6. El Flujo Git: Tareas Atómicas y Conventional Commits
&lt;/h2&gt;

&lt;p&gt;En un flujo SDD profesional, Git no es un vertedero de cambios acumulados. Cada commit representa la culminación verificable de una tarea o refinamiento:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Historial real del repositorio Virtual Studio Companion&lt;/span&gt;
58deef0 feat&lt;span class="o"&gt;(&lt;/span&gt;desktop&lt;span class="o"&gt;)&lt;/span&gt;: support independent obs disconnect and fix websocket transport race condition
66c9063 docs&lt;span class="o"&gt;(&lt;/span&gt;specs&lt;span class="o"&gt;)&lt;/span&gt;: refine obs connection and disconnect specifications and tasks
81e14c8 feat&lt;span class="o"&gt;(&lt;/span&gt;android&lt;span class="o"&gt;)&lt;/span&gt;: implement jpeg frame streaming, live telemetry, and camera controls synchronization
0aaff28 feat&lt;span class="o"&gt;(&lt;/span&gt;desktop&lt;span class="o"&gt;)&lt;/span&gt;: implement video frame ingestion, skia &lt;span class="k"&gt;return &lt;/span&gt;monitor, and mjpeg preview &lt;span class="k"&gt;for &lt;/span&gt;obs
36e58fc docs&lt;span class="o"&gt;(&lt;/span&gt;specs&lt;span class="o"&gt;)&lt;/span&gt;: refine video streaming and camera controls &lt;span class="nb"&gt;sync &lt;/span&gt;specifications
8395458 fix&lt;span class="o"&gt;(&lt;/span&gt;android&lt;span class="o"&gt;)&lt;/span&gt;: prevent premature screen &lt;span class="nb"&gt;exit &lt;/span&gt;and initialize CameraStreamViewModel &lt;span class="k"&gt;in &lt;/span&gt;pairing state
75e5b79 docs&lt;span class="o"&gt;(&lt;/span&gt;specs&lt;span class="o"&gt;)&lt;/span&gt;: refine mobile pairing initialization and transition guard &lt;span class="k"&gt;in &lt;/span&gt;spec and tasks
2a64def docs&lt;span class="o"&gt;(&lt;/span&gt;specs&lt;span class="o"&gt;)&lt;/span&gt;: refine desktop, android, and design system specs and tasks
0c9d6dc fix&lt;span class="o"&gt;(&lt;/span&gt;desktop&lt;span class="o"&gt;)&lt;/span&gt;: handle client disconnect, restore session disconnect and obs connect &lt;span class="k"&gt;in &lt;/span&gt;dashboard
a2db8ef fix&lt;span class="o"&gt;(&lt;/span&gt;android&lt;span class="o"&gt;)&lt;/span&gt;: consume scanned config, handle server disconnect and lifecycle events
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Observemos la simetría: &lt;strong&gt;cada cambio de código (&lt;code&gt;feat&lt;/code&gt; o &lt;code&gt;fix&lt;/code&gt;) está precedido o acompañado por su correspondiente actualización en especificaciones (&lt;code&gt;docs(specs)&lt;/code&gt;)&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Conclusiones: El Futuro del Desarrollo Asistido por IA
&lt;/h2&gt;

&lt;p&gt;El debate actual no debería ser si los modelos de lenguaje pueden escribir código rápido —ya sabemos que pueden—. El verdadero desafío es &lt;strong&gt;cómo gobernar a la IA para construir sistemas duraderos, tolerantes a fallos y arquitectónicamente limpios&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Al adoptar &lt;strong&gt;Spec-Driven Development (SDD)&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;La IA se convierte en un multiplicador de ingeniería:&lt;/strong&gt; En lugar de perder horas persiguiendo regresiones aleatorias, guías a la IA a través de contratos estrictos, TDD y validación determinista en terminal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El proyecto es 100% auditable:&lt;/strong&gt; Cualquier ingeniero humano o nuevo agente puede inspeccionar &lt;code&gt;specs/&lt;/code&gt;, leer los requisitos EARS, revisar los diagramas de secuencia y entender la totalidad del sistema en minutos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El código resiste el paso del tiempo:&lt;/strong&gt; La arquitectura Clean y la separación de capas garantizan que el núcleo multiplataforma permanezca puro e inmune a las vicisitudes de las APIs de plataforma.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Recursos y Código Fuente
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Repositorio completo (Kotlin 2.x, Compose Multiplatform, Ktor, CameraX, Skia, OBS WebSocket v5):
🔗 &lt;strong&gt;&lt;a href="https://github.com/dhbernardo/virtual-studio-companion" rel="noopener noreferrer"&gt;https://github.com/dhbernardo/virtual-studio-companion&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Tag de la versión estable analizada: &lt;code&gt;v1.0.1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;¿Has probado implementar Spec-Driven Development en tus flujos de trabajo con agentes de IA? ¡Déjame tus comentarios, dudas o experiencias abajo!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>kotlin</category>
      <category>compose</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Spec-Driven Development (SDD) en Acción: Conectando una Extensión de Chrome (MV3) con Windows usando Node.js</title>
      <dc:creator>David Bernardo</dc:creator>
      <pubDate>Sun, 20 Sep 2026 04:56:14 +0000</pubDate>
      <link>https://dev.to/davidbernardo/spec-driven-development-sdd-en-accion-conectando-una-extension-de-chrome-mv3-con-windows-4iei</link>
      <guid>https://dev.to/davidbernardo/spec-driven-development-sdd-en-accion-conectando-una-extension-de-chrome-mv3-con-windows-4iei</guid>
      <description>&lt;p&gt;¿Alguna vez has necesitado que una extensión de Google Chrome interactúe directamente con el hardware de la máquina anfitriona, imprima en impresoras térmicas de tickets, lea puertos serie o extraiga telemetría en tiempo real del sistema operativo?&lt;/p&gt;

&lt;p&gt;Si lo has intentado, probablemente te hayas topado con la jaula de cristal: &lt;strong&gt;el sandbox del navegador&lt;/strong&gt;. Por motivos evidentes de seguridad, una extensión web no puede invocar libremente comandos del sistema ni acceder a la memoria de la máquina.&lt;/p&gt;

&lt;p&gt;Para romper este aislamiento de forma controlada, Google provee la API &lt;strong&gt;Chrome Native Messaging&lt;/strong&gt;. Sin embargo, cualquiera que haya intentado implementarla en un entorno de producción para Windows sabe que es un campo minado de errores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un simple &lt;code&gt;console.log&lt;/code&gt; en el host nativo &lt;strong&gt;corrompe el socket binario&lt;/strong&gt; y el navegador desconecta la extensión al instante sin dar pistas.&lt;/li&gt;
&lt;li&gt;Obligar al usuario final a tener &lt;strong&gt;Node.js o Python preinstalado&lt;/strong&gt; en su máquina arruina la adopción.&lt;/li&gt;
&lt;li&gt;Exigir al usuario que abra &lt;code&gt;regedit.exe&lt;/code&gt; para registrar claves en Windows es inviable para usuarios no técnicos.&lt;/li&gt;
&lt;li&gt;En la era de la IA, pedirle a un LLM que construya este sistema sin una metodología estricta casi siempre termina en alucinaciones y código incompatible entre subsistemas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En este artículo veremos cómo resolvimos este desafío de ingeniería aplicando &lt;strong&gt;Spec-Driven Development (SDD)&lt;/strong&gt;: diseñamos un sistema completo de &lt;strong&gt;Monitoreo de Hardware (CPU, RAM y Uptime)&lt;/strong&gt; compuesto por una extensión en &lt;strong&gt;Manifest V3&lt;/strong&gt;, un ejecutable nativo &lt;code&gt;.exe&lt;/code&gt; autocontenido en Node.js y un instalador automatizado en &lt;strong&gt;Inno Setup 6&lt;/strong&gt;, con &lt;strong&gt;cero dependencias externas y 100% de pruebas automatizadas&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsfgrp6x2gtm2krlw4xz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsfgrp6x2gtm2krlw4xz.png" alt="System Monitor Extension Popup en Tiempo Real" width="512" height="605"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figura 1: Popup de la extensión System Monitor en Google Chrome mostrando telemetría de hardware en tiempo real (UI oscura con glassmorphism).&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🏛️ 1. Spec-Driven Development (SDD): El Antídoto contra el Caos
&lt;/h2&gt;

&lt;p&gt;Cuando desarrollas un proyecto con tecnologías heterogéneas (Frontend Web MV3 + Backend nativo PE x64 + Scripts de instalación en Pascal y claves del Registro de Windows), &lt;strong&gt;empezar tirando código es la receta garantizada para el fracaso&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;En lugar de improvisar, adoptamos &lt;strong&gt;Spec-Driven Development (SDD)&lt;/strong&gt;. La regla de oro es simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Ningún archivo de código fuente se crea ni se modifica si el cambio no está previamente especificado en un contrato formal."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  La Constitución del Proyecto
&lt;/h3&gt;

&lt;p&gt;Todo el proyecto está gobernado por una constitución innegociable (&lt;a href="https://github.com/dhbernardo/chrome-native-messaging-sdd/blob/main/docs/constitution.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/constitution.md&lt;/code&gt;&lt;/a&gt;) que define 7 principios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Protocolo Estricto de 4 Bytes:&lt;/strong&gt; Toda comunicación por &lt;code&gt;stdin&lt;/code&gt;/&lt;code&gt;stdout&lt;/code&gt; debe anteponer un prefijo little-endian &lt;code&gt;UInt32LE&lt;/code&gt;. Prohibido emitir texto plano o logs a &lt;code&gt;stdout&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cero Dependencia de Node.js en el Cliente:&lt;/strong&gt; El host debe compilarse como binario &lt;code&gt;.exe&lt;/code&gt; x64 independiente.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instalación Transparente y Automatizada:&lt;/strong&gt; Registro en Windows (&lt;code&gt;HKCU\Software\Google\Chrome\NativeMessagingHosts&lt;/code&gt;) gestionado 100% por el instalador.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compatibilidad Estricta con Manifest V3:&lt;/strong&gt; Operación exclusiva mediante Service Workers asíncronos sin background pages obsoletas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aislamiento de Errores y Resiliencia:&lt;/strong&gt; Captura de desconexiones y timeouts con degradación visual elegante sin bloquear el navegador.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desarrollo Guiado por Especificación (SDD):&lt;/strong&gt; Trazabilidad documental de cada requisito mediante sintaxis EARS (&lt;em&gt;Easy Approach to Requirements Syntax&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arquitectura Hexagonal (Puertos y Adaptadores):&lt;/strong&gt; El dominio puro debe estar aislado de &lt;code&gt;node:os&lt;/code&gt;, &lt;code&gt;process.stdin&lt;/code&gt; o &lt;code&gt;chrome.runtime&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  La Topología del Monorepo
&lt;/h3&gt;

&lt;p&gt;Siguiendo las directrices constitucionales, el repositorio se organizó en 3 subsistemas independientes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chrome-native-messaging-sdd/
├── host/                             # SUBSISTEMA 1: Host Nativo en Node.js
│   ├── src/                          # Código fuente (core, ports, adapters)
│   ├── scripts/set-metadata.js       # Inyector PE que preserva el overlay de pkg
│   ├── dist/                         # Binario compilado: system_monitor_host.exe
│   └── tests/                        # Pruebas unitarias, integración y benchmarks
│
├── extension/                        # SUBSISTEMA 2: Extensión Chrome MV3
│   ├── manifest.json                 # Manifiesto V3 con clave RSA determinista
│   ├── background.js                 # Service Worker (Composition Root)
│   ├── core/                         # Formateadores matemáticos y máquina de estados
│   ├── popup/                        # Interfaz visual oscura (HTML/CSS/JS)
│   └── tests/                        # Suite de pruebas con simulación JSDOM
│
├── installer/                        # SUBSISTEMA 3: Instalador de Windows
│   ├── setup.iss                     # Script de Inno Setup 6 (Modo Usuario)
│   ├── manifest-template.json        # Plantilla JSON del host
│   └── output/                       # Artefacto final: Setup_SystemMonitor.exe
│
├── specs/                            # ESPECIFICACIONES SDD FORMALES
│   ├── 001-native-protocol-host/     # Fase 1: Protocolo y Host
│   ├── 002-chrome-extension-ui/      # Fase 2: Extensión y UI
│   └── 003-windows-installer-pkg/    # Fase 3: Empaquetado e Instalador
└── docs/constitution.md              # Constitución y principios innegociables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔌 2. El Protocolo Binario: 4 Bytes UInt32LE y la Trampa de stdout
&lt;/h2&gt;

&lt;p&gt;Chromium se comunica con los ejecutables nativos mediante tuberías estándar (&lt;code&gt;process.stdin&lt;/code&gt; y &lt;code&gt;process.stdout&lt;/code&gt;). La especificación oficial exige que cada mensaje tenga un formato exacto:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prefijo de Longitud (4 bytes):&lt;/strong&gt; Un entero sin signo de 32 bits en formato little-endian (&lt;code&gt;UInt32LE&lt;/code&gt;) con la longitud $L$ del mensaje en bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload JSON ($L$ bytes):&lt;/strong&gt; El string JSON serializado en UTF-8 (tamaño máximo: 1 MB).
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Estructura Binaria del Stream:
[ Byte 0 ] [ Byte 1 ] [ Byte 2 ] [ Byte 3 ] [ Byte 4 ... Byte 4 + L - 1 ]
└─────────────── UInt32LE (L) ──────────────┘└────── Payload JSON (UTF-8) ──────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  La Trampa Mortal: ¿Por qué &lt;code&gt;console.log&lt;/code&gt; Rompe Todo?
&lt;/h3&gt;

&lt;p&gt;En Node.js, la primera reacción de cualquier desarrollador para depurar es escribir &lt;code&gt;console.log("Mensaje recibido")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;En Chrome Native Messaging, &lt;strong&gt;hacer esto es fatal&lt;/strong&gt;: &lt;code&gt;console.log&lt;/code&gt; emite texto plano por &lt;code&gt;process.stdout&lt;/code&gt; sin la cabecera de 4 bytes. Chromium lee los primeros 4 caracteres ASCII como si fueran un entero binario (por ejemplo, el texto &lt;code&gt;"Hola"&lt;/code&gt; equivale al número &lt;code&gt;0x616C6F48&lt;/code&gt; = 1.634.553.672 bytes). Como el tamaño supera el límite de 1 MB o agota el stream, &lt;strong&gt;Chrome destruye el socket y cierra la extensión inmediatamente&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nuestra Solución en SDD:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Todo el diagnóstico del sistema se redirige estrictamente a &lt;strong&gt;&lt;code&gt;process.stderr&lt;/code&gt;&lt;/strong&gt;, el cual Chrome captura en sus logs internos sin tocar el socket de datos.&lt;/li&gt;
&lt;li&gt;La salida por &lt;code&gt;stdout&lt;/code&gt; se gestiona a través de un adaptador exclusivo:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// host/src/adapters/outbound/binary-stream-writer.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BinaryStreamWriter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;writableStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;writableStream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jsonString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payloadBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jsonString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lengthBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;lengthBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeUInt32LE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payloadBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Emisión atómica de cabecera + payload&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;lengthBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payloadBuffer&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tolerancia a Fragmentación de Chunks
&lt;/h3&gt;

&lt;p&gt;En Windows, las tuberías de consola pueden entregar los datos fragmentados (por ejemplo, 2 bytes de longitud en un paquete y el resto en el siguiente). Nuestro lector acumula los fragmentos en memoria hasta completar exactamente la longitud esperada antes de intentar deserializar el JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; npm --prefix host test

# Subtest: Inbound Adapter: BinaryStreamReader
    ok 1 - debe deserializar un mensaje completo en un único chunk
    ok 2 - debe tolerar fragmentación en múltiples chunks contiguos
    ok 3 - debe procesar múltiples mensajes consecutivos en un mismo buffer
    ok 4 - debe emitir error si la longitud supera el límite de 1 MB
    ok 5 - debe emitir error ante JSON corrupto o malformado
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚡ 3. La Extensión Chrome MV3: Determinismo y Sondeo Secuencial
&lt;/h2&gt;

&lt;p&gt;En Google Chrome Manifest V3, las extensiones ya no tienen páginas de fondo (&lt;em&gt;background pages&lt;/em&gt;) persistentes; ahora usan &lt;strong&gt;Service Workers&lt;/strong&gt; efímeros que se suspenden cuando no hay actividad.&lt;/p&gt;

&lt;h3&gt;
  
  
  El Desafío del Extension ID
&lt;/h3&gt;

&lt;p&gt;Para que el host nativo acepte comunicarse con una extensión, debe declararla explícitamente en el arreglo &lt;code&gt;"allowed_origins"&lt;/code&gt; de su manifiesto JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"allowed_origins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"chrome-extension://nknhjeknlgdehgjeoddeiibmpoicbgdc/"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normalmente, al cargar una extensión descomprimida en desarrollo, Chrome calcula un ID dinámico basado en la ruta absoluta de la carpeta en disco. Si mueves el proyecto de carpeta, el ID cambia y la conexión falla.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo lo resolvimos?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Generamos un par de claves RSA e inyectamos la clave pública en el campo &lt;code&gt;"key"&lt;/code&gt; de &lt;a href="https://github.com/dhbernardo/chrome-native-messaging-sdd/blob/main/extension/manifest.json" rel="noopener noreferrer"&gt;&lt;code&gt;extension/manifest.json&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manifest_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"System Monitor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"nativeMessaging"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"background"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"service_worker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"background.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"module"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv4RuKyfoD6m..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gracias a esto, Chrome genera &lt;strong&gt;siempre el mismo ID determinista e inmutable&lt;/strong&gt;:&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7jtajnw0g4ucrkade55.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7jtajnw0g4ucrkade55.png" alt="Tarjeta de la Extensión System Monitor en Chrome" width="600" height="315"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figura 2: La extensión cargada en &lt;code&gt;chrome://extensions/&lt;/code&gt; con su ID determinista garantizado por la clave pública RSA.&lt;/em&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  Sondeo Secuencial Anti-Solapamiento
&lt;/h3&gt;

&lt;p&gt;En aplicaciones de monitoreo es común caer en la tentación de usar &lt;code&gt;setInterval(fetchMetrics, 1000)&lt;/code&gt;. Si una llamada tarda 1.2 segundos, las peticiones comienzan a encolarse en vuelo, saturando la tubería de Windows.&lt;/p&gt;

&lt;p&gt;En su lugar, nuestro Service Worker implementa un &lt;strong&gt;sondeo secuencial con control de ciclo de vida&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Envía &lt;code&gt;{ "action": "GET_METRICS" }&lt;/code&gt; a través del puerto &lt;code&gt;chrome.runtime.connectNative&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Espera la respuesta (con un timeout de seguridad de 5000 ms).&lt;/li&gt;
&lt;li&gt;Tras procesar la respuesta, programa un reposo de 1000 ms mediante &lt;code&gt;setTimeout&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Si el usuario cierra el popup, el Service Worker llama a &lt;code&gt;port.disconnect()&lt;/code&gt; y Windows termina el proceso del host nativo de inmediato, liberando los recursos de la máquina.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  📦 4. De Script a Binario Independiente (.exe) y el Secreto del Overlay
&lt;/h2&gt;

&lt;p&gt;El Principio 2 de nuestra constitución prohíbe exigir Node.js en la máquina del usuario. Para compilar el código de Node.js a un único binario x64 utilizamos &lt;code&gt;@yao-pkg/pkg&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build:exe"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pkg . --targets node18-win-x64 --output dist/system_monitor_host.exe"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  El "Gotcha" de Ingeniería: La Corrupción del Overlay PE
&lt;/h3&gt;

&lt;p&gt;Cuando compilas un script con &lt;code&gt;pkg&lt;/code&gt;, este empaqueta el runtime de Node.js junto con un archivo virtual que contiene tu código JS anexado al final del ejecutable (lo que en la estructura PE de Windows se conoce como &lt;em&gt;Overlay&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;Al intentar inyectar el icono de la aplicación (&lt;code&gt;icon.ico&lt;/code&gt;) y los metadatos de versión utilizando la herramienta estándar &lt;code&gt;rcedit&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;rcedit&lt;/code&gt; expande la sección &lt;code&gt;.rsrc&lt;/code&gt; de la cabecera PE.&lt;/li&gt;
&lt;li&gt;Esto desplaza físicamente los bytes del overlay hacia adelante.&lt;/li&gt;
&lt;li&gt;Al arrancar el &lt;code&gt;.exe&lt;/code&gt;, el bootstrap de Node.js busca su código en las posiciones originales y arroja:
&lt;code&gt;Cannot find entry point / Invalid payload offset&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Para solucionarlo, construimos un script automatizado (&lt;a href="https://github.com/dhbernardo/chrome-native-messaging-sdd/blob/main/host/scripts/set-metadata.js" rel="noopener noreferrer"&gt;&lt;code&gt;host/scripts/set-metadata.js&lt;/code&gt;&lt;/a&gt;) que opera quirúrgicamente sobre el binario:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  A["system_monitor_host.exe"] --&amp;gt; B["1. Extraer Overlay (Payload JS)"]
  B --&amp;gt; C["2. Ejecutar rcedit (Inyectar Icono en .rsrc)"]
  C --&amp;gt; D["3. Re-anexar Overlay al final del binario"]
  D --&amp;gt; E["4. Recalcular y parchar PAYLOAD_POSITION y PRELUDE_POSITION"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;El resultado es un ejecutable nativo de ~40 MB, con icono personalizado de Windows y arranque instantáneo.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ 5. Instalador Inno Setup: Cero Clics en el Registro y UTF-8 sin BOM
&lt;/h2&gt;

&lt;p&gt;El último eslabón de la suite es la experiencia de instalación. Nadie quiere obligar a sus usuarios a abrir &lt;code&gt;regedit.exe&lt;/code&gt; para registrar un archivo JSON.&lt;/p&gt;

&lt;p&gt;Creamos un instalador con &lt;strong&gt;Inno Setup 6&lt;/strong&gt; (&lt;a href="https://github.com/dhbernardo/chrome-native-messaging-sdd/blob/main/installer/setup.iss" rel="noopener noreferrer"&gt;&lt;code&gt;installer/setup.iss&lt;/code&gt;&lt;/a&gt;) configurado bajo premisas estrictas:&lt;/p&gt;

&lt;h3&gt;
  
  
  Despliegue en Modo Usuario (Sin UAC)
&lt;/h3&gt;

&lt;p&gt;Configuramos &lt;code&gt;PrivilegesRequired=lowest&lt;/code&gt; y desplegamos en &lt;code&gt;{localappdata}\Programs\SystemMonitorHost&lt;/code&gt;. Cualquier usuario estándar puede instalar el software sin requerir permisos de Administrador ni ver advertencias de UAC.&lt;/p&gt;

&lt;h3&gt;
  
  
  Registro Automatizado en Windows
&lt;/h3&gt;

&lt;p&gt;El instalador crea la clave de integración en la colmena del usuario actual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight pascal"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Registry&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;Root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HKCU&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;Subkey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;Software&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="n"&gt;Google&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="n"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="n"&gt;NativeMessagingHosts&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tuempresa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemmonitor&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;
    &lt;span class="n"&gt;ValueType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;ValueData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="cm"&gt;{app}&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tuempresa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemmonitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;Flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;uninsdeletekey&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  El Detalle Crítico: UTF-8 sin BOM
&lt;/h3&gt;

&lt;p&gt;Chromium exige que el archivo de manifiesto JSON esté codificado en &lt;strong&gt;UTF-8 estricto sin BOM&lt;/strong&gt; (&lt;em&gt;Byte Order Mark&lt;/em&gt;). Si se guarda con los 3 bytes iniciales de BOM (&lt;code&gt;\xEF\xBB\xBF&lt;/code&gt;), el parser C++ de Chrome falla en silencio y arroja &lt;code&gt;Specified native messaging host not found&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;En el script Pascal del instalador forzamos la codificación limpia:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight pascal"&gt;&lt;code&gt;&lt;span class="n"&gt;SaveStringsToUTF8FileWithoutBOM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ManifestPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Lines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;False&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Terminación Atómica de Procesos
&lt;/h3&gt;

&lt;p&gt;Si la extensión está abierta mientras el instalador se ejecuta, Windows bloquea el archivo &lt;code&gt;.exe&lt;/code&gt; con un handle abierto. Las rutinas &lt;code&gt;PrepareToInstall&lt;/code&gt; y &lt;code&gt;CurUninstallStepChanged&lt;/code&gt; ejecutan de fondo &lt;code&gt;taskkill /F /IM system_monitor_host.exe&lt;/code&gt; antes de reemplazar archivos o desinstalar, garantizando una instalación y remoción 100% limpia.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 6. Resultados y Verificación de Calidad
&lt;/h2&gt;

&lt;p&gt;Gracias a la metodología &lt;strong&gt;SDD&lt;/strong&gt;, cada requisito funcional especificado cuenta con su correspondiente prueba automatizada:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; npm test

✔ Host Nativo (26 tests)
  - Deserialización de stream binario y tolerancia a fragmentación
  - Enrutamiento de comandos y envelopes estructurados
  - Rendimiento y latencia de respuesta (&amp;lt;50 ms)
✔ Extensión Chrome MV3 (61 tests)
  - Puerto de mensajería y ciclo de vida del Service Worker
  - Formateadores matemáticos de RAM (GB binarios 1024^3) y Uptime
  - Máquina de estados finita (LOADING, DASHBOARD, ERROR)
✔ Binario Independiente Windows (3 tests)
  - Validación de cabeceras PE x64
  - Ejecución aislada sin Node.js en variables de entorno
  - Respuesta a GET_METRICS vía stdio directo
----------------------------------------------------------------------
Total: 90 tests pasando al 100%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Además, el script de PowerShell &lt;a href="https://github.com/dhbernardo/chrome-native-messaging-sdd/blob/main/tests/installer/verify-install.ps1" rel="noopener noreferrer"&gt;&lt;code&gt;tests/installer/verify-install.ps1&lt;/code&gt;&lt;/a&gt; ejecuta un ciclo completo desatendido:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Instala el host en modo silencioso (&lt;code&gt;/VERYSILENT&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Valida que el archivo JSON esté en UTF-8 sin BOM y con rutas dobles (&lt;code&gt;\\&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Consulta el registro de Windows con &lt;code&gt;Get-ItemProperty&lt;/code&gt; verificando la subclave en &lt;code&gt;HKCU&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Desinstala la aplicación comprobando que no quede ningún rastro en el sistema.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🚀 Conclusiones y Código Fuente
&lt;/h2&gt;

&lt;p&gt;Conectar la web con el sistema operativo anfitrión a través de &lt;strong&gt;Chrome Native Messaging&lt;/strong&gt; es una herramienta con un potencial inmenso para arquitecturas híbridas, aplicaciones empresariales y herramientas de desarrollo. &lt;/p&gt;

&lt;p&gt;La diferencia entre un prototipo frágil que se rompe con cualquier log y una solución de grado de producción radica en la disciplina metodológica:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spec-Driven Development (SDD)&lt;/strong&gt; proporcionó el marco para planificar contratos binarios antes de escribir código.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La Arquitectura Hexagonal&lt;/strong&gt; aisló la lógica del DOM y del sistema operativo, permitiendo probar el 100% de la lógica sin mocks complejos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La automatización de empaquetado e instalación&lt;/strong&gt; convirtió un conjunto de scripts en un producto listo para el usuario final.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Todo el código fuente, especificaciones EARS y scripts de compilación están disponibles de forma abierta:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Repositorio en GitHub:&lt;/strong&gt; &lt;a href="https://github.com/dhbernardo/chrome-native-messaging-sdd" rel="noopener noreferrer"&gt;https://github.com/dhbernardo/chrome-native-messaging-sdd&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;¿Has trabajado antes con Chrome Native Messaging o aplicado Spec-Driven Development en tus proyectos? ¡Cuéntame tus experiencias y dudas en los comentarios!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sdd</category>
      <category>architecture</category>
      <category>javascript</category>
      <category>chrome</category>
    </item>
    <item>
      <title>Cómo construí mi portafolio en 4 frameworks (React 19, Vue 3.5, Angular 19 y Compose HTML) usando Clean Architecture</title>
      <dc:creator>David Bernardo</dc:creator>
      <pubDate>Fri, 18 Sep 2026 19:43:40 +0000</pubDate>
      <link>https://dev.to/davidbernardo/como-construi-mi-portafolio-en-4-frameworks-react-19-vue-35-angular-19-y-compose-html-usando-3i4b</link>
      <guid>https://dev.to/davidbernardo/como-construi-mi-portafolio-en-4-frameworks-react-19-vue-35-angular-19-y-compose-html-usando-3i4b</guid>
      <description>&lt;p&gt;Como desarrollador &lt;strong&gt;Full Stack &amp;amp; Mobile Senior&lt;/strong&gt;, a menudo me encuentro con el mismo dilema en las entrevistas técnicas:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Veo en tu CV que manejas React, Vue, Angular y Kotlin Multiplatform... ¿pero en cuál te sientes más cómodo?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cualquiera puede listar una docena de tecnologías en un PDF. Pero en el mundo del desarrollo de software, &lt;strong&gt;el código habla más fuerte que las palabras&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Por eso me propuse un reto de ingeniería: en lugar de crear un portafolio tradicional en una sola tecnología, decidí &lt;strong&gt;construir una misma aplicación idéntica en los cuatro ecosistemas más potentes de la actualidad&lt;/strong&gt;, permitiendo al usuario cambiar de framework en caliente con un solo clic, sin perder el estado ni la posición de lectura:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;⚛️ &lt;strong&gt;React 19&lt;/strong&gt; (Vite + Hooks personalizados)&lt;/li&gt;
&lt;li&gt;💚 &lt;strong&gt;Vue 3.5&lt;/strong&gt; (Composition API + Composables)&lt;/li&gt;
&lt;li&gt;🅰️ &lt;strong&gt;Angular 19&lt;/strong&gt; (Standalone Components + Nuevas Signals)&lt;/li&gt;
&lt;li&gt;🟣 &lt;strong&gt;Kotlin Multiplatform / Compose HTML&lt;/strong&gt; (Kotlin compilado a JS/Web)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;¿La trampa? Si creas 4 proyectos separados, estás cuadruplicando el esfuerzo de mantenimiento: cada vez que actualizas un empleo, una habilidad o un proyecto, tendrías que modificar 4 bases de código distintas.&lt;/p&gt;

&lt;p&gt;Aquí es donde entra la verdadera protagonista de este proyecto: &lt;strong&gt;Clean Architecture (Arquitectura Limpia) en un Monorepo&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5hp4rv7bymvda8gjxxef.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5hp4rv7bymvda8gjxxef.png" alt="Portafolio con selector Dock de microfrontends" width="1899" height="1061"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🏛️ La Arquitectura: Separando la lógica del framework
&lt;/h2&gt;

&lt;p&gt;Para evitar duplicar código, la regla de oro fue: &lt;strong&gt;ningún framework debe ser dueño de la lógica ni de los datos&lt;/strong&gt;. Los frameworks son simplemente &lt;em&gt;mecanismos de entrega&lt;/em&gt; (presenters o adaptadores de entrada).&lt;/p&gt;

&lt;p&gt;Estructuré el proyecto en un monorepo con &lt;strong&gt;Yarn Workspaces&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── packages/
│   ├── core/                  # 🧠 Dominio puro (Entidades, Casos de Uso, Puertos)
│   ├── content-json-adapter/  # 🔌 Adaptador de infraestructura (i18n, lectura de JSON)
│   └── tokens/                # 🎨 Variables CSS de diseño y temas
├── apps/
│   ├── react-app/             # ⚛️ UI en React 19
│   ├── vue-app/               # 💚 UI en Vue 3.5
│   ├── angular-app/           # 🅰️ UI en Angular 19
│   └── compose-app/           # 🟣 UI en Kotlin Compose HTML
└── shell/                     # 🐚 Host orquestador y Dock Switcher
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. El Núcleo de Dominio (&lt;code&gt;packages/core&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Este paquete es &lt;strong&gt;TypeScript puro&lt;/strong&gt; y no tiene una sola línea de código acoplada al navegador, al DOM o a librerías externas. Contiene:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Entidades:&lt;/strong&gt; Modelos de dominio como &lt;code&gt;Experience&lt;/code&gt;, &lt;code&gt;Project&lt;/code&gt;, &lt;code&gt;Profile&lt;/code&gt;, &lt;code&gt;SkillCategory&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Puertos (Interfaces):&lt;/strong&gt; Define el contrato &lt;code&gt;ContentRepository&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ContentRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;getExperiences&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Experience&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nf"&gt;getProjects&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nf"&gt;getSkills&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;SkillCategory&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Casos de Uso:&lt;/strong&gt; Funciones puras con la lógica de negocio, por ejemplo &lt;code&gt;getExperienceTimeline&lt;/code&gt; o &lt;code&gt;filterProjectsByTech&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getExperienceTimeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ContentRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SupportedLocale&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ExperienceVM&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getExperiences&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;mapToExperienceVM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. El Adaptador de Datos (&lt;code&gt;packages/content-json-adapter&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Implementa el puerto &lt;code&gt;ContentRepository&lt;/code&gt; leyendo la fuente única de verdad (archivos JSON estructurados) y gestionando la traducción dinámica entre Español e Inglés.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚔️ La Comparativa: ¿Cómo consume cada framework el mismo caso de uso?
&lt;/h2&gt;

&lt;p&gt;Aquí viene la parte más fascinante: &lt;strong&gt;el consumo del caso de uso en cada tecnología&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Al estar la lógica en &lt;code&gt;packages/core&lt;/code&gt;, los componentes de la interfaz solo tienen una responsabilidad: &lt;em&gt;renderizar el ViewModel&lt;/em&gt;. Veamos cómo se implementa la reactividad en cada uno:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. ⚛️ React 19: Con Custom Hooks
&lt;/h3&gt;

&lt;p&gt;En React creamos un hook &lt;code&gt;usePortfolio&lt;/code&gt; que encapsula la suscripción al idioma:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/react-app/src/hooks/usePortfolio.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;usePortfolio&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useI18n&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;JsonContentRepository&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;experiences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getExperienceTimeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;experiences&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. 💚 Vue 3.5: Con Composition API y Composables
&lt;/h3&gt;

&lt;p&gt;En Vue aprovechamos la reactividad nativa con &lt;code&gt;computed&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/vue-app/src/composables/usePortfolio.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;usePortfolio&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useI18n&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;JsonContentRepository&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;experiences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="nf"&gt;getExperienceTimeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;experiences&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. 🅰️ Angular 19: Con Signals Nativas
&lt;/h3&gt;

&lt;p&gt;Angular 19 ha revolucionado su reactividad abandonando la complejidad excesiva de RxJS en componentes para adoptar &lt;strong&gt;Signals&lt;/strong&gt;. Observa qué limpio queda:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/angular-app/src/app/services/portfolio.service.ts&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;providedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PortfolioService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;contentRepo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;JsonContentRepository&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;i18n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;I18nService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Signal calculada automáticamente cuando cambia locale()&lt;/span&gt;
  &lt;span class="nx"&gt;experiences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="nf"&gt;getExperienceTimeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentRepo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. 🟣 Kotlin Compose HTML: Declaratividad en la Web
&lt;/h3&gt;

&lt;p&gt;En Compose para Web utilizamos el paradigma declarativo de Jetpack Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/compose-app/src/jsMain/kotlin/.../App.kt&lt;/span&gt;
&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PortfolioViewModel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;locale&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collectAsState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;experiences&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
        &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getExperienceTimeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;ExperienceSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experiences&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;experiences&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Lección clave:&lt;/strong&gt; Cada framework brilla a su manera, pero &lt;strong&gt;ninguno reinventa la rueda&lt;/strong&gt;. Si mañana decido añadir un nuevo empleo a mi trayectoria profesional, solo edito un archivo JSON y los 4 frameworks lo reflejan al instante con sus respectivos badges (&lt;code&gt;Full-time&lt;/code&gt; o &lt;code&gt;Freelance&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6w14kske2851wcav8yp4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6w14kske2851wcav8yp4.png" alt="Línea de tiempo de Experiencia unificada" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🎨 Sistema de Diseño Agnóstico: Vanilla CSS
&lt;/h2&gt;

&lt;p&gt;Uno de los errores comunes al hacer microfrontends es usar Tailwind o librerías pesadas (Material UI, Vuetify, PrimeNG) en cada aplicación, provocando colisiones de nombres de clases y una descarga masiva de megabytes innecesarios.&lt;/p&gt;

&lt;p&gt;La solución fue &lt;strong&gt;Vanilla CSS moderno con Tokens de Diseño compartidos&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Se definieron variables CSS para colores, espaciados, fuentes y sombras (&lt;code&gt;--color-primary&lt;/code&gt;, &lt;code&gt;--color-surface&lt;/code&gt;, &lt;code&gt;--radius-lg&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Se implementaron temas &lt;strong&gt;Oscuro y Claro (Dark/Light Mode)&lt;/strong&gt; con &lt;code&gt;color-scheme&lt;/code&gt; y variables en el elemento raíz.&lt;/li&gt;
&lt;li&gt;Cada microfrontend utiliza exactamente las mismas clases semánticas (&lt;code&gt;.timeline-card&lt;/code&gt;, &lt;code&gt;.badge&lt;/code&gt;, &lt;code&gt;.project-card&lt;/code&gt;). El resultado es una &lt;strong&gt;paridad visual del 100%&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🐚 El Shell Host y el Switcher Dinámico
&lt;/h2&gt;

&lt;p&gt;¿Cómo orquestamos los 4 microfrontends en una sola página?&lt;/p&gt;

&lt;p&gt;Diseñé un &lt;strong&gt;Shell Host&lt;/strong&gt; ligero en TypeScript puro que:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Monta la aplicación seleccionada mediante un contenedor dinámico.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dock lateral interactivo&lt;/strong&gt; en pantallas de escritorio y &lt;strong&gt;Floating Action Button (FAB)&lt;/strong&gt; flotante al estilo Android en dispositivos móviles.&lt;/li&gt;
&lt;li&gt;Sincroniza el idioma actual (&lt;code&gt;es&lt;/code&gt; / &lt;code&gt;en&lt;/code&gt;), el tema visual y la posición de scroll para que la transición entre frameworks sea instantánea e imperceptible.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9livt8dg71k0xxklnmy1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9livt8dg71k0xxklnmy1.png" alt="Botón Flotante FAB en Mobile" width="752" height="1127"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🐳 Despliegue y Build con Docker y Nginx
&lt;/h2&gt;

&lt;p&gt;Compilar 4 frameworks con herramientas tan diversas (Vite, Angular CLI y el compilador de Kotlin/Gradle) puede ser un desafío en CI/CD.&lt;/p&gt;

&lt;p&gt;Para resolverlo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Usamos &lt;strong&gt;Docker&lt;/strong&gt; para garantizar un entorno reproducible con Node.js 22 y OpenJDK 17.&lt;/li&gt;
&lt;li&gt;Un script orquestador (&lt;code&gt;scripts/assemble-dist.mjs&lt;/code&gt;) compila cada aplicación y las ensambla en una estructura de producción limpia:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   dist/
   ├── index.html        # Shell host
   ├── react/            # Bundle de React 19
   ├── vue/              # Bundle de Vue 3.5
   ├── angular/          # Bundle de Angular 19
   └── compose/          # Bundle Wasm/JS de Compose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Un contenedor &lt;strong&gt;Nginx&lt;/strong&gt; sirve los artefactos con compresión Gzip, cabeceras de caché inmutable para assets versionados y soporte de rutas SPA.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  📊 Conclusiones y Aprendizajes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clean Architecture funciona de maravilla en Frontend:&lt;/strong&gt; A menudo se piensa que la arquitectura limpia es solo para backend. Desacoplar el dominio de la vista demostró ser la mejor decisión para mantener la coherencia del proyecto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Angular 19 con Signals es un placer:&lt;/strong&gt; La simplicidad de las Signals en Angular 19 lo acerca a la agilidad de Vue y React sin perder la robustez de TypeScript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose HTML / Kotlin Web es el futuro móvil/web unificado:&lt;/strong&gt; Escribir interfaces web con la misma sintaxis que uso en Android nativo permite una reutilización de habilidades sin precedentes.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🚀 Pruébalo en vivo y Código Fuente
&lt;/h2&gt;

&lt;p&gt;Puedes interactuar con el portafolio en vivo y cambiar de framework en tiempo real:&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Demo en vivo:&lt;/strong&gt; &lt;a href="https://www.davidbernardo.dev" rel="noopener noreferrer"&gt;davidbernardo.dev&lt;/a&gt;&lt;br&gt;&lt;br&gt;
⭐ &lt;strong&gt;Repositorio en GitHub:&lt;/strong&gt; &lt;a href="https://github.com/dhbernardo/portfolio" rel="noopener noreferrer"&gt;github.com/dhbernardo/portfolio&lt;/a&gt;&lt;br&gt;&lt;br&gt;
💼 &lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://www.linkedin.com/in/david-henry-bernardo-cardenas" rel="noopener noreferrer"&gt;linkedin.com/in/david-henry-bernardo-cardenas&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;¿Qué framework de los cuatro prefieres tú para tus proyectos? ¡Déjame tu opinión en los comentarios!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>architecture</category>
      <category>frontend</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
