File size: 19,272 Bytes
06e4b6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
//! `python::pensamento_demos` — extraído do monólito na v0.10.0
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).

use super::*;


/// Classificação de erro por taxonomia (diagnóstico de refatoração).
#[pyfunction]
pub(crate) fn classificar_erro(msg: String) -> Vec<(String, String, String)> {
    diagnostico::classificar_erro(&msg)
        .into_iter()
        .map(|(a, b, c)| (a.to_string(), b.to_string(), c.to_string()))
        .collect()
}


/// Confiança de uma distribuição (T-P1): `1 − H(softmax(z))/log K`.
#[pyfunction]
pub(crate) fn confianca_de_logits(logits: Vec<f32>) -> PyResult<f64> {
    let dev = candle_core::Device::Cpu;
    let k = logits.len();
    let t = candle_core::Tensor::from_vec(logits, (1, k), &dev)
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
    pensamento::confianca_de_logits(&t).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
}


/// Projeção ridge (T-P3 — montador algébrico): `r* = (WᵀW+εI)⁻¹Wᵀr`.
#[pyfunction]
pub(crate) fn projetar_ridge(w: Vec<Vec<f64>>, r_alvo: Vec<Vec<f64>>, eps: f64) -> PyResult<Vec<Vec<f64>>> {
    let dev = candle_core::Device::Cpu;
    let wt = tensor_de_2d(w, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
    let rt = tensor_de_2d(r_alvo, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
    let r = pensamento::projetar_ridge(&wt, &rt, eps)
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
    let out = r.to_vec2::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
    Ok(out.into_iter().map(|l| l.into_iter().map(|v| v as f64).collect()).collect())
}


/// Demo completa da tecnologia Pensamento: treino cooperativo das N redes
/// (item a) + ciclo d com verificador externo simulado + telemetria.
#[pyfunction]
pub(crate) fn demo_pensamento(
    py: Python,
    d_cond: usize,
    d_saida: usize,
    n_tentativas: usize,
    passos_treino: usize,
    limiar_conf: f64,
    orcamento: usize,
    semente: u64,
) -> PyResult<PyObject> {
    use candle_core::Tensor;
    let dev = candle_core::Device::Cpu;
    crate::rng::definir_semente(semente);
    let mut motor = pensamento::MotorPensamento::novo(d_cond, d_saida, n_tentativas, limiar_conf, orcamento, &dev)
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
    let mut otim = crate::treino::AdamWManual::novo(motor.vars(), 5e-3, 0.0);
    let mut rng = crate::rng::RngDeterministico::novo(semente);

    // dados sintéticos: alvo = função determinística não-linear de cond (d=a+b+c)
    let n_dados = 256usize;
    let conds: Vec<Vec<f32>> = (0..n_dados)
        .map(|_| (0..d_cond).map(|_| rng.normal(0.0, 1.0) as f32).collect())
        .collect();
    let alvos: Vec<Vec<f32>> = conds
        .iter()
        .map(|c| {
            (0..d_saida)
                .map(|j| {
                    let cj = c[j % d_cond];
                    let ck = c[(j + 1) % d_cond];
                    0.5 * cj + 0.3 * cj * ck + 0.1
                })
                .collect()
        })
        .collect();
    let cond_t = Tensor::from_vec(conds.into_iter().flatten().collect(), (n_dados, d_cond), &dev)
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
    let alvo_t = Tensor::from_vec(alvos.into_iter().flatten().collect(), (n_dados, d_saida), &dev)
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;

    // item a: treino cooperativo
    let mut perda_hist = Vec::new();
    for passo in 0..passos_treino {
        let perda = motor.perda_treino(&cond_t, &alvo_t)
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
        perda_hist.push(perda.to_scalar::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))? as f64);
        otim.passo(crate::treino::grads_achatados(&perda, &motor.vars())
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?);
        let _ = passo;
    }

    // item c+d: consulta fora do padrão (OOD) — confiança baixa ⇒ Pensamento
    let consulta: Vec<f32> = (0..d_cond).map(|j| 3.0 + (j as f32) * 0.1).collect();
    let consulta_t = Tensor::from_vec(consulta, (d_cond,), &dev)
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
    // verificador externo: negativo do MSE contra o alvo verdadeiro da consulta
    let alvo_verdadeiro: Vec<f32> = (0..d_saida)
        .map(|j| {
            let cj = 3.0 + (j % d_cond) as f32 * 0.1;
            let ck = 3.0 + ((j + 1) % d_cond) as f32 * 0.1;
            0.5 * cj + 0.3 * cj * ck + 0.1
        })
        .collect();
    let alvo_verd = Tensor::from_vec(alvo_verdadeiro.clone(), (d_saida,), &dev)
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
    let verifica = |resp: &Tensor| -> f64 {
        // confiança = 1/(1+10·MSE) — medível e causaL (só da resposta)
        let dif = (resp.broadcast_as((1, d_saida)).unwrap() - &alvo_verd.broadcast_as((1, d_saida)).unwrap()).unwrap();
        let mse = dif.sqr().unwrap().mean_all().unwrap().to_scalar::<f32>().unwrap() as f64;
        1.0 / (1.0 + 10.0 * mse)
    };
    let mut rng2 = crate::rng::RngDeterministico::novo(semente + 1);
    let res = motor.raciocinar(&consulta_t, &verifica, &mut rng2)
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;

    // linha de base SEM pensamento: uma tentativa isolada, sem agregação
    let base = motor.tentativas[0].fwd(&consulta_t.broadcast_as((1, d_cond)).unwrap())
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
    let conf_base = verifica(&base);
    let conf_pensamento = res.confianca;

    let d_ = serde_json::json!({
        "versao": pensamento::versao(),
        "n_tentativas": n_tentativas,
        "perda_treino_inicial": perda_hist.first(),
        "perda_treino_final": perda_hist.last(),
        "treino_aprendeu": perda_hist.last().copied().unwrap_or(f64::INFINITY) < perda_hist.first().copied().map(|p| p * 0.5).unwrap_or(f64::INFINITY),
        "iteracoes_ciclo_d": res.iteracoes,
        "confianca_base_1_rede": conf_base,
        "confianca_pensamento": conf_pensamento,
        "ganho_confianca": conf_pensamento - conf_base,
        "orcamento_esgotado": res.telemetria.get("orcamento_esgotado").copied().unwrap_or(0.0),
        "telemetria": res.telemetria,
        "eventos_ciclo": res.trajetoria.iter().map(|e| match e {
            pensamento::EventoCiclo::Planejamento { iteracao, temperatura } =>
                serde_json::json!({"tipo": "planejamento", "iteracao": iteracao, "temperatura": temperatura}),
            pensamento::EventoCiclo::DistribuicaoCompetencias { por_tentativa } =>
                serde_json::json!({"tipo": "distribuicao", "pesos": por_tentativa}),
            pensamento::EventoCiclo::RePlanejamento { motivo } =>
                serde_json::json!({"tipo": "replanejamento", "motivo": motivo}),
            pensamento::EventoCiclo::Resposta { confianca, iteracoes } =>
                serde_json::json!({"tipo": "resposta", "confianca": confianca, "iteracoes": iteracoes}),
        }).collect::<Vec<_>>(),
    });
    Ok(pythonize::pythonize(py, &d_)?.into())
}


