Skip to content

Migrate & Compare (ERP)

The Server Is Dead
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 lifecycleDocAction (prepareIt · completeIt · voidIt …) walked by the DocumentEngine FSM.
  • The posting idiomDoc_Order turns the document into fact_acct lines; every document has a Doc_* 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.
Proven, not assumed: the other ~25 document classes are isomorphs of MOrder, each walked as a measured delta and diffed to zero. The ~5 deep exceptions: 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

And the carrier floats at 10,000 tills! 10,000 TILLS · STORES ON A WAN HEAD OFFICE · WAREHOUSE · BOOKS TO THE PENNY 2 messages a day PER TILL ☀ MORNING — REPLENISHMENT OUT (HQ → TILLS) 🌙 NIGHT — REPORTS IN (TILLS → HQ)
See the full animated one-pager →  ·  proof you can run: W-POS-WAN-SCALE
The six interlocked things it takes  
Double-entry ledger
Luca Pacioli · codified 1494
The books are a fold of postings — Σdebit ≡ Σcredit.
▸ Our journal is a fold — ΣDr ≡ ΣCr poc_postings.js
The log is the truth
Linus Torvalds · git, 2005
Hash-chained, signable history no central machine owns; the host is disposable.
▸ The signed op-log + verifyChain() — “the host is a Git remote.”
Event sourcing / replay
Martin Fowler · Greg Young · 2005
State = deterministic replay of an append-only log.
▸ The kernel folds the log OpLogERP.md
The active data dictionary
Jörg Janke · Compiere → iDempiere
An app can describe itself as data — tables, windows, rules.
▸ The 925-table AD rides as data, folded through 5 relations + verbs.
Hash trees + public-key signatures
Merkle · Diffie–Hellman · RSA
A fact can carry its own integrity and authenticity anywhere.
▸ ECDSA-P256 signed ops; tamper caught on replay poc_sign.js · poc_chain.js
SQLite, embeddable
D. Richard Hipp
A full SQL engine with no server — and now WASM.
▸ Folds the log locally, inside the browser tab.

Thesis — 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.

A chess scoresheet folds back into the board position
The scoresheet (the log) folds back into the position (the state) — lose the board, keep the sheet, rebuild it exactly.

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 rowsfoldStatement 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 · server of record≥1 round-trip / gesture
browser · thin client
user gesture
renders only the row it’s sent back
— network —
▼ request▲ rendered row
server of record · owns the truth
app server
databaseowns the truth
posting / validation
Ours · the browser is the server0 round-trips · read/fold
browser · local & complete
user gesture → op
local WASM kernelcommit · hash-chain · sign — the log is the truth
replay / fold SQLite-WASM, in-memory → paint · 0 network
— network · crossed only later —
⇣ async · disposable
owns nothing
dumb facilitatordisposable host

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).

✗ server of recorddeleted
▾   every job redistributes to four things that own nothing   ▾
the signed op-log
the kernel on each client
signatures + hash-chain
user’s channel + dumb facilitator

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:

git · source code
ours · ERP transactions
hash-chained, signed commit history
hash-chained, signed op-log
every clone holds the whole history
every client folds the whole log
verifies it · rebuilds from it
verifyChain() · folds the live state
GitHub = a convenience, not the truth
the host = a disposable relay, not the truth
+ the one thing git lacks, we add: invariant enforcement — no double-spend — via the owner-gate + a single compare-and-set op-class

Full doctrine + the hard multi-writer cases (shared stock, credit limits, client version skew): DistributedERP.md §0, §9.


