국회 출석부 is a public web app and data pipeline for tracking plenary vote activity and asset disclosure history in the National Assembly of Korea. The project collects official upstream data, normalizes it into stable datasets, publishes lightweight JSON exports, and renders those exports in a static client.
The current product is organized around two main views. The home view focuses on recent vote activity and accountability signals, while the calendar view helps readers inspect participation patterns, compare individual lawmakers over time, and review published asset disclosure history for current members of the 22nd Assembly.
- Accountability leaderboard for lawmakers in the current assembly
- Latest vote feed with official tallies, highlighted no or abstain votes, and verified absences
- Weekly accountability trend summaries for the assembly
- Member search from the published ranking data
- Member activity calendar with per-day participation states
- Compare mode for viewing two members side by side in the calendar experience
- Shareable deep links for member and compare views
- Lazy member detail loading so detailed vote records are fetched only when needed
- Member asset disclosure history in the calendar view for current 22nd Assembly lawmakers
- Asset summary cards with latest total, first disclosure date, latest disclosure date, and cumulative delta
- Category toggles for asset subtotal series plus a real-estate-focused readout for land and building categories
- Scope toggle for family-included totals versus self-only totals when item-level disclosure parsing supports it
- Member history analysis at
#analysis: source-backed career periods, role/activity timelines, period-adjusted peers, cross-party voting relationships, topic continuity, and asset/role chronology. See the analytical model and verification notes.
apps/web: Vite + React frontend deployed to GitHub Pagespackages/ingest: ingestion scripts, parsers, normalization, export builders, source monitoring, and document mirroringpackages/schemas: shared Zod schemas and TypeScript types for normalized records and published contractstests/fixtures: fixed upstream payload fixtures used by pipeline and parser tests.github/workflows: scheduled and manual workflows for ingest, build, deployment, monitoring, and document mirroring
ingest-livefetches official Assembly API payloads and writes a raw snapshot artifact for vote, member, committee, and meeting data.mirror-property-disclosuresqueries the official Assembly file-service JSON listing for property disclosure PDFs, mirrors the files into the public data repository, and refreshes a JSON-only property member context cache.build-datareads the raw ingest snapshot plus the mirrored property inputs from the public data repository, builds normalized datasets, and publishes public exports plus a manifest.deploy-webbuilds the frontend against the published data base URL and deploys the static site to GitHub Pages.monitor-sourceschecks official upstream payloads for changes that could break parsing assumptions.mirror-documentsmirrors other public Assembly documents into the public data repository and updates mirror state metadata.
This repository owns the application code, schemas, tests, and workflow orchestration. Published datasets and exports are meant to live in a separate public data repository referenced by the web app at runtime.
The asset disclosure path is intentionally split from the XML-derived vote snapshot path. Property mapping uses a JSON-only cached roster and tenure context built from official Assembly OpenAPI responses and does not depend on the XML member snapshot used by the main vote pipeline.
The frontend reads public JSON exports and validates them with the schemas package.
exports/latest_votes.json: home feed payload for recent plenary votesexports/accountability_summary.json: ranked lawmaker summary used by the leaderboard and searchexports/accountability_trends.json: weekly trend points and mover windows for the overview chartsexports/member_activity_calendar.json: calendar summary payload for the current assemblyexports/member_activity_calendar_members/<memberId>.json: lazily loaded member vote record detailsexports/member_statement_summaries/index.json: members with published bill-related minutes summaries and their export pathsexports/member_statement_summaries/<memberId>.json: locally generated summaries of each member's officially attributed bill-related statements with source evidenceexports/member_assets_index.json: current-member asset disclosure index with latest summary and per-member history pointersexports/member_assets_history/<memberId>.json: lazily loaded member asset disclosure history and category subtotal seriesexports/member_sponsorship_accounts.json: official sponsorship committee registration and online donation routes without direct-deposit account detailscurated/asset_disclosures.parquet: mirrored disclosure file metadatacurated/asset_disclosure_records.parquet: parsed disclosure record summaries per named filercurated/asset_disclosure_categories.parquet: parsed category subtotal rows per disclosure recordcurated/asset_disclosure_items.parquet: parsed asset item rows with relation and detail text fields when availablemanifests/latest.json: manifest with dataset and export metadata, checksums, and current assembly information
Shared contracts for these files are defined in @lawmaker-monitor/schemas.
Install dependencies and run the shared checks:
npm install
npm run build
npm test
npm run typecheckUseful commands during development:
npm run dev:web
npm run ingest:live --workspace @lawmaker-monitor/ingest
npm run build:data --workspace @lawmaker-monitor/ingest
npm run sync:property-member-context --workspace @lawmaker-monitor/ingest
npm run test:uiUI flow coverage and screenshot outputs are documented in docs/testing/ui-test-cases.md.
Additional operational scripts:
npm run mirror:documents --workspace @lawmaker-monitor/ingest
npm run sync:property-member-context --workspace @lawmaker-monitor/ingest
npm run monitor:sources --workspace @lawmaker-monitor/ingestingest-live: runs the live ingest pipeline and uploads a raw snapshot artifactbuild-data: consumes a raw snapshot, builds normalized outputs, materializes public exports, and syncs them into the public data repositorydeploy-web: builds the frontend and deploys it to GitHub Pagesmonitor-sources: checks upstream source stability and opens an incident issue when parser assumptions failmirror-documents: mirrors public Assembly documents into the public data repositorysummarize-minutes: runs a local lightweight model after document mirroring and publishes member-level bill statement summariesmirror-property-disclosures: mirrors official property disclosure PDFs, refreshes property member context JSON caches, and publishes those raw inputs into the public data repository
Only the following Assembly ingest variables are part of the supported surface and should be documented or configured for the live ingest workflow:
ASSEMBLY_API_KEYASSEMBLY_PAGE_SIZEASSEMBLY_BILL_FEED_CONCURRENCYASSEMBLY_VOTE_DETAIL_CONCURRENCYASSEMBLY_BILL_VOTE_SUMMARY_CONCURRENCYASSEMBLY_CONNECT_TIMEOUT_MSASSEMBLY_FETCH_TIMEOUT_MSASSEMBLY_FETCH_RETRIES
ASSEMBLY_API_KEY is required for production-like ingest runs. The remaining values tune page size, concurrency, timeout, and retry behavior for official API collection.
ASSEMBLY_CONNECT_TIMEOUT_MS controls connection establishment to
open.assembly.go.kr (default 15000 ms), separately from the full request/body
deadline ASSEMBLY_FETCH_TIMEOUT_MS (default 20000 ms). Keep the full request
deadline greater than the connection deadline. ASSEMBLY_FETCH_RETRIES=0 disables
retries; transient transport errors and HTTP 408/429/500/502/503/504 are retried
with bounded exponential backoff and jitter, while authentication and validation
errors fail immediately.
The property mirror's file-list POST uses MIRROR_TIMEOUT_MS and
MIRROR_FETCH_RETRIES, not the ASSEMBLY_FETCH_* settings. Its workflow
defaults are 45000 ms and 3 retries (at most 4 attempts). Increasing a timeout is
not a substitute for diagnosing persistent source or network failures.
Ingest, build and property-mirror workflows retain a separate diagnostic artifact on failure. It records this attempt's timestamps, outcome, run ID and sanitized error. It is not a successful source snapshot; failed collection never advances published mirror state, and incomplete raw/build outputs are not promoted.
The project also uses repository or local settings for data publication and document mirroring.
Data publication settings:
DATA_REPODATA_REPO_BRANCHDATA_REPO_BASE_URLDATA_REPO_PATCOLLECT_SPONSORSHIP_ACCOUNTSSPONSORSHIP_FETCH_TIMEOUT_MSSPONSORSHIP_FETCH_CONCURRENCY
Web analytics settings:
GA_MEASUREMENT_ID: GA4 web stream measurement ID configured as a GitHub Actions repository variable. The deploy workflow exposes it to Vite asVITE_GA_MEASUREMENT_ID.
The web app does not load Google Analytics when the measurement ID is absent or
invalid. When enabled, it records one page view for the initial screen and each
distinct hash route, retains public member and region identifiers while dropping
other hash parameters from analytics URLs, disables advertising signals, and
defaults all Consent Mode v2 storage settings to denied.
Public document mirror settings:
MIRROR_MODEMIRROR_SOURCE_IDMIRROR_START_URLMIRROR_READY_SELECTORMIRROR_ROW_SELECTORMIRROR_TITLE_SELECTORMIRROR_LINK_SELECTORMIRROR_LINK_ATTRIBUTEMIRROR_DATE_SELECTORMIRROR_NEXT_SELECTORMIRROR_MAX_PAGESMIRROR_MAX_DOWNLOADSMIRROR_PAGE_DELAY_MSMIRROR_TIMEOUT_MSMIRROR_FETCH_RETRIESMIRROR_TIME_ZONEMIRROR_RECENT_DAYSMIRROR_MIN_RECENT_DAYSMIRROR_BACKFILL_START_DATEMIRROR_BACKFILL_DAYSMIRROR_BACKFILL_WINDOWS_PER_RUNMIRROR_CATCHUP_WINDOWS_PER_RUNMIRROR_INCLUDE_APPENDICES
The scheduled minutes mirror always applies MIRROR_MIN_RECENT_DAYS as a
freshness floor, even when MIRROR_RECENT_DAYS is configured to a smaller
value. This rolling overlap captures minutes that are published after their
meeting date. Historical coverage advances independently from
MIRROR_BACKFILL_START_DATE; successful runs dispatch bounded catch-up runs
without repeating the recent window until the backfill reaches current
coverage. Mirror state reports the latest discovered and mirrored meeting
dates, whether backfill remains, and transcript parser refresh counts.
Minutes summary settings:
LLAMA_CPP_RELEASEMINUTES_SUMMARY_MODELMINUTES_SUMMARY_CHAT_TEMPLATE_URLMINUTES_SUMMARY_MAX_DOCUMENTSMINUTES_SUMMARY_MAX_GROUPSMINUTES_SUMMARY_CONCURRENCYMINUTES_SUMMARY_REQUEST_TIMEOUT_MS
The summary workflow runs an open-weight GGUF model directly on the GitHub
Actions runner. The default is the official EXAONE 4.0 1.2B Q8 GGUF with its
pinned chat template. The model ranks important source sentences, but published
summary text is copied verbatim from the collected minutes. Generated guidance
is rejected when it introduces unsupported names, institutions, numbers, or
sentences with insufficient source overlap. Long agenda-level statement groups
are also segmented by size and intervening speaker turns before ranking. Legacy
abstractive artifacts are excluded by the active prompt version and remain
hidden until regenerated. It does not send minutes to a hosted inference API.
The Assembly search endpoint is used only as a document catalog to discover
minutes IDs and dates. Every summary input must come from a separately
downloaded official minutes viewer document at
record.assembly.go.kr/assembly/viewer/minutes/xml.do. The pipeline splits
those documents into member statements before the model is invoked. Speaker and
agenda attribution come from that structured minutes document.
Collection and summarization are intentionally incremental. A mirror run downloads at most 20 documents and advances at most two catch-up windows. A summary run processes one transcript and up to eight member-agenda groups with two concurrent local-model requests. Each model request times out after 60 seconds, completed work is committed after every run, and the next scheduled run continues remaining work. Successful runs do not recursively dispatch more summaries.
The daily pipeline is fully chained in this order:
mirror-documentssearches the rolling recent window and writes official transcript artifacts.- A successful mirror explicitly dispatches
summarize-minutes. summarize-minutespublishes one checkpointed batch. Unattempted work gets priority over earlier attempts; transient failures receive exponential cooldowns, and source groups without a publishable sentence are explicitly recorded as skipped. Partial progress is committed before reporting any transient failure.- A successful summary run triggers
deploy-web.
Recent-window collection, historical backfill, summary processing, and the
other data builders share a FIFO concurrency queue. The queue retains up to 100
pending runs so a newly dispatched summary batch cannot displace the daily
recent-window collection. All generated-data writers publish through
scripts/publish-data-repo.sh, which stages only the declared paths and retries
fetch, rebase, and push operations. Failed mirror or summary batches
automatically dispatch up to three recovery runs. Manual cancellation is not
retried.
Use
docs/operations/minutes-reclassification-verification.md
to verify workflow completion, published coverage, source grounding, regression
samples, and the final production deployment after a reclassification run.
Property disclosure mirror settings use the same document mirror infrastructure plus Assembly file-service specific inputs:
MIRROR_SERVICE_INF_IDMIRROR_SERVICE_INF_SEQMIRROR_INDEX_PATHMIRROR_STATE_PATHPROPERTY_SOURCE_IDPROPERTY_MEMBER_CONTEXT_MANIFEST_PATHPROPERTY_MEMBER_INFO_PATHPROPERTY_MEMBER_HISTORY_PATH
See .env.example for a concrete local configuration template.
Lawmaker Monitor is built around official Assembly sources for vote, meeting, member, committee, and related plenary data. The asset disclosure pipeline also stays on official Assembly surfaces: the property PDF list comes from the public file-service JSON endpoint, and property member mapping comes from cached official JSON OpenAPI responses for current member roster and member history. Sponsorship collection matches the current member roster against the National Election Commission Political Donation Center and publishes only official committee registration and online donation routes. Direct-deposit bank, account-number, and account-holder details are neither collected for the public export nor rendered in the product or external share cards. The ingest pipeline, source monitoring checks, and public export contracts are all designed around keeping that official-source path stable over time.
docs/references/assembly-openapi-reference.md: concise human-readable notes about the Assembly OpenAPI surface used by this projectdocs/references/assembly-openapi-endpoints.json: machine-readable endpoint registry for prompts, tooling, and automation
Lawmaker Monitor depends on open-source tools and libraries across the web application, geospatial visualization, data ingestion, and test pipeline.
- UI and visualization: React, Recharts, and Phosphor Icons
- Social preview rendering: resvg-js and Noto Sans KR via Expo Google Fonts
- Geospatial processing: H3, TopoJSON, and Proj4js
- Data ingestion: PDF.js, Playwright, Cheerio, and fast-xml-parser
- Contracts and tooling: Zod, Vite, Vitest, TypeScript, ESLint, and Prettier
See NOTICE for the dependency license index, required upstream attributions, and the current project-license status.
build-data runs with an explicit 4096 MiB V8 old-space budget; this is not a
larger-heap workaround. Normalized/property NDJSON is written with backpressure,
with checksums computed from the exact written bytes. After DuckDB materializes
Parquet, scripts/finalize-data-manifest.mjs replaces dataset checksums with hashes
of the final published Parquet bytes. JSON export hashes are unchanged. Existing
NDJSON seed, newline, and row-count conventions are preserved.
The calendar reuses one Korean date formatter and computes each roll-call date once, rather than allocating ICU formatters for every member/roll-call pair. Geography writes one province's boundary and hexmap artifacts at a time, retaining only index metadata after each province. The full source GeoJSON is still parsed once; the build does not claim memory independent of the source dataset size.
Property PDFs are parsed one document at a time. Each page is cleaned after text extraction and each loading task is destroyed even on errors. Raw ingest payloads are not passed into the publish stage.
bash scripts/run-data-build.sh retains code/data SHA provenance, the source ingest
run ID, process exit code and peak RSS (/usr/bin/time on Linux and Python 3 standard
library child resource usage on macOS). build-memory.ndjson contains
synchronous stage checkpoints (heapUsed, rss, external/ArrayBuffer memory and
row/byte counts). Its sampled peaks are not continuous heap profiling. No heap
snapshot, environment dump, source text or API key is included. An unfinished
running diagnostic plus a non-zero process exit is a failure, not a success.
BUILD_DIAGNOSTICS_DIR optionally redirects the diagnostic files. Publication and
deployment still require the build to succeed.
Scheduled monitor-sources validates the latest successful live ingest artifact,
checks that it and the published manifest are at most 96 hours old, probes the
live official member API, and checks mirror/summary failure counters. Local runs
without SOURCE_MONITOR_LIVE=true remain fixture regression checks and report that
mode explicitly. The monitor workflow opens or updates an incident on failure.
Minutes collection overlaps the preceding 30 days and resumes historical gaps
from the last mirrored meeting date when the old same-day mode cleared the cursor.
A seven-day revalidation interval prevents unchanged documents from consuming every
bounded batch. PROPERTY_MIRROR_TIMEOUT_MS controls the property workflow timeout
(default 60000 ms), independently of legacy MIRROR_TIMEOUT_MS repository values.
Playwright request timeouts are retried with the same bounded transient-error policy.
PR checks now run the responsive browser suite and retain screenshot evidence.