File size: 6,753 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
#!/usr/bin/env python3
"""KHTST-RUST — CLI MLOps unificada (v0.10.0, crítica Qwen3.8 lote 3 nº 2).

**Por que FAÇADA e não substituição:** a proposta original ("substituir
por um CLI declarativo") violaria duas regras do projeto: (a) os scripts
numerados são a trilha de rastreabilidade das memórias r-XX (cada prova
cita "scripts 39-74"); (b) aprimoramentos não removem funcionalidade
eficaz. O CLI unifica o ENTRY POINT (um só comando para todo o pipeline)
delegando via subprocess aos scripts canônicos — zero duplicação de
lógica, zero remoção — e acrescenta o que não existia: validação de
integridade criptográfica dos checkpoints ANTES do resume (Teo CP-1..3,
r-35 §4) e gestão de estados locais.

Uso:
    python scripts/75_cli.py versao
    python scripts/75_cli.py treinar --validar-checkpoints
    python scripts/75_cli.py verificar-checkpoints
    python scripts/75_cli.py estados [--apagar]
    python scripts/75_cli.py coletar|suite|telemetria|publicar [--confirmar]
"""
from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Optional

import typer

app = typer.Typer(help="Pipeline KHTST-RUST v0.10.0 — fachada unificada (r-35 §6)")

RAIZ = Path(__file__).resolve().parent.parent
RESULTADOS = RAIZ / "resultados"
CHECKPOINTS = [
    RESULTADOS / "tokenizador_conceitos.json",
    RESULTADOS / "gerador_conceitos.json",
]

# scripts canônicos por estágio (trilha r-XX preservada)
DELEGACAO = {
    "coletar": "70_coleta_shards_v090.py",
    "treinar": "71_treino_v090.py",
    "suite": "77_suite_v010.py",
    "telemetria": "76_telemetria_v010.py",
    "publicar": "78_publicar_v010.py",
}

# padrões de estados de treino (mesma lista da varredura pós-publicação)
PADROES_ESTADO = [
    "*.pt", "*.npz", "*.ckpt", "*.safetensors", "*.estado.json",
    "**/*.pt", "**/*.npz", "**/*.ckpt", "**/*.safetensors", "**/*.estado.json",
]


def _delega(estagio: str, extra: list[str] | None = None) -> int:
    script = DELEGACAO[estagio]
    cmd = [sys.executable, str(RAIZ / "scripts" / script)] + (extra or [])
    typer.echo(f"→ {estagio}: {' '.join(cmd)}", err=True)
    return subprocess.run(cmd, cwd=RAIZ).returncode


@app.command()
def versao() -> None:
    """Versão do núcleo Rust e informação de build."""
    import khtst_rust as k
    typer.echo(k.versao())
    typer.echo(k.info_build())


@app.command()
def coletar() -> None:
    """Coleta shards reais (Wikipédia PT-BR, streaming controlado)."""
    raise typer.Exit(_delega("coletar"))


def _valida_ou_regenera(ck: Path, k) -> str:
    """Política de resume com prova de integridade (CP-1..CP-3):
    - não existe              → 'ausente'   (treina)
    - manifesto bate (SHA256) → 'válido'    (reusa — rápido)
    - manifesto divergente    → 'corrompido' (APAGA e retreina: nunca
      reusa estado em silêncio)
    - sem manifesto (legado)  → 'legado'    (reusa; o carregador legado
      falha alto em JSON truncado — detecção por parse)
    """
    if not ck.exists():
        return "ausente"
    if k.checkpoint_valido(str(ck)):
        return "válido"
    tem_manifesto = k.checkpoint_manifesto(str(ck)) is not None
    if tem_manifesto:
        ck.unlink(missing_ok=True)
        Path(str(ck) + ".manifest.json").unlink(missing_ok=True)
        return "corrompido (apagado — será retreinado)"
    return "legado (sem manifesto; parse do carregador detecta truncamento)"


@app.command()
def treinar(
    validar_checkpoints: bool = typer.Option(
        True, "--validar-checkpoints/--nao-validar",
        help="Valida SHA-256 dos checkpoints antes do resume (Teo CP-3)."),
) -> None:
    """Pipeline A/B/C (script 71) com pré-validação de integridade."""
    import khtst_rust as k
    if validar_checkpoints:
        for ck in CHECKPOINTS:
            estado = _valida_ou_regenera(ck, k)
            typer.echo(f"checkpoint {ck.name}: {estado}")
    raise typer.Exit(_delega("treinar"))


@app.command()
def verificar_checkpoints() -> None:
    """Relatório de integridade (SHA-256 + tamanho) dos checkpoints."""
    import khtst_rust as k
    achou = False
    for ck in RESULTADOS.rglob("*.json"):
        if ck.name.endswith(".manifest.json"):
            continue
        manifesto = k.checkpoint_manifesto(str(ck))
        if manifesto is None:
            continue
        achou = True
        m = json.loads(manifesto)
        valido = k.checkpoint_valido(str(ck))
        typer.echo(
            f"{'✓' if valido else '✗'} {ck.name}: sha256={m['hash_sha256'][:16]}… "
            f"tamanho={m['tamanho']}B algoritmo={m['algoritmo']} "
            f"versão={m['versao']}")
    if not achou:
        typer.echo("nenhum checkpoint com manifesto encontrado")


@app.command()
def suite() -> None:
    """Suíte consolidada de gates v0.10.0 (script 77)."""
    raise typer.Exit(_delega("suite"))


@app.command()
def telemetria() -> None:
    """Coleta telemetria do projeto v0.10.0 (script 76)."""
    raise typer.Exit(_delega("telemetria"))


@app.command()
def publicar(
    confirmar: bool = typer.Option(False, "--confirmar",
                                   help="Executa o upload único ao HF (script 74)."),
) -> None:
    """Publicação organizada no HF — exige --confirmar."""
    if not confirmar:
        typer.echo("publicação não confirmada: use --confirmar (upload único, "
                   "HF_TOKEN apenas via variável de ambiente)")
        raise typer.Exit(2)
    if not os.environ.get("HF_TOKEN"):
        typer.echo("HF_TOKEN ausente no ambiente — defina antes de publicar")
        raise typer.Exit(2)
    raise typer.Exit(_delega("publicar"))


@app.command()
def estados(
    apagar: bool = typer.Option(False, "--apagar",
                                help="Apaga os estados encontrados (regeneráveis)."),
) -> None:
    """Varre (e opcionalmente apaga) estados de treino do ambiente local."""
    achados: list[Path] = []
    for padrao in PADROES_ESTADO:
        achados.extend(RAIZ.rglob(padrao))
    achados = sorted(set(p for p in achados if "target" not in p.parts
                         and ".git" not in p.parts))
    if not achados:
        typer.echo("0 estados de treino no ambiente local")
        return
    total = 0
    for p in achados:
        tam = p.stat().st_size
        total += tam
        typer.echo(f"{'apagando' if apagar else 'achado'}: {p.relative_to(RAIZ)} "
                   f"({tam / 1e6:.1f} MB)")
        if apagar:
            p.unlink()
    typer.echo(f"total: {len(achados)} arquivos, {total / 1e6:.1f} MB"
               + (" — apagados" if apagar else " (use --apagar para remover)"))


if __name__ == "__main__":
    app()