> ## 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

# Underwriting — System Reference

**What this is:** the single current-state reference for the underwriting subsystem — architecture, typed taxonomy, the workflow model, every locked design decision, and a symbol-anchored code index. Internal design note — **not** part of the Mintlify docs site (`docs/docs.json`).

**What this is NOT:** a status tracker. Phase status, remaining work, and step-by-step execution instructions live in exactly one place: **[`docs/plans/underwriting-roadmap.md`](../plans/underwriting-roadmap.md)** (the roadmap). Never add "Updated YYYY-MM-DD — phase X merged" banners to this file; update the roadmap instead. Update this file only when the *system itself* changes (new field, new evaluator, a locked decision reversed).

**Consolidation note (2026-07-01):** this doc replaces and supersedes five earlier docs — `docs/funding-workflow-paths.md` (design working doc + Decisions Log), `docs/design/underwriting-rework-plan.md` (phase tracker), `docs/design/underwriting-system-map.md` (visual map), `docs/design/underwriting-uiux-rework-breakdown.md` (founding audit), and `docs/superpowers/plans/2026-06-17-underwriting-legacy-demolition.md` (demolition plan). Every currently-operative decision from those docs is carried into §9 here; the full chronological reasoning trail (including superseded decisions) is preserved in the **git history of the deleted files**. *(2026-07-10: the `docs/superpowers/` tree was retired; the roadmap moved to `docs/plans/underwriting-roadmap.md` and the completed Phase 6 plan was deleted — history in git.)*

**Conventions for code references in this doc:** references are anchored to **symbol names**, never line numbers (line numbers rot). To locate a symbol, use `rg -n "symbolName" <path>`.

***

## 1. Mental model

Underwriting is **one pure function** (`runUnderwriting` in `convex/lib/runUnderwriting.ts`) that runs a sequence of independent *evaluators*, each answering one question about one product family. A presentation projector (`deriveWorkflow` in `convex/lib/deriveWorkflow.ts`) folds the five product verdicts into one structured `workflow` object plus coaching next-actions without overriding those verdicts. Results persist to the `underwritingResults` table; operators then act (approve / revise / park / decline) via mutations whose handlers live in `convex/lib/underwritingOperator.ts`.

Historically the same file was described by **five vocabularies at once** (decision strings, TL strings, an internal coaching path, a persisted workflow-path column, and the `workflow` object), and the UI re-parsed the human-readable strings with \~6 non-identical matchers. The multi-phase rework (2026-06 → 2026-07) collapsed that fan-out onto **typed status codes** as the single machine-readable authority. As of Phase 5e (2026-07-08): **codes are the only routing authority; display surfaces render codes via `lib/underwriting/statusDisplay.ts`; decision strings survive only as display labels and legacy persisted data.**

The current architecture implements the **Per-Product Decision Model** (§7): every run emits an equal, independent verdict for all five products, and `deriveWorkflow` is a pure presentation/sequencing projection over `products[]`. Phase 9a (PR #1182) landed the evaluators and persistence; Phase 9c (PR #1213) removed file-level vetoes and switched workflow derivation; Phase 9d + 9e (PR #1281) verified cards/TL override compatibility and cut UI, tables, and coaching consumers over. Cards/TL display projects the stored product decisions through override-aware effective codes, while Revenue Based MCA/BLOC/SBA rows read `products[]` directly; SBA remains non-confirmable pending its questionnaire.

***

## 2. Pipeline

```mermaid theme={null}
flowchart TD
    HTTP["analyzeCreditReport (httpAction)<br/>convex/underwriting.ts"] --> IMPL["analyzeCreditReportImpl (adapter)<br/>convex/lib/underwritingIngest.ts<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<br/>~28 typed thresholds + ruleset hash"]
    THR --> RUN["runUnderwriting() — PURE<br/>convex/lib/runUnderwriting.ts"]
    RUN --> STORE["storeResults<br/>convex/lib/underwritingStore.ts<br/>writes underwritingResults row<br/>(incl. cardStatusCode / termLoanStatusCode)"]
    STORE --> GHL["syncDecisionToGhl()<br/>convex/lib/underwritingGhlSync.ts<br/>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/>handlers in convex/lib/underwritingOperator.ts"]
```

Inside `runUnderwriting` (evaluator sequence):

```mermaid theme={null}
flowchart TD
    A["parseTradelines()<br/>tradelineParser.ts"] --> B["splitBankruptcies()<br/>runUnderwriting.ts"]
    B --> C["computeCreditMetrics()<br/>creditMetrics.ts<br/>7 multipliers, funding ranges, paydowns, gates"]
    C --> D["evaluateEstablished()<br/>establishedUnderwriting.ts<br/>→ MCA / SBA / BLOC verdicts<br/>+ strangler booleans"]
    D --> E["evaluateTermLoan()<br/>termLoanUnderwriting.ts<br/>→ tlDecision + status{code,label}"]
    E --> F["evaluateCards()<br/>cardUnderwriting.ts<br/>→ reportStatus + status{code,label}, declineStatus, creditRepairFlag"]
    F --> G["applyHardStopFlags() + getThinFileStrategy()<br/>preprocessor.ts / lenderMatching.ts"]
    G --> P["deriveProductDecisions()<br/>productDecisions.ts<br/>→ ProductDecision[5]"]
    P --> H["deriveWorkflow()<br/>deriveWorkflow.ts<br/>→ workflow projection + coachingPath + coachingNextAction"]
    H --> I["determineFundingCategories()<br/>fundingCategories.ts — PARALLEL, presentation-only"]
    I --> K["buildUnderwritingTrace() + mergeNextActions() + buildResults()<br/>→ final UnderwritingDecision"]
```

Three differently-named operations that are frequently confused (they are NOT variants of each other):

* `app/api/credit-report/underwriting/route.ts` — a **READ** (lists persisted results). Misleadingly named.
* `reviseUnderwriting` (handler in `convex/lib/underwritingOperator.ts`) — operator override; **does not re-run the engine**; patches `operatorOverrides` (strings + code mirrors).
* `app/api/credit-report/re-underwrite/route.ts` → `reUnderwriteCreditReport` (`convex/creditReports.ts`) — the **only** true re-run-and-persist path (`useStoredReportData: true` → `analyzeCreditReportImpl` → `storeResults`).

***

## 3. The evaluators

| # | System                               | Question                                                                 | Code (symbol)                                                                                                                                            | Emits                                                                                                                                                                                                                                                                                                                                                                                                  |
| - | ------------------------------------ | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 1 | **Cards / Revolving**                | Qualifies for 0% card stacking, under what conditions?                   | `evaluateCards` — `convex/lib/cardUnderwriting.ts`                                                                                                       | `reportStatus` string + `status: {code,label}` (`CardStatusCode`), `declineStatus: {code,label}` (`CardDeclineReasonCode`), `creditRepairFlag`, card `nextActions`                                                                                                                                                                                                                                     |
| 2 | **Term loan**                        | Qualifies for a personal term loan (income + DTI + installment history)? | `evaluateTermLoan` — `convex/lib/termLoanUnderwriting.ts`                                                                                                | `tlDecision` string + `status: {code,label}` (`TermLoanStatusCode`), `tl_*` next actions                                                                                                                                                                                                                                                                                                               |
| 3 | **Revenue Based (MCA / SBA / BLOC)** | Per-product eligibility for MCA, SBA, BLOC?                              | `evaluateEstablished` — `convex/lib/establishedUnderwriting.ts`                                                                                          | Three per-product verdicts (`EstablishedProductVerdict {status, reasons}` each for `mca`/`sba`/`bloc`) **plus** strangler booleans `mcaEligible`/`sbaEligible`/`blocEligible`. Gates (ADR-0009): MCA score 600+ / TIB 6mo+ with **tiered revenue** (600–639 → $50k+; 640+ → $20k+); BLOC 660+ / $20k+ / 2yr+; SBA baseline 680+ / $20k+ / 2yr+ (UI questionnaire-gated and non-confirmable, ADR-0003). |
| 4 | **Credit-repair flag** (orthogonal)  | Would credit repair meaningfully improve outcomes?                       | `buildCreditRepairFlag` — `convex/lib/creditRepairFlag.ts`, set inside `evaluateCards` + post-pass `resolveCreditRepairPostPass` in `runUnderwriting.ts` | `creditRepairFlag {severity, items[], timeframe, note}` → stored as `creditRepairRecommended` + `creditRepairFlag`. NOT a GHL tag. Orthogonal — a file can be `Qualified w/ Paydowns` AND carry the flag.                                                                                                                                                                                              |
| 5 | **Workflow projection**              | Which workflow sequence follows from the five product decisions?         | `deriveWorkflow` — `convex/lib/deriveWorkflow.ts`                                                                                                        | Pure presentation/sequencing projection over `products[]`: `workflow` object (§5) + `coachingPath` (internal `CoachingPathCode`) + `coachingNextAction`                                                                                                                                                                                                                                                |
| 6 | **Funding categories** (parallel)    | Which marketing category (1, 1a, 2 …) does the file present as?          | `determineFundingCategories` — `convex/lib/fundingCategories.ts`                                                                                         | Category codes + confidence. **Presentation-only** since Phase 2 — zero routing authority. Consumed by AI chat tools (presentation fields only).                                                                                                                                                                                                                                                       |

***

## 4. The typed taxonomy

Single source of truth: **`convex/lib/underwritingStatus.ts`** — a **purity-locked, types-only module** (no `convex/values` import; Convex validators live separately in `convex/lib/underwritingStatusValidators.ts` with compile-time `Equals<…>` drift guards in both directions).

### Status codes

```
CardStatusCode = credit_not_found | qualified | qualified_paydowns
  | qualified_inq_removal | qualified_paydowns_inq_removal
  | pending_seasoning | manual_review | doesnt_qualify

TermLoanStatusCode = tl_qualified | tl_qualified_inq_removal
  | tl_manual_review | tl_declined | tl_needs_income | tl_not_applicable

CardDeclineReasonCode = 14-member union (see isRepairReferralDecline /
  isParkedThinFileDecline / isParkedRecentActivityDecline predicates)
```

Notes:

* `tl_not_applicable` is only produced by `runUnderwriting` (credit-not-found override sets `tlDecision = "NA"` in lockstep) — the TL evaluator never emits it.
* `qualified_paydowns` unifies the historical spaced/no-space string duplication at the code level. The engine emits only the spaced canonical string (`"Qualified w/ Paydowns"`) since PR #967.

### Where the codes live (persistence)

| Column                                                                      | Table / location                           | Written by                                                                                                                                                                      |
| --------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cardStatusCode`, `termLoanStatusCode`                                      | top-level on `underwritingResults`         | `storeResults` (`convex/lib/underwritingStore.ts`) from `fullResults.cardStatus.code` / `termLoanStatus.code`; historical rows filled by `oneOffMigrations.backfillStatusCodes` |
| `operatorOverrides.cardsStatusCode`, `operatorOverrides.termLoanStatusCode` | inside `operatorOverrides` on the same row | `reviseUnderwriting` on every revise (via the tolerant override classifiers); historical rows filled by `oneOffMigrations.backfillOverrideStatusCodes`                          |

**Effective code** (the value everything routing-related must use) = `operatorOverrides.<code> ?? <top-level code>`. Server-side resolvers: `getEffectiveCardStatusCode` / `getEffectiveTermLoanStatusCode` in `convex/lib/underwritingOperator.ts` (client code computes the same `??` inline because server modules can't be imported client-side).

### Classifier inventory (`convex/lib/underwritingStatus.ts`)

| Symbol                                                                                                                                     | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | Use for                                                                 |
| ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `classifyCardStatus`, `classifyTermLoanStatus`                                                                                             | **Strict — THROW on unmapped input.** Only current engine output.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | Engine-side classification. Keep loud; never add legacy tolerance here. |
| `classifyOverrideCardStatus`, `classifyOverrideTermLoanStatus`                                                                             | Throwing, override-aware (map `"Manual Review - Operator Override"`, bare `"Manual Review"`, pre-`"TL "` generic values).                                                                                                                                                                                                                                                                                                                                                                                                                                  | Revise-dialog option values.                                            |
| `tryClassifyCardStatusCode`, `tryClassifyTermLoanStatusCode`, `tryClassifyOverrideCardStatusCode`, `tryClassifyOverrideTermLoanStatusCode` | **Tolerant wrappers** — run a legacy-normalization layer (`classifyLegacy*Status`) mapping every historical prod string (`" - Pending Review"` family, `Manual Review - …` sub-labels, `Additional Underwriting Required` → `manual_review`, `Doesn't Qulify` → `doesnt_qualify`, `N/A`/`NA` → `tl_not_applicable`, imported `Likely` → `tl_qualified`, `Unlikely` → `tl_declined`, `Pre-Qualified with Verifiable Income` → `tl_needs_income`, `Qualified w/ Credit Repair` → `qualified`); return `null` instead of throwing on genuinely unknown input. | Migrations, backfills, and any import path writing decision strings.    |
| `recoverTermLoanStatusFromCardDecision`                                                                                                    | Cross-field recovery: pulls the TL qualification trapped in the **card** field for the 278 pre-split contamination rows (`TL Qualified w/ Income …` → `tl_qualified`, `TL Qualified w/ Inq Removal and Income …` → `tl_qualified_inq_removal`); takes priority over the row's contradictory `N/A`/`Likely`/`Unlikely` TL field.                                                                                                                                                                                                                            | Backfills / import paths only.                                          |
| `isCardsQualified`, `cardsRequirePaydowns`, `cardsRequireInqRemoval`, `isTlQualified`, `tlRequiresInqRemoval`                              | Exhaustive-switch predicates over the codes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | ALL qualification checks, server and client.                            |
| `isRepairReferralDecline`, `isParkedThinFileDecline`, `isParkedRecentActivityDecline`                                                      | Exhaustive predicates over `CardDeclineReasonCode`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `deriveWorkflow` decline/parked routing.                                |
| `classifyCardDeclineReason`                                                                                                                | Maps the 14 legacy decline-reason strings → typed code.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | Card evaluator.                                                         |

### Routing predicates (`lib/underwriting/workflowState.ts` — pure, browser-safe + Convex-safe)

All take **effective codes**, not strings (since Phase 5d-3):

* `deriveCardWorkflowPath(code)` → `'prime' | 'paydowns' | 'inq_removal' | null` — exhaustive switch over `CardStatusCode`.
* `isQualifiedCardsDecision(code)` / `isQualifiedTermLoanDecision(code)` — delegate to the canonical predicates.
* `deriveWorkflowState({effectiveCardStatusCode, effectiveTermLoanStatusCode})` → `{cardsQualified, tlQualified, canStartWorkflow}` — **the** "can start a Creative funding workflow" gate (cards OR term loan qualifies).
* `deriveCreativeWorkflowPath(input)` → cards path wins, else `term_loan`, else null.
* `resolveFundingWorkflowPath(row)` → the wide `FundingWorkflowPath` (`prime | paydowns | inq_removal | term_loan | parked | established | null`). Priority: parked (exclusive) → approved+Creative-selected → approved+Revenue-Based-only → Creative fallback. The `established` member is a retained code id. Non-approved, non-parked rows resolve `null`.
* `isCreativeFundingWorkflowPath(path)` — set membership over the 4 Creative templates.

`fundingWorkflowPath` is **persisted** on `underwritingResults` by every operator-state mutation (approve / approve-established / update-selected-types / revoke / park / decline). Read side uses the persisted field as authoritative (Phase 5e dropped the `?? resolveFundingWorkflowPath` dual-read).

### Display layer (post 5d-4/5d-5 — codes everywhere)

`lib/underwriting/statusDisplay.ts` (pure, client-safe) is the single display module: `cardStatusLabel(code)` / `termLoanStatusLabel(code)` (exhaustive switches matching the engine's emitted labels) and `statusTone(code)` → `'qualified' | 'review' | 'declined' | 'na'` (the ONE tone bucketing used by the queue, hero, CRM tables, and credit-reports table). All display surfaces render from **effective codes**; raw decision strings appear only as fallback text for hypothetical rows without codes (defensive — the 5d backfills + 5d-3.5 classify-at-write keep prod at 0 such rows). The 5d-era string matchers (`deriveCardWorkflowPathFromDecision`, `isQualifiedCardsDecisionString`, `normalizeCardsDecision`/`normalizeTermLoanDecision`) are deleted.

***

## 5. The `workflow` object (products + overlays model)

Shape (persisted on `underwritingResults.workflow`, `null` when pre-workflow):

```ts theme={null}
{
  primaryPaths: PrimaryPath[],   // ordered; see sequencing rules
  overlays: Overlay[],           // zero or more, attached to specific primaries
  declineReason?: 'repair_referral' | 'no_fit' | 'insufficient_income'
                  | 'duplicate_file' | 'other',   // only when primaryPaths === ['decline']
  declineReasonNotes?: string,   // required when declineReason === 'other'
  parkedReason?: 'pending_seasoning' | 'recent_credit_activity' | 'thin_file',
  parkedCallbackDate?: number,   // unix ms; when Funding Queue resurfaces the file
}

type PrimaryPath = 'card_funding' | 'term_loan' | 'established_funding' | 'parked' | 'decline';
type Overlay = { id: 'paydowns_required' | 'inq_removal_required', appliesTo: PrimaryPath, ... };
```

### Sequencing rules (implemented in `deriveWorkflow`)

1. **`decline` is exclusive at the file level** — if present, it's the only primary. A declined individual product does not block another actionable product (cross-track non-exclusivity, §9).
2. **`parked` is exclusive** — the file is waiting; no funding primaries co-occur.
3. **`term_loan` runs before `card_funding`** — the term loan must fund before card stacking (frequently the TL proceeds ARE the paydown funding source; see §6 spines).
4. **Cross-track (Creative vs Revenue Based) is operator-driven** — both primaries are emitted; the UI shows two start buttons; both started → tabbed UI (Tab 1 Creative, Tab 2 Revenue Based). Shared steps (`approve_underwriting`, `send_agreement`) de-duplicate.
5. When an operator confirmed BOTH tracks, the single-value `resolveFundingWorkflowPath` selector lets **Creative win** (locked decision — preserve).

### Pre-workflow states (`workflow === null`)

* **`Manual Review - *`** — operator must revise/override (re-runs routing) or close (→ decline). Surface: `NextActionCard` "needs operator review" + `ReviseUnderwritingDialog`.
* **`Credit Not Found`** — bureau couldn't match the PII; a data-mismatch issue, NOT a thin-file credit issue. Surface: "Re-try Credit Pull" prompt. (Locked 2026-04-27 — earlier design routed this to `parked/thin_file`; reversed.)

### Parked semantics

* `pending_seasoning` — accounts need to age; passive wait; default callback +6 months.
* `recent_credit_activity` — too many new accounts/inquiries; wait AND don't open new credit; callback = `max(youngestAccountOpenedDate + 13 months, today + 30 days)`.
* `thin_file` — not enough history to underwrite; client actively builds credit; callback +6 months.
* All parked reasons share ONE GHL pipeline stage; `parkedReason` is a contact field/tag, not a stage. Funding Queue (Brock's feature) batch-surfaces parked files at their callback dates.

### Decline semantics

* Reasons: `repair_referral` (ASAP Credit Repair referral; the referral steps live in the decline spine), `no_fit`, `insufficient_income`, `duplicate_file`, `other` (+ freeform notes).
* **Only `repair_referral` and `no_fit` are emitted by the engine today.** `insufficient_income` / `duplicate_file` / `other` are **forward-reserved for operator/automation declines — NEVER delete them** (schema validator + `automationOutcome.ts` depend on them).
* Decline is terminal: one generic email template, no nurture, no follow-up. A returning client re-enters as a new lead.

### GHL pipeline mapping

Engine is the source of truth; GHL stages mirror it via sync (`syncDecisionToGhl`), never the reverse. Pipeline IDs are **per-location** — resolve via `getGHLPipelineStages` (name-match), never hardcode. Parked uses one stage for all reasons.

### Funding workflow spines (operator step checklists)

Rendered by `components/funding/FundingWorkflow.tsx` (static template lookup keyed by `fundingWorkflowPath`) with steps advanced via `completedWorkflowSteps`. Steps named `send_*`/`collect_*` are **trigger-only** (server assembles content; the step is a button — do NOT build composition UIs). The as-designed spines (Tier-2 walkthroughs, 2026-04-23 — some steps shipped, some still design-only; the shipped templates are the authority in `FundingWorkflow.tsx`):

* **card\_funding:** approve\_underwriting → send\_agreement → *(overlay slots: paydowns\_required, inq\_removal\_required — both block submit)* → submit\_processing (partner: **7 Figures Funding**) → wait\_card\_stacking\_results (24–72h; returns a funding plan of 4–7 card applications) → deliver\_funding\_plan (trigger-only) → track\_card\_statuses (daily loop, soft cutoff prompt day 7) → collect\_invoice (GHL invoice, `operatorFeePercent × totalApprovedAmount`).
* **term\_loan** (partner: **Engine by MoneyLion**, `engine.tech` — API-mediated offer fan-out; Engine v2 create-lead is the ONLY term-loan path, v1 removed May 2026): approve\_underwriting → send\_agreement → *(inq\_removal overlay slot)* → generate\_term\_loan\_offers → review\_and\_select\_offer (with client, by phone, BEFORE any email) → send\_offer\_link → await\_acceptance → wait\_for\_funding → collect\_invoice (`feePct × termLoanAmount`) → send\_next\_steps.
* **`established_funding`** (Revenue Based, LENDER-PAID — no client invoice): approve\_underwriting → send\_agreement → gather\_documentation (MCA: 3–6 mo bank statements; SBA: tax returns/P\&L/balance sheet) → submit\_to\_partner (system filters FM Lender DB by sub-variant + eligibility; manual email submission) → handoff\_to\_lender → record\_payment\_received (manual GHL payment record). The current approval dialog offers confirmable MCA and BLOC only; `selectedFundingTypes` records the product, while retained `establishedSubVariant: 'mca'|'sba'|'both'` maps BLOC through the legacy `'sba'` branch for the shared lender path. SBA remains non-confirmable until its questionnaire ships.
* **parked:** send\_parked\_email (one template per `parkedReason`) → mark\_parked (atomic: GHL stage + reason tag + callback date).
* **decline:** *(repair\_referral only:)* send\_repair\_referral (ASAP link) → mark\_in\_credit\_repair → *(all reasons:)* send\_decline\_email → close\_file.

### Compensation models

* **Client-paid** (`term_loan`, `card_funding`): operator invoices client \~9% (per-operator configurable) of funding received, via GHL invoicing at each `collect_invoice` step.
* **Lender-paid** (`established_funding`): lender pays the operator a commission directly; operator records receipt manually in GHL. No client invoice — never build one for this primary.
* FM → operator reconciliation (internal, NOT workflow): FM charges operators 15% of received compensation on portions where FM (7 Figures) did the processing.
* One consultancy agreement template covers all primaries; `send_agreement` de-duplicates across active primaries.

### Partners (authoritative list = FM Lender DB; this is orientation only)

Card stacking: **7 Figures Funding**. Term loans: **Engine by MoneyLion** (API). MCA: Credibly, Kapitus, OnDeck. SBA: Cadence Bank, Grasshopper Bank. Credit repair referral: **ASAP Credit Repair USA** (per-operator affiliate links). All non-Engine submissions are manual email.

***

## 6. Coaching layer (`nextActions`)

* The engine emits `nextActions[]` (action types: `credit_repair`, `credit_repair_required`, `income_verification_required`, `starter_step`, `wait_for_seasoning`, `approved_aged_lates`, `borderline_score_approved`, `paydown_required`, `paydown_strategy`, `dispute_inquiries`, `complex_review`, `prime_approval`, `mca_option`, `tl_approved_low_income`, `tl_approved_seasoned_unsecured`, `tl_approved_moderate_dti`).
* The 5 operator "Priority" cards (`prime_approval`, `paydown_strategy`, `wait_for_seasoning`, `mca_option`, `credit_repair_required`) are emitted by `deriveCoachingNextAction` over the internal `CoachingPathCode` (`prime | near_prime | seasoning | mca_nuclear | credit_repair | thin_file`) inside `deriveWorkflow`. `coachingPath` is **never persisted** (the `fundingPath` string was dropped in Phase 5b).
* `components/funding/FundingRecommendations.tsx` renders them: `ACTION_CONFIG` dispatcher + `collapseActions` (`HARD_SUPPRESSOR_PRECEDENCE` = `credit_repair_required`/`complex_review`/`starter_step` render alone; `SOFT_MERGE_PRIMARIES` absorb a co-firing `credit_repair` advisory via the caution-swap).
* Qualification-nuance cards (`borderline_score_approved`, `approved_aged_lates`) collapse into their co-firing primary card via the `QUALIFICATION_NUANCE` rule in `collapseActions` (task QW-1, PR #1116) — the nuance surfaces as caution context on the primary instead of a competing Priority card.

***

## 7. Per-Product Decision Model (Phase 9 complete)

Roadmap **Phase 9** implemented the model locked in Phase 9-pre by ADRs 0002–0009. `evaluateEstablished` returns per-product verdicts (real BLOC evaluator, tiered MCA revenue), `deriveProductDecisions` (`convex/lib/productDecisions.ts`) assembles `ProductDecision[5]`, and `storeResults` persists it on `underwritingResults.products` (`convex/schemas/creditFunding.ts`). `deriveWorkflow` consumes those decisions through `deriveWorkflowFromProducts`; the resulting `workflow` is presentation/sequencing only.

### The pre-Phase-9 gaps it closed

Before Phase 9, the products+overlays model still folded everything into a single file-level verdict in three ways:

1. **`decline`/`parked` were file-level exclusive vetoes.** The workflow-level `hasCreditRepairHardStop` short-circuit returned `primaryPaths: ["decline"]` before any product was considered, contradicting the locked 2026-04-27 "not strictly exclusive across tracks" decision (§9). Phase 9c deleted that veto; the same-named coaching computation remains.
2. **`primaryPaths` listed only qualifying products.** A declined product was an *absence*, not a verdict with reasons, so "Cards: declined because X; term loan: qualified" could not be rendered.
3. **MCA/SBA were collapsed into one `established_funding` primary and BLOC was omitted.** `evaluateEstablished` returned two booleans, and the SBA gate silently covered BLOC.

Important boundary: `workflow.primaryPaths.includes("established_funding")` means "at least one Revenue Based product can proceed." It does **not** mean MCA, SBA, and BLOC are all eligible. Per-product consumers read `products[]`; the legacy `mcaPossible` / `sbaPossible` columns remain compatibility fields, not current eligibility authority. The five-product panel projects cards/TL through the override-aware effective status codes, while Revenue Based MCA/BLOC/SBA verdicts read `products[]` directly. MCA and BLOC are confirmable when their product status is `qualified` or `qualified_conditional`; SBA shows a disabled Coming Soon questionnaire CTA and is not confirmable (ADR-0003).

### The shape

```ts theme={null}
type FundingProduct =            // matches lib/config/fundingTypes.ts keys
  | "card_stacking" | "term_loan" | "mca" | "sba" | "bloc";

type ProductDecisionStatus =
  | "qualified"
  | "qualified_conditional"   // gated by a prerequisite (inq removal, paydowns, seasoning)
  | "manual_review"
  | "declined"
  | "not_evaluated";          // not applicable this run (e.g. no business info yet)

type ProductDecision = {
  product: FundingProduct;
  status: ProductDecisionStatus;
  statusCode?: CardStatusCode | TermLoanStatusCode | EstablishedStatusCode;
                                     // evaluator-specific code, kept for display/routing within the product
  reasons: ProductReasonCode[];      // scoped to THIS product (cards side seeds from CardDeclineReasonCode)
  prerequisites: Prerequisite[];     // per product. NOT restricted to qualified_conditional: a DECLINED
                                     // product may carry a time-based prerequisite (thin_file_building,
                                     // recent_activity_cooldown) — that is what makes file-level "parked"
                                     // derivable per locked decision 4 without a router veto.
  estimate?: { low: number; high: number } | null;
};

type UnderwritingDecision = {
  products: ProductDecision[];       // ALWAYS all 5, each with its own verdict
  workflow: DerivedWorkflow;         // derived, presentation/sequencing only — NOT authoritative
};
```

### Locked Phase 9-pre decisions (ADRs 0002–0009, 2026-07-09)

1. **MCA/SBA/BLOC split at the decision layer (ADR-0002).** UX groups them under one Revenue Based tab, but `products[]` carries separate verdicts for `mca`, `sba`, and `bloc`. `workflow.primaryPaths` stays a file-level presentation sequence, never per-product eligibility.
2. **Real BLOC evaluator + SBA questionnaire gate (ADR-0003 / ADR-0009).** BLOC is evaluated independently (660+ / $20k+ / 2yr+). SBA keeps today's baseline gates as a Product Decision but is **not Confirmable** until its questionnaire ships; the UI shows a disabled Coming Soon questionnaire CTA. MCA uses tiered revenue (600–639 → $50k+; 640+ → \$20k+) with 6mo+ TIB.
3. **Credit-repair reasons attach to `card_stacking` + `term_loan` only (ADR-0004).** Not SBA/MCA/BLOC. File-level `hasCreditRepairHardStop` is deleted. Coaching `credit_repair*` nextActions remain; settings referral-link is a follow-up.
4. **Decline and parked are emergent file states (ADR-0005).** Overall decline means no product is qualified, conditionally qualified, or manual review. Overall parked means no product can proceed now, and at least one product carries a time-based prerequisite. Actionable TL/MCA/BLOC means the file is **not** parked. Per-product reasons are visible in the five-product panel.
5. **Operator overrides stay cards + TL only (ADR-0006).** MCA/SBA/BLOC have no revise override.
6. **The UX label is "Revenue Based" (ADR-0007).** Code ids (`established_funding`) remain unchanged.
7. **MCA/BLOC `estimate` plumbing only (ADR-0008).** Null until a follow-up ships formulas; SBA never estimated.

### Four properties this buys

1. **Every product gets a verdict every run.** A declined product is `status: "declined"` with reasons, not an absence. BLOC is a first-class Product Decision with its own evaluator.
2. **Reasons are per-product.** A credit-repair reason attaches to `card_stacking.reasons` and `term_loan.reasons` (ADR-0004) but NOT to MCA/SBA/BLOC. Example: `card_stacking` → declined `[severe_lates_repair]`; `term_loan` → declined `[severe_lates_repair]` or own TL reason; `mca` → qualified; `sba` → declined `[score]` (baseline); `bloc` → qualified independently.
3. **The file-level short-circuit is gone.** `hasCreditRepairHardStop → ["decline"]` is deleted; "overall decline" is *emergent* (all products declined/not\_evaluated). File-level `parked` is emergent too (ADR-0005).
4. **`deriveWorkflow` is a pure function over `products[]`** — sequences and groups products for downstream GHL/automation mapping; it can never override a verdict.

### Phase 9 implementation notes

* 9a (DONE, #1182) was additive: emit and persist `products[]` while leaving existing booleans, status codes, and `workflow` behavior intact. BLOC is a **real** evaluator (ADR-0003/0009), not an SBA mirror.
* 9b (#1196/#1203) backfilled historical `products[]` from the persisted typed cards/TL codes plus `mcaPossible`/`sbaPossible`; historical rows had no BLOC signals, so `bloc` was `not_evaluated`.
* 9c (#1213) made the deliberate behavior change: deleted the workflow-level repair veto and derived `workflow` from `products[]`, with only the documented golden deltas.
* 9d (#1281 verification) confirmed cards/TL overrides remain compatible without code changes. Overrides still apply only to cards/TL (ADR-0006).
* 9e (#1281) added the override-aware five-product panel, split Revenue Based into MCA/BLOC/SBA rows, made MCA/BLOC confirmability `products[]`-driven, kept SBA disabled Coming Soon/non-confirmable, and cut credit-report tables plus coaching/chat/MCP consumers over. GHL sync and automation already consumed the emergent persisted `workflow`; no new stages or sync code were required. The engine and golden snapshots remained byte-identical.

### Migration seam

The legacy `workflow` object is **derivable from `products[]`**, which let existing UI + GHL sync keep working during the surface-by-surface consumer cutover. Current consumers use `products[]` where product detail matters; the persisted `workflow` remains the sequencing and downstream GHL/automation projection. Phase 5d was the substrate: per-product = "extend the persisted typed codes from 2 products to 5, and stop letting the router override them."

***

## 8. Data writers — everything that writes decision fields

This inventory matters because the "every row carries typed codes" invariant is only as good as its **writers**. Any writer of `underwritingDecision`/`termLoanDecision` that does not also write the codes re-contaminates the table.

| Writer                 | Path                                                                                                                                                                                                                                                                         | Writes codes?           | Notes                                                                                                                                                                |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Engine persist         | `storeResults` — `convex/lib/underwritingStore.ts`                                                                                                                                                                                                                           | ✅                       | From `fullResults.cardStatus.code`/`termLoanStatus.code`.                                                                                                            |
| Operator revise        | `reviseUnderwriting` — `convex/lib/underwritingOperator.ts`                                                                                                                                                                                                                  | ✅                       | Override strings + code mirrors on every revise.                                                                                                                     |
| Zoho backfill importer | `backfillUnderwritingFromZoho` — registered in `convex/creditReports.ts`, body in `convex/lib/creditReports/mutations.ts` (`backfillUnderwritingFromZohoConfig`); called from `app/api/zoho/credit-reports/import/route.ts` and `convex/migrations/creditReportMigration.ts` | ✅ (since 5d-3.5, #1119) | Classifies at write time via `deriveStatusCodesForRow` (`convex/lib/underwritingStatusBackfill.ts`); unmappable strings insert without the code column (tolerant).   |
| Migration upsert       | `upsertMigratedUnderwritingResult` — registered in `convex/creditReports.ts`, body in `convex/lib/creditReports/mutations.ts`; called from `convex/migrations/creditReportMigration.ts`                                                                                      | ✅ (since 5d-3.5, #1119) | Same classify-at-write helper, both patch and insert branches.                                                                                                       |
| Zoho round-trip        | `app/api/zoho/credit-report/route.ts`                                                                                                                                                                                                                                        | ✅                       | Writes spaced canonical card labels to Zoho field `Initial_Revolving_Underwriting_Decision`; Phase 5d-5 drained the historical no-space round-trip values from prod. |
| One-off backfills      | `oneOffMigrations.backfillStatusCodes` / `backfillOverrideStatusCodes`                                                                                                                                                                                                       | ✅ (they only ADD codes) | Tolerant, paginated, idempotent; safe to re-run any time; report `unmappedCard`/`unmappedTermLoan` + samples.                                                        |
| Demo seed              | `convex/seedDemoData.ts`                                                                                                                                                                                                                                                     | ✅                       | Classifies its decision strings → codes + persists `fundingWorkflowPath` (since 5d-3).                                                                               |

***

## 9. Locked decisions (currently operative)

Distilled from the retired Decisions Log — every entry below is the **current** state of its topic. Chronology + superseded intermediate states: git history of `docs/funding-workflow-paths.md`. Do not silently change any of these (see `VISION.md` spec gate).

**Model & routing**

1. **Typed status codes are the routing authority; decision strings are display labels** (Phase 1 design lock, realized in 5d-3). Never route on strings.
2. **Products + overlays**, not a flat decision enum: `primaryPaths[]` + composable `overlays[]`. The 3 legacy card templates collapsed into one `card_funding` primary + 2 overlays.
3. **Term loan is its own primary** (not a card overlay); sequences before `card_funding`. `paydowns_required` NEVER applies to `term_loan` (the TL usually *funds* the paydowns). `inq_removal_required` applies to both; in the combined case it attaches to `term_loan` only (TL runs first; cards pick up the improved profile).
4. **Revenue Based stays grouped at the workflow layer** — one `established_funding` primary sequences MCA/BLOC/SBA lender work. The retained `establishedSubVariant: 'mca'|'sba'|'both'` machine field maps BLOC through the legacy `'sba'` branch, while `selectedFundingTypes` distinguishes new BLOC selections. The current approval dialog confirms MCA/BLOC only; SBA is questionnaire-gated. A workflow primary is file-level presentation and must never be reused as proof that every Revenue Based product is eligible.
5. **Cross-track non-exclusivity (2026-04-27):** a file may be cards-declined (even credit-repair-class) while term loan or a Revenue Based product legitimately proceeds. The strict "credit repair blocks everything" rule was dropped on audit evidence (0.05% incidence). Phase 9c removed the contradictory workflow-level `hasCreditRepairHardStop`; the same-named coaching-path computation remains by ADR-0004.
6. **Credit-repair-as-decline blanket rule DROPPED** (2026-04-27): evaluators keep emitting `creditRepairFlag` on qualified decisions. When decline is emergent, `deriveWorkflowFromProducts` selects `repair_referral` if the cards product carries a `credit_repair` reason or its decline code is repair-referral-class; otherwise it selects `no_fit`.
7. **Manual review is pre-workflow** (`workflow: null`), never a decline reason. Operator revises (re-routes) or closes (standard decline).
8. **`Credit Not Found` is pre-workflow** (`workflow: null`), NOT `parked/thin_file` — it's a PII-match failure needing a re-pull, not a credit judgment.
9. **`parkedReason` enum is final:** `pending_seasoning | recent_credit_activity | thin_file`. `recent_credit_activity` merged the earlier `too_many_new_accounts`/`too_many_recent_inquiries`. Callback formula for `recent_credit_activity`: `max(youngestAccountOpenedDate + 13mo, today + 30d)`.
10. **`declineReason` enum is final:** `repair_referral | no_fit | insufficient_income | duplicate_file | other` (+`declineReasonNotes`). The last three are **forward-reserved** (operator/automation declines) — never delete.
11. **`repair_referral` stays in decline** (not parked): ASAP takes over the relationship; stale queue files add noise.
12. **Decline is terminal** — one generic email, no nurture. Returning clients re-enter as new leads.
13. **Replace, don't wrap:** legacy path systems get absorbed and deleted, never wrapped (this killed `fundingPath.ts`, the `workflowPath` column, the persisted `fundingPath` string, `reconcileSummaryRecommendation`/`summaryRecommendation`).
14. **`fundingCategories` is presentation-only** — zero routing authority.
15. **Both-tracks-confirmed → Creative wins** the single-value `fundingWorkflowPath` selector.
16. **Override codes persist ALONGSIDE override strings** (compat), with a later cleanup to drop the strings once nothing reads them.
17. **Strict classifiers stay loud; legacy tolerance lives ONLY in the tolerant migration/override wrappers.** Live engine output must fail loudly on anything new.
18. **5d-2b legacy string mappings are locked** (see §4 tolerant-wrapper row): incl. `Likely`→`tl_qualified`, `Unlikely`→`tl_declined`, `Qualified w/ Credit Repair`→`qualified`, TL-in-card-field → card `doesnt_qualify` + recovered TL code.

**Process & safety**

19. **Golden snapshots are the safety gate** for all behavior-preserving work: `tests/__snapshots__/underwriting.test.ts.snap` + `realFileGoldens.test.ts.snap` byte-identical for `workflow` + `nextActions`. Deliberate deltas use `bun test -u` + documented diffs in the PR. Phase 9c carried the documented behavior deltas; the 9d/9e compatibility and consumer cutover kept both snapshots byte-identical.
20. **Backfills run to `hasMore:false` on prod before any phase that depends on them**; all backfills tolerant + paginated + idempotent. Prod runs need the user's explicit go-ahead.
21. **bun only**; `bunx convex codegen` before `bunx tsc --noEmit`; don't stage `convex/_generated/api.d.ts` drift. Commit author `Brock Jeppesen <brock@7figures.com>`; never commit to `main`; one branch/PR per sub-phase. Convex auto-deploys to prod on `main` pushes touching `convex/**`.
22. **`mock.module()` is process-global** in `bun test` — always spread the real module (`const actual = await import(...)`; `mock.module(path, () => ({ ...actual, ...overrides }))`).
23. **User-facing copy says "CRM", never "GHL"/"GoHighLevel".**
24. **The what-if override seam** (`docs/adr/0001-underwriting-whatif-override-seam.md`) must be preserved: `runUnderwriting`/`computeCreditMetrics` accept optional `MultiplierOverrides` defaulting to live constants; a no-knob run is byte-identical.

***

## 10. Known contradictions & gaps (each mapped to a roadmap task)

Open gaps only — resolved gaps live in the roadmap's Done table and git history.

| # | Gap                                                                                                                                                                                                                                                                                         | Where   | Fixed by     |
| - | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ------------ |
| 4 | God-files: `runUnderwriting.ts` (\~1,400), `useContactData.ts` (\~900), `underwritingQueue.ts` (\~850); UI: `EstablishedWorkflowDialog` (1,701), `FundingPlanTable` (1,306), `FundingPlanRow` (1,142), `app/admin/internal/processing/page.tsx` (\~1,300+, duplicates `ContactPageContent`) | various | Phases 7 & 8 |

***

## 11. Code index (symbol-anchored jump table)

| What                                         | Where (file → symbol)                                                                                                                                                                                                                                                                                                                                                                                                             |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| HTTP entry                                   | `convex/underwriting.ts` → `analyzeCreditReport` (thin wrappers only; all 23 registered functions stay here)                                                                                                                                                                                                                                                                                                                      |
| Ingest adapter                               | `convex/lib/underwritingIngest.ts` → `analyzeCreditReportImpl`                                                                                                                                                                                                                                                                                                                                                                    |
| Pure orchestrator                            | `convex/lib/runUnderwriting.ts` → `runUnderwriting`                                                                                                                                                                                                                                                                                                                                                                               |
| Threshold resolution                         | `convex/lib/resolveThresholds.ts` → `resolveThresholds`                                                                                                                                                                                                                                                                                                                                                                           |
| Cards evaluator                              | `convex/lib/cardUnderwriting.ts` → `evaluateCards`                                                                                                                                                                                                                                                                                                                                                                                |
| Term-loan evaluator                          | `convex/lib/termLoanUnderwriting.ts` → `evaluateTermLoan`                                                                                                                                                                                                                                                                                                                                                                         |
| Established gates (per-product)              | `convex/lib/establishedUnderwriting.ts` → `evaluateEstablished` (returns `mca`/`sba`/`bloc` verdicts + strangler booleans)                                                                                                                                                                                                                                                                                                        |
| Product-decision assembler                   | `convex/lib/productDecisions.ts` → `deriveProductDecisions`                                                                                                                                                                                                                                                                                                                                                                       |
| Credit-repair flag                           | `convex/lib/creditRepairFlag.ts`; post-pass in `runUnderwriting.ts` → `resolveCreditRepairPostPass`                                                                                                                                                                                                                                                                                                                               |
| Workflow router                              | `convex/lib/deriveWorkflow.ts` → `deriveWorkflow`, `deriveCoachingPath`, `deriveCoachingNextAction`                                                                                                                                                                                                                                                                                                                               |
| Typed taxonomy (pure)                        | `convex/lib/underwritingStatus.ts` (see §4 inventory)                                                                                                                                                                                                                                                                                                                                                                             |
| Convex validators for the codes              | `convex/lib/underwritingStatusValidators.ts`                                                                                                                                                                                                                                                                                                                                                                                      |
| Routing predicates (shared)                  | `lib/underwriting/workflowState.ts` (see §4)                                                                                                                                                                                                                                                                                                                                                                                      |
| Effective-code resolvers                     | `convex/lib/underwritingOperator.ts` → `getEffectiveCardStatusCode` / `getEffectiveTermLoanStatusCode`                                                                                                                                                                                                                                                                                                                            |
| Persistence                                  | `convex/lib/underwritingStore.ts` → `storeResults`                                                                                                                                                                                                                                                                                                                                                                                |
| GHL sync                                     | `convex/lib/underwritingGhlSync.ts` → `syncDecisionToGhl`                                                                                                                                                                                                                                                                                                                                                                         |
| Operator mutations (handlers)                | `convex/lib/underwritingOperator.ts` (approve / approve-established / revise / park / decline / revoke / …)                                                                                                                                                                                                                                                                                                                       |
| Queue bucketing                              | `convex/lib/underwritingQueue.ts` → `getOperatorQueue`                                                                                                                                                                                                                                                                                                                                                                            |
| One-off migrations (registered)              | `convex/oneOffMigrations.ts` (shared helpers only in `convex/migrations/`)                                                                                                                                                                                                                                                                                                                                                        |
| Result schema                                | `convex/schemas/creditFunding.ts` → `underwritingResults` (schema split into `convex/schemas/` domain modules by #1178)                                                                                                                                                                                                                                                                                                           |
| Public result type                           | `lib/types/underwriting-result.ts`                                                                                                                                                                                                                                                                                                                                                                                                |
| Trace narration                              | `convex/lib/buildUnderwritingTrace.ts`                                                                                                                                                                                                                                                                                                                                                                                            |
| Narration code fallback                      | `lib/underwriting/resolveNarrationCodes.ts`                                                                                                                                                                                                                                                                                                                                                                                       |
| UI: contact data hub                         | `components/contact/hooks/useContactData.ts`                                                                                                                                                                                                                                                                                                                                                                                      |
| UI: contact actions root                     | `components/contact/hooks/useContactActions.ts` (composition over `useCreditReportActions` / `useContactFieldUpdates` / `usePipelineActions` / `useFundingActions`)                                                                                                                                                                                                                                                               |
| UI: hero CTA                                 | `components/contact/NextActionCard.tsx` → `getNextAction`                                                                                                                                                                                                                                                                                                                                                                         |
| UI: coaching cards                           | `components/funding/FundingRecommendations.tsx` → `collapseActions`, `ACTION_CONFIG`                                                                                                                                                                                                                                                                                                                                              |
| UI: workflow checklist                       | `components/funding/FundingWorkflow.tsx`                                                                                                                                                                                                                                                                                                                                                                                          |
| UI: summary card                             | `components/contact/FundingSummaryCard.tsx`                                                                                                                                                                                                                                                                                                                                                                                       |
| UI: status labels + tones                    | `lib/underwriting/statusDisplay.ts` → `cardStatusLabel` / `termLoanStatusLabel` / `statusTone`                                                                                                                                                                                                                                                                                                                                    |
| UI: operator queue                           | `components/funding/OperatorUnderwritingQueue.tsx`                                                                                                                                                                                                                                                                                                                                                                                |
| UI: processing hero                          | `components/processing/ProcessingUnderwritingHero.tsx`                                                                                                                                                                                                                                                                                                                                                                            |
| UI: CRM tables                               | `components/crm/ContactCRMTable.tsx`, `components/credit-reports/CreditReportsTable.tsx`                                                                                                                                                                                                                                                                                                                                          |
| UI: revise dialog                            | `components/funding/ReviseUnderwritingDialog.tsx`                                                                                                                                                                                                                                                                                                                                                                                 |
| Contact-page status derivation (code-driven) | `components/contact/underwritingDecisionStatus.ts` → `deriveCardsStatus` / `deriveTermLoanStatus`                                                                                                                                                                                                                                                                                                                                 |
| Review console                               | `app/admin/internal/underwriting-review/page.tsx`; replay in `convex/lib/underwritingReviewReplay.ts`; ADR `docs/adr/0001-underwriting-whatif-override-seam.md`                                                                                                                                                                                                                                                                   |
| Zoho import surfaces                         | `app/api/zoho/credit-report/route.ts` (round-trip), `app/api/zoho/credit-reports/import/route.ts` (importer), `backfillUnderwritingFromZoho` / `upsertMigratedUnderwritingResult` (registered in `convex/creditReports.ts`, bodies in `convex/lib/creditReports/mutations.ts`). Retirement scoping: `docs/internal/zoho-retirement-deletion-plan.md` (these are LIVE paths; do not gate/delete with the legacy migration wizard). |
| Golden snapshots                             | `tests/__snapshots__/underwriting.test.ts.snap`, `tests/__snapshots__/realFileGoldens.test.ts.snap` (12 personas + 7 real-file goldens)                                                                                                                                                                                                                                                                                           |

***

## 12. Worked examples

### A — Cards qualified w/ paydowns + TL qualified (the common combined case)

FICO 690, util 55%, 1 old 30-day late, income \$120k, DTI 38%, no business revenue.

* Cards → `qualified_paydowns` (+ `paydown_required` action, + orthogonal `creditRepairFlag`)
* TL → `tl_qualified`; Revenue Based → MCA/SBA/BLOC declined (no revenue)
* `workflow`: `primaryPaths: [term_loan, card_funding]`, `overlays: [paydowns_required → card_funding]`
* UI: can-start gate opens (either product qualifies); coaching shows the paydown card with the credit-repair advisory merged in; operator sequence = TL funds → proceeds pay down accounts → verify + re-pull → cards submit.

### B — Recent bankruptcy: cards decline, TL still qualifies (cross-track non-exclusivity)

FICO 700, BK 30 months ago, income \$130k, DTI 35%, clean installments.

* Cards → `doesnt_qualify` (decline code: recent bankruptcy); TL → `tl_qualified` (TL takes no hard-stop input)
* `products[]` records `card_stacking: declined [recent_bankruptcy]` and `term_loan: qualified`; `workflow.primaryPaths: [term_loan]` follows from those verdicts, so the TL path proceeds.
* **Pre-Phase-9 behavior:** the workflow-level `hasCreditRepairHardStop` short-circuit incorrectly returned `[decline]` while the TL code said qualified. Phase 9c removed that contradiction and retained only the same-named coaching classification.
