p='crates/spt-runtime/src/registry.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('''    CoreFloor { adapter: String, core: String, floor: String },
}
''','''    CoreFloor { adapter: String, core: String, floor: String },
    /// The manifest declares `[adapter].changelog` but the file is missing from
    /// the installed tree, unreadable, or not UTF-8 markdown. Refused BEFORE any
    /// registration write, so no adapter installs whose changelog page would
    /// answer with a broken page.
    // [impl->REQ-ADAPTER-CHANGELOG]
    Changelog { adapter: String, path: String, why: String },
}
''')
rep('''                 runs spt-core {core}. Update spt-core first (`spt update`), then retry."
            ),
''','''                 runs spt-core {core}. Update spt-core first (`spt update`), then retry."
            ),
            RegistryError::Changelog { adapter, path, why } => write!(
                f,
                "ADAPTER_CHANGELOG_UNREADABLE:{adapter}: the manifest declares changelog \
                 {path:?} but it {why} — the install is refused so the adapter's changelog \
                 page never serves broken. Ship the file inside the adapter archive, or drop \
                 [adapter].changelog."
            ),
''')
rep('''    manifest
        .validate_role_templates()
        .map_err(RegistryError::Manifest)?;
''','''    manifest
        .validate_role_templates()
        .map_err(RegistryError::Manifest)?;

    // A declared changelog is read and rendered HERE, before the first write:
    // a missing/unreadable file refuses the whole registration (the add AND the
    // update verb pass this choke point), and rendering once at install is what
    // lets the daemon stay a byte-server.
    // [impl->REQ-ADAPTER-CHANGELOG]
    let changelog_html = render_declared_changelog(&manifest, Path::new(&source_dir))?;
''')
rep('''    spt_store::perch::create_adapter_web_dir_at(adapters_dir, &record.name)?;
    if mode == RecordMode::Copy {''','''    spt_store::perch::create_adapter_web_dir_at(adapters_dir, &record.name)?;
    // The sidecar follows the declaration it was rendered from: an update that
    // drops `[adapter].changelog` retires the page rather than serving the
    // last version's.
    // [impl->REQ-ADAPTER-CHANGELOG]
    let sidecar = changelog_sidecar_file(adapters_dir, &record.name);
    match &changelog_html {
        Some(html) => atomic_write_string(&sidecar, html)?,
        None => match std::fs::remove_file(&sidecar) {
            Ok(()) => {}
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
            Err(e) => return Err(RegistryError::Io(e)),
        },
    }
    if mode == RecordMode::Copy {''')
rep('''/// Resolve the manifest file inside a source: `<dir>/manifest.toml`, or the''','''/// Core's rendered changelog sidecar for `name` — the file the node serves at
/// `/<node>/a/<name>/changelog` (`spt_store::serving::adapter_changelog_page_in`
/// names the same path from the home).
// [impl->REQ-ADAPTER-CHANGELOG]
fn changelog_sidecar_file(adapters_dir: &Path, name: &str) -> PathBuf {
    adapters_dir.join(name).join("changelog.html")
}

/// Read and render the manifest's declared `[adapter].changelog`, resolved
/// against the installed tree. `Ok(None)` = nothing declared (the common case).
/// The load-time shape check already refused absolute / `..` / non-`.md`
/// paths; this is the half that needs the files.
// [impl->REQ-ADAPTER-CHANGELOG]
fn render_declared_changelog(
    manifest: &Manifest,
    source_dir: &Path,
) -> Result<Option<String>, RegistryError> {
    let Some(rel) = manifest.adapter.changelog.as_deref() else {
        return Ok(None);
    };
    let refuse = |why: String| RegistryError::Changelog {
        adapter: manifest.adapter.name.clone(),
        path: rel.to_string(),
        why,
    };
    let path = source_dir.join(rel);
    if !path.is_file() {
        return Err(refuse("is not present in the installed adapter tree".into()));
    }
    let bytes = std::fs::read(&path).map_err(|e| refuse(format!("could not be read ({e})")))?;
    let markdown =
        String::from_utf8(bytes).map_err(|_| refuse("is not UTF-8 markdown text".into()))?;
    Ok(Some(render_changelog_html(&manifest.adapter.name, &markdown)))
}

/// Render changelog markdown into the standalone page core serves. Pure, so
/// the rendering contract is a unit rather than an install.
// [impl->REQ-ADAPTER-CHANGELOG]
pub fn render_changelog_html(adapter: &str, markdown: &str) -> String {
    let mut body = String::new();
    let parser = pulldown_cmark::Parser::new_ext(markdown, pulldown_cmark::Options::all());
    pulldown_cmark::html::push_html(&mut body, parser);
    let title: String = adapter
        .chars()
        .map(|c| match c {
            '&' => "&amp;".to_string(),
            '<' => "&lt;".to_string(),
            '>' => "&gt;".to_string(),
            '"' => "&quot;".to_string(),
            c => c.to_string(),
        })
        .collect();
    format!(
        "<!DOCTYPE html>\n<html><head><meta charset=\\"utf-8\\"><title>{title} changelog</title>\
         </head><body>\n{body}</body></html>\n"
    )
}

/// Resolve the manifest file inside a source: `<dir>/manifest.toml`, or the''')
if crlf: s=s.replace('\n','\r\n')
open(p,'wb').write(s.encode())
p='crates/spt-runtime/Cargo.toml'
b=open(p,'rb').read(); crlf=b'\r\n' in b; s=b.decode().replace('\r\n','\n')
a='regex = "1"\n'
assert s.count(a)==1
s=s.replace(a,a+'''# [adapter].changelog (releases#340): the declared markdown is rendered to
# HTML once, at install/update, so the daemon only serves bytes. Small,
# pure-Rust, no default features (drops the getopts CLI); nothing else in the
# tree renders markdown (mdbook is release-side tooling, not a dependency).
pulldown-cmark = { version = "0.13", default-features = false, features = ["html"] }
''')
if crlf: s=s.replace('\n','\r\n')
open(p,'wb').write(s.encode())
