use std::time::SystemTime;
use buffa::Message as _;
use polyc_proto::proto::polychrome::events::v1::{
ConversationSearchDetail, QuerySurface, ReadAuditEvent, ReadAuditOutcome, ReadAuditPhase,
ReadOperationKind, SearchHitFetchDetail, SqlReadDetail, read_audit_event,
};
#[derive(Debug, Clone)]
pub struct ReadAuditContext {
pub caller_identity: Option<String>,
pub conversation_id: Option<String>,
pub turn_id: Option<String>,
pub web_session_id: Option<String>,
pub surface: QuerySurface,
}
#[derive(Debug, Clone, Default)]
pub struct SqlDetail {
pub sql: String,
pub truncated: bool,
pub skipped_partitions: u32,
pub row_count: u64,
}
#[derive(Debug, Clone, Default)]
pub struct SearchDetail {
pub canonical_query: String,
pub scope_hash: String,
pub scope_count: u32,
pub coverage_complete: bool,
pub hit_count: u32,
pub partitions_read: u32,
}
#[derive(Debug, Clone, Default)]
pub struct FetchDetail {
pub origin_read_id: String,
pub handle_hash: String,
pub truncated: bool,
pub byte_count: u64,
}
#[derive(Debug, Clone)]
pub enum ReadDetail {
Sql(SqlDetail),
ConversationSearch(SearchDetail),
SearchHitFetch(FetchDetail),
}
impl ReadDetail {
#[must_use]
pub const fn operation(&self) -> ReadOperationKind {
match *self {
Self::Sql(_) => ReadOperationKind::Sql,
Self::ConversationSearch(_) => ReadOperationKind::ConversationSearch,
Self::SearchHitFetch(_) => ReadOperationKind::SearchHitFetch,
}
}
#[must_use]
fn intent_only(self) -> Self {
match self {
Self::Sql(detail) => Self::Sql(SqlDetail {
sql: detail.sql,
truncated: false,
skipped_partitions: 0,
row_count: 0,
}),
Self::ConversationSearch(detail) => Self::ConversationSearch(SearchDetail {
canonical_query: detail.canonical_query,
scope_hash: detail.scope_hash,
scope_count: detail.scope_count,
coverage_complete: false,
hit_count: 0,
partitions_read: 0,
}),
Self::SearchHitFetch(detail) => Self::SearchHitFetch(FetchDetail {
origin_read_id: detail.origin_read_id,
handle_hash: detail.handle_hash,
truncated: false,
byte_count: 0,
}),
}
}
}
impl From<&SqlDetail> for SqlReadDetail {
fn from(detail: &SqlDetail) -> Self {
Self {
sql: detail.sql.clone(),
truncated: detail.truncated,
skipped_partitions: detail.skipped_partitions,
row_count: detail.row_count,
__buffa_unknown_fields: buffa::UnknownFields::default(),
}
}
}
impl From<&SearchDetail> for ConversationSearchDetail {
fn from(detail: &SearchDetail) -> Self {
Self {
canonical_query: detail.canonical_query.clone(),
scope_hash: detail.scope_hash.clone(),
scope_count: detail.scope_count,
coverage_complete: detail.coverage_complete,
hit_count: detail.hit_count,
partitions_read: detail.partitions_read,
__buffa_unknown_fields: buffa::UnknownFields::default(),
}
}
}
impl From<&FetchDetail> for SearchHitFetchDetail {
fn from(detail: &FetchDetail) -> Self {
Self {
origin_read_id: detail.origin_read_id.clone(),
handle_hash: detail.handle_hash.clone(),
truncated: detail.truncated,
byte_count: detail.byte_count,
__buffa_unknown_fields: buffa::UnknownFields::default(),
}
}
}
impl From<&ReadDetail> for read_audit_event::Detail {
fn from(detail: &ReadDetail) -> Self {
match *detail {
ReadDetail::Sql(ref inner) => Self::Sql(Box::new(inner.into())),
ReadDetail::ConversationSearch(ref inner) => {
Self::ConversationSearch(Box::new(inner.into()))
}
ReadDetail::SearchHitFetch(ref inner) => Self::SearchHitFetch(Box::new(inner.into())),
}
}
}
#[derive(Debug, Clone)]
pub struct ReadAuditRecord {
read_id: String,
context: ReadAuditContext,
detail: ReadDetail,
timestamp: SystemTime,
phase: ReadAuditPhase,
outcome: ReadAuditOutcome,
duration_ms: u64,
}
impl ReadAuditRecord {
#[must_use]
pub fn intent(
read_id: String,
context: ReadAuditContext,
detail: ReadDetail,
timestamp: SystemTime,
) -> Self {
Self {
read_id,
context,
detail: detail.intent_only(),
timestamp,
phase: ReadAuditPhase::Intent,
outcome: ReadAuditOutcome::Unspecified,
duration_ms: 0,
}
}
#[must_use]
pub const fn completion(
read_id: String,
context: ReadAuditContext,
detail: ReadDetail,
timestamp: SystemTime,
outcome: ReadAuditOutcome,
duration_ms: u64,
) -> Self {
Self {
read_id,
context,
detail,
timestamp,
phase: ReadAuditPhase::Completion,
outcome,
duration_ms,
}
}
#[must_use]
pub fn read_id(&self) -> &str {
&self.read_id
}
#[must_use]
pub const fn phase(&self) -> ReadAuditPhase {
self.phase
}
#[must_use]
pub const fn outcome(&self) -> ReadAuditOutcome {
self.outcome
}
#[must_use]
pub const fn detail(&self) -> &ReadDetail {
&self.detail
}
#[must_use]
pub const fn context(&self) -> &ReadAuditContext {
&self.context
}
#[must_use]
pub const fn kind() -> &'static str {
polyc_proto::kinds::READ_AUDIT
}
#[must_use]
#[allow(
clippy::wrong_self_convention,
reason = "encode is non-consuming by design; see the doc comment above"
)]
pub fn into_event_payload(&self) -> Vec<u8> {
ReadAuditEvent {
read_id: self.read_id.clone(),
phase: self.phase.into(),
operation: self.detail.operation().into(),
caller_identity: self.context.caller_identity.clone().unwrap_or_default(),
conversation_id: self.context.conversation_id.clone().unwrap_or_default(),
turn_id: self.context.turn_id.clone().unwrap_or_default(),
web_session_id: self.context.web_session_id.clone().unwrap_or_default(),
surface: self.context.surface.into(),
recorded_at_ms: epoch_millis(self.timestamp),
outcome: self.outcome.into(),
duration_ms: self.duration_ms,
detail: Some((&self.detail).into()),
__buffa_unknown_fields: buffa::UnknownFields::default(),
}
.encode_to_vec()
}
}
fn epoch_millis(timestamp: SystemTime) -> u64 {
timestamp
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |elapsed| {
u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX)
})
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use polyc_proto::events_decode::try_decode_event_payload;
use super::*;
const FIXED: Duration = Duration::from_mins(29_200_000);
fn fleet_context() -> ReadAuditContext {
ReadAuditContext {
caller_identity: None,
conversation_id: None,
turn_id: None,
web_session_id: None,
surface: QuerySurface::Http,
}
}
fn sql_detail() -> ReadDetail {
ReadDetail::Sql(SqlDetail {
sql: "SELECT 1".to_string(),
truncated: false,
skipped_partitions: 0,
row_count: 0,
})
}
fn sample() -> ReadAuditRecord {
ReadAuditRecord::intent(
"rid-1".to_string(),
fleet_context(),
sql_detail(),
SystemTime::UNIX_EPOCH + FIXED,
)
}
fn decode(record: &ReadAuditRecord) -> ReadAuditEvent {
try_decode_event_payload::<ReadAuditEvent>(&record.into_event_payload())
.expect("a freshly encoded ReadAuditEvent must decode")
}
#[test]
fn kind_is_the_registered_read_audit_constant() {
assert_eq!(ReadAuditRecord::kind(), polyc_proto::kinds::READ_AUDIT);
}
#[test]
fn into_event_payload_round_trips_through_the_real_proto_decoder() {
let decoded = decode(&sample());
assert_eq!(decoded.read_id, "rid-1");
assert_eq!(decoded.caller_identity, "");
assert_eq!(decoded.conversation_id, "");
assert_eq!(decoded.turn_id, "");
assert_eq!(decoded.recorded_at_ms, 1_752_000_000_000);
assert_eq!(decoded.surface, buffa::EnumValue::from(QuerySurface::Http));
assert_eq!(
decoded.phase,
buffa::EnumValue::from(ReadAuditPhase::Intent)
);
assert_eq!(
decoded.outcome,
buffa::EnumValue::from(ReadAuditOutcome::Unspecified)
);
assert_eq!(
decoded.operation,
buffa::EnumValue::from(ReadOperationKind::Sql)
);
assert!(matches!(
decoded.detail,
Some(read_audit_event::Detail::Sql(ref sql)) if sql.sql == "SELECT 1"
));
}
#[test]
fn none_identity_fields_encode_to_proto3_empty_string_not_absence() {
let record = ReadAuditRecord::intent(
"rid-2".to_string(),
ReadAuditContext {
caller_identity: Some("persona-42".to_string()),
conversation_id: Some("conv-9".to_string()),
turn_id: Some("turn-3".to_string()),
web_session_id: Some("web-7".to_string()),
surface: QuerySurface::Http,
},
sql_detail(),
SystemTime::UNIX_EPOCH + FIXED,
);
let decoded = decode(&record);
assert_eq!(decoded.caller_identity, "persona-42");
assert_eq!(decoded.conversation_id, "conv-9");
assert_eq!(decoded.turn_id, "turn-3");
assert_eq!(decoded.web_session_id, "web-7");
}
#[test]
fn timestamp_before_epoch_saturates_to_zero_instead_of_panicking() {
let record = ReadAuditRecord::intent(
"rid-3".to_string(),
fleet_context(),
sql_detail(),
SystemTime::UNIX_EPOCH - Duration::from_secs(1),
);
assert_eq!(decode(&record).recorded_at_ms, 0);
}
#[test]
fn completion_carries_its_outcome_and_completeness_fields() {
let record = ReadAuditRecord::completion(
"rid-4".to_string(),
fleet_context(),
ReadDetail::Sql(SqlDetail {
sql: "SELECT 1".to_string(),
truncated: true,
skipped_partitions: 2,
row_count: 9,
}),
SystemTime::UNIX_EPOCH + FIXED,
ReadAuditOutcome::Success,
123,
);
let decoded = decode(&record);
assert_eq!(decoded.read_id, "rid-4");
assert_eq!(
decoded.phase,
buffa::EnumValue::from(ReadAuditPhase::Completion)
);
assert_eq!(
decoded.outcome,
buffa::EnumValue::from(ReadAuditOutcome::Success)
);
assert_eq!(decoded.duration_ms, 123);
let Some(read_audit_event::Detail::Sql(sql)) = decoded.detail else {
panic!("a SQL read must encode a SQL detail");
};
assert!(sql.truncated);
assert_eq!(sql.skipped_partitions, 2);
assert_eq!(sql.row_count, 9);
}
#[test]
fn intent_and_completion_share_the_same_kind() {
let intent = sample();
let completion = ReadAuditRecord::completion(
"rid-1".to_string(),
fleet_context(),
sql_detail(),
SystemTime::UNIX_EPOCH + FIXED,
ReadAuditOutcome::Success,
0,
);
assert_eq!(ReadAuditRecord::kind(), polyc_proto::kinds::READ_AUDIT);
assert_eq!(intent.read_id(), completion.read_id());
assert_eq!(intent.phase(), ReadAuditPhase::Intent);
assert_eq!(completion.phase(), ReadAuditPhase::Completion);
}
#[test]
fn operation_is_derived_from_the_detail_variant() {
for (detail, expected) in [
(sql_detail(), ReadOperationKind::Sql),
(
ReadDetail::ConversationSearch(SearchDetail::default()),
ReadOperationKind::ConversationSearch,
),
(
ReadDetail::SearchHitFetch(FetchDetail::default()),
ReadOperationKind::SearchHitFetch,
),
] {
let record = ReadAuditRecord::intent(
"rid-op".to_string(),
fleet_context(),
detail,
SystemTime::UNIX_EPOCH + FIXED,
);
assert_eq!(
decode(&record).operation,
buffa::EnumValue::from(expected),
"the wire operation must name the populated detail variant"
);
}
}
#[test]
fn intent_zeroes_completion_only_fields_the_caller_left_set() {
let record = ReadAuditRecord::intent(
"rid-5".to_string(),
fleet_context(),
ReadDetail::ConversationSearch(SearchDetail {
canonical_query: "where did we decide the timeout".to_string(),
scope_hash: "ab".repeat(32),
scope_count: 12,
coverage_complete: true,
hit_count: 7,
partitions_read: 3,
}),
SystemTime::UNIX_EPOCH + FIXED,
);
let Some(read_audit_event::Detail::ConversationSearch(search)) = decode(&record).detail
else {
panic!("a conversation search must encode a search detail");
};
assert_eq!(search.canonical_query, "where did we decide the timeout");
assert_eq!(
search.scope_count, 12,
"scope is known before the read runs"
);
assert_eq!(search.hit_count, 0, "an intent has observed no hits");
assert_eq!(search.partitions_read, 0, "an intent has read nothing");
assert!(
!search.coverage_complete,
"an intent has established no coverage"
);
}
#[test]
fn fetch_detail_records_its_origin_search_and_byte_count() {
let record = ReadAuditRecord::completion(
"rid-6".to_string(),
fleet_context(),
ReadDetail::SearchHitFetch(FetchDetail {
origin_read_id: "rid-4".to_string(),
handle_hash: "cd".repeat(32),
truncated: true,
byte_count: 8_000,
}),
SystemTime::UNIX_EPOCH + FIXED,
ReadAuditOutcome::Success,
5,
);
let Some(read_audit_event::Detail::SearchHitFetch(fetch)) = decode(&record).detail else {
panic!("a hit fetch must encode a fetch detail");
};
assert_eq!(fetch.origin_read_id, "rid-4");
assert_eq!(fetch.handle_hash, "cd".repeat(32));
assert!(fetch.truncated);
assert_eq!(fetch.byte_count, 8_000);
}
}