File size: 27,829 Bytes
4dbc907
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28430a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4dbc907
 
 
28430a8
 
 
 
 
 
4dbc907
28430a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4dbc907
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28430a8
 
4dbc907
 
28430a8
 
 
 
 
 
 
 
 
 
4dbc907
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28430a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4dbc907
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28430a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4dbc907
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
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
//! arvore_pesos.rs — **Árvore Enraizada com Arestas Duplas** indexada dos
//! pesos do projeto (requisito v0.7.0: "arquitetura de pesos em árvore e
//! subárvores indexada" + ajuste bidirecional do quebra-cabeça).
//!
//! Estrutura (r-28 §4.2, provas r-30):
//! - **Nós** = módulos do projeto (raiz "khtst"; filhos: tronco, quantizador,
//!   som, jepa, moe, pensamento, verificador, fnh, nlp, nlg, acoplador).
//! - **Partição de índices**: cada nó possui um intervalo [ini, fim) do índice
//!   global de parâmetros — disjuntos e cobrindo [0, N) (T-A1).
//! - **Arestas duplas**: cada relação pai↔filho é UMA aresta com DOIS
//!   enlaces direcionados (pai→filho = regulação; filho→pai = crédito) —
//!   `vizinho` e `aresta_de` são O(1) por índice de adjacência (T-A4).
//! - **Fundo-topo**: agregação de métricas (telemetria/crédito) sobe —
//!   soma exata por indução (T-A2).
//! - **Topo-fundo**: regulação desce com decaimento por profundidade —
//!   causal por filtração de níveis e terminante por potencial (T-A3).

use std::collections::HashMap;

/// Um nó da árvore — um módulo com sua fatia do índice de parâmetros.
#[derive(Clone, Debug)]
pub struct NoArvore {
    pub nome: String,
    pub pai: Option<usize>,
    pub filhos: Vec<usize>,
    /// Intervalo [ini, fim) no índice global de parâmetros (partição).
    pub ini: usize,
    pub fim: usize,
    /// Profundidade (raiz = 0).
    pub profundidade: usize,
    /// Métricas agregadas do nó (a própria + descendentes).
    pub metricas: HashMap<String, f64>,
}

/// Enlace direcionado de uma aresta dupla.
#[derive(Clone, Copy, Debug)]
pub struct Enlace {
    /// id do PAR (compartilhado pelos dois enlaces da aresta dupla).
    pub id_par: usize,
    pub de: usize,
    pub para: usize,
}

/// Árvore de pesos indexada com arestas duplas.
pub struct ArvorePesos {
    pub nos: Vec<NoArvore>,
    por_nome: HashMap<String, usize>,
    /// Enlaces (2 por aresta: regulação desce, crédito sobe).
    pub enlaces: Vec<Enlace>,
    /// Índice de adjacência: nó → enlaces incidentes — O(1).
    adj: Vec<Vec<usize>>,
    /// Fluxo de crédito por enlace (sob na direção filho→pai).
    pub credito: Vec<f64>,
    /// Regulação por enlace (desce na direção pai→filho).
    pub regulacao: Vec<f64>,
    pub n_params: usize,
}

/// Layout padrão do projeto KHTST_RUST v0.7.0 — (nome, n_params, filhos).
/// A ordem define os intervalos da partição (T-A1).
pub const LAYOUT_PADRAO: &[(&str, usize)] = &[
    ("khtst", 0),        // raiz — sem parâmetros próprios (agrega)
    ("tronco", 1),
    ("quantizador", 1),
    ("som", 1),
    ("jepa", 1),
    ("moe", 1),
    ("pensamento", 1),
    ("verificador", 1),
    ("fnh", 1),
    ("nlp", 1),
    ("nlg", 1),
    ("acoplador", 1),
];

