use std::sync::Arc;
use arrow::array::{ArrayRef, BinaryBuilder, StringBuilder, UInt64Builder};
use arrow::error::ArrowError;
use arrow::record_batch::RecordBatch;
use polyc_eventlog::Event;
use polyc_proto::kinds;
pub(crate) mod approvals;
pub(crate) mod attribution;
pub(crate) mod dashboard;
pub(crate) mod fires;
pub(crate) mod grant_replays;
pub(crate) mod handoffs;
pub(crate) mod message_content;
pub(crate) mod model_call;
pub(crate) mod payments;
pub(crate) mod persona;
pub(crate) mod refusals;
pub(crate) mod routine_lifecycle;
pub(crate) mod routine_setup;
pub(crate) mod routines;
pub(crate) mod summary;
pub(crate) mod turn_dispatch;
pub(crate) mod turn_failed;
pub(crate) mod usage;
pub(crate) mod wallet_link_lifecycle;
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Decode {
Typed(&'static str),
Opaque,
}
#[allow(dead_code)] pub(crate) const REGISTRY: &[(&str, Decode)] = &[
(kinds::TURN_START, Decode::Opaque),
(kinds::TURN_COMPLETE, Decode::Opaque),
(kinds::TURN_TEXT_WITHHELD, Decode::Opaque),
(kinds::GROUNDED_CONTENT, Decode::Opaque),
(kinds::TURN_FAILED, Decode::Typed("turn_failed")),
(kinds::TURN_AMBIGUOUS, Decode::Opaque),
(kinds::TURN_DISPATCHED, Decode::Typed("turn_dispatch")),
(kinds::USER_MSG, Decode::Typed("message_content")),
(kinds::OUTPUT_MSG, Decode::Typed("message_content")),
(kinds::USAGE, Decode::Typed("usage")),
(kinds::STEP_COMMIT, Decode::Opaque),
(kinds::ADMISSION_REFUSED, Decode::Opaque),
(kinds::CONVERSATION_NAMESPACE, Decode::Opaque),
(kinds::NAMESPACE_REFUSED, Decode::Opaque),
(kinds::CONVERSATION_MULTIPARTY_FLOOR, Decode::Opaque),
(kinds::MODEL_CALL, Decode::Typed("model_call")),
(kinds::SUMMARY, Decode::Typed("summary")),
(kinds::SUMMARY_GATE_REJECTED, Decode::Opaque),
(kinds::SUMMARY_GATE_ADMITTED, Decode::Opaque),
(kinds::COMPACTION_CHECKPOINT, Decode::Opaque),
(kinds::APPROVAL_REQUEST, Decode::Typed("approvals")),
(kinds::APPROVAL_RESPONSE, Decode::Typed("approvals")),
(kinds::APPROVAL_DEFERRED, Decode::Opaque),
(kinds::QUESTION_REQUEST, Decode::Opaque),
(kinds::QUESTION_RESPONSE, Decode::Opaque),
(kinds::TOOL_INPUT_REWRITE, Decode::Opaque),
(kinds::TOOL_CONTEXT_INJECTION, Decode::Opaque),
(kinds::TOOL_RESULT_REDACTION, Decode::Opaque),
(kinds::PAYMENT_RECEIPT, Decode::Typed("payments")),
(kinds::CALLER, Decode::Typed("attribution")),
(kinds::PARTICIPANT, Decode::Typed("attribution")),
(kinds::OBSERVED_IDENTITY, Decode::Opaque),
(kinds::OUTBOUND_PAYMENT_RECEIPT, Decode::Typed("payments")),
(kinds::OUTBOUND_PAYMENT_ATTEMPT, Decode::Opaque),
(kinds::PAYMENT_REFUSAL, Decode::Typed("refusals")),
(
kinds::WALLET_LINK_LIFECYCLE,
Decode::Typed("wallet_link_lifecycle"),
),
(kinds::TAINT_EXCISION, Decode::Opaque),
(kinds::ENROLLMENT, Decode::Opaque),
(kinds::ROUTINE_GRANT, Decode::Opaque),
(kinds::ROUTINE_FIRED, Decode::Typed("fires")),
(kinds::ROUTINE_FIRE_OUTCOME, Decode::Typed("fires")),
(kinds::ROUTINE_CREATED, Decode::Typed("routine_lifecycle")),
(kinds::ROUTINE_PAUSED, Decode::Typed("routine_lifecycle")),
(kinds::ROUTINE_RESUMED, Decode::Typed("routine_lifecycle")),
(kinds::ROUTINE_DELETED, Decode::Typed("routine_lifecycle")),
(
kinds::ROUTINE_SCOPE_CHANGED,
Decode::Typed("routine_lifecycle"),
),
(
kinds::ROUTINE_SETUP_COMPLETED,
Decode::Typed("routine_setup"),
),
(kinds::GRANT_REVOCATION, Decode::Opaque),
(kinds::GRANT_SUSPENSION, Decode::Opaque),
(kinds::GRANT_REPLAY, Decode::Typed("grant_replays")),
(kinds::NUDGE_SENT, Decode::Opaque),
(kinds::MEMORY_ADDED, Decode::Opaque),
(kinds::MEMORY_INVALIDATED, Decode::Opaque),
(kinds::MEMORY_CORROBORATED, Decode::Opaque),
(kinds::PROFILE_DOC_UPDATED, Decode::Opaque),
(kinds::CONVERSATION_SUMMARY, Decode::Opaque),
(kinds::INCOGNITO_SET, Decode::Opaque),
(kinds::MEMORY_EXTRACTED, Decode::Opaque),
(kinds::ERASURE_RECORDED, Decode::Opaque),
(kinds::CONVERSATION_ERASURE_INTENT, Decode::Opaque),
(kinds::QUERY_AUDIT, Decode::Opaque),
(kinds::READ_AUDIT, Decode::Opaque),
(kinds::ADMIN_MODEL_CHANGE, Decode::Opaque),
(kinds::CREDENTIAL_ENROLLED, Decode::Opaque),
(kinds::CREDENTIAL_KEY_ADDED, Decode::Opaque),
(kinds::CREDENTIAL_KEY_RETIRED, Decode::Opaque),
(kinds::CREDENTIAL_REVOKED, Decode::Opaque),
(kinds::HANDOFF, Decode::Typed("handoffs")),
(kinds::HANDOFF_DENIED, Decode::Typed("handoffs")),
(kinds::SUBAGENT_SPAWN, Decode::Opaque),
(kinds::SUBAGENT_RESULT, Decode::Opaque),
(kinds::SUBAGENT_MODEL_CALL, Decode::Opaque),
(kinds::INGRESS_DIRECTIVE, Decode::Opaque),
];
#[must_use]
#[allow(dead_code)] pub(crate) fn decode_decision(kind_base: &str) -> Option<Decode> {
REGISTRY
.iter()
.find(|(base, _)| *base == kind_base)
.map(|(_, decision)| *decision)
}
pub(crate) fn events_batch(
partition: &str,
events: &[(u64, Event)],
) -> Result<RecordBatch, ArrowError> {
let mut partition_b = StringBuilder::with_capacity(events.len(), events.len() * 8);
let mut position_b = UInt64Builder::with_capacity(events.len());
let mut kind_b = StringBuilder::with_capacity(events.len(), events.len() * 16);
let mut kind_base_b = StringBuilder::with_capacity(events.len(), events.len() * 16);
let mut turn_id_b = StringBuilder::with_capacity(events.len(), events.len() * 36);
let mut trust_b = StringBuilder::with_capacity(events.len(), events.len() * 20);
let mut payload_b = BinaryBuilder::with_capacity(events.len(), events.len() * 32);
let mut payload_json_b = StringBuilder::with_capacity(events.len(), events.len() * 32);
for (position, event) in events {
let (base, turn_id) = kinds::parse(&event.kind);
partition_b.append_value(partition);
position_b.append_value(*position);
kind_b.append_value(&event.kind);
kind_base_b.append_value(base);
match turn_id {
Some(id) => turn_id_b.append_value(id.to_string()),
None => turn_id_b.append_null(),
}
trust_b.append_value(event.trust.as_str());
payload_b.append_value(&event.payload);
match decode_payload_json(&event.payload) {
Some(text) => payload_json_b.append_value(text),
None => payload_json_b.append_null(),
}
}
let columns: Vec<ArrayRef> = vec![
Arc::new(partition_b.finish()),
Arc::new(position_b.finish()),
Arc::new(kind_b.finish()),
Arc::new(kind_base_b.finish()),
Arc::new(turn_id_b.finish()),
Arc::new(trust_b.finish()),
Arc::new(payload_b.finish()),
Arc::new(payload_json_b.finish()),
];
RecordBatch::try_new(crate::provider::EventsTableProvider::schema(), columns)
}
fn decode_payload_json(payload: &[u8]) -> Option<String> {
let text = std::str::from_utf8(payload).ok()?;
serde_json::from_str::<serde_json::Value>(text).ok()?;
Some(text.to_string())
}
fn decode_typed_kind_events<T, R, E>(
partition: &str,
events: &[(u64, Event)],
kind_bases: &[&str],
table_name: &'static str,
decode: impl Fn(&[u8]) -> Result<T, E>,
build: impl Fn(String, u64, Option<String>, T) -> R,
) -> Vec<R>
where
E: std::fmt::Display,
{
events
.iter()
.filter_map(|(position, event)| {
let (base, turn_id) = kinds::parse(&event.kind);
if !kind_bases.contains(&base) {
return None;
}
match decode(&event.payload) {
Ok(decoded) => Some(build(
partition.to_string(),
*position,
turn_id.map(|id| id.to_string()),
decoded,
)),
Err(e) => {
if !event.payload.is_empty() {
tracing::warn!(
error = %e,
len = event.payload.len(),
table = table_name,
"corrupt event payload; skipping row"
);
}
None
}
}
})
.collect()
}
#[cfg(test)]
mod tests {
use arrow::array::Array as _;
use polyc_eventlog::TrustTag;
use uuid::Uuid;
use super::*;
fn kind_constants_from_source() -> Vec<&'static str> {
const KINDS_SRC: &str = include_str!("../../../proto/src/kinds.rs");
extract_pub_const_str_values(KINDS_SRC)
}
fn extract_pub_const_str_values(source: &'static str) -> Vec<&'static str> {
let mut out = Vec::new();
let mut cursor = source;
while let Some(start) = cursor.find("pub const ") {
let after_keyword = &cursor[start + "pub const ".len()..];
let Some(statement_end) = after_keyword.find(';') else {
break;
};
let statement = &after_keyword[..statement_end];
cursor = &after_keyword[statement_end + 1..];
if let Some(value) = str_value_after_colon(statement) {
out.push(value);
}
}
out
}
fn str_value_after_colon(statement: &'static str) -> Option<&'static str> {
let (_name, rest) = statement.split_once(':')?;
let rest = rest.trim_start().strip_prefix("&str")?.trim_start();
let rest = rest.strip_prefix('=')?.trim_start();
let rest = rest.strip_prefix('"')?;
let (value, _) = rest.split_once('"')?;
Some(value)
}
#[test]
fn registry_is_exhaustive_over_kinds_rs() {
let mut from_source = kind_constants_from_source();
from_source.sort_unstable();
from_source.dedup();
let mut from_registry: Vec<&str> = REGISTRY.iter().map(|(base, _)| *base).collect();
from_registry.sort_unstable();
assert_eq!(
from_registry.len(),
REGISTRY.len(),
"REGISTRY has a duplicate kind-base entry"
);
assert_eq!(
from_source, from_registry,
"kinds.rs and decode::REGISTRY disagree — a kind constant landed \
without a typed/opaque decode decision, or REGISTRY names a kind \
kinds.rs no longer declares"
);
}
#[test]
fn extract_pub_const_str_values_tolerates_a_rustfmt_wrapped_declaration() {
const FIXTURE: &str = concat!(
"/// A constant whose name alone is long enough that rustfmt wraps\n",
"/// the declaration across two physical lines.\n",
"pub const A_DELIBERATELY_VERY_LONG_KIND_CONSTANT_NAME_TO_FORCE_WRAPPING:\n",
" &str = \"a_deliberately_very_long_kind_constant_name_to_force_wrapping\";\n",
"\n",
"/// A normal, single-line declaration, to prove both shapes coexist.\n",
"pub const SHORT: &str = \"short\";\n",
);
let values = extract_pub_const_str_values(FIXTURE);
assert_eq!(
values,
vec![
"a_deliberately_very_long_kind_constant_name_to_force_wrapping",
"short",
],
"both the wrapped and single-line declarations must be extracted"
);
}
#[test]
#[allow(clippy::too_many_lines)] fn usage_model_call_attribution_turn_failed_payments_message_content_approvals_handoffs_grant_replays_summary_fires_turn_dispatch_routine_lifecycle_and_routine_setup_kinds_are_the_only_typed_kinds()
{
let typed: Vec<&str> = REGISTRY
.iter()
.filter_map(|(base, decision)| match decision {
Decode::Typed(_) => Some(*base),
Decode::Opaque => None,
})
.collect();
assert_eq!(
typed,
vec![
kinds::TURN_FAILED,
kinds::TURN_DISPATCHED,
kinds::USER_MSG,
kinds::OUTPUT_MSG,
kinds::USAGE,
kinds::MODEL_CALL,
kinds::SUMMARY,
kinds::APPROVAL_REQUEST,
kinds::APPROVAL_RESPONSE,
kinds::PAYMENT_RECEIPT,
kinds::CALLER,
kinds::PARTICIPANT,
kinds::OUTBOUND_PAYMENT_RECEIPT,
kinds::PAYMENT_REFUSAL,
kinds::WALLET_LINK_LIFECYCLE,
kinds::ROUTINE_FIRED,
kinds::ROUTINE_FIRE_OUTCOME,
kinds::ROUTINE_CREATED,
kinds::ROUTINE_PAUSED,
kinds::ROUTINE_RESUMED,
kinds::ROUTINE_DELETED,
kinds::ROUTINE_SCOPE_CHANGED,
kinds::ROUTINE_SETUP_COMPLETED,
kinds::GRANT_REPLAY,
kinds::HANDOFF,
kinds::HANDOFF_DENIED,
]
);
assert_eq!(
decode_decision(kinds::TURN_FAILED),
Some(Decode::Typed("turn_failed"))
);
assert_eq!(
decode_decision(kinds::USER_MSG),
Some(Decode::Typed("message_content"))
);
assert_eq!(
decode_decision(kinds::OUTPUT_MSG),
Some(Decode::Typed("message_content"))
);
assert_eq!(
decode_decision(kinds::TURN_DISPATCHED),
Some(Decode::Typed("turn_dispatch"))
);
assert_eq!(decode_decision(kinds::USAGE), Some(Decode::Typed("usage")));
assert_eq!(
decode_decision(kinds::MODEL_CALL),
Some(Decode::Typed("model_call"))
);
assert_eq!(
decode_decision(kinds::SUMMARY),
Some(Decode::Typed("summary"))
);
assert_eq!(
decode_decision(kinds::APPROVAL_REQUEST),
Some(Decode::Typed("approvals"))
);
assert_eq!(
decode_decision(kinds::APPROVAL_RESPONSE),
Some(Decode::Typed("approvals"))
);
assert_eq!(
decode_decision(kinds::CALLER),
Some(Decode::Typed("attribution"))
);
assert_eq!(
decode_decision(kinds::PARTICIPANT),
Some(Decode::Typed("attribution"))
);
assert_eq!(
decode_decision(kinds::PAYMENT_RECEIPT),
Some(Decode::Typed("payments"))
);
assert_eq!(
decode_decision(kinds::OUTBOUND_PAYMENT_RECEIPT),
Some(Decode::Typed("payments"))
);
assert_eq!(
decode_decision(kinds::PAYMENT_REFUSAL),
Some(Decode::Typed("refusals"))
);
assert_eq!(
decode_decision(kinds::WALLET_LINK_LIFECYCLE),
Some(Decode::Typed("wallet_link_lifecycle"))
);
assert_eq!(
decode_decision(kinds::ROUTINE_FIRED),
Some(Decode::Typed("fires"))
);
assert_eq!(
decode_decision(kinds::ROUTINE_FIRE_OUTCOME),
Some(Decode::Typed("fires"))
);
assert_eq!(
decode_decision(kinds::ROUTINE_CREATED),
Some(Decode::Typed("routine_lifecycle"))
);
assert_eq!(
decode_decision(kinds::ROUTINE_PAUSED),
Some(Decode::Typed("routine_lifecycle"))
);
assert_eq!(
decode_decision(kinds::ROUTINE_RESUMED),
Some(Decode::Typed("routine_lifecycle"))
);
assert_eq!(
decode_decision(kinds::ROUTINE_DELETED),
Some(Decode::Typed("routine_lifecycle"))
);
assert_eq!(
decode_decision(kinds::ROUTINE_SCOPE_CHANGED),
Some(Decode::Typed("routine_lifecycle"))
);
assert_eq!(
decode_decision(kinds::ROUTINE_SETUP_COMPLETED),
Some(Decode::Typed("routine_setup"))
);
assert_eq!(
decode_decision(kinds::GRANT_REPLAY),
Some(Decode::Typed("grant_replays"))
);
assert_eq!(
decode_decision(kinds::HANDOFF),
Some(Decode::Typed("handoffs"))
);
assert_eq!(
decode_decision(kinds::HANDOFF_DENIED),
Some(Decode::Typed("handoffs"))
);
}
#[test]
fn unknown_kind_base_has_no_decision() {
assert_eq!(decode_decision("totally_invented_kind"), None);
}
fn sample_events() -> Vec<(u64, Event)> {
let turn = Uuid::from_u128(0x0195_abcd_ef01_2345_6789_abcd_ef01_2345);
vec![
(10, Event::new(kinds::TURN_START, Vec::new())),
(
11,
Event::trusted(kinds::tagged(kinds::USER_MSG, &turn), b"hello".to_vec()),
),
(
12,
Event::quarantined(kinds::tagged(kinds::USAGE, &turn), b"usage-bytes".to_vec()),
),
(
13,
Event::new(
kinds::APPROVAL_DEFERRED,
br#"{"reason":"awaiting reviewer"}"#.to_vec(),
),
),
]
}
#[test]
fn events_batch_round_trips_every_column() {
let events = sample_events();
let batch = events_batch("conv-42", &events).expect("batch build");
assert_eq!(batch.num_rows(), 4);
assert_eq!(
batch.schema(),
crate::provider::EventsTableProvider::schema()
);
let partition = batch
.column(0)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
.unwrap();
for i in 0..4 {
assert_eq!(partition.value(i), "conv-42");
}
let position = batch
.column(1)
.as_any()
.downcast_ref::<arrow::array::UInt64Array>()
.unwrap();
assert_eq!(position.values(), &[10, 11, 12, 13]);
let kind = batch
.column(2)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
.unwrap();
assert_eq!(kind.value(0), kinds::TURN_START);
assert_eq!(
kind.value(1),
kinds::tagged(
kinds::USER_MSG,
&Uuid::from_u128(0x0195_abcd_ef01_2345_6789_abcd_ef01_2345)
)
);
let kind_base = batch
.column(3)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
.unwrap();
assert_eq!(kind_base.value(0), kinds::TURN_START);
assert_eq!(kind_base.value(1), kinds::USER_MSG);
assert_eq!(kind_base.value(2), kinds::USAGE);
assert_eq!(kind_base.value(3), kinds::APPROVAL_DEFERRED);
let turn_id = batch
.column(4)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
.unwrap();
assert!(turn_id.is_null(0));
let expected_turn = Uuid::from_u128(0x0195_abcd_ef01_2345_6789_abcd_ef01_2345).to_string();
assert_eq!(turn_id.value(1), expected_turn);
assert_eq!(turn_id.value(2), expected_turn);
assert!(
turn_id.is_null(3),
"the bare approval_deferred kind tags no turn"
);
let trust = batch
.column(5)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
.unwrap();
assert_eq!(trust.value(0), TrustTag::Unspecified.as_str());
assert_eq!(trust.value(1), TrustTag::TrustedUser.as_str());
assert_eq!(trust.value(2), TrustTag::QuarantinedContent.as_str());
assert_eq!(trust.value(3), TrustTag::Unspecified.as_str());
let payload = batch
.column(6)
.as_any()
.downcast_ref::<arrow::array::BinaryArray>()
.unwrap();
assert_eq!(payload.value(0), b"");
assert_eq!(payload.value(1), b"hello");
assert_eq!(payload.value(2), b"usage-bytes");
assert_eq!(payload.value(3), br#"{"reason":"awaiting reviewer"}"#);
let payload_json = batch
.column(7)
.as_any()
.downcast_ref::<arrow::array::StringArray>()
.unwrap();
assert!(
payload_json.is_null(0),
"an empty payload is not valid JSON"
);
assert!(
payload_json.is_null(1),
"plain text (\"hello\") is not valid JSON"
);
assert!(
payload_json.is_null(2),
"opaque non-JSON bytes decode to NULL, not a decode error"
);
assert_eq!(
payload_json.value(3),
r#"{"reason":"awaiting reviewer"}"#,
"a schema-less kind's JSON payload round-trips as text verbatim"
);
}
#[test]
fn events_batch_empty_input_has_zero_rows_and_the_full_schema() {
let batch = events_batch("conv-empty", &[]).expect("batch build");
assert_eq!(batch.num_rows(), 0);
assert_eq!(
batch.schema(),
crate::provider::EventsTableProvider::schema()
);
}
#[test]
fn decode_payload_json_accepts_a_json_object() {
assert_eq!(
decode_payload_json(br#"{"a":1,"b":"two"}"#),
Some(r#"{"a":1,"b":"two"}"#.to_string())
);
}
#[test]
fn decode_payload_json_accepts_a_bare_json_scalar() {
assert_eq!(
decode_payload_json(br#""just a string""#),
Some(r#""just a string""#.to_string())
);
assert_eq!(decode_payload_json(b"42"), Some("42".to_string()));
}
#[test]
fn decode_payload_json_rejects_empty_payload() {
assert_eq!(decode_payload_json(b""), None);
}
#[test]
fn decode_payload_json_rejects_non_json_utf8_text() {
assert_eq!(decode_payload_json(b"hello, not json"), None);
}
#[test]
fn decode_payload_json_rejects_non_utf8_bytes() {
assert_eq!(decode_payload_json(&[0xFF, 0xFE, 0x00, 0x01]), None);
}
}