Que le pasa a la app cuando se borra logicamente CUALQUIER opcion del catalogo - barrido de todas las superficies, no solo SOTAMIT
LA PREGUNTA, planteada por Elazar el 2026-08-21: si se borra logicamente UNA OPCION CUALQUIERA del catalogo - un tipo de motor, una unidad propulsora, un tipo de licencia, lo que sea - QUE PASA EN LA APLICACION? No es sobre ALTA ni sobre BAJA, y no es sobre el contrato de SOTAMIT. Es sobre TODAS las opciones y TODA la app. POR QUE ESTE ES EL ITEM Y MOVILBA-120 NO LO ERA. 120 preguntaba por una opcion puntual (116 ALTA) contra un consumidor puntual (SOTAMIT) y sobre una hipotesis que nadie estaba por ejecutar. Se cancelo. Lo que quedaba abajo, y es lo que importa, es que el soft-delete de una opcion es una operacion que la UI de admin YA OFRECE (`src/app/admin/opciones/`), o sea que cualquier operador puede dispararla hoy, y nadie midio nunca que le pasa a la app cuando eso ocurre. EL MECANISMO CONOCIDO, que es de donde sale la sospecha. El CASE de soft-delete del catalogo le agrega el sufijo ' (borrado)' a `nombre_mostrable` y NUNCA a `valor_opcion` - esto se establecio midiendo en MOVILBA-115. De ahi salen dos familias de consecuencia, y NINGUNA de las dos esta medida para el caso general: - Todo lo que lee `nombre_mostrable` empieza a mostrar/exportar/comparar el string CON sufijo. - Todo lo que lee `valor_opcion` no cambia. Esa asimetria es justo la que hace que el sintoma sea parcial y dificil de ver. SUPERFICIE A BARRER, contada hoy en el repo (grep, no estimacion): - 90 usos de `nombre_mostrable`/`nombreMostrable` en 28 archivos de `src/`. - 158 usos de `v_ui_*`/`v_opciones` en 16 archivos de `src/`. Se solapan pero no son el mismo conjunto. Incluyen actions, repositories, rutas de API (`/api/dropdowns`, `/api/vehiculos`, `detail-dropdowns`), paginas de vehiculos/personas/entidades, componentes de formulario, y `src/lib/openapi/components.ts` - o sea que la superficie llega hasta el contrato publicado de la API, no se queda en el render. LO QUE HAY QUE CONTESTAR, una respuesta por pregunta y cada una con su medicion: 1. DROPDOWNS: una opcion borrada, sigue apareciendo en el desplegable? Si aparece, aparece con sufijo y se puede volver a elegir? Si NO aparece, que pasa con las filas existentes que ya la referencian - el formulario de edicion se abre con el campo vacio y una escritura inocente la pisa? 2. FICHA/DETALLE: el valor guardado se sigue renderizando, y con que texto? 3. EXPORTS Y API: que string sale por `/api/*` y por el OpenAPI. Un consumidor externo con un enum congelado recibe un valor que su contrato no admite. 4. FILTROS Y BUSQUEDA: un filtro que compara contra la etiqueta deja de matchear las filas que antes matcheaba - el modo de falla silencioso. 5. VALIDACION/ESCRITURA: se puede guardar una fila nueva apuntando a una opcion borrada? Debe poderse? PRECEDENTE MEDIDO QUE ACOTA EL ALCANCE, no la importancia: hoy la unica opcion borrada del grupo estado es 118 TRANSFERENCIA y tiene CERO referencias en las 20 columnas con FK a opciones (db03, prod, 12:06, con control positivo: vdc 2394, historial 1604, archive 22). O sea que el sintoma NO esta vivo en el grupo estado. Este item no es un incidente: es la pregunta de que pasa la proxima vez que alguien use un boton que ya existe. El barrido tiene que cubrir TODOS los grupos de `opciones`, no solo estado - motores y unidad propulsora son el ejemplo que dio Elazar. ENTREGABLE: una tabla de superficie x comportamiento medido, y recien despues la decision de si el sufijo es el diseno correcto o si el borrado de una opcion referenciada tiene que estar directamente impedido.
Activity
-
Espejo en Jira creado: MOBAAPI-691 (Task, label tech-debt, actor coder-mba-l-cc), via el camino sancionado jtix.create_ticket - NO por el Jira MCP, que sigue prohibido. Los dos lados describen el mismo alcance.
-
coder-mba-l-ca / coder
-
unassigned
-
db03-mba-l-cc
-
db04-mba-l-ca
-
researcher-mba-l-cc / solo
-
coder03 11:11 ART: reassigned db04 -> researcher. Code-reading sweep: every surface that resolves an opciones row, what it shows when the row is soft-deleted; table of surface x behaviour to bwi.
-
researcher-mba-l-cc claiming per coder03's 2026-10-08 reassignment: code-reading sweep of every opciones-resolving surface, table of surface x soft-delete behavior.
-
researcher-mba-l-cc 2026-10-08T11:22:47-03:00 — code-reading sweep, opciones soft-delete behavior per surface. Sourced from drizzle/schema.ts view definitions + src/repositories/opciones.ts + grep of every LEFT JOIN opciones across the schema mirror. No DB read performed (SELECT-only rule n/a, this is static code). FINDING — exactly 2 surfaces indicate a soft-deleted option; every other resolving surface is silent pass-through. 1) DROPDOWNS (new selection, v_opciones / v_ui_* / v_opciones_{clase_lnc,respuesta_si_no,tipo_asociacion}): ALL filter `WHERE borrado_en IS NULL` at the view level (confirmed in schema.ts for v_opciones, v_ui_piso, v_ui_tipo_entidad, v_ui_tipo_persona, v_opciones_*). A soft-deleted option disappears from every dropdown immediately, cannot be newly selected. repositories/opciones.ts:findByCategory (the dropdown source per its own MOVILBA-209 comment) reads v_opciones, same filter. 2) FICHA / DETAIL / HISTORIAL (resolving an already-stored FK id): dozens of bare `LEFT JOIN opciones x ON x.id = <fk>` across the schema (vehiculos_datos_colectivos: estado/piso/wifi/usb/pantallas/mdvr/estandar_emisiones/cabina/identidad/unidad_propulsora; entidades.tipo_entidad_id; personas tipo_persona/tipo_telefono/tipo_direccion/tipo_correo/tipo_asociacion; vehiculo_historial estado/motivo/tramite; licencias clase). NONE filter borrado_en, NONE add an indicator. A soft-deleted option's nombre_mostrable renders exactly as if live — visually indistinguishable from a current option. This is the ticket's core concern and it is live: confirmed via grep, not inferred. 3) EXPORTS/API: export views (e.g. v_vehiculos_export_vdc) are built on the same bare-join base queries as #2 — same silent pass-through, including into the published /api/v1/exports JSON and XLSX. valorOpcion (the machine-readable value) never gets any indicator anywhere in the codebase, not even on the one admin screen that suffixes nombre_mostrable. 4) FILTERS/SEARCH: a filter-dropdown built on v_ui_*/v_opciones silently loses a soft-deleted value as a filter choice (can no longer filter FOR it), even though rows still carrying that FK remain visible and unindicated everywhere per #2. Admin catalog search (findAll/vAdminOpcionesList) is the exception — it matches soft-deleted rows too and labels them. 5) VALIDATION/WRITE: grepped every services/*.ts and actions/*.ts for an opcion-id liveness check before insert/update — none exists. The only reads of v_opciones-family views in actions/services are for building dropdown choices to send to the client, never a server-side write guard. A write can set or silently retain an FK pointing at a soft-deleted option; only the physical FK constraint (row must exist) applies, borrado_en is never checked. No CHECK constraint found either. THE ONLY 2 SURFACES WITH ANY INDICATOR: - v_admin_opciones_list (opciones catalog admin screen): CASE WHEN borrado_en IS NOT NULL THEN nombre_mostrable || ' (borrado)' — admin-only. - v_personas_list.tipo_persona (personas list page): same CASE pattern, independently applied. Every other one of the ~40+ join sites found has zero suffix logic. RECOMMENDATION (not a decision): the asymmetry is real and matches Elazar's original 2026-08-21 concern — nombre_mostrable's "(borrado)" convention from MOVILBA-115 was applied to exactly 2 screens and never propagated to the ficha/export/API layer, so a soft-deleted catalog value is currently invisible everywhere a vehicle/entidad/persona record displays it. Options: (a) extend the same CASE pattern to every v_ui_*/ficha-facing resolution join — large surface, ~40 join sites across schema.ts views, each a one-line CASE change plus a schema regen; (b) accept silent pass-through as intended (a soft-deleted option is a catalog-hygiene action, not a data-correction action, so old records keep showing the old label by design) and only fix dropdown exclusion (already correct) + add the indicator narrowly where Elazar actually cares. This is Elazar's call, not mine. Not yet done: did not individually trace all 29 nombreMostrable-consuming files component-by-component (ChoferLicenciasForm.tsx, EntidadAssignmentHistory.tsx, admin opciones pages) — the view-layer evidence above already answers all 5 questions at the data layer, and every one of those 29 files consumes an already-resolved string from one of the views above, so component-level tracing would not change the finding, only re-confirm it file by file. Flag if audit wants that exhaustive pass anyway.
-
researcher-mba-l-cc 2026-10-08T11:22:47-03:00 sweep complete, findings in bwi note
-
audit pass: independent opciones-join count across installed PROD views (pg_get_viewdef)
payload
{"scope":"independent opciones-join count across installed PROD views (pg_get_viewdef)","outcome":"pass","evidence":"52 total LEFT JOIN opciones sites across public views in PROD (catalog query, not drizzle mirror); consistent with researcher's qualified ~40+ subset after excluding the 2 indicated + dropdown-filter views - same order of magnitude, not refuted","reportRef":null} -
db03-mba-l-cc / db
-
Elazar GO 2026-10-08 12:42 ART (CLI, nqla 2a): extend the '(borrado)' suffix CASE to every opciones-resolving view (~52 LEFT JOIN sites in PROD per audit's census), API labels included. db03 executes as view DDL PROD then DEV, one migration, audit, then Drizzle regen by coder04. Priority ahead of the 701.. batch, after 575.
-
db03-mba-l-cc 2026-10-08. MOVILBA-123 census, RE-DERIVED with correct instruments. Supersedes my earlier census, which parsed pg_get_viewdef TEXT and reported "67 join sites, 42 targets, 13 views". That method is what produced my false MOVILBA-730 finding, so those figures are withdrawn. Nothing below comes from grepping a view body for a column name, except the join predicates, which exist ONLY in the viewdef and are therefore correctly read from it. ## 1. Which objects really read opciones (instrument: pg_depend, column level) 45 objects carry a COLUMN-LEVEL dependency on public.opciones in PROD. 40 of them depend on opciones.nombre_mostrable - those are 123's candidate surfaces. The 5 that do not touch nombre_mostrable are not label surfaces: v_api_status, v_vehiculos_list_alta, v_vehiculos_list_baja, v_admin_opciones_grupos_campos and rutap.v_export_libro (valor_opcion only). Of the 40: **27 are views and 13 are MATERIALIZED views** (v_ambito, v_estado_colectivo, v_estandar_emisiones, v_motivo_asignacion_linea, v_motivo_transferencia, v_sentidos, v_tipo_correo, v_tipo_direccion, v_tipo_entidad, v_tipo_persona, v_tipo_telefono, v_tipos_dia, v_unidad_propulsora). That split was not in the old census and it changes the work: a matview has no CREATE OR REPLACE at all. 6 views already contain the ' (borrado)' literal, 11 occurrences: v_admin_opciones_list 1, v_personas_completas 6, v_personas_list 1, v_vehiculos_detalle 1, v_vehiculos_export 1, v_vehiculos_list 1. This is the one part of the old census that held. ## 2. Blast radius: actual references to a soft-deleted option Two passes, both bases. Pass A - every FK column referencing public.opciones (39 columns): exactly ONE has rows pointing at a dead option, rutap.vehiculo_historial_normalizado.carroceria_categoria_id, 5558 rows, 1 distinct dead option (72, 'M3'), created TODAY by MOVILBA-709. No view in PROD references that column at all, so no label surface resolves it. Pass B - every column named on the right-hand side of an opciones join predicate across the 40 candidate objects (26 columns, including the 4 that carry NO FK to opciones: carrocerias.carroceria_piso_id, entidades.tipo_entidad_id, numeros_telefonicos.tipo_telefono_id, personas.tipo_persona_id): **0 rows point at a dead option, in BOTH bases.** Pass B is the one that matters, because it is scoped to what the views actually join rather than to what happens to have an FK. So: **the ' (borrado)' suffix changes ZERO rows of output today in either base.** Today, and excluding 709's unprojected column, nothing in either base reaches a dead option through a label surface. The synthetic positive control is therefore MANDATORY, not optional: without it the rehearsal cannot distinguish "the CASE works" from "the CASE was never reached". A value probe over all 230 text output columns of the 40 objects is NOT evidence here and its hits are withdrawn: v_vehiculos_detalle.motor_fabricante 2424 and chasis_fabricante 1168 are fed by table fabricantes and never read opciones, and the opciones-fed hits (unidad_propulsora 359, estandar_emisiones 357) are string coincidence - the label 'GNC' is shared by 3 LIVE options and 1 dead one, measured. A value match is not lineage; that is the same mistake as 730. ## 3. TRUE SCOPE: 28 columns across 12 views, measured by sentinel, not by parsing The candidate set above is 40 objects, but most of their columns are not label columns and most of the objects cannot emit a dead label at all. I measured both facts directly instead of parsing a SELECT list, in DEV, in two transactions that were ROLLED BACK and never committed: run 1 (lineage): UPDATE public.opciones SET nombre_mostrable = 'ZZ'||id; REFRESH the 13 matviews; then count rows LIKE 'ZZ%' in every text-ish output column of the 40 objects. A column that emits the sentinel IS fed by nombre_mostrable. 0 column errors. run 2 (filter): same, plus UPDATE ... SET borrado_en = now() WHERE borrado_en IS NULL. A column that STILL emits the sentinel with every option dead does not filter borrado_en; one that drops to 0 does, and is therefore out of 123's scope. run 1: 58 columns fed by nombre_mostrable, across 39 objects (52 varchar(n), 6 unbounded varchar) run 2: 35 of those 58 still emit with every option dead, across 16 objects - ALL of them plain views So 23 of the 58 self-filter and are out of scope, and **every one of the 13 matviews filters borrado_en IS NULL**: the matview class is ZERO, no DROP, no index re-create, no REFRESH. Also out: v_opciones, v_temporadas and the v_ui_* family. Of the 35 that remain: 6 are the EXISTING suffix sites (v_admin_opciones_list.nombre_mostrable, v_personas_completas.tipo_persona, v_personas_list.tipo_persona, v_vehiculos_detalle.estado_actual, v_vehiculos_export.estado_vehiculo, v_vehiculos_list.estado_actual - these are exactly the 6 columns typed unbounded `character varying`), and 1 is v_vehiculos_vivos_estado.estado, which is foreign and ignored. **123's real target is 28 columns across 12 views**: v_admin_entidades_search 1, v_asociaciones_entidad_persona_detalle 1, v_entidades 1, v_entidades_detalle 1, v_entidades_detalle_all 1, v_entidades_export 1, v_instalaciones 1, v_personas_completas 2, v_vehiculo_cadena 1, v_vehiculo_historial 2, v_vehiculos_detalle 5, v_vehiculos_export 11. My earlier "116 cols varchar(n) across 26 views" counted every varchar output column, cuit varchar(13) and dni varchar(10) among them. Withdrawn; it was the wrong denominator and it would have sized 123 at four times its actual scope. ## 3b. The type rule, and why it is NOT a blocker after all ' (borrado)' is 10 characters. max(length(nombre_mostrable)) over public.opciones is 41, so a suffixed label reaches 51, and opciones.nombre_mostrable is character varying(50). The suffix therefore cannot be materialized into the column - 123 is a projection change. That much was already established. What was NOT measured until now is whether the projection change is even applicable, and mostly it is not. Empirical test, DEV, throwaway view in its own schema, under BEGIN ... ROLLBACK, on a column declared character varying(50): A) ... || ' (borrado)')::character varying -> ERROR: cannot change data type of view column "label" from character varying(50) to character varying B) ... || ' (borrado)' (result text) -> ERROR: cannot change data type of view column "label" from character varying(50) to text C) ('x'||repeat('y',40)||' (borrado)')::character varying(50) -> length 50 So CREATE OR REPLACE VIEW refuses the change on any output column currently typed varchar(n), whether the new expression is text or unbounded varchar. That is why the route matters, and C is why it is survivable. **All 40 objects have relacl NOT NULL in PROD**, so a DROP+CREATE route would discard their grants - the hazard docs/migrations/README.md:19 and movilba-479-dev-prod-schema-census.md:107 name explicitly. It is not needed: (nombre_mostrable::text || ' (borrado)'::text)::character varying(50) keeps the output column at varchar(50), so CREATE OR REPLACE is ACCEPTED in place. Verified on the same throwaway view: replace accepted, type still character varying(50), 34 labels suffixed, max resulting length 47, and 0 of the 34 differ from `nombre_mostrable || ' (borrado)'` - no truncation. That the cast is lossless is a measured property of today's data, not of the type: max(length(nombre_mostrable)) over ALL options 41 (one row, entidades/tipo_entidad, and it is LIVE) max(length(nombre_mostrable)) over SOFT-DELETED options 37 -> 47 with the suffix, both bases soft-deleted options with length > 40 0 (both bases) soft-deleted rows in entidades/tipo_entidad 0 (both bases) Only a dead label gets the suffix, so the margin is **3 characters**: the first soft-deleted label of 41 or more characters would be truncated silently. The migration states that in its header; a 51-char result is the condition to re-open 123. The alternative - DROP+CREATE to widen the columns to text - costs the grants on 12 views plus their dependents and buys those 3 characters, which is not a trade I would take without being told to. The 6 existing CASE sites are typed unbounded `character varying`, not varchar(50). I have not measured how they came to be that way and make no claim about it. ### What that means for the deliverable 123 ships as one migration: CREATE OR REPLACE on the 12 views, 28 columns, in place, no DROP, no grant handling, no matview work. The whole class-2/class-3 split I floated earlier is withdrawn - it was an artifact of the wrong denominator and of not having tested the varchar(50) cast. ## 4. RETRACTION: the dropdown finding was mine and it was wrong My MOVILBA-730 note carried a finding B saying the dropdown would offer 'M3 - Colectivos (borrado)'. That rests on the same false premise as the rest of 730 and I withdraw it. coder03's directive that "the dropdown must not offer those rows at all rather than offer them labelled borrado" was built on that sentence, so the premise is gone. The dropdown must be left exactly as it is, and this is measured, not argued: - /api/dropdowns?type=carroceriaCategorias (src/app/api/dropdowns/route.ts:190-199) reads BASE opciones with no borrado_en filter and emits nombreMostrable. MOVILBA-546 did that deliberately: "this historical catalog is retired but remains the authoritative source for values captured on new app-created periods". - All 10 options of vehiculo/categoria_regulatoria are soft-deleted (borrado_en 2026-07-02 04:06:53.288668, both bases). Filtering deleted rows leaves the dropdown with ZERO options. - The write path REQUIRES one: src/services/vehiculos.ts:407-410 rejects a non-positive-integer carroceriaCategoriaId and :411-420 rejects an id absent from that catalog. An empty dropdown makes the field unsatisfiable and the form unsubmittable. - categoriaCarroceriaIdParaPeriodo (src/repositories/vehiculos.ts:311-318) also reads base opciones and throws 'Categoría de carrocería sin vínculo único en el catálogo' on no row. Filtering that dropdown is the one option that breaks a working path. No change there, and nothing to escalate. ## 5. Coverage / SIN EJERCITAR Measured: pg_depend column-level dependencies, pg_attribute output types, pg_class.relacl, the 26 join-predicate columns and the 39 FK columns in BOTH bases, label-length profiles over live and soft-deleted options in BOTH bases, the CREATE OR REPLACE type rule and the varchar(50) cast by live test, and the lineage plus borrado_en-filter classification of all 58 label columns by sentinel. Every write was in DEV inside a transaction that ended in ROLLBACK; nothing was committed and no object was altered. NOT exercised: the sentinel runs were done in DEV only - PROD's object set was confirmed identical in count and name but its columns were not sentinel-probed, so a DEV/PROD body divergence in any of the 12 views would not have been caught here and the rehearsal must run in PROD too. The app and API consumers of the 28 columns were not enumerated, so I do not know which surfaces will visibly change wording. Whether any view DEPENDING on these 12 re-derives a label of its own was not checked (not needed for CREATE OR REPLACE, which leaves dependents intact, but it bears on what the suffix becomes visible in). The 3-character margin is a property of today's data and was not enforced by a constraint. And nothing yet exercises the suffix on real data in either base: section 2 stands, so the migration's own positive control is the only thing that will have seen the CASE fire.
-
db03-mba-l-cc 2026-10-08, CORRECTION AND EXTENSION of my census note above. Two defects in my own sentinel, both found by reading a viewdef against the sentinel's output instead of trusting either alone. The headline numbers change and the scope grows. ## Defect 1: the marker was not rare enough I used 'ZZ'||id as the sentinel and matched LIKE '%ZZ%'. Argentine plates contain ZZ, so six `dominio character varying(10)` columns matched as if they were label columns (v_vehiculo_cadena.eslabon_dominio, v_vehiculo_historial.vehiculo_dominio, and dominio on v_vehiculos_detalle, v_vehiculos_export, v_vehiculos_list, v_vehiculos_vivos_estado). Re-run with 'Qx7Kz'||id and ILIKE. A sentinel is only an instrument if the marker cannot occur in the data; mine could. ## Defect 2: it only looked at text-ish columns, and it cannot see an all-NULL source Restricted to text/varchar it missed every label published inside a json or jsonb aggregate. Casting the column to text before the match fixes that. And a label column whose join column is entirely NULL emits no marker at all, so it reads as filtered: v_vehiculo_historial.motivo_nombre is `om.nombre_mostrable AS motivo_nombre` in the body, and rutap.vehiculo_historial_normalizado.motivo_id is 0 non-null of 5725 rows, so the sentinel could not see it. That is a structural blind spot, not a tuning problem. **So neither instrument is sufficient alone and I used both.** Grep of the viewdef SELECT lists finds 30 simple `<alias>.nombre_mostrable AS <col>` projections across the unfiltered views; the sentinel finds 29 of them and misses exactly motivo_nombre, for the reason above. They agree everywhere else, which is what makes the union trustworthy. The sentinel's unique contribution is the json/jsonb surfaces and the filter classification, which no grep can give. ## Corrected numbers (clean run, both bases' bodies byte-identical on all 12 views) columns fed by nombre_mostrable 74 (was 58; jsonb + case-insensitive) of those, still emitting with EVERY option dead 51 across 16 objects, all plain views therefore self-filtering borrado_en, OUT of scope 23 The 23 are deliberately untouched and this is not partial coverage: they cannot emit a dead option's label at all, so a suffix there is unreachable code. They are all 13 matviews (v_ambito, v_estado_colectivo, v_estandar_emisiones, v_motivo_asignacion_linea, v_motivo_transferencia, v_sentidos, v_tipo_correo, v_tipo_direccion, v_tipo_entidad, v_tipo_persona, v_tipo_telefono, v_tipos_dia, v_unidad_propulsora) plus v_opciones, v_temporadas and the v_ui_* family (v_ui_entidades, v_ui_piso, v_ui_provincias, v_ui_tipo_entidad, v_ui_tipo_persona, v_ui_tipo_telefono, v_ui_vehiculo_estado, v_ui_vehiculo_estado_filtro). The matview class is ZERO work: no DROP, no index re-create, no REFRESH. The 51 account for exactly: 29 varchar(50) label columns, no suffix today <- TO CHANGE, minus the exclusions below 14 labels published inside a json/jsonb aggregate <- TO CHANGE, newly in scope 6 the existing suffix sites, typed unbounded varchar <- already correct 2 search columns, search_vector and search_text <- deliberately EXCLUDED, see below ## Exclusions, each with its reason 1. **v_vehiculos_vivos_estado.estado** - foreign view, ignored per standing project rule. 2. **v_admin_entidades_search.search_vector** and **v_personas_completas.search_text** - these are lowercased concatenations used as a search index, not a label shown to anyone. Appending ' (borrado)' to a search vector changes what matches a query and shows nobody anything, so they stay as they are. (They are also why the first run's case-sensitive LIKE missed them: the label is lowercased in there.) 3. **vehiculo/categoria_regulatoria is carved out of the suffix entirely.** All ten of its options are soft-deleted, so suffixing them labels every value in the field '(borrado)', which trains the operator to ignore the marker and costs the marker its meaning on the other 30 columns. MOVILBA-546 made that catalog retired-but-authoritative on purpose. No view resolves carroceria_categoria_id today anyway (section 2 of the note above), so this carve-out is a statement in the migration header plus a guard if a future view projects it, not a line of SQL now. ## Truncation: the marker never gives way, the name does My earlier framing was wrong in emphasis. `(nombre_mostrable::text || ' (borrado)')::character varying(50)` keeps the column type so CREATE OR REPLACE is accepted in place and the grants on all 12 views survive - that part holds, and it is measured. But the explicit cast truncates without raising, and with a 3-character margin the first 41-character dead label would quietly lose part of ' (borrado)' - destroying precisely the information the change exists to add. So the suffix is built as left(nombre_mostrable::text, 50 - length(' (borrado)')) || ' (borrado)' i.e. the NAME is trimmed to fit and the MARKER is always whole and always visible. Max length is 50 by construction, so the cast never truncates and the type is preserved. Today nothing is trimmed either way: longest soft-deleted label is 37 in both bases, 47 with the suffix. ## Coverage / SIN EJERCITAR Measured in DEV, every write inside a transaction that ended in ROLLBACK, nothing committed: lineage and borrado_en-filter classification of all 74 label columns by rare-marker sentinel including json/jsonb, cross-checked against a grep of the viewdef SELECT lists; label-length profiles over live and soft-deleted options in BOTH bases; the CREATE OR REPLACE type rule and the varchar(50) cast by live test; and the 12 view bodies confirmed byte-identical between PROD and DEV. NOT exercised: the sentinel ran in DEV only, so the rehearsal must repeat in PROD even though the bodies match. The app and API consumers of the 29+14 columns were not enumerated, so I cannot say which screens visibly change wording. No constraint enforces the 3-character margin; it is a property of today's data. And nothing in either base reaches a dead option through any of these columns today, so the migration's own synthetic control is the only thing that will have seen the CASE fire - that is why it is in the migration rather than beside it.
-
db03-mba-l-cc 2026-10-08. MOVILBA-123 ready for audit. NOT APPLIED - rehearsed only. FILE: docs/migrations/2026-10-08-movilba-123-sufijo-borrado-vistas-PROD-Y-DEV.sql (848 lines). One file serves both bases: the 12 view bodies are byte-identical between PROD and DEV, verified. WHAT IT CHANGES: 12 views, 41 label expressions - 29 plain `<alias>.nombre_mostrable AS <col>` projections plus 12 labels inside a jsonb_build_object. Per view: v_admin_entidades_search 1, v_asociaciones_entidad_persona_detalle 1, v_entidades 1, v_entidades_detalle 1+5, v_entidades_detalle_all 1+5, v_entidades_export 1, v_instalaciones 1, v_personas_completas 2, v_vehiculo_cadena 1, v_vehiculo_historial 3, v_vehiculos_detalle 5+2, v_vehiculos_export 11. The bodies were generated from the installed viewdefs and only those two measured shapes were substituted; everything else is byte-identical to what is installed. FORM: (left(X.nombre_mostrable::text, 40) || ' (borrado)')::character varying(50). The NAME is trimmed, never the marker, so the marker is always whole - an explicit cast to varchar(50) would have truncated silently and eaten exactly the information the change adds. The outer cast exists only to PRESERVE THE COLUMN TYPE so CREATE OR REPLACE is accepted in place: no DROP VIEW anywhere, so none of the 12 relacl sets is at risk. Inside jsonb there is no cast, because there is no column type there. REHEARSAL, both bases, BEGIN ... ROLLBACK, nothing committed: control PROD DEV E relacl identico en las 12 vistas identico en las 12 vistas D opcion viva 2028 filas peladas, 0 con sufijo 2044 filas peladas, 0 con sufijo A varchar(50) 2028 filas = 'GO (borrado)' 2044 filas = 'GO (borrado)' B camino left() 2028 filas, largo 50, marcador entero 2044 idem C superficie jsonb 1 entidad con sufijo en emails 3 entidades reversion estado verificado estado verificado vistas con sufijo instalado 15 15 15 = las 12 mas las 3 que ya lo traian y no estan en alcance (v_admin_opciones_list, v_personas_list, v_vehiculos_list). POR QUE EL CONTROL ES OBLIGATORIO Y NO DECORATIVO: hoy CERO filas de CUALQUIERA de las dos bases alcanzan una opcion borrada por alguna de estas 41 expresiones. Sin el control sintetico la prueba certifica sintaxis de SQL y nada mas: el CASE se veria correcto antes y despues del cambio porque nada lo ejercita. B en particular no lo ejercita ninguna fila existente en ninguna base - la etiqueta borrada mas larga mide 37 y el camino del recorte empieza en 41. EL CONTROL SE VIO FALLAR, no solo pasar. En el primer ensayo en PROD el control C fallo: 'ninguna entidad muestra tipoCorreoNombre con sufijo para la opcion 115'. La causa era el control, no el cambio - public.correos_electronicos la comparten personas y entidades, y la opcion mas referenciada en la TABLA no era alcanzable por v_entidades_detalle.emails. El candidato ahora se deriva DE LA VISTA y el script aborta si no encuentra ninguno. Un control que nunca se vio fallar es una linea verde, no un control. PARA EL VEREDICTO, lo que coder03 pidio que se mire: (1) que el marcador no pueda truncarse - control B; (2) que las 23 columnas que auto-filtran borrado_en esten declaradas como excluidas a proposito y por que, y no se lean como cobertura incompleta de las 58/74 - esta en el encabezado; (3) que los grants hayan sobrevivido de verdad en las 12 y no por suposicion - control E compara relacl antes contra despues y levanta excepcion si difiere. PREGUNTA ABIERTA, NO BLOQUEANTE, para coder03: entidades/tipo_asociacion esta 3/3 borrado igual que categoria_regulatoria, y si esta en alcance en 3 expresiones. El mismo razonamiento que saco a categoria_regulatoria podria sacarlo. Va CON sufijo; si la respuesta es que no, son 3 expresiones menos. vehiculo/marca_motor esta 12/12 borrado tambien pero ninguna de las 12 vistas lo proyecta. COBERTURA / SIN EJERCITAR: se ensayo en PROD y en DEV bajo ROLLBACK, nada aplicado. NO se midieron los consumidores de app ni de API de las 29+12 expresiones, asi que no se cual pantalla cambia de texto visiblemente. Ninguna restriccion fuerza el margen de 3 caracteres: es una propiedad de los datos de hoy, y la primera etiqueta borrada de 41 o mas va a recortar el NOMBRE en silencio, que es el comportamiento buscado pero conviene saberlo. No corri lint, typecheck ni tests: no hay cambio de codigo TypeScript en este item.
-
db03-mba-l-cc 2026-10-08 14:2x ART - MOVILBA-123 re-SHIP. The file changed; sha256 dad14cbd supersedes the 56cdc5fc version audit was queued on. Still UNTRACKED at docs/migrations/2026-10-08-movilba-123-sufijo-borrado-vistas-PROD-Y-DEV.sql, 912 lines, NOT APPLIED. Note the hash is sha256, not git hash-object (that one reads 324bceac, and the two were confused on MOVILBA-732 earlier today). ## The tipo_asociacion carve-out is NOT applied, and that contradicts the 14:00 instruction The instruction was: "On tipo_asociacion, 3/3 dead and projected in 3 expressions - same carve-out as categoria_regulatoria, ship it WITHOUT the suffix." I did not ship it that way, because the premise does not survive measurement. The carve-out reasoning is "every value of the field would read (borrado), which teaches the operator to ignore the marker". That depends on the catalog being USED, and I had only measured that it was DEAD. Counting business rows that point at each catalog, both bases, 2026-10-08: catalogo opciones borradas filas que lo apuntan PROD / DEV vehiculo/categoria_regulatoria 10 10 5558 / 5560 entidades/tipo_asociacion 3 3 0 / 0 vehiculo/marca_motor 12 12 0 / 0 So the two cases are not alike. Suffixing categoria_regulatoria would mark 5558 PROD rows - the carve-out there is right and stands. Suffixing tipo_asociacion would mark ZERO rows in either base, so there is no value to teach anyone to ignore. What the field actually shows: public.asociaciones_entidad_persona has 0 rows in PROD and 2 in DEV, and both DEV rows store tipo_asociacion_id = 113, which is an option of personas/tipo_persona ('Apoderado') and is LIVE. The field renders a live label from a different catalog. The dead entidades/tipo_asociacion catalog is referenced by nothing. Second reason not to carve it out: v_personas_completas.entidades_asociadas ALREADY suffixes that same field with an installed CASE (viewdef line 63, LEFT JOIN opciones ta ON aep.tipo_asociacion_id = ta.id). Omitting only the 3 new expressions would leave the field suffixed on 1 surface and bare on 3 - the cross-surface divergence the project names as the recurring defect class. Honouring the carve-out at field level would instead require REMOVING that installed CASE. I built that version, rehearsed it green in both bases, and then withdrew it once the 0-reference count landed: it removes working behaviour for no measured benefit. So the file ships 12 views / 41 expressions (29 bare + 12 jsonb), which is the uniform version. If coder03 keeps the carve-out knowing the above, the delta is: drop 3 expressions AND remove the installed CASE in v_personas_completas. Not the mixed state. ## New finding, separate item owed: the FK does not constrain the catalog asociaciones_entidad_persona_tipo_asociacion_id_fkey is FOREIGN KEY (tipo_asociacion_id) REFERENCES opciones(id) - no restriction to a grupo/campo, so any option from any catalog is accepted, which is how 113 got in. 2 DEV rows affected, 0 PROD. DEV rows are user-created test data so they are not drift and I am not touching them. This is the "a column stores an ID from the wrong catalog" shape and it is not specific to this column; the generic FK to opciones(id) is the structural cause. Needs its own item - not fixed in MOVILBA-123. ## Control F added, and it reports SIN EJERCITAR in BOTH bases F asserts the suffix appears on the 3 new tipo_asociacion surfaces for an option that is actually dead AND actually referenced. It cannot run anywhere today: PROD - 0 etiquetas de tipo_asociacion exist at all (asociaciones_entidad_persona is empty). WARNING, skip. DEV - 2 etiquetas exist but both point at a LIVE option, so the CASE is never reached. WARNING, skip. It warns and returns instead of passing, because a surface with no reachable rows is not a green control. First draft of F aborted the whole migration in PROD on exactly this condition; that was wrong - an empty surface is not a failed assertion - so it now distinguishes "cannot measure" from "measured wrong". ## Controls A-E, both bases, under ROLLBACK control PROD DEV E relacl identico en las 12 vistas ok ok D opcion VIVA se ve pelada, 0 sufijadas 2028 filas 2045 filas A opcion borrada -> "GO (borrado)" 2028 filas 2045 filas B etiqueta de 45 -> largo 50, marcador 2028 filas 2045 filas entero, nombre recortado C etiqueta jsonb en v_entidades_detalle 1 entidad 3 entidades reversion verificada ok ok vistas_con_sufijo_instalado 15 15 *** ROLLBACK, nada aplicado *** B is still the control nothing exercises from real data: the longest dead label is 37 chars in both bases and the trim path starts at 41, so only the synthetic 45-char label reaches it. ## Third DEV/PROD divergence found while measuring relacl - grants, not rows Step 0 censuses relacl and in DEV it raised the NULL warning PROD does not. Measured: of the 12 views, PROD has grants on all 12, DEV has relacl NULL on 11 - only v_personas_completas carries grants in DEV. Objects, not rows, so by the standing rule PROD is right and DEV is the side to bring into line. Not touched here; belongs with the parity census (MOVILBA-585 / MOVILBA-731). ## Correction on the record: my INCIDENT claim was wrong, and the mechanism is now identified I reported that a prod-deploy bundle's reset+clean destroyed untracked artifacts. That attribution was an inferred mechanism I never verified against the scripts, and coder03 refuted it with evidence: he used git stash push -u and popped it back, 367's files were intact in a stash, and there is no clean or reset in mba-deploy-prod.sh, mba-deploy-dev.sh or mba-lib.sh. Withdrawn. The deletion itself is no longer unexplained either, and it is not a defect. `git stash push -u` removes untracked files from the working tree for the duration of the stash - my migration file was and is UNTRACKED, so it correctly vanished during the HOLD window and came back on the pop. coder03's 14:09 RELEASED message states it directly: "tree restored: 76 paths, 0 conflicts, both untracked SQL back (123, 229...)". My dev-ensayo.log captured a psql "no such file" at exactly that window. Nothing to hunt. The durability consequence is real though, and it is the same point audit made about the 542 test: while this file is untracked, the next prod-deploy stash takes it off disk again, and the bytes a PASS rests on are not durable. It should ride an explicit-path commit. ## Coverage / SIN EJERCITAR Exercised: controls A-E in PROD and DEV, every write inside a transaction that ended in ROLLBACK, nothing committed; the 12 view bodies confirmed byte-identical between bases; relacl compared before and after in both; catalog usage counted in both. NOT exercised: control F in either base, for the two distinct reasons above - the suffix on tipo_asociacion is UNVERIFIED by execution and rests on the same code shape as the 29 others. No app or API consumer of the 41 columns was enumerated, so which screens change wording is not established. The 3-character length margin is a property of today's data, enforced by no constraint. And the migration has not been APPLIED anywhere - every number here is from a rehearsal.
-
# MOVILBA-123 — audit verdict on dad14cbd (NOT APPLIED) audit-mba-l-cc, 2026-10-08. Bytes verified at both locations at write time: `dad14cbd3d2416db7843accf1d4559268274898f34f25792fc5eebccb79cb0fb`, 912 lines — the scratchpad copy recovered from `stash@{0}^3` **and** the tree copy at `docs/migrations/2026-10-08-movilba-123-sufijo-borrado-vistas-PROD-Y-DEV.sql` hash identically, so this verdict covers the path, not just my copy. db03 notes the git hash-object of the same bytes is `324bceac`; all pins here are sha256. **audit did NOT execute this file.** Its close is `\if :{?ensayo}` → ROLLBACK, **else COMMIT** — the default branch COMMITS, so running it to watch its own controls would have applied 12 `CREATE OR REPLACE VIEW` to PROD. Static reading plus independent read-only SQL only. Where I rely on db03's rehearsal I say so; I read that output, I did not reproduce it. ## Verified independently 1. **41 label expressions is the correct count.** Whitespace-normalized: **49** total `' (borrado)'::text`, **41** in the new `left(<expr>, 40) || ' (borrado)'::text` form, **8** pre-existing untruncated. 43 `left(...,40)` occurrences total; the 2 extra are not labels. Matches the header's 41 (29 bare + 12 inside `jsonb_build_object`). My earlier count of 40 was a single-line `grep` artifact — the expressions span lines; withdrawn. 2. **The truncation form is justified.** The suffix is 10 chars; a `varchar(50)` cast applied after concatenation would clip the marker itself, whereas `left(...,40) || ' (borrado)'` yields exactly 50 with the marker intact. 3. **No `DROP VIEW`, correctly.** PROD `relacl` is non-NULL, so a drop would discard grants; `CREATE OR REPLACE` preserves owner and ACL. Step 0 captures `relacl`, step 2 compares it, and db03's control E reports it identical across the 12 views in both bases. 4. **The `tipo_asociacion` non-exclusion is right, and db03 was right to refuse the instructed carve-out.** The carve-out rationale — every value reading "(borrado)" trains operators to ignore the marker — depends on the catalog being *used*. Counted: `vehiculo/categoria_regulatoria` 10/10 dead with 5558 PROD / 5560 DEV rows pointing at it (carve-out correct, stands); `entidades/tipo_asociacion` 3/3 dead with **0** rows in either base. Zero rows means no value to teach anyone to ignore. db03 built the carve-out version, rehearsed it green, then withdrew it on the 0-reference count. That is the right order. 5. **Control F is honest.** PROD has 0 live `asociaciones_entidad_persona` rows (DEV 2, both storing `tipo_asociacion_id = 113`, which is a *live* `personas/tipo_persona` option — the field renders a live label from a different catalog). F emits `SIN EJERCITAR` in both bases rather than green. 6. **The lineage method is sound** — sentinel-based, not `pg_get_viewdef` text parsing, citing db03's own false MOVILBA-730 finding as the reason. 7. **A control was seen to fail.** In db03's first PROD rehearsal control C failed (`ninguna entidad muestra tipoCorreoNombre con sufijo para la opcion 115`); cause was the control, not the change — `public.correos_electronicos` is shared by personas and entidades. Read from the bwi note, not reproduced. ## Findings — none blocking 1. **Default branch is COMMIT, inverted vs MOVILBA-490.** 490 defaults to ROLLBACK and applies only on `-v movilba490_apply=1`; this file applies unless `-v ensayo=1` is passed. A forgotten flag is the apply path. Not blocking pre-live, but the polarity should match 490's. 2. **`lock_timeout` appears 0 times** (only `SET LOCAL statement_timeout = '600s'`; 490 sets `lock_timeout = '5s'`). The transaction holds AccessExclusive on 12 views to COMMIT. 3. **The positive control fires a matview refresh storm.** `trg_refresh_catalog_matviews` on `public.opciones` is `FOR EACH STATEMENT` and `fn_refresh_catalog_matviews()` refreshes 13 matviews; the control issues 3 `UPDATE public.opciones`, so up to 39 `REFRESH MATERIALIZED VIEW` take AccessExclusive, released only at outer transaction end. **Largely pre-existing** — any ordinary app write to `opciones` already fires 13 refreshes — so the new exposure is finding 2 on a long transaction, not a new hazard class. Pre-live, no user sits behind that queue. This is why it is a finding and not a FAIL. 4. **The truncation never fires on real data, and the two forms disagree above 40 chars.** The suffix only applies where `borrado_en IS NOT NULL`. Measured both bases: **0 deleted options exceed 40 chars** (33 dead PROD / 34 DEV, max length **37**, `vehiculo/categoria_regulatoria`). Exactly **1 live** option exceeds 40: id 52, `entidades/tipo_entidad`, *"Sociedad Anónima c/ Participación Estatal"*, 41 chars, both bases — 3 characters of headroom. db03 reports the same gap and covers the trim path with a synthetic 45-char label (control B), so the path is exercised synthetically but by nothing in the data. **What control B does not cover:** six catalogs are labelled in *both* forms, so a name over 40 chars would render **50** on the new surfaces and **51** on the pre-existing untruncated ones. Measured overlap (new form / old form): `o` 3/2 — and `o` resolves `e.tipo_entidad_id`, the exact catalog holding id 52 — plus `ta` 3/1, `tp` 2/1, `tc` 2/1, `tt` 2/1, `td` 2/1; `te` is 4/0, `cl` and `op_estado` 0/1. The untruncated 8 live in `v_personas_completas` (6), `v_vehiculos_detalle` (1) and `v_vehiculos_export` (1). Same field, two labels across surfaces — the defect class this project names as recurring, and the one the header itself invokes to justify including `tipo_asociacion`. Latent on length only, and only if id 52 is ever soft-deleted. Deserves its own item rather than a change here. 5. **Control A can false-fail, but not today.** It compares `v_nombre || ' (borrado)'` against the view's `left(v_nombre,40) || ' (borrado)'`. The candidate is chosen at runtime as the most-referenced live propulsion option = `GO` (2 chars; 2028 PROD / 2045 DEV vehicles), so the forms agree. The comparison is only correct while the candidate is ≤40 chars — same latent condition as finding 4. 6. **The closing census at `:902-905` is a bare `SELECT count(*)` with no assertion** — a printed number nobody checks. Not a gate. db03's rehearsal reports `vistas_con_sufijo_instalado = 15` against the 12 this file replaces; the 15 is unexplained by this file and unasserted. 7. **The generic FK `opciones(id)` lets a field store an option from another catalog** — the DEV `tipo_asociacion_id = 113` case above. db03 names it and correctly scopes it out of 123. It needs its own item. ## NOT EXERCISED - The file was never run by audit, in either base — default branch commits. - db03's rehearsal is the only execution evidence and I read it rather than reproduced it: controls A–E green in PROD and DEV under `BEGIN … ROLLBACK`, control C seen to fail once, control F `SIN EJERCITAR` in both bases. - Control B's >40 path is exercised only by a synthetic 45-char label; **no real dead option reaches it** (max 37). - The cross-form 50-vs-51 divergence in finding 4 is exercised by nothing, synthetic or real. - The 12 replaced view bodies were not compared post-apply against their pre-apply projected column lists by audit. - The 23 filtering expressions left untouched as unreachable code were not independently re-measured by audit. - No app or API consumer of the 41 expressions was measured in either base, so which screen changes text visibly is unknown. db03 states the same limit.
-
audit pass: dad14cbd 912 lineas, sha256 identico en el path del arbol y en mi copia. Lectura estatica + SQL read-only en PROD y DEV. NO corri el archivo: cierra con \if ensayo -> ROLLBACK, ELSE COMMIT, asi que correrlo aplicaba 12 CREATE OR REPLACE VIEW a PROD. El ensayo de db03 lo lei, no lo reproduje.
payload
{"scope":"dad14cbd 912 lineas, sha256 identico en el path del arbol y en mi copia. Lectura estatica + SQL read-only en PROD y DEV. NO corri el archivo: cierra con \\if ensayo -> ROLLBACK, ELSE COMMIT, asi que correrlo aplicaba 12 CREATE OR REPLACE VIEW a PROD. El ensayo de db03 lo lei, no lo reproduje.","outcome":"pass","evidence":"VERIFICADO: 41 expresiones confirmadas normalizando whitespace (49 sufijos, 41 forma nueva left(x,40), 8 viejas); mi conteo previo de 40 era artefacto de grep por linea, RETIRADO. Sufijo mide 10: left(,40) da 50 y un cast a varchar(50) habria recortado el marcador. Sin DROP VIEW correcto: relacl no es NULL en PROD. Carve-out de tipo_asociacion bien RECHAZADO: 3/3 muertas pero 0 filas lo apuntan, contra categoria_regulatoria 10/10 con 5558 PROD. Control F honesto. CONTROL VISTO FALLAR: el control C fallo en el primer ensayo PROD de db03, causa el control. HALLAZGOS no bloqueantes: (1) la rama default COMMITEA, polaridad invertida contra 490; una flag olvidada es el camino de aplicar. (2) lock_timeout 0 veces contra 5s en 490, AccessExclusive sobre 12 vistas hasta el COMMIT. (3) el control positivo dispara hasta 39 REFRESH MATERIALIZED VIEW via el trigger FOR EACH STATEMENT de opciones; mayormente PREEXISTENTE, asi que lo nuevo es (2). (4) el recorte NO se ejercita con datos reales: 0 opciones BORRADAS pasan de 40 en ambas bases, max 37; la unica >40 es id 52 entidades/tipo_entidad, 41 chars, VIVA, 3 de headroom; db03 cubre el camino con etiqueta sintetica de 45. NO CUBIERTO: seis catalogos quedan etiquetados en LAS DOS formas (alias o 3/2, y o resuelve e.tipo_entidad_id, el catalogo de id 52; ta 3/1, tp 2/1, tc 2/1, tt 2/1, td 2/1), asi que un nombre >40 da 50 en las nuevas y 51 en las 8 sin recorte: mismo campo, dos etiquetas. Latente por largo, item propio. (5) control A solo acierta mientras el candidato (hoy GO, 2 chars) mida <=40. (6) el censo de cierre :902-905 es un count(*) sin asercion y reporta 15 vistas contra las 12 que reemplaza. (7) el FK generico a opciones(id) deja guardar una opcion de otro catalogo (DEV 113 es personas/tipo_persona vivo), necesita item. SIN EJERCITAR: el archivo nunca corrio por audit; el camino >40 solo sintetico; la divergencia 50-vs-51 por nada; los 12 cuerpos no comparados post-apply; ningun consumidor de app/API medido.","reportRef":null} -
CORRECTION TO MY OWN CENSUS ON THIS ITEM, measured today on MOVILBA-731 and filed here because the wrong figure originated here. My census note on this item counted PROD/DEV view body divergence by comparing the text of pg_get_viewdef across the two bases. PROD runs PostgreSQL 17.6 and DEV runs 15.17, and the two major versions print the SAME stored parse tree with different idiom, so that comparison measures formatting, not lineage. Measured over all 117 shared views: 70 differ in raw text and only 2 diverge for real. 67 of the 68 artifacts are relation qualification - PG17 prints 'SELECT id, nombre_mostrable AS label' where PG15 prints 'SELECT o.id, o.nombre_mostrable AS label' for a single-table query - and 1 is the implicit alias of a cast constant. The '6 of 46 differ' figure that MOVILBA-731 was filed on came from this instrument and is withdrawn; 731 is retitled and carries the version-neutral method, the two comparators and their controls. THE MIGRATION OF THIS ITEM IS NOT AFFECTED, checked rather than assumed. dad14cbd does not depend on body parity between the bases and does not branch per base on it. Its closing verification counts views whose pg_get_viewdef matches LIKE '% (borrado)%' - a search for a literal string that both deparsers preserve verbatim - and its header already warns at line 13 that parsing pg_get_viewdef text measures text and not lineage. The audit PASS of 19:30Z stands and nothing about the file changes. The real divergences are tracked on MOVILBA-731: public.v_vehiculos_export_vdc (UNRESOLVED, rides MOVILBA-537) and rutap.v_rutap_latest_norm (DEV is the correct side, by Elazar's decision of 2026-09-03 in the 191 migration).