impl ArvorePesos {
    /// Constrói a árvore padrão: raiz "khtst" com os módulos como filhos
    /// diretos (profundidade 1) — subárvores podem ser anexadas depois.
    /// `contagens`: número de parâmetros por módulo (default 1 quando 0).
    pub fn nova(contagens: &HashMap<String, usize>) -> Self {
        let mut nos: Vec<NoArvore> = Vec::new();
        let mut por_nome = HashMap::new();
        let mut pos = 0usize;
        for (i, (nome, _)) in LAYOUT_PADRAO.iter().enumerate() {
            let n = if i == 0 {
                0
            } else {
                contagens.get(*nome).copied().unwrap_or(1).max(1)
            };
            let no = NoArvore {
                nome: nome.to_string(),
                pai: if i == 0 { None } else { Some(0) },
                filhos: Vec::new(),
                ini: pos,
                fim: pos + n,
                profundidade: if i == 0 { 0 } else { 1 },
                metricas: HashMap::new(),
            };
            pos = no.fim;
            por_nome.insert(nome.to_string(), i);
            if i != 0 {
                nos[0].filhos.push(i);
            }
            nos.push(no);
        }
        let n_nos = nos.len();
        // arestas duplas: raiz↔filho
        let mut enlaces = Vec::new();
        let mut adj = vec![Vec::new(); n_nos];
        for (id_par, &f) in nos[0].filhos.iter().enumerate() {
            // enlace pai→filho (regulação desce)
            adj[0].push(enlaces.len());
            enlaces.push(Enlace { id_par, de: 0, para: f });
            // enlace filho→pai (crédito sobe)
            adj[f].push(enlaces.len());
            enlaces.push(Enlace { id_par, de: f, para: 0 });
        }
        let n_enlaces = enlaces.len();
        ArvorePesos {
            nos,
            por_nome,
            enlaces,
            adj,
            credito: vec![0.0; n_enlaces],
            regulacao: vec![0.0; n_enlaces],
            n_params: pos,
        }
    }

    /// Consulta O(1) por nome (T-A4).
    pub fn no(&self, nome: &str) -> Option<usize> {
        self.por_nome.get(nome).copied()
    }

    pub fn n_nos(&self) -> usize {
        self.nos.len()
    }

    /// Enlace direcionado `de`→`para` (aresta dupla — T-A1 simetria).
    pub fn enlace(&self, de: usize, para: usize) -> Option<usize> {
        self.adj[de]
            .iter()
            .copied()
            .find(|&e| self.enlaces[e].para == para)
    }

    /// Enlaces incidentes ao nó (ambas as direções) — O(1) pelo índice.
    pub fn incidentes(&self, no: usize) -> &[usize] {
        &self.adj[no]
    }

    /// FatiA do índice de parâmetros da subárvore enraizada em `no`
    /// (o próprio intervalo quando não há filhos anexados).
    pub fn subarvore(&self, no: usize) -> (usize, usize) {
        // varre a subárvore em profundidade e devolve o envoltório dos
        // intervalos (a partição é contígua por construção pós-ordem se os
        // filhos forem anexados em ordem — aqui calculamos o envoltório)
        let (mut ini, mut fim) = (self.nos[no].ini, self.nos[no].fim);
        for &f in &self.nos[no].filhos {
            let (i2, f2) = self.subarvore(f);
            ini = ini.min(i2);
            fim = fim.max(f2);
        }
        (ini, fim)
    }

    /// Caminho do nó até a raiz (sobe pelos pais — termina: pai único, raiz
    /// sem pai; T-A3 terminação).
    pub fn caminho_ate_raiz(&self, no: usize) -> Vec<usize> {
        let mut caminho = vec![no];
        let mut atual = no;
        while let Some(p) = self.nos[atual].pai {
            caminho.push(p);
            atual = p;
        }
        caminho
    }

    /// Anexa um filho com ARESTA DUPLA ao nó `pai` (nome, n_params) —
    /// mantém o índice de adjacência e a partição do índice global (o
    /// intervalo do novo nó continua o índice). Devolve o índice do novo nó.
    /// v0.8.0: habilita subárvores profundas (teste T-A7b) sem reconstruir
    /// a árvore — a pós-ordem iterativa não depende da profundidade.
    pub fn anexar_filho(&mut self, pai: usize, nome: &str, n_params: usize) -> usize {
        let idx = self.nos.len();
        let prof = self.nos[pai].profundidade + 1;
        let ini = self.n_params;
        let fim = ini + n_params.max(1);
        self.nos.push(NoArvore {
            nome: nome.to_string(),
            pai: Some(pai),
            filhos: Vec::new(),
            ini,
            fim,
            profundidade: prof,
            metricas: HashMap::new(),
        });
        self.por_nome.insert(nome.to_string(), idx);
        self.nos[pai].filhos.push(idx);
        // aresta dupla pai↔filho (novo id_par)
        let id_par = self.enlaces.len() / 2;
        self.adj.push(Vec::new());
        self.adj[pai].push(self.enlaces.len());
        self.enlaces.push(Enlace { id_par, de: pai, para: idx });
        self.adj[idx].push(self.enlaces.len());
        self.enlaces.push(Enlace { id_par, de: idx, para: pai });
        // 1 entrada por ENLACE (2 por aresta dupla) — credito/regulacao são
        // indexadas por enlace, não por aresta
        self.credito.push(0.0);
        self.credito.push(0.0);
        self.regulacao.push(0.0);
        self.regulacao.push(0.0);
        self.n_params = fim;
        idx
    }

