Skip to main content

Codify

CI PyPI Python License Akoma Ntoso

Codify turns a statute book into law a machine can read: each act structured as Akoma Ntoso 3.0, the open standard for legal documents, addressable by FRBR URI and citable to the provision, with quoted amending text lifted out of the prose into markup a machine can address.

This repository provides all the components of the core pipeline: acquisition, transcription, anchor-driven structuring, retrieval and comparison. Documents converted from scans include coverage metrics and validation reports, while pre-existing Akoma Ntoso documents skip the scan passes and are normalised (identifiers, unique eIds) and validated.

The code is Apache 2.0 and fully standalone. While TBI offers commercial services built on top of it, this core pipeline requires no external proprietary services and can be run independently.

This package contains the domain logic for manipulating legal text, exposed as reusable functions and classes. Application-level concerns like HTTP routing and workflow orchestration are downstream.

Quick start

You need:

  • Python 3.12 or newer and uv
  • poppler for scanned PDFs (brew install poppler / apt install poppler-utils); text input needs nothing
  • access to a chat model behind an OpenAI-compatible endpoint. The repo makes it easy to add a Gemini API key, but a LiteLLM gateway will also easily work
  • Docker, only for the database tests

To use the library from your own project, uv add codify-core (or pip install codify-core); the extras are serve, mcp and migrations. To work in this repository:

uv sync
cp .env.example .env   # add your key
uv run codify ingest-one data/fixtures/synthetic/xa/legislation-act-1992.pdf --jurisdiction xa --out bundle/

The sample command processes a five-page synthetic act from xa, a fictional test jurisdiction. It takes under a minute, costs a few cents in model API calls, and outputs locally without touching any database.

The generated bundle/ directory contains:

File Contents
pages/ Page images rendered for inspection after the run
source.txt Extracted text transcription
anchors.jsonl Structural markers detected during the scanning pass
coverage.json Provision count metrics (expected vs. captured)
scaffold.bluebell Structural skeleton generated prior to filling provision text
final.akn.xml Generated Akoma Ntoso 3.0 document
validator.json Structural validation findings
manifest.json Run metadata, including model identifiers and config hashes

Structuring follows an anchor-driven model: a deterministic skeleton is parsed using the jurisdiction's rules, and the LLM then fills the text within each anchored block.

Inspection order. Inspect anchors.jsonl before final.akn.xml. The model only populates text inside anchored basic units (e.g., sections, articles). If a pass identifies higher-level containers but no basic units, it logs body_fill_skipped and exports only the skeleton. If no anchors are found, it logs scaffold_no_anchors and preserves the source text unparsed. (Note: the bundled xa fixture places section numbers in the margin, which the parser does not currently extract; it anchors six parts but no sections.)

Dry runs. To inspect detected anchors across .txt and text-layer .pdf files without making model calls or requiring a database, run the anchor scan directly:

uv run codify scan-corpus <dir> --jurisdiction xa

The summary is JSON on stdout; diagnostics such as an empty sweep go to stderr, and --per-document writes one row per scanned document to a path of your choosing.

Log messages.

  • layout_pass_failed ... OcrNotConfigured: the optional secondary OCR engine (Azure AI Foundry) is not configured. The pipeline reads the scan once via the model's vision endpoint rather than twice, and the run otherwise proceeds.
  • page_diverted_to_ocr: the page's extracted text layer was rejected (the event's reason names the test it failed: too short, garbled, letter-spaced, presentation forms, divergent from the scan, or a visible annotation) and the page was rasterised for OCR.

Store, search, compare

The bundle is the end of ingest-one. To search it, or set it beside another version, load it into Postgres. The compose file builds one with pgvector and pg_textsearch:

docker compose up -d --wait postgres          # first build takes a minute or two
uv run alembic -c alembic.ini upgrade head
uv run codify load bundle/ --embed
uv run codify search "right of access" --jurisdiction xa

load reads final.akn.xml and manifest.json from a bundle and writes the law, its version and its provisions; the jurisdiction and the title come from the manifest, so a bundle needs no flags. A bare .akn.xml has no manifest and needs --jurisdiction and --title. --embed also embeds the provisions, which search needs. Loading the same file twice returns the version it already holds. search runs the hybrid retrieval (lexical and vector, fused) and prints one JSON match per line, best first, with the provision's eId, score and text; it exits 1 when nothing matched.

Embeddings go to an OpenAI-compatible embeddings endpoint. EMBEDDING_BASE_URL, EMBEDDING_API_KEY and EMBEDDING_MODEL name it; unset, the LITELLM_* chat settings are used, so one gateway can serve both.

uv run codify compare a.akn.xml b.akn.xml --out report.json
uv run codify compare <version-id> <version-id>

compare aligns the second document's provisions against the first, article by article, through the chat model, and writes a report with a summary (aligned, partial, gap) and every alignment. Either side is an AKN file or a stored version id. It costs a model call per provision.

