May 29, 2026 · 10 min read

AuditForge: Why the Best Configuration Audit Has Zero AI In It

I got a brief from a client: audit their Oracle Fusion configuration across 12 Business Units and tell them where they're non-compliant against their internal baseline. My first instinct was to reach for an LLM. My second instinct, about 30 minutes later, was to throw it out entirely.

This is the story of AuditForge — a tool I built to do this job deterministically, traceably, and without a single call to any language model. In 2026, that feels like a contrarian decision. I think it's the right one.

The Oracle Fusion Problem

Oracle Fusion ERP is not a tidy system. A medium-sized deployment has 500+ configuration parameters per module. Semi-annual updates — Oracle ships two major and four minor releases per year — regularly rename parameters. BusinessUnitId becomes BusinessUnitName. CurrencyMnemonic becomes LedgerCurrency. These aren't breaking changes in Oracle's documentation, but they are silent landmines when you're comparing an audit baseline written six months ago against a fresh configuration export.

The client's workflow is this:

  1. Export current configuration from Oracle Fusion as a spreadsheet — one row per parameter, one column per Business Unit.
  2. Load the approved "ideal" baseline — also a spreadsheet, maintained by the ERP team.
  3. Compare them: for every parameter in the ideal baseline, find the corresponding row in the export, compare the values, and flag deviations.

That sounds simple. In practice: the column names don't match because different BUs have added their own labels. The values use inconsistent boolean representations — Y, Yes, TRUE, 1, Enabled, Active are all the same thing, but Excel sees them as different strings. And every Oracle Fusion update introduces new parameter names that break previous mappings.

The deliverable at the end is a 5-sheet Excel workbook that a SOX auditor will read. It has to say, for each finding: what parameter, what was expected, what was found, and how we determined they're the same parameter. The last part is where LLMs fall apart.

Why LLMs Don't Work Here

The obvious approach in 2026 is to throw an embedding model at the column matching problem. Ask GPT-4o to map AP_PAYMENT_TERMS_LOOKUP to the closest ideal-baseline column. Done.

Except it isn't done, for four reasons:

Non-determinism. Run the same audit twice with an LLM in the matching loop and you may get different findings. A regulator asks: "Why is this a control gap?" You say: "The model said the columns didn't match." That doesn't fly. SOX auditors in 2026 are increasingly asking for explicit, reproducible decision trails — "can you demonstrate the same result under identical inputs?" LLMs cannot.

Hallucination in the wrong direction. An LLM might confidently match AP_PAYMENT_TERMS_LOOKUP with AR_PAYMENT_TERMS. Those are not the same thing — one is accounts payable, one is accounts receivable. In a compliance context, a false positive match means a real control gap gets hidden. That is worse than reporting "unmatched — needs review."

No inspectable audit trail. LLM decisions aren't decomposable at the method level. "Cosine similarity: 0.87" is not an audit-workpaper-compatible justification. You cannot write that in a finding and hand it to a partner at a Big Four firm.

Data residency. The client will not send Oracle configuration data — which includes internal approval limits, payment terms, and security settings — to any external API. Full stop.

AuditForge 4-stage deterministic matching pipeline

Matching Pipeline At a Glance

STAGEMETHODCONFIDENCEAUDIT EVIDENCE
1 — ExactString equality after trim100%Method: Exact
2 — NormalizedLowercase + strip spaces / underscores95%Method: Normalized
3 — Manual AliasHardcoded Oracle Fusion rename dict92%Method: ManualAlias (commit SHA)
4 — Fuzzytoken_sort_ratio ≥ 80 (thefuzz)Score in outputMethod: Fuzzy (score: NN) — flagged if < 90
UnmatchedAll 4 stages failed< 80Method: Unmatched — manual review required

The 4-Stage Pipeline

AuditForge solves the column matching problem with four explicit stages. Every match carries its method — Exact, Normalized, ManualAlias, Fuzzy(Medium) — and that method appears in the output workbook next to the finding.

