ERP Rules & Processes — Coverage Matrix ("have we covered all the bases?")¶
Companion to: Migrate & Compare (ERP) (the §estimate buckets, now measured — its 4-state status panel is the at-a-glance view of this matrix: 🟢 folds-today · 🟠 extraction · 🔴 fold-gap · 🔵 deleted-by-architecture) · The Holy Grail (the DocAction corpus) · iDempiere logic-admission model (the six layers this audit scores). This page is the exhaustive enumeration those three reference.
Prompt: prompts/ERP_RULES_AND_PROCESSES.md (§M-1 of prompts/SERVERLESS_HARDENING_RESUME.md).
Method: every surface enumerated in both homes — CODE (~/idempiere-dev-setup/idempiere, a live checkout) and
AD DATA (build/erp/ad_full.db, 927 tables, snapshot 2026-05-29) — and mapped to our browser engine
(build/erp/*.js). Three legs per surface: AD count (real sqlite3 query, cited) · code home (path + wc -l) ·
engine handling (file:fn or absent). Verdict: ✅ COVERED / 🟡 PARTIAL / ⛔ GAP. No number is invented — each
traces to a query or a find … | wc -l. Counts are a snapshot; re-run the cited query to refresh.
Gap-closure governance. How a ⛔/🟡 row here becomes ✅ is governed by
GapClosureSpec.md(the oracle protocol · §FALSIFIER law · gap taxonomy · Definition-of-Done) and executed via the operational backlogprompts/GAP_CLOSURE_LANE.md(prioritized gaps + verified recon, repo-local). A row flips only on a real oracle diff (maxDiff=0c), never a claim.
Headline¶
| verdict | count | meaning |
|---|---|---|
| ✅ COVERED | 7 | the UI-bridge lane flips (2026-06-11, prompts/UI_UNPARK_RESUME.md, bim-ootb PR #264 / sw v647): AD_Role + AD_Window_Access (W-AD-ACCESS-LIVE — role login + grant-pruned menu: Admin 294/332 · User 163/332 · WebSvc 0/332) · AD_Process_Access + AD_Form_Access (W-AD-MENU-PRF-LIVE — P/R/X leaves pruned by real grants per role, admin-only leaves vanish for GardenUser in the live DOM) · C_DocType FSM (W-AD-DOCFSM-LIVE — FSM-gated doc-action bar on the record form, legal sets exact incl. no-VO-on-completed + zero-actions-on-closed, chip transitions, real periodcontrol probe) · AD_Field·DisplayLogic (W-AD-DISPLAYLOGIC-LIVE — 27 of 60 Sales-Order fields hidden by real AD logic in the live form via the proven evaluator) · AD_Process (W-AD-PROC-LIVE 2026-06-11, bim-ootb PR #267 / sw v650 — menu P/R leaf + procSet-gated ?process= deep link dispatch through the W-PROC spine on the live ad_seed.db; param dialog folded from real ad_process_para with the prepare-gate REJECT on-screen (§PROC_PARAM_VALIDATE); registered classnames run real rows (233 rows=52, 110 rows=1); unregistered classname → the honest absent-handler card, 333 falsifier) |
| 🟡 PARTIAL | 32 | static demo slice + the AD logic-expression evaluator (ad_evaluator.js, W-LOGIC-EVAL): the 7 Display/ReadOnly/MandatoryLogic surfaces now parse 100% (3044/3044 boolean rows) and evaluate correctly; wired into crud_overlay.validateField/effectiveFlags (headless-proven). + the AD role/access gate (ad_access.js, W-ACCESS): the 6 security surfaces (Role/AccessLevel/Window/Process/Form_Access/EntityType) now interpret 1303 window + 1309 process + 145 form grants + 6 accesslevels across all 5 roles headless (build/erp/poc_access.log). + the AD_Process dispatch spine (ad_process.js, W-PROC): SvrProcess (§A) + AD_Process (§B) move ⛔→🟡 — classname→handler-registry→prepare/validate-params→doIt, 5 handlers registered, 22/476 procs dispatched (report procs fold via report_overlay), 454 named-deferred (build/erp/poc_proc.log). Residual: live DOM show/hide/disable render-wiring (and tab-level GridTab render); access gate not wired into live window/process render; the 454-proc SvrProcess corpus stays unported (mechanism, not corpus). + the AD_Val_Rule SQL-where interpreter (ad_valrule.js, W-VALRULE): the 332 AD_Val_Rule rows (114 static + 213 token = 327 interpretable) now substitute @token@ from context and apply the where-clause as a real filter (build/erp/poc_valrule.log); the 4 AD_Rule are SQL ruletype-Q Fact_Reconciliation rules (not Groovy), n/a-in-seed. + the AD_Column.Callout dispatch spine (ad_callout.js, W-CALLOUT): the 284 callout cols now resolve class.method→a JS registry and fire on field change → DERIVED sibling values (6 real line-callout atoms / 18 cols dispatched / 139 named-deferred; build/erp/poc_callout.log). + the model-validator timing-hook engine (ad_modelval.js, W-MODELVAL): BEFORE/AFTER × NEW/SAVE/COMPLETE validators dispatch around a record/doc action (first error aborts), real invariants ported (qty>0, order-has-lines), build/erp/poc_modelval.log. + the 2 reporting surfaces newly enumerated 2026-06-10 (PA_Report Financial Report + AD_PrintFormat, §B): the metadata-driven foldStatement is oracle-equivalent for ALL 3 statements (BS 108 + IS 148 + CF 140 cells maxDiff=0c, W-PA-REPORT ✅, bundle-alone in-app twin green) and foldPrint is oracle-equivalent for the real Invoice master-detail tree (8/8 invoices, 48 cells maxDiff=0c vs base tables + stored grandtotal, W-PRINTFORMAT ✅, 3 falsifiers; docs/ReportingFold.md §4b/§4c) — so the enumeration is now 7✅ / 32🟡 / 3⛔ of 42 (was 39🟡/3⛔; the UI-bridge lane flipped AD_Role · AD_Window_Access · AD_Process_Access · AD_Form_Access · C_DocType FSM · AD_Field·DisplayLogic ✅ via the live-UI witnesses W-AD-ACCESS-LIVE / W-AD-MENU-PRF-LIVE / W-AD-DOCFSM-LIVE / W-AD-DISPLAYLOGIC-LIVE, 2026-06-11; AD_Process flipped ✅ later the same day — W-AD-PROC-LIVE, bim-ootb PR #267 / sw v650, live dispatch wiring landed; the SvrProcess corpus row stays 🟡, 454 named-deferred unchanged). |
| ⛔ GAP | 3 | all 3 remaining ⛔ are n/a-in-seed (no data to interpret): AD_Rule (4 SQL Fact_Reconciliation rules — ruletype Q, not Groovy; target fact_acct/fact_reconciliation empty + Postgres-only SQL) + the 2 empty *_Access tables (0 rows). Every interpretable behavioural surface with seed data is now 🟡 — zero real interpreter gaps remain. |
Cross-ERP scorecard — where each tenant stands (the SAP column added 2026-06-16)¶
The headline above scores the iDempiere surfaces (the canonical model). This scorecard scores the fold of each
migrated tenant on the journey axes — so SAP can be read at a glance against the others. Verdicts:
✅ folded to the cent (oracle-diffed maxDiff=0c) · 🟡 partial (headless or stub, UI/oracle pending) ·
○ reference (master-mapping install only — public model re-keyed, no doc cycle) · ⛔ pending (named, not built).
Honest frame: only iDempiere + Odoo are doc-complete; SAP/Oracle/Dynamics are master-mapping PoCs (the campaign ahead).
| Capability axis | iDempiere | Odoo 17 | SAP S/4 (ACDOCA) | Oracle EBS | Dynamics BC |
|---|---|---|---|---|---|
| Master data (BP · Product · CoA) | ✅ canonical | ✅ 38 BP / 35 prod / 47 CoA | 🟡 /DMO/ stub (14 carriers→BP · 10 conn→Product) |
○ SCOTT EMP/DEPT | ○ CRONUS items/customers |
| Document lifecycle (O2C · FSM) | ✅ oracle (W-MORDER-FSM) | ✅ signed Complete (J5, #329) | 🟡 stub (travel DR→CO; no FI; wfmc block in 14-sap-chain.json) |
⛔ pending | ⛔ pending |
| Posting / GL (to the cent) | ✅ fact_acct 0c |
✅ J6 maxDiff=0c (#338) |
⛔ pending — the headline: fold ACDOCA, diff to the cent (ACDOCA_FOLD_PLAN S‑2, §ACDOCA-FOLD) |
⛔ pending | ⛔ pending |
| Reporting (statement · receipt) | ✅ W-PA-REPORT / receipt | 🟡 receipt ✅ · statement needs posted journal | ⛔ pending (rides the posting fold) | ⛔ pending | ⛔ pending |
| Audit by construction (op-log) | ✅ kernel_ops |
✅ folded tenant | ⛔ pending the fold (free once S‑2 lands — any folded tenant inherits it) | ⛔ pending | ⛔ pending |
SAP, read honestly: today SAP is a master-mapping + lifecycle stub (build/erp/14-sap.db · 14-sap-chain.json,
CREATE_DOCUMENT/SET_STATUS only — no POST/ALLOCATE/MATCH, the flight model has no FI). The marquee leg —
folding the Universal Journal (ACDOCA) to the cent — is ⛔ pending its oracle (a real posted SAP export; IDES is
licensed). The plan, sequencing and acceptance bar live in ACDOCA Fold Plan (S‑0 oracle →
S‑2 §ACDOCA-FOLD maxDiff=0c). The model fits unusually well (SAP's own redesign converged on one journal of effects +
VBFA flow) — what is blocked is access to a posted oracle, not the shape. No row above is green-washed.
Second axis — EQUIVALENCE (oracle-diffed), not just coverage¶
The verdicts above are coverage — does the engine interpret the surface? A second, harder axis is
equivalence — does the engine's output == real iDempiere's output? Tracked in prompts/HARDEN_MATRIX.md,
anchored on ERP_MODEL_ARCHETYPE.md (MOrder archetype + ~25 document deltas = the real
denominator, not 496 classes / 735k LOC).
| equivalence | status (2026-06-09) |
|---|---|
| ✅ oracle-equivalent | Trial-balance / posting-read — test_report_fin.js folds the real GardenWorld fact_acct (300 rows, client 11, captured from the Docker Postgres via extract_fact_acct.sh) → ΣDr==ΣCr=46574.97, maxDiff=0c vs the source. Our reporting reproduces iDempiere's posted journal to the cent. |
| ✅ oracle-equivalent | Migrated-tenant POSTING CONFIG (MIGRATE_POSTING_CONFIG → # DONE 2026-06-12, W-MIGRATE-POSTCFG) — the resolvable acct-config contract (c_bp_customer_acct/m_product_category_acct/c_tax_acct/c_acctschema_default/c_validcombination/c_elementvalue) now ships on the DEFAULT seed + BOTH migrated tenants, every account from a real source column. Client 11+13 (iDempiere): §MIGRATE-POSTCFG tokens_resolved=3/3 coverage=complete balanced=Y · derived journal == fact_acct(318) maxDiff=0c · seed carries fact_acct 300 rows TB 46574.97 (gen_ad_idmp.sh fact_acct_id re-band bug found+fixed: un-banded tenant ledger was silently dropped). Client 12 (Odoo 17): tokens_resolved=5/5 from ir_property company defaults + account_tax_repartition_line + property_stock_valuation_account_id (replaced the one previously-invented {Product.Asset} copy) · §FRAME-FIT … oracle=live odoodemo maxDiff=0c verdict=ORACLE-EQUIVALENT vs Odoo's own account_move_line. §FALSIFIERs load-bearing both paths. Shipped live bim-ootb PR #271 sw v653: §POS-CENT … maxDiff=0c + TrialBalance(310) rows=21 on the live page. Logs poc_migrate_postcfg_{idmp,odoo}.log (bim-compiler 0986251b). (Evidence row — exercises already-counted resolver/TB surfaces on new tenants; does not change the 42-surface tally.) |
| ✅ oracle-equivalent | P4 Odoo master data extraction (W-P4-MASTERS, 2026-06-14) — gen_ad_odoo.js extended to pull ALL live odoodemo master data; poc_p4_masters.js diffs shard counts vs live search_count (non-invent): C_BPartner=38/38 (all res.partner) · c_elementvalue=47/47 (all account.account) · c_tax=2/2 (sale + purchase) · account.journal=8 (logged; maps to DocTypes, not a shard entity) · M_Product=35/35 (regression). S00023 C_Order 1200001 still folds coverage:complete basis=invoice balanced=Y (regression guard). §FALSIFIER: 1-partner shard < live → gap DETECTED. Logs: build/erp/poc_p4_masters.log exit 0 · build/erp/gen_ad_odoo.log (§P4-EMIT bpartners=38 elementvalues=47 purchaseTax=15% journals=8). (Evidence row — extraction coverage, not a new fold surface.) |
| ✅ oracle-equivalent | P4 Odoo live buy-side fold (W-P4-BUYSIDE-LIVE, 2026-06-14) — upgrades poc_odoo_fold_3way.js (static oracle) to a LIVE connection; poc_p4_buyside_live.js pulls P00011 chain from the running odoodemo (PO P00011 → WH/IN/00006 receipt → BILL/2026/06/0002 → 3 GL lines) and folds it: §LIVE-STATIC live totals == odoo_oracle_p2p.json (export faithful) · §BUY-FOLD 4 P2P events committed via dispatch (mapped=5/5 incl. match) · §MATCH 3-way via erp_engine.match (2 calls: receipt↔bill + PO↔bill, miPairs=poPairs=nLines=1) · §GL-BALANCE ΣDR==ΣCR=6596.40 maxDiff=0c vs live AML · §NEW-VERBS newVerbs=[] (Odoo P2P folds with existing 6 verbs) · §FALSIFIER corrupt partner → 0 pairs (settlement-boundary enforced). build/erp/poc_p4_buyside_live.log exit 0. (Evidence row — live buy-side proof; buy-side architecture not a new matrix surface.) |
| ✅ oracle-equivalent | Per-document GL derivation (H-1 keystone, W-POST-HARDEN) — poc_post_harden.js now diffs post_resolver's DERIVED lines against the oracle fact_acct re-captured WITH ad_table_id/record_id/line_id granularity (schema 101, the 8 C_Invoice docs). Result: 4/4 sales invoices resolve iDempiere's EXACT posting accounts (518/758/596), 3/4 oracle-equivalent to the cent (diff=0c), 0 derivation-gaps; the 4th (doc 109) is a named post-posting amount-drift (line 127 posted 254.00, source later edited to 215.90 on 2004-01-04 — fact_acct=posting-time, source=edited, so current-source derivation legitimately can't reproduce the historical journal). §FALSIFIER (revenue→receivable) = maxDiff=5035c. build/erp/poc_post_harden.log. Residual: schema-200000 (EUR) + purchase-invoice manifest deferred (same fold, different token set). |
| ✅ oracle-equivalent | completeIt(C_Order) full posting chain (F-1 keystone, W-FOLD-COMPLETE) — poc_fold_complete.js folds MOrder.completeIt onto the op-log (ONE signed op-group: SET_STATUS CO + config-gated Ship/Invoice fan-out via buildDoc) and diffs the Order→Ship→Invoice journal: fan-out lines == oracle m_inoutline/c_invoiceline; invoice posting == fact_acct(318) (3/4 cent + 1 named post-posting drift); shipment COGS/Inventory == fact_acct(319) (amount from m_costdetail, accounts from master); DocStatus=CO; maxDiff=0c; §FALSIFIER load-bearing. build/erp/poc_fold_complete.log. |
| ✅ oracle-equivalent | Doc_Payment receipt (F-2 MPayment, W-FOLD-PAYMENT) — poc_money_post.js: DR {Bank.InTransit}/CR {Bank.UnallocatedCash} = payamt; 2/2 == fact_acct(335) maxDiff=0c; §FALSIFIER drops DR → diff≠0. build/erp/poc_money_post.log. |
| ✅ oracle-equivalent | Doc_AllocationHdr incl. VAT tax-correction (the Money DEEP half, W-FOLD-ALLOC) — poc_alloc_post.js: per SO-invoice line DR {Payment.UnallocatedCash}|{CashBook.CashTransfer} + DR {BPGroup.PayDiscount} + DR {BPGroup.WriteOff} / CR {BPartner.Receivable}, then the proportional tax-correction sub-cents (round(tax/total×{disc,wo}) per invoice header fact line — alloc 101 = 0.11/0.02). 2/2 == fact_acct(735) maxDiff=0c (7-posting + 2-posting docs); §FALSIFIER-A drop tax-correction → maxDiff=13c, §FALSIFIER-B truncate≠HALF_UP (10c≠11c). This folds the §FOLD-DEFERRED named by W-FOLD-PAYMENT. build/erp/poc_alloc_post.log. Residual: schema-200000 (foreign-ccy) — now folded (W-FOLD-ALLOC-FX, next row). |
| ✅ oracle-equivalent | Foreign-currency Doc_AllocationHdr — the 2nd acctschema (W-FOLD-ALLOC-FX) — poc_alloc_fx.js folds the SAME line manifest into schema 200000 (EUR, ccy 102; source USD 100) with the two schema-deltas: (a) each fact leg's accounted amount = round(AmtSource × multiplyRate, 2 HALF_UP) — rate read from c_conversion_rate (default Spot, valid on dateacct: 0.85 on 2002-02-22), multiplied in BigInt off the exact TEXT-preserved decimal so HALF_UP can't float-drift; (b) TaxCorrectionType='N' → no VAT sub-cents — then Fact.balanceAccounting posts ONE CurrencyBalancing line (c_acctschema_gl.currencybalancing_acct → 724) absorbing the per-doc accounted imbalance (alloc 101 = 0.01 CR). 2/2 == fact_acct(735) schema 200000, maxDiff=0c. §FALSIFIER-A drop the balancing line (1c), §FALSIFIER-B the date-invalid 0.80064 rate (8373c≠7886c). Closes the §DEFERRED named by W-FOLD-ALLOC. build/erp/poc_alloc_fx.log. |
| ✅ oracle-equivalent | StorageOnHand QTY spine (the inventory fold, W-FOLD-QTYONHAND) — erp_engine.qtyOnHand/movementSign reconstruct on-hand = Σ(sign(MovementType)×|qty|) from the real movement ledger m_transaction (28 events) and diff TWO independent iDempiere oracles: (a) the sign rule reproduces every stored signed movementqty (28/28); (b) the per-cell accumulation == m_storageonhand.qtyonhand (20/20 cells, maxDiff=0) — separate code paths (MTransaction.create vs MStorageOnHand.add), so it proves no movement dropped/double-counted. Source-decomposed (receipt +401 / shipment −15 / internal-movement 0); §FALSIFIER-A flip-polarity, §FALSIFIER-B drop-movement both fire. The spine the backflush DECREMENT + replenishment trigger ride. build/erp/poc_qtyonhand.log. |
| ✅ oracle-equivalent | Inter-org M_Movement posting (W-FOLD-MOVEMENT) — poc_movement.js folds Doc_Movement for a cross-org transfer (locator 101/org 11 → 102/org 12): amt = round(qty × cost, 2), DR/CR {Product.Asset} (to/from-org inventory) + DR {Schema.IntercompanyDueFrom} / CR {Schema.IntercompanyDueTo} (the intercompany bridge). Cost selection (non-invent): c_acctschema.costingmethod ('A'=Average) → the m_costelement with matching costingmethod (103 Average PO) → m_cost.currentcostprice (product 123 = 51.45 → 4×51.45 = 205.80). 1/1 == fact_acct(323) schema 101, maxDiff=0c. §FALSIFIER-A swap Intercompany Due-To/From (20580c), §FALSIFIER-B use the Material/Standard cost element not the schema Average (10980c). The cost-selection rule is the same one M_MatchInv (472) needs. build/erp/poc_movement.log. Residual: schema-200000 — now folded (W-FOLD-MOVEMENT-FX, next row); same-org movement (no intercompany leg) absent in seed. |
| ✅ oracle-equivalent | Inter-org M_Movement in the EUR schema 200000 (W-FOLD-MOVEMENT-FX) — poc_movement_fx.js folds the SAME Doc_Movement manifest at the schema-200000 cost. Unlike Doc_AllocationHdr, a movement does NOT convert a source amount: m_cost holds a SEPARATE per-schema cost (200000 = EUR 43.7325 for product 123 vs 101's USD 51.45), so the posting is round(qty × that schema's cost) with no conversion step. The cost is carried at FULL 4-decimal precision and the LINE amount rounded — 4 × 43.7325 = 174.93, not round(43.73) × 4 = 174.92; schema 101's 2dp cost masked this, schema 200000's 4dp cost exposed a latent 1c round-cost-first bug now fixed in poc_movement.js too (output-identical at schema 101). 1/1 == fact_acct(323) schema 200000, maxDiff=0c. §FALSIFIER-A swap Intercompany Due-To/From (17493c), §FALSIFIER-B round per-unit cost to cents before ×qty (1c — the exact bug schema 200000 caught). build/erp/poc_movement_fx.log. |
| ✅ oracle-equivalent | M_MatchInv posting — the matched-clearing loop (W-FOLD-MATCHINV) — poc_matchinv.js folds Doc_MatchInv: DR {BPGroup.NotInvoicedReceipts} = round(matchQty × PO price) / CR {Product.InventoryClearing} = round(matchQty × invoice price) — multiplied at full price precision then rounded (price 2.975 × 30 = 89.25, not 89.40). 18/18 == fact_acct(472) schema 101, maxDiff=0c, including the 1 variance match (doc 100, PO 30 / invoice 20 → IPV 100): the avg-cost split rides the qty spine — onHandAtMatch = Σ m_transaction.movementqty up to the match date (product 130 = 7 of matchQty 10) → round(IPV × min(onHand,qty)/qty) = 70 {Product.Asset} / 30 {Product.AverageCostVariance}. §FALSIFIER-A swap NIR↔InventoryClearing (36000c), §FALSIFIER-B all-IPV→Asset ignoring the on-hand cap (3000c). build/erp/poc_matchinv.log. Completes the PO match→clearing loop in full — the deepest item that had been the named NEXT. Residual: schema-200000 — now folded (W-FOLD-MATCHINV-FX, next row). |
| ✅ oracle-equivalent | M_MatchInv in the EUR schema 200000 (W-FOLD-MATCHINV-FX) — poc_matchinv_fx.js folds the SAME USD source manifest (NIR/Clearing + the avg-cost IPV split riding the qty spine) then converts each fact leg independently to EUR — amtacct = round(amtsource × multiplyRate, 2 HALF_UP), rate read from c_conversion_rate (default Spot, valid on the match date: 0.85), multiplied in BigInt off the exact TEXT-preserved decimal (the W-FOLD-ALLOC-FX rule). At 0.85 every leg converts exactly (300→255.00, the 70/30 IPV split → 59.50/25.50) so no currency-balancing residual arises (contrast poc_alloc_fx.js). 18/18 == fact_acct(472) schema 200000, maxDiff=0c (incl. the 1 variance match). §FALSIFIER-A post the USD source as the EUR amount / skip conversion (4500c), §FALSIFIER-B all-IPV→Asset ignoring the on-hand cap (2550c). build/erp/poc_matchinv_fx.log. The PO/inventory trade loop now folds to the cent in both acctschemas. |
| ✅ oracle-equivalent | Standalone completeIt(C_Invoice) doc-action (W-FOLD-INVOICE) — erp_engine.completeInvoice emits [SET_STATUS C_Invoice CO] for every invoice (8/8) + the PO-side MatchInv fan-out gated on !IsSOTrx && line.M_InOutLine_ID<>0 — the emitted M_MatchInv set == real m_matchinv per (invoiceline,inoutline) tuple, 18/18 junctions across PO invoices 102/104/105/106 (one CREATE_LINE op per junction, newVerbs=0). Completed SALES invoices fold to fact_acct(318) maxDiff=0 (3/4; doc 109 = named post-posting drift). §FALSIFIER-A corrupt-junction, §FALSIFIER-B SO-emits-no-MatchInv. build/erp/poc_invoice_complete.log. Residual: PO-invoice GL value-derivation (V_Liability token set) — now folded (W-FOLD-AP-INVOICE, next row). |
| ✅ oracle-equivalent | Vendor Doc_Invoice GL derivation — the purchase manifest (W-FOLD-AP-INVOICE) — poc_invoice_post_ap.js derives the AP-invoice GL via NEW post_resolver tokens {Vendor.V_Liability} (per-vendor c_bp_vendor_acct, mirrors customer receivable) + {Product.InventoryClearing} (matched-receipt item lines → 51400, via product→category, mirrors {Product.Revenue}) + {Tax.Credit} (input VAT, zero in seed). 4/4 vendor invoices (102/104/105/106) == fact_acct(318) per (account,side), maxDiff=0c, resolving iDempiere's EXACT accounts (CR 419/749 vendor payable / DR 780 Inventory Clearing). §FALSIFIER-A misderive clearing→liability (20000c), §FALSIFIER-B flip Dr/Cr sides (20000c). build/erp/poc_invoice_post_ap.log. This completes the PO-invoice GL half that W-FOLD-INVOICE named-deferred — Doc_Invoice now folds on both sales and purchase sides. Residual: charge-line + service-product expense DR absent in seed (all 4 lines are matched item receipts); schema-200000 deferred. |
| ✅ oracle-equivalent | Δ-B replenishment PO (ReplenishReport, W-FOLD-REPLENISH) — the engine derives on-hand by FOLDING m_transaction (rides the qty spine, never reads m_storageonhand) per product within the warehouse's locators, applies the exact ReplenishReport QtyToOrder formula (type-1 reorder-below-min / type-2 maintain-max), and emits the PO through the EXISTING erp_engine.buildDoc archetype (newVerbs=0). 8/8 products == iDempiere's own formula-SQL over m_storageonhand, maxDiff=0 (a different execution path + on-hand source). PO op-group = 1 CREATE_DOCUMENT(C_Order,PO) + 8 CREATE_LINE. §FALSIFIER-A max↔min (20), §FALSIFIER-B type-1-always-order (16≠8). build/erp/poc_replenish.log. |
| ✅ oracle-equivalent | Manual GL_Journal posting incl. inter-org balancing (W-FOLD-GLJOURNAL) — poc_gljournal.js folds Doc_GLJournal: each line posts directly (amtacct = round(amtsource × currencyrate, 2), derived not copied — reproduces the stored amtacct), THEN the load-bearing half — GardenWorld's journal is INTER-ORG (DR Checking@org11 / CR Checking@org12, same natural account 508 via different C_ValidCombinations), so Fact.balanceAccounting adds per-org Intercompany Due-To(600)/Due-From(741) lines via the SAME c_acctschema_gl rule proven in W-FOLD-MOVEMENT (net>0 → CR Due-To / net<0 → DR Due-From). 2/2 journals == fact_acct(224) maxDiff=0c across BOTH acctschemas (101 USD + 200000 EUR). §FALSIFIER-A swap Due-To/From (10000c), §FALSIFIER-B drop per-org balancing → single-org (10000c) — proves it is NOT the near-tautological direct-post. build/erp/poc_gljournal.log. Residual: foreign-RATE journal (rate≠1) converts identically to W-FOLD-ALLOC-FX (degenerate rate=1 in seed). |
| ✅ oracle-equivalent | reverseCorrect / void DocAction family — ORACLE-ANCHORED (W-FOLD-REVERSE) — poc_reverse.js ENACTS the reversal our seed never posted: it re-derives each completed doc's FORWARD posting from source (the proven post_resolver paths), re-confirms it == the REAL fact_acct (anchor holds), feeds it to NEW pure verb erp_engine.reversePosting (swap Dr↔Cr, never reads the books), and proves the engine's reversal summed with the real original NETS TO ZERO in every account — iDempiere's reverseCorrect contract (books return to pre-document state). 6/6 docs (C_Payment 100/101 + vendor C_Invoice 102/104/105/106) annihilate to the cent (residual=0c, swapDiff=0c); FSM CO→RE asserted (pure ad_docfsm.transition RC/RA→RE, VO→VO). reverseAccrual posting == reverseCorrect (only the booked date differs — date-shift named-deferred: no c_period in this extract). The transform is iDempiere's, the anchor is real client-11 data — non-tautological. §FALSIFIER-A same-sign (no swap) → doc DOUBLES (19700c), §FALSIFIER-B wrong reversal account (9850c). Enacted in a SANDBOX copy; the read-only anchor stays CO. build/erp/poc_reverse.log. |
| ✅ oracle-equivalent | AD_Val_Rule SQL-where engine (H-3, W-VALRULE-HARDEN) — poc_valrule_harden.js drives 10 real AD_Val_Rule rows through ad_valrule.js (token substitution + clause application) and diffs the resulting row-membership set SQLite (ours) vs the live iDempiere Postgres (docker idempiere, GardenWorld client 11 — the seed our ad_full.db was extracted from): 10/10 rules, every membership set matches, diff=0 across master-data + transactional tables (e.g. id 100 AD_Column.AD_Table_ID=318→76, id 200047 AD_Table.IsView='N'→914, id 106 @AD_Client_ID@→11→2). Two INDEPENDENT db engines (SQLite vs Postgres) over the same source, our JS token-substitution standing in for Env.parseContext — non-tautological. §FALSIFIER flips the operator (IsSOTrx<>'Y' vs oracle ='Y') → diff=8 (load-bearing). build/erp/poc_valrule_harden.log. Residual: @SQL= dynamic-subselect rules + live lookup render-wiring (parked UI bridge). |
| ✅ oracle-equivalent | AD_Ref_Table FK engine (H-3, W-REFERENCE-HARDEN) — poc_reference_harden.js diffs ad_reference.js against the live Postgres oracle on BOTH legs per reference: (1) resolution — our readRefTable (fkTable,keyCol) == the oracle's ad_ref_table⋈ad_table⋈ad_column join; (2) membership — the FULL keyCol id-set of the resolved FK table, ours vs oracle, diffed (a diff=0 over the whole set proves fkExists equivalent for EVERY id). 12/12 references, resolution + membership all match, diff=0, including the 26,519-row ad_column set exact + a fkExists(real)=true/fkExists(bogus)=false probe each. §FALSIFIER diffs ref-4's members (ad_reference, 606) against a wrong table (ad_column, 26519) → diff=27125. build/erp/poc_reference_harden.log. (The applyVFormat mask leg is a pure Java-port string check with NO independent SQL oracle → named-deferred, not faked.) |
| ✅ oracle-equivalent | AD role/access gate — MRole (H-3, W-ACCESS-HARDEN) — poc_access_harden.js diffs ad_access.js against the live iDempiere Postgres MRole oracle: for all 5 roles × {window,process,form} the engine's buildRole access map (id→readWrite) is diffed against the MRole.getXxxAccess SQL (SELECT id,IsReadWrite FROM AD_*_Access WHERE AD_Role_ID=? AND IsActive='Y') — 15/15 maps, diff=0 over the real 1303+1309+145 grant rows (e.g. role 102 window=413, process=406). The canView AccessLevel-SCO switch is cross-checked against the INDEPENDENT bitmask-intersection definition over 42 combos (6 real table-levels × 7 valid userLevels), 0 mismatches (pure Java logic ⇒ no live-JVM oracle; the empty out-of-domain userLevel where case '7'→always-true legitimately diverges is excluded). §FALSIFIER injects a bogus grant (+1) → diff=1 (load-bearing). build/erp/poc_access_harden.log. Residual: AD_Column_Access/AD_Record_Access empty in seed (n/a); live menu/window gate render-wiring (parked UI bridge). |
| ✅ oracle-equivalent | AD_Column.Callout derive engine (H-3, W-CALLOUT-HARDEN) — poc_callout_harden.js diffs ad_callout.js's derived values against the live iDempiere Postgres c_orderline (the values CalloutOrder wrote at entry) over the FULL 27-line population: CalloutOrder.product reproduces the price-list PriceActual/PriceList == stored on 27/27 lines (0 overrides); CalloutOrder.amt LineNetAmt=round(PriceActual×Qty) == stored on 26/27 — the 1 residual (line 119: 2.975×30=89.25 vs stored 89.40) is NAMED price-precision/source-drift (stored amt computed from a since-refined price, the doc-109 pattern), the derive contract intact. §FALSIFIER corrupts the qty → derived 367.03 ≠ stored 215.90 (load-bearing). build/erp/poc_callout_harden.log. Residual: the 139 named-deferred callout atoms (10k-LOC corpus) + live field-change render-wiring (parked UI bridge). |
| ✅ oracle-equivalent | Per-document oracle-capture fidelity (H-1.1, W-FACTACCT-DOC) — the granular fact_acct capture (ad_table_id/record_id/line_id) == the LIVE idempiere_test ROW BY ROW keyed by fact_acct_id: 300/300 rows identical on (table/record/line/account/schema/DR/CR, cents), line_id populated exactly where the oracle has one (180/180), and reconciles to the 46574.97 aggregate anchor (same journal, not a fork). §FALSIFIER drop-a-row fires. This is the fixture universe every per-document H-1 diff reads. build/erp/poc_factacct_doc.log. |
| ✅ oracle-equivalent | MOrder Doc_Order posting + the order chain at LINE granularity (H-1.2, W-MORDER-POST) — Doc_Order derives the oracle's ZERO fact set for all 8 real orders, GATED by the real config (commitmenttype='N' both schemas — MAcctSchema.java:635/649; oracle fact_acct(259)=0 rows); §FALSIFIER-A flips the gate (N→S) → the engine emits commitment legs ≠ the oracle's 0, proving zero is config-derived. Then the order's whole chain diffed per (record, line_id, account, side) in cents — finer than W-FOLD-COMPLETE's per-account totals: shipments (COGS/Asset @ m_costdetail), receipts (DR Asset / CR NotInvoicedReceipts @ qty×PO-priceactual), sales AND vendor invoices (header legs line_id=0, line legs per c_invoiceline_id) — 7/8 chains diff=0c (order 108 = the named doc-109 post-posting drift), 0 derivation-gaps. §FALSIFIER-B +1c on one line fires. build/erp/poc_morder_post.log. |
| ✅ oracle-equivalent | Cost-valued inventory GL as an ENGINE VERB (P2.3, W-FOLD-INOUTGL) — build/erp/report_inout_gl.foldInOutGL LIFTS the witness-local shipment derivation into a reusable build/erp/ verb (per GAP_CLOSURE_LANE §6 consume-don't-fork), driven through the post_resolver seam, folding Doc_InOut for both seed polarities over ALL completed M_InOut: C- shipment COGS DR {Product.Cogs}/CR {Product.Asset} @ posted m_costdetail (327.50) + V+ vendor receipt DR {Product.Asset}/CR {BPGroup.NotInvoicedReceipts} @ round(qty×C_OrderLine.PriceActual) (9209.00, the PO-price NIR basis, same as Doc_MatchInv's NIR leg). Both maxDiff=0c vs fact_acct(319) schema 101. §FALSIFIER-A drop COGS DR (32750c), §FALSIFIER-B bend PO price (V+ diff≠0), §FALSIFIER-C swap DR/CR (32750c). Closes the named §FOLD-DEFERRED. build/erp/poc_fold_inout_gl.log. |
| ✅ oracle-equivalent | MOrder.beforeSave invariants (H-1.3, W-MORDER-SAVE) — MOrder.java:1183-1396 ported as 11 ad_modelval hooks (installMOrderSaveHooks, each citing its Java lines): 8/8 stored orders ACCEPT with 0 derived contradictions (the stored row IS the oracle — real iDempiere wrote it through this very beforeSave); stripped defaults re-derive the STORED value 8/8 for Bill_BPartner/Bill_Location (:1272) and C_Currency←price-list (:1292); price-list (:1282) / payment-term (:1315) / doctype-target (:1311) defaults derive the Java query result with EXPLICIT user picks named, never failed silently; foreign BP-location CLEARED not rejected (:1243). All 4 cited reject paths fire on the right hooks: client=0 (:1195) · warehouse-mandatory (:1206) + the ctx fallback accepts (:1208) · prepay+cash (:1373, proven CONJUNCTIVE — real cash orders 100/102 stay accepted) · CannotChangePl (:1352, change-gated). build/erp/poc_morder_save.log. Residual: :1361 price-list-version date twin (no m_pricelist_version in capture), named. |
| ✅ oracle-equivalent | MOrder FULL DocAction FSM (H-1.4, W-MORDER-FSM) — ad_docfsm.legalActionsOrder/transitionOrder (the additive per-table C_Order port) == an oracle PARSED FROM THE CHECKOUT AT RUNTIME (non-tautological: DocumentEngine.getValidActions:1008-1090 generic+Order blocks incl. the canReactivateThisDocType doctype gate; the action methods' m_status outcomes; MOrder deltas prepareIt→IP :1705 / completeIt→CO :2245 / prepay→WP :2145 / reverseCorrectIt=voidIt :3016 / reverseAccrualIt NOT IMPLEMENTED :3042): 23 legal-set fixtures + 12 outcome fixtures + 8/8 stored-order docstatus replays, all diff=0. The measured narrowing this surface exists for: a completed ORDER offers CL/VO/(RE) — never RC/RA/PO (reversal rides inside voidIt). Vocabulary ⊂ captured ad_ref_list 131/135. §FALSIFIER-A illegal PR@CO rejected; §FALSIFIER-B inject RC@CO → set-diff vs the parsed oracle fires. build/erp/poc_morder_fsm.log. |
| ✅ oracle-equivalent | MInOut.beforeSave invariants (H-2.1, W-MINOUT-SAVE) — MInOut.java:1304-1370 as 5 ad_modelval hooks (installMInOutSaveHooks): 9/9 stored inouts ACCEPT with 0 derived contradictions; stripped defaults re-derive the stored value 9/9 for MovementType ←doctype (:1306 ∘ getMovementType:1275-1287 — MMS→C-/V-, MMR→C+/V+, the class-defining delta) and DeliveryRule empty→Availability (:1322); SalesRep←order (:1357) = a measured SOURCE-EVOLUTION drift: all 9 stored rows hold NULL (pre-:1356 seed), the port derives the ORDER's rep 7/7 == c_order.salesrep_id (also real), named not faked. Rejects fire: warehouse-org conflict on a NEW record (:1311, REAL org-12 warehouse 104; the newRecord gate proven — same conflict as an UPDATE accepts) · Order+RMA both set (:1326). build/erp/poc_minout_save.log. Residual: RMA-doctype derive + shipper/freight (data-absent), Force→Availability arm (no disallow-neg-inv warehouse). |
| ✅ oracle-equivalent | M_InOut per-table DocAction FSM (H-2.1, W-MINOUT-FSM) — ad_docfsm.legalActionsFor(319)/transitionFor (the table-keyed H-2 generalization) == the runtime-parsed oracle (gate-aware docfsm_oracle.js parse of DocumentEngine.getValidActions:1016-1106 + MInOut method deltas): 154 fixtures diff=0 — legal sets across 11 statuses × periodOpen × backDate × 3 doctypes (CO = CL + RC gated periodOpen∧backDate + RA ungated — never VO/RE/PO), outcomes (prepareIt→IP, completeIt→CO :2196, reActivateIt NOT IMPLEMENTED :2970, voidIt@CO delegates to RC/RA → lands RE :2649-2672 + DocumentEngine:616-618 preserve), 9/9 stored replays. §FALSIFIER-A VO@CO rejected; §FALSIFIER-B inject RC into the period-closed set fires. build/erp/poc_minout_fsm.log. |
| ✅ oracle-equivalent | MInvoice.beforeSave invariants (H-2.2, W-MINVOICE-SAVE) — MInvoice.java:1144-1283 as 8 ad_modelval hooks: 8/8 stored invoices ACCEPT with 0 contradictions (incl. the :1257 Processed-skip of the currency-rate block); BP-location ←billto/payfrom 8/8 MUST + term/pricelist/rule == the captured c_bpartner so/po master columns (:631-673); C_Currency←pricelist 8/8 MUST; pricelist/doctype-target(ARI|API :804-822)/paymentterm defaults derive the Java query with EXPLICIT picks named; SalesRep←ctx only; the EUR doc 109 unprocessed+override=N → CurrencyRate CLEARED. Rejects fire: CannotChangePlIn (:1219, product lines + change-gated) · FillMandatory CurrencyRate (:1265, the REAL EUR invoice vs the captured ad_clientinfo→primary-schema hop). build/erp/poc_minvoice_save.log. Residual: DateInvoiced pricelist-version twin (no m_pricelist_version), payment-term re-apply (write-path), setBPartner contact derive. |
| ✅ oracle-equivalent | C_Invoice per-table DocAction FSM (H-2.2, W-MINVOICE-FSM) — engine == runtime-parsed getValidActions:1108-1125 + MInvoice deltas: 153 fixtures diff=0 — the NESTED gate structure parsed from source (CO → if(periodOpen){ RE if canReact · RC if backDate } + RA: period-closed kills BOTH RE and RC even when back-date is allowed — a different nesting than InOut, MEASURED via the gate sweep), outcomes (prepareIt→IP :1814, completeIt→CO :2347, reActivateIt IMPLEMENTED → RE lands IP, voidIt@CO→RE delegation :2536), 8/8 stored replays. §FALSIFIER-A RE on the real react=N ARI doctype rejected; §FALSIFIER-B inject RC into the period-closed set fires. build/erp/poc_minvoice_fsm.log. |
| ✅ oracle-equivalent | MPayment.beforeSave invariants (H-2.3, W-MPAYMENT-SAVE) — MPayment.java:670-830 as 10 ad_modelval hooks; K=2 stated honestly (the whole stored universe): 2/2 payments ACCEPT with 0 contradictions (IsPrepayment formula + IsReceipt←doctype re-derive == stored); C_DocType←ARR default 2/2 MUST; DateAcct←DateTrx 2/2 MUST; OverUnderAmt re-zeroed; AD_Org←bank-account org both arms (stored ba 100 org-0 derives nothing; REAL ba 200000 derives org 11); the cashbook-vs-bank gate is config-derived from the CAPTURED ad_sysconfig CASH_AS_PAYMENT='Y' (:237-239). Rejects fire: PaymentAlreadyProcessed (:672, per-column — a non-financial change accepts) · bank-account mandatory on the tendertype-X doc (:696) · BP-required non-cash (:719, with the :721 cash exemption proven) · BPDifferentFromBPInvoice (:790, REAL invoice 101/BP 112). build/erp/poc_mpayment_save.log. Residual: charge resets/reversal-copy/prepay-resets (data-absent), documentno + credit-card encryption (write-path). |
| ✅ oracle-equivalent | C_Payment per-table DocAction FSM (H-2.3, W-MPAYMENT-FSM) — engine == runtime-parsed getValidActions:1127-1141 + MPayment deltas: 103 fixtures diff=0 — the family's THIRD distinct gate nesting parsed from source (CO → if(periodOpen){ RC ungated-by-backdate · RE if canReact } + RA — back-date does NOT gate the Payment RC, ≠ InOut, measured cross-table), outcomes (prepareIt→IP :2006, completeIt→CO :2153, reActivateIt IMPLEMENTED→IP, voidIt@CO→RE; the on-bank-statement void→RC guard :2596 parsed + named), 2/2 stored replays (doctype 119 = the seed's one react=Y doctype, exercising the canReact arm live). §FALSIFIER-A RC on a closed period rejected; §FALSIFIER-B inject RE into a react=N set fires. Posting + allocation CITED (W-FOLD-PAYMENT/W-FOLD-ALLOC/-FX), not redone. build/erp/poc_mpayment_fsm.log. |
| ✅ oracle-equivalent | Inventory-family FSM — M_Movement + M_Inventory + M_Production (H-2.4, W-MINVENTORY-FAMILY-FSM) — engine == runtime-parsed source for all three tables (161 fixtures diff=0): Movement+Inventory share ONE source block (:1200-1213) and Production's (:1233-1244) has the same shape — CO = CL + RC(periodOpen∧backDate) + RA; per-class outcomes parsed from each M.java (prepareIt→IP / completeIt→CO / reActivateIt NOT IMPLEMENTED in all three / voidIt@CO→RE delegation). Honest reduced scope per the card: M_Movement's 1 stored doc replayed (DR-CO→CO, stored docaction CL legal) + its beforeSave doctype-default re-derives the stored 143 (MMovement.java:212-222 ∘ getOfDocBaseType:74); M_Inventory/M_Production stored-replay = ⛔ no-seed* (zero docs; fixtures would be synthesized — refused, their beforeSave reject conditions cited unported for the tail). §FALSIFIER-A RE on completed Inventory rejected; §FALSIFIER-B inject RC into the backDate-blocked Production set fires. build/erp/poc_minventory_family_fsm.log. |
| ✅ oracle-equivalent | GL Journal family FSM — GL_Journal + GL_JournalBatch (isomorph tail, W-MJOURNAL-FSM) — ad_docfsm.legalActionsFor(224/225) == the runtime-parsed SHARED Journal block (:1143-1157 — CO = CL + RC⊂periodOpen + RE⊂periodOpen∧canReact + RA, the Payment-style nesting): 207 fixtures diff=0 — legal sets (2 tables × 11 statuses × gates × 2 doctypes), per-class outcomes (MJournal.voidIt voids ONLY DR/IN :711-735, narrower than the engine's unprocessed set; MJournalBatch.voidIt ALWAYS false :540-553; RC/RA delegate→RE both classes; reActivateIt IMPLEMENTED both, journal period+doctype gated :932-963), 2 journals + 1 batch replayed (K=2+1 = the whole seed). §FALSIFIER-A VO@CO rejected; §FALSIFIER-B inject RE into the period-closed set fires. build/erp/poc_mjournal_fsm.log. |
| ✅ oracle-equivalent | MJournal+MJournalBatch.beforeSave (isomorph tail, W-MJOURNAL-SAVE) — MJournal.java:298-380 as 8 hooks + MJournalBatch.java:946-978 as 3 (installMJournal(Batch)SaveHooks): 3/3 stored docs ACCEPT with 0 contradictions; derives reproduce the stored values — DateDoc↔DateAcct mutual defaults (MUST 3/3) · C_Period←DateAcct standard-period lookup = stored 155 (MUST 3/3; PeriodNotFound reject proven on an out-of-calendar date) · GL_Category←doctype=108 (MUST) · C_AcctSchema←client primary=101 (journal 100 MUST; journal 200000 stores the EUR schema EXPLICITLY — classified) · C_ConversionType←default=114 (MUST). Reject: NEW journal into the REAL Processed batch 100 (:300-306). The :350-370 frozen gate proven on REAL ProcessedOn data through its flag-N ACCEPT arm (no overwrite-flag-Y doctype in seed — reject arm named). build/erp/poc_mjournal_save.log. |
| ✅ oracle-equivalent | C_AllocationHdr per-table FSM (isomorph tail, W-MALLOCHDR-FSM) — engine == runtime-parsed Allocation block (:1159-1172 — CO = CL + RC⊂periodOpen + RA, NO RE arm: even a react-Y doctype never offers ReActivate): 100 fixtures diff=0; outcomes (voidIt = the H-2 period-probe delegation :567-630 → VO@CO lands RE; RC/RA→reverseIt→RE; reActivateIt always false :718-732), both stored allocations replayed (K=2 = the whole seed). §FALSIFIER-A RE@CO rejected; §FALSIFIER-B inject RC into the period-closed set fires. build/erp/poc_mallochdr_fsm.log. |
| ✅ oracle-equivalent | MAllocationHdr.beforeSave (isomorph tail, W-MALLOCHDR-SAVE) — MAllocationHdr.java:305-313 is ONE guard (K=1 stated, never padded): a deactivated allocation cannot be re-activated. 2/2 stored ACCEPT with zero derives (the override has no setter — asserted); IsActive N→Y on an existing record REJECTED, the Y→N and new-record arms ACCEPTED (the !newRecord gate). build/erp/poc_mallochdr_save.log. |
| ✅ oracle-equivalent | C_Cash per-table FSM (isomorph tail, W-MCASH-FSM) — engine == runtime-parsed Cash block (:1174-1183 — the SMALLEST: CO → Void ONLY, ungated; C_Cash has no doctype column): 34 fixtures diff=0; the measured delta — MCash.voidIt→reverseIt sets DOCSTATUS_Reversed ITSELF from ANY non-terminal status (:758) and DocumentEngine preserves it (:603) → Void lands RE even from DRAFT; RC→reverseIt→RE yet never offered; RA always false; reActivateIt implemented-but-unreachable (no RE arm). 3/3 stored cash journals replayed (2 CO + 1 DR; K=3 = the whole seed). §FALSIFIER-A RC@CO rejected; §FALSIFIER-B inject RA fires. build/erp/poc_mcash_fsm.log. |
| ✅ oracle-equivalent | MCash.beforeSave (isomorph tail, W-MCASH-SAVE) — MCash.java:321-331 as 2 hooks: 3/3 stored ACCEPT with 0 contradictions; AD_Org←cashbook 101's org re-derives the stored 11 (MUST 3/3) · EndingBalance = Beginning+StatementDifference cent-exact (MUST 3/3) · CROSS-CHECK: the stored StatementDifference itself == Σ(captured c_cashline.amount) on all 3 — the derive's input is the real line fold. Reject: unresolvable cashbook → org 0 → @AD_Org_ID@ (:324-328). build/erp/poc_mcash_save.log. |
| ✅ oracle-equivalent | C_BankStatement per-table FSM (isomorph tail, W-MBANKSTMT-FSM) — engine == runtime-parsed BankStatement block (:1185-1198 — a FOURTH gate nesting: the periodOpen frame encloses BOTH arms, RE⊂periodOpen∧canReact AND VO⊂periodOpen; period closed → a completed statement offers ONLY Close): 99 fixtures diff=0; outcomes (voidIt voids unprocessed AND completed, never sets Reversed → VO@CO lands VO, no delegation — unlike InOut/Alloc; RC/RA always false :629-666; reActivateIt implemented, period+doctype gated :670-696), both stored statements replayed (K=2). §FALSIFIER-A VO@CO period-closed rejected; §FALSIFIER-B inject RE for a react-N doctype fires. build/erp/poc_mbankstatement_fsm.log. |
| ✅ oracle-equivalent | MBankStatement.beforeSave (isomorph tail, W-MBANKSTMT-SAVE) — MBankStatement.java:258-272 as 3 hooks: statement 100 (processed) ACCEPTs with 0 contradictions; C_DocType←getDocType(CMB) re-derives the stored 146 (MUST 2/2) · EndingBalance cent-exact (MUST). The draft statement 101 is the state-dependent case stated head-on: Java's :264-269 reads the bank account's CURRENT balance (148, moved by statement 100's completion AFTER 101 was saved) — the engine derives exactly what iDempiere would derive TODAY, diffed against the captured master; the stored 0s classified as save-time state, nothing skipped. Both arms of the :265 gate falsified (isProcessed blocks; nonzero not overwritten). build/erp/poc_mbankstatement_save.log. |
| ✅ oracle-equivalent | Generic-block document tail FSM — 11 classes (isomorph tail, W-GENERIC-TAIL-FSM) — every tail class with NO DocumentEngine block falls through to the GENERIC region (completed docs offer ONLY Close — the fall-through IS the narrowing): 333 fixtures diff=0 across M_RMA/M_Requisition/S_TimeExpense (seeded, 3/3 replayed — RMA's stored IP via DR-PR) + C_BankTransfer/C_DepositBatch/M_ProjectIssue/the 5 Fixed-Assets tables (⛔ stored-replay honestly n/a — 0 seed docs each, source-parse FSM only, never synthesized). Per-class parsed deltas: MRMA void = live-shipment data gate; MRequisition/MTimeExpense void→closeIt; MBankTransfer RC/RA reverse the child payments yet RETURN FALSE (the parsed quirk); MDepositBatch RE implemented; MProjectIssue delegates to DocActionDelegate WITH RC/RA callables → reversals succeed; MAssetAddition the one FA class with live void/reActivate. Vocabulary ⊂ captured AD_Ref_List 135/131 across all 11. §FALSIFIER-A VO@CO on RMA rejected (implemented in the class yet NEVER offered); §FALSIFIER-B inject RA fires. build/erp/poc_generic_tail_fsm.log. |
| ✅ oracle-equivalent | Generic-tail beforeSave — MRMA + MRequisition + the MTimeExpense absence (isomorph tail, W-GENERIC-TAIL-SAVE) — MRMA.java:256-297 as 5 hooks (BP←shipment re-derives the stored 118 MUST · currency←order-through-shipment 102 MUST · IsSOTrx-flip vs the SO shipment REJECTED · SalesRep no-derive when the shipment carries none, the stored 102 classified EXPLICIT · NEW-RMA C_Order cleared) + MRequisition.java:198-203 (M_PriceList←getDefault re-derives the stored 101 through the FALLBACK arm — no default purchase price list exists in the capture, both arms proven) + MTimeExpense: the beforeSave ABSENCE itself proven by parse (zero hooks = the faithful port; 0 fired on replay = explicit no-op). 3/3 stored ACCEPT, K=1 each stated. build/erp/poc_generic_tail_save.log. |
| ✅ oracle-equivalent | AD logic-expression evaluator — GridField display/readonly/mandatory (B-1, W-LOGIC-HARDEN) — poc_logic_harden.js diffs ad_evaluator.evaluate against the REAL compiled iDempiere evaluator classes (ANTLR SimpleBooleanParser + EvaluationVisitor from ~/idempiere-dev-setup/idempiere/org.adempiere.base/target/classes, driven headless by scripts/logic_oracle/LogicOracle.java = LogicEvaluator.evaluateLogic:68-86 minus only its CLogger static) over 2751 real (record, field-logic) pairs: every AD_Field·DisplayLogic / AD_Column·ReadOnlyLogic / AD_Column·MandatoryLogic expression whose @vars@ ground in its own table (845 distinct exprs, 225 tables), context = dep-column values of up to 5 REAL records each pulled from the live iDempiere Postgres (first 3 + last 2 by pk, never authored). display 2211 + readonly 463 + mandatory 77 = 2751/2751, diff=0; verdict population two-sided (T=985/F=1766); expression md5-sets ad_full.db == live PG (754/124/25, setdiff=0); 0 oracle parse errors (no grammar gap). §FALSIFIER flips ONE captured value (a_asset 1100001 IsOwned Y→N on @IsOwned@=Y) → verdict flips on BOTH sides (T→F, engine==oracle). build/erp/poc_logic_harden.log. Honest skips (named, counted): 3 @SQL= (Evaluator.parseSQLLogic — separate surface) · 542 window/login-context exprs (#/$/parent-tab vars, no record ground headless) · 32 no-single-pk tables · 236 zero-row tables in live PG. |
| 🟡 recipe-equivalent | Δ-A recursive BOM backflush (W-FOLD-BACKFLUSH) — erp_engine.explodeBOM on the real nested recipe (Patio Chair ×30 → Screw×480…) == independent path-enumeration oracle; multi-path accumulation invariant; flat §FALSIFIER fires. (No fact_acct oracle — m_production=0 in seed; oracle = recipe explosion.) build/erp/poc_backflush.log. |
| 🟡 rule-consistent | MProduction movement fold (W-FOLD-PRODUCTION) — poc_production.js ENACTS a production GardenWorld never posted (m_production=0, NO oracle): explodeBOM → synthesize the P+ (finished +Q) / P- (each leaf −used) MTransaction ledger → it folds through the PROVEN qty spine (movementSign/qtyOnHand) to finished +Q / leaf −used (Patio Chair ×30 → +30 / 6 leaves incl. 480 screws). Closes the StorageOnHand DECREMENT that W-FOLD-BACKFLUSH + W-FOLD-QTYONHAND named-deferred. §FALSIFIER-A flip a P-→P+ (leaf goes +used), §FALSIFIER-B flat explosion (6→4 leaves, misses sub-assemblies). GL named-deferred (leaf-component m_cost absent in seed → component-cost CR can't be valued without inventing; the cost rule is proven, the data is absent). Tier = rule-consistent, NOT "== iDempiere". build/erp/poc_production.log. |
| 🟡 rule-consistent | MInventory physical-count fold (W-FOLD-INVENTORY) — poc_inventory.js ENACTS a count (no I± in seed, NO oracle): book on-hand = FOLD of real m_transaction (== m_storageonhand) → I+/I- picked by sign(counted−book) → folding the adjustment through the qty spine lands on-hand == counted (6/6 products, gain+loss), and the GL adjustment value = |adjQty| × cost via the PROVEN cost-selection rule (Oak Tree +3 × 51.45 = 154.35) and BALANCES. Closes the MInventory I± rider qtyonhand named-deferred. §FALSIFIER-A wrong polarity (on-hand ≠ counted), §FALSIFIER-B Material cost element (24.00 ≠ Average 51.45). GL offset account named-deferred (Inventory-Gain/Loss ACCTTYPE_InvDifferences has no extractable column; the leg value + balance are proven). Tier = rule-consistent, NOT "== iDempiere". build/erp/poc_inventory.log. |
| ✅ oracle-equivalent | AD_Workflow node-walk + state engine (B-2, W-WF-HARDEN — the LAST ⬜ closed, ledger 42→43) — poc_wf_harden.js diffs ad_workflow.js's new replay arm against REAL iDempiere workflow traces in the live PG idempiere_test (11 ad_wf_process / 13 ad_wf_activity / 13 ad_wf_eventaudit written by real iDempiere; captured verbatim → build/erp/oracle/wf_oracle.json, never hand-authored): §HARDEN surface=ad_workflow fixtures=11 diff=0 oracle=iDempiere-PG-trace — node SEQUENCE + transitions taken + per-activity WFState/eventtype + process WFState all identical (10× wf131 BP-Approval UserWindow-suspend [244]→OS/SC + 1× wf116 Process_Order [183→185→186]→CC/PX std-user XOR transition; threaded docstatus ends CO == the live c_order row, §HARDEN-DOC). Definitions md5-set ad_full.db == live PG §HARDEN-SRC kind=wf setdiff=0 (58 wf/262 node/207 next/1 cond); document context EXTRACTED (C_Order AD_Column defaultvalues DR/CO, identical both schemas). Semantics arm = scripts/logic_oracle/WorkflowOracle.java (the B-1 LogicOracle headless-compiled-classes technique one level up): REAL compiled StateEngine MUTATORS agree 6/6 (engine hops ON→OR→CC/OS applied identically, closed→open REJECTED) + std-user gate == verbatim MWFNodeNext.isValidFor:215-243 over compiled DocAction constants (PO/Env/DB-dragging arms = named omissions). 2 §FALSIFIERs load-bearing (flip DocAction CO→'--' reroutes 183→184 on BOTH sides; dropped node 185 → LOUD CA abort, diff flags it). Small-K honesty: 7 §HARDEN-SKIPS lines (actions F/X/P/R/C unexercised — replay THROWS; 1 conditioned transition wf115 untraced; AND-split absent in seed 262/262 X/X; 56/58 wfs traceless; claim = "11 real processes, diff=0", not corpus-wide). build/erp/poc_wf_harden.log. |
Remaining ⬜ declarative surfaces: NONE (ad_evaluator fell to B-1 W-LOGIC-HARDEN, ad_workflow fell to B-2 W-WF-HARDEN, 2026-06-12).
The honest read: coverage 37🟡 ≠ equivalence. FORTY-THREE surfaces are now oracle-equivalent to the unit/cent
(TB-read · per-document GL derivation H-1 · completeIt Order→Ship→Invoice chain · Doc_Payment · Doc_AllocationHdr
incl. tax-correction · StorageOnHand qty · ReplenishReport PO · standalone completeIt(C_Invoice)+MatchInv ·
vendor Doc_Invoice purchase manifest · foreign-currency Doc_AllocationHdr · inter-org M_Movement + its EUR-schema
variant · M_MatchInv posting + its EUR-schema variant · reverseCorrect/void anchored-negation · inter-org
GL_Journal · + the first FOUR declarative engines H-3: AD_Val_Rule SQL-where · AD_Ref_Table FK · AD role/access MRole · AD_Column.Callout derive — each membership/verdict diffed to the live iDempiere Postgres, diff=0 · + the H-1 MOrder-archetype completion (2026-06-11, Fable lane): capture-fidelity row-diff W-FACTACCT-DOC · Doc_Order zero-set + LINE-granular chain W-MORDER-POST · MOrder.beforeSave 11-hook port W-MORDER-SAVE · MOrder full DocAction FSM vs runtime-parsed source W-MORDER-FSM · + the H-2 DEEP-delta walk (2026-06-11, Fable lane, prompts/FABLE5_H2_DELTAS.md): MInOut save+FSM W-MINOUT-SAVE/-FSM · MInvoice save+FSM W-MINVOICE-SAVE/-FSM · MPayment save+FSM W-MPAYMENT-SAVE/-FSM · the inventory-family FSM (Movement replayed; Inventory/Production source-parse, no-seed stated) W-MINVENTORY-FAMILY-FSM — every per-table legal-set/outcome diffed against the gate-aware runtime parse of DocumentEngine.getValidActions, every beforeSave against the stored rows + cited Java · + the ISOMORPH TAIL (2026-06-11, prompts/H2_ISOMORPH_TAIL.md): GL Journal family W-MJOURNAL-FSM/-SAVE · Allocation W-MALLOCHDR-FSM/-SAVE · Cash W-MCASH-FSM/-SAVE · BankStatement W-MBANKSTMT-FSM/-SAVE · the 11-class generic-block tail W-GENERIC-TAIL-FSM/-SAVE — every DocumentEngine per-table block and every generic-fall-through document class now walked; the 8 zero-seed classes source-parse-only, ⛔ stated · + the B-1 logic-evaluator oracle-diff (2026-06-12, W-LOGIC-HARDEN): ad_evaluator GridField display/readonly/mandatory == the REAL compiled SimpleBooleanParser+EvaluationVisitor over 2751 live-PG record-grounded fixtures, diff=0 · + the B-2 workflow oracle-diff (2026-06-12, W-WF-HARDEN): ad_workflow replay == 11 REAL iDempiere PG traces diff=0 + compiled StateEngine/DocAction semantics arm), plus THREE rule-consistent (backflush recipe + MProduction movement + MInventory count — enacted, no seed oracle, verified against the proven qty/cost rules + balance + falsifier); ⬜ = NONE — ad_workflow fell 2026-06-12 to W-WF-HARDEN (11 REAL PG traces diff=0 + compiled StateEngine/DocAction semantics arm, build/erp/poc_wf_harden.log) — the modelval/FSM half of the old blocker fell to the source-parse + stored-state oracle pattern and the logic half to the headless compiled-classes oracle (W-LOGIC-HARDEN). The inventory loop
folds end-to-end (movements → on-hand → replenishment PO) and the trade-doc loop folds order→ship→invoice→match→
pay→allocate — every step diffed to its iDempiere oracle. The equivalence campaign is prompts/HARDEN_MATRIX.md (H-1 MOrder archetype ✅ COMPLETE — all five surface rows of ERP_MODEL_ARCHETYPE.md GREEN-by-diff (metadata via H-3, save/FSM/posting/callout via their witnesses) → H-2 DEEP deltas ✅ COMPLETE (2026-06-11): MInOut/MInvoice/MPayment beforeSave+FSM and the inventory-family FSM walked to oracle-equivalence; posting for these classes was already folded (cited); the trade-pattern ISOMORPH tail ✅ COMPLETE (2026-06-11, prompts/H2_ISOMORPH_TAIL.md — 10 witnesses, Journal/Batch/Allocation/Cash/BankStatement blocks + the 11-class generic tail; no unwalked DocAction table remains) → H-3 the declarative engines: valrule + reference + access + callout + (B-1, 2026-06-12) the logic evaluator oracle-diffed (5 ✅ — W-LOGIC-HARDEN dissolved the live-JVM blocker: the compiled evaluator classes run headless minus CLogger); workflow ✅ B-2 DONE (W-WF-HARDEN — the idempiere_test trace was the oracle; no synthesis)).
Logic-folded % off ~0.2%: the Money delta (payment + allocation incl. the tax-correction sub-cents) AND the
inventory qty spine — both among the deepest of the 25 — are now CLOSED; the GL value-half (Doc_InOut/Doc_Invoice)
and the qty-half (MTransaction→MStorageOnHand) of the trade cycle both fold to their iDempiere oracle.
Third axis — ADDON LENSES (engine-riding verticals; separate ledger, does NOT change the 42-surface tally)¶
| lens | status (2026-06-12) | evidence |
|---|---|---|
| POS lens §P-1..§P-4 | ✅ LIVE (bim-ootb PR #269, sw v652) | W-POS-LIVE re-run on the bumped tree (build/erp/poc_pos_live.log, exit 0): §POS-LIVE open station=100 tiles=16 priced=16 handAuthored=0 · §POS-SALE lines=2 dispatch=SALE newVerbs=[] chainOk=Y gid=e3607c9a-… ops=12 sealed=12 · §POS-DOC order=910001 completeIt ok (C_Order+M_InOut+C_Invoice CO in ONE group) · §POS-LIVE-REPLENISH suggestions=8. Live to-the-cent ring CLOSED 2026-06-12 (MIGRATE_POSTING_CONFIG shipped, bim-ootb PR #271 sw v653): the default ad_seed.db now carries resolvable posting config + fact_acct — §POS-CENT live db=ad_seed.db order=910001 basis=order accounts=2 coverage=complete balanced=Y Dr=137.75 Cr=137.75 cartCents=13775 maxDiff=0c (poc_pos_live.js §4d step, frozen DocPoster/PostResolver, zero new verbs). Spec §5 bar met — the row is FULLY lit. Station wh 104 carries no policy/ledger rows — replenish legs ring wh 103 (§-named). |
| POS lens §L-1 CRUD (POS_FULL_LOOP 2026-06-12) | ✅ headless-green (W-POS-CRUD) | §POS-CRUD edit=description cols=description statusOp=none verifyChain=ok · §POS-CRUD listOptions cur=CO selected=CO DR.selected=false · §POS-CRUD docstatus-edit=VO statusOp.op_type=DOC_ACTION to=VO · §FALSIFIER no-docstatus → statusOp=null. Rides #268 CRUD rails (listOptions + splitStatusChange). build/erp/poc_pos_crud.log exit 0. |
| POS lens §L-2 void/reverse | ✅ headless-green (W-POS-VOID) | §POS-VOID order=100 CO→VO group ops=2 chainOk=Y · §POS-VOID postings-net=0c accounts=3 maxNet=0c · §POS-VOID onhand-restored=Y (product=130 before=18 afterSale=17 afterVoid=18) · §POS-VOID backflush=N/A products-are-leaves=Y · §FALSIFIER double-VO refused reason=illegal-action. W-FOLD-REVERSE recipe on the live seed. build/erp/poc_pos_void.log exit 0. |
| POS lens §L-3 replenish loop enacted | ✅ LIVE (W-POS-REPLENISH-LOOP headless; wiring shipped bim-ootb PR #274, sw v655, Pages verified CACHE_VERSION = 'v655' + live pos_lens.js carries vendorOf/buildReplenishPO) |
§POS-LOOP suggest qty=11 product=124 (Elm Tree) · §POS-LOOP vendor=114 (Tree Farm Inc.) pricepo=30 newVerbs=[] · §POS-LOOP po=CO dispatch=DR→CO ok=true · §POS-LOOP receipt=CO dispatch=DR→CO ok=true · §POS-LOOP onhand +11@locator=101 product=124 before=9 after=20 · §POS-LOOP suggestions: gone product=124 available=20 min=10 cleared=Y · §FALSIFIER-A receipt-no-po lines=0 · §FALSIFIER-B short-receive clears when available>min (po-remainder=6 noted). Vendor from real m_product_po, no invented rows. Post-merge: W-POS-LIVE re-run §POS-CENT Dr=137.75 Cr=137.75 maxDiff=0c newVerbs=[] all 5 stages green. build/erp/poc_pos_replenish_loop.log exit 0. |
| POS lens §P-9 register (killer demo, POS_ENGINE_LANE E-1) | ✅ LIVE (engine W-POS-REGISTER; live Import pill bim-ootb PR #276 sw v656) | build/erp/poc_pos_register.log exit 0: ONE signed group of 4 CRUD_CREATE ops (M_Product+M_ProductPrice+AD_Image+C_POSKey), chainOk=Y, defaults EXTRACTED (§POS-REG defaults src=dictionary cat=108 uom=100 tax=107 client=11 org=0 … entitytype=U nextseqno=180); the new tile RINGS at the keyed price through the UNCHANGED §P-2 path. Falsifiers: no-barcode · over-cap (40002B>32768B) · price-not-keyed · duplicate barcode = PROPOSE-MERGE (named decision: §P-8 scan needs barcode→ONE product; upc uniqueness NOT enforced in real iDempiere). |
| POS lens §P-10 edit (E-2) | ✅ headless-green (W-POS-EDIT) | build/erp/poc_pos_edit.log 13🟢/0🔴: price 1.00→2.50 = ONE CRUD_UPDATE carrying exactly pricestd/pricelist/pricelimit {old,new}; the NEXT ring reads 2.50 through unchanged ringLine; name edit = M_Product.name + c_poskey.name; photo swap on the EXISTING AD_Image row under the ≤32KB cap. Falsifiers: no-change edit emits ZERO ops (#268 no-op suppression) · unknown product refused · chainOk=Y. |
| POS lens §P-13 hold/recall (E-4) | ✅ headless-green (W-POS-HOLD) | build/erp/poc_pos_hold.log 12🟢/0🔴: buildSaleGroup split into buildOrderOps + completionOps halves (replay-equality by CONSTRUCTION; 7 sibling witnesses re-run green). Park = the order half alone (real DR C_Order, lists in Sales-Order-window + Kanban queries — same ledger row, NOT a private store) · recall reloads exact lines to the cent (109.25==109.25) · complete = the completion half on the EXISTING order. Falsifiers: exactly ONE C_Order (1 CREATE op in the whole log) · CO-recall refused. |
| POS lens §P-12 confirmation fold (E-5) | 🟡 RULE-CONSISTENT (W-WH-CONFIRM — like MProduction/MInventory: verbatim-source oracle, no seed fact-cycle to diff; G-3 PG-drive ⛔ see below) | build/erp/poc_wh_confirm.log 21🟢/0🔴: build/erp/inout_confirm.js, oracle = real iDempiere Java line-cited (MInOut.prepareIt:1551 spawn · completeIt:1648 gate→IP · pendingCustomerConfirmations:2203 XC-never-blocks · MInOutConfirm.completeIt:394-480 · createDifferenceDoc:604-669 · beforeSave:202 diff=target−confirmed−scrapped); anchored on the seed's OWN confirm rows. PROVEN: doctype 148 IsPickQAConfirm=Y spawns PC from the DICTIONARY row · InOut waits IP · ON-HAND MOVES AT CONFIRM-COMPLETE by the PICKED qty · SO-side short pick → NO difference doc, MovementQty REDUCED · scrap → M_Inventory diff doc · PO-side linked diff → AP credit memo. ⛔ G-3 oracle-upgrade parked (one fact): Adempiere.startup(false) cannot run headless — SecureEngine init NPEs on BaseActivator.getBundleContext()==null (Service locator needs a live OSGi BundleContext; the JUnit plugin tests run under tycho/PDE). The rollback-safe drive is WRITTEN+COMPILED (scripts/logic_oracle/ConfirmOracle.java) — runs the moment an OSGi runtime hosts it. |
| POS images folder + copy job (E-3 + G-2) | ✅ LIVE (W-IMG-FOLDER/W-IMG-SYNC headless; W-IMG-LIVE on the live page, bim-ootb PR #277 sw v657) | build/erp/poc_img_{folder,sync}.log: img_store.js UMD — content-addressed put/get/has, resolveImage tiers full→thumb→none, syncFromLog = the out-of-band COPY JOB (idempotent, missing keys REPORTED; transport NAMED explicit-export-import). G-2 closed the live fork (poc_img_live.log exit 0): imageKey = 'sha256:'+hex(SubtleCrypto digest) of the decoded bytes (the content-length stub removed), §IMG-LIVE folder=idb put=Y get=Y tier=full key=sha256:… on idempiere.html, tier walk full→thumb→none, §FALSIFIER tampered blob under a sha256 key DETECTED (§IMG-TAMPER detected=Y tier=thumb — resolveImage re-hashes before full-tier renders). |
| POS §P-12/§S-2 deliver-later sale (G-1, the seam gap) | ✅ headless-green (W-POS-DELIVERLATER, bim-compiler 5bc4b389) | build/erp/poc_pos_deliverlater.log exit 0: policy DERIVED from the doctype 132 row (SO/N/N; WR refuses → buildSaleGroup), ONE signed group = C_Order→CO + shipment via the SAME buildDoc spec born DR (NO SET_STATUS M_InOut; dictionary ship-link 132→120), invoice timing NAMED from extracted C_Order.InvoiceRule='I'; on-hand UNMOVED after the sale; the DR doc SURFACES in the §S-2 selector (docstatus IN ('DR','IP'), POS-generated) then empties after the pick; scan-commit moves on-hand by the PICKED qty (short-pick 2→1: moved 2, asked 3); confirm-demanding doctype (148) REFUSES → inout_confirm gate. Falsifiers: WR sale zero-open-docs (W-POS-WR byte-unchanged) · double-complete refused · cents maxDiff=0c. (Walk-side §S-2 selector wiring = next wave.) |
| Spatial §S-1 warehouse model | ✅ compiled (regression green 2026-06-12) | W-WH-COMPILE 11/11 bins == m_locator + W-WH-SMOKE no-cubes (§WH_SMOKE_BINS bins=11 guid==m_locator_id); db buildings/warehouse_gardenworld.db uploaded to the OCI COMMON bucket (HEAD 200, 61440 B, md5-verified). |
| Spatial §S-2 pick list → route | ✅ LIVE (bim-ootb PR #270, viewer sw v643) | W-WH-ROUTE (build/erp/poc_wh_route.log, exit 0): route = m_bom_line.ordinal walk order not line-no (§WH_ROUTE_ORDER input=[102,101,101] → route=[101,101,102] walk_seq=[1,1,7]) · deterministic + permutation-invariant (§WH_ROUTE_DET repeat=identical permuted=identical) · all lines exactly once (§WH_ROUTE_COVER steps=3/3 lineKeys=[10,20,30] each-once=Y) · §WH_FALSIFIER off-model locator → explicit unroutable tail step, dropped=NONE. Engine module build/erp/wh_route.js (UMD == viewer copy); draft via ERPEngine.buildDoc from REAL seed rows (§WH_DRAFT doc=M_Movement DR doctype=143 lines=3 qty=[4,4,4] newVerbs=[]). |
| Spatial §S-3 the walk (viewer UX) | ✅ LIVE (PR #270) | W-WH-LIVE on the phone viewport (build/erp/poc_wh_walk_live.log, exit 0, 25 verdicts 🟢, 390×844 touch): data-gated Lucide-route pill (§WH PILL gate=on on the warehouse / gate=off + #pill-whwalk absent on SampleHouse — the safe-to-ship falsifier) · per-step §WH step=i/3 locator=… fly=done lit=1 · FIND-lens depth (§WH_XRAY_DIM opacity=0.1 mats=scene:9) · long-press skip → ANNOTATE op (§WH SKIP step=3/3 reason="bin blocked") · non-target tap step-held (§WH TAP bin=50003 target=N step-held=1/3). Live Pages probe §W-WH-LIVE-PAGES PASS (build/erp/poc_wh_live_pages.log, exit 0): §BBOX_CLEARED, no §BBOX_KEEP/§BLOB_MISS, §WH PILL gate=on on the LIVE page via the COMMON-bucket db deep-link. |
| Spatial §S-4 scan = the one clean act | ✅ LIVE (PR #270; camera QR unverified on a physical phone — headless has no BarcodeDetector, no Safari/iOS guarantee) | W-WH-SCAN: wrong bin REFUSED (§WH scan=50004 expected=101 via=typed REFUSED) · typed code rides the SAME gate · honest fallback (§WH QR supported=N) · right bin → qty confirm (short-pick 3 of 4) → ONE KernelOps.commitGroup of 2 ENACT_MOVE ops (§WH PICK step=1/3 locator=101 qty=4 … ops=2 committed=true chainOk=Y). |
| Spatial §S-5 completion + the books | ✅ LIVE (PR #270; postings n/a — M_Movement carries no acct linkage in this seed, same data-gate as Posting-Preview) | W-WH-COMPLETE: last step → AdDocFsm.dispatchFor(323) DR→CO (§WH COMPLETE doc=wh-pick-1 status=CO via=dispatchFor(323) gid=wh-pick-1-complete foldKeys=4 diffs=0 chainOk=Y) · qtyOnHand fold OF THE OP LOG == expected per-(product,locator) deltas for every touched bin (§WH FOLD 123@101:-4 123@50000:4 127@101:-3 127@50000:3, skip excluded) · verifyChain ok. Walk op log = in-memory sql.js per page session (hash-chained + group-sealed, NOT IDB-persisted, no edge-signer on the viewer page — offline walk rides the §P-5 sync-FSM story, v2). |
The honest answer: no — not all the bases are covered. The engine demonstrates the fold model on a thin slice —
the CO DocAction transition, the double-entry posting fold, and a fixed receipt/TB/P&L report set — for a handful of
demo documents. The irreducible behavioural surface named in MigrateComparisonPaper §estimate
(process 54k LOC · workflow 7k · callouts 10k · validators · the logic-expression evaluator · the entire security layer)
is enumerated below and is overwhelmingly ⛔. This is what makes the ≈51× shell → ~21× conservative full parity story precise: the 51× counts
delivery/definition, not behavioural parity — most behaviour is named-and-deferred, not folded.
Engine architecture fact (sets the ceiling): all engine field-logic is descriptor-driven from a hand-authored
crud_ops.json(5 tables — c_order, m_inout, c_invoice, c_payment, c_allocationline; ~23 fields) whose keys mirror the AD shape (readonly=!IsUpdateable,required=IsMandatory, …) but are authored, not read from the AD DB. No code path readsAD_Column.Callout,AD_Val_Rule.Code,AD_Rule.Script,DisplayLogic,AccessLevel, or any*_Accesstable at runtime. UPDATE (W-LOGIC-EVAL): there is now a logic-expression evaluator —build/erp/ad_evaluator.js(recursive-descent port of iDempiereSimpleBoolean.g4+EvaluationVisitor) — wired intocrud_overlay.js:effectiveFlags/validateFieldviawindow.AdEvaluator. It readsdisplaylogic/readonlylogic/mandatorylogicand evaluates them against (record, context). Witnessscripts/poc_logic_eval.js→build/erp/poc_logic_eval.log(§LOGIC_COVERAGE evaluated=3044 of 3044 boolean-logic rows 100.00%; §FALSIFIER verdict flips DR→CO). UPDATE (W-ACCESS): the security surfaces are now read too —build/erp/ad_access.js(port ofMRole.getWindowAccess/getProcessAccess/getFormAccess+canView) interpretsad_window_access/ad_process_access/ad_form_access/ad_table.accesslevel/ad_entitytypefrom the AD; witnessscripts/poc_access.js→build/erp/poc_access.log. UPDATE (W-PROC): there is now an AD_Process dispatch spine —build/erp/ad_process.js(port ofProcessUtil.startJavaProcess:156-205+SvrProcess.startProcess:132-232/prepare/doIt+ProcessInfo) readsad_process/ad_process_para, resolvesclassnameagainst a JS handler registry (report procs resolve toreport_overlay's folds), validates the supplied params against the para rows, runs the handler; an unregistered classname returns an explicit absent-handler (not a silent no-op). 5 handlers / 22 of 476 dispatched / 454 named-deferred. Witnessscripts/poc_proc.js→build/erp/poc_proc.log. UPDATE (W-POST-DERIVE): per-document GL derivation is read too —scripts/post_resolver.jsresolves manifest tokens{Master.Role}to real GL accounts from the*_acctmaster columns (override→c_acctschema_defaultfallback, never synthesized); a real C_Invoice folds to balanced Fact_Acct lines on canonicalad_full.db(scripts/poc_post_derive.js→build/erp/poc_post_derive.log; 1 of 20 Doc_ doc-types, 19 named-deferred). UPDATE (W-VALRULE):AD_Val_Ruleis now read too —build/erp/ad_valrule.jsreadsad_val_rule.code, substitutes@token@from context, and applies the where-clause as a real filter (327 of 332 interpretable;build/erp/poc_valrule.log). UPDATE (W-CALLOUT):AD_Column.Calloutis now dispatched too —build/erp/ad_callout.jsresolves eachclass.methodagainst a JS registry and fires on the field change → derived sibling values (6 real line-callout atoms;build/erp/poc_callout.log). The 4AD_Ruleare SQLFact_Reconciliationrules (ruletype Q, not* Groovy) over emptyfact_acct/fact_reconciliation(Postgres-only SQL) — n/a-in-seed.
A · Rules/processes defined in CODE (Java)¶
| surface | AD count (query) | code home (path, LOC) | engine handling | verdict |
|---|---|---|---|---|
| DocAction lifecycle (prepareIt/completeIt/reverseCorrectIt/reverseAccrualIt/voidIt/closeIt/reActivateIt/unlockIt) | 31 classes match public String completeIt (grep -rl), ~28 real M* models |
DocAction.java iface 319 LOC; per-model methods in each M*.java |
crud_overlay.js:docActionOutcome + buildDocActionGroup — folds only CO (DR→CO/IP via a field-presence requires check); action is pass-through metadata, no dispatch table |
🟡 PARTIAL |
Posting (Doc_*.java GL fold + org.compiere.acct) |
n/a (Java; see §B acct-config) | 20 Doc_*.java (12,789 LOC) + Doc/Fact/FactLine/DocLine in org/compiere/acct (21,443 LOC total, 35 files) |
per-document GL derivation EXISTS (post_resolver.js §3.5.1 + the §13 Sales-Invoice manifest, W-POST-DERIVE) — re-proven on canonical ad_full.db: a real C_Invoice (id 100) derives DR {BPartner.Receivable}=GrandTotal / CR {Product.Revenue}=LineNet / CR {Tax.Due}=TaxAmt → balanced (§POST_DERIVE id=100 lines=3 ΣDR=ΣCR=50.35 bal=0); accounts RESOLVED from real master columns (override→c_acctschema_default), never synthesized; §FALSIFIER drop-tax-CR → ΣDR≠ΣCR. erp_period_close.foldBalances/erp_postings.readPostings then sum the DERIVED lines. 1 of 20 Doc_* doc-types derived, 19 named-deferred (a manifest each; resolver generalizes). Was "no derivation / pre-folded only" — corrected. build/erp/poc_post_derive.log. C_BankStatement (Doc_Bank) ORACLE-EQUIVALENT (W-FOLD-BANKSTMT, 2026-06-14): build/erp/report_bank_statement.js:foldBankStatement — port of Doc_BankStatement.createFacts (CMB doc type); tokens {Bank.Asset}/{Bank.InTransit}/{Charge.Expense}/{Bank.InterestExp}/{AcctSchema.CurrencyBalance} added to post_resolver.js; 13 fact_acct(392) rows across 2 schemas (101=USD, 200000=EUR) maxDiff=0c; currency conversion ROUND(amt×0.85, 2) HALF_UP exact (83.30/83.73/0.21 match); usecurrencybalancing='Y' balance line 0.01 DR to 82550 reproduced; §FALSIFIER stmtAmt+100 → 10000c diverge. build/erp/poc_fold_bank_statement.log exit 0. |
🟡 PARTIAL |
SvrProcess (org.compiere.process.* / org.idempiere.process.*) |
see §B AD_Process (476) | 220 files / 54,377 LOC (198 + 22) | dispatch spine BUILT (ad_process.js:dispatch, W-PROC) — classname→registry→prepare/validate→doIt, 5 handlers, 22/476 dispatched, 454 named-deferred (the corpus itself stays unported). Was "absent"; now the mechanism exists, not the 54k-LOC corpus. ON-DEMAND PICKER BUILT (W-PROC-PICKER, 2026-06-14): ad_process.js:pickUsedProcesses — AD_Process ⋈ AD_Process_Access ⋈ AD_WF_Node returns the actually-used subset on demand (451/451 active processes oracle-equivalent vs live PG; byWorkflow=9 wf-node refs; §FALSIFIER role 99999 → byAccess=0); build/erp/poc_proc_picker.log exit 0. Corpus (337 classnames) stays named-deferred. |
🟡 PARTIAL |
Workflow engine (org.compiere.wf / MWF*) |
see §B AD_Workflow (58) | 19 files / 7,366 LOC | ad_workflow.js:walk (W-WF) — reads ad_workflow/ad_wf_node/ad_wf_nodenext, walks the node graph from the start node, creates a WF_Activity per node, routes to the next (std-user-next by seqno or an explicit approval decision). §WF node=50000 next=50002 activity=created (full walk 50000>50002>50001); split node 200033 routes std→200034 vs approval→200036; §FALSIFIER off-graph route falls back. Mechanism + 1 walked workflow; the 58-wf corpus named-deferred. build/erp/poc_wf.log |
🟡 PARTIAL |
Callouts (Callout*.java / org.adempiere.base.callout) |
284 cols carry a Callout, 148 distinct callout-strings (145 distinct class.method atoms) |
56 files / 10,340 LOC (+ pkg 10 files / 1,012) | dispatch spine BUILT (ad_callout.js:dispatch, W-CALLOUT) — classname→registry→derived-value map, 6 real atoms ported / 18 cols dispatched / 139 named-deferred (the 10k-LOC corpus stays unported). crud_overlay.js:validateField still does the static type/min/max/regex/list shape. Was "no callout class dispatched"; now the mechanism exists |
🟡 PARTIAL |
Model Validators (*ModelValidator + ModelValidationEngine) |
AD_ModelValidator = 3 |
5 files / 1,165 LOC + ModelValidationEngine.java 1,048 LOC |
ad_modelval.js:fireHooks (W-MODELVAL) — timing-hook engine over 11 modeled timings (BEFORE/AFTER × NEW/SAVE/DELETE + PREPARE/COMPLETE/VOID), faithful port of fireModelChange/fireDocValidate (first non-null error aborts). 4 real hooks ported (qty>0, order-has-lines, total≥0); §MODELVAL_HOOK timing=BEFORE_COMPLETE table=C_Order fired=2 ok=true; §FALSIFIER 0-line order BLOCKED before complete. Was "single SET_STATUS op"; now a real timing engine. build/erp/poc_modelval.log |
🟡 PARTIAL |
Per-model invariants (beforeSave/afterSave on M*) |
n/a (Java overrides) | 213 beforeSave / 109 afterSave method defs across M*.java |
crud_overlay.js:validate (generic field checks) + ad_modelval.js timing hooks (W-MODELVAL) — model-specific invariants now dispatch at BEFORE/AFTER timings; 12 install*SaveHooks installers (MOrder…MRequisition, every hook citing its Java lines, oracle-diffed by the W--SAVE equivalence rows). LIVE save-path (W-AD-MODELVAL-LIVE 2026-06-11, sw v647):* glassbowl crud_overlay.saveForm fires AdModelVal.fireHooks('BEFORE_SAVE') — MOrder.priceListImmutable REJECTS on-screen (CannotChangePl off the REAL c_orderline count, form stays open, nothing commits) and MOrder.billDefaults DERIVES into the form (§AD-MODELVAL-LIVE … derived={"bill_bpartner_id":112…} fired=11), then the accepted save commits signed (§CRUD-PERSIST … verifyChain=ok). scripts/poc_ad_modelval_live.js. Remainder: the ~200 unported Java overrides + master tables absent from glassbowl_data.db (bundle-gap §-named) |
🟡 PARTIAL |
B · Rules/processes defined in the AD (DATA)¶
| surface | AD count (query) | code home | engine handling | verdict |
|---|---|---|---|---|
| C_DocType FSM | C_DocType=52; DocAction list=14 (AD_Reference 135); DocStatus list=12 (131) | DocumentEngine.java (action→method off C_DocType) |
ad_docfsm.js (W-DOCFSM) — port of DocumentEngine.getValidActions/processIt: legalActions(db,doctypeId,status) (per-C_DocType, reads the real row) + transition(action,status)→toStatus. Reaches 11 of 12 statuses (was 2: CO,IP); a Completed order dispatches the full reversal family {CL,RC,RA,RE,VO,PO} (CO--RC-->RE, CO--RE-->IP, …); §FALSIFIER Prepare-from-Completed + Complete-from-Closed REJECTED. build/erp/poc_docfsm.log. Per-table narrowing = the H-1/H-2/tail equivalence rows (every DocAction table walked). LIVE (W-AD-DOCFSM-LIVE 2026-06-11, sw v647): the idempiere.html record form renders a doc-action bar GATED by legalActionsFor — only legal actions render (completed Invoice/Journal/Payment = CL/RC/RA, no VO; closed order = NO actions), click dispatches via dispatchFor → status chip per transitionFor; periodOpen = real c_period+c_periodcontrol probe (§AD-DOCFSM-LIVE … periodOpen=true(period=155 GLJ=O)). scripts/poc_ad_docfsm_live.js (6 cases + falsifiers). Seed residuals DISSOLVED 2026-06-11 (FULL-WIDTH ad_seed.db, prompts/IDMP_FULLWIDTH_SEED.md, bim-ootb PR #265 sw v648): c_doctype now carries iscanbereactivated/docsubtypeso → completed docs whose doctype canReact=Y legally render RE (Order 135 / GL_Journal 115 / Payment·ARR 119; C_Invoice·ARI = the no-RE falsifier), and M_InOut.MovementType is present → window 169 scopes rows so the no-VO-on-completed-InOut falsifier runs ON M_InOut (W-AD-DOCFSM-LIVE re-derived by query, build/erp/seed_case_derive.log) |
✅ COVERED |
| AD_Process (+ Para / Classname / report) | 476 (Classname≠null 337, IsReport=Y 119); AD_Process_Para 1208 | the §A 54k-LOC home | dispatch spine BUILT (ad_process.js, W-PROC): reads ad_process+ad_process_para, resolves classname→a JS handler registry, validates params against the para rows (prepare→doIt, port of ProcessUtil.startJavaProcess:156-205/SvrProcess.startProcess:132-232), runs + logs. 5 classnames registered, 22 of 476 dispatched (report procs 110/116 → report_overlay.foldReceipt; 310→foldTrialBalance; 175/233 doc-actions); the remaining 454 are named-deferred (54k-LOC SvrProcess corpus, intentionally not ported). §PROC run / §PROC_PARAM_VALIDATE (missing-mandatory rejected) / §FALSIFIER (unregistered classname → explicit absent-handler, not silent) — build/erp/poc_proc.log. LIVE (W-AD-PROC-LIVE 2026-06-11, bim-ootb PR #267 / sw v650): menu P/R leaves + procSet-gated ?process= deep link dispatch via the spine on the browser ad_seed.db (FULL-WIDTH seed PR #265 fell the gate); param dialog from real ad_process_para (DefaultValue prefill incl. @#AD_Client_ID@ session resolution), §PROC_PARAM_VALIDATE rejects on-screen; unregistered classname → honest absent-handler card (333 falsifier); B-4 menu pruning intact (§IDMP-SESSION 116/159 procs). scripts/poc_ad_process_live.js → build/erp/poc_ad_process_live.log exit 0. Named residuals: ~~TrialBalance (310) folds honest-empty~~ CLOSED 2026-06-12 — seed regen ships fact_acct (PR #271 sw v653): §AD-PROC-LIVE proc=310 name="Trial Balance" classname=org.compiere.report.TrialBalance dispatched=Y ok=Y rows=21 live · handler registry = 5 classnames (the 454 SvrProcess corpus stays named-deferred, see §A) |
✅ COVERED |
| PA_Report (Financial Report, Window 216) — Line/Column/Source | PA_Report 3 (Balance Sheet · Income Statement · Cash Flows) · PA_ReportLine 113 · PA_ReportColumn 17 · PA_ReportSource 93 (same rows live in idempiere_test) |
org.compiere.report.* (FinReport/FinStatement) |
ORACLE-EQUIVALENT, all 3 statements (W-PA-REPORT ✅ 2026-06-10/11) — metadata-driven foldStatement == live idempiere_test FinReport to the cent: BS maxDiff=0c (108 seg cells) · IS maxDiff=0c (148) · CF maxDiff=0c (140, non-vacuous 21 nonzero cells); §FALSIFIER (drop leaf 508 → 148.35→0.35) fires; bundle-alone twin poc_statement_browser.js proves the IN-APP path from glassbowl_data.db ALONE (+ independent calc-col cross-check). Launched the iDempiere way via AD_Menu 278→280→281 (W-PA-MENU). The strong fold-vs-independent-product class. Honest scope: segment cols × S-lines diffed; calc cells compose per the verified operators. Jasper/ReportEngine deleted. Logs: poc_pa_report.log/poc_statement_browser.log/poc_pa_menu.log. Residual to ✅: live-UI bridge (parked). |
🟡 PARTIAL (oracle-equivalent, headless ceiling) |
| AD_PrintFormat (+ Item · Master-Detail + row engine) | AD_PrintFormat 93 · AD_PrintFormatItem 2780 (Field 2711 / Text 28 / 'P'=27 master-detail / Image 13) — group-by 10 · sorted 114 · summarized 58 |
DataEngine.java 1529 + LayoutEngine.java 2170 + PrintData 907 ≈ 4.6K LOC |
ORACLE-EQUIVALENT for the real master-detail case (W-PRINTFORMAT ✅ 2026-06-11) — generic foldPrint (recursive 'P' + IsGroupBy/IsSummarized/IsCounted/IsAveraged/SortNo reductions, ONE BigDecimal pass — replaces DataEngine+PrintDataGroup; DOM renderPrint replaces LayoutEngine/Jasper) reproduces the PrintData row tree + break subtotals for format 120 Invoice Header →'P'→ 121 Invoice LineTax, ALL 8 seed invoices: §PRINTFORMAT diffedCells=48 maxDiff=0c — oracle = live BASE tables + the stored c_invoice.grandtotal real iDempiere wrote; falsifiers A (drop 'P'→no detail) / B (+1c→Σ diverges) / C (drop link→whole-view leak) ALL fire. Inputs bundle-alone (extract_printformat.sh: 93 formats + 2780 items + print views materialized by PG, never reimplemented). Browser: renderPrint + data-gated "⎙ iDempiere format" + Trial-Balance menu leaf (502→foldTrialBalance); Statement of Accounts (350) named-deferred. NOT pixels (stated non-goal); group-by/count/avg implemented but unexercised by the seed format. Log: poc_printformat.log. |
🟡 PARTIAL (oracle-equivalent, headless ceiling) |
Report scratch tables (T_*) — the materialize-then-read tier behind PA_Report/AD_PrintFormat (newly indexed 2026-06-10, swept from live idempiere_test + source) |
15 report temp-tables (ad_table IsView=N, verified real tables in PG): T_Report·T_ReportStatement·T_TrialBalance·T_CashFlow·T_InventoryValue·T_Aging·T_InvoiceGL·T_BankRegister·T_Reconciliation·T_Replenish·T_DistributionRunDetail·T_1099Extract·T_BOM_Indented·T_BOMLine·T_MRP_CRP (+3 convenience views T_*_v; excluded non-report UI scratch T_Selection*/T_Spool/T_Transaction/T_AlterColumn/T_MoveClient) |
each report process INSERTs its table then the print format reads it back: FinReport→T_Report · FinStatement→T_ReportStatement · TrialBalance→T_TrialBalance · Aging/MAging→T_Aging · BankRegister→T_BankRegister · FactReconciliation→T_Reconciliation … |
the fold DELETES this whole tier — 0 temp rows written. PROVEN for 6 members: T_Report→foldStatement (BS + IS + CF, maxDiff=0c, W-PA-REPORT, poc_pa_report.log) + T_TrialBalance→value-fold (§REPORT-FIN, maxDiff=0c) + T_Aging→report_aging.foldAging (all ~21 MAging buckets, maxDiff=0c over 88 cells vs an independent SQL bucketer over live rv_openitem, W-AGING, poc_fold_aging.log) + T_InventoryValue→report_inventory_value.foldInventoryValue (cost-valuation core CostStandard/QtyOnHand/CostStandardAmt, EXACT over 20 rows vs an independent SQL re-derivation over the base tables, W-INVVALUE, poc_fold_inventory_value.log; price columns are param-driven, named not folded) + T_Replenish→report_replenish.foldReplenish (min/max planning QtyToOrder + QtyOnHand/Reserved/Ordered, EXACT over 18 candidates / 8 survivors vs an independent SQL CTE re-derivation, W-REPLENISH, poc_fold_replenish.log; createPO/etc actions out of scope) + T_InvoiceGL→report_invoice_gl.foldInvoiceGL (currency-revaluation core AmtRevalDr/Cr + reval diffs + OpenAmt/Percent proration, maxDiff=0c over 108 cells / 6 cross-currency rows vs the live iDempiere currencyConvert/invoiceOpen on idempiere_test, W-INVOICEGL, poc_fold_invoice_gl.log; createGLJournal action out of scope). + 2 HYBRID + 1 THIN: T_DistributionRunDetail→report_distribution_run.foldDistributionRun (rawSplit ll.Ratio/RatioTotal*TotalQty ORACLE-EQUIVALENT vs iDempiere's own insertDetails SQL maxDiff=0; the integer-allocation loop RULE-CONSISTENT — sum-exact under MinQty floors, 929/200/371, no live oracle without a process run, W-DISTRUN, poc_fold_distribution_run.log; createOrders action out of scope) + T_CashFlow→report_cashflow (InitialBalance Σ acctBalance ORACLE-EQUIVALENT maxDiff=0c over 21 accounts vs the live acctBalance() plpgsql, both sign branches exercised; CommitmentsOrders value-math + ActualDebtInvoices sign-flip ORACLE-EQUIVALENT vs SQL; Plan feed verified-EMPTY no-op; due-date gate + pay-schedule loop RULE-CONSISTENT, W-CASHFLOW, poc_fold_cashflow.log; X_T_CashFlow.save() action out of scope) + T_BankRegister→report_bank_register (the config-chain join SELECTION fact_acct ⋈ C_Payment ⋈ C_BankAccount ⋈ baa ⋈ vc ⋈ ev is ORACLE-EQUIVALENT vs the live BankRegister INSERT…SELECTs — detail SELECT DISTINCT=4 rows + balance-line SUM=549.46 over 8 join rows, the DISTINCT/SUM multiplicity quirk reproduced; THIN — Balance=Dr−Cr passes through, Cr=0 in seed so arithmetic NAMED not tested; §FALSIFIER drops fa.Account_ID=vc.Account_ID → join leaks 8→32, load-bearing; W-BANKREGISTER, poc_fold_bank_register.log; T_BankRegister row-writes out of scope). The T_* tier is now DRAINED — T_1099Extract = 0-row seed (n/a, do NOT synthesize) is the only remainder. Cross-ref MigrateComparisonPaper §temp-tables. (Index row — detail of the reporting tier already counted above; does not change the 42-surface tally.) |
🟡 PARTIAL |
| AD_Workflow (/ WF_Node / NodeNext / Responsible) | AD_Workflow 58, WF_Node 262, NodeNext 207, Responsible 2 | org.compiere.wf (§A) |
ad_workflow.js (W-WF) — §WF_COVERAGE workflows=58 nodes=262 nexts=207; walk(db,wfId) reads the AD_WF_* graph, walks node→next from the start node, creates an activity per node, routes splits (std-user-next / approval decision). 1 workflow walked, corpus named-deferred. build/erp/poc_wf.log |
🟡 PARTIAL |
| AD_Rule (SQL reconciliation rules) | 4 — all RuleType Q = SQL (NOT script/Groovy): AR/AP Trade · Bank-in-Transit · Payment Clearing · Not-Invoiced-Receipts | Fact_Reconciliation auto-grouping |
n/a-in-seed — target fact_acct/fact_reconciliation are 0 rows in this DB + the SQL is Postgres-specific (least(), not SQLite); a niche GL-reconciliation surface, not a behavioural gap. (The earlier "JSR-223 Groovy / no JS interpreter" label was WRONG — verified ruletype='Q'.) |
⛔ GAP (n/a) |
| AD_Val_Rule (SQL validation) | 332 (all Type S / SQL) | MValRule / lookup |
ad_valrule.js:evalValRule (W-VALRULE) — reads ad_val_rule.code, substitutes @Field@/@#Global@ tokens, applies the where-clause as a real filter (MValRule semantics): 332 classified = 114 static + 213 token = 327 interpretable, 4 unsafe-subselect + 1 empty explicit-deferred; §VALRULE id=143 sql="IsSOTrx='Y'" rows=4 · id=211 @AD_Table_ID@→318 rows=6; §FALSIFIER paid invoice 100 EXCLUDED by the NotPaid filter. build/erp/poc_valrule.log. Residual: live lookup/list render-wiring |
🟡 PARTIAL |
| AD_ModelValidator | 3 (Libero MFG, Fixed Assets, Product Price) | ModelValidationEngine |
ad_modelval.js (W-MODELVAL) — the 3 registered classes are read from ad_modelvalidator (Java bodies named-deferred); the timing-hook engine dispatches BEFORE/AFTER validators around a record/doc action (build/erp/poc_modelval.log). Residual: the 3 Java validator bodies + live action wiring |
🟡 PARTIAL |
| AD_Column · Callout | 284 | CalloutEngine.java (338) |
ad_callout.js:dispatch (W-CALLOUT) — reads ad_column.callout (the ;-separated class.method list), resolves each against a JS handler registry, fires on the field change → DERIVED sibling values. 6 real line-callout atoms ported (CalloutOrder/CalloutInvoice .amt/.qty/.product), 18 cols dispatched, 139 atoms named-deferred; §CALLOUT col=M_Product_ID derived={PriceActual:21.59,PriceList:22.73,LineNetAmt:215.90} == stored row; §FALSIFIER unregistered CalloutOrder.charge → explicit absent-handler. build/erp/poc_callout.log. Residual: live field-change render-wiring |
🟡 PARTIAL |
| AD_Column · DefaultValue | 5647 | MColumn/GridField.getDefault |
defaultsFor() — literal/today/auto only (19 entries); SQL & @var@ defaults absent |
🟡 PARTIAL |
| AD_Column · ReadOnlyLogic | 289 | Evaluator.java (197) |
ad_evaluator.js+crud_overlay.effectiveFlags — all 289 (129 distinct) parse + evaluate; flat f.readonly is now fallback (W-LOGIC-EVAL §LOGIC_COVERAGE). Residual: live input-disable render-wiring |
🟡 PARTIAL |
| AD_Column · MandatoryLogic | 29 | Evaluator.java |
ad_evaluator.js+effectiveFlags — all 29 (28 distinct) parse+evaluate; drives required (flat f.required fallback). Residual: render * marker (W-LOGIC-EVAL) |
🟡 PARTIAL |
AD_Column · ValueFormat (vformat) |
1 (negligible) | DisplayType.java |
ad_reference.js:applyVFormat (W-REFERENCE) — per-char VFormat mask (0/9/L/l/A/a/c/_) validate+transform; §VFORMAT mask=L in=a→A, §FALSIFIER digit fails L, 00LL validates 2-digit+2-upper. build/erp/poc_reference.log |
🟡 PARTIAL |
| AD_Column · IsUpdateable=N | 14705 | MColumn/GridField.setValue |
validateField blocks change via readonly=!IsUpdateable — bool modeled, ~22 authored fields only |
🟡 PARTIAL |
| AD_Column · IsMandatory=Y | 12577 | GridField.isMandatory |
validateField 'required' via required=IsMandatory — ~22 fields only |
🟡 PARTIAL |
| AD_Field · DisplayLogic | 2588 | GridField.isDisplayed + Evaluator |
ad_evaluator.js — all 2588 (776 distinct) parse+evaluate; effectiveFlags.visible computed; validateField skips hidden (W-LOGIC-EVAL). LIVE (W-AD-DISPLAYLOGIC-LIVE 2026-06-11, sw v647): idempiere.html buildForm hides fields whose logic is false for the open record via the proven evaluator — Sales Order 100: §AD-DISPLAYLOGIC-LIVE table=C_Order shown=33 hidden=27 (e.g. ChargeAmt[@HasCharges@='Y'] absent from the DOM; falsifier: no-logic DocumentNo still renders); glassbowl applyAdLogic + the erp.html accordion ride the same engine. scripts/poc_ad_displaylogic_live.js. Named residual: window-context vars (@OrderType@, @$Element_*@ — callout/session-populated in real iDempiere) evaluate against the record only; tab-level GridTab render |
✅ COVERED |
| AD_Field · ReadOnlyLogic | 52 | Evaluator | ad_evaluator.js+effectiveFlags — all 52 (42 distinct) parse+evaluate (W-LOGIC-EVAL). Residual: render-wiring |
🟡 PARTIAL |
| AD_Field · MandatoryLogic | 14 | Evaluator | ad_evaluator.js+effectiveFlags — all 14 (8 distinct) parse+evaluate (W-LOGIC-EVAL). Residual: render-wiring |
🟡 PARTIAL |
| AD_Field · DefaultValue | 34 | GridField.getDefault |
partial via defaultsFor (descriptor, not field-override) |
🟡 PARTIAL |
| AD_Tab · WhereClause | 85 | GridTab.java (3735) |
ad_tabquery.js:applyWhere (W-TABQUERY) — ad_tab.whereclause (token-substituted) applied as the row filter: §TAB_FILTER tab=186 where="C_Order.IsSOTrx='Y'" rows=4; §FALSIFIER PO order 104 excluded from the SO tab. build/erp/poc_tabquery.log |
🟡 PARTIAL |
| AD_Tab · OrderByClause | 173 | GridTab |
ad_tabquery.js:orderedKeys (W-TABQUERY) — ad_tab.orderbyclause applied as the sort: §TAB_ORDER orderBy="M_Product.Value" rows=55 rows come out sorted ascending. build/erp/poc_tabquery.log |
🟡 PARTIAL |
| AD_Tab · ReadOnlyLogic | 42 | GridTab + Evaluator |
ad_evaluator.js — all 42 (4 distinct) parse+evaluate (W-LOGIC-EVAL §LOGIC_COVERAGE). Residual: tab-level GridTab render not wired |
🟡 PARTIAL |
| AD_Tab · DisplayLogic | 35 | GridTab + Evaluator |
ad_evaluator.js — all 35 (25 distinct) parse+evaluate (W-LOGIC-EVAL). Residual: tab-level GridTab render not wired |
🟡 PARTIAL |
| AD_Tab · IsInsertRecord=N | 320 | GridTab.isInsertRecord |
verbs[] gates create but not AD-derived |
🟡 PARTIAL |
| AD_Reference (header) | 606 (D=52, L=311, T=243) | DisplayType/MLookup |
f.type tags drive validateField — hand-set, not from AD_Reference |
🟡 PARTIAL |
| AD_Ref_List (list valid.) | 1545 | MLookup/Lookup |
validateField 'list' checks store.__meta[f.ref] — ~7 lists vs 1545 |
🟡 PARTIAL |
| AD_Ref_Table (table valid.) | 243 | MLookup (1337) |
ad_reference.js:fkExists (W-REFERENCE) — resolves the reference's FK table+key (ad_ref_table→ad_table/ad_column) and runs the real membership query: §FK_CHECK table=ad_reference id=1 exists=true; §FALSIFIER id=999999 → exists=false reject (not isFinite only). build/erp/poc_reference.log |
🟡 PARTIAL |
| AD_EntityType | 12 | MEntityType |
ad_access.js:RoleContext.entityTypeAllowed — all 12 active entitytypes loaded into the role scope (W-ACCESS §ACCESS_COVERAGE entitytypes=12). Residual: scope not enforced at live dictionary-object render |
🟡 PARTIAL |
| AccessLevel (AD_Table) | 1076 (org/client/system bits) | MRole/MTable |
ad_access.js:canView+gateRecord — faithful port of MRole.canView:2422-2465; all 6 distinct accesslevels (1076 ad_table rows) interpreted for both roles, 0 err; System-only (4) denied to userLevel ' O' (W-ACCESS §ACCESS_DENY). Residual: not wired into live record read |
🟡 PARTIAL |
| AD_Role | 5 | MRole.java (3533) |
ad_access.js:buildRole — all 5 roles built from ad_role (userLevel/orgs/clients/isaccessallorgs) + their grant maps (W-ACCESS §ACCESS_COVERAGE roles=5). LIVE (W-AD-ACCESS-LIVE 2026-06-11): idempiere.html login selects a real role via IdmpSession.rolesForUser/buildContext (Role dropdown, §IDMP-SESSION rolesForUser); per-role scoping proven in-browser for all 4 login-capable roles (scripts/poc_ad_access_live.js) |
✅ COVERED |
| AD_Window_Access | 1303 | MRole.getWindowAccess |
ad_access.js:gateWindow (port of MRole.getWindowAccess:1610-1693) — all 1303 active grant rows interpreted (no-row=DENY, IsReadWrite=rw); §ACCESS_OK + §ACCESS_DENY (no-grant) + §FALSIFIER proven (W-ACCESS). LIVE (W-AD-ACCESS-LIVE 2026-06-11): the live idempiere.html menu IS pruned by these grants — IdmpSession.accessibleWindows+scopeMenu in-browser: Admin grants=413→visible 294/332 · User grants=254→163/332 · WebSvc grants=0→0/332 (§AD-ACCESS-LIVE, scripts/poc_ad_access_live.js) |
✅ COVERED |
| AD_Process_Access | 1309 | MRole.getProcessAccess |
ad_access.js:gateProcess (port of MRole.getProcessAccess:1701-1789) — all 1309 active grant rows interpreted; §ACCESS_OK proven (W-ACCESS). LIVE (W-AD-MENU-PRF-LIVE 2026-06-11, sw v647): the idempiere.html menu prunes P/R leaves by these grants (IdmpSession.accessibleProcesses + scopeMenu): Admin 137/159 · User 116/159 · WebSvc 1/159 visible proc leaves; admin-only "Reset Accounting" absent from GardenUser's live DOM (falsifier). scripts/poc_ad_menu_prf_live.js. Named residual CLOSED 2026-06-11: launch-time dispatch gate is live (W-AD-PROC-LIVE, PR #267 sw v650) — P/R leaf + ?process= deep link are procSet-gated before dispatch |
✅ COVERED |
| AD_Form_Access | 145 | MRole.getFormAccess |
ad_access.js:gateForm (port of MRole.getFormAccess:1888-1972) — all 145 active grant rows interpreted (W-ACCESS §ACCESS_COVERAGE forms_gated=145). LIVE (W-AD-MENU-PRF-LIVE 2026-06-11, sw v647): the menu prunes X (form) leaves by these grants (accessibleForms): Admin 21/24 · User 14/24 · WebSvc 0/24; "Import File Loader" absent from GardenUser's live DOM (falsifier). scripts/poc_ad_menu_prf_live.js |
✅ COVERED |
| AD_Column_Access | 0 (table empty) | MRole.getColumnAccess |
n/a in seed — table empty (0 rows); gate has no column-grant data to interpret | ⛔ GAP |
| AD_Record_Access | 0 (table empty) | MRole.getRecordAccess |
n/a in seed — table empty (0 rows); record-org/client scope is covered separately via gateRecord |
⛔ GAP |
Ranked GAP list (smallest witness that proves/disproves coverage — §-log first)¶
The gaps rank by AD surface size × behavioural weight. Each names the smallest whitebox §-log that would settle it
(per docs/TestArchitecture.md — a log line, not a Playwright test). For the ⛔ rows the grep-empty result is the
disproof: the line can never fire today.
- Logic-expression evaluator — ✅ BUILT (W-LOGIC-EVAL).
build/erp/ad_evaluator.js(port of iDempiereSimpleBoolean.g4+EvaluationVisitor) +crud_overlay.effectiveFlagswiring. Unblocks 7 rows (now 🟡):§LOGIC_COVERAGE evaluated=3044 of 3044 boolean-logic rows (100.00%)+ 5@SQL=rows named-deferred (separateparseSQLLogicsurface); §FALSIFIER readonly flips rw→ro on@Processed@N→Y and display flips hide→show on@DocStatus@!DRDR→CO; 25 operator-class samples pass. Witness:scripts/poc_logic_eval.js→build/erp/poc_logic_eval.log. Residual to ✅: live DOM render-wiring (show/hide/disable) + tab-level GridTab render. - Security layer — ✅ BUILT (W-ACCESS).
build/erp/ad_access.js(port of iDempiereMRole.getWindowAccess/getProcessAccess/getFormAccess+canViewAccessLevel-SCO semantics) interprets the real grant rows: §ACCESS_COVERAGE roles=5 windows_gated=1303 processes_gated=1309 forms_gated=145 accesslevels=6(1076 rows) entitytypes=12 errors=0; §ACCESS_DENY proven for a no-grant window (GardenWorld User vs Admin), a wrong-org record (org 12 ∉ User's {11,50000,50001,50007}), and a System-only record (no 'S' in userLevel ' O'); §FALSIFIER shows gate-OFF would let User SEE the no-grant window → gate is load-bearing. Witness:scripts/poc_access.js→build/erp/poc_access.log. UPDATE (W-AD-ACCESS-LIVE 2026-06-11): the menu/window half IS live-wired —idempiere.htmlrole login + grant-pruned menu proven in-browser (AD_Role + AD_Window_Access → ✅). Residual to ✅ (rest): live process/form render gating (P/R/F menu leaves) + AccessLevel record-read wiring; AD_Column_Access/AD_Record_Access are empty in this seed (n/a). - SvrProcess runner — ✅ DISPATCH SPINE BUILT (W-PROC).
build/erp/ad_process.js(port of iDempiereProcessUtil.startJavaProcess:156-205+SvrProcess.startProcess:132-232→process()→prepare()→doIt()+ProcessInfo.getClassName:535/getParameter:717) readsad_process+ad_process_para, resolvesclassname→a JS handler registry, validates supplied params against the para rows, runs + logs:§PROC_COVERAGE registered=5 dispatched=22 of 476 AD_Process (classname≠null 337) absent=454 parseErrors=0. Real dispatches proven — Order Print (110)→report:c_order→report_overlay.foldReceipt(1 line, total 50.35), Invoice Print (116)→report:c_invoice, Resubmit Posting (175,org.compiere.process.FactAcctReset)→doc-action handler.§PROC_PARAM_VALIDATErejects TrialBalance (310) when the mandatoryPostingTypeis dropped (dispatched=false before doIt, not a silent run).§FALSIFIER—org.compiere.report.FinBalance(203, real classname, no handler) → explicit "Failed to create new process instance" absent-handler (a silent no-op would dispatch=true) → classname→handler resolution is load-bearing. Witness:scripts/poc_proc.js→build/erp/poc_proc.log. Residual to ✅: the 454 named-deferred classnames are the 54k-LOC SvrProcess corpus (intentionally not ported — the deliverable is the mechanism, not the corpus) + live process-launch render-wiring — the render-wiring landed 2026-06-11 (W-AD-PROC-LIVE, bim-ootb PR #267 sw v650); the corpus is the only remaining residual. - Workflow engine (58 wf / 262 nodes) — ✅ BUILT (W-WF).
build/erp/ad_workflow.jsreadsad_workflow/ad_wf_node/ad_wf_nodenext, walks the node graph from the start node, creates a WF_Activity per node, routes the next:§WF node=50000 action=F next=50002 activity=created(full walk 50000>50002>50001 terminates); split node 200033 routes std→200034 (seqno 10) vs explicit approval→200036;§FALSIFIERleaf terminates (no invented edge) + off-graph route falls back to std-next. Witnessscripts/poc_wf.js→build/erp/poc_wf.log. Residual to ✅: the 58-wf corpus + transition-condition eval + live activity persistence/wiring. - DocAction FSM beyond CO (14 actions × 12 statuses) — ✅ BUILT (W-DOCFSM).
build/erp/ad_docfsm.jsportsDocumentEngine.getValidActions/processIt:§DOCTYPE_FSM_COVERAGE doctypes=52 actions=14 statuses=12 reachableAsTarget=11(was 2: CO,IP); per-C_DocTypelegalActions(reads the realc_doctyperow) + a transition table dispatch the full reversal family —§DOCTYPE_FSM from=CO action=RC to=RE,CO--RE-->IP,CO--CL-->CL,CO--VO-->VO;§FALSIFIERPrepare-from-Completed + Complete-from-Closed REJECTED, a Closed doc offers no actions. Witnessscripts/poc_docfsm.js→build/erp/poc_docfsm.log. Residual to ✅: finer doctype-conditioned action gating + live action-launch wiring. - Callout dispatch (284 cols / 148 callout-strings) — ✅ SPINE BUILT (W-CALLOUT).
build/erp/ad_callout.jsreadsad_column.callout, resolves eachclass.methodagainst a JS registry, fires on the field change → derived sibling values:§CALLOUT_COVERAGE cols=284 registered=6 colsDispatched=18;§CALLOUT col=QtyEntered fired=[CalloutOrder.qty,CalloutOrder.amt] derived={QtyOrdered:10,LineNetAmt:215.90}andcol=M_Product_ID derived={PriceActual:21.59,PriceList:22.73,LineNetAmt:215.90}(both == the stored row);§FALSIFIERunregisteredCalloutOrder.charge→ fired=0/absent-named, corrupted qty → derived≠stored. Witnessscripts/poc_callout.js→build/erp/poc_callout.log. Residual to ✅: the 139 named-deferred atoms (10k-LOC corpus) + live field-change render-wiring. - AD_Val_Rule SQL (332) — ✅ BUILT (W-VALRULE).
build/erp/ad_valrule.jsreadsad_val_rule.code, substitutes@token@from context, applies the where-clause as a real filter on canonicalad_full.db:§VALRULE_COVERAGE total=332 static=114 token=213(327 interpretable, 5 explicit-deferred);§VALRULE id=143 sql="IsSOTrx='Y'" rows=4·id=211 @AD_Table_ID@→318 rows=6;§FALSIFIERpaid invoice 100 EXCLUDED by the NotPaid filter (member=false), unpaid 109 survives. Witnessscripts/poc_valrule.js→build/erp/poc_valrule.log. AD_Rule (4 SQL ruletype-QFact_Reconciliationrules — not Groovy) = n/a-in-seed (target fact tables empty + Postgres SQL). Residual to ✅: live lookup/list render-wiring. - Per-document GL derivation (the 20 Doc_) — ✅ MECHANISM BUILT (W-POST-DERIVE). Was mis-stated as "pre-folded only": derivation already exists as the §13 metadata-posting plugin (
scripts/post_resolver.js+ the Sales-Invoice manifest), re-proven on canonicalad_full.db—§POST_DERIVE doc=C_Invoice id=100 lines=3 ΣDR=ΣCR=50.35 bal=0(DR Receivable 518 / CR Revenue 758 / CR Tax-Due 596, every account resolved from real master columns, override→c_acctschema_default);§FALSIFIERdrop-tax-CR →ΣDR≠ΣCR(derivation load-bearing). Coverage 1 of 20 Doc_ derived, 19 named-deferred (mechanism not corpus — each is a manifest the resolver already generalizes). Witnessscripts/poc_post_derive.js→build/erp/poc_post_derive.log(+ the priorscripts/poc_post.js, replay-exact, op-log-coupled). Residual to ✅:** the other 19 doc-type manifests + live posting-launch wiring (parked UI bridge). - Model-validator timing hooks (BEFORE/AFTER COMPLETE) — ✅ BUILT (W-MODELVAL).
build/erp/ad_modelval.jsdispatches registered validators across 11 timings (faithfulfireModelChange/fireDocValidate— first error aborts):§MODELVAL_HOOK timing=BEFORE_COMPLETE table=C_Order fired=2 ok=true;§FALSIFIERqty≤0 line BLOCKED before save, 0-line order BLOCKED before complete (reaches the COMPLETE timing — wasgroupOps.len=1SET_STATUS only); un-hooked (table,timing) → explicit no-op. 3AD_ModelValidatorJava bodies + 322beforeSave/afterSaveoverrides named-deferred. Witnessscripts/poc_modelval.js→build/erp/poc_modelval.log. Residual to ✅: live doc-action wiring. - FK table validation (AD_Ref_Table, 243) — ✅ BUILT (W-REFERENCE).
build/erp/ad_reference.js:fkExistsresolves the reference's FK table+key and runs the membership query:§FK_CHECK table=ad_reference id=1 exists=true,§FALSIFIER id=999999 exists=false reject. +applyVFormatfor the 1AD_Column.ValueFormatmask. Witnessscripts/poc_reference.js→build/erp/poc_reference.log. - AD_Tab WhereClause/OrderBy (85/173) — ✅ BUILT (W-TABQUERY).
build/erp/ad_tabquery.js:applyWhererunsad_tab.whereclauseas the row filter (§TAB_FILTER tab=186 where="C_Order.IsSOTrx='Y'" rows=4, §FALSIFIER PO excluded from SO tab);orderedKeysrunsad_tab.orderbyclauseas the sort (§TAB_ORDER orderBy="M_Product.Value"rows sorted). Witnessscripts/poc_tabquery.js→build/erp/poc_tabquery.log. - SQL/@var@ defaults (subset of 5647). Witness:
§DEFAULT col=dateordered val=<resolved-from-@#Date@>. - Report
T_*scratch-table folds (15 report temp-tables —T_Report✅ +T_TrialBalance✅ +T_Aging✅ +T_InventoryValue✅ +T_Replenish✅ +T_InvoiceGL✅ foldedmaxDiff=0c;T_DistributionRunDetail◐ hybrid — rawSplit oracle-equivmaxDiff=0, allocation loop rule-consistent, W-DISTRUN;T_CashFlow◐ hybrid — InitialBalanceΣ acctBalanceoracle-equivmaxDiff=0cover 21 accounts + CommitmentsOrders/ActualDebt value-folds oracle-equiv, Plan empty no-op, due-date/pay-schedule loop rule-consistent, W-CASHFLOW;T_BankRegister✦ thin — config-chain join SELECTION oracle-equiv (detail DISTINCT=4 + balance SUM=549.46/8 rows, multiplicity quirk reproduced; Balance=Dr−Cr passes through, Cr=0 so arithmetic named; §FALSIFIER drop account-selection → 8→32, W-BANKREGISTER;T_1099Extractn/a 0-row seed). TheT_*tier is DRAINED — all members folded/closed exceptT_1099Extract(n/a). Each: smallest witness = fold the report in-memory and diff cell totals vs the liveidempiere_test/rv_openitem/base-table re-derivation (maxDiff=0c), proving the temp table is deletable. NEXT: P2 (cost-valued inventory GL). See §B "Report scratch tables (T_*)" +MigrateComparisonPaper §temp-tables.
Feed back to the conversion estimate¶
The real AD counts sharpen MigrateComparisonPaper §"Realistic conversion estimate": the irreducible buckets are now
enumerated, not asserted — process 54,377 LOC (476 AD_Process, 337 with a classname), Doc_ posting 12,789 LOC
(20 doc types), callouts 10,340 LOC (284 cols / 148 classes), workflow 7,366 LOC (58 wf), validators 1,165 LOC
(3 registered). The ~3,000-row logic-expression surface (W-LOGIC-EVAL) and the ~4,200-row security surface
(W-ACCESS) now have an engine home (ad_evaluator.js / ad_access.js, headless-proven; live render-wiring is the
residual). Folded today: the CO transition, the double-entry posting fold, a fixed report set — for ~5–7 demo tables.
This is the precise content behind "≈51× shell → ~21× conservative full parity": the 51× is delivery/definition (the 28,184-LOC engine shell re-measured 2026-06-12, ~0.2% of the M-class logic folded — the ratio falls from the earlier 76×/18,614 as real coverage grows); behavioural parity is a long, named* tail — see ERP_MODEL_ARCHETYPE.md.
Provenance / caveats¶
- All §B numbers from
build/erp/ad_full.db(927 tables) — table names are lowercase there (ad_column, notAD_Column). - The shipping seed
~/bim-ootb/erp/ad_seed.dbis FULL-WIDTH since 2026-06-11 (380 tables — the 378 +ad_process/ad_process_para; every table carries ALL columns incl.AD_Column.Callout;prompts/IDMP_FULLWIDTH_SEED.md, bim-ootb PR #265, sw v648, IDB key v14). Still absent from the seed:ad_rule/ad_val_rule/ad_modelvalidatortables —ad_full.dbstays the authoritative source for those three surfaces. Where both carry a surface (C_DocType FSM 52/14/12, acct-config) they agree. AD_Processhas noAD_Rule_IDcolumn (report linkage isjasperreport/ad_reportview_id, 98 rows) — the prompt's "AD_Rule process link" does not exist as a column; counted the real columns instead.AD_Column_Access/AD_Record_Accessexist but are empty in this DB (0 rows) — counted as 0; no engine code regardless.