@fuzzle/opencode-accountant 0.7.2-next.1 → 0.7.2
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/agent/accountant.md +4 -2
- package/dist/index.js +106 -46
- package/docs/tools/import-pipeline.md +64 -37
- package/package.json +1 -1
package/agent/accountant.md
CHANGED
|
@@ -118,7 +118,8 @@ The `import-pipeline` tool operates directly on the working directory:
|
|
|
118
118
|
3. **Automatic Processing**: The tool:
|
|
119
119
|
- Classifies CSV files by provider/currency (creates import contexts in `.memory/`)
|
|
120
120
|
- Extracts required accounts from rules files and updates year journal
|
|
121
|
-
- Validates
|
|
121
|
+
- Validates all transactions have matching rules (dry run)
|
|
122
|
+
- Imports transactions to the appropriate year journal
|
|
122
123
|
- Reconciles closing balance (auto-detected from CSV metadata or data, or manual override)
|
|
123
124
|
- CSV files move: `incoming/` → `pending/` → `done/`
|
|
124
125
|
4. **After Pipeline**: All changes remain uncommitted in your working directory for inspection
|
|
@@ -192,7 +193,8 @@ The following are MCP tools available to you. Always call these tools directly -
|
|
|
192
193
|
1. Classifies CSV files and creates import contexts (unless `skipClassify: true`)
|
|
193
194
|
2. For each context (sequentially, fail-fast):
|
|
194
195
|
- Extracts accounts from matched rules and updates year journal with declarations
|
|
195
|
-
- Validates
|
|
196
|
+
- Validates all transactions have matching rules (dry run)
|
|
197
|
+
- Imports transactions to year journal, moves CSV from `pending/` to `done/`
|
|
196
198
|
- Reconciles closing balance (auto-detected from CSV metadata/data or manual override)
|
|
197
199
|
3. All changes remain uncommitted in the working directory
|
|
198
200
|
|
package/dist/index.js
CHANGED
|
@@ -23667,6 +23667,28 @@ async function importStatements(directory, agent, options, configLoader = loadIm
|
|
|
23667
23667
|
}
|
|
23668
23668
|
const hasUnknowns = totalUnknown > 0;
|
|
23669
23669
|
const hasErrors = filesWithErrors > 0 || filesWithoutRules > 0;
|
|
23670
|
+
if (options.checkOnly !== false) {
|
|
23671
|
+
const result = {
|
|
23672
|
+
success: !hasUnknowns && !hasErrors,
|
|
23673
|
+
files: fileResults,
|
|
23674
|
+
summary: {
|
|
23675
|
+
filesProcessed: fileResults.length,
|
|
23676
|
+
filesWithErrors,
|
|
23677
|
+
filesWithoutRules,
|
|
23678
|
+
totalTransactions,
|
|
23679
|
+
matched: totalMatched,
|
|
23680
|
+
unknown: totalUnknown
|
|
23681
|
+
}
|
|
23682
|
+
};
|
|
23683
|
+
if (hasUnknowns) {
|
|
23684
|
+
result.message = `Found ${totalUnknown} transaction(s) with unknown accounts. Add rules to categorize them.`;
|
|
23685
|
+
} else if (hasErrors) {
|
|
23686
|
+
result.message = `Some files had errors. Check the file results for details.`;
|
|
23687
|
+
} else {
|
|
23688
|
+
result.message = "All transactions matched. Ready to import with checkOnly: false";
|
|
23689
|
+
}
|
|
23690
|
+
return JSON.stringify(result);
|
|
23691
|
+
}
|
|
23670
23692
|
if (hasUnknowns || hasErrors) {
|
|
23671
23693
|
return buildErrorResultWithDetails("Cannot import: some transactions have unknown accounts or files have errors", fileResults, {
|
|
23672
23694
|
filesProcessed: fileResults.length,
|
|
@@ -23675,7 +23697,7 @@ async function importStatements(directory, agent, options, configLoader = loadIm
|
|
|
23675
23697
|
totalTransactions,
|
|
23676
23698
|
matched: totalMatched,
|
|
23677
23699
|
unknown: totalUnknown
|
|
23678
|
-
}, "
|
|
23700
|
+
}, "Run with checkOnly: true to see details, then add missing rules");
|
|
23679
23701
|
}
|
|
23680
23702
|
const importResult = await executeImports(fileResults, directory, pendingDir, doneDir, hledgerExecutor);
|
|
23681
23703
|
if (!importResult.success) {
|
|
@@ -23715,19 +23737,32 @@ var import_statements_default = tool({
|
|
|
23715
23737
|
|
|
23716
23738
|
This tool processes CSV files in the pending import directory and uses hledger's CSV import capabilities with matching rules files.
|
|
23717
23739
|
|
|
23718
|
-
**
|
|
23719
|
-
-
|
|
23720
|
-
-
|
|
23740
|
+
**Check Mode (checkOnly: true, default):**
|
|
23741
|
+
- Runs hledger print to preview transactions
|
|
23742
|
+
- Identifies transactions with 'income:unknown' or 'expenses:unknown' accounts
|
|
23743
|
+
- These indicate missing rules that need to be added
|
|
23744
|
+
|
|
23745
|
+
**Import Mode (checkOnly: false):**
|
|
23746
|
+
- First validates all transactions have known accounts
|
|
23747
|
+
- If any unknowns exist, aborts and reports them
|
|
23721
23748
|
- If all clean, imports transactions and moves CSVs to done directory
|
|
23722
23749
|
|
|
23750
|
+
**Workflow:**
|
|
23751
|
+
1. Run with checkOnly: true (or no args)
|
|
23752
|
+
2. If unknowns found, add rules to the appropriate .rules file
|
|
23753
|
+
3. Repeat until no unknowns
|
|
23754
|
+
4. Run with checkOnly: false to import
|
|
23755
|
+
|
|
23723
23756
|
Note: This tool is typically called via import-pipeline for the full workflow.`,
|
|
23724
23757
|
args: {
|
|
23725
|
-
contextId: tool.schema.string().describe("Context ID from classify step. Used to locate the specific CSV file to process.")
|
|
23758
|
+
contextId: tool.schema.string().describe("Context ID from classify step. Used to locate the specific CSV file to process."),
|
|
23759
|
+
checkOnly: tool.schema.boolean().optional().describe("If true (default), only check for unknown accounts without importing. Set to false to perform actual import.")
|
|
23726
23760
|
},
|
|
23727
23761
|
async execute(params, context) {
|
|
23728
23762
|
const { directory, agent } = context;
|
|
23729
23763
|
return importStatements(directory, agent, {
|
|
23730
|
-
contextId: params.contextId
|
|
23764
|
+
contextId: params.contextId,
|
|
23765
|
+
checkOnly: params.checkOnly
|
|
23731
23766
|
});
|
|
23732
23767
|
}
|
|
23733
23768
|
});
|
|
@@ -24985,6 +25020,61 @@ async function buildSuggestionContext(context, contextId, logger) {
|
|
|
24985
25020
|
logger
|
|
24986
25021
|
};
|
|
24987
25022
|
}
|
|
25023
|
+
async function executeDryRunStep(context, contextId, logger) {
|
|
25024
|
+
logger?.startSection("Step 3: Dry Run Import");
|
|
25025
|
+
logger?.logStep("Dry Run", "start");
|
|
25026
|
+
const dryRunResult = await importStatements(context.directory, context.agent, {
|
|
25027
|
+
contextId,
|
|
25028
|
+
checkOnly: true
|
|
25029
|
+
}, context.configLoader, context.hledgerExecutor);
|
|
25030
|
+
const dryRunParsed = JSON.parse(dryRunResult);
|
|
25031
|
+
const message = dryRunParsed.success ? `Dry run passed: ${dryRunParsed.summary?.totalTransactions || 0} transactions ready` : `Dry run failed: ${dryRunParsed.summary?.unknown || 0} unknown account(s)`;
|
|
25032
|
+
logger?.logStep("Dry Run", dryRunParsed.success ? "success" : "error", message);
|
|
25033
|
+
if (dryRunParsed.summary?.totalTransactions) {
|
|
25034
|
+
logger?.info(`Found ${dryRunParsed.summary.totalTransactions} transactions`);
|
|
25035
|
+
}
|
|
25036
|
+
let postingsWithSuggestions = [];
|
|
25037
|
+
if (!dryRunParsed.success) {
|
|
25038
|
+
const allUnknownPostings = [];
|
|
25039
|
+
for (const file2 of dryRunParsed.files ?? []) {
|
|
25040
|
+
if (file2.unknownPostings && file2.unknownPostings.length > 0) {
|
|
25041
|
+
allUnknownPostings.push(...file2.unknownPostings);
|
|
25042
|
+
}
|
|
25043
|
+
}
|
|
25044
|
+
if (allUnknownPostings.length > 0) {
|
|
25045
|
+
try {
|
|
25046
|
+
const { suggestAccountsForPostingsBatch: suggestAccountsForPostingsBatch2 } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
|
|
25047
|
+
const suggestionContext = await buildSuggestionContext(context, contextId, logger);
|
|
25048
|
+
postingsWithSuggestions = await suggestAccountsForPostingsBatch2(allUnknownPostings, suggestionContext);
|
|
25049
|
+
} catch (error45) {
|
|
25050
|
+
logger?.error(`[ERROR] Failed to generate account suggestions: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
25051
|
+
postingsWithSuggestions = allUnknownPostings;
|
|
25052
|
+
}
|
|
25053
|
+
}
|
|
25054
|
+
}
|
|
25055
|
+
const detailsLog = postingsWithSuggestions.length > 0 ? formatUnknownPostingsLog(postingsWithSuggestions) : undefined;
|
|
25056
|
+
context.result.steps.dryRun = buildStepResult(dryRunParsed.success, message, {
|
|
25057
|
+
success: dryRunParsed.success,
|
|
25058
|
+
summary: dryRunParsed.summary,
|
|
25059
|
+
unknownPostings: postingsWithSuggestions.length > 0 ? postingsWithSuggestions : undefined,
|
|
25060
|
+
detailsLog
|
|
25061
|
+
});
|
|
25062
|
+
if (!dryRunParsed.success) {
|
|
25063
|
+
if (detailsLog) {
|
|
25064
|
+
logger?.error("Dry run found unknown accounts or errors");
|
|
25065
|
+
logger?.info(detailsLog);
|
|
25066
|
+
}
|
|
25067
|
+
logger?.endSection();
|
|
25068
|
+
context.result.error = "Dry run found unknown accounts or errors";
|
|
25069
|
+
context.result.hint = "Add rules to categorize unknown transactions, then retry. See details above for suggestions.";
|
|
25070
|
+
throw new Error("Dry run failed");
|
|
25071
|
+
}
|
|
25072
|
+
if (dryRunParsed.summary?.totalTransactions === 0) {
|
|
25073
|
+
logger?.endSection();
|
|
25074
|
+
throw new NoTransactionsError;
|
|
25075
|
+
}
|
|
25076
|
+
logger?.endSection();
|
|
25077
|
+
}
|
|
24988
25078
|
function formatUnknownPostingsLog(postings) {
|
|
24989
25079
|
if (postings.length === 0)
|
|
24990
25080
|
return "";
|
|
@@ -25021,40 +25111,18 @@ function formatUnknownPostingsLog(postings) {
|
|
|
25021
25111
|
}
|
|
25022
25112
|
async function executeImportStep(context, contextId, logger) {
|
|
25023
25113
|
const importContext = loadContext(context.directory, contextId);
|
|
25024
|
-
logger?.startSection(`Step
|
|
25114
|
+
logger?.startSection(`Step 4: Import Transactions (${importContext.accountNumber || contextId})`);
|
|
25025
25115
|
logger?.logStep("Import", "start");
|
|
25026
25116
|
const importResult = await importStatements(context.directory, context.agent, {
|
|
25027
|
-
contextId
|
|
25117
|
+
contextId,
|
|
25118
|
+
checkOnly: false
|
|
25028
25119
|
}, context.configLoader, context.hledgerExecutor);
|
|
25029
25120
|
const importParsed = JSON.parse(importResult);
|
|
25030
25121
|
const message = importParsed.success ? `Imported ${importParsed.summary?.totalTransactions || 0} transactions` : `Import failed: ${importParsed.error || "Unknown error"}`;
|
|
25031
25122
|
logger?.logStep("Import", importParsed.success ? "success" : "error", message);
|
|
25032
|
-
let postingsWithSuggestions;
|
|
25033
|
-
let detailsLog;
|
|
25034
|
-
if (!importParsed.success) {
|
|
25035
|
-
const allUnknownPostings = [];
|
|
25036
|
-
for (const file2 of importParsed.files ?? []) {
|
|
25037
|
-
if (file2.unknownPostings && file2.unknownPostings.length > 0) {
|
|
25038
|
-
allUnknownPostings.push(...file2.unknownPostings);
|
|
25039
|
-
}
|
|
25040
|
-
}
|
|
25041
|
-
if (allUnknownPostings.length > 0) {
|
|
25042
|
-
try {
|
|
25043
|
-
const { suggestAccountsForPostingsBatch: suggestAccountsForPostingsBatch2 } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
|
|
25044
|
-
const suggestionContext = await buildSuggestionContext(context, contextId, logger);
|
|
25045
|
-
postingsWithSuggestions = await suggestAccountsForPostingsBatch2(allUnknownPostings, suggestionContext);
|
|
25046
|
-
} catch (error45) {
|
|
25047
|
-
logger?.error(`[ERROR] Failed to generate account suggestions: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
25048
|
-
postingsWithSuggestions = allUnknownPostings;
|
|
25049
|
-
}
|
|
25050
|
-
detailsLog = postingsWithSuggestions && postingsWithSuggestions.length > 0 ? formatUnknownPostingsLog(postingsWithSuggestions) : undefined;
|
|
25051
|
-
}
|
|
25052
|
-
}
|
|
25053
25123
|
context.result.steps.import = buildStepResult(importParsed.success, message, {
|
|
25054
25124
|
success: importParsed.success,
|
|
25055
25125
|
summary: importParsed.summary,
|
|
25056
|
-
unknownPostings: postingsWithSuggestions,
|
|
25057
|
-
detailsLog,
|
|
25058
25126
|
error: importParsed.error
|
|
25059
25127
|
});
|
|
25060
25128
|
if (importParsed.success) {
|
|
@@ -25065,26 +25133,16 @@ async function executeImportStep(context, contextId, logger) {
|
|
|
25065
25133
|
});
|
|
25066
25134
|
}
|
|
25067
25135
|
if (!importParsed.success) {
|
|
25068
|
-
|
|
25069
|
-
logger?.error("Import found unknown accounts or errors");
|
|
25070
|
-
logger?.info(detailsLog);
|
|
25071
|
-
} else {
|
|
25072
|
-
logger?.error("Import failed", new Error(importParsed.error || "Unknown error"));
|
|
25073
|
-
}
|
|
25136
|
+
logger?.error("Import failed", new Error(importParsed.error || "Unknown error"));
|
|
25074
25137
|
logger?.endSection();
|
|
25075
|
-
context.result.error =
|
|
25076
|
-
context.result.hint = detailsLog ? "Add rules to categorize unknown transactions, then retry. See details above for suggestions." : undefined;
|
|
25138
|
+
context.result.error = `Import failed: ${importParsed.error || "Unknown error"}`;
|
|
25077
25139
|
throw new Error("Import failed");
|
|
25078
25140
|
}
|
|
25079
|
-
if (importParsed.summary?.totalTransactions === 0) {
|
|
25080
|
-
logger?.endSection();
|
|
25081
|
-
throw new NoTransactionsError;
|
|
25082
|
-
}
|
|
25083
25141
|
logger?.endSection();
|
|
25084
25142
|
}
|
|
25085
25143
|
async function executeReconcileStep(context, contextId, logger) {
|
|
25086
25144
|
const importContext = loadContext(context.directory, contextId);
|
|
25087
|
-
logger?.startSection(`Step
|
|
25145
|
+
logger?.startSection(`Step 5: Reconcile Balance (${importContext.accountNumber || contextId})`);
|
|
25088
25146
|
logger?.logStep("Reconcile", "start");
|
|
25089
25147
|
const reconcileResult = await reconcileStatement(context.directory, context.agent, {
|
|
25090
25148
|
contextId,
|
|
@@ -25163,6 +25221,7 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
25163
25221
|
const importContext = loadContext(context.directory, contextId);
|
|
25164
25222
|
logger.info(`Processing: ${importContext.filename} (${importContext.accountNumber || "unknown account"})`);
|
|
25165
25223
|
await executeAccountDeclarationsStep(context, contextId, logger);
|
|
25224
|
+
await executeDryRunStep(context, contextId, logger);
|
|
25166
25225
|
await executeImportStep(context, contextId, logger);
|
|
25167
25226
|
await executeReconcileStep(context, contextId, logger);
|
|
25168
25227
|
totalTransactions += context.result.steps.import?.details?.summary?.totalTransactions || 0;
|
|
@@ -25200,8 +25259,9 @@ This tool orchestrates the full import workflow:
|
|
|
25200
25259
|
**Pipeline Steps:**
|
|
25201
25260
|
1. **Classify**: Moves CSVs from import/incoming to import/pending (optional, skip with skipClassify)
|
|
25202
25261
|
2. **Account Declarations**: Ensures all required accounts are declared in year journal
|
|
25203
|
-
3. **
|
|
25204
|
-
4. **
|
|
25262
|
+
3. **Dry Run**: Validates all transactions have known accounts
|
|
25263
|
+
4. **Import**: Imports transactions to the journal (moves CSVs to import/done)
|
|
25264
|
+
5. **Reconcile**: Validates closing balance matches CSV metadata
|
|
25205
25265
|
|
|
25206
25266
|
**Important:**
|
|
25207
25267
|
- All changes remain uncommitted in your working directory
|
|
@@ -12,8 +12,9 @@ The pipeline automates these sequential steps:
|
|
|
12
12
|
1a. **Preprocess BTC CSV** _(Revolut only)_ - Add computed columns (fees in BTC, total, clean price) for hledger rules
|
|
13
13
|
1b. **Generate BTC Purchases** _(Revolut only)_ - Cross-reference fiat + BTC CSVs for equity conversion entries
|
|
14
14
|
2. **Account Declarations** - Ensure all accounts exist in year journals
|
|
15
|
-
3. **
|
|
16
|
-
4. **
|
|
15
|
+
3. **Dry Run** - Validate transactions, check for unknown accounts
|
|
16
|
+
4. **Import** - Add transactions to journals, move files to done
|
|
17
|
+
5. **Reconcile** - Verify balances match expectations
|
|
17
18
|
|
|
18
19
|
Steps 1a and 1b are Revolut-specific and are automatically skipped when no Revolut BTC CSV is present.
|
|
19
20
|
|
|
@@ -48,6 +49,10 @@ When all steps complete successfully:
|
|
|
48
49
|
"success": true,
|
|
49
50
|
"message": "Account declarations complete"
|
|
50
51
|
},
|
|
52
|
+
"dryRun": {
|
|
53
|
+
"success": true,
|
|
54
|
+
"message": "Dry run complete - all transactions validated"
|
|
55
|
+
},
|
|
51
56
|
"import": {
|
|
52
57
|
"success": true,
|
|
53
58
|
"message": "Imported 42 transactions"
|
|
@@ -81,9 +86,9 @@ When the incoming directory is empty:
|
|
|
81
86
|
}
|
|
82
87
|
```
|
|
83
88
|
|
|
84
|
-
### Failure - Unknown Accounts in
|
|
89
|
+
### Failure - Unknown Accounts in Dry Run
|
|
85
90
|
|
|
86
|
-
When
|
|
91
|
+
When dry run detects unknown accounts:
|
|
87
92
|
|
|
88
93
|
```json
|
|
89
94
|
{
|
|
@@ -98,13 +103,13 @@ When import detects unknown accounts:
|
|
|
98
103
|
"success": true,
|
|
99
104
|
"message": "Account declarations complete"
|
|
100
105
|
},
|
|
101
|
-
"
|
|
106
|
+
"dryRun": {
|
|
102
107
|
"success": false,
|
|
103
|
-
"message": "
|
|
108
|
+
"message": "Dry run failed: 3 transactions with unknown accounts"
|
|
104
109
|
}
|
|
105
110
|
},
|
|
106
|
-
"error": "
|
|
107
|
-
"hint": "
|
|
111
|
+
"error": "Dry run validation failed: 3 transactions with unknown accounts",
|
|
112
|
+
"hint": "Update rules file to categorize unknown transactions"
|
|
108
113
|
}
|
|
109
114
|
```
|
|
110
115
|
|
|
@@ -119,6 +124,7 @@ When reconciliation fails:
|
|
|
119
124
|
"steps": {
|
|
120
125
|
"classify": { "success": true },
|
|
121
126
|
"accountDeclarations": { "success": true },
|
|
127
|
+
"dryRun": { "success": true },
|
|
122
128
|
"import": { "success": true, "message": "Imported 42 transactions" },
|
|
123
129
|
"reconcile": {
|
|
124
130
|
"success": false,
|
|
@@ -200,17 +206,34 @@ See [classify-statements](classify-statements.md) for details.
|
|
|
200
206
|
- Multiple years in single CSV
|
|
201
207
|
- Cannot create year journal file
|
|
202
208
|
|
|
203
|
-
### Step 3:
|
|
209
|
+
### Step 3: Dry Run
|
|
210
|
+
|
|
211
|
+
**Purpose**: Validate all transactions can be categorized before importing
|
|
212
|
+
|
|
213
|
+
**What happens** (per context):
|
|
214
|
+
|
|
215
|
+
1. Loads context to find CSV file
|
|
216
|
+
2. Runs `hledger print` with rules file
|
|
217
|
+
3. Checks for `income:unknown` or `expenses:unknown` accounts
|
|
218
|
+
4. Reports unknown postings if found
|
|
219
|
+
|
|
220
|
+
**Failure scenarios**:
|
|
221
|
+
|
|
222
|
+
- Unknown accounts detected → **Pipeline stops here**
|
|
223
|
+
- CSV parsing errors
|
|
224
|
+
- Rules file syntax errors
|
|
204
225
|
|
|
205
|
-
**
|
|
226
|
+
**User action required**: Update rules file to categorize unknown transactions, then re-run pipeline.
|
|
227
|
+
|
|
228
|
+
### Step 4: Import
|
|
229
|
+
|
|
230
|
+
**Purpose**: Add transactions to journal and mark files as processed
|
|
206
231
|
|
|
207
232
|
**What happens** (per context):
|
|
208
233
|
|
|
209
234
|
1. Loads context to find CSV file
|
|
210
|
-
2.
|
|
211
|
-
3.
|
|
212
|
-
4. Imports transactions using `hledger import`
|
|
213
|
-
5. Moves CSV from `pending/` to `done/`
|
|
235
|
+
2. Imports transactions using `hledger import`
|
|
236
|
+
3. Moves CSV from `pending/` to `done/`
|
|
214
237
|
4. Updates context with:
|
|
215
238
|
- `rulesFile` path
|
|
216
239
|
- `yearJournal` path
|
|
@@ -219,14 +242,13 @@ See [classify-statements](classify-statements.md) for details.
|
|
|
219
242
|
|
|
220
243
|
**Failure scenarios**:
|
|
221
244
|
|
|
222
|
-
- Unknown accounts detected → **Pipeline stops here** with account suggestions
|
|
223
245
|
- Duplicate transactions detected
|
|
224
246
|
- Journal file write error
|
|
225
247
|
- File move operation fails
|
|
226
248
|
|
|
227
249
|
See [import-statements](import-statements.md) for details.
|
|
228
250
|
|
|
229
|
-
### Step
|
|
251
|
+
### Step 5: Reconcile
|
|
230
252
|
|
|
231
253
|
**Purpose**: Verify imported transactions result in correct closing balance
|
|
232
254
|
|
|
@@ -298,7 +320,7 @@ See [reconcile-statement](reconcile-statement.md) for details.
|
|
|
298
320
|
│ │
|
|
299
321
|
▼ │
|
|
300
322
|
┌────────────────────────────────────────┐ │
|
|
301
|
-
│ STEPS 2-
|
|
323
|
+
│ STEPS 2-5 (for uuid-1): │ │
|
|
302
324
|
│ • Account Declarations │ │
|
|
303
325
|
│ • Dry Run │ │
|
|
304
326
|
│ • Import │ │
|
|
@@ -309,9 +331,10 @@ See [reconcile-statement](reconcile-statement.md) for details.
|
|
|
309
331
|
│ │
|
|
310
332
|
│ ▼
|
|
311
333
|
│ ┌────────────────────────────────────────┐
|
|
312
|
-
│ │ STEPS 2-
|
|
334
|
+
│ │ STEPS 2-5 (for uuid-2): │
|
|
313
335
|
│ │ • Account Declarations │
|
|
314
|
-
│
|
|
336
|
+
│ │ • Dry Run │
|
|
337
|
+
│ │ • Import │
|
|
315
338
|
│ │ • Reconcile │
|
|
316
339
|
│ │ • UPDATE: .memory/uuid-2.json │
|
|
317
340
|
│ └────────────────────────────────────────┘
|
|
@@ -330,7 +353,7 @@ The pipeline processes contexts **one at a time**:
|
|
|
330
353
|
```
|
|
331
354
|
for each contextId:
|
|
332
355
|
1. Load context
|
|
333
|
-
2. Run steps 2-
|
|
356
|
+
2. Run steps 2-5 for this context
|
|
334
357
|
3. If ANY step fails → STOP PIPELINE
|
|
335
358
|
4. Update context with results
|
|
336
359
|
5. Move to next context
|
|
@@ -394,8 +417,8 @@ classify-statements
|
|
|
394
417
|
# Output: { "contexts": ["uuid-1", "uuid-2"] }
|
|
395
418
|
|
|
396
419
|
# Import each manually
|
|
397
|
-
import-statements --contextId "uuid-1"
|
|
398
|
-
import-statements --contextId "uuid-2"
|
|
420
|
+
import-statements --contextId "uuid-1" --checkOnly false
|
|
421
|
+
import-statements --contextId "uuid-2" --checkOnly false
|
|
399
422
|
|
|
400
423
|
# Reconcile with different balances
|
|
401
424
|
reconcile-statement --contextId "uuid-1" --closingBalance "EUR 1000.00"
|
|
@@ -415,12 +438,12 @@ import-pipeline --skipClassify
|
|
|
415
438
|
### Scenario 5: Handling Unknown Accounts
|
|
416
439
|
|
|
417
440
|
```bash
|
|
418
|
-
# First run:
|
|
441
|
+
# First run: dry run fails
|
|
419
442
|
import-pipeline
|
|
420
|
-
# Output: "
|
|
443
|
+
# Output: "Dry run failed: 3 transactions with unknown accounts"
|
|
421
444
|
|
|
422
|
-
# Check which transactions are unknown
|
|
423
|
-
import-statements --contextId {from-output}
|
|
445
|
+
# Check which transactions are unknown
|
|
446
|
+
import-statements --contextId {from-output} --checkOnly true
|
|
424
447
|
# Shows unknown postings with suggestions
|
|
425
448
|
|
|
426
449
|
# Update rules file
|
|
@@ -439,7 +462,7 @@ import-pipeline --skipClassify
|
|
|
439
462
|
| ----------------------- | ------------------- | ----------------------------- | --------------------------------------------------- |
|
|
440
463
|
| File collision | Classify | Target file already exists | Move existing file to done or delete |
|
|
441
464
|
| Unrecognized CSV | Classify | Unknown provider | Add provider config or move to pending manually |
|
|
442
|
-
| Unknown accounts |
|
|
465
|
+
| Unknown accounts | Dry Run | Transactions without rules | Update rules file to categorize |
|
|
443
466
|
| Balance mismatch | Reconcile | Missing transactions | Check skip rules, verify all transactions imported |
|
|
444
467
|
| Context not found | Import/Reconcile | Invalid context ID | Re-run classify-statements |
|
|
445
468
|
| Multi-year CSV | Account Declaration | CSV spans multiple years | Split CSV by year or import to single year manually |
|
|
@@ -458,9 +481,9 @@ The output shows which step failed:
|
|
|
458
481
|
"steps": {
|
|
459
482
|
"classify": { "success": true },
|
|
460
483
|
"accountDeclarations": { "success": true },
|
|
461
|
-
"
|
|
484
|
+
"dryRun": { "success": false } // ← Failed here
|
|
462
485
|
},
|
|
463
|
-
"error": "
|
|
486
|
+
"error": "Dry run validation failed..."
|
|
464
487
|
}
|
|
465
488
|
```
|
|
466
489
|
|
|
@@ -471,7 +494,7 @@ The output shows which step failed:
|
|
|
471
494
|
# contexts: ["abc123-..."]
|
|
472
495
|
|
|
473
496
|
# Run the failed step individually
|
|
474
|
-
import-statements --contextId "abc123-..."
|
|
497
|
+
import-statements --contextId "abc123-..." --checkOnly true
|
|
475
498
|
```
|
|
476
499
|
|
|
477
500
|
**Step 3: Fix issue and resume**
|
|
@@ -563,7 +586,7 @@ Context 3: classify → import → reconcile ✓
|
|
|
563
586
|
The pipeline invokes these tools internally:
|
|
564
587
|
|
|
565
588
|
1. `classify-statements` → Returns context IDs
|
|
566
|
-
2. `import-statements` (per context) →
|
|
589
|
+
2. `import-statements` (per context) → Updates contexts
|
|
567
590
|
3. `reconcile-statement` (per context) → Updates contexts
|
|
568
591
|
|
|
569
592
|
### Manual Tool Usage
|
|
@@ -575,8 +598,11 @@ You can run tools independently for more control:
|
|
|
575
598
|
classify-statements
|
|
576
599
|
# → Get contextIds
|
|
577
600
|
|
|
578
|
-
import-statements --contextId {uuid}
|
|
579
|
-
# → Validate
|
|
601
|
+
import-statements --contextId {uuid} --checkOnly true
|
|
602
|
+
# → Validate first
|
|
603
|
+
|
|
604
|
+
import-statements --contextId {uuid} --checkOnly false
|
|
605
|
+
# → Import
|
|
580
606
|
|
|
581
607
|
reconcile-statement --contextId {uuid}
|
|
582
608
|
# → Reconcile
|
|
@@ -605,10 +631,11 @@ For typical import (1-2 CSVs, 50-100 transactions each):
|
|
|
605
631
|
|
|
606
632
|
- **Classify**: <1 second
|
|
607
633
|
- **Account Declarations**: <1 second
|
|
608
|
-
- **
|
|
634
|
+
- **Dry Run**: 1-2 seconds (hledger processing)
|
|
635
|
+
- **Import**: 1-2 seconds (hledger processing)
|
|
609
636
|
- **Reconcile**: 1-2 seconds (hledger queries)
|
|
610
637
|
|
|
611
|
-
**Total**: ~
|
|
638
|
+
**Total**: ~5-10 seconds per context
|
|
612
639
|
|
|
613
640
|
### Scalability
|
|
614
641
|
|
|
@@ -625,5 +652,5 @@ If you regularly import 50+ CSVs, consider:
|
|
|
625
652
|
|
|
626
653
|
- [Import Context Architecture](../architecture/import-context.md) - Deep dive into context system
|
|
627
654
|
- [classify-statements Tool](classify-statements.md) - Step 1: Classification
|
|
628
|
-
- [import-statements Tool](import-statements.md) - Step
|
|
629
|
-
- [reconcile-statement Tool](reconcile-statement.md) - Step
|
|
655
|
+
- [import-statements Tool](import-statements.md) - Step 4: Import
|
|
656
|
+
- [reconcile-statement Tool](reconcile-statement.md) - Step 5: Reconciliation
|