DocumentLifecycleController

Substrate-level document-lifecycle bookkeeping shared by every configuration-shaped app: which file backs the in-memory document (or null for an unsaved fresh document) and whether the in-memory state differs from that file.

Pre-decomposition, each of the four app controllers (Single, Scenario, Experiment, Simopt) reimplemented this with identical mechanics — currentFile: StateFlow<Path?> + isDirty: StateFlow<Boolean> + a markDirty() private mutator that flips the flag. This type captures that shared pair so each app controller can compose one rather than re-declare four.

What this owns

What this deliberately does NOT own

  • editedSinceLastRun / "stale result" flags. These cross-flow with the run lifecycle and stay with each app controller (or move to a substrate RunLifecycleController in a later sub-phase).

  • Side-effect fan-out on dirty flip — refreshers for step completion, validation, model-aware staleness, etc. are app-specific orchestration. Host controllers call markDirty and then fan their own side effects.

  • Config-payload typing. This controller never touches the config object — only the path and the boolean. The load/save methods that produce or consume a typed config live on the app controller, which delegates to this type for the file/dirty bookkeeping at the appropriate moments.

Semantics of the five mutators

MethodcurrentFileisDirtyTypical caller
markDirtyunchangedtrueevery editing mutator
markSavedpathfalseafter a successful save
bindFilepathunchangedafter a load whose mismatched / warning state the caller wants to keep flagged
clearDirtyunchangedfalseafter a load whose loaded state matches the file
resetnullfalseNew document / reset to fresh

Substrate-level API — usable by any UI shell. Plain class — no background work owned, no scope, not AutoCloseable.

Constructors

Link copied to clipboard
constructor()

Properties

Link copied to clipboard
val currentFile: StateFlow<Path?>

Path of the file currently bound to the in-memory document, or null when the document has never been saved or has been reset. Updated by markSaved, bindFile, and reset.

Link copied to clipboard
val isDirty: StateFlow<Boolean>

true when the in-memory document differs from the file on disk (or, when no file is bound, from a fresh document). Updated by markDirty, markSaved, clearDirty, and reset.

Functions

Link copied to clipboard
fun bindFile(path: Path)

Bind path as the file backing this document without changing the dirty flag. Use this when the load path has produced an in-memory state that may legitimately differ from the file (e.g. a legacy-decode that surfaces a warning, or an Open whose decoded state was intentionally edited before binding). Hosts that load-and-immediately-clean call bindFile then clearDirty — or call markSaved which combines both.

Link copied to clipboard

Clear the dirty flag without changing the bound file. Use after bindFile when the loaded state matches the file exactly, or whenever the host wants to assert "the in-memory state is now equivalent to whatever the bound file holds."

Link copied to clipboard
fun markDirty()

Idempotently mark the document dirty. No-op when already dirty (no spurious StateFlow emission). Hosts fan their own side effects (e.g. an editedSinceLastRun flag flip, validation refresh, last-result clearing) off this call; this method only flips the dirty flag.

Link copied to clipboard
fun markSaved(path: Path)

Bind path as the file backing this document AND mark the document clean. Called after a successful Save or Save As — the in-memory state now matches what's on disk at path.

Link copied to clipboard
fun reset()

Reset to a fresh, unbound, clean document. Called by the app controller's newDocument / resetConfiguration entry point after the per-app state has been reset.