Visión general de la arquitectura¶
Cadrumo es una aplicación local-first que prepara declaraciones tributarias españolas a través de la interfaz de línea de comandos aeat. Modela el registro normativo de la autoridad tributaria, ingiere y clasifica tus registros financieros y calcula las casillas numeradas de cada modelo. Después comprueba el borrador y exporta un fichero que tú mismo presentas. La AEAT es la Agencia Estatal de Administración Tributaria, la agencia tributaria de España.
Usa esta página como punto de entrada al leer el código por primera vez. Es un mapa, no un directorio: nombra las capas, los conceptos fundamentales y los tipos canónicos a los que navegará, y ahí se detiene. Para las firmas y los comandos exactos, sigue los enlaces de Entrar en el código.
Por qué el sistema tiene esta forma¶
Preparar a mano una declaración tributaria española es propenso a errores y difícil de auditar. Una cifra de un modelo tiene que poder rastrearse hasta un movimiento bancario, una norma y una regla publicada. Un modelo anual posterior tiene que mantenerse coherente con los trimestrales que lo alimentaron. Tres decisiones de diseño responden a ese problema, y dan forma a todo lo demás:
Local-first. Tus registros financieros nunca salen de tu máquina. La herramienta funciona sin conexión y guarda cargas cifradas en una base de datos local en disco.
El registro es la autoridad. Las reglas tributarias (tipos, tramos, plazos y la base legal de cada casilla) no están escritas en el código. Se redactan como datos, se compilan, se validan y se leen a través de una única autoridad. Una cifra siempre puede rastrearse hasta la regla que la produjo.
Presenta la persona, no la herramienta. El flujo termina en un fichero de exportación. Enviarlo a la agencia es un paso humano deliberado. La herramienta no tiene ninguna vía de envío, así que la presentación en vivo está ausente, no simplemente protegida.
La estructura que sigue es hexagonal: las reglas de negocio se sitúan en el centro, y los parsers, los navegadores y el almacenamiento que tocan el mundo exterior se sitúan en el borde. La mayoría de las dependencias apuntan hacia dentro. La composición de la aplicación tiene una excepción explícita y controlada que llega a los adaptadores. Esto mantiene la lógica tributaria comprobable por sí sola y permite cambiar un adaptador sin tocar una regla.
Cómo leer esta página¶
Cuatro vistas cubren el sistema en su nivel más alto. Cada una combina un diagrama con una breve explicación:
Las capas: las cinco capas hexagonales y de qué se encarga cada una.
El flujo de la autoridad del registro: cómo los datos de las reglas tributarias se convierten en los snapshots que lee el código en tiempo de ejecución.
El ciclo de vida del modelo: el recorrido desde los registros hasta un fichero de exportación, y cómo se fundamenta cada cifra.
La persistencia y el límite de seguridad: dónde vive el estado y por qué nada sale.
Las capas¶
Cadrumo separa las responsabilidades en cinco capas bajo src/cadrumo/. Las capas normalmente dependen solo de las capas interiores a ellas; la composición de la aplicación tiene la excepción documentada de los adaptadores.
flowchart TD
EP["entrypoints — CLI (config · app)"]
AD["adapters — inbound · outbound · persistence"]
APP["application — use-case services"]
DOM["domain — tax rules and entities"]
CORE["core — enums · JavaScript Object Notation (JSON) contract · config · primitives"]
EP --> APP
EP --> CORE
APP --> DOM
APP --> AD
APP --> CORE
AD --> DOM
AD --> CORE
DOM --> CORE
DOM -.->|"secure-repo seam"| AD
Alternativa textual. Los entrypoints llaman a las capas application y core. La application llama a las capas domain, adapters y core; los adapters llaman a domain y core; y domain llama a core. El domain llega a los adapters solo a través de la costura anotada del repositorio seguro.
core` es la capa más interna. Es dueña de los primitivos transversales que comparten todas las demás capas. Sus enums tipados definen conjuntos cerrados de valores como los códigos de periodo, los dominios tributarios, los identificadores de modelo y los tipos de origen de binding. Core también es dueña del contrato de la envoltura JSON, la configuración, los tipos de dinero y de tiempo, y la taxonomía de errores.
**domain
** contiene reglas y registros tributarios puros: el registro de modelos, las definiciones de casilla (una casilla es cada recuadro numerado de un modelo), las observaciones de presentación y los cálculos sobre ellas. Depende solo decore`.application` orquesta los casos de uso. Une las reglas del domain con los adaptadores para construir borradores de presentación, agregar el ledger, ejecutar diagnósticos y proyectar el estado. No realiza ninguna entrada ni salida por sí misma.
**adapters
** conecta la aplicación con el mundo exterior en tres partes.inboundanaliza los extractos entrantes en Portable Document Format (PDF), comma-separated values (CSV), Open Financial Exchange (OFX) y Microsoft Excel Open XML (XLSX).outboundalcanza los servicios externos (sesiones de navegador de la AEAT, oráculos de cálculo y proveedores de autenticación).persistence` almacena las cargas cifradas en la base de datos local.**entrypoints
** expone la aplicación a los operadores. La línea de comandos vive aquí, y su superficie raíz se limita a dos familias de comandos,configyapp`.
Dos reglas de frontera mantienen honestas a las capas. Los datos de frontera cruzan como modelos pydantic v2 validados, nunca como diccionarios sueltos. Los conjuntos cerrados de valores se declaran como enums tipados en core y circulan como miembros del enum, de modo que un valor no válido se rechaza en la frontera.
Las flechas de dependencia apuntan hacia dentro con una única excepción anotada. Tres repositorios del domain (filing, justificante y submission) se apoyan directamente en la clase base de almacenamiento cifrado de adapters.persistence. El diagrama dibuja esa costura como una arista discontinua en lugar de ocultarla.
Dentro de domain y application, los subpaquetes se agrupan en cinco grupos conceptuales. Algunos subpaquetes se omiten por claridad; la referencia de la API los enumera todos.
Grupo |
Subpaquetes de |
Subpaquetes de |
|---|---|---|
Registro y cálculos |
|
|
Ledger y transacciones |
|
|
Presentación y exportación |
|
|
Live, portales y autenticación |
|
|
Perfil y almacenamiento |
|
|
El ciclo de vida del modelo¶
Un modelo es un formulario tributario numerado de la AEAT. Sus datos fluyen en un solo sentido, desde tus registros hasta un fichero que tú mismo subes.
flowchart LR
REC["Financial records — statements · invoices · evidence"]
ING["Ingest and classify — transactions · invoices · evidence"]
LED["Ledger / aggregation substrate — encrypted bucket · resolver mesh"]
CRE["work create"]
CAL["calculate — produces CalculationRevision (casillas)"]
VER["verify — completeness gate"]
FIL["file — local final marker"]
EXP["export — fichero-BOE file on disk"]
HUM{{"Human upload at AEAT sede — outside the tool"}}
AEAT["AEAT"]
REC --> ING --> LED
CRE --> CAL
LED --> CAL
CAL --> VER
VER --> FIL
VER --> EXP
FIL --> EXP
EXP --> HUM
HUM -.->|"manual submission"| AEAT
Alternativa textual. Los registros financieros se ingieren y se clasifican en el ledger cifrado. Un operador crea el trabajo, lo calcula, lo verifica, lo marca como presentado localmente y exporta un fichero. Luego una persona sube ese fichero a la AEAT fuera de Cadrumo.
La herramienta ingiere y clasifica los registros financieros (extractos bancarios, facturas y su evidencia) en el ledger cifrado. A partir de ahí, el recorrido sigue una secuencia fija de verbos de la línea de comandos:
aeat app modelo work createfija una presentación a un modelo, un año y un periodo.calculateresuelve cada casilla y guarda unaCalculationRevision.verifyejecuta una comprobación de integridad y coherencia sobre el borrador.filemarca una revisión verificada como presentada internamente.aeat app modelo exportescribe en disco el fichero oficial de subida (un artefacto fichero-BOE de formato fijo).
La frontera es estructural. file es un marcador local, no un envío, y export escribe un fichero en tu disco. Ningún comando transmite nada a la agencia: la subida es una acción humana en el portal de la AEAT, modelada como un paso más allá de la frontera de la herramienta. Las lecturas en vivo del operador requieren su autenticación normal y las salvaguardas de perfil activo; siguen siendo de solo lectura. La opción aparte CADRUMO_LIVE_TESTS_ENABLED controla las pruebas de lectura en vivo de los desarrolladores.
Cómo se fundamenta una cifra¶
El valor de una casilla no es un número a secas. Lleva su procedencia (las referencias legales, las referencias de origen y el identificador de fórmula que lo produjeron) desde la definición del registro hasta la salida dirigida al operador.
flowchart LR
DEF["Registry casilla definition — legal_refs · source_refs · formula_id"]
subgraph mesh["source-resolver mesh — merge_source_resolutions"]
direction TB
R1["Ledger aggregation resolvers"]
R2["Invoice catalogue resolver"]
R3["Previous-filing resolver"]
R4["Relation-prefill resolver"]
end
ENG["Registry formula engine — evaluates formula_id"]
OBS["CasillaObservation — value + legal_refs + source_refs + formula_id"]
REV["CalculationRevision.observations (+ flat casilla_values)"]
PAY["CLI payloads"]
OPS["Operator surface — JSON envelope + text"]
DEF --> ENG
DEF --> mesh
R1 --> ENG
R2 --> ENG
R3 --> ENG
R4 --> ENG
ENG --> OBS --> REV --> PAY --> OPS
Alternativa textual. Una definición de casilla del registro y las entradas resueltas de ledger, factura, presentación anterior y relación alimentan el motor de fórmulas. Este emite una observación portadora de procedencia que se convierte en una revisión de cálculo, en la carga de la CLI y en la salida dirigida al operador.
Cada binding de una casilla declara un origen tipado. Una malla de resolvers (agregación del ledger, catálogo de facturas, arrastre de la presentación anterior y prellenado por relación entre modelos) convierte esos orígenes en valores de binding, fusionados mediante merge_source_resolutions. El motor de fórmulas del registro evalúa la fórmula de la casilla sobre las entradas resueltas y sella el resultado como una CasillaObservation portadora de sus legal_refs, source_refs y formula_id. Esa observación viaja dentro de la CalculationRevision persistida, luego a las cargas de la CLI y luego hasta el operador. La procedencia nunca se pierde por el camino.
Una vinculación es un contrato de proyección, no un espacio para adjuntar datos. Identifica la familia de origen propietaria, valida el selector y la agregación e indica cómo ese origen se convierte en una entrada de la presentación. Un contrato escalar produce un valor. Las familias de registros repetidos declaran una vinculación tipada por campo de fila. Unen los valores mediante agrupación y un índice de fila que empieza en uno. La revisión de cálculo congela las vinculaciones de fila resueltas. Las rutas que producen filas de detalle validadas las almacenan por separado. Nunca adjunta un bloque opaco de registros a una casilla.
El Modelo 720 demuestra la estructura repetida: su resolvedor de activos extranjeros está inscrito en la malla de orígenes y proyecta campos de activos tipados en vinculaciones de filas del registro. El inventario de existencias es distinto. InventoryLedger es un agregado empresarial implementado y cifrado. La taxonomía de orígenes de vinculaciones no tiene actualmente un tipo de inventario ni un resolvedor de inventario inscrito. Por tanto, permanece fuera de las revisiones de cálculo de los modelos hasta que se añada una proyección con fundamento legal.
La persistencia y el límite de seguridad¶
Todos los datos financieros sensibles viven en un único lugar: un almacén cifrado, por perfil. Es la estructura, no una convención, la que los mantiene ahí.
flowchart TD
OP(["Operator"])
subgraph boundary["Encrypted bucket-scoped store — financial data never leaves (no temp / scratch / plaintext)"]
direction TB
PROF["Active profile — resolve_active_bucket_id"]
SESS["BucketSession — KEK/DEK · idle-timed · one taxpayer at a time"]
FAC["Runtime factories — secure_object_repository_for_active_bucket"]
REPO["SecureObjectRepository — AEAD payloads · HMAC-SHA256 lookup digests"]
MK["MasterKeyProvider — Keyring / FileFallback · AES-256"]
DB[("SQLite with encrypted payload columns")]
ATT["AttachmentStore — put_bytes / read_bytes · content-addressed"]
EV["Evidence bytes — invoices · statements (FINANCIAL)"]
PROF --> SESS --> FAC --> REPO
REPO --> MK
REPO --> DB
ATT --> REPO
EV --> ATT
end
SEDE["adapters/outbound/aeat — read-only live checks"]
AEAT["AEAT"]
HUM{{"Human filing — outside the app"}}
OP --> PROF
SEDE -.->|"read-only operator live checks"| AEAT
OP --> HUM
HUM -.->|"manual submission"| AEAT
Alternativa textual. Un operador selecciona un perfil activo. La sesión de bucket resultante abre las factorías de tiempo de ejecución y un repositorio seguro, que cifra las cargas en la base de datos local y la evidencia adjunta. Las comprobaciones del operador contra la AEAT siguen siendo de solo lectura; solo la acción humana de presentación puede enviar a la AEAT.
Las claves de cifrado de claves (KEK) protegen las claves de cifrado de datos (DEK). El repositorio usa cargas con cifrado autenticado con datos asociados (AEAD) y resúmenes de búsqueda HMAC-SHA256 derivados de material maestro. HMAC significa código de autenticación de mensajes basado en hash. El cifrado es el Advanced Encryption Standard con claves de 256 bits (AES-256).
Trabajas con un contribuyente a la vez. Seleccionar un perfil abre una sesión de bucket, que acota un SecureObjectRepository al almacén cifrado de ese contribuyente. El repositorio cifra cada carga en la frontera de la columna con una clave del MasterKeyProvider: el llavero del sistema operativo, o un respaldo derivado de una frase de paso. Persiste el texto cifrado en una base de datos SQLite local con columnas de carga cifrada. Los bytes de evidencia (facturas, extractos y documentos descifrados) pasan por el AttachmentStore direccionado por contenido, que almacena los bytes en sí, nunca un enlace.
Los bytes de evidencia (facturas, extractos y documentos descifrados) pasan por el AttachmentStore direccionado por contenido, que almacena los propios bytes, nunca un enlace.
Los registros empresariales analizados no se convierten en adjuntos solo porque puedan servir después para una presentación. Las transacciones, los catálogos de facturas y el inventario de existencias siguen siendo agregados tipados separados en el almacenamiento cifrado. El Modelo 720 recibe, en cambio, observaciones tipadas de activos extranjeros del llamador. Un resolvedor de orígenes inscrito proyecta en una revisión de cálculo solo los campos que solicita el registro. En el registro actual del Modelo 100, las casillas de existencias 0177, 0181 y 0182 siguen siendo entradas manuales. El dominio de inventario divide ahora la variación de apertura a cierre de 2025 entre 0177 y 0182, pero esa proyección de dominio no demuestra que exista una ruta activa de inventario a Renta ni afirma todavía que 0181 incluya todas las compras.
Nada de este almacén cruza hacia fuera, hacia la agencia, como escritura. Las comprobaciones en vivo de solo lectura alcanzan la agencia únicamente para leer. La única vía que alcanza la agencia como escritura es la subida humana, que queda más allá de la frontera de la herramienta. Una prueba estructural exige la ausencia de cualquier verbo de escritura en el adaptador de la sede de la AEAT, de modo que la postura de seguridad no pueda erosionarse por accidente.
Cómo se mantiene la documentación fiel al código¶
La disciplina que gobierna las capas gobierna también la documentación. Cadrumo mantiene tres superficies de documentación en inglés cerca del código que aporta sus datos.
Los autores de documentación escriben el markdown del repositorio (esta guía y las demás bajo docs/) para las personas que se están orientando en el proyecto. Los revisores técnicos comprueban cada afirmación contra el código antes de que se incorpore. Los docstrings dentro del código aportan la referencia de la API, de modo que los autores nunca copian una firma en el texto. Los generadores levantan las referencias del código fuente y de la línea de comandos a partir del propio código.
Como los generadores derivan las referencias de la API y de la línea de comandos a partir del código fuente, esas superficies no pueden desviarse en silencio. Una construcción estricta trata las advertencias como errores y falla ante una referencia cruzada rota, un stub que falta o una referencia de comando que ya no coincide con los comandos.
Entrar en el código¶
Usa esta visión general para orientarte y luego baja al detalle:
El árbol de capas: recorra
src/cadrumo/core,src/cadrumo/domain,src/cadrumo/application,src/cadrumo/adaptersysrc/cadrumo/entrypointspara ver los subpaquetes que nombra cada grupo.La referencia de la línea de comandos: Referencia de la CLI para los verbos, las opciones y la salida exactos.
La referencia de la API: /api/cadrumo para las firmas de los módulos y la lista completa de subpaquetes.
La guía de redacción del registro: Redactar y revisar la documentación para saber cómo redactar el TOML que compila el flujo.
La explicación del flujo: Cómo Cadrumo convierte tus registros en un fichero de declaración para el mismo recorrido contado para el contribuyente.
El glosario: Glosario para cualquier término (casilla, modelo, fichero-BOE o justificante) que quieras ver definido.
Ponerlo en marcha: Guía rápida: produzca un fichero de modelo para instalar y preparar una primera presentación.
Contribuir: el código fuente del proyecto y el gestor de incidencias están en GitHub.