WTWilly Tai
Case studyAI Governance & Assurance

Recompute the disclosure, to the gram.

When an assurer signs a GHG number, they are signing that it can be re-derived from evidence: the activity data, a named emission factor, a stated method, all inside a defined boundary. This workbench does exactly that. It recomputes every disclosed figure independently, runs a tolerance test, and assigns Pass · Exception · Cannot-Verify. The arithmetic is the easy part. The hard part is the factor and method judgement, so the tool makes that judgement visible and interactive, and never lets a language model near a single number.

01 / Why this matters now

Assurance over emissions just became mandatory, and the numbers can't be traced.

From financial year 2025 the EU's Corporate Sustainability Reporting Directive requires limited assurance over the ESRS sustainability statement. The IAASB's ISSA 5000, effective for periods beginning on or after 15 December 2026, becomes the global baseline for how that assurance is done. The criteria are explicit: a reported emissions figure must be supported by evidence and based on proper methods, and an assurer must be able to reproduce it.

In practice they can't. A GHG number is a chain of activity × emission factor × unit conversion × estimation method, summed inside a Scope 1/2/3 boundary. Companies disclose the total but rarely the defensible trail behind it: which DEFRA fuel factor, which IEA grid vintage, which Scope-3 method, sourced and versioned. So the assurer either burns days rebuilding the calculation by hand, or issues a modified (qualified) opinion because the figure cannot be verified. This is the work, and it is almost entirely deterministic.

20
disclosed metrics
Scope 1/2/3 · synthetic statement.
18/20
recompute within ±5%
Independently, from activity data.
2 + 1
exceptions + cannot-verify
→ a modified opinion is flagged.
20/20
replayed to the gram
Hashes intact on re-run.

Figures from a synthetic suite (Helios Manufacturing SE, FY2025). They demonstrate the mechanism, not a real-world outcome.

02 / Why naive AI fails here

"Do these emissions look reasonable?" is the one question you must not ask a model.

Point a language model at a sustainability statement and ask whether the numbers are right, and it will pattern-match to a comforting yes: fluent, confident, and untethered to any factor, any activity row, any source. That is the precise opposite of assurance. Several things break on contact with the real task:

So the real job is a recompute against a cited, versioned factor library, with a tolerance test and an honest Cannot-Verify, plus a clear rule about where deterministic code stops and a human's judgement begins.

03 / The trust boundary

Code recomputes and decides. The model only phrases the conclusion.

This is the load-bearing decision of the whole system. Every number, every tolerance test, every verdict is computed deterministically from the activity rows and a cited factor: reproducible to the gram, re-runnable, hashable. The language model sits strictly downstream and read-only. It drafts the negative-form limited-assurance conclusion, the standard "nothing has come to our attention…" wording, about verdicts the engine already produced. It never selects a factor, never sets materiality, never decides a verdict, never touches a number. If the teeth are anywhere, they are in the code.

DETERMINISTIC · recomputed, reproducible, it DECIDES MODEL · language only, read-only, cited Activitylitres·kWh·km·€ Factor librarycited · versioned Recomputeqty × factor → tCO₂e Tolerance test±5% → verdict Opinionmodified? Conclusiontemplate · cited Sign-offengagement lead Append-only, hash-chained ledgerevery recompute · verdict · opinion · sign-off written here · tamper-evident · replayable
fig.1 · the trust boundary: deterministic recompute decides; the model only phrases the conclusion

The decision function is small on purpose, small enough to read, audit, and re-run. It resolves the cited factor (honouring the live version/method swap), recomputes from the activity rows, runs the tolerance test, and abstains when the evidence isn't there:

the deterministic decision · app.jsthis code, not a model, assigns the verdict
function verdict(m) {
  const rc = recompute(m);                       // qty × cited factor → tCO2e
  if (rc === null) {                             // no cited factor id / source…
    return { code: "CANNOT_VERIFY",              // …so we ABSTAIN, we do not
             reason: "no cited factor id / source on the disclosure" };
  }                                              //   assert correctness.
  const devPct = (m.reported_tco2e - rc.tco2e) / rc.tco2e * 100;
  const code = Math.abs(devPct) <= m.tolerance_pct ? "PASS" : "EXCEPTION";
  return { code, recomputed: rc.tco2e, devPct, factorId: rc.factorId };
}
The abstain rule is an actual code branch. No cited factor → Cannot-Verify → propose a modified opinion. The model is never on this path.
04 / Worked example

One factor-version decision, and two metrics cross the line.

Take the German grid electricity, metric M06. The engine joins its two activity rows, Munich and Leipzig, to the cited IEA grid factor, sums to tonnes, and compares to the reported figure. Under the 2024 vintage (0.381 kgCO₂e/kWh) it recomputes to 1,733.6 t against 1,759.6 reported: +1.5%, inside the ±5% band → Pass. Switch the assurer's control to the 2023 vintage (0.425) and the same metric recomputes to 1,933.8 t: −9.0%Exception. Nothing about the company changed. Only the factor judgement did.