    /// **Fundo-topo**: agrega a métrica dos filhos no pai (pós-ordem —
    /// T-A2: a soma na raiz = soma global de todas as folhas + próprias).
    /// Devolve o total agregado na raiz.
    /// v0.8.0: **ITERATIVA** (proposta Qwen-3.8 nº 2 VALIDADA, com correções
    /// de robustez) — pilha explícita no heap, SEM recursão e SEM clones de
    /// `filhos`: O(V+E) tempo, O(V) espaço, zero alocação por nó; segura
    /// para árvores profundas degeneradas (T-A7b). Equivalência BIT-A-BIT
    /// com a recursiva (T-A7): mesma ordem de pós-ordem e mesma ordem de
    /// soma — prova em r-33 §3.
    pub fn agregar_fundo_topo(&mut self, metrica: &str) -> f64 {
        match self.agregar_pos_ordem(metrica) {
            Ok(v) => v,
            Err(e) => {
                // estrutura malformada (ciclo/filho duplicado): a recursiva
                // somaria duas vezes ou não terminaria — aqui é explícito
                log::warn!("agregar_fundo_topo: {e}");
                f64::NAN
            }
        }
    }

    /// Pós-ordem iterativa com verificação estrutural — erro em ciclo ou
    /// filho duplicado. O flag `empilhado` é marcado NO MOMENTO DO PUSH:
    /// um nó empilhado duas vezes (duplicata no mesmo pai, ou re-entrada de
    /// ciclo) é recusado imediatamente — a marcação pós-expansão deixaria
    /// escapar o caso "pai lista o filho duas vezes" (o 2º push ocorre
    /// antes da expansão do 1º — errata da própria correção, r-33 §3.4).
    fn agregar_pos_ordem(&mut self, metrica: &str) -> Result<f64, String> {
        let n = self.nos.len();
        if n == 0 {
            return Ok(0.0);
        }
        // fase 1: ordem de visita (pós-ordem: filhos ANTES do pai)
        let mut estado = vec![0u8; n]; // 0 = novo, 1 = filhos empilhados
        let mut empilhado = vec![false; n]; // marcado NO PUSH (unicidade)
        let mut pilha: Vec<usize> = Vec::with_capacity(n);
        let mut pos_ordem: Vec<usize> = Vec::with_capacity(n);
        pilha.push(0usize);
        empilhado[0] = true;
        while let Some(&atual) = pilha.last() {
            if estado[atual] == 0 {
                estado[atual] = 1;
                for &f in self.nos[atual].filhos.iter().rev() {
                    if f >= n {
                        return Err(format!("filho {f} fora do intervalo (nó {atual})"));
                    }
                    if empilhado[f] {
                        return Err(format!(
                            "filho {f} duplicado ou ciclo detectado no nó {atual}"
                        ));
                    }
                    empilhado[f] = true;
                    pilha.push(f);
                }
            } else {
                pilha.pop();
                pos_ordem.push(atual);
            }
        }
        // fase 2: processa folhas primeiro (a pós-ordem já é fundo-topo)
        for &no in pos_ordem.iter() {
            let mut total = self.nos[no].metricas.get(metrica).copied().unwrap_or(0.0);
            for &f in self.nos[no].filhos.iter() {
                let v_filho = self.nos[f].metricas.get(metrica).copied().unwrap_or(0.0);
                total += v_filho; // MESMA ordem da recursiva (bit-a-bit — T-A7)
                if let Some(e) = self.enlace(f, no) {
                    self.credito[e] = v_filho; // crédito sobe filho→pai
                }
            }
            self.nos[no].metricas.insert(metrica.to_string(), total);
        }
        Ok(self.nos[0].metricas.get(metrica).copied().unwrap_or(0.0))
    }

