///|
/// One recorded visit to a locus, flattened for display.
///
/// The projection of `@model.Residue` that crosses a surface boundary: the
/// domain record additionally carries markdown encode/decode and content
/// hashes, none of which a renderer needs. Keeping the rendered shape separate
/// is what lets the CLI print a box, the TUI lay out a pane, and the desktop
/// emit DOM from one value instead of three.
pub(all) struct Residue {
  /// Locus this residue belongs to.
  locus : String
  /// Session surface or tier: "haiku", "sonnet", "opus", "codex".
  tier : String
  /// Short session identifier, e.g. "a7f3c".
  session_short_id : String
  /// ISO8601 timestamp of exit.
  timestamp : String
  /// Prior residue read on entry; `None` for a cold entry.
  picked_up_from : String?
  /// What the session accomplished.
  what_i_did : Array[String]
  /// What the session left unfinished.
  what_i_left_open : Array[String]
  /// Advice for whoever enters next.
  recommendation : String?
} derive(Eq, Debug)

///|
/// Canonical residue filename, matching `@model.Residue::filename`.
pub fn Residue::filename(self : Residue) -> String {
  "\{self.tier}-\{self.session_short_id}-exit.md"
}

///|
/// A locus and, when it has been visited, its most recent residue.
pub(all) struct Locus {
  /// Directory name under the loci root.
  name : String
  /// One-line statement of what the locus is for.
  spirit : String
  /// Free-form classification tags.
  tags : Array[String]
  /// Number of residues filed against this locus.
  trail_depth : Int32
  /// Most recent residue, or `None` when the locus has never been entered.
  last_visit : Residue?
} derive(Eq, Debug)

///|
/// Every residue filed against one locus, newest first.
pub(all) struct Trail {
  /// Locus the residues belong to.
  locus : String
  /// Residues ordered newest first, as `trail` and `enter` present them.
  sessions : Array[Residue]
} derive(Eq, Debug)

///|
/// Open threads carried by the newest residue, or an empty array when the
/// locus is unvisited or the last session closed everything.
///
/// The question every surface asks on entry, so it is answered once here
/// rather than re-derived by each of them.
pub fn Trail::open_threads(self : Trail) -> Array[String] {
  match self.sessions {
    [newest, ..] => newest.what_i_left_open
    [] => []
  }
}