KAudit — guía de referencia¶
Referencia por temas del comportamiento y del código generado. Para la visión general, ver el README.
La función generada¶
Por cada clase @Auditable se genera <Simple>Diff.kt en su paquete, con:
public fun Account.diff(other: Account): List<FieldChange>
Recorre los campos en orden de declaración, acumula un FieldChange por diferencia y devuelve
la lista (vacía si no hubo cambios).
Estrategias por campo¶
El builder clasifica cada propiedad (tras excluir las @AuditIgnore) en una estrategia:
| Estrategia | Cuándo | Emisión |
|---|---|---|
Direct |
escalar u objeto anidado no @Auditable |
if (this.f != other.f) changes += FieldChange("f", this.f, other.f) |
Redact |
@Sensitive |
if (this.f != other.f) changes += FieldChange("f", null, null, redacted = true) |
Recurse |
tipo @Auditable |
changes += this.f.diff(other.f).map { it.copy(path = "f." + it.path) } |
OpaqueCollection |
colección sin elemento @AuditKey |
un FieldChange con la lista entera |
KeyedCollection |
colección cuyo elemento tiene @AuditKey |
diff por clave (ver abajo) |
KeyedMap |
Map<K, V> |
diff por la clave natural del mapa (sin @AuditKey) |
SealedRecurse |
propiedad de tipo sealed @Auditable |
mismo subtipo → recorre; subtipo distinto → objeto entero (is, sin reflexión) |
Recurse con propiedad nullable¶
Si el campo @Auditable es nullable, se compara la presencia antes de recursar:
run {
val a = this.owner; val b = other.owner
if (a == null || b == null) { if (a != b) changes += FieldChange("owner", a, b) }
else changes += a.diff(b).map { it.copy(path = "owner." + it.path) }
}
KeyedCollection¶
run {
val oldByKey = this.members.associateBy { it.id }
val newByKey = other.members.associateBy { it.id }
for (k in oldByKey.keys - newByKey.keys) changes += FieldChange("members[" + k + "]", oldByKey.getValue(k), null, kind = ChangeKind.REMOVED)
for (k in newByKey.keys - oldByKey.keys) changes += FieldChange("members[" + k + "]", null, newByKey.getValue(k), kind = ChangeKind.ADDED)
for (k in oldByKey.keys intersect newByKey.keys) {
val a = oldByKey.getValue(k); val b = newByKey.getValue(k)
// elemento @Auditable → se recorre; si no, un MODIFIED por equals:
changes += a.diff(b).map { it.copy(path = "members[" + k + "]." + it.path) }
}
}
- ADDED:
kind = ADDED,old = null,new = <elemento>. - REMOVED:
kind = REMOVED,old = <elemento>,new = null. - MODIFIED: solo para elementos que persisten con la misma clave. Si el elemento es
@Auditable, se recorre campo a campo (members[42].role); si no, un únicoFieldChangecon el elemento entero. - Reordenar sin cambiar contenido → sin cambios (la clave, no el índice, manda).
Recursión y ciclos¶
El generado delega (owner.diff(other.owner)): cada tipo genera su propio diff y se
llama entre sí. No se expande el modelo anidado, así que un tipo autorreferencial
(Node(next: Node?)) produce una función recursiva correcta sin detección de ciclos en
build-time. Un grafo de datos cíclico en runtime (p. ej. a.next === a) haría bucle infinito;
es una limitación documentada, no un caso de auditoría normal.
Decisiones cerradas¶
- Igualdad:
equalsestándar.BigDecimal("1.0") != BigDecimal("1.00")(scale). Override por comparador de tipo → extensión futura, no v1. @AuditKey: tipo con identidad clara (primitivos, String, UUID, value class, enum); nullable prohibido. Ambos son errores de compilación.Array: prohibido como propiedad auditada (equals= identidad de referencia). UsaListo@AuditIgnore.@Sensitive: nunca lleva valor (estructural, no configurable).- Anidado no anotado: opaco (equals sobre el objeto entero).
Diagnósticos¶
| Código | Severidad | Fix sugerido |
|---|---|---|
kaudit.key.nullable |
error | hacer la propiedad no-nullable. |
kaudit.key.type |
error | usar un tipo con identidad clara. |
kaudit.array.property |
error | usar List, o excluir con @AuditIgnore. |
Todos se acumulan en una pasada y apuntan al símbolo de origen (KSPLogger sobre el KSNode).
Incrementalidad¶
Gracias a la delegación, el archivo generado de un tipo depende solo de su propia fuente:
si un tipo anidado cambia, se regenera su propio diff, no el de quien lo usa. El
CodeWriter declara ese origen para KSP (originatingKSFiles).