Zero round-trips on the read path. Up to ~50,000× faster cross-region.
An ERP normally crosses the network for every gesture. The kernel answers locally and relays later — so the real win isn’t faster storage, it’s no network on the hot path. On-box, durable Postgres beats us per-op (5.24 ms vs 208 ms for 1,000 ops) — and we say so. But an ERP is never on-box: every interaction crosses the network to the server of record, RTT-bound, and blocks when offline. Our kernel answers locally (~0.01 ms/op) and relays async, so per sale the legacy round-trip costs 256–674× at 0.5 ms LAN and ~8,500–50,000× cross-region, while ours stays flat. Bootstrap from a signed checkpoint is ~53× faster than genesis replay (0.90 ms vs 47.70 ms); batched commit hits ~22,500 ops/s; the fold stays linear to 20M ops. On footprint: a 26.1 MB full-width seed (1.7× smaller than the 45.2 MB dump) and 28,184 JS LOC against 1,427,147 Java. The win isn’t faster storage — it’s zero network on the hot path. Serious read
Back up the recipe, not the result. 244× less DR storage.
Like a chess scoresheet, the op-log stores the moves, not the board — so meeting the same backup guarantee (restore any of the last 50 days, survive primary loss) costs a sliver of the space. Hold the durability guarantee constant — restore to any of the last 50 days, RPO ≤ 24 h, survive primary loss — and ask only what it costs over a year of 50-branch ops. A standard weekly-full + daily-incremental rotation needs 192 GB; the 50-day op-log needs 0.78 GB244× less (30× even against a storage-minimal 1-full-plus-50-diffs scheme), because a snapshot must keep re-storing the whole database while the op-log never stores a base image. Per device it stays tiny: a branch carries ~13 MB, only the single full-replica bucket holds the whole recipe (~0.8 GB for 50 days). And recovery is brutal-simple — lose the relay entirely, no backup, and 50 branches rebuild the consolidated books to the cent from their own slices, to an identical signed tip. Unit costs measured on the real kernel; legacy figures derived on constants chosen conservative for us. Serious read
Matched against the iDempiere oracle on 43 surfaces — to the cent. 1% is the DNA, the rest is noise — not feature parity, by design.
Every fold is diffed against real iDempiere output (maxDiff=0c), each carrying a load-bearing falsifier; money is exact decimal, not float. The engine’s output is diffed against real iDempiere output, not asserted — 43 surfaces oracle-equivalent: 16 cent/unit-exact folds (maxDiff=0c — the whole order→ship→invoice→match→pay→allocate trade loop, inventory movement→on-hand→replenish, inter-org GL + FX, and reverse/void, in both USD and EUR schemas), 6 declarative engines diffed against the live iDempiere Postgres / its compiled classes to diff=0 (the last one — the workflow state engine — fell 2026-06-12 to 11 real traces; no declarative surface remains undiffed), and 21 model-layer surfaces — the beforeSave + DocAction-FSM walk of every document class (MOrder archetype → the deep family → the full isomorph tail; legal-action sets and transitions diffed against a runtime parse of the actual DocumentEngine.java, saves replayed against the stored seed rows). Each witness carries a load-bearing §FALSIFIER — corrupt the rule and the diff must go non-zero — so a pass can’t be a tautology. Money never touches float: amounts accumulate as integer cents, the only non-integer steps multiply in BigInt off the exact-decimal rate, rounded HALF_UP — bit-equal to Java BigDecimal. But this is plainly not feature parity: only ~1% of the M-class logic is ported. The win is delivery/definition — the AD is self-describing, the whole server/build stack is gone. Serious read

