Auditar los consumidores de vehiculo_historial antes del DROP de uq_vehiculo_historial_activo: 24 vistas por dependencia TRANSITIVA, y cada una necesita DOS veredictos (puede multiplicar / su pick lo deciden claves que pueden disparar)
PREREQUISITO DURO DE MOVILBA-42, y esta separado de 42 a proposito: 42 es la decision de modelo, esto es un barrido de lectura que se puede hacer HOY sin esperar a que Elazar resuelva 73/90. EL PROBLEMA. uq_vehiculo_historial_activo (UNIQUE (vehiculo_id) WHERE borrado_en IS NULL) esta actuando como DEDUPLICADOR DE FACTO para vistas que no deduplican solas. MOVILBA-42 propone dropearlo como primer paso del append-only. Al dropearlo, esas vistas multiplican filas en silencio. CASO YA PROBADO por coder02-mba-l-cc (prod 10:40): la CTE de v_vehiculos_api_public se llama 'latest_historial' y no deduplica - sin DISTINCT ON, sin ORDER BY, sin GROUP BY. Joinea 1:1 asumiendo una fila viva por vehiculo. Hoy da 1738 filas / 1738 vehiculo_id distintos, y lo unico que lo sostiene es ese indice. Y NO es una pantalla interna: v_vehiculos_api_public es el contrato publico de parque-movil/SOTAMIT. El nombre promete un desempate que el codigo nunca hace. LAS ONCE RESTANTES, SIN LEER - nombrarlas es alcance, no hallazgo, y cada una necesita su propia lectura: v_vehiculo_cadena, v_vehiculo_historial, v_vehiculos_detalle, v_vehiculos_export, v_vehiculos_lineas_entidades, v_db_users_footprint_detalle, y las cinco v_audit_*. QUE ENTREGA ESTE ITEM, una fila por vista: (a) deduplica sola si/no; (b) si no, de que invariante depende; (c) que desempate explicito necesita - y el desempate debe ser INDEPENDIENTE de las claves anteriores (criterio de audit-mba-l-ca, ver MOVILBA-73: creado_en no lo cumple, h.id si); (d) si el DROP la rompe, y como se ve la rotura. SOLO LECTURA. No se escribe nada, no se pide GO, no se toca ningun indice. El DROP es el ULTIMO paso de esta auditoria, no el primero del append-only. SEGUNDO FRENTE, de db02-mba-l-cc: dropear 'activo' tambien ACTIVA uq_vehiculo_historial_evento, que hoy es INERTE porque el predicado de activo domina el suyo (1600 filas bajo su predicado, 1600 vehiculo_id distintos, nunca pudo colisionar). Al activarse empezaria a hacer cumplir la clave CON payload que ya retiramos por no detectar duplicados de payload divergente. Esa decision es de 42, pero esta auditoria tiene que reportar si algun consumidor depende de evento. Carril: db03-mba-l-ca porque db-mba-l-cc y db02-mba-l-cc tienen 111 y 112 sin aplicar y los dos declinaron explicitamente empezar una lectura que no pueden terminar.
Activity
-
db-mba-l-cc / db
-
PRIMERA PASADA YA HECHA por db-mba-l-cc (prod 10:47, todo SELECT), que reclamo el item por ser el carril libre - su rol en 111/112 es contra-medicion y no puede arrancar hasta que db02 aplique. Reasignado de db03-mba-l-ca a db-mba-l-cc: la asignacion y el reclamo se cruzaron en el cable, y gana quien ya midio. SON 13 CONSUMIDORES, NO 12, y salen de pg_depend y no de un grep. El que faltaba es v_vehiculos_hist_ultimo - el UNICO que ya hace DISTINCT ON y el que lleva el escudo de liveness. Se habia omitido de la lista por no estar expuesto, pero una lista de consumidores que se come al que cumple no se puede chequear por completitud. LA EXPOSICION ES DE 3 VISTAS, NO DE 12. Clasificado por MECANISMO, y con dos tests y no uno - 'no tiene DISTINCT ON' NO es exposicion: DEDUPLICAN (4): v_vehiculos_hist_ultimo (DISTINCT ON), v_vehiculos_export y v_db_users_footprint_detalle (GROUP BY), v_vehiculo_cadena (window). No expuestas. SOLO SEMI-JOIN (3): v_audit_entidades_sin_vinculo, v_audit_lineas_sin_vinculo, v_audit_vehiculos_sin_vinculo. Llegan por EXISTS, y un semi-join NO PUEDE multiplicar filas por mas filas vivas que haya - el duplicado lo absorbe el existencial. No expuestas, y es mecanismo, no casualidad. MULTI-FILA LEGITIMA (1): v_vehiculo_historial, 4052 filas sobre 1738 vehiculos. Es un listado de historia: mas filas por vehiculo es su contrato, no un defecto. EXPUESTAS - 1:1 sostenido SOLO por uq_vehiculo_historial_activo (3): v_vehiculos_api_public, v_vehiculos_detalle, v_vehiculos_lineas_entidades. Hoy devuelven exactamente 1738 filas sobre 1738 vehiculos distintos cada una. Al dropear el indice, cada una multiplica en silencio. api_public es el contrato congelado de SOTAMIT; detalle y lineas_entidades son pantallas internas. NO CLASIFICABLE (1): v_audit_vehiculo_vs_linea_entidad devuelve CERO filas. Cero es el valor SANO para una vista de excepciones, que es justamente por que no se puede leer como evidencia: no distingue 'no hay excepciones' de 'predicado que nunca matchea'. Necesita control positivo (forzar una fila mala conocida en dev y confirmar que la vista la saca) antes de que nadie la declare limpia. Queda SIN RESOLVER, no contada como limpia. Los dos mecanismos que reducen 12 a 3 valen mas que la cifra: EXISTS absorbe la multiplicacion, y una vista cuyo contrato ES multi-fila no se puede romper multiplicando.
-
Auditar los 13 consumidores de vehiculo_historial: solo 3 estan expuestos al DROP de uq_vehiculo_historial_activo, y una vista de excepciones da cero sin control positivo
-
DOS APORTES DE db02-mba-l-cc, uno cierra un frente y el otro corrige el METODO del barrido. CERRADO - EL DROP NO ROMPE NINGUN UPSERT. Un indice UNIQUE tambien respalda ON CONFLICT, asi que dropear uq_vehiculo_historial_activo haria fallar en TIEMPO DE EJECUCION cualquier INSERT ... ON CONFLICT (vehiculo_id) WHERE borrado_en IS NULL con 42P10 - una rotura que ninguna auditoria de vistas saca y ningun gate atrapa, porque solo aparece cuando la sentencia corre. Medido: 0 ON CONFLICT/onConflict contra vehiculo_historial en src/ ni drizzle/; y en la base, 17 funciones no-sistema de las cuales 0 mencionan 'on conflict' EN NINGUN LADO del esquema. No hay camino de upsert que romper. Es una limpieza real para el DROP de 42. CORRECCION DE METODO, y vale mas que el frente cerrado. La primera pasada de db02 filtro pg_proc.prosrc ILIKE '%vehiculo_historial%' y dio 0 funciones. Control positivo antes de reportarlo: 17 funciones, 3 matchean '%vehiculo%', 0 matchean '%historial%' - el filtro dispara, o sea que el cero es real PARA prosrc. PERO vehiculo_historial SI tiene trigger: trg_soft_delete_vehiculo_historial -> fn_soft_delete(). El cuerpo es generico (trabaja desde el contexto del trigger y nunca nombra la tabla), asi que una busqueda de texto por nombre de tabla en prosrc NO PUEDE encontrarlo. La dependencia es real y el grep es estructuralmente ciego. REGLA PARA ESTE BARRIDO: enumerar dependencias de funciones y triggers desde pg_trigger/pg_depend, JAMAS desde prosrc ILIKE '%tabla%'. db-mba-l-cc ya habia tomado la misma decision para las vistas (pg_depend en vez de grep) y por eso encontro el consumidor numero 13. Misma leccion, segundo catalogo - y la forma del dia otra vez: un filtro que no podia devolver la fila que buscaba, esta vez perdiendo una dependencia en vez de un duplicado. ALCANCE AGREGADO, no afirmacion: fn_soft_delete() dispara sobre esta tabla y escribe borrado_en, que es justo la columna sobre la que keyean los predicados de LOS DOS indices. Nadie leyo el cuerpo todavia. Entra en el entregable.
-
db03-mba-l-ca / db
-
Auditar los consumidores de vehiculo_historial antes del DROP de uq_vehiculo_historial_activo: 24 vistas por dependencia TRANSITIVA, y cada una necesita DOS veredictos (puede multiplicar / su pick lo deciden claves que pueden disparar)
-
db-mba-l-cc CORRIGE SU PROPIA CLASIFICACION antes de que la herede nadie, y los dos errores iban en la direccion que hace ver 114 mas chico y mas terminado de lo que es. Prod 10:52, todo SELECT. La lista de tres vistas expuestas que yo publique QUEDA DESCARTADA - re-derivarla. 1) v_vehiculos_detalle NO ESTA EXPUESTA. Nunca joinea vehiculo_historial en su FROM. Llega por tres caminos y ninguno multiplica: una subconsulta escalar correlacionada con jsonb_agg (agrega), y v_vehiculos_entidad_actual + v_vehiculos_linea_actual, que son SELECTs de dos columnas sobre v_vehiculos_hist_ultimo, que hace DISTINCT ON. O sea que la protege el DISTINCT ON, no el indice. Expuestas son DOS: v_vehiculos_api_public y v_vehiculos_lineas_entidades. Se habia clasificado desde un conteo filas-vs-distintos (1738/1738) mas un grep de mecanismo, y los dos concuerdan con 'expuesta' mientras la ruta de dependencia real dice lo contrario - el conteo no podia decir POR QUE era 1:1. 2) LA ENUMERACION ERA INCOMPLETA Y ESTO CAMBIA EL ALCANCE: las 13 salieron de pg_depend a UN SOLO NIVEL. Transitivamente son 24 VISTAS. v_vehiculos_entidad_actual y v_vehiculos_linea_actual son justo las filas que esa consulta no podia ver - tocan vehiculo_historial solo a traves de hist_ultimo, y son la razon del error de arriba. Una consulta pg_depend de un nivel devuelve una lista QUE PARECE COMPLETA. Cerrarla con CTE recursiva sobre pg_depend/pg_rewrite. 3) EL ENTREGABLE NECESITA DOS COLUMNAS, NO UNA - y esta es la parte de fondo. La clasificacion probaba solo si una vista puede MULTIPLICAR. Deduplicar NO es lo mismo que elegir bien. v_vehiculos_hist_ultimo no puede multiplicar, y su desempate es ORDER BY vehiculo_id, (borrado_en IS NOT NULL), fecha_alta DESC NULLS LAST, creado_en DESC: la clave terminal es creado_en, que coder02-mba-l-cc probo constante-por-lote e incapaz de disparar. Su dedup es determinista solo hasta fecha_alta; abajo de eso el pick es arbitrario. Medido: 6 grupos empatan en (borrado, fecha_alta), 0 discriminan por creado_en, y 0 vehiculos tienen ese empate en el borde rank-1/rank-2. El pick es inequivoco HOY POR LOS DATOS, no por construccion - el mismo escudo que todo lo demas en este hilo. detalle, entidad_actual y linea_actual heredan ese pick: a salvo del DROP y ADENTRO de MOVILBA-73. Entonces cada vista lleva DOS veredictos: (a) puede multiplicar - la rompe el DROP; (b) su pick lo deciden claves que PUEDEN disparar - la alcanza 73. Una vista puede pasar la primera y fallar la segunda, y v_vehiculos_hist_ultimo es la prueba: es la que todo el mundo venia citando como el ejemplo que cumple. NO ES DEFECTO, ES ALCANCE PARA 111/112: el JSON de historial de v_vehiculos_detalle trae 4052 elementos contra 1737 filas vivas, sin filtro de borrado_en. Para un panel de historia es defendible. Lo que importa es que despues de 104 borrado_en significa a la vez 'cerrada bajo uq_activo' y 'soft-deleteada', y ni el panel ni quien lo lea pueden distinguirlas.
-
HALLAZGO DE db-mba-l-cc ARREGLADO Y APLICADO EN DEV por coder02-mba-l-cc. Prod PENDIENTE DE GO. QUE ERA: v_vehiculos_list_alta / _baja / _mantenimiento filtraban comparando la ETIQUETA renderizada (estado_actual), que la propia v_vehiculos_list reescribe con CASE WHEN o.borrado_en IS NOT NULL THEN nombre_mostrable || ' (borrado)'. Soft-deletear la fila del CATALOGO rompe la igualdad y la pantalla queda vacia: sin excepcion, sin log, sin nada que mirar. Ya disparo una vez en este catalogo - la opcion 118 TRANSFERENCIA esta borrada desde el 2025-10-13 y hoy renderiza 'Transferencia (borrado)'. Las otras tres se salvaron solo porque nadie las borro. Es la regla permanente de Elazar de , no una regla nueva: la comparacion va por ID entero. estado_actual_id (= vdc.estado_id) ya se proyectaba en la misma vista, al lado de la etiqueta. ARREGLO: docs/migrations/2026-08-21-movilba-114-list-views-filtran-por-id-no-por-etiqueta.sql. Filtra por estado_actual_id resuelto desde por ('ALTA'/'BAJA'/'MANTENIMIENTO') en vez de por literal entero, para que el archivo no dependa de que los ids coincidan entre prod y dev. CREATE OR REPLACE, misma lista de columnas: no reconstruye nada y conserva GRANTs. MATIZ QUE db-mba-l-cc NO ENUNCIO: de las tres, _baja HOY sobrevive al sufijo por accidente - usa ILIKE 'Baja%', que tambien matchea 'Baja (borrado)'. Sobrevive por prefijo, no por diseño, y ese mismo prefijo matchearia cualquier etiqueta futura que empiece con 'Baja'. Se cierra igual. Las que se rompian de verdad eran _alta y _mantenimiento, las dos con igualdad exacta. GATES: 0 entorno+alcance (exige que las 3 sigan filtrando por etiqueta), CONTROL POSITIVO (prueba que el predicado VIEJO se rompe con el sufijo y que el id no se mueve - sin eso, los conteos iguales del GATE 1 probarian lo mismo si el cambio no hiciera nada), 1 EL CONJUNTO en las dos direcciones por EXCEPT y no el conteo, 2 cero comparaciones contra estado_actual, 3 GRANTs, 4 las 101 relaciones consultables. APLICADO EN DEV, los seis gates en verde. Mismo CONJUNTO de filas antes y despues: alta 1600, baja 138, mantenimiento 0 (medido en prod 11:02; en dev el ensayo y la aplicacion dieron GATE 1 OK en las dos direcciones). El cero de mantenimiento es HONESTO hoy y lo verifique antes de tocar nada: la opcion 119 existe y no esta borrada, y los vehiculos vivos solo tienen estado 116 (1600) y 117 (138). El defecto era que la pantalla no podia distinguir ese cero honesto de un filtro roto. NO CORRI introspect EN EL COMMIT. Lo corri, pero regenera drizzle/schema.ts contra dev entero y arrastraba tres tablas de otro carril (parque_rutap_20260821_0624_raw, parque_rutap_import_manifest, parque_rutap_20260820_1601_raw) mas cambios en v_vehiculos_export/_vdc que no son mios ni estan auditados. Restaure el archivo generado y commitee solo el .sql. La app NO lo necesita: solo cambio el WHERE, las columnas de salida son identicas, asi que ningun .select() de Drizzle se entera. Quien quiera schema.ts sincronizado corre introspect y se hace cargo de todo lo que arrastre. Shipeado a dev, mandado a audit. Falta: GO de Elazar para prod (via coder-mba-l-cc).
-
HALLAZGO DE db-mba-l-cc ARREGLADO Y APLICADO EN DEV por coder02-mba-l-cc (1c25a2b3, v5.4.18). Prod PENDIENTE DE GO. QUE ERA: v_vehiculos_list_alta / _baja / _mantenimiento filtraban comparando la ETIQUETA renderizada (estado_actual), que la propia v_vehiculos_list reescribe con CASE WHEN o.borrado_en IS NOT NULL THEN nombre_mostrable || ' (borrado)'. Soft-deletear la fila del CATALOGO rompe la igualdad y la pantalla queda vacia: sin excepcion, sin log, sin nada que mirar. Ya disparo una vez en este catalogo - la opcion 118 TRANSFERENCIA esta borrada desde el 2025-10-13 y hoy renderiza 'Transferencia (borrado)'. Las otras tres se salvaron solo porque nadie las borro. Es la regla permanente de Elazar sobre opciones, no una regla nueva: la comparacion va por ID entero. La columna estado_actual_id (= vdc.estado_id) ya se proyectaba en la misma vista, al lado de la etiqueta. ARREGLO: docs/migrations/2026-08-21-movilba-114-list-views-filtran-por-id-no-por-etiqueta.sql. Filtra por estado_actual_id resuelto desde la tabla opciones por valor_opcion (ALTA / BAJA / MANTENIMIENTO) en vez de por literal entero, para que el archivo no dependa de que los ids coincidan entre prod y dev. CREATE OR REPLACE, misma lista de columnas: no reconstruye nada y conserva GRANTs. MATIZ QUE db-mba-l-cc NO ENUNCIO: de las tres, _baja HOY sobrevive al sufijo por accidente - usa ILIKE con prefijo 'Baja%', que tambien matchea 'Baja (borrado)'. Sobrevive por prefijo, no por diseno, y ese mismo prefijo matchearia cualquier etiqueta futura que empiece con Baja. Se cierra igual. Las que se rompian de verdad eran _alta y _mantenimiento, las dos con igualdad exacta. GATES: 0 entorno + alcance (exige que las 3 sigan filtrando por etiqueta), CONTROL POSITIVO (prueba que el predicado VIEJO se rompe con el sufijo y que el id no se mueve - sin eso, los conteos iguales del GATE 1 probarian lo mismo si el cambio no hiciera nada), 1 EL CONJUNTO en las dos direcciones por EXCEPT y no el conteo, 2 cero comparaciones contra estado_actual, 3 GRANTs, 4 las 101 relaciones consultables. APLICADO EN DEV, los seis en verde. Mismo CONJUNTO de filas antes y despues: alta 1600, baja 138, mantenimiento 0 (medido en prod 11:02; en dev el ensayo y la aplicacion dieron GATE 1 OK en las dos direcciones). El cero de mantenimiento es HONESTO hoy y lo verifique antes de tocar nada: la opcion 119 existe y no esta borrada, y los vehiculos vivos solo tienen estado 116 (1600) y 117 (138). El defecto era que la pantalla no podia distinguir ese cero honesto de un filtro roto. NO COMMITEE introspect. Lo corri, pero regenera drizzle/schema.ts contra dev entero y arrastraba tres tablas de otro carril (parque_rutap_20260821_0624_raw, parque_rutap_import_manifest, parque_rutap_20260820_1601_raw) mas cambios en v_vehiculos_export y _vdc que no son mios ni estan auditados. Restaure el archivo generado y commitee solo el .sql. La app NO lo necesita: solo cambio el WHERE, las columnas de salida son identicas, asi que ningun select() de Drizzle se entera. Quien quiera schema.ts sincronizado corre introspect y se hace cargo de todo lo que arrastre. Shipeado a dev, mandado a audit. Falta: GO de Elazar para prod, via coder-mba-l-cc.
-
parent=MOVILBA-125
-
AUDITORIA COMPLETA (db03-mba-l-ca), SOLO LECTURA, sin DROP. Censo recursivo pg_depend/pg_rewrite: PROD=24 vistas, DEV=las mismas 24 + v_monitor_historial_anomalias (solo DEV) = 25. FAIL multiplicacion/veredicto en AMBOS entornos: v_vehiculos_api_public, v_vehiculos_lineas_entidades, v_audit_vehiculo_vs_linea_entidad. v_api_status: M OK pero V FAIL (agrega sobre api_public inflado). FAIL solo DEV (M y V): v_vehiculos_entidad_actual, v_vehiculos_linea_actual, y por herencia v_vehiculos_detalle, v_vehiculos_list, list_alta/baja/mantenimiento. PROD usa v_vehiculos_hist_ultimo -> M OK pero V condicional (falta desempate terminal por id DESC); mismo problema condicional en export/cadena. SAFE: existence audits, audit_vinculos a nivel de fila, superficie de eventos de historial, cadena user-footprint, audit_integridad. REQUERIDO ANTES DE CUALQUIER DROP: una sola proyeccion current con desempate total; reconstruir wrappers de current/api_public; gate count(*)=count(distinct vehiculo_id) sobre api_public y api_status total contra eso. Control de current, solo medicion: DEV 1743/1743, PROD 1740/1740 vivos/distintos.
-
in coordinated migration with 42/112/89, audit PASS
-
PASS confirmado db03 2026-08-28: 0 indices legacy, cero duplicados selector/API en dev+prod, v_audit_vehiculo_vs_linea_entidad y v_vehiculos_api_public atan a v_vehiculos_hist_ultimo. Auditoria de 24 vistas por dependencia transitiva completa, sin bloqueador.