/// **Ciclo d do Pensamento com o VERIFICADOR NCM** (produção, sem oráculo):
/// backbone CABEÇA TREINADA (alvo → classes) + verificador calibrado em
/// dados retidos; N tentativas agregadas; ganho medido em consultas retidas
/// EM DISTRIBUIÇÃO (onde a calibração vale — V-2) e OOD reportado à parte.
#[pyfunction]
pub(crate) fn demo_pensamento_ncm(
    d_cond: usize,
    d_saida: usize,
    n_tentativas: usize,
    passos_treino: usize,
    limiar_conf: f64,
    orcamento: usize,
    semente: u64,
) -> PyResult<PyObject> {
    Python::with_gil(|py| {
        use candle_core::Tensor;
        let dev = candle_core::Device::Cpu;
        crate::rng::definir_semente(semente);
        let mut motor = pensamento::MotorPensamento::novo(
            d_cond, d_saida, n_tentativas, limiar_conf, orcamento, &dev)
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
        let mut otim = treino::AdamWManual::novo(motor.vars(), 5e-3, 0.0);
        let mut rng = crate::rng::RngDeterministico::novo(semente);
        // ---- dados sintéticos (item a: treino cooperativo) ----
        let n_dados = 320usize;
        let conds: Vec<Vec<f32>> = (0..n_dados)
            .map(|_| (0..d_cond).map(|_| rng.normal(0.0, 1.0) as f32).collect())
            .collect();
        let alvos: Vec<Vec<f32>> = conds.iter().map(|c| {
            (0..d_saida).map(|j| {
                let cj = c[j % d_cond];
                let ck = c[(j + 1) % d_cond];
                0.5 * cj + 0.3 * cj * ck + 0.1
            }).collect()
        }).collect();
        let mk = |c: &[f32], j: usize| -> f32 {
            let cj = c[j % d_cond];
            let ck = c[(j + 1) % d_cond];
            0.5 * cj + 0.3 * cj * ck + 0.1
        };
        let cond_t = Tensor::from_vec(conds.iter().flatten().cloned().collect(), (n_dados, d_cond), &dev)
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
        let alvo_t = Tensor::from_vec(alvos.iter().flatten().cloned().collect(), (n_dados, d_saida), &dev)
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
        // ---- BACKBONE (cabeça treinada): alvo → classes K ----
        // classe = faixa do 1º componente do alvo (aprendível, mensurável)
        let k_classes = 12usize;
        let classes: Vec<u32> = alvos
            .iter()
            .map(|a| {
                let v = a[0];
                let f = (((v + 2.0) / 4.0) * (k_classes as f32)).floor();
                (f.clamp(0.0, (k_classes - 1) as f32)) as u32
            })
            .collect();
        let cabeca = crate::jepa::MlpJepa::novo(d_saida, 4 * d_saida, k_classes, &dev)
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
        let classe_t = Tensor::from_vec(classes.clone(), (n_dados,), &dev)
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
        let mut otim_cab = treino::AdamWManual::novo(cabeca.vars(), 1e-2, 0.0);
        for _ in 0..200 {
            let logits = cabeca.fwd(&alvo_t)
                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
            let ce = candle_nn::loss::cross_entropy(&logits, &classe_t)
                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
            otim_cab.passo(treino::grads_achatados(&ce, &cabeca.vars())
                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?);
        }
        // ---- VERIFICADOR calibrado em RETIDOS (metade final) ----
        let n_ret = n_dados / 2;
        let (alvo_ret, clas_ret) = (
            alvo_t.narrow(0, n_ret, n_dados - n_ret).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?,
            classe_t.narrow(0, n_ret, n_dados - n_ret).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?,
        );
        let logits_ret = cabeca.fwd(&alvo_ret).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
        let mut verif = verificador::VerificadorNcm::novo();
        let (ece_antes, ece_depois) = verif.ajustar(&logits_ret, &clas_ret)
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
        // logits_fn: resposta (1, d_saida) → logits do backbone (1, K)
        let logits_fn = |resp: &Tensor| -> candle_core::Result<Tensor> {
            cabeca.fwd(resp)
        };
        // ---- treino cooperativo do motor (item a) ----
        let cond_tr = cond_t.narrow(0, 0, n_ret).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
        let alvo_tr = alvo_t.narrow(0, 0, n_ret).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
        let mut hist = Vec::new();
        for _ in 0..passos_treino {
            let perda = motor.perda_treino(&cond_tr, &alvo_tr)
                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
            let pv = perda.to_scalar::<f32>()
                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))? as f64;
            hist.push(pv);
            otim.passo(treino::grads_achatados(&perda, &motor.vars())
                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?);
        }
        // ---- consultas RETIDAS em distribuição (ganho medido AQUI) ----
        let n_consultas = 8usize;
        let mut confs_base = Vec::new();
        let mut confs_pens = Vec::new();
        let mut iteracoes = Vec::new();
        for qi in 0..n_consultas {
            let consulta: Vec<f32> = conds[n_ret + qi].clone();
            let consulta_t = Tensor::from_vec(consulta, (d_cond,), &dev)
                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
            let mut rng2 = crate::rng::RngDeterministico::novo(semente + 1 + qi as u64);
            let res = motor.raciocinar_ncm(&consulta_t, &verif, &logits_fn, &mut rng2)
                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
            // base: 1 tentativa isolada, mesma verificação NCM
            let base = motor.tentativas[0]
                .fwd(&consulta_t.broadcast_as((1, d_cond)).unwrap()).unwrap();
            let conf_base = verif.confianca_1d(&logits_fn(&base).unwrap()).unwrap();
            confs_base.push(conf_base);
            confs_pens.push(res.confianca);
            iteracoes.push(res.iteracoes);
        }
        let media = |v: &[f64]| v.iter().sum::<f64>() / v.len().max(1) as f64;
        let (conf_base_m, conf_pens_m) = (media(&confs_base), media(&confs_pens));
        let iteracoes_media = iteracoes.iter().sum::<usize>() as f64 / iteracoes.len().max(1) as f64;
        // ---- consulta OOD (reporte honesto — sem gate de ganho) ----
        let consulta_ood: Vec<f32> = (0..d_cond).map(|j| 3.0 + (j as f32) * 0.1).collect();
        let consulta_ood_t = Tensor::from_vec(consulta_ood, (d_cond,), &dev)
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
        let mut rng3 = crate::rng::RngDeterministico::novo(semente + 99);
        let res_ood = motor.raciocinar_ncm(&consulta_ood_t, &verif, &logits_fn, &mut rng3)
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
        let base_ood = motor.tentativas[0]
            .fwd(&consulta_ood_t.broadcast_as((1, d_cond)).unwrap()).unwrap();
        let conf_base_ood = verif.confianca_1d(&logits_fn(&base_ood).unwrap()).unwrap();
        let _ = mk;
        let d_ = serde_json::json!({
            "versao": "pensamento-0.6.0-ncm",
            "n_tentativas": n_tentativas,
            "perda_treino_inicial": hist.first(), "perda_treino_final": hist.last(),
            "treino_aprendeu": hist.last().copied() < hist.first().map(|p| p * 0.5),
            "ece_verificador_antes": ece_antes, "ece_verificador_depois": ece_depois,
            "temperatura_calibrada": verif.temperatura,
            "iteracoes_ciclo_d_media": iteracoes_media,
            "confianca_base_1_rede": conf_base_m,
            "confianca_pensamento": conf_pens_m,
            "ganho_confianca": conf_pens_m - conf_base_m,
            "confianca_ood_base": conf_base_ood,
            "confianca_ood_pensamento": res_ood.confianca,
            "iteracoes_ood": res_ood.iteracoes,
            "orcamento_esgotado_ood": res_ood.telemetria.get("orcamento_esgotado").copied().unwrap_or(0.0),
            "verificador": "ncm-calibrado (sem oraculo)",
            "gates": {
                "gate_verificador_sem_oraculo": true,
                "gate_calibrado": ece_depois <= ece_antes,
                "gate_ganho_nao_negativo": conf_pens_m >= conf_base_m - 1e-3,
                "gate_terminou": iteracoes_media <= orcamento as f64,
            },
        });
        Ok(pythonize::pythonize(py, &d_)?.into())
    })
}