POSTGRES_URL names the database. Unset, it is the compose one while ENVIRONMENT is unset, localhost or development; under any other value an unset URL is an error.

Serve

The same reads and the ingest, behind HTTP, for a script or a UI that is not on the box:

uv sync --extra serve
uv run codify serve                            # http://127.0.0.1:8000, docs at /docs
curl -F file=@act.pdf -F jurisdiction=xa localhost:8000/runs/ingest
curl -N localhost:8000/runs/<run-id>/stream    # server-sent events until the run ends

Reads: /jurisdictions, /jurisdictions/{code}, /laws, /laws/{id}, /versions/{id} (with the AKN), /search?q=&jurisdiction=. Ingest: POST /runs/ingest (a file) or POST /runs/ingest-url (a URL) return a run at once; /runs/{id} is its state, /runs/{id}/stream replays every event so far and then follows it, and /runs/{id}/cancel and /runs/{id}/retry do what they say. A succeeded ingest is stored, so the reads see it. ingest-url takes an EU publications URL (eur-lex or publications.europa.eu) under jurisdiction eu, the only lane that fetches; a PDF goes through ingest. Runs live in the server's memory: a restart forgets them, each run says so (lost_on_restart), and the 200 most recent finished runs stay readable. There is no authentication; bind it to localhost or put it behind something that has.

Contract

contract/openapi.json is the server's OpenAPI schema, generated from the routes' response models (codify/serve/schemas.py and the library models they carry, such as JurisdictionConfig); a test fails when it drifts. After a route change:

uv run codify serve --openapi > contract/openapi.json

A client generates its types from that file (the UI does, with openapi-typescript), so the schema is the one place the two agree.

MCP server

The same reads, and compare, as tools for an agent over the Model Context Protocol:

uv sync --extra mcp
uv run codify mcp                     # stdio, for a client that launches the server
uv run codify mcp --http --port 8001  # streamable HTTP at http://127.0.0.1:8001/mcp

A client that speaks stdio launches the command itself; this is the shape most take:

{
  "mcpServers": {
    "codify": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/codify", "codify", "mcp"],
      "env": { "POSTGRES_URL": "postgresql://codify:codify@localhost:5432/codify" }
    }
  }
}

The tools, all reads:

  • search_provisions(query, jurisdiction, language?, k?): hybrid search over one jurisdiction's provisions; matches best first, each with its provision id, eId, score and text. Needs the embeddings endpoint, as search does; unset, the tool says so.
  • list_laws(jurisdiction?, doctype?, year?, q?, limit?, offset?): a page of stored laws with their ids.
  • get_law(law_id): one law's fields, its jurisdiction and every stored version.
  • get_version(version_id, include_xml?): one version's metadata; with include_xml, the Akoma Ntoso XML, cut at a million characters and flagged when cut.
  • list_jurisdictions(): every configured jurisdiction's code, name and languages.
  • get_jurisdiction(code): a jurisdiction's names, tradition, calendar, languages and document classes.
  • compare_versions(reference_version_id, domestic_version_id): the compare report, each provision of the reference assessed against the domestic version. Needs the chat and embeddings endpoints and costs a model call per reference provision that carries text; refused above 200 of them.

A failure reads back as the tool's error with a plain message. --http binds to localhost unless --host says otherwise; there is no authentication here either.

Configuration

The CLI loads environment variables from a .env file in the working directory or parent directories; explicitly exported shell variables take precedence. Note that pytest and alembic do not read .env files automatically and require variables to be exported in your environment. See .env.example for all available options.

Variable Purpose
LITELLM_BASE_URL Base URL for the OpenAI-compatible chat endpoint
LITELLM_API_KEY API key for the chat endpoint
LITELLM_MODEL Default model used for body fill (overridden by --model)
POSTGRES_URL Postgres connection string for migrations and tests (defaults to local Compose service)
AZURE_OPENAI_ENDPOINT Endpoint for the optional secondary Azure AI Foundry OCR engine
AZURE_OPENAI_API_KEY API key for the optional Azure OCR engine
LANGFUSE_PUBLIC_KEY Public key for optional Langfuse tracing
LANGFUSE_SECRET_KEY Secret key for optional Langfuse tracing

Jurisdictions

Configurations are included in distributed package wheels if their config.json sets either synthetic or public_reference to true. This filtering is enforced by codify/open_wheel.py at build time to prevent the bundled set from drifting out of sync.

  • Synthetic jurisdictions: contain mock legislation formatted to real-world legislative structures, allowing test suites to assert against known-good parses without distributing copyrighted corpora.
  • Public reference configurations: cover jurisdictions that publish their legal texts openly.

A source checkout reads the repository's data/ directory, wherever the command is run from; an installed wheel reads the data bundled inside the package. To supply a custom dataset, set CODIFY_DATA_ROOT to an absolute path containing jurisdictions/ and frameworks/ directories before starting Python. Relative paths are rejected, and custom data roots completely replace bundled data.

