§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.
Result returned by Graph.simulate — the §5 dry-run API.