Migrate & Compare (ERP)
The DB is the log is the kernel — in the browser
Already proven by
Pacioli · Torvalds · Hipp
Assembled by RED1,
Why MOrder is the DNA — what 1% buys you
Examining the MOrder cycle (≈3,287 lines — ~0.2% of the codebase) gives you the whole organism, because MOrder is the archetype. From that one class you read:- How it ORMs — every table is
X_<Table>(generated boilerplate) +M<Table>(logic subclass);MOrder extends X_Order— the idiom for all 925 tables. - The document lifecycle —
DocAction(prepareIt·completeIt·voidIt…) walked by theDocumentEngineFSM. - The posting idiom —
Doc_Orderturns the document intofact_acctlines; every document has aDoc_*poster of the same shape. - How it treats its model — model-driven: the AD describes it as data; the class enforces
beforeSave+ binds callouts; the poster derives the GL.
MInOut in-transit · MPayment allocation · MProduction BOM · MInventory count · MAllocationHdr headerless.Source: ERP_MODEL_ARCHETYPE.md
What is Done and Pending — the four-state migration honesty map 🟢🟠🔴🔵 ↗
But ERP is tough — an aircraft carrier is about to go into a bathtub¶
W-POS-WAN-SCALEThe six interlocked things it takes
poc_postings.jsverifyChain() — “the host is a Git remote.”OpLogERP.mdpoc_sign.js · poc_chain.jsThesis — ERP as git¶
A classic ERP is a server of record: every read, write and posting is a round-trip to a machine that owns the truth. We keep the accounting and the document flow but delete that server. The truth becomes a signed, hash-chained op-log; the live numbers are a deterministic fold of it, replayed by a SQLite-WASM kernel in the browser. The host turns disposable (a Git remote); the user owns the log; period-end compaction is a signed checkpoint carrying balances forward — not a batch job with a down-window (the close postings are themselves folds, still being built).
The browser client that renders this is now branded Kernel-ERP — the product name; "folded from the iDempiere oracle" and "iDempiere-faithful" remain accurate descriptions of what the engine does (the oracle of record is a real iDempiere, the idempiere_test Postgres, diffed to the cent). By deliberate design the surface is indistinguishable from real iDempiere: classic ADWindow chrome only — one toolbar (New / Copy / Save / Save&New / Delete / Ignore / Process), folder tabs, the standard header — with everything not native to iDempiere moved off onto a single ⋯ pill rail (share / home / graph / kanban / POS / Ninja / history). Editing is in-place (form-view edits inline, no modal); Zoom Across is a real where-used drill, the Process button greys when a record has none, and leaving an unsaved record raises the native dirty-exit prompt — all diffed against the iDempiere source, not approximated (W-ZOOM-ACROSS, W-DIRTY-GATE).
A claim about substrate and delivery, not features — the legacy stacks have vastly more. What we show: the architecture folds the same transactions with zero network on the read/fold path — proven by folding a live Odoo, iDempiere's own flows (it's the AD we render), and a SAP Business One flow (mock export) into the same six verbs. S/4HANA is still pending a real oracle.
ERP as git — what git did to source code, we do to transactions: the log is the truth, every client folds the whole history, and the host is a disposable remote. The one thing git lacks — invariant enforcement (no double-spend) — we add. See the full git ∷ ours parallel ↓
What a fold is — the chess scoresheet¶
You don't store the chessboard — you store the move list, and replay it. The position is a deterministic fold of those moves: lose the board, keep the sheet, rebuild it exactly; anyone who replays the same moves reaches the same position. Our ERP is identical — the signed op-log is the move list, the live balances are the board. A fold, never a stored snapshot.
The kill points — what the architecture actually deletes¶
The shocker, in one line: there is no server of record. The browser holds the kernel; a signed, hash-chained log holds the truth; the host (if any) is a disposable relay. Every row below follows from that.
| What changes | Legacy ERP | Our WASM event-source | The cut |
|---|---|---|---|
| The server of record | a machine that owns the truth — JVM + Postgres + 3.7 GB build 12 | ✗ none — the browser runs the kernel; the signed log is the truth 17 | the whole tier is deleted |
| Read / fold round-trips | 1 network hop per interaction 18 | 0 — the kernel answers locally 6 | network off the hot path |
| Ownership / trust | the server DB owns your record 18 | you own a signed op-log; the host is disposable 17 | trust model inverted |
| Document schema | ≈925 AD tables, each a hand-written model class 13 | 5 core relations (containers · items · documents · document_lines · journal) + verbs — the rest of the AD rides as data 20 | hardcoded schema, not ERP scope (the AD is unchanged — it's the seed) |
| Runtime code | 1,427,147 Java LOC / 4,465 files 12 | 28,184 JS LOC / 132 files (the engine shell + flows folded so far) 12 | ≈51× built-so-far · ~21× at conservative full parity below |
| Bootstrap (open the books) | re-query the server 18 | signed checkpoint — 0.90 ms vs 47.70 ms genesis 3 | ≈53× |
| Seed DB | 45.2 MB dump 12 | 26.1 MB full-width self-describing AD (was 12.7 MB sliced — completeness chosen over ratio) 12 | ≈1.7× |
| Live DB → SQLite | 143 MB Postgres 13 | 43 MB SQLite (gzip 11.7 MB) 13 | ≈3.3× |
| Backup / DR | backup rotation, 30–50 copies = many× the state; restore = down-window 4 | the recipe is the backup — one signed log ×3 replicas, restore = replay, unbounded restore points 45 | at least 30× less DR storage (strategy-dependent); 0 branch downtime |
| Report scratch tables | reports INSERT ~15 T_* temp tables to read them back (T_Report, T_InventoryValue, T_Aging…) |
in-memory fold, 0 temp rows — foldStatement proven maxDiff=0c (BS + IS + CF) + foldPrint (invoice PrintData tree, 8/8) + NinjaExcel template xlsx delivery (W-NINJA, maxDiff=0c); 13 remaining T_* folds pending per-report witness |
the materialize-then-read tier deleted (proven for all 3 financial statements + document print + xlsx template) below |
How it differs — the architecture¶
Legacy: every read & write is a network round-trip; the DB owns the truth; period close is a server batch
job with a down-window. Ours: state = a deterministic fold of the signed op-log — 0 network on the
read/fold path; the host is disposable (Git-like), the log is the truth; period close is a signed checkpoint.
Source: docs/DistributedERP.md §0 (lines 53–85, server→serverless table) + §10 (lines 467–468).
But where's the server? — what replaced each job¶
If you deleted the server, who does its work? "Serverless" doesn't mean no machine ever talks to another — it means no server of record, no machine that owns the truth. Every job the server did still happens; each moved onto the signed log, the kernel on each client, the user's own channel, or a dumb facilitator that owns nothing.
Every job the server did still happens — it just moves to one of four owners that own nothing, each proven by a POC in scripts/19. For an independent read of how well those proof scripts are built — separation, determinism, non-invention, adversarial falsifiers, and a per-script PASS scoreboard — see the Fold-Engine code-quality scorecard (all 18 witnesses green).
- Hold the authoritative state poc_distributed.js
- Merge concurrent edits poc_distributed.js
- Reconcile discrepancies poc_postings.js
- Run / validate business logic poc_kernel.js
- Mint record IDs poc_distributed.js
- Prevent double-write poc_distributed.js
- Detect tampering poc_chain.js
- Authenticate / authorise poc_sign.js
- Durably store / back up poc_persist.js
- Sequence multi-party order poc_remote_pos.js
- Be always-on — nothing; work offline
The analogy: git. No central machine owns your code history — every clone has it all, verifies it, rebuilds from it; GitHub is a convenience, not the truth. We do to transactions what git did to source code — the same shape, line for line:
verifyChain() · folds the live stateFull doctrine + the hard multi-writer cases (shared stock, credit limits, client version skew): DistributedERP.md §0, §9.
You Are Not Seriously Gonna Read This
The long read — the same topics as the blurbs above, in full: the tables, the witnesses, the honest caveats. Each Serious read → lands here.
Disaster recovery & TCO — serious read¶
The 244× storage math.
The fair comparison holds the durability guarantee constant — restore to any of the last 50 days · RPO ≤ 24 h · survive primary loss — and asks only: what does it cost to meet it, amortised over a year of 50-branch ops? Unit costs are measured on the real kernel (314 B/op uncompacted snapshot; fold, restore-to-arbitrary-op, and per-branch additivity all witnessed); the year-level figures are derived over modelled constants for the traditional side (no Postgres on the bench), each chosen conservative for us — 230 B/row and 5 rows/op are low versus Postgres+index and real iDempiere, so the real gap is wider, not narrower (constants named in GAPS #7 below). 4
Durable storage to meet the 50-day SLA (Retail, 1k sales/branch/day, one durable copy each):
| Backup strategy | Traditional | Ours (50-day op-log) | ratio |
|---|---|---|---|
| weekly full + daily incremental (standard DBA) | 192 GB | 0.78 GB | 244× |
| minimal: 1 full + 50 diffs (storage-min, replay-heavy restore) | 24 GB | 0.78 GB | 30× |
Incremental backup barely shrinks the gap — the weekly fulls dominate. Only the storage-minimal scheme reaches ~30×, and its restore is replay-heavy. The structural reason: a snapshot scheme must periodically re-store the whole database; the op-log never stores a base image (the deltas are the system). And the advantage grows with business age — their fulls grow yearly while the op-log's 50-day window stays constant (§VOL).
Who carries the whole log? Per-device storage — does every device carry it? No.
| Role | Stores | Resident | Bounded by |
|---|---|---|---|
| Edge / branch | engine shard + own open-period ops + last checkpoint | ~13 MB | period-close + gravity shard (DistributedERP §13) |
| Facilitator / relay | open-period union (ordering only) | ~16 MB/day, disposable | reconstructible from the edges |
| Full-replica (bucket) | the whole compacted recipe | ~0.8 GB (50-day) – ~5.7 GB/yr | one per business, not per device |
Recovery & the honest trades — all witnessed 54:
- Total relay loss, no backup rebuilds consolidated state from the branches' own slices —
§BLACKOUT-RESUME: 50 branches, a fresh empty relay, an identical signed tip, books to the cent, idempotent re-push. The only loss is a bounded, ledger-reconciled CAS-arbitration sliver (the one shared op-class, §5);§ORDER-HONESTshows disjoint folds commute but cross-branch CAS order is not reconstructible from signed logs alone (honest correction to "total order is reconstructible"). - Consolidated restore is additive — per-branch folds combine (
maxDiff=0c), so 12.5M ops at 5k/branch/day restore in ~0.5 s 50-way-parallel; only the contended op-class needs merge logic. - 0 branch downtime trades against a double-sale risk — but only for stock that is not physically partitioned (≤0.1% of ops; located stock can't double-sell — the scan is possession), and it is value-tier-bounded (high-value blocked → 0, low-value → a receivable). Traditional avoids it only by requiring connectivity (then the branch stops when the link drops — the very downtime we removed) or by allowing offline POS (carrying the same risk).
- "0 always-on server-hours" is 0 always-on compute-VM, not 0 cost — object storage, the CAS touch, and the intermittent relay remain, itemised: storage-priced + pay-per-invocation, no OS / patch / licence. An illustrative annual bill (public list prices, volatile; excluding DB licence + DBA labour, which widen it) runs >10× cheaper, compute-dominated.
What breaks first as this scales? A quantitative limits analysis — max in-memory DB before OOM, genesis vs checkpoint re-fold time, writer-conflict probability at 50+ devices, OPFS vs IndexedDB, and the mobile ceilings — is worked end-to-end in Fold-Engine Constraints Analysis. Short version: single-writer-per-shard is the only hard limit; the mobile genesis re-fold (~25 s @ 100M ops) and the ~200 MB mobile memory ceiling bite first, and both are already ~90% handled by the checkpoint design above.
Vitals — speed, footprint, ownership — serious read¶
Three tables, not one wall. Columns are architecture, not a feature scorecard; numbers measured on this box / browser unless marked. "n/a — architectural" = the legacy stack has no comparable number because the property is structural (it always needs a server).
A · Speed & latency¶
| Vital | iDempiere | Odoo | SAP | Our WASM event-source |
|---|---|---|---|---|
| Period-end carry-forward | server batch job + down-window (per-row saveEx ≈ ~1M round-trips on a 40-yr depreciation run) 9 |
server batch job 18 | server batch job 18 | signed checkpoint = balance b/f (the compaction step — accrual/FX/depreciation postings are themselves folds), no down-window; 40k-op close-fold ≈ 2.68 s, archived 40000→live 1, reconcile maxDiff=0c 23 |
| Server round-trip (read/fold) | round-trip per interaction 18 | round-trip per interaction 18 | round-trip per interaction 18 | 0 — the kernel answers locally 6 |
| Bootstrap (open the books) | re-query the server 18 | re-query the server 18 | re-query the server 18 | ~53× faster from checkpoint — 0.90 ms vs 47.70 ms genesis replay, same result 3 |
| Commit throughput (5000 ops) | n/a — architectural 18 | n/a 18 | n/a 18 | batch commitGroup ~22,492 ops/s = 2.4× naive 10 |
| Fold/append ceiling | n/a — architectural 18 | n/a 18 | n/a 18 | linear to 20,000,000 ops (~437 B/op; fold ~40M ops/s hot) 11 |
| Storage primitive (1000 ops, 1 commit) | Postgres WAL+fsync 5.24 ms (0.0052 ms/op) 7 | (same engine) 18 | n/a 18 | sql.js +sha256 chain 208.45 ms — slower per-op, buys no server; Postgres durability/concurrency DEFERRED to the install 7 |
Where we actually beat them: the network. The storage-primitive row above is on-box, where durable Postgres wins per-op — and we say so. But an ERP is never on-box: every interaction crosses a network to the server of record, which pays a round-trip per interaction (RTT-bound — and it blocks when offline). Our kernel answers locally (~0.01 ms/op) and relays asynchronously — 0 round-trips on the read/fold path. That is the whole win: not faster storage, no network on the hot path. A remote-POS drive puts numbers on it — locals measured, network leg modelled, legacy excludes iDempiere ORM/OSGi so it's a floor 8: per sale, legacy is RTT-bound — ~256–674× at 0.5 ms LAN, ~8,500–50,000× at 50 ms cross-region — while ours stays flat at local speed. The iDempiere 40-year depreciation run shows where that cost really sits: per-row
saveEx≈ ~1M round-trips 9.
B · Footprint & bloat¶
| Vital | iDempiere | Odoo | SAP | Our WASM event-source |
|---|---|---|---|---|
| DB seed | Adempiere_pg.dmp 45.2 MB 12 |
n/a — diff schema 18 | n/a 18 | erp/ad_seed.db 26.1 MB (≈1.7× smaller, full-width — the earlier 12.7 MB column-slice was 3.5× but left windows unreachable; completeness won); the 26.1 MB IS the self-describing AD 12 |
| Runtime LOC | 1,427,147 Java LOC / 4,465 files + JVM + Postgres + 3.7 GB build 12 | n/a — diff codebase 18 | n/a 18 | 28,184 JS LOC / 132 files, static + SQLite-WASM, offline (≈51× fewer built-so-far; ~21× at conservative full parity, zero server/JVM/DB) 12 |
| Live DB → SQLite | Postgres 143 MB on-disk (GardenWorld) 13 | n/a 18 | n/a 18 | 43 MB SQLite (925 tables, 187,133 rows ≈ 3.3×); gzip 11.7 MB (3.7×) 13 |
C · Migration & ownership¶
| Vital | iDempiere | Odoo | SAP | Our WASM event-source |
|---|---|---|---|---|
| Migration fold (does the legacy flow fold into our verbs?) | native — it renders this AD; handlers diffed cell-by-cell vs an iDempiere oracle (diff_oracle.log; one GL cell needs live docker) |
PROVEN vs LIVE Odoo 17 — SO S00023, 5/5 hops, newVerbs=[], GL ΣDr==ΣCr 5002.50 14 | B1 PROVEN vs a MOCK export (5/5, journal 770.00); S/4HANA NOT-RUN — gated on a real oracle 1516 | every hop maps to CREATE_DOCUMENT / CREATE_LINE / SET_STATUS / POST / ALLOCATE 1415 |
| Data ownership / durability | server DB owns the record 18 | server DB owns the record 18 | server DB owns the record 18 | user-owned signed op-log; host disposable (Git analogy); tamper caught by verifyChain(), forgery by ECDSA-P256 sig 172 |
Method & honesty — serious read¶
What is measured (real, on this box / browser):
- Period-close fold, balance-b/f, reconcile-to-0c, tamper/forgery rejection, determinism — on the
real kernel (scripts/test_kernel_period_close.js) and against real double-entry POST ops
(scripts/test_integ_postings_reconcile.js).
- Browser-measured 40k-op close-fold timing, bootstrap 53× speedup, reconcile maxDiff=0
(build/erp/period_close_drive.log, an in-browser drive).
- Storage primitive sql.js-vs-Postgres (build/erp/bench_oplog_pg.log), batch throughput
(build/erp/sync_poc_smoke.log), volume ceiling to 20M ops (build/erp/poc_volume_ceiling.log).
- Bloat figures du/wc/sqlite-measured 2026-06-06 (internal/BLOAT_MEASUREMENT.md, summarised in
the bloat memory).
- The Odoo fold is against a running Odoo 17 instance (build/erp/odoo_fold_live.log,
§ODOO-FOLD-LIVE PASS).
- Engine output == real iDempiere output — 43 surfaces oracle-diffed, not asserted (16 cent-exact · 6 declarative · 21 model-layer), + 3 rule-consistent, each carrying a load-bearing §FALSIFIER so a passing diff can't be a tautology. Full surface-by-surface enumeration, witnesses, and the live-Postgres diffs → ERP_COVERAGE_MATRIX.md §Equivalence + FoldEngineQuality.md. How a save/completeIt/Doc_* posting maps from your iDempiere code → ERP Rosetta Stone.
maxDiff=0c is exact, not "close enough" — the money-math holy war, settled. Money never touches float: amounts accumulate as integer cents, and the only non-integer steps (FX, proportional tax) multiply in BigInt off the exact-decimal rate, rounded HALF_UP — unit-proven bit-equal to Java BigDecimal (build/erp/bigdecimal.js, poc_money_fold.js). The lesson: the discipline ports, the language doesn't matter — exact decimal money is a ~140-line library, not a reason to keep the server. (The mechanism — why set+save becomes an op and how the fold stays exact — is the Fold Engine Black Book.) What this costs to change — and why the op-log is a fixed substrate, not a per-rule tax (the elementary "lock a field read-only" change matrix, the imperative-vs-declarative gradient, and the cost equation) → Cross-ERP Rosetta Stone §7–§8.
What is architectural (a property, not a number): - "0 round-trip" — structural (no server of record on the read/fold path), not a benchmark. Honest counter: server-removal only wins over a network; on-box, durable Postgres is faster per-op (it buys durability + concurrency we defer). - Most ERP cells are n/a — architectural: the legacy stack exposes no comparable single number (throughput ceiling, batch-vs-naive) — server-bound by design.
NOT feature parity — plainly. iDempiere, Odoo and SAP have vastly more features, localisations and integrations. The ~28K LOC renders the dictionary and folds the paths built so far (the order→ship→invoice→match→pay→allocate trade loop, inventory movement→on-hand→replenish, GL posting incl. inter-org + FX + reverseCorrect/void, signed rule-edit, period-close, and the beforeSave+DocAction-FSM model layer of every document class — 43 surfaces now oracle-equivalent to real iDempiere) — it does not re-implement the full transactional server. Only ~1% of the M-class business logic is ported (the M*.java model logic = 104,940 code-LOC; ~205 LOC of transactional verbs folded + ≈830 LOC of cited beforeSave regions ported as hooks across 16 document classes) — the engine is the host, the M-class logic is the work still ahead. The win is delivery/definition: the AD-interpreter is lean because the AD is self-describing, and the whole server/build stack is gone. That self-description is load-bearing for breadth, not just one curated surface: CRUD generality (#348) derives each table's editability — column type, read-only, defaults — from its own Application Dictionary rows, so any table is editable per its own dictionary rather than hand-curated screen by screen. Each transactional verb still has to be folded deterministically. See feedback_erp_perf_claims.
Report scratch tables — serious read¶
Legacy ERP reporting creates data in order to read it. A financial report does not query the journal directly — a
process first INSERTs aggregated rows into a per-run temp table, then the print format reads that table back.
iDempiere ships a whole family of these scratch tables — ~15 across the codebase: T_Report (financial statements,
57 refs), T_InventoryValue (53), T_CashFlow (22), T_TrialBalance (12), T_ReportStatement (12), T_InvoiceGL
(11), T_Aging (10 — AR/AP aging), T_Reconciliation (9), T_BankRegister (8)… (counts: grep -roE "T_[A-Z]\w+"
org.compiere --include=*.java over the iDempiere source.)
A fold deletes this tier. Because state is a deterministic reduce over the journal, the report computes its cells in memory and paints them — zero temp rows written. So the answer to "does a report engine create more data when it is only fetching?" is: legacy does (scratch rows); the fold does not — a fetch is a projection of existing truth, materialised only in memory and discarded.
Proven — not asserted (the discipline of this paper): foldStatement reproduces iDempiere's FinReport for
all three seed statements to the cent — Balance Sheet, Income Statement and Statement of Cash Flows,
maxDiff = 0c (108 + 148 + 140 segment cells), witness W-PA-REPORT
(scripts/poc_pa_report.js → build/erp/poc_pa_report.log), diffed against an independent live idempiere_test
re-derivation — by replacing T_Report with an in-memory cells matrix. No T_Report, no temp rows, identical
totals. And the document half: foldPrint reproduces the PrintData master-detail row tree + break subtotals for
the real Invoice Header → Invoice LineTax format across all 8 seed invoices, maxDiff = 0c against live base
tables and the stored c_invoice.grandtotal that real iDempiere wrote — witness W-PRINTFORMAT
(scripts/poc_printformat.js → build/erp/poc_printformat.log), three load-bearing falsifiers.
A third delivery mode is now proven — NinjaExcel. Where foldStatement paints cells in the browser and foldPrint drives a formatted document, NinjaExcel fills a user-authored .xlsx template: a 3-sheet workbook (BACKUP = stored oracle columns; Input = date-range bindings; Process = SQL row descriptors with @token@ placeholders). The NinjaExcel engine reads the manifest, validates completeness (gate() refuses dangling binds, empty TABLE fields, uncovered @token@ holes — 3/3 falsifiers each produce a named error; run on a gated manifest throws), fills cells via an independent SQL path, then verifies maxDiff=0c against the BACKUP column — W-NINJA, 9 cells, 4 grandtotal amounts + count. A deliberate wrong binding (swapped documentno) is falsified (maxDiff=5035c), proving the equality oracle is load-bearing. A phrase-to-SQL compiler (W-NINJA-RULE) extends this further: a human phrase such as "SUM GrandTotal of Invoices, completed, is Sales Transaction, from 2002-01-01 to 2003-12-31" resolves to SQL candidate(s) using only vocabulary extracted from ad_table / ad_column / ad_ref_list — zero LLM, zero hardcoded strings. The sample falsifies wrong candidates (wrong status → 0 accepted); unknown entities are explicitly refused. The browser pill (W-NINJA-RULE-UI) runs the same compile→falsify→human-pick loop live: maxDiff=0c after the human picks the date column from a tie. The discipline is the same as foldStatement and foldPrint: a phrase that cannot be falsified is not a rule; the oracle kills it before it runs.
Generalises to (candidate — each pending its OWN witness, claimed only as it lands): the same materialize-then-read
pattern underlies every other member of the family, so Aging (T_Aging), Inventory Value, Cash Flow, Trial Balance and
the rest are structurally the same fold — a pure aggregation that needs no scratch table. They are not yet built or
diffed; this paper claims the deletion only for the three financial statements, the document print, and the NinjaExcel template delivery above. Aging in particular folds over
open items + a due-date-bucketing function (not the GL), so it is a distinct verb on distinct seed data — named here,
not claimed. This narrows GAP #8 to its tail: reporting parity is now witnessed for all 3 financial statements,
the master-detail document print (foldPrint, W-PRINTFORMAT), and template xlsx delivery (NinjaExcel, W-NINJA); what stays open is the remaining T_* family
(Aging, Inventory Value, …), the unexercised PrintDataGroup functions (group-by/count/avg — implemented, no seed
format uses them) and pixel layout (a stated non-goal — DOM is the render).
Gap Analysis — serious read¶
Functional gaps are tracked below; the operational limits (what the browser substrate itself can take before it breaks — memory, fold time, device-scale conflict) are quantified separately in the Fold-Engine Constraints Analysis.
In plain words — start here¶
The objective. Not "re-clone all of iDempiere." It is: the engine works, and each customer's actually-used screens run identically to the original — to the cent. "Finished" is measured per deployment (a tenant's live surface), not against all 496 of iDempiere's programs. By that measure the hard part is done; the rest is routine and on-demand.
iDempiere is a library of ~496 pre-written programs, one per business document. We did not rewrite them. We built one small engine that treats every document as data + one shared recipe, and records every action in a signed journal — only ever added to, never erased (the way accountants have worked since 1494). The current state of the books is computed from that journal on demand; change anything and the journal grows, nothing is overwritten — so it is auditable, reversible, serverless, and runs in a browser.
Is it finished? Depends what "finished" means. The engine is proven: for every flow we have folded, its output is identical to real iDempiere, to the cent. We have folded the flows a real business uses — order→ship→invoice→pay, inventory, GL. The other programs are not lost: they are either plumbing we no longer need, rules that have become data, or flows we fold when a customer needs them — each proven the same way.
One word, two jobs — the thing outsiders trip on. "Fold" means two different things. Fold-to-run = compute the current books from the journal in the browser; automatic, instant, done. Fold-to-build = translate one iDempiere program's rule into the engine and prove it matches, to the cent; that is human work, and it is what "not yet done" refers to. So fetching the whole database gives you the data, not the logic — a document type whose recipe has not been written yet will not process, even with all its data present. Nothing is lost; the recipe simply has to be written and proven. That is development, not a browser refresh. What follows is the technical version of exactly that.
Against a live Odoo 17 instance, the migrate currently pulls one of 27 sale orders (a single order-to-cash chain, S00023) and no master data. Everything else constitutes the gap, which divides into two categories:
- Extraction gap (the majority). Data the migrate could pull but does not yet. The engine already folds these flows — order-to-cash, procure-to-pay with three-way match, GL journals, reversal and void — each backed by a passing witness; the records are simply not yet wired into the extractor. This is extraction work, not engine development.
- Fold gap (the smaller remainder). Capabilities the engine cannot yet compute even when the data is present: cost-valued inventory posting, financial-report and print-format mapping, aging, analytic accounting, and server-action interpretation.
The sections below proceed from overview to detail: (1) coverage by capability, (2) a proven fold in code, (3) an unbuilt fold, (4) the code-size estimate for full parity, and (5) the open caveats that lack a measured benchmark.
Data shape — how the live odoodemo models thread through a few document types, collapse onto the five core
relations, and fold:
ODOO odoodemo · live (real counts) → 5 core relations → fold (proven) captured today
─────────────────────────────────── ──────────────── ─────────────── ──────────────
MASTER partner ×38 · product ×30/35 containers · items (master — ░ 0 pulled
cat ×9 · uom ×28 · account ×47 no fold needed) ◄ the P0 gap
journal ×8 · tax ×2 · terms ×11
DOCS sale.order ×27 (lines ×56) documents + completeIt → ▓ 1 of 27
purchase.order ×13 document_lines W-FOLD-COMPLETE (S00023 chain)
stock.picking ×31 (O2C / P2P spine) fact_acct 0c ✓ ░ 26 + all 13 PO
account.move ×34
JRNL account.move.line ×99 journal posting → ▓ ~4 of 99
payment ×3 · reconcile 1+3 GL ΣDr==ΣCr ✓ ░ the rest
▓ pulled & folded (engine proven) ░ EXTRACTION gap — pull the data; the fold already exists
Read left to right: the source's many models thread through a few document types, which collapse onto five relations; the fold is already witnessed. The gap (mostly ░) is therefore remaining data to extract, not engine capability to build.
1 · Coverage by capability — source ERP vs. our fold
Axis = capability, not table-by-table (the three schemas don't align). The Odoo column is real search_count
from the live odoodemo (Odoo 17) instance our agent folds (build/erp/odoo_survey.json); iDempiere = the seeded
GardenWorld client; OOTB = our ~28K-LOC engine. EXTRACTION = data the migrate could pull but doesn't yet ·
FOLD = a capability the engine can't compute even if pulled.
| Capability | iDempiere (GardenWorld) | Odoo (odoodemo, live) |
OOTB — ours (✓ = proven1) | Migrate GAP |
|---|---|---|---|---|
| Partners / customers | C_BPartner |
38 (4 cust) | master (fold n/a) | 38 not pulled · EXTRACTION |
| Products + cat + UoM | M_Product |
30/35 · 9 cat · 28 uom | inline refs only | not pulled as master · EXTRACTION |
| Chart of accounts | C_ElementValue (tree) |
47 accounts | foldStatement expands COA tree |
47 not pulled (takes inline resolved strings) · EXTRACTION |
| Journals / taxes / terms | GL_Journal/C_Tax |
8 / 2 / 11 | tax rides inline | not pulled as master · EXTRACTION |
| Sales O2C (order→ship→invoice) | C_Order chain |
27 SO (22 conf) | ✓ — W-FOLD-COMPLETE maxDiff=0c |
1 of 27 pulled · EXTRACTION (loop the proven fold) |
| Purchase P2P (PO→receipt→bill→match) | C_Order PO + M_MatchInv |
13 PO / 6 bills | ✓ — W-FOLD-AP-INVOICE/MATCHINV; adapter buildBuyEvents written |
0 pulled — adapter unused · EXTRACTION |
| Inventory moves + on-hand | M_InOut/M_Movement |
31 pick · 59 move · 43 quant · 13 valuation | movement fold PROVEN; cost-valued GL deferred | quants/moves not pulled · EXTRACTION + cost-GL FOLD |
| Customer invoices (AR) | C_Invoice SO |
7 posted | ✓ maxDiff=0c |
1 of 7 pulled · EXTRACTION |
| Vendor bills (AP) | C_Invoice PO |
6 (5 posted) | ✓ — W-FOLD-AP-INVOICE | 0 pulled · EXTRACTION |
| Credit notes / reverse-void | reverseCorrect/void |
7 refunds | ✓ — W-FOLD-REVERSE | docs not pulled · EXTRACTION |
| Payments + reconciliation | C_Payment/C_AllocationHdr |
3 pay · 1+3 reconcile | ✓ — W-FOLD-PAYMENT/ALLOC | synthesised, not real — 3 real pay/reconcile not pulled · EXTRACTION |
| Manual GL journals | GL_Journal |
13 entries | ✓ — W-FOLD-GLJOURNAL (inter-org) | 0 pulled · EXTRACTION |
| Financial statements BS/P&L/CF | PA_Report→T_Report (15-table T_* tier) |
3 account.report (BS/P&L code-side v17) | ✓ BS+IS+CF maxDiff=0c — W-PA-REPORT (iDempiere-side) |
Odoo report defs unmapped · FOLD |
| AR/AP Aging | Aging→T_Aging |
on-the-fly wizard (no stored rows) | not built | distinct fold (open-items+date buckets) · FOLD |
| Document print / layout | AD_PrintFormat ('P' grids-in-grids) |
41 QWeb defs | foldReceipt (W-PROC); ✓ foldPrint maxDiff=0c — W-PRINTFORMAT (8/8 invoices, iDempiere-side) |
Odoo QWeb defs unmapped · FOLD |
| Workflow / automation | AD_Workflow |
64 server actions · 18 cron | ✓ ad_workflow.js walk + replay — W-WF-HARDEN (11 real traces diff=0, iDempiere-side) |
no Odoo server-action interpreter · FOLD |
| Analytic / cost centres | acct dims / C_Project |
19 analytic accts | not built | no analytic fold · FOLD |
Prioritised extraction backlog (to pull the full Odoo dataset) — tracked in prompts/MIGRATE_INSTALL_TENANT.md §RESUME
and the machine-readable build/erp/odoo_survey.json:
1. Master-data pass (P0) — partners 38 · products 30/35 + cat 9 + UoM 28 · COA 47 · journals 8 · taxes 2 · terms 11 · companies 2. Pure search_read dumps; every document dangles without it.
2. Loop all 22 confirmed SOs (today 1/27) — fold path already proven.
3. Wire the existing buy-side adapter → 6 POs + 6 vendor bills + 3-way match (code written, unused).
4. Pull real payments + reconciles (3 + 1/3) instead of the synthetic ALLOCATE.
5. Manual journals (13) + credit notes (7) → the already-proven GL-journal / reverse-void folds.
6. Inventory state — on-hand quants 43 + valuation 13 as opening balances (cost-valued GL is itself a fold gap).
7. Fold gaps (defer) — financial-report defs (3) + QWeb print (41) + aging + server actions (64): mapping/interpreter work, not extraction.
Every migrated row must trace to a real Odoo record (NON-INVENT) — the survey counts above are both the extraction
targets and the §-witness oracles (records pulled == live search_count). Capability-coverage detail (per AD
surface, with witnesses) → ERP Coverage Matrix; the T_* reporting-tier subset → § temp-tables.
The next two folds show the contrast in code — a fold we have completed (completeIt, shipped, with its full listing)
and one we have not (T_Aging, proposed). The line-count reduction comes from dropping the getX/setX/saveEx, SQL, and
try-catch boilerplate — only the business decisions remain.
2 · A proven fold in code — MOrder.completeIt()
iDempiere · MOrder.completeIt() — Java, ~250 LOC¶
if (!m_justPrepared) prepareIt(); // re-check → InProgress unless ready
if (fireDocValidate(BEFORE_COMPLETE) != null) // model validators (first error aborts)
return STATUS_Invalid;
if (!isApproved()) approveIt(); // implicit approval
createCounterDoc(); // intercompany
shipment = createShipment(dt, getDateOrdered()); // auto-generate the delivery
invoice = createInvoice(dt, shipment, …); // auto-generate the invoice
if (fireDocValidate(AFTER_COMPLETE) != null) return STATUS_Invalid;
setProcessed(true); setDocAction(Close);
return STATUS_Completed; // Doc_Order posts fact_acct later, at post time
OOTB · completeIt fold — JS, ~50 LOC¶
if (CrudOverlay.docActionOutcome(entry,order).to!=='CO') return {status};
if (!AdModelVal.fireHooks('BEFORE_COMPLETE',{…}).ok) return {blocked}; // same validators
const childOps = [ ...buildDoc('M_InOut',…), // ship + invoice =
...buildDoc('C_Invoice',…) ]; // the SAME verb, recursed
const post = postRecipe('C_Order',order,lines)…; // DR/CR derived, ΣDR==ΣCR
const group = [DOC_ACTION_CO, {op:'POST',post}, ...childOps];
return KernelOps.commitGroup(db, group); // ONE signed, hash-chained op-group · all-or-none
createShipment/createInvoice become buildDoc(…) recursion (the MOrder archetype one level down), and the books
are a FOLD of the emitted ops, not in-place saveEx().
Full block-for-block listing — the shipped completeIt annotated against
org.compiere.model.MOrder.completeIt(), the primitives it stands on, and the what-it-demonstrates read
(~50 JS vs ~250 Java; createShipment/createInvoice = buildDoc recursion; both folds in one function) —
now lives in its natural home: ERP Rosetta Stone §3 — Worked example.
Witness W-FOLD-COMPLETE maxDiff=0c.1 Code-quality scorecard: FoldEngineQuality.md.
3 · An unbuilt fold — the T_Aging aging report
iDempiere · Aging.doIt() → fills the T_Aging scratch table¶
sql = "SELECT … oi.DueDate, oi.DaysDue, GrandTotal, OpenAmt FROM <open invoices>";
rs = DB.prepareStatement(sql).executeQuery(); // one pass over open items
while (rs.next()) {
if (bpKeyChanged) {
aging.saveEx(); // ← INSERT a T_Aging ROW (scratch)
aging = new MAging(…, DueDate, IsSOTrx);
}
aging.add(DueDate, DaysDue, GrandTotal, OpenAmt); // bucket the open amount
}
aging.saveEx(); // final T_Aging row written
// MAging.add(): daysDue<=0→Due0 · >=-7→Due0_7 · [-31,-60]→Due31_60 · <=-91→Due91_Plus …
OOTB · foldAging — PROPOSED, ~12 LOC (not built)¶
// PROPOSED · NOT BUILT — the T_Aging FOLD gap. Folds OPEN ITEMS, not the GL. 0 temp rows.
function foldAging(openItems, asOf) {
const out = {}; // bpId → {Due0,Due0_7,Due31_60,…} cents
for (const it of openItems) { // host-injected open C_Invoice rows
const daysDue = days(asOf, it.dueDate); // SAME DaysDue as Aging.java
const b = out[it.bpId] ??= zeroBuckets();
if (daysDue <= 0) add(b,'Due0', it.openAmt); // SAME boundaries
if (daysDue >= -7) add(b,'Due0_7', it.openAmt); // as MAging.add()
if (daysDue<=-31 && daysDue>=-60) add(b,'Due31_60', it.openAmt);
}
return out; // in-memory — no T_Aging; the temp tier is gone
}
Status. foldAging is a candidate fold — it mirrors MAging.add()'s exact bucket boundaries, but it is not a
shipped witness. It needs the open-item extraction (Odoo stores no aging; it computes it in an on-the-fly wizard,
while iDempiere derives it from C_Invoice open amounts). Tracked: matrix GAP item 13 · § temp-tables.
4 · Code size for full parity (LOC estimate)
51× is honest for the engine shell folded today (28,184 JS LOC, re-measured 2026-06-12 — down from 76× as real coverage grew, exactly as a non-overclaim should move) — but it measures the thinnest, highest-compression slice (order-to-cash + posting), where iDempiere is mostly generated boilerplate and ZK UI that collapse to ~0. It does not extrapolate to a full port: only ~1% of the M* business logic (104,940 code-LOC) is actually ported. We headline the conservative ~21× to avoid overclaiming.
Exhaustive coverage map → ERP Coverage Matrix. Two axes, both measured from real
ad_full.dbqueries (not asserted): the interpreter-coverage ladder — 6 covered / 33 partial / 3 gap of 42 surfaces (closed for every seed-data surface; the first six flipped to covered 2026-06-11 when the live UI itself became the witness — theW-AD-*-LIVEfamily) — and the equivalence axis — 43 surfaces match the real iDempiere oracle (16 cent-exact · 6 declarative diffed to the live Postgres · 21 model-layerbeforeSave+FSM walks) + 3 rule-consistent, each with a load-bearing §FALSIFIER. The surface-by-surface breakdown, witnesses, and M-class denominator live in the matrix + ERP_MODEL_ARCHETYPE.md (MOrder archetype + ~25 deltas, deepest deltas foldmaxDiff=0c). The buckets below are measured, not asserted.
Splitting all 1,427,147 Java LOC by fate 21:
| Fate | ~LOC | Share | What |
|---|---|---|---|
| Deleted outright | ~490K | 34% | ZK web UI (190K), tests (78K), server-side HTML lib (44K), print/report + Jasper (38K), import/migration (25K), webservices, app-server daemon, installer, JDBC drivers, OSGi/HTTP plumbing |
| Generic-replaced by the interpreter | ~735K | 52% | generated models X_*/I_* (573K) + PO / dictionary / runtime core (Env, DB, GridField, util… ~162K) — a new table is data, not code |
| Irreducible — must be folded | ~200K (~6.2 MB src) | 14% | M* model logic, Doc_* posting, the acct / costing / tax / payment / allocation / matching engines, callouts, validators, document process/ |
Read it: ~86% is UI, boilerplate, or server plumbing the browser deletes outright. Even the irreducible 14% is ceremony-heavy — in the M* models a third is blank/comment/signatures and most of the rest is generic accessor/lifecycle code the dictionary already handles; the behavioural logic (state transitions, posting math, tax rounding) is a minority.
And the irreducible 14% is not one up-front lump. A large slice of it is the SvrProcess corpus — org.compiere.process.*, ~54K LOC across 476 AD_Process reports/procedures. We do not port that corpus to reach parity; we built and proved the dispatch mechanism (ad_process.js, W-PROC: classname→handler-registry→prepare/validate-params→doIt, a port of ProcessUtil.startJavaProcess + SvrProcess.startProcess) and fold individual procs on demand — when a customer actually invokes that report or routine, gated on need + an oracle, never as a sequencing prerequisite. The remaining procs are named-deferred, not blocking — the deliverable is the mechanism, not the corpus (coverage matrix §A/§B). So ~54K of the ~200K "must-fold" bucket amortises over real demand rather than gating the conversion estimate.
The process fold lane — a server action re-derived from the log. The point is sharper than dispatch: an iDempiere "process" (its server-side document actions and reports) is re-expressed as an op-log fold — its result is a deterministic re-derivation by replaying the signed log, requiring no new primitive verb (newVerbs=0) and gated to the already-ported DOC_FAMILY (the consequences are extracted from the iDempiere source, never invented). A demand audit of the live corpus sorts the 451 actually-used processes (the exact subset a real role can reach, W-PROC-PICKER vs the live oracle, diff=0) into three kinds — 148 KIND-1 (reports), 16 KIND-2 (document generators), 287 KIND-3 (the rest). Shipped folds so far: the KIND-2 document generators — ProjectGenOrder (AD_Process 164, project → Sales Order, #352), InOutGenerate (118, confirmed Sales Order → M_InOut shipment, #355) and InvoiceGenerate (119, confirmed Sales Order → C_Invoice, #358) — each folding through the same buildDoc recursion as completeIt; and a run of KIND-1 report folds — M_InOut (117), M_Movement (290), M_Inventory (291), C_Project Print (217), PP_Order (53028) and the C_Payment voucher (313) — each W-PROC-* diffed to the cent against the iDempiere print/report output. These join foldStatement / foldPrint / NinjaExcel as proof that the process tier folds, not just the document tier.
Folding that behavioural core into declarative verbs compresses ~5–8× (no Java/OSGi ceremony, no per-field getters) — though costing and MRP fold least cleanly (stateful cost rollups, landed cost), pulling toward the conservative end:
| Scenario | irreducible folded | ÷ ratio | full JS (+ engine 28,184) | overall |
|---|---|---|---|---|
| Optimistic | 150K | 8× | ~47K | ~30× |
| Mid | 175K | 6.5× | ~55K | ~26× |
| Conservative (headline) | 200K | ~5× | ~68K | ~21× |
Realistic full parity — we headline the conservative ≈ 68K JS LOC ≈ ~21× (mid ~55K/~26×, optimistic ~47K/~30×) — vs ~51× for the engine shell folded so far. Leading with the conservative bound is the point: it does not overclaim. The fold ratio is the one estimated input (GAPS #6); every LOC count is measured (incl. the M-class denominator — M*.java 104,940 code-LOC, ~1% folded/ported: ~205 LOC of transactional verbs + ≈830 LOC of cited beforeSave regions).
Full breakdown by iDempiere module (org.adempiere.base, org.adempiere.ui.zk, …) — expand
Measured 2026-06-08 21. Fate = DELETED (no equivalent in a browser substrate) · GENERIC (the interpreter renders it from the dictionary) · FOLD (re-expressed as verbs / handlers). Buckets are disjoint and sum to 1,427,147.
| iDempiere module / package | LOC | Fate |
|---|---|---|
| org.adempiere.base | 959,659 | mixed — split below |
X_* generated models |
345,490 | GENERIC |
I_* interfaces |
227,100 | GENERIC |
M* business models |
198,679 | FOLD (behavioural subset) |
Doc_* posting |
12,789 | FOLD |
| acct · wf · process engines | 28,686 | FOLD |
| PO · GridField · Env · DB · util · OSGi core | 116,302 | GENERIC |
| print · report · impexp · db-conn | 30,613 | DELETED |
| org.adempiere.ui.zk — ZK web client | 189,786 | DELETED |
| org.idempiere.test | 78,389 | DELETED |
| org.apache.ecs — server-side HTML lib | 43,816 | DELETED |
| org.adempiere.ui — shared UI base | 18,349 | DELETED |
| org.adempiere.pipo + .handlers — 2-way migration | 16,713 | DELETED |
| org.idempiere.webservices — SOAP/REST | 11,572 | DELETED |
| org.adempiere.server — scheduler / daemon | 11,285 | DELETED |
| org.adempiere.install — installer | 10,366 | DELETED |
| org.adempiere.base.callout | 8,997 | FOLD |
| org.compiere.db.{oracle,postgresql}.provider — JDBC | 7,185 | DELETED |
| org.adempiere.replication[.server] | 3,410 | DELETED |
| org.adempiere.report.jasper | 3,354 | DELETED |
| org.idempiere.printformat.editor | 2,752 | DELETED |
| org.adempiere.eclipse.equinox.http.servlet — OSGi HTTP | 2,677 | DELETED |
| tablepartition · hazelcast · keikai · felix.webconsole · sso.oidc · plugin.utils · payment.processor · event.test | 5,144 | DELETED |
| … + ~40 smaller modules (UI widgets, adapters, gateways) | 53,693 | mostly DELETED |
| Total | 1,427,147 |
5 · Open caveats — claims without a measured benchmark
- SAP S/4HANA fold — BLOCKED.
build/erp/sap_fold.logsays§SAP-FOLD NOT-RUN (skeleton ready; gated on oracle access). No real SAP O2C+FI export has been folded; only SAP Business One (B1) against a hand-authored MOCK has (build/erp/b1_fold.log). The "SAP" column is therefore partly mock, partly not-run — never present S/4HANA as proven. The plan is published and the target fixed: the ACDOCA Fold Plan documents how S/4HANA's Universal Journal (ACDOCA) + document-flow graph (VBFA) map onto the engine's own one-journal/op-log shape, and the Cross-ERP Rosetta Stone now carries an SAP/ACDOCA column in its concept matrix — but both are doctrine ahead of a run, not evidence. The fold stays NOT claimed until a real S/4HANA oracle is foldednewVerbs=0. - Odoo / SAP server-side period-close timing & down-window — no measured number; marked architectural. We have our own 2.68 s/40k-op figure but no head-to-head legacy batch-close time.
- Odoo / SAP server round-trip latency (ms) — not measured here. The closest real datum is the
iDempiere depreciation run (
DepreciationPerf.md: per-rowsaveEx≈ ~1M round-trips), and thefeedback_erp_perf_claimsmatrix (REMOTE per-txn 2–5 orders, RTT-bound) — both iDempiere-flavoured, not Odoo/SAP. Cite as illustrative, not as an Odoo/SAP measurement. - Postgres per-op floor vs our per-op is a primitive-only comparison (no callouts/posting/JVM on
either side) —
bench_oplog_pg.logstates this explicitly; do not extrapolate to whole-document cost. - Live-DB → SQLite (143 MB → 43 MB) was measured on a static dump + repo (Docker Postgres was NOT running at measure time) — see the bloat memory caveat.
- Full-conversion LOC (~68K / ~21×) — the per-bucket LOC are measured (
find/wcon~/idempiere-dev-setup/idempiere, 2026-06-08), but the 5–8× fold-compression ratio on the irreducible business core — and the share ofM*that is real logic vs accessor/lifecycle ceremony — are estimates (no full port exists to measure them). Headline the conservative ~21× forecast (range ~21–30×); ~51× is the measured built-so-far engine shell (28,184 LOC, 2026-06-12), a high-compression slice (~1% of the M-class logic) that does not extrapolate. - DR / TCO model constants — the unit costs (314 B/op snapshot; fold, restore-to-op, per-branch additivity)
are measured; the year-level storage/compute/bill figures are derived over modelled constants for the
traditional side (no Postgres on the bench):
DB_BYTES_PER_ROW=230(SQLite, no index — Postgres+index ≈ 1.5–3× higher),ROWS_PER_OP=5(real iDempiere order-complete ≈ 10–20),IO_RESTORE=200 MB/s,3 always-on VMs. All chosen conservative for us (a higher real value widens the gap, not narrows it). The illustrative bill uses public list prices (~Jan-2026, volatile) and excludes DB licence + DBA labour. The ratios use the uncompacted 314 B/op (no shorthand) — the compression ladder (~90 B/op) widens them ~3.5×. Witness:build/erp/poc_tco_skeptic.log. - Financial Reporting —
PA_Reportstatements +AD_PrintFormat(witnessed core, named tail). The metadata-driven report layer is LANDEDmaxDiff=0c(2026-06-11):W-PA-REPORTfolds the seed's 3 real user-defined Financial Reports — Balance Sheet / Income Statement / Cash Flows (113PA_ReportLine· 17PA_ReportColumn· 93PA_ReportSource) — driven from thePA_*metadata itself, not hardcoded, proven on the in-app bundle-alone path; andW-PRINTFORMAT(foldPrint) reproduces the real Invoice master-detailPrintDatatree, 8/8 seed invoices vs live base tables + the stored grandtotal real iDempiere wrote. Both are the strong fold-vs-independent-product class, not a tautology (the Trial-Balance base caseΣDr=ΣCr=46574.97,test_report_fin.js§REPORT-FIN, came first). The Jasper /ReportEnginestack (~38K LOC) stays deleted — reports render as a browser fold of the journal, not a server print pipeline. The honest REMAINDER: the other 13T_*scratch-table folds (each pending its own witness), unexercised group-by/count/avg break functions (implemented, no seed format uses them), pixel layout (non-goal — DOM is the render). Spec + verdicts:ReportingFold.md.
Roadmap — serious read¶
Migration is the on-ramp, not the destination. The kernel folds forward into new op-log-native apps — the same ledger, no new server. Both shipped 2026-06 and run LIVE on GitHub Pages:
- uniCenta POS reborn — LIVE → POS Addon Spec — the 2012 Unicenta/AutoBOMOrder
loop with the middleware deleted, now witnessed end-to-end on the live app: ring → ONE signed group
(WR order + shipment + invoice, all-or-none, hash-chained) → BOM backflush → replenishment enacted
to closure (suggestion → PO to a real seed vendor → receipt → on-hand rises to the unit → the
suggestion clears,
newVerbs=[]); the live ring posts to the cent (§POS-CENT maxDiff=0c) and void/reverse nets postings to 0c with on-hand restored. Doctrine in The POS Lens; user guide → ERP User Guide. - Warehouse mobile walk — LIVE → Spatial Picking Spec — a phone-first
pick "walk the aisles" app over the same tenant: the warehouse compiled as a BIM-like model (the
same BOM recursion that compiles a building), fly-to + lens to the next bin, QR scan as the one
clean act, every pick a signed op;
qtyOnHandand the ERP window agree to the unit (W-WH-LIVE). - BIM → Project Order — LIVE → spec — the fold generalizes beyond
accounting. Any selection in the BIM Find panel folds into an iDempiere
C_Project(4D schedule fromsequence_rules.json, 5D cost from the active rate pack) → Generate-PO; delivery (C_ProjectIssue) folds back as an "Actual" schedule for a split-screen as-planned-vs-as-built Time Machine; rollback is the same history dots (crudFoldBack), scoped to the project. The same kernel that migrates an ERP now folds a building. The Find→ERP deep-link is wired live — a priced selection pushes its Project into Kernel-ERP as an order, with status + audio on every push outcome (#401); the Hospital BIM Project Order now ships baked into the GardenWorld seed (W-GW-HOSP-FOLD, #415) so it opens with the tenant. Note: an earlier in-window 3D embed (the viewer fused inside the ERP's Project window) was deliberately retired (W-STRIP-EMBED, #409) — cross-surface flow now goes forward as a loosely-coupled cross-tab message - URL cold-launch, BIM and ERP staying separate surfaces over one signed op-log, not a fused viewer.
That loose coupling is now a closed 360 loop (PR #462, LIVE): the project plays as a 4D/5D
construction twin — the Time Machine reads the stored
PlannedAmt↔CommittedAmtvariance off the records (not a re-computation;W-PC-TWIN-SOURCE), a stacked cost-element S-curve folds the manufacturing orders as the cursor scrubs (16PP_Orders, every Material/Labor/Burden/Overhead bucket summing to the phase'sPlannedAmtto the rupiah;W-SHOP-SCURVE), and selecting a model element lights its matchingC_ProjectLineback in the ERP over the sharedConnectbus (W-CONNECT-ERP) — one identity across viewer and ERP, no server. The manufacturing orders also fall into the standard kanban (drag = a signedSET_STATUS, no special-casing) and a general pivot cross-tab, both the same fold rather than new code. This unifies onto one op-log what is normally four tools — Primavera (schedule/EVM) · Unifier (cost) · Synchro (4D) · CostX (5D); the honest claim is the unification + the log-native what-if, not a CPM scheduling engine (no resource levelling / critical-path depth). - System Administration as op-logged genesis — LIVE — the same fold reaches upstream, to where a
tenant is born. We reproduce iDempiere's System Administrator (role 0 / System) on the engine, with
its Initial Tenant Setup rewritten as genesis: the wizard births a tenant as a signed op-log
and posts it to the cent, in-browser (#356), and a born tenant becomes a login-able resident
client (#359). The canonical path is System-only — System login → menu "Initial Tenant Setup" →
the wizard embedded in the chrome, gated so a client admin cannot mint companies (
W-GENESIS-SYSADMIN-LIVE, #397). Two surfaces iDempiere's own System role has no serverless analogue for were added: a System Monitor + login-info panel (#406) and a Plugins & Releases page — a gated release/update surface iDempiere lacks (#408) — plus a surgical "Reset demo/seed ERPs" that rebuilds only the seed band and keeps born tenants (W-SEED-RESET, #413). The point is the same one fold: a company's birth is just the genesis of its own op-log.
Items 1–2 are the two ERP objectives stated in the bim-ootb README; both landed downstream of the migration this paper measures — possible precisely because the kernel is the same fold whether it is migrating an existing ERP or running a new app on it. Item 3 is the shared BIM↔ERP objective — a single parallel op|view history timeline across the BIM building and the ERP context — now wired live (the in-window viewer embed was tried then retired in favour of loose cross-surface coupling). Item 4 carries that same op-log discipline upstream, to a tenant's birth. (The wider roadmap also carries one BIM-only objective: a 2D grid editor.)
Further Reading
The on-ramp ends here. To see how each claim is built:
- ERP.md — the "AD-in-a-browser" blueprint: how the iDempiere Application Dictionary is
folded from SQLite and rendered as a live client, the six verbs (
CREATE_DOCUMENT / CREATE_LINE / SET_STATUS / POST / ALLOCATE / MATCH) every document flow reduces to, and the full engine reference. Start here if you want the whole architecture. - HolyGrail.md — the end-state vision and its "hard parts": multi-site sync, durability on disposable hosts, and compaction = the period-close signed checkpoint = balance b/f you just saw. Read this for where the whole effort is converging and why these were the hard problems.
- OpLogERP.md — the event-sourcing model in one page: why the authoritative state is a signed, hash-chained op-log and the current numbers are a deterministic fold of it — not a row in a server DB. The shortest explanation of "the log is the truth."
- DistributedERP.md — the serverless / secured doctrine + adversarial contention map: the server→serverless table behind the "0 round-trip" claim, the Git-remote "host is disposable" analogy, and the honest counter-arguments. Read this for the distributed-systems reasoning and the proof scripts.
- BIMERPPaper.md — the "why / provenance" piece (Redhuan Oon, 30 years of ERP): the motivation, the lineage from iDempiere/Adempiere/Compiere, and what problem this is really solving.
Status
DRAFT (2026-06-08, currency pass 2026-06-21: the Kernel-ERP rebrand + iDempiere-fidelity surface, the AD_Process fold lane, genesis / System Admin, the live BIM→Project push, and the closed 360 BIM↔ERP loop — 4D/5D variance twin + shopfloor cost-element S-curve + cross-surface record-light). The evaluator-facing companion to the deep papers (ERP.md · DistributedERP.md · BIMERPPaper.md). Every number here traces to a real source file (path cited per cell); where no head-to-head number exists, the cell says so — nothing is invented.
Footnote sources¶
-
✓ = oracle-equivalent fold, witnessed
maxDiff=0cagainst real GardenWorld data (the 16 green fold witnesses + 4 declarative diffs). Engine + witnesses on branchfeat/erp-substrate-phase012:scripts/(poc_fold_*.js · poc_pa_report.js) +build/erp/; each carries a load-bearing §FALSIFIER and is graded in FoldEngineQuality.md. "adapter written/unused", "spec'd", and "not built" mean exactly that — code present but not wired, design only, or absent. ↩↩ -
build/erp/test_kernel_period_close.log—§PCLOSE-FOLDarchived=15→live=1,§PCLOSE-RECONCILE … maxDiff=0c, tamper/forgery/determinism all PASS on the real kernel. ↩↩↩ -
build/erp/period_close_drive.log— in-browser drive:close N=20000 closeFold=2681.8ms archived=40000 live=1;bootstrap fromCkpt=0.90ms fromGenesis=47.70ms speedup=53.0x same=true;reconcile maxDiff=0c. ↩↩↩ -
build/erp/poc_tco_skeptic.log—W-TCO-HARDENED(scripts/poc_tco_skeptic.js): measured 314 B/op snapshot + fold/restore; two backup strategies (Retail) weekly-incremental 244× / minimal 30×; per-branch-fold additivitymaxDiff=0c; billable-resource inventory (>10× cheaper excl. licence + labour); double-sale trade bounded to the ≤0.1% shared op-class, value-tiered. Model constants per GAPS #7 (conservative for us). ↩↩↩↩ -
build/erp/poc_blackout_resume.log—W-BLACKOUT(scripts/poc_blackout_resume.js): 50 branches, total blackout + relay drive lost (no backup), rebuilt from each branch's own slice to an identical signed tip, booksmaxDiff=0c, idempotent re-push (acc=0); the CAS-arbitration sliver is named + ledger-routed;§ORDER-HONEST— disjoint folds commute, cross-branch CAS order is not reconstructible from signed logs alone. ↩↩ -
docs/DistributedERP.md§0 (server→serverless table, lines 53–85) + §10 lines 467–468 ("no per-interaction network round-trip (the kernel answers locally)"). ↩↩ -
build/erp/bench_oplog_pg.log— N=1000 ops, one atomic commit: sql.js 208.45 ms (0.2084 ms/op, incl. sha256 chain); Postgres durable WAL+fsync 5.24 ms (0.0052 ms/op). Explicitly "NOT a head-to-head". ↩↩ -
build/erp/poc_remote_pos.log—§RPOS: local op-group fold 0.01 ms/sale (167,219 sales/s). Networked legacy per sale = RTT + measured Postgres per-doc; locals MEASURED, the network leg is a transparent model, and legacy EXCLUDES iDempiere ORM/OSGi so it is a floor: LAN 0.5 ms → 256–674×, metro 10 ms → 1,844–10,205×, cross-region 50 ms → 8,533–50,338×, intercontinental 150 ms → 25,255–150,669×. ↩ -
docs/DepreciationPerf.md— iDempiere 40-year asset depreciation: per-rowsaveExthrough the PO layer ≈ ~2 DB round-trips × ~480 periods/asset ≈ ~960/asset → a base of thousands of assets ≈ ~1M round-trips (recalled ~20 min). The cost is the round-trips, not the maths. ↩↩ -
build/erp/sync_poc_smoke.log— 5,000 events: naive 9,390 ops/s; batch commitGroup 22,492 ops/s = 2.4× (corroboratedsync_poc_prod_smoke.log). ↩ -
build/erp/poc_volume_ceiling.log— append/fold stay LINEAR; largestFit=20,000,000 ops, ~437 B/op retained; fold ~40.8M ops/s hot at 5M. ↩ -
bloat memory (
reference_bloat_reduction.md, Java side measured 2026-06-06 from~/idempiere-dev-setup/idempiere; JS side re-measured 2026-06-12 as the dedup union ofbuild/erp+origin/main:erpnon-lib non-min JS) — seed 45.2 MB → 26.1 MB full-width (≈1.7×; the earlier 12.7 MB/3.5× column-slice left windows unreachable — completeness won, bim-ootb #265); 1,427,147 Java LOC → 28,184 JS LOC / 132 files engine shell (≈51× built-so-far — was 76× at 18,614/60, the ratio falls as real coverage grows; ~21× at conservative full parity, ~68K JS). Full evidenceinternal/BLOAT_MEASUREMENT.md. ↩↩↩↩↩↩↩↩↩↩ -
same memory — LIVE GardenWorld DB Postgres 143 MB on-disk → 43 MB SQLite (925 tables, 187,133 rows, ≈3.3×); gzip 11.7 MB (3.7×). ↩↩↩↩↩
-
build/erp/odoo_fold_live.log—§ODOO-FOLD-LIVE PASS: live odoodemo (Odoo 17, :8069) SO S00023, 5/5 hops mapped, newVerbs=[], total 5002.50 == oracle, GL ΣDr==ΣCr. ↩↩ -
build/erp/b1_fold.log—§B1-FOLD PASS: SAP Business One O2C + OJDT/JDT1, 5/5 hops, journal 770.00==770.00. Source = a hand-authored MOCK Service-Layer shape (user-authorized 2026-06-05), NOT a real export. ↩↩ -
build/erp/sap_fold.log—§SAP-FOLD NOT-RUN/BLOCKED — awaiting a REAL SAP oracle. No fold claimed.(S/4HANA). ↩ -
docs/DistributedERP.md§0 lines 74–80 (the Git analogy — log is truth, host disposable) + the signed-checkpoint/tamper proofs in 2. ↩↩↩ -
Architectural property of a server-of-record ERP — no comparable single measured number in this repo; stated as structure, not benchmarked. Honest-caveat doctrine:
feedback_erp_perf_claims. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
The server→serverless mapping + per-line proofs:
docs/DistributedERP.md§0 ("From server to serverless — what moved where"). POCs live inscripts/poc_*.js(poc_distributed.js,poc_kernel.js,poc_chain.js,poc_sign.js,poc_persist.js,poc_remote_pos.js,poc_postings.js); witnessed logs underbuild/erp/. ↩ -
The ≈925-table → 5-relation reduction:
docs/DistributedERP.md§"domain reduction" ("iDempiere AD (925 tables,M*classes) → 5 tables + deterministic verbs") +docs/OpLogERP.md("≈925 tables reduce to five relations plus verbs") +docs/ERP.md§12 (the 5 core tables) +docs/FeatureComparison.md("5 core tables: containers, items, documents, document_lines, journal"). ↩ -
iDempiere swept 2026-06-08 via
find … -exec cat {} + | wc -lover~/idempiere-dev-setup/idempiere(4,465 files, 1,427,147 LOC; same tree as 12). Key buckets: generatedX_*=345,490,I_*=227,100; ZK web UI=189,786; tests=78,389; baseM*models=198,679;Doc_*posting=12,789; acct engine=21,443; tree-wideprocess/=69,782; costing=22,056; payment=7,359; tax=2,507; matching=1,959; allocation=1,472; callouts=10,340; validators=3,336; plus server-side HTML lib 43,816, print/report+Jasper ~21K, import/migration ~25K, webservices/server/installer/JDBC/OSGi. Disjoint buckets sum to 1,427,147. The 5–8× fold ratio and the ceremony fraction ofM*are estimates (GAPS #6), not measured. ↩↩