plasalid 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +3 -4
  2. package/dist/ai/agent.d.ts +6 -7
  3. package/dist/ai/agent.js +27 -11
  4. package/dist/ai/personas.js +48 -46
  5. package/dist/ai/system-prompt.js +1 -1
  6. package/dist/ai/tools/account-mutex.d.ts +1 -0
  7. package/dist/ai/tools/account-mutex.js +16 -0
  8. package/dist/ai/tools/index.js +4 -12
  9. package/dist/ai/tools/ingest.d.ts +1 -1
  10. package/dist/ai/tools/ingest.js +282 -242
  11. package/dist/ai/tools/merchants.js +1 -28
  12. package/dist/ai/tools/read.js +8 -8
  13. package/dist/ai/tools/record.js +3 -36
  14. package/dist/ai/tools/resolve.js +25 -22
  15. package/dist/ai/tools/scan.js +0 -1
  16. package/dist/ai/tools/types.d.ts +14 -21
  17. package/dist/cli/commands/record.js +1 -82
  18. package/dist/cli/commands/resolve.d.ts +5 -2
  19. package/dist/cli/commands/resolve.js +36 -5
  20. package/dist/cli/commands/revert.js +4 -2
  21. package/dist/cli/commands/rules.js +2 -2
  22. package/dist/cli/commands/scan.js +199 -128
  23. package/dist/cli/commands/status.js +5 -5
  24. package/dist/cli/index.js +8 -29
  25. package/dist/cli/ink/ScanDashboard.d.ts +49 -0
  26. package/dist/cli/ink/ScanDashboard.js +214 -0
  27. package/dist/cli/ink/scan_dashboard.d.ts +40 -25
  28. package/dist/cli/ink/scan_dashboard.js +139 -44
  29. package/dist/db/queries/account-balance.d.ts +1 -1
  30. package/dist/db/queries/questions.d.ts +62 -0
  31. package/dist/db/queries/questions.js +110 -0
  32. package/dist/db/queries/transactions.d.ts +1 -1
  33. package/dist/db/queries/unknowns.d.ts +17 -15
  34. package/dist/db/queries/unknowns.js +35 -39
  35. package/dist/db/schema.js +6 -28
  36. package/dist/scanner/audit/auditor.d.ts +31 -0
  37. package/dist/scanner/audit/auditor.js +72 -0
  38. package/dist/scanner/audit/engine.d.ts +10 -0
  39. package/dist/scanner/audit/engine.js +98 -0
  40. package/dist/scanner/audit/eventBus.d.ts +60 -0
  41. package/dist/scanner/audit/eventBus.js +35 -0
  42. package/dist/scanner/audit/passes/index.d.ts +11 -0
  43. package/dist/scanner/audit/passes/index.js +9 -0
  44. package/dist/scanner/audit/passes/types.d.ts +23 -0
  45. package/dist/scanner/audit/passes/types.js +1 -0
  46. package/dist/scanner/audit/types.d.ts +27 -0
  47. package/dist/scanner/audit/types.js +1 -0
  48. package/dist/scanner/auditor.d.ts +51 -0
  49. package/dist/scanner/auditor.js +80 -0
  50. package/dist/scanner/buffer/engine.d.ts +9 -0
  51. package/dist/scanner/buffer/engine.js +110 -0
  52. package/dist/scanner/buffer/sharedBuffer.d.ts +78 -0
  53. package/dist/scanner/buffer/sharedBuffer.js +130 -0
  54. package/dist/scanner/buffer/types.d.ts +67 -0
  55. package/dist/scanner/buffer/types.js +1 -0
  56. package/dist/scanner/buffer.d.ts +45 -38
  57. package/dist/scanner/buffer.js +93 -61
  58. package/dist/scanner/bus/engine.d.ts +11 -0
  59. package/dist/scanner/bus/engine.js +42 -0
  60. package/dist/scanner/bus/types.d.ts +53 -0
  61. package/dist/scanner/bus/types.js +1 -0
  62. package/dist/scanner/bus.d.ts +38 -0
  63. package/dist/scanner/bus.js +37 -0
  64. package/dist/scanner/chunk-worker.d.ts +19 -0
  65. package/dist/scanner/chunk-worker.js +67 -0
  66. package/dist/scanner/chunkWorker.d.ts +20 -0
  67. package/dist/scanner/chunkWorker.js +59 -0
  68. package/dist/scanner/chunker/chunker.d.ts +7 -0
  69. package/dist/scanner/chunker/chunker.js +60 -0
  70. package/dist/scanner/chunker.d.ts +7 -0
  71. package/dist/scanner/chunker.js +60 -0
  72. package/dist/scanner/converge.d.ts +29 -0
  73. package/dist/scanner/converge.js +15 -0
  74. package/dist/scanner/decrypt.d.ts +10 -0
  75. package/dist/scanner/decrypt.js +80 -0
  76. package/dist/scanner/engine/scanEngine.d.ts +24 -0
  77. package/dist/scanner/engine/scanEngine.js +87 -0
  78. package/dist/scanner/engine/types.d.ts +90 -0
  79. package/dist/scanner/engine/types.js +1 -0
  80. package/dist/scanner/engine.d.ts +90 -0
  81. package/dist/scanner/engine.js +84 -0
  82. package/dist/scanner/file-worker.d.ts +33 -0
  83. package/dist/scanner/file-worker.js +28 -0
  84. package/dist/scanner/fileWorker.d.ts +33 -0
  85. package/dist/scanner/fileWorker.js +22 -0
  86. package/dist/scanner/hooks/types.d.ts +25 -0
  87. package/dist/scanner/hooks/types.js +1 -0
  88. package/dist/scanner/hooks.d.ts +23 -0
  89. package/dist/scanner/hooks.js +1 -0
  90. package/dist/scanner/parse.d.ts +10 -0
  91. package/dist/scanner/parse.js +47 -0
  92. package/dist/scanner/passes/index.d.ts +8 -0
  93. package/dist/scanner/passes/index.js +6 -0
  94. package/dist/scanner/passes/types.d.ts +22 -0
  95. package/dist/scanner/passes/types.js +1 -0
  96. package/dist/scanner/pdf/chunker.d.ts +7 -0
  97. package/dist/scanner/pdf/chunker.js +60 -0
  98. package/dist/scanner/pdf/password-store.d.ts +34 -0
  99. package/dist/scanner/pdf/password-store.js +83 -0
  100. package/dist/scanner/pdf/pdf-unlock.d.ts +17 -0
  101. package/dist/scanner/pdf/pdf-unlock.js +50 -0
  102. package/dist/scanner/pdf/pdf.d.ts +17 -0
  103. package/dist/scanner/pdf/pdf.js +36 -0
  104. package/dist/scanner/pdf/state-machine.d.ts +60 -0
  105. package/dist/scanner/pdf/state-machine.js +64 -0
  106. package/dist/scanner/pdf/unlock.d.ts +22 -0
  107. package/dist/scanner/pdf/unlock.js +121 -0
  108. package/dist/scanner/phase-decrypt.d.ts +10 -0
  109. package/dist/scanner/phase-decrypt.js +80 -0
  110. package/dist/scanner/phase-parse.d.ts +10 -0
  111. package/dist/scanner/phase-parse.js +46 -0
  112. package/dist/scanner/phases/chunk.d.ts +8 -0
  113. package/dist/scanner/phases/chunk.js +13 -0
  114. package/dist/scanner/phases/commit.d.ts +12 -0
  115. package/dist/scanner/phases/commit.js +140 -0
  116. package/dist/scanner/phases/decrypt.d.ts +10 -0
  117. package/dist/scanner/phases/decrypt.js +80 -0
  118. package/dist/scanner/phases/parse.d.ts +10 -0
  119. package/dist/scanner/phases/parse.js +46 -0
  120. package/dist/scanner/phases/resolve.d.ts +10 -0
  121. package/dist/scanner/phases/resolve.js +17 -0
  122. package/dist/scanner/phases/review.d.ts +10 -0
  123. package/dist/scanner/phases/review.js +12 -0
  124. package/dist/scanner/progress.d.ts +14 -0
  125. package/dist/scanner/progress.js +21 -0
  126. package/dist/scanner/resolver-memory.d.ts +8 -0
  127. package/dist/scanner/resolver-memory.js +24 -0
  128. package/dist/scanner/resolver.d.ts +39 -0
  129. package/dist/scanner/resolver.js +196 -0
  130. package/dist/scanner/result.d.ts +17 -0
  131. package/dist/scanner/result.js +19 -0
  132. package/dist/scanner/run-passes.d.ts +30 -0
  133. package/dist/scanner/run-passes.js +15 -0
  134. package/dist/scanner/unlock.js +1 -1
  135. package/dist/scanner/worker.d.ts +19 -0
  136. package/dist/scanner/worker.js +67 -0
  137. package/dist/scanner/workers/chunkWorker.d.ts +20 -0
  138. package/dist/scanner/workers/chunkWorker.js +65 -0
  139. package/dist/scanner/workers/fileWorker.d.ts +32 -0
  140. package/dist/scanner/workers/fileWorker.js +22 -0
  141. package/package.json +1 -1
