LedgerMap: what we could track

Proposal · 16/10/2026 · Nothing here is built yet. You choose what to build, what to leave out, and how to split it.

All tracking stays in our own PostgreSQL. We do not use PostHog now. Each item below says what we would record, which question it answers, where we capture it, and how important it is. The pictures are mockups with made-up numbers. They show what the report could look like. They are not screenshots of the app.

Coreneeded to judge completeness Usefulhelps find problems faster Laternice to have

1. What we track today

Each entry has one row in statement_entry_ledgers. Each new choice overwrites it. We keep the final state, not the path to it. A failed parse leaves no record.

QuestionTodayWhat exists
What did we suggest?PartlyOnly exact matches are saved (source='matched', match_kind). Other suggestions are computed live and are not saved.
Auto-accepted or manual?Partlymatched / auto (a suggestion was picked) / manual / books. We do not record the method (Ctrl+Enter, picker, bulk, same payee).
What did the user change? Twice?NoWe keep only the last value. A changed exact match leaves a rejection row with the ledger ID and no time.
Changes in the review phase?NoWe do not record the step. statement_entry_exports stores only the export time.
Searches, empty searches, gave up?NoThe search text in the picker never leaves the app.
Suspense: a decision or neglect?PartlyThe user can save the suspense ledger. An entry with no ledger also goes to suspense at export, and we cannot tell the two apart.
Parser failuresNoThe server returns the error code and saves nothing. Only successful statements (with their parser JSON) are saved.
Which app versions people runNoThe app does not send its version to the server.
App errors and speedNoThe server logs only database and start-up errors. The app sends no crash reports.

2. Decision trail for each entry

These are your items, plus the missing parts around them. Together they give a full history for each entry, from the first suggestion to the export.

A1 · What we showed

CoreServer

The suggestion and the “Possible” list (up to 3). For each: ledger, matcher kind, situation, the % likely shown, and its rank.

Was the right ledger in front of the user?

A2 · Each choice, with the method

CoreApp + Server

Old ledger → new ledger, source, and method: Enter twice, Ctrl+Enter, a typed search, a suggested option, a bulk selection, same payee (Alt+G), contra Accept, or books Accept.

Auto-accepted, or chosen by hand?

A3 · Change count and flip-backs

CoreServer

We keep every overwrite, so we see “changed 2+ times” and A → B → A.

Where is the user unsure, or are we confusing them?

A4 · The step of each change

CoreApp

Contra, Suggestions, Suspense, Ledgers (review), Export, or All entries. Also changes after an export, from a later import of the same PDF.

How much does the review phase correct?

A5 · Silent acceptance

CoreServer

An exact match that the user never touched and exported as it was. Compare it with one the user opened and kept, and with one the user changed.

Are exact matches trusted, or checked?

A6 · Picker search sessions

CoreApp

The picker opened. For each query step: the match count and the rank picked. Also queries with no match, Escape without a choice, and the time spent. We would not save the full query text unless you approve it (§8).

Did the user search, fail, and give up?

A7 · How an entry reached suspense

CoreApp + Server

The suspense ledger saved on purpose, or no ledger at export. Whether a search or a suggestion came before it.

Is suspense a decision, or a dead end?

A8 · Clears, unpairs, “don’t map”

CoreServer

A cleared choice, an unpaired contra, and an entry left out of mapping, with what it held before.

Which automatic results do users undo?

3. Parser completeness

The largest gap is failures. Today we see only the PDFs that parsed. We cannot say “X% of uploads work for bank Y”.

B1 · Every parse attempt

CoreServer

Each upload, failures too: upload SHA-256, page count, size, outcome code (validated, needs_review, scanned_pdf, unknown_format…), reason codes, duration, queue wait, and parser build hash.

What share of uploads work, for each bank and each build?

B2 · The route that read it

CoreServer

Saved recipe, layout detection, a recipe Codex learned, or codex-kept-reading. Also connector_id + version, match_score, and the Codex result (installed, failed, timed out, model).

Which layouts depend on Codex? Does Codex pay off?

B3 · What happens after needs_review

CoreApp

The reason codes, the rows marked to check, how many of those the user opened in the PDF, and the result: kept it, or Leave this PDF out.

Are review results usable, or thrown away?

B4 · Balance evidence for each statement

UsefulReport only

Reconciled steps compared with checked steps, and warnings (rows moved, repeated rows, notes skipped). This is already in the saved parser JSON. We need only a summary.

Which statements pass narrowly?

B5 · “Show in PDF” checks

UsefulApp

Alt+O and Alt+Enter opens for each entry, and whether a change follows. Many opens for one recipe suggest the user doubts its reading.

Where do users not trust the parser?

B6 · Validator findings

UsefulApp

For each statement and recipe: how many entries Validator found missing or repeated in Miracle. We record counts only, never book content.

Do some recipes drop or duplicate rows?

B7 · Bank confirmation

UsefulApp

The bank ledger that the file name suggested, compared with the one the user confirmed.

Is the file-name guess right?

B8 · Password flow

UsefulServer

Locked or not, whether a saved password worked, how many prompts, and how many wrong tries. We never record the password.

How much friction do locked PDFs cause?

B9 · Re-uploads and scans

LaterServer

The same PDF uploaded again, and scans per user or bank.

Do users retry failures, or bring the wrong file?

4. Auto-matcher

Today bun run matcheval measures the matcher on past books. These items measure it on real use, so the two can be compared.

C1 · Real precision of each exact matcher

CoreServer

Matched entries kept at export, compared with matched entries the user overturned, for each kind (upi, account, narration, ledgerName).

Is “exact” really exact in use?

C2 · Calibration of “% likely”

CoreServer

For each situation: the % shown, compared with how often the user accepted it. Real use could then update ledger_confidence_table.go, which today comes only from the replay.

When we say 80%, are we right 80% of the time?

C3 · Rank of the final ledger

CoreServer

The first suggestion, the 2nd or 3rd, “Possible”, found by search, or never listed.

How often was the answer close, but not first?

C4 · Coverage funnel for each import

CoreServer

Entries → auto contra → exact match → in books → suggested → suspense, then the result at export.

How much work is left to the user?

C5 · Contra results

CoreServer

Possible contra accepted or rejected, books transfers accepted or set aside, manual Alt+C pairs (the matcher missed them), and unpairs (the auto pair was wrong).

What does contra detection miss, and what does it get wrong?

C6 · Matcher version tag

CoreServer

Each suggestion has the matcher version (a commit or a number). After a change, we compare before and after on live data, as matcheval does on books.

Did the last matcher change help real users?

C7 · Learning effect

UsefulServer

The “N other entries matched” count after a choice, and how many of those entries the user later changed.

Does one choice teach good matches or bad ones?

C8 · Miracle-history suggestions

UsefulApp

Only the clue kind (same day of month, always this amount) and the result. Book content stays on the PC.

Are book-based suggestions worth their cost?

C9 · Effort for each entry

UsefulApp

The time from shown to decided, the keys pressed, and entries per minute in each step.

Where does the user spend time?

C10 · Suggestions that changed

LaterServer

An entry’s suggestion changed between two views, because of other choices.

Do suggestions jump around under the user?

5. General app

D1 · App version and platform

CoreApp + Server

The app sends a version header with each request (desktop or browser, OS). Also the update steps: checked, downloaded, installed, restarted.

Who still runs an old app? Is it safe to drop an old API?

D2 · Import funnel and drop-off

CoreApp

Started, PDFs added, banks linked, each step entered and left, export saved. Also where imports stop, and the time in each step.

Where do users stop or get stuck?

D3 · Errors

CoreApp + Server

App crashes (the error boundary), API errors by code and route, 409 setup and ledger_revision conflicts, and offline-gate hits.

What breaks that users do not report?

D4 · Speed

UsefulApp + Server

API time for each route, parse time and parser_busy waits, and on the desktop the table render time and key-repeat lag (WebKitGTK).

Is it fast on real PCs, not only ours?

D5 · Keyboard or mouse

UsefulApp

How often each shortcut is used, actions done with the mouse that have a key, and opens of the shortcuts help.

Is keyboard-first working? Which shortcuts are unknown?

D6 · Export results

UsefulApp + Server

Files saved, entries exported, entries left out (in books), entries exported again, and whether Validator later finds them in Miracle.

Does the export reach Miracle completely?

D7 · Ledger sync

UsefulServer

Sync runs, ledgers added or changed, failures, and revision conflicts.

Are ledger lists fresh when users map?

D8 · Parties, tabs, storage

LaterServer

Parties for each user, open tabs, party switches, and storage used compared with the quota.

How do users work across parties?

6. What the report could show

These are four views from one report. All names and numbers are made up.

Mockup

Decision trail for one entry

A1–A8 together: the full history of one entry.

Statement #4182 · row 37 Date 04/09/2026 Debit ₹18,450.00 Narration UPI/DR/62411…/SHREE GANESH TR/…
  1. 10:02:11Suggested Shree Ganesh Traders · similarName · 62% likely · rank 1 of 3 (also: Ganesh Enterprises 21%, Shree Traders 9%)
  2. 10:02:40Accepted Shree Ganesh Traders · Ctrl+Enter · step Suggestions · 29 s after shown
  3. 10:02:41Taught 4 other entries matched (UPI ID)
  4. 10:09:05Changed → Ganesh Enterprises · typed search (3 queries, 1 with no match) · step Ledgers (review) · change 2
  5. 10:09:20Opened in PDF Alt+O
  6. 10:11:52Exported as Ganesh Enterprises · file 1 of 2
