Qué prueba gana un caso de residencia fiscal, según los tribunales.
Proyecto con 106 sentencias fuente del Tribunal Supremo y la Audiencia Nacional (2015-2025) sobre residencia fiscal de personas físicas (Art. 9 LIRPF). El corpus estructurado v3 está validado por ahora sobre una muestra de cinco para consulta jurídica:
- Criterios de residencia aplicados: 183 días, ausencias esporádicas, centro de intereses económicos y vitales, presunción familiar, tie-breaker del CDI.
- Pruebas aportadas por AEAT y por el contribuyente, con su categoría, su peso, si el tribunal las aceptó y por qué, con cita literal y página.
- Razonamiento judicial: doctrina citada, sobre quién recaía la carga de la prueba y si la cumplió.
- Resultado:
GANA_AEAT/GANA_CONTRIBUYENTE/PARCIAL/RETROACCION/INADMISION.
La pregunta de fondo no es qué dice la ley, sino qué acepta un juez como prueba. Eso solo se ve agregando sentencias.
El corpus de hoy es español. El pipeline no lo es: cualquier jurisdicción puede entrar si alguien aporta su jurisprudencia — ver Un país, un corpus.
Warning
La estructura jurídica la propone un agente y puede contener errores. No
es asesoramiento jurídico ni fiscal. Python conserva el texto literal, hashes
y validaciones; para citar una resolución, usa siempre el texto oficial del
CENDOJ. Ver sentencias/AVISO_LEGAL.md.
El corpus se prepara offline sin llamadas del repositorio a APIs de modelos. El gateway se reserva para responder preguntas del chat online, que permanece cerrado por defecto mientras no se autorice su despliegue.
flowchart LR
PDFS["sentencias/<br/>PDF oficiales"] --> PY["Python<br/>texto · páginas · hashes"]
PY --> AGENT["Agente<br/>propuesta jurídica"]
AGENT --> GATES["Python<br/>gates · citas · compilación"]
GATES --> CORPUS["knowledge/jurisprudencia-v3"]
QUESTION["Pregunta del usuario"] --> NETLIFY["Netlify Function V1<br/>/api/chat · A y B en paralelo"]
NETLIFY --> A["A · recuperación v3<br/>Luna + high"]
CORPUS --> A
PDFS --> B["B · Gemini File Search<br/>PDF de la muestra"]
NETLIFY --> B
A --> ANSWER["Dos respuestas separadas<br/>citas + coste USD"]
B --> ANSWER
| Archivo | Función |
|---|---|
src/verbatim_*.py |
Extracción literal por páginas y hashes |
src/jurisprudence_*.py |
Compilación, validación y recuperación del corpus v3 |
src/chat_model_policy.py |
Política de inferencia de la estrategia A del chat |
src/gateway_setup.py |
Clientes, uso y costes de las respuestas del chat |
src/api/ |
Prototipo local del chat y posible runtime futuro para llamadas largas |
frontend/ |
SPA React y Function Netlify-only V1, implementada y cerrada por configuración |
La vista completa de componentes, flujos e invariantes está en
docs/ARCHITECTURE.md. La guía sobre dónde debe vivir
cada tipo de archivo está en
docs/REPOSITORY_STRUCTURE.md.
El repositorio versiona fuentes originales y, a partir de ellas, corpus derivados y regenerables. Las fuentes nunca se editan a mano y su texto no se reescribe en ningún punto del pipeline.
| Fuente | Derivado | Cómo se genera |
|---|---|---|
sentencias/ — 106 PDF del CENDOJ |
knowledge/jurisprudencia-v3/ |
Python + agente + gates literales |
normativa/es/ — 106 normas en XML del BOE |
knowledge/normativa/es/preceptos/ |
Determinista, sin LLM (make export-normativa) |
Del corpus normativo se publica un Markdown por artículo, no por ley: los
preceptos que deciden o prueban la residencia fiscal, más el artículo de
residencia de cada uno de los 98 convenios de doble imposición firmados por
España, que se guardan enteros en normativa/es/ tal como los
sirve el BOE (inventario). De ahí sale el convenio
que publica la página de cada país: /francia enlaza el convenio
España-Francia, su artículo de residencia y el texto oficial en el BOE. Un
tercer artefacto, knowledge/normativa/es/enlaces/, resuelve qué preceptos cita
cada sentencia y con qué redacción del ejercicio enjuiciado.
Ver docs/normativa/NORMATIVA.md.
La residencia fiscal se decide en los tribunales de cada país, y la pregunta es la misma en todos: qué prueba acepta un juez. Lo que cambia es el articulado y quién lo interpreta.
España está publicada porque su jurisprudencia se delimitó con criterio jurídico-tributario, no porque el proyecto sea español: qué resoluciones importan, qué criterios del art. 9 LIRPF se aplican y en qué doce categorías se clasifica la prueba son decisiones de derecho tributario, no de un modelo. El pipeline es agnóstico de la jurisdicción; el criterio, no. Por eso el proyecto se nutre de la contribución de expertos en fiscalidad y tributación internacional: abogados y asesores fiscales, académicos, documentalistas jurídicos, traductores jurídicos, economistas y peritos, además de desarrolladores.
Tip
Propón tu país o escribe a info@residenciafiscal.org — cualquier jurisdicción, no solo las que ya tienen ruta en la web. No hace falta saber programar: lo que falta es criterio jurídico, no código. Página pública: residenciafiscal.org/colaborar.
Un país entra cuando existen tres cosas. Rara vez las aporta una sola persona:
| Lo que hace falta | Por qué |
|---|---|
| Una fuente pública oficial de resoluciones, con sus condiciones de reutilización | El corpus se publica desde la fuente original y sin licencia clara no se publica. Los PDF deben llevar capa de texto: no hay OCR |
| El precepto nacional que decide la residencia — el equivalente al art. 9 LIRPF — y el artículo de desempate de sus convenios | El análisis de una sentencia no se sostiene sin la norma que aplica |
| Un especialista que valide el resultado | La propuesta jurídica del agente puede equivocarse. Ningún país se publica sin que un profesional del derecho tributario de esa jurisdicción lo valide |
Lo que no hace falta aportar: el pipeline, la verificación de citas contra el documento fuente, el schema de extracción ni el frontend. Eso ya existe y es común a todos los países.
Dos invariantes rigen cualquier corpus nuevo, igual que el español:
- El texto de una resolución no se reescribe. Ni se corrige, ni se completa, ni se parafrasea. Una cita solo se publica desde una subcadena exacta del texto extraído del documento oficial. Las correcciones viven en metadatos aparte.
- Cada corpus se aísla del resto. Una consulta sobre un país no puede devolver una cita de otro, y hay tests que lo comprueban.
El criterio para arrancar el siguiente país no es el orden de llegada: es el primero que reúna una fuente reutilizable y un revisor comprometido. El proyecto lo mantiene una persona en su tiempo libre, así que no hay plazos prometidos.
El detalle operativo y la tabla de perfiles están en
CONTRIBUTING.md; el
estado de las páginas por país, en
docs/product/COUNTRY_PAGES.md.
Requiere uv — gestiona Python y las dependencias.
curl -LsSf https://astral.sh/uv/install.sh | sh # si no lo tienes
make setup # instala Python 3.13 + dependencias en .venv
make help # lista todos los comandosNo hace falta activar ningún entorno: uv run lo resuelve solo.
make export-verbatim # extrae el PDF piloto sin LLM
make export-case-v3 # compila el caso propuesto por el agente
make export-case-v3-sample # regenera y valida la muestra de cinco
make dev # API + frontend en desarrollo
make dev-api # solo API HTTP, Swagger en http://127.0.0.1:8010/docs
make fast-check # lint + typecheck + testsLa propuesta jurídica de cada sentencia se prepara en una sesión de agente. No existe un target que envíe los PDF a OpenAI. Los comandos Python extraen, validan y compilan de forma determinista.
| Método | Ruta | Descripción |
|---|---|---|
| GET | /health |
Estado y frontera entre corpus y chat |
| GET | /config |
Política del chat, criterios y categorías |
| GET | /docs |
Swagger UI |
| POST | /chat |
Comparación A/B por SSE; cerrada por defecto |
La API no expone /analizar. /chat utiliza el gateway únicamente después de
recuperar evidencia y exige habilitación y autenticación server-side.
SPA React (Vite 8 + React 19 + TypeScript 7 + Tailwind 4) desplegada en Netlify: un chatbot que consulta el corpus de sentencias en lenguaje natural.
cd frontend
npm install
npm run dev # http://127.0.0.1:5174
npm run fast-check # lint + typecheck + tests
npm run build # genera el corpus y compila a dist/Note
El chat está activo en Production desde el 31 de julio de 2026: la Function
Netlify-only V1 ejecuta A y B en paralelo y conserva Luna high. El rollback
es volver a stub. La activación técnica no cierra la privacidad ni la
revisión jurídica del corpus, que siguen pendientes. El recorrido
Edge → FastAPI se conserva como opción futura para llamadas de más de 60 s.
Runbook: docs/operations/CHAT_DEPLOYMENT.md.
Sentry instrumenta los tres runtimes, cada uno en su propio proyecto: la API
FastAPI (src/api/sentry_config.py), la SPA React
(frontend/src/lib/sentry-runtime.ts) y la Netlify Function del chat
(frontend/netlify/functions/chat/observability.ts).
Los tres se inicializan solo con la telemetría habilitada y un DSN presente,
envían sendDefaultPii: false y borran cabeceras, cookies y cuerpo de la
petición antes de enviar el evento: una pregunta del chat es dato fiscal y no
debe viajar a un servicio de errores. La suite de pytest no instrumenta nada,
aunque el .env local tenga la telemetría encendida, porque provoca excepciones
a propósito.
La Function no usa el SDK: construye el envelope con fetch y envía un evento
sintético con código de fallo, etapa, request_id y nombre de clase del error.
Nunca viaja la pregunta, la respuesta ni el message de la excepción del
proveedor. El coste se sigue observando por chat_cost_reconciled en los logs y
por el resumen diario del ledger, no por Sentry.
Las incidencias accionables de los tres proyectos entran en el mismo autofix
aislado y con gates que Presupuestor; arquitectura y guardrails en
docs/operations/AUTOFIX.md.
| Variable | Ámbito | Nota |
|---|---|---|
SENTRY_ENABLED, SENTRY_BACKEND_DSN |
API | Sin ambas, la API no inicializa Sentry |
SENTRY_ENVIRONMENT, SENTRY_RELEASE, SENTRY_TRACES_SAMPLE_RATE |
API | Entorno, versión y muestreo de trazas |
VITE_SENTRY_ENABLED, VITE_SENTRY_DSN |
Frontend | Todo VITE_* es público en el bundle; el DSN identifica el proyecto, no es un token |
VITE_SENTRY_ENVIRONMENT, VITE_SENTRY_TRACES_SAMPLE_RATE |
Frontend | Equivalentes de navegador |
CHAT_SENTRY_ENABLED, CHAT_SENTRY_DSN |
Netlify Function | Proyecto residencia-fiscal-chat. Runtime de servidor: nunca con prefijo VITE_. La cuenta Legacy no admite scope Functions; van como variables ordinarias de production |
SENTRY_ORG_SLUG, SENTRY_TOKEN |
Build | Solo para subir sourcemaps. Nunca con prefijo VITE_ |
Nombres y valores de ejemplo, en .env.example.
| Documento | Contenido |
|---|---|
docs/README.md |
Índice temático de toda la documentación |
docs/ARCHITECTURE.md |
Componentes, flujos, límites e invariantes |
docs/REPOSITORY_STRUCTURE.md |
Convenciones de carpetas y rutas |
CLAUDE.md |
Guía operativa, comandos, costes y troubleshooting |
CONTRIBUTING.md |
Entorno, gates de CI y qué se espera de un PR |
SECURITY.md |
Cómo reportar una vulnerabilidad y qué está en el alcance |
Código y documentación bajo licencia MIT.
Los documentos jurídicos que el repositorio incluye no están cubiertos por
esa licencia; la salvedad completa está en NOTICE.md. Cada corpus
se rige por las condiciones de reutilización de su fuente:
- Las resoluciones judiciales de
sentencias/son documentos públicos del CENDOJ, publicados ya pseudonimizados —sentencias/AVISO_LEGAL.md. - Los textos legales de
normativa/proceden del BOE, cuya edición oficial es la única versión con valor jurídico —normativa/es/AVISO_LEGAL.md.
- Art. 9 LIRPF — residencia habitual en territorio español
- Modelo de Convenio OCDE, Art. 4 — reglas de desempate de los CDI
- CENDOJ — buscador de jurisprudencia del CGPJ
- Datos abiertos del BOE — origen del corpus normativo de
normativa/