@@ -1,165 +1,236 @@
1
1
  import chalk from "chalk";
2
- import { runScan } from "../../scanner/pipeline.js";
2
+ import { getDb } from "../../db/connection.js";
3
+ import { runScan } from "../../scanner/engine.js";
3
4
  export async function runScanCommand(opts) {
4
- if (opts.regex !== undefined) {
5
+ if (opts.regex) {
5
6
  try {
6
7
  new RegExp(opts.regex, "i");
7
8
  }
8
9
  catch (err) {
9
- console.error(chalk.red(`Invalid regex: ${err.message}`));
10
+ console.error(chalk.red(`Invalid regex: ${err instanceof Error ? err.message : String(err)}`));
10
11
  process.exitCode = 1;
11
12
  return;
12
13
  }
13
14
  }
14
- const events = process.stdout.isTTY
15
- ? await inkScanEvents(opts.parallel ?? 3)
16
- : plainScanEvents();
17
- const summary = await runScan({
15
+ const parallel = opts.parallel ?? 5;
16
+ const isTTY = !!process.stdout.isTTY;
17
+ const hooks = isTTY ? await buildTtyHooks() : buildPlainHooks();
18
+ const result = await runScan(getDb(), {
18
19
  regex: opts.regex,
19
20
  force: opts.force,
20
21
  interactive: true,
21
- concurrency: opts.parallel,
22
- events,
23
- });
24
- renderScanSummary(summary);
22
+ maxFileWorkers: parallel,
23
+ }, hooks);
24
+ renderSummary(result.state);
25
25
  }
26
- function logDecryptProgress(e) {
27
- const marker = e.outcome === "decrypted" ? chalk.dim("·")
28
- : e.outcome === "skipped" ? chalk.dim("")
29
- : chalk.red("");
30
- console.log(` ${marker} [${e.index + 1}/${e.total}] ${e.fileName} (${e.outcome})`);
31
- }
32
- /**
33
- * Hooks every mode shares: the decrypt phase, commit notice, and inspector
34
- * summary all render the same way in TTY and non-TTY runs. Each mode-specific
35
- * factory below spreads this base and overrides the scan-phase hooks
36
- * (`scanStart` / `scanProgress` / `scanEnd`) to render differently.
37
- *
38
- * Returns `Partial<ScanRunEvents>` because the scan-phase hooks are filled in
39
- * by the caller — composition, not inheritance.
40
- */
41
- function baseScanEvents() {
42
- let decryptTotal = 0;
26
+ /* TTY mode — Ink dashboard with one in-place row per file. */
27
+ async function buildTtyHooks() {
28
+ const { render } = await import("ink");
29
+ const { createElement } = await import("react");
30
+ const { ScanDashboard, createScanDashboardController } = await import("../ink/ScanDashboard.js");
31
+ const controller = createScanDashboardController();
32
+ let inkInstance = null;
33
+ let unsubscribeProgress = null;
34
+ const chunkLookup = new Map();
43
35
  return {
44
- decryptStart: (count) => {
45
- decryptTotal = count;
46
- if (count > 0)
47
- console.log(chalk.dim(`Decrypting ${count} file(s)...`));
36
+ afterDecrypt: (s) => {
37
+ const total = s.decrypted.length + s.skipped.length + s.failed.length;
38
+ if (total === 0) {
39
+ console.log(chalk.dim("No files to scan."));
40
+ return;
41
+ }
42
+ console.log(chalk.dim(`Decrypted ${s.decrypted.length}, skipped ${s.skipped.length}, failed ${s.failed.length}.`));
43
+ },
44
+ afterChunk: (s) => {
45
+ if (s.chunks.length === 0)
46
+ return;
47
+ console.log(chalk.dim(`Chunked into ${s.chunks.length} page(s). Mounting dashboard…`));
48
48
  },
49
- decryptProgress: logDecryptProgress,
50
- decryptDone: (e) => {
51
- if (decryptTotal === 0)
49
+ beforeParse: (s) => {
50
+ for (const c of s.chunks)
51
+ chunkLookup.set(c.chunkId, {
52
+ fileId: c.fileId,
53
+ pageNumber: c.pageNumber,
54
+ });
55
+ if (s.decrypted.length === 0)
52
56
  return;
53
- console.log(chalk.dim(`Decrypted ${e.decrypted}, skipped ${e.skipped}, failed ${e.failed}.`));
57
+ process.stdout.write("\x1b[2J\x1b[H\x1b[?25l");
58
+ const files = s.decrypted.map((d) => ({
59
+ fileId: d.path,
60
+ fileName: d.fileName,
61
+ totalPages: s.chunks.filter((c) => c.fileId === d.path).length,
62
+ }));
63
+ inkInstance = render(createElement(ScanDashboard, {
64
+ controller,
65
+ files,
66
+ }), {
67
+ exitOnCtrlC: false,
68
+ patchConsole: false,
69
+ });
70
+ unsubscribeProgress = s.progress.subscribe((event) => {
71
+ const map = chunkLookup.get(event.chunkId);
72
+ if (!map)
73
+ return;
74
+ controller.publish({
75
+ type: event.kind === "tx" ? "chunk-tx" : "chunk-question",
76
+ fileId: map.fileId,
77
+ pageNumber: map.pageNumber,
78
+ });
79
+ });
54
80
  },
55
- committing: () => { console.log(chalk.dim("Committing...")); },
56
- inspecting: (r) => {
57
- if (r.total > 0)
58
- console.log(chalk.dim(`Inspectors flagged ${r.total} unknown(s).`));
81
+ onWorkerStart: (_id, chunk) => {
82
+ controller.publish({
83
+ type: "chunk-start",
84
+ fileId: chunk.fileId,
85
+ fileName: chunk.fileName,
86
+ pageNumber: chunk.pageNumber,
87
+ totalPages: chunk.totalPages,
88
+ });
89
+ },
90
+ onWorkerEnd: (_id, chunk, ok) => {
91
+ controller.publish({
92
+ type: "chunk-end",
93
+ fileId: chunk.fileId,
94
+ pageNumber: chunk.pageNumber,
95
+ ok,
96
+ });
97
+ },
98
+ afterParse: () => {
99
+ unsubscribeProgress?.();
100
+ unsubscribeProgress = null;
101
+ },
102
+ beforeResolve: () => {
103
+ controller.publish({ type: "phase-set", phase: "resolve" });
104
+ },
105
+ afterResolve: () => {
106
+ controller.publish({ type: "phase-set", phase: "done" });
107
+ inkInstance?.unmount();
108
+ inkInstance = null;
109
+ process.stdout.write("\x1b[?25h");
59
110
  },
60
111
  };
61
112
  }
62
- /** TTY mode: mount the Ink dashboard during the scan phase. */
63
- async function inkScanEvents(parallel) {
64
- // Lazy-load ink + react so this module stays importable in non-TTY contexts.
65
- const { render } = await import("ink");
66
- const { createElement } = await import("react");
67
- const { ScanDashboard, ScanDashboardController } = await import("../ink/scan_dashboard.js");
68
- const controller = new ScanDashboardController();
69
- let inkInstance = null;
70
- let mountedFiles = 0;
71
- const base = baseScanEvents();
113
+ const FINALIZE_RULES = [
114
+ { when: (t) => t.failedChunks === 0, kind: "success" },
115
+ { when: (t) => t.failedChunks === t.totalChunks, kind: "all-failed" },
116
+ { when: () => true, kind: "partial" },
117
+ ];
118
+ const FINALIZE_RENDER = {
119
+ success: (t) => ` ${chalk.green("ok")} ${t.fileName} ${chalk.dim(`${t.completedChunks} of ${t.totalChunks} pages · ${t.txAdded} transactions${t.questionsAdded > 0 ? `, ${t.questionsAdded} questions` : ""}`)}`,
120
+ "all-failed": (t) => ` ${chalk.red("fail")} ${t.fileName} ${chalk.dim("every chunk failed")}`,
121
+ partial: (t) => ` ${chalk.yellow("partial")} ${t.fileName} ${chalk.dim(`${t.completedChunks} of ${t.totalChunks} pages · ${t.failedChunks} chunks failed · ${t.txAdded} transactions`)}`,
122
+ };
123
+ function classifyFinalize(t) {
124
+ for (const r of FINALIZE_RULES)
125
+ if (r.when(t))
126
+ return r.kind;
127
+ return "partial";
128
+ }
129
+ function buildPlainHooks() {
130
+ const tallies = new Map();
131
+ const fileIdByChunkId = new Map();
132
+ let unsubscribeProgress = null;
133
+ const finalize = (fileId) => {
134
+ const t = tallies.get(fileId);
135
+ if (!t || t.completedChunks + t.failedChunks < t.totalChunks)
136
+ return;
137
+ console.log(FINALIZE_RENDER[classifyFinalize(t)](t));
138
+ };
72
139
  return {
73
- ...base,
74
- decryptDone: (e) => {
75
- base.decryptDone?.(e);
76
- console.log("");
77
- mountedFiles = e.decrypted;
78
- if (e.decrypted > 0) {
79
- inkInstance = render(createElement(ScanDashboard, { controller, totalFiles: e.decrypted, parallel }));
140
+ afterDecrypt: (s) => {
141
+ const total = s.decrypted.length + s.skipped.length + s.failed.length;
142
+ if (total === 0) {
143
+ console.log("No files to scan.");
144
+ return;
80
145
  }
146
+ console.log(`Decrypted ${s.decrypted.length}, skipped ${s.skipped.length}, failed ${s.failed.length}.`);
81
147
  },
82
- scanStart: (e) => controller.publish({ type: "scan-start", fileName: e.fileName }),
83
- scanProgress: (e) => controller.publish({ type: "scan-progress", fileName: e.fileName, step: e.step }),
84
- scanEnd: (e) => controller.publish({
85
- type: "scan-end",
86
- fileName: e.fileName,
87
- status: e.status,
88
- transactions: e.transactions,
89
- unknowns: e.unknowns,
90
- error: e.error,
91
- }),
92
- committing: () => {
93
- if (inkInstance) {
94
- inkInstance.unmount();
95
- inkInstance = null;
96
- }
97
- if (mountedFiles > 0)
98
- base.committing?.();
148
+ afterChunk: (s) => {
149
+ if (s.chunks.length > 0)
150
+ console.log(`Chunked into ${s.chunks.length} page(s).`);
99
151
  },
100
- };
101
- }
102
- /** Non-TTY mode: print one line per file as it progresses. */
103
- function plainScanEvents() {
104
- // De-dupe scan-progress chatter: only print when the step text changes per file.
105
- const lastStepByFile = new Map();
106
- return {
107
- ...baseScanEvents(),
108
- scanStart: (e) => {
109
- console.log(`${chalk.cyan("→")} ${e.fileName} ${chalk.dim("starting...")}`);
152
+ beforeParse: (s) => {
153
+ for (const c of s.chunks)
154
+ fileIdByChunkId.set(c.chunkId, c.fileId);
155
+ unsubscribeProgress = s.progress.subscribe((event) => {
156
+ const fileId = fileIdByChunkId.get(event.chunkId);
157
+ if (!fileId)
158
+ return;
159
+ const t = tallies.get(fileId);
160
+ if (!t)
161
+ return;
162
+ if (event.kind === "tx")
163
+ t.txAdded++;
164
+ else
165
+ t.questionsAdded++;
166
+ });
167
+ },
168
+ onWorkerStart: (_id, chunk) => {
169
+ if (!tallies.has(chunk.fileId)) {
170
+ tallies.set(chunk.fileId, {
171
+ fileName: chunk.fileName,
172
+ totalChunks: chunk.totalPages,
173
+ completedChunks: 0,
174
+ failedChunks: 0,
175
+ txAdded: 0,
176
+ questionsAdded: 0,
177
+ });
178
+ }
110
179
  },
111
- scanProgress: (e) => {
112
- if (lastStepByFile.get(e.fileName) === e.step)
180
+ onWorkerEnd: (_id, chunk, ok) => {
181
+ const t = tallies.get(chunk.fileId);
182
+ if (!t)
113
183
  return;
114
- lastStepByFile.set(e.fileName, e.step);
115
- console.log(chalk.dim(` ${e.fileName} · ${e.step}`));
184
+ if (ok)
185
+ t.completedChunks++;
186
+ else
187
+ t.failedChunks++;
188
+ finalize(chunk.fileId);
116
189
  },
117
- scanEnd: (e) => {
118
- lastStepByFile.delete(e.fileName);
119
- const line = e.status === "scanned"
120
- ? `${chalk.green("✓")} ${e.fileName} ${chalk.dim(`(${e.transactions} transactions, ${e.unknowns} unknowns)`)}`
121
- : `${chalk.red("✗")} ${e.fileName} ${chalk.dim(`— ${e.error ?? "failed"}`)}`;
122
- console.log(line);
190
+ afterParse: () => {
191
+ unsubscribeProgress?.();
192
+ unsubscribeProgress = null;
193
+ },
194
+ beforeResolve: () => {
195
+ console.log("Resolving...");
123
196
  },
124
197
  };
125
198
  }
126
- /** Terse summary */
127
- function renderScanSummary(summary) {
128
- console.log("");
129
- const headline = `Scanned ${summary.total} file(s) — ` +
130
- `${summary.scanned + summary.replaced} ok, ` +
131
- `${summary.failed} failed, ` +
132
- `${summary.unknowns} unknown${summary.unknowns === 1 ? "" : "s"} flagged`;
133
- console.log(chalk.bold(headline));
199
+ function renderSummary(state) {
134
200
  console.log("");
135
- for (const d of summary.details) {
136
- const label = d.relPath;
137
- switch (d.status) {
138
- case "scanned": {
139
- const tag = chalk.dim(`${d.transactions} transactions${d.unknowns > 0 ? ` · ${d.unknowns} unknowns` : ""}`);
140
- console.log(` ${chalk.green("✓")} ${label} ${tag}`);
141
- break;
142
- }
143
- case "replaced": {
144
- const tag = chalk.dim(`${d.transactions} transactions${d.unknowns > 0 ? ` · ${d.unknowns} unknowns` : ""} (replaces prior)`);
145
- console.log(` ${chalk.cyan("↻")} ${label} ${tag}`);
146
- break;
147
- }
148
- case "skipped": {
149
- console.log(` ${chalk.dim("•")} ${label} ${chalk.dim("(already scanned)")}`);
150
- break;
151
- }
152
- case "failed": {
153
- console.log(` ${chalk.red("✗")} ${label} ${chalk.dim(`— ${d.error ?? "failed"}`)}`);
154
- break;
155
- }
201
+ const txCount = countTransactions(state);
202
+ console.log(chalk.bold(`Scanned ${state.decrypted.length} file(s) → ${txCount} transactions.`));
203
+ const r = state.resolveSummary;
204
+ if (r && r.total > 0) {
205
+ console.log(`Resolved ${r.resolved}/${r.total} questions.`);
206
+ if (r.remaining > 0) {
207
+ console.log(chalk.yellow(`${r.remaining} question(s) remain — run ${chalk.cyan("plasalid resolve")} to finish them.`));
156
208
  }
157
209
  }
158
- const newlyProcessed = summary.scanned + summary.replaced;
159
- if (newlyProcessed > 0) {
210
+ if (state.errors.length > 0) {
211
+ console.log(chalk.yellow(`${state.errors.length} phase error(s):`));
212
+ for (const e of state.errors) {
213
+ console.log(chalk.dim(` - [${e.phase}] ${e.target ?? ""} ${e.error?.message ?? ""}`));
214
+ }
215
+ }
216
+ if (txCount > 0) {
160
217
  console.log("");
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.")}`);
218
+ console.log(chalk.dim(`Next: run ${chalk.cyan("plasalid")} to chat with your ledger about what just landed.`));
164
219
  }
165
220
  }
221
+ /**
222
+ * Snapshot transaction count attributable to this scan. Reads from
223
+ * scanned_files via the file ids assigned in decryptPhase.
224
+ */
225
+ function countTransactions(state) {
226
+ const ids = state.decrypted
227
+ .map((d) => d.scannedFileId)
228
+ .filter((s) => !!s);
229
+ if (ids.length === 0)
230
+ return 0;
231
+ const placeholders = ids.map(() => "?").join(",");
232
+ const row = getDb()
233
+ .prepare(`SELECT COUNT(*) AS n FROM transactions WHERE source_file_id IN (${placeholders})`)
234
+ .get(...ids);
235
+ return row.n;
236
+ }
@@ -4,7 +4,7 @@ import { getNetWorth } from "../../db/queries/account-balance.js";
4
4
  import { countTransactions } from "../../db/queries/transactions.js";
5
5
  import { getRecurringSummary } from "../../db/queries/recurrences.js";
6
6
  import { countScannedFiles } from "../../db/queries/files.js";
7
- import { countOpenUnknowns } from "../../db/queries/unknowns.js";
7
+ import { countQuestions } from "../../db/queries/questions.js";
8
8
  import { countMemories } from "../../ai/memory.js";
9
9
  import { formatAmount } from "../../currency.js";
10
10
  import { visibleLength } from "../format.js";
@@ -38,7 +38,7 @@ function systemRows(db) {
38
38
  const tx = countTransactions(db);
39
39
  const files = countScannedFiles(db);
40
40
  const memories = countMemories(db);
41
- const unknowns = countOpenUnknowns(db);
41
+ const questions = countQuestions(db);
42
42
  const rows = [
43
43
  {
44
44
  label: "Transactions",
@@ -63,10 +63,10 @@ function systemRows(db) {
63
63
  if (memories > 0) {
64
64
  rows.push({ label: "Memories", value: formatInteger(memories) });
65
65
  }
66
- if (unknowns > 0) {
66
+ if (questions > 0) {
67
67
  rows.push({
68
- label: "Unknowns",
69
- value: chalk.yellow(formatInteger(unknowns)),
68
+ label: "Questions",
69
+ value: chalk.yellow(formatInteger(questions)),
70
70
  suffix: chalk.dim("run `plasalid resolve`"),
71
71
  });
72
72
  }
package/dist/cli/index.js CHANGED
@@ -118,23 +118,6 @@ program
118
118
  const { runScanCommand } = await import("./commands/scan.js");
119
119
  await runScanCommand({ regex: regexes[0], force: !!opts.force, parallel });
120
120
  });
121
- program
122
- .command("resolve")
123
- .description("Walk every open unknown from the last scan one at a time and apply your decision (categorize, merge duplicates, link recurrences, skip).")
124
- .option("-a, --account <id>", "Limit to unknowns attached to a single account")
125
- .option("--from <date>", "Only consider entries on or after this date (YYYY-MM-DD)")
126
- .option("--to <date>", "Only consider entries on or before this date (YYYY-MM-DD)")
127
- .option("-k, --kind <kind>", "Filter by unknown kind (uncategorized_expense, duplicate, correlation, recurrence_candidate, similar_accounts)")
128
- .action(async (opts) => {
129
- ensureConfigured();
130
- const { runResolveCommand } = await import("./commands/resolve.js");
131
- await runResolveCommand({
132
- accountId: opts.account,
133
- from: opts.from,
134
- to: opts.to,
135
- kind: opts.kind,
136
- });
137
- });
138
121
  program
139
122
  .command("rules")
140
123
  .description("List rules the system has learned")
@@ -152,12 +135,12 @@ program
152
135
  forgetRule(regex);
153
136
  });
154
137
  program
155
- .command("revert <regex>")
156
- .description("Delete scanned files matching <regex> and all their transactions")
157
- .action(async (regex) => {
138
+ .command("resolve")
139
+ .description("Resolve every question across the ledger")
140
+ .action(async () => {
158
141
  ensureConfigured();
159
- const { runRevertCommand } = await import("./commands/revert.js");
160
- await runRevertCommand(regex);
142
+ const { runResolveCommand } = await import("./commands/resolve.js");
143
+ await runResolveCommand();
161
144
  });
162
145
  program.configureHelp({
163
146
  formatHelp: () => helpScreen([
@@ -170,7 +153,7 @@ program.configureHelp({
170
153
  desc: "Open the data folder in your OS file explorer (alias: open)",
171
154
  },
172
155
  { name: "accounts", desc: "Browse the chart of accounts (interactive TTY) or list them (piped)" },
173
- { name: "status", desc: "Show financial and system status (net worth, recurring, unknowns)" },
156
+ { name: "status", desc: "Show financial and system status (net worth, recurring, questions)" },
174
157
  {
175
158
  name: "transactions",
176
159
  desc: "Browse transactions (interactive TTY) or list them (piped/--no-interactive)",
@@ -183,10 +166,6 @@ program.configureHelp({
183
166
  name: "scan",
184
167
  desc: "Scan new PDFs (optionally by regex; --force to re-scan)",
185
168
  },
186
- {
187
- name: "resolve",
188
- desc: "Walk open unknowns one at a time and apply your decision",
189
- },
190
169
  {
191
170
  name: "rules",
192
171
  desc: "List rules the system has learned",
@@ -196,8 +175,8 @@ program.configureHelp({
196
175
  desc: "Delete learned rules whose ids match <regex> (anchored)",
197
176
  },
198
177
  {
199
- name: "revert",
200
- desc: "Delete scanned files matching <regex> and their transactions",
178
+ name: "resolve",
179
+ desc: "Resolve every question across the ledger",
201
180
  },
202
181
  ]),
203
182
  });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Events the CLI publishes into the dashboard. The CLI subscribes to the
3
+ * scanner's ScanProgress sink and routes per-chunk ticks here via chunkLookup.
4
+ */
5
+ export type CurrentPhase = "parse" | "resolve" | "done";
6
+ export type DashboardEvent = {
7
+ type: "chunk-start";
8
+ fileId: string;
9
+ fileName: string;
10
+ pageNumber: number;
11
+ totalPages: number;
12
+ } | {
13
+ type: "chunk-tx";
14
+ fileId: string;
15
+ pageNumber: number;
16
+ } | {
17
+ type: "chunk-question";
18
+ fileId: string;
19
+ pageNumber: number;
20
+ } | {
21
+ type: "chunk-end";
22
+ fileId: string;
23
+ pageNumber: number;
24
+ ok: boolean;
25
+ } | {
26
+ type: "phase-set";
27
+ phase: CurrentPhase;
28
+ };
29
+ export interface ScanDashboardController {
30
+ publish(event: DashboardEvent): void;
31
+ subscribe(handler: (e: DashboardEvent) => void): () => void;
32
+ }
33
+ export declare function createScanDashboardController(): ScanDashboardController;
34
+ export interface FileSeed {
35
+ readonly fileId: string;
36
+ readonly fileName: string;
37
+ readonly totalPages: number;
38
+ }
39
+ interface Props {
40
+ controller: ScanDashboardController;
41
+ files: ReadonlyArray<FileSeed>;
42
+ }
43
+ /**
44
+ * Tree-layout scan dashboard. Header carries the only animated element (one
45
+ * `<Spinner>`). All other status indicators are static glyphs that only
46
+ * redraw when their data changes.
47
+ */
48
+ export declare function ScanDashboard(props: Props): import("react/jsx-runtime").JSX.Element;
49
+ export {};