    /// Referência RECURSIVA (v0.6.0) — mantida como oráculo de equivalência
    /// do teste T-A7 e documentação do ganho (não usa em produção: clone de
    /// `filhos` por nó + quadros de pilha O(profundidade)).
    pub fn agregar_fundo_topo_rec(&mut self, metrica: &str) -> f64 {
        self.agregar_rec(0, metrica)
    }

    fn agregar_rec(&mut self, no: usize, metrica: &str) -> f64 {
        let mut total = self.nos[no].metricas.get(metrica).copied().unwrap_or(0.0);
        let filhos = self.nos[no].filhos.clone();
        for f in filhos {
            let v = self.agregar_rec(f, metrica);
            // crédito do filho sobe pelo enlace filho→pai
            if let Some(e) = self.enlace(f, no) {
                self.credito[e] = v;
            }
            total += v;
        }
        self.nos[no].metricas.insert(metrica.to_string(), total);
        total
    }

    /// **Topo-fundo**: regulação desce do nó `origem` com decaimento
    /// λ^k por nível de profundidade relativa (T-A3: causal — desce por
    /// níveis da filtração da árvore; terminante — profundidade finita).
    /// v0.8.0: ITERATIVA (pré-ordem com pilha explícita — mesmo ganho da
    /// agregação: sem clones, sem recursão; mesma ordem de escrita).
    pub fn regular_topo_fundo(&mut self, origem: usize, delta: f64, lambda: f64) {
        let base = self.nos[origem].profundidade;
        let mut pilha = vec![origem];
        while let Some(u) = pilha.pop() {
            // k do PAI u (semântica v0.6.0 exata: a aresta u→f usa λ^k(u))
            let k = self.nos[u].profundidade.saturating_sub(base);
            let d = delta * lambda.powi(k as i32);
            for &f in self.nos[u].filhos.iter().rev() {
                if let Some(e) = self.enlace(u, f) {
                    self.regulacao[e] = d; // desce pelo enlace pai→filho
                }
                pilha.push(f);
            }
        }
    }

    /// **Ajuste indexado regulado** (quebra-cabeça do Pensamento — r-31):
    /// aplica `delta` (com teto por componente |δ| ≤ delta_max) na métrica
    /// `metrica` do nó `no` e propaga o crédito fundo-topo até a raiz.
    /// Devolve o delta efetivamente aplicado (clamped).
    pub fn aplicar_ajuste_indexado(
        &mut self,
        no: usize,
        metrica: &str,
        delta: f64,
        delta_max: f64,
    ) -> f64 {
        let d = delta.clamp(-delta_max, delta_max);
        let atual = self.nos[no].metricas.get(metrica).copied().unwrap_or(0.0);
        self.nos[no].metricas.insert(metrica.to_string(), atual + d);
        d
    }

    /// Verificação dos invariantes estruturais (T-A1) — usada nos gates.
    pub fn invariantes_ok(&self) -> Result<(), String> {
        // (i) pai único exceto raiz; profundidade consistente
        for (i, no) in self.nos.iter().enumerate() {
            if i == 0 {
                if no.pai.is_some() {
                    return Err("raiz não pode ter pai".into());
                }
            } else if let Some(p) = no.pai {
                if p >= self.nos.len() {
                    return Err(format!("pai {p} fora do intervalo (nó {i})"));
                }
                if !self.nos[p].filhos.contains(&i) {
                    return Err(format!("pai de {i} não o lista como filho"));
                }
            } else {
                return Err(format!("não-raiz {i} sem pai"));
            }
        }
        // (ii) partição: intervalos disjuntos dentro de [0, n_params)
        let mut marcas = vec![false; self.n_params];
        for no in &self.nos {
            if no.ini >= no.fim && no.nome != "khtst" {
                return Err(format!("nó {} com intervalo vazio", no.nome));
            }
            for k in no.ini..no.fim {
                if marcas[k] {
                    return Err(format!("índice {k} particionado duas vezes"));
                }
                marcas[k] = true;
            }
        }
        if marcas.iter().any(|m| !m) {
            return Err("partição não cobre todo o índice".into());
        }
        // (iii) simetria das arestas duplas: (u,v) ∈ E ⇔ (v,u) ∈ E, mesmo id_par
        for e in &self.enlaces {
            let espelhado = self
                .enlace(e.para, e.de)
                .ok_or_else(|| format!("aresta ({},{}) sem espelho", e.de, e.para))?;
            if self.enlaces[espelhado].id_par != e.id_par {
                return Err(format!(
                    "espelho de ({},{}) com id_par diferente",
                    e.de, e.para
                ));
            }
        }
        // (iv) simetria pai↔filho (v0.8.0 — T-A8): todo filho listado por u
        // deve ter pai = u (captura o 2-ciclo A↔B anexado à raiz, que as
        // checagens (i)-(iii) deixam passar)
        for (u, no) in self.nos.iter().enumerate() {
            for &f in &no.filhos {
                if self.nos[f].pai != Some(u) {
                    return Err(format!(
                        "nó {u} lista filho {f} cujo pai é {:?} (assimetria)",
                        self.nos[f].pai
                    ));
                }
            }
        }
        // (v) alcançabilidade (v0.8.0 — T-A9): todo nó atingível da raiz —
        // com pai único + simetria (iv), isso equivale a ACICLICO+conectado
        let mut visto = vec![false; self.nos.len()];
        let mut fila = vec![0usize];
        visto[0] = true;
        let mut alcancados = 1usize;
        while let Some(u) = fila.pop() {
            for &f in &self.nos[u].filhos {
                if !visto[f] {
                    visto[f] = true;
                    alcancados += 1;
                    fila.push(f);
                }
            }
        }
        if alcancados != self.nos.len() {
            return Err(format!(
                "{} nós inalcançáveis da raiz (ciclo desconectado?)",
                self.nos.len() - alcancados
            ));
        }
        Ok(())
    }
}

