Cuándo no confiar en la IA
Este es el documento de supervivencia. El código de Mi Plante tiene términos de dominio en español, lógica de negocio no documentada y patrones de bug que la IA no conoce por defecto. El nuevo equipo usa Claude Code y Cursor como pair programmers. Si confías en la IA acríticamente en este código, vas a despachar código que alucina un método, trata la intención documentada como comportamiento garantizado, o duplica un landmine ya existente.
La calibración de confianza es una habilidad de supervivencia en este repo.
Cada modo de falla abajo tiene un ejemplo concreto y una mitigación. Ninguno es hipotético. Los errores aquí salieron en sesiones reales de onboarding.
Por qué esto importa
Tres razones por las que la calibración de confianza es únicamente difícil en Mi Plante:
-
Nomenclatura de dominio en español. El training data de la IA es mayoritariamente en inglés. Cuando predice qué viene después en este código, tiene que tender un puente desde
customer/order/installmenthastaCliente/OrdenCompra/Cuota. A veces tiende el puente mal. -
Brecha entre documentado y garantizado. La auditoría original (
docs/audit/01-15) describe el sistema como fue diseñado. La validación de seguimiento (docs/audit/16-deep-validation-study.md) muestra que varios flujos importantes no garantizan lo que el diseño describió. La IA lee ambos. Si pesa más los docs de diseño, te da respuestas arquitectónicamente limpias que son incorrectas en runtime. -
Landmines activos. Bugs abiertos en código de producción (ver
docs/onboarding/04-the-landmines/01-known-bugs.md). La IA no los conoce a menos que los cites. Felizmente propondrá código que interactúa con estos landmines y los empeora.
Los 12 modos de falla abajo están ordenados por qué tan probable es que los encuentres en tu primer mes. Léelos todos.
Modo de Falla 1: Alucinar código nombrado en español que no existe
La trampa. La IA extrapola desde patrones Laravel en inglés e inventa nombres de método en español que suenan plausibles pero no existen.
Ejemplo concreto. Le preguntas: “Marca esta Venta como completada.”
La IA escribe con confianza:
$venta->completar();$venta->marcarComoCompletada();$venta->actualizarEstado(EstadoVenta::COMPLETADA);Ninguno de estos existe en app/Models/Facturacion/Venta.php. El modelo usa llamadas simples de update de Eloquent:
$venta->update(['estado' => EstadoVenta::COMPLETADA]);Los métodos inventados por la IA son adivinanzas basadas en “en código Laravel en inglés, los modelos tienen métodos como markAsCompleted o complete”. Mi Plante no sigue esa convención.
Mitigación.
- Antes de usar cualquier método de modelo o servicio, hazle grep:
grep -rn "function completar" app/Models/Facturacion/ - Para servicios, la respuesta correcta casi siempre es el
update()explícito o un método de servicio (VentaService::actualizarVenta) — no un método mágico de transición de estado. - El doc de auditoría
docs/audit/15-glossary.mdlista cada término de dominio. Si un nombre de método no aparece en el glosario o en código real, no existe.
Modo de Falla 2: Tratar la intención documentada como garantizada
La trampa. Varios documentos de docs/audit/01-15 describen el diseño intencionado, no la realidad en runtime. La validación profunda en docs/audit/16 los corrige pero es fácil pasarlo por alto. La IA lee los docs originales y los trata como verdad fundamental.
Ejemplo concreto. Le preguntas: “Camíname el pipeline de aprobación crediticia. El OTP debe ser verificado antes del cuestionario, ¿cierto?”
La IA dice que sí, citando docs/audit/12. Pero docs/audit/16 §12 nota explícitamente:
El cuestionario no esta completamente blindado por backend detras de un OTP ya verificado y del flag requiere_cuestionario; varias precondiciones reales dependen solo de cache previa.
En el código, el endpoint de generación de cuestionario (IdentityValidationService::generarCuestionario) chequea la {userId}.validacion cacheada (de la fase 2) pero NO chequea que el OTP de la fase 4 fue realmente verificado. Si un usuario sofisticado retiene la cache key validacion y pega la ruta del cuestionario directamente, bypassa el OTP.
Otro ejemplo concreto. La auditoría docs/audit/12 muestra una secuencia estricta de 7 fases. La IA confiadamente dice “el cliente debe completar las 7 fases en orden”. Pero AprobarCupoService::validarSiElClienteTieneSuCupoAprobado en :17 requiere >= 5 ProcessTypes con FINISH_SUCCESS este mes — no las 7, no en orden.
Mitigación.
- Cuando la IA cite
docs/audit/01-15, pregunta: “¿docs/audit/16-deep-validation-study.mdconfirma o corrige esto?” - Cruza referencia contra el código real en el archivo/método citado.
- El documento
docs/audit/16tiene un bloque “Validated Corrections” sobre la mayoría de los otros docs de auditoría — lee esos bloques primero.
Modo de Falla 3: Perder el contexto de duplicación de Cuotas
La trampa. Cuando se le pide agregar nuevo código de generación de cuotas, la IA propone agregarlo en un lugar nuevo porque no sabe que las cuotas ya se están generando DOS VECES en producción hoy.
Ejemplo concreto. Le preguntas: “Agrega un job que haga backfill de cuotas para ventas legacy que no las tienen.”
La IA escribe un job que llama a VentaService::generarCuotas($venta) para cada Venta donde cuotas()->count() == 0. El código se ve correcto.
Lo que la IA no sabe: en producción hoy, generarCuotas corre dos veces para muchas ventas — una en VentaService::crearVenta en :294 (o :441-442 vía generarCuotasPorOrden), y una en ProcesarPagareDigital::handle en :80 (o :119). Agregar un tercer punto de disparo en un job de backfill crea la misma carrera: si el backfill corre mientras una venta está en vuelo, obtienes un triple-write.
El arreglo que la IA no propuso: agregar una guarda de idempotencia al inicio de generarCuotas:
if (Cuota::where('venta_id', $venta->id)->exists()) { Log::warning('Cuotas already generated', ['venta_id' => $venta->id]); return;}Ese único cambio hace seguros los TRES puntos de disparo. Sin él, cada lugar que llama a generarCuotas está en carrera.
Mitigación.
- Siempre corre
/find-landmines-near <archivo>(Claude Code) antes de editar en los flujos de creación de venta, generación de cuotas o pagaré. - Lee
docs/onboarding/04-the-landmines/01-known-bugs.mdde principio a fin. El landmine de duplicación de cuotas (L-10) está en el top 10.
Modo de Falla 4: Usar con confianza created_at en subclases de Modelo
La trampa. La mayoría de modelos en app/Models/ extienden App\Models\Modelo que sobrescribe:
const CREATED_AT = 'creado_en';const UPDATED_AT = 'actualizado_en';Esto significa que la columna created_at no existe en las tablas subyacentes de base de datos para esos modelos. Cuando accedes a $venta->created_at, Eloquent retorna null silenciosamente.
La IA no sabe esto por defecto. Escribirá código como:
// Mal — Venta extiende Modelo$venta->where('created_at', '>', now()->subDays(30))->get();$venta->orderBy('created_at', 'desc');$fecha = $venta->created_at;Los tres están silenciosamente rotos. El primero retorna CERO filas (no hay columna created_at). El segundo ordena por NULL. El tercero retorna null.
El código correcto:
$venta->where('creado_en', '>', now()->subDays(30))->get();$venta->orderBy('creado_en', 'desc');$fecha = $venta->creado_en;Este es también uno de los landmines activos en producción: app/Services/VentaService.php:582, :657, :704 leen created_at en Venta y OrdenCompra (ambos extienden Modelo). El resultado es que la matemática de fecha_vencimiento de cuotas corre desde now() en lugar del tiempo real de creación del registro.
Mitigación.
- Antes de escribir filtros de fecha en cualquier modelo, chequea si extiende
Modelo. Grep rápido:grep -A 1 "class Venta extends" app/Models/Facturacion/Venta.phpmuestraextends Modelo. - Si sí, usa
creado_en/actualizado_en. - Modelos que NO extienden Modelo:
User,Cuota,BackendRequestLog. Estos usan los defaults de Laravel.
Modo de Falla 5: Sugerir middleware de Spatie Permission que no funcionará
La trampa. Spatie Permission está instalado (composer.json), las migraciones existen, los aliases de middleware están registrados en bootstrap/app.php:31-33. Pero el modelo User no tiene trait HasRoles, ninguna ruta usa el middleware role/permission, no existe seeder de roles.
La IA ve Spatie en composer.json y sugiere:
Route::middleware('permission:create-empresa')->group(function () { ... });Esto no funcionará en Mi Plante. No hay permiso create-empresa, ningún rol con ese permiso, ningún usuario tiene ningún rol asignado en las tablas de Spatie.
El patrón real que usa Mi Plante:
// Chequeo inline de rol en el controllerif (!in_array(auth()->user()->rol, ['administrador', 'administrador_comercial', 'administrador_financiero'])) { abort(403);}Los roles viven en users.rol (columna string plana). Los valores de rol descubiertos: cliente, aliado, empleado, administrador, administrador_comercial, administrador_financiero.
Mitigación.
- Usa chequeos inline de rol para autorización hasta que Spatie esté correctamente cableado.
- Si quieres usar el middleware de Spatie, primero debes: agregar
HasRolesaUser, sembrar roles + permisos, asignar rol a usuarios existentes en una migración. - La auditoría (
docs/audit/07) y elCLAUDE.mdraíz ambos llaman la atención sobre esta brecha. Reléelos si tienes dudas.
Modo de Falla 6: Mockear integraciones que no existen
La trampa. Al escribir tests para código que pega DataCrédito, Certicámara, Experian, etc., la IA puede inventar una clase *Mock.
Ejemplo concreto. Le preguntas: “Escribe un unit test para DataCreditoService::consultarHistorialCredito.”
La IA escribe:
$mock = $this->createMock(DataCreditoMock::class);$mock->expects($this->once()) ->method('consultarHistorialCredito') ->willReturn(['responseCode' => 13]);No hay clase DataCreditoMock en el código. Mi Plante no usa mocks estilo Mockery para servicios externos.
El patrón real: usar Http::fake() de Laravel contra el macro HTTP:
Http::fake([ '*/cs/credit-history/*' => Http::response([ 'ReportHDCplus' => [ 'productResult' => ['responseCode' => 13], ], ], 200),]);
$svc = app(DataCreditoService::class);$response = $svc->consultarHistorialCredito('12345678', 1, 'GARCIA');El macro Http::datacredito() está registrado en app/Providers/AppServiceProvider.php. Http::fake() intercepta a nivel de URL.
Mitigación.
- Para cada servicio externo (TransUnion, Experian, DataCrédito, Certicámara, EMCALI, SHIVAM), el patrón de test es
Http::fake()contra el patrón de URL. - Los macros HTTP son:
Http::transunion(),Http::datacredito(),Http::certicamara(),Http::expirianCrossCore(),Http::expirianCrossCoreAuth(). Fake-éalos por su patrón de URL, no por nombre de macro. - Ver
app/Services/CLAUDE.mdpara la plantilla canónica de servicio y enfoque de test.
Modo de Falla 7: Hacer ingeniería inversa de enums en español incorrectamente
La trampa. La IA adivina un caso de enum basado en convención en inglés. El enum real en español es diferente.
Ejemplo concreto. Le preguntas: “Pon esta venta a cancelada.”
La IA escribe:
$venta->update(['estado' => EstadoVenta::CANCELADA]);NO hay caso CANCELADA en el enum. app/Enum/Facturacion/EstadoVenta.php define:
case PENDIENTE = 'pendiente';case APROBADA = 'aprobada';case ENTREGADA = 'entregada';case DEVUELTA = 'devuelta';case LEGALIZADA = 'legalizada';case COMPLETADA = 'completada';case RECHAZADA = 'rechazada';case ABANDONADA = 'abandonada';Los dos estados terminales son RECHAZADA (aprobación crediticia falló o pagaré bloqueado) y ABANDONADA (timeout después de 60 min). No hay un “cancelado” general.
Este es en realidad uno de los landmines reales en producción: app/Listeners/VentaCanceladaListener.php referencia EstadoVenta::CANCELADA y crashearía el listener si alguna vez se disparara. El listener es código muerto hoy; si lo “arreglas” quitando la referencia, eliminas un riesgo de crash.
Mitigación.
- Abre el archivo del enum antes de referenciar valores. Dos segundos te ahorran una hora.
- Enums de Mi Plante:
EstadoVenta,EstadoCuota,OrdenCompraEstado,EstadoPlanCredito(NO operativo — el modelo no existe),BeneficiarioParentescto,AprobarCupo\ProcessType,AprobarCupo\EventType,EmpresaType,CargaMasivaProductoErrorType. docs/audit/15-glossary.md§4 lista cada caso deEstadoVentacon traducción y disparador.
Modo de Falla 8: Sugerir arreglos para “rutas fantasma”
La trampa. Mi Plante tiene rutas registradas en routes/web.php y routes/api.php cuyos métodos de controller no están implementados. Pegarlas retorna 500. La IA ve el 500 y propone implementar los métodos faltantes.
Ejemplo concreto. Le preguntas: “POST /lineas retorna 500. Arréglalo.”
La IA mira routes/web.php línea ~30:
Route::apiResource('lineas', LineaController::class);Ve que no hay store() / update() / destroy() en LineaController, y los escribe.
Pero: según docs/audit/02-route-inventory.md (correcciones validadas):
Rutas de
lineasde escritura existen en el router pero no son funcionales porque el controlador no implementa esos metodos.
Las escrituras NUNCA fueron diseñadas para funcionar. Están inactivas a propósito — el catálogo de lineas es curado por admins, no editable por el usuario. Implementar los métodos expondría silenciosamente acceso de escritura a un área inactiva.
Mitigación.
- Antes de “arreglar” un 500 en una ruta, chequea
docs/audit/02-route-inventory.mdpara la ruta en cuestión. Si está marcada como “registered but not functional”, el 500 es intencional. - Mejor arreglo: quita el registro de la ruta si el equipo está de acuerdo, o agrega los métodos faltantes solo después que el equipo confirme que la ruta debe estar activa.
- Otras rutas fantasma: las escrituras en
marcasson PÚBLICAS y escribibles (bug diferente — landmine de seguridad). Confirma la intención antes de tocar.
Modo de Falla 9: Tratar el ExceptionHandler de la API como funcionando
La trampa. El doc de auditoría docs/audit/06-architecture-diagram.md y docs/audit/14-request-lifecycle-flowchart.md describen el exception handler de la API como retornando JSON estructurado de errores. En el código, el handler es invocado pero su valor de retorno se descarta.
bootstrap/app.php:55-83:
if (array_key_exists($className, $handlers)) { $method = $handlers[$className]; $apiHandler = new \App\Exceptions\ApiExceptionHandler(); $apiHandler->$method($exception, $request); // <- sin return} else { // ...}// cae al handler genérico de Inertia/redirect en las líneas 86-95La respuesta JSON intencionada se construye pero nunca se retorna al cliente. El camino de fallback retorna un render de Inertia o un redirect.
La IA lee los docs de auditoría y asume que el JSON estructurado funciona para errores de API. Escribe código o tests que dependen del envelope JSON:
$response = $this->postJson('/api/v1/some-endpoint', ['bad' => 'data']);$response->assertJson(['error' => 'something']); // FALLA — el envelope JSON no está en su lugarMitigación.
- Para endpoints de API, verifica el envelope de error pegándole a un endpoint que erroré manualmente con
curl -H "Accept: application/json". - Si dependes de la forma JSON del error en tests, PRIMERO arregla el
returnfaltante enbootstrap/app.php:55. Ese arreglo es una línea:return $apiHandler->$method($exception, $request); - Hasta que ese arreglo llegue, no escribas aserciones que dependan del envelope de la API.
Modo de Falla 10: Confusión Inertia vs API
La trampa. Mi Plante es un monolito Inertia.js. Las rutas en routes/web.php y routes/ally/web.php retornan respuestas Inertia. Las rutas en routes/api.php retornan JSON. La IA a veces los mezcla.
Ejemplo concreto. Le preguntas: “Agrega una llamada axios desde el frontend para obtener todas las ventas del aliado.”
La IA escribe:
const { data } = await axios.get(route('aliado.ventas.render'));aliado.ventas.render es una ruta Inertia. Llamarla vía axios retorna una respuesta HTML de Inertia — no JSON. El cliente axios puede recibir un 200, pero data será o bien un string HTML o un 409 de version-mismatch de Inertia (dependiendo de los headers).
Los patrones correctos:
- Si necesitas JSON: construye una ruta paralela bajo
routes/api.php(p. ej.GET /api/v1/aliado/ventas) con un método de controller JSON. - Si puedes usar Inertia: usa
<Link>orouter.visit()/router.post()de@inertiajs/vue3, que envía los headers Inertia correctos.
Mitigación.
- Trata
routes/web.php,routes/ally/web.phpyroutes/settings.phpcomo solo-Inertia. No las llames desde axios. - Trata
routes/api.phpyroutes/webhooks/*como solo-JSON. No las llames desde Inertiarouter.visit(). - Si ves una llamada axios a un nombre de ruta que empieza con
aliado.ohome, casi seguro está mal.
Modo de Falla 11: Asunciones del contrato frontend
La trampa. Varios archivos Vue asumen formas de respuesta del backend que no coinciden con lo que el backend realmente envía. La IA elegirá un lado (a menudo el del frontend, porque ve el archivo Vue abierto) y “arreglará” el lado equivocado.
Ejemplos concretos del catálogo de landmines:
-
L-18 (
resources/js/pages/settings/Profile.vue:32-34): El form envía{ name, email }. El backendapp/Http/Requests/Settings/ProfileUpdateRequest.phprequierenombresyapellidos. Las actualizaciones de perfil fallan con errores de validación 422 hoy. -
L-19 (
resources/js/composables/useWishlist.ts:29): Espera que la respuesta sea un array plano. El backendListaDeseoController::indexretorna un paginador{ data: [], meta: {} }. El chequeo del composableArray.isArray(data)es false (el paginador es un objeto), así queitemstermina vacío. -
L-20 (
resources/js/pages/ally/ventas/Page.vue:88): La UI maneja un estado legacycancelada. El enum del backend tieneRECHAZADAyABANDONADA, noCANCELADA.
La IA mirará un archivo y “arreglará” con confianza para que coincida. Si pides “arregla el form del Profile para que coincida con el backend”, edita el archivo Vue. Si pides “arregla el backend para que coincida con el frontend”, edita el Form Request.
Cualquier arreglo está mal sin alineación del equipo. El EQUIPO es dueño de si el contrato debe estandarizarse en español (nombres/apellidos) o inglés (name/email). Elegir unilateralmente es un bug de proceso aunque el cambio de código funcione.
Mitigación.
- Para mismatches de contrato frontend-backend, la IA no puede decidir. Tú decides.
- Lee ambos lados. Identifica el mismatch. Abre un ticket. Consigue alineación del equipo sobre qué lado se mueve.
- Luego pídele a la IA que ejecute la dirección acordada.
docs/audit/13-frontend-page-map.mdlista mismatches conocidos.
Modo de Falla 12: Sugerir uso de Redis
La trampa. composer.json incluye predis/predis. .env.example tiene entradas REDIS_*. La IA ve estas señales y sugiere “usemos Redis para X”.
Pero: config/cache.php por defecto al driver database. config/session.php usa database. config/queue.php usa database. Redis está instalado pero no cableado activamente para ninguno de estos.
Si la IA dice “usemos Redis para este cache para reducir carga de BD”, suena razonable. Pero:
- La conexión Redis en producción puede no existir. Las env vars pueden tener valores placeholder.
- Cambiar cache stores requiere testeo — cada llamada existente
Cache::remember()funciona con el driver database pero puede comportarse diferente bajo Redis (atomicidad, semántica de locks, precisión de TTL de keys). - El equipo no se ha comprometido con Redis como dependencia de producción.
Mitigación.
- Antes de sugerir Redis, chequea
config/cache.php,config/session.php,config/queue.phppara ver qué está realmente cableado. - Si Redis no está cableado, propone Redis solo si el equipo es dueño de la migración. De lo contrario quédate en el driver database.
Reglas de calibración
Usa estas heurísticas para fijar tu nivel de confianza en cada respuesta de IA.
| Confianza | Cuándo | Verificación |
|---|---|---|
| ALTA | Patrones rutinarios de Laravel, Vue idiomático, APIs de librería (Carbon, Eloquent, Tailwind, Reka UI) | Hojea el diff, confía en el código |
| MEDIA | Lógica de negocio en servicios, controllers, jobs que puedas leer | Cruza referencia contra el archivo de servicio o job que la IA cite |
| BAJA | Nomenclatura en español, enums de dominio, el pipeline de crédito, cualquier cosa en una zona “do not refactor without asking” | Lee el archivo real, hazle grep al método real, verifica el caso del enum |
| MUY BAJA | Fechas, timestamps, idempotencia, atomicidad, caminos de código concurrentes | Verifica manualmente línea por línea; idealmente agrega un test antes de aceptar |
El pipeline de aprobación crediticia es confianza MUY BAJA. Los contratos frontend-backend son confianza MUY BAJA. “Mejores prácticas” genéricas de React/Vue/Laravel son confianza ALTA — pero Mi Plante usa nomenclatura en español y convenciones no documentadas, así que cualquier respuesta que toque la capa de dominio baja a BAJA.
El checklist “la IA me mintió”
Cuando la IA te da una respuesta y tu instinto dice “¿en serio?”, corre este checklist antes de aceptar:
-
Grep al archivo real.
grep -n "function methodName" app/Services/SomeService.php. Si el método no aparece, la IA alucinó. -
Lee la migración.
database/migrations/<fecha>_create_<tabla>_table.php. La migración es verdad fundamental para columnas y tipos. El docblock del modelo no. -
Lee el enum.
app/Enum/<Facturacion>/<Enum>.php. Los casos del enum son exhaustivos — lo que no está en el archivo no existe. -
Lee el test (si existe). Los tests son la spec ejecutable. Si
tests/Feature/Market/VentaTest.phpafirma el comportamiento X, el comportamiento X es lo que el código hace (hasta que se demuestre lo contrario). Si no existe test, esa es su propia señal — sé más cauto. -
Córrelo localmente y velo.
composer dev, pega el endpoint, mira el log. Cinco minutos de correr el código real le ganan a una hora de discutir con la IA.
Cuándo anular a la IA
Estate dispuesto a empujar de vuelta.
La IA es una herramienta. El código es tu responsabilidad. Si la IA afirma con confianza algo que contradice:
- El
CLAUDE.mdraíz - Un
CLAUDE.mda nivel de directorio - Un doc de auditoría que hayas leído (especialmente
docs/audit/16-deep-validation-study.md) - El catálogo de landmines conocidos (
docs/onboarding/04-the-landmines/01-known-bugs.md) - El código real en el archivo del que la IA está hablando
…la IA está equivocada. Dilo:
Eso contradice docs/audit/16 §X. Por favor relee ese doc y dime quédice sobre Y. Luego revisa tu respuesta.O:
Dijiste que el método foo() existe en VentaService, pero grep no encuentra talmétodo. Vuelve a revisar leyendo el archivo.A la IA le da igual estar equivocada. Va a revisar. Lo que NO debes hacer es aceptar silenciosamente y despachar.
Resumen
Doce modos de falla, ordenados por frecuencia en tu primer mes:
- Alucinar métodos en español que no existen
- Tratar la intención documentada (auditoría
01-15) como realidad garantizada (la auditoría16corrige) - Perder contexto de duplicación de cuotas — landmine L-10
created_aten subclases deModelo(la columna escreado_en)- Middleware de Spatie Permission que no está cableado
- Clases
*Mockhechas a mano (usarHttp::fake()) - Casos de enum en español que no existen (
CANCELADAno es un caso) - “Arreglar” rutas fantasma que están inactivas a propósito
- Tratar el exception handler de la API como funcionando (falta el return)
- Confusión Inertia vs API (axios en rutas Inertia → 409)
- Asunciones de contrato frontend-backend — Profile, useWishlist, ally/ventas
- Sugerir Redis cuando no está realmente cableado
Para cada uno: un ejemplo concreto, una mitigación y dónde mirar en el código.
Marca esto. Reléelo después de tu primer incidente de producción. La IA es un excelente pair programmer cuando está calibrada. Sin calibrar, es un alucinador confiado. La diferencia es si lees este documento.