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 (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
InsiderunUnderwriting (evaluator sequence):
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 inconvex/lib/underwritingOperator.ts) — operator override; does not re-run the engine; patchesoperatorOverrides(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
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
tl_not_applicableis only produced byrunUnderwriting(credit-not-found override setstlDecision = "NA"in lockstep) — the TL evaluator never emits it.qualified_paydownsunifies 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)
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)
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 overCardStatusCode.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, elseterm_loan, else null.resolveFundingWorkflowPath(row)→ the wideFundingWorkflowPath(prime | paydowns | inq_removal | term_loan | parked | established | null). Priority: parked (exclusive) → approved+Creative-selected → approved+Revenue-Based-only → Creative fallback. Theestablishedmember is a retained code id. Non-approved, non-parked rows resolvenull.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):
Sequencing rules (implemented in deriveWorkflow)
declineis 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).parkedis exclusive — the file is waiting; no funding primaries co-occur.term_loanruns beforecard_funding— the term loan must fund before card stacking (frequently the TL proceeds ARE the paydown funding source; see §6 spines).- 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. - When an operator confirmed BOTH tracks, the single-value
resolveFundingWorkflowPathselector 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 toparked/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;
parkedReasonis 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_referralandno_fitare emitted by the engine today.insufficient_income/duplicate_file/otherare forward-reserved for operator/automation declines — NEVER delete them (schema validator +automationOutcome.tsdepend 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 bycomponents/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;selectedFundingTypesrecords the product, while retainedestablishedSubVariant: '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 eachcollect_invoicestep. - 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_agreementde-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 byderiveCoachingNextActionover the internalCoachingPathCode(prime | near_prime | seasoning | mca_nuclear | credit_repair | thin_file) insidederiveWorkflow.coachingPathis never persisted (thefundingPathstring was dropped in Phase 5b). components/funding/FundingRecommendations.tsxrenders them:ACTION_CONFIGdispatcher +collapseActions(HARD_SUPPRESSOR_PRECEDENCE=credit_repair_required/complex_review/starter_steprender alone;SOFT_MERGE_PRIMARIESabsorb a co-firingcredit_repairadvisory via the caution-swap).- Qualification-nuance cards (
borderline_score_approved,approved_aged_lates) collapse into their co-firing primary card via theQUALIFICATION_NUANCErule incollapseActions(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:decline/parkedwere file-level exclusive vetoes. The workflow-levelhasCreditRepairHardStopshort-circuit returnedprimaryPaths: ["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.primaryPathslisted 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.- MCA/SBA were collapsed into one
established_fundingprimary and BLOC was omitted.evaluateEstablishedreturned two booleans, and the SBA gate silently covered BLOC.
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
Locked Phase 9-pre decisions (ADRs 0002–0009, 2026-07-09)
- MCA/SBA/BLOC split at the decision layer (ADR-0002). UX groups them under one Revenue Based tab, but
products[]carries separate verdicts formca,sba, andbloc.workflow.primaryPathsstays a file-level presentation sequence, never per-product eligibility. - Real BLOC evaluator + SBA questionnaire gate (ADR-0003 / ADR-0009). BLOC is evaluated independently (660+ / 50k+; 640+ → $20k+) with 6mo+ TIB.
- Credit-repair reasons attach to
card_stacking+term_loanonly (ADR-0004). Not SBA/MCA/BLOC. File-levelhasCreditRepairHardStopis deleted. Coachingcredit_repair*nextActions remain; settings referral-link is a follow-up. - 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.
- Operator overrides stay cards + TL only (ADR-0006). MCA/SBA/BLOC have no revise override.
- The UX label is “Revenue Based” (ADR-0007). Code ids (
established_funding) remain unchanged. - MCA/BLOC
estimateplumbing only (ADR-0008). Null until a follow-up ships formulas; SBA never estimated.
Four properties this buys
- 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. - Reasons are per-product. A credit-repair reason attaches to
card_stacking.reasonsandterm_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. - The file-level short-circuit is gone.
hasCreditRepairHardStop → ["decline"]is deleted; “overall decline” is emergent (all products declined/not_evaluated). File-levelparkedis emergent too (ADR-0005). deriveWorkflowis a pure function overproducts[]— 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, andworkflowbehavior 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 plusmcaPossible/sbaPossible; historical rows had no BLOC signals, soblocwasnot_evaluated. - 9c (#1213) made the deliberate behavior change: deleted the workflow-level repair veto and derived
workflowfromproducts[], 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 persistedworkflow; no new stages or sync code were required. The engine and golden snapshots remained byte-identical.
Migration seam
The legacyworkflow 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 ofunderwritingDecision/termLoanDecision that does not also write the codes re-contaminates the table.
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 ofdocs/funding-workflow-paths.md. Do not silently change any of these (see VISION.md spec gate).
Model & routing
- Typed status codes are the routing authority; decision strings are display labels (Phase 1 design lock, realized in 5d-3). Never route on strings.
- Products + overlays, not a flat decision enum:
primaryPaths[]+ composableoverlays[]. The 3 legacy card templates collapsed into onecard_fundingprimary + 2 overlays. - Term loan is its own primary (not a card overlay); sequences before
card_funding.paydowns_requiredNEVER applies toterm_loan(the TL usually funds the paydowns).inq_removal_requiredapplies to both; in the combined case it attaches toterm_loanonly (TL runs first; cards pick up the improved profile). - Revenue Based stays grouped at the workflow layer — one
established_fundingprimary sequences MCA/BLOC/SBA lender work. The retainedestablishedSubVariant: 'mca'|'sba'|'both'machine field maps BLOC through the legacy'sba'branch, whileselectedFundingTypesdistinguishes 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. - 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. - Credit-repair-as-decline blanket rule DROPPED (2026-04-27): evaluators keep emitting
creditRepairFlagon qualified decisions. When decline is emergent,deriveWorkflowFromProductsselectsrepair_referralif the cards product carries acredit_repairreason or its decline code is repair-referral-class; otherwise it selectsno_fit. - Manual review is pre-workflow (
workflow: null), never a decline reason. Operator revises (re-routes) or closes (standard decline). Credit Not Foundis pre-workflow (workflow: null), NOTparked/thin_file— it’s a PII-match failure needing a re-pull, not a credit judgment.parkedReasonenum is final:pending_seasoning | recent_credit_activity | thin_file.recent_credit_activitymerged the earliertoo_many_new_accounts/too_many_recent_inquiries. Callback formula forrecent_credit_activity:max(youngestAccountOpenedDate + 13mo, today + 30d).declineReasonenum is final:repair_referral | no_fit | insufficient_income | duplicate_file | other(+declineReasonNotes). The last three are forward-reserved (operator/automation declines) — never delete.repair_referralstays in decline (not parked): ASAP takes over the relationship; stale queue files add noise.- Decline is terminal — one generic email, no nurture. Returning clients re-enter as new leads.
- Replace, don’t wrap: legacy path systems get absorbed and deleted, never wrapped (this killed
fundingPath.ts, theworkflowPathcolumn, the persistedfundingPathstring,reconcileSummaryRecommendation/summaryRecommendation). fundingCategoriesis presentation-only — zero routing authority.- Both-tracks-confirmed → Creative wins the single-value
fundingWorkflowPathselector. - Override codes persist ALONGSIDE override strings (compat), with a later cleanup to drop the strings once nothing reads them.
- Strict classifiers stay loud; legacy tolerance lives ONLY in the tolerant migration/override wrappers. Live engine output must fail loudly on anything new.
- 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 → carddoesnt_qualify+ recovered TL code.
- Golden snapshots are the safety gate for all behavior-preserving work:
tests/__snapshots__/underwriting.test.ts.snap+realFileGoldens.test.ts.snapbyte-identical forworkflow+nextActions. Deliberate deltas usebun 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. - Backfills run to
hasMore:falseon prod before any phase that depends on them; all backfills tolerant + paginated + idempotent. Prod runs need the user’s explicit go-ahead. - bun only;
bunx convex codegenbeforebunx tsc --noEmit; don’t stageconvex/_generated/api.d.tsdrift. Commit authorBrock Jeppesen <brock@7figures.com>; never commit tomain; one branch/PR per sub-phase. Convex auto-deploys to prod onmainpushes touchingconvex/**. mock.module()is process-global inbun test— always spread the real module (const actual = await import(...);mock.module(path, () => ({ ...actual, ...overrides }))).- User-facing copy says “CRM”, never “GHL”/“GoHighLevel”.
- The what-if override seam (
docs/adr/0001-underwriting-whatif-override-seam.md) must be preserved:runUnderwriting/computeCreditMetricsaccept optionalMultiplierOverridesdefaulting 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.11. Code index (symbol-anchored jump table)
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_requiredaction, + orthogonalcreditRepairFlag) - 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[]recordscard_stacking: declined [recent_bankruptcy]andterm_loan: qualified;workflow.primaryPaths: [term_loan]follows from those verdicts, so the TL path proceeds.- Pre-Phase-9 behavior: the workflow-level
hasCreditRepairHardStopshort-circuit incorrectly returned[decline]while the TL code said qualified. Phase 9c removed that contradiction and retained only the same-named coaching classification.

