#!/usr/bin/env python3 """¿Se le puede preguntar al archivo personal por lo que DECÍA? (SDD 26 §6.3, mitad semántica) python3 scripts/test-atuq-archivo-semantico.py # con el modelo de la imagen python3 scripts/test-atuq-archivo-semantico.py --negative-control # sin modelo: lo DICE ── QUÉ SE EJERCITA ─────────────────────────────────────────────────────────────────────────── El navegador archiva tres páginas de temas distintos —eso es el §6.3 literal, que ya existía— y después se le pregunta al host **desde dentro de la jaula y por el cable de verdad** (marco de 4 bytes), con los paths de PRODUCCIÓN. La respuesta tiene que ordenar por parecido: la página del tema preguntado primero. Y se pregunta DOS cosas distintas, que ganan páginas distintas. Con una sola pregunta, un ranking constante —o uno que siempre devuelve la más reciente— pasaría el test sin haber entendido nada. ── POR QUÉ EL HOST SE INVOCA A MANO Y NO POR LA EXTENSIÓN ──────────────────────────────────── Porque así se comprueba lo que de verdad importa del archivo: **que quedó en disco y se consulta sin el navegador**. Es el mismo camino que usa `scripts/test-atuq-archivo.py` para la búsqueda literal. REQUISITOS: rootfs de `atuq-nested.sh`, y `atuq` + `puriy-costura` + `llama-cpp` + `ia-modelo-embeddings` sellados. """ import http.server import json import os import shutil import struct import subprocess import sys import tempfile import threading ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) RFS = os.environ.get("RFS", "/mnt/cosecha/escritorios/atuq-rootfs") ALPINE = os.path.join(ROOT, ".dev-fs/alpine") HAMMER = os.path.join(ROOT, "target/release/takana") STORE = os.environ.get("HAMMER_STORE", os.path.join(ROOT, "store")) MODELO_EN_IMAGEN = "/usr/share/takana/ia/embeddings.gguf" # Tres páginas de temas bien separados, y dos preguntas que NO comparten palabras con el texto de # su página: si compartieran, la literal también acertaría y esto no probaría nada semántico. PAGINAS = { "/gato.html": ("El felino en el tejado", "Un minino atigrado dormía sobre las tejas calientes toda la siesta."), "/red.html": ("Cerrar la salida de una aplicación", "Reglas de egress por cgroup, denegar por defecto y permitir sólo lo declarado."), "/pan.html": ("Masa madre", "Harina, agua y sal; fermentación lenta durante la noche y horno muy caliente."), } PREGUNTAS = [ ("¿dónde se echó a dormir el gato?", "/gato.html"), ("cómo bloqueo internet a un programa", "/red.html"), ] def fatal(msg): print(f"✗ {msg}", file=sys.stderr) raise SystemExit(1) def artefacto(receta, nombre, escotilla=None): forzado = os.environ.get(escotilla) if escotilla else None if forzado: if not os.path.isdir(forzado): fatal(f"{escotilla}={forzado} no existe") print(f"⚠ {escotilla}: se mide {os.path.basename(forzado)}, que NO es necesariamente el vigente") return forzado r = subprocess.run([HAMMER, "--store", STORE, "hash", f"recipes/{receta}.toml"], cwd=ROOT, capture_output=True, text=True) if r.returncode != 0: fatal(f"no pude hashear {receta}: {r.stderr.strip()}") h = r.stdout.strip().splitlines()[-1].removeprefix("b3:") d = os.path.join(STORE, f"{h}-{nombre}") if not os.path.isdir(d): fatal(f"{nombre} b3:{h[:8]} no está sellado — construilo antes") if not os.listdir(d): fatal(f"{nombre} b3:{h[:8]} está VACÍO: eso no es un artefacto, es un nombre") return d class Servidor(http.server.SimpleHTTPRequestHandler): def log_message(self, *a): pass def do_GET(self): ruta = self.path.split("?")[0] if ruta in PAGINAS: titulo, texto = PAGINAS[ruta] # Cada página lleva a la siguiente: tres cargas de PRIMER NIVEL con un solo arranque. orden = list(PAGINAS) i = orden.index(ruta) siguiente = orden[i + 1] if i + 1 < len(orden) else "/fin.html" datos = (f'
{texto}
").encode() elif ruta == "/ir.html": # ⚠ La primera versión de esto metía las tres páginas en `