Saltearse al contenido

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:

  1. 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/installment hasta Cliente/OrdenCompra/Cuota. A veces tiende el puente mal.

  2. 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.

  3. 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.md lista 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.md confirma o corrige esto?”
  • Cruza referencia contra el código real en el archivo/método citado.
  • El documento docs/audit/16 tiene 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.md de 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.php muestra extends 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 controller
if (!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 HasRoles a User, sembrar roles + permisos, asignar rol a usuarios existentes en una migración.
  • La auditoría (docs/audit/07) y el CLAUDE.md raí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.md para 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 de EstadoVenta con 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 lineas de 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.md para 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 marcas son 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-95

La 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 lugar

Mitigació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 return faltante en bootstrap/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> o router.visit()/router.post() de @inertiajs/vue3, que envía los headers Inertia correctos.

Mitigación.

  • Trata routes/web.php, routes/ally/web.php y routes/settings.php como solo-Inertia. No las llames desde axios.
  • Trata routes/api.php y routes/webhooks/* como solo-JSON. No las llames desde Inertia router.visit().
  • Si ves una llamada axios a un nombre de ruta que empieza con aliado. o home, 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 backend app/Http/Requests/Settings/ProfileUpdateRequest.php requiere nombres y apellidos. 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 backend ListaDeseoController::index retorna un paginador { data: [], meta: {} }. El chequeo del composable Array.isArray(data) es false (el paginador es un objeto), así que items termina vacío.

  • L-20 (resources/js/pages/ally/ventas/Page.vue:88): La UI maneja un estado legacy cancelada. El enum del backend tiene RECHAZADA y ABANDONADA, no CANCELADA.

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.md lista 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:

  1. La conexión Redis en producción puede no existir. Las env vars pueden tener valores placeholder.
  2. 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).
  3. 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.php para 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.

ConfianzaCuándoVerificación
ALTAPatrones rutinarios de Laravel, Vue idiomático, APIs de librería (Carbon, Eloquent, Tailwind, Reka UI)Hojea el diff, confía en el código
MEDIALógica de negocio en servicios, controllers, jobs que puedas leerCruza referencia contra el archivo de servicio o job que la IA cite
BAJANomenclatura 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 BAJAFechas, timestamps, idempotencia, atomicidad, caminos de código concurrentesVerifica 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:

  1. Grep al archivo real. grep -n "function methodName" app/Services/SomeService.php. Si el método no aparece, la IA alucinó.

  2. 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.

  3. Lee el enum. app/Enum/<Facturacion>/<Enum>.php. Los casos del enum son exhaustivos — lo que no está en el archivo no existe.

  4. Lee el test (si existe). Los tests son la spec ejecutable. Si tests/Feature/Market/VentaTest.php afirma 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.

  5. 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.md raíz
  • Un CLAUDE.md a 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 tal
mé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:

  1. Alucinar métodos en español que no existen
  2. Tratar la intención documentada (auditoría 01-15) como realidad garantizada (la auditoría 16 corrige)
  3. Perder contexto de duplicación de cuotas — landmine L-10
  4. created_at en subclases de Modelo (la columna es creado_en)
  5. Middleware de Spatie Permission que no está cableado
  6. Clases *Mock hechas a mano (usar Http::fake())
  7. Casos de enum en español que no existen (CANCELADA no es un caso)
  8. “Arreglar” rutas fantasma que están inactivas a propósito
  9. Tratar el exception handler de la API como funcionando (falta el return)
  10. Confusión Inertia vs API (axios en rutas Inertia → 409)
  11. Asunciones de contrato frontend-backend — Profile, useWishlist, ally/ventas
  12. 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.