morphos / PLAN_MODERNIZACION.md
Jose Salazar
Corregir afirmación falsa sobre la guarda de auth en /api/papers
dd5a294
|
Raw
History Blame Contribute Delete
30.2 kB

Morphos — Modernization, AI/RAG, Evals & Security Plan

Estado de implementación (auditoría 2026-07-26)

Leyenda: ✅ hecho y verificado · 🟡 parcial (núcleo hecho, falta un subpunto) · ⬜ pendiente

Fase Estado Resumen
1 — Frontend (Vite/TS) Todos los módulos portados a TS (analisis, ui, pdf-parser, auth, papers, main, tooltip, ia); index.html recableado al bundle TS; 32 tests ✅. Menor: falta archivo de config ESLint/Prettier.
2 — Capa IA (FastAPI) Endpoints, salida estructurada, clientes medGemma/Claude, prompt endurecido, validación imágenes. Orquestación con ClienteModelo propio (no la librería Pydantic AI).
3 — RAG Ingesta + retriever con citas + degradación ✅. Nuevo: búsqueda híbrida (denso + BM25/FTS fusionados con RRF) + rerank cross-encoder bge-reranker-v2-m3, ambos conmutables; traducción ES→EN de consulta. Corpus real ingerido: 6763 fragmentos de 2 libros en instance/rag_index/ (LanceDB, 69 MB, bge-m3).
4 — Evals 🟡 Dataset dorado (17 casos), juez clínico, promptfoo, puerta CI ✅ (bloqueo verificado), arnés de evaluación de recuperación aislada (run_retrieval_eval.py). Faltan: código Ragas (0 líneas), human-in-the-loop, split reservado, informe por-ruta/por-corpus.
5 — Seguridad Guarda auth, rate limit, CORS, CSRF, BD/RAG fuera del webroot, cabeceras, validación imágenes — verificado. Menor: rate limit por-usuario y redirección HTTP→HTTPS.

Verificado ejecutando (2026-07-26): 32 tests frontend (Vitest), 97 tests backend (pytest, incluye retriever híbrido + integración con índice real + ingesta/troceo + traducción de consulta), ruff limpio, typecheck limpio. Previamente verificado en el navegador contra el backend FastAPI: motor de patrones (anemia microcítica), import de PDF (parser TS portado), registro/login real (sesión + CSRF), y el botón "Análisis IA" llamando a POST /api/interpret con token CSRF. Puerta de evals bloquea (exit≠0), 401 sin sesión, 403 sin CSRF. No verificado en vivo: salida estructurada renderizada con un modelo real (falta ollama pull de medGemma), promptfoo ejecutado, rechazo CORS de orígenes externos, 429 bajo carga, /security-review.

Fuera de este plan: la integración de analizadores de laboratorio (ASTM/HL7) ha avanzado en paralelo — backend/app/routers/lab.py, bridge/, data/lab_mapeos/, tests/test_lab_*.py, frontend/tests/lab-import.test.ts. Ver INTEGRACION_ANALIZADORES.md.

