use sha2::{Digest, Sha256};
use super::context_reduce::reduce_source;
use super::map::{build_map, render_card};
use super::skeleton::extract_rust_skeleton;
use super::{ContextMode, Graph, NodeLayer};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProjectedDocument {
pub id: String,
pub path: String,
pub content: String,
pub tokens: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContextEnvelope {
pub mode: ContextMode,
pub graph_revision: String,
pub included: Vec<String>,
pub documents: Vec<ProjectedDocument>,
pub total_tokens: usize,
pub content_hash: String,
}
pub fn build_envelope(
graph: &Graph,
mode: &ContextMode,
included: &[String],
graph_revision: &str,
read_source: impl Fn(&str) -> Option<String>,
) -> ContextEnvelope {
build_envelope_with_root(
graph,
mode,
included,
graph_revision,
std::path::Path::new("."),
read_source,
)
}
pub fn build_envelope_with_root(
graph: &Graph,
mode: &ContextMode,
included: &[String],
graph_revision: &str,
root: &std::path::Path,
read_source: impl Fn(&str) -> Option<String>,
) -> ContextEnvelope {
let map_cards = if matches!(mode, ContextMode::Map) {
Some(build_map(graph, root))
} else {
None
};
let mut documents = Vec::new();
for id in included {
let Some(node) = graph.nodes.iter().find(|n| &n.id == id) else {
continue;
};
if node.layer != NodeLayer::Code && !matches!(mode, ContextMode::FullExtended) {
continue;
}
let Some(rel) = node.path.as_deref() else {
continue;
};
let content = match mode {
ContextMode::Map => map_cards
.as_ref()
.and_then(|m| m.cards.iter().find(|c| &c.component == id))
.map(render_card),
ContextMode::Lite | ContextMode::Custom => read_source(rel).map(|src| {
if rel.ends_with(".rs") {
extract_rust_skeleton(std::path::Path::new(rel), &src).render()
} else {
src
}
}),
ContextMode::Compact => read_source(rel).map(|src| reduce_source(&src)),
_ => read_source(rel),
};
let Some(content) = content else {
continue;
};
documents.push(ProjectedDocument {
id: id.clone(),
path: rel.to_string(),
tokens: crate::token_count::estimate_content_tokens(&content),
content,
});
}
let total_tokens = documents.iter().map(|d| d.tokens).sum();
let mut hasher = Sha256::new();
fn hash_field(hasher: &mut Sha256, bytes: &[u8]) {
hasher.update((bytes.len() as u64).to_le_bytes());
hasher.update(bytes);
}
hash_field(&mut hasher, graph_revision.as_bytes());
hash_field(&mut hasher, mode.name().as_bytes());
for id in included {
hash_field(&mut hasher, id.as_bytes());
}
for doc in &documents {
hash_field(&mut hasher, doc.path.as_bytes());
hash_field(&mut hasher, doc.content.as_bytes());
}
let content_hash = format!("{:x}", hasher.finalize());
ContextEnvelope {
mode: mode.clone(),
graph_revision: graph_revision.to_string(),
included: included.to_vec(),
documents,
total_tokens,
content_hash,
}
}
#[cfg(test)]
#[path = "../../tests/unit/evolve/envelope_test.rs"]
mod envelope_test;