Skip to content

Commit 4ebcd55

Browse files
committed
Versión 0.2.0: identificadores estables (schema 1.1)
- statement_id, transactions[].id e installment_plans[].id: HMAC-SHA256 con la llave de seudónimos sobre datos crudos estables; el número de cuenta solo entra al HMAC - Ordinal de ocurrencia para movimientos idénticos el mismo día - Escáner residual: exención exacta por campo y formato de los ids - Aviso en stderr al procesar varios archivos o NDJSON sin llave - Documentación, skill y guía para agentes explican cómo reimportar sin duplicar - Pruebas: estabilidad, privacidad, duplicados, planes entre meses y colisiones
1 parent d58bc5b commit 4ebcd55

20 files changed

Lines changed: 456 additions & 47 deletions

‎.claude/skills/mxbank/SKILL.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ mxbank schema # JSON Schema completo
2828
Si `mxbank` no está en el PATH: `uv tool install git+https://github.com/proteo5/mx-bank-statement-reader` (pide permiso antes de instalar), o desde un clon del repo: `uv run --project <ruta-del-repo> mxbank …`.
2929

3030
- **PDF con contraseña** (código 11): pide al usuario que la ponga en una variable de entorno y usa `--password-env NOMBRE`. Nunca la pases como argumento ni la escribas en la conversación.
31-
- **Varios meses**: usa la misma llave de seudónimos en todas las ejecuciones (`--pseudonym-key-env NOMBRE`), para que la misma cuenta o contraparte tenga el mismo alias.
31+
- **Varios meses o importación**: usa **siempre la misma llave** (`--pseudonym-key-env NOMBRE`) para que la misma cuenta, contraparte, movimiento y plan a meses tengan el mismo alias e id en todas las ejecuciones. Sin llave, la CLI avisa en stderr y los ids no sirven fuera de esa ejecución.
3232
- `--include-concepts` agrega los conceptos de transferencias (redactados); úsalo solo si el usuario lo pide.
3333

