Visió general de l’arquitectura

Cadrumo és una aplicació local-first que prepara presentacions d’impostos espanyols mitjançant la interfície de línia d’ordres aeat. Modela el registre normatiu de l’autoritat fiscal, ingereix i classifica els teus registres financers i calcula les caselles numerades de cada formulari fiscal. Després comprova l’esborrany i exporta un fitxer que presentes tu mateix. L’AEAT és l’Agencia Estatal de Administración Tributaria, l’agència tributària d’Espanya.

Utilitza aquesta pàgina com a punt d’entrada quan llegeixis el codi base per primera vegada. És un mapa, no un directori: anomena les capes, els conceptes fonamentals i els tipus canònics als quals navegaràs, i s’atura aquí. Per a signatures i ordres exactes, segueix els enllaços de Entrar al codi.

Per què el sistema té aquesta forma

Preparar una declaració d’impostos espanyola a mà és propens a errors i difícil d’auditar. Una xifra d’un formulari ha de poder-se rastrejar fins a un moviment bancari, una norma i una regla publicada. Un formulari anual posterior ha de mantenir-se coherent amb els trimestrals que l’han alimentat. Tres decisions de disseny responen a aquest problema i donen forma a tota la resta:

  • Local-first. Els teus registres financers no surten mai de la teva màquina. L’eina funciona fora de línia i desa càrregues útils xifrades en una base de dades local al disc.

  • El registre és l’autoritat. Les regles fiscals (tipus, trams, terminis i la base legal de cada casella) no estan codificades de manera fixa. S’escriuen com a dades, es compilen, es validen i es llegeixen a través d’una única autoritat. Una xifra sempre es pot rastrejar fins a la regla que la va produir.

  • Qui presenta és la persona, no l’eina. El pipeline acaba en un fitxer d’exportació. Enviar-lo a l’agència és un pas humà deliberat. L’eina no té cap via de presentació, de manera que la presentació en directe és absent en comptes d’estar protegida.

L’estructura que segueix és hexagonal: les regles de negoci se situen al centre, i els analitzadors, els navegadors i l’emmagatzematge que toquen el món exterior se situen a la vora. La majoria de les dependències apunten cap endins. La composició de l’aplicació té una excepció explícita i controlada que arriba als adaptadors. Això manté la lògica fiscal comprovable per si mateixa i permet canviar un adaptador sense tocar cap regla.

Com llegir aquesta pàgina

Quatre vistes cobreixen el sistema al nivell més alt. Cadascuna combina un diagrama amb una explicació breu:

Les capes

Cadrumo separa les responsabilitats en cinc capes dins de src/cadrumo/. Normalment, les capes només depenen de les capes que tenen a dins; la composició de l’aplicació té l’excepció d’adaptador documentada.

        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. Els entrypoints criden les capes application i core. L’application crida les capes domain, adapters i core; els adapters criden domain i core; i domain crida core. El domain arriba als adapters només a través de la costura anotada del repositori segur.

  • core` és la capa més interna. Posseeix les primitives transversals que comparteixen totes les altres capes. Els seus enums tipats defineixen conjunts tancats de valors com ara els codis de període, els dominis fiscals, els identificadors de modelo i els tipus de font de binding. core també posseeix el contracte d’embolcall JSON, la configuració, els tipus de diners i de temps, i la taxonomia d’errors.

  • **domain** conté regles i registres fiscals purs: el registre de modelos, les definicions de casella (una casella és un requadre numerat d'un formulari), les observacions de presentació i els càlculs que s'hi fan. Només depèn de core`.

  • application` orquestra els casos d’ús. Uneix les regles de domain amb els adapters per construir esborranys de presentació, agregar el ledger, executar diagnòstics i projectar l’estat. No fa cap entrada ni sortida per si mateixa.

  • **adapters** connecta l'aplicació amb el món exterior en tres parts. inboundanalitza els extractes entrants en Portable Document Format (PDF), valors separats per comes (CSV), Open Financial Exchange (OFX) i Microsoft Excel Open XML (XLSX).outboundarriba a serveis externs (sessions de navegador de l'AEAT, oracles de càlcul i proveïdors d'autenticació).persistence` desa càrregues útils xifrades a la base de dades local.

  • **entrypoints** exposa l'aplicació als operadors. La línia d'ordres és aquí, i la seva superfície arrel es limita a dues famílies d'ordres, configiapp`.

