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