previews / firm-migrations / LEAP boundary mapping

Platform · Firm migrations

Mapping the LEAP exports into the import boundary schemas

The boundary schemas met their second source. Two LEAP report exports become 192,492 canonical rows across all 17 staged files, and the v2 transform's verifier passes unchanged. What blocks a real run is three specific gaps in the exports, not the mapping.

Status: MVP, working end to end
What this page is for

This is the LEAP counterpart to the v2 boundary mapping: for each boundary schema, which LEAP export columns feed it, how their meaning translates, and what had to be adjusted or left behind. Use it to review whether the mapping decisions are right; it takes the boundary schemas as they stand. One difference in kind from the v2 page: that run used the dev database, this one used Woodstock's production exports, so no data appears here at all. Every example is masked (letters → A, digits → 9) and the staged files live outside every repository.

TL;DR. From two LEAP report exports, the mapping produced 192,492 canonical rows across all 17 staged files, each row valid against its boundary schema, the whole set passing the v2 transform's verify.mjs unchanged: sorted, referentially closed, and the loader walk consumes every row. That is the boundary catalogue's source-agnostic claim demonstrated on a second source. 100% of source rows are resolved, mapped or excluded with a written reason. Three gaps in the exports keep this a rehearsal artifact: the two files share no join key (client links reach 65% of matters, by name matching), the export has no completion dates (16,536 completed matters carry a fabricated one), and contacts have no stable id (references break on the next re-export). All three have the same fix: a better export, asked of LEAP, not code.

Where the exports come from

The team follows Clio's LEAP migration recipe for extraction: six report exports run from inside LEAP by a firm admin. The two staged here are the first two, run by George on 2026-08-14:

Clio recipe stepLEAP reportStatus
MattersMatter List, all statuses, archived included, as Excelstaged here
ContactsClient Detail, as Excelstaged here
Unbilled time entriesTime and Feespending, money slice: no boundary target yet
Unbilled expensesAged Expensespending, same
Outstanding balancesaged debtors / matter balancespending, same
Trust balancestrust trial balancepending, same

The four pending exports are all money-side, which the boundary catalogue defers deliberately. They should still be exported and archived while access is easy; they become reconciliation baselines, and the balances are snapshots at export time, not ledgers.

Two things the recipe does not do, so they stay open regardless of it: it never links contacts to matters, and it has no documents step.

What differs from the v2 run

The v2 mapping read a database dump: real primary keys, foreign keys, timestamps, soft-delete flags. These LEAP files are display-layer reports: no ids of any kind (not one GUID in either file), dates as dd/mm/yyyy strings, one of them a formatted report with a title block, 96 staff group-subheader rows interleaved with the data, and a totals footer. The transform's first stage exists purely to undo that formatting. Everything downstream is the same machinery as v2: the same boundary schemas, the same sort rules, the same verifier, byte-identical output on every run.

What one run produced

The run covers Woodstock's full book: 27,198 matters against 27,005 in the warehouse, statuses including Archived and Complete, instruction dates 2014 to the export day.

Staged fileRowsFed by
matter.jsonl27,198Matter List rows
matter_participant.jsonl62,295Staff Resp. / Staff Act. / Staff Assist. columns
identity.jsonl302distinct staff initials across the four staff columns, as stubs
contact.jsonl26,027Client Details rows
contact_method.jsonl38,606First Email Address, Mobile, Phone
contact_address.jsonl20,290the postal columns
matter_contact.jsonl17,774Matter List Client joined to Client Details Client Name by normalised name
the other 10 files0no such entities in these exports; emitted empty, matching the v2 convention

The verifier reports all 27,198 matters as having no client-side participant. That is expected here, more so than in v2: LEAP clients are not platform users, so they land as matter contacts, not participants.

The mapping, source by source

External refs are the source's own identifier where one exists. LEAP's reports carry none for contacts, which is the root of two of the three gaps below.

Matter List → matter

