> ## Documentation Index
> Fetch the complete documentation index at: https://docs.myfundingmachine.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Underwriting system map

# 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

```mermaid theme={null}
flowchart TD
    HTTP["analyzeCreditReport (httpAction)<br/>convex/underwriting.ts:400"] --> IMPL["analyzeCreditReportImpl (adapter)<br/>convex/underwriting.ts:423<br/>· validate body · normalize SSN/state<br/>· load operator customValues<br/>· source creditData (CRS pull OR stored row)"]
    IMPL --> THR["resolveThresholds()<br/>convex/lib/resolveThresholds.ts:117<br/>~28 typed thresholds + ruleset hash"]
    THR --> RUN["runUnderwriting() — PURE<br/>convex/lib/runUnderwriting.ts:318"]
    RUN --> STORE["storeResults (internalMutation)<br/>convex/underwriting.ts:862<br/>writes underwritingResults row"]
    STORE --> GHL["syncDecisionToGhl()<br/>convex/underwriting.ts:272<br/>writes GHL custom fields (best-effort)"]
    STORE --> UI["UI reads the row via hooks<br/>useContactData / useContactActions"]
    UI --> OPS["Operator acts: approve / revise / park / decline<br/>mutations in convex/underwriting.ts:1756+"]
```

### Inside `runUnderwriting` — the evaluator sequence

```mermaid theme={null}
flowchart TD
    A["parseTradelines()<br/>tradelineParser.ts:140<br/>→ ~30 metrics from raw tradelines"] --> B["splitBankruptcies()<br/>runUnderwriting.ts:113<br/>→ recent vs old BK counts"]
    B --> C["computeCreditMetrics()<br/>creditMetrics.ts:195<br/>→ 7 multipliers, funding ranges, paydowns, gates"]
    C --> D["evaluateEstablished()<br/>establishedUnderwriting.ts:32<br/>→ mcaEligible, sbaEligible"]
    D --> E["evaluateTermLoan()<br/>termLoanUnderwriting.ts:66<br/>→ tlDecision string"]
    E --> F["evaluateCards()<br/>cardUnderwriting.ts:149<br/>→ reportStatus, declineReason, creditRepairFlag"]
    F --> G["applyHardStopFlags() + getThinFileStrategy()<br/>preprocessor.ts / lenderMatching.ts"]
    G --> H["deriveWorkflow()<br/>deriveWorkflow.ts:490<br/>→ workflow OBJECT + legacyFundingPath + systemTwoNextAction"]
    H --> I["determineFundingCategories()<br/>fundingCategories.ts (PARALLEL engine)<br/>→ category codes (presentation-only)"]
    I --> K["buildUnderwritingTrace() + mergeNextActions() + buildResults()<br/>→ final UnderwritingDecision"]
```

> 🔶 **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

|                      |                                                                                                                                                                                                                                                                                                                                                                   |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Question**         | Does this file qualify for 0% card stacking, and under what conditions?                                                                                                                                                                                                                                                                                           |
| **Code**             | `convex/lib/cardUnderwriting.ts:149` (`evaluateCards`)                                                                                                                                                                                                                                                                                                            |
| **Emits**            | `reportStatus` (headline string), `declineReason`, `creditRepairFlag`, card `nextActions`                                                                                                                                                                                                                                                                         |
| **Shape**            | \~370-line nested if/else; **the most complex branch in the engine**                                                                                                                                                                                                                                                                                              |
| **Possible outputs** | `Qualified` · `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

|                      |                                                                                                                                                                                   |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Question**         | Does this person qualify for a personal term loan (income + DTI + installment history)?                                                                                           |
| **Code**             | `convex/lib/termLoanUnderwriting.ts:66` (`evaluateTermLoan`)                                                                                                                      |
| **Emits**            | `tlDecision` string, `tl_*` next actions                                                                                                                                          |
| **Shape**            | early-return guard ladder → DTI-band tree                                                                                                                                         |
| **Possible outputs** | `TL 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

