Chunking contracts: the clause is the unit
Someone asks what the notice period is for termination and gets clause 11.2, which says thirty days. The answer is wrong, because clause 11.4 says the period is ninety days where the terminating party is the customer, and clause 2 defines “Business Day” in a way that changes what thirty means.
Contracts are the one document type that arrives pre-chunked. They also punish chunking harder than any other, because a clause’s meaning is distributed across the document by design.
What contracts give you for free
A numbering hierarchy that is the structure. 11, 11.2, 11.2(a), 11.2(a)(i). Somebody chose
those boundaries deliberately, they are machine-detectable with a regex, and each one delimits exactly
one obligation. No other document type hands you this.
Stable identifiers. “Clause 11.2” is a citation that a lawyer will recognise and can verify. If your chunk IDs encode the clause number, your citations become checkable by a human, which in this domain is the difference between a usable system and a liability.
A definitions section. Usually clause 1 or 2, usually exhaustive, and formatted consistently enough to parse into a term/definition map.
So the split itself is mostly solved. The work is everything that has to travel with each clause.
The split, shown
=== SIZE-DRIVEN CUT ===
--- chunk 1 ---
11.2 Either party may terminate this Agreement for
convenience on not less than thirty (30) days' written
notice to the other party, such notice to be given in
accordance with clause 19.
11.3 Either party may terminate this Agreement immediately
on written notice if the other party commits a material
breach which it fails to remedy within fifteen (15)
--- chunk 2 ---
Business Days of notice of the breach.
11.4 Notwithstanding clause 11.2, where the terminating
party is the Customer and the Agreement is within its
Initial Term, the notice period shall be ninety (90) days.
Chunk 1 ends mid-clause. Chunk 2 begins with the tail of 11.3, so a query about material breach retrieves a fragment whose subject is in the previous chunk. And 11.2 is retrievable on its own while 11.4 — the clause that overrides it — is not attached to it in any way.
=== CLAUSE-LEVEL, WITH DEFINITIONS AND CONTEXT ===
--- chunk 11.2 {doc: MSA-2026-014, clause: 11.2,
parent: "11 Termination",
refs_out: [19], referenced_by: [11.4]} ---
[Master Services Agreement MSA-2026-014
→ 11 Termination → 11.2 Termination for convenience]
11.2 Either party may terminate this Agreement for
convenience on not less than thirty (30) days' written
notice to the other party, such notice to be given in
accordance with clause 19.
Defined terms used: "Agreement" (cl. 1.1), "party" (cl. 1.4).
Note: clause 11.4 modifies this clause.
--- chunk 11.4 {doc: MSA-2026-014, clause: 11.4,
parent: "11 Termination",
modifies: [11.2]} ---
[Master Services Agreement MSA-2026-014
→ 11 Termination → 11.4 Customer termination in Initial Term]
11.4 Notwithstanding clause 11.2, where the terminating
party is the Customer and the Agreement is within its
Initial Term, the notice period shall be ninety (90) days.
Defined terms used: "Customer" (cl. 1.2), "Initial Term"
(cl. 1.3).
The clause text is untouched — that matters here more than anywhere else — and everything added is a labelled annotation around it. Chunk 11.2 now says out loud that something modifies it, which is the one fact that would have prevented the wrong answer.
The procedure
1. Parse the numbering into a tree. Match the clause-number patterns your corpus uses, build the hierarchy, and record each clause’s parent and its heading. Contracts from different sources use different conventions — decimal, roman, lettered, sometimes all three at different depths — so this is per-template work rather than one regex.
2. Chunk at the deepest numbered level that is a complete obligation. Usually the second level
(11.2), sometimes the third where subclauses are substantive. Lettered sub-items that read as a list
of conditions belong with their parent clause, not as separate chunks; splitting them is the list
failure in legal clothing.
3. Carry the full clause path. Document title, contract identifier, parties, part, clause number and clause heading. In this corpus the surrounding fields are load bearing rather than nice to have: which contract a clause is from is the first thing anyone asks.
4. Attach the definitions actually used. Build the term map from the definitions section, then for each clause, find which defined terms appear in it and attach those definitions — or at least their clause numbers. Do not attach the whole definitions section to every chunk; it is long and it will dominate the vector. Attaching the two or three terms a clause uses is proportionate.
5. Extract the cross-reference graph. Every “clause 19,” “Schedule 2,” “notwithstanding clause 11.2” is an edge. Store the outbound references on each chunk and the inbound ones too. You are not resolving them — you are recording that they exist, so a chunk can declare that it is qualified elsewhere.
6. Index the schedules and annexes separately, and say what they are. A schedule is often where the commercial substance lives, it is frequently a table, and it is usually appended after the signature block, so a naive splitter treats it as a trailing fragment.
7. Never rewrite the operative text. Expand acronyms, resolve pronouns and add context in annotations around the clause, never in place of it. A paraphrased clause presented as a quotation is not a retrieval problem, it is a professional one. Store the verbatim text as the field you display and cite.
What still breaks
Cross-reference chains. Clause 11.2 refers to 19, which refers to Schedule 2, which is qualified by 11.4. The full answer requires four chunks. Recording the edges makes that visible; it does not make one chunk sufficient, and no split does.
“Notwithstanding” and “subject to.” A clause can be silently overridden from anywhere in the document. You can catch the ones that name a clause number. You cannot catch a general override in a later section that does not reference the earlier one.
Amendments and side letters. The operative document is the contract plus three amendments, and the
current text of clause 11.2 exists in the third of them. This is a corpus-versioning problem that will
present itself as a chunking problem, and the only honest handling is to keep the amendment’s
effective_date and its target clause number on the chunk and expect a human to reconcile.
Definitions that depend on definitions. “Initial Term” is defined using “Commencement Date,” which is defined using “Effective Date.” Attaching one level of definitions leaves the reader one level short.
Tables in schedules, especially pricing tables with stacked headers and footnotes. Those go to table handling and are the part of a contract corpus most likely to retrieve badly.
Documents that are not really contracts. Policies, terms of service and standard-form documents often use heading text rather than clause numbers, which puts them back with ordinary structural splitting.
How to tell if it worked
Check the numbering first, mechanically. For each document, list the clause numbers you extracted and
look for gaps and duplicates. A missing 7.3 means the parser failed on a clause, and the clause it
failed on is not in your index at all — which is a silent omission rather than a bad answer, and the
worst outcome available in a legal corpus.
Then take a handful of clauses you know are qualified elsewhere and check that each one’s annotation says so. If clause 11.2 comes back without any indication that 11.4 exists, the cross-reference graph is not doing its job, and the system will keep producing answers that are quotable, accurate and wrong.