a3s_code_core/session_review/
mod.rs1mod error;
28mod finding;
29mod scenario;
30mod store;
31mod subject;
32
33pub use error::SessionReviewError;
34pub use finding::{
35 SessionReviewAnchorV1, SessionReviewFindingV1, SessionReviewSeverityV1, SessionReviewStatusV1,
36 SESSION_REVIEW_FINDING_SCHEMA_V1,
37};
38pub use scenario::{
39 admit_finding_drafts, register_default_scenarios, ReplyTranscriptScenario, ReviewFindingDraft,
40 ReviewScenario, ReviewScenarioRegistry, ReviewTriggerPolicy, SCENARIO_REPLY_TRANSCRIPT,
41};
42pub use store::{SessionReviewStoreV1, SESSION_REVIEW_STORE_SCHEMA_V1};
43pub use subject::ReviewSubjectV1;
44
45pub const SESSION_REVIEW_MAX_MESSAGE_BYTES: usize = 2 * 1024 * 1024;
47pub const SESSION_REVIEW_MAX_ID_BYTES: usize = 256;
48pub const SESSION_REVIEW_MAX_TEXT_BYTES: usize = 16 * 1024;
49pub const SESSION_REVIEW_MAX_FINDINGS: usize = 256;
50
51pub(crate) fn decode_json_slice<T>(bytes: &[u8]) -> Result<T, SessionReviewError>
52where
53 T: serde::de::DeserializeOwned,
54{
55 if bytes.len() > SESSION_REVIEW_MAX_MESSAGE_BYTES {
56 return Err(SessionReviewError::Encoding);
57 }
58 serde_json::from_slice(bytes)
59 .map_err(|error| SessionReviewError::Serialization(error.to_string()))
60}
61
62pub(crate) fn encode_json<T: serde::Serialize + ?Sized>(
63 value: &T,
64) -> Result<Vec<u8>, SessionReviewError> {
65 let bytes = serde_json::to_vec(value)
66 .map_err(|error| SessionReviewError::Serialization(error.to_string()))?;
67 if bytes.len() > SESSION_REVIEW_MAX_MESSAGE_BYTES {
68 return Err(SessionReviewError::Encoding);
69 }
70 Ok(bytes)
71}
72
73pub(crate) fn validate_id(field: &'static str, value: &str) -> Result<(), SessionReviewError> {
74 if value.is_empty()
75 || value.len() > SESSION_REVIEW_MAX_ID_BYTES
76 || value.contains('\0')
77 || value.contains(['\r', '\n'])
78 {
79 return Err(SessionReviewError::InvalidField(field));
80 }
81 Ok(())
82}
83
84pub(crate) fn validate_multiline_text(
86 field: &'static str,
87 value: &str,
88 max_bytes: usize,
89) -> Result<(), SessionReviewError> {
90 if value.is_empty() || value.len() > max_bytes || value.contains('\0') {
91 return Err(SessionReviewError::InvalidField(field));
92 }
93 Ok(())
94}
95
96pub(crate) fn clamp_session_review_text(value: &str, max_bytes: usize) -> String {
98 if value.len() <= max_bytes {
99 return value.to_owned();
100 }
101 let mut end = max_bytes.saturating_sub("…".len());
102 while end > 0 && !value.is_char_boundary(end) {
103 end -= 1;
104 }
105 format!("{}…", &value[..end])
106}