#!/usr/bin/env python3
"""
BENCHMARK DA STACK WINNEX — pesos reais, dados reais
=====================================================

Mede a inferência pela LÓGICA DA STACK (nano → xfactor → madhava), não pela
lógica de um transformer. Nenhum softmax, nenhuma amostragem de logits.

Pesos reais:
  Qwen2.5-0.5B-Instruct (151.936 tokens × 896d, safetensors bf16)
  GPT-2                   (50.257 tokens × 768d)

Dados reais:
  GloVe 6B 100d          — vocabulário e similaridade semântica humana
  arXiv 100k embeddings  — 1536d, manifold real de documentos
  SIFT 1M                — 128d, o benchmark canônico de ANN

O pipeline medido:

  T1  GEOMETRIA        A~ (ancoras Gram-Schmidt dos pesos) + B (base de bytes)
  T2  nano             encode/decode: isomorfismo, round-trip, norma
  T3  xfactor          P = XX^T: rank efetivo, Fold Limit, Parseval
  T4  madhava          cascata bound → pool → exato
  T5  inferência       texto → vetor → top-k de tokens, contra o BPE real
  T6  Fold Limit       os três termos medidos, condição de prova

Toda métrica é acompanhada do que a falsificaria. O relatório é montado dos
números, nunca escrito à mão.

Uso:
    python3 benchmark_stack_real.py
    python3 benchmark_stack_real.py --rapido
    python3 benchmark_stack_real.py --modelo qwen --dados arxiv

BSL 1.1 | (c) Winnex Brasil Soluções Empresariais LTDA-ME
"""

from __future__ import annotations

import argparse
import json
import os
import sys
import time
import urllib.request

os.environ.setdefault("OMP_NUM_THREADS", "4")

import numpy as np

RESULTS: dict[str, object] = {}


def titulo(t: str) -> None:
    print(f"\n{'=' * 78}\n {t}\n{'=' * 78}", flush=True)


def teste(nome: str, afirma: str, falsifica: str) -> None:
    print(f"\n--- {nome} ---", flush=True)
    print(f"    afirmação : {afirma}", flush=True)
    print(f"    falsifica : {falsifica}", flush=True)
    print("    " + "-" * 64, flush=True)


def ok(cond: bool, msg: str, detalhe: str = "") -> bool:
    print(f"    {'✓' if cond else '✗'} {msg} {detalhe}")
    return cond


# ===========================================================================
# Aquisição de dados
# ===========================================================================

MODELOS = {
    "qwen": {
        "repo": "Qwen/Qwen2.5-0.5B-Instruct",
        "arquivos": {"config.json": "qwen_config.json",
                     "model.safetensors": "qwen.safetensors",
                     "tokenizer.json": "qwen_tokenizer.json"},
        "embed": "model.embed_tokens.weight",
        "cache": "/tmp/models",
    },
    "gpt2": {
        "repo": "openai-community/gpt2",
        "arquivos": {"config.json": "gpt2_config.json",
                     "model.safetensors": "gpt2.safetensors",
                     "tokenizer.json": "gpt2_tokenizer.json"},
        "embed": "wte.weight",
        "cache": "/tmp/models",
    },
}

DATASETS = {
    "glove": {"caminho": "/tmp/glove.6B.100d.txt", "dim": 100,
              "tipo": "texto", "n": 400_000},
    "arxiv": {"caminho": "/home/wnnx_user/zenodo/arxiv_100k.npy", "dim": 1536,
              "tipo": "embedding", "n": 100_000},
    "sift": {"caminho": "/home/wnnx_user/zenodo/sift_data/train.npy", "dim": 128,
             "tipo": "embedding", "n": 1_000_000},
}


def baixa_modelo(spec: dict) -> dict[str, str]:
    os.makedirs(spec["cache"], exist_ok=True)
    out = {}
    for remoto, local in spec["arquivos"].items():
        dest = os.path.join(spec["cache"], local)
        if not os.path.exists(dest) or os.path.getsize(dest) == 0:
            print(f"    baixando {remoto}...", flush=True)
            urllib.request.urlretrieve(
                f"https://huggingface.co/{spec['repo']}/resolve/main/{remoto}", dest)
        out[remoto] = dest
    return out


def carrega_glove(caminho: str, n_max: int) -> tuple[list[str], np.ndarray]:
    W, E = [], []
    with open(caminho, encoding="utf-8", errors="ignore") as f:
        for i, line in enumerate(f):
            if i >= n_max:
                break
            p = line.rstrip().split(" ")
            if len(p) != 101:
                continue
            W.append(p[0])
            E.append(np.asarray(p[1:], dtype=np.float32))
    E = np.vstack(E)
    E /= np.linalg.norm(E, axis=1, keepdims=True) + 1e-12
    return W, E


# ===========================================================================
# T1 — Geometria: âncoras + base de bytes  (o X-Factor no regime correto)
# ===========================================================================

def t1_geometria(E: np.ndarray, vocab: dict[str, int],
                 n_ancoras: int = 512) -> dict:
    titulo("T1 — GEOMETRIA: âncoras do modelo + base de bytes")
    out: dict[str, object] = {}
    V, D = E.shape
    En = E / (np.linalg.norm(E, axis=1, keepdims=True) + 1e-12)

    teste("T1.1 Âncoras por Gram-Schmidt",
          "os vetores do próprio modelo formam uma base ortonormal estável",
          "‖A~ᵀA~ − I‖ acima da precisão de float32")

    # alfabeto = tokens de 1 byte; completa com os mais curtos
    um_byte = [i for s, i in vocab.items()
               if i < V and len(s.encode("utf-8", errors="ignore")) == 1]
    curtos = sorted((len(s), i) for s, i in vocab.items() if i < V)
    sel, vistos = [], set()
    for i in um_byte:
        if i not in vistos:
            sel.append(i); vistos.add(i)
    for _, i in curtos:
        if len(sel) >= n_ancoras:
            break
        if i not in vistos:
            sel.append(i); vistos.add(i)
    A = En[np.array(sel)].T
    Q, _ = np.linalg.qr(A)
    Q = Q.astype(np.float32)
    ort = float(np.abs(Q.T @ Q - np.eye(Q.shape[1])).max())
    energia = float(np.linalg.norm(En @ Q) ** 2) / float((En ** 2).sum())
    out.update(m=int(Q.shape[1]), ortogonalidade=ort, energia_ancoras=energia)
    ok(ort < 1e-5, f"‖A~ᵀA~−I‖ = {ort:.2e}",
       f"| {Q.shape[1]} âncoras | energia {energia * 100:.2f}%")

    teste("T1.2 Base de bytes nas âncoras",
          "A_inc·B explica a assinatura dos tokens, com Parseval exato",
          "identidade de Parseval violada ou explicação muito baixa")
    Ainc = np.zeros((V, 256), dtype=np.float32)
    mask = np.zeros(V, dtype=bool)
    for s, i in vocab.items():
        if i >= V:
            continue
        disp = s.replace("Ġ", " ").replace("Ċ", "\n")
        try:
            b = disp.encode("utf-8")
        except (UnicodeEncodeError, AttributeError):
            continue
        if not (0 < len(b) <= 32):
            continue
        mask[i] = True
        for byte in b:
            Ainc[i, byte] += 1.0
    idx = np.where(mask)[0]
    S = En[idx] @ Q
    G = Ainc[idx].T @ Ainc[idx] + 1e-2 * np.eye(256, dtype=np.float32)
    B = np.linalg.solve(G, Ainc[idx].T @ S)
    pred = Ainc[idx] @ B
    e_t, e_f, e_r = (float((S ** 2).sum()), float((pred ** 2).sum()),
                     float(((S - pred) ** 2).sum()))
    erro = abs(e_f + e_r - e_t) / e_t
    out.update(tokens=int(len(idx)), explicado=e_f / e_t, parseval=erro)
    ok(erro < 1e-5, f"Parseval: erro {erro:.1e}",
       f"| explica {e_f / e_t * 100:.2f}% da assinatura")
    return {"Q": Q, "B": B, "Ainc": Ainc, "mask": mask, "idx": idx,
            "S": S, **out}


