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

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 de core`.

  • 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 domain

Subpaquetes de application

Registro y cálculos

calculations, modelos, normatives, manuals

calculations, modelo, registry, aggregation, verification

Ledger y transacciones

transactions, invoices, categories, iva, currency, attachments

ledger, transactions, invoices, evidence, inventory

Presentación y exportación

filing, justificante, submission

filing, export, review

Live, portales y autenticación

portals, auth, fincas

live, portals, auth, workflow

Perfil y almacenamiento

buckets, user_profile, contribuyente, deadlines

profile, storage, bucket_maintenance, setup, wizard

El flujo de la autoridad del registro

Las definiciones autorizadas de los modelos tributarios no están escritas en el código. Recorren un flujo determinista, desde la fuente redactada a mano hasta los snapshots inmutables que lee el código en tiempo de ejecución. TOML es Tom’s Obvious Minimal Language, un formato pensado para editar.

        flowchart LR
    subgraph impl["implementation detail — runtime never calls this"]
        A["Authoring tree — TOML fragments"] --> B["Loader / compiler — deterministic merge, conflict refusal"] --> C["Strict schema — ModeloDefinition · ModeloRevision (frozen)"]
    end
    subgraph prod["production boundary — all registry access"]
        D["ValidatedRegistryAuthority — validate-once · cache"] --> E["RegistrySnapshot — frozen (modelo, year, period), integrity-checked"] --> F["Runtime projections — filing · formula · export · verification"]
    end
    C --> D
    FP["whole-tree fingerprint (path, size, mtime)"]
    FP -.->|"invalidates"| B
    FP -.->|"invalidates"| D
    

Alternativa textual. Los fragmentos de autoría recorren una carga, una compilación y una validación de esquema deterministas antes de que la autoridad de registro validada cree snapshots congelados para las proyecciones en tiempo de ejecución. Una huella de todo el árbol invalida tanto la caché del compilador como la de la autoridad cada vez que cambian los ficheros del registro.

Cada modelo y cada revisión se redacta como fragmentos de TOML bajo src/cadrumo/_data/registry/. El cargador y compilador (_loader.py) fusionan esos fragmentos en un orden determinista, rechazan los conflictos ambiguos y los compilan en objetos ModeloDefinition y ModeloRevision estrictos y congelados. El cargador indexa su caché con una huella de todo el árbol: la ruta, el tamaño y la hora de modificación de cada fichero. Tras un breve tiempo de vida de la caché de la huella, una edición hace que el cargador revalide la huella antes de servir el siguiente resultado.

ValidatedRegistryAuthority es la frontera de producción. Valida un modelo una vez, almacena en caché el resultado y construye un RegistrySnapshot para un modelo, un año y un periodo dados. El snapshot es congelado y está ligado a su contexto: sus referencias legales y de origen están indexadas, y su integridad referencial se comprueba en el momento de la construcción. Los consumidores en tiempo de ejecución (proveedores de esquema de presentación, ejecución de fórmulas, análisis de exportaciones y verificación) leen de los snapshots, nunca del cargador en crudo.

La instantánea también es la autoridad del modelo de lectura de la presentación. Sus definiciones de casillas indican qué campos numerados existen, si cada campo es manual, vinculado o calculado, qué fórmulas y contratos de vinculación se aplican y qué campos repetidos y posiciones de exportación pertenecen a esa revisión. Describe lo que requiere una presentación; no es propietaria de las transacciones, facturas, activos extranjeros ni movimientos de existencias de un contribuyente.

La separación importa. Las etapas de autoría y compilación previas a la autoridad son detalles de implementación. El código de producción solicita modelos validados, ventanas de plazo y snapshots a través de la autoridad, de modo que el cargador se mantiene detrás de esa línea.

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 create fija una presentación a un modelo, un año y un periodo.

  • calculate resuelve cada casilla y guarda una CalculationRevision.

  • verify ejecuta una comprobación de integridad y coherencia sobre el borrador.

  • file marca una revisión verificada como presentada internamente.

  • aeat app modelo export escribe 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/adapters y src/cadrumo/entrypoints para 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.