p='crates/spt-daemon/src/relcache.rs'
b=open(p,'rb').read(); crlf=b'\r\n' in b; s=b.decode().replace('\r\n','\n')
def rep(a,n):
    global s
    assert s.count(a)==1,a
    s=s.replace(a,n)
rep('''        #[serde(default)]
        prior_version: Option<u64>,
    },
    /// The trial brain reached `ready` — the swap is accepted.''','''        #[serde(default)]
        prior_version: Option<u64>,
        /// The PRODUCT version (semver) running before this swap — the `old`
        /// half of the divulge row the promotion writes (releases#337), since
        /// the promoting broker may itself still be the old image or already
        /// the new one and cannot answer it from its own build. Additive like
        /// `prior_version`: absent reads `None`, and the divulge degrades.
        // [impl->REQ-NOW-SIGNAL-UPDATE-DIVULGE]
        #[serde(default, skip_serializing_if = "Option::is_none")]
        prior_product_version: Option<String>,
    },
    /// The trial brain reached `ready` — the swap is accepted.''')
rep('''    fn applied_state_path(&self) -> PathBuf {''','''    fn subject_applies_path(&self) -> PathBuf {
        self.dir.join(SUBJECT_APPLIES_FILE)
    }

    /// Record that `subject` (`spt-core`, or `adapter:<name>`) MOVED from one
    /// product version to another — the apply record the now-signal divulge
    /// row reads its old version and applied-at time from (releases#337).
    /// Newest wins per subject. Written by every writer that can apply: the
    /// CLI (a daemonless or `--finish` core swap, an adapter update) and the
    /// broker's trial promotion. Best-effort at the call site like the other
    /// records here: an unrecorded move degrades the row to its unmoved shape,
    /// never the apply. An unreadable file is started over rather than failing.
    // [impl->REQ-NOW-SIGNAL-UPDATE-DIVULGE]
    pub fn record_subject_apply(&self, subject: &str, apply: &SubjectApply) -> io::Result<()> {
        std::fs::create_dir_all(&self.dir)?;
        let mut all = self.subject_applies();
        all.insert(subject.to_string(), apply.clone());
        atomic_write_string(&self.subject_applies_path(), &serde_json::to_string(&all)?)
    }

    /// Every recorded subject move. Absent or corrupt reads as empty: the
    /// reader rides a turn-boundary hook, where absence is silence.
    // [impl->REQ-NOW-SIGNAL-UPDATE-DIVULGE]
    pub fn subject_applies(&self) -> BTreeMap<String, SubjectApply> {
        std::fs::read_to_string(self.subject_applies_path())
            .ok()
            .and_then(|s| serde_json::from_str(&s).ok())
            .unwrap_or_default()
    }

    fn applied_state_path(&self) -> PathBuf {''')
rep('''/// A staged update can be the legacy single-artifact shape or the newer''','''/// The apply-record file: one [`SubjectApply`] per subject, newest wins.
pub const SUBJECT_APPLIES_FILE: &str = "subject-applies.json";

/// The subject key a core move is recorded under.
pub const SUBJECT_CORE: &str = "spt-core";

/// The subject key an adapter's move is recorded under — one per adapter, so a
/// harness adapter and every shell running the same adapter read one record.
pub fn subject_adapter(name: &str) -> String {
    format!("adapter:{name}")
}

/// One subject's last version MOVE (releases#337): what it moved from and to,
/// when (epoch ms), the changelog link when the subject has one, and the
/// post-update notice the apply printed, folded into the same divulge row.
// [impl->REQ-NOW-SIGNAL-UPDATE-DIVULGE]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SubjectApply {
    pub old: String,
    pub new: String,
    pub at_ms: u64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub changelog: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub notice: Option<String>,
}

/// A staged update can be the legacy single-artifact shape or the newer''')
if crlf: s=s.replace('\n','\r\n')
open(p,'wb').write(s.encode())
