cata-centavo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/dist/bin/cata-centavo.js +226 -0
  4. package/dist/bin/cata-centavo.js.map +1 -0
  5. package/dist/cli/dispatch.js +67 -0
  6. package/dist/cli/dispatch.js.map +1 -0
  7. package/dist/cli/doctor.js +178 -0
  8. package/dist/cli/doctor.js.map +1 -0
  9. package/dist/cli/init.js +125 -0
  10. package/dist/cli/init.js.map +1 -0
  11. package/dist/cli/progress.js +85 -0
  12. package/dist/cli/progress.js.map +1 -0
  13. package/dist/cli/sync.js +26 -0
  14. package/dist/cli/sync.js.map +1 -0
  15. package/dist/config.js +119 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/core/account.js +12 -0
  18. package/dist/core/account.js.map +1 -0
  19. package/dist/core/accounts.js +91 -0
  20. package/dist/core/accounts.js.map +1 -0
  21. package/dist/core/aggregate.js +83 -0
  22. package/dist/core/aggregate.js.map +1 -0
  23. package/dist/core/balance.js +68 -0
  24. package/dist/core/balance.js.map +1 -0
  25. package/dist/core/bill-rows.js +132 -0
  26. package/dist/core/bill-rows.js.map +1 -0
  27. package/dist/core/bill.js +103 -0
  28. package/dist/core/bill.js.map +1 -0
  29. package/dist/core/category-source.js +26 -0
  30. package/dist/core/category-source.js.map +1 -0
  31. package/dist/core/category.js +72 -0
  32. package/dist/core/category.js.map +1 -0
  33. package/dist/core/consent.js +19 -0
  34. package/dist/core/consent.js.map +1 -0
  35. package/dist/core/contracts.js +9 -0
  36. package/dist/core/contracts.js.map +1 -0
  37. package/dist/core/counterparty.js +69 -0
  38. package/dist/core/counterparty.js.map +1 -0
  39. package/dist/core/date.js +25 -0
  40. package/dist/core/date.js.map +1 -0
  41. package/dist/core/description.js +58 -0
  42. package/dist/core/description.js.map +1 -0
  43. package/dist/core/diagnose.js +32 -0
  44. package/dist/core/diagnose.js.map +1 -0
  45. package/dist/core/mcc.js +124 -0
  46. package/dist/core/mcc.js.map +1 -0
  47. package/dist/core/refresh.js +142 -0
  48. package/dist/core/refresh.js.map +1 -0
  49. package/dist/core/self-transfer.js +22 -0
  50. package/dist/core/self-transfer.js.map +1 -0
  51. package/dist/core/taxonomy-tree.js +148 -0
  52. package/dist/core/taxonomy-tree.js.map +1 -0
  53. package/dist/core/taxonomy.js +70 -0
  54. package/dist/core/taxonomy.js.map +1 -0
  55. package/dist/core/transaction.js +2 -0
  56. package/dist/core/transaction.js.map +1 -0
  57. package/dist/core/transactions.js +46 -0
  58. package/dist/core/transactions.js.map +1 -0
  59. package/dist/logging.js +93 -0
  60. package/dist/logging.js.map +1 -0
  61. package/dist/mcp/cursor.js +64 -0
  62. package/dist/mcp/cursor.js.map +1 -0
  63. package/dist/mcp/format.js +38 -0
  64. package/dist/mcp/format.js.map +1 -0
  65. package/dist/mcp/server.js +50 -0
  66. package/dist/mcp/server.js.map +1 -0
  67. package/dist/mcp/source.js +2 -0
  68. package/dist/mcp/source.js.map +1 -0
  69. package/dist/mcp/tools/accounts.js +144 -0
  70. package/dist/mcp/tools/accounts.js.map +1 -0
  71. package/dist/mcp/tools/balance.js +96 -0
  72. package/dist/mcp/tools/balance.js.map +1 -0
  73. package/dist/mcp/tools/bill-summary-format.js +161 -0
  74. package/dist/mcp/tools/bill-summary-format.js.map +1 -0
  75. package/dist/mcp/tools/bill-summary.js +105 -0
  76. package/dist/mcp/tools/bill-summary.js.map +1 -0
  77. package/dist/mcp/tools/bills.js +144 -0
  78. package/dist/mcp/tools/bills.js.map +1 -0
  79. package/dist/mcp/tools/closing-days.js +90 -0
  80. package/dist/mcp/tools/closing-days.js.map +1 -0
  81. package/dist/mcp/tools/list-transactions.js +122 -0
  82. package/dist/mcp/tools/list-transactions.js.map +1 -0
  83. package/dist/mcp/tools/result.js +12 -0
  84. package/dist/mcp/tools/result.js.map +1 -0
  85. package/dist/mcp/tools/set-category.js +74 -0
  86. package/dist/mcp/tools/set-category.js.map +1 -0
  87. package/dist/mcp/tools/sources.js +73 -0
  88. package/dist/mcp/tools/sources.js.map +1 -0
  89. package/dist/mcp/tools/transaction-details.js +146 -0
  90. package/dist/mcp/tools/transaction-details.js.map +1 -0
  91. package/dist/mcp/tools/transaction-input.js +51 -0
  92. package/dist/mcp/tools/transaction-input.js.map +1 -0
  93. package/dist/mcp/tools/transactions.js +186 -0
  94. package/dist/mcp/tools/transactions.js.map +1 -0
  95. package/dist/pluggy/client.js +123 -0
  96. package/dist/pluggy/client.js.map +1 -0
  97. package/dist/pluggy/errors.js +152 -0
  98. package/dist/pluggy/errors.js.map +1 -0
  99. package/dist/pluggy/mapper.js +155 -0
  100. package/dist/pluggy/mapper.js.map +1 -0
  101. package/dist/pluggy/money.js +53 -0
  102. package/dist/pluggy/money.js.map +1 -0
  103. package/dist/pluggy/transaction-mapper.js +124 -0
  104. package/dist/pluggy/transaction-mapper.js.map +1 -0
  105. package/dist/pluggy/transport.js +170 -0
  106. package/dist/pluggy/transport.js.map +1 -0
  107. package/dist/pluggy/wire.js +207 -0
  108. package/dist/pluggy/wire.js.map +1 -0
  109. package/dist/storage/categories.js +101 -0
  110. package/dist/storage/categories.js.map +1 -0
  111. package/dist/storage/category-sql.js +37 -0
  112. package/dist/storage/category-sql.js.map +1 -0
  113. package/dist/storage/closing-days.js +27 -0
  114. package/dist/storage/closing-days.js.map +1 -0
  115. package/dist/storage/db.js +162 -0
  116. package/dist/storage/db.js.map +1 -0
  117. package/dist/storage/diagnostics.js +57 -0
  118. package/dist/storage/diagnostics.js.map +1 -0
  119. package/dist/storage/harvest.js +87 -0
  120. package/dist/storage/harvest.js.map +1 -0
  121. package/dist/storage/migrations.js +125 -0
  122. package/dist/storage/migrations.js.map +1 -0
  123. package/dist/storage/transaction-row.js +74 -0
  124. package/dist/storage/transaction-row.js.map +1 -0
  125. package/dist/storage/transactions.js +213 -0
  126. package/dist/storage/transactions.js.map +1 -0
  127. package/package.json +70 -0
