Structured contract facts extracted per document version, with value deltas

Three commits add a "Key Terms" panel that extracts structured economic data from each contract - effective date, term, value, renewal terms, governing law - and tracks how those values change each time a document is re-uploaded. The schema is built to chart across versions; nothing charts yet.

contract-reviewanalytics

b4c38597 introduces migration 004_contract_facts.sql: one row per (document, document_version) capturing effective_date, term_months, total_value_minor (stored in minor units - cents - so values can be summed as integers), currency, auto_renew, notice_days, and governing_law. Original text is preserved in raw_extraction jsonb for auditing.

lib/contractFacts.ts handles extraction. The system prompt asks for a single minified JSON object with all seven fields; maxTokens 400, document text truncated at 30,000 characters. The call runs fire-and-forget alongside counterparty extraction - it never blocks the upload path. A new GET /projects/:id/facts endpoint returns rows newest-first, and KeyTermsPanel.tsx (~152 LOC) renders them as a compact table with formatted money, term months, and dates. The panel defaults open on the Documents tab.

c2c2a147 adds version-over-version deltas to the panel. The implementation builds a chronological sequence of total_value_minor values per project, then for each row computes (current - prior) / prior * 100 and renders an inline ↑ 20% or ↓ 8% indicator. The renderDelta function returns null if either value is null or if prior is zero - avoiding divide-by-zero - and also suppresses changes under 0.5%.

59760a0d is nine lines in routes/documents.ts: it calls maybeExtractContractFacts on each new document version. First upload alone is not enough - re-uploading a renewal contract must also produce a new facts row, or the deltas would never populate in practice.

So what Worth pulling for anyone doing contract review. The {effective date, term, value, auto-renew, notice period} table is what users ask for first, and storing per-version means amendments and renewals show up as diffs rather than overwrites. Two things to verify before adopting: extraction failures are silent (fire-and-forget, no UI signal when the LLM call fails), and the % delta will render misleadingly if a contract goes from null value to a set value - the `prior === 0` guard handles zero but not the null-to-set transition.

View this fork on GitHub →

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

Commits in this thread

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

SHA Subject Author Date
b4c38597 Lifecycle: extract & display structured contract facts Custos 2026-05-04 ↗ GitHub
commit body
- New contract_facts table tracks effective_date, term_months, total_value
  (in minor units), currency, auto_renew, notice_days, governing_law per
  document version. Re-uploads/renewals add new rows so changes can be
  charted later.
- New lib/contractFacts.ts: maybeExtractContractFacts() runs an LLM call
  on uploaded text and inserts a row keyed to the doc's current version.
  Fires fire-and-forget alongside counterparty extraction; never blocks.
- GET /projects/:id/facts returns rows sorted newest-first.
- New KeyTermsPanel on the project documents tab (collapsible, default
  open) renders the rows as a compact table with formatted money/term/dates.
c2c2a147 Lifecycle deltas: % change vs prior version in Key Terms Custos 2026-05-04 ↗ GitHub
59760a0d Re-extract contract facts on each new document version Custos 2026-05-04 ↗ GitHub

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-46.md from inside the repo you want the changes in.

⬇ Download capture-thread-46.md