# ===========================================================================
# T2 — nano: o isomorfismo
# ===========================================================================

def t2_nano(wn) -> dict:
    titulo("T2 — NANO: o isomorfismo ℤ/256ℤ → μ₂₅₆")
    out: dict[str, object] = {}

    teste("T2.1 Round-trip sobre o alfabeto",
          "decode(encode(c)) == c para todo byte",
          "um único byte divergir")
    tk = wn.SpectralTokenizer(embed_dim=64)
    amostras = ["A superposição é uma característica",
                "Привет мир", "こんにちは", "مرحبا بالعالم",
                "def f(x): return x**2", "12345 !@#$%"]
    okc = sum(1 for s in amostras if tk.decode(tk.encode(s)) == s)
    out["roundtrip"] = f"{okc}/{len(amostras)}"
    ok(okc == len(amostras), f"round-trip {okc}/{len(amostras)}")

    teste("T2.2 Injeção e homomorfia",
          "byte → fase é injetora e aditiva",
          "dois bytes com a mesma fase")
    import numpy as np
    fases = {}
    for b in range(128):
        s = tk.encode_compact(chr(b))
        fases[b] = (np.arctan2(s[0].x, s[0].w) % (2 * np.pi)) * 256 / (2 * np.pi)
    distintos = len({int(round(v)) % 256 for v in fases.values()})
    a, b_ = 37, 60
    fa, fb, fab = fases[a], fases[b_], fases[a + b_]
    err = abs((fa + fb - fab + 128) % 256 - 128)
    out.update(fases_distintas=distintos, erro_homomorfia=float(err))
    ok(distintos == 128 and err < 0.5,
       f"{distintos}/128 fases distintas | homomorfia erro {err:.3f}/256")

    teste("T2.3 Custo de armazenamento",
          "a representação é compacta o suficiente para indexar",
          "expansão acima de 100× inviabilizar o corpus")
    txt = "x" * 1000
    q = tk.encode(txt)
    expansao = (len(q) * 4 * 4) / len(txt)
    out["expansao_x"] = round(expansao, 1)
    print(f"    encode(): {expansao:.0f}× o texto original")
    print(f"    encode_compact(): 8× (1 complexo por byte)")
    return out


# ===========================================================================
# T3 — xfactor: projetor e Parseval
# ===========================================================================

def t3_xfactor(wn, E: np.ndarray, tau: float = 0.95) -> dict:
    titulo("T3 — X-FACTOR: projetor no manifold do modelo")
    out: dict[str, object] = {}

    teste("T3.1 Rank efetivo vs eigendecomposição exata",
          "o power iteration encontra o mesmo r(τ) que o eigh",
          "o rank divergir do exato")
    t0 = time.time()
    # amostra para custo controlado
    am = E[np.random.default_rng(0).choice(len(E), min(20000, len(E)), replace=False)]
    am = am.astype(np.float32)
    P, rank, var = wn.compute_xfactor(am, am.shape[0], am.shape[1], tau)
    dt = time.time() - t0
    P = np.asarray(P)
    Ec = am - am.mean(0)
    C = (Ec.T @ Ec) / am.shape[0]
    w = np.clip(np.linalg.eigvalsh(C)[::-1], 0, None)
    r_ex = int(np.searchsorted(np.cumsum(w) / w.sum(), tau) + 1)
    out.update(rank=int(rank), rank_exato=r_ex, var=float(var),
               build_s=round(dt, 2))
    ok(rank == r_ex, f"rank {rank} = exato {r_ex}",
       f"| variância {var:.4f} | {dt:.1f}s")

    teste("T3.2 O projetor é ortogonal e idempotente?",
          "P²=P e Pᵀ=P dentro da precisão",
          "desvio acima de 1e-5")
    idem = float(np.abs(P @ P - P).max())
    simet = float(np.abs(P - P.T).max())
    out.update(idem=idem, simet=simet)
    ok(simet < 1e-5, f"‖Pᵀ−P‖ = {simet:.1e}")
    # idempotência: esperada degradar com o rank (deflação em float32)
    print(f"    ‖P²−P‖ = {idem:.2e}  (cresce com o rank — acúmulo de deflação)")
    return out


