EKOS · LedgerSMB · ClickHouse · dbt

From an open-source ERP to an audited analytical layer — with evidence at every step

LedgerSMB (Perl + PostgreSQL) migrated to ClickHouse by EKOS Migrate, modelled in dbt, orchestrated in Python, and reconciled to the cent against LedgerSMB's own reports. Every AI interaction is logged, with tokens per model.

30/30
tables pass V1+V2+V3
6/6
checks vs LedgerSMB's own reports
162/162
dbt models + tests
16
EKOS defects found & fixed
Synthetic data: Harbor Mill Supply Co. is simulated, posted through LedgerSMB's own procedures.

← → / space to navigate · O overview · T theme

Agenda

What this deck covers

1 · Context

The goal, the source system, the synthetic company, the tools and the rules of evidence.

2 · Understanding the source

Building LedgerSMB faithfully, generating data through its own procedures, EKOS knowledge and discovery.

3 · The migration

EKOS Migrate stage by stage: drift, PII, findings, type mapping, risk, approvals, load, validation.

4 · The analytical layer

dbt layers and models, conventions, EKOS-sourced documentation.

5 · The reports

P&L, balance sheet, cash, working capital, aging, products, customers, inventory, suppliers.

6 · Trust & accounting for AI

Four levels of data quality, the independent oracle, EKOS vs Claude Code vs other LLMs, tokens, findings.

1 · Context

The goal

Take a real ERP's database, move it into an analytical engine, and build reporting on it, so that every step can be checked rather than trusted.

Requirements

  • Use the real LedgerSMB sources: schema, stored procedures, chart of accounts
  • ClickHouse target, dbt + Python pipelines
  • Finance reporting and product/materials reporting
  • Data-quality tests, documentation (dbt, Python, Confluence)
  • Report where EKOS, Claude Code and other LLMs were used, with tokens per model
  • Log every query and answer

Rules of evidence

  • Every number comes from a log or a query — this deck is generated from them
  • Every check has been shown to fail on a planted defect
  • A step that did not work says so
  • Claude Code tokens are estimates (context counter); model tokens are provider-reported
1 · Context

The source: LedgerSMB

An open-source double-entry accounting ERP. Most of its business logic lives in the database, in PL/pgSQL.

168
tables (after all changes)
504
functions / procedures
175
schema-change scripts
52
SQL modules

What matters for analytics

AreaTables
General ledgertransactions · acc_trans (journal lines) · gl · account · account_heading · account_link
Receivables / payablesar · ap · open_item · payment · entity_credit_account · company
Materialsparts · partsgroup · invoice (lines) · inventory_report[_line]
Ordersoe · orderitems · oe_class
1 · Context

The company: Harbor Mill Supply Co. (synthetic)

LedgerSMB ships no data, so 18 months (2025-01 → 2026-06) of a fictional industrial-components distributor were simulated. Deterministic: the same seed produces the same books.

60
customers, 4 segments
25
vendors
114
parts in 8 groups (6 services)
$3,142,871
revenue over 18 months

Realism built in

The first generated company ended $1.34M overdrawn: over-stocking with no financing. Credible to a computer, not to a finance reader. Reorder quantities were tightened and a revolving facility added (decision D8).

1 · Context

Who did what

ActorRole
EKOS — compiled knowledgeCompiled the LedgerSMB repository (SQL, Perl, JS, docs, git) into a queryable, evidence-backed model; answered the design questions; supplied source documentation
EKOS Migrate (RFC 0154–0162)The whole PostgreSQL → ClickHouse move: discover, drift, profile, assess, map, risk + approval, load, validate, report
Claude Code (Opus 5.5)All engineering: loader, generator, orchestration, dbt project, pipelines, docs, deck; found and fixed 16 EKOS defects
DeepSeek V4 Flash (cloud)EKOS's configured LLM: semantic naming during compile; ekos ask answers
Llama 3 8B (local, Ollama)The same questions, answered locally, for comparison
dbt + ClickHouse38 models, 124 tests; the analytical engine

All LLM traffic goes through a logging proxy (tools/llm_proxy.py): prompts, answers and provider-reported tokens in logs/llm_calls.jsonl.

1 · Context

Architecture

