p='crates/spt/src/api/nowsignal.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('''pub fn gather_updates(ctx: &Ctx, input: &PollInput, seen: &mut SeenSet) -> Vec<String> {
    let mut out = Vec::new();
''','''pub fn gather_updates(ctx: &Ctx, input: &PollInput, seen: &mut SeenSet) -> Vec<String> {
    let mut out = Vec::new();
    // The apply record supplies only the two facts a row cannot know — the
    // OLD version and WHEN — and is consulted only for a subject the seen-set
    // already said is new. It never decides that anything moved.
    // [impl->REQ-NOW-SIGNAL-UPDATE-DIVULGE]
    let applies =
        spt_daemon::ReleaseCache::open(&perch::spt_home().join("releases")).subject_applies();
    let row = |label: &str, version: &str, subject: &str| {
        update_row(label, version, applies.get(subject), &chrono::Local)
    };
''')
rep('''    if seen.take_new(&format!("core={core}")) {
        out.push(format!("spt-core {core}"));
    }''','''    if seen.take_new(&format!("core={core}")) {
        out.push(row("spt-core", core, spt_daemon::SUBJECT_CORE));
    }''')
rep('''        if seen.take_new(&key) {
            out.push(format!(
                "harness adapter {} {}",
                m.adapter.name, m.adapter.version
            ));
        }''','''        if seen.take_new(&key) {
            out.push(row(
                &format!("harness adapter {}", m.adapter.name),
                &m.adapter.version,
                &spt_daemon::subject_adapter(&m.adapter.name),
            ));
        }''')
rep('''        if seen.take_new(&key) {
            out.push(format!(
                "shell {shell_id} adapter {} {}",
                info.adapter_name, manifest.adapter.version
            ));
        }
    }

    out
}
''','''        if seen.take_new(&key) {
            out.push(row(
                &format!("shell {shell_id} adapter {}", info.adapter_name),
                &manifest.adapter.version,
                &spt_daemon::subject_adapter(&info.adapter_name),
            ));
        }
    }

    out
}

/// One UPDATES row. A subject whose recorded move LANDS on the version being
/// told reads `UPDATED <subject> <old> → <new> at <HH:MMAM|PM YYYY-MM-DD>`,
/// then ` — changelog: <url>` only when the subject has one, then the
/// post-update notice folded onto the same line. Every other case — no
/// record, a record for a different version (a rollback, a stale move), an
/// empty old version, an unrenderable time — is the unmoved one-line shape,
/// byte for byte: this rides a turn-boundary hook, where a half-filled row or
/// an error line is noise the reader cannot act on. Pure over the time zone.
// [impl->REQ-NOW-SIGNAL-UPDATE-DIVULGE]
pub fn update_row<Tz: chrono::TimeZone>(
    label: &str,
    version: &str,
    moved: Option<&spt_daemon::SubjectApply>,
    tz: &Tz,
) -> String {
    let unmoved = format!("{label} {version}");
    let Some(m) = moved else {
        return unmoved;
    };
    if m.new != version || m.old.trim().is_empty() || m.old == m.new {
        return unmoved;
    }
    let Some(at) = applied_at(m.at_ms, tz) else {
        return unmoved;
    };
    let mut row = format!("UPDATED {label} {} → {} at {at}", m.old, m.new);
    if let Some(url) = m.changelog.as_deref().filter(|u| !u.trim().is_empty()) {
        row.push_str(" — changelog: ");
        row.push_str(url);
    }
    if let Some(notice) = &m.notice {
        let one_line = notice.split_whitespace().collect::<Vec<_>>().join(" ");
        if !one_line.is_empty() {
            row.push_str(" — ");
            row.push_str(&one_line);
        }
    }
    row
}

/// `HH:MMAM|PM YYYY-MM-DD` in `tz` (12-hour, zero-padded), or `None` for an
/// instant the calendar cannot place.
// [impl->REQ-NOW-SIGNAL-UPDATE-DIVULGE]
fn applied_at<Tz: chrono::TimeZone>(at_ms: u64, tz: &Tz) -> Option<String> {
    let utc = chrono::DateTime::from_timestamp_millis(i64::try_from(at_ms).ok()?)?;
    Some(utc.with_timezone(tz).naive_local().format("%I:%M%p %Y-%m-%d").to_string())
}
''')
if crlf: s=s.replace('\n','\r\n')
open(p,'wb').write(s.encode())
