Skip to main content

Underwriting — Visual System Map

Status: Visual companion to underwriting-uiux-rework-breakdown.md. Captured 2026-06-08 — this is the pre-rework audit snapshot; treat diagrams as historical where superseded.
Updated 2026-06-11 — what has changed since capture (see underwriting-rework-plan.md for detail): Phases 0–3 are merged. The typed cardStatus/termLoanStatus/declineStatus codes are routing-authoritative; the UI surfaces below (NextActionCard, FundingSummaryCard, OperatorUnderwritingQueue, ProcessingUnderwritingHero) now read deriveWorkflowState (lib/underwriting/workflowState.ts) instead of string-matching (#929/#930). The duplicate paydowns strings are collapsed at emission (#967). fundingWorkflowPath is now persisted server-side on underwritingResults and consumed by useContactData + lib/chat/tool-handlers.ts with client fallback (#969) — prod backfill still pending. reconcileSummaryRecommendation/summaryRecommendation are deleted (#970). Legacy workflowPath/legacyFundingPath columns and normalize* canonicalizers still exist pending Phase 5. Purpose: See every system/process in the underwriting pipeline, what each one emits, concrete worked examples of how they combine, and exactly where the code lives. How to read: the Mermaid diagrams render in Cursor’s Markdown preview (open this file, ⇧⌘V). Code references use path:line.
Mental model. Underwriting is one pure function (runUnderwriting) that runs a sequence of independent evaluators, each answering one question and emitting its own label. A router (deriveWorkflow) then tries to fold those labels into one structured workflow object. The labels are also kept (legacy), so the same file ends up described by 5 vocabularies at once. The UI then re-parses those labels with string matching. That fan-out is the thing the rework collapses.

1. The whole pipeline at a glance

Inside runUnderwriting — the evaluator sequence

🔶 Two routers run on every file. deriveWorkflow() (the structured model) and determineFundingCategories() (the parallel category engine) both classify the same file. The old reconcileSummaryRecommendation() persistence shim was deleted 2026-06-11 (PR #970) once every summaryRecommendation consumer was gone — routing authority lives in workflow, and fundingCategories is presentation-only.

2. The systems/processes — one card each

Each “system” is an independent process with its own question, code home, and output label.

System 1 — Cards / Revolving evaluator

QuestionDoes this file qualify for 0% card stacking, and under what conditions?
Codeconvex/lib/cardUnderwriting.ts:149 (evaluateCards)
EmitsreportStatus (headline string), declineReason, creditRepairFlag, card nextActions
Shape~370-line nested if/else; the most complex branch in the engine
Possible outputsQualified · Qualified w/ Paydowns · Qualified w/Paydowns (no space) · Qualified w/ Inq Removal · Qualified w/Paydowns/Inq Removal · Qualified - Pending Seasoning · Doesn't Qualify (+ a declineReason) · Manual Review - Borderline Score · Manual Review - Late Payments Pattern · Manual Review - Complex Profile · Credit Not Found

System 2 — Term Loan evaluator

QuestionDoes this person qualify for a personal term loan (income + DTI + installment history)?
Codeconvex/lib/termLoanUnderwriting.ts:66 (evaluateTermLoan)
EmitstlDecision string, tl_* next actions
Shapeearly-return guard ladder → DTI-band tree
Possible outputsTL Qualified · TL Qualified - Higher DTI · TL Qualified w/ Inq Removal · 11× TL Manual Review - * · 7× TL Declined - * · Enter Personal Income for TL Decision · NA

System 3 — Established (MCA / SBA) gates

QuestionIs the business eligible for MCA and/or SBA products?
Codeconvex/lib/establishedUnderwriting.ts:32 (evaluateEstablished)
EmitsmcaEligible: boolean, sbaEligible: boolean
RulesMCA: FICO ≥ FICO_MCA + revenue ≥ MIN_REVENUE_MCA + TIB ≥ 6mo. SBA: FICO ≥ FICO_SBA + TIB = “2 years +” + revenue ≥ MIN_REVENUE_SBA

System 4 — Credit Repair flag (orthogonal)

QuestionWould credit repair meaningfully improve this file’s outcome?
CodecreditRepairFlag.ts (buildCreditRepairFlag), set inside evaluateCards (5 branches) + a post-pass in runUnderwriting.ts:1072 (resolveCreditRepairPostPass)
EmitscreditRepairFlag ({severity, items[], timeframe, note}) → stored as creditRepairRecommended + creditRepairFlag on the row (schema.ts:250-251); also a credit_repair/credit_repair_required next action
NoteThis is NOT a GHL tag — it’s a flag on the underwriting doc + a recommendation card. It is orthogonal to the funding decision (a file can be Qualified w/ Paydowns AND carry a credit-repair flag).

System 5 — Workflow router (the target model)

QuestionGiven all the above labels, what workflow does this file belong in?
Codeconvex/lib/deriveWorkflow.ts:490 (deriveWorkflow)
Emitsworkflow object + legacyFundingPath string + systemTwoNextAction
workflow shape{ primaryPaths[], overlays[], declineReason?, parkedReason?, parkedCallbackDate? } or null (pre-workflow)
primaryPathscard_funding · term_loan · established_funding · parked · decline
overlayspaydowns_required · inq_removal_required
parkedReasonpending_seasoning · recent_credit_activity · thin_file
declineReasonrepair_referral · no_fit (only these two are emitted; the other 3 enum values are dead)

System 6 — Funding Categories (parallel classifier)

QuestionWhich marketing/funding “category” (1, 1a, 2, 3, 4…) does this file present as, and what’s the success probability?
Codeconvex/lib/fundingCategories.ts (determineFundingCategories)
Emitscategory codes, primaryTrack (presentation fields for chat/UI)
TensionRuns independently of Systems 1–5; presentation-only since Phase 2 — routing authority is workflow, not category codes

3. The Cards decision tree (System 1, visualized)

This is the single most important — and most complex — branch. Source: cardUnderwriting.ts:240-609. Simplified to the decision spine:

The decline-reason ladder (cardUnderwriting.ts:306-327)

When the hard-DQ gate fires, the reason is the first match, strongest signal first:

4. How labels become a workflow (System 5 mapping)

deriveWorkflow() reads the evaluator labels and maps them. Order is load-bearing.
⚠️ Confirmed drift. The two card decline reasons "Recent severe late payment history requires credit repair" and "Collections with recent late payment history requires credit repair" (cardUnderwriting.ts:383,405) are not in REPAIR_REFERRAL_DECLINE_REASONS (deriveWorkflow.ts:137-143). Those files fall through to decline / no_fit instead of repair_referral — the credit-repair routing is silently lost for exactly the files that need it.

Status → workflow quick-reference

reportStatus / situationprimaryPathsoverlayparked/decline reason
Qualified[card_funding]
Qualified w/ Paydowns[card_funding]paydowns_required
Qualified w/ Inq Removal[card_funding]inq_removal_required
Qualified + TL Qualified[term_loan, card_funding]
Qualified - Pending Seasoning[parked]pending_seasoning
Doesn't Qualify + Too Many Newly Opened[parked]recent_credit_activity
Doesn't Qualify + Lack of History/Thin[parked]thin_file
Doesn't Qualify + BK/Severe Lates/Low Score[decline]repair_referral
Doesn't Qualify + Card limits too low[decline]no_fit
Credit Not Found / Manual Review - *null(operator acts first)
MCA/SBA eligible (alone)[established_funding]

5. Worked end-to-end examples

Each example shows: the file → what every system emits → what gets stored → what the UI renders. This is the “System y says X and System x adds Y” view you asked for.

Example A — “Qualified w/ Paydowns for cards, credit-repair flagged, term loan also qualifies”

Input: FICO 690, util 55% (over-utilized but under threshold), 1 old 30-day late within 24mo, income $120k, DTI 38%, 1 open installment, business: no revenue data. What the UI does today (/contact):
  • canStartFundingWorkflow = true (matches 'Qualified w/ Paydowns' literal, useContactData.ts:694-704).
  • FundingRecommendations renders the paydown_required card; the co-firing credit_repair advisory is merged into the primary card (SOFT_MERGE_PRIMARIES, FundingRecommendations.tsx:723).
  • getCardsStatus → “Qualified w/ Paydowns” pill; getTermLoanStatus → “Qualified” pill (useContactActions.ts:1031-1083).
  • ⚠️ The UI does not read workflow.overlays or the term_loan → card_funding sequencing — it reconstructs the path client-side from the strings + selectedFundingTypes.

Example B — “Recent bankruptcy → decline, but term loan still qualifies” (cross-track non-exclusivity)

Input: FICO 700, BK filed 30 months ago (recent), income $130k, DTI 35%, clean installment history. Why it matters for the UI: the structured workflow says decline, but the raw tlDecision string says qualified. A UI that reads workflow and a UI that reads tlDecision would show opposite things. (Decision log entry 2026-04-27 in funding-workflow-paths.md documents this is intentional, not a bug.)

Example C — “Pending Seasoning → parked” (qualified-but-wait)

Input: FICO 760, 2 revolving accounts, avg age 7 months, no derogs, no lates.

Example D — “Thin file” vs “Manual Review” (two different parked/null outcomes)

Input 1 (thin): 1 revolving account, 14 months old, score 710, no derogs → cards falls to Doesn't Qualify / Lack of Credit Historyparked / thin_file (callback +6mo). Input 2 (complex): score 672, mixed recent lates + high util + 3 new cards, none individually blocking → cards Manual Review - Complex Profileworkflow = null (operator must revise/override before any workflow applies).

6. Where every output lives, and who reads it

Field → consumer table

OutputStored asRead by (file:line)Notes
reportStatusunderwritingDecision columnuseContactData.ts:693, useContactActions.ts:1031, NextActionCard:193, FundingSummaryCard:213, OperatorUnderwritingQueue:116, ProcessingHero:286 different string matchers
tlDecisiontermLoanDecision columnuseContactActions.ts:1056-1083regex strip + startsWith
workflowworkflow object column (schema.ts:323)only useContactData.ts:645-649, 728-732mostly unread
workflow.parkedReasoninside workflowFundingSummaryCard:244, NextActionCard:165, SendParkedEmailDialog:70parked email template select
workflowPathworkflowPath columnuseContactData.ts:662-668FundingWorkflowset at approve time, not by deriveWorkflow
legacyFundingPathinside fullResultsbuildUnderwritingTrace, aiUnderwriting.ts:121trace narration (stale for parked)
nextActions[]nextActions columnFundingRecommendations.tsxaction-type dispatcher
creditRepairFlagcreditRepairFlag + creditRepairRecommendedAddressTheseFirstCard, recommendation mergeorthogonal flag, not a tag

7. The dual-system overlay (why the rework exists)

The same file is described 5 ways simultaneously. The UI re-derives meaning from whichever it happens to trust per surface: The rework’s thesis (green path): make workflow (V5) authoritative, render everything off it, and delete V1–V4 string coupling. The structured model already exists; the UI just doesn’t trust it yet. See underwriting-uiux-rework-breakdown.md §8 for the sequencing.

8. Code-location index (jump table)

WhatWhere
HTTP entry + adapterconvex/underwriting.ts:400 / :423
Pure orchestratorconvex/lib/runUnderwriting.ts:318
Threshold resolutionconvex/lib/resolveThresholds.ts:117
Tradeline parsingconvex/lib/tradelineParser.ts:140
Credit metrics / multipliersconvex/lib/creditMetrics.ts:195
Cards evaluatorconvex/lib/cardUnderwriting.ts:149
Term loan evaluatorconvex/lib/termLoanUnderwriting.ts:66
Established (MCA/SBA) gatesconvex/lib/establishedUnderwriting.ts:32
Credit repair flagconvex/lib/creditRepairFlag.ts + runUnderwriting.ts:1072
Workflow routerconvex/lib/deriveWorkflow.ts:490
Parallel category engineconvex/lib/fundingCategories.ts
Summary reconciliationconvex/lib/runUnderwriting.ts:282
Trace narrationconvex/lib/buildUnderwritingTrace.ts:341
Persistenceconvex/underwriting.ts:862 (storeResults)
GHL field syncconvex/underwriting.ts:272 (syncDecisionToGhl)
Operator mutationsconvex/underwriting.ts:1756+ (approve/park/revise/decline/queue)
Result schemaconvex/schema.ts:240+ (underwritingResults)
Public result typelib/types/underwriting-result.ts:177
Thresholds + multiplier configlib/types/funding-categories.ts
UI: contact data hookcomponents/contact/hooks/useContactData.ts (852)
UI: contact actions hookcomponents/contact/hooks/useContactActions.ts (1925)
UI: next-action herocomponents/contact/NextActionCard.tsx
UI: recommendation cardscomponents/funding/FundingRecommendations.tsx
UI: workflow checklistcomponents/funding/FundingWorkflow.tsx
UI: summary + product rowscomponents/funding/FundingSummaryCard.tsx / components/contact/FundingSummaryCard.tsx
UI: operator queuecomponents/funding/OperatorUnderwritingQueue.tsx
UI: processing (read-only)components/processing/ProcessingUnderwritingHero.tsx
Review consoleapp/admin/internal/underwriting-review/page.tsx + _components/*
Console replay engineconvex/lib/underwritingReviewReplay.ts

9. Cross-references

  • docs/design/underwriting-uiux-rework-breakdown.md — the prose audit + prioritized findings + rework sequencing.
  • docs/funding-workflow-paths.md — the intended products+overlays design model and full Decisions Log.
  • docs/adr/0001-underwriting-whatif-override-seam.md — the multiplier-override seam used by the review console.