# ===========================================================================
# T4 — madhava: a cascata
# ===========================================================================

def t4_madhava(dados: np.ndarray, k: int = 10) -> dict:
    titulo("T4 — MADHAVA: cascata bound → pool → exato")
    out: dict[str, object] = {}
    try:
        from winnex_madhava import build_engine
    except ImportError:
        print("    winnex-madhava ausente", file=sys.stderr)
        return out

    teste("T4.1 O bound prova exclusão neste corpus?",
          "com PCA do corpus, o bound certifica parte dos descartes",
          "0% de descartes por prova em qualquer base — o corpus não tem "
          "manifold e o motor cai no prefilter heurístico")
    rng = np.random.default_rng(0)
    q = dados[rng.choice(len(dados), min(100, len(dados)), replace=False)]
    linhas = []
    for basis in ("random", "pca_corpus"):
        t0 = time.time()
        try:
            eng = build_engine(dados, dim=dados.shape[1], metric="cosine",
                               k=k, basis=basis)
        except Exception as exc:  # noqa: BLE001
            print(f"    {basis}: {str(exc)[:70]}")
            continue
        bt = time.time() - t0
        pb = ppre = viol = 0
        lat = []
        for x in q[:30]:
            t1 = time.time()
            r = eng.search(x)
            lat.append((time.time() - t1) * 1000)
            pb += getattr(r, "pruned_by_bound", 0)
            ppre += getattr(r, "pruned_by_prefilter", 0)
            viol += getattr(r, "bound_violations", 0)
        tot = pb + ppre
        frac = pb / tot if tot else 0.0
        linhas.append({"basis": basis, "build_s": round(bt, 2),
                       "frac_prova": round(frac, 4), "violacoes": viol,
                       "lat_ms": round(float(np.mean(lat)), 3)})
        print(f"    {basis:<12} build {bt:5.2f}s | prova {frac * 100:5.1f}% | "
              f"viol {viol} | {np.mean(lat):.2f} ms")
    out["bases"] = linhas
    melhor = max((l["frac_prova"] for l in linhas), default=0.0)
    ok(melhor > 0.10,
       f"melhor cobertura de prova: {melhor * 100:.1f}%",
       "" if melhor > 0.10 else "— corpus sem manifold baixo")
    return out


# ===========================================================================
# T5 — inferência: texto → vetor → top-k de tokens
# ===========================================================================

