A production-grade mortgage underwriting engine, CLI, and persistence layer. Built around explicit domain models, deterministic decimal math, and a typed computational DAG with first-class eligibility checks. 690+ tests, zero skipped.
Mortgage underwriting is full of program-specific edge cases (FHA MIP schedules, VA IRRRL fee tiers, USDA upfront / annual splits, jumbo tier overlays, AUS-determined reserves with floor layering, multi-borrower FICO blending, departure-residence rental income, self-employed 24-month averaging) that get hand-coded into spreadsheets and one-off scripts. LoanScope treats every rule as a first-class declarative ProgramRules value, every calculation as a node in a typed DAG, and every check as a graph edge — so adding a new program or refining an existing one is a contained change with explicit test coverage rather than a search-and-edit across a large procedural codebase.
- 13 workspace packages under
@loanscope/*, each with a single responsibility and explicit dependency direction (domain->math/graph/program-rules->calculations/products/lenders->engine/compare/sim->cli). - Full Agency / Government / Portfolio coverage: Fannie, Freddie, FHA (incl. Streamline), VA (incl. IRRRL), USDA (incl. Streamline), UWM Jumbo Pink, Prime Jumbo, Prime Jumbo Max.
- Branded primitive types for every domain quantity (
Money,Ratio,RatePct,Months,Units,Fico) so unit confusion is caught at compile time. - Deterministic decimal math via
decimal.jsend-to-end; no floating-point drift in qualifying-payment, LTV, or fee calculations. - Typed DAG evaluator with provenance tracking, scoped evaluation, and a
BlockedNodesurface for missing inputs (no silent defaults). - 18 first-class eligibility checks (LTV, CLTV, DTI, FICO, loan amount, reserves, cash-to-close, occupancy, purpose, property type, units, borrower restrictions, state restrictions, cash-out, buydown, MI, AUS, appraisal) with margin computation for goal-seek.
- SQLite + Drizzle persistence adapter for scenarios, comparisons, simulations, custom product sets, catalog imports with content-hashing, scenario versioning with restore, and reproducible audit sessions. Adapter layer only; underwriting rules never live in SQL.
- CLI commands:
quote,evaluate,compare,goalseek,simulate,diff,batch,export-scenario, plus 30+dbsubcommands for persistence management. - Comparison grids and goal-seek across 12 dimensions (term, rate, LTV, loan amount, occupancy, products, lenders, borrower sets, FICO, down payment, property type, loan purpose).
- Simulation engine with deterministic cash accounting and Pareto-ranked outcomes for "what if I pay down $X" / "what if I add a co-borrower" exploration.
- Strict TypeScript:
strict,noUncheckedIndexedAccess,exactOptionalPropertyTypes,noImplicitReturns,noFallthroughCasesInSwitch,noImplicitOverride. Zeroany, zero@ts-ignorein production source.
- Node.js
>=22 - pnpm
10.x
pnpm installQuick quote:
pnpm loanscope quote --loan 1000000 --price 1250000 --fico 740Quick quote with ARM inputs:
pnpm loanscope quote --loan 1000000 --price 1250000 --fico 740 --program ARM --arm-fixed 60Full evaluation from config:
pnpm loanscope evaluate --config packages/loanscope/cli/scenarios/default.yamlGrid comparison:
pnpm loanscope compare --ltv 0.75:0.95:0.05 --terms 360,480Goal seek:
pnpm loanscope goalseek max-loan --product uwm_jumbo_pinkSimulation:
pnpm loanscope simulate --config packages/loanscope/cli/scenarios/default.yamlOutput formats:
pnpm loanscope quote --loan 1000000 --price 1250000 --fico 740 --output json
pnpm loanscope evaluate --config packages/loanscope/cli/scenarios/default.yaml --output csvSupported output values are table, json, and csv.
The loanscope db subcommands manage the local SQLite database used for persistent lender catalogs, presets, custom product sets, and (incrementally) saved scenarios, comparisons, simulations, and audit sessions. The default database path is loanscope.db in the working directory; override with --path <file>.
Initialize and seed:
pnpm loanscope db init
pnpm loanscope db seedInspect:
pnpm loanscope db status
pnpm loanscope db list-lenders
pnpm loanscope db list-scenariosPersistence is an adapter layer; underwriting rules do not live in SQL.
Config-backed commands read YAML files through @loanscope/config.
Common command-level overrides include:
--rate--term--program--arm-fixed--lender--products--product-source
For ARM scenarios, --program ARM requires --arm-fixed, and --arm-fixed may only be used with --program ARM.
Example scenario:
id: default-scenario
scenario:
loanPurpose: Purchase
occupancy: Primary
propertyType: SFR
purchasePrice: 1250000
requestedLoanAmount: 1000000
rateNote:
noteRatePct: 6.875
amortizationMonths: 360
monthlyHousing:
propertyTax: 1042
insurance: 365
borrowers:
- id: b1
fico: 740
incomes:
- id: inc1
type: W2
monthlyAmount: 24405
assets:
- id: checking
type: Checking
amount: 500000
variants:
- id: solo
label: Single borrower
includedBorrowerIds: [b1]Workspace layout:
packages/loanscope/
domain/ Branded primitives, enums, canonical model interfaces. Zero runtime deps.
graph/ Generic typed DAG evaluator with provenance and scoped evaluation.
math/ Pure mortgage math primitives (PMT, LTV, DTI, reserves, FHA/VA/USDA fees).
program-rules/ Canonical ProgramRules resolver primitives (toProgramRules, mergeRules, resolveVariant).
calculations/ Computational graph node/edge definitions wiring math into the DAG.
config/ YAML/JSON scenario parser with zod boundary validation.
products/ Declarative Agency/Government/Portfolio product catalogs.
lenders/ Lender registry with preset and custom product-source resolution.
engine/ In-memory evaluation orchestrator (tier resolution, evaluateAll, scoped responses).
compare/ Comparison grid builder, executor, and goal-seek.
sim/ Borrower-action simulator with Pareto-ranked outcomes.
db/ SQLite + Drizzle persistence adapter (schema, repos, mappers, audit).
cli/ Commander-based CLI surface (the user-facing edge).
Each package has a focused responsibility and explicit dependency direction.
Useful scripts:
pnpm loanscope --help- CLI is the user surface. The
@loanscope/clibinary is the only maintained entry point. Theapps/webNext.js app exists as a workspace peer but is not the focus of this repository. - Eligibility, not pricing. The engine determines whether a transaction passes program rules and at what margin. Rates are user-supplied inputs; no lender rate sheets, no pricing adjustments, no LLPAs.
- Declarative rules only. Every program rule lives in a typed
ProgramRulesvalue with explicit overrides per variant, occupancy, and loan-amount tier. No runtime rule DSL, no spreadsheet imports, no string-based rule evaluation. - Deterministic. Same inputs produce byte-identical outputs. No randomized tie-breaks, no clock-dependent branches outside the audit layer.
- SQLite as an adapter. Persistence is bolt-on; the engine has no SQL dependency. Underwriting rules never live in the database.
- Example scenarios and catalogs (parseable by the CLI):
examples/README.md - Test-only fixtures (referenced by
*.test.tsonly):fixtures/README.md
See CHANGELOG.md for repo-wide history. Per-package changelogs are generated by Changesets and live alongside each package at packages/loanscope/<name>/CHANGELOG.md.
MIT