Dues regles de límit mantenen honestes les capes. Les dades de límit creuen com a models pydantic v2 validats, mai com a diccionaris solts. Els conjunts tancats de valors es declaren com a enums tipats a core i circulen com a membres d’enum, de manera que un valor no vàlid es rebutja al límit.

Les fletxes de dependència apunten cap endins amb una excepció anotada. Tres repositoris de domain (filing, justificante i submission) se situen directament sobre la classe base d’emmagatzematge xifrat a adapters.persistence. El diagrama dibuixa aquesta costura com una aresta discontínua en lloc d’amagar-la.

Dins de domain i application, els subpaquets s’agrupen en cinc grups conceptuals. Alguns subpaquets s’ometen per claredat; la referència de l’API els enumera tots.

Grup

Subpaquets de domain

Subpaquets de application

Registre i càlculs

calculations, modelos, normatives, manuals

calculations, modelo, registry, aggregation, verification

Ledger i transaccions

transactions, invoices, categories, iva, currency, attachments

ledger, transactions, invoices, evidence, inventory

Presentació i exportació

filing, justificante, submission

filing, export, review

Live, portals i auth

portals, auth, fincas

live, portals, auth, workflow

Perfil i emmagatzematge

buckets, user_profile, contribuyente, deadlines

profile, storage, bucket_maintenance, setup, wizard

El pipeline de l’autoritat del registre

Les definicions autoritzades dels models fiscals no estan codificades de manera fixa. Es mouen a través d’un pipeline determinista, des del codi font escrit a mà fins als snapshots immutables que llegeix el codi en temps d’execució. TOML és Tom’s Obvious Minimal Language, un format pensat per 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. Els fragments d’autoria flueixen a través d’una càrrega, una compilació i una validació d’esquema deterministes abans que l’autoritat del registre validada creï snapshots congelats per a les projeccions en temps d’execució. Una empremta de tot l’arbre invalida tant la memòria cau del compilador com la de l’autoritat cada cop que canvien els fitxers del registre.

Cada modelo i revisió s’escriu com a fragments de TOML dins de src/cadrumo/_data/registry/. El carregador i compilador (_loader.py) fusiona aquests fragments en un ordre determinista, rebutja els conflictes ambigus i els compila en objectes ModeloDefinition i ModeloRevision estrictes i congelats. El carregador indexa la seva memòria cau amb una empremta de tot l’arbre: el camí, la mida i l’hora de modificació de cada fitxer. Després d’un curt temps de vida de la memòria cau de l’empremta, una edició fa que el carregador revalidi l’empremta abans de servir el resultat següent.

ValidatedRegistryAuthority és el límit de producció. Valida un modelo una vegada, desa el resultat a la memòria cau i construeix un RegistrySnapshot per a un modelo, any i període determinats. El snapshot és congelat i lligat al context: les seves referències legals i de font estan indexades, i la seva integritat referencial es comprova en temps de construcció. Els consumidors en temps d’execució (els proveïdors d’esquema de presentació, l’execució de fórmules, l’anàlisi d’exportació i la verificació) llegeixen dels snapshots, mai del carregador en brut.

La instantània també és l’autoritat del model de lectura de la presentació. Les seves definicions de caselles indiquen quins camps numerats existeixen, si cada camp és manual, vinculat o calculat, quines fórmules i contractes de vinculació s’apliquen i quins camps repetits i posicions d’exportació pertanyen a aquesta revisió. Descriu què requereix una presentació; no és propietària de les transaccions, factures, actius estrangers ni moviments d’existències d’un contribuent.

La separació importa. Les etapes d’autoria i compilació anteriors a l’autoritat són detalls d’implementació. El codi de producció sol·licita modelos validats, finestres de terminis i snapshots a través de l’autoritat, de manera que el carregador es queda darrere d’aquesta línia.

El cicle de vida del modelo

Un modelo és un formulari fiscal numerat de l’AEAT. Les seves dades flueixen en un sol sentit, dels teus registres a un fitxer que puges tu mateix.

        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. Els registres financers s’ingereixen i es classifiquen dins del ledger xifrat. Un operador crea la feina, la calcula, la verifica, la marca com a presentada localment i exporta un fitxer. Després, una persona puja aquest fitxer a l’AEAT fora de Cadrumo.