LedgerSMB repo→EKOS compile (11,084 objects)
PostgreSQL 16 · ledgersmb→ catalog, profiles, workload →EKOS Migrate→ INSERT … SELECT FROM postgresql(named collection) →ClickHouse lsmb_raw (31)
lsmb_raw→ dbt build + 124 tests →lsmb_analytics (38)→ reconcile →LedgerSMB's own reports

No credentials in statements

ClickHouse reads PostgreSQL through a server-side named collection; generated SQL only names it.

No credentials in the ledger

Config names environment variables; a DSN carrying a password is refused.

Sandboxes bound to 127.0.0.1

Docker Compose from the EKOS repo, local-only passwords.

1 · Context

The pipeline, end to end: 352 seconds

One command, from an empty database: python -m lsmb_pipelines.run. Each step must succeed before the next; a failing data-quality test stops the run.

Source: logs/pipeline_runs.jsonl (last run)

2 · Understanding the source

Building LedgerSMB faithfully: 0 failed scripts

The schema is built from LedgerSMB's own sql/ tree, in LedgerSMB's order. The first attempt failed on 8 files; each failure was a way the loader differed from LedgerSMB's installer.

FailureCauseFix (= what LedgerSMB does)
drop_arap_cols.sql: column does not existListed twice in LOADORDERSkip content already applied (LedgerSMB's db_patches hash)
Roles.sql: syntax error at ":"psql variable lsmb_schema unsetPass -v lsmb_schema=public
no-inv-entity-tables.sql + 4 cascadesTEMP … ON COMMIT DROP under autocommitOne transaction per change
Duplicates_Functions.sql\copy of a relative pathRun from the LedgerSMB root
entity_credit_account: language FKSeed data never loadedLoad initial-data.xml after the base schema, before changes

Result: 1 base + 175 changes + 52 modules + seed, 0 failures (logs/schema_load.tsv).

2 · Understanding the source

Data posted through LedgerSMB's own logic

Not an imitation of LedgerSMB — LedgerSMB itself. The generator calls its procedures wherever one exists.

Procedures used

  • account_heading_save, account__save — the real US chart (66 accounts)
  • company__save, eca__save, eca__location_save — 85 counterparties
  • cogs__add_for_ar_line — FIFO COGS, posts Dr COGS / Cr Inventory itself
  • cogs__add_for_ap_line — purchase allocation
  • payment_post — receipts and payments settle open items

Verified before scaling up

  • 0 unbalanced transactions
  • COGS posted on 100% of goods lines
  • Inventory GL = Σ on-hand × cost, to the cent
  • Open AR on the control account = the generator's own count of unpaid invoices
  • 8 of 16 guessed account numbers were wrong (2310 is a 401K accrual, not sales tax): every account is now checked against the loaded chart
2 · Understanding the source

EKOS compiles the LedgerSMB repository

ekos build → recover → resolve → compile → commit over the whole checkout: SQL, Perl, JavaScript, Markdown, git history.

11,084
objects compiled
9,228
relationships
185
tables recovered from sql/
2,511
Perl packages + symbols (this compile)

What it surfaced

2 · Understanding the source

Discovery: asking EKOS before designing

305 structural queries through EKOS's MCP tools (0 errors), every one logged with its purpose and full answer.

QuestionEKOS toolAnswer
Where are the finance/materials tables?ekos_ekl24 of 24 located, 185 in total
What are the columns of acc_trans, and what do they mean?ekos_stateColumns + COMMENT ON text, cited to sql/Pg-database.sql:1142
What joins to acc_trans?ekos_neighborhoodFKs and referencing code
What depends on parts?ekos_dependents / ekos_impact27 compiled dependents (becomes the risk gate)
Where is COGS / trial balance / open item logic?ekos_searchCOGS feature tests, xt/42-cogs-fifo.pg, modules

EKOS also supplied the dbt source documentation: 46 of 303 deployed columns carry LedgerSMB's own comments, cited to file and line.

2 · Understanding the source

ekos ask: cloud vs local model

The same six questions, answered by EKOS's grounded Q&A on DeepSeek V4 Flash (cloud) and Llama 3 8B (local). Answers shortened.

QuestionCloudLocal
How does LedgerSMB compute the cost of goods sold for a sales invoice line?LedgerSMB computes cost of goods sold using the FIFO (First In, First Out) method: the first inventory in the system that is unallocated is the first inventory used for costs. This behavior is described in connection with the `LedLedgerSMB computes the cost of goods sold for a sales invoice line by considering the inventory activity screens, parts groups, price groups, and FIFO or First In, First Out method.
Which table stores general ledger journal lines, and how are debits and credits represented?The table is `journal_line` (also referenced as `public.journal_line`); it replaces `acc_trans` as the main account transaction line table. The evidence does not describe how debits and credits are represented in that table.The table that stores general ledger journal lines is `public.journal_line`. Debits and credits are represented in this table.
What is an open item in LedgerSMB and how are AR invoices settled by payments?An open item in LedgerSMB is a tracking mechanism for items to be cleared or handled in subsequent transactions, as described in the open-item-tracking SQL change. The evidence does not describe how AR invoices are settled by payAn open item in LedgerSMB is an item that has not been cleared or settled, allowing for tracking of items to be handled in subsequent transactions. AR invoices are settled by payments through the LedgerSMB::Report::Invoices::Paym
What does the inventory_report table record and how is a count variance posted?Insufficient evidence. I looked for claims describing what the inventory_report table records and how a count variance is posted, but the available claims only contain partial term matches and no descriptive content about either.The inventory_report table records the variance between budgeted and actual amounts for a given period, as well as other relevant information such as report dates. A count variance is posted by calculating the difference between
How does LedgerSMB distinguish customers from vendors in entity_credit_account?LedgerSMB's `entity_credit_account` table stores the general relationship for moneys owed on invoices, covering both AR and AP sides, and the claims show it has a column named `public.entity_credit_account.entity_class`. That coluLedgerSMB distinguishes customers from vendors in the entity_credit_account by using the entity_class attribute, which specifies whether the credit account is for a customer or vendor.
Which database objects depend on the parts table?Insufficient evidence. I looked for any claim naming database objects or a dependency on a "parts table"; the provided claims only describe a Perl subroutine named `database` in `xt/lib/PageObject/Setup/CredsSection.pm`.The parts table depends on the database object.

Verdict: cautious. It refused ("insufficient evidence") rather than invented, which is the right failure mode. But it was thin: structural MCP tools answered the same questions better, and every rule the model relies on was confirmed in LedgerSMB's source.

2 · Understanding the source

The rules the analytics rests on — each with its source

RuleEstablished by
acc_trans.amount_bc: negative = debit, positive = creditLedgerSMB sql/modules/trial_balance.sql
COGS is FIFO, posted by the database, lines tagged with the sales-invoice lineEKOS ask + COGS.sql + measured (9,322/9,322 tagged)
Invoice qty: positive on sales, negative on purchasesCOGS.sql
AR/AP lines carry open_item_id; payments settle open itemstrigger_open_item_maintenance, payment_post
Customer vs vendor = entity_class 2 / 1EKOS ask (column) + measured
Ids are identity columns since 1.12measured + changes/1.12/migrate_to_identity.sql
tax.validto = infinity means "no end date"measured + EKOS finding
3 · The migration

EKOS Migrate: the stages

init→discover→profile→assess→map→review / approve→load→validate→report

A migration proof system, not a SQL converter. Every scope decision, finding, mapping, approval and validation result is a ledger fact with provenance, and the report cites those facts or does not ship.

Measured, not guessed

Types from profiled data; findings with the SQL that measured them.

Human-only decisions

Approvals cannot be made through MCP; the requester cannot approve their own request.

Validation that can fail

Three tiers plus planted controls; a green without a fired control proves nothing.

3 · The migration

discover: the live catalog

168
migration units
1,205
columns
298
foreign keys
503
functions
KindCountKindCount
primary keys158indexes62
unique constraints66sequences86
check constraints72triggers29
views16extensions2

The first attempt died on unknown pg_constraint.contype "t": LedgerSMB uses a constraint trigger that EKOS's fixtures never had. Fixed (defect #1).

3 · The migration

Drift: is the repository what is deployed?

EKOS compares its compiled view of the repository with the live catalog. 316 findings, 298 of which change the migration.

246
columns live-only
51
columns repo-only
18
tables repo-only
1
table live-only
public.acc_trans.amount — in the repository's DDL but not deployed
public.acc_trans.amount_bc — deployed but absent from the repository's DDL

Predicted during discovery (EKOS's ekos_state listed amount) and confirmed here. Part of the drift is real history. Part is an EKOS limitation: it compiles the base CREATE TABLE but does not replay the 175 ALTER scripts (open finding).

3 · The migration

profile: statistics, not values — and PII suppressed

P0 / P1 / P2 tiers

P0 catalog only · P1 bounded sample · P2 exact and budgeted. The demo profiles at P1.

28 columns classified as personal data: no bounds and no top-k are ever recorded for them, at any tier.

Two lessons from real data

  • ANALYZE first. P1 estimates come from planner statistics; a freshly loaded database reported tax as 0 rows.
  • Sampling must be reproducible. An unseeded TABLESAMPLE made the DDL differ run to run, and approvals pinned to its hash could never match (defect #8). Now seeded, and small tables are read whole.
3 · The migration

assess: 674 findings, 276 blocking

Data-quality and ClickHouse-compatibility rules, each measured against real rows with the SQL kept as evidence.

FindingWhat it means
BLOCK COMPAT.CH.INFINITE_TIMESTAMP — tax.validto, 1 rowPostgreSQL infinity, which no ClickHouse type holds. Predicted here; the load later failed on exactly that row.
BLOCK COMPAT.CH.NO_CONSTRAINT_ENFORCEMENTClickHouse will not enforce PKs/FKs: duplicates would accumulate silently
BLOCK COMPAT.CH.NUMERIC_UNCONSTRAINED — amount_bcUnconstrained numeric; the measured scale is 22 (LedgerSMB's payment_post stores unrounded products)
noise DQ.UNIQ.001 on acc_trans.trans_idFlags FK columns as "looks like a key": open EKOS finding
28 inferred-FK candidatesFrom real code joins + pg_stat_statements; wrong attributions stayed "not measurable", never claimed
3 · The migration

map: ClickHouse DDL that explains itself

-- source: public.account
-- engine: no updates or deletes recorded against the source, so the table is append-only
-- order by: no query-shape evidence available for this table, so this is the primary key
-- codecs chosen from the profile, NOT emitted: the RFC 0160 classifier cannot parse a CODEC clause
CREATE TABLE `lsmb_raw`.`acc_trans` (
    `trans_id` Int32, `chart_id` Int32, `transdate` Date32,
    `source` LowCardinality(Nullable(String)),   -- was Nullable(LowCardinality(…)): rejected (defect #7)
    `amount_bc` Decimal128(22),                    -- scale measured, not guessed
    `amount_tc` Decimal64(2), `curr` String, `open_item_id` Nullable(Int32), …
) ENGINE = MergeTree ORDER BY (`entry_id`);

DDL for 158 tables: clickhouse/ddl/ekos_generated_raw.sql.

3 · The migration

Risk and human approval: 8 units gated, self-approval refused 8/8

EKOS computes risk from statement class × environment × lossiness × blast radius (compiled dependents) × rows. Above R1 a load needs an approval pinned to the artifact hashes and an evidence snapshot.

UnitComputedRequester tried to approveLoad
accountR3refused10.96 s
acc_transR3refused11.17 s
transactionsR3refused11.03 s
partsR3refused10.84 s
entityR3refused10.84 s
entity_credit_accountR3refused10.87 s
countryR3refused10.85 s
locationR3refused10.82 s

Three governance defects were found here and fixed: #4 the requester could approve their own request by typing a prefixed name; #15 blast radius matched tables by suffix, so review said R1 while load said R3; #16 load accepted an approval granted at a lower class.

3 · The migration

load: 30 tables into lsmb_raw

Every statement is parsed and classified before it runs; unparseable, unknown or credential-carrying statements are refused.

INSERT INTO `lsmb_raw`.`acc_trans`
SELECT * FROM postgresql(lsmb_source, schema = 'public', table = 'acc_trans')
WHERE `entry_id` >= 1 AND `entry_id` < 100001

Integer keys

Bounded, half-open key-range chunks.

Text / no key

One whole-table statement, stated as unbounded. account is keyed by text, which broke the old planner (defect #2).

tax

Loaded by Python with the recorded decision infinity → NULL; reconciled 1/1.

3 · The migration

validate: 30/30 pass all three tiers

TierComparesResult
V1row counts30/30
V2per column: nulls, min, max, total byte length of the canonical form30/30
V3engine-independent row hashes, summed per bucket30/30

Planted control: one cent

+0.01 on one journal line, in ClickHouse only → V1 passed, V2 passed (as designed: neither can see it), V3 failed on bucket 43, exit 1. Reverted → all pass.

Before defect #9 was fixed, unconstrained numeric was hashed at scale 0: V3 could not see cents at all, and "passed". The planted control is how you know a green is real.

3 · The migration

Five ways the validator disagreed with itself

Each one only shows up on real data: non-ASCII text, empty tables, all-NULL columns, unconstrained numerics.

#PostgreSQL sideClickHouse sideFix
9to_char(604.74, scale 0) → 605truncates → 604scale from the deployed type; never 0
10NULL bool → 'f'NULL → \Ndecide NULL on the column, both sides
11length() = characterslength() = bytesoctet_length
12empty min/max = NULLtype default ''/0aggregate_functions_null_for_empty
13SQL NULL → ''raw TSV NULL = sentinel bytesformat_tsv_null_representation

Verified against real PostgreSQL 16 and ClickHouse 24.8 with EKOS's live three-way tests (12 pass).

3 · The migration

report: compiled, cited — and "Not signable"

The migration report is compiled from 2,217 ledger facts with groundedness 1.000. It refuses sign-off, and it is right to.

BlockerWhy
Units not validated in the state machineThe lifecycle needs assessed → … → validated; units with blocking findings cannot advance until dispositions exist, and load/validate swallow the illegal-transition error (open EKOS finding)
Unexplained divergencesNo approved dispositions yet (the disposition workflow is not built)
Planted control missedThe one-cent control was run by hand, not through ekos migrate validate

The data is proven elsewhere (V1–V3, the planted control, the reconciliation). The report correctly refuses to sign what its own workflow has not recorded.

4 · The analytical layer

dbt on ClickHouse: 38 models in three layers

lsmb_raw (31)→staging · 12 views→intermediate · 4 tables→marts · 22 tables

staging

1:1 with sources: renamed, typed, cents-exact; debit/credit split from LedgerSMB's signed amount so nothing downstream re-derives it.

intermediate

Journal lines + accounts + natural sign; sales lines + exact FIFO COGS (via the invoice-line tag); open-item balances as of a date.

marts

Facts, dimensions and report-shaped marts for finance (11) and materials (11).

Session join_use_nulls = 1 gives standard outer joins; account numbers are dbt vars; aliases never shadow aggregated columns (a ClickHouse trap hit twice).

4 · The analytical layer

The marts

Finance

  • fct_journal_lines, dim_account, dim_counterparty, dim_date
  • mart_trial_balance_monthly: account × month, zero rows included
  • mart_income_statement_monthly, mart_balance_sheet_monthly
  • mart_ar_aging, mart_ap_aging
  • mart_cash_flow_monthly (direct method), mart_working_capital_monthly

Product & materials

  • fct_sales_lines, fct_purchase_lines, dim_part
  • mart_sales_by_part_group_monthly, mart_customer_profitability
  • mart_inventory_movements_monthly, mart_inventory_position
  • mart_stock_count_variance, mart_order_backlog
  • mart_abc_analysis, mart_supplier_spend_monthly
4 · The analytical layer

Documentation that cites its source

dbt source docs are generated from EKOS's compiled knowledge: LedgerSMB's own COMMENT ON text, with file and line.

- name: acc_trans
  description: "This table stores line items for financial transactions. Please note that payments
    in 1.3 are not full-fledged transactions. [EKOS: defined in sql/Pg-database.sql]"
  columns:
    - name: "source"
      description: "Document Source identifier for individual line items, usually used for
        payments. [EKOS: COMMENT ON COLUMN] ClickHouse type LowCardinality(Nullable(String))."
31
sources documented
46 / 303
columns with LedgerSMB's own text
257
say "no description" rather than invent one
5 · The reports

18 months at a glance

$3,142,871
revenue
41.00%
gross margin (FIFO)
$176,756
net income
5.00%
net margin
$1,422,079
total assets at 2026-06-30
$373,670
open receivables
$460,634
inventory value
6
open sales orders

Source: logs/kpis.json — each figure is a query on the marts (queries in docs/confluence/05-kpis.md)

5 · The reports

Monthly profit & loss

Revenue and FIFO cost of goods sold by month, net income as the line. Seasonality and growth are visible, and so is Q1.

mart_income_statement_monthly

5 · The reports

Margins by month

5 · The reports

Balance sheet — and it balances every month

Assets = liabilities + equity at every month-end: a dbt test, and independently LedgerSMB's report__balance_sheet agrees account by account.

No year-end close in the ledger, so earnings to date are an explicit equity line (as LedgerSMB's own report does).

5 · The reports

Cash flow (direct method)

Classified by the other side of each bank transaction. Financing includes share capital and the revolving facility.

5 · The reports

Working capital: DSO, DPO, DIO

5 · The reports

Receivables aging at 2026-06-30

Most overdue customers

The subledger ties to the AR control account (dbt test) and to the open-item ledger in PostgreSQL (371/371 items, exact).

5 · The reports

Product groups: revenue and margin

Revenue

Gross margin %

Services carry no COGS (100% margin by construction); raw materials run thin; hydraulics is the volume business.

5 · The reports

Customers: who pays, and how fast

5 · The reports

ABC classification (trailing 12 months)

A = the parts making up the first 80% of revenue. A handful of parts carry the business, a standard Pareto shape.

5 · The reports

Inventory position and reorder alerts

Below reorder point

Movements (receipts − sales + count adjustments) reproduce LedgerSMB's on-hand for every part (dbt test).

5 · The reports

Shrinkage from physical counts

Variance value ties to the inventory-adjustment postings (dbt test).

5 · The reports

Order backlog and suppliers

Open sales orders

Top suppliers by spend

6 · Trust

Four levels of data quality — all passing, each proven able to fail

On the real data every check passes. To prove the checks are real, a defect was planted at each level, the check was shown to catch it, and the defect was removed: a check that has only ever passed proves nothing.

LevelWhatOn the real dataPlanted defect → caughtAfter removing it
MigrationEKOS V1 / V2 / V330/30 pass+0.01 on one journal line, ClickHouse only → V3 failed (bucket 43, exit 1); V1/V2 blind by designpass
Structuredbt generic: unique, not-null, relationships, accepted values, non-negative, row count vs source114/114 passaccount 1200 duplicated in lsmb_raw.account → 2 uniqueness tests failed, 70 downstream nodes skipped162/162
Accounting10 invariants: balances, trial balance, balance sheet, subledgers, revenue, COGS, inventory, shrinkage10/10 pass+10.00 on a sales line → 2 invariants failed, 24 downstream skipped162/162
Oraclemarts vs LedgerSMB's own reports in PostgreSQL6/6 exactstale target (PostgreSQL regenerated, ClickHouse not reloaded) → 6/6 failed6/6

The duplicate was removed with OPTIMIZE … FINAL DEDUPLICATE (drops only identical rows) and the table re-validated V1–V3 against PostgreSQL, so the revert is exact. Logs: logs/dbt_planted_defect*.log, logs/negative_checks/.

6 · Trust

The independent oracle: LedgerSMB agrees, to the cent

dbt tests check the model against itself. This checks it against LedgerSMB's own reporting functions, run in PostgreSQL.

CheckLedgerSMB oracleComparedResult
trial balancetrial_balance__generate()66exact
income statement (accrual)pnl__income_statement_accrual()10exact
balance sheetreport__balance_sheet(…, 'ultimo')19exact
AR open itemsacc_trans on the AR control account371exact
stock on handparts.onhand108exact
raw row countscount(*) per table31exact

Two conventions had to be matched rather than "fixed": heading subtotal rows in the statements, and raw ledger sign on the balance sheet. LedgerSMB's own aging reports fail on this 1.14 development schema, an upstream finding.

6 · Accounting for AI

Tokens by model (provider-reported)

ModelCallsSucceededInputOutputTotal
qwen2.5:1.5b (ollama)1135338
deepseek-v4-flash (openai)632821,688102,117123,805
llama3:latest (ollama)667,2876707,957

The first 32 cloud calls failed with HTTP 403 (the proxy sent a Python-urllib User-Agent) and consumed no tokens. DeepSeek's hidden reasoning tokens count as output.

6 · Accounting for AI

Claude Code: 499,877 tokens of context (estimate)

Measured from the session's context counter at each phase. It is context consumed in the conversation, not a billing figure, which the session does not expose.

PhaseWhatRemaining
startcounter when the demo request arrived (session context budget; an ESTIMATE of Claude Code usage, not a billing figure)15,000,000
0-scaffoldplan + repo scaffold + environment discovery14,985,472
2-ekos-rebuildschema loader (4 fixes) + EKOS config/proxy + LedgerSMB rebuild + schema/procedure research14,919,734
3-datadiscovery + generator (5 fixes found by checking against LedgerSMB) + integrity verification14,868,014
5-validatedmigration: 9 EKOS defects found+fixed (pg-live, load, policy, approval, evidence, typemap, sampling, canonical scale, NULL/length/empty-set, exit code)14,730,494
6-pipelinesdbt project (38 models/124 tests) + python pipelines + realism fix; starting full end-to-end run14,648,042
7-docsconfluence pages, report/query-log generators, 50-slide deck, analyst SQL, DDL dump; 2 more EKOS governance defects fixed14,524,750
8-finalfinal release run (8 R3 gates, 30/30, 6/6, 162/162) + regeneration14,509,816
9-completefinal docs/report/deck regeneration and commit14,500,123
6 · Accounting for AI

Where each one earned its keep

EKOS

305 structural queries, 0 errors: exact locations, columns, comments, dependents. 584 migrate commands. It predicted the infinity blocker and the amount → amount_bc drift before they bit. Its risk gate and validator caught real problems once fixed.

Claude Code

Wrote everything, and refused to trust green: planted defects at every level, the oracle reconciliation, and root-causing 16 EKOS defects, including two critical governance holes and a cents-blind validator.

Other LLMs

Cloud: semantic naming in the compile, cited Q&A (cautious, sometimes thin). Local Llama 3: fast via cache, weaker answers. Neither was on the critical path. Every modelling rule was confirmed in source.

6 · Findings

16 EKOS defects found by one real run

#DefectSeverity
4Requester could approve their own R3 request (--as cli:<me>)critical
9Validator hashed unconstrained numeric at scale 0: cents invisiblecritical
15Blast radius by table-name suffix: review R1, load R3critical
16Load accepted an approval granted at a lower classcritical
1, 2, 7Constraint triggers; text-keyed chunking; invalid Nullable(LowCardinality)blocker
5, 6, 8Evidence collisions / prefix matching; non-reproducible DDLmajor
10–14NULL, byte-length, empty-set, TSV-NULL asymmetries; validate exited 0 on failuremajor
3Policy file kebab-caseminor

All fixed, each with a regression test that fails without its fix (EKOS devlog_226).

6 · Findings

Still open

EKOS

  • Cross-language homonyms stop resolve
  • Base DDL compiled, ALTER history not replayed
  • DQ.UNIQ.001 blocks FK columns; join-alias misattribution
  • No transform disposition (infinity → NULL lived outside EKOS)
  • jsonb has no canonical rule
  • State machine not advanced by the verbs; errors swallowed
  • ask --json logs to stdout; a compile whose LLM calls all fail exits 0

LedgerSMB (upstream)

  • Aging reports fail on 1.14-dev (result-type mismatch; bad USING)
  • A change listed twice in LOADORDER (harmless thanks to db_patches)
  • payment_post stores scale-22 amounts

This demo

  • Proxy User-Agent → first 32 cloud calls 403 (fixed)
Operate it

Run it yourself

cd ../EKOS && docker compose -f docker-compose.migrate.yml up -d
# once: migration/clickhouse_setup.sql (named collection + databases)
python3 tools/llm_proxy.py 8765 &
cd pipelines && PROJECT=my-run ../.venv/bin/python -m lsmb_pipelines.run
PathWhat
sql/, data_generator/LedgerSMB schema loader + seed; synthetic data through LedgerSMB's procedures
migration/, clickhouse/ddl/EKOS Migrate scripts, policy, EKOS-generated DDL
dbt/38 models, 124 tests, macros, docs
pipelines/orchestrator, tax loader, reconciliation, KPI and usage reports
docs/Confluence pages, REPORT.md, QUERY_LOG.md, migration_report.md
logs/every query, answer, LLM call, command and run
Summary

Checked, not trusted

A real ERP's schema and logic, a migration whose every step is a recorded fact, an analytical layer tested four ways and reconciled to LedgerSMB's own books — and an honest account of where each tool, human and model helped.

30/30
V1+V2+V3
162/162
dbt
6/6
oracle checks
16
EKOS defects fixed