Result returned by Graph.simulate — the §5 dry-run API.

§5 names exactly three commit-mode shapes — strict, with-conflicts, and a separate graph.simulate(...) API for dry-run. simulate answers the question "what would happen if I ran this transaction?" without ever advancing Graph.now, appending to the commit log, or firing any subscriber. After return, engine state is byte-identical to the pre-call moment — the dry-run is observer-invisible.

The discriminator splits two arms:

  • 'clean' — the simulated transaction would have committed successfully. SimulateResultClean.commit is the Commit record that would have been published, with the GraphTime it would have landed at; SimulateResultClean.stagedDiff carries only the input ids whose value the staged write would have changed; SimulateResultClean.derivedDiff carries only the transitively-affected derived ids whose recomputed value would have differed. Their union is byte-equal to commit.changedNodes (modulo the engine-owned commitLog node, which simulate does not touch and therefore does not include).
  • 'failed' — the simulated transaction would have thrown. The typed error that would have escaped commit is surfaced on SimulateResultFailed.error instead, so the caller can branch on instanceof CycleError / NotAnInputNodeError / UnknownNodeError / etc. without a try/catch. stagedDiff reports the input ids the user callback staged before the throw — useful for debugging which write closed the cycle.

Re-entrancy: a simulate invoked inside an in-flight commit (or another simulate) throws CommitInProgressError synchronously — the same contract as nested commit. The throw is the one exception to "errors return as part of the result"; nesting is a structural misuse caught by absence-of-API, not a transactional failure mode.