← Work

001 · 2026

The Peruvian legal corpus, as git history

Peru publishes its laws, but not as data you can search, diff, or trust the history of. The engine behind legalize.pe turns the whole corpus into Markdown and version-controls it with git, every reform a commit dated to the day the norm was published. I built the read side: the web app people actually use, and the API that serves the corpus and its history.

Role
Design engineer · web app, public API, git-history reader. With Railly Hugo on the scraping engine
Status
Open source · live · pre-1.0
TypeScriptBunAstroTurborepoMCPcivic tech
The Peruvian legal corpus, as git history

What the corpus is

Two repos, on purpose. The corpus is plain Markdown plus git history, so `git log` on the 1993 Constitution replays its full 32-commit amendment history and anyone can clone the raw data and build on it. The engine is the monorepo that produces and serves it: scraper, CLI, web app, API and an MCP server so AI clients can query the law directly. It holds 21,244 norms today, 11,045 national and 10,199 regional across 26 jurisdictions.

How it's built

What I built

The web app and the API, not the scraper. On the front: a bilingual es/en interface with a language toggle that does not flash on load, a command palette with a platform-aware shortcut hint, and an accessibility and craft pass over the whole UI. On the back: the endpoints that read the corpus history, plus search and generated OG images.

Decision

The wall was serverless, not scraping

The history API worked locally for a boring reason: there was a clone of the corpus on disk and git plumbing to read it with. On Vercel there is no clone. So reading history became one factory behind a single shape, with two backends underneath: local git through simple-git when a repo path exists, the GitHub API when running on Vercel. Pulling the diff parser out of both meant the code that turns a commit into hunks and lines got tested once instead of twice. Around it, three smaller versions of the same lesson: the search index had to be bundled into the function instead of read off disk, GET params had to come from request.url, and the API routes had to be excluded from ISR before they would stop crashing.

Honest scope

This is a collaboration and most of the engine is not mine. Railly Hugo wrote the scraper, the corpus pipeline and the data model, and has roughly twice my commits on it. What I owned is the read side: the web app, the API, and a hardening pass that closed seven open issues at once. The corpus is live and open source, and still pre-1.0, so the honest status is a growing corpus, expect breaking changes, not a finished product.