尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Sitio web de analisis de decisiones politicas
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗Votaciones del Pleno del Congreso de los Diputados y de parlamentos autonómicos, juntas generales y
ayuntamientos en SQLite, con una ficha por iniciativa (resumen neutro, tema, etiquetas, leyes
afectadas) y una web que se abre sin servidor. Implementa las fases 1 y 2 de Análisis de votaciones parlamentarias con IA.md para el Congreso, de la X a la XV legislatura, y las amplía al nivel
autonómico y local según Votaciones autonómicas y locales fuentes por territorio.md. Se actualiza
cada día con GitHub Actions y DeepSeek, o desde cualquier equipo con el repositorio clonado.
Web publicada: https://migueltubia.github.io/escrutinio/
Solo usa la biblioteca estándar de Python (3.10 o superior): ni dependencias ni servidor. Para leer
los PDF de algunas fuentes territoriales usa pdftotext (poppler-utils) o, si no está, pypdf. Si
hay varios pdftotext en el PATH se usa el de poppler, el mismo que en GitHub Actions: el de xpdf
que trae Git para Windows ordena distinto algunos PDF.
Abre web/index.html con doble clic. El navegador carga SQLite compilado a WebAssembly
(sql.js, MIT, incluido en web/vendor/) y la página hace sus
consultas SQL directamente sobre la base de datos, en memoria.
Los navegadores no permiten a una página file:// leer ficheros con fetch, así que
python -m escrutinio web genera una SQLite compacta y la guarda comprimida y en base64 dentro de
ficheros .js, que la página carga con etiquetas <script>. Van troceados igual que la base:
| Fichero | Contenido |
|---|---|
web/datos/indice.js | Lista de ficheros de datos con su institución, su huella y la fecha de generación |
web/datos/comun.js | Catálogos, instituciones y árbol de ámbitos, gobiernos, diputados, informe y los índices de la base |
web/datos/programas.js | Compromisos de los programas electorales, sus iniciativas relacionadas y su estado. Solo se descarga al entrar en Activismo · Programas electorales, al abrir una iniciativa que aparece en algún programa o al pulsar «¿Qué prometieron sobre esto?» en una causa |
web/datos/<institución>/legNN.js | Votaciones, voto nominal, iniciativas, fichas y afinidades de la legislatura NN de esa institución (congreso/leg15.js, parl-AS/leg12.js…) |
En el navegador se juntan en una sola SQLite en memoria, pero solo los de las instituciones del
ámbito elegido (ver «Ámbito territorial»): con el Congreso son unos 4,5 MB (el mayor, 1,6 MB);
con todas las instituciones (28), unos 27 MB.
Solo se reescriben los ficheros cuyo contenido cambia, así que cada semana solo cambian las
legislaturas en curso, comun.js y el índice. La misma carpeta web/ es la que se publica en
GitHub Pages.
| Sección | Qué responde |
|---|---|
| Resumen | Cifras generales, lo que se vota frente a lo que se aprueba, actividad mensual y análisis de las cifras |
| Votaciones / Iniciativas | Buscador con filtros (también derrotas del Gobierno), detalle con voto nominal, grupo decisivo, quién gobernaba y exportación a CSV |
| Temas → ficha de tema | Cómo vota cada grupo en el tema, quién propone y quién lo consigue y matriz votación a votación, con enlaces a la afinidad en el tema (Grupos) y a su evolución por legislatura (Comparar) |
| Grupos → perfil de grupo | Su voto por tema, sus iniciativas, con quién coincide y en qué, y cuándo su voto decidió el resultado |
| Comparar | Dos o más grupos frente a frente en los mismos asuntos: en cuántos votan igual, cuánto apoya cada uno lo que presentan los demás, a quién apoya cada uno, tema a tema y legislatura a legislatura, y asunto a asunto |
| Comparar · Programas electorales | Los programas de los partidos entre sí, sin votos: qué quiere quitar y qué añadir cada uno (por tipo de medida y por tema) y cuánto pesa cada tema en su programa frente a lo que presentó después |
| Análisis · Coaliciones ganadoras | Qué combinaciones de grupos aprueban y tumban cada cosa |
| Análisis · Mapa ideológico y polarización | Posición de los grupos según sus votos (MDS sobre la afinidad) y polarización por trimestre |
| Análisis · Disciplina y ausencias | Diputados que votan distinto a su grupo, votos no emitidos y votaciones que habrían cambiado con los ausentes |
| Análisis · Enmiendas | Qué grupo consigue cambiar las leyes, quién apoya las enmiendas de quién y leyes con más enmiendas aprobadas |
| Activismo · Mis causas | Seguimiento guardado en el navegador (texto, tema o etiqueta), novedades desde la última visita, scorecard con tu criterio, qué prometían los partidos sobre lo mismo en sus programas y CSV |
| Activismo · Qué viene | Iniciativas abiertas de la legislatura actual: recién abiertas a enmiendas, pendientes de debate y paradas en comisión, con los partidos que llevaban en su programa algo en su dirección |
| Activismo · Programas electorales | Lo que prometió cada partido en su programa frente a lo que votó: primero, dónde votó en contra de lo que prometía; después, cuánto coincide cada programa con lo votado, en el Gobierno y en la oposición, y compromiso a compromiso con su cita y las iniciativas relacionadas (ver «Programas electorales»). El detalle de cada iniciativa dice qué partidos llevaban algo relacionado en su programa, y el de cada votación enlaza con él |
En listas y tablas los títulos oficiales se acortan (abreviatura del tipo, número y asunto: «RDL 23/2026 · Medidas fiscales…», «PNL · Digitalización de…»); el título completo sale al pasar el ratón, en el detalle y en los CSV.
Todos los filtros admiten varias opciones a la vez (legislaturas, temas, tipos, grupos…). En Grupos y en el Mapa, con varias legislaturas o temas se suman las votaciones de cada partido; en el perfil de grupo, varios grupos se analizan juntos como un bloque.
En el móvil (pantalla estrecha, o un teléfono girado) la web cambia de interfaz: secciones en una barra inferior (con «Más» para Análisis y Activismo), filtros plegados tras un botón, selectores como hojas inferiores, gráficos al ancho de la pantalla, tablas con desplazamiento horizontal y tooltips que se abren al tocar.
En todas las vistas de análisis se usa el «apoyo» a la iniciativa: en las enmiendas a la totalidad votar sí es votar contra el proyecto, así que ese voto se invierte. Los porcentajes de apoyo por grupo cuentan asuntos, no votaciones: si un asunto se votó por puntos (PNL, mociones), los puntos se reparten su peso, y en los debates con propuestas de resolución las de cada grupo cuentan como un asunto. Cada grupo apoya casi siempre lo que presenta él mismo, y el partido del Gobierno, lo que presenta el Gobierno: en la ficha de tema y en el perfil de grupo, «a favor» no cuenta lo que presenta el propio grupo ni, mientras gobierna, lo del Gobierno (en la oposición, lo del Gobierno sí cuenta), y el total con lo propio queda como dato secundario. Comparar deja fuera además todo lo del Gobierno, para medir a todos los grupos con la misma base.
Los debates de política general (estado de la nación, de la comunidad o de la región, orientación
política del Gobierno) no tienen tema: cada grupo presenta propuestas de resolución sobre cualquier
asunto. Se reconocen por el título (texto.es_debate_general); sus votaciones cuentan en los totales,
pero en ningún tema. En el Congreso, cada propuesta se atribuye al grupo que la presenta.
GitHub rechaza ficheros de más de 100 MB y la base completa ocupa casi 400 MB, sobre todo por los
casi 7 millones de filas de voto nominal. Por eso lo que se versiona es data/bd/, troceado por
institución y legislatura, que es la unidad natural de los datos y además estable (una legislatura
cerrada no cambia):
| Fichero | Contenido |
|---|---|
data/bd/comun.sqlite | Legislaturas de todas las instituciones, catálogos, diputados e informe |
data/bd/<institución>/legNN.sqlite | Todo lo de la legislatura NN de una institución (congreso/leg15.sqlite, parl-AN/leg12.sqlite, ayto-gijon/leg12.sqlite…): días de pleno, votaciones, agregados por grupo, iniciativas, fichas, afinidades… |
data/bd/manifiesto.json | Huella del contenido de cada fichero y recuentos |
Cada institución tiene un número fijo (escrutinio/territorio.py) y su legislatura N se guarda con
el id número * 100 + N. El Congreso es el 0, así que sus ids son los de siempre (10 a 15) y todo lo
que filtra por legislatura vale para cualquier institución.
El voto nominal va compacto en voto_compacto: por cada votación, una cadena con el sentido de cada
diputado de la plantilla (S sí, N no, A abstención, - no vota, . no era diputado) y otra con
su grupo (índice en grupo_idx). Los ficheros no llevan índices de SQLite: solo guardan los datos, y
unir crea los índices al reconstruir la base de trabajo. El mayor fichero ronda los 14 MB (Congreso,
XIV legislatura) y el total, unos 160 MB: 40 del Congreso y el resto de las demás instituciones.
data/escrutinio.sqlite es la base de trabajo (todo junto, con voto en filas): no se versiona y
se reconstruye con unir. partir vuelve a generar los troceados y solo reescribe los que han
cambiado, así que cada semana en git solo cambia la legislatura en curso.
python -m escrutinio unir # base de trabajo desde data/bd/ (menos de un minuto)
python -m escrutinio actualizar # todo el ciclo: datos nuevos, reglas, fichas si hay clave, web y partir
python -m escrutinio estado # resumen de la base de trabajo
Pasos sueltos, por si hacen falta:
python -m escrutinio votaciones / iniciativas / procesar / analizar / web / partir
python -m escrutinio fichas-deepseek --limite 100 # fichas nuevas con DeepSeek (DEEPSEEK_API_KEY)
python -m escrutinio informe-deepseek # informe de análisis con DeepSeek
python -m escrutinio fichas-exportar # lotes JSONL para generarlas por otra vía
python -m escrutinio fichas-validar 1
python -m escrutinio fichas-importar "data/llm/resultados/x/*.jsonl" --modelo <modelo>
python -m escrutinio estadisticas
python -m escrutinio territorial [--fuente asturias,andalucia] [--completo] # votaciones autonómicas y locales
python -m escrutinio territorial-estado # cobertura de cada conector
python -m escrutinio territorial-probar <conector> --limite 30 # probar un conector sin tocar la base
python -m escrutinio actas-deepseek --limite 40 # votaciones de diarios de sesiones y actas (DEEPSEEK_API_KEY)
python -m escrutinio actas-exportar --limite 20 # textos pendientes para procesarlos por otra vía
python -m escrutinio actas-importar "data/llm/actas/respuestas/*.json" --modelo <modelo>
python -m escrutinio programas-descargar / programas-estado # programas electorales (ver «Programas electorales»)
python -m escrutinio programas-leer / programas-emparejar / programas-verificar / programas-calcular
La actualización es un solo programa, python -m escrutinio actualizar, y da igual quién lo lance:
GitHub Actions cada día, o cualquier persona desde su equipo con el repositorio clonado. Los dos
caminos parten de la misma base troceada (data/bd/) y de las mismas fichas y actas guardadas
(data/llm/), y dejan el resultado en el repositorio, así que quien actualiza después, sea el runner
o una persona, sigue donde lo dejó el anterior.
Dos workflows:
| Workflow | Cuándo | Qué hace |
|---|---|---|
actualizar.yml («Actualizar datos») | Diaria, de lunes a sábado a las 04:20 UTC y a las 22:40 UTC: la de la noche recoge las votaciones del mismo día y cubre a la de la mañana si GitHub la descarta | python -m escrutinio actualizar, commit de data/llm (las fichas generadas) y publicación de la web con los datos del día |
| Semanal, el domingo a las 04:20 UTC, y a mano | Lo mismo, más el informe de análisis de la portada (--informe) y commit también de data/bd y web/datos | |
pages.yml («Publicar web») | Al subir cambios de web/ a main y a mano | Publica web/ en GitHub Pages, tal cual |
La base y los datos de la web se guardan en git solo una vez por semana para que el historial no
sume cada día una copia binaria de varios MB; la web publicada sí se actualiza a diario, porque la
publica el propio workflow con lo que acaba de calcular. Las fichas (data/llm) se guardan
cada día porque no se pueden volver a descargar. Al subir un cambio de código, «Publicar web» publica
los datos de la última copia semanal hasta la siguiente actualización diaria.
Configuración, una sola vez, en el repositorio de GitHub:
Settings > Pages > Build and deployment > Source: GitHub Actions.
Settings > Secrets and variables > Actions:
| Tipo | Nombre | Valor |
|---|---|---|
| Secret | DEEPSEEK_API_KEY | Clave de la API de DeepSeek. Sin ella se actualiza todo menos las fichas nuevas |
| Variable | DEEPSEEK_MODEL | Opcional; por defecto deepseek-flash (también deepseek-v4-pro) |
Pestaña Actions: ejecutar «Publicar web» para la primera publicación (luego va sola) y, si se quiere probar ya, «Actualizar datos».
Los workflows declaran los permisos que necesitan (contents: write para el commit, pages: write
para publicar). Si el commit falla con un 403, en Settings > Actions > General > Workflow
permissions hay que elegir «Read and write permissions».
Qué hace cada día:
data/bd/ ha cambiado desde entonces (la copia del domingo, o una actualización subida desde un
equipo), la reconstruye desde ahí.Cada ejecución tiene 95 minutos (MINUTOS_MAX) repartidos entre los pasos, con tiempo reservado
siempre para el 7 y el 8. Lo que no da tiempo a hacer queda para la ejecución siguiente: así GitHub
no corta el trabajo a las dos horas, que perdería todo lo hecho. Al terminar deja en la ejecución un
aviso con lo que ha tardado cada paso. Si aun así algo falla, se guardan las fichas y actas ya
leídas (data/llm).
Qué se actualiza y cada cuánto, contado para quien usa la web, está también en la página de ayuda
(#/ayuda), que además explica cada pestaña con el botón «Cómo se lee».
Hace falta Python 3.10 o superior y git, nada más (para los PDF de algunas fuentes, pdftotext o
pypdf, ver arriba). La clave de DeepSeek es opcional: sin ella se actualiza todo menos las fichas
nuevas, la lectura de diarios y actas y el informe (ver el apartado siguiente). Los pasos, siempre en
este orden y de una vez:
git pull # 1. la última base y las últimas fichas
python -m escrutinio unir # 2. base de trabajo desde data/bd/ (sustituye la que hubiera)
python -m escrutinio actualizar # 3. lo mismo que hace Actions: de unos minutos a más de una hora
git add data/bd web/datos data/llm # 4. base troceada, datos de la web, fichas y actas
git commit -m "Actualización de datos"
git push # 5. enseguida, antes de que actualice otro
Los mismos comandos valen en PowerShell. Opciones de actualizar: --limite-fichas N (400 por
defecto), --limite-actas N (sin límite por defecto: lee actas hasta que se acaba el tiempo; sin
--minutos, todas las pendientes que se listen; 0 para no leer ninguna; las ejecuciones programadas de
Actions leen 150, o lo que diga la variable LIMITE_ACTAS), --informe (redacta el informe de la portada), --sin-ia (no
llamar a DeepSeek aunque haya clave), --sin-territorial (solo el Congreso) y --minutos N (tiempo
máximo, como en Actions; sin él, no hay límite). Antes de subir se puede
ver el resultado abriendo web/index.html. La web publicada se actualiza con el siguiente «Actualizar
datos» o lanzando «Publicar web» a mano en la pestaña Actions.
Dos reglas para no chocar con Actions ni con otra persona:
data/bd son SQLite y web/datos va en base64: git no
puede fusionarlos, solo elegir una versión. Dos actualizaciones seguidas, cada una a partir de la
anterior, no dan problema; dos cruzadas, sí: la segunda no puede subir.Si el push falla porque main ha avanzado mientras tanto, no hay que fusionar los binarios sino
regenerarlos: git pull --no-rebase; en los conflictos de data/bd y web/datos, quedarse con los
propios (git checkout --ours -- data/bd web/datos); en los de data/llm, conservar las líneas de
las dos versiones (cada línea de los .jsonl es independiente; si choca el informe, elegir uno);
volver a ejecutar python -m escrutinio actualizar, que importa las fichas y actas recién traídas y
regenera la base y la web; y hacer commit y push.
DeepSeek hace tres cosas en la actualización: las fichas de las iniciativas nuevas, la lectura de los
diarios de sesiones y actas que solo se publican en texto libre, y el informe de la portada. Todo lo
que devuelve se guarda en data/llm/ y se versiona, así que la clave la necesita solo quien
actualiza; ni la web ni unir la usan. Dónde va, según quién actualiza:
| Quién | Dónde |
|---|---|
| GitHub Actions | Settings > Secrets and variables > Actions: secret DEEPSEEK_API_KEY y, opcional, la variable DEEPSEEK_MODEL |
| Una persona, en su equipo | Fichero .env en la raíz del repositorio, copiado de .env.ejemplo. Está en .gitignore y no se sube. También vale como variable de entorno del sistema, que tiene prioridad |
| Variable | Para qué |
|---|---|
DEEPSEEK_API_KEY | La clave. Sin ella, actualizar lo avisa en el registro y hace todo lo demás |
DEEPSEEK_MODEL | Modelo; por defecto deepseek-flash (también deepseek-v4-pro) |
DEEPSEEK_BASE_URL | Dirección de la API; por defecto https://api.deepseek.com |
Sin clave, o con otro sistema: fichas-exportar y actas-exportar dejan en data/llm/pendientes/
y data/llm/actas/pendientes/ los lotes pendientes con sus instrucciones (data/llm/INSTRUCCIONES.md),
para que los procese otro modelo, otro servicio u otra persona; fichas-validar y actas-validar
comprueban las respuestas y fichas-importar y actas-importar las cargan. El cliente
(escrutinio/llm/deepseek.py) habla con una API compatible con la de OpenAI usando solo la
biblioteca estándar, así que DEEPSEEK_BASE_URL y DEEPSEEK_MODEL son el punto de partida para otro
servicio compatible, aunque no está probado con ninguno.
Los servidores de GitHub Actions están fuera de España y varias webs de instituciones no aceptan esas conexiones. En la primera ejecución no respondieron el Parlamento Vasco, el Ayuntamiento de Zaragoza y las Cortes de Aragón (se agota el tiempo o cortan la conexión), y el Parlamento de Navarra contestó con un 403: su web está detrás de una protección contra robots, que no se intenta saltar. Una web que no deja ni conectar dos veces seguidas se da por caída hasta el final de la ejecución, para no esperar minutos en cada petición. Esas instituciones se quedan como estaban, y se avisa en tres sitios:
Cada recogida anota por institución si respondió y, si no, por qué (escrutinio/territorial/estado.py);
los avisos viajan a la web en web/datos/indice.js. Para poner al día esas instituciones basta hacer
la actualización en local desde España, con los pasos de «En local: desde cualquier equipo»: cada
conector trae lo que falte desde la última votación guardada de su institución.
Otras tres webs, las de las Cortes de Castilla y León, las Juntas Generales de Bizkaia y el Parlament
de les Illes Balears, fallaban en Actions por otro motivo: envían su certificado sin el intermedio,
que Windows completa por su cuenta y Linux no. Esos intermedios van en
escrutinio/territorial/certificados.pem y ya no fallan. En el mismo fichero va la raíz nueva de la
FNMT-RCM (G2R, de diciembre de 2025) con la que la Asamblea de Extremadura firma su certificado desde
septiembre de 2026: ni el paquete de certificados de Linux ni el almacén de Windows la traen todavía y
sin ella fallaba en los dos.
Casi todo es determinista: descarga, cruces, tipo de votación, resultados, grupo decisivo, afinidades, coaliciones, enmiendas… DeepSeek describe y clasifica cada iniciativa sin recibir votos ni resultados; en lo autonómico y local, además, lee los días de diario o acta que las reglas no saben leer (ver «Ámbito territorial»).
| Qué | Cómo | Dónde queda |
|---|---|---|
| Ficha de cada iniciativa (resumen, tema de una lista cerrada de 23, etiquetas, leyes afectadas, marcas) | Generada con DeepSeek; todo se valida contra los catálogos antes de guardarse | data/llm/resultados/**.jsonl (fuente de verdad) y ficha_llm |
| Tipo de las votaciones que las reglas no reconocen | DeepSeek, con lista cerrada | data/llm/tipos_votacion.jsonl |
| Tema provisional de las leyes sin ficha | Reglas sobre la comisión competente: coincide con la ficha definitiva en el 90% de los casos | tema_provisional |
| Equivalencias de etiquetas y nombres de leyes | Tabla revisada una vez; ahora es código | escrutinio/llm/etiquetas.py |
| Quién gobernaba en cada fecha (presidente, partido y socios) | Tabla a mano: Gobierno de España, comunidades autónomas, diputaciones forales y ayuntamientos | escrutinio/gobiernos.py |
| Informe de la portada | DeepSeek a partir de estadisticas | data/llm/informe.md |
| Votaciones de los diarios y actas que las reglas no leen | DeepSeek, solo con lo que está escrito | data/llm/actas/<institución>.jsonl |
Las fichas del lote inicial se generaron antes de montar la actualización automática
(data/llm/resultados/modelos.json las marca como «lote inicial»); las nuevas de DeepSeek llevan su
origen en cada línea. Quedan unas 11.900 iniciativas sin ficha, casi todas de las instituciones
autonómicas incorporadas al final: el workflow diario hace hasta 400 por ejecución, así que se
completan en alrededor de un mes. Todas las peticiones a DeepSeek piden responder en español de
España. Control de calidad: reclasificadas a ciegas 60 fichas al azar, el tema principal coincide
en el 93% (98% contando secundarios). El resultado calculado con los totales se contrasta con el
oficial: en convalidaciones y votaciones de conjunto solo discrepa una votación, marcada con aviso.
Implementa las fases 0 y 1 de Programas y votos lo que dicen frente a lo que votan.md y la sección
«Programas electorales» de Activismo en la web: los compromisos del programa de cada partido, cada uno con su
cita literal y su página, enlazados con las iniciativas del Pleno que tratan lo mismo y con lo que votó
el partido en ellas. Están los programas de las generales de 2011 a 2023 de los partidos con grupo propio,
cada uno frente a la legislatura que empezó tras la elección:
| Elecciones | Legislatura | Programas |
|---|---|---|
| 2011 | X | PSOE, PP, CiU, IU (La Izquierda Plural), UPyD y PNV |
| 2015 | XI | PSOE, PP, Podemos, ERC, Democràcia i Llibertat y PNV |
| 2016 | XII | PSOE, PP, Unidos Podemos, Ciudadanos, ERC y PNV |
| Abril de 2019 | XIII | PSOE, PP, Unidas Podemos, Ciudadanos, VOX, ERC y PNV |
| Noviembre de 2019 | XIV | PSOE, PP, VOX, Unidas Podemos, Ciudadanos, ERC y PNV |
| 2023 | XV | PSOE, PP, VOX, Sumar, ERC, Junts, EH Bildu y PNV |
Faltan el de Ciudadanos de 2015, que solo existe escaneado, y el de EH Bildu de noviembre de 2019, que no
se ha encontrado. La XI y la XIII duraron pocos meses, así que sus programas tienen pocas iniciativas con las
que compararse. Los de CiU, Democràcia i Llibertat, ERC y Junts están en catalán: la cita se guarda en la
lengua del programa y el compromiso, en castellano. Los programas se añaden en
escrutinio/programas/registro.py (PROGRAMAS) o, sin tocar código, en data/programas.json (mismo
formato, pensado para una elección nueva): la ruta del PDF se pone a mano cuando el partido lo publica,
no se descubre sola. Los partidos que votan dentro del Grupo
Mixto (BNG, CC, UPN, y Podemos desde diciembre de 2023) no tienen voto propio de grupo con el que
comparar: harían falta sus diputados uno a uno.
Cada programa se descarga, se lee y se guarda una sola vez. Lo que manda son los ficheros de
data/llm/programas/, versionados como las fichas, y la base se reconstruye desde ellos (unir,
web y actualizar los cargan):
| Fichero | Contenido |
|---|---|
data/llm/programas/registro.jsonl | Una línea por documento: partido, elección, URL, sha256, formato (pdf o html), páginas, estado (pendiente, leido, error), modelo, versión del prompt, compromisos y tokens gastados |
data/raw/programas/<id>.txt | Texto extraído del PDF (o de la página web), con un salto de página entre páginas (en las páginas web, entre apartados). Se versiona (el PDF no) para que las citas sigan siendo comprobables aunque el partido retire el programa |
data/llm/programas/<id>.jsonl | Una línea por compromiso: texto, cita literal, página, tema, tipo de acción, de quién depende y si es verificable |
data/llm/programas/emparejamientos/<id>.jsonl | Una línea por par compromiso–iniciativa decidido, también los que no tienen que ver, para no volver a preguntarlos |
data/llm/programas/verificaciones/<id>.jsonl | Segunda revisión de cada relación con dirección (misma o contraria): manda sobre la primera y no se repite |
python -m escrutinio programas-descargar # descarga y registra; si el sha256 no cambia, no hace nada
python -m escrutinio programas-estado # qué está leído, qué falta, qué falló y el gasto estimado
python -m escrutinio programas-leer # compromisos de los pendientes, con DeepSeek (modelo Pro)
python -m escrutinio programas-emparejar # candidatas nuevas de cada compromiso, con DeepSeek
python -m escrutinio programas-verificar # segunda revisión de las relaciones con dirección (modelo Pro)
python -m escrutinio programas-verificar --cifras --modelo <modelo rápido> # solo las que cuentan, como cada día
python -m escrutinio programas-verificar --rehacer --solo contraria # tras cambiar el prompt, solo esas
python -m escrutinio programas-calcular # recarga todo y recalcula el estado, sin DeepSeek
python -m escrutinio programas-rehacer-citas # vuelve a buscar las citas con las respuestas guardadas, sin DeepSeek
python -m escrutinio programas-rehacer-texto --id generales-2019-04-psoe # vuelve a extraer el texto y rehace sus citas
python -m escrutinio programas-releer --id generales-2023-pp --version compromisos-v2 # solo a propósito
Cómo se hace, paso a paso:
error. El texto se
extrae con pdftotext separando las columnas por la posición de cada palabra (en los programas a dos
columnas, sin separarlas, las frases salían mezcladas); los del PNV de 2019, que meten los rótulos
laterales en medio de las palabras, en el orden interno del PDF (EXTRACCION). Los de Ciudadanos de 2019
solo se publicaron como página web: cada apartado hace de página y la web enlaza con él como «apartado N».data/raw/programas/trozos/, sin versionar) y no se vuelven a pagar; si una
respuesta sale truncada, el trozo se parte en dos. Con esa caché, programas-rehacer-citas y
programas-rehacer-texto rehacen las citas sin volver a leer.--cifras).La actualización diaria lee los programas registrados que estén pendientes, decide las candidatas
nuevas (hasta 300 compromisos por ejecución), revisa con el modelo rápido las relaciones nuevas que
cuentan en las cifras y recalcula el estado. Descargar un programa nuevo es
siempre a mano. En la web, la lista de programas va en web/datos/comun.js y los compromisos, en web/datos/programas.js,
que solo se descarga cuando hace falta.
Pendiente, según el plan: las intervenciones en el Pleno, los programas autonómicos y el contraste con la Chapel Hill Expert Survey. Cuando la web del partido no deja descargar el programa (psoe.es está tras una protección contra robots que no se intenta saltar) o ya no lo enlaza, se usa la copia que publicó un medio o la del archivo de Internet (web.archive.org), y se guarda también la URL oficial si se conoce.
| Dato | Fuente |
|---|---|
| Votaciones y voto nominal | Datos abiertos de votaciones del Congreso (un JSON por votación) |
| Expediente, autor y resultado de tramitación | Buscador de iniciativas del Congreso (endpoint JSON) |
| Situación, comisión, plazos y enlaces al BOCG (legislatura actual) | Datos abiertos de iniciativas |
SIN/...) con el texto de la votación.Además del Congreso se recogen las votaciones de parlamentos autonómicos, Juntas Generales vascas y
ayuntamientos. Las fuentes, lo que publica cada una y lo difícil que es sacarlo están en
Votaciones autonómicas y locales fuentes por territorio.md.
La cabecera de la web tiene un selector de ámbito con un árbol:
Marcar un nodo marca todo lo que tiene debajo: una comunidad incluye su parlamento, lo provincial y
los ayuntamientos; se puede desmarcar lo que sobre o elegir solo un ayuntamiento. Los atajos por
nivel eligen un nivel entero sin lo de debajo: por ejemplo, todos los parlamentos autonómicos
sin juntas ni ayuntamientos, o todos los ayuntamientos. La elección va en la URL (amb=congreso,
amb=AS, amb=*autonomico, amb=es…), así que los enlaces compartidos la conservan. Al abrir la
web sin ámbito en la URL se carga solo lo nacional, que es lo más ligero; el selector ofrece volver a
la última elección y avisa cuando lo marcado pesa mucho (la primera carga puede tardar y el navegador
usa bastante memoria).
La web solo descarga y monta los ficheros de las instituciones elegidas, y todas las vistas (votaciones, temas, grupos, coaliciones, mapa, disciplina…) trabajan sobre ese ámbito sin más cambios. Con varias instituciones, las legislaturas llevan su nombre corto («Asturias XII») y los grupos se suman por partido (el PSOE del Congreso y el de un parlamento cuentan juntos), lo que permite ver, por ejemplo, con quién coincide cada partido en toda una comunidad. Cada votación enlaza a su fuente original.
Cada institución tiene un conector en escrutinio/territorial/fuentes/ que devuelve las
votaciones ya normalizadas (escrutinio/territorial/modelo.py); guardarlas, calcular resultados,
votación decisiva, grupos y grupos decisivos es común a todas (territorial/cargar.py y
territorial/procesar.py). Hay tres tipos de conector:
| Tipo | Qué hace | fuente |
|---|---|---|
| Datos estructurados | Descarga XML, JSON, CSV o tablas HTML con el voto nominal, por grupo o los totales | xml, json, csv, html |
| PDF de formato fijo | Lee con reglas el PDF que genera el sistema de votación o un acta muy regular | pdf-reglas, html-reglas |
| Diario de sesiones o acta en texto libre | El conector lista los documentos del pleno; DeepSeek lee el texto condensado y devuelve cada votación con su asunto, expediente, totales, voto por grupo y resultado, sin añadir nada que no esté escrito | pdf-llm, html-llm |
Lo leído de diarios y actas se guarda en data/llm/actas/<institución>.jsonl (una línea por
documento): es la fuente de verdad de esa parte, así que reconstruir la base no repite nada y cada
ejecución sigue con el atraso donde lo dejó la anterior. Sin clave de DeepSeek, actas-exportar deja
los textos y unas instrucciones para procesarlos por otra vía y actas-importar carga el resultado.
Nada se inventa: si una fuente solo da el resultado, no hay totales; si solo da los totales, no hay
voto por grupo. El resultado es el oficial cuando la fuente lo publica; si no, se calcula con los
totales y la mayoría de esa cámara (sus escaños), y si ambos no cuadran la votación lleva un aviso.
Los grupos se reconocen por el nombre que publica cada fuente (territorial/partidos.py) para que
el mismo partido tenga las mismas siglas y el mismo color en todas partes. Un grupo que no encaje con
ningún patrón no se pierde: sale con su propio nombre y un color neutro.
Para un partido nuevo no hace falta tocar el código: se da de alta en data/partidos.json (patrón,
siglas y color), que la actualización —en la nube o en local— lee al arrancar. Y para saber qué
partidos están apareciendo sin reconocer, python -m escrutinio partidos-pendientes lista los grupos
que no casan con ningún patrón, los que salen en más instituciones primero (candidatos más claros a
ser un partido que merece la pena dar de alta). La actualización diaria también los resume al final, y
en GitHub Actions avisa de los que aparecen en varias instituciones.
Para añadir una institución basta un módulo nuevo en escrutinio/territorial/fuentes/ que declare
CUERPOS (con su número, legislaturas y escaños) y descargar(ctx) o documentos(ctx); se prueba
con python -m escrutinio territorial-probar <módulo> sin tocar la base.
Situación a 27 de septiembre de 2026. Casi todos los diarios y actas se leen ya con reglas; «para leer» son los días que las reglas no saben leer (investiduras por llamamiento, elecciones con papeleta, escaneos ilegibles), que DeepSeek va procesando con un cupo por ejecución (40 documentos al día, repartidos entre instituciones). La web calcula la tabla con lo que haya cargado en cada momento.
En total, 106.277 votaciones de 27 instituciones autonómicas, provinciales y municipales, además de las 16.928 del Congreso.
Parlamentos autonómicos
| Institución | Conector | Legislaturas | Votaciones | Detalle | Cómo |
|---|---|---|---|---|---|
| Parlamento de Andalucía | andalucia | IX–XIII (2012–) | 4.650 | Voto de cada diputado (98 %), por grupo y totales | PDF del sistema de votación, con reglas; fichas de iniciativa |
| Junta General del Principado de Asturias | asturias | X–XII (2015–) | 5.074 | Voto de cada diputado hasta 2023; después, totales y resultado | XML de eParlamento cruzado con el Diario de Sesiones |
| Parlamento de Canarias | canarias | IV–XI (1995–) | 9.847 | Totales y resultado | Texto del Diario (API de datos abiertos) y, antes de 2007, el PDF del Diario, con reglas; 4.609 iniciativas |
| Cortes de Castilla y León | castilla_leon | IX–XII (2015–) | 2.781 | Totales y resultado | Versión de texto del Diario, con reglas |
| Parlament de Catalunya | cataluna | XII–XV (2018–) | 11.836 | Totales y resultado | Diari de Sessions, con reglas |
| Corts Valencianes | valencia | V–XI (1999–) | 3.018 | Totales y resultado; voto de cada diputado en 2019–2020 | Sumario del Diari de Sessions, con reglas, y XML por sesión (2019–2020); los días de presupuestos e investiduras, para leer |
| Asamblea de Extremadura | extremadura | IX–XII (2015–) | 3.620 | Voto de cada diputado y por grupo | Informe de votaciones en PDF, con reglas |
| Parlamento de Galicia | galicia | X–XII (2016–) | 2.956 | Totales y resultado | Diario de Sesiones, con reglas |
| Parlamento de La Rioja | la_rioja | IX–XI (2015–) | 1.435 | Totales, resultado y voto por grupo (IX–X) | Diario de Sesiones, con reglas; 962 iniciativas |
| Cortes de Aragón | aragon | X–XII (2019–) | 3.125 | Totales, resultado y, a veces, voto por grupo | Diario de Sesiones leído entero en la primera carga; los nuevos, para leer |
Provincial e insular
| Institución | Conector | Votaciones | Detalle | Cómo |
|---|---|---|---|---|
| Juntas Generales de Álava | juntas_generales | 4.125 (1999–) | Totales y resultado | Fichas HTML de expediente; catálogo de iniciativas |
| Juntas Generales de Bizkaia | juntas_generales | 286 (2013–) | Totales, resultado y voto por grupo | Actas en PDF, con reglas |
| Juntas Generales de Gipuzkoa | juntas_generales | 708 (2005–) | Totales | Listado de sesiones; el resto, actas para leer |
| Cabildo de Lanzarote, Consell de Mallorca | cabildos_consells | — | Actas para leer (61 y 79) |
Ayuntamientos
| Ayuntamiento | Conector | Periodo | Votaciones | Detalle | Cómo |
|---|---|---|---|---|---|
| Zaragoza | zaragoza | 2015– | 7.206 | Totales, resultado y voto por grupo | Extracto de acuerdos en HTML, con reglas |
| Alicante | alicante | 1999– | 5.056 | Totales, resultado y voto por grupo | Actas y extractos, con reglas |
| Córdoba | cordoba | 2012– | 4.513 | Totales, resultado y voto por grupo | Extracto de pleno en PDF, con reglas |
| Málaga | malaga | 2019– | 2.959 | Totales, resultado y voto por grupo | Videoactas en PDF, con reglas |
| Valladolid | valladolid | 2015– (sin oct. 2023–may. 2025) | 2.103 | Totales, resultado y voto por grupo | «Acuerdos adoptados», con reglas |
| Gijón | gijon | 2015– | 1.580 | Voto de cada concejal (JSON municipal, hasta 2025) y por grupo | Datos abiertos y actas con reglas; actas antiguas para leer |
| Valencia | ayto_valencia | 2018– | 705 | Resultado y grupo proponente | CSV de mociones por sesión; actas para leer (voto por grupo) |
| Las Palmas de Gran Canaria | las_palmas | 2025– | 284 | Totales, resultado y voto por grupo | Actas en PDF, con reglas (las anteriores son imágenes escaneadas) |
| 69 ayuntamientos de Cataluña de más de 20.000 habitantes | aoc_actes | 2015– | — | Actas para leer (10.231) | Dataset «Actes de Ple» del Consorci AOC |
Descartados tras comprobarlos: Séneca (la API de votos pide identificarse), los datos abiertos del Ayuntamiento de Madrid (no hay acuerdos ni votos), el Cabildo de Tenerife (los acuerdos piden Cl@ve) y el de Gran Canaria (no se puede listar sus actas).
Código bajo licencia MIT (LICENSE). sql.js, en web/vendor/, es MIT. Los datos proceden de los
datos abiertos del Congreso de los Diputados y de las webs de cada parlamento, juntas generales y
ayuntamiento, que se enlazan desde cada votación.
| Ayuda | Qué es, qué hace, de dónde salen los datos, qué se actualiza y cada cuánto, conceptos y limitaciones. Cada pestaña tiene además su «Cómo se lee» |
| Parlamento de Cantabria | cantabria | VIII–XI (2013–) | 3.373 | Totales, resultado y voto por grupo (80 %) | Actas del Pleno en PDF, con reglas; 2.422 iniciativas |
| Cortes de Castilla-La Mancha | castilla_la_mancha | VIII–XI (2011–) | 2.767 | Totales y resultado | Diario de Sesiones, con reglas |
| Asamblea Regional de Murcia | murcia | IX–XI (2015–) | 1.365 | Totales, resultado y voto por grupo (sobre todo XI) | Actas en PDF, con reglas (las de IX y X están escaneadas y se leen peor) |
| Parlament de les Illes Balears | baleares | VIII–XI (2011–) | 12.374 | Totales y resultado | Diario de Sesiones en PDF, con reglas, contrastado con los «Resultats de votacions» (2014–2020) |
| Parlamento de Navarra | navarra | IX–XI (2015–) | 3.135 | Totales y resultado | Diario de Sesiones en PDF, con reglas |
| Parlamento Vasco | pais_vasco | X–XIII (2012–) | 5.396 | Totales y voto de cada grupo | PDF oficial con el resultado de cada votación (datos abiertos y buscador del Parlamento) |
| Asamblea de Madrid | — | — | Sin conector | Su web exige ejecutar JavaScript antes de servir nada (protección contra robots). No se ha intentado saltarla: hace falta que la Asamblea lo permita o publique los datos |