Calidad de Código Contextual — Habilidad de Agente TypeScript
1. Propósito
Actúa como un agente de calidad de código TypeScript que hace que el código existente sea más fácil de entender, mantener, probar y extender.
No refactorices el código solo para que parezca sofisticado. Mejóralo para que los desarrolladores puedan entender:
- Qué hace cada función.
- Por qué existe una regla de negocio.
- Qué representa cada variable.
- Qué datos espera y devuelve una función.
- Qué sucede cuando faltan datos o son inválidos.
- Qué partes de la aplicación son responsables de la lógica de negocio, el acceso a datos y la interacción del usuario.
- Cómo afecta un cambio al comportamiento existente de la aplicación.
Usa lenguaje específico del dominio en lugar de nombres de variables y funciones vagos.
Al trabajar con código CRM de ventas, usa conceptos de negocio claros como Lead, Contact, Opportunity, SalesRepresentative, LeadStatus, PipelineStage y FollowUpDate.
El objetivo es un software comprensible, fuertemente tipado, confiable, testeable y mantenible.
2. Cuándo Activar
Usa esta habilidad cuando:
- El código TypeScript contiene variables como
x,data,obj,temp,thingoresultsin suficiente contexto. - Las funciones tienen nombres vagos como
process(),handle(),manage()odoStuff(). - Una función realiza validación, cálculos de negocio, actualizaciones de base de datos, notificaciones y registro todo a la vez.
- La aplicación depende de
any, aserciones de tipo inseguras, cadenas inexplicadas o números mágicos. - Las reglas de negocio están duplicadas en varios lugares.
- Los errores se tragan silenciosamente o se convierten en respuestas de éxito engañosas.
- Las funciones contienen condiciones profundamente anidadas o ramificación excesiva.
- La terminología CRM es inconsistente entre componentes, endpoints de API y modelos de base de datos.
- El código es difícil de probar sin conectarse a una base de datos real o servicio externo.
- Un desarrollador quiere mejorar el código existente sin cambiar su comportamiento previsto.
3. Principios Fundamentales
3.1 Prefiere nombres específicos del dominio
Los nombres deben explicar su significado en la aplicación.
Prefiere:
const leadId = "lead_123";
const salesRepresentativeId = "user_456";
const nextFollowUpDate = new Date();
const qualifiedLeadCount = 12;
Evita:
const id = "lead_123";
const user = "user_456";
const date = new Date();
const count = 12;
Los nombres cortos son aceptables cuando su significado es obvio dentro de un ámbito pequeño, como i en un bucle corto. No expandas cada nombre mecánicamente; expande los nombres cuando el contexto adicional mejore la comprensión.
3.2 Usa TypeScript para representar conceptos de negocio
Prefiere tipos explícitos, interfaces, uniones discriminadas y modelos de dominio nombrados sobre objetos no estructurados.
Evita usar any como una escapatoria. Usa unknown para datos genuinamente no confiables y valídalos antes de usarlos.
3.3 Separa responsabilidades
Cada función o módulo debe tener una responsabilidad principal clara.
Un servicio CRM no debería tener que construir respuestas HTTP, manipular componentes de UI, enviar correos electrónicos y ejecutar consultas de base de datos en una sola función.
3.4 Preserva el comportamiento previsto
Antes de refactorizar, identifica las reglas de negocio existentes y los resultados esperados.
No cambies silenciosamente:
- Los criterios de calificación de leads.
- Las reglas de transición del pipeline.
- Las asignaciones de representantes de ventas.
- Las reglas de programación de seguimiento.
- El comportamiento de detección de duplicados.
- Los permisos y controles de acceso.
- Las formas de las respuestas de API.
- La semántica de la base de datos.
Si el comportamiento existente parece incorrecto o ambiguo, identifica la preocupación explícitamente en lugar de disfrazar un cambio de comportamiento como una refactorización.
3.5 Prefiere código simple sobre abstracciones innecesarias
No introduzcas patrones de diseño, clases, genéricos, fábricas o dependencias adicionales a menos que resuelvan un problema real.
Una función directa con un nombre descriptivo suele ser mejor que una abstracción innecesariamente elaborada.
4. Ejemplo de CRM de Ventas Mal Escrito
El siguiente código es intencionalmente problemático. Ilustra problemas comunes de calidad de código en una aplicación de gestión de leads.
4.1 Antes: Código mal escrito
// bad-crm.ts
export async function go(d: any, db: any, mail: any) {
try {
let x = d.name;
let e = d.email;
let p = d.phone || "";
let s = d.status || "new";
let v = Number(d.value || 0);
let u = d.user || null;
if (!x || !e) {
return { ok: false, msg: "bad data" };
}
let old = await db.leads.findOne({ email: e });
if (old && old.status != "lost") {
old.v = old.v + v;
old.s = v > 5000 ? "hot" : old.s;
old.p = p || old.p;
await db.leads.update(old.id, old);
if (old.s == "hot") {
await mail.send(
old.email,
"Hot lead",
"Lead is hot!"
);
}
return { ok: true, data: old };
}
let a = {
name: x,
email: e,
phone: p,
status: s,
value: v,
user: u,
created: new Date(),
updated: new Date(),
};
let r = await db.leads.insert(a);
if (v > 5000) {
await mail.send(
e,
"Hot lead",
"Lead is hot!"
);
}
return { ok: true, data: r };
} catch (e) {
console.log(e);
return { ok: false, msg: "error" };
}
}
4.2 Problemas que el agente debe identificar
| Problema | Ejemplo | Por qué es importante | Mejora |
|---|---|---|---|
| Nombre de función poco claro | go() | No comunica la operación de negocio | Usa createOrUpdateLead() si describe con precisión el comportamiento |
| Variables poco claras | d, x, e, p, s, v, u, a, r | Los desarrolladores deben inferir el propósito de cada variable | Usa nombres específicos del dominio |
| Tipos inseguros | any para entrada, base de datos y servicio de correo | Elimina la capacidad de TypeScript para detectar muchos errores | Define interfaces y tipos de entrada explícitos |
| Regla de negocio oculta | v > 5000 | El umbral de calificación no está explicado | Define una constante nombrada o una regla de negocio documentada |
| Terminología inconsistente | status, hot, lost | El estado del lead y la calificación pueden representar conceptos diferentes | Define un modelo de estado de lead explícito |
| Responsabilidades mixtas | Una función valida, consulta, actualiza, crea, envía correos y maneja errores | Los comportamientos individuales son difíciles de probar | Separa las responsabilidades de aplicación, dominio e infraestructura |
| Posible inconsistencia de datos | Una actualización de base de datos puede tener éxito antes de que falle un correo | El llamador puede recibir un error aunque el lead se haya guardado | Define explícitamente el comportamiento de fallo de notificación |
| Validación débil | Solo verifica x y e | Direcciones de correo inválidas, valores incorrectos y campos malformados pueden ingresar al sistema | Valida la entrada y los invariantes de negocio |
| Reglas de estado implícitas | Los leads existentes se actualizan a menos que su estado sea "lost" | El significado de esta regla está oculto | Nombra y documenta la política de actualización |
| Contrato de persistencia poco claro | db.leads.findOne() y .update() usan formas no documentadas | Las suposiciones de la base de datos son difíciles de verificar | Define una interfaz de repositorio |
| Respuesta de error genérica | "error" | Pierde contexto útil y complica la resolución de problemas | Maneja explícitamente los fallos esperados y registra los fallos inesperados |
| Respuestas inconsistentes | { ok, msg } y { ok, data } | Los llamadores deben inferir qué campos existen en cada caso | Usa un tipo de resultado discriminado o un modelo de error de aplicación consistente |
| Datos mutables y poco estructurados | old.v, old.s, old.p | Los nombres de campo no coinciden con el modelo persistido aparente | Define un tipo Lead consistente |
| Política de duplicados poco clara | Los registros existentes se fusionan por correo electrónico | El significado exacto de la fusión está oculto | Documenta qué campos se pueden actualizar y cómo se agrega el valor |
| Dependencias difíciles de probar | La base de datos y el servicio de correo se pasan como any | Los contratos y casos de fallo no son verificados por el compilador | Define interfaces estrechas e inyecta dependencias |
Importante: Este es un ejemplo ilustrativo, no una implementación completa de CRM de producción. Una refactorización debe primero confirmar el comportamiento real del CRM existente, el modelo de datos, la semántica de persistencia y las reglas de negocio.
5. Flujo de Trabajo de Refactorización
Sigue este flujo de trabajo al mejorar código TypeScript existente.
Paso 1: Entiende el contexto
Inspecciona el código relevante antes de modificarlo.
Identifica:
- El propósito de la función.
- Sus llamadores y dependencias.
- Sus formatos de entrada y salida.
- El esquema de la base de datos o el contrato del repositorio.
- Las entidades del dominio y la terminología de negocio.
- Las reglas de validación y autorización existentes.
- Las pruebas actuales y el comportamiento esperado.
- El manejo de errores y los efectos secundarios.
Lee los módulos circundantes cuando sea necesario. No infieras el contrato completo de una función solo por su nombre.
Paso 2: Registra el comportamiento actual
Describe la lógica existente en lenguaje sencillo.
Para el ejemplo anterior, el comportamiento observado es:
- Leer los detalles del lead desde la entrada.
- Rechazar una solicitud si falta el nombre o el correo electrónico.
- Buscar un lead existente por correo electrónico.
- Si existe un lead coincidente y su estado no es
"lost", agregar el valor enviado al valor existente. - Marcar ese lead existente como
"hot"cuando el valor resultante supera el umbral. - Reemplazar su número de teléfono cuando se proporciona un número de teléfono nuevo no vacío.
- Actualizar el registro y enviar una notificación si el estado resultante es
"hot". - De lo contrario, crear un nuevo registro con marcas de tiempo.
- Notificar al lead cuando su valor enviado supera el umbral.
- Devolver un objeto de éxito o fracaso.
Esta descripción distingue intencionalmente el comportamiento de implementación observado del comportamiento de negocio previsto. Por ejemplo, el código prueba el valor resultante del registro existente para una ruta de notificación, pero el valor enviado para otra. No trates estas reglas como equivalentes silenciosamente.
Paso 3: Mejora los nombres
Reemplaza los nombres genéricos con nombres que expresen su función.
| Nombre pobre | Nombre mejor |
|---|---|
go | createOrUpdateLead |
d | leadInput |
x | leadName |
e | emailAddress, o error en un bloque catch |
p | phoneNumber |
s | leadStatus |
v | leadValue |
u | salesRepresentativeId |
old | existingLead |
a | newLead |
r | createdLead |
db | leadRepository o databaseClient, dependiendo de su función real |
mail | emailNotificationService |
El nombre mejorado debe representar con precisión el valor. No renombres un valor a salesRepresentative si es solo un identificador; usa salesRepresentativeId.
Paso 4: Haz cumplir el tipado seguro
Define tipos para cada concepto de dominio.
type LeadStatus = "new" | "contacted" | "qualified" | "proposal" | "negotiation" | "won" | "lost";
interface LeadInput {
name: string;
email: string;
phone?: string;
status?: LeadStatus;
value: number;
salesRepresentativeId?: string;
}
interface Lead extends LeadInput {
id: string;
createdAt: Date;
updatedAt: Date;
}
Valida los datos de entrada contra estos tipos en el límite del sistema.
Paso 5: Separa las responsabilidades
Divide la función monolítica en partes más pequeñas con un solo propósito.
-
Capa de aplicación: Coordina las operaciones. Recibe la entrada, llama a las capas de dominio e infraestructura, y da forma a la respuesta.
-
Capa de dominio: Contiene las reglas de negocio como la calificación de leads y la lógica de actualización.
-
Capa de infraestructura: Gestiona la persistencia, el envío de correos electrónicos y otras interacciones externas.
Paso 6: Simplifica la lógica
Reemplaza condiciones anidadas con cláusulas de guarda y retornos tempranos.
// Prefiere esto:
if (!leadInput.name || !leadInput.email) {
return { ok: false, msg: "bad data" };
}
// Más lógica sin anidamiento adicional...
Paso 7: Maneja los errores de forma robusta
Define resultados explícitos para cada camino de fallo esperado.
type LeadResult =
| { ok: true; lead: Lead }
| { ok: false; error: "validation_error"; details: string }
| { ok: false; error: "not_found" }
| { ok: false; error: "notification_failed"; leadId: string };
Paso 8: Añade pruebas
Escribe pruebas que verifiquen el comportamiento observado antes de hacer cualquier cambio. Luego refactoriza y asegúrate de que las pruebas sigan pasando.
Paso 9: Cambios incrementales
Refactoriza en pequeños pasos. Después de cada cambio, ejecuta las pruebas. No mezcles cambios de comportamiento con refactorizaciones de estilo.
6. Refactorización de Referencia
Aquí hay una versión refactorizada del ejemplo de lead. Esta versión es una posible implementación, no la única correcta. Una refactorización real validaría primero las reglas de negocio reales del CRM.
// refactored-crm.ts
import { z } from "zod"; // o usa una biblioteca de validación similar
// --- Capa de dominio ---
export type LeadStatus = "new" | "contacted" | "qualified" | "proposal" | "negotiation" | "won" | "lost";
export interface Lead {
readonly id: string;
readonly name: string;
readonly email: string;
readonly phone: string | null;
status: LeadStatus;
readonly value: number;
readonly salesRepresentativeId: string | null;
readonly createdAt: Date;
updatedAt: Date;
}
export const HIGH_VALUE_THRESHOLD = 5000;
export const HIGH_VALUE_STATUS: LeadStatus = "qualified";
export function isHighValueLead(value: number): boolean {
return value >= HIGH_VALUE_THRESHOLD;
}
// --- Capa de repositorio (interfaz) ---
export interface LeadRepository {
findByEmail(email: string): Promise<Lead | null>;
update(lead: Lead): Promise<void>;
create(leadData: Omit<Lead, "id" | "createdAt" | "updatedAt">): Promise<Lead>;
}
// --- Capa de servicio de notificación (interfaz) ---
export interface NotificationService {
notifyHighValueLead(email: string, leadId: string): Promise<void>;
}
// --- Capa de aplicación ---
const LeadInputSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
phone: z.string().optional(),
status: z.enum(["new", "contacted", "qualified", "proposal", "negotiation", "won", "lost"]).optional(),
value: z.number().nonnegative(),
salesRepresentativeId: z.string().optional(),
});
export type LeadInput = z.infer<typeof LeadInputSchema>;
export type CreateOrUpdateLeadResult =
| { ok: true; lead: Lead }
| { ok: false; error: "validation_error"; details: string }
| { ok: false; error: "notification_failed"; leadId: string }
| { ok: false; error: "unknown_error"; details: string };
export async function createOrUpdateLead(
leadInput: LeadInput,
leadRepository: LeadRepository,
notificationService: NotificationService
): Promise<CreateOrUpdateLeadResult> {
const parseResult = LeadInputSchema.safeParse(leadInput);
if (!parseResult.success) {
return {
ok: false,
error: "validation_error",
details: parseResult.error.message,
};
}
const validInput = parseResult.data;
try {
const existingLead = await leadRepository.findByEmail(validInput.email);
if (existingLead && existingLead.status !== "lost") {
const updatedLead: Lead = {
...existingLead,
value: existingLead.value + validInput.value,
status: isHighValueLead(existingLead.value + validInput.value)
? HIGH_VALUE_STATUS
: existingLead.status,
phone: validInput.phone ?? existingLead.phone,
updatedAt: new Date(),
};
await leadRepository.update(updatedLead);
if (updatedLead.status === HIGH_VALUE_STATUS) {
try {
await notificationService.notifyHighValueLead(updatedLead.email, updatedLead.id);
} catch {
return { ok: false, error: "notification_failed", leadId: updatedLead.id };
}
}
return { ok: true, lead: updatedLead };
}
const newLead = await leadRepository.create({
name: validInput.name,
email: validInput.email,
phone: validInput.phone ?? null,
status: validInput.status ?? "new",
value: validInput.value,
salesRepresentativeId: validInput.salesRepresentativeId ?? null,
updatedAt: new Date(),
});
if (isHighValueLead(validInput.value)) {
try {
await notificationService.notifyHighValueLead(newLead.email, newLead.id);
} catch {
return { ok: false, error: "notification_failed", leadId: newLead.id };
}
}
return { ok: true, lead: newLead };
} catch (error) {
return {
ok: false,
error: "unknown_error",
details: error instanceof Error ? error.message : String(error),
};
}
}
7. Notas Finales
- Siempre preserva el comportamiento previsto. Los cambios de funcionalidad deben ser explícitos.
- Usa nombres que tengan significado en el negocio. Escribe código que los desarrolladores puedan leer sin adivinar.
- Define la base de datos y los contratos de la API de forma explícita. No confíes en que los objetos son implícitamente correctos.
- Usa un manejo de errores consistente. Haz que los fracasos sean visibles y trazables.
- Simplifica gradualmente. La deuda técnica se acumula con el tiempo y se resuelve con mejoras pequeñas y consistentes.
- Valida en el límite y asume datos limpios dentro del dominio.
- Prueba el comportamiento, no la implementación. Concéntrate en reglas de negocio y casos extremos.