L’eina ingereix i classifica els registres financers (extractes bancaris, factures i les seves proves) dins del ledger xifrat. A partir d’aquí, el trajecte segueix una seqüència fixa de verbs de línia d’ordres:

  • aeat app modelo work create fixa una presentació a un modelo, any i període.

  • calculate resol cada casella i desa una CalculationRevision.

  • verify executa una comprovació d’integritat i coherència sobre l’esborrany.

  • file marca una revisió verificada com a presentada internament.

  • aeat app modelo export escriu el fitxer de pujada oficial (un artefacte fichero-BOE de format fix) al disc.

El límit és estructural. file és un marcador local, no una presentació, i export escriu un fitxer al teu disc. Cap ordre no transmet res a l’agència: la pujada és una acció humana al portal de l’AEAT, modelada com un pas més enllà del límit de l’eina. Les lectures en directe de l’operador requereixen la seva autenticació habitual i les proteccions de perfil actiu; continuen sent només de lectura. L’opció separada CADRUMO_LIVE_TESTS_ENABLED controla les proves de lectura en directe dels desenvolupadors.

Com es fonamenta una xifra

El valor d’una casella no és un simple nombre. Porta la seva procedència (les referències legals, les referències de font i l’identificador de fórmula que el van produir) des de la definició del registre fins a la sortida orientada a l’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ó de casella del registre i les entrades resoltes de ledger, factura, presentació anterior i relació alimenten el motor de fórmules. Aquest emet una observació que porta la procedència i que es converteix en una revisió de càlcul, una càrrega útil de la CLI i una sortida orientada a l’operador.

Cada binding d’una casella declara una font tipada. Una malla de resolutors (agregació del ledger, catàleg de factures, arrossegament de la presentació anterior i prefill de relació entre modelos) converteix aquestes fonts en valors de binding, fusionats mitjançant merge_source_resolutions. El motor de fórmules del registre avalua la fórmula de la casella sobre les entrades resoltes i segella el resultat com una CasillaObservation que porta els seus legal_refs, source_refs i formula_id. Aquesta observació viatja dins de la CalculationRevision persistida, després cap a les càrregues útils de la CLI i després cap a l’operador. La procedència no es perd mai en el camí de sortida.

Una vinculació és un contracte de projecció, no un espai per adjuntar dades. Identifica la família d’origen propietària, valida el selector i l’agregació i indica com aquest origen es converteix en una entrada de la presentació. Un contracte escalar produeix un valor. Les famílies de registres repetits declaren una vinculació tipada per camp de fila. Uneixen els valors mitjançant agrupació i un índex de fila que comença en u. La revisió de càlcul congela les vinculacions de fila resoltes. Les rutes que produeixen files de detall validades les emmagatzemen per separat. Mai no adjunta un bloc opac de registres a una casella.

El Model 720 demostra l’estructura repetida: el seu resolvedor d’actius estrangers està inscrit a la malla d’orígens i projecta camps d’actius tipats en vinculacions de files del registre. L’inventari d’existències és diferent. InventoryLedger és un agregat empresarial implementat i xifrat. La taxonomia d’orígens de vinculacions actualment no té cap tipus d’inventari ni cap resolvedor d’inventari inscrit. Per tant, roman fora de les revisions de càlcul dels models fins que s’afegeixi una projecció amb fonament legal.

La persistència i el límit de seguretat

Totes les dades financeres sensibles resideixen en un sol lloc: un magatzem xifrat per perfil. És l’estructura, no una convenció, la que les hi manté.

        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 actiu. La sessió de bucket resultant obre les fàbriques d’execució i un repositori segur, que xifra les càrregues útils a la base de dades local i les proves adjuntes. Les comprovacions de l’AEAT de l’operador continuen sent només de lectura; només l’acció humana de presentació pot enviar res a l’AEAT.

Les claus de xifratge de claus (KEK) protegeixen les claus de xifratge de dades (DEK). El repositori utilitza càrregues útils de xifratge autenticat amb dades associades (AEAD) i resums de cerca HMAC-SHA256 derivats de material mestre. HMAC vol dir codi d’autenticació de missatges basat en resum (hash-based message authentication code). El xifratge és l’Advanced Encryption Standard amb claus de 256 bits (AES-256).

