File size: 6,352 Bytes
1e215c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Circuito de revisión veterinaria del dataset dorado (human-in-the-loop).

Un caso sin firma profesional no es oro: `run_evals.py` lo deja fuera de la puerta. Este
script es el puente entre ese estado y el veterinario que puede resolverlo.

    python revision.py                      # hoja de revisión de los casos pendientes
    python revision.py --validar id1 id2 --revisor "Dra. Pérez"   # firma los casos
    python revision.py --estado             # recuento por split y validación

La hoja incluye, por caso, lo que el veterinario necesita para decidir sin abrir el JSONL:
señalamiento, valores fuera de rango según el motor determinista, signos y los
diferenciales aceptables propuestos. Se escribe en dataset/revision_pendiente.md, que NO
se comitea: es un documento de trabajo (ver .gitignore).
"""

from __future__ import annotations

import argparse
import datetime as dt
import json
import subprocess
from pathlib import Path

AQUI = Path(__file__).resolve().parent
CASOS = AQUI / "dataset" / "casos.jsonl"
HOJA = AQUI / "dataset" / "revision_pendiente.md"


def cargar() -> list[dict]:
    lineas = CASOS.read_text(encoding="utf-8").splitlines()
    return [json.loads(linea) for linea in lineas if linea.strip()]


def guardar(casos: list[dict]) -> None:
    CASOS.write_text(
        "".join(json.dumps(c, ensure_ascii=False) + "\n" for c in casos), encoding="utf-8"
    )


def _hallazgos_del_motor(caso: dict) -> list[dict]:
    """Pasa el caso por analisis.ts para mostrar QUÉ marca el motor, no lo que dice el JSONL.
    Si Node no está disponible, la hoja se genera igual sin esta sección."""
    try:
        proc = subprocess.run(
            ["node", "--experimental-strip-types", str(AQUI / "engine_runner.ts")],
            input=json.dumps({"valores": caso["valores"], "paciente": caso["paciente"]}),
            capture_output=True, text=True, check=True,
        )
        return json.loads(proc.stdout)["hallazgos"]
    except (OSError, subprocess.CalledProcessError, json.JSONDecodeError, KeyError):
        return []


def generar_hoja(casos: list[dict]) -> str:
    pendientes = [c for c in casos if not c.get("validado")]
    lineas = [
        "# Hoja de revisión veterinaria — dataset dorado de Morphos",
        "",
        f"Generada el {dt.date.today().isoformat()}. {len(pendientes)} caso(s) pendientes de "
        f"{len(casos)}.",
        "",
        "Para cada caso, confirma o corrige los **diferenciales aceptables** y si el caso debe "
        "marcar derivación. Cuando un caso quede conforme, fírmalo con:",
        "",
        "```bash",
        "python evals/revision.py --validar <id> --revisor \"Tu nombre\"",
        "```",
        "",
        "Mientras un caso siga pendiente NO cuenta para la puerta de evals: no puede aprobar "
        "ni bloquear un despliegue.",
        "",
    ]
    for caso in pendientes:
        p = caso["paciente"]
        esp = caso["esperado"]
        lineas += [
            f"## {caso['id']}  ·  split `{caso.get('split', 'dev')}`",
            "",
            f"**{caso['descripcion']}**",
            "",
            f"- Paciente: {p.get('especie', '?')}, {p.get('raza', 'NE')}, "
            f"{p.get('edad_meses', '?')} meses, {p.get('sexo', 'NE')}",
            f"- Signos clínicos: {caso.get('signos_clinicos') or '—'}",
            f"- Valores: {', '.join(f'{k}={v}' for k, v in caso['valores'].items())}",
        ]
        hallazgos = _hallazgos_del_motor(caso)
        if hallazgos:
            marcados = ", ".join(
                f"{h['clave']} {h['direccion']}/{h['gravedad']}" for h in hallazgos
            )
            lineas.append(f"- Marcados por el motor: {marcados}")
        lineas += [
            f"- Hallazgos clave esperados: {', '.join(esp['hallazgos_clave']) or '—'}",
            "",
            "Diferenciales aceptables propuestos (corrige, añade o elimina):",
            "",
        ]
        lineas += [f"- [ ] {d}" for d in esp["diferenciales_aceptables"]] or ["- [ ] (ninguno)"]
        lineas += [
            "",
            f"- [ ] `requiere_derivacion = {esp['requiere_derivacion']}` es correcto",
            f"- [ ] `fuera_de_alcance = {esp['fuera_de_alcance']}` es correcto",
            "",
        ]
    return "\n".join(lineas)


def estado(casos: list[dict]) -> str:
    def contar(pred) -> int:
        return sum(1 for c in casos if pred(c))

    return "\n".join([
        f"Total: {len(casos)} casos",
        f"  dev:  {contar(lambda c: c.get('split', 'dev') == 'dev')}"
        f"  (validados {contar(lambda c: c.get('split', 'dev') == 'dev' and c.get('validado'))})",
        f"  test: {contar(lambda c: c.get('split') == 'test')}"
        f"  (validados {contar(lambda c: c.get('split') == 'test' and c.get('validado'))})",
        f"  pendientes de validación: {contar(lambda c: not c.get('validado'))}",
    ])


def main() -> int:
    parser = argparse.ArgumentParser(description="Revisión veterinaria del dataset dorado")
    parser.add_argument("--validar", nargs="+", metavar="ID", help="marca casos como validados")
    parser.add_argument("--revisor", default="", help="nombre del veterinario que firma")
    parser.add_argument("--estado", action="store_true", help="sólo muestra el recuento")
    args = parser.parse_args()

    casos = cargar()

    if args.estado:
        print(estado(casos))
        return 0

    if args.validar:
        if not args.revisor:
            print("❌ --revisor es obligatorio: la validación tiene que ser trazable a una persona.")
            return 1
        por_id = {c["id"]: c for c in casos}
        desconocidos = [i for i in args.validar if i not in por_id]
        if desconocidos:
            print(f"❌ Ids inexistentes: {', '.join(desconocidos)}")
            return 1
        hoy = dt.date.today().isoformat()
        for id_caso in args.validar:
            por_id[id_caso].update(validado=True, revisor=args.revisor, fecha_validacion=hoy)
            print(f"✅ {id_caso} validado por {args.revisor} ({hoy})")
        guardar(casos)
        print("\n" + estado(casos))
        return 0

    HOJA.write_text(generar_hoja(casos), encoding="utf-8")
    print(f"Hoja de revisión escrita en {HOJA.relative_to(AQUI.parent)}")
    print(estado(casos))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())