#[cfg(test)]
mod testes {
    use super::*;

    fn arvore_exemplo() -> ArvorePesos {
        let mut contagens = HashMap::new();
        contagens.insert("tronco".to_string(), 40usize);
        contagens.insert("quantizador".to_string(), 10);
        contagens.insert("som".to_string(), 6);
        contagens.insert("jepa".to_string(), 6);
        contagens.insert("moe".to_string(), 5);
        contagens.insert("pensamento".to_string(), 8);
        contagens.insert("verificador".to_string(), 3);
        contagens.insert("fnh".to_string(), 4);
        contagens.insert("nlp".to_string(), 5);
        contagens.insert("nlg".to_string(), 5);
        contagens.insert("acoplador".to_string(), 2);
        ArvorePesos::nova(&contagens)
    }

    /// T-A1: invariantes — pai único, partição disjunta completa, simetria.
    #[test]
    fn ta1_invariantes() {
        let a = arvore_exemplo();
        a.invariantes_ok().expect("invariantes T-A1");
        assert_eq!(a.n_params, 94);
    }

    /// T-A2: agregação fundo-topo = soma global (indução pós-ordem).
    #[test]
    fn ta2_agregacao_fundo_topo() {
        let mut a = arvore_exemplo();
        // métricas locais por nó
        let valores = [
            ("tronco", 3.0),
            ("quantizador", 5.0),
            ("som", 1.0),
            ("jepa", 2.0),
            ("moe", 4.0),
            ("pensamento", 7.0),
            ("verificador", 1.0),
            ("fnh", 6.0),
            ("nlp", 2.0),
            ("nlg", 3.0),
            ("acoplador", 1.0),
        ];
        let mut esperado = 0.0;
        for (nome, v) in valores {
            let no = a.no(nome).unwrap();
            a.nos[no].metricas.insert("punicao".into(), v);
            esperado += v;
        }
        let total = a.agregar_fundo_topo("punicao");
        assert!((total - esperado).abs() < 1e-12, "T-A2: {total} ≠ {esperado}");
        let raiz = a.no("khtst").unwrap();
        let na_raiz = a.nos[raiz].metricas["punicao"];
        assert!((na_raiz - esperado).abs() < 1e-12, "T-A2: raiz {na_raiz}");
        // crédito nos enlaces filho→pai = valor agregado do filho
        let f = a.no("fnh").unwrap();
        let e = a.enlace(f, raiz).unwrap();
        assert!((a.credito[e] - 6.0).abs() < 1e-12, "T-A2: crédito do fnh");
    }

    /// T-A3: regulação topo-fundo — decaimento λ^k e causalidade por níveis.
    #[test]
    fn ta3_regulacao_topo_fundo() {
        let mut a = arvore_exemplo();
        let raiz = a.no("khtst").unwrap();
        a.regular_topo_fundo(raiz, 0.8, 0.5);
        // filhos diretos da raiz: k=0 ⇒ regulação = 0,8 (λ^0)
        for &f in &a.nos[raiz].filhos.clone() {
            let e = a.enlace(raiz, f).unwrap();
            assert!((a.regulacao[e] - 0.8).abs() < 1e-12, "T-A3: nível 1");
        }
        assert!(a.invariantes_ok().is_ok());
    }

