Opinión técnica · El porqué, no el qué Junio de 2026
Comentarios de código en la era de los agentes de IA
El código ya se produce en masa y quien más tiempo pasa leyéndolo es un agente de IA. Construí una app entera con uno, en un lenguaje que no domino, y el resultado me obligó a repensar la vieja regla de «el código no se comenta».
Estamos en un punto en el que el código se produce en masa y quien más lo lee es un agente de IA. Creo que eso obliga a repensar cosas que dábamos por cerradas, y una de ellas es el uso de los comentarios.
Durante años escribí los mínimos comentarios posibles: mantenerlos era caro y acababan mintiendo. Pero la historia ha cambiado: el código lo genera un agente, el lector principal es otro agente, y hay hooks capaces de verificar que un comentario sigue siendo cierto. Escribir un comentario cuesta tokens, sí. ¿Y cuánto cuesta inferir esa misma información cada vez que no está?
Construí una app entera con un agente, en un lenguaje que no domino, y el resultado me obligó a tomarme la pregunta en serio.
La pieza que hace esto sostenible es el arnés del diagrama: hooks de pre-commit y pre-push que lanzan agentes de revisión adversarial sobre cada diff, a la caza de derivas entre el código y sus comentarios (y, como veremos, el resto del conocimiento del proyecto). No es un proceso determinista, pero converge: cada pasada adversarial sincroniza un poco más código y comentarios, y cuantas más revisiones independientes se cruzan, más fácil es distinguir una deriva real de una alucinación.
El contexto
#s1No todos los proyectos son iguales: presupuesto, tiempo y riesgo cambian el enfoque. La reflexión vale para cualquier tamaño de producto, pero nace de un contexto muy concreto: la primera celda del diagrama.
El proyecto
- App iOS: SwiftUI, SwiftData, CloudKit.
- Desarrollada en solitario con Claude Code.
- Vengo de TypeScript: no domino Swift ni la plataforma.
- Sin tablero ni docs de producto: solo el código, el agente y yo.
El resultado
- Velocidad de desarrollo altísima.
- Comprendí el proyecto a fondo sin dominar el lenguaje.
- Muchos más comentarios de los que mi tradición de equipo toleraría.
- Nunca había escrito tantos porqués: ¿es lo óptimo o estoy normalizando un exceso?
La regla de siempre: el porqué, no el qué
#s2Esto no lo he inventado yo: está documentado desde hace décadas, y Robert C. Martin lo recoge en Clean Code (el buen comentario es el que explica la intención). La versión corta del consenso:
- ¿Comentarios, sí o no? Sí, los justos: cada uno es texto que hay que mantener y que puede mentir.
- ¿El qué? Nunca. El lector, humano o agente, infiere el qué del código más rápido y con más fiabilidad que de cualquier explicación escrita. Con IA, además, es coste puro en tokens.
- ¿El porqué? Siempre que el código no pueda mostrarlo: el bug del framework que se esquiva, el contrato interno, el comportamiento que reportó un usuario. Aquí la información no está en el código a ningún precio.
- ¿Dónde se complica? Cuando el porqué suena a requisito: no sabemos cuándo va en documentación separada y cuándo pegado al código. Ese es el verdadero debate, y lo dejo para más abajo.
Dos comentarios de mi código real
#s3Los dos bloques que siguen los escribió el agente en mi código real. La pregunta para cada uno es la misma: ¿qué información contienen que el código no contiene?
El primero gestiona la identidad del usuario al importar una planilla. El código muestra un estado y un @AppStorage; no muestra por qué la identidad no siempre se resuelve sola, por qué existe un selector «¿Quién eres tú?» ni por qué el nombre se ancla a la grafía de la planilla recibida. Y deja una pista clave: ADR-0005 §4, el comentario local apuntando a la decisión transversal. El segundo es críptico a propósito: el comentario documenta el contrato, por qué la identidad del modelo no basta y hay que comparar por valor.
/// Planilla recibida cuyo receptor no se
/// resolvió solo: presenta el selector
/// "¿Quién eres tú?" (ADR-0005 §4).
@State private var identityPick: IncomingPlanilla?
/// Nombre del usuario (su fila isSelf).
/// Auto-resuelve la identidad al importar
/// un fichero y se ancla a la grafía de
/// la planilla recibida.
@AppStorage("import.userName") private var userName: String = "" /// Firma de contenido de los turnos
/// personalizados. `onChange` la observa
/// para refrescar la caché cuando cambia
/// cualquier campo, no solo el alta o
/// baja: editar nombre, color o
/// clasificación no cambia la identidad
/// del modelo: se compara por valor.
private var customShiftSignature: [String] {
customShiftModels.map {
"\($0.code)\u{1}\($0.label)\u{1}\($0.colorHex)\u{1}\($0.isWork)"
}
} Una auditoría como banco de pruebas
#s4Hace poco lancé sobre el proyecto una auditoría completa de seguridad: varios agentes en paralelo, movidos por Fable, el último modelo de frontera de Claude Code, leyendo todo el código fuente. El objetivo era buscar leaks de seguridad, no evaluar cómo de bien estaba autodocumentado el proyecto. Y aun así, la auditoría no solo agradeció que el código estuviera comentado: me lo demostró. Repetí exactamente el mismo análisis tras eliminar todos los comentarios, y la comparativa nos proporcionó algo más de advertencia y perspectiva sobre estos dos caminos:
- Con comentarios: la auditoría citó la densidad de porqués como lo que le evitó re-derivar cada decisión. Fue más barata, más rápida y con un veredicto más fiable.
- Sin comentarios: sobre el mismo código sin un solo comentario, gastó muchos más tokens, tardó más y alucinó más. Cada sesión vuelve a pagar la re-derivación y cada edición equivocada añade un pico.
Más allá de los números, esto es lo que dejó la auditoría:
- El veredicto me sorprendió: sin los comentarios, los hacks documentados de SwiftUI habrían parecido ruido a limpiar, y la auditoría me habría empujado a introducir regresiones.
- Razonamiento persistido entre sesiones: el agente escribió cada comentario con el contexto completo (el bug reproducido, la alternativa descartada); cada sesión futura, de la IA o de un humano, lo hereda por el precio de leerlo.
- ¿Y el exceso de contexto? Degrada cuando es irrelevante o contradictorio; el comentario co-localizado es lo contrario: solo entra en contexto cuando se toca ese fichero.
- El riesgo real es la obsolescencia: un comentario incorrecto es un bug y se revisa como un test roto. En un flujo agéntico, un hook de pre-commit puede vigilar esa deriva (da para un post aparte); y cuando la caducidad no nace de un diff sino de fuera del repo: #s5.
// la lecciónUna revisión es tan buena como el contexto que puede leer: sin el porqué escrito, un hack que esquiva un bug parece ruido que limpiar.
Cuando el comentario caduca por motivos externos
#s5El ejemplo del bug del framework deja una pregunta en el aire: ¿cómo detectas que la siguiente versión de la librería arregla el bug? Vuelve al hack de SwiftUI de la auditoría: hoy el workaround esquiva un bug real, y su comentario es lo que protege el código de una limpieza bienintencionada. El día que Apple arregle ese bug en un SDK nuevo, sin avisar, la situación se invierte: el workaround pasa a ser complejidad muerta y el comentario, cierto hasta ayer, empieza a mentir. El arnés vigila la deriva entre código y comentario, pero esta deriva no nace en el repo: llega de fuera.
El primer instinto es alimentar el hook con los changelogs de las dependencias. No se sostiene:
- No hay fuente estructurada: puede ser un CHANGELOG.md, GitHub Releases, un post en un blog o nada en absoluto.
- Son incompletos por naturaleza: fixes agrupados en «misc» o arreglados de rebote sin documentar.
- El match comentario↔changelog es semántico, no léxico, y su coste es asimétrico: el falso positivo retira un workaround vigente y te regala una regresión; el falso negativo deja complejidad muerta para siempre.
- En mi caso real ni siquiera hay changelog: para los bugs de SwiftUI, las release notes de Apple son incompletas y Feedback Assistant es privado.
- La salida no es un parser de changelogs mejor: es convertir el comentario en algo que una máquina pueda verificar. El porqué se sigue escribiendo igual; lo que se estandariza son las anclas que lo acompañan. Tres, de más fuerte a más débil:
| Ancla | Cómo avisa | Su límite |
|---|---|---|
| Test canario | Un test que reproduce el bug y fija el comportamiento roto: XCTExpectFailure pasa mientras el bug se reproduce y falla el día que el framework lo arregla (equivalentes: withKnownIssue, test.failing, xfail). Señal computacional pura, y el único sensor que también detecta que un SDK nuevo rompe distinto. | No todo bug es testeable: visuales, timing, device-only. |
| Issue público | El estado del issue es estructurado y consultable por API (gh api … --jq .state): un issue cerrado es el propio proyecto afirmando el fix, con su PR y su milestone. Si no existe y la librería es open source, reportarlo es el mejor ratio coste/beneficio de la escalera. | Necesita tracker público; el de Apple es privado. |
| Cota de versión | dep=swiftui sdk<=15.4: el bump del lockfile o del SDK marca el comentario para reevaluación, no para retirada. Es el patrón de expiring-todo-comments (ESLint) y todo_or_die (Ruby). | La más débil: dice «mira esto», no «está arreglado». |
Las tres no compiten: se apilan, cada una donde compensa su coste:
- La cota de versión, en todos los workarounds sin excepción: cuesta cero y un linter puede exigirla: un WORKAROUND sin
dep=y versión no pasa. - El issue, en todos los que tengan tracker, creándolo cuando aún no existe.
- El canario, solo donde duele: workarounds cuya regresión sale cara (pérdida de datos) o cuyo bug es silencioso.
- Las tres juntas en un workaround crítico no son redundancia: son defensa en profundidad.
- ¿Y el changelog? Queda como último recurso: un agente con el changelog y el diff entre versiones como contexto, cuyo output es siempre una propuesta con evidencia citada, nunca una retirada automática. La confirmación final es conductual: canario en verde o verificación manual.
// el detectorEl bump de una dependencia lista los tags afectados y cada uno se resuelve con el detector más fuerte disponible: canario, issue, reevaluación; solo el caso residual quema un agente leyendo changelogs. El formato exacto del comentario estandarizado queda fijado en las reglas (#s10); el pipeline completo ya es harness engineering y da para su propio post.
¿Dónde debe vivir cada conocimiento?
#s6Lo que nace observando comentarios en el código acaba evolucionando en una pregunta más amplia: dónde almacenamos la información del proyecto. Es el quid de la cuestión, y una objeción razonable que cabe plantearse: eso que escribes en comentarios son requisitos, funcionales o no funcionales, y deberían vivir en un documento aparte. Es verdad que es fácil mezclar comentarios con documentación. Pero muchos de estos porqués no son requisitos de producto a ningún nivel: son bugs del framework, contratos internos, alternativas descartadas. El test que propongo no es ¿es funcional o no funcional?, sino: ¿con qué cambia este conocimiento?
| Dónde vive | Cuándo usarlo | Ejemplo |
|---|---|---|
| Spec (OpenSpec) | Comportamiento de producto: cambia cuando cambia el producto. | «El plan gratuito tiene un límite diario de planillas.» |
| ADR | Decisión transversal: cruza módulos y sobrevive a reescrituras del código. | ADR-0005: cómo se resuelve la identidad del usuario en toda la app. |
| Comentario de porqué | Restricción local: cambia con el código y se ve entera desde una función. Si depende de una decisión transversal, enlaza el ADR. | El contrato de la caché; el hack que esquiva un bug de SwiftUI (ADR-0005 §4). |
| Test | Comportamiento observable: debe seguir siendo cierto aunque el código se reescriba. | «Importar dos veces la misma planilla no duplica turnos.» |
La tabla clasifica; lo que no dice es por dónde empezar ni cómo se relacionan sus filas entre sí. En corto:
- La primera opción es siempre el propio código: buenos nombres, tipos, invariantes y, con DDD, el propio modelo de dominio. Solo hay que buscar fila a lo que el código no puede mostrar.
- Las filas se enlazan entre sí: un porqué que cambia con el código puede vivir en un documento aparte si la función deja un comentario apuntando a él. Funciona, pero son dos piezas que mantener sincronizadas y hace falta un arnés que vigile la deriva; ese trabajo desaparece cuando el porqué va pegado al código.
- Y en sentido inverso: el comportamiento de producto vive en su spec, pero el código debería apuntar a ella.
- Con ese criterio, la objeción se desmonta sola: «si la identidad no se resuelve sola, pregunta al usuario» sí es un micro-requisito; el anclaje a la grafía es implementación pura; y la parte transversal ni se copia ni se pierde: se enlaza (ADR-0005).
- ¿Y no serán ADRs encubiertos? El comentario de porqué es, en el fondo, un micro-ADR inline: demasiado pequeño para merecer fichero propio, demasiado no obvio para omitirlo. En mi proyecto conviven sin fricción: ADRs para decisiones transversales, comentarios para restricciones locales.
- El test merece mención propia: es la única documentación que no puede mentir mucho tiempo (un test desactualizado es un test rojo) y cumple cuatro propósitos según el momento: especificar antes de escribir el código, verificar al escribirlo, proteger como red de regresión y documentar con ejemplos ejecutables.
- Test y comentario son complementos, no sustitutos: el test congela el qué observable; el comentario guarda el porqué que el test no puede afirmar. Sin el test, un agente puede romper el comportamiento sin enterarse; sin el comentario, puede «arreglar» código correcto.
// la heurísticaSi la decisión cruza módulos o sobrevive a reescrituras del código, pide un ADR. Si es una restricción local que vive y muere con el código que la rodea, normalmente basta un comentario.
Specs y ADRs en el repositorio
#s7Para gestionar las especificaciones, ahora mismo me parece muy relevante OpenSpec: specs vivas de lo ya construido y propuestas de cambio que, al archivarse, actualizan las specs. Y el directorio de ADRs conviene tenerlo también repositado.
Que todo viva en el repositorio hace trivial la integración con los agentes: pueden leer las specs y los ADRs, citarlos (como el ADR-0005 del ejemplo 1) y vigilar su sincronía con el código. Abajo, el ciclo de OpenSpec y el árbol mínimo que esperaría encontrar.
repo/
├── openspec/
│ ├── project.md # convenciones y contexto
│ ├── specs/ # lo que YA está construido
│ └── changes/ # propuestas en vuelo
│ └── archive/ # al completarse, actualizan specs/
├── docs/
│ └── adr/ # decisiones transversales
│ └── ADR-0005-identidad-usuario.md
├── src/ # código + porqués co-localizados
└── tests/ # el qué observable, congelado La estructura crece con el proyecto
#s8Ese árbol es un punto de partida, no una talla única. Y la documentación no sube por niveles: se compone por grupos de piezas. Está el código con sus porqués y los tests de lo crítico (el suelo común de cualquier repo); las decisiones (ADRs, convenciones); el comportamiento de producto (specs vivas, contratos); y el modelo compartido: DDD, donde el código comparte el modelo de dominio con producto (el lenguaje ubicuo), se vuelve en gran parte autodocumentado y sus unit tests de dominio son lo más parecido que existe a una spec ejecutable; los comentarios que quedan ahí son los técnicos: frameworks, contratos, rendimiento.
Cada proyecto combina los grupos que su contexto pide, y la mezcla no es determinista: cuanta más gente y más riesgo, más rica tiende a ser la combinación, pero es una tendencia, no una regla. Este proyecto es unipersonal y tiene ADRs; tengo otro, también unipersonal, con OpenSpec; hay proyectos sin DDD y proyectos que no testean igual. Y algo ha cambiado con los agentes: en un proyecto greenfield, dejar la documentación montada desde el día uno es más barato que nunca, porque el agente la genera y la mantiene contigo.
¿Se puede meter toda la documentación que exige un proyecto de cien personas en uno de una? Poderse, se puede. La pregunta es qué obtienes a cambio: pierdes velocidad hoy y el beneficio no crece al mismo ritmo; son rendimientos decrecientes. En el lado contrario de la balanza, diseñar la estructura desde el principio abarata adoptarla después. ¿Dónde parar, entonces? No hay una respuesta universal: depende de la naturaleza del proyecto, y cada escenario salda la balanza de forma distinta:
- Un MVP o un experimento: estructura mínima. Muchos proyectos se quedan por el camino; si este muere pronto, no habrás enterrado tiempo en documentación que nadie leerá: lo habrás usado de forma más eficiente.
- Un producto pequeño con intención de crecer: los proyectos tienden a crecer, y lo difícil es gestionar ese crecimiento. La dedicación que inviertes al inicio es la que se cobra al final: cada porqué y cada decisión escritos hoy son contexto que nadie tendrá que reconstruir mañana.
- Algo pensado para escalar desde el día uno: montar la documentación exhaustiva de entrada no es incorrecto, es una inversión; con agentes, además, cuesta menos que nunca.
- Mi caso: en el proyecto de este post me ha funcionado empezar pequeño e ir incrementando la documentación junto al proyecto. Es la respuesta para esta naturaleza de proyecto, no una verdad universal.
// el criterioNo hay un óptimo universal de documentación: lo marca la naturaleza del proyecto, y se mueve con ella.
Cuando la localidad no basta: la vista agregada
#s9Todo lo anterior optimiza una sola cosa: la localidad. Cada porqué vive pegado a su uso, perfecto para editar y para un agente que toca un único fichero. Pero en el extremo alto de la escala de la sección anterior sobrevive una objeción legítima, la que plantea un responsable de QA, un auditor o alguien de compliance: con el conocimiento distribuido, ¿cómo respondo a ¿cuáles son todos los requisitos funcionales?, ¿qué test cubre cada uno?, ¿qué NFR justifica este ADR?, ¿qué cambió con esta feature? La localidad dispersa justo la vista global que necesita la auditoría. Es la tensión clásica, localidad de referencia frente a trazabilidad, y solo aparece en el extremo regulado y multiequipo de la escalera: un MVP en solitario nunca hace estas preguntas; un producto bajo auditoría las hace a todas horas.
| ID | Type | Source | ADR | Tests | Status |
|---|---|---|---|---|---|
| FR-014 | Functional | import-planilla.md | — | ImportPlanillaTests | covered |
| NFR-003 | Privacy | privacy.md | ADR-0007 | PrivacyTests | covered |
| NFR-004 | Performance | perf.md | ADR-0008 | — | gap: no test |
El arreglo ingenuo es un requirements-index.md y una traceability-matrix.md mantenidos a mano. No se sostiene: una matriz a mano es una segunda fuente de verdad que diverge de specs, tests y ADRs, y necesitaría su propio harness vigilando la deriva; estarías resolviendo el problema recreando justo lo que este post combate. El arreglo es de una palabra: la vista agregada es derivada, no escrita. Un agente o un script recorre lo que ya existe y computa el inventario: los requisitos funcionales desde las specs, la justificación de los NFR desde los ADRs, la cobertura desde los nombres de los tests, el estado desde el delta spec↔código que el post ya llama computable. Es una salida del harness, como un informe de cobertura: se regenera en CI, nunca se edita.
- Dos ejes ortogonales, no uno que sustituye al otro: ¿con qué cambia? decide dónde vive la fuente (la heurística anterior); ¿funcional o no funcional? es la lente con la que ordena la vista agregada. Un requisito se escribe una vez en su spec y aparece como fila; no se guarda dos veces.
- Los NFR son donde esto rinde: «borra el PDF tras procesarlo» se reparte entre una spec (la regla), un ADR (el cómo) y un test (la prueba). Un FR suele vivir en una sola spec; un NFR casi nunca. La matriz es lo que recompone el NFR disperso en una única fila auditable.
- La vista informa y audita a la vez: un FR sin test, o un NFR sin ADR que lo justifique, sale como hueco, la misma alerta roja que una línea sin cubrir. La matriz no solo responde preguntas, destapa lo que falta.
- «¿Qué cambió con esta feature?» se vuelve un diff: regeneras la vista en dos commits y los comparas, en vez de una sesión de arqueología por specs, ADRs y tests.
// la proyecciónLa vista agregada es una proyección del repo, no una copia paralela: se escribe una vez en specs, ADRs y tests; se lee de muchas formas. El día que se mantiene a mano, ya ha empezado a mentir.
Las reglas, en corto
#s10Para mi contexto actual, desarrollo en solitario con IA, el enfoque ha funcionado muy bien, y la idea de fondo se exporta: la documentación se puede repositar. Cuando specs, ADRs y porqués comparten repositorio con el código, el clásico «los docs van por un lado y el código por otro» desaparece, y el crecimiento del sistema sale beneficiado: quien desarrolla, humano o agente, usa la documentación sin salir del repo en vez de ir a buscarla fuera. Y cuando producto y desarrollo no son el mismo equipo, el riesgo es el comportamiento de producto atrapado en el código, donde un tester o un PM nunca lo encontrará; ahí se pueden plantear herramientas que sincronicen la documentación y la proyecten hacia fuera, frontends que generen la vista de producto desde el repositorio, en lugar de mantener dos fuentes que divergen. En equipo, eso sí, negociaría la densidad: ocho líneas de doc-comment tienen un coste de lectura humana que el agente no paga.
- Nunca el qué, siempre el porqué que el código no puede mostrar: la regla clásica de «no comentar» no estaba mal, estaba incompleta. Siempre fue una regla contra los comentarios de qué; los dominios con mucha restricción no obvia por línea necesitan más comentarios de porqué.
- Un comentario incorrecto es un bug: se revisa y se mantiene como el código. El qué observable se documenta en un test, la única documentación que se verifica sola.
- El conocimiento vive donde cambia, y todo repositado: porqués con el código, decisiones en ADRs, comportamiento de producto en specs, el qué observable en tests. Un solo repositorio, legible por humanos y por agentes.
- Un arnés vigila la sincronía (harness engineering): hooks de pre-commit y pre-push que lanzan agentes adversariales sobre cada diff, a la caza de derivas entre el código fuente del repo y la documentación del repo.
- El comentario también caduca por motivos externos (#s5): el framework arregla el bug y el workaround se convierte en complejidad muerta. Todo workaround lleva al menos un ancla verificable por máquina: test canario > issue público > cota de versión; un porqué sin ancla no se admite.
- Los deltas se identifican, no se intuyen: con specs vivas (OpenSpec es un gran apoyo aquí), la distancia entre lo que la spec promete y lo que el código hace se vuelve computable, y un agente puede leerla, citarla y reconciliarla.
- En el peldaño de auditoría, la vista agregada se genera, no se escribe (#s9): el inventario FR/NFR y la matriz de trazabilidad son una proyección computada desde specs, ADRs y tests, nunca una segunda fuente mantenida a mano.
- La sincronía no es determinista, pero converge: cada pasada adversarial acerca código y documentación, y cuantas más revisiones independientes se cruzan, más fácil es distinguir una deriva real de una alucinación.
/// WORKAROUND(FrameworkBug): dep=swiftui sdk<=15.4
/// issue=FB13241001
/// repro=Tests/Canaries/FB13241001_Test.swift
/// <el porqué, redactado como siempre> // lo que ha cambiadoLa regla es la de siempre; lo nuevo es que ahora se puede medir: el coste de un comentario se cuenta en tokens y su beneficio se nota en la calidad de las ediciones. Con esa medida, el comentario de porqué es de las inversiones más rentables del repositorio.