To add a jurisdiction, see docs/jurisdictions/adding-a-jurisdiction.md.

Layout

  • codify/akn/: AKN 3.0 element models, parsing, rendering, eId generation, reference resolution, and schema validation
  • codify/pipeline/: End-to-end ingestion from raw bytes to AKN. Contains input format parsers (formats/) and structural enrichment passes (enrich/).
  • codify/acquisition/: Source adapters, scrape manifests, and rate limiting for jurisdiction data sources.
  • codify/embed/: Provider-agnostic text embedding client over OpenAI-compatible endpoints.
  • codify/retrieve/: Hybrid retrieval over statutory provisions using dense embeddings and BM25 fused via Reciprocal Rank Fusion (RRF).
  • codify/compare/: Statutory compliance comparator, including alignment logic, prompt templates, and schema-validated model outputs.
  • codify/storage/: Typed PostgreSQL data access layer.
  • codify/lenses/: Analysis plugins and the extensible lens registry.
  • codify/repair/: Automated AKN repair agent performing finding-grounded, transactional XML edits.
  • codify/translate/: Structure-preserving legal translation with batching, clause parity checks, and quality scoring.
  • codify/core/: Shared LLM client, OpenTelemetry/Langfuse tracing, internationalization, and log redaction.
  • codify/frbr.py: FRBR URI generation and parsing.
  • codify/jurisdictions.py: Jurisdiction configuration loader and schema validator.

Tests

For a detailed breakdown of test scopes, see the test guide.

Unit tests do not require an API key or a database:

uv sync --group dev --extra migrations
uv run pytest tests -m "not integration and not live_llm" -q

This runs the unit suite in about a minute and matches the standard CI check. Tests for unbundled jurisdictions are skipped automatically.

Note: Integration tests require the same Postgres as Store, search, compare, with the pgvector and pg_textsearch extensions:

# Start test database and apply schema migrations
docker compose up -d --wait postgres
uv run alembic -c alembic.ini upgrade head

# Run integration suite
REQUIRE_DB=1 uv run pytest tests -m "integration and not live_llm" -q
  • Database safety: Integration tests write and drop data. Never set POSTGRES_URL to a production or shared database. If port 5432 is already bound locally, set CODIFY_PG_PORT and update POSTGRES_URL accordingly.
  • Live LLM tests: Tests marked live_llm issue requests to the configured OpenAI-compatible gateway and incur API charges.
  • CI checks: In addition to unit tests, CI enforces formatting, type checking, and wheel builds via Ruff, strict mypy, and uv build with the Hatchling backend (see .github/workflows/ci.yml).

Standards

Codify targets the Akoma Ntoso 3.0 specification. Generated documents pass a structural validator whose findings ride the bundle and the run; the OASIS schema is checked on one acquisition route only, not on every output. FRBR URIs identify works, expressions and manifestations, and eIds address the individual provision within them.

Structural validity alone does not guarantee semantic fidelity to the source text or seamless compatibility with external tooling. Internal structural conventions, such as how annex content is inlined, can diverge from specific downstream profiles like AKN4EU or platforms such as Indigo. You should validate intended interchange workflows using representative documents directly within the consuming system. For details on compatibility boundaries and known profile differences, see the interoperability scope documentation.

More

  • docs/architecture.md
  • docs/ocr-cascade.md
  • docs/decisions/: architecture decision records
  • CONTRIBUTING.md
  • CODE_OF_CONDUCT.md
  • SECURITY.md
  • AGENTS.md: conventions for coding agents working in this repository

Codify is built by CentreAI at the Tony Blair Institute for Global Change. For more information on the vision and access to a hosted version, visit codify.centreai.global.

Release files for codify-core 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for codify-core 0.4.0
File Size Uploaded
codify_core-0.4.0.tar.gz 7.8 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for codify-core 0.4.0
File Interpreter ABI Platform
codify_core-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 8.8 MB

Release files / codify_core-0.4.0.tar.gz

Download URL codify_core-0.4.0.tar.gz
Size 7.8 MB
Tags Source
SHA-256 checksum
How to use checksums
25ab31b8f1198c682f01ddea66d95e9d84a74c22cab5b3f956e092c05ebe7a68
BLAKE2b-256 checksum
How to use checksums
1a5b04b20dc189f52d522a3f5be99890fc0ee730d2bc10971082b7ff6f0a51ae
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.

Transparency log

Release files / codify_core-0.4.0-py3-none-any.whl

Download URL codify_core-0.4.0-py3-none-any.whl
Size 1.0 MB
Tags Python 3
SHA-256 checksum
How to use checksums
5dfd3a200685d9d07def930caaddfed86e35b230f4ba41a635708551c4e5bce0
BLAKE2b-256 checksum
How to use checksums
2b9ac5f38e714e09a7a4fabf87793d29fa926cd6ca5c459f58b1aac20c2acc85
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.

Transparency log

Release history Release notifications | RSS feed

0.5.0

2 release files

This release

0.4.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page