|              |                                                                                                                                            |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Question** | Is the business eligible for MCA and/or SBA products?                                                                                      |
| **Code**     | `convex/lib/establishedUnderwriting.ts:32` (`evaluateEstablished`)                                                                         |
| **Emits**    | `mcaEligible: boolean`, `sbaEligible: boolean`                                                                                             |
| **Rules**    | MCA: 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)

|              |                                                                                                                                                                                                                      |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Question** | Would credit repair meaningfully improve this file's outcome?                                                                                                                                                        |
| **Code**     | `creditRepairFlag.ts` (`buildCreditRepairFlag`), set inside `evaluateCards` (5 branches) + a post-pass in `runUnderwriting.ts:1072` (`resolveCreditRepairPostPass`)                                                  |
| **Emits**    | `creditRepairFlag` (`{severity, items[], timeframe, note}`) → stored as `creditRepairRecommended` + `creditRepairFlag` on the row (`schema.ts:250-251`); also a `credit_repair`/`credit_repair_required` next action |
| **Note**     | This 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)

|                      |                                                                                                               |
| -------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Question**         | Given all the above labels, what workflow does this file belong in?                                           |
| **Code**             | `convex/lib/deriveWorkflow.ts:490` (`deriveWorkflow`)                                                         |
| **Emits**            | `workflow` object **+** `legacyFundingPath` string **+** `systemTwoNextAction`                                |
| **`workflow` shape** | `{ primaryPaths[], overlays[], declineReason?, parkedReason?, parkedCallbackDate? }` or `null` (pre-workflow) |
| **primaryPaths**     | `card_funding` · `term_loan` · `established_funding` · `parked` · `decline`                                   |
| **overlays**         | `paydowns_required` · `inq_removal_required`                                                                  |
| **parkedReason**     | `pending_seasoning` · `recent_credit_activity` · `thin_file`                                                  |
| **declineReason**    | `repair_referral` · `no_fit` *(only these two are emitted; the other 3 enum values are dead)*                 |

### System 6 — Funding Categories (parallel classifier)

|              |                                                                                                                                  |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| **Question** | Which marketing/funding "category" (1, 1a, 2, 3, 4…) does this file present as, and what's the success probability?              |
| **Code**     | `convex/lib/fundingCategories.ts` (`determineFundingCategories`)                                                                 |
| **Emits**    | category codes, `primaryTrack` (presentation fields for chat/UI)                                                                 |
| **Tension**  | Runs **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:

```mermaid theme={null}
flowchart TD
    START([evaluateCards]) --> CNF{creditNotFound?}
    CNF -- yes --> R1["Credit Not Found"]
    CNF -- no --> CLEAN{"clean file?<br/>cleanInquiries + noLates + noMajors<br/>+ score≥qualified + noOverUtil<br/>+ minLimitCard + fewNewCards<br/>+ hasSufficientFileDepth"}
    CLEAN -- yes --> R2["Qualified"]
    CLEAN -- no --> PEX{"needsPaydown AND<br/>overUtilized ≥ midRange<br/>(paydowns exceed preapproval)?"}
    PEX -- yes --> R3["Doesn't Qualify<br/>declineReason: Paydowns exceed preapproval"]
    PEX -- no --> QPD{"score≥paydowns + clean +<br/>needsPaydown + depth?"}
    QPD -- yes --> R4["Qualified w/Paydowns"]
    QPD -- no --> QPDI{"needsInqRemoval650 + clean +<br/>needsPaydown + depth?"}
    QPDI -- yes --> R5["Qualified w/Paydowns/Inq Removal"]
    QPDI -- no --> DQGATE{"hard-DQ gate?<br/>score&lt;paydowns OR chargeOffs&gt;max<br/>OR collections&gt;max OR no minLimitCard<br/>OR overUtilized≥thresh OR tooManyNewCards<br/>OR recentBankruptcies&gt;0"}
    DQGATE -- yes --> LADDER["Doesn't Qualify<br/>→ decline-reason ladder (see below)<br/>(unless Prime Single-Card override → Qualified)"]
    DQGATE -- no --> OPT["OPTIMIZED branch<br/>(conditional approvals)"]

    OPT --> SEAS{"revCount≤3 AND<br/>avgAgeRev≤12mo?"}
    SEAS -- yes --> R6["Qualified - Pending Seasoning<br/>+ wait_for_seasoning action"]
    SEAS -- no --> STRONG{"score≥inqRemoval AND<br/>(lates OR minor derogs) AND minLimitCard?"}
    STRONG -- "lates old, no CO/coll" --> R7["Qualified (+ approved_aged_lates)"]
    STRONG -- "blocking lates" --> R8["Doesn't Qualify<br/>'...requires credit repair'<br/>+ creditRepairFlag"]
    STRONG -- "collections + recent lates" --> R9["Doesn't Qualify<br/>'...requires credit repair'<br/>+ creditRepairFlag"]
    STRONG -- "else (non-blocking)" --> R10["Qualified [w/ Paydowns]<br/>+ creditRepairFlag (orthogonal)"]
    STRONG -- no --> BORD{"score in 650-680 band?"}
    BORD -- "clean except score" --> R11["Qualified (+ borderline_score_approved)"]
    BORD -- "blocking lates" --> R8
    BORD -- "minor derogs ok" --> R10
    BORD -- "else" --> R12["Manual Review - Borderline Score"]
    BORD -- no --> LATE{"noMajors + latePaymentsHigh + minLimitCard?"}
    LATE -- "lates old" --> R7
    LATE -- "lates recent" --> R13["Manual Review - Late Payments Pattern"]
    LATE -- no --> UTIL{"high util but under thresh?"}
    UTIL -- yes --> R14["Qualified w/ Paydowns<br/>+ paydown_required action"]
    UTIL -- no --> INQ{"inquiries the ONLY issue + score≥qualified?"}
    INQ -- yes --> R15["Qualified w/ Inq Removal<br/>+ dispute_inquiries action"]
    INQ -- no --> R16["Manual Review - Complex Profile<br/>+ complex_review action"]
```

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

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

```mermaid theme={null}
flowchart LR
    L1["recentBankruptcies&gt;0<br/>→ Recent Bankruptcy on File"] --> L2["hasBlockingLateHistory<br/>→ Recent Severe Late Payment History"]
    L2 --> L3["score≤paydowns<br/>→ Credit Score Too Low"]
    L3 --> L4["chargeOffs+collections≥2<br/>→ Too Many Collections/Charge-Offs"]
    L4 --> L5["tooManyNewCards<br/>→ Too Many Newly Opened Accounts"]
    L5 --> L6["no minLimitCard<br/>→ Current card limits are too low"]
    L6 --> L7["latePaymentsHigh<br/>→ Too Many Late Payments"]
    L7 --> L8["thin (revCount&lt;3)<br/>→ Lack of Credit History"]
    L8 --> L9["revCount&lt;3 + instCount&lt;1<br/>→ Thin credit file (insufficient accounts)"]
```

***

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

`deriveWorkflow()` reads the evaluator labels and maps them. Order is load-bearing.

```mermaid theme={null}
flowchart TD
    IN["reportStatus, tlDecision, declineReason,<br/>mcaEligible, sbaEligible, hardStopFlags"] --> PRE1{"Credit Not Found<br/>OR Manual Review *?"}
    PRE1 -- yes --> WNULL["workflow = null<br/>(pre-workflow; operator acts first)"]
    PRE1 -- no --> HS{"hard-stop:<br/>bankruptcy OR derogatory_marks?"}
    HS -- yes --> WDEC1["primaryPaths: [decline]<br/>declineReason: repair_referral"]
    HS -- no --> SEAS{"reportStatus == Pending Seasoning?"}
    SEAS -- yes --> WPARK1["primaryPaths: [parked]<br/>parkedReason: pending_seasoning<br/>callback: +6mo"]
    SEAS -- no --> BUILD["build primaryPaths[]"]

    BUILD --> CQ{"cards Qualified*?"}
    CQ -- yes --> ADDC["push card_funding<br/>+ paydowns_required if 'Paydown'<br/>+ inq_removal_required if 'Inq Removal' (and TL not also)"]
    CQ -- no --> TLQ
    ADDC --> TLQ{"TL Qualified*?"}
    TLQ -- yes --> ADDT["unshift term_loan (runs first)<br/>+ inq_removal_required if TL 'Inq Removal'"]
    TLQ -- no --> ESTQ
    ADDT --> ESTQ{"mcaEligible OR sbaEligible?"}
    ESTQ -- yes --> ADDE["push established_funding"]
    ESTQ -- no --> ANY
    ADDE --> ANY{"any primaryPaths?"}
    ANY -- yes --> WACTIVE["workflow = primaryPaths + overlays"]
    ANY -- no --> DQ{"Doesn't Qualify → which decline?"}
    DQ -- "Too Many Newly Opened" --> WPARK2["parked / recent_credit_activity"]
    DQ -- "Lack of History / Thin / isThinFile" --> WPARK3["parked / thin_file (+6mo)"]
    DQ -- "BK / Severe Lates / Score Low / Collections / Many Lates" --> WDEC2["decline / repair_referral"]
    DQ -- "anything else" --> WDEC3["decline / no_fit"]
```

> ⚠️ **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 / situation                      | `primaryPaths`              | overlay                | parked/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.

```mermaid theme={null}
flowchart LR
    subgraph Systems
      C["Cards (System 1)<br/>→ 'Qualified w/ Paydowns'<br/>+ paydown_required action<br/>+ creditRepairFlag (lates in window)"]
      T["Term Loan (System 2)<br/>→ 'TL Qualified'<br/>(score ok, DTI&lt;45, installment ok)"]
      E["Established (System 3)<br/>→ mca=false, sba=false<br/>(no revenue)"]
    end
    Systems --> W["Workflow (System 5)<br/>primaryPaths: [term_loan, card_funding]<br/>overlays: [paydowns_required → card_funding]<br/>legacyFundingPath: near_prime"]
    W --> STORE["Stored on underwritingResults:<br/>underwritingDecision='Qualified w/ Paydowns'<br/>termLoanDecision='TL Qualified'<br/>creditRepairRecommended=true<br/>workflow={...}"]
```

**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.

```mermaid theme={null}
flowchart LR
    subgraph Systems
      C["Cards (System 1)<br/>recentBankruptcies&gt;0 → hard-DQ gate<br/>→ 'Doesn't Qualify'<br/>declineReason='Recent Bankruptcy on File'"]
      T["Term Loan (System 2)<br/>does NOT take hardStop input<br/>→ 'TL Qualified'"]
      H["Hard-stop (preprocessor)<br/>flags: ['bankruptcy']"]
    end
    Systems --> W["Workflow (System 5)<br/>hard-stop short-circuit fires FIRST →<br/>primaryPaths: [decline]<br/>declineReason: repair_referral"]
    W --> NOTE["⚠ Documented behavior:<br/>workflow says DECLINE,<br/>but tlDecision column still says 'TL Qualified'.<br/>Operator may pursue the TL."]
```

**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.

```mermaid theme={null}
flowchart LR
    C["Cards (System 1)<br/>clean but revCount≤3 & avgAge≤12mo<br/>→ 'Qualified - Pending Seasoning'<br/>+ wait_for_seasoning action"] --> W["Workflow (System 5)<br/>PENDING_SEASONING is EXCLUSIVE →<br/>primaryPaths: [parked]<br/>parkedReason: pending_seasoning<br/>callback: now + 6 months"]
    W --> UI["UI: parked banner + 'Park for Callback' CTA<br/>(this IS one of the 3 spots that reads workflow:<br/>useContactData.ts:645-649, parkedReason)"]
```

***

### 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 History` → **`parked / thin_file`** (callback +6mo).
**Input 2 (complex):** score 672, mixed recent lates + high util + 3 new cards, none individually blocking → cards `Manual Review - Complex Profile` → **`workflow = null`** (operator must revise/override before any workflow applies).

```mermaid theme={null}
flowchart TD
    TH["Lack of Credit History / Thin"] --> P["parked / thin_file<br/>(time-based recovery)"]
    MR["Manual Review - Complex Profile"] --> N["workflow = null<br/>(pre-workflow; NextActionCard 'needs operator review')"]
```

***

## 6. Where every output lives, and who reads it

```mermaid theme={null}
flowchart LR
    subgraph runUnderwriting
      RS["reportStatus"]
      TL["tlDecision"]
      DR["declineReason"]
      CRF["creditRepairFlag"]
      NA["nextActions[]"]
      WF["workflow{}"]
      LFP["legacyFundingPath"]
      FC["fundingCategories"]
    end

    RS --> ROW["underwritingResults row<br/>schema.ts:240+"]
    TL --> ROW
    DR --> ROW
    CRF --> ROW
    NA --> ROW
    WF --> ROW
    FC --> ROW
    LFP --> FR["fullResults blob (v.any())"]
    FR --> ROW

    ROW --> GHLF["GHL custom fields<br/>updateGHLContactWithDecision"]
    ROW --> HOOK["useContactData / useContactActions"]

    HOOK --> R_NEXT["NextActionCard (12-branch waterfall)"]
    HOOK --> R_REC["FundingRecommendations (nextActions dispatcher)"]
    HOOK --> R_WORK["FundingWorkflow (workflowPath templates)"]
    HOOK --> R_SUM["FundingSummaryCard (string banners)"]
    HOOK --> R_QUEUE["OperatorUnderwritingQueue (badge colors)"]
```

### Field → consumer table

| Output                  | Stored as                                      | Read by (file:line)                                                                                                                                        | Notes                                            |
| ----------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `reportStatus`          | `underwritingDecision` column                  | `useContactData.ts:693`, `useContactActions.ts:1031`, `NextActionCard:193`, `FundingSummaryCard:213`, `OperatorUnderwritingQueue:116`, `ProcessingHero:28` | 6 different string matchers                      |
| `tlDecision`            | `termLoanDecision` column                      | `useContactActions.ts:1056-1083`                                                                                                                           | regex strip + `startsWith`                       |
| `workflow`              | `workflow` object column (`schema.ts:323`)     | only `useContactData.ts:645-649, 728-732`                                                                                                                  | **mostly unread**                                |
| `workflow.parkedReason` | inside `workflow`                              | `FundingSummaryCard:244`, `NextActionCard:165`, `SendParkedEmailDialog:70`                                                                                 | parked email template select                     |
| `workflowPath`          | `workflowPath` column                          | `useContactData.ts:662-668` → `FundingWorkflow`                                                                                                            | set at **approve time**, not by `deriveWorkflow` |
| `legacyFundingPath`     | inside `fullResults`                           | `buildUnderwritingTrace`, `aiUnderwriting.ts:121`                                                                                                          | trace narration (stale for parked)               |
| `nextActions[]`         | `nextActions` column                           | `FundingRecommendations.tsx`                                                                                                                               | action-type dispatcher                           |
| `creditRepairFlag`      | `creditRepairFlag` + `creditRepairRecommended` | `AddressTheseFirstCard`, recommendation merge                                                                                                              | orthogonal 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:

```mermaid theme={null}
flowchart TD
    FILE["ONE credit file"] --> V1["reportStatus string<br/>'Qualified w/ Paydowns'"]
    FILE --> V2["tlDecision string<br/>'TL Qualified'"]
    FILE --> V3["legacyFundingPath<br/>'near_prime'"]
    FILE --> V4["workflowPath (set at approve)<br/>'paydowns'"]
    FILE --> V5["workflow object<br/>primaryPaths:[term_loan,card_funding]<br/>overlays:[paydowns_required]"]

    V1 --> UI1["NextActionCard, FundingSummaryCard,<br/>OperatorQueue, ProcessingHero<br/>(string matching ×6)"]
    V2 --> UI1
    V3 --> UI2["trace + AI prompts"]
    V4 --> UI3["FundingWorkflow templates"]
    V5 --> UI4["only 3 narrow reads<br/>(parked + established routing)"]

    style V5 stroke:#2a2,stroke-width:3px
    style UI4 stroke:#2a2,stroke-width:3px
```

**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)

| What                            | Where                                                                                     |
| ------------------------------- | ----------------------------------------------------------------------------------------- |
| HTTP entry + adapter            | `convex/underwriting.ts:400` / `:423`                                                     |
| Pure orchestrator               | `convex/lib/runUnderwriting.ts:318`                                                       |
| Threshold resolution            | `convex/lib/resolveThresholds.ts:117`                                                     |
| Tradeline parsing               | `convex/lib/tradelineParser.ts:140`                                                       |
| Credit metrics / multipliers    | `convex/lib/creditMetrics.ts:195`                                                         |
| **Cards evaluator**             | `convex/lib/cardUnderwriting.ts:149`                                                      |
| **Term loan evaluator**         | `convex/lib/termLoanUnderwriting.ts:66`                                                   |
| **Established (MCA/SBA) gates** | `convex/lib/establishedUnderwriting.ts:32`                                                |
| Credit repair flag              | `convex/lib/creditRepairFlag.ts` + `runUnderwriting.ts:1072`                              |
| **Workflow router**             | `convex/lib/deriveWorkflow.ts:490`                                                        |
| Parallel category engine        | `convex/lib/fundingCategories.ts`                                                         |
| Summary reconciliation          | `convex/lib/runUnderwriting.ts:282`                                                       |
| Trace narration                 | `convex/lib/buildUnderwritingTrace.ts:341`                                                |
| Persistence                     | `convex/underwriting.ts:862` (`storeResults`)                                             |
| GHL field sync                  | `convex/underwriting.ts:272` (`syncDecisionToGhl`)                                        |
| Operator mutations              | `convex/underwriting.ts:1756+` (approve/park/revise/decline/queue)                        |
| Result schema                   | `convex/schema.ts:240+` (`underwritingResults`)                                           |
| Public result type              | `lib/types/underwriting-result.ts:177`                                                    |
| Thresholds + multiplier config  | `lib/types/funding-categories.ts`                                                         |
| **UI: contact data hook**       | `components/contact/hooks/useContactData.ts` (852)                                        |
| **UI: contact actions hook**    | `components/contact/hooks/useContactActions.ts` (1925)                                    |
| UI: next-action hero            | `components/contact/NextActionCard.tsx`                                                   |
| UI: recommendation cards        | `components/funding/FundingRecommendations.tsx`                                           |
| UI: workflow checklist          | `components/funding/FundingWorkflow.tsx`                                                  |
| UI: summary + product rows      | `components/funding/FundingSummaryCard.tsx` / `components/contact/FundingSummaryCard.tsx` |
| UI: operator queue              | `components/funding/OperatorUnderwritingQueue.tsx`                                        |
| UI: processing (read-only)      | `components/processing/ProcessingUnderwritingHero.tsx`                                    |
| Review console                  | `app/admin/internal/underwriting-review/page.tsx` + `_components/*`                       |
| Console replay engine           | `convex/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.