Boundary columnSourceNotes
external_ref, human_readable_idMatter Noverbatim; unique across all 27,198 rows. Imported matters keep the firm's own reference, per the human-readable ids design
titleMatter Descriptionverbatim; free text, PII-dense
matter_statusStatus58 LEAP statuses collapse to v3's four; the draft table is below and needs firm sign-off
created_at, last_activity_atInstruction Datedd/mm/yyyy string → ISO UTC midnight; the export carries no activity timestamps
completed_atnone existsset to the instruction date when status collapses to COMPLETED, because the boundary invariant requires a value. Wrong by construction, 16,536 rows; see decisions
market, primary_jurisdiction_id, external_sourceconstantsGB, gb-england-wales, leap

Matter No comes in three formats: 69% modern 99/99999, 30% 99/9999/AA where the suffix is staff initials (verified against the staff columns on the same rows, correcting an earlier belief that these were client surnames), and 191 legacy AAA999/999 references whose prefix genuinely is a client-surname fragment.

Matter List staff columns → identity + matter_participant

The export identifies staff only by initials. One identity stub is staged per distinct value (302), with external_ref staff:<initials> and the initials as the name; real names join when the firm's staff directory arrives. Participants: Staff Resp. becomes the lead lawyer, Staff Act. and Staff Assist. become non-lead lawyers, duplicates on the same row deduplicated, the placeholder "Unassigned" value skipped. The Credit column is the introducing fee earner, which has no boundary shape: no participant row is emitted (27,009 counted), though the identity stub survives.

Client Details → contact, contact_method, contact_address

Boundary columnSourceNotes
contact.external_refnone existscd:<row index>: deterministic within this file, broken by any re-export. See decisions
title, first_name, last_nameClient Nameleading title token split off; last token as surname; single-token names keep first_name only (86)
contact.notesShort Name, Date of Birthcarried as text lines; no boundary columns exist for them. DOB kept only when 1900 ≤ dob ≤ today (6 typos dropped, one dated 9580)
contact_method EMAILFirst Email Addressmust be email-shaped and ≤ 128 chars; 408 dropped with reason; primary
contact_method PHONEMobile, Phonekept when ≤ 30 chars; 4 unambiguous two-number cells split as fix-ups, 31 junk cells dropped; Mobile beats Phone for primary
contact_addressthe postal columns- cells are empty by LEAP convention; emitted only with both lines and city (699 dropped for missing city, 20 for postcode-only); Country defaults to United Kingdom

Every contact is staged as an INDIVIDUAL because the export has no individual-versus-company discriminator. Clio's recipe says to select all card types when exporting; whether this file includes companies needs George's confirmation.

The join → matter_contact

The two files share no key, so the only bridge is the client's name, normalised (lowercase, alphanumeric only). A link is emitted only when the normalised name maps to exactly one contact row:

OutcomeMattersMeaning
matched17,774unique name hit; matter_contact emitted, role CLIENT
ambiguous2,265name maps to several contact rows; no link emitted
unmatched-multiparty6,617the client cell concatenates several parties (up to 301 chars); no splitting attempted in v1
unmatched455no name hit
empty client87the matter has no client cell at all

65% of matters get a client link. The remaining 35% is not a mapping problem to solve with cleverer fuzzy matching; it is the missing-client-id problem wearing a costume.

The status collapse, draft

The collapse answers one narrow question: which v3 lifecycle state a matter is in. It deliberately does not try to carry what LEAP's statuses mostly are, which is practice-area progress ("Sent To Land Registry" is a conveyancing milestone, not a lifecycle state). The proposal is to keep both meanings:

  • matter_status collapses to v3's four lifecycle states, per the table below.
  • The original LEAP status is preserved on each matter as a firm-scoped custom field (working name "Internal Status"; the business picks the real one). v3 already has the machinery (firm-scoped field definitions, applied at matter creation), and the boundary catalogue deferred custom fields "until the first source where they carry the data". LEAP is that source, so this asks for that schema now. Matter Type (97 values, currently dropped) is the same shape and should ride along.

