DEV Community

Todd Sullivan
Todd Sullivan

Posted on

The Exporter Was Easy. Making It Deterministic Was the Work.

The Exporter Was Easy. Making It Deterministic Was the Work.

I spent this week building a mobile export pipeline for UK energy assessment data.

The output is not glamorous: take completed survey responses, turn them into an RdSAP XML file, package the evidence, write a manifest, calculate a checksum, and keep a local audit trail.

The interesting part was not generating XML. The interesting part was making the whole thing deterministic and testable before adding more export formats.

The shape ended up like this:

responses
  -> buildAssessment()
  -> validateAssessment()
  -> exporter.serialize(assessment)
  -> sha256
  -> write package
  -> persist audit log
Enter fullscreen mode Exit fullscreen mode

The exporter itself is deliberately boring:

serialize(assessment): string
Enter fullscreen mode Exit fullscreen mode

No file system. No clock. No database. No random IDs. Same assessment in, same bytes out.

That one rule makes the rest of the pipeline much easier to reason about. If the XML changes, it changed because the input changed or the serializer changed. Not because a timestamp moved, a native module behaved differently, or a test environment had a different document directory.

Side effects at the edge

The orchestrator is the only layer allowed to do side effects:

const body = exporter.serialize(assessment);
const checksum = await deps.sha256Hex(body);
const pkg = await writeExportPackage(
  { inspectionId, exporter, assessment, body, checksum },
  deps.fs
);
await deps.persistLog(log);
Enter fullscreen mode Exit fullscreen mode

Those dependencies are ports:

export type ExportDeps = {
  fs: FileSystemPort;
  sha256Hex: (input: string) => Promise<string>;
  persistLog: (entry: ExportLogEntry) => Promise<void>;
};
Enter fullscreen mode Exit fullscreen mode

In production, they map to Expo file system, Expo crypto, and a local SQLite audit table.

In tests, they are an in-memory file system, Node crypto, and an array log sink.

That was not architectural theatre. Under jest-expo, native modules are often partially unavailable. documentDirectory can be undefined. Crypto enums can be missing. If the export logic directly imports and touches those modules, the “end-to-end” test either becomes a mock festival or stops covering the actual path.

With ports, the test runs the real pipeline:

const { deps, files, logs } = memoryDeps();
const run = await runExport(input(), deps);

const body = files.get(expectedPath)!;
expect(run.result.checksum).toBe(
  createHash("sha256").update(body, "utf8").digest("hex")
);
Enter fullscreen mode Exit fullscreen mode

That test proves a few things at once:

  • the package path is correct
  • the written XML is exactly the pure exporter output
  • the checksum is a real SHA-256 of the bytes
  • the manifest references the same checksum
  • one audit entry is written

There are also tests for evidence attachment copying, missing attachment sources, validation-blocked exports, checksum reproducibility, and a second JSON archive exporter running through the same pipeline.

Validation blocks, but still logs

One design choice I care about: failed validation writes an audit record too.

If a mandatory field is missing, the export does not create files. But the attempt is still logged with success: false, validation counts, schema version, and no checksum.

That makes the export feature behave like a real operational system rather than a button that either emits a file or silently refuses.

Why this matters for AI-built software

This is the seam I want when using AI heavily in engineering work.

LLMs are very good at producing a first version of “convert this shape into that schema.” They are much less reliable if the codebase lets schema mapping, file IO, logging, hashing, and UI state collapse into one blob.

The fix is not to use less AI. The fix is to give the code stronger boundaries:

  • pure transformations for the model or human to edit safely
  • deterministic outputs that can be snapshot-tested or checksummed
  • injected IO so tests exercise the pipeline without native dependencies
  • audit trails for both success and blocked paths
  • format-specific exporters behind a small registry

Once those seams exist, adding the next export format is not a rewrite. It is another serializer plus a few tests.

That is the part I keep finding in real AI-assisted development: the model can help move fast, but the architecture has to make fast changes safe.


Source: Recent mobile export module work: RdSAP XML export pipeline, pure serializers, injectable IO ports, SHA-256 packaging, local audit logging, and 24 export tests.
Tags: ai, testing, typescript, mobile
Status: published

Top comments (0)