    /// T-A4: consultas O(1) — por nome, enlace e incidentes.
    #[test]
    fn ta4_consultas_o1() {
        let a = arvore_exemplo();
        let f = a.no("fnh").expect("fnh");
        assert_eq!(a.nos[f].nome, "fnh");
        let raiz = a.no("khtst").unwrap();
        assert!(a.enlace(f, raiz).is_some());
        assert!(a.enlace(raiz, f).is_some());
        assert_eq!(a.incidentes(f).len(), 1); // 1 aresta dupla = 1 enlace incidente listado? não: 1 enlace filho→pai
        // subárvore do fnh = seu próprio intervalo
        let (ini, fim) = a.subarvore(f);
        assert_eq!((ini, fim), (a.nos[f].ini, a.nos[f].fim));
        // caminho fnh→raiz
        assert_eq!(a.caminho_ate_raiz(f), vec![f, raiz]);
    }

    /// T-A5: ajuste indexado regulado com teto — clamping simétrico.
    #[test]
    fn ta5_ajuste_indexado_com_teto() {
        let mut a = arvore_exemplo();
        let f = a.no("pensamento").unwrap();
        let d1 = a.aplicar_ajuste_indexado(f, "confianca", 0.3, 0.1);
        assert!((d1 - 0.1).abs() < 1e-12, "T-A5: teto superior");
        let d2 = a.aplicar_ajuste_indexado(f, "confianca", -50.0, 0.1);
        assert!((d2 + 0.1).abs() < 1e-12, "T-A5: teto inferior");
        let v = a.nos[f].metricas["confianca"];
        assert!((v - 0.0).abs() < 1e-12, "T-A5: 0,1 − 0,1 = 0");
    }

    /// **T-A7 (v0.8.0):** equivalência BIT-A-BIT da agregação iterativa com
    /// a recursiva em árvores aleatórias (mesma pós-ordem, mesma ordem de
    /// soma — prova r-33 §3.3). Proposta Qwen-3.8 nº 2 validada.
    #[test]
    fn ta7_equivalencia_bit_a_bit() {
        let mut rng = crate::rng::RngDeterministico::novo(909);
        for tentativa in 0..8u32 {
            let mut a = arvore_exemplo();
            // anexa subárvores aleatórias (ramificação E profundidade)
            for j in 0..12u32 {
                let pai = rng.inteiro(a.n_nos());
                let nome = format!("extra_{tentativa}_{j}");
                a.anexar_filho(pai, &nome, 1 + rng.inteiro(4));
            }
            // métricas locais aleatórias (registradas para repor depois)
            let locais: Vec<f64> = (0..a.n_nos())
                .map(|no| {
                    let v = rng.normal(0.0, 2.0);
                    a.nos[no].metricas.insert("punicao".into(), v);
                    v
                })
                .collect();
            let esperado: f64 = locais.iter().sum();
            a.invariantes_ok().expect("árvore aleatória válida");
            // iterativa
            let total_it = a.agregar_fundo_topo("punicao");
            let credito_it = a.credito.clone();
            // repõe as métricas locais e roda a recursiva (oráculo v0.6.0)
            for (no, v) in locais.iter().enumerate() {
                a.nos[no].metricas.insert("punicao".into(), *v);
            }
            let total_rec = a.agregar_fundo_topo_rec("punicao");
            let credito_rec = a.credito.clone();
            assert_eq!(
                total_it.to_bits(), total_rec.to_bits(),
                "T-A7: total bit-a-bit (tentativa {tentativa}: {total_it} vs {total_rec})"
            );
            for (i, (x, y)) in credito_it.iter().zip(credito_rec.iter()).enumerate() {
                assert_eq!(x.to_bits(), y.to_bits(), "T-A7: crédito[{i}] bit-a-bit");
            }
            assert!(
                (total_it - esperado).abs() < 1e-9,
                "T-A7: soma global (tentativa {tentativa})"
            );
        }
    }

