La ficha en blanco era la respuesta HONESTA: el que miente es el export, que muestra el alta de OTRO vehiculo en 121 de 126
Introducido por el cambio de MOVILBA-88 que shipeo a prod el 2026-08-21 07:58 (main c7b4f22d, v5.4.12). Medido por db-mba-l-cc contra prod 08:10:51 ART: de 1738 filas del export, 155 discrepan con la ficha, y esas 155 son DOS defectos distintos - 19 con fecha contra fecha (desacuerdo real de valor) y 136 con NULL contra valor, de los cuales 125 son la ficha en blanco mientras el export si muestra fecha. CAUSA: getFechaAltaMasReciente (src/actions/vehiculos.ts:813) filtra tramite_valor='ALTA' y devuelve null si no hay ninguna. Los 126 vehiculos afectados SI tienen historial - 0 de ellos carece de historial - pero su cadena entera es MODIF. Y eso NO es un hueco de datos: bajo la semantica del libro 'Alta' marca CAMBIO DE ASIGNACION, asi que un vehiculo que nunca cambio de asignacion legitimamente no tiene ninguna. O sea 7,2% de la flota exportada ve 'sin dato' en pantalla y una fecha en el Excel. OJO CON LA PREMISA VIEJA: el comentario en el codigo (:810) anticipa el caso null pero se lo atribuye a 'las 104 filas sin tramite_id derivable'. Eso es falso - tramite esta poblado en prod (ALTA 1770, MODIF 2272, solo 10 NULL) - y hay que corregir el comentario junto con el codigo o queda documentando una causa equivocada. DECIDIR el fallback: la primera MODIF (min fecha_expediente), la vigencia de vdc, o dejar en blanco a proposito. Recomendacion: la primera MODIF, porque bajo la semantica de Eugenia el vehiculo que nunca cambio de asignacion arranco su asignacion vigente en su primera fila. Pedida a db la medicion que lo confirma: si min(fecha_expediente) sobre las MODIF coincide con lo que muestra el export en los 126, se arregla con una linea y sin consultar a Eugenia; si se parte, es pregunta para ella.
Activity
-
Tomado. Es lo que sigue: es una regresion viva en prod, la introdujimos hace media hora y toca el 7,2% de la flota. Esperando de db la medicion que decide el fallback.
-
La ficha en blanco era la respuesta HONESTA: el que miente es el export, que muestra el alta de OTRO vehiculo en 121 de 126
-
SE DA VUELTA EL ITEM. Medido por db-mba-l-cc, prod 08:14:28 y dev 08:14:30 ART. Yo lo abri como regresion nuestra y como una correccion de una linea; las dos cosas son falsas. (1) MECANISMO, y es lo que cambia todo: v_vehiculos_export.fecha_alta NO lee vehiculo_historial. Lee rutap.v_rutap_latest_norm, joineando por CHASIS (chasis_nro_norm), filtrando por el texto del libro r.tramite='Alta' y tomando max(fecha_alta) acotado por la subconsulta de vigencia. O sea que las dos superficies no son dos lecturas de una fuente: la ficha lee NUESTRO historial por vehiculo_id y el export lee el snapshot de RUTAP por chasis. Hacerlas coincidir no es ajustar un fallback, es elegir cual fuente manda para una fecha en pantalla. (2) El fallback que yo proponia FRACASA: min(fecha_expediente) sobre las MODIF coincide con el export en 1 de 126 en prod (1 con exp_fa null, 1 con min_modif null, 125 que difieren directamente). En dev 2 de 127. (3) POR QUE fracasa, y este es el hallazgo: de los 126, 125 tienen una fila Alta en RUTAP bajo el MISMO CHASIS, y en 121 de ellos esa fila Alta pertenece a OTRO DOMINIO. El export les esta mostrando la fecha de alta de otro vehiculo que compartio su chasis. En dev, 123 de 127, misma forma. Es otra vez la trampa madre - una clave demasiado angosta para ver el campo que distingue - la misma que contamino el backfill de 94. CONCLUSION: el blanco de la ficha es la respuesta honesta y la fecha del export es la equivocada en 121 de 126. NO se toca la ficha. El defecto es del export y se lleva a su propio item. (4) Premisa confirmada limpia: de los 126, filas con ALTA y fecha_expediente NULL = 0. Es blanco-por-ausencia, no blanco-por-null, asi que el arreglo opuesto queda correctamente descartado. Y dev sirve para construir esto: misma forma que prod (127 vs 126, 0 vs 0, 123 vs 121) y las 100 filas danadas de borrado_en no tocan esta consulta porque nada aca lee borrado_en.
-
Sigue siendo mio y sigue siendo lo proximo, pero ya no es un arreglo de la ficha: es el export. Y el export lo consume GCABA, asi que el impacto es mayor que el que le puse cuando lo abri como cosmetico de pantalla.
-
OPCION D, converjida por los dos carriles db por caminos distintos y reproducida celda por celda. Prod 08:19-08:20 ART. REGLA: fecha_alta = COALESCE(max(fecha_expediente) sobre los tramites ALTA, vdc.fecha_vigencia_desde), SIEMPRE por vehiculo_id. El tramite ALTA gana donde exista; la vigencia del vehiculo se usa SOLO para los que no tienen ninguna fila ALTA. Ninguna de las dos mitades toca la clave por chasis, que es lo que causo todo esto. IMPACTO sobre 1738 filas: 1582 sin cambio, 144 corridas, 12 que ganan fecha, 0 en blanco, 126 usando el fallback. COMO SE DESCARTARON LAS OTRAS. A (leer solo nuestro historial) dejaba 125 lineas EN BLANCO en un artefacto que consume GCABA - correcto como modelo, indefendible como entregable. B (agregarle identidad al join por chasis) llegaba casi al mismo blanco pagando el costo del join. C (leer vdc.fecha_vigencia_desde) la propuso db-mba-l-cc y la RETIRO el mismo carril al medirla: cambiaba 1388 de 1738, con 1376 corrimientos silenciosos de hasta 391 dias, y encima discrepaba con la ficha en 1246 de 1612. La leccion que dejo dicha el propio carril: recomendo C por COBERTURA (1738/1738 poblado) sin medir CONCORDANCIA - presencia no es correccion, la misma distincion que ese mismo carril habia usado una hora antes para impedir que yo cerrara MOVILBA-13. POR QUE D es una regla y no un parche: la ultima Alta donde la haya, si no el inicio del periodo vigente. Para un vehiculo cuya cadena es toda MODIF, el inicio del periodo vigente es la unica respuesta honesta a 'desde cuando'. Y el fallback esta VALIDADO, no solo presente: vdc.fecha_vigencia_desde es igual al fecha_alta de la fila abierta del historial en 1600 de 1600 vehiculos vivos con fila abierta, cero excepciones - o sea que se valida contra el historial de prod, que se reconcilio al libro 0624 esta manana, y NO necesita esperar la carga del snapshot 5. CONDICION DURA, la ponen los dos carriles: D va a LAS DOS SUPERFICIES o no va. Export con D y ficha con max(ALTA) pelado se contradicen en los 126. Si la ficha adopta el mismo COALESCE, coinciden por construccion en las 1738 y los blancos que abrieron este hilo desaparecen tambien de la ficha. EJECUCION: es CREATE OR REPLACE, no DROP+CREATE - fecha_alta es date y las dos fuentes candidatas tambien, asi que nombre/tipo/orden no se mueven y los GRANT sobreviven. Unico grant: svc_nextjs SELECT. Cero vistas dependientes. La advertencia de DROP+CREATE que habia levantado db02 era del cambio de consumo (text->numeric) y quedo retirada; no bundlear las dos cosas o se convierte un cambio que preserva GRANTs en uno que los pierde. PARA LA NOTA QUE ACOMPANIE EL PROXIMO EXPORT: las 144 lineas corridas son correcciones - antes imprimian la fecha prestada de otro vehiculo - pero para quien lee el archivo se ven igual que un cambio inexplicado.
-
D QUEDA TOTALMENTE VALIDADA, sin ninguna celda sin cubrir. Prod 08:20:48 ART, medido por db-mba-l-cc despues de que db02 corrigiera su propio caveat. Los dos huecos que se levantaron y se cerraron: (1) los 126 vehiculos todo-MODIF tienen los 126 fila abierta de historial, asi que caen dentro de los 1600 donde el fallback esta probado 1600/1600; (2) los 138 vehiculos en BAJA sin fila abierta - poblacion real, distinta, sin solapamiento con los 126 - los sirve LOS 138 la rama ALTA de D, cero caen al fallback de vdc. O sea que D nunca usa el fallback fuera de los 1600 y no espera la carga del snapshot 5. Descartada tambien una variante intermedia: leer directo el fecha_alta de la fila abierta en las dos superficies es equivalente para los 1600 pero deja sin nada a los 138 en BAJA, que no tienen fila abierta. La rama ALTA de D los cubre. Por eso D es estrictamente mejor, y vdc.fecha_vigencia_desde queda fuera del camino que ve GCABA salvo como fallback de los vehiculos con periodo abierto y sin ALTA - que son exactamente los 126 y exactamente donde esta probado. REPARTO: db-mba-l-cc escribe el paquete CORV de la vista y ownea el loop; yo contra-mido y hago la mitad de la ficha. Nada se mueve sin el GO descrito de Elazar y la pre-lectura de audit.
-
DEPENDENCIA CON MOVILBA-113, hallada por db02-mba-l-cc: la opcion D toca consumo (text -> numeric), y un cambio de TIPO exige DROP+CREATE de v_vehiculos_export, no CREATE OR REPLACE. Eso reescribe la definicion entera de la vista, incluida la expresion ultima_modificacion que es el objeto de MOVILBA-113. Los dos tickets NO son independientes: el segundo que shipee revierte al primero salvo que un mismo owner los ordene explicitamente. Se suma a la condicion dura que ya tenia este item (export y ficha juntos o es regresion).
-
Mitad EXPORT de la opcion D escrita y ensayada hasta donde llega el rol de app: docs/migrations/2026-08-21-movilba-109-d-export-fecha-alta-por-vehiculo-PROD.sql, md5 cfd3c9aefbf8b4de4a7c199bfe29f929, SIN commit (viaja en un commit por path), PROD-only, sin aplicar, sin GO. Expresion: COALESCE(max(fecha_alta) de historial con tramite_id ALTA resuelto de opciones INTO STRICT + liveness, vdc.fecha_vigencia_desde). La variante sobre TODAS las filas (no solo vivas) es la que reproduce los numeros firmados 1582/156/0 - filtrar por liveness descartaria las ALTAs cerradas porque borrado_en en esta tabla tambien significa cerrado-bajo-uq_activo (350/1388/0, equivocada). CONSECUENCIA ESTRUCTURAL: D elimina el CTE de rutap entero, y por eliminacion caen las dos comparaciones crudas r.tramite = 'Alta' (el control de cuatro aserciones queda MOOT) y un ~~* 'Baja%' sobre nombre_mostrable que el barrido de 115 no conto (sobrevive por prefijo, por accidente). El export deja de leer rutap por completo. Gates: GATE 0 con estados (a)/(c) - el (c) aprendido de 115, aborta con 'nada que hacer'; GATE 1 gatea el INVARIANTE (cada fecha_alta publicada = COALESCE recomputado por subquery estructuralmente distinto, EXCEPT en dos direcciones, 0 NULLs) y no los conteos absolutos; GATE CONTROL: tramite_id=317 matchea 1770/4052 - tiene entrada Y discrimina. Ensayo prod: GATE 0 y CONTROL en verde, para en permission denied en el CORV (DDL prod = SUPERUSER_ELAZAR_URL, no sin GO); GATES 1-2 sin ejercitar en prod. Mismo AccessExclusiveLock al aplicar que 115: misma ventana tranquila. ultima_modificacion intacta (113 es fix de escritura). CONDICION VIGENTE: export Y ficha juntos en el mismo GO o es regresion; no hay restriccion de orden entre ESCRIBIR las mitades, solo entre APLICARLAS. Mitad ficha: coder-mba-l-cc, en curso.
-
Mitad FICHA lista y auditada. v5.4.25 implemento el contrato del export en getFechaAltaMasReciente (COALESCE(max fecha_alta de ALTA fechada, vdc.fecha_vigencia_desde de la fila viva) + filtro fecha NOT NULL: el orderBy desc anterior era NULLS FIRST y una ALTA sin fecha tapaba a las fechadas - bug pre-existente arreglado de paso). Hallazgo de audit en la primera revision, REAL: el filtro por tramite_valor='ALTA' era paridad por DATOS, no por construccion - ciego a liveness (matchea por el join propio de cada fila, asi que un soft-delete+recreate de ALTA dejaria a la ficha reteniendo filas del id viejo que el export descarta) y ciego a grupo (el FK tramite_id no tiene scope de grupo/campo, un id de vehiculo/estado renderizaria 'ALTA'). Arreglado en v5.4.31 (d135a39b): la ficha usa el MISMO subquery de resolucion del export (grupo vehiculo_historial / campo tramite / valor ALTA / borrado_en IS NULL). Re-revision de audit: CLEAR. Medicion de coder02 (prod, 11:24): los dos predicados seleccionan el MISMO conjunto hoy, 1770 filas, EXCEPT 0/0 en las dos direcciones - el swap no movio nada hoy y cierra las dos brechas de construccion. GO de Elazar '111 112 109d go' recibido: db02 aplica el export (cfd3c9ae) tras 111 y 112; la ficha va a prod via gitdeployprod.sh cuando el export reporte verde - las dos mitades cambian en la misma ventana.
-
OPCION D COMPLETA EN PROD, las dos mitades en la misma ventana, GO verbatim de Elazar ('111 112 109d go' + 'go a' para el alcance del deploy). MITAD EXPORT: aplicada en prod por db03-mba-l-ca 11:30 (heredada de db02-mba-l-cc, que Elazar retiro a mitad de la tanda). md5 cfd3c9ae verificado en disco, ensayo verde, apply verde. GATE 0: id ALTA vivo 317, estado de entrada esperado. CONTROL: 1770/4052 filas ALTA - el filtro tiene entrada Y discrimina. GATE 1: 1738 -> 1738 mismo conjunto, 156 fecha_alta cambiadas, 0 NULL, 0 diferencias contra la prediccion. GATE 2: sin dependencias de rutap ni de etiqueta, GRANTs y tipo date intactos. Post-commit como svc_nextjs: 1738 filas, 0 fecha_alta NULL. Contra-medido independiente por db-mba-l-cc 11:32: 0 filas discrepan contra un recomputo estructuralmente distinto del COALESCE, y el cero de menciones a rutap lleva control positivo (el mismo regex SI encuentra rutap.v_rutap_latest_norm donde existe). MITAD FICHA: shipeada a prod en el merge 18af383f. getFechaAltaMasReciente implementa el MISMO contrato: COALESCE(max fecha_alta sobre ALTA fechada con tramite_id resuelto de la fila VIVA del catalogo, vdc.fecha_vigencia_desde de la fila viva). Audit CLEAR en d135a39b/v5.4.31. DOS DEFECTOS ARREGLADOS DE PASO, ninguno era el pedido de D: 1. El orderBy desc de la ficha era NULLS FIRST en Postgres, asi que UNA fila ALTA sin fecha tapaba a todas las fechadas y la ficha mostraba 'Sin dato'. max() ignora NULLs; ahora el filtro exige fecha NOT NULL y coincide. 2. El filtro por tramite_valor='ALTA' era paridad por DATOS y no por construccion (hallazgo de audit): ciego a liveness - matchea por el join propio de cada fila, asi que un soft-delete+recreate de ALTA dejaria a la ficha reteniendo filas del id viejo - y ciego a grupo, porque vehiculo_historial_tramite_id_fkey referencia opciones(id) SIN scope de grupo/campo. Medido por coder02-mba-l-cc antes del cambio: los dos predicados daban el MISMO conjunto de 1770, EXCEPT 0 en las dos direcciones. El swap no movio nada hoy y cierra las dos brechas. VENTANA DE SPLIT: existio y se cerro. Entre el apply del export (11:30) y el merge a prod (11:31) el export servia la fecha nueva y la ficha la vieja - dos superficies, un rotulo, dos fechas, que es exactamente la regresion que la condicion de D existe para impedir. Se sostuvo el hold hasta que Elazar contesto en vez de shipear la rama bajo un GO que no la describia: el deploy arrastraba ademas MOVILBA-100 y MOVILBA-53, que se le nombraron antes del GO y quedaron cubiertos por 'a'. PROD runtime-verified por gates y por svc_nextjs, NO UI-walked.