📝 CHANGELOG#
Todos los cambios notables de este laboratorio se registran aqui con foco en madurez tecnica y documental.
2026-08-17 - Caso 20: la dead letter queue olvidada — el Eje 1 queda cerrado#
Octavo y ultimo caso del Eje 1 del ROADMAP. El lab llega a 20 casos x 7 stacks = 140 endpoints, y el eje se cierra: sus casos ya no son plan, son laboratorio.
Added — caso 20, la dead letter queue olvidada#
cases/20-forgotten-dead-letter-queue/ con las 7 implementaciones.
Contrato uniforme: /consume-silent y /consume-observed, mas /dlq/stats con
profundidad, antiguedad del mensaje mas viejo y desglose por clase de error, y
/dlq/drain para el replay.
Cierra el arco del caso 15#
En el [caso 15] la dead letter queue nace: es la politica de rechazo que salva al productor de bloquearse cuando la cola se llena. Es la decision correcta. Aca se ve que pasa cuando nadie vuelve a mirarla.
Los dos casos son el mismo mecanismo en dos momentos distintos — y el segundo demuestra que la decision del primero solo esta completa cuando incluye quien la observa y como se sale de ella.
La distincion que ordena el caso#
transitorio — el mismo mensaje funciona en el proximo intento venenoso — el mismo mensaje NUNCA va a funcionar
Reintentar lo venenoso es quemar CPU. Mandar lo transitorio a la DLQ es tirar trabajo que se podia salvar. El consumidor que no distingue hace las dos cosas mal a la vez, y el costo se mide.
El numero que cierra el caso#
Con 3.000 mensajes, 12% transitorios y 4% venenosos, identico en los 7 stacks:
silencioso: ok=2584 reintentos=0 a la DLQ=416 (13,87%)
by_error_class = { unclassified: 416 } alertas=0 muestras=0
observado: ok=2881 reintentos=297 a la DLQ=119 (3,97%)
by_error_class = { schema_mismatch: 29, unknown_field: 31,
null_required: 31, invalid_encoding: 28 }
alertas=1 muestras=20Y drenar la DLQ del consumidor silencioso:
recuperados = 297 de 416 → 71,39% ← nunca debieron estar ahi
siguen fallando = 119 ← veneno de verdadEl 71,39% de esa cola era trabajo que se podia salvar con un reintento, y estaba ahi porque el consumidor no miro que error era. Drenar la del consumidor observado recupera 0%: ahi solo hay veneno, que es lo que una DLQ deberia tener.
El caso ordena por que tan dificil hace cada lenguaje clasificar MAL#
| Stack | Contra clasificar mal | Contra tragarse los bugs propios |
|---|---|---|
| 🦀 Rust | enum + match exhaustivo: una variante nueva no compila | panic! no es un Result |
| 🔵 .NET | catch (e) when (...): filtra sin desenrollar la pila | Nada |
| ☕ Java | Jerarquia sealed ... permits | Nada, y Error queda fuera de Exception |
| 🐹 Go | errors.Is / errors.As sobre cadenas %w | Los panic son canal aparte |
| 🐘 PHP | catch (A|B $e) — sin exhaustividad | Nada, y Throwable lo hace explicito |
| 🐍 Python | Jerarquia de excepciones — sin exhaustividad | Nada |
| 🟢 Node | instanceof, fragil entre paquetes y workers | Nada |
Rust gana con su decimo oro: el enum es la primitiva exacta de un caso que
trata de clasificar, y panic! como canal separado hace estructuralmente
imposible que un bug del consumidor termine en la DLQ disfrazado de dato malo.
.NET segundo por los filtros de excepcion: la unica primitiva del laboratorio que decide antes de desenrollar la pila. Para un registro de DLQ, conservar el punto de falla original es la diferencia entre poder depurarlo y no.
Node septimo, como en el 19: su herramienta de clasificacion es fragil por diseño, y el caso entero depende de clasificar bien.
Changed — el Eje 1 del ROADMAP queda cerrado#
Los ocho casos (13-20) estan entregados y operativos en los 7 stacks. Las especificaciones pendientes se retiraron del ROADMAP —ya no son plan— y en su lugar quedo el registro de que se construyo y que se aprendio.
Tres cosas que el eje dejo claras:
- El ranking se cruza, y eso es el punto. Java gana el 17 y queda septimo
en el 18; Rust queda sexto en el 17 y segundo en el 18; PHP, ultimo del agregado, sube al segundo puesto en el 17. Un caso que siempre ordena igual a los siete stacks no esta midiendo nada.
- La ausencia de una primitiva enseña tanto como su presencia. Python sin
read-write lock (17), Go sin tipo conjunto (19): la ausencia se paga en el mismo lugar — codigo propio donde deberia haber biblioteca.
- Los peores modos de falla no rompen nada. Healthcheck en verde durante
veinte minutos de 503 (17), un pipeline sano que rechaza el 40% del trafico (18), una busqueda con 98,95% de recall (19), un error rate de cero mientras se pierde el 14% de los mensajes (20). Los cuatro se ven bien desde el dashboard.
Changed — integracion#
- Dispatchers: registro del caso 20 y puerto interno en los siete
(
:9020PHP/Python/Node,:9420Java,:9520.NET,:9620Go,:9720Rust). ci.yml: matrizcompose-configde 141 a 148 archivos;hub-probevalida 20 casos por stack;
compose-smokesumacase20-phpycase20-java.shared/catalog/cases.json+docs/case-catalog.md+ los cinco SVG.- Perfiles de lenguaje: agregado final sobre 19 comparativas que rankean.
Rust cierra con 10 oros y el mejor promedio (2,1); Go con 7 oros.
Verificado#
Los 7 stacks levantados con Docker, con resultados identicos hasta el ultimo digito. El drenaje de la DLQ del consumidor silencioso recupera 297 de 416 mensajes (71,39%) en los siete.
2026-08-17 - Caso 19: deriva del indice de busqueda y CDC roto en los 7 stacks#
Septimo caso del Eje 1 del ROADMAP. El lab pasa a 19 casos x 7 stacks = 133 endpoints.
Added — caso 19, deriva del indice de busqueda y CDC roto#
cases/19-search-index-drift-and-broken-cdc/ con las 7 implementaciones.
Contrato uniforme: /search-drifted y /search-reconciled con recall y
precision medidos con consultas reales contra los dos lados, mas
/index/state con las tres caras de la deriva y /reconcile para el barrido
suelto.
Este caso no rompe nada, y esa es toda su dificultad. Un servicio caido dispara alertas. Un indice que devuelve el 98,9% de lo que deberia no dispara nada: responde rapido, responde 200, y sus resultados se ven razonables.
La deriva no es una cosa, son tres — y se arreglan distinto#
| Cara | Que es | Que ve el usuario |
|---|---|---|
missing | Esta en la base, no en el indice | No lo encuentra |
stale | Esta en los dos, con version vieja | Lo encuentra mal |
orphan | Esta en el indice, borrado en la base | Fantasmas — clic que da 404 |
Un reindexado que no borra arregla las dos primeras y deja la tercera intacta.
Y sin numero de version en el documento, stale es directamente
indetectable: la unica comparacion posible es «esta o no esta».
La correccion son tres mecanismos y hacen falta los tres: outbox (el cambio se escribe en la misma transaccion que el dato), checkpoint (avanza solo con la confirmacion, y se frena en vez de saltear) y barrido de reconciliacion (la red de seguridad para lo que los dos primeros no cubren — un indice restaurado de un backup viejo, una reindexacion parcial, un borrado manual).
El outbox garantiza que ningun cambio nuevo se pierda. No arregla los que ya se perdieron. Por eso el barrido no es opcional.
El caso ordena por una dimension que ninguno de los otros dieciocho usa#
El bug entero es una escritura que fallo y que nadie miro. Ahi los siete stacks son radicalmente distintos:
| Stack | Contra ignorar el error | Para el diff de tres caras |
|---|---|---|
| 🦀 Rust | #[must_use] en la std + deny(unused_must_use) → no compila | HashSet::difference |
| 🐹 Go | _ = visible en el diff + errcheck en CI | Sin tipo conjunto: a mano |
| 🔵 .NET | Nada (_ = IndexarAsync() es aun mas silencioso) | Except / Join tipados |
| 🐍 Python | Nada (except: lo tapa) | - y & sobre set: el mas corto |
| 🐘 PHP | Nada (@ o catch vacio) | array_diff_key |
| ☕ Java | Nada, y @Transactional sugiere atomicidad que no da | removeAll / retainAll |
| 🟢 Node | Nada, y el bug es no escribir await | Map / Set a mano |
Rust gana por ser el unico con las dos piezas: el bug original no compila sin escribirlo a proposito, y el diff no se escribe a mano. Y la defensa esta en la biblioteca estandar, no en una herramienta que hay que instalar.
Java queda sexto no por lo que le falta sino por lo que promete de mas.
@Transactional hace que el dual-write parezca atomico —el metodo se lee como
una unidad, y el indice no participa de la transaccion— y nada en el codigo
marca donde termina su alcance. Un framework que engaña pesa mas que una
primitiva que ayuda.
Node queda septimo por ser el unico stack donde el bug se produce por no
escribir algo: indice.escribir(doc) sin await compila, parece correcto en
una revision rapida, y manda el error a un rechazo sin dueño.
PHP sube por su restriccion: en un runtime share-nothing no hay proceso de larga vida donde vivir un consumidor de CDC, asi que el consumidor es un comando de cron y el checkpoint tiene que ser durable desde el primer dia. Lo que en los stacks con procesos largos es una decision que se posterga, en PHP no tiene alternativa.
Changed — integracion#
- Dispatchers: registro del caso 19 y puerto interno en los siete
(
:9019PHP/Python/Node,:9419Java,:9519.NET,:9619Go,:9719Rust). ci.yml: matrizcompose-configde 134 a 141 archivos;hub-probevalida 19 casos por stack;
compose-smokesumacase19-pythonycase19-node.shared/catalog/cases.json+docs/case-catalog.md+ los cinco SVG.- Perfiles de lenguaje: agregado recalculado. Rust pasa a 9 oros.
Verificado#
Los 7 stacks levantados con Docker. Con 2.000 escrituras y 8% de fallo del indice, los siete producen resultados identicos hasta el ultimo digito:
dual-write: missing=10 stale=50 orphan=19 drift=79
recall 98,95% precision 98,02% silent_failures=158
outbox+barrido: missing=0 stale=0 orphan=0 drift=0
recall 100% precision 100% retries=157 checkpoint=2000El escenario es determinista a proposito: cuando el numero es el mismo en los siete, lo unico que queda para comparar es como se escribe.
2026-08-17 - Caso 18: arranque en frio y retraso del autoescalado en los 7 stacks#
Sexto caso del Eje 1 del ROADMAP. El lab pasa a 18 casos x 7 stacks = 126 endpoints.
Added — caso 18, arranque en frio y retraso del autoescalado#
cases/18-cold-start-and-autoscale-lag/ con las 7 implementaciones.
Contrato uniforme: /boot-cold y /boot-warmed con clientes concurrentes que
miden availability_pct durante el escalado, mas /health y /ready
separados de verdad y /warmup para construir el pool tibio antes del trafico.
Metrica central: health_vs_ready_gap_ms — la ventana exacta en la que el
sistema afirma estar disponible sin estarlo. No es cuanto tarda un servicio en
arrancar: es cuanto tiempo miente mientras arranca.
Es el unico caso del lab que MIDE el runtime en vez de simularlo#
En los diecisiete casos anteriores, el fenomeno se modela. Aca no.
El trabajo por peticion es el mismo lazo entero puro en los siete stacks,
sin un solo sleep, sin I/O, sin asignacion. warmup_speedup_x es el cociente
entre el p99 de las primeras 100 peticiones y el de las que siguen a la 1000:
que hace ese runtime con el mismo codigo repetido mil veces.
| Stack | warmup_speedup_x | Que lo explica |
|---|---|---|
| ☕ Java 21 | 51,9x | interpretado → C1 (~200 llamados) → C2 (~10.000, con perfil) |
| 🔵 .NET 8 | 2,3x | Tier 0 → Tier 1 a los ~30 llamados, con OSR |
| 🐍 Python 3.12 | 1,8x | no es JIT: es contencion con los hilos que inicializan |
| 🟢 Node.js 22 | 1,1x | V8 llega a TurboFan enseguida en un lazo asi de simple |
| 🐘 PHP 8.3 | 1,1x | el JIT existe desde 8.0 y viene apagado |
| 🐹 Go 1.23 | 1,0x | binario AOT: la peticion 1 corre el mismo codigo que la 100.000 |
| 🦀 Rust 1.83 | 1,00x | igual, y sin runtime ni GC que inicializar |
Lo que si esta modelado, y queda escrito en cada README: la parte de I/O de la
inicializacion —abrir el pool, DNS, TLS— es un sleep de io_ms, porque
esperar a la red no quema CPU y fijarla es lo que vuelve comparables a los
siete. Y en la variante fria, p99_first_100_ms mezcla el calentamiento del
runtime con la contencion de las instancias que estan inicializando: los dos
efectos ocurren de verdad en produccion, pero es una mezcla. El 1,8x de Python
es contencion pura. El 51,9x de Java no.
El hallazgo que no estaba en la especificacion: el sistema se realimenta#
Del postmortem del caso: las instancias frias de Java atienden lento despues de declararse listas. Esa lentitud mantiene la CPU alta. La CPU alta vuelve a disparar al autoescalador. El autoescalador produce mas instancias frias.
Ninguna de las dos partes esta rota. Un sistema que escala por una metrica que su propio arranque empeora se realimenta, y eso no aparece en ningun dashboard porque no hay nada en rojo.
Changed — el ranking se cruza en un caso#
- Go toma su septimo oro. No gana por rapido: gana por **no tener nada que
calentar**, y porque
sync.Oncees la forma mas legible del lab de decir "esto cuesta una sola vez". Que tambien es la trampa: unasync.Onceen el camino de la peticion convierte a la primera peticion de cada proceso en la mas lenta de todas. - Rust segundo, un caso despues de quedar sexto.
OnceLockes elequivalente de
sync.Oncey deLazy<T>con algo que ninguno de los dos tiene: el tipo garantiza que el valor no se puede leer antes de estar inicializado. Olvidar el chequeo de readiness deja de ser un bug de runtime y pasa a ser un error de compilacion. - .NET tercero por una razon que no es tecnica sino de friccion: tiene la
curva, y tiene la respuesta en la caja.
PublishReadyToRun,TieredPGOyPublishAotson tres lineas del.csproj. - Java septimo, un caso despues de ganar el 17. Tiene las herramientas mas
potentes contra su propio problema —AppCDS,
TieredStopAtLevel, GraalVMnative-image— y ninguna viene activada. La diferencia con .NET no esta en tener herramientas: esta en que las de .NET vienen puestas.
Ese cruce —Java 🥇 en el 17 y 7º en el 18; Rust 6º en el 17 y 🥈 en el 18— es el punto del laboratorio. Un caso que siempre ordena igual a los siete stacks no esta midiendo nada.
Changed — integracion#
- Dispatchers: registro del caso 18 y puerto interno en los siete
(
:9018PHP/Python/Node,:9418Java,:9518.NET,:9618Go,:9718Rust). ci.yml: matrizcompose-configde 127 a 134 archivos;hub-probevalida 18 casos por stack;
compose-smokesumacase18-dotnetycase18-rust.shared/catalog/cases.json+docs/case-catalog.md+ los cinco SVG.- Perfiles de lenguaje: agregado recalculado. Go pasa a 7 oros.
Verificado#
Los 7 stacks levantados con Docker. Con 2.400 peticiones, 3 instancias y 8
clientes: la variante fria rechaza entre el 12% y el 42% del trafico segun el
throughput de cada runtime, con el proceso vivo y /health en 200 todo el
tiempo; la variante con pool tibio y enrutado por readiness rechaza cero, con
100% de disponibilidad. Identico en los siete.
2026-08-17 - Caso 17: migracion de esquema sin downtime en los 7 stacks#
Quinto caso del Eje 1 del ROADMAP. El lab pasa a 17 casos x 7 stacks = 119 endpoints.
Added — caso 17, migracion de esquema sin downtime#
cases/17-zero-downtime-schema-migration/ con las 7 implementaciones.
Contrato uniforme: /migrate-blocking y /migrate-expand-contract con lectores
concurrentes que miden availability_pct durante la migracion, mas
/migration/state y /backfill. Metrica central: longest_single_lock_ms,
que resulto ser la que decide si la app se cae — distinta del tiempo total.
Expand-contract en cuatro fases, con el orden documentado:
- Expand — columna nullable. Es metadata: instantaneo.
- Backfill — por lotes, soltando el lock entre cada uno.
- Switch — feature flag que cambia lecturas y escrituras.
- Contract — recien ahora, en un despliegue posterior, se borra la vieja.
El switch va antes del contract porque el flag es lo unico reversible en un segundo. Si se borra la columna vieja primero, volver atras requiere otra migracion — y a esa altura ya no hay a donde volver.
La premisa del ROADMAP resulto equivocada, y eso cambio el caso#
El ROADMAP planeaba este caso solo para PHP + PostgreSQL, con el argumento de que "los stacks de SQLite embebido lo modelan mas como ejercicio".
Al implementarlo en los siete quedo claro que el caso no necesita un motor: necesita un read-write lock. Y ahi cada runtime tiene algo distinto que decir, incluido el que no tiene la primitiva.
| Stack | Read-write lock | Deadline del lector |
|---|---|---|
| PHP | flock del sistema operativo, entre procesos | LOCK_NB de fabrica |
| Python | no existe — se construye sobre Condition | Condition.wait(timeout) |
| Node | no existe — el lock es el event loop | imposible |
| Java | ReentrantReadWriteLock | tryLock(timeout, unit) |
| .NET | ReaderWriterLockSlim (IDisposable) | TryEnterReadLock(ms) |
| Go | sync.RWMutex | armado con goroutine + select |
| Rust | std::sync::RwLock | solo spin acotado |
Tres movimientos en el ranking que no habian pasado antes#
- PHP sube al segundo puesto — primera vez en el Eje 1 que sale del ultimo
lugar. Su
flockconLOCK_SH/LOCK_EXes el unico read-write lock del laboratorio provisto por el sistema operativo, y el unico que coordina procesos en vez de hilos: exactamente lo que hace un motor de base de datos. - Rust cae al sexto, y es **el primer caso del lab donde su respuesta es peor
que la de los otros seis**. La
stdno ofreceRwLockcon deadline de ninguna clase — nitry_read_for, ni nada equivalente — asi que la unica opcion sin crates externas es un spin que consume CPU en vez de dormir en el kernel. Quedo escrito con el mismo enfasis con el que se documentan sus ventajas en los casos 12, 14 y 16: un laboratorio que solo muestra donde gana un lenguaje no es un laboratorio, es publicidad. - Node septimo con el modo de falla mas severo del caso: el lock exclusivo
es el event loop entero, asi que ni siquiera el timeout del lector puede dispararse. En los otros seis un lector con
tryLock(120ms)al menos falla rapido y devuelve 503; aca no falla — no responde.
Java primero por ser el unico stack con deadline y equidad de fabrica:
tryLock(timeout, unit) y el flag de justicia en el constructor. Sin ese flag,
el trafico de lectura constante puede impedir que el escritor entre nunca — la
migracion no arranca y la aplicacion funciona perfecto, que es el peor modo de
fallar porque nada se ve roto.
Changed — integracion#
- Dispatchers: registro del caso 17 y puerto interno en los siete
(
:9017PHP/Python/Node,:9417Java,:9517.NET,:9617Go,:9717Rust). ci.yml: matrizcompose-configde 120 a 127 archivos;hub-probevalida 17 casos por stack;
compose-smokesumacase17-javaycase17-go.shared/catalog/cases.json+docs/case-catalog.md+ los cinco SVG.- Perfiles de lenguaje: agregado recalculado. Java pasa a 2 oros.
Verificado#
Los 7 stacks levantados con Docker. Con 20.000 filas y 8 lectores concurrentes: la variante bloqueante mantiene el lock ~400 ms de corrido y rechaza lectores (24 en la mayoria de los stacks, 8 en PHP y Node por su modelo de ejecucion); expand-contract hace el mismo trabajo en 10 lotes, baja el lock mas largo a ~40 ms y deja 0 lectores rechazados con 100% de disponibilidad. Identico en los siete.
lock_held_ms total es casi el mismo en las dos variantes: el trabajo no
desaparece, se reparte.
2026-08-17 - Caso 16: idempotencia y efectos duplicados en los 7 stacks#
Cuarto caso del Eje 1 del ROADMAP. El lab pasa a 16 casos x 7 stacks = 112 endpoints. Mitad del eje entregada.
Added — caso 16, idempotencia y efectos duplicados#
cases/16-idempotency-and-duplicate-effects/ con las 7 implementaciones.
Contrato uniforme: /charge-unsafe y /charge-idempotent sobre los mismos N
reintentos de una misma Idempotency-Key, mas /idempotency/state y /outbox.
Metrica central: charges_applied — y overcharged_cents, que traduce el
bug a la unidad en que el negocio lo discute.
El caso tiene dos mitades:
- La reserva atomica de la clave.
if (!existe) { crear }son dosoperaciones con una ventana en el medio; con cinco reintentos concurrentes esa ventana produce cinco cobros. La version correcta es una sola operacion:
putIfAbsent,TryAdd,LoadOrStore,entry(),INSERT ... ON CONFLICT. - El outbox pattern. El cargo va a la base y el email a una cola, sin
transaccion que los abarque. El outbox escribe el efecto en la misma escritura que el cargo y deja que un worker lo entregue — at-least-once, y es deliberado: duplicar un email es visible y corregible, perderlo no.
El hallazgo del caso: el ranking y la realidad operativa no coinciden#
Es el primer caso del lab donde la conclusion del veredicto y la decision de despliegue apuntan a stacks distintos, y quedo documentado en vez de escondido.
Seis de las siete implementaciones resuelven la carrera dentro de su proceso:
putIfAbsent (Java), TryAdd (.NET), LoadOrStore (Go), entry() (Rust),
setdefault (Python) y el Map de Node. Todas correctas con una replica, todas
incorrectas con dos — cada pod tiene su tabla, ninguno ve las claves del
otro, y el mismo pago se cobra una vez por pod.
La septima es la de PHP. Sin heap compartido entre requests, esta obligada a
poner la clave en almacenamiento con una operacion atomica del motor
(ON CONFLICT DO NOTHING, modelado con flock). Es la que peor puntua en fit de
primitivas — septimo puesto — y la unica que se podria desplegar con tres
replicas.
El ranking mide expresividad. La pregunta operativa es otra. Las dos respuestas
conviven en el comparison.md sin que una tape a la otra.
Lo que distingue a cada stack#
- Rust primero: el unico donde **ignorar el resultado de la reserva no
compila**. El
matchsobreOccupied/Vacantes exhaustivo, y elEntrypresta el mapa mientras existe — asi que la ventana check-then-act no es dificil de escribir, es inexpresable. En Java, .NET y Go,putIfAbsent(k, v);con el retorno descartado compila sin queja, y ese descarte es el bug. - .NET cuarto por una razon interna al propio lab:
TryAddsi esatomico, a diferencia de
GetOrAddcon fabrica, que en el caso 13 hubo que envolver enLazy<T>. Dos APIs en la misma clase con garantias distintas. - Go tercero y con un contraste util contra el caso 13: alli
sync.Maperala eleccion equivocada porque cada entrada se creaba y se borraba en cada expiracion; aca es la documentada, porque las claves se escriben una vez y se leen muchas. Mismo lab, dos casos, dos respuestas opuestas.
- Python quinto:
setdefaultexpresa bien la operacion, pero su atomicidadviene del GIL y no del contrato del lenguaje. Por eso el codigo toma igual un
Lockexplicito: apoyarse en un detalle de CPython para decir "esto es indivisible" es escribir codigo que depende de algo que puede cambiar. - Node sexto con el matiz mas incomodo:
has()+set()es atomico porqueno hay otro hilo, asi que el codigo correcto es el mas corto de los siete — y deja de ser correcto al escalar a dos procesos, sin ningun aviso.
Changed — integracion#
- Dispatchers: registro del caso 16 y puerto interno en los siete
(
:9016PHP/Python/Node,:9416Java,:9516.NET,:9616Go,:9716Rust). ci.yml: matrizcompose-configde 113 a 120 archivos;hub-probevalida 16 casos por stack;
compose-smokesumacase16-phpycase16-rust.shared/catalog/cases.json+docs/case-catalog.md+ los cinco SVG.- Perfiles de lenguaje: agregado recalculado. Rust pasa a 8 oros, y su
media baja a 1.9 — empata con Go por primera vez.
Verificado#
Los 7 stacks levantados con Docker. Con 5 reintentos de un pago de $25: sin clave 5 cargos, $100 cobrados de mas y 5 emails; con clave 1 cargo, 4 duplicados evitados y 1 email por outbox. Identico en los siete.
2026-08-17 - Caso 15: backpressure en colas de mensajes en los 7 stacks#
Tercer caso del Eje 1 del ROADMAP. El lab pasa a 15 casos x 7 stacks = 105 endpoints.
Added — caso 15, backpressure en colas de mensajes#
cases/15-message-queue-backpressure/ con las 7 implementaciones.
Contrato uniforme: /produce-unbounded y /produce-bounded con tres politicas
por parametro (block, drop_oldest, dead_letter), mas /queue/state y
/dlq. Metricas centrales: queue_depth_peak y oldest_msg_age_ms_peak —
las dos que casi nunca estan en el dashboard y son las unicas que delatan el
problema.
El caso es sobre que no hay opcion gratis:
| Politica | Que paga |
|---|---|
block | latencia: la lentitud viaja aguas arriba hasta el cliente |
drop_oldest | datos: se pierden mensajes, en silencio salvo que se cuenten |
dead_letter | deuda operativa: alguien tiene que mirar esa cola (caso 20) |
La cola sin limite parece una cuarta opcion sin costo. No lo es: solo difiere el pago hasta el OOM, y mientras tanto el throughput se ve perfecto.
El criterio de ranking cambio respecto de los otros casos#
Aca no se midio cual stack expresa mejor la solucion sino cual hace mas dificil escribir el bug, porque el bug tiene dos formas: la cola sin techo y el descarte que nadie cuenta.
- Go primero porque no existe
make(chan T)con buffer infinito. La versionincorrecta hay que construirla a mano con una slice y un mutex, y sale mas larga que la correcta.
- Rust segundo: el limite esta en el tipo (
Sender<T>vsSyncSender<T>),asi que la confusion no compila. Y
TrySendError::Full(T)devuelve la propiedad del mensaje rechazado — la mejor primitiva del set para una DLQ. - .NET tercero: unico stack donde la politica es un enum del constructor,
decidida una vez para todo el sistema, con callback de descarte incluido.
- Java quinto pese a tener la mejor taxonomia de rechazo (
put/offer/offer(timeout), espejo de lasRejectedExecutionHandler): porqueConcurrentLinkedQueueimplementa la misma interfazQueuequeArrayBlockingQueuey no tiene capacidad. Sacar el freno del sistema entero es un cambio de una linea que compila y pasa los tests. - Node sexto siendo el **unico stack donde el backpressure es parte del
protocolo del runtime** (
write()devuelvefalse,'drain'avisa cuando seguir) — porque tambien es el unico donde ignorar esa señal compila, pasa los tests y funciona en desarrollo. - PHP septimo, y con la leccion mas transferible: no tiene cola en proceso,
asi que su backpressure vive en
listen.backlogde FPM, enpm.max_childreny en la DLQ del broker. Es el stack que mejor enseña que el freno es una propiedad del sistema entero, no de la cola.
Fuera de alcance a proposito#
El ROADMAP pedia "slow-down al producer devolviendo 429". No se implemento: devolver 429 sin backoff del cliente alimenta una tormenta de reintentos, que es el caso 04. Queda anotado como frontera entre casos, no como deuda.
Fixed durante la construccion#
self._stop = threading.Event() en el consumidor de Python pisaba
Thread._stop(), un metodo interno que join() llama. El sintoma era un
TypeError: 'Event' object is not callable en cada rafaga. Renombrado a
_halt. Es el tipo de colision que solo aparece al heredar de Thread.
Changed — integracion#
- Dispatchers: registro del caso 15 y puerto interno en los siete
(
:9015PHP/Python/Node,:9415Java,:9515.NET,:9615Go,:9715Rust). ci.yml: matrizcompose-configde 106 a 113 archivos;hub-probevalida 15 casos por stack;
compose-smokesumacase15-pythonycase15-node.shared/catalog/cases.json+docs/case-catalog.md+ los cinco SVG.- Perfiles de lenguaje: agregado de veredictos recalculado. **Go pasa a 6
oros**, Rust conserva 7.
Verificado#
Los 7 stacks levantados con Docker. Con 120 mensajes y consumidor 3x mas lento:
sin limite queue_depth_peak = 120 y oldest_msg_age_ms_peak ~ 250-475 ms;
acotada a 32 esa espera baja a ~70-134 ms. Las tres politicas producen su costo
propio: ~200 ms de productor frenado en block, 87-88 descartados en
drop_oldest, 87-88 a la DLQ en dead_letter. Identico en los siete.
2026-08-17 - Caso 14: agotamiento del pool de conexiones en los 7 stacks#
Segundo caso del Eje 1 del ROADMAP. El lab pasa a 14 casos x 7 stacks = 98 endpoints.
Added — caso 14, agotamiento del pool de conexiones#
cases/14-connection-pool-exhaustion/ con las 7 implementaciones, no las
tres que el ROADMAP preveia, por la misma razon estructural del caso 13.
Contrato uniforme: /pool-leaky y /pool-managed sobre la misma carga, con
leaked = acquired - released como metrica central, mas hung,
failed_timeout, pool_available_after, pool_wait_ms_p99 y littles_law.
El caso combina dos defectos que se necesitan mutuamente: la devolucion solo en el camino feliz (cada excepcion se lleva una conexion) y la adquisicion sin deadline (el que llega tarde no falla, se queda). El resultado es una indisponibilidad que no produce errores: los requests no terminan, asi que no generan muestras de latencia y el p99 no se dispara — desaparece del grafico.
| Stack | Pool | Deadline | Garantia de devolucion |
|---|---|---|---|
| PHP | array en el proceso | — (un solo proceso) | finally |
| Python | queue.Queue(maxsize=N) | get(timeout=...) | @contextmanager |
| Node | array + cola de waiters | AbortSignal.timeout() | finally en async |
| Java | ArrayBlockingQueue | poll(timeout, unit) | try-with-resources |
| .NET | SemaphoreSlim + ConcurrentBag | WaitAsync(timeout) → false | using var |
| Go | chan *conn bufferizado | select + time.NewTimer | defer |
| Rust | Mutex<Vec<Conn>> + Condvar | wait_timeout | impl Drop |
El hallazgo del caso: Rust gana por lo que impide#
Es el unico caso del laboratorio donde Rust primero no es por expresividad
sino por lo que el lenguaje no deja escribir. Con impl Drop la fuga no se
puede producir por descuido: el Drop corre en el return feliz, en el temprano
y durante el desenrollado por panic.
Por eso la variante leaky de Rust tuvo que escribirse a proposito con
std::mem::forget(lease) — la unica forma de perder un recurso en Rust seguro.
En los otros seis stacks el leak es lo que pasa si uno se distrae; aca hay que
pedirlo por su nombre, y el nombre es grepeable.
El reverso: Go baja al quinto puesto por una sola linea. El canal
bufferizado como pool es la expresion mas economica del set, pero defer
p.release(c) hay que acordarse de escribirlo, y olvidarlo compila igual.
Una decision de fidelidad al reves que la del caso 13#
Aca el trabajo que retiene la conexion si es un sleep. Una conexion se
retiene mientras se espera a la red, no mientras se quema CPU. En el caso 13 un
sleep habria escondido el punto; aca quemar CPU lo esconderia. Misma pregunta
—que recurso escasea de verdad—, respuestas opuestas, y las dos documentadas.
Fixed durante la construccion#
(idx % 100) < fail_rate parecia un reparto de fallos razonable y no lo era:
con 24 requests y fail_rate=25 fallaban las 24, porque todos los indices
son menores que 25. La variante managed reportaba 24 fallos de query y el
contraste quedaba ilegible. Se reemplazo por (idx * 37) % 100 < fail_rate, que
dispersa los fallos por toda la tanda. Aplicado en los siete stacks.
Changed — integracion#
- Dispatchers: registro del caso 14 y puerto interno en los siete
(
:9014PHP/Python/Node,:9414Java,:9514.NET,:9614Go,:9714Rust). ci.yml: matrizcompose-configde 99 a 106 archivos;hub-probevalida 14 casos por stack;
compose-smokesumacase14-javaycase14-dotnet.shared/catalog/cases.json+docs/case-catalog.md+ los cinco SVG.- Perfiles de lenguaje: agregado de veredictos recalculado desde los
comparison.md. Rust pasa a 7 oros (gana tambien el 14).
Verificado#
Los 7 stacks levantados con Docker. Con pool de 4, 24 requests y 25% de fallo:
leaked=4, hung≈12, pool_available_after=0/4 y ~2 s de pared en la variante
con fuga; leaked=0, pool_available_after=4/4 y ~155 ms en la corregida.
Identico en los siete.
2026-08-17 - Eje 1 abre con el caso 13: cache stampede en los 7 stacks#
Primer caso del Eje 1 del ROADMAP (casos nuevos de la vida real, 13-20). El lab pasa de 12 a 13 casos x 7 stacks = 91 endpoints.
Added — caso 13, cache stampede y thundering herd#
cases/13-cache-stampede-and-thundering-herd/ con las 7 implementaciones,
no las tres que el ROADMAP preveia. La razon es estructural: validate-structure.sh
exige las siete carpetas de stack por caso, y servir /13/ en tres hubs y 404 en
los otros cuatro habria roto la simetria que es la identidad del laboratorio.
Contrato uniforme en los siete: /cache-naive y /cache-singleflight sobre la
misma rafaga, con origin_computations como metrica central, mas
stampede_depth, coalesced_waiters, served_stale y p99_wait_ms.
Primitiva idiomatica distinta por runtime:
| Stack | Primitiva | De donde sale la garantia de ejecucion unica |
|---|---|---|
| PHP | flock(LOCK_EX) + double-checked locking | del sistema de archivos, entre procesos |
| Python | dict de vuelos + threading.Event | del Lock que protege el dict |
| Node | Map<key, Promise> | del orden que escribe el autor (set antes del await) |
| Java | ConcurrentHashMap.computeIfAbsent | del mapa: atomica por clave |
| .NET | Lazy<Task<T>> en ConcurrentDictionary | del Lazy, no del diccionario |
| Go | sync.WaitGroup + map bajo Mutex | del mutex que protege el registro |
| Rust | Arc<Flight> con Mutex + Condvar | del mutex, y el Arc la hace segura de por vida |
Fixed durante la construccion — dos cosas que el caso enseñaba mal#
1. Single-flight sin double check da 3 o 4 recalculos, no 1. La primera
version registraba el vuelo, calculaba y borraba la entrada. Con cost chico, el
lider de la primera generacion terminaba antes de que los ultimos llamadores
llegaran al registro, y esos se volvian lideres de una segunda generacion. Java
daba 3, Go 2, Rust 7. El arreglo es una relectura de la cache dentro del
vuelo — el mismo double check que PHP no puede omitir porque su lock vive en el
almacenamiento. Quedo aplicado en los siete y documentado como la mitad del
patron que se olvida.
2. En Python la estampida no se dejaba observar. Sin barrera, el primer hilo
completaba su digest dentro de su propio quantum del GIL y los otros quince
encontraban el valor fresco: origin_computations daba 1 y la variante naive
parecia correcta. Un falso verde que dependia de sys.setswitchinterval. La
barrera de dos fases no infla el numero: reproduce que, cuando una clave caliente
expira, los N requests ya estaban en vuelo y todos leyeron la cache antes de que
ninguno alcanzara a escribirla.
Changed — integracion en los 7 hubs#
- Dispatchers: registro del caso 13 y puerto interno en los siete
(
:9013PHP/Python/Node,:9413Java,:9513.NET,:9613Go,:9713Rust), mas elCOPYcorrespondiente en cada Dockerfile, elspawn_casedeentrypoint.shy el miembrocase13del workspace de cargo. ci.yml: matrizcompose-configde 92 a 99 archivos;hub-probevalida los 13 casos por stack en un solo boot;
compose-smokesumacase13-goycase13-rust.shared/catalog/cases.json: entrada completa del caso 13 conruntime_entriesde los siete stacks.docs/case-catalog.mdy los cinco SVG dedocs/assets/regenerados desde ahi.
Changed — generadores que dejan de hardcodear el conteo#
generate_case_catalog.php, generate_diagrams.py y check-language-versions.sh
derivaban el numero de casos de una constante escrita a mano. Ahora lo cuentan.
Es lo que evita que el proximo caso deje cinco diagramas diciendo "12" para
siempre.
Changed — barrido documental#
README.md, ARCHITECTURE.md, RECRUITER.md, RUNBOOK.md, INSTALL.md,
SECURITY.md, AWS_MIGRATION.md, ROADMAP.md, docs/architecture.md,
docs/executive-summary.md, docs/problem-map.md, docs/QUE-ES-ESTO.md,
docs/stack-map.md, docs/docker-strategy.md, docs/BEGINNERS_GUIDE.md,
docs/usage-and-scope.md, docs/positioning-and-objective.md,
docs/language-upgrade-protocol.md y los siete docs/languages/*.md.
En los perfiles de lenguaje se recalculo el agregado de veredictos leyendo los
comparison.md con el mismo parser de generate_diagrams.py, en vez de editar
los numeros a mano: Go pasa a 5 oros (gana tambien el 13), Rust conserva 6.
Los ADR de docs/adr/ y las entradas historicas de este CHANGELOG no se
tocaron: son registros fechados, no afirmaciones sobre el estado de hoy.
Verificado#
Los siete hubs levantados con Docker y probados caso por caso: 13/13 en
compose.root.yml, compose.python.yml, compose.nodejs.yml,
compose.java.yml, compose.dotnet.yml, compose.go.yml y compose.rust.yml.
Con concurrency=16: los siete dan 16 recalculos en la variante naive y
1 recalculo con 15 coalesced_waiters en la corregida.
2026-08-04 - Perfiles de lenguaje, protocolo de version y dossier PDF#
El workflow language-drift.yml (2026-08-03) detecta que un lenguaje publico
version nueva. Faltaba lo que viene despues: donde esta escrito que revisar.
Sin eso, el aviso llega y nadie sabe si el caso 03 sigue siendo correcto o si
paso a enseñar la forma vieja de hacer las cosas.
Added — perfiles de lenguaje#
docs/languages/ con un perfil por stack (php, python, node, java,
dotnet, go, rust) mas indice. Cada uno documenta seis cosas:
| Seccion | Que responde |
|---|---|
| Identidad | Que es el lenguaje y para que se usa fuera del lab |
| Modelo de ejecucion | Como corre el codigo, porque de ahi sale que primitiva es la correcta |
| Primitivas en el lab | Que usa cada uno de los 12 casos, con enlace al codigo |
| Rendimiento | Que mide el lab en ese stack y como reproducirlo, con comandos |
| Limites y problemas sin solucion | Lo que ese runtime no puede hacer y que caso lo deja visible |
| Ciclo de versiones | Version fijada, cadencia upstream y que revisar en el proximo salto |
Las secciones de rendimiento no publican benchmarks entre lenguajes: documentan que señal expone cada runtime, de donde sale y como reproducir la medicion. La pendiente legacy/optimized dentro de un mismo stack es comparable; el tiempo absoluto entre stacks no lo es.
Added — protocolo de actualizacion#
docs/language-upgrade-protocol.md: checklist de 10 puntos, en orden. El punto
de partida no es el Dockerfile sino el perfil del lenguaje — al reves se
termina con un repositorio que compila en la version nueva y sigue enseñando lo
de la version vieja. Enlazado desde README.md, desde el body del issue que
genera language_drift.py y desde los siete perfiles.
Disparadores concretos ya anotados: ScopedValue fuera de preview (Java 25,
caso 03), node:sqlite fuera de experimental (Node 24, casos 01 y 02),
free-threading de Python (PEP 703, caso 11) y cancelacion en std de Rust
(casos 04 y 09).
Added — diagramas derivados del catalogo#
scripts/generate_diagrams.py emite cinco SVG en docs/assets/. No se dibujan
a mano: salen de shared/catalog/cases.json y —el mapa de calor de fit— de la
seccion Veredicto de los once comparison.md que la tienen. --check corre
en CI: si entra un octavo stack y nadie redibuja, el PR falla.
| Diagrama | Fuente |
|---|---|
stack-matrix.svg | catalogo: 12 casos x 7 stacks |
case-map.svg | catalogo: casos por categoria |
execution-models.svg | catalogo: bloque languages |
fit-ranking.svg | veredictos de los comparison.md |
language-upgrade-flow.svg | el protocolo |
Added — dossier PDF#
scripts/build_dossier_pdf.py compila la documentacion en un PDF con portada,
indice, tablas, bloques de codigo y los SVG embebidos como vectores (via
svglib, sin rasterizar). Dos perfiles: completo (todos los .md) y
ejecutivo (raiz + docs/ + README y comparativa de cada caso). Salida en
dist/.
Added — explicacion para personas no tecnicas#
docs/QUE-ES-ESTO.md: los 12 problemas en lenguaje de todos los dias, sin jerga,
con la analogia del taller mecanico. docs/BEGINNERS_GUIDE.md se reescribio
—decia "operativos en PHP, Python y Node.js" cuando ya eran siete stacks— y
ahora incluye un primer experimento reproducible.
Changed — barrido visual sobre 215 archivos#
- H1 con icono coherente por familia documental.
- Navegacion de retorno en los 84 README por stack (caso · comparativa · perfil del lenguaje) y en los 84 documentos de
cases/NN/docs/. - Los stubs finos (
business-value.md,context.md,shared/README.md) pasan a traer ficha del caso, URLs por stack y nota de honestidad, sincronizados con el catalogo.
Fixed — incoherencias que el barrido dejo a la vista#
| Hallazgo | Estado anterior | Ahora |
|---|---|---|
cases.json → languages | 5 stacks; decia que Java "aun sin casos operativos" | 7 stacks con version, imagen, hub, modelo de ejecucion y perfil |
runtime_entries.node | apuntaba a los puertos aislados (821…) con compose_path por caso | hub :8300 con /NN/, simetrico con Java y .NET; el aislado se conserva en isolated_port |
portal/Dockerfile | php:8.2-apache contra php:8.3-cli-alpine en los casos | php:8.3-apache — el repositorio fijaba dos versiones de PHP |
| Badge de stacks en 9 README de caso | PHP · Python · Node · Java · .NET | los 7, enlazando a docs/languages/ |
cases/03/README.md | sin H1 ni badges | H1, estado, stacks y categoria |
recommended_github_topics | sin go ni rust | completos |
proof_points del caso 03 | "transferibilidad en PHP, Node.js y Python" | los siete stacks |
2026-08-03 - Barrido documental: el lab pasa de 5 a 7 stacks#
Cierra el ciclo abierto por los dos stacks nuevos. Toda la documentacion que afirmaba "5 stacks / 60 endpoints / 5 hubs / 8 puertos" quedaba desalineada con el arbol desde el momento en que Go y Rust entraron.
Changed (conteos y afirmaciones de estado)#
| Afirmacion | Antes | Ahora |
|---|---|---|
| Stacks operativos | 5 | 7 |
| Endpoints (12 casos × stacks) | 60 | 84 |
| Hubs simetricos | 5 (8100-8500) | 7 (8100-8700) |
| Puertos que cubren el lab | 8 | 10 |
compose-config en CI | 66 archivos | 92 archivos |
hub-probe en CI | Python/Node/Java/.NET | + Go / Rust |
| Lambdas en la ruta serverless (AWS) | 60 | 84 |
| Services en ECS Fargate | 6 | 9 |
Archivos tocados: README.md, ARCHITECTURE.md, ROADMAP.md, RUNBOOK.md,
INSTALL.md, RECRUITER.md, AWS_MIGRATION.md, docs/architecture.md,
docs/executive-summary.md, docs/usage-and-scope.md, docs/docker-strategy.md,
docs/stack-map.md, docs/adr/0003-docker-per-case-per-stack.md, los 12
comparison.md y los 12 README.md de caso.
Added (la comparativa, que era el punto)#
- Secciones Go y Rust en los 12
comparison.md, con el codigo real de cadastack — no una traduccion generica del texto de Java.
- Tabla "Primitiva central por stack" en los 12 casos: siete filas, una por
lenguaje, diciendo cual es la primitiva y donde duele el problema en cada uno.
docs/stack-map.md: filas de Go y Rust con su fortaleza y su contrapartida.
Corregido: una afirmacion que se pasaba de la raya#
El README de Go caso 03 decia que perder la correlacion "pasa de ser un bug de
runtime a un error de compilacion". Es falso. Lanzar go func(){}() sin
pasar el ctx compila perfectamente y ese trabajo queda sin correlacionar. Go
hace la dependencia visible en la firma, no obligatoria. La garantia de
compilador para ese problema esta en Rust (&RequestCtx con lifetime acotado),
no en Go. Corregido, y la distincion quedo explicita en el comparison.md del
caso 03.
Lo que la documentacion ahora dice y antes no#
Tres limitaciones que estaban en el codigo pero no en los documentos:
- Caso 04: Go es el unico stack donde el deadline cancela el trabajo aguas
abajo. Rust con
stdy Java quedan en el mismo lugar —recv_timeoutyorTimeout()cortan la espera, no el trabajo. Es el unico caso del lab donde Rust queda por detras de Go en la primitiva central, y esta escrito asi. - Caso 05: Rust no impide la fuga. El borrow checker previene use-after-free
y data races; no previene "guardar de mas". Un
Vecglobal que crece compila sin warnings. - Caso 11: ni Go ni Rust tienen pool de threads que agotar, asi que el caso
no se traduce literal desde Java. Se modela con semaforo de concurrencia.
Changed (metadatos del repositorio)#
- Descripcion de GitHub: de "5 stacks" a "7 stacks", con Go y Rust listados.
De paso se corrigio el mojibake que arrastraba (
ingenier<?>a). - Topics: se agregan
goyrust.
GitHub Pages queda fuera de alcance por decision explicita: no esta habilitado en el repositorio y no se habilita en esta entrega.
2026-08-03 - Fidelidad universal del caso 01: Node/Java/.NET pasan a SQLite real#
Cierra la ultima deuda de fidelidad abierta del lab. El caso 01 vendia "los 5 stacks resuelven el mismo problema" mientras 3 de los 5 simulaban el substrato del fallo con setTimeout / sleepMicros / Thread.SpinWait sobre listas en memoria. Ahora los cinco ejecutan SQL real contra un motor.
El problema que se cierra#
db_hits era una metrica derivada en Node/Java/.NET — contaba iteraciones de un bucle, no ejecuciones contra un motor. Peor: el caso enseña filtro no sargable, un concepto que solo existe si hay un query planner. Sin motor, "no sargable" era una afirmacion del README que nada respaldaba.
Changed (codigo)#
- Caso 01 Node:
node:sqlite(DatabaseSync, built-in desde Node 22.5, sinnpm installni bindings nativos). Esquema completo concustomers,orders,customer_daily_summary,worker_state,job_runs. La ruta legacy ejecuta1 + 2Nqueries reales; la optimizada resuelve los detalles conROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY created_at DESC)en una sola query. Imagen basenode:20-alpine→node:22-alpinecon--experimental-sqlite. - Caso 01 Java:
sqlite-jdbc3.46.1.3, archivo bajo/tmpconjournal_mode=WAL, conexion por request contry-with-resources. El worker corre con su propia conexion. - Caso 01 .NET:
Microsoft.Data.Sqlite8.0.10, misma estrategia de archivo + WAL,using/IDisposablepara el cierre deterministico. Espejo exacto del Java: mismo esquema, mismas queries, mismos resultados fila por fila. java-dispatcher: el caso 01 se compila y ejecuta con/opt/sqlite-jdbc.jaren classpath, igual que el caso 02.Dispatcher.javapasaSQLITE_JDBC_JARcomoextraCpdel caso 01.
Por que WAL no es un detalle de implementacion#
El worker refresca customer_summary mientras los handlers leen. Sin journal_mode=WAL, el DELETE + INSERT ... SELECT del worker bloquea cada lectura concurrente — que es exactamente el fallo que el caso enseña a evitar. WAL es el equivalente embebido del MVCC que da PostgreSQL en el stack PHP, y por eso Java y .NET lo activan explicitamente.
El filtro no sargable, ahora verificable#
En Java y .NET la ruta legacy usa WHERE LOWER(region) LIKE 'n%' y la optimizada el mismo predicado reescrito como rango. El planner lo confirma:
… WHERE LOWER(region) LIKE 'n%' → SCAN orders
… WHERE region >= 'n' AND region < 'o' → SEARCH orders USING INDEX idx_orders_regionDeja de ser una afirmacion en prosa y pasa a ser reproducible con EXPLAIN QUERY PLAN.
Evidencia medida (via hub, limit=20)#
| Stack | Legacy | Optimized |
|---|---|---|
Node :8300/01 | 41 queries · 66.3 ms | 2 queries · 12.8 ms |
Java :8400/01 | 21 hits · 10.8 ms | 4 hits · 5.0 ms |
.NET :8500/01 | 21 hits · 13.4 ms | 4 hits · 8.3 ms |
Java y .NET devuelven cifras identicas y la misma primera fila (Customer 1315, order_id 12), con 1.531 filas en customer_summary en ambos — el determinismo cross-stack es verificable, no declarado.
Lo que NO cambio, a proposito#
El contrato JSON de cada stack. Java y .NET conservan su shape (variant/rows/db_hits, /reset-lab), distinto del de PHP/Python/Node (mode/data/db_queries_in_request, /reset-metrics). Converger esos contratos es el item "Suite de tests cross-stack" del ROADMAP, no este cambio: tocarlo aqui habria roto READMEs de los 12 casos y referencias en AWS_MIGRATION.md sin relacion con la fidelidad del substrato.
Deuda que queda registrada#
Node y Python conservan un round-trip artificial (ROUNDTRIP_*_MS, artificial_roundtrip_ms) que modela el hop de red que SQLite embebido no tiene. No es substrato simulado — es transporte simulado, y esta documentado en el codigo y en los README de stack.
Changed (docs)#
cases/01/comparison.md: la seccion "Fidelidad del substrato — asimetria honesta" se reemplaza por "los 5 stacks contra un motor real", con tabla de motor/driver/concurrencia y el bloqueEXPLAIN QUERY PLAN. Las tres secciones profundas (Node/Java/.NET) se reescriben con el SQL real en lugar de los snippets deMap/HashMap/Dictionary. La tabla final suma filas de driver y de cierre de recursos.cases/01/{node,java,dotnet}/README.md: seccion## Fidelidadreescrita, bloques de contraste con SQL real, y en Node el titulo y la nota de honestidad actualizados.cases/01/README.md: secciones por stack y arbol de directorios actualizados.README.mdraiz: la tabla "Honestidad de fidelidad" pasa a declarar fidelidad universal en casos 01 y 02; la asimetria restante se reencuadra como naturaleza del motor (solo PHP cruza un socket TCP).ROADMAP.md: "Fidelidad universal de caso 01" marcada completada con las dos decisiones de diseño que salieron del camino; "Estado actual" actualizado.shared/catalog/cases.json:level_detaildel caso 01 refleja los motores reales por stack.
2026-08-03 - .NET entra a CI + drift de docs corregido + --check del catalogo portable#
Cierra una brecha que quedo abierta al agregar el quinto stack: .NET tenia paridad de codigo pero cero cobertura en CI. Los 12 casos .NET y el hub :8500 podian romperse sin que ningun workflow se enterara, mientras ARCHITECTURE.md ya afirmaba que CI los validaba.
El problema descubierto#
fae2296 llevo .NET a los 12 casos, pero .github/workflows/ci.yml no mencionaba dotnet ni una sola vez. De los 78 compose versionados, la matriz compose-config validaba 53 — los 13 archivos .NET (compose.dotnet.yml + los 12 per-case) nunca se parseaban, y hub-probe solo levantaba Python/Node/Java.
Peor: la documentacion afirmaba lo contrario. ARCHITECTURE.md decia hub-probe los 5 hubs en CI y hub-probe (Python/Node/Java/.NET); ROADMAP.md se contradecia a si mismo entre la linea 13 (Python/Node/Java) y la 177 (Python/Node/Java/.NET). El repo declara que CI bloquea el drift entre lo que dice y lo que ejecuta — pero nada validaba esas frases.
Changed (CI)#
compose-config: matriz de 53 → 66 archivos. Se agregancompose.dotnet.ymly los 12cases/*/dotnet/compose.yml. Cobertura completa de los 5 hubs + portal + los 60 compose per-case.hub-probe: nueva entradadotnet-hub(compose.dotnet.yml, puerto8500) que valida los 12 casos .NET en un solo boot, igual que Python/Node/Java.hub-probe: la espera inicial del hub pasa de 50 a 120 intentos (100s → 240s). El dispatcher .NET spawnea 12 subprocesos y espera health secuencialmente antes de escuchar; en un runner de 2 vCPU el margen anterior quedaba justo. Es una cota superior — en el camino feliz sale al primer intento.
Fixed#
scripts/generate_case_catalog.php:--checkdaba un falso negativo permanente en Windows. Escribia conPHP_EOL(\r\nen Windows) y comparaba byte a byte contra un archivo que, concore.autocrlf=true, esta en CRLF en la copia de trabajo y en LF en el blob versionado. Ahora escribe LF explicito y compara normalizando fines de linea.make catalog-checkyvalidate-structure.shpasan en Windows, Linux y macOS por igual..gitignore: se ignora.claude/worktrees/. Los worktrees de agentes dejan copias completas del repo dentro del arbol (3.3 MB en el caso detectado) que ungit add .distraido habria versionado.
Changed (docs)#
ARCHITECTURE.md: diagrama de CI corregido —compose-config 66 archivos,portal-probe hub PHPcomo nodo propio yhub-probe Python/Node/Java/.NETen lugar delos 5 hubs. El hub PHP se valida porportal-probe, no porhub-probe; la tabla de mecanismos pasa de cinco a seis filas con esa distincion explicita.ROADMAP.md: se elimina la contradiccion interna sobre la cobertura dehub-probe;40+ archivospasa a66 archivosen ambas menciones. Fase 3 se marca completada — los 12docs/postmortem.mdexisten desde1102a5a, el ROADMAP todavia los daba por pendientes.README.md: la descripcion deAWS_MIGRATION.mdmencionaba los hubsPHP/Python/Node/Java— se agrega .NET, que el propio documento ya cubre.
2026-05-20 - Fidelidad de caso 02 restaurada en los 5 stacks + asimetria de caso 01 documentada + ROADMAP nuevo#
Cierra una asimetria de fidelidad que el lab tenia oculta: caso 02 (N+1) simulaba el N+1 en memoria con Map/HashMap/Dictionary en 3 de los 5 stacks, mientras vendia el caso como "los 5 stacks ejecutan el mismo problema". Esta entrega lleva los 5 stacks a SQL real, deja explicita la asimetria que queda en caso 01, y publica el roadmap de los proximos casos.
El problema descubierto#
Caso 02 estudia N+1 — un patron que es DB-shape por definicion. Modelarlo con Map<id, item> en memoria es didacticamente debil: el lector senior detecta que no hay prepare() ni executeQuery() ni round-trip, y el contraste pierde peso. La narrativa del caso ("N+1 sobre el mismo problema en 5 lenguajes") no se sostenia.
La solucion aplicada (cambios de codigo — los hace otro agente)#
- Caso 02 Node:
node:sqlite(modulo built-in desde Node 22.5, sinnpm install, sin bindings nativos). - Caso 02 Java:
sqlite-jdbc(single jar agregado al classpath en build-time, sin Maven). - Caso 02 .NET:
Microsoft.Data.Sqlite(paquete oficial Microsoft, ADO.NET-style). - DB embebida en
:memory:por instancia o/tmp/case02.db. Sin contenedor extra, sin servicio externo. Single-binary se preserva. db_hitspasa de ser una metrica derivada a un contador real de ejecuciones contra el motor. El contrato JSON externo no cambia.
La asimetria aceptada y documentada (caso 01)#
Caso 01 (latencia bajo carga) se mantiene como esta — PHP con PostgreSQL real, Python con SQLite stdlib, Node/Java/.NET con substrato simulado (setTimeout/sleepMicros/Task.Delay). La diferencia con caso 02 es que en caso 01 el patron de solucion (worker concurrente refrescando cache + readers no bloqueados) es lo enseñable, y ese patron es real en los 5 stacks gracias a ConcurrentHashMap/ConcurrentDictionary/Map con primitivas concurrentes reales. Lo que es simulado es el substrato del fallo, no la solucion.
Esta asimetria ahora esta documentada explicitamente:
- Seccion "Fidelidad del substrato" agregada al inicio de
cases/01-api-latency-under-load/comparison.mdcon tablareal vs simuladopor stack. - Seccion
## Fidelidadagregada al final de los 3 README de stack (node, java, dotnet) de caso 01, con link al ROADMAP. - Tabla "Honestidad de fidelidad" agregada al
README.mdraiz contrastando caso 01 vs caso 02.
Added#
ROADMAP.mdreescrito completo con tres ejes:- Eje 1 — 8 casos nuevos de la vida real (13-20): cache stampede, connection pool exhaustion, message queue backpressure, idempotencia y efectos duplicados, migracion de esquema sin downtime, cold start y autoscale lag, search index drift, dead letter queue olvidada.
- Eje 2 — Mejoras de plataforma: fidelidad universal de caso 01 (mover Node/Java/.NET a SQLite siguiendo el patron de caso 02), observabilidad Prometheus en los 5 stacks, suite de tests cross-stack, CI completa con loadtest, proof cards live en el portal.
- Eje 3 — Honestidad tecnica: seccion "Fidelidad" obligatoria en cada
comparison.mdcon substrato no uniforme, tabla maestra "real vs simulado" en elREADME.mdraiz, postmortems del propio lab endocs/lab-postmortems.md.
Changed (docs)#
cases/02-n-plus-one-and-db-bottlenecks/comparison.md: rewrite completo. El header narrativo deja de ser "PHP+Python tienen DB, los demas simulan" y pasa a "los 5 stacks ejecutan N+1 real sobre SQL, primitivas idiomaticas distintas". Tabla de fidelidad del substrato. Secciones por stack reescritas con la primitiva real (node:sqlite/db.prepare(),sqlite-jdbc/PreparedStatement,Microsoft.Data.Sqlite/SqliteCommand). Tabla final "Diferencias de decision" actualizada con columnasMotor DB(cinco motores reales) yPrimitiva de query(cinco APIs idiomaticas).cases/02-n-plus-one-and-db-bottlenecks/{node,java,dotnet}/README.md: reescritos. Las menciones aMap/HashMap/Dictionaryse reemplazan por la primitiva real (Databasedenode:sqlite,PreparedStatementJDBC,SqliteConnection/SqliteCommand). Tabla de primitivas actualizada. Sin claim de "datos en memoria".cases/02-n-plus-one-and-db-bottlenecks/README.md: tabla de stacks actualizada — todos los stacks dicen "SQLite real" o "PostgreSQL real" (no "datos en memoria"). Subsecciones Node/Java/.NET reescritas para mencionar la primitiva y el batch real.cases/01-api-latency-under-load/comparison.md: nueva seccion "Fidelidad del substrato" al inicio (despues del intro). Tablareal vs simuladopor stack. Explicacion de por que se acepta la asimetria hoy (enseñar la forma idiomatica del patron sin obligar a cada stack a montar PostgreSQL) y link al ROADMAP como compromiso futuro.cases/01-api-latency-under-load/{node,java,dotnet}/README.md: seccion## Fidelidadagregada al final. 3-5 lineas reconociendo que el substrato es simulado mientras el patron es real, con link al stack PHP para ver contencion real y al ROADMAP para el compromiso de mover a SQLite.README.mdraiz: bulletsOPERATIVO en Node.js/Java 21/.NET 8mencionan SQLite real en caso 02 con la primitiva especifica. Nueva seccion "🎯 Honestidad de fidelidad" con tabla contrastando caso 01 vs caso 02 por stack.ARCHITECTURE.md: fila de caso 02 en la tabla "Casos operativos actuales" actualizada — "PostgreSQL (PHP) + SQLite real en los otros 4" en lugar de solo "PostgreSQL".shared/catalog/cases.json:level_detailde caso 02 reescrito ("Los 5 stacks ejecutan N+1 real sobre SQL embebido...").
Out of scope (mantenidos sin cambios)#
- Codigo en
cases/02/{node,java,dotnet}/app/— lo reescribe otro agente con la migracion real anode:sqlite/sqlite-jdbc/Microsoft.Data.Sqlite. - Dispatchers (
node-dispatcher/,java-dispatcher/,dotnet-dispatcher/) — sin cambios. - Casos 03-12 — sin cambios en ningun sentido.
- PHP y Python caso 02 — ya correctos.
Why#
La narrativa del lab es honestidad tecnica. Vender que los 5 stacks ejecutan N+1 sobre SQL real cuando 3 simulan en memoria erosiona ese principio. Esta entrega cierra esa brecha en caso 02 y formaliza la deuda restante (caso 01) con plazo y compromiso explicito en el ROADMAP. El lab gana credibilidad senior — pierde una afirmacion debil, gana una afirmacion verificable.
2026-05-20 - .NET 8 cierra paridad multi-stack: los 12 casos operativos en los 5 stacks#
.NET 8 pasa de cubrir los primeros 6 casos a cubrir los 12. Paridad multi-stack completa entre PHP, Python, Node.js, Java 21 y .NET 8 — los 60 endpoints (12 casos × 5 stacks) operativos detras de 5 hubs simetricos.
Added (6 Program.cs reales con primitiva BCL distintiva por caso)#
- Caso 07 (
Modernizacion incremental):ConcurrentDictionary<string, Func<Request, Response>>como routing table mutable en runtime;Func<Request,Response>delegate como ACL closure;record Request/Response. Espejo delConcurrentHashMap<String,Function>Java. - Caso 08 (
Extraccion critica):Func<PriceRequestOld, PriceRequestNew>como proxy de compatibilidad de contrato +ImmutableList<Action<string>>conImmutableInterlocked.Updatecomo event bus thread-safe (reads sin lock, writes generan nueva lista persistente). Espejo delFunctionproxy +CopyOnWriteArrayListJava. - Caso 09 (
Integracion externa inestable):SemaphoreSlim.Wait(0)como budget de cuota no bloqueante +ConcurrentDictionarycomo snapshot cache +Interlocked.CompareExchangesobre el estado del breaker. - Caso 10 (
Arquitectura cara para algo simple): CPU real medido como N hops deJsonSerializer.Serialize/Deserialize(alocacion + parsing, presion al LOH cuando los blobs superan 85 KB) vsDictionary.TryGetValueO(1).Stopwatchpara medicion directa. - Caso 11 (
Reportes que bloquean operacion):ConcurrentExclusiveSchedulerPair.ExclusiveScheduleroThreaddedicado como aislamiento del trabajo CPU-bound;Task.Factory.StartNew(task, ..., scheduler)para submission explicita;ThreadPool.GetAvailableWorkerThreadscomo senal nativa de saturacion (equivalente almonitorEventLoopDelayNode y alThreadPoolExecutor.getActiveCount()Java). - Caso 12 (
Punto unico de conocimiento): operadores?.(null-conditional) +??(null-coalescing) con Nullable Reference Types habilitado (<Nullable>enable</Nullable>) como runbook codificado en el sistema de tipos — el compilador advierte sobre desreferencias inseguras. Espejo delOptional<T>Java y del optional chaining?.Node. - 12 README.md .NET per caso reescritos en formato Senior espejado al de Java: primitivas BCL, contraste legacy vs solucion con snippets C#, tabla de rutas, comando hub (
http://localhost:8500/0X/...), modo aislado y notas idiomaticas comparativas con los otros 4 stacks. - 12 secciones
.NET 8agregadas a cadacomparison.mdcon runtime, snippets legacy/optimizado en C#, primitivas distintivas (AsyncLocal<T>vsThreadLocal<T>,ConcurrentDictionaryvsConcurrentHashMap,Interlocked.CompareExchangevsAtomicReference.compareAndSet,SemaphoreSlimvsSemaphore,?.+??vsOptional<T>).
Changed#
dotnet-dispatcher/: lista de cases ampliada a 12. Puertos internos:9501-:9512. (Maneja el otro agente.)compose.dotnet.yml: comentario y healthcheck reflejan 12 casos. (Maneja el otro agente.)- 12
compose.ymlper-case .NET generados con healthcheck. Puertos host:851,852,853,854,855,856(01-06 ya estaban),857,858,859,8510,8511,8512. (Maneja el otro agente.) shared/catalog/cases.json: los 12 casos ahora listandotnetenoperational_stacksconruntime_entries.dotnetcompleto (port,compose_path,readme_path,health_path,root_path,isolated_compose,isolated_port).level_detailde cada caso suma mencion a la primitiva .NET distintiva. Entradalanguages.dotnetactualizada a estado operativo.docs/case-catalog.mdregenerado manualmente con los 5 stacks por caso.README.md: tabla de stacks compose.NET 8 OPERATIVO(eraOPERATIVO (01-06)); 60 endpoints (era 48); 5 puertos hub (era 4); 8 puertos cubren el lab entero (era 7); tabla de catalogo con columna🟦 .NETpor caso y links acases/0X-.../dotnet/README.md; bulletOPERATIVO en .NET 8describe los 12 casos con primitivas; quita "DOCUMENTADO / SCAFFOLD: casos 07-12 de .NET"; "Lo que este repo no vende" reformulado a paridad multi-stack universal a nivel funcional.ARCHITECTURE.md: tabla de casos operativos con.NET ✅en los 12;pdsl-dotnet-labcon 12 subprocesos:9501-:9512; capa 3 lista los 5 composes operativos.ROADMAP.md: Fotografia actual y Fase 2 reflejan paridad completa .NET (12 casos) con primitivas BCL distintivas por caso.RECRUITER.md: "12 casos × 5 stacks operativos"; comparison.md cubre los 5 stacks en los 12; 60 endpoints (era 48).INSTALL.md: tabla de hubs.NET 8 OPERATIVO; agrega seccion## 🟦 Laboratorio .NET completocon comandos hub; sin nota de "scaffold .NET".- 12
cases/0X/README.md: badgeStackssuma.NET; fila🔵 .NET 8de la tabla "Stacks disponibles" cambia de "🔧 Estructura lista" aOPERATIVOcon la primitiva BCL distintiva; seccion "### .NET 8 (implementacion operativa)" reemplaza la antigua "### .NET (espacio de crecimiento)" con descripcion concreta de primitivas, link al README .NET del caso, puerto aislado y URL del hub:8500.
Out of scope (mantenidos sin cambios)#
- Implementaciones PHP, Python, Node, Java: sin tocar.
- CI workflows: sin actualizar en este pase (los maneja el agente que escribe codigo .NET).
Java 21 pasa de cubrir los primeros 6 casos a cubrir los 12. Paridad multi-stack completa entre PHP, Python, Node.js y Java — los 48 endpoints (12 casos × 4 stacks) operativos detras de 4 hubs simetricos.
Added (6 Main.java reales con primitiva distintiva por caso)#
- Caso 07 (
Modernizacion incremental):ConcurrentHashMap<String, Function<Request, Response>>como routing table mutable en runtime;Functioncomo ACL closure. Espejo delMap<consumer, handler>Node. - Caso 08 (
Extraccion critica):Function<PriceRequestOld, PriceRequestNew>como proxy de compatibilidad de contrato +CopyOnWriteArrayList<Consumer<String>>como event bus thread-safe (reads paralelos sin lock, writes copian array). Espejo deProxy+EventEmitterNode. - Caso 09 (
Integracion externa inestable):Semaphorecomo budget de cuota (tryAcquireno bloqueante) +ConcurrentHashMapcomo snapshot cache +AtomicReference<String>como breaker state. - Caso 10 (
Arquitectura cara para algo simple): CPU real medido como N hops deStringBuilder(alocacion + traversal por hop) vsHashMap.getO(1).System.nanoTime()para medicion directa. - Caso 11 (
Reportes que bloquean operacion):ThreadPoolExecutoracotado a 4 threads como pool principal (saturacion realista);ExecutorServicededicado para reporting;CompletableFuture.supplyAsync(task, executor)para submission explicita.mainPool.getActiveCount()ygetQueue().size()como senal nativa de saturacion (equivalente almonitorEventLoopDelayNode). - Caso 12 (
Punto unico de conocimiento):Optional<T>+map/flatMap/orElsecomo runbook codificado en el sistema de tipos;AtomicIntegerpara coverage y bus_factor. Espejo del optional chaining?.Node — el tipo obliga a manejar el caso vacio. - 6 README.md Java per caso con primitivas, contraste de codigo, rutas, modo hub y aislado.
- 6 secciones Java en
comparison.md(cases 07-12) con runtime, snippets legacy/optimizado, primitiva distintiva.
Changed#
java-dispatcher/app/Dispatcher.java: lista de cases ampliada de 6 a 12 entradas. Puertos internos:9401-:9412.java-dispatcher/Dockerfile: COPY de los 12 Main.java + 12 invocacionesjavacseparadas (cada Main.class en su/cases/0X/).compose.java.yml: comentario y healthcheck reflejan 12 casos.- 6
compose.ymlper-case generados para cases 07-12 con healthcheck. Puertos host:847,848,849,8410,8411,8412(sin colisiones con 01/02/03 que usan 841/842/843). shared/catalog/cases.json: cases 07-12 ahora listanjavaenoperational_stacksconruntime_entries.javacompleto.docs/case-catalog.mdregenerado.README.md: tabla composeOPERATIVO(eraPARCIAL); 48 endpoints (era 42); tabla de catalogo con celdas Java pobladas en 07-12 con primitiva especifica en la columna "Que deja como prueba"; sin "(3 stacks)" residual.ARCHITECTURE.md: tabla de casos operativos con Java ✅ en los 12; pdsl-java-lab con 12 subprocesos:9401-:9412.docs/architecture.md: status table Java ✅ en los 12.docs/docker-strategy.md: tabla principal y reglas reflejan 12 casos Java.docs/executive-summary.md: intro + cases 07-12 listanJava 21en stacks operativos.docs/usage-and-scope.md: fila "01 al 12 operativos en Java 21"; paridad ajustada.AWS_MIGRATION.md: inventario y costos reflejan 12 casos Java (java-lab USD 7); 48 endpoints (12 × 4); ALB suma/java/01..12/*; DoD/java/01..12/health.RECRUITER.md: "12 casos × 4 stacks operativos"; comparison.md cubre los 4 stacks en los 12.RUNBOOK.md: 48 endpoints / 4 hubs; tabla casos aislados suma 6 filas Java (07-12); seccion diagnostico Java actualizada a 12 casos.INSTALL.md: Java OPERATIVO; URLs01..12/health; sin nota de "07-12 pendientes".ROADMAP.md: Fotografia actual y Fase 2 reflejan paridad completa Java (12 casos).- CI (
.github/workflows/ci.yml):compose-configmatrix suma 6 java composes 07-12;hub-probejava-hub cases"01..12"(era"01..06").
Smoke test#
Boot real docker compose -f compose.java.yml up -d con los 12 casos:
- Build OK (12
javacseparados por colision de claseMain) - Hub healthy en ~3s
- Los 12
/0X/healthresponden 200 con payload coherente (case + stack) - Shutdown limpio via SIGTERM
2026-05-15 - Barrido documental post-Java + verificacion funcional de los 6 casos#
Tras agregar Java 21 como 4to stack operativo, varias docs y READMEs seguian afirmando "3 stacks" / "3 hubs" / "Java planificado", y los 6 comparison.md por caso eran "PHP · Python · Node.js" sin seccion Java. Esta entrega es un barrido honesto que sincroniza narrativa con estado real + verificacion funcional de los 6 casos Java contra el patron Node.
Changed#
README.md: "Tres hubs" → "Cuatro hubs operativos"; "36 endpoints / 3 puertos" → "42 endpoints / 4 puertos"; filacompose.java.ymlPARCIAL (casos 01-06); nota AWS_MIGRATION ahora dice "hubs PHP/Python/Node/Java".ARCHITECTURE.md: tabla de casos operativos con columna Java (✅ en 01-06, — en 07-12); seccion "Modelo de containerizacion" pasa a "4 stacks"; agregapdsl-java-labconProcessBuilderen:9401-:9406; lista de composes raiz incluyecompose.java.yml.docs/architecture.md: lista de composes raiz suma Java; corrige tabla de estado operativo — antes mostrabanode=scaffolden cases 06-12 (Node ya era operativo en los 12); ahora Java ✅ en 01-06 y Node ✅ en los 12; tabla "Modelo de ejecucion" incluyecompose.nodejs.ymlycompose.java.yml.docs/usage-and-scope.md: fila nueva "Casos 01-06 operativos en Java 21"; nota de paridad ajustada a "Java 01-06; .NET scaffold".INSTALL.md: tabla muestra JavaPARCIAL (01-06)en8400(antes851-859 PLANIFICADO); nueva seccion "Laboratorio Java" con comandoup; alcance honesto al final menciona Java 01-06 y deuda 07-12.- 6 README.md de caso (
cases/01..06/README.md): fila "☕ Java | 🔧 Estructura lista" → "☕ Java 21 | OPERATIVO (\<primitiva\>)" con la primitiva especifica del caso. Caso 01 ademas tiene seccion narrativa Java conConcurrentHashMap/LongAdder/ScheduledExecutorService. - 6 comparison.md (
cases/01..06/comparison.md): titulo suma· Java; seccion Java agregada con runtime, snippet legacy, snippet correccion, primitiva distintiva (~40 lineas por caso). Tablas finales "Diferencias de decision" se dejan estables — el contenido nuevo cubre el contraste sin refactorizar el resumen.
Verified#
Smoke funcional de los 6 casos Java corriendo java Main directo (sin Docker):
- Caso 01:
/report-legacyretorna rows sinlifetime_orders;/report-optimizedretorna rows conlifetime_ordersylifetime_amount(la cacheConcurrentHashMapesta poblada por el worker — 1531 customer summaries por ciclo). - Caso 02:
/orders-legacycondb_hits=N+1;/orders-optimizedcondb_hits=2(1 orders + 1 batch IN). - Caso 03:
/checkout-legacyretornastatus:errorsin id;/checkout-observableretornacorrelation_idUUID que tambien aparece en/logscon campos estructurados. - Caso 04:
/quote-legacy?fail=onretornastatus:failed, attempts:5;/quote-resilient?fail=onretornastatus:fallbackconbreaker:closed; tras 3 fallos consecutivos pasa ashort_circuitedconbreaker:open. - Caso 05:
/batch-legacyincrementaretained_countmonoticamente;/batch-optimizedse mantiene encap=1000. - Caso 06:
/deploy-legacy?scenario=secret_driftdejaprodendegraded;/deploy-controlled?scenario=secret_driftdejaproden la version previa (rolled_back).
No son demos: cada uno computa, muta estado y devuelve evidencia distinta entre legacy y optimizada.
2026-05-15 - Java 21 entra como 4to stack operativo: casos 01-06 + hub consolidado#
Hasta hoy los stacks Java/.NET vivian como scaffolds genericos (un Main.java con /fast, /slow, /cpu sin solucionar el problema del caso). Esta entrega convierte los 6 primeros casos en implementaciones Java reales que resuelven cada problema con primitivas distintivas del lenguaje y los pone detras de un hub consolidado al estilo Python/Node.
Added#
compose.java.ymlen raiz, puerto8400. Mirror simetrico decompose.python.ymlycompose.nodejs.yml: un solo contenedor, un solo puerto, dispatcher interno que enruta/01..06/*a subprocesosjava Mainen puertos internos9401-9406. Healthcheck en/01/health.java-dispatcher/conDockerfileyapp/Dispatcher.java. Compila todos losMain.javade los 6 casos + el dispatcher en build-time (arranque rapido), spawna cada caso comoProcessconProcessBuilder, proxy viaHttpClient(JDK built-in). Shutdown hook propaga SIGTERM.cases/01..06/java/app/Main.javareescritos como implementaciones reales (no scaffolds). Cada uno con/health, dos rutas contraste (-legacyvs-optimized/-resilient/-observable/-controlled),/diagnostics/summary,/metrics,/reset-lab. Sin Maven — single-file por caso, compilado en build conjavac.- Primitivas Java distintivas por caso:
- 01 (API latency):
ConcurrentHashMappara summary cache lock-free entre worker y handlers;LongAdderpara p95/p99;ScheduledExecutorServicepara el workerreport-refresh-java. - 02 (N+1):
HashMap<Integer,List<Item>>precomputado como tabla relacional indexada; batchIN(...)simulado;recordtypes. - 03 (Observability):
ThreadLocal<RequestContext>para propagarcorrelation_id(equivalente aScopedValuesin preview flags); log estructurado JSON inline;/logsendpoint con ultimos 200. - 04 (Timeouts):
CompletableFuture.orTimeout(Duration)como deadline cooperativo;AtomicReference<BreakerState>con CAS para transiciones closed→open→half_open; fallback cacheado. - 05 (Memory):
LinkedHashMap.removeEldestEntrycomo LRU built-in del JDK;Runtime.getRuntime().totalMemory()/freeMemory()/maxMemory()para medir heap directo;System.gc()opcional en/reset-lab. - 06 (Pipeline):
record EnvStateyrecord Deploymentinmutables;ConcurrentHashMappor ambiente; state machine como guards en codigo (preflight → smoke → promote | rollback).
- 01 (API latency):
- 6
README.mdJava per caso (no stubs) con tabla de primitivas, snippet de contraste, rutas, ejemplos hub + aislado, y diferencias de runtime vs PHP/Python/Node. - Healthcheck en los 6
compose.ymlper-case (/healthcada 10s, 10 reintentos). Modo aislado (docker compose -f cases/0X/java/compose.yml up) sigue funcionando con puertos host841-846.
Changed#
.github/workflows/ci.yml:compose-configmatrix amplia a 46 archivos (sumacompose.java.yml+ los 6 java per-case).hub-probematrix incluyejava-hubcon la lista de cases parametrizada (01 02 03 04 05 06), reusando el mismo job pero respetando que Java es parcial.
shared/catalog/cases.json: cases 01-06 ahora listanjavaenoperational_stacksconruntime_entries.java(port 8400, compose.java.yml, isolated_compose, isolated_port).docs/case-catalog.mdregenerado desdecases.json.README.md: tabla de hubs marca Java como PARCIAL (casos 01-06), comandos de levantamiento incluyendocker compose -f compose.java.yml up, conteo de endpoints sube a 42 (12 PHP + 12 Python + 12 Node + 6 Java) detras de 4 puertos.ROADMAP.md: Fotografia actual y Fase 2 reflejan Java 21 como 4to stack operativo parcial; mencion explicita de las primitivas por caso. Anuncio de que casos 07-12 Java quedan pendientes.
Why#
El roadmap historicamente mencionaba "sumar Java o .NET para algun caso especifico". Tras cerrar PHP/Python/Node con paridad completa, Java entra como contraste fuerte: tipado estatico + GC + thread pool real + CompletableFuture + ConcurrentHashMap son primitivas que los otros stacks no expresan limpio. Hacerlo via hub (no 12 contenedores) preserva la simetria arquitectonica establecida con Python/Node — sigue habiendo "un compose por lenguaje" como afirma docs/docker-strategy.md.
Smoke test#
javacsobre los 6 Main.java + Dispatcher.java → OK local.- Boot local sin Docker (
java Maindirecto) del caso 01 java:/health,/report-legacy,/report-optimized,/batch/status,/metricstodos responden 200 con payload coherente. Workerreport-refresh-javarefrescando 1531 customer summaries en ~4ms. Contraste medible: legacy ~18ms (4 db_hits) vs optimized ~3ms (2 db_hits). docker compose -f compose.java.yml configOK.docker compose -f cases/0X/java/compose.yml configOK para los 6.bash scripts/validate-structure.sh→ OK (estructura + catalogo regenerado).
2026-05-15 - Resumen ejecutivo: los 12 casos en una pagina#
Faltaba una vista agregada para lectores no tecnicos (recruiters, lideres de producto, finanzas, CTO sin tiempo). Los README.md por caso y docs/case-catalog.md cubren bien el detalle tecnico, pero ninguno respondia "¿que problema de negocio resuelve cada uno y que evidencia deja en 5 minutos?" en una sola pasada. Esta entrega abre Fase 3.
Added#
docs/executive-summary.md: pagina unica con tabla resumen + seccion por caso (problema · valor · evidencia · honestidad · link al detalle). Contenido derivado deshared/catalog/cases.jsonpara mantener consistencia con la fuente de verdad. Incluye seccion final "Que NO encontraras" para honestidad de scope y rutas rapidas por audiencia.
Changed#
README.md: fila "Recruiter / hiring manager" en la tabla "Como evaluarlo rapido" ahora apuntaRECRUITER.md→docs/executive-summary.md. Nueva entrada en la tabla de documentos.ROADMAP.md: Fase 3 pasa de planificada a en progreso con la vista agregada cubierta.
Why#
RECRUITER.md es la puerta de entrada para evaluacion ejecutiva, pero queda en nivel "narrativa del producto". El catalogo tecnico vive en docs/case-catalog.md. Faltaba el puente: una pagina donde alguien escanea los 12 casos en orden y entiende valor de negocio + evidencia sin entrar a leer 12 README. Esa pieza ahora existe.
2026-05-15 - CI: smoke de los 3 hubs (cierra asimetria PHP-only)#
Hasta ahora CI solo probaba boot real del hub PHP (portal-probe). Los hubs Python (compose.python.yml) y Node (compose.nodejs.yml) quedaban fuera del smoke, asi como la mayoria de los compose.yml per-case de esos dos stacks. Esta entrega cierra esa asimetria sin disparar la matriz de CI.
Added#
- Nuevo job
hub-probeen.github/workflows/ci.ymlcon matriz de 2 entradas paralelas (python-huben:8200,node-huben:8300). Cada entrada hacedocker compose up -d --build, espera/01/healthy luego probea los 12 casos via/01..12/health. Un solo boot por hub valida la paridad de los 12 casos del stack.
Changed#
compose-configmatrix ampliada de 16 a 40 archivos: ahora incluyecompose.python.yml,compose.nodejs.ymly los 24compose.ymlper-case de Node y Python (antes solo caso 03 de cada uno). Sigue siendo un check barato — solodocker compose config.ROADMAP.md: Fase 4 marca CI minima como parcialmente cubierta (smoke de los 3 hubs + validacion estructural completa); pendiente smoke per-case node/python si llega a hacer falta.
Why#
portal-probe (PHP) demostraba boot real del laboratorio entero en cada PR. Sin equivalentes en Python/Node, un regression en el dispatcher Node o en compose.python.yml solo se detectaba al correrlos a mano. El job hub-probe por stack mantiene el costo CI bajo (2 boots paralelos cubren 24 casos) y replica la garantia que ya existia para PHP.
Smoke test#
python -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))"OK (workflow parsea).docker compose -f compose.python.yml configOK;docker compose -f compose.nodejs.yml configOK.- Los 24
compose.ymlnode+python validan viadocker compose configlocalmente.
2026-05-08 - PHP dispatcher operativo: paridad arquitectonica completa con Python/Node#
Cierra la asimetria que hasta ayer documentabamos como "deuda reconocida": PHP usaba ~20 contenedores (12 apps separadas + nginx hub + DB + observabilidad), mientras Python y Node usaban 1 contenedor con 12 subprocesos. Ahora los tres stacks comparten el mismo patron arquitectonico (1 dispatcher por lenguaje), preservando los servicios reales del caso 01 que NO son procesos PHP.
Added#
php-dispatcher/conDockerfile,app/entrypoint.shyapp/dispatcher.php. Espejo del patron depython-dispatcher/ynode-dispatcher/:entrypoint.shspawnea los 12 servidores PHP (php -S) como subprocesos en127.0.0.1:9001-:9012con env DB-aware (caso 01 conecta acase01-db, caso 02 acase02-db, casos 03-12 sin DB).dispatcher.phpactua como router script dephp -S 0.0.0.0:8100que enruta/01..12/*proxy-eando confile_get_contents+stream_context. Forward de query strings, headers, body POST/PUT/DELETE.tinicomo PID 1 para signal forwarding limpio (SIGTERM/SIGINT propagado al shell, que mata los 12 hijos antes de salir).
php-dispatcher/Dockerfileconpdo_pgsqlinstalado (necesario para casos 01 y 02 que conectan a PostgreSQL).
Changed#
compose.root.ymlreescrito: pasa de 14 services PHP (php-hub+ 12caseXX-app) a 1 servicephp-labcon dispatcher. Servicios reales del caso 01 (PostgreSQL, worker, Prometheus, Grafana, exporter) NO se tocan — siguen siendo contenedores aparte porque son servicios independientes del lenguaje. Conteo total: ~20 contenedores → ~7 contenedores. RAM total: ~2.5 GB → ~1 GB.cases/01-api-latency-under-load/shared/observability/prometheus.yml: target del scrape pasa deapp:8080aphp-lab:8100conmetrics_path: /01/metrics-prometheus(Prometheus llega al caso 01 via el dispatcher, en vez del contenedorcase01-appque ya no existe).docker/nginx/php-hub.confeliminado — el dispatcher PHP hace el routing ahora, ya no necesita nginx.
Documentation sweep#
docs/docker-strategy.md: la seccion "Tres modelos" pasa a llamarse "Modelo de containerización (simétrico para los 3 stacks)". Tabla unica con los 3 stacks siguiendo el mismo patron. Antes/despues del refactor PHP. Trade-offs heredados (hub vs per-case modo aislado).README.mdraiz: nota debajo de la tabla de hubs aclara que los 3 hubs son simetricos; PHP tiene contenedores extras solo por los servicios reales del caso 01.ARCHITECTURE.md: subseccion de containerizacion actualizada con la nueva simetria y el refactor.AWS_MIGRATION.md: inventario reemplazaphp-hub+case01-app..case12-apppor una sola filaphp-lab. Topologia ALB con/php/*→tg-php-lab(en vez de 12 target groups). Costos recalculados: total 24x7 baja de USD ~165 a USD ~130-140/mes (PHP pasa de USD 42 en 12 services a USD 7 en 1 dispatcher). Apagado fuera de horario: USD ~70-90/mes.docs/architecture.md: descripcion decompose.root.ymlactualizada al nuevo modelo.
Smoke test#
php -l dispatcher.phpOK;sh -n entrypoint.shOK.- Spawn local sin Docker de los 10 casos PHP sin DB (03-12) detras del dispatcher: 10/10 responden 200 a
/XX/health. Casos 06 y 12 retornan payloads completos end-to-end con query strings (/06/deploy-controlled?...y/12/incident-distributed?...). - Casos 01 y 02 PHP no se testean localmente (requieren PostgreSQL + worker), pero el codigo de los casos no se toco — solo cambia el contenedor donde corren.
2026-05-07 - Asimetria de containerizacion por stack documentada explicitamente#
Documentation#
docs/docker-strategy.md: nueva seccion 🧱 Tres modelos de containerización (uno por stack) — y por qué son distintos. Aclara que los tres hubscompose.root.yml/compose.python.yml/compose.nodejs.ymlparecen simetricos pero adentro son arquitecturas distintas:- PHP: ~20 contenedores Docker reales (12 apps separadas + DB + observabilidad). Microservicios con aislamiento OS-level.
- Python: 1 contenedor con 12 subprocesos
subprocess.Popeninternos. - Node.js: 1 contenedor con 12 subprocesos
child_process.spawninternos.
- Tabla de trade-offs explicitos: RAM total (~2.5 GB vs ~512 MB), tiempo de boot (15-20s vs 3-5s), aislamiento (OS-level vs cooperativo), failure domain por memory leak, costo en AWS Fargate (12 services vs 1).
- Tabla "cuando elegir cada modelo" en tu propio proyecto.
- Justificacion explicita de por que NO se uniformaron los tres stacks (PHP no se puede colapsar a 1 contenedor por el caso 01; Python y Node si pueden por no tener estado externo; mantener los 3 modelos lado a lado muestra patrones reales que se ven en produccion).
Changed#
README.md: nota visible debajo de la tabla de los 3 hubs apuntando a la nueva seccion. Aclara que "1 puerto por lenguaje" no implica "1 contenedor por lenguaje".ARCHITECTURE.md: subseccion nueva "Modelos de containerizacion por stack" debajo de la tabla de casos operativos, con link al detalle en docker-strategy.
2026-05-07 - AWS_MIGRATION.md actualizado: paridad Node + hubs + mapping de seguridad#
Changed#
AWS_MIGRATION.mdrefleja la realidad del repo post-Node:- Inventario incluye
node-lab(dispatcher Node, 12 casos internos en:9101 + :9002-:9012) junto alpython-labyphp-hub. - Topologia objetivo ECS Fargate documenta los 3 hubs por lenguaje detras de un ALB con path routing por lenguaje (
/php/*,/py/*,/node/*) — espejo del modelo local de los 3 composes (compose.root.yml,compose.python.yml,compose.nodejs.yml). - Tabla de costos Opcion A actualizada: 3 hubs Fargate (php-hub via 12 services, python-hub y node-hub como tasks unicas con dispatchers internos). Total 24x7 sube de USD ~145 a USD ~165/mes (incluye node-hub + WAF), con apagado fuera de horario en USD ~85–110.
- Opcion B Lambda escala a 36 funciones (12 PHP + 12 Python + 12 Node) compartiendo Aurora Serverless v2 + CloudFront + WAF.
- Inventario incluye
Added#
- Nueva seccion 🛡️ Como AWS resuelve los hallazgos abiertos del SECURITY.md que mapea cada hallazgo (A1-A2 altos, M1-M4 medios) a la mitigacion AWS recomendada con costo aproximado:
- A1 (sin auth) → ALB OIDC + Cognito User Pool, o Lambda@Edge, o WAF X-API-Key
- A2 (DoS event loop caso 11) → WAF rate-based rule + ALB health checks + Auto Scaling
- M1 (verbo HTTP) → WAF custom rule por path/metodo
- M2 (Host reflejado) → CloudFront origin request policy + WAF managed rules
- M3 (sin rate limiting) → WAF rate-based rules + CloudFront cache + API Gateway throttling
- M4 (atomicidad de state) → DynamoDB con conditional writes / RDS / S3 ETag — el problema desaparece al moverse fuera de
/tmp
- Ejemplo concreto end-to-end de como
/node/11/report-legacy?rows=5000000queda blindado en AWS (Cognito → WAF rate limit → ALB health check → CloudWatch alarm → Auto Scaling), con costo total ~USD 6-10/mes. - Tabla de defensas adicionales que AWS aporta (CloudFront edge, AWS Shield Standard, GuardDuty, CloudTrail, IAM task roles, VPC privadas, Secrets Manager + KMS, AWS Config + Security Hub).
- Definition of Done extendida con checks por stack (PHP/Python/Node) y validacion explicita del mapping de seguridad.
Documentation#
README.mdraiz: bullet del Executive Summary y fila de la tabla de docs principales mencionan ahora el mappingSECURITY.md→ AWS dentro deAWS_MIGRATION.md.
2026-05-07 - Postura de seguridad documentada con honestidad#
Security#
SECURITY.mdreescrito con un analisis completo del lab: modelo de amenaza explicito (3 escenarios localhost/LAN/Internet), defensas activas verificadas por revision manual conarchivo:linea(SQL injection, allowlist de scenarios, regex de SKU/release, clamping numerico, paths fijos, sin shell exec, sin eval, AbortSignal cooperativo, etc.), y los hallazgos abiertos clasificados por severidad — A1 sin auth, A2 DoS del event loop en caso 11, M1 sin validacion de metodo HTTP, M2 reflejo del header Host en probe.php, M3 sin rate limiting, M4 sin atomicidad en escrituras de state.- Checklist mínimo para exponer mas alla de localhost (reverse proxy + TLS + auth + rate limit + bloquear
/reset-lab). - Nota explicita sobre la complicacion del bind localhost-only: requiere mover el portal a la misma red Docker que los hubs y resolver por DNS interno (no implementado todavía).
Changed#
README.mdraiz: nueva seccion 🔐 Postura de seguridad y modelo de despliegue con tabla de 3 escenarios + resumen de garantias activas + frontera honesta de lo que no se garantiza + link aSECURITY.md. Tambien fila nueva "Security engineer" en la tabla "Como evaluarlo rapido".
2026-05-06 - Node.js hub compose.nodejs.yml operativo: tres puertos cubren el lab#
Added#
compose.nodejs.ymlen la raiz expone el dispatcher Node en8300. Sirve los 12 casos via routing por path (/01/health.../12/health) sin exponer los 12 puertos per-case. Patron espejo decompose.python.yml.node-dispatcher/conDockerfileyapp/main.js: spawnea los 12 servers como subprocesos internos (no expuestos al host) y proxy-ea por prefijo de path. Maneja shutdown graceful con SIGTERM/SIGINT. Caso 01 corre en:9101(en vez de:9001) porque algunos hosts Windows reservan9001; los demas casos usan:9002-:9012.
Changed#
cases/03-poor-observability-and-useless-logs/node/app/server.jsahora honraprocess.env.PORT(antes hardcodeaba8080). Bug que impedia correr el caso 03 dentro del hub.README.mdraiz: la filacompose.nodejs.ymlpasa dePLANIFICADOaOPERATIVO. Nueva narrativa: 6 puertos cubren el laboratorio entero (3 hubs + portal + Prometheus + Grafana). Los per-case quedan documentados como modo estudio aislado para casos donde la medicion lo requiere (05memoria,11event loop).ROADMAP.md,ARCHITECTURE.md,RUNBOOK.md: reflejan paridad de los 3 hubs y aclaran cuando usar per-case (modo estudio).
Why#
La asimetria PHP/Python (1 puerto cada uno) vs Node (12 puertos) era ruido innecesario. El plan oficial siempre fue 1 puerto por lenguaje; la deuda solo era de implementacion. Cerrarla deja el lab con 6 puertos efectivos en lugar de 42 potenciales.
2026-05-06 - Node.js multi-stack completo: casos 06 al 12 operativos#
Added#
- Caso
06Node.js: pipeline legacy vs controlled conAbortController+AbortSignalpropagado por cada paso. Cancela cooperativamente si el cliente desconecta o si el deadline se vence — limpieza nativa, sin polling. Puerto826. - Caso
07Node.js: strangler comoMap<consumer, handler>mutable en runtime. Registrar el routing del nuevo modulo es una linea, sin reload del proceso. ACL como closure que filtra contrato. Puerto827. - Caso
08Node.js:Proxynativo interceptacomputeFinalPricey traducecost_usd->priceen vuelo.EventEmitter(cutoverBus) publica cada avance del cutover. Puerto828. - Caso
09Node.js:AbortSignal.timeout(ms)(Node 18+) marca deadline del llamado externo + circuit breaker en memoria con tres estados (closed/open/half_open) y reapertura automatica tras cooldown. Puerto829. - Caso
10Node.js: el costo de la sobrearquitectura se mide como CPU real — N rondas deJSON.stringify/parsesobre arrays grandes encomplexvs acceso O(1) enright_sized. Bajoseasonal_peak, complex devuelve 502 por timeout interno. Puerto8210. - Caso
11Node.js:perf_hooks.monitorEventLoopDelay()mide el lag real del event loop.report-legacyejecuta CPU sincronico que castiga el loop entero (visible enevent_loop_lag_ms_p99);report-isolatedcede control consetImmediate. Puerto8211. - Caso
12Node.js: optional chaining (a?.b?.c ?? default) como runbook codificado en el lenguaje — distributed evita el crash que sufre legacy con acceso ciego a estructuras anidadas.share-knowledgesubecoveragey bajamttr_minde forma medible. Puerto8212. - Healthchecks Docker en
compose.ymlde los 7 casos.
Changed#
README.mdraiz: catalogo con columna "Análisis Técnico (Node.js)" completa para los 12 casos; estado actual indica paridad multi-stack PHP + Python + Node.js completa.ROADMAP.md: Fotografia actual y avance Fase 2 reflejan paridad Node.js completa con detalle de la primitiva nativa por caso.cases/06..12/node/README.md: re-escritos con el problema, la primitiva Node y endpoints reales (eran scaffolds).
2026-05-05 - Node.js multi-stack: casos 01, 02, 04 y 05 operativos#
Added#
- Caso
01Node.js: implementacion con datos en memoria + workersetInterval+ metricaevent_loop_lag_msmedida consetImmediate. - Caso
02Node.js: N+1 anidado conawaitsecuencial vs batch enMap+Set, exponiendoevent_loop_lag_mscomo senal Node-especifica. - Caso
04Node.js:AbortController/AbortSignalcomo timeout primitivo cooperativo + circuit breaker con estado persistido + fallback cacheado. - Caso
05Node.js: medicion real conprocess.memoryUsage()separandoheapUsed,heapTotal,rssyexternal; fuga real cross-request en array de modulo, sanitizacion viaMapacotado y eviction.
Changed#
README.mdraiz: nueva columna "Análisis Técnico (Node.js)" en el catalogo de casos resolutivos.comparison.mdde casos01,02,03,04y05: titulo y tabla actualizados a multi-stack (PHP · Python · Node.js); seccion Node.js agregada con codigo, decisiones y diferencias de runtime.cases/01..05/README.md: estados de stack actualizados (Node.js comoOPERATIVO); README caso 01 incorpora seccion dedicada a Node.ARCHITECTURE.md,docs/architecture.md,RUNBOOK.md,ROADMAP.md,RECRUITER.md,docs/positioning-and-objective.md,docs/usage-and-scope.md,docs/BEGINNERS_GUIDE.md: refleja paridad multi-stack honesta.shared/catalog/cases.json:nodeagregado aoperational_stacksde casos01,02,04,05conruntime_entries(puertos821,822,824,825);docs/case-catalog.mdregenerado.
2026-04-03 - Catalogo compartido, CI minima y caso 03 multi-stack#
Added#
ARCHITECTURE.mdcomo vista ejecutiva de la arquitectura actual.shared/catalog/cases.jsoncomo fuente de verdad del catalogo.scripts/generate_case_catalog.phppara generardocs/case-catalog.md..github/workflows/ci.ymlcon validacion estructural, chequeo del catalogo generado y smoke boot de compose.
Changed#
portal/app/index.phpahora consume metadatos compartidos y presenta una landing mas profesional con iconos y estados.compose.root.ymlmonta el catalogo compartido para eliminar duplicacion manual del portal.scripts/validate-structure.sh,.gitignore,Makefile,shared/README.mdytemplates/problem-metadata.jsonendurecidos para crecimiento mas limpio.- Caso
03profundizado en Node.js y Python conlegacyvsobservable, logs estructurados, trazas, metricas y endpoints de diagnostico.
2026-04-02 - Profesionalizacion documental#
Added#
RECRUITER.mdcomo ruta ejecutiva para evaluacion rapida.INSTALL.md,RUNBOOK.md,SUPPORT.md,SECURITY.mdyCONTRIBUTING.mden la raiz.docs/BEGINNERS_GUIDE.mdpara primeros pasos.
Changed#
README.mdreestructurado con rutas por audiencia, taxonomia honesta y contexto de ecosistema.ROADMAP.md,docs/recruiter-guide.md,docs/usage-and-scope.md,docs/positioning-and-objective.md,docs/case-catalog.mdydocs/docker-strategy.mdalineados con el nuevo estandar editorial.
2026-04-02 - Casos 02 y 03 operativos en PHP#
Added#
- Caso
02implementado con PostgreSQL real y comparacion N+1 legacy vs lectura optimizada. - Caso
03implementado con comparacion entre logs pobres y telemetria util.
Changed#
- Estrategia Docker consolidada como via oficial para casos implementados.
- Limpieza de artefactos versionados y endurecimiento de validacion estructural.
- Caso
01ajustado para manejar metricas temporales fuera del arbol del repositorio.