One field or one per practice area? The data answers this. Cross-tabbing Status against Matter Type across the full book: the 8 lifecycle statuses (In Progress, Archived, Complete, Not Proceeding and variants) span every practice area and cover 95% of matters, while the 50 milestone statuses are practice-area vocabulary in practice, with 39 of 50 appearing in exactly one area family (Contract Pack Sent, Exchanged and Searches Complete are purely conveyancing; Trial, Letter Before Action Sent and Judgement Date purely litigation). The vocabularies never collide, so a single "Internal Status" field is the proposed import shape; whether v3 later offers per-matter-type dropdown vocabularies is a product choice this import does not preempt. Educated guess from the export's own co-occurrence; the business validates both the field name and the single-field choice. If API access lands, LEAP's schema service (per-matter-type field definitions and critical-date schemes) answers the fields-to-practice-areas question authoritatively.

Preserving the label also lowers the stakes of every contentious row below: a matter collapsed to the wrong lifecycle state still shows its true LEAP status. Until the custom-field schema exists the transform cannot stage the labels, but nothing is lost: the transform is deterministic, so one re-run stages them the day the schema lands.

Collapse rule: only unambiguously terminal states close a matter; Not Proceeding cancels; every workflow milestone stays ACTIVE, so nothing closes without the firm's say-so. LEAP's ?? (X) display variants collapse like their base status.

Totals: COMPLETED 16,536 · ACTIVE 9,864 · CANCELLED 798 (OPENING unused).

The rows worth arguing about, all kept ACTIVE in the draft: the Registered family (registration finished, arguably COMPLETED), Will Executed (the deliverable exists), and Limitation Period Expired / Mortgage Expired (arguably dead, so CANCELLED). The firm decides; the draft only proposes.

The full 58-row collapse table
LEAP status→ v3rows
ArchivedCOMPLETED12,976
In ProgressACTIVE8,463
Completion FinalisedCOMPLETED1,530
?? (Completion Finalised)COMPLETED1,434
Not ProceedingCANCELLED798
CompleteCOMPLETED572
Sent To Land RegistryACTIVE512
Contract Pack SentACTIVE135
RegisteredACTIVE128
ExchangedACTIVE74
Lender Notified Of RegistrationACTIVE69
Client Notified Of RegistrationACTIVE66
Priority Period ExpiredACTIVE63
Contract Pack ReceivedACTIVE53
Searches CompleteACTIVE31
Letter Before Action SentACTIVE25
Offer AcceptedACTIVE24
Pre-Contract Enquiries CompleteACTIVE24
Judgement DateACTIVE23
Redemption Statement ReceivedACTIVE19
TrialACTIVE18
?? (Complete)COMPLETED17
Stamp Duty PaidACTIVE17
Registered Title ReceivedACTIVE15
Claim IssuedACTIVE11
Property Information CompleteACTIVE10
Terms Of Business ReceivedACTIVE10
Judgement GivenACTIVE9
SettledCOMPLETED7
?? (Exchanged)ACTIVE6
HearingACTIVE6
?? (Registered)ACTIVE5
Claim ServedACTIVE4
Mortgage ApprovedACTIVE4
Mortgage ExpiredACTIVE4
Pre-Commencement ProceedingsACTIVE4
?? (Limitation Period Expired)ACTIVE3
?? (Stamp Duty Paid)ACTIVE3
Limitation Period ExpiredACTIVE3
?? (Contract Pack Sent)ACTIVE2
Commencement Of ClaimACTIVE2
Fdr HearingACTIVE2
Memorandum Of Appearance LodgedACTIVE2
?? (Evidence Filed)ACTIVE1
?? (Limitation Date Reached)ACTIVE1
?? (Memorandum Of Appearance Lodged)ACTIVE1
Bail BackACTIVE1
Conditional Order GrantedACTIVE1
Defence FiledACTIVE1
Hearing DateACTIVE1
Lease AgreedACTIVE1
Original Order DateACTIVE1
Pre-Action Protocols CompleteACTIVE1
Pre-Action Protocols CompletedACTIVE1
Response FiledACTIVE1
Service AcknowledgedACTIVE1
Transfer DateACTIVE1
Will ExecutedACTIVE1

Exclusions and fix-ups, the honest ledger

Every dropped value is counted with a reason; nothing is silently skipped. The largest classes:

SourceReasonCount
Matter Listreport furniture: preamble, 96 staff group-subheaders, totals footer102 rows
staff columnssame initials already on the row, deduplicated17,383
Credit columnintroducer has no boundary shape; identity stub kept27,009
Staff Assist. / Credit"Unassigned" placeholder2,105
client emailsnot email-shaped408
client addressesmissing city or lines, required at the boundary719
phone cellsjunk (multiple numbers jammed together, up to 231 chars)31
dates of birthoutside 1900 to today6

Fix-ups: completed_at set to instruction date (16,536), single-token names kept as first name only (86), unambiguous two-number phone cells split (4).

In coverage-loop terms: 100% of both files' source rows are resolved: 27,198 of 27,300 physical Matter List rows mapped and 102 excluded as report furniture; 26,027 of 26,028 Client Details rows mapped and 1 header excluded. This is the upstream number for these two exports; what LEAP holds beyond them (documents, money, key dates) is a different denominator that grows as more exports arrive.

Decisions needing sign-off

  1. The status collapse table, above, together with the custom-field approach that preserves the LEAP label. The contentious rows are listed; preserving the label makes them cheap to get slightly wrong and cheap to revisit.
  2. Contact references are unstable. cd:<row index> breaks on any re-export. The fix is an export carrying LEAP's internal client id; until then, no real import run should use these contacts.
  3. completed_at is fabricated for 16,536 matters. The export has no completion date and the boundary requires one for COMPLETED matters. Needs either the firm's acceptance or, better, an export with completion dates.
  4. All contacts staged as individuals. No company discriminator exists in the export.
  5. Identity stubs carry initials as names. The identity contract has no free-text field, so staff: in the external ref is the only stub marker. The firm's staff directory turns these into people.
  6. The introducer relation is dropped. Credit initials keep their identity stub; the relation itself has no boundary shape and is counted, not staged.
  7. Custom-field candidates, pending the schema: the original LEAP status label and Matter Type (97 values) are not staged yet but are earmarked for the custom-field boundary schema this page asks for; a deterministic re-run stages them once it exists. Dropped outright: marketing consent (99.8% "No"), Home/Work/Fax phones, PO Box and DX columns. DOB and Short Name survive only inside contact.notes.

What to ask LEAP for next

All three gaps have the same shape: they are export problems, not mapping problems.

  1. An export carrying LEAP's internal client id (or a matters-per-client report). Fixes the unstable contact refs and replaces name-matching for the 35% of matters without a client link.
  2. Completion dates, so 16,536 matters stop carrying a fabricated one.
  3. Confirmation of card types in the Client Detail export (people only, or companies too), and the staff directory to give the 302 identity stubs real names.

And one ask that is ours, not LEAP's: a matter custom-field boundary schema in the catalogue, so the LEAP status labels and Matter Type can be staged. The catalogue deferred custom fields until a source needed them; that source has arrived.

Files and how to run it

The transform lives in this doc's folder, in the style of the v2 transform: transform/parse.py (stage 1: undo the report formatting) and transform/transform-leap.mjs (stage 2: mapping, validation and sorted writes, importing boundary-schemas.mjs from the v2 transform so there is exactly one copy of the contracts). Same inputs, byte-identical outputs, proven by manifest comparison across independent runs.

uv run --with openpyxl python transform/parse.py <dir-with-xlsx> <intermediate-dir>
node transform/transform-leap.mjs <intermediate-dir> <out-dir>
node ../v2-boundary-mapping-2026-08-12/transform/verify.mjs <out-dir>

The xlsx inputs and every output file contain real client data and live only outside the repositories. This page and the transform code carry none of it.