/// **Autoajuste do Pensamento por telemetria (A-1..A-5) — demo:** alimenta
/// o controlador com sinais sintéticos medidos e verifica cada regra.
#[pyfunction]
pub(crate) fn demo_autoajuste(semente: u64) -> PyResult<PyObject> {
    Python::with_gil(|py| {
        let mut hub = telemetria::TelemetryHub::novo(None);
        let mut rng = crate::rng::RngDeterministico::novo(semente);
        let mut auto = controlador::AutoAjustePensamento::novo(0.5, 8, 2);
        // A-1: confianças com mistura (60% altas ~0.85–0.95, 40% baixas ~0.25–0.40)
        // ⇒ quantil 0.75 cai no grupo alto (posições 0.6–1.0) ≈ 0.85+0.375·0.1 ≈ 0.89
        for _ in 0..200 {
            let c = if rng.uniforme() < 0.6 { 0.85 + rng.uniforme() * 0.1 } else { 0.25 + rng.uniforme() * 0.15 };
            auto.confs_retidas.push(c);
        }
        let tau = auto.ajustar_tau_c(0.25, &mut hub);
        // A-3: acordos altos (0.88 ⇒ p̂ = (1+√0.76)/2 ≈ 0.936 ⇒ N = ln(20)/(2·0.436²) ≈ 7.9 ⇒ 8)
        for _ in 0..32 {
            auto.acordos.push(0.88);
        }
        let n_ajustado = auto.ajustar_n(0.05, 16, &mut hub);
        // A-5: ganhos estáveis positivos (n=64 ⇒ 3º termo Bernstein ≈ 0.14 < 0.30) ⇒ gate sobe
        for _ in 0..64 {
            auto.ajustar_gate(0.30, 0.05, &mut hub);
        }
        let g_final = auto.gate_g;
        // A-2: ganhos decaindo geometricamente
        for k in 0..12 {
            auto.ganhos_por_iteracao.push(0.25 * 0.6f64.powi(k));
        }
        let b_ajustado = auto.ajustar_orcamento(0.02, &mut hub);
        // A-4: UCB1 com 3 braços
        let mut u = controlador::Ucb1::novo(vec!["n1".into(), "n4_moe".into(), "n4".into()]);
        for _ in 0..200 {
            let braco = u.escolher();
            let media = match braco { 0 => 0.35, 1 => 0.80, _ => 0.55 };
            let r = (media + rng.normal(0.0, 0.06) as f64).clamp(0.0, 1.0);
            u.registrar(braco, r);
        }
        let medias = u.medias();
        let melhor = medias.iter().enumerate().max_by(|a, b| a.1.partial_cmp(b.1).unwrap()).map(|(i, _)| i).unwrap_or(0);
        let d_ = serde_json::json!({
            "versao": "autoajuste-0.6.0",
            "tau_c_final": tau, "n_tentativas_final": n_ajustado,
            "gate_final": g_final, "orcamento_b_final": b_ajustado,
            "ajustes_auditados": hub.eventos_ajuste.len(),
            "ucb1_medias": medias, "ucb1_melhor_braco": u.nomes[melhor],
            "ucb1_arrependimento_medio": u.arrependimento_empirico() / 200.0,
            "gates": {
                "gate_a1_tau_quantil": (tau - 0.89).abs() < 0.08,
                "gate_a3_n_reduzido": n_ajustado <= 8,
                "gate_a5_gate_subiu": g_final > 0.0,
                "gate_a2_b_finito": b_ajustado >= 1 && b_ajustado <= 16,
                "gate_a4_ucb1_achou": u.nomes[melhor] == "n4_moe",
            },
        });
        Ok(pythonize::pythonize(py, &d_)?.into())
    })
}