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.
- 38,161 artículos · estructura completa (Títulos, Capítulos, fracciones, incisos, notas de reforma) · tablas vectoriales nativas (+ OCR honesto)
- Se mantiene al día con un detector de deltas que vigila la fuente y actualiza solo las leyes reformadas — ver Mantener el corpus al día.
- Fuente oficial: Cámara de Diputados — Leyes Federales Vigentes
👉 Ver índice completo de leyes
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:
- JSON canónico (AST) — Árbol sintáctico estructurado con IDs estables, ideal para APIs, búsquedas exactas y análisis programático.
- 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.
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.
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
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.
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
# 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 10El 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.
# 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 --verboseSpeedup medido con 8 cores: ~2.4x vs serial (bottleneck es OCR / I/O, no CPU puro).
python scripts/gen_indice.pyLas 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 1200estado.jsones el ledger: por ley guarda su última reforma cruda, suref_abbrev(id del historial de reformas) y susha256. Es la línea base contra la que se diffea.- Leyes abrogadas (marcadas
Ley Abrogada/numero=Aupstream, p.ej. una ley reemplazada por otra) se mueven aarchive/— salen del set vigente sin borrarse. - Watchdog automático:
.github/workflows/freshness.ymlcorre--checkcada semana y abre/actualiza un issue (labelfreshness) cuando hay leyes desactualizadas.
- LIGIE (ley de aranceles) tarda ~8 min en convertir y puede exceder timeouts cortos → usa
--timeout 1200o conviértela aparte. --applymuta 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:
--checknunca escribe; un--applyinterrumpido se re-corre sin daño (los ledgers se persisten atómicamente, antes de mover archivos).
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.
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.
PDF → extract_lines() → build_ast() → AST canónico (JSON)
├── json.dump() → canonical/{slug}.json
└── render_markdown() → markdown/{slug}.md
- Scraping —
download_leyes.pyparsea la tabla de diputados.gob.mx y descarga cada PDF. - Extracción —
extract_lines()usapdfplumberpara extraer texto, filtrando headers repetitivos y marcadores de página. Las tablas dibujadas como vectores se extraen nativamente confind_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. - AST —
build_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. - JSON — El AST se serializa como JSON. Cada archivo cumple
schema/law_ast.schema.json. - Markdown —
render_markdown()recorre el AST y produce Markdown limpio. El JSON es la fuente de verdad. - Índice —
gen_indice.pygenera unINDICE.mdcon links a cada ley y conteo de artículos.
Todos los PDFs se descargan directamente de la fuente oficial:
- Cámara de Diputados → diputados.gob.mx/LeyesBiblio
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.
- Clona el repo y ejecuta los scripts de descarga/conversión
- Si el Markdown de alguna ley tiene errores, mejora la lógica en
pdf_to_md.py - Abre un PR con los cambios
pip install -e ".[dev]" # incluye pytest, pytest-cov, ruff, mypypytest tests/ -q # suite completa (~10 s, 407 tests)
pytest tests/ --cov=scripts # con cobertura (48 % global)ruff check scripts/ tests/
mypy scripts/ # disallow_untyped_defs activadoAmbos corren automáticamente en CI (.github/workflows/ci.yml) sobre
Python 3.10, 3.11 y 3.12 en cada push.
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.pdfNiveles: DEBUG, INFO, WARNING (default), ERROR, CRITICAL.
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.
MIT — el código es libre. El contenido de las leyes es de dominio público conforme a la legislación mexicana.