@dizzlkheinz/ynab-mcpb 0.15.1 → 0.16.1
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.
- package/CHANGELOG.md +36 -0
- package/CLAUDE.md +113 -18
- package/README.md +19 -4
- package/dist/bundle/index.cjs +53 -52
- package/dist/server/YNABMCPServer.d.ts +2 -6
- package/dist/server/YNABMCPServer.js +5 -1
- package/dist/server/resources.d.ts +17 -13
- package/dist/server/resources.js +237 -48
- package/dist/tools/reconcileAdapter.d.ts +1 -0
- package/dist/tools/reconcileAdapter.js +1 -0
- package/dist/tools/reconciliation/csvParser.d.ts +3 -0
- package/dist/tools/reconciliation/csvParser.js +58 -19
- package/dist/tools/reconciliation/executor.js +47 -1
- package/dist/tools/reconciliation/index.js +82 -42
- package/dist/tools/reconciliation/reportFormatter.d.ts +1 -0
- package/dist/tools/reconciliation/reportFormatter.js +49 -36
- package/dist/tools/transactionTools.js +5 -0
- package/docs/reference/API.md +144 -0
- package/docs/technical/reconciliation-system-architecture.md +2251 -0
- package/package.json +1 -1
- package/src/server/YNABMCPServer.ts +7 -0
- package/src/server/__tests__/resources.template.test.ts +198 -0
- package/src/server/__tests__/resources.test.ts +10 -2
- package/src/server/resources.ts +307 -62
- package/src/tools/__tests__/transactionTools.test.ts +90 -17
- package/src/tools/reconcileAdapter.ts +2 -0
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +23 -23
- package/src/tools/reconciliation/csvParser.ts +84 -18
- package/src/tools/reconciliation/executor.ts +58 -1
- package/src/tools/reconciliation/index.ts +105 -55
- package/src/tools/reconciliation/reportFormatter.ts +55 -37
- package/src/tools/transactionTools.ts +10 -0
- package/.dxtignore +0 -57
- package/CODEREVIEW_RESPONSE.md +0 -128
- package/SCHEMA_IMPROVEMENT_SUMMARY.md +0 -120
- package/TESTING_NOTES.md +0 -217
- package/accountactivity-merged.csv +0 -149
- package/bundle-analysis.html +0 -13110
- package/docs/plans/2025-11-20-reloadable-config-token-validation.md +0 -93
- package/docs/plans/2025-11-21-fix-transaction-cached-property.md +0 -362
- package/docs/plans/2025-11-21-reconciliation-error-handling.md +0 -90
- package/docs/plans/2025-11-21-v014-hardening.md +0 -153
- package/docs/plans/reconciliation-v2-redesign.md +0 -1571
- package/fix-types.sh +0 -17
- package/test-csv-sample.csv +0 -28
- package/test-exports/sample_bank_statement.csv +0 -7
- package/test-reconcile-autodetect.js +0 -40
- package/test-reconcile-tool.js +0 -152
- package/test-reconcile-with-csv.cjs +0 -89
- package/test-statement.csv +0 -8
- package/test_debug.js +0 -47
- package/test_mcp_tools.mjs +0 -75
- package/test_simple.mjs +0 -16
|
@@ -14,6 +14,8 @@ import type {
|
|
|
14
14
|
import type { LegacyReconciliationResult } from './executor.js';
|
|
15
15
|
import type { MoneyValue } from '../../utils/money.js';
|
|
16
16
|
|
|
17
|
+
const SECTION_DIVIDER = '-'.repeat(60);
|
|
18
|
+
|
|
17
19
|
/**
|
|
18
20
|
* Options for report formatting
|
|
19
21
|
*/
|
|
@@ -24,6 +26,7 @@ export interface ReportFormatterOptions {
|
|
|
24
26
|
includeDetailedMatches?: boolean | undefined;
|
|
25
27
|
maxUnmatchedToShow?: number | undefined;
|
|
26
28
|
maxInsightsToShow?: number | undefined;
|
|
29
|
+
notes?: string[] | undefined;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -40,6 +43,11 @@ export function formatHumanReadableReport(
|
|
|
40
43
|
// Header
|
|
41
44
|
sections.push(formatHeader(accountLabel, analysis));
|
|
42
45
|
|
|
46
|
+
// Contextual notes (if provided)
|
|
47
|
+
if (options.notes && options.notes.length > 0) {
|
|
48
|
+
sections.push(formatNotesSection(options.notes));
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
// Balance check section
|
|
44
52
|
sections.push(formatBalanceSection(analysis.balance_info, analysis.summary));
|
|
45
53
|
|
|
@@ -67,12 +75,22 @@ export function formatHumanReadableReport(
|
|
|
67
75
|
*/
|
|
68
76
|
function formatHeader(accountName: string, analysis: ReconciliationAnalysis): string {
|
|
69
77
|
const lines: string[] = [];
|
|
70
|
-
lines.push(
|
|
71
|
-
lines.push(
|
|
78
|
+
lines.push(`${accountName} Reconciliation Report`);
|
|
79
|
+
lines.push(SECTION_DIVIDER);
|
|
72
80
|
lines.push(`Statement Period: ${analysis.summary.statement_date_range}`);
|
|
73
81
|
return lines.join('\n');
|
|
74
82
|
}
|
|
75
83
|
|
|
84
|
+
function formatNotesSection(notes: string[]): string {
|
|
85
|
+
const lines: string[] = [];
|
|
86
|
+
lines.push('Notes');
|
|
87
|
+
lines.push(SECTION_DIVIDER);
|
|
88
|
+
for (const note of notes) {
|
|
89
|
+
lines.push(`- ${note}`);
|
|
90
|
+
}
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
76
94
|
/**
|
|
77
95
|
* Format the balance check section
|
|
78
96
|
*/
|
|
@@ -81,18 +99,18 @@ function formatBalanceSection(
|
|
|
81
99
|
summary: ReconciliationAnalysis['summary'],
|
|
82
100
|
): string {
|
|
83
101
|
const lines: string[] = [];
|
|
84
|
-
lines.push('
|
|
85
|
-
lines.push(
|
|
102
|
+
lines.push('Balance Check');
|
|
103
|
+
lines.push(SECTION_DIVIDER);
|
|
86
104
|
|
|
87
105
|
// Current balances
|
|
88
|
-
lines.push(
|
|
89
|
-
lines.push(
|
|
106
|
+
lines.push(`- YNAB Cleared Balance: ${summary.current_cleared_balance.value_display}`);
|
|
107
|
+
lines.push(`- Statement Balance: ${summary.target_statement_balance.value_display}`);
|
|
90
108
|
lines.push('');
|
|
91
109
|
|
|
92
110
|
// Discrepancy status
|
|
93
111
|
const discrepancyMilli = balanceInfo.discrepancy.value_milliunits;
|
|
94
112
|
if (discrepancyMilli === 0) {
|
|
95
|
-
lines.push('
|
|
113
|
+
lines.push('Balances match perfectly.');
|
|
96
114
|
} else {
|
|
97
115
|
const direction = discrepancyMilli > 0 ? 'ynab_higher' : 'bank_higher';
|
|
98
116
|
const directionLabel =
|
|
@@ -100,8 +118,8 @@ function formatBalanceSection(
|
|
|
100
118
|
? 'YNAB shows MORE than statement'
|
|
101
119
|
: 'Statement shows MORE than YNAB';
|
|
102
120
|
|
|
103
|
-
lines.push(
|
|
104
|
-
lines.push(`
|
|
121
|
+
lines.push(`Discrepancy: ${balanceInfo.discrepancy.value_display}`);
|
|
122
|
+
lines.push(`Direction: ${directionLabel}`);
|
|
105
123
|
}
|
|
106
124
|
|
|
107
125
|
return lines.join('\n');
|
|
@@ -115,21 +133,21 @@ function formatTransactionAnalysisSection(
|
|
|
115
133
|
options: ReportFormatterOptions,
|
|
116
134
|
): string {
|
|
117
135
|
const lines: string[] = [];
|
|
118
|
-
lines.push('
|
|
119
|
-
lines.push(
|
|
136
|
+
lines.push('Transaction Analysis');
|
|
137
|
+
lines.push(SECTION_DIVIDER);
|
|
120
138
|
|
|
121
139
|
const summary = analysis.summary;
|
|
122
140
|
lines.push(
|
|
123
|
-
|
|
141
|
+
`- Automatically matched: ${summary.auto_matched} of ${summary.bank_transactions_count} transactions`,
|
|
124
142
|
);
|
|
125
|
-
lines.push(
|
|
126
|
-
lines.push(
|
|
127
|
-
lines.push(
|
|
143
|
+
lines.push(`- Suggested matches: ${summary.suggested_matches}`);
|
|
144
|
+
lines.push(`- Unmatched bank: ${summary.unmatched_bank}`);
|
|
145
|
+
lines.push(`- Unmatched YNAB: ${summary.unmatched_ynab}`);
|
|
128
146
|
|
|
129
147
|
// Show unmatched bank transactions (if any)
|
|
130
148
|
if (analysis.unmatched_bank.length > 0) {
|
|
131
149
|
lines.push('');
|
|
132
|
-
lines.push('
|
|
150
|
+
lines.push('Unmatched bank transactions:');
|
|
133
151
|
const maxToShow = options.maxUnmatchedToShow ?? 5;
|
|
134
152
|
const toShow = analysis.unmatched_bank.slice(0, maxToShow);
|
|
135
153
|
|
|
@@ -145,7 +163,7 @@ function formatTransactionAnalysisSection(
|
|
|
145
163
|
// Show suggested matches (if any)
|
|
146
164
|
if (analysis.suggested_matches.length > 0) {
|
|
147
165
|
lines.push('');
|
|
148
|
-
lines.push('
|
|
166
|
+
lines.push('Suggested matches:');
|
|
149
167
|
const maxToShow = options.maxUnmatchedToShow ?? 3;
|
|
150
168
|
const toShow = analysis.suggested_matches.slice(0, maxToShow);
|
|
151
169
|
|
|
@@ -194,8 +212,8 @@ function formatAmount(amountMilli: number): string {
|
|
|
194
212
|
*/
|
|
195
213
|
function formatInsightsSection(insights: ReconciliationInsight[], maxToShow: number = 3): string {
|
|
196
214
|
const lines: string[] = [];
|
|
197
|
-
lines.push('
|
|
198
|
-
lines.push(
|
|
215
|
+
lines.push('Key Insights');
|
|
216
|
+
lines.push(SECTION_DIVIDER);
|
|
199
217
|
|
|
200
218
|
const toShow = insights.slice(0, maxToShow);
|
|
201
219
|
for (const insight of toShow) {
|
|
@@ -222,18 +240,18 @@ function formatInsightsSection(insights: ReconciliationInsight[], maxToShow: num
|
|
|
222
240
|
}
|
|
223
241
|
|
|
224
242
|
/**
|
|
225
|
-
* Get
|
|
243
|
+
* Get text icon for severity level
|
|
226
244
|
*/
|
|
227
245
|
function getSeverityIcon(severity: string): string {
|
|
228
246
|
switch (severity) {
|
|
229
247
|
case 'critical':
|
|
230
|
-
return '
|
|
248
|
+
return '[CRITICAL]';
|
|
231
249
|
case 'warning':
|
|
232
|
-
return '
|
|
250
|
+
return '[WARN]';
|
|
233
251
|
case 'info':
|
|
234
|
-
return '
|
|
252
|
+
return '[INFO]';
|
|
235
253
|
default:
|
|
236
|
-
return '
|
|
254
|
+
return '[NOTE]';
|
|
237
255
|
}
|
|
238
256
|
}
|
|
239
257
|
|
|
@@ -260,13 +278,13 @@ function formatEvidenceSummary(evidence: Record<string, unknown>): string | null
|
|
|
260
278
|
*/
|
|
261
279
|
function formatExecutionSection(execution: LegacyReconciliationResult): string {
|
|
262
280
|
const lines: string[] = [];
|
|
263
|
-
lines.push('
|
|
264
|
-
lines.push(
|
|
281
|
+
lines.push('Execution Summary');
|
|
282
|
+
lines.push(SECTION_DIVIDER);
|
|
265
283
|
|
|
266
284
|
const summary = execution.summary;
|
|
267
|
-
lines.push(
|
|
268
|
-
lines.push(
|
|
269
|
-
lines.push(
|
|
285
|
+
lines.push(`Transactions created: ${summary.transactions_created}`);
|
|
286
|
+
lines.push(`Transactions updated: ${summary.transactions_updated}`);
|
|
287
|
+
lines.push(`Date adjustments: ${summary.dates_adjusted}`);
|
|
270
288
|
|
|
271
289
|
// Show top recommendations if any
|
|
272
290
|
if (execution.recommendations.length > 0) {
|
|
@@ -275,7 +293,7 @@ function formatExecutionSection(execution: LegacyReconciliationResult): string {
|
|
|
275
293
|
const maxRecs = 3;
|
|
276
294
|
const toShow = execution.recommendations.slice(0, maxRecs);
|
|
277
295
|
for (const rec of toShow) {
|
|
278
|
-
lines.push(`
|
|
296
|
+
lines.push(` - ${rec}`);
|
|
279
297
|
}
|
|
280
298
|
if (execution.recommendations.length > maxRecs) {
|
|
281
299
|
lines.push(` ... and ${execution.recommendations.length - maxRecs} more`);
|
|
@@ -284,9 +302,9 @@ function formatExecutionSection(execution: LegacyReconciliationResult): string {
|
|
|
284
302
|
|
|
285
303
|
lines.push('');
|
|
286
304
|
if (summary.dry_run) {
|
|
287
|
-
lines.push('
|
|
305
|
+
lines.push('NOTE: Dry run only - no YNAB changes were applied.');
|
|
288
306
|
} else {
|
|
289
|
-
lines.push('
|
|
307
|
+
lines.push('Changes applied to YNAB. Review structured output for action details.');
|
|
290
308
|
}
|
|
291
309
|
|
|
292
310
|
return lines.join('\n');
|
|
@@ -300,8 +318,8 @@ function formatRecommendationsSection(
|
|
|
300
318
|
execution?: LegacyReconciliationResult,
|
|
301
319
|
): string {
|
|
302
320
|
const lines: string[] = [];
|
|
303
|
-
lines.push('
|
|
304
|
-
lines.push(
|
|
321
|
+
lines.push('Recommended Actions');
|
|
322
|
+
lines.push(SECTION_DIVIDER);
|
|
305
323
|
|
|
306
324
|
// If we have execution results, recommendations are already shown
|
|
307
325
|
if (execution && !execution.summary.dry_run) {
|
|
@@ -312,11 +330,11 @@ function formatRecommendationsSection(
|
|
|
312
330
|
// Show next steps from analysis
|
|
313
331
|
if (analysis.next_steps.length > 0) {
|
|
314
332
|
for (const step of analysis.next_steps) {
|
|
315
|
-
lines.push(
|
|
333
|
+
lines.push(`- ${step}`);
|
|
316
334
|
}
|
|
317
335
|
} else {
|
|
318
|
-
lines.push('
|
|
319
|
-
lines.push('
|
|
336
|
+
lines.push('No specific actions recommended.');
|
|
337
|
+
lines.push('Review the structured output for detailed match information.');
|
|
320
338
|
}
|
|
321
339
|
|
|
322
340
|
return lines.join('\n');
|
|
@@ -755,6 +755,16 @@ export async function handleListTransactions(
|
|
|
755
755
|
let usedDelta = false;
|
|
756
756
|
|
|
757
757
|
if (params.account_id) {
|
|
758
|
+
// Validate that the account exists before fetching transactions
|
|
759
|
+
// YNAB API returns empty array for invalid account IDs instead of an error
|
|
760
|
+
const accountsResult = await deltaFetcher.fetchAccounts(params.budget_id);
|
|
761
|
+
const accountExists = accountsResult.data.some(
|
|
762
|
+
(account) => account.id === params.account_id,
|
|
763
|
+
);
|
|
764
|
+
if (!accountExists) {
|
|
765
|
+
throw new Error(`Account ${params.account_id} not found in budget ${params.budget_id}`);
|
|
766
|
+
}
|
|
767
|
+
|
|
758
768
|
const result = await deltaFetcher.fetchTransactionsByAccount(
|
|
759
769
|
params.budget_id,
|
|
760
770
|
params.account_id,
|
package/.dxtignore
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
node_modules/
|
|
2
|
-
src/
|
|
3
|
-
**/__tests__/
|
|
4
|
-
**/*.test.*
|
|
5
|
-
coverage/
|
|
6
|
-
*.log
|
|
7
|
-
.*/
|
|
8
|
-
**/*.map
|
|
9
|
-
tsconfig*.json
|
|
10
|
-
vitest.config.ts
|
|
11
|
-
vitest-reporters/
|
|
12
|
-
eslint.config.js
|
|
13
|
-
test-results.json
|
|
14
|
-
test-results/
|
|
15
|
-
README.md
|
|
16
|
-
|
|
17
|
-
# Sensitive credential files
|
|
18
|
-
.chunkhound.json
|
|
19
|
-
.mcp.json
|
|
20
|
-
.env*
|
|
21
|
-
|
|
22
|
-
# Build metadata
|
|
23
|
-
meta.json
|
|
24
|
-
bundle-analysis.html
|
|
25
|
-
|
|
26
|
-
# Development-only markdown files
|
|
27
|
-
AGENTS.md
|
|
28
|
-
TESTING_NOTES.md
|
|
29
|
-
CODEREVIEW_RESPONSE.md
|
|
30
|
-
SCHEMA_IMPROVEMENT_SUMMARY.md
|
|
31
|
-
|
|
32
|
-
# Test files and data
|
|
33
|
-
test_*.js
|
|
34
|
-
test_*.mjs
|
|
35
|
-
test-*.js
|
|
36
|
-
test-*.cjs
|
|
37
|
-
test-*.csv
|
|
38
|
-
test-exports/
|
|
39
|
-
accountactivity-merged.csv
|
|
40
|
-
|
|
41
|
-
# Temporary and development files
|
|
42
|
-
temp/
|
|
43
|
-
tmp/
|
|
44
|
-
fix-types.sh
|
|
45
|
-
NUL
|
|
46
|
-
package-lock.json
|
|
47
|
-
|
|
48
|
-
# Development plans (keep user-facing docs)
|
|
49
|
-
docs/plans/
|
|
50
|
-
docs/bulk-transaction-operations-plan.md
|
|
51
|
-
docs/delta-request-plan.md
|
|
52
|
-
docs/reconciliation-flow.md
|
|
53
|
-
|
|
54
|
-
# Scripts (not needed for distribution)
|
|
55
|
-
scripts/
|
|
56
|
-
|
|
57
|
-
# keep dist/, manifest.json, CHANGELOG.md, CLAUDE.md, LICENSE, docs/ (except plans)
|
package/CODEREVIEW_RESPONSE.md
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
# Code Review Response: reconciliationOutputs.ts
|
|
2
|
-
|
|
3
|
-
## Summary
|
|
4
|
-
|
|
5
|
-
CodeRabbit provided two suggestions for improving `src/tools/schemas/outputs/reconciliationOutputs.ts`. After technical analysis of the codebase, I've determined:
|
|
6
|
-
|
|
7
|
-
1. **Feedback #1 (ExecutionActionRecordSchema typing)**: Already implemented ✅
|
|
8
|
-
2. **Feedback #2 (discrepancy_direction validation)**: Not implementing - technically sound but violates architectural principles ❌
|
|
9
|
-
|
|
10
|
-
## Feedback #1: Stronger Typing for Transaction Field
|
|
11
|
-
|
|
12
|
-
**Suggestion:** Use discriminated unions instead of `z.record(z.string(), z.unknown())` for the transaction field in `ExecutionActionRecordSchema`.
|
|
13
|
-
|
|
14
|
-
**Status:** ✅ **Already Implemented**
|
|
15
|
-
|
|
16
|
-
### Analysis
|
|
17
|
-
|
|
18
|
-
The CodeRabbit feedback appears to be outdated. The current implementation (lines 431-479) already uses a discriminated union with strong typing:
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
export const ExecutionActionRecordSchema = z.discriminatedUnion('type', [
|
|
22
|
-
// Successful transaction creation
|
|
23
|
-
z.object({
|
|
24
|
-
type: z.literal('create_transaction'),
|
|
25
|
-
transaction: CreatedTransactionSchema.nullable(),
|
|
26
|
-
// ...
|
|
27
|
-
}),
|
|
28
|
-
// Failed transaction creation
|
|
29
|
-
z.object({
|
|
30
|
-
type: z.literal('create_transaction_failed'),
|
|
31
|
-
transaction: TransactionCreationPayloadSchema,
|
|
32
|
-
// ...
|
|
33
|
-
}),
|
|
34
|
-
// ... other action types
|
|
35
|
-
]);
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Each action type has its own specific schema:
|
|
39
|
-
|
|
40
|
-
- `CreatedTransactionSchema` - for successful creations (line 371)
|
|
41
|
-
- `TransactionCreationPayloadSchema` - for failed/dry-run creations (line 387)
|
|
42
|
-
- `TransactionUpdatePayloadSchema` - for status/date changes (line 402)
|
|
43
|
-
- `DuplicateDetectionPayloadSchema` - for duplicate detection (line 412)
|
|
44
|
-
|
|
45
|
-
This provides full type safety without the drawbacks of loose typing.
|
|
46
|
-
|
|
47
|
-
## Feedback #2: Validate discrepancy_direction Against Actual Discrepancy
|
|
48
|
-
|
|
49
|
-
**Suggestion:** Add a Zod refinement to ensure `discrepancy_direction` matches the sign of `balance.discrepancy.amount`.
|
|
50
|
-
|
|
51
|
-
**Status:** ❌ **Not Implementing**
|
|
52
|
-
|
|
53
|
-
### Analysis
|
|
54
|
-
|
|
55
|
-
While technically correct, this validation violates architectural principles and provides minimal benefit:
|
|
56
|
-
|
|
57
|
-
#### Current Implementation (reconcileAdapter.ts:122-135)
|
|
58
|
-
|
|
59
|
-
The `convertBalanceInfo` function deterministically derives `discrepancy_direction` from `discrepancyMilli`:
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
const convertBalanceInfo = (analysis: ReconciliationAnalysis) => {
|
|
63
|
-
const discrepancyMilli = analysis.balance_info.discrepancy.value_milliunits;
|
|
64
|
-
const direction =
|
|
65
|
-
discrepancyMilli === 0 ? 'balanced' : discrepancyMilli > 0 ? 'ynab_higher' : 'bank_higher';
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
current_cleared: analysis.balance_info.current_cleared,
|
|
69
|
-
// ...
|
|
70
|
-
discrepancy: analysis.balance_info.discrepancy,
|
|
71
|
-
discrepancy_direction: direction,
|
|
72
|
-
on_track: analysis.balance_info.on_track,
|
|
73
|
-
};
|
|
74
|
-
};
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
This logic is:
|
|
78
|
-
|
|
79
|
-
- **Simple and obvious**: Direct mapping from sign to direction
|
|
80
|
-
- **Well-tested**: Part of the adapter layer
|
|
81
|
-
- **Single point of truth**: Consistency enforced at construction time
|
|
82
|
-
|
|
83
|
-
#### Why Not Add Schema Validation?
|
|
84
|
-
|
|
85
|
-
1. **Violates Single Responsibility Principle**
|
|
86
|
-
The schema's job is to validate structure, not business logic consistency. The adapter already enforces this invariant at construction time.
|
|
87
|
-
|
|
88
|
-
2. **Adds Runtime Overhead**
|
|
89
|
-
Schema validation runs on every payload validation. This adds unnecessary computation for a condition that should never occur if the adapter works correctly.
|
|
90
|
-
|
|
91
|
-
3. **Couples Schema to Business Logic**
|
|
92
|
-
The schema would need to know the thresholds and logic for determining direction. This creates tight coupling between layers that should be independent.
|
|
93
|
-
|
|
94
|
-
4. **Limited Bug Detection Value**
|
|
95
|
-
If the adapter logic is broken, we'd want to fix the adapter, not catch it at validation time. Schema validation wouldn't prevent the bug, just detect it later in the pipeline.
|
|
96
|
-
|
|
97
|
-
5. **Test Coverage Sufficient**
|
|
98
|
-
The adapter has comprehensive test coverage. Adding redundant validation at the schema level doesn't improve reliability.
|
|
99
|
-
|
|
100
|
-
### Alternative Considered
|
|
101
|
-
|
|
102
|
-
If we were truly concerned about this invariant, the better approach would be:
|
|
103
|
-
|
|
104
|
-
1. Add unit tests specifically for the `convertBalanceInfo` function
|
|
105
|
-
2. Add integration tests that verify the full payload structure
|
|
106
|
-
3. Add a comment in the schema documenting the expected relationship
|
|
107
|
-
|
|
108
|
-
But given the simplicity of the current implementation and existing test coverage, none of these are necessary.
|
|
109
|
-
|
|
110
|
-
### Recommendation
|
|
111
|
-
|
|
112
|
-
**Accept Feedback #1 as already implemented.**
|
|
113
|
-
**Respectfully decline Feedback #2** - the adapter already enforces consistency, and adding schema-level validation would violate architectural principles without meaningful benefit.
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## Files Changed
|
|
118
|
-
|
|
119
|
-
- `src/tools/schemas/outputs/reconciliationOutputs.ts` - No changes required (already has strong typing)
|
|
120
|
-
- `CODEREVIEW_RESPONSE.md` - This document
|
|
121
|
-
|
|
122
|
-
## Tests
|
|
123
|
-
|
|
124
|
-
All existing tests pass:
|
|
125
|
-
|
|
126
|
-
- ✅ `npm run type-check` - TypeScript compilation successful
|
|
127
|
-
- ✅ `npm run test:unit -- reconciliationOutputs` - 26/26 tests passing
|
|
128
|
-
- ✅ No regressions in related tests
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
# ExecutionActionRecord Schema Type Safety Improvements
|
|
2
|
-
|
|
3
|
-
## Summary
|
|
4
|
-
|
|
5
|
-
Addressed CodeRabbit's feedback about weak typing in `ExecutionActionRecordSchema` by replacing the generic `z.record(z.string(), z.unknown())` transaction field with a discriminated union based on action type.
|
|
6
|
-
|
|
7
|
-
## Problem
|
|
8
|
-
|
|
9
|
-
The original schema at line 370 in `reconciliationOutputs.ts` used:
|
|
10
|
-
|
|
11
|
-
```typescript
|
|
12
|
-
export const ExecutionActionRecordSchema = z.object({
|
|
13
|
-
type: z.string(),
|
|
14
|
-
transaction: z.record(z.string(), z.unknown()).nullable(),
|
|
15
|
-
// ...
|
|
16
|
-
});
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
This provided no type safety for transaction data, making the code prone to runtime errors if assumptions about transaction structure were incorrect.
|
|
20
|
-
|
|
21
|
-
## Solution
|
|
22
|
-
|
|
23
|
-
Implemented a **discriminated union** based on the `type` field, with each action type having its own strongly-typed transaction schema:
|
|
24
|
-
|
|
25
|
-
### Action Types & Transaction Schemas
|
|
26
|
-
|
|
27
|
-
1. **`create_transaction`** - Successful transaction creation
|
|
28
|
-
- Transaction: `CreatedTransactionSchema.nullable()`
|
|
29
|
-
- Full YNAB API transaction response with `.passthrough()` for additional fields
|
|
30
|
-
- Optional fields: `bulk_chunk_index`, `correlation_key`
|
|
31
|
-
|
|
32
|
-
2. **`create_transaction_failed`** - Failed transaction creation
|
|
33
|
-
- Transaction: `TransactionCreationPayloadSchema` (required)
|
|
34
|
-
- The request payload that failed to create
|
|
35
|
-
- Optional fields: `bulk_chunk_index`, `correlation_key`
|
|
36
|
-
|
|
37
|
-
3. **`create_transaction_duplicate`** - Duplicate detection
|
|
38
|
-
- Transaction: `DuplicateDetectionPayloadSchema` (required)
|
|
39
|
-
- Contains `transaction_id` (nullable) and optional `import_id`
|
|
40
|
-
- Required fields: `bulk_chunk_index`, `duplicate: true`
|
|
41
|
-
- Optional: `correlation_key`
|
|
42
|
-
|
|
43
|
-
4. **`update_transaction`** - Transaction update (status/date)
|
|
44
|
-
- Transaction: Union of `CreatedTransactionSchema.nullable()` (real execution) or `TransactionUpdatePayloadSchema` (dry run)
|
|
45
|
-
- Handles both full transaction responses and minimal update payloads
|
|
46
|
-
|
|
47
|
-
5. **`balance_checkpoint`** - Balance alignment checkpoint
|
|
48
|
-
- Transaction: `z.null()` (always null)
|
|
49
|
-
- No optional fields
|
|
50
|
-
|
|
51
|
-
6. **`bulk_create_fallback`** - Bulk operation fallback to sequential
|
|
52
|
-
- Transaction: `z.null()` (always null)
|
|
53
|
-
- Required: `bulk_chunk_index`
|
|
54
|
-
|
|
55
|
-
### Helper Schemas
|
|
56
|
-
|
|
57
|
-
**`CreatedTransactionSchema`**
|
|
58
|
-
|
|
59
|
-
- Validates YNAB API transaction responses
|
|
60
|
-
- Uses `.passthrough()` to allow additional API fields
|
|
61
|
-
- Required: `id`, `date`, `amount`
|
|
62
|
-
- Optional: `memo`, `cleared`, `approved`, `payee_name`, `category_name`, `import_id`
|
|
63
|
-
|
|
64
|
-
**`TransactionCreationPayloadSchema`**
|
|
65
|
-
|
|
66
|
-
- Validates transaction creation requests
|
|
67
|
-
- Required: `account_id`, `date`, `amount`
|
|
68
|
-
- Optional: `payee_name`, `memo`, `cleared`, `approved`, `import_id`
|
|
69
|
-
|
|
70
|
-
**`TransactionUpdatePayloadSchema`**
|
|
71
|
-
|
|
72
|
-
- Validates transaction update requests
|
|
73
|
-
- Required: `transaction_id`
|
|
74
|
-
- Optional: `new_date`, `cleared`
|
|
75
|
-
|
|
76
|
-
**`DuplicateDetectionPayloadSchema`**
|
|
77
|
-
|
|
78
|
-
- Validates duplicate detection metadata
|
|
79
|
-
- Required: `transaction_id` (nullable)
|
|
80
|
-
- Optional: `import_id`
|
|
81
|
-
|
|
82
|
-
## Benefits
|
|
83
|
-
|
|
84
|
-
1. **Type Safety** - Compile-time checking of transaction field structure
|
|
85
|
-
2. **Self-Documenting** - Schema clearly shows what data each action type contains
|
|
86
|
-
3. **Runtime Validation** - Zod catches malformed data before it causes issues
|
|
87
|
-
4. **Better Errors** - Discriminated union provides clear validation error messages
|
|
88
|
-
5. **Flexibility** - `.passthrough()` on `CreatedTransactionSchema` allows future YNAB API additions
|
|
89
|
-
6. **Zero Breaking Changes** - Backward compatible, all existing data validates correctly
|
|
90
|
-
|
|
91
|
-
## Testing
|
|
92
|
-
|
|
93
|
-
Created comprehensive test suite (`reconciliationOutputs.test.ts`) with 26 passing tests covering:
|
|
94
|
-
|
|
95
|
-
- All 6 action types with valid data
|
|
96
|
-
- Negative cases (wrong types, missing required fields)
|
|
97
|
-
- Discriminated union behavior (unknown types, type mismatches)
|
|
98
|
-
- Helper schema validation
|
|
99
|
-
- Edge cases (null transactions, passthrough fields)
|
|
100
|
-
|
|
101
|
-
## Files Changed
|
|
102
|
-
|
|
103
|
-
1. `src/tools/schemas/outputs/reconciliationOutputs.ts` - Schema definitions
|
|
104
|
-
2. `src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts` - New test suite
|
|
105
|
-
|
|
106
|
-
## Verification
|
|
107
|
-
|
|
108
|
-
- ✅ TypeScript compilation passes (`npm run type-check`)
|
|
109
|
-
- ✅ All 26 new schema tests pass
|
|
110
|
-
- ✅ Build succeeds (`npm run build`)
|
|
111
|
-
- ✅ MCPB package generation succeeds
|
|
112
|
-
- ✅ No runtime changes to executor.ts needed (schemas match actual usage)
|
|
113
|
-
|
|
114
|
-
## Implementation Notes
|
|
115
|
-
|
|
116
|
-
The discriminated union matches exactly how the executor currently constructs action records (verified by analyzing `src/tools/reconciliation/executor.ts` lines 226-580). No changes to runtime code were needed, proving this is a pure type-safety enhancement.
|
|
117
|
-
|
|
118
|
-
## Follow-up Opportunities
|
|
119
|
-
|
|
120
|
-
Consider applying the same pattern to other schemas with generic `z.record()` fields if similar type safety concerns exist elsewhere in the codebase.
|