mod error;
mod finding;
mod scenario;
mod store;
mod subject;
pub use error::SessionReviewError;
pub use finding::{
SessionReviewAnchorV1, SessionReviewFindingV1, SessionReviewSeverityV1, SessionReviewStatusV1,
SESSION_REVIEW_FINDING_SCHEMA_V1,
};
pub use scenario::{
admit_finding_drafts, register_default_scenarios, ReplyTranscriptScenario, ReviewFindingDraft,
ReviewScenario, ReviewScenarioRegistry, ReviewTriggerPolicy, SCENARIO_REPLY_TRANSCRIPT,
};
pub use store::{SessionReviewStoreV1, SESSION_REVIEW_STORE_SCHEMA_V1};
pub use subject::ReviewSubjectV1;
pub const SESSION_REVIEW_MAX_MESSAGE_BYTES: usize = 2 * 1024 * 1024;
pub const SESSION_REVIEW_MAX_ID_BYTES: usize = 256;
pub const SESSION_REVIEW_MAX_TEXT_BYTES: usize = 16 * 1024;
pub const SESSION_REVIEW_MAX_FINDINGS: usize = 256;
pub(crate) fn decode_json_slice<T>(bytes: &[u8]) -> Result<T, SessionReviewError>
where
T: serde::de::DeserializeOwned,
{
if bytes.len() > SESSION_REVIEW_MAX_MESSAGE_BYTES {
return Err(SessionReviewError::Encoding);
}
serde_json::from_slice(bytes)
.map_err(|error| SessionReviewError::Serialization(error.to_string()))
}
pub(crate) fn encode_json<T: serde::Serialize + ?Sized>(
value: &T,
) -> Result<Vec<u8>, SessionReviewError> {
let bytes = serde_json::to_vec(value)
.map_err(|error| SessionReviewError::Serialization(error.to_string()))?;
if bytes.len() > SESSION_REVIEW_MAX_MESSAGE_BYTES {
return Err(SessionReviewError::Encoding);
}
Ok(bytes)
}
pub(crate) fn validate_id(field: &'static str, value: &str) -> Result<(), SessionReviewError> {
if value.is_empty()
|| value.len() > SESSION_REVIEW_MAX_ID_BYTES
|| value.contains('\0')
|| value.contains(['\r', '\n'])
{
return Err(SessionReviewError::InvalidField(field));
}
Ok(())
}
pub(crate) fn validate_multiline_text(
field: &'static str,
value: &str,
max_bytes: usize,
) -> Result<(), SessionReviewError> {
if value.is_empty() || value.len() > max_bytes || value.contains('\0') {
return Err(SessionReviewError::InvalidField(field));
}
Ok(())
}
pub(crate) fn clamp_session_review_text(value: &str, max_bytes: usize) -> String {
if value.len() <= max_bytes {
return value.to_owned();
}
let mut end = max_bytes.saturating_sub("…".len());
while end > 0 && !value.is_char_boundary(end) {
end -= 1;
}
format!("{}…", &value[..end])
}