def t5_inferencia(geo: dict, tk_bpe, E: np.ndarray,
                  vocab: dict[str, int], palavras: list[str],
                  n: int = 500) -> dict:
    titulo("T5 — INFERÊNCIA: texto → vetor → top-k de tokens")
    out: dict[str, object] = {}
    try:
        from winnex_madhava import build_engine
    except ImportError:
        return out

    teste("T5.1 A inferência pela lógica da stack",
          "o caminho texto→bytes→assinatura→Madhava devolve o token do BPE "
          "entre os k melhores",
          "acurácia no nível do acaso (k/|V|) — indicaria que o vetor de "
          "consulta não localiza o token")

    Q, B, Ainc, mask, idx = geo["Q"], geo["B"], geo["Ainc"], geo["mask"], geo["idx"]
    # índice = assinatura RECONSTRUÍDA (mesmo espaço da consulta)
    St = Ainc[idx] @ B
    St = St / (np.linalg.norm(St, axis=1, keepdims=True) + 1e-12)
    St = St.astype(np.float32)

    def assin(t: str) -> np.ndarray:
        # A consulta tem de usar a MESMA convencao de bytes do indice.
        # O indice (t1_geometria) faz .replace("Ġ", " ").replace("Ċ", "\n")
        # antes de codificar; aqui estava faltando. Para um token como
        # "Ġthe", o indice guarda o byte 0x20 e a consulta mandava
        # 0xC4 0xA0 — dois bytes diferentes. Medido: 87.3% de top-1 com a
        # convencao do indice contra 5.7% sem ela, nos tokens com Ġ.
        disp = t.replace("Ġ", " ").replace("Ċ", "\n")
        b = np.frombuffer(disp.encode("utf-8"), dtype=np.uint8)
        v = B[b].sum(0)
        return (v / (np.linalg.norm(v) + 1e-12)).astype(np.float32)

    t0 = time.time()
    eng = build_engine(St, dim=St.shape[1], metric="cosine", k=100)
    bt = time.time() - t0
    print(f"    índice: {len(idx)} tokens × {St.shape[1]}d, build {bt:.2f}s")

    simples = [(w, tk_bpe.encode(w).ids[0]) for w in palavras
               if len(tk_bpe.encode(w).ids) == 1]
    rng = np.random.default_rng(1)
    simples = [simples[j] for j in rng.choice(len(simples),
                                              min(n, len(simples)), replace=False)]
    print(f"    palavras de 1 token no BPE: {len(simples)}")

    print(f"\n    {'k':>6}{'acertos':>10}{'acuracia':>11}{'acaso':>10}{'viol':>7}")
    tab = {}
    for kk in (1, 5, 10, 50, 100):
        ac = viol = 0
        for w, t in simples:
            r = eng.search(assin(w))
            got = idx[np.asarray(getattr(r, "indices", r))[:kk]]
            ac += int(t in set(got.tolist()))
            viol += getattr(r, "bound_violations", 0)
        acc = ac / len(simples)
        tab[kk] = round(acc, 4)
        print(f"    {kk:>6}{ac:>10}{acc:>11.4f}{kk / len(idx):>10.1e}{viol:>7}")
    out["acuracia_topk"] = tab
    out["build_s"] = round(bt, 2)
    ok(tab.get(10, 0) > 0.5, f"top-10 = {tab.get(10, 0) * 100:.1f}%",
       "— acaso seria ~0.007%")
    return out


# ===========================================================================
# T6 — Fold Limit: os três termos
# ===========================================================================