@@ -0,0 +1,162 @@
1
+ import { chmodSync, mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { CACHE_MIGRATIONS, DATA_MIGRATIONS } from "./migrations.js";
5
+ const MAIN = "main";
6
+ /** `PRAGMA` and `ATTACH … AS` take no bound parameters, which is why this exists. */
7
+ function quoteSchema(schema) {
8
+ if (!/^[a-z_][a-z0-9_]*$/u.test(schema)) {
9
+ throw new Error(`refusing to interpolate an unsafe schema name: ${schema}`);
10
+ }
11
+ return schema;
12
+ }
13
+ function escapeLiteral(str) {
14
+ return str.replaceAll("'", "''");
15
+ }
16
+ /** A `data.db` written by a newer release than the one now running. */
17
+ export class SchemaTooNewError extends Error {
18
+ found;
19
+ supported;
20
+ constructor(path, found, supported) {
21
+ super(`${path} is at schema version ${found}, but this version of cata-centavo only understands ${supported}. ` +
22
+ "It was written by a newer release. Upgrade rather than downgrade — this file holds work that cannot be refetched.");
23
+ this.name = "SchemaTooNewError";
24
+ this.found = found;
25
+ this.supported = supported;
26
+ }
27
+ }
28
+ export function openDatabase(options) {
29
+ mkdirSync(dirname(options.path), { recursive: true });
30
+ const db = new DatabaseSync(options.path);
31
+ try {
32
+ restrictToOwner(options.path);
33
+ // WAL and a single writer, taken from the prior implementation's comment:
34
+ // "a single local writer avoids SQLITE_BUSY" (ADR §16.1). Set before the
35
+ // journal is created so the -wal and -shm siblings inherit the mode above.
36
+ db.exec("PRAGMA journal_mode = WAL");
37
+ db.exec("PRAGMA busy_timeout = 5000");
38
+ db.exec("PRAGMA foreign_keys = ON");
39
+ migrateSchema(db, options, MAIN);
40
+ if (options.path === ":memory:" && options.migrations === CACHE_MIGRATIONS) {
41
+ db.exec("ATTACH DATABASE ':memory:' AS userdata");
42
+ for (const m of DATA_MIGRATIONS) {
43
+ db.exec(m.up.replace(/CREATE TABLE /gi, "CREATE TABLE userdata."));
44
+ }
45
+ }
46
+ }
47
+ catch (error) {
48
+ db.close();
49
+ throw error;
50
+ }
51
+ return db;
52
+ }
53
+ /**
54
+ * The two files of ADR §10 on one connection.
55
+ *
56
+ * They share a connection because Phase 3's derivation resolves a category by
57
+ * reading both in one statement, and because the harvest writes the snapshot
58
+ * inside the walk's transaction. Two handles cannot see each other and cannot
59
+ * do either.
60
+ */
61
+ export function openDatabases(paths) {
62
+ const dataDb = openDatabase({ path: paths.dataDb, migrations: DATA_MIGRATIONS, policy: "migrate" });
63
+ dataDb.close();
64
+ const db = openDatabase({ path: paths.cacheDb, migrations: CACHE_MIGRATIONS, policy: "rebuild" });
65
+ try {
66
+ db.exec(`ATTACH DATABASE '${escapeLiteral(paths.dataDb)}' AS userdata`);
67
+ }
68
+ catch (error) {
69
+ db.close();
70
+ throw error;
71
+ }
72
+ return { db, close: () => db.close() };
73
+ }
74
+ function migrateSchema(db, options, schema = MAIN) {
75
+ const target = targetVersion(options.migrations);
76
+ const current = readUserVersion(db, schema);
77
+ if (current === target) {
78
+ return;
79
+ }
80
+ if (options.policy === "rebuild") {
81
+ dropEverything(db, schema);
82
+ apply(db, options.migrations, 0, schema);
83
+ return;
84
+ }
85
+ if (current > target) {
86
+ throw new SchemaTooNewError(options.path, current, target);
87
+ }
88
+ apply(db, options.migrations, current, schema);
89
+ }
90
+ export function targetVersion(migrations) {
91
+ return migrations.reduce((highest, migration) => Math.max(highest, migration.to), 0);
92
+ }
93
+ /**
94
+ * One transaction per step, so a chain that dies halfway leaves the file at the
95
+ * last version that actually committed rather than at a version it never
96
+ * reached.
97
+ */
98
+ function apply(db, migrations, from, schema = MAIN) {
99
+ for (const migration of migrations) {
100
+ if (migration.to <= from) {
101
+ continue;
102
+ }
103
+ db.exec("BEGIN");
104
+ try {
105
+ db.exec(migration.up);
106
+ stamp(db, schema, migration.to);
107
+ db.exec("COMMIT");
108
+ }
109
+ catch (error) {
110
+ db.exec("ROLLBACK");
111
+ throw error;
112
+ }
113
+ }
114
+ }
115
+ function dropEverything(db, schema = MAIN) {
116
+ const safeSchema = quoteSchema(schema);
117
+ const objects = db.prepare(`SELECT type, name FROM ${safeSchema}.sqlite_master WHERE name NOT LIKE 'sqlite_%'`).all();
118
+ db.exec("PRAGMA foreign_keys = OFF");
119
+ for (const object of objects) {
120
+ const type = String(object["type"]);
121
+ const name = String(object["name"]).replaceAll('"', '""');
122
+ if (type === "table") {
123
+ db.exec(`DROP TABLE IF EXISTS ${safeSchema}."${name}"`);
124
+ }
125
+ else if (type === "view") {
126
+ db.exec(`DROP VIEW IF EXISTS ${safeSchema}."${name}"`);
127
+ }
128
+ }
129
+ db.exec("PRAGMA foreign_keys = ON");
130
+ stamp(db, schema, 0);
131
+ }
132
+ /** The `PRAGMA user_version` a file currently carries. */
133
+ export function schemaVersion(db, schema = MAIN) {
134
+ return readUserVersion(db, schema);
135
+ }
136
+ function readUserVersion(db, schema = MAIN) {
137
+ const value = db.prepare(`PRAGMA ${quoteSchema(schema)}.user_version`).get()?.["user_version"];
138
+ const version = Number(value);
139
+ if (Number.isInteger(version)) {
140
+ return version;
141
+ }
142
+ return 0;
143
+ }
144
+ /** `PRAGMA` takes no bound parameters, which is why the guard is here. */
145
+ function stamp(db, schema, version) {
146
+ if (!Number.isInteger(version) || version < 0) {
147
+ throw new Error(`refusing to stamp a non-integer schema version: ${version}`);
148
+ }
149
+ db.exec(`PRAGMA ${quoteSchema(schema)}.user_version = ${version}`);
150
+ }
151
+ /**
152
+ * `cache.db` holds the full financial history in plaintext (ADR §9). The
153
+ * permission bits, not the directory, are what stop a second local account
154
+ * reading it.
155
+ */
156
+ function restrictToOwner(path) {
157
+ if (path === ":memory:" || path === "") {
158
+ return;
159
+ }
160
+ chmodSync(path, 0o600);
161
+ }
162
+ //# sourceMappingURL=db.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/storage/db.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ import { schemaVersion } from "./db.js";
2
+ /**
3
+ * What `doctor` reads off disk, without touching the network. `doctor` runs
4
+ * precisely when things are broken, so a diagnostic that throws on a fresh
5
+ * install would be worse than useless — every count here reads as zero, and
6
+ * every date as `null`, rather than raising.
7
+ */
8
+ export function readLocalState(db) {
9
+ const paths = readAttachedPaths(db);
10
+ return {
11
+ cacheDb: paths.get("main") ?? "",
12
+ dataDb: paths.get("userdata") ?? "",
13
+ cacheVersion: schemaVersion(db),
14
+ dataVersion: schemaVersion(db, "userdata"),
15
+ accountsWalked: countAccountsWalked(db),
16
+ newestLocalDate: readNewestLocalDate(db),
17
+ perConnection: readPerConnection(db),
18
+ snapshotRows: countRows(db, "userdata.category_snapshot"),
19
+ counterpartyDocuments: countRows(db, "userdata.counterparty_categories"),
20
+ mccRows: countRows(db, "mcc_categories"),
21
+ };
22
+ }
23
+ function countRows(db, table) {
24
+ const row = db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
25
+ return Number(row?.["n"] ?? 0);
26
+ }
27
+ function countAccountsWalked(db) {
28
+ const row = db.prepare("SELECT COUNT(*) AS n FROM transaction_sync").get();
29
+ return Number(row?.["n"] ?? 0);
30
+ }
31
+ function readNewestLocalDate(db) {
32
+ const row = db.prepare("SELECT MAX(local_date) AS newest FROM transactions").get();
33
+ return stringOrNull(row?.["newest"]);
34
+ }
35
+ function readPerConnection(db) {
36
+ const rows = db
37
+ .prepare(`SELECT connection_id, COUNT(*) AS accounts, MIN(last_updated_at) AS oldest_walk
38
+ FROM transaction_sync
39
+ GROUP BY connection_id`)
40
+ .all();
41
+ return new Map(rows.map((row) => [
42
+ String(row["connection_id"]),
43
+ { accounts: Number(row["accounts"]), oldestWalk: stringOrNull(row["oldest_walk"]) },
44
+ ]));
45
+ }
46
+ /** The file paths of `main` (`cache.db`) and `userdata` (`data.db`), straight off the open connection. */
47
+ function readAttachedPaths(db) {
48
+ const rows = db.prepare("PRAGMA database_list").all();
49
+ return new Map(rows.map((row) => [String(row["name"]), String(row["file"])]));
50
+ }
51
+ function stringOrNull(value) {
52
+ if (value === null || value === undefined) {
53
+ return null;
54
+ }
55
+ return String(value);
56
+ }
57
+ //# sourceMappingURL=diagnostics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../../src/storage/diagnostics.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAexC;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,EAAgB;IAC7C,MAAM,KAAK,GAAG,iBAAiB,CAAC,EAAE,CAAC,CAAC;IACpC,OAAO;QACL,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE;QAChC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE;QACnC,YAAY,EAAE,aAAa,CAAC,EAAE,CAAC;QAC/B,WAAW,EAAE,aAAa,CAAC,EAAE,EAAE,UAAU,CAAC;QAC1C,cAAc,EAAE,mBAAmB,CAAC,EAAE,CAAC;QACvC,eAAe,EAAE,mBAAmB,CAAC,EAAE,CAAC;QACxC,aAAa,EAAE,iBAAiB,CAAC,EAAE,CAAC;QACpC,YAAY,EAAE,SAAS,CAAC,EAAE,EAAE,4BAA4B,CAAC;QACzD,qBAAqB,EAAE,SAAS,CAAC,EAAE,EAAE,kCAAkC,CAAC;QACxE,OAAO,EAAE,SAAS,CAAC,EAAE,EAAE,gBAAgB,CAAC;KACzC,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,EAAgB,EAAE,KAA2F;IAC9H,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,6BAA6B,KAAK,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC;IACnE,OAAO,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,mBAAmB,CAAC,EAAgB;IAC3C,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,4CAA4C,CAAC,CAAC,GAAG,EAAE,CAAC;IAC3E,OAAO,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,mBAAmB,CAAC,EAAgB;IAC3C,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,oDAAoD,CAAC,CAAC,GAAG,EAAE,CAAC;IACnF,OAAO,YAAY,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;AACvC,CAAC;AAED,SAAS,iBAAiB,CAAC,EAAgB;IACzC,MAAM,IAAI,GAAG,EAAE;SACZ,OAAO,CACN;;8BAEwB,CACzB;SACA,GAAG,EAA+B,CAAC;IAEtC,OAAO,IAAI,GAAG,CACZ,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC;QAChB,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QAC5B,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,EAAE,UAAU,EAAE,YAAY,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,EAAE;KACpF,CAAC,CACH,CAAC;AACJ,CAAC;AAED,0GAA0G;AAC1G,SAAS,iBAAiB,CAAC,EAAgB;IACzC,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC,GAAG,EAA+B,CAAC;IACnF,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChF,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC1C,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC"}
@@ -0,0 +1,87 @@
1
+ import { learnCounterparties } from "../core/counterparty.js";
2
+ import { topCategoryOf } from "../core/taxonomy.js";
3
+ import { DERIVED_CATEGORY, DERIVED_COLUMNS } from "./category-sql.js";
4
+ const SNAPSHOT_UPSERT = `
5
+ INSERT INTO userdata.category_snapshot (transaction_id, category_id, top_category_id, harvested_at)
6
+ VALUES (?, ?, ?, ?)
7
+ ON CONFLICT(transaction_id) DO UPDATE SET
8
+ category_id = excluded.category_id,
9
+ top_category_id = excluded.top_category_id,
10
+ harvested_at = excluded.harvested_at
11
+ `;
12
+ const LEARNED_INSERT = `
13
+ INSERT INTO userdata.counterparty_categories (document, category, origin, samples, agreeing, created_at)
14
+ VALUES (?, ?, 'learned', ?, ?, ?)
15
+ ON CONFLICT(document) DO NOTHING
16
+ `;
17
+ /** Every labelled counterparty row we hold, read through the full derivation. */
18
+ const LABELLED_ROWS = `SELECT t.document AS document, ${DERIVED_CATEGORY} AS category
19
+ FROM (SELECT t.*, ${DERIVED_COLUMNS} FROM transactions t WHERE t.document IS NOT NULL) t`;
20
+ /**
21
+ * Copy the provider's enrichment into `data.db` while it still arrives.
22
+ *
23
+ * This runs inside the walk's open transaction, with every row already in hand.
24
+ * No user action, no separate command, no schedule — because the enrichment is
25
+ * expected to stop and the walk that discovers it has stopped is the same walk
26
+ * that would otherwise overwrite the history with NULL.
27
+ *
28
+ * SQLite's atomic commit across attached databases does not hold under WAL,
29
+ * which `openDatabase` enables, so a crash can commit `cache.db` and not
30
+ * `userdata`. That is survivable only because of the asymmetry in
31
+ * `snapshotCategories`: the worst case is one walk's new labels lost, and the
32
+ * next walk rewrites them.
33
+ */
34
+ export function harvest(options) {
35
+ snapshotCategories(options);
36
+ relearnCounterparties(options);
37
+ }
38
+ /**
39
+ * The snapshot only ever gains.
40
+ *
41
+ * A row arriving with no category writes nothing, so a walk that lands after
42
+ * the plan drops to free cannot erase what an earlier walk remembered. That one
43
+ * asymmetry is what makes the history survive.
44
+ */
45
+ function snapshotCategories(options) {
46
+ const upsert = options.db.prepare(SNAPSHOT_UPSERT);
47
+ const now = options.clock.now().toISOString();
48
+ for (const row of options.rows) {
49
+ if (row.categoryId !== null) {
50
+ upsert.run(row.id, row.categoryId, topCategoryFor(row, options.log), now);
51
+ }
52
+ }
53
+ }
54
+ /**
55
+ * Recomputed wholesale rather than incrementally.
56
+ *
57
+ * It is affordable at this size, and it means a manual correction, a re-sync
58
+ * and a cache rebuild all converge on the same map instead of accumulating
59
+ * drift. The `DELETE` touches only learned rows and `DO NOTHING` then declines
60
+ * to overwrite a manual row for a document that would also have been learned,
61
+ * so a human's answer always outlives the inference.
62
+ */
63
+ function relearnCounterparties(options) {
64
+ const rows = options.db.prepare(LABELLED_ROWS).all();
65
+ const learned = learnCounterparties(rows);
66
+ options.db.exec("DELETE FROM userdata.counterparty_categories WHERE origin = 'learned'");
67
+ const insert = options.db.prepare(LEARNED_INSERT);
68
+ const now = options.clock.now().toISOString();
69
+ for (const item of learned) {
70
+ insert.run(item.document, item.category, item.samples, item.agreeing, now);
71
+ }
72
+ }
73
+ /**
74
+ * The roll-up, with the unknown leaf reported rather than thrown.
75
+ *
76
+ * The tree ships as code, so a category the provider adds tomorrow is absent by
77
+ * construction. Costing that row its group is right; costing the whole
78
+ * account's walk is not (design D3).
79
+ */
80
+ export function topCategoryFor(row, log) {
81
+ const top = topCategoryOf(row.categoryId);
82
+ if (top === null && row.categoryId !== null) {
83
+ log.warn({ transactionId: row.id, categoryId: row.categoryId }, "Unknown category, left ungrouped");
84
+ }
85
+ return top;
86
+ }
87
+ //# sourceMappingURL=harvest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harvest.js","sourceRoot":"","sources":["../../src/storage/harvest.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEtE,MAAM,eAAe,GAAG;;;;;;;CAOvB,CAAC;AAEF,MAAM,cAAc,GAAG;;;;CAItB,CAAC;AAEF,iFAAiF;AACjF,MAAM,aAAa,GAAG,kCAAkC,gBAAgB;sBAClD,eAAe,sDAAsD,CAAC;AAS5F;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,OAAO,CAAC,OAAuB;IAC7C,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC5B,qBAAqB,CAAC,OAAO,CAAC,CAAC;AACjC,CAAC;AAED;;;;;;GAMG;AACH,SAAS,kBAAkB,CAAC,OAAuB;IACjD,MAAM,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IACnD,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;IAE9C,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QAC/B,IAAI,GAAG,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;YAC5B,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,UAAU,EAAE,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;QAC5E,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,qBAAqB,CAAC,OAAuB;IACpD,MAAM,IAAI,GAAG,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,GAAG,EAG/C,CAAC;IACJ,MAAM,OAAO,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAE1C,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,uEAAuE,CAAC,CAAC;IAEzF,MAAM,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IAClD,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9C,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC7E,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,GAAgB,EAAE,GAAW;IAC1D,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IAC1C,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;QAC5C,GAAG,CAAC,IAAI,CAAC,EAAE,aAAa,EAAE,GAAG,CAAC,EAAE,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,EAAE,kCAAkC,CAAC,CAAC;IACtG,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,125 @@
1
+ import { MCC_CATEGORIES } from "../core/mcc.js";
2
+ /**
3
+ * The MCC seed is generated from `src/core/mcc.ts` so the table has one source.
4
+ * Values are interpolated rather than bound because a migration's `up` is a
5
+ * script, not a statement — the inputs are a compile-time constant of integers
6
+ * and closed-list category ids, so there is no untrusted string here.
7
+ */
8
+ function seedMccCategories() {
9
+ return MCC_CATEGORIES
10
+ .map((row) => `INSERT INTO mcc_categories (mcc, category, samples, agreeing) VALUES ('${row.mcc}', '${row.category}', ${row.samples}, ${row.agreeing});`)
11
+ .join("\n");
12
+ }
13
+ /**
14
+ * Droppable: any mismatch is resolved by refetching from Pluggy (§10).
15
+ *
16
+ * **Bump `to` when the description normalizer changes.** `description_norm` is
17
+ * written at insert and never recomputed, so appending an acquirer prefix to
18
+ * `core/description.ts` leaves every cached row carrying the old value. The
19
+ * rebuild policy is the re-derive pass, and this number is what triggers it.
20
+ *
21
+ * Under `rebuild`, `apply` replays every entry from 0 against a dropped file,
22
+ * so a future `{to: 2}` is an additional statement rather than an `ALTER` — a
23
+ * `CREATE TABLE transactions` in both entries would fail on the second.
24
+ */
25
+ export const CACHE_MIGRATIONS = [
26
+ {
27
+ to: 1,
28
+ up: `
29
+ CREATE TABLE transactions (
30
+ id TEXT PRIMARY KEY,
31
+ account_id TEXT NOT NULL,
32
+ connection_id TEXT NOT NULL,
33
+ account_type TEXT NOT NULL,
34
+ account_subtype TEXT,
35
+ occurred_at TEXT NOT NULL,
36
+ local_date TEXT NOT NULL,
37
+ amount_cents INTEGER NOT NULL,
38
+ currency TEXT NOT NULL,
39
+ original_amount_cents INTEGER,
40
+ original_currency TEXT,
41
+ description TEXT NOT NULL,
42
+ description_norm TEXT NOT NULL,
43
+ category_id TEXT,
44
+ document TEXT,
45
+ counterparty_name TEXT,
46
+ payment_method TEXT,
47
+ mcc TEXT,
48
+ bill_id TEXT,
49
+ instalment_number INTEGER,
50
+ instalment_total INTEGER,
51
+ purchase_date TEXT
52
+ );
53
+ CREATE INDEX transactions_by_date ON transactions(local_date DESC, id DESC);
54
+ CREATE INDEX transactions_by_account ON transactions(account_id, local_date DESC);
55
+
56
+ CREATE TABLE transaction_sync (
57
+ account_id TEXT PRIMARY KEY,
58
+ connection_id TEXT NOT NULL,
59
+ last_updated_at TEXT
60
+ );
61
+ `,
62
+ },
63
+ {
64
+ to: 2,
65
+ up: `
66
+ ALTER TABLE transactions ADD COLUMN top_category_id TEXT;
67
+ CREATE INDEX transactions_by_document ON transactions(document);
68
+
69
+ CREATE TABLE mcc_categories (
70
+ mcc TEXT PRIMARY KEY,
71
+ category TEXT NOT NULL,
72
+ samples INTEGER NOT NULL,
73
+ agreeing INTEGER NOT NULL
74
+ );
75
+ ${seedMccCategories()}
76
+ `,
77
+ },
78
+ {
79
+ to: 3,
80
+ up: `
81
+ ALTER TABLE transactions ADD COLUMN bill_forecast_date TEXT;
82
+ `,
83
+ },
84
+ ];
85
+ /** Never dropped: overrides, rules and closing days live here (§10). */
86
+ export const DATA_MIGRATIONS = [
87
+ {
88
+ to: 1,
89
+ up: `
90
+ CREATE TABLE category_overrides (
91
+ transaction_id TEXT PRIMARY KEY,
92
+ category TEXT NOT NULL,
93
+ created_at TEXT NOT NULL
94
+ );
95
+
96
+ CREATE TABLE counterparty_categories (
97
+ document TEXT PRIMARY KEY,
98
+ category TEXT NOT NULL,
99
+ origin TEXT NOT NULL,
100
+ samples INTEGER,
101
+ agreeing INTEGER,
102
+ created_at TEXT NOT NULL
103
+ );
104
+
105
+ CREATE TABLE category_snapshot (
106
+ transaction_id TEXT PRIMARY KEY,
107
+ category_id TEXT NOT NULL,
108
+ top_category_id TEXT,
109
+ harvested_at TEXT NOT NULL
110
+ );
111
+ `,
112
+ },
113
+ {
114
+ to: 2,
115
+ up: `
116
+ CREATE TABLE card_closing_day (
117
+ account_id TEXT PRIMARY KEY,
118
+ day INTEGER NOT NULL,
119
+ created_at TEXT NOT NULL,
120
+ updated_at TEXT NOT NULL
121
+ );
122
+ `,
123
+ },
124
+ ];
125
+ //# sourceMappingURL=migrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/storage/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAOhD;;;;;GAKG;AACH,SAAS,iBAAiB;IACxB,OAAO,cAAc;SAClB,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,0EAA0E,GAAG,CAAC,GAAG,OAAO,GAAG,CAAC,QAAQ,MAAM,GAAG,CAAC,OAAO,KAAK,GAAG,CAAC,QAAQ,IAAI,CAAC;SACxJ,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAyB;IACpD;QACE,EAAE,EAAE,CAAC;QACL,EAAE,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAiCH;KACF;IACD;QACE,EAAE,EAAE,CAAC;QACL,EAAE,EAAE;;;;;;;;;;QAUA,iBAAiB,EAAE;KACtB;KACF;IACD;QACE,EAAE,EAAE,CAAC;QACL,EAAE,EAAE;;KAEH;KACF;CACF,CAAC;AAEF,wEAAwE;AACxE,MAAM,CAAC,MAAM,eAAe,GAAyB;IACnD;QACE,EAAE,EAAE,CAAC;QACL,EAAE,EAAE;;;;;;;;;;;;;;;;;;;;;;KAsBH;KACF;IACD;QACE,EAAE,EAAE,CAAC;QACL,EAAE,EAAE;;;;;;;KAOH;KACF;CACF,CAAC"}
@@ -0,0 +1,74 @@
1
+ import { resolveCategory } from "../core/category-source.js";
2
+ import { topCategoryFor } from "./harvest.js";
3
+ /**
4
+ * The codec between a SQLite row and our domain object.
5
+ *
6
+ * It lives apart from the store because it answers a different question: the
7
+ * store decides *which* rows, this decides what a row *is*. Both directions are
8
+ * here so the column list and the value list cannot drift apart.
9
+ */
10
+ export function rowToTransaction(row) {
11
+ return {
12
+ id: String(row["id"]),
13
+ accountId: String(row["account_id"]),
14
+ connectionId: String(row["connection_id"]),
15
+ accountType: row["account_type"],
16
+ accountSubtype: nullableString(row["account_subtype"]),
17
+ occurredAt: String(row["occurred_at"]),
18
+ localDate: String(row["local_date"]),
19
+ amountCents: Number(row["amount_cents"]),
20
+ currency: String(row["currency"]),
21
+ originalAmountCents: nullableNumber(row["original_amount_cents"]),
22
+ originalCurrency: nullableString(row["original_currency"]),
23
+ description: String(row["description"]),
24
+ descriptionNorm: String(row["description_norm"]),
25
+ categoryId: nullableString(row["category_id"]),
26
+ document: nullableString(row["document"]),
27
+ counterpartyName: nullableString(row["counterparty_name"]),
28
+ paymentMethod: nullableString(row["payment_method"]),
29
+ mcc: nullableString(row["mcc"]),
30
+ billId: nullableString(row["bill_id"]),
31
+ billForecastDate: nullableString(row["bill_forecast_date"]),
32
+ instalmentNumber: nullableNumber(row["instalment_number"]),
33
+ instalmentTotal: nullableNumber(row["instalment_total"]),
34
+ purchaseDate: nullableString(row["purchase_date"]),
35
+ };
36
+ }
37
+ export function rowToDerived(row) {
38
+ return {
39
+ ...rowToTransaction(row),
40
+ categoryId: nullableString(row["c_leaf"]),
41
+ ...resolveCategory({
42
+ override: nullableString(row["c_override"]),
43
+ counterparty: nullableString(row["c_counterparty"]),
44
+ pluggy: nullableString(row["c_pluggy"]),
45
+ snapshot: nullableString(row["c_snapshot"]),
46
+ learned: nullableString(row["c_learned"]),
47
+ mcc: nullableString(row["c_mcc"]),
48
+ }),
49
+ };
50
+ }
51
+ export function transactionValues(row, log) {
52
+ return [
53
+ row.id, row.accountId, row.connectionId, row.accountType, row.accountSubtype,
54
+ row.occurredAt, row.localDate, row.amountCents, row.currency, row.originalAmountCents,
55
+ row.originalCurrency, row.description, row.descriptionNorm, row.categoryId, row.document,
56
+ row.counterpartyName, row.paymentMethod, row.mcc, row.billId, row.instalmentNumber,
57
+ row.billForecastDate, row.instalmentTotal, row.purchaseDate, topCategoryFor(row, log),
58
+ ];
59
+ }
60
+ function nullableString(value) {
61
+ // Stryker disable next-line ConditionalExpression,LogicalOperator,BlockStatement: SQLite columns are NULL or concrete values; undefined is defensive input handling.
62
+ if (value === null || value === undefined) {
63
+ return null;
64
+ }
65
+ return String(value);
66
+ }
67
+ function nullableNumber(value) {
68
+ // Stryker disable next-line ConditionalExpression,LogicalOperator,BlockStatement: SQLite columns are NULL or concrete values; undefined is defensive input handling.
69
+ if (value === null || value === undefined) {
70
+ return null;
71
+ }
72
+ return Number(value);
73
+ }
74
+ //# sourceMappingURL=transaction-row.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transaction-row.js","sourceRoot":"","sources":["../../src/storage/transaction-row.ts"],"names":[],"mappings":"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"}