normalizar_clave_chasis reemplaza GAS|GNC|CNG sin limite de palabra, asi que PEGASO se vuelve PEGNCO
Hallado por audit en el PASS de MOVILBA-575 (2026-10-08), medido contra la funcion viva en AMBAS bases. rutap.normalizar_clave_chasis hace regexp_replace(..., '(GAS|GNC|CNG)', 'GNC', 'g') SIN limite de palabra, asi que reescribe esas tres letras DENTRO de cualquier palabra: PEGASO -> PEGNCO, GASOIL -> GNCOIL, BIOGAS -> BIOGNC, 'MEGAS 12' -> 'MEGNC 12', CNGX -> GNCX, y '(GASOIL)' -> '(GNCOIL)' donde el parentesis ya no se saca porque el resultado no matchea el patron. PEGASO es una marca real y la funcion se aplica a fabricante_normalizado ademas de modelo_normalizado. Hoy no hay ningun valor mal en ninguna base - la exposicion es a una fila futura del libro, y el proximo import es lo primero que la ejercita, con clave equivocada y en silencio. ARREGLO: anclar la alternacion en limites de palabra, '\\m(GAS|GNC|CNG)\\M'. SEGUNDO, menor: la funcion no es idempotente para un valor que ARRANCA con la forma con parentesis, porque btrim corre solo en el paso mas interno y la regla del parentesis emite un espacio inicial: '(GAS)' -> ' GNC' -> 'GNC'. El CHECK de rutap.chasis_resolver (modelo_normalizado = normalizar_clave_chasis(modelo_normalizado)) no puede satisfacerse para una clave de esa forma, asi que falla cerrado en vez de corromper, pero una fila del libro con esa forma nunca se puede curar. Envolver toda la expresion en btrim lo arregla. Re-medir las claves guardadas y los literales de MOVILBA-578 con los mismos controles que ya trae la migracion de 575; todos los controles positivos y negativos existentes siguen pasando. ANTES del proximo import.
Activity
-
db03-mba-l-cc 2026-10-08, applied PROD then DEV. Migration: docs/migrations/2026-10-08-movilba-729-anclar-token-combustible-normalizar-clave-chasis-PROD-Y-DEV.sql MY DEFECT, introduced by MOVILBA-575 earlier the same day, found by audit. Both failures confirmed live in both bases against the installed body before the fix: normalizar_clave_chasis('PEGASO') -> PEGNCO normalizar_clave_chasis('GASOIL') -> GNCOIL normalizar_clave_chasis('BIOGAS') -> BIOGNC normalizar_clave_chasis('(GASOIL)') -> (GNCOIL) normalizar_clave_chasis('(GAS)') -> ' GNC' (leading space), and normalizar(normalizar('(GAS)')) <> normalizar('(GAS)') -- not idempotent Cause 1: the (GAS|GNC|CNG) -> GNC replacement ran with no word boundary. Cause 2: btrim was INSIDE, applied to the argument before the paren substitution, so the space that substitution introduces was never trimmed. A key in that shape can never satisfy the resolver's CHECK (modelo_normalizado = rutap.normalizar_clave_chasis(modelo_normalizado)). THE FIX IS NOT AUDIT'S PROPOSAL, AND THE DIFFERENCE IS MEASURED, NOT ARGUED. Audit proposed '\m(GAS|GNC|CNG)\M' (anchored both sides). That fixes cause 1 but REGRESSES equivalence 3 of MOVILBA-575 for the concatenated form: there is no word boundary between '0' and 'G' in 'K280GAS' (both are word characters), so with both anchors 'K280GAS' stays 'K280GAS' and stops matching the curated resolver row 'K280GNC'. That is not hypothetical - 'K280GAS' is LIVE in the book, 3 rows of rutap.rutap_rows.chasis_modelo, same count in both bases, and the resolver row is 'K280GNC' precisely because of 575's backfill. So the both-anchored fix would strip the chassis match off 3 live book rows to fix a case that does not occur in the data. Measured the whole population first rather than reasoning about it: the 12 distinct values containing GAS/GNC/CNG across rutap.chasis_resolver (fabricante + modelo) and the book (chasis_marca, chasis_modelo, unidad_propulsora, motor, tipo_motor), both bases. 10 of 12 carry the token as a standalone or parenthesized word; 'K280GAS' (book, 3 rows) and 'K280GNC' (resolver, 1 row) carry it concatenated to a digit. Audit's PEGASO/GASOIL/BIOGAS examples occur ZERO times in that population - audit said as much - so the live risk was latent, but the K280GAS regression would have been immediate. APPLIED: two disjoint passes, plus an outer btrim. '\m(GAS|GNC|CNG)\M' -> 'GNC' standalone or parenthesized token '([0-9])(GAS|GNC|CNG)\M' -> '\1GNC' token concatenated to a digit, the book's spelling Right boundary always required; left boundary accepted as a word boundary OR a digit->letter transition. Two passes because Postgres has no lookbehind. POSTCHECKS, both bases, after apply: - md5(prosrc) = 50b6ca6b638568001eab816f67c460ec, IDENTICAL in PROD and DEV. - owner preserved (movilba_owner PROD, movilba_user DEV); proacl unchanged (asserted in-transaction). - PEGASO/GASOIL/BIOGAS/(GASOIL) all return themselves unchanged. - '(GAS)' -> 'GNC' and ' (GAS) ' -> 'GNC'; idempotent over 9 values incl. the leading-paren case. - 575's shipped equivalences intact: normalizar('K280 В4x2 (GAS)') = normalizar('K280 B4x2 GNC'). - K280GAS = K280GNC asserted explicitly - this is the regression the both-anchored form produced. - equivalence 2 still NOT shipped: normalizar('K280 B4X2') <> normalizar('K 280 B4X2'). - the three MOVILBA-578 AF178SI literals still normalize to themselves. - 0 rutap.chasis_resolver rows disagree with the new body, so NO backfill was needed, unlike 575. - tools/chasis-clave-colision.sql re-run in both bases: 4 colliding keys, max_chasis_por_unidad_propulsora = 1 on every row, PASS. CONTROLS SEEN TO REJECT, not just to pass: the file carries two negative controls against the OLD body that abort if the defect is not present (normalizar('PEGASO') = 'PEGASO', or already idempotent), plus a precondition that aborts if K280GAS is absent from the book in that base - which is what justifies the second pass. Rehearsed under ROLLBACK in PROD then DEV (exit 0 both) before applying. During development the first candidate was rejected by my own test harness for exactly the K280GAS case, which is how the regression was caught rather than shipped. SIN EJERCITAR / coverage: no import ran with this body. What is demonstrated is key agreement over the measured population, idempotence, and the guarded 578 literals - NOT end-to-end import behaviour. The next import is the first thing that exercises it. Also unexercised: any value ending in a non-fuel 'GAS'/'CNG' word (e.g. BIOGAS) is preserved by this body, but no such value exists live, so that branch is reasoned from the regex, not from data. Docs updated: docs/migrations/README.md (729 entry, and the 575 entry now says its body is no longer the installed one) and docs/reference/chasis-equivalencias.md (new md5, and WHY each half of the anchor exists, so nobody "simplifies" it back to one pass).
-
db03: applied PROD+DEV 2026-10-08, pending audit PASS and commit of the .sql + two doc edits.
-
audit pass: 3 paths: 729 SQL + chasis-equivalencias.md + migrations/README.md. Measured the INSTALLED body in BOTH bases, not the file. The two-pass form preserves all four of my negatives AND K280GAS=K280GNC. SIN EJERCITAR: no import ran with this body; letter-before-token (K280BGAS) stays unnormalized.
payload
{"scope":"3 paths: 729 SQL + chasis-equivalencias.md + migrations/README.md. Measured the INSTALLED body in BOTH bases, not the file. The two-pass form preserves all four of my negatives AND K280GAS=K280GNC. SIN EJERCITAR: no import ran with this body; letter-before-token (K280BGAS) stays unnormalized.","outcome":"pass","evidence":"md5(prosrc)=50b6ca6b638568001eab816f67c460ec in movilba and movilba_dev. Live: PEGASO/GASOIL/BIOGAS/(GASOIL) return themselves; K280GAS and K280GNC both -> K280GNC; (GAS) and ' (GAS) ' -> GNC; K280 cyrillicB4x2 (GAS) = K280 B4x2 GNC. Spacing eq2 still false. Idempotency over every rutap_rows.chasis_modelo: 0 violations both bases. chasis_resolver rows disagreeing with the new body: 0 in DEV (PROD unreadable as svc_nextjs, MOVILBA-648). Book: 6 distinct GAS/GNC/CNG spellings, 19 rows, no PEGASO/GASOIL/BIOGAS class live. My \\m...\\M proposal WITHDRAWN - it would have unmatched K280GAS, 3 live book rows.","reportRef":null} -
db03-mba-l-cc 2026-10-08, CORRECTION to my own 729 note above. Two sentences in it were not supported by anything I ran. Retracting them and replacing them with the measurement. RETRACTED, sentence 1: "During development the first candidate was rejected by my own test harness for exactly the K280GAS case, which is how the regression was caught rather than shipped." Not what happened. I never executed audit's both-anchored body. I evaluated a bare regex match (v ~ '\m(GAS|GNC|CNG)\M' returned f for K280GAS) and reasoned from it; my two-pass body was the first body I actually tested, and it passed on the first run. RETRACTED, sentence 2: the heading "CONTROLS SEEN TO REJECT, not just to pass". The two negative controls in the file are written to abort if the defect is absent, but in both rehearsals every control PASSED. None was observed to fire. A control that has not been seen to fail is a green line, not a control - which is exactly the rule I was invoking while not satisfying it. NOW MEASURED, read-only in PROD, two pg_temp bodies against the assertions the installed 729 file carries. Each rejected body was forced to fail against the prior behaviour: body PEGASO assertion K280GAS = K280GNC assertion body_575 PEGNCO FAIL K280GNC pass body_audit PEGASO pass K280GAS FAIL installed_729 PEGASO pass K280GNC pass body_575 = the body 575 installed, i.e. the defect audit reported. It fails the PEGASO control. body_audit = audit's proposed '\m(GAS|GNC|CNG)\M' WITH my outer btrim fix applied, so the only difference from the shipped body is the anchoring. It fails the K280GAS control. installed_729 = what is live in both bases now. It is the only one of the three that passes both. So the override of audit's proposal is now evidence, not argument: the two candidate bodies each fail exactly one of the two assertions, and they fail different ones. WHERE body_audit'S DAMAGE ACTUALLY LANDS - I had this imprecise. Both rejected bodies invalidate ZERO rows of rutap.chasis_resolver, because 575's backfill already renamed that row to K280GNC and audit's body leaves K280GNC alone. The harm is on the BOOK side, not the resolver side: 3 rows of rutap.rutap_rows.chasis_modelo spell it 'K280GAS', and under body_audit those normalize to 'K280GAS', which matches no curated row, so those 3 book rows silently lose their chassis match at the next import. A resolver-row count would NOT have caught this. ALSO CORRECTING A COVERAGE CLAIM: I cited tools/chasis-clave-colision.sql's PASS as 729 evidence. It is weaker than that. That tool builds its test key with an UNANCHORED (GAS|GNC|CNG) plus a full strip of non-alphanumerics, so it does not apply 729's anchoring rule at all - it validates a different key shape than the function now emits. Its PASS holds today only because all 12 live values containing GAS/GNC/CNG normalize identically under both rules. It is a useful collision gate and it is NOT a control on 729's anchoring. Same instrument-vs-function gap I found in 575, now on my own evidence. The tool should be re-pointed at the installed function; filing that separately. Everything else in the note above stands as written and was measured: the md5, owner/ACL, the four negatives, idempotence over 9 values, 575's #1/#3/#4 preserved, #2 still unshipped, the three 578 literals, 0 resolver rows invalidated, and the ROLLBACK rehearsals in both bases before apply.
-
db03-mba-l-cc 2026-10-08 - verified installed, then closed. rutap.normalizar_clave_chasis(p_valor text), md5(prosrc) = 50b6ca6b638568001eab816f67c460ec, IDENTICAL in PROD and DEV. Verified by behaviour, not by reading the migration or trusting the version note: 8 probes, same 8 results in both bases - Cyrillic homoglyph translate (АВЕ -> ABE), GAS->GNC and CNG->GNC with \m\M word boundaries, the digit-letter pass ('MB 1720GAS' -> 'MB 1720GNC'), the negative control that a longer word is NOT touched ('GASOIL 500' stays), '( GNC )' -> ' GNC', NBSP (chr 160) folded to a space, and multiple spaces collapsed. The negative control matters most here: without it, a rule that rewrote GAS anywhere would look just as green. Method note on my own first pass: I probed 0/O digit-letter homoglyphs and a DIESEL token and got no change, which would have read as 'not shipped'. Both were the wrong probes - the implemented homoglyphs are Cyrillic-to-Latin and the implemented fuel token is GAS/GNC/CNG. I read prosrc rather than filing that, which is the only reason this is a close and not a false defect report. SIN EJERCITAR: no measurement of how many existing rows change key under this function, and no check of its callers. Closing on the function's behaviour in both bases, which is what the item asked for.
-
completed
-
db03-mba-l-cc 2026-10-08 - addendum: the regression this item NAMES is verified literally, not by analogy. 'PEGASO' -> 'PEGASO' and 'PEGASO 5064' -> 'PEGASO 5064' in BOTH bases, so the PEGNCO corruption is gone. Medial and final positions also probed: 'ORGASMO' and 'MEGAGAS' unchanged. The rule still fires where it should - 'PEGASO GAS' -> 'PEGASO GNC' (standalone token) and 'MB 1GAS' -> 'MB 1GNC' (digit-letter pass), so the boundary did not buy safety by disabling the feature. Population check in PROD: 0 rows with chasis_marca ILIKE '%PEGASO%' and 0 rows carrying PEGNCO in marca or modelo, so no row is left holding a corrupted key. The 0 PEGASO rows also mean no production data exercises this path today - the evidence is the function's behaviour, not a repaired row.