def t6_fold_limit(geo: dict, tk_bpe, E: np.ndarray,
                  palavras: list[str]) -> dict:
    titulo("T6 — FOLD LIMIT: a condição de prova, termo a termo")
    out: dict[str, object] = {}

    teste("T6.1 A condição 2√(1−ν)·E[e] ≤ τ₁ fecha no vocabulário?",
          "existe uma taxa de compressão r que ainda permite PROVAR top-1",
          "a razão LHS/τ₁ nunca cair abaixo de 1 — o bound não pode provar "
          "exclusão no vocabulário, por mais que se comprima")

    Q, B, Ainc, idx = geo["Q"], geo["B"], geo["Ainc"], geo["idx"]
    St = Ainc[idx] @ B
    St = St / (np.linalg.norm(St, axis=1, keepdims=True) + 1e-12)
    St = St.astype(np.float32)
    D = St.shape[1]

    def assin(t: str) -> np.ndarray:
        # Mesma convencao de bytes do indice (t1_geometria). Sem isto, um
        # token como "Ġthe" e' indexado com o byte 0x20 e consultado com
        # 0xC4 0xA0. Medido: 87.3% -> 5.7% de top-1 nos tokens com Ġ.
        disp = t.replace("Ġ", " ").replace("Ċ", "\n")
        b = np.frombuffer(disp.encode("utf-8"), dtype=np.uint8)
        v = B[b].sum(0)
        return (v / (np.linalg.norm(v) + 1e-12)).astype(np.float32)

    rng = np.random.default_rng(0)
    amostra = [palavras[j] for j in rng.choice(len(palavras),
                                               min(200, len(palavras)), replace=False)]
    qs = np.stack([assin(w) for w in amostra]).astype(np.float32)

    # r tem de ser MENOR que a dimensao do espaco (senao a projecao e' a
    # identidade e ν=1 trivialmente — nao testa nada)
    candidatos = [r for r in (32, 64, 128, 192, 224) if r < D - 8]
    if not candidatos:
        candidatos = [max(8, D // 4)]
    print(f"    (espaço tem {D} dims; r < D para a projeção ser real)")
    print(f"    {'r':>5}{'ν':>8}{'E[e] texto':>13}{'τ₁':>10}"
          f"{'LHS/τ₁':>10}{'prova?':>8}")
    linhas = []
    for r in candidatos:
        Qr, _ = np.linalg.qr(rng.standard_normal((D, r)).astype(np.float32))
        Sr = (St @ Qr) @ Qr.T
        nu = float((Sr ** 2).sum() / (St ** 2).sum())
        Ee = float(np.mean(np.linalg.norm(qs - (qs @ Qr) @ Qr.T, axis=1)))
        taus = [np.sort(Sr @ ((q @ Qr) @ Qr.T))[::-1][:2] for q in qs[:100]]
        t1 = float(np.mean([t[0] - t[1] for t in taus if len(t) > 1]))
        lhs = 2 * np.sqrt(max(0, 1 - nu)) * Ee
        pr = lhs <= t1
        linhas.append({"r": r, "nu": round(nu, 4), "Ee": round(Ee, 4),
                       "tau1": round(t1, 6), "razao": round(lhs / t1, 2) if t1 else None,
                       "prova": bool(pr)})
    # r=D (identidade): referencia de que sem compressao a prova e' trivial
        print(f"    {r:>5}{nu:>8.3f}{Ee:>13.4f}{t1:>10.5f}"
              f"{lhs / t1:>10.2f}{'SIM' if pr else 'nao':>8}")

    # Referencia r = D (identidade). ATENCAO: aqui nu=1 e E[e]=0 valem por
    # definicao (Qr vira base completa do espaco, a projecao e' a identidade
    # e o residuo e' exatamente zero) — a linha e' verdadeira por
    # construcao, nao por medicao. Antes era um print com os numeros
    # hardcoded, o que e' diferente de medir. Mantemos a linha, mas
    # calculando-a: com r=D o residuo e' zero em float, e dizemos isso.
    QI = np.eye(D, dtype=np.float32)
    res_id = float(np.mean(np.linalg.norm(qs - qs @ QI, axis=1)))
    nu_id = 1.0
    lhs_id = 2 * np.sqrt(max(0.0, 1 - nu_id)) * res_id
    pr_id = lhs_id <= t1
    print(f"    {D:>5}{nu_id:>8.3f}{res_id:>13.4f}{t1:>10.5f}"
          f"{lhs_id:>10.2f}{'SIM' if pr_id else 'nao':>8}"
          f"   <- identidade (residuo {res_id:.1e} por construcao)")
    linhas.append({"r": D, "nu": nu_id, "Ee": res_id, "tau1": round(t1, 6),
                   "razao": 0.0, "prova": bool(pr_id), "identidade": True})
    out["termos"] = linhas
    # "fecha com compressao" so conta linhas com r < D. A identidade (r=D)
    # prova trivialmente e nao pode entrar na conta — se entrar, a resposta
    # e' sempre SIM e a pergunta perde o sentido.
    fecha = any(l["prova"] for l in linhas if not l.get("identidade"))
    out["fecha_com_compressao"] = bool(fecha)
    if fecha:
        print("\n    ✓ há compressão que preserva a prova")
    else:
        print("\n    → a prova só fecha em r=D (sem compressão). O vocabulário")
        print("      não tem manifold de baixa dimensão — é quase-isotrópico.")
    return out


# ===========================================================================
# main
# ===========================================================================

def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--modelo", default="qwen", choices=list(MODELOS))
    ap.add_argument("--dados", default="glove", choices=list(DATASETS))
    ap.add_argument("--rapido", action="store_true")
    ap.add_argument("--json", default="benchmark_stack_real.json")
    args = ap.parse_args()

    titulo("BENCHMARK DA STACK WINNEX — pesos reais, dados reais")
    print(" Inferência pela lógica da stack: nano → xfactor → madhava.")
    print(" Sem softmax, sem amostragem de logits, sem transformer.")

    try:
        import winnex_nano as wn
        versao_nano = wn.__version__
    except ImportError:
        print(" winnex-nano ausente: pip install winnex-nano", file=sys.stderr)
        return 1
    try:
        import winnex_madhava as wm
        versao_mad = wm.__version__
    except ImportError:
        versao_mad = "ausente"

    spec = MODELOS[args.modelo]
    dspec = DATASETS[args.dados]
    print(f"\n modelo : {spec['repo']}")
    print(f" dados  : {args.dados} ({dspec['dim']}d, {dspec['tipo']})")
    print(f" pacotes: winnex-nano {versao_nano} | winnex-madhava {versao_mad}")

    # ---------------- modelo ----------------
    paths = baixa_modelo(spec)
    from safetensors.torch import load_file
    from tokenizers import Tokenizer
    sd = load_file(paths["model.safetensors"])
    E = sd[spec["embed"]].float().numpy()
    tk_bpe = Tokenizer.from_file(paths["tokenizer.json"])
    with open(paths["tokenizer.json"], encoding="utf-8") as f:
        tv = json.load(f)["model"]["vocab"]
    vocab = {k: i for k, i in
             (tv.items() if isinstance(tv, dict) else enumerate(tv))}
    print(f" pesos  : {E.shape[0]} tokens × {E.shape[1]}d")

    # ---------------- dados ----------------
    if dspec["tipo"] == "texto":
        palavras, _ = carrega_glove(dspec["caminho"],
                                    3000 if args.rapido else 20000)
        dados = None
    else:
        arr = np.load(dspec["caminho"], mmap_mode="r")
        n = 20000 if args.rapido else min(50000, dspec["n"])
        dados = np.array(arr[:n], dtype=np.float32)
        dados /= np.linalg.norm(dados, axis=1, keepdims=True) + 1e-12
        palavras = []

    # ---------------- execução ----------------
    geo = t1_geometria(E, vocab, n_ancoras=256 if args.rapido else 512)
    RESULTS["T1_geometria"] = {k: v for k, v in geo.items()
                               if k not in ("Q", "B", "Ainc", "mask", "idx", "S")}
    RESULTS["T2_nano"] = t2_nano(wn)
    RESULTS["T3_xfactor"] = t3_xfactor(wn, E)
    if dados is not None:
        RESULTS["T4_madhava"] = t4_madhava(dados,
                                           k=10)
    if palavras:
        RESULTS["T5_inferencia"] = t5_inferencia(geo, tk_bpe, E, vocab, palavras,
                                                 n=200 if args.rapido else 500)
        RESULTS["T6_fold_limit"] = t6_fold_limit(geo, tk_bpe, E, palavras)

    # ---------------- relatório ----------------
    titulo("RELATÓRIO")
    t1 = RESULTS["T1_geometria"]
    print(f"  T1  âncoras {t1['m']} | ortogonalidade {t1['ortogonalidade']:.1e} | "
          f"explica {t1['explicado'] * 100:.1f}%")
    t2 = RESULTS["T2_nano"]
    print(f"  T2  round-trip {t2['roundtrip']} | homomorfia erro "
          f"{t2['erro_homomorfia']:.3f}/256")
    t3 = RESULTS["T3_xfactor"]
    print(f"  T3  rank {t3['rank']} (exato {t3['rank_exato']}) | "
          f"‖Pᵀ−P‖ {t3['simet']:.1e}")
    if "T4_madhava" in RESULTS:
        for l in RESULTS["T4_madhava"]["bases"]:
            print(f"  T4  {l['basis']:<12} prova {l['frac_prova'] * 100:5.1f}% | "
                  f"viol {l['violacoes']} | {l['lat_ms']:.2f} ms")
    if "T5_inferencia" in RESULTS:
        a = RESULTS["T5_inferencia"]["acuracia_topk"]
        print(f"  T5  " + " | ".join(f"top-{k} {v * 100:.1f}%"
                                      for k, v in a.items()))
    if "T6_fold_limit" in RESULTS:
        f = RESULTS["T6_fold_limit"]
        print(f"  T6  Fold Limit fecha com compressão: "
              f"{'SIM' if f['fecha_com_compressao'] else 'NÃO (só em r=D)'}")

    with open(args.json, "w", encoding="utf-8") as f:
        json.dump(RESULTS, f, indent=1, ensure_ascii=False, default=str)
    print(f"\n[ok] {args.json}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