Treballes amb un contribuent alhora. En seleccionar un perfil s’obre una sessió de bucket, que limita un SecureObjectRepository al magatzem xifrat d’aquell contribuent. El repositori xifra cada càrrega útil al límit de la columna amb una clau del MasterKeyProvider: el clauer del sistema operatiu, o una alternativa derivada d’una contrasenya. Persisteix el text xifrat en una base de dades SQLite local amb columnes de càrrega útil xifrades. Els bytes de prova (factures, extractes i documents desxifrats) passen pel AttachmentStore adreçat per contingut, que desa els bytes mateixos, mai un enllaç.

Els bytes d’evidència (factures, extractes i documents desxifrats) passen per l”AttachmentStore adreçat per contingut, que emmagatzema els mateixos bytes, mai un enllaç.

Els registres empresarials analitzats no es converteixen en adjunts només perquè més endavant puguin donar suport a una presentació. Les transaccions, els catàlegs de factures i l’inventari d’existències continuen sent agregats tipats separats a l’emmagatzematge xifrat. El Model 720 rep, en canvi, observacions tipades d’actius estrangers del cridant. Un resolvedor d’orígens inscrit projecta en una revisió de càlcul només els camps sol·licitats pel registre. Al registre actual del Model 100, les caselles d’existències 0177, 0181 i 0182 continuen sent entrades manuals. El domini d’inventari ara divideix la variació d’obertura a tancament de 2025 entre 0177 i 0182, però aquesta projecció de domini no demostra que hi hagi una ruta activa d’inventari a Renda ni afirma encara que 0181 inclogui totes les compres.

Res d’aquest magatzem no creua cap enfora cap a l’agència com a escriptura. Les comprovacions en directe només de lectura arriben a l’agència únicament per llegir. L’única via que arriba a l’agència com a escriptura és la pujada humana, que se situa més enllà del límit de l’eina. Una prova estructural imposa l’absència de qualsevol verb d’escriptura a l’adaptador de la sede de l’AEAT, de manera que l’estat de seguretat no es pot erosionar per accident.

Com la documentació es manté fidel al codi

La disciplina que regeix les capes també regeix la documentació. Cadrumo manté tres superfícies de documentació en anglès a prop del codi que en proporciona els fets.

Els autors de documentació escriuen el markdown del repositori (aquesta guia i les altres dins de docs/) per a les persones que s’orienten al projecte. Els revisors tècnics comproven cada afirmació contra el codi abans que s’incorpori. Les docstrings dins del codi font proporcionen la referència de l’API, de manera que els autors no copien mai una signatura a la prosa. Els generadors basteixen les referències del codi font i de la línia d’ordres a partir del codi mateix.

Com que els generadors deriven les referències de l’API i de la línia d’ordres a partir del codi font, aquestes superfícies no poden desviar-se de manera silenciosa. Una construcció estricta tracta els avisos com a errors i falla si hi ha una referència creuada trencada, un stub que falta o una referència d’ordre que ja no coincideix amb les ordres.

Entrar al codi

Utilitza aquesta visió general per orientar-te, i després endinsa’t en el detall:

  • L’arbre de capes - navega per src/cadrumo/core, src/cadrumo/domain, src/cadrumo/application, src/cadrumo/adapters i src/cadrumo/entrypoints per veure els subpaquets que anomena cada grup.

  • La referència de la línia d’ordres - Referència de la CLI per als verbs, opcions i sortida exactes.

  • La referència de l’API - /api/cadrumo per a les signatures de mòdul i la llista completa de subpaquets.

  • La guia d’autoria del registre - Redacció i revisió de la documentació per saber com escriure el TOML que compila el pipeline.

  • L’explicació del pipeline - Com Cadrumo converteix els teus registres en un fitxer tributari per al mateix trajecte explicat per al contribuent.

  • El glossari - Glossari per a qualsevol terme (casella, modelo, fichero-BOE o justificante) que vulguis definir.

  • Posar-ho en marxa - Guia ràpida: produeix un fitxer de modelo per instal·lar i preparar una primera presentació.

  • Contribuir - el codi font del projecte i el seu gestor d’incidències són a GitHub.