yog 0.0.3

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! Per-snapshot memo for frame-side view-model builds (§7.2, bl-e90a).
//!
//! The frame renders snapshots and derives nothing per frame — but a build that
//! is a function of disk *and* of frame-owned selection (§5.3) cannot be made
//! ahead by the worker: it does not know which agent is focused or which notch
//! is pinned. The honest middle is this memo: the frame builds such a
//! view-model **at most once per published snapshot per key**, because every
//! disk fact those builds read is change-tracked by the snapshot itself
//! (`last_action_unix` folds the newest `messages/` mtime, `Agent::stream` the
//! live tail — a change that matters forces a publish, §7.2). A pulse repaint
//! or a scroll frame therefore re-reads nothing: the 60 Hz cost is a pointer
//! compare, and the rebuild cadence is the snapshot's own (debounce-bounded,
//! ≤10/s under a storm).
//!
//! **Most of its callers are gone, and the reason is the ruling** (REMOTE
//! §9.7). A surface that reads over the wire needs no memo — an answer *is* the
//! cached fold, refreshed at the asker's cadence rather than the derivation's,
//! so a memo in front of one is a second cache over the same fact. bl-adcb took
//! the ops trail and the board, bl-f297 the Work tab's two, and bl-13f9 the §11
//! inspector's four (transcript, steps, rail, files). What is left is the V2
//! fork composer's choices, which is a *draft's* input rather than a §11 read.
//!
//! **The snapshot a memo keys on is the *derivation*, never the fold**
//! (bl-54f7, [`AppModel::derivation`](crate::AppModel::derivation)). A memo
//! caches a read of disk, and the two non-derived facts a frame paints — the
//! pending echo and the live tail — are by definition not on disk. Keying one
//! on the rendered snapshot would rebuild every disk read whenever an echo
//! landed or a live character did, which is exactly the cost bl-e90a removed,
//! restored with a new trigger.
//!
//! One slot, not a table: a memo serves one surface, and a surface shows one
//! subject at a time — a new key is a new question and the old answer is not
//! worth keeping (the same shape as [`super::WoundGrace`]).

use std::sync::Arc;

use super::Snapshot;

/// One surface's per-snapshot build cache: the snapshot it answered, the key
/// it answered for, and the answer. See the module doc for why this exists
/// and why snapshot identity is a sound invalidation signal.
pub(crate) struct SnapMemo<K: PartialEq, V> {
    slot: Option<(Arc<Snapshot>, K, V)>,
}

impl<K: PartialEq, V> Default for SnapMemo<K, V> {
    fn default() -> Self {
        Self { slot: None }
    }
}

impl<K: PartialEq, V> SnapMemo<K, V> {
    /// The memoized value for `key` under `snap`, building it only when the
    /// snapshot (by `Arc` identity — the worker never mutates a published one,
    /// §7.2) or the key differs from the held answer. The builder is `dyn` so
    /// `read` has one instantiation per (K, V) pair rather than one per
    /// call-site closure — the §12.1 monomorphized-per-closure llvm-cov
    /// phantom, which the coverage-excluded shell's call sites would otherwise
    /// attribute onto these lines.
    pub(crate) fn read(
        &mut self,
        snap: &Arc<Snapshot>,
        key: K,
        build: &mut dyn FnMut() -> V,
    ) -> &V {
        let hit = match &self.slot {
            Some((held, k, _)) => Arc::ptr_eq(held, snap) && *k == key,
            None => false,
        };
        if !hit {
            self.slot = None;
        }
        let fill = || (Arc::clone(snap), key, build());
        &self.slot.get_or_insert_with(fill).2
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn snap() -> Arc<Snapshot> {
        Arc::new(Snapshot::empty(0))
    }

    /// The caching contract: one build per (snapshot, key), however many
    /// frames read it.
    #[test]
    fn a_second_frame_on_the_same_snapshot_and_key_builds_nothing() {
        let mut memo: SnapMemo<&str, u32> = SnapMemo::default();
        let snap = snap();
        let mut builds = 0;
        for _ in 0..3 {
            let v = *memo.read(&snap, "agent-1", &mut || {
                builds += 1;
                7
            });
            assert_eq!(v, 7);
        }
        assert_eq!(builds, 1, "same snapshot, same key: one build");
    }

    #[test]
    fn a_new_snapshot_rebuilds_even_under_the_same_key() {
        let mut memo: SnapMemo<&str, u32> = SnapMemo::default();
        let (a, b) = (snap(), snap());
        let mut builds = 0;
        let mut bump = || {
            builds += 1;
            builds
        };
        assert_eq!(*memo.read(&a, "agent-1", &mut bump), 1);
        assert_eq!(*memo.read(&b, "agent-1", &mut bump), 2, "new snapshot");
    }

    /// The shell's one live shape holds the same contract — the structural
    /// assertion that the V2 fork composer's choices are derived once per
    /// snapshot, exercised on the exact key/value types the shell keys (each
    /// generic instantiation carries its own coverable copy of `read`).
    #[test]
    fn the_shells_live_memo_shape_builds_once_per_snapshot() {
        use crate::fork::Choices;
        use std::path::PathBuf;
        let snap = snap();
        let key = || (PathBuf::from("/ws"), "agent-1".to_string());
        let mut builds = 0;
        let mut fork: SnapMemo<(PathBuf, String), Choices> = SnapMemo::default();
        for _ in 0..2 {
            let _ = fork.read(&snap, key(), &mut || {
                builds += 1;
                Choices::default()
            });
        }
        assert_eq!(builds, 1, "one derivation of the fork's choices");
    }

    #[test]
    fn a_new_key_rebuilds_and_evicts_the_old_answer() {
        let mut memo: SnapMemo<&str, u32> = SnapMemo::default();
        let snap = snap();
        let mut builds = 0;
        let mut bump = || {
            builds += 1;
            builds
        };
        assert_eq!(*memo.read(&snap, "agent-1", &mut bump), 1);
        assert_eq!(*memo.read(&snap, "agent-2", &mut bump), 2, "new key");
        // One slot: returning to the first key is a rebuild, not a hit.
        assert_eq!(*memo.read(&snap, "agent-1", &mut bump), 3);
    }
}