bundlebuild
by kevanwee
README.md
# bundlebuild
Court bundles from a YAML index. Merge the PDFs, stamp continuous page numbers, generate a
hyperlinked index and PDF bookmarks, and pre-flight everything so the missing exhibit is
found at your desk rather than at the registry.
```yaml
title: Bundle of Documents
case: HC/OC 123/2026
parties: Alpha Pte Ltd v Beta Pte Ltd
output: out/bundle.pdf
stamp: { prefix: "AB" } # stamps AB-1, AB-2, ...
sections:
- title: Pleadings
documents:
- { tab: "1", title: Statement of Claim, date: 2026-03-02, file: docs/soc.pdf }
- { tab: "2", title: Defence, date: 2026-03-23, file: docs/defence.pdf }
- title: Correspondence
documents:
- { tab: "3", title: Letter of demand, date: 2025-11-14, file: docs/lod.pdf, pages: "1-2" }
```
```
$ bundlebuild check bundle.yaml
[WARN ] tab 3: undated; index will show 'Undated'
[WARN ] tab 4: no text layer on any page; OCR before filing
5 documents, 10 pages, 0 error(s), 2 warning(s)
$ bundlebuild build bundle.yaml
wrote out/bundle.pdf: 1 index page(s) + 10 bundle page(s), 5 tabs
index: out/bundle.index.json
```
## Why
Bundle assembly is paid-product territory (Bundledocs, Opus 2, Casedo) and the free
alternative is a paralegal with Acrobat and an afternoon. The mechanics are not hard:
`pypdf` merges and annotates, `reportlab` draws. What is missing is a tool that treats the
index as the source of truth and checks the inputs before anyone prints 800 pages.
## What it does
| Step | Detail |
|---|---|
| **Pre-flight** (`check`) | Missing files, non-PDFs, unreadable or encrypted PDFs, page selections out of range, landscape pages, pages with no text layer (scans), undated documents, tabs out of order. Errors block the build; warnings are printed. |
| **Merge** | Documents in manifest order, optional per-document page subsets (`pages: "1-3,7"`). |
| **Stamp** | Continuous page numbers with optional prefix (`AB-12`), bottom-right / bottom-centre / top-right, sized to each page. |
| **Index** | Generated front matter: court, case, parties, title, volume; then Tab / Document / Date / Page rows grouped by section. Spills across pages as needed. |
| **Hyperlinks** | Every index row is a link annotation to its first page. |
| **Bookmarks** | PDF outline: sections, then `Tab n: title` under each. |
| **Sidecars** | `<output>.index.json` (tab, title, date, page range). For authorities bundles with `citation` fields, `<output>.citecheck.json`, a manifest [citecheck](https://github.com/kevanwee/citecheck) can consume. |
## Install
```bash
pip install -e ".[dev]" # library + CLI + tests
pip install -e ".[mcp]" # adds the MCP server
```
Python 3.11+. Pure Python; no Ghostscript, no Acrobat, no Java.
## CLI
```bash
bundlebuild init [bundle.yaml] # write a template manifest
bundlebuild check bundle.yaml # pre-flight; exit 1 on errors
bundlebuild build bundle.yaml # check, then build; exit 1 on errors
bundlebuild build bundle.yaml --skip-check
```
## MCP server
```json
{ "mcpServers": { "bundlebuild": { "command": "bundlebuild-mcp" } } }
```
Tools: `manifest_template`, `check_bundle(manifest_path)`, `build_bundle(manifest_path)`.
The assistant's job is to help write the manifest from a folder of documents; the build is
deterministic and local.
## Manifest reference
| Field | Notes |
|---|---|
| `title`, `court`, `case`, `parties`, `volume` | Front matter on the index page. Only `title` is required. |
| `output` | Relative to the manifest file. |
| `start_page` | First stamped number (default 1; use e.g. 341 for volume 2). |
| `stamp.position` / `prefix` / `separator` / `font_size` / `margin_pt` | Stamp appearance. |
| `index_title` | Default `INDEX`. |
| `sections[].title` | Section heading in the index and the outline. |
| `documents[].tab` | A string label; must be unique across the bundle. |
| `documents[].title`, `date`, `description` | `date` may be a YAML date or free text ("Undated", "circa 2019"). |
| `documents[].file` | Relative to the manifest. Must be a PDF; convert first. |
| `documents[].pages` | Subset, 1-based, e.g. `"1-3,7"`. |
| `documents[].citation` | For authorities bundles; printed in the index and exported to the citecheck sidecar. |
## Conventions and limits
- The index is **not** page-numbered; bundle page 1 is the first content page. If your
practice direction requires the index within the numbering, open an issue; it is a small
change but it should be a deliberate one.
- Existing page numbers on the source documents are not removed. Stamps are placed in the
margin; if a source document already prints in that corner, choose a different
`stamp.position`.
- Rotated source pages are stamped in the page's own coordinate space, so the stamp rotates
with the page. This is the correct behaviour for landscape exhibits inserted into a
portrait bundle.
- No OCR. `check` tells you which pages lack a text layer; OCR them with whatever you use
(ocrmypdf is good) and re-run.
- No conversion from Word/email/images. Convert first, then bundle.
## Related projects
[citecheck](https://github.com/kevanwee/citecheck) consumes the authorities sidecar. [chronology](https://github.com/kevanwee/chronology)
entries cite bundle page references produced here. See also [sg-deadline](https://github.com/kevanwee/sg-deadline).
## License
MIT.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues