Custos pivots upstream to a CLM - counterparties, customer index, and document-derived membership

Six commits rewire the project model around counterparties: free-text on upload, LLM-extracted from docs, merged in the UI, and eventually derived from document-level intake rather than a field on the project itself. The shift from "project has one counterparty" to "project surfaces under every counterparty found in its documents" is the structural move worth understanding before pulling any piece of this.

contract-reviewknowledge-management

Custos starts small. Commit fa1a3bb adds template (vendor/customer/internal) and role (buyer/seller/mutual) to the projects table, with a PROJECT_TEMPLATES registry that keeps the slug→config mapping in code so product can iterate without migrations. Every later piece reads role from here.

e7a3d0c9 is the main delivery: projects.counterparty and parent_counterparty land as free-text columns, PATCH /projects/:id is added, and a GET /projects/counterparties?role= endpoint aggregates projects by counterparty for a new /customers page. The commit comment explicitly calls free-text-for-now a TODO to promote to a dedicated table later - the tech debt is acknowledged, not hidden.

5d6cc8e1 adds lib/counterpartyExtraction.ts. On document upload, it fires a small LLM call (fire-and-forget, never blocks the upload path) that returns {name, parent} using a role-hinted system prompt. It writes back only if projects.counterparty is still empty - manual values win. The same commit refactors PDF/DOCX text extraction into a shared lib/textExtraction.ts.

ce650956 adds POST /projects/counterparties/merge with a per-user-scoped, case-insensitive match: if your index shows both "Acme Inc." and "Acme, Inc." you can merge them into one canonical name. No global dedup, no canonicalization across users.

11895ef6 adds a per-counterparty timeline at /customers/[name] that renders all that party's projects, documents, and contract_facts rows chronologically. Useful as a relationship view - when did you last execute something with this entity, how many contracts, what terms.

The architecturally significant change is dd8e28e2. It stops treating projects.counterparty as the membership criterion and instead derives counterparty membership from the documents in the project - specifically their intake_counterparty column. A project called "Customer Contracts" containing Adobe, Stripe, and Intercom documents now surfaces under all three counterparties in the index. projects.counterparty becomes a "primary hint." This commit has a hard dependency on intake_counterparty existing in documents, which is the intake-triage track (dd8e28e2 is meaningless without it).

So what Worth pulling as a unit if you're building CLM features on top of the base mike fork. The arc - free-text first, auto-extract second, document-derived membership third - is well-paced. The dependency graph matters: `dd8e28e2` requires the intake-triage columns (post 523 in this series). Free-text counterparty is still outstanding tech debt (no FK table, no global dedup). Skip if your use case is tabular review only.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

6 commits from Custos/legalos, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
fa1a3bbb Project templates: vendor / customer / internal classification Custos 2026-05-04 ↗ GitHub
commit body
- Add projects.template (slug) + projects.role (buyer/seller/mutual).
- New PROJECT_TEMPLATES registry in backend (vendor, customer, internal).
- GET /projects/templates returns the registry.
- POST /projects accepts template; backend resolves slug → role.
- New Project modal: type picker pills below the CM number.
- Projects overview: filter chips by template type.
e7a3d0c9 Counterparties: per-project counterparty + customer index page Custos 2026-05-04 ↗ GitHub
commit body
- projects gain counterparty + parent_counterparty (free text for now;
  will be promoted to a dedicated counterparties table in a follow-up).
- PATCH /projects/:id accepts counterparty / parent_counterparty / template.
- New GET /projects/counterparties?role= aggregates by counterparty,
  returning project counts and last-activity for the customer index.
- Frontend: new /customers route grouping customer-template projects by
  counterparty, with role tabs (Customers / Vendors / All).
- Frontend: editable counterparty + parent fields on the project page,
  template badge surfaced under the title.
- Sidebar: 'Customers' nav entry below Projects.
- .gitignore: exclude .claude/.
5d6cc8e1 Counterparties: auto-extract from uploaded documents Custos 2026-05-04 ↗ GitHub
commit body
- New lib/counterpartyExtraction.ts: maybeAutofillCounterparty() runs an
  LLM call on the doc's text to identify the counterparty (and parent
  entity) using the project's role hint (buyer/seller/mutual).
- Move extractPdfMarkdown/extractDocxMarkdown to lib/textExtraction.ts so
  both tabular review and counterparty extraction share them.
- Document upload fires the extractor fire-and-forget for templated
  projects with no counterparty set. Never overwrites manual values.
ce650956 Counterparty merging in the customer index Custos 2026-05-04 ↗ GitHub
commit body
- POST /projects/counterparties/merge {from, to} reassigns every project
  the caller owns where counterparty matches (case-insensitive trim) to
  the target name.
- Customer index: small merge icon on each row opens an inline picker;
  pick a target counterparty and confirm. Refreshes after merge.
11895ef6 Counterparty timeline page Custos 2026-05-04 ↗ GitHub
commit body
GET /projects/counterparties/:name/timeline returns every accessible
project for one counterparty plus the documents and contract_facts tied
to those projects. New /customers/[name] route renders them on a
vertical chronological timeline with per-project metadata (effective
date, term, value), the executed/draft/etc. status of each document,
and a counterparty-level total active value.

Customer index rows now link into the detail page (except the
"(Unassigned)" group).
dd8e28e2 Projects can span multiple counterparties Custos 2026-05-04 ↗ GitHub
commit body
Projects are now treated as free-form buckets - counterparty membership
is derived from the documents in the project (intake_counterparty),
plus an optional projects.counterparty as a "primary" hint. A project
named "Customer Contracts" with Adobe + Stripe + Intercom docs now
appears under each of those three counterparties in /customers.

- GET /projects/counterparties: project rows pull every distinct
  intake_counterparty across the project's docs; project_count is
  incremented per-counterparty. Role filter applies at the document
  level so a multi-cp project surfaces correctly.
- GET /projects/counterparties/:name/timeline: includes any project
  whose docs reference this counterparty (not just primary-counterparty
  matches). Document list is scoped to docs that actually belong to
  this counterparty (or all docs when this IS the project's primary).

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-42.md from inside the repo you want the changes in.

⬇ Download capture-thread-42.md