Lex Browse everything How it works For developers

How it was built

A point-in-time legal database, an MCP server, a grounded assistant and a nightly pipeline, built solo. This page is the part usually left out: the decisions, the things that broke, and how correctness is actually proven rather than asserted.

Built with AI assistance. The architecture, the decisions and the verification are mine; a great deal of the code was written with an AI pair. That is stated here because it is true, and because the parts that matter, the decision record, the failure modes below, and the tests that catch them, are where the engineering actually lives.

The problem

Ask any legal site what a law says and you get today's text. Almost every question that matters is about a date: what applied when the contract was signed, when the fine was issued, when the breach happened. Official publishers do hold dated consolidated editions , but scattered across formats (Akoma Ntoso XML, Formex XML, legacy XHTML), with no article-level access and no machine interface. Lex turns that into one queryable, verifiable history.

The shape of it

official publishers          nightly, one scheduled job
Legilux · EUR-Lex/Cellar     ──────────────────────────────────
        │                    ingest → anomaly gate → derive →
        │  verbatim bytes      (>5% drop =        determinism
        ▼  + sha256             commit nothing)     guard
  EVIDENCE REPOS ──────────────────────────────────────────►
  append-only, never rewritten            │
        │                                 ▼
        │  deterministic extraction   DERIVED REPO (per article)
        ▼  (immutable profiles)           │
  SIGNED SQLITE INDEX (ECDSA P-256) ◄─────┘
        │
        ├──► MCP server  (public, no key)      
        └──► this site   (Container Apps, scale-to-zero)

Azure: Container Apps behind a managed certificate, Container Registry, Azure OpenAI (gpt-5-mini) for the assistant, Application Insights via OpenTelemetry, Azure DNS. The web app runs at 0.25 vCPU and scales to zero, idle cost is essentially the registry and the DNS zone.

Decisions worth defending

decisionwhy, and what it cost
Store publisher bytes verbatim The evidence layer is never "cleaned". A hash over cleaned text proves nothing about what the state published. Cost: two layers to maintain instead of one.
Extraction profiles are immutable Once akn-lu/1 is published, its output for a given input can never change, a frozen-fingerprint test fails the build if it does. Improvements ship as a new profile. Cost: no silent fixes, ever.
Refusals are part of the API Seven typed refusal codes instead of empty results. A caller can distinguish "no such law", "no version that day" and "text withheld". Cost: more surface to test.
A small model, tightly fenced The assistant only picks lookups and quotes results, no agent framework, no chain of reasoning over law. Cheap, auditable, and wrong answers are visible against the evidence shown beside them.
Nightly commits nothing when unsure A >5% drop in works, or a re-derivation that is not byte-identical, aborts the run. A partial upstream response must never rewrite history.

Forty-eight numbered decisions like these are recorded in the specification, each with its rationale, so "why did you do it that way" has a written answer rather than a recollection.

The machinery that keeps it fresh

Law changes while you sleep, so the corpus is rebuilt while I do. One scheduled job at 02:17 UTC drives the whole fleet, no manual step exists, and there is deliberately only one credential and one cron for all publishers.

stagewhat it does, and how it refuses to do damage
1. IngestAsks each publisher what versions exist, downloads any it has not seen, and writes them verbatim. Existing files are never reopened for writing , the evidence layer is append-only by construction.
2. Anomaly gateIf the work count drops more than 5%, the run assumes the upstream response was partial, discards everything and commits nothing. A bad night leaves yesterday's good data in place.
3. DeriveRegenerates the per-article layer from the verbatim files.
4. Determinism guardIf derived output changed while no source file did, that means the extractor is non-deterministic, the run fails loudly and commits nothing, because a silent extraction drift would corrupt history.
5. Index & publishRebuilds the search index, signs it (ECDSA P-256), publishes it as a release asset, regenerates the JSONL and Parquet datasets.
6. ReportWrites a three-state outcome per publisher (ran_committed / ran_no_change / failed_*) and opens an issue on failure.

The result travels with the data: every index carries a signed stamp recording when it was built and from which corpus commit, and every tool response returns it. This is that stamp, read live from the running indexes:

publisherindex builtfrom corpus commitsignature
eu-eurlex 2026-08-04T13:17:09Z 6974532 valid
lu-legilux 2026-08-04T13:15:37Z 2f51cf1 valid

Nothing here is typed by hand, if the pipeline stopped, this table would say so. The same values come back from the coverage tool in every API response.

What broke, and what it taught

A silently dead search, caught in production. The nightly job built the search index without the per-article layer. Nothing errored: the index was valid, signed, and published , it just had zero provisions in it, so search returned nothing. The automated eval suite caught it by asking a question a user would ask and noticing the assistant could no longer find a Luxembourg code.

Fix: the index step now runs after derivation and takes the article layer as a required input, so the failure cannot recur. Lesson: a green build is not a working system , the only tests that would have caught this are the ones that exercise it end to end, the way someone actually uses it.

A parser that quietly duplicated text. Adding Formex XML support introduced doubled paragraphs where an article had introductory text followed by a list. It looked plausible on screen. It was caught by re-reading real output rather than trusting a passing test, fixed the same day, and pinned with a fingerprint test so the profile can never drift again.

How correctness is proven, not claimed

mechanismwhat it guarantees
34 unit testsparsers, temporal logic, index schema, signing
Frozen profile fingerprintsa published extraction can never change output
Determinism guard in CIre-derivation is byte-identical or the run commits nothing
10 end-to-end AI evalsthe assistant picks the right tools and never cites a source it was not given
LLM-judged groundednessanswers scored against the evidence actually returned
ECDSA-signed indexesanyone can verify a build was not altered, recipe

Scale

1,409 laws 4,705 dated versions 333,000+ articles indexed ~7,200 lines of C# across 10 projects 3 XML/HTML dialects parsed nightly, unattended

What I would do differently

Version the index schema migration path from day one, schema v2 required a full rebuild rather than a migration. Put the end-to-end evals in the nightly pipeline, not only in my hands; they caught the worst bug of the project and should be a gate, not a habit. And treat the derived layer's release assets as part of the deploy, not a follow-up step, the two times something shipped stale, that was why.

Who built this

Lex is built and run by Soufien Hajji, a senior .NET, Azure and AI engineer in Luxembourg. It is a personal project, unaffiliated with any publisher or public body, built the way I build professionally: the pipeline is deterministic, the claims are testable, and the parts that cannot be verified say so rather than guessing.

About, and the other two systems built the same way →

Why it is built this way → · Use it from your own code → · The data model → · Verify a build yourself → · The dataset →