    /// **T-A7b (v0.8.0):** cadeia degenerada de 100.000 nós — a iterativa
    /// completa SEM estourar a pilha (a recursiva exigiria ~100k quadros,
    /// excedendo o limite padrão de 8 MiB — risco documentado da v0.6.0;
    /// por isso ela NÃO é executada aqui).
    #[test]
    fn ta7b_cadeia_profunda_100k() {
        let mut a = arvore_exemplo();
        let mut pai = 1usize; // tronco
        for i in 0..100_000u32 {
            pai = a.anexar_filho(pai, &format!("c{i}"), 1);
        }
        assert_eq!(a.n_nos(), 100_000 + 12);
        a.nos[pai].metricas.insert("conf".into(), 1.0);
        let total = a.agregar_fundo_topo("conf");
        assert!((total - 1.0).abs() < 1e-12, "T-A7b: total {total}");
        // o crédito sobe INTEIRO por toda a cadeia (T-A2)
        let e = a.enlace(pai, a.nos[pai].pai.unwrap()).unwrap();
        assert!((a.credito[e] - 1.0).abs() < 1e-12, "T-A7b: crédito na folha");
        assert!(a.invariantes_ok().is_ok(), "T-A7b: invariantes na cadeia");
    }

    /// **T-A7c (v0.8.0):** regulação iterativa preserva a semântica λ^k do
    /// PAI (v0.6.0) em subárvores de profundidade 3.
    #[test]
    fn ta7c_regular_iterativa_equivalente() {
        let mut a = arvore_exemplo();
        let n1 = a.anexar_filho(1, "sub1", 2); // profundidade 2 (sob o tronco)
        let n2 = a.anexar_filho(n1, "sub2", 2); // profundidade 3
        let _n3 = a.anexar_filho(n2, "sub3", 2); // profundidade 4
        let raiz = 0usize;
        a.regular_topo_fundo(raiz, 0.8, 0.5);
        // aresta raiz→tronco: k(raiz) = 0 ⇒ λ⁰
        let e = a.enlace(raiz, 1).unwrap();
        assert!((a.regulacao[e] - 0.8).abs() < 1e-12, "T-A7c: nível raiz");
        // aresta tronco→sub1: k(tronco) = 1 ⇒ 0,8·0,5
        let e1 = a.enlace(1, n1).unwrap();
        assert!((a.regulacao[e1] - 0.4).abs() < 1e-12, "T-A7c: nível 1");
        // aresta sub1→sub2: k(sub1) = 2 ⇒ 0,8·0,25
        let e2 = a.enlace(n1, n2).unwrap();
        assert!((a.regulacao[e2] - 0.2).abs() < 1e-12, "T-A7c: nível 2");
        assert!(a.invariantes_ok().is_ok());
    }

    /// **T-A8 (v0.8.0):** detecção de estruturas malformadas — filho
    /// duplicado (a iterativa recusa; a recursiva somaria DUAS vezes) e
    /// ciclos (checagens (iv) simetria e (v) alcançabilidade — que as
    /// checagens (i)-(iii) da v0.6.0 deixavam passar).
    #[test]
    fn ta8_deteccao_ciclos_e_duplicatas() {
        let mut a = arvore_exemplo();
        // (a) filho duplicado: raiz lista o tronco duas vezes
        a.nos[0].filhos.push(1);
        let total = a.agregar_fundo_topo("punicao");
        assert!(total.is_nan(), "T-A8: duplicata deve recusar (NaN), não somar 2×");
        a.nos[0].filhos.pop();
        assert!(a.invariantes_ok().is_ok(), "T-A8: restaurada");
        // (b) assimetria pai↔filho: tronco lista o quantizador (pai = raiz)
        a.nos[1].filhos.push(2);
        let err = a.invariantes_ok().unwrap_err();
        assert!(err.contains("assimetria"), "T-A8(iv): {err}");
        a.nos[1].filhos.pop();
        // (c) 2-ciclo desconectado x↔y: passa (i)-(iv), capturado por (v)
        let x = a.nos.len();
        let y = x + 1;
        a.nos.push(NoArvore {
            nome: "x".into(), pai: Some(y), filhos: vec![y],
            ini: 94, fim: 95, profundidade: 9, metricas: HashMap::new(),
        });
        a.nos.push(NoArvore {
            nome: "y".into(), pai: Some(x), filhos: vec![x],
            ini: 95, fim: 96, profundidade: 9, metricas: HashMap::new(),
        });
        a.n_params = 96;
        let err = a.invariantes_ok().unwrap_err();
        assert!(err.contains("inalcançáveis"), "T-A8(v): {err}");
    }
}