@gabrielerandelli/minus-tracker 0.8.0 → 0.8.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/README.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
4
4
  [![npm version](https://img.shields.io/npm/v/@gabrielerandelli/minus-tracker.svg)](https://www.npmjs.com/package/@gabrielerandelli/minus-tracker)
5
+ [![npm downloads](https://img.shields.io/npm/dm/@gabrielerandelli/minus-tracker.svg)](https://www.npmjs.com/package/@gabrielerandelli/minus-tracker)
6
+ [![Node](https://img.shields.io/badge/node-%3E%3D24-brightgreen.svg)](https://nodejs.org)
7
+ [![MCP](https://img.shields.io/badge/MCP-server-blueviolet.svg)](https://modelcontextprotocol.io)
8
+
9
+ ⚠️ **Ausilio al calcolo, non consulenza fiscale** — vedi l'[Avvertenza](#avvertenza).
10
+ ⚠️ **Calculation aid only, not tax advice** — see the [Disclaimer](#disclaimer).
11
+
12
+ 🤖 **Novità: server MCP** — qualsiasi agente AI può calcolare plusvalenze/minusvalenze italiane direttamente, senza passare dalla CLI.
13
+ 🤖 **New: MCP server** — any AI agent can compute Italian capital gains/losses directly, no CLI required.
14
+
15
+ ✅ Validato su tutti i 12 scenari FAQ dell'Agenzia delle Entrate · 120 test automatici · zero dipendenze runtime nella libreria core.
16
+ ✅ Validated against all 12 Agenzia delle Entrate FAQ scenarios · 120 automated tests · zero runtime dependencies in the core library.
17
+
18
+ _L'unico tool open-source che trasforma un export DEGIRO grezzo direttamente in plusvalenze/minusvalenze corrette secondo l'Agenzia delle Entrate — come CLI, libreria, o strumento MCP richiamabile dal tuo agente AI._
19
+ _The only open-source tool that turns a raw DEGIRO export directly into Agenzia-Entrate-correct capital gains/losses — as a CLI, a library, or an MCP tool your AI agent can call._
5
20
 
6
21
  ---
7
22
 
@@ -16,8 +31,9 @@ Il tool elabora i dati partendo direttamente dal formato CSV esportato da DEGIRO
16
31
  - Calcolo di plusvalenze e minusvalenze con gestione dei lotti via **LIFO o FIFO** (configurabile)
17
32
  - **Parser integrato** per i file CSV di DEGIRO
18
33
  - Gestione **multivaluta** con tassi storici BCE (EUR, USD, GBP, CHF)
19
- - **Classificazione a due categorie** (Bucket A/B) degli strumenti finanziari — azioni, ETF, titoli
20
- di stato, derivati — con classificazione automatica via OpenFIGI e sidecar JSON persistente
34
+ - **Classificazione fiscale degli strumenti finanziari in due categorie** (Bucket A/B) — azioni,
35
+ ETF, titoli di stato, derivati — con classificazione automatica tramite OpenFIGI e sidecar JSON
36
+ persistente
21
37
  - **Modello Redditi PF**: genera Quadro RT (redditi diversi) e Quadro RM (redditi di capitale) a
22
38
  partire dai lotti calcolati, con riporto delle minusvalenze pregresse (regola dei 4 anni)
23
39
  - Suite di test allineata alle **FAQ dell'Agenzia delle Entrate**
@@ -27,7 +43,7 @@ Il tool elabora i dati partendo direttamente dal formato CSV esportato da DEGIRO
27
43
  **Novità in v0.8.0:**
28
44
 
29
45
  - **Modalità stateless per `Classifier`**: `classify()` può ora funzionare senza alcun accesso al
30
- filesystem (nessun sidecar), utile per l'integrazione in agenti/automazioni
46
+ filesystem (nessun sidecar), utile per l'integrazione con agenti e automazioni
31
47
  - **Server MCP** (`minus-tracker-mcp`): espone `parse_transactions`, `classify_instruments` e
32
48
  `calculate_gains` come tool MCP su stdio, per l'uso diretto da parte di agenti AI — vedi la
33
49
  sezione [Server MCP](#server-mcp) più sotto
@@ -97,16 +113,16 @@ npx @gabrielerandelli/minus-tracker calc trades.csv
97
113
 
98
114
  ### Utilizzo CLI
99
115
 
100
- | Comando | Flag principali | Note |
101
- | ---------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
102
- | `calc <file.csv>` | `--method LIFO\|FIFO` (default: LIFO), `--lang it\|en`, `--json`, `--export-dichiarazione [path]`, `--carry-forward` | Aggiorna i tassi BCE automaticamente se la snapshot ha più di 7 giorni; usa il sidecar di `classify` se presente |
103
- | `classify <file.csv>` | `--offline` | Classifica gli strumenti (Bucket A/B) e crea/aggiorna il sidecar `*.classify.json` |
104
- | `validate <file.csv>` | `--lang it\|en` | Exit 0 con avvisi; exit 1 in caso di errori bloccanti |
105
- | `rates --check` | — | Mostra la copertura della snapshot BCE in locale |
106
- | `rates --update` | — | Scarica i tassi aggiornati dall'API BCE |
107
- | `config --lang it\|en` | — | Salva la lingua preferita |
108
- | `config --show` | — | Mostra la lingua correntemente impostata |
109
- | `stress-test` | `--range N-M`, `--keep`, `--json`, `--output-dir` | Documentato in fondo |
116
+ | Comando | Flag principali | Note |
117
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
118
+ | `calc <file.csv>` | `--method LIFO\|FIFO` (default: LIFO), `--lang it\|en`, `--json`, `--export-dichiarazione [path]`, `--carry-forward` | Non aggiorna mai i tassi BCE da solo esegui `rates --update` periodicamente; usa il sidecar di `classify` se presente |
119
+ | `classify <file.csv>` | `--offline` | Classifica gli strumenti (Bucket A/B) e crea/aggiorna il sidecar `*.classify.json`. Richiede un terminale interattivo (TTY), oppure il flag `--offline` in contesti scriptati/CI |
120
+ | `validate <file.csv>` | `--lang it\|en` | Exit 0 con avvisi; exit 1 in caso di errori bloccanti |
121
+ | `rates --check` | — | Mostra la copertura della snapshot BCE in locale |
122
+ | `rates --update` | — | Scarica i tassi aggiornati dall'API BCE |
123
+ | `config --lang it\|en` | — | Salva la lingua preferita |
124
+ | `config --show` | — | Mostra la lingua correntemente impostata |
125
+ | `stress-test` | `--range N-M`, `--keep`, `--json`, `--output-dir` | Documentato in fondo |
110
126
 
111
127
  Precedenza lingua: `--lang` > lingua salvata > italiano (default).
112
128
 
@@ -157,7 +173,7 @@ if (parser.warnings.length > 0) {
157
173
  // 2. Classificazione (Bucket A/B) — facoltativa, abilita Quadro RT/RM nel report
158
174
  const classification = await new Classifier().classify(
159
175
  transactions,
160
- "trades.classify.json", // sidecar persistente; omettilo per la modalità stateless
176
+ "trades.classify.json", // sidecar persistente; assente in modalità stateless
161
177
  );
162
178
 
163
179
  // 3. Calcolo
@@ -227,15 +243,15 @@ Configurazione tipica per un client MCP (es. Claude Desktop, `claude_desktop_con
227
243
 
228
244
  Il server espone 3 tool:
229
245
 
230
- | Tool | Descrizione |
231
- | ---------------------- | ------------------------------------------------------------------------------ |
232
- | `parse_transactions` | Effettua il parsing di un CSV DEGIRO in `transactions`/`warnings`/`incomeRows` |
233
- | `classify_instruments` | Classifica gli ISIN in Bucket A/B (modalità stateless — nessun sidecar) |
234
- | `calculate_gains` | Calcola plusvalenze/minusvalenze (LIFO/FIFO) e, se disponibile, Quadro RT/RM |
246
+ | Tool | Descrizione |
247
+ | ---------------------- | -------------------------------------------------------------------------------------- |
248
+ | `parse_transactions` | Esegue il parsing di un CSV DEGIRO, restituendo `transactions`/`warnings`/`incomeRows` |
249
+ | `classify_instruments` | Classifica gli ISIN in Bucket A/B (modalità stateless — nessun sidecar) |
250
+ | `calculate_gains` | Calcola plusvalenze/minusvalenze (LIFO/FIFO) e, se disponibile, Quadro RT/RM |
235
251
 
236
252
  `classify_instruments` supporta `existingClassification`, `overrides` e `offline: true` per
237
253
  funzionare senza rete e senza accesso al filesystem — pensato per essere invocato ripetutamente
238
- da un agente su più chiamate, mantenendo lo stato lato client.
254
+ da un agente in chiamate successive, mantenendo lo stato lato client.
239
255
 
240
256
  ### Domande frequenti
241
257
 
@@ -249,7 +265,7 @@ Le righe vengono saltate (senza bloccare il calcolo) quando: l'ISIN è vuoto, la
249
265
  Il CSV contiene una vendita per un titolo senza un acquisto precedente nel file. L'acquisto potrebbe essere in un export di un anno precedente non incluso. Usa `validate` per esaminare le transazioni parsate.
250
266
 
251
267
  **I tassi BCE sono scaduti**
252
- Esegui `minus-tracker rates --update`. Il comando `calc` esegue l'aggiornamento automaticamente se la snapshot ha più di 7 giorni.
268
+ Esegui `minus-tracker rates --update`. Il comando `calc` non aggiorna mai i tassi da solo: l'aggiornamento è sempre un'azione esplicita.
253
269
 
254
270
  **LIFO o FIFO?**
255
271
  Il Regime Dichiarativo utilizza il LIFO come metodo predefinito ai sensi della normativa fiscale italiana. Il FIFO è disponibile per confronto. Consulta il tuo commercialista per conferma.
@@ -393,16 +409,16 @@ npx @gabrielerandelli/minus-tracker calc trades.csv
393
409
 
394
410
  ### CLI Usage
395
411
 
396
- | Command | Key flags | Notes |
397
- | ---------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
398
- | `calc <file.csv>` | `--method LIFO\|FIFO` (default: LIFO), `--lang it\|en`, `--json`, `--export-dichiarazione [path]`, `--carry-forward` | Auto-fetches ECB rates if snapshot is more than 7 days old; uses `classify`'s sidecar when present |
399
- | `classify <file.csv>` | `--offline` | Classifies instruments (Bucket A/B) and creates/updates the `*.classify.json` sidecar |
400
- | `validate <file.csv>` | `--lang it\|en` | Exit 0 with warnings; exit 1 on hard errors |
401
- | `rates --check` | — | Shows bundled ECB snapshot coverage |
402
- | `rates --update` | — | Fetches fresh rates from the ECB API |
403
- | `config --lang it\|en` | — | Saves language preference |
404
- | `config --show` | — | Shows current language setting |
405
- | `stress-test` | `--range N-M`, `--keep`, `--json`, `--output-dir` | Documented below |
412
+ | Command | Key flags | Notes |
413
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
414
+ | `calc <file.csv>` | `--method LIFO\|FIFO` (default: LIFO), `--lang it\|en`, `--json`, `--export-dichiarazione [path]`, `--carry-forward` | Never fetches ECB rates on its own run `rates --update` periodically; uses `classify`'s sidecar when present |
415
+ | `classify <file.csv>` | `--offline` | Classifies instruments (Bucket A/B) and creates/updates the `*.classify.json` sidecar. Requires an interactive terminal (TTY), or the `--offline` flag in scripted/CI contexts |
416
+ | `validate <file.csv>` | `--lang it\|en` | Exit 0 with warnings; exit 1 on hard errors |
417
+ | `rates --check` | — | Shows bundled ECB snapshot coverage |
418
+ | `rates --update` | — | Fetches fresh rates from the ECB API |
419
+ | `config --lang it\|en` | — | Saves language preference |
420
+ | `config --show` | — | Shows current language setting |
421
+ | `stress-test` | `--range N-M`, `--keep`, `--json`, `--output-dir` | Documented below |
406
422
 
407
423
  Language precedence: `--lang` flag > saved config > Italian (default).
408
424
 
@@ -543,7 +559,7 @@ Rows are skipped (without aborting the calculation) when: the ISIN is empty, the
543
559
  The CSV contains a SELL for a position that has no prior BUY in the same file. The BUY may be in a prior year's export that was not included. Use `validate` to inspect the parsed transactions.
544
560
 
545
561
  **ECB rates are outdated**
546
- Run `minus-tracker rates --update`. The `calc` command also auto-updates if the snapshot is more than 7 days old.
562
+ Run `minus-tracker rates --update`. The `calc` command never fetches rates on its own — refreshing is always an explicit, user-invoked action.
547
563
 
548
564
  **LIFO or FIFO?**
549
565
  LIFO is the standard lot-matching method under Italian tax law for the Regime Dichiarativo. FIFO is available for comparison or other jurisdictions. Consult your tax advisor for confirmation.
package/dist/cli/index.js CHANGED
@@ -9,6 +9,9 @@ var it = {
9
9
  errorInvalidCsv: "CSV non valido: impossibile analizzare il file",
10
10
  errorMissingColumn: (col) => `Colonna obbligatoria mancante: ${col}`,
11
11
  errorNoOpenLots: (isin, date) => `Nessun lotto aperto per ISIN ${isin} in data ${date}`,
12
+ errorCannotReadFile: (path7) => `Impossibile leggere il file: ${path7}`,
13
+ errorCannotLoadSidecar: (path7) => `Impossibile caricare il sidecar: ${path7}`,
14
+ errorCannotWriteExport: (path7) => `Impossibile scrivere l'export della dichiarazione: ${path7}`,
12
15
  warnMissingIsin: (row) => `Riga ${row}: ISIN mancante \u2014 riga ignorata`,
13
16
  warnUnsupportedCurrency: (row, currency) => `Riga ${row}: valuta non supportata ${currency} \u2014 riga ignorata`,
14
17
  warnNoEcbRate: (row, currency, date) => `Riga ${row}: nessun tasso BCE per ${currency} in data ${date} \u2014 riga ignorata`,
@@ -88,7 +91,7 @@ var it = {
88
91
  quadroRMRitenuta: "Ritenuta estera (credito)",
89
92
  quadroRMCedole: "[RM-C] Cedole obbligazionarie",
90
93
  dichiarazioneDisclaimer: "minus-tracker \xE8 un ausilio al calcolo, non consulenza fiscale.",
91
- warnNoDichiarazioneSidecar: "Eseguire prima: minus-tracker classify <file>",
94
+ warnNoDichiarazioneSidecar: "Suggerimento: esegui prima 'minus-tracker classify <file>' per abilitare il Quadro RT/RM.",
92
95
  warnNoCarryForwardProvided: "(eventuali perdite pregresse non applicate)"
93
96
  };
94
97
 
@@ -98,6 +101,9 @@ var en = {
98
101
  errorInvalidCsv: "Invalid CSV: unable to parse",
99
102
  errorMissingColumn: (col) => `Missing required column: ${col}`,
100
103
  errorNoOpenLots: (isin, date) => `No open lots for ISIN ${isin} on ${date}`,
104
+ errorCannotReadFile: (path7) => `Cannot read file: ${path7}`,
105
+ errorCannotLoadSidecar: (path7) => `Cannot load sidecar: ${path7}`,
106
+ errorCannotWriteExport: (path7) => `Cannot write dichiarazione export: ${path7}`,
101
107
  warnMissingIsin: (row) => `Row ${row}: missing ISIN \u2014 skipped`,
102
108
  warnUnsupportedCurrency: (row, currency) => `Row ${row}: unsupported currency ${currency} \u2014 skipped`,
103
109
  warnNoEcbRate: (row, currency, date) => `Row ${row}: no ECB rate for ${currency} on ${date} \u2014 skipped`,
@@ -177,7 +183,7 @@ var en = {
177
183
  quadroRMRitenuta: "Foreign withholding (credit)",
178
184
  quadroRMCedole: "[RM-C] Bond coupons",
179
185
  dichiarazioneDisclaimer: "minus-tracker \xE8 un ausilio al calcolo, non consulenza fiscale.",
180
- warnNoDichiarazioneSidecar: "Run first: minus-tracker classify <file>",
186
+ warnNoDichiarazioneSidecar: "Tip: run 'minus-tracker classify <file>' first to enable Quadro RT/RM.",
181
187
  warnNoCarryForwardProvided: "(prior-year losses not applied)"
182
188
  };
183
189
 
@@ -377,6 +383,16 @@ var REQUIRED_COLUMNS = [
377
383
  ];
378
384
  var INCOME_KEYWORDS = ["DIVIDEND", "COUPON", "INTEREST", "CEDOLA"];
379
385
  var TAX_KEYWORDS = ["DIVIDEND TAX", "WITHHOLDING", "RITENUTA"];
386
+ var BINARY_GARBAGE_CONTROL_CHAR_RATIO = 0.1;
387
+ function looksLikeBinaryGarbage(firstLine) {
388
+ if (firstLine.length === 0) return false;
389
+ let controlChars = 0;
390
+ for (let i = 0; i < firstLine.length; i++) {
391
+ const code = firstLine.charCodeAt(i);
392
+ if (code < 32 && code !== 9) controlChars++;
393
+ }
394
+ return controlChars / firstLine.length > BINARY_GARBAGE_CONTROL_CHAR_RATIO;
395
+ }
380
396
  function parseCSVRow(line) {
381
397
  const fields = [];
382
398
  let i = 0;
@@ -439,6 +455,10 @@ var DEGIROParser = class {
439
455
  if (typeof csv !== "string" || csv.includes("\0")) {
440
456
  throw new ParseError("INVALID_CSV");
441
457
  }
458
+ const firstLine = csv.split("\n", 1)[0] ?? "";
459
+ if (looksLikeBinaryGarbage(firstLine)) {
460
+ throw new ParseError("INVALID_CSV");
461
+ }
442
462
  let rows;
443
463
  try {
444
464
  rows = parseCSV(csv);
@@ -954,12 +974,19 @@ var Calculator = class {
954
974
  );
955
975
  let remaining = bPlusvalenze - bMinusvalenze;
956
976
  let carryForwardApplied = 0;
977
+ const carryForwardEntriesRemaining = [];
957
978
  for (const entry of carryForwards) {
958
979
  if (taxYear - entry.year > 4) continue;
959
- if (remaining <= 0) break;
960
- const consumed = Math.min(entry.amount, remaining);
980
+ const consumed = remaining > 0 ? Math.min(entry.amount, remaining) : 0;
961
981
  carryForwardApplied += consumed;
962
982
  remaining -= consumed;
983
+ const residual = roundHalfUp2(entry.amount - consumed);
984
+ if (residual > 0) {
985
+ carryForwardEntriesRemaining.push({
986
+ annoOrigine: entry.year,
987
+ importo: residual
988
+ });
989
+ }
963
990
  }
964
991
  carryForwardApplied = roundHalfUp2(carryForwardApplied);
965
992
  const bNetResult = roundHalfUp2(
@@ -971,6 +998,7 @@ var Calculator = class {
971
998
  minusvalenze: bMinusvalenze,
972
999
  carryForwardApplied,
973
1000
  carryForwardRemaining,
1001
+ carryForwardEntriesRemaining,
974
1002
  netResult: bNetResult
975
1003
  };
976
1004
  const allIncomeRows = this._options.incomeRows ?? [];
@@ -1015,7 +1043,7 @@ var Calculator = class {
1015
1043
  const hasOther = allIsins.some((isin) => !isin.startsWith("IE"));
1016
1044
  if (hasIePrefix && hasOther) {
1017
1045
  warnings.push(
1018
- "AVVISO: CSV contiene tipi di strumenti misti (es. ETF + Azioni).\n Il calcolo a bucket unico pu\xF2 non essere fiscalmente corretto.\n Esegui: minus-tracker classify trades.csv"
1046
+ "WARNING: CSV contains mixed instrument types (e.g. ETFs + Stocks).\n Single-bucket calculation may not be fiscally correct.\n Run: minus-tracker classify trades.csv"
1019
1047
  );
1020
1048
  }
1021
1049
  }
@@ -1689,8 +1717,7 @@ async function runCalc(positional, flags, s, stdout, stderr) {
1689
1717
  try {
1690
1718
  csv = fs4.readFileSync(filePath, "utf8");
1691
1719
  } catch {
1692
- stderr.write(`Cannot read file: ${filePath}
1693
- `);
1720
+ stderr.write(s.errorCannotReadFile(filePath) + "\n");
1694
1721
  return 1;
1695
1722
  }
1696
1723
  const method = flags["method"] ?? "LIFO";
@@ -1705,8 +1732,7 @@ async function runCalc(positional, flags, s, stdout, stderr) {
1705
1732
  const classifier = new Classifier({ interactive: false });
1706
1733
  classification = await classifier.load(sidecarPath);
1707
1734
  } catch {
1708
- stderr.write(`Cannot load sidecar: ${sidecarPath}
1709
- `);
1735
+ stderr.write(s.errorCannotLoadSidecar(sidecarPath) + "\n");
1710
1736
  return 1;
1711
1737
  }
1712
1738
  }
@@ -1776,8 +1802,7 @@ async function runCalc(positional, flags, s, stdout, stderr) {
1776
1802
  try {
1777
1803
  await report.dichiarazione.exportTo(exportPath);
1778
1804
  } catch {
1779
- stderr.write(`Cannot write dichiarazione export: ${exportPath}
1780
- `);
1805
+ stderr.write(s.errorCannotWriteExport(exportPath) + "\n");
1781
1806
  return 1;
1782
1807
  }
1783
1808
  }
@@ -1869,18 +1894,45 @@ function getSnapshotPath() {
1869
1894
  }
1870
1895
  return path4.join(configDir, "minus-tracker", "ecb-rates.json");
1871
1896
  }
1897
+ function countMissingBusinessDays(startDate, endDate, dates) {
1898
+ let missing = 0;
1899
+ const cursor = /* @__PURE__ */ new Date(startDate + "T00:00:00Z");
1900
+ const end = /* @__PURE__ */ new Date(endDate + "T00:00:00Z");
1901
+ while (cursor <= end) {
1902
+ const day = cursor.getUTCDay();
1903
+ const iso = cursor.toISOString().slice(0, 10);
1904
+ if (day !== 0 && day !== 6 && dates[iso] === void 0) missing++;
1905
+ cursor.setUTCDate(cursor.getUTCDate() + 1);
1906
+ }
1907
+ return missing;
1908
+ }
1872
1909
  function getCoverage(snapshot) {
1873
1910
  let start = "9999-12-31";
1874
1911
  let end = "0000-01-01";
1875
1912
  const currencies = [];
1913
+ const gapEntries = [];
1876
1914
  for (const [ccy, dates] of Object.entries(snapshot)) {
1877
1915
  currencies.push(ccy);
1878
- for (const d of Object.keys(dates)) {
1916
+ const keys = Object.keys(dates).sort();
1917
+ for (const d of keys) {
1879
1918
  if (d < start) start = d;
1880
1919
  if (d > end) end = d;
1881
1920
  }
1921
+ if (keys.length > 0) {
1922
+ const missing = countMissingBusinessDays(
1923
+ keys[0],
1924
+ keys[keys.length - 1],
1925
+ dates
1926
+ );
1927
+ if (missing > 0) gapEntries.push(`${ccy}: ${missing}`);
1928
+ }
1882
1929
  }
1883
- return { start, end, currencies: currencies.sort().join(", ") };
1930
+ return {
1931
+ start,
1932
+ end,
1933
+ currencies: currencies.sort().join(", "),
1934
+ gaps: gapEntries.sort().join(", ")
1935
+ };
1884
1936
  }
1885
1937
  async function fetchEcbData(currency) {
1886
1938
  return new Promise((resolve2, reject) => {
@@ -1944,9 +1996,9 @@ async function updateRates(snapshotPath, stdout, stderr, s) {
1944
1996
  async function runRates(positional, flags, s, stdout, stderr) {
1945
1997
  if (flags["check"]) {
1946
1998
  const snapshot = getActiveSnapshot();
1947
- const { start, end, currencies } = getCoverage(snapshot);
1999
+ const { start, end, currencies, gaps } = getCoverage(snapshot);
1948
2000
  stdout.write(s.ratesCoverage(start, end, currencies) + "\n");
1949
- stdout.write(s.ratesGapsNone + "\n");
2001
+ stdout.write((gaps ? s.ratesGaps(gaps) : s.ratesGapsNone) + "\n");
1950
2002
  return 0;
1951
2003
  }
1952
2004
  if (flags["update"]) {
@@ -2340,6 +2392,7 @@ async function runStressTest(_positionals, flags, stdout, stderr) {
2340
2392
  const cliBin = process.argv[1];
2341
2393
  let outputDir;
2342
2394
  const outputDirFlag = flags["output-dir"];
2395
+ const isAutoGenerated = !outputDirFlag;
2343
2396
  if (outputDirFlag) {
2344
2397
  outputDir = outputDirFlag;
2345
2398
  fs7.mkdirSync(outputDir, { recursive: true });
@@ -2380,8 +2433,15 @@ async function runStressTest(_positionals, flags, stdout, stderr) {
2380
2433
  } else {
2381
2434
  stdout.write(formatTable(report) + "\n");
2382
2435
  }
2383
- if (!flags["keep"]) {
2384
- fs7.rmSync(outputDir, { recursive: true, force: true });
2436
+ if (isAutoGenerated) {
2437
+ if (!flags["keep"]) {
2438
+ fs7.rmSync(outputDir, { recursive: true, force: true });
2439
+ }
2440
+ } else if (!flags["keep"]) {
2441
+ stdout.write(
2442
+ `Note: --output-dir was user-specified \u2014 leaving ${outputDir} in place.
2443
+ `
2444
+ );
2385
2445
  }
2386
2446
  return report.failed === 0 ? 0 : 1;
2387
2447
  }
@@ -2427,43 +2487,9 @@ async function runClassify(positional, flags, s, stdout, stderr) {
2427
2487
  const sidecarPath = base + ".classify.json";
2428
2488
  if (offline) {
2429
2489
  stdout.write(s.classifyOfflineWarning + "\n");
2430
- const isinToProduct = /* @__PURE__ */ new Map();
2431
- for (const tx of transactions) {
2432
- if (tx.isin && !isinToProduct.has(tx.isin)) {
2433
- isinToProduct.set(tx.isin, tx.product);
2434
- }
2435
- }
2436
- const classifications = {};
2437
- for (const [isin, product] of isinToProduct.entries()) {
2438
- classifications[isin] = {
2439
- product,
2440
- assetClass: "ETF",
2441
- bucketGain: "A",
2442
- bucketLoss: "B",
2443
- taxRate: 0.26,
2444
- whiteListed: null,
2445
- confirmedByUser: true,
2446
- source: "user"
2447
- };
2448
- }
2449
- const sidecarContent = JSON.stringify(
2450
- {
2451
- version: 1,
2452
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2453
- classifications,
2454
- warnings: []
2455
- },
2456
- null,
2457
- 2
2458
- );
2459
- try {
2460
- fs8.writeFileSync(sidecarPath, sidecarContent, "utf-8");
2461
- } catch {
2462
- stderr.write(`Cannot write sidecar: ${sidecarPath}
2463
- `);
2464
- return 1;
2465
- }
2466
- stdout.write(s.classifyWritten(sidecarPath) + "\n");
2490
+ const classifier2 = new Classifier({ interactive: false });
2491
+ await classifier2.classify(transactions, sidecarPath, { offline: true });
2492
+ stdout.write(s.classifyWritten(path6.resolve(sidecarPath)) + "\n");
2467
2493
  return 0;
2468
2494
  }
2469
2495
  const classifier = new Classifier({ interactive: true });
@@ -2512,7 +2538,9 @@ async function main() {
2512
2538
  range: { type: "string" },
2513
2539
  keep: { type: "boolean", default: false },
2514
2540
  "output-dir": { type: "string" },
2515
- offline: { type: "boolean", default: false }
2541
+ offline: { type: "boolean", default: false },
2542
+ "carry-forward": { type: "string", multiple: true },
2543
+ "export-dichiarazione": { type: "string" }
2516
2544
  },
2517
2545
  allowPositionals: true,
2518
2546
  strict: false