3434
| Código | Significado / qué hacer |
@@ -48,6 +48,11 @@ Si `mxbank` no está en el PATH: `uv tool install git+https://github.com/proteo5
4848
- `installment_plans[]`: compras a meses con `pending_balance` y `installment_number`/`installments_total`.
4949
- `original_currency` / `original_amount` / `exchange_rate`: compras en otra moneda.
5050
- `merchant`: nombre canónico cuando el comercio es conocido; si no, usa `description`.
51+
- `statement_id`, `transactions[].id`, `installment_plans[].id` (schema 1.1): identificadores HMAC estables.
52+
- **Para no duplicar** al combinar estados que se enciman o reimportados, agrupa o deduplica por `transactions[].id`, nunca por fecha + monto: dos compras idénticas el mismo día tienen ids distintos y ambas son reales.
53+
- Sigue una compra a meses entre estados con `installment_plans[].id`.
54+
- Los ids solo son comparables si `privacy.pseudonym_scope == "key"` y se usó la misma llave; con `"run"` no los guardes ni los compares entre ejecuciones.
55+
- No intentes deducir nada de un id: no contiene datos.
5156

5257
## Cómo calcular bien (lecciones de estados reales)
5358

‎CHANGELOG.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
Formato basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/); versionado [SemVer](https://semver.org/lang/es/).
44
El esquema JSON de salida tiene su propia versión (`schema_version`).
55

6+
## [0.2.0] - 2026-10-10
7+
8+
Esquema de salida **1.1** (cambio aditivo; los consumidores de 1.0 siguen funcionando).
9+
10+
### Añadido
11+
12+
- `statement_id` (raíz), `transactions[].id` e `installment_plans[].id`: identificadores estables calculados con HMAC-SHA256 y la llave de seudónimos, para importar o reimportar estados (incluso estados que se enciman) sin duplicar movimientos ni planes a meses.
13+
- Con `--pseudonym-key-env` son iguales entre ejecuciones, PDFs y perfiles de privacidad; sin llave solo valen dentro de la ejecución, y la CLI lo advierte en stderr al procesar varios archivos o con `--ndjson`.
14+
- Dos movimientos idénticos el mismo día se distinguen con un ordinal de ocurrencia dentro del estado.
15+
- El escáner residual exime solo esos campos y solo con su formato exacto.
16+
- Documentación de cómo se calculan y cuándo son estables (docs/03 §7, docs/04), y cómo usarlos para no duplicar (docs/AGENT.md, skill `mxbank`).
17+
618
## [0.1.0] - 2026-10-10
719

820
Primera versión pública. Esquema de salida **1.0**.
@@ -25,4 +37,5 @@ Primera versión pública. Esquema de salida **1.0**.
2537
- Herramientas de desarrollo: volcado enmascarado de layouts (`tools/layout_dump.py`), métricas sobre PDFs reales sin exponer contenido (`tools/corpus_check.py`) y guardia de git contra PDFs y datos personales (`tools/check_no_pii.py`).
2638
- PDFs sintéticos con datos ficticios para las pruebas.
2739

40+
[0.2.0]: https://github.com/proteo5/mx-bank-statement-reader/releases/tag/v0.2.0
2841
[0.1.0]: https://github.com/proteo5/mx-bank-statement-reader/releases/tag/v0.1.0

‎README.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<p align="center">
99
<a href="https://github.com/proteo5/mx-bank-statement-reader/actions/workflows/ci.yml"><img src="https://github.com/proteo5/mx-bank-statement-reader/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
10-
<img src="https://img.shields.io/badge/versión-0.1.0-blue" alt="Versión 0.1.0">
10+
<img src="https://img.shields.io/badge/versión-0.2.0-blue" alt="Versión 0.2.0">
1111
<img src="https://img.shields.io/badge/python-3.12%2B-blue" alt="Python 3.12+">
1212
<img src="https://img.shields.io/badge/licencia-Apache%202.0-green" alt="Licencia Apache 2.0">
1313
</p>
@@ -121,8 +121,9 @@ Extracto real generado a partir del PDF **sintético** de las pruebas, con datos
121121

122122
```json
123123
{
124-
"schema_version": "1.0",
124+
"schema_version": "1.1",
125125
"ok": true,
126+
"statement_id": "ST_5c0e9a41d27b8f36",
126127
"document": { "bank": "bbva", "product": "credit", "product_name": "TARJETA AZUL BBVA (CREDITO)", "currency": "MXN" },
127128
"account": { "account_ref": "ACC_92d5", "account_last4": "3210", "holder_type": "individual" },
128129
"period": { "start": "2026-08-15", "end": "2026-09-14", "days": 31 },
@@ -132,7 +133,7 @@ Extracto real generado a partir del PDF **sintético** de las pruebas, con datos
132133
"minimum_payment": "900.00", "no_interest_payment": "1965.00", "cat_percent": "75.3"
133134
},
134135
"transactions": [
135-
{ "date": "2026-08-18", "description": "AMAZON", "type": "card_purchase", "amount": "-1200.00",
136+
{ "id": "TX_8d41f0b27c9e3a65", "date": "2026-08-18", "description": "AMAZON", "type": "card_purchase", "amount": "-1200.00",
136137
"merchant": "AMAZON", "card_type": "digital" },
137138
{ "date": "2026-08-20", "description": "NETFLIX", "type": "card_purchase", "amount": "-299.00",
138139
"original_currency": "USD", "original_amount": "15.00", "exchange_rate": "19.93" },
@@ -148,6 +149,7 @@ Extracto real generado a partir del PDF **sintético** de las pruebas, con datos
148149

149150
- Montos como **texto decimal** (`"1200.00"`), para no perder precisión. `amount` negativo = salida de dinero.
150151
- Fechas ISO-8601. `null` significa que el dato no viene en el documento; nunca se inventa.
152+
- `statement_id`, `transactions[].id` e `installment_plans[].id` son **identificadores estables** (HMAC con tu llave): con la misma `--pseudonym-key-env` puedes reimportar estados, incluso los que se enciman, sin duplicar movimientos. Ver [docs/03 §7](docs/03-esquema-salida.md#7-identificadores-estables-schema-11).
151153
- El esquema completo se obtiene con `mxbank schema`; la semántica de cada campo está en [docs/03-esquema-salida.md](docs/03-esquema-salida.md).
152154

153155
## Privacidad

‎docs/01-analisis.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,4 @@ Conclusión de diseño: **procesamiento 100 % local, sin telemetría, sin red**,
159159
| **F2** | Tarjetas de crédito de los mismos bancos, lote/directorio, consolidado multi‑mes, categorización básica de movimientos. |
160160
| **F3** | Más bancos, OCR, servidor MCP que expone las mismas herramientas, empaquetado como binario único. |
161161

162-
> **Estado en v0.1.0**: F0 y F1 completas salvo el modo `generic` (un banco no soportado se rechaza con código 20/21 en lugar de extraerse en modo de respaldo). De F2 ya están las tarjetas de crédito BBVA; consolidado y categorización siguen pendientes. Ver [02 §11](02-especificacion-tecnica.md#11-estado-de-implementación-v010).
162+
> **Estado en v0.1.0**: F0 y F1 completas salvo el modo `generic` (un banco no soportado se rechaza con código 20/21 en lugar de extraerse en modo de respaldo). De F2 ya están las tarjetas de crédito BBVA; consolidado y categorización siguen pendientes. Ver [02 §11](02-especificacion-tecnica.md#11-estado-de-implementación-v020).

‎docs/02-especificacion-tecnica.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ Detalle completo en [04-privacidad-y-redaccion.md](04-privacidad-y-redaccion.md)
238238
- **Servidor MCP (F3)**: `mxbank mcp` expone `extract_statement`, `detect_statement`, `audit_text`, `get_schema` reutilizando el mismo núcleo. El servidor recibe rutas locales, no contenido.
239239
- La salida incluye `schema_version` y un bloque `privacy` para que la IA sepa qué fue omitido y no "invente" datos.
240240

241-
## 11. Estado de implementación (v0.1.0)
241+
## 11. Estado de implementación (v0.2.0)
242242

243243
| Componente | Estado |
244244
|---|---|
@@ -247,6 +247,7 @@ Detalle completo en [04-privacidad-y-redaccion.md](04-privacidad-y-redaccion.md)
247247
| BanCoppel cuenta de débito (layout 2025.1) | ✅ 1/1 estado real conciliado, totales y saldos por renglón correctos |
248248
| BBVA tarjeta de crédito (layout 2025.1) | ✅ 4/4 estados reales conciliados con el saldo de cargos regulares; bloque `credit`, planes a meses, tipo de cambio, parcialidades |
249249
| Redacción (estructural, diccionario, patrones), seudónimos HMAC, escáner residual fail‑closed | ✅ |
250+
| Identificadores estables `statement_id`, `transactions[].id`, `installment_plans[].id` (schema 1.1) | ✅ docs/03 §7 |
250251
| Perfiles `minimal` / `standard` / `strict` | ✅ (diferencias en §6 de docs/04) |
251252
| `--consolidate`, parser genérico (`generic`), generalización de fechas/montos, OCR, NER, servidor MCP, tarjetas de otros bancos | ⏳ |
252253

‎docs/03-esquema-salida.md‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
# 03 · Esquema de salida JSON (v1, borrador)
1+
# 03 · Esquema de salida JSON (schema 1.1)
22

33
El JSON Schema formal se genera desde los modelos Pydantic (`src/mxbank/models/statement.py`) y se obtiene con `mxbank schema`; **ese es la referencia autoritativa**. Este documento describe la forma y la semántica.
44

55
> Schema 1.0 (v0.1.0), además de lo que muestra el ejemplo: `transactions[].balance_operation`, `value_date`, `counterparty_account_ref`, `concept` (solo con `--include-concepts`); `summary.minimum_average_balance`, `fees_vat`; bloque `rates`; `validation.running_balance_checked`, `bank_totals_match`; `privacy.include_concepts`. También `credit` e `installment_plans` (con `start_date` en lugar de `date`) y en movimientos `original_currency`, `original_amount`, `exchange_rate`, `installment_number`, `installments_total`, `card_type`. Pendiente: `fees[]`.
66
7+
> **Schema 1.1** (cambio aditivo): `statement_id` en la raíz, `transactions[].id` e `installment_plans[].id`. Ver §7.
8+
79
## 1. Principios
810

911
- **Allowlist**: solo existen en la salida los campos definidos aquí. Cualquier campo nuevo requiere revisión de privacidad.
@@ -16,8 +18,9 @@ El JSON Schema formal se genera desde los modelos Pydantic (`src/mxbank/models/s
1618

1719
```json
1820
{
19-
"schema_version": "1.0",
21+
"schema_version": "1.1",
2022
"ok": true,
23+
"statement_id": "ST_5c0e9a41d27b8f36",
2124
"source": {
2225
"file_name": "estado_2026_09.pdf",
2326
"file_sha256": "9f2c…",
@@ -58,6 +61,7 @@ El JSON Schema formal se genera desde los modelos Pydantic (`src/mxbank/models/s
5861
},
5962
"transactions": [
6063
{
64+
"id": "TX_8d41f0b27c9e3a65",
6165
"index": 0,
6266
"date": "2026-08-15",
6367
"posting_date": "2026-08-15",
@@ -73,6 +77,7 @@ El JSON Schema formal se genera desde los modelos Pydantic (`src/mxbank/models/s
7377
"page": 2
7478
},
7579
{
80+
"id": "TX_f20c6b9e41a7d385",
7681
"index": 1,
7782
"date": "2026-08-16",
7883
"posting_date": "2026-08-16",
@@ -136,6 +141,7 @@ El JSON Schema formal se genera desde los modelos Pydantic (`src/mxbank/models/s
136141
},
137142
"installment_plans": [
138143
{
144+
"id": "PLAN_3e7a90c5b1f4d268",
139145
"description": "LIVERPOOL MSI",
140146
"original_amount": "12000.00",
141147
"monthly_amount": "1000.00",
@@ -152,6 +158,7 @@ El JSON Schema formal se genera desde los modelos Pydantic (`src/mxbank/models/s
152158
| Campo | Significado | Nota de privacidad |
153159
|---|---|---|
154160
| `account_ref` | Token estable del número de cuenta (HMAC) | Permite a la IA agrupar meses de la misma cuenta sin conocer el número. |
161+
| `statement_id`, `transactions[].id`, `installment_plans[].id` | Identificadores estables (HMAC), §7 | No contienen ni dejan derivar la cuenta, montos ni descripciones sin la llave. |
155162
| `account_last4` | Últimos 4 dígitos | Omitido en perfil `strict`. |
156163
| `counterparty_ref` | Token de la contraparte (`PERSONA_xxxx` / `EMPRESA_xxxx`) | Misma contraparte ⇒ mismo token dentro del ámbito (`run` o `key`). |
157164
| `merchant` | Comercio normalizado | Se conserva: es útil y no identifica al titular. |
@@ -167,3 +174,23 @@ El JSON Schema formal se genera desde los modelos Pydantic (`src/mxbank/models/s
167174

168175
- `schema_version` sigue SemVer: cambios aditivos ⇒ menor; cambios que rompen ⇒ mayor.
169176
- `mxbank schema --version N` imprime cualquier versión soportada.
177+
178+
## 7. Identificadores estables (schema 1.1)
179+
180+
Sirven para que un importador (p. ej. una base de datos personal de solo agregado) pueda **reimportar** un estado o importar **estados que se enciman** sin duplicar nada: basta con insertar solo los `id` que aún no tiene.
181+
182+
| Campo | Formato | Entradas del HMAC | Igual cuando… |
183+
|---|---|---|---|
184+
| `statement_id` (raíz) | `ST_<16 hex>` | banco, producto, número de cuenta, inicio y fin del periodo | es el mismo estado de cuenta (mismo periodo de la misma cuenta), aunque sea otro PDF o se descargue de nuevo |
185+
| `transactions[].id` | `TX_<16 hex>` | banco, producto, número de cuenta, `date`, `amount`, descripción **cruda** del banco normalizada (`text.norm`), ordinal de ocurrencia | es el mismo movimiento, aunque aparezca en dos estados que se enciman o en otra posición/página |
186+
| `installment_plans[].id` | `PLAN_<16 hex>` | banco, producto, número de cuenta, descripción cruda normalizada, `start_date`, `original_amount`, ordinal de ocurrencia | es la misma compra a meses, en todos los estados donde aparezca (cambian la parcialidad y el saldo, el id no) |
187+
188+
Reglas:
189+
190+
- **Estables solo con llave**: con `--pseudonym-key-env` (`privacy.pseudonym_scope: "key"`) y siempre la misma llave, los ids son iguales entre ejecuciones, PDFs y perfiles. **Sin llave** (`"run"`) la llave es aleatoria y los ids solo valen dentro de esa ejecución; la CLI lo advierte en stderr al procesar varios archivos o usar `--ndjson`.
191+
- **No dependen** de `--privacy`, `--include-concepts`, `index`, `page`, el orden de extracción, `type`, `merchant` ni del texto redactado: se calculan con los datos crudos antes de redactar.
192+
- **Duplicados legítimos** (dos compras idénticas el mismo día): el ordinal de ocurrencia (0, 1, …) entre movimientos con la misma fecha, monto y descripción **dentro del mismo estado** los distingue. Si un estado que se encima trae solo una de dos compras idénticas, se le asigna el ordinal 0.
193+
- **Sin periodo** (el parser no lo encontró), `statement_id` se calcula con el hash del PDF: solo es estable para ese archivo exacto.
194+
- **Sin número de cuenta**, los ids no distinguen entre cuentas distintas; la salida lo avisa en `validation.warnings`.
195+
- Formato: 16 hex (64 bits). Si dos ids chocaran dentro de una ejecución con datos distintos, el hash se alarga de 2 en 2.
196+
- El HMAC usa un separador de dominio versionado (`mxbank-id/v1`): si algún día cambian las entradas, cambiará el prefijo de dominio y se documentará como cambio de esquema.

‎docs/04-privacidad-y-redaccion.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ Los números largos que **no** validan se tratan según perfil: en `strict` se e
6565
- Con `--pseudonym-key-env` (`pseudonym_scope: "key"`): tokens estables entre meses. La llave nunca se escribe en la salida ni en logs.
6666
- La longitud del hash crece automáticamente si hay colisiones dentro del documento.
6767

68+
### Identificadores estables (`statement_id`, `transactions[].id`, `installment_plans[].id`)
69+
70+
`id = PREFIJO + "_" + hex(HMAC-SHA256(llave, "mxbank-id/v1" | PREFIJO | banco | producto | número de cuenta | …))[:16]` (detalle de las entradas en [03 §7](03-esquema-salida.md#7-identificadores-estables-schema-11)).
71+
72+
**Por qué HMAC y no un hash simple.** Las entradas son datos que un atacante puede conocer o adivinar: el número de cuenta tiene 10–11 dígitos, la fecha está acotada al periodo y el monto a unos cuantos miles de valores. Con un hash sin llave (`sha256(cuenta | fecha | monto | …)`) bastaría probar combinaciones hasta reproducir el id para **confirmar o recuperar el número de cuenta**, o para saber si alguien hizo un pago concreto. Con HMAC, sin la llave cada intento es indistinguible del azar, así que el id no deja derivar nada.
73+
74+
- La llave es la misma de los seudónimos (`--pseudonym-key-env`). Guárdala como cualquier secreto: quien la tenga puede verificar suposiciones sobre los ids.
75+
- El número de cuenta entra **solo al HMAC**; nunca aparece en la salida.
76+
- El escáner residual exime estos campos **solo** si se llaman `id` o `statement_id` **y** su valor coincide exactamente con `^(TX|ST|PLAN)_[0-9a-f]{16,64}$`; cualquier otro contenido en esos campos, o ese formato en otro campo, se escanea normalmente.
77+
6878
## 6. Perfiles
6979

7080
| Elemento | `minimal` | `standard` (default) | `strict` |

‎docs/AGENT.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ Opciones útiles de `extract`: `--privacy strict`, `--include-concepts`, `--pseu
4545
- `credit` (solo tarjetas): límite, disponible, fecha de corte, fecha límite de pago, pago mínimo, pago para no generar intereses, CAT y tasa.
4646
- `installment_plans[]` (solo tarjetas): compras a meses con su saldo pendiente.
4747
- `privacy`: qué se eliminó o seudonimizó (conteos, nunca valores).
48+
- `statement_id`, `transactions[].id`, `installment_plans[].id`: identificadores estables (schema 1.1). Ver abajo.
49+
50+
## Importar sin duplicar
51+
52+
Para guardar movimientos en una base propia (p. ej. SQLite de solo agregado) y poder reimportar estados o importar estados que se enciman:
53+
54+
1. Usa **siempre la misma llave**: `--pseudonym-key-env MXBANK_KEY`. Confirma que `privacy.pseudonym_scope` sea `"key"`; si es `"run"`, los ids cambian en cada ejecución y **no deben guardarse**.
55+
2. Usa `transactions[].id` como llave primaria del movimiento e inserta solo los que no existan (`INSERT OR IGNORE`). El mismo movimiento conserva su id aunque venga en otro PDF, en otra página o con otro `--privacy`.
56+
3. Usa `statement_id` para saber si un estado ya se importó, e `installment_plans[].id` para seguir una compra a meses de un mes a otro: cambian la parcialidad y el saldo, no el id.
57+
4. Dos movimientos idénticos el mismo día tienen ids distintos: no los fusiones.
58+
5. Si cambias o pierdes la llave, los ids nuevos no coincidirán con los guardados. Trátala como un secreto y respáldala.
4859

4960
## Ejemplo de uso en un agente
5061

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "mx-bank-statement-reader"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
description = "CLI orientada a IA: estados de cuenta bancarios mexicanos (PDF) a JSON sin datos personales"
55
readme = "README.md"
66
license = "Apache-2.0"

‎site/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@
192192
<section class="hero">
193193
<div class="wrap hero-grid">
194194
<div>
195-
<span class="eyebrow">Código abierto · v0.1.0</span>
195+
<span class="eyebrow">Código abierto · v0.2.0</span>
196196
<h1>Tus estados de cuenta, <span class="hl">listos para la IA</span>. Sin tus datos personales.</h1>
197197
<p class="lead">mxbank lee estados de cuenta de bancos mexicanos en PDF <b>en tu computadora</b>, extrae saldos y movimientos, elimina todo lo que te identifica y entrega un JSON que ya puedes compartir con cualquier asistente.</p>
198198
<div class="cta">

0 commit comments

Comments
 (0)