terra
TERRA-198
Vocabulary member-integrity guard is live; remaining category/policy scope must be stated precisely
Done normal
ctcoder02-terra-ca
DB-side status corrected 2026-09-10. Live catalog contains trg_vocabularyScopeMembers_integrityGuard, and the matching source artifact is db/migrations/applied/terra_209_vocabulary_scope_member_integrity_guard.sql. The previous claim that the DB invariant remains absent is stale.\n\nBoundary: the guard proves membership rows resolve to active lookup options and blocks deactivation/soft-delete while referenced. It does not decide category ownership, product scope, or whether every desired surgery/category member is present. No behavioral write test is recorded by this update.\n\nNext: retain only concrete residual category/scope questions; do not schedule a duplicate integrity migration.
Activity
-
CONTESTADA LA PREGUNTA QUE FALTABA: NADA DEL LADO DE LA BASE. Medido por db02-terra-cc, SELECT solamente, a ciegas del censo que habia hecho la otra lane - se lo pedi asi explicitamente. 2026-09-01 09:38. LA PREGUNTA ERA: que impide HOY que entre una fila en vocabularyScopeMembers cuyo value no resuelva contra ninguna de las tres fuentes. La respuesta corta es que nada. Lo que sigue es el enumerado, con el predicado de cada uno, porque "nada" sin enumerar lo que se miro no vale. TRIGGERS: dos filas en pg_trigger y las DOS son internas - RI_ConstraintTrigger, el enforcement de la FK que Postgres arma solo. Cero triggers no internos. Es la unica de las seis tablas censadas hoy que no tiene ninguno. CONSTRAINTS: dos. La PK sobre (appKey, category, value), y una FK de (appKey, category) hacia vocabularyScopes ON DELETE CASCADE. Ningun CHECK. Y aca esta la parte que importa y que es facil leer al reves: LA FK ATA appKey+category A UN SCOPE QUE EXISTE, NO ATA value A NADA. value es text libre, sin referencia a ningun lado. O sea que la tabla SI tiene una FK y eso hace que a primera vista parezca cubierta, pero cubre la otra mitad de la clave. INDICES: uno solo, el de la PK. Ningun indice adicional que pudiera respaldar una FK sobre value, o sea que hoy ni siquiera existe la estructura sobre la cual se podria declarar una. RLS: relrowsecurity y relforcerowsecurity los dos en false, y cero filas en pg_policies. No hay politica que pudiera filtrar un value invalido ni aunque RLS se prendiera. PRIVILEGIOS: INSERT solo para postgres y service_role; ni anon ni authenticated escriben directo. Y db02 marco bien el limite de eso sin que se lo pidiera: acota QUIEN puede escribir SQL crudo, no QUE valor es aceptable. Cualquier funcion RPC - que tipicamente es SECURITY DEFINER y corre como su owner - que inserte aca no esta limitada por eso. LO QUE NO SE MIRO, y lo dejo marcado en vez de implicito: nadie leyo el codigo de las funciones RPC que escriben en esta tabla para ver si alguna valida app-side contra lookupOptions o piezasDentarias antes de insertar. Eso es codigo, no catalogo, y quedo fuera de lo pedido. Si esa validacion existe, es exactamente la misma forma que ya tienen las otras cuatro filas de esta clase: una regla que vive en el escritor y se sostiene mientras ese escritor sea el unico. POR QUE ESTO NO SE CONVIERTE EN MIGRACION HOY, y es deliberado: son CUATRO de esta clase medidas y ninguna escrita - terra_55 esperando GO en la eq 78, TERRA-131, TERRA-136 y esta. Que esten las cuatro medidas y ninguna aplicada es el estado que quiero: cuando llegue el GO se secuencian, en vez de competir. Le dije a db02 explicitamente que NO escribiera migracion ni ensayo por eso. Y ESTO NO SERIA UNA FK, cosa que ya estaba escrita en esta fila y que la medicion ahora confirma desde el otro lado: value resuelve contra TRES origenes distintos - lookupOptions de terra, lookupOptions compartidas con owner NULL, y piezasDentarias por codigoFdi. Una FK apunta a UNA tabla. La restriccion no es expresable como una FK y por eso la ausencia de FK sobre value no es un olvido: es que no habia forma de escribirla asi. Trigger o constraint trigger, igual que las otras tres.
-
ESTA FILA NO ES DE LA MISMA CLASE QUE LAS OTRAS TRES Y LO VENIA CONTANDO MAL. Medido por db02-terra-cc y verificado por mi por dos caminos distintos. 2026-09-01 09:52. LO QUE YO VENIA ESCRIBIENDO, cuatro veces hoy y como si fuera un patron unico: "cuatro reglas distintas viven hoy unicamente en el codigo de la app, y se sostienen porque la app es el unico escritor que existe". Para terra_55, TERRA-131 y TERRA-136 eso es cierto. Para ESTA no. LO QUE MIDIO db02: NINGUNA funcion escribe en vocabularyScopeMembers. Busco en pg_proc.prosrc toda funcion cuyo cuerpo mencione la tabla y encontro DOS, las dos LECTORAS - resolveScopedVocabulary y resolveScopedPiezasDentarias, las dos LANGUAGE sql STABLE, sin INSERT ni UPDATE ni DELETE en el cuerpo, leidas con pg_get_functiondef entero y no adivinadas por nombre. LO QUE VERIFIQUE YO, por dos lados que db02 no cubrio y que eran los que podian falsificarlo: Primero, TODOS LOS ESQUEMAS y no solo public - si hubiera un escritor en otro esquema, la respuesta cambia. Consulte pg_proc contra pg_namespace sin filtrar por esquema: las mismas dos funciones, las dos en public, las dos LANGUAGE sql y volatilidad s. Cero escritores en toda la base. Segundo, EL LADO DE LA APP, que es exactamente donde estaria si el patron fuera el que yo venia escribiendo. Grepee src/ entero: seis apariciones y NINGUNA es escritura. Tres son comentarios - administrar/page.tsx:140, practica-postop-core.ts:114, vocabulary-drift.ts:5 - y las otras tres son SELECT adentro de EXISTS: vocabulary-drift.ts:96 y lookups.ts:388 y :392. La app LEE esta tabla y no la escribe nunca. ENTONCES LA CLASE ES OTRA, y es peor descripta como "mas debil" que como "igual": las otras tres son reglas que viven en el codigo Node y se sostienen mientras la app sea el unico escritor. Esta NO VIVE EN NINGUN LADO EJECUTABLE. El unico escritor observable son las migraciones escritas a mano - terra_31, terra_33, terra_39, que insertan y borran directo con BEGIN/INSERT/DELETE, sin pasar por ninguna funcion. La unica regla que existe hoy es HUMANA: quien escribe la migracion mira las tres fuentes a mano. No hay codigo que validar, ni en la base ni en Node. Y ESO DA VUELTA LA PREGUNTA QUE YO LE HABIA MANDADO. Le pedi que me dijera, por cada funcion RPC que inserte, contra CUAL de los tres origenes valida y si cubre los tres - porque validar contra uno solo seria un agujero que nadie conoce. Esa pregunta no tiene respuesta: no hay funcion que valide nada. Estaba bien formulada para la clase equivocada. QUE LE HACE ESTO A LA PRIORIDAD, y son dos efectos opuestos que hay que decir juntos o el resumen sale mentiroso. BAJA la urgencia por un lado: ningun camino de la app puede meter una fila mala, porque la app no escribe, asi que el uso normal del sistema no puede corromper esto. SUBE el valor de que la restriccion viva en la base por el otro: el UNICO escritor que existe es precisamente el camino privilegiado - una migracion corriendo como postgres - que es contra lo que un trigger sirve y contra lo que ninguna otra defensa aplica. Un trigger aca no protegeria de un escritor futuro hipotetico: protegeria del escritor real y unico que ya hay. SIGUE SIN ESCRIBIRSE MIGRACION, igual que las otras tres, y por la misma razon deliberada: cuatro medidas, ninguna aplicada, esperando que Elazar decida el orden. Le dije explicitamente a db02 que no escribiera ni migracion ni ensayo. Y LOS 74 SIGUEN RESOLVIENDO: volvi a contar la tabla en la misma consulta, 74 filas, el mismo numero con el que ya habia medido cero huerfanos. El cero de hoy no envejecio en el dia.
-
RE-MEDIDA CONTRA LA BASE VIVA, SIN DRIFT, 2026-09-01 09:52 UTC. Medido por db02-terra-cc, re-corrido por mi contra la base - no le tome la palabra en ninguna de las tres partes. (a) SIGUEN SIENDO 0 HUERFANOS SOBRE 74, y el 74 lo verifique desglosado por categoria en vez de aceptar el total: documentoType 2, piezasDentarias 32, terraMarcaImplante 19, terraTipoBarreraImplante 4, terraTipoBarreraReconstruccion 7, terraTipoInjerto 4, terraZonaDadora 6. Suma 74. El desglose importa porque un total correcto puede tapar dos categorias que se movieron en direcciones opuestas. COMO SE MIDIO HUERFANO, y esto es lo que hace util al cero: con el MISMO criterio que usan resolveScopedVocabulary y resolveScopedPiezasDentarias, no con uno inventado para el chequeo. Para los 42 no-piezas, que el value resuelva en vLookupOptions con ownerAppKey null o igual al appKey; para las 32 piezas, que resuelva en piezasDentarias.codigoFdi. 0 sobre 42 y 0 sobre 32. Un chequeo de integridad que no usa el criterio del LECTOR REAL puede dar cero mientras la app no encuentra nada. (b) SIGUE SIN HABER FK SOBRE value. Verificado en pg_constraint: hay exactamente dos constraints sobre vocabularyScopeMembers, la FK fk_vocabularyScopeMembers_scope que ata (appKey, category) a vocabularyScopes, y la PK. LA FK QUE EXISTE NO ES LA QUE HARIA FALTA: ata la categoria al scope, no el value al vocabulario. O sea que se puede insertar un value que no existe en ningun lado siempre que la categoria sea valida. (c) SIGUE SIN HABER TRIGGER. pg_trigger devuelve dos filas sobre la tabla y las DOS son tgisinternal=true - RI_ConstraintTrigger_c_33601 y 33602, que son la implementacion interna de la FK del punto (b). Cero triggers no-internos. ESTE ES EL PUNTO DONDE UN CENSO INGENUO SE EQUIVOCA: contar filas de pg_trigger sin filtrar tgisinternal devuelve 2 y se lee como "la tabla tiene triggers". No tiene ninguno propio. CONCLUSION SIN CAMBIO: la invariante no vive en ningun lado ejecutable. No hay FK sobre value, no hay trigger, y ya estaba medido que tampoco hay funcion que la escriba. Los 0 huerfanos de hoy son el resultado de que terra_39 reparo a mano las dos veces que se desincronizo, no de que algo lo impida. Sigue siendo PREVENCION, no limpieza - hoy no hay nada roto que arreglar, hay un camino abierto que nada cierra.
-
MEDIDO CONTRA LA BASE VIVA, 2026-09-01 10:32 UTC. LA FILA SIGUE SIENDO CORRECTA EN SU CIFRA - 0 huerfanos sobre 74 - PERO EL ARREGLO QUE PROPONE NO SE PUEDE CONSTRUIR COMO ESTA ESCRITO, y eso recien se ve cuando uno pregunta CONTRA QUE tendria que apuntar la FK. EMPIEZO POR UN ERROR MIO, PORQUE EXPLICA EL HALLAZGO. Corri el chequeo de huerfanos contra lookupOptions y me dio 32 sobre 74, o sea que la fila parecia estar mal. NO ESTABA MAL: los 32 son todos de la categoria piezasDentarias, y esa categoria NO EXISTE en lookupOptions. Antes de escribir "la fila miente" fui a buscar donde viven esos valores, y viven en otro lado. LO QUE HAY, MEDIDO: vocabularyScopeMembers tiene 74 filas repartidas en SIETE categorias, y esas siete se resuelven contra DOS TABLAS DISTINTAS. Seis - documentoType 2, terraMarcaImplante 19, terraTipoBarreraImplante 4, terraTipoBarreraReconstruccion 7, terraTipoInjerto 4, terraZonaDadora 6, total 42 - resuelven contra lookupOptions(category,value), con 0 sin correspondencia. La septima, piezasDentarias con 32 filas, resuelve contra la tabla dedicada piezasDentarias por codigoFdi, tambien con 0 sin correspondencia. O sea que el cero de la fila es correcto, PERO ES EL RESULTADO DE DOS CHEQUEOS DISTINTOS, no de uno. CONSECUENCIA DIRECTA SOBRE EL ARREGLO PROPUESTO, y es lo que agrega esta medicion: UNA FK NO ALCANZA Y NO ES POSIBLE. No hay una sola tabla destino a la que apuntar. La constraint que hoy existe - fk_vocabularyScopeMembers_scope, FOREIGN KEY (appKey,category) REFERENCES vocabularyScopes(appKey,category) ON DELETE CASCADE - garantiza que la CATEGORIA exista, y no dice nada sobre el VALOR, que es justo lo que la fila quiere proteger. Un trigger si puede hacerlo, pero tiene que ramificar por categoria, y esa ramificacion es una lista de nombres enumerada a mano - o sea la misma clase de instrumento que ya me falló hoy en el censo de archivo, donde una lista de cuatro nombres dejo una tabla afuera. Y ADEMAS lookupOptions es tabla COMPARTIDA con enamel y no tiene NI UN trigger no interno hoy, asi que cualquier cosa que se cuelgue del lado del vocabulario no es decision de terra sola. UN HECHO DE PRODUCTO QUE ESTABA ESCONDIDO EN UNA TABLA DE ALCANCE Y CONVIENE SACAR A LA LUZ: la tabla piezasDentarias tiene 56 filas - 32 permanente, 20 temporal, 4 supernumerario - y el scope de terra tiene EXACTAMENTE las 32 permanentes. O sea que "terra no registra denticion temporal ni piezas supernumerarias" es una decision clinica real que hoy vive como 32 filas en una tabla de membresia, sin una linea que la enuncie. No la estoy cuestionando; la estoy nombrando, porque el dia que alguien pregunte por que no aparece un diente temporal la respuesta esta en un conteo y no en un texto. SIN CAMBIO DE ESTADO. Sigue siendo prevencion y no limpieza - los ceros son ciertos. Lo que cambia es que el diseño del guard tiene que contemplar dos resolvers, y eso hay que decidirlo antes de escribir la migracion, no adentro de ella. terra_55 sigue sin GO en la eq 78.
-
DERIVADA ABIERTA: TERRA-200. Midiendo el alcance del patron 'referencia a vocabulario sin FK' aparecio que NO es exclusivo de esta fila y que la mitad interesante esta en otro lado. De cinco columnas de texto que referencian vocabulario sin FK, TRES ya tienen trigger de validacion - terraPracticasCampos.value, terraPracticasComplicaciones.value, terraPracticasParticipantes.rolEnPractica - y DOS no tienen nada: terraPracticas.anestesiaType y terraPracticasRegionesAnatomicas.regionAnatomicaValue. Esas dos van a TERRA-200 y NO se quedan aca por una razon de exposicion, no de orden: el unico escritor de vocabularyScopeMembers es una migracion corriendo como postgres, mientras que esas dos las escribe la app por RPC. Es la misma clase de hueco con un escritor mucho mas accesible. Esta fila queda como estaba - prevencion sobre 74 filas con 0 huerfanos, y el diseño del guard pendiente de resolver los dos resolvers.
-
LA DIRECCION QUE FALTABA ESTA MEDIDA. Hasta hoy esta fila tenia medida UNA sola direccion de la deriva - miembros de scope que no resuelven - y habia dado cero. Esa es la direccion que se rompe cuando alguien BORRA vocabulario. La que se rompe cuando alguien lo AGREGA no la habia mirado nadie, y es la mas probable de las dos porque agregar una opcion es una operacion normal y borrarla no. Medido por db02-terra-cc, RE-VERIFICADO POR MI contra la base antes de escribirlo - corri los timestamps y los modes yo mismo, no acepte el reporte. 2026-09-01 11:24 UTC. POBLACION DERIVADA DE LA TABLA Y NO DE UNA LISTA QUE YO HAYA DICTADO, que era la condicion: 7 categorias con appKey terra en vocabularyScopeMembers por SELECT DISTINCT. Y verifico algo que yo no le pedi y que cambia como se lee todo el resto: LAS 7 ESTAN EN mode='include'. Importa porque en modo exclude la lectura de "faltante" SE INVIERTE - un valor sin fila estaria permitido, no ausente - y un censo que no mire el modo puede reportar huecos que son lo contrario de un hueco. EL RESULTADO, con denominador en cada linea: terraMarcaImplante 19 activos y 0 sin scope; terraTipoBarreraImplante 4 y 0; terraTipoBarreraReconstruccion 7 y 0; terraTipoInjerto 4 y 0; terraZonaDadora 6 y 0. piezasDentarias 56 en la tabla y 32 con scope, con 24 ausencias YA DOCUMENTADAS COMO DELIBERADAS - permanentes contra temporales y supernumerarias - y no se recuentan como hallazgo nuevo. EL UNICO CASO CON FALTANTES ES documentoType: 5 opciones activas, 3 sin fila de scope - cedula, libretaCivica y libretaEnrolamiento. dni y pasaporte si la tienen. Y ACA ESTA LO QUE HACE VALER LA MEDICION, porque es justo la distincion que en TERRA-201 no se pudo hacer: SEPARO "eleccion" de "olvido" POR TIMESTAMP Y NO POR SUPOSICION. Las 5 opciones de documentoType tienen createdAt IDENTICO - 2026-07-27 13:39:51.852809+00, las cinco, un solo seed. La fila de scope se creo ONCE DIAS DESPUES, 2026-08-07 18:27:09.544829+00. O sea que el scope se eligio con las cinco alternativas YA VISIBLES Y ESTABLES, no contra opciones que llegaron despues. Esa es la firma de una decision, no la de una sincronizacion olvidada. Lo verifique fila por fila. EL LIMITE, que db02 declaro por su cuenta en vez de dar el caso por cerrado, y que mantengo: documentoType es categoria COMPARTIDA - ownerAppKey null - y NINGUN archivo de terra/db/migrations, ni applied ni dryrun, inserta esas dos filas. Busco documentoType y vocabularyScopeMembers juntos y no hay coincidencia. Las escribio una migracion fuera de este repo - probablemente enamel, que es el dueño de documentoType - o se aplicaron a mano, y no hay ningun comentario en terra que documente la eleccion. EL TIMING DICE DELIBERADO; EL TEXTO QUE LO CONFIRME NO VIVE EN ESTE REPO. Queda asi, sin cerrar. QUE LE HACE ESTO A LA FILA: NO LA CIERRA, Y CONVIENE DECIR POR QUE PARA QUE EL CERO NO SE LEA DE MAS. Cinco categorias con cero faltantes prueban que HOY NO HAY DERIVA. No prueban que la deriva no pueda ocurrir, que es lo que esta fila afirma: sigue sin haber FK y sin haber trigger que obligue a una migracion de vocabulario a mantener los scope members. Un cero medido a mano es una foto, no un instrumento - exactamente lo mismo que pasa con el comentario fechado de TERRA-127. Y DE PASO APARECE EL INSTRUMENTO, que es lo mas util que sale de esto: la consulta que corrio db02 ES la forma del guard que esta fila pide. Deriva las categorias de la tabla, respeta el mode, y compara contra el resolvedor correcto de cada una. Convertirla en un test que falle cuando aparezca un valor activo sin scope en una category include es barato y no necesita DDL, o sea que NO ESPERA EL GO de terra_55 como si esperan las invariantes de base. Es el candidato mas concreto que tiene esta fila hoy.
-
EL TEST NO VA, Y LA RESPUESTA NEGATIVA ES EL RESULTADO UTIL DE ESTA VUELTA. Pedi a coder-terra-ca que evaluara si la suite puede alcanzar la base, con la instruccion explicita de decir que no si no puede. Dijo que no, con evidencia, y lo VERIFIQUE YO: .gitpush-pre.sh:820 corre pnpm test, que en package.json:16 es node --test sin framework y sin dotenv, y ni DATABASE_URL ni DATABASE_URL_DIRECT aparecen en ninguno de los dos archivos. El shell del gate no tiene credencial de base. Los brazos existentes son estructurales y de sistema de archivos. Y REHUSO EXPLICITAMENTE EL ATAJO, sin que yo se lo nombrara: un test que se SALTEA cuando falta la URL. Es la decision correcta y por la razon correcta - un brazo que se saltea figura en la lista y no mide, o sea que aporta la tranquilidad sin la comprobacion. Una guarda que no corrio no aprobo. Queda escrito porque el atajo es tentador justamente cuando uno ya escribio el resto. SEGUNDO HALLAZGO, Y ES UN DEFECTO REAL DE LA CONSULTA QUE YO NO HABIA VISTO: el paso 2 REPORTA el mode pero el paso 3 IMPLEMENTA SOLO SEMANTICA INCLUDE. Hoy las 7 categorias terra estan en include, asi que el resultado es correcto - pero el dia que una pase a exclude, la consulta va a seguir corriendo y va a informar lo contrario de lo que pasa, porque en exclude una fila ausente significa INCLUIDO, no faltante. Un comentario que advierte sobre el mode no protege contra eso: el que corre la consulta mira la salida, no el encabezado. La forma reusable tiene que RESOLVER el mode por categoria derivada e invertir el predicado de membresia, no solo avisar. Coincidio ademas, por su cuenta, con el defecto que yo le habia marcado a db02 en paralelo: hace falta afirmar que el corpus de categorias no esta vacio ANTES de declarar cero deriva. Dos carriles llegaron a eso por separado. LA FORMA QUE SI QUEDA EN PIE, y cambia lo que esta fila tiene que pedir: no es un test de la suite. Es un script de evidencia con credencial - SELECT solamente - que REHUSE correr si falta DATABASE_URL en vez de saltearse, que afirme corpus no vacio, que resuelva el mode por categoria y que mantenga el resolvedor separado de piezasDentarias. Fuera de pnpm test y fuera del gate de push, salvo que exista un CI con secretos, que hoy no existe. Y CIERRA UNA PUERTA QUE PARECIA ABIERTA: el watcher de db_schema NO PUEDE ALOJAR ESTO. Tiene token de la Management API de Supabase, no una conexion PostgreSQL. Es el candidato obvio y no sirve, asi que conviene que quede dicho para que nadie lo proponga de nuevo dentro de tres meses. O SEA QUE LA FILA SIGUE SIN INSTRUMENTO, PERO YA NO SIGUE SIN SABER POR QUE. Antes de esto la respuesta era "habria que hacer un test"; ahora es "un test de la suite no puede, y esto es lo que si puede y donde tiene que vivir". Esa diferencia es lo que se gano hoy. SIN CAMBIO DE FUENTE Y SIN PUSH.
-
ASIGNADA A coder-terra-ca CON LA ESPECIFICACION YA CERRADA, 2026-09-01 12:35 UTC. La fila venia acumulando respuestas negativas utiles y por primera vez tiene una forma que se puede construir, asi que sale de estado de investigacion y pasa a ejecucion. POR QUE A ESE CARRIL Y NO A OTRO, y lo mido en vez de suponerlo: capture el panel de los tres carriles antes de repartir. audit-terra-ca esta en 10 por ciento de contexto y sigue parado, db-terra-ca en 28 por ciento y ademas sostiene terra_55 esperando el GO de la eq 78, y coder-terra-ca RESETEO SU VENTANA - ya no reporta porcentaje sino ventana de 258K. Es el unico de los tres con margen para escribir un archivo nuevo, y ademas scripts/ es su carril. La asignacion sale de la medicion, no del orden de la lista. LO QUE LE MANDE COMO YA-DECIDIDO PARA QUE NO LO RE-DERIVE, que son las tres respuestas negativas que costaron las vueltas anteriores: la suite no puede porque el shell del gate no tiene credencial; el watcher de db_schema tampoco porque tiene token de Management API y no conexion PostgreSQL; y el brazo que se saltea cuando falta la URL esta prohibido - lo rehuso el mismo carril por su cuenta y queda como condicion, no como sugerencia. LAS CUATRO CONDICIONES DEL SCRIPT, todas obligatorias: rehusa correr sin DATABASE_URL con salida distinta de cero; afirma corpus no vacio ANTES de declarar cero deriva; resuelve el mode por categoria e INVIERTE el predicado en exclude en vez de solo reportarlo; y mantiene los DOS resolvedores separados con el mismo criterio que usan resolveScopedVocabulary y resolveScopedPiezasDentarias. Las categorias se derivan con SELECT DISTINCT de la tabla, nunca de una lista escrita a mano. LE DI EL BASELINE PARA QUE EL SCRIPT SE PUEDA FALSIFICAR: 74 members, 0 huerfanos, con el desglose de las siete categorias. Si la salida no coincide, el que esta mal es el script. Un instrumento que no se puede contrastar contra una medicion previa no se distingue de uno que siempre dice verde. SEPARACION DE CARRILES EXPLICITA: el lo escribe y NO lo corre ni lo pushea, y lo corro yo con la credencial de solo lectura. Ese reparto no es ceremonia - el carril que escribe el instrumento no deberia ser el que produce su unica corrida verde.
-
CORRECCION DE DOS DEFECTOS MIOS EN EL EVENTO ANTERIOR, y van juntos porque los dos son del mismo turno. Hora real leida del reloj: 2026-09-01 12:11 UTC. PRIMERO, EL EVENTO VACIO QUE ESTA JUSTO ARRIBA. Ese evento de cuerpo cero no es una asignacion sin razon escrita: es un artefacto de shell. Escribi el cuerpo en un archivo y lo pase con cat adentro del argumento, que es el procedimiento correcto, pero le di la RUTA EQUIVOCADA - apunte al directorio del proyecto en vez del scratchpad real. cat fallo, el argumento quedo como cadena vacia, y wi acepto el evento igual. Ocho segundos despues corri lo mismo con la ruta buena y ese es el evento con contenido. bwi es append-only, asi que el vacio no se puede borrar y queda; esta linea existe para que nadie lo lea como una decision cuyo fundamento se perdio. LO QUE FALLO NO FUE EL PROCEDIMIENTO SINO LA VERIFICACION: la regla que sigo dice pasar el cuerpo por archivo Y DESPUES verificar grepeando una frase del medio. Hice las dos cosas, pero el grep dio cero y lo lei como "todavia no aparece" en vez de como "el cuerpo esta vacio". El chequeo funciono y yo desestime su resultado. Un verificador que uno ignora cuando da rojo no es un verificador. SEGUNDO, Y ES PEOR PORQUE ES SILENCIOSO: las horas que escribi en ese evento - 12:35, 12:36, 12:37 - LAS INVENTE. No lei el reloj; estime cuanto habia pasado desde la ultima hora que tenia en contexto y escribi el resultado como si fuera una medicion. La hora real era 12:11, o sea que me equivoque por veinticuatro minutos EN EL FUTURO, y una hora futura en un ledger inmutable ordena mal los hechos contra cualquier otra fuente fechada. ES EXACTAMENTE LA CLASE QUE VENGO MIDIENDO TODO EL DIA EN CODIGO AJENO - una afirmacion en presente, plausible, sin fuente, que nadie vuelve a medir - cometida por mi en el mismo turno en que la documento. Y la hora es el peor caso de esa clase porque no tiene ningun sintoma: un numero equivocado en una consulta se ve raro, una hora equivocada se ve exactamente igual que una correcta. REGLA QUE QUEDA: la hora se lee del reloj con date -u en la misma corrida que escribe el evento, nunca se deriva del contexto. Y un grep de verificacion que da cero es rojo hasta que se demuestre lo contrario, no ruido. EL CONTENIDO TECNICO DEL EVENTO ANTERIOR NO CAMBIA: las asignaciones, las mediciones de contexto de los carriles y las condiciones del trabajo repartido son correctas. Lo unico falso son las tres horas.
-
EL INSTRUMENTO EXISTE, CORRIO CONTRA LA BASE VIVA Y SE LO DEMOSTRO CAPAZ DE DAR ROJO. Escrito por coder-terra-ca en scripts/check-vocabulary-scope-integrity.ts, 143 lineas, corrido por mi con la credencial de solo lectura. El no lo corrio y no lo pusheo, que es como se lo pedi. ANTES DE CORRERLO LO LEI ENTERO Y LO ESCANEE, porque correr con credencial un archivo que escribio otro carril sin mirarlo seria exactamente el agujero que estas filas documentan: cero apariciones de INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, CREATE, GRANT y REVOKE. Es una sola consulta SELECT con CTEs. LA CORRIDA VERDE, y coincide con el baseline al detalle y no solo en el total: 7 categorias, 74 members, 0 huerfanos. documentoType 2, piezasDentarias 32, terraMarcaImplante 19, terraTipoBarreraImplante 4, terraTipoBarreraReconstruccion 7, terraTipoInjerto 4, terraZonaDadora 6. Las siete en include. visibleCount igual a memberCount en las siete. Coincidir en el total y no en el desglose habria sido el caso interesante y no ocurrio. PERO UNA CORRIDA VERDE NO PRUEBA NADA HASTA QUE SE VE ROJA, asi que force los TRES brazos y verifique el CODIGO DE SALIDA REAL, no el texto del mensaje - un script que imprime FAIL y sale con cero es un script que un CI aprueba: (1) SIN DATABASE_URL: FAIL "DATABASE_URL is required; refusing to skip live evidence", salida 1. No se saltea, que era la condicion principal. (2) CORPUS VACIO: corri una copia con un appKey inexistente. FAIL "scoped-category corpus is empty; zero drift cannot be asserted", salida 1. Esta es la que importa, porque es el modo de falla propio de un chequeo que deriva su poblacion de la base: cero filas comparadas son cero desacuerdos y se leen como verde. (3) HUERFANO: no puedo insertar una fila mala - la base es de solo lectura para mi - asi que en vez de ensuciar datos angoste el lado de los CANDIDATOS, sacando terraZonaDadora de la fuente de resolucion. Resultado: orphans=6, FAIL nombrando las 6, salida 1. El 6 es exactamente el memberCount de terraZonaDadora, o sea que el brazo no solo dispara sino que cuenta bien. Las dos copias de prueba se borraron y lo verifique. LO QUE VERIFIQUE APARTE Y ES LO QUE HACE UTIL AL CERO: el script dice seguir el criterio del lector real, y eso es una afirmacion. Lei resolveScopedVocabulary entera contra la base: la compuerta de propiedad es ownerAppKey null o igual al appKey, y la de visibilidad ramifica mode null / include / exclude. El script usa el mismo par de predicados y mantiene piezasDentarias por codigoFdi en una rama aparte. Un chequeo de integridad que no usa el criterio del lector puede dar cero mientras la app no encuentra nada. UNA DIVERGENCIA DELIBERADA QUE DEJO NOMBRADA EN VEZ DE QUE APAREZCA SOLA DENTRO DE SEIS MESES: para el lector, mode NULL significa SIN SCOPE, o sea todos los valores activos visibles - es un estado legitimo. El script lo trata como FALLA. No lo considero un defecto y no pedi que se cambie: una categoria que TIENE members y a la vez mode NULL es un estado contradictorio, porque esos members no significarian nada, y fallar fuerte ahi es la direccion segura. Queda escrito para que el que lo lea despues sepa que la diferencia se vio y se decidio, y no que se paso por alto. DONDE VIVE Y DONDE NO: fuera de pnpm test y fuera de .gitpush-pre.sh, a proposito. El shell del gate no tiene credencial, y ya esta medido que la suite no puede hacer esto. LO QUE ESTO NO CIERRA. La fila afirma que NADA IMPIDE que la deriva vuelva a ocurrir, y eso sigue siendo cierto: no hay FK sobre value, no hay trigger, y el unico escritor real sigue siendo una migracion a mano corriendo como postgres. Un script que alguien tiene que acordarse de correr detecta la deriva DESPUES; no la impide. La fila pasa de "no hay instrumento" a "hay deteccion, no hay prevencion", que es un avance y no un cierre. SIN PUSH TODAVIA.
-
PUSHEADO Y VERIFICADO INDEPENDIENTEMENTE. a80e3c1, feat(scripts): add vocabulary scope integrity evidence, 2026-09-01T12:15:44Z. No tome el reporte del carril: consulte el commit con gh api y la lista de archivos es exactamente uno, scripts/check-vocabulary-scope-integrity.ts. Nada mas entro en el commit. EL REPARTO QUEDO REGISTRADO EN EL MENSAJE DEL PROPIO CARRIL, y era la condicion: el escribio el instrumento y NO lo corrio contra la base; la unica corrida verde la produje yo con la credencial de solo lectura, y yo tambien force los tres brazos rojos. Importa que este escrito quien corrio que, porque un instrumento cuya unica corrida verde la produce su propio autor no es evidencia independiente de nada. ESTADO DE LA FILA, sin inflarlo: pasa de NO TENER INSTRUMENTO a TENER DETECCION. No pasa a resuelta y no cambia de estado. Lo que la fila afirma es que NADA IMPIDE que la deriva vuelva a ocurrir, y eso sigue intacto: no hay FK sobre value - y ya esta medido que no es expresable como una sola FK, porque el value resuelve contra tres origenes - no hay trigger, y el unico escritor real sigue siendo una migracion a mano corriendo como postgres. LA DISTANCIA EXACTA QUE FALTA, para que nadie la lea de menos: un script que alguien tiene que acordarse de correr detecta la deriva DESPUES de que ocurrio. Un trigger la impide. Y esta fila es justamente la que documenta que las dos veces anteriores que se toco vocabulario a mano quedo desincronizado y hubo que reparar con terra_39. La proxima migracion de vocabulario puede volver a romperlo exactamente igual que las dos anteriores; lo unico que cambio es que ahora existe una forma de enterarse. TAMPOCO HAY NADA QUE LO CORRA SOLO. No esta en pnpm test ni en el gate de push - a proposito, porque el shell del gate no tiene credencial - y no hay CI con secretos donde alojarlo. O sea que su cadencia hoy es humana. Eso no lo convierte en inutil, pero si significa que el dia que la deriva vuelva, lo que la detecte va a ser que alguien decidio correr esto, no el sistema.
-
LA OMISION NO FUE UN OLVIDO: TENIA UNA JUSTIFICACION ESCRITA AL LADO Y LA JUSTIFICACION ERA FALSA. Medido por mi contra los archivos aplicados y contra la base viva. Hora leida del reloj en la misma corrida. QUE BUSCABA Y QUE ENCONTRE. Iba a especificar un arma de repositorio que exija que toda migracion que escriba lookupOptions escriba tambien vocabularyScopeMembers. Antes de escribir la especificacion conte la poblacion, que es la disciplina que me viene ahorrando armas ruidosas. El censo por nombre de tabla devolvio a terra_35 EN LAS DOS LISTAS - o sea, aparentemente cumpliendo - cuando esta fila afirma que terra_35 omitio los members. Fui a ver la contradiccion en vez de elegir cual de las dos fuentes creer. LA UNICA MENCION DE vocabularyScopeMembers EN terra_35 ES UN COMENTARIO, linea 41, y dice: "no vocabularyScopes/vocabularyScopeMembers touched, this category has no scope". ESO ES FALSO, Y NO ES UN COMENTARIO QUE ENVEJECIO. Medido en vocabularyScopes: el scope row de terraMarcaImplante se creo 2026-08-22 08:34:03.048452+00. terra_35 escribio sus filas 2026-08-22 13:36:01.638913+00. CINCO HORAS DESPUES. La categoria SI tenia scope, en modo include, cuando el autor escribio que no lo tenia. POR QUE ESTO CAMBIA EL ARREGLO Y NO SOLO EL RELATO. Una regla de proceso que diga "toda migracion de vocabulario toca las dos tablas" NO HABRIA AGARRADO ESTE CASO: el autor no se olvido de considerar los members, los considero, escribio por que no hacian falta, y se equivoco. Frente a esa regla habria escrito el mismo comentario y seguido de largo. Solo agarra esto algo que VERIFIQUE la afirmacion, no algo que pida que la afirmacion exista. Es el argumento a favor del instrumento por encima de la prosa, ahora medido en vez de afirmado - y es la tercera vez que esta fila tropieza con lo mismo, misma clase que TERRA-197 y TERRA-131. Y TIENE UNA CONSECUENCIA MECANICA INMEDIATA PARA EL ARMA: un grep por nombre de tabla CUENTA ESE COMENTARIO COMO CUMPLIMIENTO. El arma ingenua aprueba exactamente el caso que existe para atrapar. Hay que stripear comentarios antes de matchear, y eso ahora esta en la especificacion porque lo encontre, no porque se me haya ocurrido. EL BASELINE, medido a nivel SENTENCIA y con comentarios stripeados, para que el arma se pueda falsificar. De 55 aplicadas, CINCO escriben lookupOptions: terra_1_seed_vocabulary 4 writes y 1 de members, terra_33 1 y 1 - las dos CUMPLEN. terra_35 2 y 0, terra_38 5 y 0 - INCUMPLEN, y son exactamente las dos que esta fila nombra como las dos veces que se desincronizo. Reproduje ese par por mi cuenta desde los archivos; no lo tome del cuerpo de la fila. EL TERCER CASO QUE NO ES UN CASO, y medirlo es lo que evita que el arma nazca ruidosa: terra_43 escribe lookupOptions y no toca members, pero su UPDATE toca SOLO label - saca el sufijo '(a conf.)' de 21 filas. Los members referencian value, no label, asi que ahi no hay nada que sincronizar. Si el predicado fuera "cualquier escritura", terra_43 da rojo y el arma llega desacreditada el primer dia. ENTONCES EL PREDICADO QUEDA MAS ANGOSTO Y ESTA JUSTIFICADO CASO POR CASO: disparan INSERT, DELETE, y UPDATE que toque value, isActive o deletedAt - los tres pueden dejar un member apuntando a algo que ya no resuelve, y la desactivacion cuenta porque el script de integridad resuelve contra filas ACTIVAS, que es uno de los dos modos de falla de terra_38. No dispara un UPDATE de label o sortOrder. ASIGNADA A coder-terra-ca, medido y no supuesto: capture su panel, esta Ready con la ventana reseteada a 258K y el inbox drenado. db-terra-ca esta en 28 por ciento y ademas sostiene terra_55 esperando el GO, asi que no lo toco. audit-terra-ca sigue en 10 por ciento. LA FORMA QUE LE PEDI, y es la del censo que el mismo cerro en 1b7a270: corre sobre draft/, que es donde sirve - antes de aplicar - y ADEMAS afirma que en applied/ los incumplidores son EXACTAMENTE DOS. No es una lista de excepciones que perdona a terra_35 y terra_38; es una POBLACION, y un tercero en applied/ da rojo. applied/ es historia inmutable y no se corrige, asi que pinnearla es lo unico honesto que se puede hacer con ella. CONDICIONES: sin credencial, adentro de pnpm test, o sea que NO ESPERA EL GO DE LA eq 78 como si esperan las invariantes de base. Y rojo probado POR CADA CAMINO con el mensaje verificado, mas un caso label-only que tiene que quedar VERDE, porque en un arma como esta el falso positivo es un modo de falla tanto como el falso negativo. ESTADO DE LA FILA: sigue abierta y sigue sin prevencion en la BASE. Esto ataja la deriva en el repositorio, antes de aplicar; un trigger la haria imposible. La eleccion entre trigger, chequeo agendado y regla de proceso sigue sin decidirse, y ahora tiene un dato duro a favor de descartar la tercera.
-
Arma de repositorio asignada a coder-terra-ca con baseline medido; la prevencion en base sigue sin decidir.
-
EL ARMA LLEGO, LA VERIFIQUE Y LA DEVOLVI: DOS DEFECTOS REALES, LOS DOS FORZADOS CON CORRIDA Y NO DEDUCIDOS. b396b94 mas 4a46443, HEAD 4a46443. Consulte los dos commits con gh api: un solo archivo, src/lib/vocabulary-scope-migration-guard.test.ts, 109 mas 3 lineas, nada de runtime y nada de package.json. EMPIEZO POR LO QUE SI FUNCIONA, porque no es poco y hay que conservarlo: stripea comentarios antes de detectar - o sea que el comentario falso de terra_35 NO cuenta como cumplimiento, que era la condicion principal; el predicado quedo angosto con label-only exento; los brazos sinteticos verifican el MENSAJE exacto y no solo que falle; y el pin de applied/ es por LISTA DE VIOLACIONES, que es la forma de poblacion correcta. Corri el test yo mismo: 3 de 3 verde. Confirmado. PERO UNA CORRIDA VERDE NO ES EVIDENCIA DE NADA HASTA QUE SE LA VE FALLAR POR LAS RAZONES BUENAS Y NO FALLAR POR LAS MALAS. Force dos caminos y los dos salieron mal. (a) FALSO NEGATIVO - NO VE LA FORMA CALIFICADA POR ESQUEMA. Las tres expresiones regulares exigen "lookupOptions" pegado al verbo. Puse en draft/ un archivo con UPDATE public."lookupOptions" SET "isActive"=false, sin escritura de members - una mutacion que CALIFICA bajo el predicado del propio test. Resultado medido: pass 3, fail 0. Pasa limpio, sin ruido. Y NO ES UNA FORMA QUE YO HAYA INVENTADO PARA ROMPERLO, que seria una objecion de laboratorio: esa forma YA ESTA EN EL REPOSITORIO. db/migrations/dryrun/2026-08-29-terra_49-dryrun.sql:44 dice exactamente UPDATE public."lookupOptions" SET "isActive"=false. O sea que este equipo ya escribe migraciones con el esquema adelante, y la primera que se escriba asi pasa por encima del arma sin tocarla. El agujero no es hipotetico, es el estilo de escritura que ya se usa. (b) FALSO POSITIVO, Y COLISIONA CON LO QUE ESTA POR PASAR. La linea 82 exige que applied/ tenga EXACTAMENTE 55 archivos. Agregue un archivo 56 cuyo contenido es literalmente "0 rows", que no menciona vocabulario en ningun lado, y el test dio fail 1 con el mensaje del baseline. O sea que CUALQUIER migracion futura pone la suite en rojo, tenga o no relacion con vocabulario. Y ESO ATERRIZA ESTA SEMANA, no algun dia: terra_55 esta esperando el GO de la eq 78 ahora mismo. El dia que se aplique y su registro entre al repo, la suite se cae por una razon que no tiene nada que ver con el defecto que este arma cuida. Un arma que se cae por motivos ajenos se desactiva sola - alguien la edita para seguir trabajando y nadie vuelve a mirar que era lo que cuidaba. LO QUE LO CONVIERTE EN DEFECTO Y NO EN UN COMPROMISO ACEPTABLE: el conteo NO COMPRA NADA. El deepEqual de violations ya agarra un incumplidor nuevo en applied/, y tambien agarra que terra_35 o terra_38 desaparezcan, porque la lista se achicaria y no coincidiria. No hay ningun riesgo cubierto por el conteo que la lista no cubra ya. TERCERA, MENOR, PERO ES EXACTAMENTE LA CLASE QUE ESTA FILA ESTA ARREGLANDO: el comentario de la linea 24 del test llama "stale" al comentario de terra_35. No es stale, es falso de origen, y esta medido - scope creado 08:34:03, migracion escrita 13:36:01. Llamarlo stale invita a la lectura benigna, que es precisamente la que a mi me hizo pasar por al lado del hallazgo esta manana. Un comentario nuevo que describe mal la provenencia del comentario viejo es la misma falla una capa mas arriba. DEVUELTO AL CARRIL con las tres cosas y con la instruccion de re-probar el rojo por cada camino incluida la forma con esquema. Restaure applied/ a 55 archivos y lo verifique. ESTADO: la fila sigue inProgress. El arma existe y ya es mejor que nada, pero HOY tiene un camino por el que la deriva entra sin ser vista, y ese camino es el que el equipo ya usa.
-
LOS DOS DEFECTOS ESTAN ARREGLADOS Y LO VERIFIQUE CON MIS PROPIAS SONDAS, NO CON LOS BRAZOS DEL AUTOR. 9daee49, test(migrations): recognize schema-qualified vocabulary writes, 2026-09-01T12:59:34Z. Consultado con gh api: un solo archivo, el mismo test, +15/-8. Nada de runtime. POR QUE NO ALCANZABA CON MIRAR SUS BRAZOS SINTETICOS: el carril agrego un control sintetico para la forma con esquema, y ese control lo escribio el mismo que escribio el scanner. Si su regex tuviera un sesgo, el control lo comparte. Por eso volvi a poner ARCHIVOS REALES en draft/ y en applied/, que es como llega el defecto de verdad. LAS CINCO SONDAS, con archivos reales y borrados despues: (a) draft con UPDATE public."lookupOptions" SET "isActive"=false, sin members: ROJO, fail 1, y el mensaje NOMBRA EL ARCHIVO - terra_99_probe.sql. Antes esto pasaba verde. Es el agujero que reporte, cerrado. (b) archivo 56 en applied/ con contenido "0 rows", que no toca vocabulario: VERDE, pass 3. Antes daba rojo. O sea que terra_55 puede aplicarse sin voltear la suite, que era la colision concreta de esta semana. (c) draft label-only CON esquema adelante: VERDE. El arreglo del esquema no se llevo puesta la exencion de label, que era el riesgo obvio de ampliar la expresion. (d) INSERT INTO public."lookupOptions": ROJO. (e) UPDATE "public"."lookupOptions" SET "value": ROJO. LAS DOS ULTIMAS LAS AGREGUE YO Y NO ESTABAN EN SUS CONTROLES. El carril probo la forma con esquema solo para UPDATE de isActive. INSERT con esquema y esquema ENTRECOMILLADO son caminos distintos por la misma expresion, y un arreglo de regex puede cubrir uno y no los otros dos. Cubre los tres. Baseline restaurado a 55 archivos y verde, verificado. TAMBIEN QUEDO CORREGIDO EL COMENTARIO: la linea 29 ahora dice que el comentario de terra_35 afirma FALSAMENTE, en vez de llamarlo stale. Importa porque era la misma clase de defecto una capa mas arriba - un comentario nuevo describiendo mal la provenencia del viejo, e invitando a la lectura benigna que me hizo pasar por al lado del hallazgo esta mañana. Y EL CENSO GLOBAL DE 55 ARCHIVOS DESAPARECIO, quedando el pin por LISTA EXACTA de las dos violaciones historicas. Eso es lo correcto y por la razon que le di: la lista ya agarra un incumplidor nuevo y tambien agarra que terra_35 o terra_38 se borren, mientras que el conteo solo agregaba rojos por motivos ajenos. Un arma que se cae por motivos ajenos se desactiva sola. DONDE QUEDA LA FILA, sin inflarlo. Ahora hay DOS instrumentos y cubren cosas distintas: el script con credencial (a80e3c1) DETECTA deriva que ya ocurrio en la base, y este test (9daee49) IMPIDE que entre por el repositorio, sin credencial y dentro de pnpm test. La segunda es la unica de las dos que corre sola. LO QUE SIGUE ABIERTO Y NO ME LO GUARDO: nada de esto es una invariante de BASE. Un INSERT hecho con psql por fuera del repositorio sigue pudiendo desincronizar los members sin que ningun test lo vea - y ese es exactamente el escritor real de las dos veces que se rompio, una migracion a mano corriendo como postgres. La eleccion entre trigger, chequeo agendado y regla de proceso sigue sin decidirse; lo unico que cambio hoy es que la tercera opcion quedo descartada con evidencia, porque terra_35 demuestra que el autor SI documento su razon y la razon era falsa. ESA DECISION NECESITA A db-terra-ca, que hoy esta en 28 por ciento de contexto y sostiene terra_55 esperando el GO de la eq 78. No se la asigno encima de eso.
-
SEGUIMIENTO MENOR, VERIFICADO: b345387, test(migrations): cover qualified vocabulary writes. Solo el archivo de test, +8/-0, puramente aditivo. Incorpora como regresiones explicitas las DOS rutas que yo habia sondeado a mano y que sus controles no cubrian - INSERT INTO public."lookupOptions" y UPDATE "public"."lookupOptions" - conservando la de UPDATE public."lookupOptions" que ya estaba. Ahora los tres caminos con esquema quedan asertados en el archivo en vez de depender de que alguien los vuelva a sondear. LO RE-VERIFIQUE EN VEZ DE ACEPTAR EL REPORTE, porque un commit que solo AGREGA aserciones igual puede mover el comportamiento: guard 3/3 verde; un draft real con UPDATE public."lookupOptions" SET "isActive"=false sigue dando ROJO; applied/ sigue en 55 archivos. Sin cambio de conducta, sin cambio de baseline. Esto no mueve el estado de la fila ni toca lo que queda abierto, que sigue siendo la invariante del lado de la BASE.
-
LOS DOS CAMINOS QUE YO HABIA SONDEADO A MANO QUEDARON FIJADOS COMO REGRESION. b345387, 2026-09-01T13:01:47Z, consultado con gh api: un solo archivo, el mismo test, mas 8 lineas y cero borradas, nada de runtime. Agrega dos deepEqual - INSERT INTO public."lookupOptions" y UPDATE "public"."lookupOptions" - cada uno exigiendo el mensaje completo, no solo que falle. DEJO ESCRITO EL REPARTO PORQUE NO ES EL MISMO TRABAJO Y CONFUNDIRLO ES COMO SE PIERDE COBERTURA. La COBERTURA la verifique yo esta mañana con archivos reales puestos en draft/ y borrados despues, que es como llega el defecto de verdad; estos brazos son SINTETICOS y los escribio el mismo carril que escribio el scanner, o sea que por si solos no probarian nada nuevo - un sesgo de la expresion regular lo comparten los dos. Lo que si hacen, y hace falta, es IMPEDIR QUE LA COBERTURA SE PIERDA en el proximo cambio de la expresion. Verificacion y regresion son cosas distintas: la primera dice que hoy anda, la segunda dice que mañana alguien se entera si deja de andar. ESTADO DE LA FILA, SIN INFLARLO: la mitad de REPOSITORIO esta cerrada y verificada por dos vias. La mitad de BASE sigue abierta y es la que importa mas, porque el escritor real de las dos desincronizaciones historicas fue una migracion a mano corriendo como postgres - exactamente el camino que ningun test del repo ve. Asigne a db02-terra-cc la medicion que decide entre trigger y chequeo agendado: disparadores existentes sobre las dos tablas, tamaño de la deriva viva declarando isActive y deletedAt, y la direccion contraria - members apuntando a un lookupOption borrado - que nadie miro y que decide si alcanza con cubrir INSERT o hace falta DELETE tambien. No le pedi la migracion: ese es carril de db-terra-ca y hoy esta reservado para terra_55. APUNTE DE VERSION, que pertenece a TERRA-194 pero se mide aca: este commit tampoco movio package.json. Es el noveno seguido sin bump desde b7130ad de las 11:39.
-
MEDIDO POR db02-terra-cc Y RE-MEDIDO POR MI CONTRA LA BASE VIVA. Sus tres numeros dan bien y los reproduje, pero encontre un cuarto hecho que ellos no miraron y que es el que decide el diseño, asi que lo escribo entero. LOS TRES QUE PEDI, confirmados por mi con consulta propia: (1) DISPARADORES sobre lookupOptions y vocabularyScopeMembers: CERO no internos. Campo libre en las dos tablas. (2) DERIVA VIVA, declarando isActive y deletedAt: documentoType 3 filas activas y no borradas - esa es la unica deriva VIVA. terraTipoInjerto 3 y terraZonaDadora 9 estan las doce borradas, o sea que el total crudo de 15 no es una poblacion. (3) DIRECCION CONTRARIA: NO es cero. terraTipoInjerto tiene 2 miembros - autologo y aloplastico - apuntando a filas con deletedAt 2026-08-22 14:48:45, sobre 4 miembros que tiene hoy la categoria. Un disparador solo de INSERT no alcanza. EL CUARTO HECHO, Y CAMBIA EL DISEÑO: NO EXISTE NINGUNA CLAVE FORANEA ENTRE vocabularyScopeMembers Y lookupOptions. Lo unico que hay en esa tabla es PRIMARY KEY (appKey, category, value) y FOREIGN KEY (appKey, category) REFERENCES vocabularyScopes ON DELETE CASCADE. O sea que la pertenencia se guarda como TEXTO y nada la ata al catalogo. Eso explica el punto 3 sin misterio: borrar una opcion no puede cascadear porque no hay quien la referencie, y los dos huerfanos de tipoInjerto son ese mecanismo ya ocurrido, no un accidente. Y ESO MATA EL ARREGLO OBVIO CON EVIDENCIA, que era agregar la clave foranea que falta: piezasDentarias tiene 32 filas en vocabularyScopeMembers y CERO filas en lookupOptions - lo verifique como conteo directo, no por el join. Esa categoria resuelve contra la tabla piezasDentarias por codigoFdi, cosa ya establecida. Entonces una foranea contra lookupOptions rechazaria de entrada 32 filas legitimas. No es que sea caro: es imposible sin romper una categoria que hoy funciona bien. LA CONSECUENCIA INCOMODA, Y LA DIGO PORQUE ES LA MISMA TRAMPA QUE ESTA FILA VIENE ESQUIVANDO: cualquier disparador tiene que RAMIFICAR POR CATEGORIA, porque 32 miembros no se resuelven contra lookupOptions y el resto si. Ramificar por una lista de nombres es exactamente el modo de falla que ya tengo escrito - la categoria que alguien agregue mañana y no ponga en la rama queda sin guardia y se ve identica a una guardada. No digo que no se haga; digo que si se hace, la rama tiene que derivarse de una PROPIEDAD consultable - por ejemplo, existe fila en lookupOptions para esa categoria - y no de un CASE enumerado, y que eso hay que decidirlo antes de escribir la migracion, no despues. UN DATO DE PRODUCTO QUE APARECIO DE COSTADO Y NO ES DE ESTA FILA: la deriva viva de documentoType son 3 tipos de documento ACTIVOS que el scope no incluye - los miembros son solo dni y pasaporte, del 2026-08-07, mientras el catalogo tiene ademas cedula, libreta civica y libreta de enrolamiento activas. Como el modo es include, hoy en terra esos tres no se pueden elegir. Puede ser deliberado. No lo toco ni lo asumo defecto; lo dejo anotado para preguntarlo aparte. UNA CORRECCION DE FORMA AL REPORTE, chica pero importa: db02 acompaño sus numeros con una consulta que menciona m.scopeId y m.lookupOptionId, y esas columnas NO EXISTEN - la tabla no tiene ni id ni referencias, solo appKey, category, value y createdAt. Los numeros estan bien, asi que la consulta que corrieron no es la que escribieron. Una consulta que no corre pegada a un numero correcto es peor que ninguna, porque es la que el proximo copia y pega. SIGUE SIN DECIDIRSE trigger contra chequeo agendado, y ahora la decision tiene los insumos que le faltaban.
-
Nada obliga a que una migracion de vocabulario mantenga los scope members. MEDIDO 2026-09-01: no hay NINGUNA FK entre vocabularyScopeMembers y lookupOptions - la pertenencia es texto suelto - y agregarla es imposible, porque piezasDentarias tiene 32 miembros y CERO filas en lookupOptions. Tampoco hay disparadores. El titulo anterior decia 0 huerfanos sobre 74 y hoy eso es falso: terraTipoInjerto tiene 2 de sus 4 miembros apuntando a filas borradas el 2026-08-22, y hay 3 opciones vivas de documentoType fuera del scope. La mitad de REPOSITORIO esta cerrada y verificada por dos vias; falta la invariante de BASE, y cualquier disparador tiene que ramificar por categoria desde una propiedad consultable, nunca desde un CASE enumerado
-
DISEÑO DE LA MITAD DE BASE, DECIDIDO. Y arranca con que MI discriminador era malo y db-terra-ca lo volteo con razon. YO PROPUSE derivar la rama del trigger de una propiedad consultable: existe alguna fila en lookupOptions para esta categoria. FALLA ABIERTO en dos estados silenciosos - categoria lookup recien declarada antes de su primera opcion, y categoria cuya ultima opcion quedo con deletedAt. En los dos casos una categoria lookup se disfraza de no-lookup y el miembro invalido pasa. Es la misma clase de defecto que esta fila persigue, ausencia leida como benigna, y la propuse yo despues de haberla nombrado tres veces hoy. La fuente del resolver tiene que ser METADATA EXPLICITA, no una observacion sobre filas presentes. CENSO DE RESOLVERS, verificado por mi contra pg_proc y no tomado de palabra: exactamente DOS funciones vivas mencionan vocabularyScopeMembers - resolveScopedVocabulary, que resuelve via vLookupOptions, y resolveScopedPiezasDentarias, que resuelve codigoFdi para la categoria fija piezasDentarias. No hay tercer mecanismo. PROPIEDAD, MEDIDA: vocabularyScopes tiene 7 filas y las 7 son appKey terra, cero de otra app. lookupOptions tiene 95 filas ownerAppKey terra y 55 con owner NULO - o sea que lookupOptions SI es catalogo compartido de verdad y ahi vive documentoType. ACL de las dos tablas de scope: solo postgres y service_role. Las dos mitades de eso no se cancelan y por eso la decision no es obvia: las FILAS de scope son 100% terra, asi que ninguna otra app cambia de comportamiento; pero la TABLA es de esquema compartido y agregarle una columna es DDL sobre algo que terra no posee. DECIDIDO: la metadata de resolver NO va como columna de vocabularyScopes. Va en una TABLA NUEVA propiedad de terra, clave (appKey, category), FK contra vocabularyScopes. Terra posee el artefacto entero, no hace falta GO de otro dueño, y la propiedad que se busca - que una categoria sin resolver declarado sea imposible - se consigue igual porque el trigger despacha desde esa tabla y FALLA CERRADO cuando no encuentra fila. Asimetria que decide: mover despues una tabla propia hacia enamel es barato, sacar una columna de una tabla compartida no. TRIGGER, NO CHEQUEO AGENDADO, con la razon de db-terra-ca que acepto: un censo agendado deja un miembro invalido consultable durante una ventana arbitraria, y en un entorno unico eso es peor y no mas seguro. El agendado queda solo como DETECTOR. Cobertura obligatoria: miembros INSERT/UPDATE/DELETE, y cambios en la fuente resuelta - lookupOptions UPDATE de value y de category, y transiciones de deletedAt. Solo-INSERT no alcanza y esta probado por los huerfanos de tipoInjerto. El trigger tiene que mirar estado OLD/NEW de category para que borrar la ultima opcion no haga desaparecer la guardia. PENDIENTE ANTES DE ESCRIBIR MIGRACION: la forma exacta de la tabla nueva y el conjunto exacto de disparadores, pedidos a db-terra-ca para que queden en esta fila y no en un hilo. LIMITE CONOCIDO QUE NO CIERRA ESTA FILA Y LO ANOTO PARA QUE NO SE LEA COMO CUBIERTO: coder-terra-ca dejo dryrun/ fuera del guard a proposito y su argumento es correcto - son artefactos de ensayo, escanearlos da rojos por fixtures. Al discutirlo vi que yo miraba el agujero equivocado. El agujero real es MIGRACIONES QUE SE APLICAN SIN PASAR NUNCA POR draft/, y no es hipotetico: terra_52 y terra_54 entraron asi, ejecutadas directo por db-enamel-cc, documentadas en REPLAY-ORDER.md sin numero de registro por eso mismo. Contra ese camino el pin de applied/ agarra DESPUES, nunca antes.
-
CORRECCION MEDIDA: los 2 huerfanos de terraTipoInjerto que figuran en el titulo de esta fila YA NO EXISTEN. Los reparo terra_39 el 2026-08-22. Mi numero del 2026-09-01 estaba vencido y lo di por vivo. Consulta corrida hoy, 2026-09-05, uniendo miembros contra opciones vivas por categoria y valor: terra|documentoType|dni|ok terra|documentoType|pasaporte|ok terra|terraTipoInjerto|aloinjerto|ok terra|terraTipoInjerto|aloplastico|ok terra|terraTipoInjerto|autologo|ok terra|terraTipoInjerto|xenoinjerto|ok Los cuatro miembros de terraTipoInjerto resuelven contra una opcion viva. Cero huerfanos en las dos categorias que el titulo acusa. LAS 3 OPCIONES DE documentoType FUERA DE ALCANCE SIGUEN AHI, y son otra cosa: cedula, libretaCivica y libretaEnrolamiento estan vivas en el catalogo y no son miembros del alcance de terra. Eso no es un huerfano - es la direccion contraria, una opcion viva que terra no ofrece. No la cuento como defecto de esta fila. Que sea DELIBERADO es una afirmacion, no una medicion: un alcance include con 2 miembros tiene exactamente la misma forma si la exclusion se decidio y si se olvido, y la base no guarda cual de las dos fue. Eso es literalmente lo que TERRA-203 dice y por lo que espera una respuesta de la catedra. Lo dejo ahi y no lo cierro desde aca. LO QUE NO CAMBIA, y es lo unico que mantiene viva esta fila: no hay invariante en la BASE. Sigue sin haber FK entre vocabularyScopeMembers y lookupOptions, y sigue siendo imposible agregarla mientras piezasDentarias tenga 32 miembros y cero filas en lookupOptions. La guarda de npm test atrapa una semilla nueva sin alcance; no impide que la base quede inconsistente por cualquier otro camino. Esa mitad es DDL y es del carril de base, no mio.
-
La mitad de BASE quedo cerrada y versionada. terra_209 esta aplicado y ahora tambien en el repositorio, junto con las otras dos migraciones que estaban vivas sin versionar y con el orden de reproduccion actualizado. Verificado en vivo antes de subirlo, no por reporte de otro carril: existen los dos disparadores y cubren las dos direcciones. Uno rechaza un miembro de alcance que no resuelva contra una opcion viva y activa. El otro impide desactivar o borrar en blando una opcion mientras un miembro la siga nombrando. Un guard de sola insercion habria dejado abierta exactamente la mitad por la que entraron los huerfanos de agosto, que es la que esta fila venia marcando. Y cumple la condicion de forma que puse cuando revise el planteo: ramifica por una PROPIEDAD CONSULTABLE - si la categoria tiene alguna fila en lookupOptions - y no por un CASE enumerado. Por eso piezasDentarias queda exenta sola, con sus 32 miembros y cero opciones, sin que ninguna migracion la nombre. Una lista escrita a mano habria envejecido con la primera categoria que se agregue. Estado de las dos mitades: la de repositorio ya estaba cerrada y verificada por dos vias, la guarda de npm test que atrapa una semilla nueva sin alcance. La de base es esta. Ya no queda nada mio abierto en esta fila. Lo unico que sigue afuera, y a proposito, son las 3 opciones de documentoType fuera del alcance de terra: no son huerfanos sino la direccion contraria, y si esa exclusion fue decidida u olvidada no lo puede contestar la base. Eso vive en TERRA-203 esperando a la catedra.
-
coder02-terra-ca
-
Vocabulary member-integrity guard is live; remaining category/policy scope must be stated precisely
-
was: MEDIDO 2026-09-01 08:40 contra la base viva y contra los archivos aplicados. Sale de una pregunta de db02-terra-cc sobre TERRA-141 y de una medicion mia que EMPEZO MAL; escribo las dos cosas porque el error acota el hallazgo. EL HECHO ESTRUCTURAL. vocabularyScopeMembers.value es text NOT NULL y NO tiene ninguna restriccion referencial contra el vocabulario que nombra. Medido en pg_constraint: la unica clave foranea de la tabla es fk_vocabularyScopeMembers_scope, que apunta a vocabularyScopes. Ninguna apunta a lookupOptions - de hecho NINGUNA restriccion en toda la base referencia lookupOptions. Y medido en pg_trigger: vocabularyScopes, vocabularyScopeMembers y lookupOptions tienen CERO triggers no internos, las tres. POR QUE NO ALCANZA CON "FALTA LA FOREIGN KEY", que es lo que yo iba a escribir: el vocabulario que estos members nombran NO VIVE EN UNA SOLA TABLA. Son tres origenes distintos, medidos: (a) lookupOptions con ownerAppKey='terra' - terraMarcaImplante, terraTipoInjerto, terraZonaDadora, terraTipoBarreraImplante, terraTipoBarreraReconstruccion; (b) lookupOptions con ownerAppKey NULL, o sea compartido y no de terra - documentoType, 5 filas; (c) una TABLA APARTE, piezasDentarias, con 56 filas, que no es una categoria de lookupOptions. O sea que la restriccion no es expresable como UNA foreign key aunque lookupOptions fuera de terra, que no lo es: es una tabla COMPARTIDA con enamel y terra no puede agregarle nada. Eso es mas fuerte que "nadie la puso": no hay una forma obvia de ponerla. LA CONSECUENCIA, que es lo que hace que esto sea una fila y no una curiosidad: cualquier migracion que agregue o reemplace valores en una categoria con scope en modo include desincroniza los members, salvo que la MISMA migracion toque tambien vocabularyScopeMembers. Nada lo obliga y nada avisa. Ya paso las DOS veces que se toco vocabulario a mano: terra_35 agrego 4 opciones de marca y omitio agregar los members, y terra_38 reemplazo sets enteros y ni lo intento. terra_39 existe unicamente para reparar eso, y tuvo que tocar LAS CUATRO categorias por DOS mecanismos distintos - en marca fue drift aditivo (faltaban members, ninguno stale, 4 INSERT); en tipoInjerto y zonaDadora fue reemplazo total (members apuntando a values retirados, DELETE + INSERT); y en tipoBarrera el scope row ENTERO quedo obsoleto porque terra_38 partio la categoria en dos, asi que hubo que borrar el scope y crear dos nuevos. Medido por db02-terra-cc en el cuerpo de terra_39, lineas 123-175. ESTADO HOY: SANO, Y LO DIGO CON EL NUMERO. 74 members en total y CERO huerfanos - los 5 categorias de lookupOptions resuelven contra filas vivas y activas, los 2 de documentoType resuelven contra las filas de owner NULL, y los 32 de piezasDentarias resuelven los 32 contra piezasDentarias.codigoFdi. terra_39 dejo esto consistente. La fila NO es una limpieza: es que nada impide que se vuelva a romper la proxima vez, y las dos veces anteriores se rompio. MI ERROR, QUE ACOTA EL HALLAZGO Y POR ESO VA ESCRITO: mi primera consulta conto 34 huerfanos sobre 74. Los 34 eran enteramente un defecto de mi join - cruce TODOS los members contra lookupOptions con ownerAppKey='terra', y ni documentoType (owner NULL) ni piezasDentarias (otra tabla) viven ahi. Casi presento como deuda de datos lo que era una lectura contra la fuente equivocada, y el numero se veia perfectamente plausible. Es la tercera vez hoy que un predicado o un filtro mal elegido devuelve una cifra verdadera de otra cosa. LO QUE HAY QUE DECIDIR, y no es mio: si esto se ataja con un trigger de validacion sobre vocabularyScopeMembers que resuelva contra los tres origenes, con una comprobacion de drift que corra sola y avise, o con una regla de proceso de que toda migracion de vocabulario toque las dos tablas. La primera opcion tropieza con que lookupOptions es de enamel; la tercera es prosa y ya sabemos que la prosa no es una frontera - misma clase que TERRA-197 y TERRA-131.
-
Live guard verified; residual scope/category questions and behavior evidence remain.
-
DB scope complete: live registry has 20260905143518 terra_209_vocabulary_scope_member_integrity_guard. Live catalog has ENABLE ALWAYS vocabularyScopeMembers integrity trigger and lookupOptions anti-orphan trigger. Prior rollback-only controls recorded the expected P0001 invalid-scope rejection; no duplicate DDL is warranted. Category ownership and deliberate documentType scope exclusions are policy questions tracked outside this DB enforcement item.
bug
6w ago by wi-cli-venus
5w ago
2026-09-10 18:23