Legacy writes data just to read it. The fold writes zero temp rows.
iDempiere ships ~15 T_* scratch tables — a report INSERTs rows into a temp table, then reads them back. A fold computes the cells in memory and discards them. iDempiere reporting creates data in order to read it: a process INSERTs aggregated rows into a per-run temp table, then the print format reads them back. There are ~15 such scratch tables — T_Report (57 refs), T_InventoryValue (53), T_CashFlow (22), T_TrialBalance, T_Aging… A fold deletes the whole tier: because state is a deterministic reduce over the journal, the report computes its cells in memory and paints them — zero temp rows. Proven, not asserted: foldStatement reproduces all three seed statements — Balance Sheet, Income Statement, Cash Flow — to the cent (W-PA-REPORT, maxDiff=0c) by replacing T_Report with an in-memory matrix; foldPrint reproduces the real Invoice print tree across all 8 seed invoices (W-PRINTFORMAT); and NinjaExcel delivers a third path — a user-authored .xlsx template (3-sheet: BACKUP / Input / Process) filled from the same fold engine, verify-by-example against the BACKUP column to the cent (W-NINJA, maxDiff=0c, 9 cells); a phrase-to-SQL compiler (W-NINJA-RULE) builds Process rows from natural-language phrases using only vocabulary extracted from the AD dictionary — no LLM — sample-falsified so a plausible-but-wrong phrase is caught. The remaining 13 T_* scratch folds are structurally the same — named here, each pending its own witness. Serious read
A full migrated Odoo client behind one URL — 38 partners · 35 products · 27 orders, no install.
The resident demo tenant carries the whole Odoo catalog and opens from a single link with no server. Separately, the live extraction agent still pulls one order-to-cash chain — that gap is extractor wiring, not engine. The bundled Odoo demo tenant is the full catalog — 38 partners · 35 products · 27 sale orders (build/erp/12-odoo.db), installed lazily when you pick it from a five-tenant front door — no dialog, no ?shard=, no server (P0, erp sw v692). Separately, the live JSON-RPC migrate agent against a running Odoo 17 currently pulls 1 of 27 sale orders (one order-to-cash chain) and no master data — the rest (the other 26 orders, the 13 POs, real payments) is the extraction gap: the engine already folds these flows (each with a passing witness), the records just aren’t wired into the live extractor yet. The smaller fold gap is now narrow — analytic accounting (data-blocked: 0 analytic postings) and Odoo’s server actions (all 64 are Python code type — no declarative subset to interpret). The coverage-by-capability table, the block-for-block completeIt listing, the full LOC-to-parity estimate (~25× conservative, 76× for the shell built so far), and the open caveats are all below. Serious read
Snap-and-sell POS, a warehouse-walk app, and a BIM→ERP project push. Same ledger, no new server — all LIVE.
Migration is the on-ramp, not the destination. The kernel has already folded forward into three op-log-native flows — all shipped and running on the same ledger. Migration is the on-ramp, not the destination — and the downstream flows are no longer roadmap: they run LIVE today on the same ledger, no new server. (1) uniCenta POS reborn — ring → ONE signed group (order + shipment + invoice, all-or-none) → BOM backflush → replenishment enacted to closure (suggestion → PO → receipt → on-hand rises → suggestion clears); the live ring folds to the cent (§POS-CENT maxDiff=0c) and void/reverse nets postings to 0c with on-hand restored — and the till now runs the whole shop: snap + scan + key the price and a new product registers as ONE signed group then sells immediately (§POS-IMPORT); hold/recall parks a real DR order in the same ledger the Sales-Order window reads (no private store); and a "with-pick" shipment doctype gates completion behind the warehouse confirm — on-hand moves at the pick, not the promise (W-WH-CONFIRM, W-POS-DELIVERLATER) — POS Addon Spec · User Guide. (2) A phone-first warehouse pick walk — the warehouse compiled as a BIM-like model, fly-to the bin, QR scan as the one clean act, every pick a signed op; on-hand and the ERP window agree to the unit (W-WH-LIVE) — Spatial Picking Spec. (3) BIM → Project Order — a priced building selection folds into a real iDempiere C_Project tree (phases · tasks · priced lines): on the Duplex, 6 phases · 9 tasks · 16 issued lines whose PlannedAmt folds to the 5D golden to the cent (§PROJ_FOLD plannedAmt==golden, BigDecimal), phase SeqNo tracing to sequence_rules.json, the › ERP button wired live (W-PROJ-FOLD/PUSH/SEQ, PR #316) — BIM → Project blueprint. The first two are the two ERP objectives in the bim-ootb README; all three are the same fold, delivered precisely because the kernel is identical whether migrating an existing ERP or running a new app on it. Serious read
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 constantrestore 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 us230 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-HONEST shows 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 class43 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.jsbuild/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.jsbuild/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_ReportT_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 AgingT_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.db queries (not asserted): the interpreter-coverage ladder6 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 — the W-AD-*-LIVE family) — and the equivalence axis43 surfaces match the real iDempiere oracle (16 cent-exact · 6 declarative diffed to the live Postgres · 21 model-layer beforeSave+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 fold maxDiff=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 corpusorg.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 blockingthe 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 generatorsProjectGenOrder (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 foldsM_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 ~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
  1. SAP S/4HANA fold — BLOCKED. build/erp/sap_fold.log says §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 folded newVerbs=0.
  2. 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.
  3. Odoo / SAP server round-trip latency (ms) — not measured here. The closest real datum is the iDempiere depreciation run (DepreciationPerf.md: per-row saveEx ≈ ~1M round-trips), and the feedback_erp_perf_claims matrix (REMOTE per-txn 2–5 orders, RTT-bound) — both iDempiere-flavoured, not Odoo/SAP. Cite as illustrative, not as an Odoo/SAP measurement.
  4. Postgres per-op floor vs our per-op is a primitive-only comparison (no callouts/posting/JVM on either side) — bench_oplog_pg.log states this explicitly; do not extrapolate to whole-document cost.
  5. 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.
  6. Full-conversion LOC (~68K / ~21×) — the per-bucket LOC are measured (find/wc on ~/idempiere-dev-setup/idempiere, 2026-06-08), but the 5–8× fold-compression ratio on the irreducible business core — and the share of M* 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.
  7. 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.
  8. Financial Reporting — PA_Report statements + AD_PrintFormat (witnessed core, named tail). The metadata-driven report layer is LANDED maxDiff=0c (2026-06-11): W-PA-REPORT folds the seed's 3 real user-defined Financial Reports — Balance Sheet / Income Statement / Cash Flows (113 PA_ReportLine · 17 PA_ReportColumn · 93 PA_ReportSource) — driven from the PA_* metadata itself, not hardcoded, proven on the in-app bundle-alone path; and W-PRINTFORMAT (foldPrint) reproduces the real Invoice master-detail PrintData tree, 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 / ReportEngine stack (~38K LOC) stays deleted — reports render as a browser fold of the journal, not a server print pipeline. The honest REMAINDER: the other 13 T_* 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:

  1. 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.
  2. 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; qtyOnHand and the ERP window agree to the unit (W-WH-LIVE).
  3. 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 from sequence_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
  4. 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 PlannedAmtCommittedAmt variance 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 (16 PP_Orders, every Material/Labor/Burden/Overhead bucket summing to the phase's PlannedAmt to the rupiah; W-SHOP-SCURVE), and selecting a model element lights its matching C_ProjectLine back in the ERP over the shared Connect bus (W-CONNECT-ERP) — one identity across viewer and ERP, no server. The manufacturing orders also fall into the standard kanban (drag = a signed SET_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).
  5. 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


  1. ✓ = oracle-equivalent fold, witnessed maxDiff=0c against real GardenWorld data (the 16 green fold witnesses + 4 declarative diffs). Engine + witnesses on branch feat/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. 

  2. build/erp/test_kernel_period_close.log§PCLOSE-FOLD archived=15→live=1, §PCLOSE-RECONCILE … maxDiff=0c, tamper/forgery/determinism all PASS on the real kernel. 

  3. 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

  4. build/erp/poc_tco_skeptic.logW-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 additivity maxDiff=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). 

  5. build/erp/poc_blackout_resume.logW-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, books maxDiff=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. 

  6. docs/DistributedERP.md §0 (server→serverless table, lines 53–85) + §10 lines 467–468 ("no per-interaction network round-trip (the kernel answers locally)"). 

  7. 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". 

  8. 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×. 

  9. docs/DepreciationPerf.md — iDempiere 40-year asset depreciation: per-row saveEx through 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. 

  10. build/erp/sync_poc_smoke.log — 5,000 events: naive 9,390 ops/s; batch commitGroup 22,492 ops/s = 2.4× (corroborated sync_poc_prod_smoke.log). 

  11. 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. 

  12. 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 of build/erp + origin/main:erp non-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 evidence internal/BLOAT_MEASUREMENT.md

  13. 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×). 

  14. 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. 

  15. 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. 

  16. build/erp/sap_fold.log§SAP-FOLD NOT-RUN / BLOCKED — awaiting a REAL SAP oracle. No fold claimed. (S/4HANA). 

  17. docs/DistributedERP.md §0 lines 74–80 (the Git analogy — log is truth, host disposable) + the signed-checkpoint/tamper proofs in 2

  18. 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

  19. The server→serverless mapping + per-line proofs: docs/DistributedERP.md §0 ("From server to serverless — what moved where"). POCs live in scripts/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 under build/erp/

  20. 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"). 

  21. iDempiere swept 2026-06-08 via find … -exec cat {} + | wc -l over ~/idempiere-dev-setup/idempiere (4,465 files, 1,427,147 LOC; same tree as 12). Key buckets: generated X_*=345,490, I_*=227,100; ZK web UI=189,786; tests=78,389; base M* models=198,679; Doc_* posting=12,789; acct engine=21,443; tree-wide process/=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 of M* are estimates (GAPS #6), not measured.