# Stage 1: exact
if normalize(actual_col) == normalize(ideal_col):
    return MappingResult(method="Exact", score=100, status="Matched")

# Stage 2: normalized (strip, lowercase, replace separators)
norm_a = re.sub(r'[\s_\-]+', '', actual_col.lower().strip())
norm_i = re.sub(r'[\s_\-]+', '', ideal_col.lower().strip())
if norm_a == norm_i:
    return MappingResult(method="Normalized", score=95)

# Stage 3: manual aliases — institutional knowledge in code
ORACLE_ALIASES = {
    "businessunitid": "businessunitname",
    "currencymnemonic": "ledgercurrency",
    "paymenttermsid": "paymenttermsname",
    # 40+ more from two years of Oracle Fusion releases
}
if ORACLE_ALIASES.get(norm_a) == norm_i:
    return MappingResult(method="ManualAlias", score=92)

# Stage 4: fuzzy fallback
score = fuzz.token_sort_ratio(actual_col, ideal_col)
if score >= 80:
    confidence = "Low" if score < 90 else "Medium"
    return MappingResult(method=f"Fuzzy({confidence})", score=score)

return MappingResult(method="Unmatched", score=0, status="Review required")

The cascade is intentional. Exact match is used first — it's free and unambiguous. Normalized match catches the vast majority of minor formatting inconsistencies. Manual aliases handle known Oracle Fusion rename history. Fuzzy matching is a last resort, and when it fires, the output is flagged for human review.

The comparator that runs after matching is equally explicit. Values go through a normalization step before comparison:

TRUE / true / True / 1 / Y / Yes / yes / Enabled / enabled / On / on / Active / active  →  y
FALSE / false / False / 0 / N / No / no / Disabled / Off / Inactive                      →  n

That's 30+ synonyms across Oracle Fusion's inconsistent export formats. Without this, a correctly-configured system gets flagged as non-compliant because one BU exports Enabled and the baseline says Y. That's not a control gap. It's a string-comparison failure.

The Manual Aliases Dict Is Not a Workaround

This is the part I want to push back on most.

When I show engineers the ORACLE_ALIASES dict — a Python dictionary with 40+ entries mapping old Oracle Fusion parameter names to new ones — the first reaction is often: "Can't you automate that?" The subtext is: this looks like technical debt.

It isn't.

When Oracle releases Fusion update 24C, they rename BusinessUnitId to BusinessUnitName. The ideal baseline (written against the pre-24C schema) uses the old name. The live export uses the new one. A fuzzy matcher gives this pair a score of 72, which falls below the threshold. Without the alias, every BU shows "Unmatched" for that parameter. The audit flags 12 phantom control gaps.

The aliases dict is version-controlled Python that encodes Oracle Fusion's rename history. It's reviewable in a pull request. It's updateable in 30 seconds when Oracle ships the next release. It's not a workaround — it's the institutional knowledge made explicit and auditable.

The most important thing about the aliases dict isn't that it solves the matching problem. It's that it's in version control. When a regulator asks "why did you match these two columns?", the answer is: "commit abc123, added June 2025, based on Oracle Fusion 24B release notes." That's defensible. An LLM match isn't.

And it grows. Two years of Oracle Fusion releases have contributed 40+ entries. Each entry is a decision — reviewed, committed, traceable. That's a better audit trail than a probability score from a model whose weights you don't control.

Typical AuditForge output breakdown

Desktop Delivery: Why the .exe Matters

AuditForge ships as a PyInstaller-built .exe. The auditor downloads it, double-clicks it, selects their two Excel files, and gets back a 5-sheet Excel workbook. No network calls. No API keys. No Python installation required.

This was a hard requirement, not an engineering preference.

The client cannot send Oracle configuration data to any external service. This isn't paranoia — it's standard practice. Internal approval limits, payment terms, GL account configurations, and security role assignments are all in those spreadsheets. GDPR, confidentiality obligations, and internal IT policy all say: this data stays on-premise.

