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.
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).
Spotted something wrong? Or know the PR text has fresher detail than the writeup above?