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('''    fn applied_state_path(&self) -> PathBuf {''','''    /// The DAEMON writer of the core apply record: a trial promotion is the
    /// moment the broker accepts a swapped core, so it records the move there —
    /// old from the pending record's `prior_product_version` (captured at the
    /// swap), new from the staged set's signed product version. Called BEFORE
    /// the promotion overwrites the pending record. Writes nothing when either
    /// half is unknown or nothing moved, so the row degrades rather than lies.
    // [impl->REQ-NOW-SIGNAL-UPDATE-DIVULGE]
    pub fn record_core_promotion(&self, at_ms: u64, docs_base: Option<&str>) -> Option<SubjectApply> {
        let Some(AppliedRecord::AppliedPending {
            prior_product_version: Some(old),
            ..
        }) = self.applied_state()
        else {
            return None;
        };
        let new = self.staged_update()?.product_version()?;
        let apply = core_subject_apply(&old, &new, at_ms, docs_base)?;
        self.record_subject_apply(SUBJECT_CORE, &apply).ok()?;
        Some(apply)
    }

    fn applied_state_path(&self) -> PathBuf {''')
rep('''/// A staged update can be the legacy single-artifact shape or the newer''','''/// Where the core changelog lives: the node's own served docs when they are
/// discoverable, else the release channel's page — the same pair the apply
/// messages and the consent notif already link.
// [impl->REQ-NOW-SIGNAL-UPDATE-DIVULGE]
pub fn core_changelog_url(docs_base: Option<&str>) -> String {
    match docs_base {
        Some(base) => format!("{base}changelog.html"),
        None => CORE_RELEASES_URL.to_string(),
    }
}

/// Offline fallback for the core changelog link.
pub const CORE_RELEASES_URL: &str = "https://github.com/BigscreenVR/spt-bs-releases/releases";

/// The core subject's move, shared by BOTH writers (the CLI's completed swap
/// and the broker's promotion) so a core moved by either reads identically.
/// `None` when either version is empty or nothing moved.
// [impl->REQ-NOW-SIGNAL-UPDATE-DIVULGE]
pub fn core_subject_apply(
    old: &str,
    new: &str,
    at_ms: u64,
    docs_base: Option<&str>,
) -> Option<SubjectApply> {
    let (old, new) = (old.trim(), new.trim());
    if old.is_empty() || new.is_empty() || old == new {
        return None;
    }
    Some(SubjectApply {
        old: old.to_string(),
        new: new.to_string(),
        at_ms,
        changelog: Some(core_changelog_url(docs_base)),
        notice: None,
    })
}

/// 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())

p='crates/spt-daemon/src/brainproc.rs'
b=open(p,'rb').read(); crlf=b'\r\n' in b; s=b.decode().replace('\r\n','\n')
rep('''    fn record_promoted(&self, version: u64) {
        let cache = self.cache();
        let _ = cache.record_applied_state(&AppliedRecord::Applied { version });''','''    fn record_promoted(&self, version: u64) {
        let cache = self.cache();
        // The daemon writer of the divulge's apply record — read off the
        // pending record, so it runs BEFORE that record is overwritten below.
        // [impl->REQ-NOW-SIGNAL-UPDATE-DIVULGE]
        let docs_base = crate::docshost::live_docs_url();
        let _ = cache.record_core_promotion(crate::release::now_ms(), docs_base.as_deref());
        let _ = cache.record_applied_state(&AppliedRecord::Applied { version });''')
if crlf: s=s.replace('\n','\r\n')
open(p,'wb').write(s.encode())
