p='crates/spt-runtime/src/manifest.rs'
b=open(p,'rb').read(); crlf=b'\r\n' in b; s=b.decode().replace('\r\n','\n')
anchor='''    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub host_binaries: Vec<String>,
}
'''
assert s.count(anchor)==1
new='''    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub host_binaries: Vec<String>,
    /// Optional one-paragraph plain-text summary of what the adapter is, at
    /// most 300 characters and free of control characters (newlines
    /// included). Additive: a manifest without it stays valid.
    // [impl->REQ-ADAPTER-CHANGELOG]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub summary: Option<String>,
    /// Optional ARCHIVE-RELATIVE path to the adapter's markdown changelog, so
    /// the changelog ships inside the artifact it describes. Install and
    /// update render it to HTML once and the node serves the result at
    /// `/<node>/a/<adapter>/changelog`. Absolute paths, parent-directory
    /// escapes and non-`.md` names are refused at load; a declared file that
    /// is missing or unreadable fails the install.
    // [impl->REQ-ADAPTER-CHANGELOG]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub changelog: Option<String>,
}

/// Longest `[adapter].summary`, in characters.
pub const ADAPTER_SUMMARY_MAX_CHARS: usize = 300;

/// The load-time half of the `[adapter].summary` / `[adapter].changelog`
/// contract: shape only. Whether the changelog file exists and reads is an
/// install-time question, answered against the extracted archive.
// [impl->REQ-ADAPTER-CHANGELOG]
fn validate_summary_and_changelog(adapter: &Adapter) -> Result<(), ManifestError> {
    let name = &adapter.name;
    let refuse = |why: String| Err(ManifestError::Validation(why));
    if let Some(summary) = &adapter.summary {
        let n = summary.chars().count();
        if n > ADAPTER_SUMMARY_MAX_CHARS {
            return refuse(format!(
                "MANIFEST_SUMMARY_TOO_LONG:{name}: adapter.summary is {n} characters \
                 (at most {ADAPTER_SUMMARY_MAX_CHARS})"
            ));
        }
        if summary.chars().any(char::is_control) {
            return refuse(format!(
                "MANIFEST_SUMMARY_NOT_PLAIN:{name}: adapter.summary is one plain paragraph \
                 and must not contain control characters or line breaks"
            ));
        }
    }
    if let Some(changelog) = &adapter.changelog {
        let path = std::path::Path::new(changelog);
        if changelog.trim().is_empty() {
            return refuse(format!(
                "MANIFEST_CHANGELOG_PATH:{name}: adapter.changelog must name a markdown file"
            ));
        }
        for component in path.components() {
            match component {
                std::path::Component::Normal(_) | std::path::Component::CurDir => {}
                std::path::Component::ParentDir => {
                    return refuse(format!(
                        "MANIFEST_CHANGELOG_PATH:{name}: adapter.changelog must not contain a \
                         parent-directory component (it is archive-relative)"
                    ));
                }
                std::path::Component::RootDir | std::path::Component::Prefix(_) => {
                    return refuse(format!(
                        "MANIFEST_CHANGELOG_PATH:{name}: adapter.changelog is archive-relative \
                         and must not be absolute"
                    ));
                }
            }
        }
        let is_md = path
            .extension()
            .and_then(|e| e.to_str())
            .is_some_and(|e| e.eq_ignore_ascii_case("md") || e.eq_ignore_ascii_case("markdown"));
        if !is_md {
            return refuse(format!(
                "MANIFEST_CHANGELOG_NOT_MARKDOWN:{name}: adapter.changelog must name a \
                 markdown file (.md), got {changelog:?}"
            ));
        }
    }
    Ok(())
}
'''
s=s.replace(anchor,new)
a2='''        // kind ↔ [shell] body agreement.
        match self.adapter.kind {'''
assert s.count(a2)==1
s=s.replace(a2,'''        // [impl->REQ-ADAPTER-CHANGELOG]
        validate_summary_and_changelog(&self.adapter)?;

'''+a2)
if crlf: s=s.replace('\n','\r\n')
open(p,'wb').write(s.encode())
print(crlf)
