compctl 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ /**
3
+ * Compliance check implementations
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ADVERSE_ACTION_REASONS = void 0;
7
+ exports.checkTridTiming = checkTridTiming;
8
+ exports.checkAtrQm = checkAtrQm;
9
+ exports.checkEcoa = checkEcoa;
10
+ exports.generateAdverseAction = generateAdverseAction;
11
+ /**
12
+ * Check TRID timing requirements
13
+ */
14
+ function checkTridTiming(data) {
15
+ const findings = [];
16
+ const appDate = new Date(data.applicationDate);
17
+ const today = new Date();
18
+ // LE must be provided within 3 business days of application
19
+ const leDeadline = addBusinessDays(appDate, 3);
20
+ const daysUntilLeDeadline = businessDaysBetween(today, leDeadline);
21
+ let leCompliant = true;
22
+ if (data.leDisclosureDate) {
23
+ const leDate = new Date(data.leDisclosureDate);
24
+ if (leDate > leDeadline) {
25
+ leCompliant = false;
26
+ findings.push({
27
+ code: 'TRID-LE-001',
28
+ severity: 'critical',
29
+ description: 'Loan Estimate not provided within 3 business days',
30
+ remediation: 'Issue LE immediately and document reason for delay',
31
+ });
32
+ }
33
+ }
34
+ else if (daysUntilLeDeadline < 0) {
35
+ leCompliant = false;
36
+ findings.push({
37
+ code: 'TRID-LE-002',
38
+ severity: 'critical',
39
+ description: 'Loan Estimate deadline has passed',
40
+ remediation: 'Issue LE immediately',
41
+ });
42
+ }
43
+ // CD must be provided at least 3 business days before closing
44
+ let cdCompliant = true;
45
+ let daysUntilCdDeadline;
46
+ if (data.closingDate) {
47
+ const closingDate = new Date(data.closingDate);
48
+ const cdDeadline = subtractBusinessDays(closingDate, 3);
49
+ daysUntilCdDeadline = businessDaysBetween(today, cdDeadline);
50
+ if (data.cdDisclosureDate) {
51
+ const cdDate = new Date(data.cdDisclosureDate);
52
+ if (cdDate > cdDeadline) {
53
+ cdCompliant = false;
54
+ findings.push({
55
+ code: 'TRID-CD-001',
56
+ severity: 'critical',
57
+ description: 'Closing Disclosure not provided 3 business days before closing',
58
+ remediation: 'Reschedule closing or document consumer waiver',
59
+ });
60
+ }
61
+ }
62
+ else if (daysUntilCdDeadline !== undefined && daysUntilCdDeadline < 0) {
63
+ cdCompliant = false;
64
+ findings.push({
65
+ code: 'TRID-CD-002',
66
+ severity: 'critical',
67
+ description: 'CD deadline has passed for scheduled closing',
68
+ remediation: 'Issue CD immediately or reschedule closing',
69
+ });
70
+ }
71
+ }
72
+ return {
73
+ applicationDate: data.applicationDate,
74
+ leDisclosureDate: data.leDisclosureDate,
75
+ cdDisclosureDate: data.cdDisclosureDate,
76
+ closingDate: data.closingDate,
77
+ leCompliant,
78
+ cdCompliant,
79
+ daysUntilLeDeadline: daysUntilLeDeadline > 0 ? daysUntilLeDeadline : undefined,
80
+ daysUntilCdDeadline: daysUntilCdDeadline !== undefined && daysUntilCdDeadline > 0 ? daysUntilCdDeadline : undefined,
81
+ findings,
82
+ };
83
+ }
84
+ /**
85
+ * Check ATR/QM requirements
86
+ */
87
+ function checkAtrQm(loan) {
88
+ const findings = [];
89
+ const riskyFeatures = [];
90
+ // Check for risky features that disqualify QM
91
+ if (loan.negativeAmortization) {
92
+ riskyFeatures.push('Negative amortization');
93
+ }
94
+ if (loan.interestOnly) {
95
+ riskyFeatures.push('Interest-only payments');
96
+ }
97
+ if (loan.balloonPayment) {
98
+ riskyFeatures.push('Balloon payment');
99
+ }
100
+ if (loan.prepaymentPenalty) {
101
+ riskyFeatures.push('Prepayment penalty');
102
+ }
103
+ if (loan.termMonths > 360) {
104
+ riskyFeatures.push('Term exceeds 30 years');
105
+ }
106
+ // Points and fees limit (3% for loans >= $100k)
107
+ const pointsAndFeesLimit = loan.loanAmount >= 100000
108
+ ? loan.loanAmount * 0.03
109
+ : Math.min(loan.loanAmount * 0.03, 3000);
110
+ const pointsAndFeesExceeded = loan.pointsAndFees > pointsAndFeesLimit;
111
+ if (pointsAndFeesExceeded) {
112
+ riskyFeatures.push('Points and fees exceed limit');
113
+ findings.push({
114
+ code: 'QM-PF-001',
115
+ severity: 'critical',
116
+ description: `Points and fees ($${loan.pointsAndFees}) exceed ${(pointsAndFeesLimit).toFixed(0)} limit`,
117
+ remediation: 'Reduce fees or document as non-QM',
118
+ });
119
+ }
120
+ // DTI check
121
+ const dtiLimit = 43;
122
+ const dtiExceeded = loan.dti > dtiLimit;
123
+ if (dtiExceeded) {
124
+ findings.push({
125
+ code: 'QM-DTI-001',
126
+ severity: 'major',
127
+ description: `DTI (${loan.dti}%) exceeds QM safe harbor limit (${dtiLimit}%)`,
128
+ remediation: 'May still qualify with AUS approval or as non-QM',
129
+ });
130
+ }
131
+ // Determine QM status
132
+ const hasRiskyFeatures = riskyFeatures.length > 0;
133
+ let isQm = !hasRiskyFeatures && !pointsAndFeesExceeded;
134
+ let qmType;
135
+ if (isQm) {
136
+ if (loan.dti <= 43) {
137
+ qmType = 'safe-harbor';
138
+ }
139
+ else {
140
+ qmType = 'rebuttable-presumption';
141
+ isQm = true; // Still QM but not safe harbor
142
+ }
143
+ }
144
+ else {
145
+ qmType = 'non-qm';
146
+ }
147
+ return {
148
+ isQm,
149
+ qmType,
150
+ dti: loan.dti,
151
+ dtiLimit,
152
+ pointsAndFees: loan.pointsAndFees,
153
+ pointsAndFeesLimit,
154
+ hasRiskyFeatures,
155
+ riskyFeatures,
156
+ findings,
157
+ };
158
+ }
159
+ /**
160
+ * Check ECOA compliance and generate adverse action notice
161
+ */
162
+ function checkEcoa(loan, decision, reasons) {
163
+ const findings = [];
164
+ const warnings = [];
165
+ if (decision === 'denied' || decision === 'countered') {
166
+ if (!reasons || reasons.length === 0) {
167
+ findings.push({
168
+ code: 'ECOA-AA-001',
169
+ severity: 'critical',
170
+ description: 'Adverse action requires specific reasons',
171
+ remediation: 'Provide specific reasons for denial',
172
+ });
173
+ }
174
+ else if (reasons.length > 4) {
175
+ warnings.push('Best practice: limit adverse action reasons to 4 primary factors');
176
+ }
177
+ }
178
+ return {
179
+ regulation: 'ECOA',
180
+ passed: findings.filter(f => f.severity === 'critical').length === 0,
181
+ findings,
182
+ warnings,
183
+ recommendations: ['Document all adverse action reasons contemporaneously'],
184
+ };
185
+ }
186
+ /**
187
+ * Generate ECOA-compliant adverse action notice
188
+ */
189
+ function generateAdverseAction(applicantName, applicationDate, creditorName, creditorAddress, reasons) {
190
+ return {
191
+ applicantName,
192
+ applicationDate,
193
+ decisionDate: new Date().toISOString().split('T')[0],
194
+ creditorName,
195
+ creditorAddress,
196
+ reasons: reasons.slice(0, 4), // Max 4 reasons
197
+ ecoaNotice: `The federal Equal Credit Opportunity Act prohibits creditors from discriminating against credit applicants on the basis of race, color, religion, national origin, sex, marital status, age (provided the applicant has the capacity to enter into a binding contract); because all or part of the applicant's income derives from any public assistance program; or because the applicant has in good faith exercised any right under the Consumer Credit Protection Act. The federal agency that administers compliance with this law concerning this creditor is: Consumer Financial Protection Bureau, 1700 G Street NW, Washington, DC 20552.`,
198
+ fcraNotice: `You have the right to obtain a free copy of your credit report from the credit reporting agency that provided information used in this decision. You also have the right to dispute the accuracy or completeness of any information in your credit report.`,
199
+ };
200
+ }
201
+ /**
202
+ * Standard adverse action reasons
203
+ */
204
+ exports.ADVERSE_ACTION_REASONS = [
205
+ 'Credit score does not meet minimum requirements',
206
+ 'Debt-to-income ratio exceeds guidelines',
207
+ 'Insufficient credit history',
208
+ 'Delinquent past or present credit obligations',
209
+ 'Collection action or judgment',
210
+ 'Bankruptcy',
211
+ 'Insufficient income for amount requested',
212
+ 'Unable to verify employment',
213
+ 'Unable to verify income',
214
+ 'Length of employment',
215
+ 'Insufficient property value',
216
+ 'Property type not eligible',
217
+ 'Loan-to-value ratio exceeds guidelines',
218
+ 'Unacceptable collateral',
219
+ 'Unable to verify residence',
220
+ 'Temporary or irregular employment',
221
+ ];
222
+ // Helper functions
223
+ function addBusinessDays(date, days) {
224
+ const result = new Date(date);
225
+ let added = 0;
226
+ while (added < days) {
227
+ result.setDate(result.getDate() + 1);
228
+ if (result.getDay() !== 0 && result.getDay() !== 6) {
229
+ added++;
230
+ }
231
+ }
232
+ return result;
233
+ }
234
+ function subtractBusinessDays(date, days) {
235
+ const result = new Date(date);
236
+ let subtracted = 0;
237
+ while (subtracted < days) {
238
+ result.setDate(result.getDate() - 1);
239
+ if (result.getDay() !== 0 && result.getDay() !== 6) {
240
+ subtracted++;
241
+ }
242
+ }
243
+ return result;
244
+ }
245
+ function businessDaysBetween(start, end) {
246
+ let count = 0;
247
+ const current = new Date(start);
248
+ while (current < end) {
249
+ if (current.getDay() !== 0 && current.getDay() !== 6) {
250
+ count++;
251
+ }
252
+ current.setDate(current.getDate() + 1);
253
+ }
254
+ return count;
255
+ }
256
+ //# sourceMappingURL=checks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checks.js","sourceRoot":"","sources":["../../src/lib/checks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Compliance types for compctl
3
+ */
4
+ /** Regulation identifiers */
5
+ export type Regulation = 'TRID' | 'ATR_QM' | 'HMDA' | 'ECOA' | 'RESPA' | 'TILA' | 'FCRA' | 'UDAAP' | 'SCRA' | 'FLOOD';
6
+ /** Compliance check result */
7
+ export interface ComplianceCheck {
8
+ regulation: Regulation;
9
+ passed: boolean;
10
+ findings: Finding[];
11
+ warnings: string[];
12
+ recommendations: string[];
13
+ }
14
+ /** Individual finding */
15
+ export interface Finding {
16
+ code: string;
17
+ severity: 'critical' | 'major' | 'minor' | 'info';
18
+ description: string;
19
+ remediation?: string;
20
+ }
21
+ /** TRID timing check */
22
+ export interface TridTiming {
23
+ applicationDate: string;
24
+ leDisclosureDate?: string;
25
+ cdDisclosureDate?: string;
26
+ closingDate?: string;
27
+ leCompliant: boolean;
28
+ cdCompliant: boolean;
29
+ daysUntilLeDeadline?: number;
30
+ daysUntilCdDeadline?: number;
31
+ findings: Finding[];
32
+ }
33
+ /** ATR/QM check result */
34
+ export interface AtrQmResult {
35
+ isQm: boolean;
36
+ qmType?: 'safe-harbor' | 'rebuttable-presumption' | 'non-qm';
37
+ dti: number;
38
+ dtiLimit: number;
39
+ pointsAndFees: number;
40
+ pointsAndFeesLimit: number;
41
+ hasRiskyFeatures: boolean;
42
+ riskyFeatures: string[];
43
+ findings: Finding[];
44
+ }
45
+ /** HMDA LAR data */
46
+ export interface HmdaLarData {
47
+ loanId: string;
48
+ actionTaken: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
49
+ actionTakenDate: string;
50
+ loanType: 1 | 2 | 3 | 4;
51
+ loanPurpose: 1 | 2 | 31 | 32 | 4 | 5;
52
+ lienStatus: 1 | 2;
53
+ loanAmount: number;
54
+ combinedLtv?: number;
55
+ interestRate?: number;
56
+ rateSpread?: number;
57
+ hoepaStatus: 1 | 2 | 3;
58
+ propertyType: 1 | 2 | 3;
59
+ constructionMethod: 1 | 2;
60
+ occupancyType: 1 | 2 | 3;
61
+ propertyValue?: number;
62
+ applicantEthnicity: string[];
63
+ applicantRace: string[];
64
+ applicantSex: 1 | 2 | 3 | 4;
65
+ applicantAge?: number;
66
+ income?: number;
67
+ creditScore?: number;
68
+ dti?: number;
69
+ ausResult?: string;
70
+ }
71
+ /** Adverse action reasons */
72
+ export interface AdverseActionNotice {
73
+ applicantName: string;
74
+ applicationDate: string;
75
+ decisionDate: string;
76
+ creditorName: string;
77
+ creditorAddress: string;
78
+ reasons: string[];
79
+ ecoaNotice: string;
80
+ fcraNotice?: string;
81
+ }
82
+ /** Loan data for compliance checks */
83
+ export interface LoanData {
84
+ loanId: string;
85
+ applicationDate: string;
86
+ loanAmount: number;
87
+ propertyValue: number;
88
+ interestRate: number;
89
+ termMonths: number;
90
+ monthlyPayment: number;
91
+ dti: number;
92
+ ltv: number;
93
+ creditScore: number;
94
+ pointsAndFees: number;
95
+ prepaymentPenalty?: boolean;
96
+ negativeAmortization?: boolean;
97
+ interestOnly?: boolean;
98
+ balloonPayment?: boolean;
99
+ armFeatures?: {
100
+ initialRate: number;
101
+ margin: number;
102
+ caps: number[];
103
+ };
104
+ borrowerIncome: number;
105
+ propertyType: string;
106
+ occupancy: string;
107
+ loanPurpose: string;
108
+ loanType: string;
109
+ }
110
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /**
3
+ * Compliance types for compctl
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA;;GAEG"}
package/jest.config.js ADDED
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ testMatch: ['**/tests/**/*.test.ts'],
5
+ collectCoverageFrom: ['src/**/*.ts', '!src/index.ts'],
6
+ coverageDirectory: 'coverage',
7
+ coverageReporters: ['text', 'lcov'],
8
+ };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "compctl",
3
+ "version": "0.1.0",
4
+ "description": "Lending compliance checks (TRID, ATR/QM, HMDA, ECOA) - part of the LendCtl Suite",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "compctl": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test": "jest",
13
+ "prepublishOnly": "npm run build && npm test"
14
+ },
15
+ "keywords": ["compliance", "lending", "trid", "hmda", "ecoa", "mortgage", "cli", "mcp"],
16
+ "author": "Avatar Consulting",
17
+ "license": "MIT",
18
+ "repository": { "type": "git", "url": "https://github.com/rsatyan/compctl" },
19
+ "dependencies": { "commander": "^12.0.0", "chalk": "^5.3.0" },
20
+ "devDependencies": {
21
+ "@types/jest": "^29.5.0",
22
+ "@types/node": "^20.0.0",
23
+ "jest": "^29.7.0",
24
+ "ts-jest": "^29.1.0",
25
+ "typescript": "^5.3.0"
26
+ },
27
+ "engines": { "node": ">=18.0.0" }
28
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * compctl adverse command - Generate adverse action notice
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { generateAdverseAction, ADVERSE_ACTION_REASONS } from '../lib/checks';
7
+
8
+ export function createAdverseCommand(): Command {
9
+ const adverse = new Command('adverse')
10
+ .description('Generate ECOA-compliant adverse action notice')
11
+ .option('--applicant <name>', 'Applicant name', 'Applicant')
12
+ .option('--app-date <date>', 'Application date', new Date().toISOString().split('T')[0])
13
+ .option('--creditor <name>', 'Creditor name', 'Lender')
14
+ .option('--creditor-address <addr>', 'Creditor address', '123 Main St')
15
+ .option('--reasons <codes>', 'Comma-separated reason codes (1-16) or text')
16
+ .option('--list-reasons', 'List standard adverse action reasons', false)
17
+ .option('--format <type>', 'Output format (json|text)', 'text')
18
+ .action(async (options) => {
19
+ if (options.listReasons) {
20
+ console.log('═══════════════════════════════════════════════════════════════');
21
+ console.log('STANDARD ADVERSE ACTION REASONS');
22
+ console.log('═══════════════════════════════════════════════════════════════');
23
+ ADVERSE_ACTION_REASONS.forEach((reason, i) => {
24
+ console.log(`${(i + 1).toString().padStart(2)}. ${reason}`);
25
+ });
26
+ console.log('═══════════════════════════════════════════════════════════════');
27
+ return;
28
+ }
29
+
30
+ let reasons: string[] = [];
31
+ if (options.reasons) {
32
+ const parts = options.reasons.split(',');
33
+ for (const part of parts) {
34
+ const trimmed = part.trim();
35
+ const num = parseInt(trimmed);
36
+ if (!isNaN(num) && num >= 1 && num <= ADVERSE_ACTION_REASONS.length) {
37
+ reasons.push(ADVERSE_ACTION_REASONS[num - 1]);
38
+ } else {
39
+ reasons.push(trimmed);
40
+ }
41
+ }
42
+ }
43
+
44
+ const notice = generateAdverseAction(
45
+ options.applicant,
46
+ options.appDate,
47
+ options.creditor,
48
+ options.creditorAddress,
49
+ reasons
50
+ );
51
+
52
+ if (options.format === 'json') {
53
+ console.log(JSON.stringify(notice, null, 2));
54
+ } else {
55
+ console.log('═══════════════════════════════════════════════════════════════');
56
+ console.log('NOTICE OF ADVERSE ACTION');
57
+ console.log('═══════════════════════════════════════════════════════════════');
58
+ console.log('');
59
+ console.log(`Date: ${notice.decisionDate}`);
60
+ console.log(`To: ${notice.applicantName}`);
61
+ console.log('');
62
+ console.log(`Your application for credit dated ${notice.applicationDate} has been`);
63
+ console.log('denied based on the following reason(s):');
64
+ console.log('');
65
+ if (notice.reasons.length > 0) {
66
+ for (const reason of notice.reasons) {
67
+ console.log(` • ${reason}`);
68
+ }
69
+ } else {
70
+ console.log(' [No specific reasons provided - COMPLIANCE WARNING]');
71
+ }
72
+ console.log('');
73
+ console.log('───────────────────────────────────────────────────────────────');
74
+ console.log('EQUAL CREDIT OPPORTUNITY ACT NOTICE');
75
+ console.log('───────────────────────────────────────────────────────────────');
76
+ console.log(notice.ecoaNotice);
77
+ console.log('');
78
+ console.log('───────────────────────────────────────────────────────────────');
79
+ console.log('FAIR CREDIT REPORTING ACT NOTICE');
80
+ console.log('───────────────────────────────────────────────────────────────');
81
+ console.log(notice.fcraNotice);
82
+ console.log('');
83
+ console.log(`${notice.creditorName}`);
84
+ console.log(`${notice.creditorAddress}`);
85
+ console.log('═══════════════════════════════════════════════════════════════');
86
+ }
87
+ });
88
+
89
+ return adverse;
90
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * compctl atr command - Check ATR/QM compliance
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import * as fs from 'fs';
7
+ import { checkAtrQm } from '../lib/checks';
8
+ import { LoanData } from '../types';
9
+
10
+ export function createAtrCommand(): Command {
11
+ const atr = new Command('atr')
12
+ .description('Check ATR/QM compliance')
13
+ .option('-f, --file <path>', 'Loan data JSON file')
14
+ .option('--loan-amount <amount>', 'Loan amount')
15
+ .option('--dti <percent>', 'Debt-to-income ratio')
16
+ .option('--points-fees <amount>', 'Total points and fees')
17
+ .option('--neg-am', 'Has negative amortization', false)
18
+ .option('--interest-only', 'Has interest-only period', false)
19
+ .option('--balloon', 'Has balloon payment', false)
20
+ .option('--prepay-penalty', 'Has prepayment penalty', false)
21
+ .option('--term <months>', 'Loan term in months', '360')
22
+ .option('--format <type>', 'Output format (json|table)', 'table')
23
+ .action(async (options) => {
24
+ let loan: LoanData;
25
+
26
+ if (options.file) {
27
+ const content = fs.readFileSync(options.file, 'utf-8');
28
+ loan = JSON.parse(content);
29
+ } else {
30
+ loan = {
31
+ loanId: 'CLI-INPUT',
32
+ applicationDate: new Date().toISOString().split('T')[0],
33
+ loanAmount: parseFloat(options.loanAmount || '300000'),
34
+ propertyValue: parseFloat(options.loanAmount || '300000') * 1.25,
35
+ interestRate: 6.5,
36
+ termMonths: parseInt(options.term),
37
+ monthlyPayment: 0,
38
+ dti: parseFloat(options.dti || '35'),
39
+ ltv: 80,
40
+ creditScore: 720,
41
+ pointsAndFees: parseFloat(options.pointsFees || '0'),
42
+ negativeAmortization: options.negAm,
43
+ interestOnly: options.interestOnly,
44
+ balloonPayment: options.balloon,
45
+ prepaymentPenalty: options.prepayPenalty,
46
+ borrowerIncome: 100000,
47
+ propertyType: 'single-family',
48
+ occupancy: 'primary',
49
+ loanPurpose: 'purchase',
50
+ loanType: 'conventional',
51
+ };
52
+ }
53
+
54
+ const result = checkAtrQm(loan);
55
+
56
+ if (options.format === 'json') {
57
+ console.log(JSON.stringify(result, null, 2));
58
+ } else {
59
+ console.log('═══════════════════════════════════════════════════════════════');
60
+ console.log('ATR/QM COMPLIANCE CHECK');
61
+ console.log('═══════════════════════════════════════════════════════════════');
62
+ console.log('');
63
+ const qmStatus = result.isQm ? '✓ QUALIFIED MORTGAGE' : '✗ NON-QM';
64
+ console.log(`QM Status: ${qmStatus}`);
65
+ if (result.qmType) {
66
+ console.log(`QM Type: ${result.qmType.toUpperCase()}`);
67
+ }
68
+ console.log('');
69
+ console.log('KEY METRICS');
70
+ console.log('───────────────────────────────────────────────────────────────');
71
+ const dtiStatus = result.dti <= result.dtiLimit ? '✓' : '⚠';
72
+ console.log(`${dtiStatus} DTI: ${result.dti}% (limit: ${result.dtiLimit}%)`);
73
+ const pfStatus = result.pointsAndFees <= result.pointsAndFeesLimit ? '✓' : '✗';
74
+ console.log(`${pfStatus} Points & Fees: $${result.pointsAndFees.toLocaleString()} (limit: $${result.pointsAndFeesLimit.toLocaleString()})`);
75
+
76
+ if (result.riskyFeatures.length > 0) {
77
+ console.log('');
78
+ console.log('RISKY FEATURES');
79
+ console.log('───────────────────────────────────────────────────────────────');
80
+ for (const feature of result.riskyFeatures) {
81
+ console.log(` ✗ ${feature}`);
82
+ }
83
+ }
84
+
85
+ if (result.findings.length > 0) {
86
+ console.log('');
87
+ console.log('FINDINGS');
88
+ console.log('───────────────────────────────────────────────────────────────');
89
+ for (const f of result.findings) {
90
+ const icon = f.severity === 'critical' ? '🚨' : f.severity === 'major' ? '⚠' : 'ℹ';
91
+ console.log(`${icon} [${f.code}] ${f.description}`);
92
+ if (f.remediation) console.log(` → ${f.remediation}`);
93
+ }
94
+ }
95
+ console.log('═══════════════════════════════════════════════════════════════');
96
+ }
97
+ });
98
+
99
+ return atr;
100
+ }