diff --git a/dashboard.html b/dashboard.html
index ca78c4c..f4f4d46 100644
--- a/dashboard.html
+++ b/dashboard.html
@@ -3717,6 +3717,68 @@
return { kind, certainty, signals, funded, spent, residual, txCount, sweepCount };
}
+ // Cambio conductual: para cada output de una tx, ¿se gasta rápido (pocos
+ // bloques después) o queda quieto (no gastado todavía)? spendInfo ya viene
+ // resuelto por el motor de rastreo desde /api/address/{addr}/txs — esta
+ // función no hace peticiones, solo interpreta.
+ // Umbral de "rápido" = 6 bloques, igual que el barrido automático de
+ // addressProfile, por coherencia dentro del módulo.
+ function behavioralChangeGuess(tx, spendInfo) {
+ const outputs = (tx.vout||[]).map((v,i) => ({ index:i, ...(spendInfo.get(i)||{spent:false,blocksLater:null}) }));
+ const quiet = outputs.filter(o => !o.spent).map(o=>o.index);
+ const fast = outputs.filter(o => o.spent && o.blocksLater != null && o.blocksLater <= 6).map(o=>o.index);
+ return { outputs, quiet, fast };
+ }
+
+ // Cruza la señal estructural (guessChangeOutput) con la conductual: cuando
+ // coinciden —el output señalado como cambio queda quieto mientras el otro
+ // se mueve rápido— la certeza sube un nivel. Cuando discrepan, se reporta
+ // la discrepancia en vez de forzar una conclusión. "Queda quieto" solo
+ // significa "no gastado todavía", nunca una prueba por sí sola.
+ function combineChangeSignals(structural, behavioral) {
+ if (!structural.identifiable) {
+ return { index: structural.index, certainty: null, agreement: null,
+ note: "Sin señal estructural suficiente para cruzar con el comportamiento." };
+ }
+ const idx = structural.index;
+ const otherIdx = idx === 0 ? 1 : 0;
+ const idxQuiet = behavioral.quiet.includes(idx);
+ const idxFast = behavioral.fast.includes(idx);
+ const otherQuiet = behavioral.quiet.includes(otherIdx);
+ const otherFast = behavioral.fast.includes(otherIdx);
+
+ if (idxQuiet && otherFast) {
+ return { index: idx, certainty: "PROBABLE", agreement: "coincide",
+ note: `El output señalado como cambio (#${idx}) sigue sin gastar mientras el otro (#${otherIdx}) se movió pocos bloques después de esta tx — coincide con lo esperado de un cambio.` };
+ }
+ if (idxFast && otherQuiet) {
+ return { index: idx, certainty: structural.certainty, agreement: "discrepa",
+ note: `El output señalado estructuralmente como cambio (#${idx}) se movió rápido mientras el otro (#${otherIdx}) sigue quieto — discrepa con la señal estructural. No se fuerza una conclusión: se deja constancia de la discrepancia.` };
+ }
+ return { index: idx, certainty: structural.certainty, agreement: "sin_dato",
+ note: "Sin datos de gasto suficientes (ambos gastados o ambos quietos) para cruzar con la señal estructural." };
+ }
+
+ // Compara la huella de software (detectWallets) entre dos saltos
+ // consecutivos del rastro. Un cambio de huella es INFERENCIA/POSIBLE de
+ // "cambio de actor o entrada en infraestructura de un servicio" — nunca
+ // CERTEZA, porque una misma persona puede cambiar de wallet sin cambiar
+ // de dueño de los fondos.
+ function compareFingerprints(prevFp, nextFp) {
+ const prevTop = prevFp?.[0]?.name || null;
+ const nextTop = nextFp?.[0]?.name || null;
+ if (!prevTop || !nextTop) {
+ return { changed: null, certainty: null,
+ note: "Huella insuficiente en uno de los dos saltos para comparar." };
+ }
+ if (prevTop === nextTop) {
+ return { changed: false, certainty: null,
+ note: `Misma huella de software en ambos saltos (${prevTop}) — compatible con el mismo actor.` };
+ }
+ return { changed: true, certainty: "POSIBLE",
+ note: `La huella cambia de ${prevTop} a ${nextTop} entre saltos — posible cambio de actor o entrada en infraestructura de un servicio (custodio, exchange). No es una prueba por sí sola.` };
+ }
+
// Eslabón recursivo: muestra una tx de origen y permite seguir SUS inputs.
// depth limita la profundidad visual; cache y getTx se comparten desde arriba.
function Eslabon({txData, fluyo, vout, depth, getTx, base}) {