Skip to content

JoshuaPozos/leyes-mexicanas-markdown

Repository files navigation

🇲🇽 mx-md — Leyes mexicanas en Markdown y JSON canónico

Las 316 leyes federales vigentes de México en Markdown estructurado y JSON canónico (AST), listas para agentes de IA, RAG, búsqueda semántica, APIs legales o cualquier herramienta que consuma texto o datos estructurados.

👉 Ver índice completo de leyes


¿Por qué existe esto?

Los PDFs de la Cámara de Diputados son la fuente oficial de la legislación mexicana, pero son difíciles de consumir programáticamente:

  • Encabezados y pies de página repetidos en cada hoja
  • Marcadores de página embebidos en mitad del texto
  • Sin estructura semántica aprovechable

Este repo los convierte a dos formatos complementarios:

  1. JSON canónico (AST) — Árbol sintáctico estructurado con IDs estables, ideal para APIs, búsquedas exactas y análisis programático.
  2. Markdown — Renderizado desde el JSON, con jerarquía clara (## por Título/Capítulo, ### por Artículo), ideal para RAG, LLMs y lectura humana.

El JSON es la fuente de verdad; el Markdown es una de sus vistas.


📊 Progreso

316 leyes vigentes — catálogo completo y mantenido al día vía el detector de deltas (las abrogadas se mueven a archive/).

Consulta el CHANGELOG y el INDICE para el estado actualizado ley por ley.


📂 Estructura del repositorio

mx-md/
├── canonical/              # JSON canónico (AST) — fuente de verdad
│   ├── CPEUM_constitucion_politica_de_los_estados_unidos_mexicanos.json
│   ├── LISR_ley_del_impuesto_sobre_la_renta.json
│   └── ...                 # 316 archivos, uno por ley vigente
├── markdown/               # Markdown renderizado desde el JSON
│   ├── CPEUM_constitucion_politica_de_los_estados_unidos_mexicanos.md
│   ├── LISR_ley_del_impuesto_sobre_la_renta.md
│   └── ...                 # Convención: {ABREV}_{nombre_snake_case}.md
├── archive/                # Leyes abrogadas / bajas (markdown + canonical), fuera del set vigente
├── schema/
│   ├── law_ast.schema.json # JSON Schema del AST canónico
│   └── example_cpeum_fragment.json
├── scripts/
│   ├── download_leyes.py   # Descarga PDFs + detección de deltas (--check/--apply/--init-snapshot)
│   ├── batch_convert.py    # Convierte PDFs a JSON + Markdown (paralelo; --only-slugs incremental)
│   ├── pdf_to_md.py        # Conversión individual (CLI)
│   ├── gen_indice.py       # Genera INDICE.md con stats por ley
│   ├── constants.py        # Constantes operativas con docstrings (umbrales, timeouts)
│   └── _log.py             # Logging estructurado (MX_MD_LOG_LEVEL)
├── .github/workflows/
│   ├── ci.yml              # CI: ruff + mypy + pytest (Py 3.10-3.12)
│   └── freshness.yml       # Watchdog: --check semanal, abre issue si hay leyes desactualizadas
├── tests/                  # Suite de pytest (407 tests, cobertura 48 %)
├── origen-docs/            # PDFs descargados (no versionados)
├── catalogo.json           # Catálogo de leyes vigentes con SHA-256 por PDF
├── estado.json             # Ledger de frescura del detector de deltas (última reforma + sha por ley)
├── INDICE.md               # Índice navegable con conteo de artículos
├── CHANGELOG.md            # Historial de cambios
├── pyproject.toml          # Empaquetado y dependencias (extras: [dev])
├── requirements.lock       # Lockfile reproducible (versiones exactas)
└── README.md

Convención de nombres

Los archivos Markdown siguen el patrón {ABREV}_{nombre_snake_case}.md:

  • ABREV — la sigla oficial de la ley (ej. CPEUM, CFF, LISR). Para las pocas leyes cuyo PDF tiene nombre numérico, se deriva del nombre completo (máx. 8 letras).
  • nombre_snake_case — el nombre completo normalizado a ASCII y snake_case, truncado a 70 caracteres.

🚀 Uso rápido

1. Instalar dependencias

python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate

# Instalación normal (rangos de versión flexibles)
pip install -e .

# Instalación reproducible (versiones exactas del lockfile)
pip install -r requirements.lock && pip install -e . --no-deps

# Para desarrollo (incluye ruff, mypy, pytest)
pip install -e ".[dev]"

Tesseract debe estar instalado a nivel de sistema operativo para el OCR de tablas: macOS: brew install tesseract · Ubuntu/Debian: apt install tesseract-ocr

2. Descargar todos los PDFs

# Descargar las 315+ leyes (~2 GB)
python scripts/download_leyes.py

# Solo ver el catálogo sin descargar
python scripts/download_leyes.py --list

# Descargar solo las que faltan
python scripts/download_leyes.py --skip-existing

# Descargar solo las primeras 10 (para probar)
python scripts/download_leyes.py --limit 10

El descargador valida cada PDF antes de aceptarlo (bytes mágicos %PDF-, host en allowlist diputados.gob.mx, retry con backoff exponencial ante fallos transitorios) y anota el SHA-256 de cada archivo en catalogo.json para detectar reformas upstream entre corridas.

3. Convertir a JSON + Markdown

# Convertir todos los PDFs en paralelo (default: os.cpu_count() workers)
python scripts/batch_convert.py

# Controlar el paralelismo (1 = serial, útil para depurar)
python scripts/batch_convert.py --workers 4

# Timeout por PDF (default 300 s) — un PDF que se cuelgue se reporta como
# error sin bloquear al pool
python scripts/batch_convert.py --timeout 600

# Solo los que no se han convertido
python scripts/batch_convert.py --skip-existing

# Solo JSON (sin Markdown)
python scripts/batch_convert.py --format json

# Solo Markdown (sin JSON)
python scripts/batch_convert.py --format md

# Convertir con validación contra schema
python scripts/batch_convert.py --validate

# Convertir un PDF específico
python scripts/pdf_to_md.py origen-docs/LISR_ley_del_impuesto_sobre_la_renta.pdf --verbose

Speedup medido con 8 cores: ~2.4x vs serial (bottleneck es OCR / I/O, no CPU puro).

4. Regenerar el índice

python scripts/gen_indice.py

🔄 Mantener el corpus al día (detección de deltas)

Las leyes se reforman constantemente. En vez de regenerar las 316 leyes cada vez, un detector de deltas compara el índice vivo de diputados contra un ledger versionado (estado.json) y actualiza solo lo que cambió.

# Una vez: graba el ledger de frescura desde el índice vivo (sin descargar PDFs)
python scripts/download_leyes.py --init-snapshot

# Read-only: ¿qué leyes cambiaron, se agregaron o se abrogaron? (exit 0=limpio, 10=hay deltas, 2=inconcluso)
python scripts/download_leyes.py --check

# Aplica: descarga SOLO las leyes cambiadas+altas, archiva las abrogadas, y escribe delta.txt
python scripts/download_leyes.py --apply

# Reconvierte SOLO las leyes del delta (force overwrite); LIGIE necesita timeout amplio
python scripts/batch_convert.py --only-slugs delta.txt --timeout 1200
  • estado.json es el ledger: por ley guarda su última reforma cruda, su ref_abbrev (id del historial de reformas) y su sha256. Es la línea base contra la que se diffea.
  • Leyes abrogadas (marcadas Ley Abrogada / numero=A upstream, p.ej. una ley reemplazada por otra) se mueven a archive/ — salen del set vigente sin borrarse.
  • Watchdog automático: .github/workflows/freshness.yml corre --check cada semana y abre/actualiza un issue (label freshness) cuando hay leyes desactualizadas.

Notas operativas

  • LIGIE (ley de aranceles) tarda ~8 min en convertir y puede exceder timeouts cortos → usa --timeout 1200 o conviértela aparte.
  • --apply muta el corpus (markdown/canonical/catálogo/estado). Tras correrlo, revisa el diff y regenera el baseline de regresión antes de commitear.
  • El detector es read-only y resumible: --check nunca escribe; un --apply interrumpido se re-corre sin daño (los ledgers se persisten atómicamente, antes de mover archivos).

🤖 Cómo usarlo en un agente / RAG

Markdown

El Markdown tiene esta estructura consistente:

# Ley del Impuesto Sobre la Renta

## TÍTULO I
DISPOSICIONES GENERALES

### Artículo 1
Las personas físicas y las morales están obligadas al pago...

### Artículo 2
Para los efectos de esta Ley, se considera establecimiento permanente...

Puedes chunkearlo por artículo (cada ### Artículo N es un chunk natural), por capítulo, o cargarlo completo.

JSON canónico (AST)

Cada ley tiene un JSON estructurado con nodos tipados e IDs estables:

{
  "schema_version": "1.0.0",
  "id": "LISR_ley_del_impuesto_sobre_la_renta",
  "abbreviation": "LISR",
  "name": "LEY del Impuesto Sobre la Renta",
  "structure": [
    {
      "type": "titulo",
      "id": "titulo-i",
      "heading": "TÍTULO I",
      "descriptor": "DISPOSICIONES GENERALES",
      "children": [
        {
          "type": "articulo",
          "id": "titulo-i.articulo-1",
          "heading": "Artículo 1",
          "content": [
            { "type": "paragraph", "text": "Las personas físicas y las morales están obligadas..." },
            { "type": "fraccion", "ordinal": "I", "text": "Las residentes en México..." }
          ]
        }
      ]
    }
  ]
}

Los IDs son paths jerárquicos estables: titulo-i.capitulo-ii.articulo-15. Puedes usarlos para referencia directa, linking cruzado, y versionado.


🔧 Cómo funciona

PDF → extract_lines() → build_ast() → AST canónico (JSON)
                                        ├── json.dump() → canonical/{slug}.json
                                        └── render_markdown() → markdown/{slug}.md
  1. Scrapingdownload_leyes.py parsea la tabla de diputados.gob.mx y descarga cada PDF.
  2. Extracciónextract_lines() usa pdfplumber para extraer texto, filtrando headers repetitivos y marcadores de página. Las tablas dibujadas como vectores se extraen nativamente con find_tables(); las tablas-imagen, con OCR (Tesseract). Cada tabla lleva su procedencia (source_method + source_page); cuando el OCR no es plausible, se emite un marcador honesto en vez de fabricar datos.
  3. ASTbuild_ast() construye un árbol canónico: detecta Títulos, Capítulos, Secciones, Artículos, fracciones, incisos, notas de reforma y tablas. Asigna IDs estables jerárquicos.
  4. JSON — El AST se serializa como JSON. Cada archivo cumple schema/law_ast.schema.json.
  5. Markdownrender_markdown() recorre el AST y produce Markdown limpio. El JSON es la fuente de verdad.
  6. Índicegen_indice.py genera un INDICE.md con links a cada ley y conteo de artículos.

📥 Fuentes

Todos los PDFs se descargan directamente de la fuente oficial:

Los PDFs no están versionados en este repositorio por su tamaño y porque cambian con cada reforma. El script siempre descarga la versión vigente.


🤝 Contribuir

  1. Clona el repo y ejecuta los scripts de descarga/conversión
  2. Si el Markdown de alguna ley tiene errores, mejora la lógica en pdf_to_md.py
  3. Abre un PR con los cambios

🛠️ Desarrollo

Instalar dependencias de dev

pip install -e ".[dev]"     # incluye pytest, pytest-cov, ruff, mypy

Tests

pytest tests/ -q             # suite completa (~10 s, 407 tests)
pytest tests/ --cov=scripts  # con cobertura (48 % global)

Lint y type-check

ruff check scripts/ tests/
mypy scripts/                # disallow_untyped_defs activado

Ambos corren automáticamente en CI (.github/workflows/ci.yml) sobre Python 3.10, 3.11 y 3.12 en cada push.

Logging estructurado

Los scripts usan getLogger(__name__) con nivel controlable por variable de entorno:

MX_MD_LOG_LEVEL=DEBUG python scripts/pdf_to_md.py origen-docs/CPEUM.pdf

Niveles: DEBUG, INFO, WARNING (default), ERROR, CRITICAL.

Constantes operativas

scripts/constants.py centraliza los umbrales y timeouts que afectan la conversión (tolerancias OCR, DPI, magic bytes, retries, paralelismo, etc.). Cada constante tiene un docstring explicando por qué el valor elegido. Cambiarlos requiere revalidar el baseline de regresión.


Licencia

MIT — el código es libre. El contenido de las leyes es de dominio público conforme a la legislación mexicana.

About

Convierte los PDFs oficiales de leyes y códigos mexicanos a **Markdown limpio y estructurado**, listo para usarse en agentes de IA, RAG, búsqueda semántica o cualquier herramienta que consuma texto.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages