use super::{
invert_messages, invert_one_messages, project_messages, stub, verify_log_messages,
ReductionLog, ReductionPolicy, REDUCTION_SENTINEL,
};
use crate::message::ChatMessage;
use crate::session::{Session, SessionFormat};
use crate::{Error, Result};
pub fn project(
session: &Session,
policy: &ReductionPolicy,
prior: &ReductionLog,
) -> (Vec<ChatMessage>, ReductionLog) {
project_messages(&session.messages, policy, prior)
}
pub fn invert(
reduced: &[ChatMessage],
log: &ReductionLog,
sidecar_session: &Session,
) -> Result<Vec<ChatMessage>> {
Ok(invert_messages(reduced, log, &sidecar_session.messages)?)
}
pub fn verify_log(log: &ReductionLog, sidecar: &Session) -> Result<()> {
verify_log_messages(log, &sidecar.messages)?;
Ok(())
}
pub fn invert_one(
reduced: &[ChatMessage],
log: &ReductionLog,
id: &str,
sidecar_session: &Session,
) -> Result<(Vec<ChatMessage>, ReductionLog)> {
Ok(invert_one_messages(
reduced,
log,
id,
&sidecar_session.messages,
)?)
}
pub fn export_session(sidecar: &str, format: SessionFormat) -> Result<String> {
let session = Session::from_sidecar_str(sidecar)?;
reject_reduced_sidecar(&session, "export_session")?;
Ok(session.to_jsonl(format)?)
}
pub fn export_session_spliced(
sidecar: &str,
format: SessionFormat,
session_id: Option<&str>,
) -> Result<String> {
export_session_spliced_with_overrides(sidecar, format, session_id, None)
}
pub fn export_session_spliced_with_overrides(
sidecar: &str,
format: SessionFormat,
session_id: Option<&str>,
cwd: Option<&std::path::Path>,
) -> Result<String> {
let mut session = Session::from_sidecar_str(sidecar)?;
reject_reduced_sidecar(&session, "export_session_spliced")?;
if let Some(cwd) = cwd {
session.meta.cwd = Some(cwd.to_path_buf());
}
Ok(session.to_jsonl_spliced(format, session_id)?)
}
fn reject_reduced_sidecar(session: &Session, operation: &str) -> Result<()> {
if session_contains_reduction_stub(session) {
return Err(Error::Other(format!(
"{operation}: refusing to export — the sidecar contains a grammar-valid \
reduction stub beginning with {REDUCTION_SENTINEL:?}; a reduced view leaked \
into an export path that must only read the full-fidelity sidecar"
)));
}
Ok(())
}
fn string_contains_reduction_stub(value: &str) -> bool {
value.lines().any(|line| stub::parse(line).is_some())
}
fn json_value_contains_reduction_stub(value: &serde_json::Value) -> bool {
match value {
serde_json::Value::String(value) => string_contains_reduction_stub(value),
serde_json::Value::Array(values) => values.iter().any(json_value_contains_reduction_stub),
serde_json::Value::Object(fields) => {
fields.values().any(json_value_contains_reduction_stub)
}
_ => false,
}
}
fn session_contains_reduction_stub(session: &Session) -> bool {
session.messages.iter().any(|message| {
message
.content
.as_deref()
.is_some_and(string_contains_reduction_stub)
|| message
.content_parts
.as_ref()
.is_some_and(|parts| parts.iter().any(json_value_contains_reduction_stub))
|| message.tool_calls().iter().any(|call| {
serde_json::from_str::<serde_json::Value>(&call.function.arguments)
.map(|value| json_value_contains_reduction_stub(&value))
.unwrap_or_else(|_| string_contains_reduction_stub(&call.function.arguments))
})
})
}