MetricCited factorReportedRecomputedDeviationVerdict
M06 · DE gridF-IEA-GRID-DE-20241,759.61,733.6+1.5%Pass
M06 · DE gridF-IEA-GRID-DE-20231,759.61,933.8−9.0%Exception
M20 · APAC elec.none disclosed847.0n/aabstainedCannot-Verify
The judgement is the point. A tool that hid the factor choice would quietly answer a contested question. This one surfaces it: the assurer swaps the vintage and watches exactly two metrics (M06, M07) cross the modified-opinion line. For M20, where no factor is cited at all, it refuses to guess and proposes a qualification instead. Getting the unverifiable case right is what separates assurance from a calculator.
05 / What the run produced

The numbers, and, flatly, what they do and don't prove.

On the default (2024) configuration the engine recomputed 18 of 20 disclosed metrics within the ±5% tolerance. Two are exceptions, upstream road freight (M14, +10.1%) and modelled commuting (M19, +11.5%), and one is Cannot-Verify (M20, no cited factor). Together those three drive a modified (qualified) opinion, and the template drafts only that conclusion's wording. A full replay recomputes all twenty figures from scratch and reproduces them to the gram, with the ledger hash chain intact.

Where these come from, honestly. Helios Manufacturing SE is a synthetic company. The activity data, the factor library, and the seeded exceptions are all generated by a fixed-seed script (gen_data.py), so a third party can re-run the seed and obtain byte-identical data, identical verdicts, and identical hashes. That makes the figures a clean demonstration of the mechanism (recompute, tolerance, abstain, replay), not a finding about any real company's inventory.

The honest limit, stated plainly. Reproducibility is not the same as correctness of the factor or method choice. This workbench automates the part that should never have been manual, the arithmetic, and it makes the genuinely hard part visible and interactive: the factor vintage, the Scope-3 method, the unverifiable line. But it cannot adjudicate the contested judgements themselves: which grid vintage is appropriate for this entity, where a Scope-3 boundary should be drawn, whether a supplier-specific method is better evidenced than a spend-based one. Those remain the assurer's call. And the language model must stay strictly on conclusion wording. The moment it drifts toward factor selection or materiality, the control loses its teeth. The tool's job is to put the right question in front of a human, computed correctly, with the evidence attached.

AI Control Ledger · entryintegrity verified · replay 20/20 to the gram
seq        : 8
action     : recompute_metric
subject    : M06   (Purchased electricity , DE plants, location-based)
verdict    : PASS          reported : 1759.6 t   recomputed : 1733.6 t   dev : +1.50%
factor     : F-IEA-GRID-DE-2024   (IEA Emission Factors 2024)
prev_hash  : cf131ea7
hash       : 0a9d4e6b   ← = fnv1a(prev_hash + canonical(entry))
Threat model: append-only. Flip the verdict or a number without re-hashing and the chain breaks at exactly that seq. The Tamper button on the live app demonstrates it.
06 / The standard, met

How each governance requirement lands in the build.

ISSA 5000 / CSRD expectationHow this workbench meets it
Figures supported by evidenceEvery recomputed number traces to its activity row id(s) + factor id/version + source, all shown in the trace.
Based on proper, reproducible methodsSeeded generator + deterministic engine; replay reproduces all 20 figures to the gram.
Honest treatment of insufficient evidenceMissing factor → Cannot-Verify → proposes a modified opinion, rather than asserting correctness.
Judgement made by a qualified humanFactor/method choice is surfaced as a control; the engagement lead signs off the opinion before it is recorded.
Conclusion in the prescribed negative formThe template drafts only the "nothing has come to our attention…" wording, and nothing more.

Why I built it this way.

Sustainability assurance is where a lot of AI hope is about to meet a lot of regulatory reality, and the tempting shortcut, asking a model whether the emissions look right, is exactly the move that destroys the assurance. So I drew the trust boundary first. The calculation lives in code, reproducible to the gram, and the model is confined to the one thing it is genuinely good at: phrasing a standard conclusion, with citations, after the verdict is already decided. Then I refused to hide the part that's actually difficult. The factor vintage and the Scope-3 method are judgements, not facts, so I made them a dial the assurer turns and watches the opinion move. A demo that buried that choice would be lying about where the difficulty is. The real work is naming that difficulty and keeping the model's hands off every number.

See it run.

The recompute register, the live factor-version and method swap, the Cannot-Verify trace, the modified-opinion draft, and the tamper-and-replay ledger all run as a working, offline application over the synthetic statement.

Open the live workbench →Runs entirely in your browser: no network, no data leaves the page. Synthetic data throughout.