plasalid 0.5.8 → 0.6.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.
- package/README.md +9 -9
- package/dist/accounts/taxonomy.d.ts +1 -1
- package/dist/accounts/taxonomy.js +2 -2
- package/dist/ai/agent.d.ts +7 -6
- package/dist/ai/agent.js +9 -8
- package/dist/ai/personas.d.ts +1 -1
- package/dist/ai/personas.js +69 -66
- package/dist/ai/prompt-sections.d.ts +4 -5
- package/dist/ai/prompt-sections.js +11 -11
- package/dist/ai/system-prompt.d.ts +2 -3
- package/dist/ai/system-prompt.js +5 -5
- package/dist/ai/tools/common.js +13 -5
- package/dist/ai/tools/index.js +15 -15
- package/dist/ai/tools/ingest.d.ts +2 -2
- package/dist/ai/tools/ingest.js +210 -87
- package/dist/ai/tools/merchants.js +27 -12
- package/dist/ai/tools/read.js +36 -20
- package/dist/ai/tools/record.js +79 -19
- package/dist/ai/tools/resolve.d.ts +2 -0
- package/dist/ai/tools/resolve.js +195 -0
- package/dist/ai/tools/types.d.ts +5 -7
- package/dist/cli/commands/accounts.js +2 -2
- package/dist/cli/commands/record.js +4 -2
- package/dist/cli/commands/resolve.d.ts +2 -0
- package/dist/cli/commands/resolve.js +13 -0
- package/dist/cli/commands/scan.js +18 -22
- package/dist/cli/commands/status.js +4 -2
- package/dist/cli/index.js +9 -9
- package/dist/cli/ink/hooks/useFooterText.js +1 -1
- package/dist/cli/ink/hooks/useTextInput.js +0 -3
- package/dist/cli/ink/scan_dashboard.d.ts +2 -2
- package/dist/cli/ink/scan_dashboard.js +3 -3
- package/dist/cli/setup.js +6 -3
- package/dist/cli/ux.js +1 -1
- package/dist/db/queries/account-balance.d.ts +140 -0
- package/dist/db/queries/account-balance.js +355 -0
- package/dist/db/queries/account_balance.d.ts +0 -1
- package/dist/db/queries/account_balance.js +0 -10
- package/dist/db/queries/action-log.d.ts +29 -0
- package/dist/db/queries/action-log.js +27 -0
- package/dist/db/queries/action_log.d.ts +1 -1
- package/dist/db/queries/concerns.d.ts +10 -0
- package/dist/db/queries/concerns.js +21 -0
- package/dist/db/queries/transactions.d.ts +3 -22
- package/dist/db/queries/transactions.js +4 -5
- package/dist/db/queries/unknowns.d.ts +62 -0
- package/dist/db/queries/unknowns.js +114 -0
- package/dist/db/schema.js +3 -3
- package/dist/resolver/pipeline.d.ts +16 -0
- package/dist/resolver/pipeline.js +38 -0
- package/dist/resolver/prompts.d.ts +8 -0
- package/dist/resolver/prompts.js +26 -0
- package/dist/scanner/account-mutex.d.ts +1 -0
- package/dist/scanner/account-mutex.js +16 -0
- package/dist/scanner/buffer.d.ts +10 -10
- package/dist/scanner/buffer.js +15 -15
- package/dist/scanner/decrypt-queue.d.ts +57 -0
- package/dist/scanner/decrypt-queue.js +114 -0
- package/dist/scanner/detectors/correlations.d.ts +2 -0
- package/dist/scanner/detectors/correlations.js +51 -0
- package/dist/scanner/detectors/duplicates.d.ts +2 -0
- package/dist/scanner/detectors/duplicates.js +75 -0
- package/dist/scanner/detectors/index.d.ts +18 -0
- package/dist/scanner/detectors/index.js +39 -0
- package/dist/scanner/detectors/recurrences.d.ts +2 -0
- package/dist/scanner/detectors/recurrences.js +49 -0
- package/dist/scanner/detectors/similar_accounts.d.ts +2 -0
- package/dist/scanner/detectors/similar_accounts.js +64 -0
- package/dist/scanner/detectors/similarities.d.ts +2 -0
- package/dist/scanner/detectors/similarities.js +73 -0
- package/dist/scanner/detectors/types.d.ts +16 -0
- package/dist/scanner/detectors/types.js +1 -0
- package/dist/scanner/inspectors/correlations.d.ts +2 -0
- package/dist/scanner/inspectors/correlations.js +47 -0
- package/dist/scanner/inspectors/duplicates.d.ts +2 -0
- package/dist/scanner/inspectors/duplicates.js +75 -0
- package/dist/scanner/inspectors/index.d.ts +19 -0
- package/dist/scanner/inspectors/index.js +39 -0
- package/dist/scanner/inspectors/recurrences.d.ts +2 -0
- package/dist/scanner/inspectors/recurrences.js +49 -0
- package/dist/scanner/inspectors/similarities.d.ts +2 -0
- package/dist/scanner/inspectors/similarities.js +73 -0
- package/dist/scanner/inspectors/types.d.ts +16 -0
- package/dist/scanner/inspectors/types.js +1 -0
- package/dist/scanner/pipeline.d.ts +6 -4
- package/dist/scanner/pipeline.js +51 -88
- package/dist/scanner/prompts.js +2 -2
- package/package.json +2 -1
|
@@ -59,20 +59,10 @@ async function buildInkEvents(parallel) {
|
|
|
59
59
|
fileName: e.fileName,
|
|
60
60
|
status: e.status,
|
|
61
61
|
transactions: e.transactions,
|
|
62
|
-
|
|
62
|
+
unknowns: e.unknowns,
|
|
63
63
|
error: e.error,
|
|
64
64
|
}),
|
|
65
|
-
correlating: (pairs) => {
|
|
66
|
-
if (inkInstance) {
|
|
67
|
-
inkInstance.unmount();
|
|
68
|
-
inkInstance = null;
|
|
69
|
-
}
|
|
70
|
-
if (mountedFiles > 0 && pairs > 0) {
|
|
71
|
-
console.log(chalk.dim(`Correlating across files... ${pairs} pair(s) flagged.`));
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
65
|
committing: () => {
|
|
75
|
-
// In case correlating fired with 0 pairs, ink may still be mounted; unmount now.
|
|
76
66
|
if (inkInstance) {
|
|
77
67
|
inkInstance.unmount();
|
|
78
68
|
inkInstance = null;
|
|
@@ -80,6 +70,11 @@ async function buildInkEvents(parallel) {
|
|
|
80
70
|
if (mountedFiles > 0)
|
|
81
71
|
console.log(chalk.dim("Committing..."));
|
|
82
72
|
},
|
|
73
|
+
inspecting: (result) => {
|
|
74
|
+
if (mountedFiles > 0 && result.total > 0) {
|
|
75
|
+
console.log(chalk.dim(`Inspectors flagged ${result.total} unknown(s).`));
|
|
76
|
+
}
|
|
77
|
+
},
|
|
83
78
|
};
|
|
84
79
|
}
|
|
85
80
|
/** Plain-text progress (non-TTY or fallback) */
|
|
@@ -112,19 +107,20 @@ function buildPlainTextEvents() {
|
|
|
112
107
|
scanEnd: (e) => {
|
|
113
108
|
lastStepByFile.delete(e.fileName);
|
|
114
109
|
if (e.status === "scanned") {
|
|
115
|
-
console.log(`${chalk.green("✓")} ${e.fileName} ${chalk.dim(`(${e.transactions} transactions, ${e.
|
|
110
|
+
console.log(`${chalk.green("✓")} ${e.fileName} ${chalk.dim(`(${e.transactions} transactions, ${e.unknowns} unknowns)`)}`);
|
|
116
111
|
}
|
|
117
112
|
else {
|
|
118
113
|
console.log(`${chalk.red("✗")} ${e.fileName} ${chalk.dim(`— ${e.error ?? "failed"}`)}`);
|
|
119
114
|
}
|
|
120
115
|
},
|
|
121
|
-
correlating: (pairs) => {
|
|
122
|
-
if (pairs > 0)
|
|
123
|
-
console.log(chalk.dim(`Correlating across files... ${pairs} pair(s) flagged.`));
|
|
124
|
-
},
|
|
125
116
|
committing: () => {
|
|
126
117
|
console.log(chalk.dim("Committing..."));
|
|
127
118
|
},
|
|
119
|
+
inspecting: (result) => {
|
|
120
|
+
if (result.total > 0) {
|
|
121
|
+
console.log(chalk.dim(`Inspectors flagged ${result.total} unknown(s).`));
|
|
122
|
+
}
|
|
123
|
+
},
|
|
128
124
|
};
|
|
129
125
|
}
|
|
130
126
|
/** Terse summary */
|
|
@@ -133,19 +129,19 @@ function renderScanSummary(summary) {
|
|
|
133
129
|
const headline = `Scanned ${summary.total} file(s) — ` +
|
|
134
130
|
`${summary.scanned + summary.replaced} ok, ` +
|
|
135
131
|
`${summary.failed} failed, ` +
|
|
136
|
-
`${summary.
|
|
132
|
+
`${summary.unknowns} unknown${summary.unknowns === 1 ? "" : "s"} flagged`;
|
|
137
133
|
console.log(chalk.bold(headline));
|
|
138
134
|
console.log("");
|
|
139
135
|
for (const d of summary.details) {
|
|
140
136
|
const label = d.relPath;
|
|
141
137
|
switch (d.status) {
|
|
142
138
|
case "scanned": {
|
|
143
|
-
const tag = chalk.dim(`${d.transactions} transactions${d.
|
|
139
|
+
const tag = chalk.dim(`${d.transactions} transactions${d.unknowns > 0 ? ` · ${d.unknowns} unknowns` : ""}`);
|
|
144
140
|
console.log(` ${chalk.green("✓")} ${label} ${tag}`);
|
|
145
141
|
break;
|
|
146
142
|
}
|
|
147
143
|
case "replaced": {
|
|
148
|
-
const tag = chalk.dim(`${d.transactions} transactions${d.
|
|
144
|
+
const tag = chalk.dim(`${d.transactions} transactions${d.unknowns > 0 ? ` · ${d.unknowns} unknowns` : ""} (replaces prior)`);
|
|
149
145
|
console.log(` ${chalk.cyan("↻")} ${label} ${tag}`);
|
|
150
146
|
break;
|
|
151
147
|
}
|
|
@@ -162,8 +158,8 @@ function renderScanSummary(summary) {
|
|
|
162
158
|
const newlyProcessed = summary.scanned + summary.replaced;
|
|
163
159
|
if (newlyProcessed > 0) {
|
|
164
160
|
console.log("");
|
|
165
|
-
console.log(`${chalk.dim("Next:")} ${chalk.cyan("plasalid
|
|
166
|
-
? " — to
|
|
167
|
-
: " —
|
|
161
|
+
console.log(`${chalk.dim("Next:")} ${chalk.cyan("plasalid resolve")}${chalk.dim(summary.unknowns > 0
|
|
162
|
+
? " — to walk every open unknown and apply your decision."
|
|
163
|
+
: " — no unknowns surfaced this run; nothing to do.")}`);
|
|
168
164
|
}
|
|
169
165
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { getDb } from "../../db/connection.js";
|
|
3
|
-
import { getNetWorth, getPeriodTotals } from "../../db/queries/
|
|
3
|
+
import { getNetWorth, getPeriodTotals, } from "../../db/queries/account-balance.js";
|
|
4
4
|
import { formatAmount } from "../../currency.js";
|
|
5
5
|
export function showStatus() {
|
|
6
6
|
const db = getDb();
|
|
@@ -8,7 +8,9 @@ export function showStatus() {
|
|
|
8
8
|
console.log(chalk.bold("Net worth: ") + formatAmount(nw.net_worth));
|
|
9
9
|
console.log(chalk.dim(`Assets ${formatAmount(nw.assets)} − Liabilities ${formatAmount(nw.liabilities)}`));
|
|
10
10
|
const now = new Date();
|
|
11
|
-
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
|
|
11
|
+
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
|
|
12
|
+
.toISOString()
|
|
13
|
+
.slice(0, 10);
|
|
12
14
|
const today = now.toISOString().slice(0, 10);
|
|
13
15
|
const totals = getPeriodTotals(db, monthStart, today);
|
|
14
16
|
console.log("");
|
package/dist/cli/index.js
CHANGED
|
@@ -115,20 +115,20 @@ program
|
|
|
115
115
|
await runScanCommand({ regex: regexes[0], force: !!opts.force, parallel });
|
|
116
116
|
});
|
|
117
117
|
program
|
|
118
|
-
.command("
|
|
119
|
-
.description("
|
|
120
|
-
.option("-a, --account <id>", "Limit
|
|
118
|
+
.command("resolve")
|
|
119
|
+
.description("Walk every open unknown from the last scan one at a time and apply your decision (categorize, merge duplicates, link recurrences, skip).")
|
|
120
|
+
.option("-a, --account <id>", "Limit to unknowns attached to a single account")
|
|
121
121
|
.option("--from <date>", "Only consider entries on or after this date (YYYY-MM-DD)")
|
|
122
122
|
.option("--to <date>", "Only consider entries on or before this date (YYYY-MM-DD)")
|
|
123
|
-
.option("-
|
|
123
|
+
.option("-k, --kind <kind>", "Filter by unknown kind (uncategorized_expense, duplicate, correlation, recurrence_candidate, similar_accounts)")
|
|
124
124
|
.action(async (opts) => {
|
|
125
125
|
ensureConfigured();
|
|
126
|
-
const {
|
|
127
|
-
await
|
|
126
|
+
const { runResolveCommand } = await import("./commands/resolve.js");
|
|
127
|
+
await runResolveCommand({
|
|
128
128
|
accountId: opts.account,
|
|
129
129
|
from: opts.from,
|
|
130
130
|
to: opts.to,
|
|
131
|
-
|
|
131
|
+
kind: opts.kind,
|
|
132
132
|
});
|
|
133
133
|
});
|
|
134
134
|
program
|
|
@@ -164,8 +164,8 @@ program.configureHelp({
|
|
|
164
164
|
desc: "Scan new PDFs (optionally by regex; --force to re-scan)",
|
|
165
165
|
},
|
|
166
166
|
{
|
|
167
|
-
name: "
|
|
168
|
-
desc: "
|
|
167
|
+
name: "resolve",
|
|
168
|
+
desc: "Walk open unknowns one at a time and apply your decision",
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
name: "revert",
|
|
@@ -12,7 +12,7 @@ const HINTS = [
|
|
|
12
12
|
"try: list my subscriptions",
|
|
13
13
|
"try: how much liquid cash do I have?",
|
|
14
14
|
"try: net worth trend this year?",
|
|
15
|
-
"try: open
|
|
15
|
+
"try: open unknowns from last scan?",
|
|
16
16
|
];
|
|
17
17
|
export function useFooterText(db) {
|
|
18
18
|
const [tick, setTick] = useState(0);
|
|
@@ -13,9 +13,6 @@ const ENTER = 13;
|
|
|
13
13
|
const BACKSPACE = 127;
|
|
14
14
|
const BACKSPACE_ALT = 8;
|
|
15
15
|
const ESC = 27;
|
|
16
|
-
function cloneBuf(b) {
|
|
17
|
-
return { lines: [...b.lines], row: b.row, col: b.col };
|
|
18
|
-
}
|
|
19
16
|
function wordLeft(line, col) {
|
|
20
17
|
let p = col;
|
|
21
18
|
while (p > 0 && line[p - 1] === " ")
|
|
@@ -10,7 +10,7 @@ export type ScanDashboardEvent = {
|
|
|
10
10
|
fileName: string;
|
|
11
11
|
status: "scanned" | "failed";
|
|
12
12
|
transactions: number;
|
|
13
|
-
|
|
13
|
+
unknowns: number;
|
|
14
14
|
error?: string;
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
@@ -31,7 +31,7 @@ interface Props {
|
|
|
31
31
|
/**
|
|
32
32
|
* Multi-row live dashboard for the scan phase. Rows appear when a file starts
|
|
33
33
|
* scanning, update as steps flow, and freeze when the agent loop ends. Counts
|
|
34
|
-
* shown are the in-buffer counts at scan-end; correlation may add
|
|
34
|
+
* shown are the in-buffer counts at scan-end; correlation may add unknowns
|
|
35
35
|
* later, which the terse summary reflects.
|
|
36
36
|
*/
|
|
37
37
|
export declare function ScanDashboard({ controller, totalFiles, parallel }: Props): import("react/jsx-runtime").JSX.Element;
|
|
@@ -23,7 +23,7 @@ export class ScanDashboardController {
|
|
|
23
23
|
/**
|
|
24
24
|
* Multi-row live dashboard for the scan phase. Rows appear when a file starts
|
|
25
25
|
* scanning, update as steps flow, and freeze when the agent loop ends. Counts
|
|
26
|
-
* shown are the in-buffer counts at scan-end; correlation may add
|
|
26
|
+
* shown are the in-buffer counts at scan-end; correlation may add unknowns
|
|
27
27
|
* later, which the terse summary reflects.
|
|
28
28
|
*/
|
|
29
29
|
export function ScanDashboard({ controller, totalFiles, parallel }) {
|
|
@@ -41,7 +41,7 @@ export function ScanDashboard({ controller, totalFiles, parallel }) {
|
|
|
41
41
|
break;
|
|
42
42
|
case "scan-end":
|
|
43
43
|
next.set(event.fileName, event.status === "scanned"
|
|
44
|
-
? { kind: "done", transactions: event.transactions,
|
|
44
|
+
? { kind: "done", transactions: event.transactions, unknowns: event.unknowns }
|
|
45
45
|
: { kind: "failed", error: event.error ?? "failed" });
|
|
46
46
|
break;
|
|
47
47
|
}
|
|
@@ -56,7 +56,7 @@ function FileRow({ name, state }) {
|
|
|
56
56
|
return (_jsxs(Text, { children: [" ", _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), " ", name, " ", _jsxs(Text, { dimColor: true, children: ["\u00B7 ", state.step] })] }));
|
|
57
57
|
}
|
|
58
58
|
if (state.kind === "done") {
|
|
59
|
-
return (_jsxs(Text, { children: [" ", _jsx(Text, { color: "green", children: "\u2713" }), " ", name, " ", _jsxs(Text, { dimColor: true, children: ["(", state.transactions, " transactions, ", state.
|
|
59
|
+
return (_jsxs(Text, { children: [" ", _jsx(Text, { color: "green", children: "\u2713" }), " ", name, " ", _jsxs(Text, { dimColor: true, children: ["(", state.transactions, " transactions, ", state.unknowns, " unknowns)"] })] }));
|
|
60
60
|
}
|
|
61
61
|
return (_jsxs(Text, { children: [" ", _jsx(Text, { color: "red", children: "\u2717" }), " ", name, " ", _jsxs(Text, { dimColor: true, children: ["\u2014 ", state.error] })] }));
|
|
62
62
|
}
|
package/dist/cli/setup.js
CHANGED
|
@@ -34,9 +34,12 @@ function printSummary(dataDir) {
|
|
|
34
34
|
console.log(chalk.dim(`Data: ${dataDir}`));
|
|
35
35
|
console.log("");
|
|
36
36
|
console.log("Next steps:");
|
|
37
|
-
console.log(` 1. Run ${chalk.cyan("plasalid data")} to drop your bank/credit
|
|
38
|
-
console.log(` 2. Run ${chalk.cyan("plasalid scan")} to
|
|
39
|
-
console.log(` 3. Run ${chalk.cyan("plasalid")} to
|
|
37
|
+
console.log(` 1. Run ${chalk.cyan("plasalid data")} to drop your bank / credit-card statement PDFs in.`);
|
|
38
|
+
console.log(` 2. Run ${chalk.cyan("plasalid scan")} to parse them.`);
|
|
39
|
+
console.log(` 3. Run ${chalk.cyan("plasalid resolve")} to work through anything the scanner flagged.`);
|
|
40
|
+
console.log(` 4. Run ${chalk.cyan("plasalid")} to chat with your money.`);
|
|
41
|
+
console.log("");
|
|
42
|
+
console.log(chalk.dim(` Optional: ${chalk.cyan(`plasalid record "..."`)}${chalk.dim(" to record manual/undocumented transaction, balance, or account at any time.")}`));
|
|
40
43
|
}
|
|
41
44
|
/**
|
|
42
45
|
* Wraps inquirer's list prompt with a blank line above and below, and inserts
|
package/dist/cli/ux.js
CHANGED
|
@@ -118,7 +118,7 @@ export function makeAgentOnProgress(spinner, subject) {
|
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
120
|
/**
|
|
121
|
-
* Render the structured facts the
|
|
121
|
+
* Render the structured facts the resolve agent attaches to ask_user as a
|
|
122
122
|
* single colored line above the inquirer prompt. Each category has a fixed
|
|
123
123
|
* chalk color so the user's eye picks out the type without reading prose.
|
|
124
124
|
* Returns null when there's nothing to render (so the caller can skip the
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type Database from "libsql";
|
|
2
|
+
export type AccountType = "asset" | "liability" | "income" | "expense" | "equity";
|
|
3
|
+
export declare const TOP_LEVEL_TYPES: ReadonlyArray<AccountType>;
|
|
4
|
+
export interface AccountRow {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
type: AccountType;
|
|
8
|
+
parent_id: string | null;
|
|
9
|
+
subtype: string | null;
|
|
10
|
+
bank_name: string | null;
|
|
11
|
+
account_number_masked: string | null;
|
|
12
|
+
currency: string;
|
|
13
|
+
due_day: number | null;
|
|
14
|
+
statement_day: number | null;
|
|
15
|
+
points_balance: number | null;
|
|
16
|
+
metadata_json: string | null;
|
|
17
|
+
pii_flag: number;
|
|
18
|
+
has_unknown: number;
|
|
19
|
+
created_at: string;
|
|
20
|
+
}
|
|
21
|
+
export interface AccountBalance extends AccountRow {
|
|
22
|
+
balance: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Balance per account using the natural debit/credit convention:
|
|
26
|
+
* asset / expense → debit-normal → balance = debits − credits
|
|
27
|
+
* liability / income / equity → credit-normal → balance = credits − debits
|
|
28
|
+
*/
|
|
29
|
+
export declare function getAccountBalances(db: Database.Database, opts?: {
|
|
30
|
+
type?: AccountType;
|
|
31
|
+
}): AccountBalance[];
|
|
32
|
+
export interface NetWorth {
|
|
33
|
+
assets: number;
|
|
34
|
+
liabilities: number;
|
|
35
|
+
net_worth: number;
|
|
36
|
+
}
|
|
37
|
+
export declare function getNetWorth(db: Database.Database): NetWorth;
|
|
38
|
+
export interface PeriodTotals {
|
|
39
|
+
income: number;
|
|
40
|
+
expenses: number;
|
|
41
|
+
}
|
|
42
|
+
export declare function getPeriodTotals(db: Database.Database, from: string, to: string): PeriodTotals;
|
|
43
|
+
export declare function findAccountById(db: Database.Database, id: string): AccountRow | null;
|
|
44
|
+
export declare function renameAccount(db: Database.Database, id: string, name: string): number;
|
|
45
|
+
/**
|
|
46
|
+
* Idempotently insert one of the five top-level type roots (id = type name,
|
|
47
|
+
* parent_id = null). Called by `createAccount` when a child's declared parent
|
|
48
|
+
* is a missing top-level root.
|
|
49
|
+
*/
|
|
50
|
+
export declare function ensureTopLevelRoot(db: Database.Database, type: AccountType): void;
|
|
51
|
+
/**
|
|
52
|
+
* Idempotently insert one of the structural accounts the system auto-creates:
|
|
53
|
+
* - `expense:uncategorized` (suspense for unclassifiable expense postings)
|
|
54
|
+
* - `equity:adjustments` (balancing side of `adjust_account_balance`)
|
|
55
|
+
* - `equity:opening-balance` (starting state imports)
|
|
56
|
+
* The top-level root is bootstrapped first when missing.
|
|
57
|
+
*/
|
|
58
|
+
export declare function ensureStructuralAccount(db: Database.Database, id: "expense:uncategorized" | "equity:adjustments" | "equity:opening-balance"): void;
|
|
59
|
+
export interface CreateAccountInput {
|
|
60
|
+
id: string;
|
|
61
|
+
name: string;
|
|
62
|
+
type: AccountType;
|
|
63
|
+
parent_id?: string | null;
|
|
64
|
+
subtype?: string | null;
|
|
65
|
+
bank_name?: string | null;
|
|
66
|
+
account_number_masked?: string | null;
|
|
67
|
+
currency?: string;
|
|
68
|
+
due_day?: number | null;
|
|
69
|
+
statement_day?: number | null;
|
|
70
|
+
metadata?: Record<string, unknown> | null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Insert a new account row. Enforces the three hierarchy invariants:
|
|
74
|
+
* 1. Top-level roots: parent_id null, id == type, one of TOP_LEVEL_TYPES.
|
|
75
|
+
* 2. Children: parent_id non-null, parent must exist (the top-level root is
|
|
76
|
+
* auto-bootstrapped if missing — intermediate categories must be created
|
|
77
|
+
* explicitly), parent.type must equal input.type, input.id must start with
|
|
78
|
+
* parent.id + ':'.
|
|
79
|
+
* 3. UNIQUE on id (surfaces as code: 'ACCOUNT_EXISTS').
|
|
80
|
+
*/
|
|
81
|
+
export declare function createAccount(db: Database.Database, input: CreateAccountInput): void;
|
|
82
|
+
export interface UpdateAccountMetadataPatch {
|
|
83
|
+
due_day?: number | null;
|
|
84
|
+
statement_day?: number | null;
|
|
85
|
+
points_balance?: number | null;
|
|
86
|
+
account_number_masked?: string | null;
|
|
87
|
+
bank_name?: string | null;
|
|
88
|
+
metadata?: Record<string, unknown>;
|
|
89
|
+
}
|
|
90
|
+
export interface UpdateAccountMetadataResult {
|
|
91
|
+
before: Record<string, unknown>;
|
|
92
|
+
after: Record<string, unknown>;
|
|
93
|
+
changed: boolean;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Patch metadata fields on an account. Returns before/after snapshots of the
|
|
97
|
+
* touched fields so callers can persist a reversible audit record. `metadata`
|
|
98
|
+
* is shallow-merged into the existing metadata_json blob.
|
|
99
|
+
*/
|
|
100
|
+
export declare function updateAccountMetadata(db: Database.Database, id: string, patch: UpdateAccountMetadataPatch): UpdateAccountMetadataResult;
|
|
101
|
+
/**
|
|
102
|
+
* Re-point every posting on `fromId` to `toId`, then delete the source account.
|
|
103
|
+
* Wrapped in a transaction. Refuses if the source still has children. Returns
|
|
104
|
+
* the number of postings moved.
|
|
105
|
+
*/
|
|
106
|
+
export declare function mergeAccounts(db: Database.Database, fromId: string, toId: string): number;
|
|
107
|
+
/** Delete an account only if no postings reference it AND it has no children. */
|
|
108
|
+
export declare function deleteAccount(db: Database.Database, id: string): void;
|
|
109
|
+
/**
|
|
110
|
+
* Recursive CTE walk over `accounts.parent_id` returning the root and every
|
|
111
|
+
* descendant. Used by `getRollupBalance` and by hierarchical rendering paths.
|
|
112
|
+
*/
|
|
113
|
+
export declare function getAccountSubtree(db: Database.Database, rootId: string): AccountRow[];
|
|
114
|
+
/**
|
|
115
|
+
* Sum the natural balance of every account in a subtree (root inclusive).
|
|
116
|
+
* Uses the same debit-normal / credit-normal convention as `getAccountBalances`.
|
|
117
|
+
*/
|
|
118
|
+
export declare function getRollupBalance(db: Database.Database, rootId: string): number;
|
|
119
|
+
export interface SimilarAccountPair {
|
|
120
|
+
a: AccountRow;
|
|
121
|
+
b: AccountRow;
|
|
122
|
+
similarity: number;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Pairwise Levenshtein similarity over `accounts.name`. Returns pairs above the
|
|
126
|
+
* threshold (0–1, where 1 = identical), sorted highest first. Quadratic in the
|
|
127
|
+
* number of accounts — fine for the small N a personal chart of accounts holds.
|
|
128
|
+
*/
|
|
129
|
+
export declare function findSimilarAccounts(db: Database.Database, threshold?: number): SimilarAccountPair[];
|
|
130
|
+
export interface FuzzyAccountMatch {
|
|
131
|
+
account: AccountRow;
|
|
132
|
+
similarity: number;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Rank the chart of accounts by name similarity to a free-text query. Returns
|
|
136
|
+
* matches at or above `threshold`, highest first. Bonus weight when the query
|
|
137
|
+
* is a substring of the name so "ttb saving" still finds "TTB Savings ••1234"
|
|
138
|
+
* even though pure Levenshtein on the full strings is mediocre.
|
|
139
|
+
*/
|
|
140
|
+
export declare function findAccountsByFuzzyName(db: Database.Database, query: string, threshold?: number): FuzzyAccountMatch[];
|