Location: Google Drive
2075→Macbackup/Downloads/Projects/gl_demoCreated: 2026-07-31 · Status: MVP built & tested — Phases 1, 2, 4, 5 implemented on local SQLite + DeepSeek (mock data). See Development History and Test Results.📄 Companion docs:
PROJECT_BRIEF.md(vision/personas/roadmap) ·PLAN.md(Phase 1 design, implemented) ·CLAUDE.md(architecture, commands, security for agents)
Every accounting team lives in a fog of unstructured decisions. A purchase approval says "Feb consulting — Acme India" — which of 200+ accounts does it land on? Month-end means thousands of small classification calls made by humans, inconsistently, under deadline. The pain isn't data entry — it's judgment at scale, currently done by scarce senior accountants.
The idea: an AI agent that reads the messy, free-text reality of accounting (memos, descriptions, source documents) and turns it into correct, audit-ready double-entry journal entries, while answering GL questions, flagging anomalies, and driving the month-end close.
Deliberately NOT AP/AR: Accounts Payable/Receivable agents are document-workflow automation (commoditized). GL is where the judgment and controls live — classification, reconciliation, accruals, allocations, FX, intercompany, close, audit support.
Why GL: journal entries have a checkable right answer: a mapping either lands on the right account or it doesn't, and an entry either balances or it doesn't. So the demo keeps a labelled eval set (data/eval/mapping_golden.jsonl) and reports its mapping accuracy against it (see section 7).
The core act: free-text memo → correct account code + balanced journal entry, validated by a rules engine so the LLM can never post unchecked.
The guardrail (product principle, enforced end-to-end):
LLM proposes → rules engine validates → human approves → ledger posts
No code path lets a model write to the books unchecked.
| # | Capability | Status |
|---|---|---|
| 1 | GL mapping — memo → account code + confidence | ✅ built |
| 2 | JE proposal — free-text → balanced, validated JE | ✅ built |
| 3 | Validation engine — rules gate every proposal (balance, account exists, ISO date, min lines, single-sided) | ✅ built (closed-period + duplicate checks pending) |
| 4 | GL Q&A / analytics (ask the ledger in plain English) | ⏳ not built |
| 5 | Reconciliation assistant | ⏳ not built |
| 6 | Anomaly detection | 🟡 data flagged (438 anomalies found in the anonymized GL); no product surface yet |
| 7 | Close management | ⏳ not built |
| 8 | Policy RAG | ⏳ not built |
Real GL data arrives from many systems in many formats. The connector framework normalizes every source into one canonical schema (which mirrors the Australian SME dataset):
chart_of_accounts— code, name, type, sub_type, postablejournal_entries— id, entry_number, date, description, source, period, statusjournal_entry_lines— entry_id, account, debit, credit, description- plus
periods,trial_balanceviews; and append-only audit tablesingest_log,validation_failures,je_proposals,je_proposal_events
The pluggable seam: Connector ABC → TabularConnector → CSVConnector/JSONConnector. Each connector declares mapping = {canonical_field: source_column}. A future Tally/bank/REST source is a new connector + a mapping, never an edit to the pipeline. A new client of a known format is just a config mapping — no code.
Other inputs the system needs:
- Source files (CSV/JSON/beancount) on local disk
- A chart of accounts (the CoA defines the labels)
- The DeepSeek API key (
.env) for LLM classification/proposal - Human approval for anything that posts to the books
- A canonical SQLite store (
gl.db): CoA + journal entries + lines, idempotently loaded - Validation reports — every violation persisted to
validation_failures(immutable triage list) - Mapping results —
memo → account code + confidence + method(frommap_memo) - Proposed journal entries — balanced, rules-validated JEs, recorded to
je_proposals(never auto-posted) - Posted journal entries — only via explicit approval (
gl.approve), with full traceability (source_id= proposal id) and an audit event inje_proposal_events - Eval reports —
reports/mapping_eval_{full,offline}.txt,reports/je_eval_{full,offline}.txt
Now (MVP, running locally): SQLite 3.43 + bare Python stdlib + the DeepSeek API. Zero pip dependencies.
Production north-star (AWS): the design is port-ready from day one.
| MVP (now) | AWS production |
|---|---|
| SQLite 3.43 | RDS/Aurora PostgreSQL + pgvector (schema is Postgres-portable) |
| Local file reads | S3 (checksum idempotency = S3 ETag; never depend on paths/mtimes) |
| DeepSeek API | AWS Bedrock (the gl_core/llm.py provider seam is the swap point) |
| CLI scripts | Lambda / Step Functions (each connector = a stateless unit) |
.env / credentials/ |
AWS Secrets Manager / SSM Parameter Store |
| — | Snowflake = GL data store of record; on-prem SLM for data-residency-sensitive enterprises |
Enterprise hardening (documented in CLAUDE.md → Security): KMS encryption at rest, TLS in transit, IAM least-privilege, VPC with private subnets, S3 Object Lock for tamper-proof retention, CloudTrail, regional pinning for customer data.
All datasets are free / mock / public. No real client financials — DeepSeek is China-hosted, so only mock data ever goes to it.
| Dataset | Source | Files | What it is | Used for |
|---|---|---|---|---|
| Australian SME | Kaggle (Mindweave sample; full set is paid on Gumroad) | kaggle/australian-sme-business-dataset/ (26 tables) |
Day-by-day retail SME bookkeeping simulation — 56-account CoA, 200 JEs / 500 lines in the sample (full: 7,407 / 18,623) | Primary training/eval — defines the canonical schema |
| Anonymized GL | Kaggle (CC0) | kaggle/anonymized-general-ledger-dataset/GL_Dataset.csv (21,551 rows) |
Real-world account-level ledger with anomalies (Benford) | Real-data stress test; anomaly detection |
| US Standard GL | Kaggle (MIT) | kaggle/us-standard-general-ledger/ledger.csv (241 Q&A) + definitions PDF |
USSGL account Q&A pairs | RAG/eval for US federal accounting |
| GL financial | Kaggle | kaggle/generalledger/Data file for students.xlsx |
Classic student GL | Reference (deferred — needs openpyxl) |
| Receiver General | Kaggle | kaggle/receiver-general-accounting-transactions/tcrg-rgat.csv (9,282 rows) |
Canadian federal GL journal vouchers, EN/FR headers | Real government GL, international flavor |
| Beancount | GitHub (MIT) | beancount/*.beancount (4 ledgers, 8,312 lines) |
Realistic plain-text double-entry ledgers | Memo/description texture |
| SEC EDGAR | sec.gov (public domain) | sec/company_tickers.json (10,412 cos) + companyfacts_*.json |
Public-company XBRL financials | Analytics QA, not JE-level |
The key missing piece (being built): a large labeled (free-text memo → account) eval set. Currently 740 rows in data/eval/mapping_golden.jsonl, grown from 52 via the augment_golden.py generator.
Downloaded and verified the datasets above into data/, wrote PROJECT_BRIEF.md and PLAN.md. No code.
Built: gl_core/models.py (dataclasses + coercers to_amount/to_bool/to_int), schema.py (DDL + 3 views), connectors.py (Connector/Tabular/CSV/JSON with the mapping seam), validation.py (rules R1 balance · R2 account_exists · R3 date_iso · R4 min_lines · R5 single_sided), pipeline.py (run_ingest: load → validate → one atomic transaction, INSERT OR IGNORE = hard idempotency). CLIs scripts/ingest_sme.py + scripts/verify.py. .gitignore created, git init.
Tested:
- Ingest of the SME sample: 56 CoA / 200 JEs / 500 lines, 756 records, all inserted
- Idempotency: a re-run inserts 0 new rows (checksum-keyed, stable UUID PKs)
- 11 of the 200 JEs are orphaned (0 lines) — expected; flagged by R4, still loaded
verify.py— ALL 9 CHECKS PASSED:PRAGMA integrity_check=ok, counts 56/200/500, 0 unbalanced JEs, 0 orphan lines, Σdebit = Σcredit = 711,379.29 (diff 0.00), trial-balance view (top-10 by |net| sensible: opening equity −200k, sales −173k, COGS +37.5k, salaries +124.7k), sample queries, audit tables present- Coercers proven on the messy anonymized-GL shapes (comma groups
2,380,000,(1,200.00)negatives,cr/dr,$,—→ all handled)
Built: gl_core/mapper.py (normalize, lexical_score, classify_with_llm, map_memo — LLM proposes, rules gate: account exists + postable), gl_core/providers/deepseek.py (first real provider on the llm.py seam, pure urllib), scripts/map_memo.py, scripts/eval_mapping.py, scripts/build_mapping_golden.py.
Tested (52-row eval set):
- Offline lexical baseline: 51.9%
- Full stack (DeepSeek + lexical + rules): 92.3% top-1
- After accounting-principles prompt guidance + a corrected label: 98.1% (asset/equity/liability/revenue 100%, expense 96.8%)
- The remaining miss was a derived-label artifact ("Supplier invoice received" → data says COGS, model says AP — a defensible answer)
Built per-mode reports (mapping_eval_{full,offline}.txt), added --workers parallelism, grew the eval set (see Eval set growth below).
Built: gl_core/proposer.py (propose_je: LLM proposes lines → deterministic balance plug → R1–R5 validate), scripts/propose_je.py (records to je_proposals, never posts), scripts/build_je_golden.py + scripts/eval_je.py.
The balance guarantee: a pure arithmetic correction computes Σdebit − Σcredit and plugs the residual into the correct side (or appends a bank leg). 100% balance is guaranteed by construction — never subject to model drift. (An early plug_to-hint design let the model override the arithmetic and dropped balance to 92%; the hint was removed, restoring the guarantee.)
Tested (25-row JE eval):
- Balance rate 25/25 = 100% (the Phase 4 DoD)
- Amounts 26/26 = 100% on hand-authored exact-amount JEs (payroll, GST sales, loan repayment, depreciation…)
- Structural match 80.3% overall; hand-authored free-text JEs 100% on both structure and amounts (templated derived memos without amounts score lower — a labeling-signal limitation, not a defect)
Built: gl_core/mcp_server.py (minimal pure-stdlib stdio MCP server, newline-delimited JSON-RPC 2.0), gl_core/posting.py (maker-checker: post_proposal re-validates R1–R5 from the stored payload, blocks double-posts and rejected proposals, one transaction, audit-logged), je_proposal_events audit table, scripts/mcp_server.py launcher.
Tested:
- Unit + subprocess round-trip:
initialize→tools/list(4 tools) →gl.propose_je→gl.validate→gl.query→gl.approve→ posted JE lands withsource_id= proposal id and anapprovedevent; unknown tool → JSON-RPC error - Live smoke (real LLM): "Paid vendor 50,000 for annual software license" → proposal 7 → approved → posted JE
Dr 6-3200 50,000 / Cr 1-1000 50,000, Σdebit = Σcredit = 50,000, diff 0.00, clean protocol stream
Fixed: stale docs (CLAUDE.md/README/PLAN said "no code exists"), zero tests → 67-test suite, raw-traceback empty-db UX → friendly "run ingest_sme.py first", eval-report overwrites → per-mode files, no LLM retry → 3× retry with backoff, deterministic CoA ordering, je_proposals verify check.
Built: scripts/augment_golden.py — grows mapping_golden.jsonl with label-safe free-text memos (slot-filled templates from the real SME products/suppliers/months + LLM paraphrases of the templated seeds).
An eval-hygiene bug caught: the first LLM paraphrase pass leaked the answer into the memo ("Monthly depreciation expense on store shelving - account 6-2800"). Fixed with a code-leak filter (drops any memo containing its own account code) + an anti-leak prompt instruction. Final set: 740 rows, 0 code-leaks (was 52).
Re-tested on the 740-row set:
- Offline lexical baseline: 48.1% (harder set — expected drop from 51.9%)
- Full stack: 97.4% top-1 (revenue/asset/equity 100%, expense 98.7%, liability 76.4%) — the mapper generalizes to 14× more varied free text without collapsing
- The liability dip is mostly a template-design miss ("GST on {product} sale" → model reads "sale" as revenue; GST-on-sale belongs to GST Payable — a real, minor product gap)
Built: gl_core/anonymized.py (connector set for the 21,551-row Kaggle anonymized GL: dd-mm-yy date normalization, comma-grouped amounts, space-y headers, duplicate-line dedup) + scripts/ingest_anonymized.py.
A real bug found by "see how it goes": the first design made one JE per row → 21,551 false single-sided "JEs". The data is actually double-entry when grouped by transaction (Date + Transaction ID). After the fix:
- 5,765 real transactions (from 21,551 rows → 18,703 distinct lines), 102 accounts
- R1 flags 438 transactions as unbalanced (~7.6%) — the genuine anomalies this dataset exists for (e.g.
OT00359: debits 73,402.00 vs credits 73,401.00 — a $1 discrepancy) - The canonical
gl.dbwas left untouched
Unit tests — python3 -m unittest discover -s tests → 67/67 PASS (stdlib, no pip):
test_coercers— every parsing quirk (comma groups, parens, cr/dr,$,—, None/empty)test_validation— R1–R5 on crafted balanced/unbalanced/unknown-account/bad-date/1-line/double-sided casestest_connectors— CSV + JSON (array + dict-of-columns), themappingrename seam, empty files, checksumstest_pipeline— ingest counts, idempotency (2nd run = 0 inserted), violation persistence, atomic rollback on errortest_mapper— normalize, lexical ranking, offline fallback, unknown-code rejection, provider-failure degradationtest_proposer— the invariant: adversarial LLM output (single-line, all-debit, wrong-side imbalance) still yieldsbalanced=Trueafter the plug; audit recording is append-onlytest_parsing— fenced/stray/missing-field JSON from the LLMtest_posting— post lands balanced with traceability; double-post blocked; invalid proposal refused; rejected-proposal post blocked; proposal row immutabletest_mcp_server— full subprocess stdio round-trip (prove "any MCP client can drive it")test_anonymized— transaction grouping, messy coercions, duplicate-line deduptest_augment_golden— leak filter + slot-fill volume
scripts/verify.py — 9 DB assertions (integrity, counts, every JE balances, no orphan lines, global balance, orphans flagged, trial balance, sample queries, audit tables).
Eval harnesses — measure and gate the DoD (summary in §7; current numbers in reports/).
All commands run from the repo root; pure stdlib, no install. Full list in CLAUDE.md → Commands. Key ones:
python3 scripts/ingest_sme.py # load the SME sample into gl.db
python3 scripts/verify.py # assert the store is sound (exit 0/1)
python3 scripts/map_memo.py "Feb consulting — Acme" # memo → account
python3 scripts/propose_je.py "Paid vendor 50k for software license" # propose + record (never posts)
python3 scripts/eval_mapping.py [--offline] [--workers N] # mapping accuracy
python3 scripts/eval_je.py # JE balance rate (DoD 100%)
python3 scripts/build_mapping_golden.py && python3 scripts/augment_golden.py # regenerate/grow the eval set
python3 scripts/mcp_server.py --db gl.db # the MCP server (stdio JSON-RPC)
python3 -m unittest discover -s tests # the full test suiteMaintenance routine: after any change, run the test suite + the two evals; regenerate goldens only when the data changes. Register the MCP server in a client via:
{"mcpServers": {"gl": {"command": "python3", "args": ["scripts/mcp_server.py", "--db", "gl.db"]}}}- Python 3.14.6 (Homebrew) — deliberately zero-pip (
csv,json,sqlite3,dataclasses,urllib,unittest) - SQLite 3.43 — the dev store (Postgres-portable by design)
LLM — DeepSeek (OpenAI-compatible chat API). Set DEEPSEEK_API_KEY in your environment or a local .env file (gitignored — never commit it). The provider is a swappable seam (gl_core/llm.py), so the endpoint can change without touching any callers.
Compute (future) — no GPU compute has been used yet. Reaching the SLM fine-tuning phase (Phase 6) will require renting or purchasing GPU compute (e.g. a cloud GPU provider).
- The guardrail — LLM proposes → rules validate → human approves → ledger posts;
gl.approveis the only book-writing tool and it re-validates + audits. - Append-only audit trail —
ingest_log,validation_failures,je_proposals,je_proposal_eventsare never UPDATE/DELETEd; violations are evidence, not errors. - Mock data only to DeepSeek (China-hosted). Real client financials → Bedrock/on-prem only (the north-star).
- AWS hardening (production) — KMS at rest, TLS in transit, IAM least-privilege, private-subnet VPC, S3 versioning + Object Lock, CloudTrail, regional pinning.
- Secrets never enter the repo —
.env*,credentials/, and all*.dbare gitignored (see.gitignore). Never commit API keys or credentials; if one leaks, rotate it.
- Capability #4 — GL Q&A / analytics (NL→SQL over the store) and capability #6 — anomaly detection as a product surface (the 438 anomalies are already findable)
- Phase 3 formal CI — gate accuracy across prompt changes
- More connectors / data — Receiver-General, beancount parser, fresh Kaggle GL datasets; grow the eval set toward 2,000+
- Phase 6 — SLM tier — fine-tune a small classifier on the GL corpus (requires renting/purchasing GPU compute)
- Phase 7 — Enterprise — Bedrock + Snowflake + on-prem deployment