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