PyInstaller packages the entire runtime — Python interpreter, pandas, openpyxl, thefuzz — into a single executable. The bundle is roughly 180MB, which is large but acceptable for a tool that gets run quarterly. The main packaging challenge is hidden imports: pandas and openpyxl have dynamic import patterns that PyInstaller's static analysis misses. The --hidden-import flags for openpyxl.cell._writer, pandas._libs.tslibs.np_datetime, and a handful of others are the kind of thing you discover by running the binary on a clean machine.

The output is a workbook that a non-technical auditor can read without training:

  • Sheet 1: Summary — pass rate, gap count, unmatched count, methodology note
  • Sheet 2: Control gaps — parameter, BU, expected value, actual value, matching method
  • Sheet 3: Captured extracts — parameters present in the export but not in the baseline
  • Sheet 4: Unmatched columns — flagged for human review
  • Sheet 5: Methodology — the exact pipeline version, date, and alias dict hash

Sheet 5 is not decoration. It's the artifact that lets the audit team say: "This workbook was produced by AuditForge v1.4, pipeline hash abc123, on 2025-06-14." Reproducibility at the document level.

Where Deterministic Falls Short

Honest accounting: the pipeline has real limitations.

New Oracle Fusion modules with naming conventions that haven't been seen before will land in the fuzzy-fallback bucket, and some will fall below threshold and land in "Unmatched — Review required." Someone has to review those and, if appropriate, add an alias. The pipeline doesn't self-extend.

Multi-language deployments are harder. A client running Oracle Fusion with German-language configuration exports gets column names in German. The ideal baseline is in English. Stage 1 and Stage 2 won't help. The fuzzy matcher will fail. The aliases dict needs German entries. That's doable, but it requires someone who knows both the Oracle Fusion schema and the localized column naming conventions.

What v2 looks like: keep the deterministic pipeline in the critical path — it's not going anywhere. But add a suggestion layer for the unmatched bucket. On columns that fall below threshold, an LLM (running locally, no external API) generates candidate alias suggestions: "This column might correspond to X or Y — please review." The human reviews the candidates, approves or rejects, and the decision gets committed to the aliases dict. The LLM proposes. The human decides. The decision is recorded.

That's the architecture: deterministic in the critical path, AI in the advisory layer, human in the approval loop.

The Bigger Point

In 2026, the pressure to add AI to everything is real. Every tools pitch starts with "AI-powered." Every job description asks for "LLM integration experience." The agentic AI wave — autonomous multi-step agents handling complex workflows — is accelerating this. If your agent can write code, book meetings, and draft emails, surely it can match Oracle Fusion column headers.

AuditForge is a deliberate counter-example.

There are domains where "probably correct" is worse than "I don't know." Compliance and financial audit are two of them. A false positive match — two columns incorrectly identified as the same parameter — hides a real control gap. Hidden control gaps in a SOX audit are not a "model accuracy" problem. They're a liability problem.

The right instinct from the agentic AI wave is not "add AI to the critical path of every decision." It's: "now that I know what AI can do, I can be more precise about where it belongs." Advisory layer, yes. Suggestion engine, yes. Human-in-the-loop for irreversible decisions, yes. Sole decision-maker in a regulated audit workflow, no.

AuditForge handles hundreds of Oracle Fusion configuration parameters per audit. Every finding in the output workbook traces to a specific method in the pipeline. That traceability — method, score, commit hash, date — is not overhead. It is the product. It's what makes the finding defensible when a regulator asks the question auditors are now trained to ask: "Show me how you got that answer."

A deterministic pipeline can show its work. An LLM cannot.


AuditForge is a bespoke internal tool, not publicly available. If you're dealing with Oracle Fusion configuration audits at scale, the pipeline architecture described here — four-stage matching, explicit boolean normalization, version-controlled alias dictionary — is a reasonable starting point regardless of implementation language.

Related topics
AuditForgeDeterministic AIOracle Fusion +3

T
Tanmay Bohra
Full Stack Engineer at Grant Thornton Bharat. Building high-concurrency systems in Go and TypeScript.
← portfolio chat with tanmay ↗