Buscar/Modificar un pipeline de datos con plugins que corre en dos runtimes sin romperlo

Modificar un pipeline de datos con plugins que corre en dos runtimes sin romperlo

Úsalo cuando un agente de código trabaja en un backend con plugins sobre dos runtimes. Guarda las versiones en un archivo, aplica los filtros de test a la tarea correcta, sigue las reglas de plugins, concurrencia y capas de servicio, y comprueba que cada test nuevo falle sin su cambio.

Para usar estas reglas de IA
## Guardar las versiones en un solo archivo y ejecutar el build siempre desde la misma raíz Mantén las versiones y las coordenadas de las herramientas de compilación en un lugar canónico, y compila siempre desde el mismo sitio. - Guarda cada número de versión y cada coordenada de herramientas de compilación en un único archivo canónico; todos los consumidores (scripts de build, helpers, tests, descriptores de paquetes) lo leen desde ahí, y tú solo actualizas ese archivo. - Cuando un valor está codificado o duplicado intencionalmente en varios lugares, como un segmento de ruta de compatibilidad de runtime, no lo trates como un error ni lo calcules dinámicamente; busca el valor actual en el lugar canónico y hazlo coincidir. - Ejecuta todos los comandos de build y test desde una única raíz consistente, para que las referencias relativas se resuelvan siempre de la misma manera. - Prefiere una cadena de herramientas vendorizada y fijada, provista por un paso de bootstrap, en lugar de un lenguaje o gestor de versiones instalado globalmente, para que cada entorno compile de la misma manera. ## Aplicar los filtros de test a la tarea que realmente contiene los tests Entiende cómo están conectadas las tareas de test antes de ejecutar un subconjunto filtrado. - Una tarea de test paraguas suele excluir todas las clases en sí misma y solo delega en subtareas, así que un filtro por nombre aplicado a ella no coincide con nada de forma silenciosa y parece que pasó. Aplica los filtros a la tarea concreta que realmente contiene los tests. - Las suites se dividen a propósito: algunos tests se sacan de su suite obvia; por ejemplo, los tests que arrancan un segundo runtime se ejecutan en la suite que provee ese runtime. Si un filtro no encuentra nada, lo más probable es que el objetivo esté en la otra suite. - Conoce dónde vive cada tipo de test (unitarios, de integración y de aceptación) para invocar el punto de entrada correcto. ## Reestructurar el código para que cada caso nuevo parezca planeado desde el inicio Cuando modifiques código, no te limites a añadir algo encima de lo que ya existe. Evalúa si el código circundante sigue teniendo sentido con tu cambio. Si una abstracción fue diseñada para un solo caso y tú estás agregando un segundo, reestructúrala para que el resultado se lea como si ambos casos hubieran sido contemplados desde el principio. Dale a cada concepto distinto su propia unidad, claramente nombrada, y compón esas unidades en código de nivel superior para que la intención sea evidente sin necesidad de leer cada implementación. ## Crear plugins sobre las clases base y el registro del framework Construye las extensiones sobre los propios puntos de extensión del framework, no alrededor de ellos. - Hereda de la clase base específica de cada tipo y sobrescribe sus hooks de ciclo de vida: un hook de configuración o registro más el punto de entrada del tipo. - Declara la configuración a través del DSL declarativo de opciones del framework en lugar de leer parámetros en crudo, para que la validación, la conversión de tipos y la generación de documentación queden resueltas por ti. - Define la concurrencia a nivel de clase: compartida cuando varios hilos de trabajo pueden llamar a la instancia a la vez, única cuando las llamadas deben serializarse para que solo un hilo la ejecute en cada momento. - Encamina toda búsqueda de plugins a través del registro central, registra ahí los nuevos plugins integrados, y permite que los plugins externos se descubran como unidades empaquetadas. ## Poner las métricas y la lógica común en un delegador que envuelve cada plugin Mantén las responsabilidades transversales en una capa envolvente y obtén los colaboradores desde el framework. - Ejecuta cada plugin detrás de un delegador que añade responsabilidades transversales como métricas y seguridad entre hilos; al cambiar un comportamiento, decide si pertenece al delegador o al propio plugin. - Deja que las métricas sigan una jerarquía de namespaces, creando hijos mediante una llamada de namespace. - Proporciona una métrica de tipo objeto nulo que implemente la misma interfaz pero descarte los datos, para cuando las métricas estén deshabilitadas o en pruebas. - Nunca instancies objetos de métricas directamente; acéptalos desde el framework. Los mixins estándar, como logging y configuración, se incluyen automáticamente, mientras que las capacidades más pesadas siguen siendo opcionales. ## Mantener rápido el bucle de workers y arrancar y detener en el orden correcto Protege la ruta crítica y respeta el orden de arranque y parada. - Trata el bucle del worker como la ruta crítica: extrae lotes, los pasa por el pipeline compilado y señala su finalización. No agregues llamadas bloqueantes ni asignaciones pesadas dentro de él. - Sigue el orden estricto del ciclo de vida: primero se registran los outputs y los filters, después arrancan los workers, y por último arrancan los inputs; al apagar, primero se detienen los inputs, los workers se vacían y finalmente se cierran los filters y los outputs. Violar este orden provoca pérdida de datos o bloqueos. - Usa primitivas atómicas para el estado compartido entre hilos, no simples campos booleanos, porque de lo contrario no se garantiza la visibilidad de memoria entre hilos. ## Compilar la configuración del pipeline en código y volcarlo para depurar Algunos motores compilan la configuración declarativa en clases ejecutables en tiempo de ejecución a través de un pipeline por etapas: analizan el texto de configuración con una gramática para obtener un árbol de sintaxis; reducen ese árbol a una representación intermedia, un grafo acíclico dirigido de nodos y condicionales; generan el código fuente de cada nodo y lo compilan en clases en tiempo de ejecución dentro de un namespace dedicado; y finalmente conectan las unidades compiladas al bucle de ejecución. Los condicionales se compilan en funciones predicado conectadas a nodos de bifurcación. Proporciona un flag de depuración que vuelque el código fuente generado a un directorio para que puedas inspeccionar lo que realmente se ejecuta. ## Tratar igual los errores, los nulos y el logging en todo el código Aplica convenciones consistentes de estructura, errores, nulos y logging en toda la base de código. - Organiza los módulos por dominio funcional, no por capa arquitectónica, y coloca cada tipo nuevo en el dominio al que pertenece. - Mantén una nomenclatura consistente: sufijos y prefijos reservados para tipos de extensión, delegador y abstractos; marca las clases como finales salvo que se pretenda heredar de ellas. - Prefiere excepciones de runtime no verificadas; usa las verificadas solo cuando sea inevitable, siempre con un mensaje descriptivo y conservando la cadena de causas. - En cuanto a los nulos, valida los argumentos del constructor con una comprobación explícita de no nulidad, anota los contratos que son nulables de forma intencional, y usa un tipo opcional para búsquedas que puedan fallar, no como un sustituto general de null. - Declara un único logger estático y final por clase, y elige los niveles de forma deliberada: diagnósticos detallados en nivel bajo, problemas recuperables como advertencias, fallos no capturados como errores. - Favorece los campos finales e inmutables, usa tipos de mapa concurrentes para las cachés compartidas, y documenta la seguridad entre hilos cuando un tipo se comparte. ## Enviar las respuestas de la API por un solo helper y poner la lógica en clases de comando Organiza el servicio en capas de modo que el enrutamiento, las respuestas y la lógica tengan, cada uno, un único lugar donde vivir. - Haz que los módulos de la API hereden de una base compartida que extiende la clase base del framework web y recibe las dependencias comunes a través de su constructor. - Monta los endpoints en prefijos de ruta declarados en un único mapa central de namespaces; añade ahí los namespaces nuevos, no mediante enrutamiento improvisado. - Envía cada respuesta a través de un único helper de respuestas que envuelve los payloads con metadatos por defecto; no escribas respuestas en crudo a mano. - Representa los errores con un conjunto pequeño de subclases de error tipadas, como not-found, bad-request y timeout; lánzalas desde los handlers y deja que la base formatee un único envoltorio consistente. - Mantén la lógica de negocio en clases de comando dedicadas, no en los handlers de rutas; un handler construye un comando y pasa su resultado al helper de respuestas. ## Conectar dos runtimes y probar el recorrido de ida y vuelta por ambos Cuando el código conecta dos runtimes, como una máquina virtual anfitriona que controla un segundo lenguaje, registra las clases conectadas en el punto de registro designado por el framework y sigue los patrones que ya existen ahí. Obtén los objetos de contexto por hilo en el punto de uso, nunca los guardes en el momento de la construcción, porque un contexto almacenado falla cuando se reutiliza desde otro hilo. Prueba el recorrido de ida y vuelta a través de ambas capas en lugar de cada lado por separado, para detectar errores de coerción de tipos y de registro que los tests unitarios de una sola capa no detectan. Los tests que cargan el segundo runtime deben apuntar sus rutas de búsqueda de módulos a la ubicación vendorizada; esa ruta es intencional, así que no la cambies mientras editas esos tests. ## Probar con los helpers del pipeline real y limpiar el estado al terminar Ejercita el comportamiento real con helpers construidos para ese propósito, y no dejes estado residual. - Prefiere los helpers de pipeline provistos que levantan un pipeline real y capturan los resultados, en lugar de construir objetos de pipeline a mano. - Para aislar, define clases de plugin desechables dentro del archivo de specs y regístralas con el registro real. - Usa ejemplos compartidos para los contratos de interfaz y contexto compartido para la configuración común; conduce los tests de integración con un patrón de fixture y servicio que arranca a partir de archivos de fixtures. - Confirma que los bloques rescue o catch existentes realmente cubren las rutas de código recién añadidas. - Limpia el estado global o de la máquina virtual en el teardown: cada propiedad que estableces necesita su correspondiente limpieza, para que el estado no se filtre a otros tests. - Prefiere dobles concretos, construidos para ese propósito, que hereden de clases base reales, en lugar de mocks genéricos; reserva los mocks para las fronteras de infraestructura, y deja que el método real se ejecute cuando un stub es solo para la configuración. ## Comprobar que cada test nuevo falla sin su cambio Asegúrate de que cada test dependa realmente del cambio que protege. - Verifica que cada test nuevo falle sin su cambio correspondiente; un test que pasa con o sin la corrección no demuestra nada. - Haz las aserciones sobre el comportamiento específico que introduce el cambio, no sobre condiciones preexistentes: ancla la aserción a una diferencia observable, como un campo nuevo, un valor modificado o una línea de log que antes no existía. - Para una corrección de errores, reproduce primero el error en el test; observa cómo falla por la misma razón que se manifiesta el error, y luego aplica la corrección y observa cómo pasa. - Ten cuidado con los mocks tautológicos: hacer que un método devuelva mediante stub el mismo valor que luego afirmas en la aserción es circular, y si al eliminar la implementación y dejar solo el mock el test sigue pasando, es que el test solo está ejercitando el mock. ## Introducir los cambios incompatibles por etapas y ocultar los secretos en los logs Cambia el comportamiento siguiendo una escalera por etapas, protege los valores sensibles y mantén la documentación al día. - Escalona los cambios que rompen compatibilidad: introduce el nuevo comportamiento como opcional y añade advertencias de deprecación en versiones menores, luego cambia el valor por defecto manteniendo una opción para desactivarlo, y finalmente elimina el comportamiento anterior en versiones mayores. - Para la configuración que contiene valores sensibles, aplica el tipo o wrapper de secretos dedicado del framework y sobrescribe la representación en texto para que el valor quede enmascarado; nunca escribas esos valores en los logs, en ningún nivel, por defecto. - Trata la documentación como parte del cambio: actualiza la documentación de referencia y de contribuidores relevante cada vez que cambie el comportamiento, sigue las convenciones de cada formato, y exige documentación antes de fusionar funcionalidades visibles para el usuario.