SAP ECC mainstream maintenance ends on 31 December 2027, and every migration programme ends the same way: someone must attest that GL balances, open items and vendor masters arrived in S/4HANA complete and unchanged. Today that attestation rests on throwaway spreadsheets that auditors distrust and that vanish before the ICFR walkthrough. Migration Recon Gate replaces the spreadsheet with a control. Deterministic code reconciles every batch in three tiers, ending in per-record SHA-256 hashes over canonicalised field-level data. The model layer may only locate the differing records and quote both verbatim. A named migration lead dispositions every mismatch, and the go-live button is a code decision that stays dead until every wave is signed.
Synthetic extracts · 1,672 source rows vs 1,671 target rows across two waves · seeded generator + deterministic engine → identical hashes and verdicts on every run
SAP committed in 2020 to mainstream maintenance for Business Suite 7 until the end of 2027, and the analysis brief behind this demo puts migration completion at roughly 30% of the installed base. That arithmetic produces a 2026 to 2027 crunch in which thousands of finance functions will move their entire ledger between systems under time pressure, with contractors doing the loads and the year-end audit arriving months later.
The proving step is where the discipline collapses. The Basis team extracts ECC tables, a consultant runs migration cockpit loads in waves, and then someone exports both sides to Excel and compares row counts and a few control totals. Mismatches get explained verbally in the cutover war room. The spreadsheet is overwritten by the next wave's version. Go-live is declared on a status slide, and at year-end the auditor asks for migration completeness evidence and receives a screenshot. Data-migration completeness and accuracy is a standard ITGC and ICFR focus area in any ERP change, and the standard evidence for it is a file nobody can re-execute.
The tempting move is to hand both extracts to a language model and ask whether the migration reconciled. Every property that makes migration assurance hard is a property models lack:
So the allocation is strict. Code owns the verdict on every batch. The model layer earns one narrow job: for records the hashes have already proven different, drill to the pair, quote both sides verbatim, and name a cause only when the quoted values prove the pattern mechanically. In this demo that layer is a deterministic stub, stated on screen, so the whole run reproduces exactly; the cage around it is what a real LLM would live in.
Each load batch is a pair of extracts: an ECC table on one side and its S/4HANA destination on the other, with the field mapping declared as data and applied in code (vendor numbers become Business Partner keys, dates become ISO, amounts become fixed two-decimal strings). Tier 1 compares row counts. Tier 2 compares control totals by company code or account group. Tier 3 canonicalises every record, hashes it with SHA-256, and compares the full multisets, then pairs the leftovers by business key to locate each difference at field level. A batch passes only if all three tiers match, and that verdict belongs to code alone.
The explainer's honesty rule is mechanical. It may assert "trailing zero dropped" only because the target amount is exactly the source amount divided by ten; it may assert "day and month transposed" only because the two date strings swap their middle segments; it may assert "deduplicated in Business Partner conversion" only because a second source record with identical name and city exists in the extract and its converted twin does. Where no such test holds it must print cause not determinable from extracts and quote the two values anyway. A code check confirms every quoted value is literally present in the extracts before the explanation renders.
This is the shipped decision function, condensed. Nothing downstream of it, model output included, can change what it returns.
// A batch passes only if all three tiers match. The mapping is data, // applied in code; canonical records are sorted-key strings, hashed whole. function reconcileBatch(batch) { var srcC = batch.src.map(function (r) { return canonSource(r, batch.map); }); var tgtC = batch.tgt.map(function (r) { return canonTarget(r, batch.map); }); var t1 = { pass: srcC.length === tgtC.length }; // tier 1: counts var t2pass = totalsMatch(controlTotals(srcC, batch.ctl), // tier 2: totals controlTotals(tgtC, batch.ctl)); // tier 3: per-record SHA-256 over every canonicalised row, then pair // the leftovers by business key to locate each difference field by field var mismatches = locateDifferences(srcC, tgtC, batch.map); return { srcHash: sha256(srcC.map(recordHash).sort().join("")), tgtHash: sha256(tgtC.map(recordHash).sort().join("")), pass: t1.pass && t2pass && mismatches.length === 0 }; }
Load Wave 1 and the three master-data batches reconcile clean: 580 source rows, three exact hash matches, one signature. Load Wave 2 and six batch cards hash themselves in sequence until two flip red. The engine locates exactly five differing records among 1,092 source rows and the explainer handles each on its own evidence:
Two of the five change no row count and no control total, which is the argument for tier 3 in one sentence: the checks migrations actually run today would have passed them. The go-live button sits grey through all of it, its label counting the undispositioned mismatches. Remediate a record and the batch reloads and re-reconciles clean, with the new hash in the ledger. Accept a difference and the ledger takes your rationale and your name. Sign both waves and the gate opens; the click that declares go-live is the last entry in the chain.
The extracts are synthetic and the five mismatches exist because I seeded them into a deterministic generator; the app says so on screen. No public ECC-to-S/4 extract pair exists, so the shapes mimic real, named SAP structures (SKA1, LFA1, KNA1, BSID/BSIK on the source side; ACDOCA and BUT000 on the target), and what changes shape follows SAP's own simplification list for the Business Partner conversion. What the demo proves is the control architecture: 100% of rows reconciled at field level rather than sampled, all five differences located to the exact field, every disposition named, and go-live provably blocked until the last one.
The limits are real. Demo scale is hundreds of rows per batch and 3,343 records in total; a real migration moves billions, and while the pattern scales, the browser does not. Delta and near-zero-downtime loads, unicode conversion and custom Z-tables are out of scope. And browser-side hashing proves that the extracts given to the app reconcile with each other; it cannot prove the extracts faithfully represent the source system, which remains an ITGC matter about extraction controls, and the app says that too.
Watch the batch cards hash themselves, open a red one to see the source and target records side by side with the differing field highlighted, disposition all five mismatches, sign both waves, and only then watch the go-live button come alive. Then tamper with the ledger and watch Verify locate the exact broken entry, and hit Replay to re-execute the reconciliation the way a year-end auditor would.
Migration sign-off is the one control decision a finance function makes while its control environment is packed in boxes. Every habit that makes the close auditable, versioned evidence, named approvals, re-performable checks, tends to dissolve for exactly the weeks the ledger is in transit, and the year-end audit arrives after the consultants have left. This build is my answer to that gap, and it reflects how I approach agentic AI in regulated finance generally: put the arithmetic beyond argument, confine the model to quoting what it can exhibit, and make the consequential click impossible until a named person has answered for every exception. S/4 programmes will be signed off thousands of times before the end of 2027. The artefact worth keeping from each one is a chain the auditor can re-run.