Mockup

Is “% likely” honest?

C2 · first suggestions, last 30 days

Accepted in use% shown to user
upi · all
94%
narration · some
71%
similarName
58%
category · many
22%
recurring
49%
0%50%100%

The bar is the accepted share. The black mark is the % shown. In this example, category · many promises 41% but delivers 22%.

Show as a table
SituationShownAcceptedn
upi · all96%94%812
narration · some78%71%344
similarName62%58%290
category · many41%22%518
recurring45%49%96
Mockup

Where entries end up

C4 · 2,140 entries in 31 exported imports

Auto contra
6%
Exact match, kept
38%
Exact match, changed
2%
Suggestion accepted
27%
Suggestion changed
11%
Chosen by search
9%
Suspense, saved
4%
Suspense, no ledger
3%
0%50%100%
Show as a table
ResultEntriesShare
Auto contra1286%
Exact match, kept81338%
Exact match, changed432%
Suggestion accepted57827%
Suggestion changed23511%
Chosen by search1939%
Suspense, saved864%
Suspense, no ledger643%
Mockup

Parser scoreboard by recipe

B1 + B2 · all uploads in the last 30 days, failures included · build 9c41…

RecipeUploadsValidatedNeeds reviewFailedCodex sentMedian timeState
hdfc-savings v421298%2%0%01.4 s✓ Good
sbi-current v214791%6%3%22.1 s✓ Good
layout detection6371%19%10%183.8 s▲ Watch
codex learned (new)2162%24%14%2148 s▲ Watch
no recipe (failed)17——100%931 s✕ Missing

Today we could not build the “Failed” column or the “no recipe” row, because failures are not saved.

Mockup

One picker search session

A6 · entry #4182/52 · step Suspense · ended in suspense after 41 s

“ram”14 matches · none picked
“ramesh t”2 matches · none picked
“ramesh tex”0 matches
Escapeclosed without a choice
Suspense savedbulk, 6 entries

This shows the case you asked about: the user searched, found nothing, and left the entry in suspense. With text masked (§8), the report would show “3 chars → 7 chars → 10 chars” and keep the match counts.

7. Rules for the data

StorageAppend-only tables in our PostgreSQL, for example entry_events, parse_attempts and app_events. We never edit rows. Rows are scoped to the owner, like statements.
No copies of customer textEvents refer to the statement ID and the row number. The narration and the PDF are already saved, so the report joins them. We store no extra copies.
Miracle books stay on the PCFrom book-based steps we send only the ledger the user chose and the clue kind. We send no postings, GSTIN, dates, or counts from the company files (this is an existing project rule).
No secretsWe never record passwords, session tokens, or file paths.
No slowdownThe app queues events in memory and sends them in batches every few seconds and when a tab closes. A key press never waits for a send. A failed send drops the batch. The app does not retry forever.
Old appsThe server accepts requests without events or a version header. Old apps keep working, and the report shows them as “version unknown”.
SizeAbout 5–10 events per entry, of about 200 bytes each, so about 1–2 KB per entry. 100,000 entries need about 100–200 MB. This is small for PostgreSQL.
Where you read itProduction serves only the API, so we add no web pages there. Suggestion: a local command (like bun run matcheval) that reads the data and writes an HTML report into .local/.

8. Decisions for you

  1. Which items to build. Choose from A1–D8. My suggestion: all “Core” items first.
  2. Search text (A6). (a) Save the full query. This is best for finding missing ledgers, but it stores payee words a second time. (b) Save the length and match counts only. (c) Save the query only when it has no match.
  3. Retention. Keep events forever, or remove them after N months, with the statement, or with the party.
  4. Whose data. Production users only, or also local development. Mark or exclude your own test imports.
  5. Where you read the report. The local command (my suggestion), an admin-only screen in the desktop app, or later a tool such as Metabase.
  6. PR split. My suggestion is four small PRs:
    1. Tables and server-side capture: B1, B2, and A1–A5, A8, C1, C3–C6 from the server’s view, plus the D1 version header.
    2. App events: one batched endpoint, plus A2 method, A4 step, A6 search, A7, B3, B5, D2, and D3.
    3. The report command and HTML: the trail, calibration, funnel, and parser scoreboard.
    4. Later: D4 speed, D5 keyboard use, D6 export check, C8, C9.
  • Today we keep only each entry’s final ledger. Failures, history, searches, the step, and app versions are not recorded.
  • The largest gaps for completeness: parse failures (B1), real matcher precision and calibration (C1, C2), and the decision trail (A1–A8).
  • Everything stays in our PostgreSQL. Events point to existing rows and copy no customer text. Book data stays on the PC.
  • Your decisions: the items, the search text, retention, the report location, and the PR split.