Legacy pendiente de retirar: los 8 js/*.js y los 5 api/*.php siguen en el repo pero YA NO se cargan (index.html no referencia ninguno). Son código muerto a eliminar.

Lo que queda, en orden sugerido

  1. Borrar el legacy js/ + api/ — riesgo cero, ya es código muerto.
  2. Ragas + split reservado, y ejecutar la suite completa de evals contra el índice real — el mayor valor pendiente, y ya desbloqueado por el corpus ingerido.
  3. Config ESLint/Prettier; rate limit por-usuario en /api/interpret; informe de evals por-ruta-de-modelo y por-versión-de-corpus.
  4. Human-in-the-loop (mayor alcance, requiere decisiones de producto).

Ver detalle marcado por tarea abajo y MIGRACION.md para el siguiente incremento.

Context

Morphos is a veterinary diagnostic-support SPA (canine/feline) that detects clinical patterns from lab values in real time and optionally asks an AI model (medGemma 1.5 4B via a HuggingFace Gradio Space, or local Ollama) for a clinical interpretation. It ships as vanilla JS (no build step) + thin PHP proxies, deployed as Docker on HF Spaces.

The app works, but three things block it from being a trustworthy diagnostic assistant:

  1. The AI layer fights the model instead of controlling it. js/ia.js builds the prompt by string concatenation, injects a raw <unused95> control token, and post- processes output through limpiarRespuesta() — ~70 lines of regex hacks stripping control tokens, English-only reasoning, LaTeX, and looped paragraphs. These are symptoms of an uncontrolled inference path (raw Gradio Space, no chat template, no structured output, no grounding).
  2. There are zero evals. For a tool that suggests differentials on real patients, there is no measurement of accuracy, groundedness, or safety. This is the highest-risk gap.
  3. Security holes: the AI and papers proxies are unauthenticated with open CORS (anyone can call api/hf_proxy.php and burn the HF key), there is no rate limiting anywhere, the SQLite DB with password hashes may be web-reachable, and MySQL defaults to root/empty-password.

The vet-validated pattern engine (js/analisis.js, data/*.json) is the crown jewel and must be preserved exactly through the migration.

Decisions (confirmed with the user)

  • AI model strategy: Hybrid. Keep self-hosted medGemma/Ollama as the private default; add an optional hosted frontier-model (Claude) route for higher accuracy. The eval judge always uses a strong model.
  • Backend: Python FastAPI for the AI/RAG/eval service (best ecosystem).
  • Frontend: Incremental. Add Vite + TypeScript, port modules (especially analisis.js) to typed TS with unit tests, keep the existing DOM UI. No React rewrite.
  • Hosting: stay on HF Spaces (PRO). Because the RAG corpus is static licensed books, build the vector index offline and bake it read-only into the Docker image — no paid persistent storage needed. App Space (CPU-basic, FastAPI + static assets) + the existing ZeroGPU medGemma Space + optional Claude route.

HF hosting check: Free Spaces have an ephemeral filesystem; persistent storage is a paid add-on ($5/mo for 20 GB up). PRO ($9/mo) gives 10 ZeroGPU/Docker Spaces + 1 TB repo storage. Since the RAG corpus is static, the vector index is baked into the image and needs no persistent-storage purchase — the plan fits inside the PRO subscription.


Target Architecture

┌────────────────────────────────────────────────────────────────┐
│  App Space (HF, CPU-basic, Docker)                               │
│                                                                  │
│  Frontend (Vite build → static)          Backend (FastAPI)       │
│  ├─ analisis.ts  (ported engine, tested) ├─ /api/auth   (session)│
│  ├─ ui.ts / pdf-parser.ts / papers.ts    ├─ /api/interpret (AI)  │
│  └─ ia.ts (calls /api/interpret)         ├─ /api/papers  (PubMed)│
│                                          ├─ RAG retriever        │
│                                          │   └─ baked vector idx │
│                                          └─ rate limit + authz   │
│                                                                  │
│  Persistence: SQLite (users) outside webroot                     │
└───────────────┬──────────────────────────────┬──────────────────┘
                │ hybrid route                  │
     ┌──────────▼─────────┐          ┌──────────▼──────────┐
     │ medGemma ZeroGPU   │          │ Claude API (opt.)   │
     │ Space (private)    │          │ (higher accuracy)   │
     └────────────────────┘          └─────────────────────┘

Monorepo layout after migration:

morphos/
├── frontend/         # Vite + TS; ported js/ modules, unit tests (Vitest)
├── backend/          # FastAPI: auth, interpret, papers, rag, security
│   ├── app/
│   │   ├── main.py            # FastAPI app, middleware (CORS, rate limit)
│   │   ├── routers/           # auth.py, interpret.py, papers.py
│   │   ├── ai/                # model clients (medgemma, claude), prompt, schema
│   │   ├── rag/               # ingestion, chunking, embeddings, retriever
│   │   └── security/          # authz deps, rate limiter, session
│   └── tests/
├── evals/            # golden dataset, promptfoo + Ragas configs, CI gate
├── data/             # valores_referencia.json, alteraciones.json (unchanged)
│   └── rag_index/    # baked vector index (built in CI, shipped read-only)
├── Dockerfile        # multi-stage: build frontend + index, run FastAPI+static
└── docker-entrypoint.sh

Phase 1 — Frontend modernization (incremental, low risk) — ✅ (salvo config ESLint)

Goal: add tooling and types without changing behavior. The engine is vet-validated; migration must be behavior-preserving and proven so by tests.

  • 🟡 Introduce frontend/ with Vite + TypeScript + Vitest. Add package.json, tsconfig.json, ESLint/Prettier. Keep Spanish naming per CLAUDE.md; no alignment spaces; no !important in CSS. — Vite + TS + Vitest ✅ (frontend/package.json, tsconfig.json, vite.config.ts). Falta: no hay archivo de config ESLint/Prettier (sólo la dependencia y el script lint).
  • ✅ Port js/analisis.jsfrontend/src/analisis.ts first. It is pure/stateless (single export analizarResultados), so it types cleanly and is fully unit-testable. Write a golden test suite that pins current behavior (severity classification, age/breed/sex adjustments, all ~60 pattern rules) using fixtures derived from data/*.json. This suite doubles as the regression net for the whole migration. — analisis.ts + tipos.ts + 27 tests Vitest (32 en total con lab-import.test.ts), typecheck limpio. Verificado.
  • ✅ Port remaining modules (main.js, ui.js, ia.js, pdf-parser.js, auth.js, papers.js, tooltip.js) to .ts. Preserve the getter-callback wiring in main.js and the imagenesDataUrl / capturasMicroscopio shared arrays in ui.js. — Todos portados (frontend/src/*.ts) + helper dom.ts. Typecheck limpio; verificados en el navegador (motor, PDF, auth). auth.ts/papers.ts apuntan a los endpoints nuevos.
  • ✅ Keep index.html markup and css/styles.css as-is initially; wire the Vite-built bundle in place of the ?v=N module tag. Remove the leftover console.log debug blocks in ia.js. — index.html carga /frontend/src/main.ts (Vite lo empaqueta). El ia.js legacy ya no se carga (código muerto); el nuevo ia.ts no tiene console.log.
  • ✅ Update stale CLAUDE.md facts (analisis.js is 817 lines / 90 analytes / 78 clinical entities; auth + papers layers exist).

Files: new frontend/**; modify index.html script tag; delete debug logging in ia.js.


Phase 2 — AI layer rebuilt on FastAPI (replaces the PHP proxy + regex cleanup) — ✅

Goal: a controlled, model-agnostic inference path with structured, validated output, so limpiarRespuesta() becomes unnecessary.

  • ✅ New FastAPI service (backend/). Replace api/hf_proxy.php with POST /api/interpret and api/papers_proxy.php with GET /api/papers. Keep the PubMed disk-cache logic (port papers_proxy.php behavior). Keep api/auth.php semantics but reimplement in FastAPI (Phase 5 hardens it). — Endpoints creados; caché de PubMed portada; auth reimplementada. Los .php legacy aún no se han borrado (siguen en api/) — retirada pendiente hasta migrar la UI.
  • 🟡 Orchestration: Pydantic AI (model-agnostic, typed, structured outputs). Define a ModelClient interface with two implementations: MedGemmaClient (self-hosted default; talk to the ZeroGPU Space / Ollama using the proper chat template, not raw <unused95> injection) and ClaudeClient (hybrid opt-in via the Anthropic SDK). Backend selection replaces the current localStorage mx-ia-backend toggle and is enforced server-side. — Desviación: se implementó un protocolo ClienteModelo propio (app/ai/base.py) con MedGemmaClient + ClaudeClient y selección server-side, en vez de la librería Pydantic AI. Cumple el objetivo (salida tipada/validada) con menos dependencias.
  • Structured output schema (Pydantic) — the core fix. Instead of free text, the model returns: interpretacion (es), hallazgos_clave[] (analyte, direccion, gravedad), diferenciales[] (nombre, probabilidad, evidencia[], citas[] → RAG sources), siguientes_pruebas[], confianza, requiere_derivacion (safety flag), idioma="es". Enforce with structured/JSON output; validate server-side; on validation failure, retry or return a typed error — never ship unparsed model text. This deletes the entire limpiarRespuesta regex pile. — app/schemas.py::InterpretacionClinica; reintento + error tipado en service.py.
  • ✅ Prompt construction moves server-side into backend/app/ai/prompt.py, assembled from patient data + flagged findings + detected patterns (reuse the enrichment already in construirPrompt) + retrieved RAG context (Phase 3). System prompt hardened: Spanish- only, scope limits, mandatory hedging/deferral language, citation requirement, injection resistance for free-text signos-clinicos and citology images.
  • ✅ Vision path preserved: medGemma multimodal for citology images; keep the 1024px client resize and ≤4 images limit, but validate size/count/mime server-side (Phase 5).
  • ✅ Frontend ia.ts becomes a thin typed client of /api/interpret that renders the structured result (findings, ranked differentials with citations, next steps, a visible "requiere derivación veterinaria" banner when flagged). — Código completo (frontend/src/ia.ts), cableado vía el bundle TS y ejercitado en el navegador. Falta ver el render con salida de un modelo real (ollama pull pendiente).

Files: new backend/app/ai/**, backend/app/routers/interpret.py, .../papers.py; rewrite frontend/src/ia.ts; remove api/hf_proxy.php, api/papers_proxy.php.


Phase 3 — RAG layer (structure now; books ingested later) — ✅

Goal: ground interpretations in the licensed veterinary literature with verifiable citations, built so the corpus can be dropped in when provided.

  • Stack: LlamaIndex for ingestion/retrieval; LanceDB (embedded, file-based → bakes cleanly into the image, zero runtime services) as the vector store. Embeddings via a self-hosted multilingual model (e.g. BGE-M3 through sentence-transformers) to keep the privacy positioning — no document content leaves the box at index time. — Dependencias en el grupo rag de pyproject.toml; ingesta usa pypdf + sentence-transformers + lancedb directamente (llama-index-core disponible).
  • Ingestion pipeline (backend/app/rag/ingest.py, run offline/CI, not at request time): PDF/EPUB → text extraction → structure-aware chunking (section/heading, ~512–1024 tokens, overlap) → metadata per chunk (libro, edicion, capitulo, pagina, especie?) → embed → write LanceDB table under data/rag_index/. A manifest records corpus version + hash for eval reproducibility. — Escribe en instance/rag_index/ (no data/, por seguridad — ver Fase 5). Manifiesto ✅.
  • Retriever (backend/app/rag/retriever.py): hybrid search (dense + BM25/keyword), metadata filtering by species, top-k with a rerank step; returns chunks with source metadata so the interpret schema's citas[] point to libro/edicion/página. Retrieval is triggered by detected patterns + flagged analytes (reuse ultimoAnalisis), so it targets the patient's actual findings. — Denso + BM25/FTS fusionados con RRF + rerank con cross-encoder bge-reranker-v2-m3, filtro por especie y citas ✅. Conmutables por rag_hibrido/rag_rerank (config.py), con degradación en cascada: rerank → RRF → sólo denso. Añadido traduccion_consulta.py (consulta ES → corpus EN). Cubierto por test_retriever_hibrido.py + test_retriever_integracion.py.
  • Copyright safety: retrieval returns short grounding snippets to the model only; the API never exposes full book text to the client, and the endpoint is auth-gated.
  • Ship path: index built in CI and copied into the Docker image read-only. No HF persistent storage required. Rebuild only when the corpus changes. — Dockerfile copia instance/ de sólo lectura.
  • Placeholder now: commit the full pipeline + an empty/sample index and a books/README.md describing the expected input layout, so ingesting the real books later is a single make ingest step. The interpret path must degrade gracefully to no-RAG when the index is empty. — Superado: ya no es un placeholder. Corpus real ingerido con make ingest — 6763 fragmentos de 2 libros (Fundamentals of Veterinary Clinical Pathology 3e; Veterinary Hematology, Clinical Chemistry, and Cytology 3e) en instance/rag_index/ (LanceDB 69 MB, bge-m3, chunks 450 tok / solape 64, troceo estructural markdown, índice FTS, manifiesto con hash_corpus). Degradación a sin-RAG sigue probada. El índice NO se comitea (queda fuera del repo; se construye en CI y se copia a la imagen).

Files: new backend/app/rag/** (ingest.py, retriever.py, traduccion_consulta.py), instance/rag_index/ (artefacto construido, gitignored — no data/, por seguridad), books/README.md, Makefile (ingest, retrieval-eval).


Phase 4 — Rigorous evals (the core requirement for a diagnostic tool) — 🟡

Goal: measure accuracy, groundedness, and safety continuously, and gate deploys on it.

  • 🟡 Golden dataset (evals/dataset/): curate expert-validated cases. The user already has real lab results + citologies from practicing vets (per USO_DE_IA.md) — turn these into labeled cases: patient signalment + lab values → expected key findings, expected differentials (acceptable set), red-flag/safety expectations, and out-of-scope cases the model must decline. Version the dataset; keep a held-out split. — casos.jsonl ampliado de 7 a 17 casos (IMHA, hipertiroidismo, HAC, pancreatitis, trombocitopenia, hipercalcemia, enteropatía perdedora, gammapatía, hepatocelular agudo, leucocitosis inflamatoria… + el caso fuera-de-alcance) + README con esquema ✅. Falta: split reservado (ningún caso lleva campo split) y casos reales del veterinario.
  • Frameworks (layered):
    • promptfoo — config-driven prompt/response regression in CI: deterministic assertions (language = es, valid schema, required derivation flag present when expected, no forbidden claims), plus LLM-rubric graders. — evals/promptfooconfig.yaml presente (no ejecutado aquí, requiere modelo).
    • Ragas — RAG-specific metrics: faithfulness/groundedness (no claims beyond retrieved context), context precision/recall, answer relevance, citation correctness. — Dependencia en el grupo evals (pyproject.toml) y mencionada en los READMEs, pero cero líneas de código Ragas en el repo. Ya está desbloqueado: el índice está poblado. Es el hueco más grande que queda del plan.
    • Evaluación de recuperación aislada (no estaba en el plan original, añadida para decidir por datos): evals/run_retrieval_eval.py + dump_retrieval.py miden si los fragmentos recuperados son relevantes al diagnóstico esperado, permitiendo A/B de modelo de embeddings × idioma de consulta (bge-m3 ES vs EN vs MedCPT) antes de invertir en rerank. Juzga con Claude si hay ANTHROPIC_API_KEY, si no con heurístico de solape. Expuesto como make retrieval-eval.
    • Custom clinical rubric (evals/judge/) — LLM-as-judge (strong model, e.g. Claude) scoring: differential correctness vs. expected set, appropriate hedging, safety (no dangerous/overconfident recommendations), completeness. — evals/judge/clinical_judge.py (se activa con ANTHROPIC_API_KEY).
  • 🟡 Metrics & thresholds: per-metric pass bars (e.g. groundedness ≥ X, differential recall ≥ Y, safety violations = 0). Track per model route (medGemma vs Claude) and per corpus version so RAG changes are attributable. — Umbrales + agregación en run_evals.py ✅. Falta: informe explícito por-ruta/por-versión-de-corpus.
  • CI gate: GitHub Actions runs the eval suite on PRs touching backend/app/ai, backend/app/rag, prompts, or the index; block merge/deploy on regression or any safety failure. Store run artifacts/scorecards. — .github/workflows/evals.yml; bloqueo verificado (exit=1 ante violación de seguridad).
  • Human-in-the-loop: a lightweight review workflow for a veterinarian to accept/correct model outputs, feeding new cases back into the golden set. Log (privacy-safe) production interpretations for periodic expert audit. — No implementado.

Files: new evals/**, .github/workflows/evals.yml.


Phase 5 — Security audit & rate limiting — ✅

Findings from the current code, severity-ranked, each with the fix folded into the new FastAPI backend.

Critical

  1. Unauthenticated AI/papers proxies + open CORS (api/hf_proxy.php, papers_proxy.php send Access-Control-Allow-Origin: *, no session check). Anyone can call them directly and burn the HF key / abuse PubMed. → Require an authenticated session on /api/interpret (and rate-limit papers); lock CORS to the app origin. — Depends(usuario_actual) en interpret (401 verificado en producción). Corrección (2026-07-26): papers NO lleva guarda de auth/api/papers?query=… responde 200 sin sesión; sólo está limitado a 30/min por IP. Coincide con lo que pide el punto («auth en interpret, rate-limit en papers»), pero la anotación anterior decía «interpret/papers» y era falsa. No hay clave de API en juego (eutils se consulta sin credencial), así que el riesgo se limita a abuso del proxy. CORS a orígenes fijos: verificado en local — un origen no listado no recibe Access-Control-Allow-Origin. Ojo en producción: el proxy de HF Spaces añade por su cuenta Access-Control-Allow-Origin: <origen> y expose-headers: * por encima de la app, así que el bloqueo de CORS queda neutralizado en la plataforma. Lo que sostiene la defensa ahí es SameSite=Strict en las cookies de sesión y CSRF: una petición cross-site no las lleva.
  2. Password-hash DB may be web-served. SQLite fallback writes data/morphos.db under the web root; .htaccess only denies .env/setup.php, so /data/morphos.db may be downloadable, and .htaccess is Apache-only. → Move the DB outside the served root; in FastAPI nothing under the DB path is a static route. Never rely on .htaccess for secret protection. — BD en instance/ (fuera del webroot); sólo dist/ y /data (JSON) se montan.

High 3. 🟡 No rate limiting anywhere. → Add slowapi (or Redis-backed) limits: per-IP + per- user on /api/interpret (expensive), per-IP on /api/auth login (brute force) and /api/papers. Return 429 with retry-after. — slowapi por-IP en interpret/login/papers + manejador 429 con Retry-After ✅. Falta: límite adicional por-usuario (actualmente sólo por-IP). 4. ✅ Login brute force — no throttling/lockout. → Per-account + per-IP attempt throttling with backoff; keep the already-generic "email o contraseña incorrectos" message. — Tabla intentos_login + throttle por email+IP; mensaje genérico. 5. ✅ MySQL root / empty password default (conexion.php). → Require DB credentials from env; no hardcoded defaults; fail closed if unset. — El backend nuevo no tiene credenciales por defecto (config por entorno); SQLite por defecto.

Medium 6. ✅ Secrets via .env in webroot guarded only by .htaccess. → Load secrets from real env vars (HF Spaces secrets); keep .env out of any served directory. — pydantic-settings; backend/.env gitignored; nada bajo el root servido. 7. ✅ No CSRF protection on session POSTs. → SameSite=Strict + Secure + HttpOnly session cookies, plus a CSRF token (or move to short-lived bearer tokens). — Cookie firmada HttpOnly/SameSite/Secure + CSRF de doble token (403 verificado). 8. ✅ Server-side upload validation missing for citology images (base64 decoded with no size/count/mime cap → memory DoS). → Enforce ≤4 images, max bytes, allowed mime, safe decode server-side. — interpret.py::_validar_imagenes (nº, mime, tamaño, decode seguro). 9. ✅ Prompt-injection surface via free-text signos-clinicos and citology images. → Harden system prompt, treat user text as data, validate structured output, cap output scope. — System prompt endurecido (trata el texto como datos) + salida estructurada validada.

Low 10. ✅ Remove console.log prompt/PII leakage in ia.js. — El nuevo ia.ts (ya en uso) no registra el prompt. El ia.js legacy es código muerto (no se carga) pendiente de borrar junto al resto de js/*.js. 11. 🟡 Raise password min length (6 → ≥8) + optional breach check; registration keeps 409 but is now rate-limited (acceptable enumeration risk). — Mínimo 8 ✅ (RegistroBody). El chequeo de brechas (opcional) no se implementó. 12. 🟡 Add security headers (CSP, HSTS, X-Content-Type-Options, Referrer-Policy) and enforce HTTPS. — Cabeceras ✅ (headers.py; HSTS en prod). Falta: redirección explícita HTTP→HTTPS (se delega en la plataforma). Nota: CSP ajustada a worker-src blob: para PDF.js.

New surface introduced by RAG: ✅ licensed book text must never be exfiltratable — retrieval returns only short grounding snippets to the model, the API never serves full book content, and the corpus lives read-only in the image. — Índice en instance/, endpoint auth-gated.

Files: backend/app/security/**, backend/app/routers/auth.py, DB path change in the connection layer, Dockerfile/entrypoint (secrets, DB location, headers).


Verification

  • Engine parity (Phase 1): the analisis.ts golden test suite (Vitest) passes with outputs identical to the current JS engine across fixture cases spanning both species and all pattern rules. This is the gate that the migration preserved validated behavior. — 27/27 verde (32/32 con los tests de import de laboratorio). Reejecutado 2026-07-26.
  • 🟡 AI path (Phase 2): integration test that /api/interpret returns schema-valid structured output for representative cases on both the medGemma and Claude routes; confirm limpiarRespuesta is gone and no unparsed text can reach the client. Drive the real UI to confirm findings, ranked differentials with citations, and the derivation banner render. — UI real verificada: el botón "Análisis IA" llama a POST /api/interpret con CSRF y el backend alcanza el modelo (Ollama). Salida estructurada validada por test de integración; limpiarRespuesta no existe en el código nuevo. Falta: ollama pull del modelo para ver el render de diferenciales/citas con datos reales, y la ruta Claude ejercitada en vivo.
  • RAG (Phase 3): with a small sample corpus, verify retrieved citations resolve to the correct libro/página and that an empty index degrades gracefully to no-RAG. — Degradación a sin-RAG ✅. Resolución de citas ✅: test_retriever_integracion.py construye un índice LanceDB real (con FTS) y comprueba recuperación end-to-end y filtro por especie; el corpus real de 2 libros está ingerido. 97 tests backend verde (2026-07-26).
  • 🟡 Evals (Phase 4): promptfoo eval + Ragas + the clinical rubric run green locally and in CI; deliberately regress a prompt and confirm the CI gate blocks it; confirm a safety- violation case fails the suite. — Puerta CI: bloqueo por violación de seguridad verificado (exit=1). Falta: ejecutar promptfoo (requiere modelo) y Ragas (sin implementar).
  • 🟡 Security (Phase 5): confirm unauthenticated /api/interpret returns 401; CORS rejects foreign origins; rate limits return 429 under load; the DB path is not reachable as a static route; MySQL refuses to start without credentials. Run /security-review on the diff. — 401 sin sesión ✅, 403 sin CSRF ✅, BD fuera del webroot ✅. Falta verificar en vivo: rechazo CORS de orígenes externos, 429 bajo carga, y ejecutar /security-review.

Sequencing & risk notes

  • Ship in order: Phase 1 → 5 → 2 → 3 → 4. Rationale: lock behavior with tests first (1), close the exploitable holes early (5), then rebuild the AI path (2), add grounding (3), and stand up evals (4) — though the golden dataset from Phase 1 tests and the eval dataset should be gathered in parallel from the start.
  • Biggest risk is regressing the vet-validated engine; the Phase 1 golden suite mitigates it and must exist before any TS port lands.
  • The real licensed books are pendingresuelto (2026-07-26): 2 libros ingeridos (6763 fragmentos). Los PDF con licencia viven en books/ sin comitear y el índice en instance/ (gitignored); ambos se reconstruyen con make ingest. Queda pendiente medir con evals la mejora de exactitud que aporta el corpus (bloqueado por Ragas + promptfoo sin ejecutar).
  • Hybrid model routing and the eval judge may use a hosted model; confirm a DPA covers any patient data sent on the Claude route (the medGemma route stays fully self-hosted for the privacy-default positioning).