1extern crate alloc;
4use alloc::string::String;
5use alloc::vec::Vec;
6use core::fmt;
7
8use crate::{Header, Id128, SubstrateKind};
9
10#[derive(Clone, Debug)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub struct Event {
14 #[cfg_attr(feature = "serde", serde(flatten))]
15 pub header: Header,
16 pub verb: String,
18 pub substrate: SubstrateKind,
20 pub actor: Option<String>,
22 pub kind: EventKind,
24 pub payload: EventPayload,
26 pub payload_schema_version: u32,
28 pub profile_state_version: Option<u64>,
30 pub aggregate: Option<AggregateRef>,
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
38pub enum EventOutcome {
39 #[default]
41 Success,
42 Denied,
44 Error,
46}
47
48impl EventOutcome {
49 pub const fn name(self) -> &'static str {
51 match self {
52 Self::Success => "success",
53 Self::Denied => "denied",
54 Self::Error => "error",
55 }
56 }
57}
58
59impl fmt::Display for EventOutcome {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 f.write_str(self.name())
62 }
63}
64
65#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
71#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
72pub enum EventSink {
73 CallerEventStore,
75 OperatorAudit,
77}
78
79impl EventSink {
80 pub const fn name(self) -> &'static str {
82 match self {
83 Self::CallerEventStore => "caller_event_store",
84 Self::OperatorAudit => "operator_audit",
85 }
86 }
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
93#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
94#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
95pub enum EventKind {
96 Audit,
98 RecallExecuted,
100 RerankExecuted,
102 SearchExecuted,
104 ToolCheckDecided,
106 LinkCreated,
108 EntityCreated,
110 EntityUpdated,
112 EntityDeleted,
114 EntityMerged,
116 NoteMerged,
118 NoteCreated,
120 NoteUpdated,
122 NoteDeleted,
124 EdgeUpdated,
126 EdgeDeleted,
128 TaskTransitioned,
130 FeedbackExplicit,
132 FeedbackUnjudged,
134 ProfileResolutionRecommended,
136 ProfileMerged,
138 EmbeddingModelChanged,
140 EmbeddingMigrationCompleted,
142 EmbeddingMigrationFailed,
144 EmbeddingDriftDetected,
146 EmbedderInitialized,
148 ProposalCreated,
150 ProposalReviewed,
152 ProposalApplied,
154 ProposalWithdrawn,
156 ChannelPollStarted,
158 ChannelPollSucceeded,
160 ChannelPollFailed,
162 ChannelBackoffArmed,
164 ChannelBackoffReset,
166 ChannelHeartbeatPersistFailed,
168 ConfigLocked,
170 CheckpointOutcomeRecorded,
172 PhaseStarted,
174 PhaseCompleted,
176 PhaseCancelled,
178 Refusal,
180}
181
182impl EventKind {
183 pub const ALL: [Self; 42] = [
185 Self::Audit,
186 Self::RecallExecuted,
187 Self::RerankExecuted,
188 Self::SearchExecuted,
189 Self::ToolCheckDecided,
190 Self::LinkCreated,
191 Self::EntityCreated,
192 Self::EntityUpdated,
193 Self::EntityDeleted,
194 Self::EntityMerged,
195 Self::NoteMerged,
196 Self::NoteCreated,
197 Self::NoteUpdated,
198 Self::NoteDeleted,
199 Self::EdgeUpdated,
200 Self::EdgeDeleted,
201 Self::TaskTransitioned,
202 Self::FeedbackExplicit,
203 Self::FeedbackUnjudged,
204 Self::ProfileResolutionRecommended,
205 Self::ProfileMerged,
206 Self::EmbeddingModelChanged,
207 Self::EmbeddingMigrationCompleted,
208 Self::EmbeddingMigrationFailed,
209 Self::EmbeddingDriftDetected,
210 Self::EmbedderInitialized,
211 Self::ProposalCreated,
212 Self::ProposalReviewed,
213 Self::ProposalApplied,
214 Self::ProposalWithdrawn,
215 Self::ChannelPollStarted,
216 Self::ChannelPollSucceeded,
217 Self::ChannelPollFailed,
218 Self::ChannelBackoffArmed,
219 Self::ChannelBackoffReset,
220 Self::ChannelHeartbeatPersistFailed,
221 Self::ConfigLocked,
222 Self::CheckpointOutcomeRecorded,
223 Self::PhaseStarted,
224 Self::PhaseCompleted,
225 Self::PhaseCancelled,
226 Self::Refusal,
227 ];
228
229 pub const fn sink(self) -> EventSink {
234 match self {
235 Self::Audit => EventSink::CallerEventStore,
236 Self::RecallExecuted => EventSink::CallerEventStore,
237 Self::RerankExecuted => EventSink::CallerEventStore,
238 Self::SearchExecuted => EventSink::CallerEventStore,
239 Self::ToolCheckDecided => EventSink::CallerEventStore,
240 Self::LinkCreated => EventSink::CallerEventStore,
241 Self::EntityCreated => EventSink::CallerEventStore,
242 Self::EntityUpdated => EventSink::CallerEventStore,
243 Self::EntityDeleted => EventSink::CallerEventStore,
244 Self::EntityMerged => EventSink::CallerEventStore,
245 Self::NoteMerged => EventSink::CallerEventStore,
246 Self::NoteCreated => EventSink::CallerEventStore,
247 Self::NoteUpdated => EventSink::CallerEventStore,
248 Self::NoteDeleted => EventSink::CallerEventStore,
249 Self::EdgeUpdated => EventSink::CallerEventStore,
250 Self::EdgeDeleted => EventSink::CallerEventStore,
251 Self::TaskTransitioned => EventSink::CallerEventStore,
252 Self::FeedbackExplicit => EventSink::CallerEventStore,
253 Self::FeedbackUnjudged => EventSink::CallerEventStore,
254 Self::ProfileResolutionRecommended => EventSink::CallerEventStore,
255 Self::ProfileMerged => EventSink::CallerEventStore,
256 Self::EmbeddingModelChanged => EventSink::CallerEventStore,
257 Self::EmbeddingMigrationCompleted => EventSink::CallerEventStore,
258 Self::EmbeddingMigrationFailed => EventSink::CallerEventStore,
259 Self::EmbeddingDriftDetected => EventSink::CallerEventStore,
260 Self::EmbedderInitialized => EventSink::CallerEventStore,
261 Self::ProposalCreated => EventSink::CallerEventStore,
262 Self::ProposalReviewed => EventSink::CallerEventStore,
263 Self::ProposalApplied => EventSink::CallerEventStore,
264 Self::ProposalWithdrawn => EventSink::CallerEventStore,
265 Self::ChannelPollStarted => EventSink::CallerEventStore,
266 Self::ChannelPollSucceeded => EventSink::CallerEventStore,
267 Self::ChannelPollFailed => EventSink::CallerEventStore,
268 Self::ChannelBackoffArmed => EventSink::CallerEventStore,
269 Self::ChannelBackoffReset => EventSink::CallerEventStore,
270 Self::ChannelHeartbeatPersistFailed => EventSink::CallerEventStore,
271 Self::ConfigLocked => EventSink::CallerEventStore,
272 Self::CheckpointOutcomeRecorded => EventSink::CallerEventStore,
273 Self::PhaseStarted => EventSink::CallerEventStore,
274 Self::PhaseCompleted => EventSink::CallerEventStore,
275 Self::PhaseCancelled => EventSink::CallerEventStore,
276 Self::Refusal => EventSink::CallerEventStore,
277 }
278 }
279
280 pub const fn name(self) -> &'static str {
282 match self {
283 Self::Audit => "audit",
284 Self::RecallExecuted => "recall_executed",
285 Self::RerankExecuted => "rerank_executed",
286 Self::SearchExecuted => "search_executed",
287 Self::ToolCheckDecided => "tool_check_decided",
288 Self::LinkCreated => "link_created",
289 Self::EntityCreated => "entity_created",
290 Self::EntityUpdated => "entity_updated",
291 Self::EntityDeleted => "entity_deleted",
292 Self::EntityMerged => "entity_merged",
293 Self::NoteMerged => "note_merged",
294 Self::NoteCreated => "note_created",
295 Self::NoteUpdated => "note_updated",
296 Self::NoteDeleted => "note_deleted",
297 Self::EdgeUpdated => "edge_updated",
298 Self::EdgeDeleted => "edge_deleted",
299 Self::TaskTransitioned => "task_transitioned",
300 Self::FeedbackExplicit => "feedback_explicit",
301 Self::FeedbackUnjudged => "feedback_unjudged",
302 Self::ProfileResolutionRecommended => "profile_resolution_recommended",
303 Self::ProfileMerged => "profile_merged",
304 Self::EmbeddingModelChanged => "embedding_model_changed",
305 Self::EmbeddingMigrationCompleted => "embedding_migration_completed",
306 Self::EmbeddingMigrationFailed => "embedding_migration_failed",
307 Self::EmbeddingDriftDetected => "embedding_drift_detected",
308 Self::EmbedderInitialized => "embedder_initialized",
309 Self::ProposalCreated => "proposal_created",
310 Self::ProposalReviewed => "proposal_reviewed",
311 Self::ProposalApplied => "proposal_applied",
312 Self::ProposalWithdrawn => "proposal_withdrawn",
313 Self::ChannelPollStarted => "channel_poll_started",
314 Self::ChannelPollSucceeded => "channel_poll_succeeded",
315 Self::ChannelPollFailed => "channel_poll_failed",
316 Self::ChannelBackoffArmed => "channel_backoff_armed",
317 Self::ChannelBackoffReset => "channel_backoff_reset",
318 Self::ChannelHeartbeatPersistFailed => "channel_heartbeat_persist_failed",
319 Self::ConfigLocked => "config_locked",
320 Self::CheckpointOutcomeRecorded => "checkpoint_outcome_recorded",
321 Self::PhaseStarted => "phase_started",
322 Self::PhaseCompleted => "phase_completed",
323 Self::PhaseCancelled => "phase_cancelled",
324 Self::Refusal => "refusal",
325 }
326 }
327}
328
329impl fmt::Display for EventKind {
330 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331 f.write_str(self.name())
332 }
333}
334
335const EVENT_KIND_VALID: &[&str] = &[
336 "audit",
337 "recall_executed",
338 "rerank_executed",
339 "search_executed",
340 "tool_check_decided",
341 "link_created",
342 "entity_created",
343 "entity_updated",
344 "entity_deleted",
345 "entity_merged",
346 "note_merged",
347 "note_created",
348 "note_updated",
349 "note_deleted",
350 "edge_updated",
351 "edge_deleted",
352 "task_transitioned",
353 "feedback_explicit",
354 "feedback_unjudged",
355 "profile_resolution_recommended",
356 "profile_merged",
357 "embedding_model_changed",
358 "embedding_migration_completed",
359 "embedding_migration_failed",
360 "embedding_drift_detected",
361 "embedder_initialized",
362 "proposal_created",
363 "proposal_reviewed",
364 "proposal_applied",
365 "proposal_withdrawn",
366 "channel_poll_started",
367 "channel_poll_succeeded",
368 "channel_poll_failed",
369 "channel_backoff_armed",
370 "channel_backoff_reset",
371 "channel_heartbeat_persist_failed",
372 "config_locked",
373 "checkpoint_outcome_recorded",
374 "phase_started",
375 "phase_completed",
376 "phase_cancelled",
377 "refusal",
378];
379
380impl core::str::FromStr for EventKind {
381 type Err = crate::error::UnknownVariant;
382
383 fn from_str(s: &str) -> Result<Self, Self::Err> {
384 match s.trim().to_ascii_lowercase().as_str() {
385 "audit" => Ok(Self::Audit),
386 "recall_executed" => Ok(Self::RecallExecuted),
387 "rerank_executed" => Ok(Self::RerankExecuted),
388 "search_executed" => Ok(Self::SearchExecuted),
389 "tool_check_decided" => Ok(Self::ToolCheckDecided),
390 "link_created" => Ok(Self::LinkCreated),
391 "entity_created" => Ok(Self::EntityCreated),
392 "entity_updated" => Ok(Self::EntityUpdated),
393 "entity_deleted" => Ok(Self::EntityDeleted),
394 "entity_merged" => Ok(Self::EntityMerged),
395 "note_merged" => Ok(Self::NoteMerged),
396 "note_created" => Ok(Self::NoteCreated),
397 "note_updated" => Ok(Self::NoteUpdated),
398 "note_deleted" => Ok(Self::NoteDeleted),
399 "edge_updated" => Ok(Self::EdgeUpdated),
400 "edge_deleted" => Ok(Self::EdgeDeleted),
401 "task_transitioned" => Ok(Self::TaskTransitioned),
402 "feedback_explicit" => Ok(Self::FeedbackExplicit),
403 "feedback_unjudged" => Ok(Self::FeedbackUnjudged),
404 "profile_resolution_recommended" => Ok(Self::ProfileResolutionRecommended),
405 "profile_merged" => Ok(Self::ProfileMerged),
406 "embedding_model_changed" => Ok(Self::EmbeddingModelChanged),
407 "embedding_migration_completed" => Ok(Self::EmbeddingMigrationCompleted),
408 "embedding_migration_failed" => Ok(Self::EmbeddingMigrationFailed),
409 "embedding_drift_detected" => Ok(Self::EmbeddingDriftDetected),
410 "embedder_initialized" => Ok(Self::EmbedderInitialized),
411 "proposal_created" => Ok(Self::ProposalCreated),
412 "proposal_reviewed" => Ok(Self::ProposalReviewed),
413 "proposal_applied" => Ok(Self::ProposalApplied),
414 "proposal_withdrawn" => Ok(Self::ProposalWithdrawn),
415 "channel_poll_started" => Ok(Self::ChannelPollStarted),
416 "channel_poll_succeeded" => Ok(Self::ChannelPollSucceeded),
417 "channel_poll_failed" => Ok(Self::ChannelPollFailed),
418 "channel_backoff_armed" => Ok(Self::ChannelBackoffArmed),
419 "channel_backoff_reset" => Ok(Self::ChannelBackoffReset),
420 "channel_heartbeat_persist_failed" => Ok(Self::ChannelHeartbeatPersistFailed),
421 "config_locked" => Ok(Self::ConfigLocked),
422 "checkpoint_outcome_recorded" => Ok(Self::CheckpointOutcomeRecorded),
423 "phase_started" => Ok(Self::PhaseStarted),
424 "phase_completed" => Ok(Self::PhaseCompleted),
425 "phase_cancelled" => Ok(Self::PhaseCancelled),
426 "refusal" => Ok(Self::Refusal),
427 other => Err(crate::error::UnknownVariant::new(
428 "event_kind",
429 other,
430 EVENT_KIND_VALID,
431 )),
432 }
433 }
434}
435
436#[derive(Clone, Debug, PartialEq, Eq)]
441#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
442pub struct AggregateRef {
443 pub kind: String,
445 pub id: Id128,
447}
448
449#[derive(Clone, Debug, PartialEq)]
455#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
456#[cfg_attr(
457 feature = "serde",
458 serde(tag = "kind", content = "payload", rename_all = "snake_case")
459)]
460pub enum EventPayload {
461 Json(String),
463 RerankExecuted(RerankExecutedPayload),
465 ToolCheckDecided(ToolCheckDecidedPayload),
467 #[cfg(feature = "serde")]
469 ProposalCreated(ProposalCreatedPayload),
470 ProposalReviewed(ProposalReviewedPayload),
472 ProposalApplied(ProposalAppliedPayload),
474 ProposalWithdrawn(ProposalWithdrawnPayload),
476}
477
478impl Default for EventPayload {
479 fn default() -> Self {
480 Self::Json("{}".into())
481 }
482}
483
484#[derive(Clone, Debug, PartialEq, Eq)]
486#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
487pub struct ToolCheckDecidedPayload {
488 pub actor: String,
489 pub tool: String,
490 pub registered: bool,
491 pub decision: String,
492 pub source: String,
493 pub id: Option<String>,
494 pub scope: Option<String>,
495 pub caller_verb: String,
496}
497
498#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
501#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
502#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
503pub enum RerankerKind {
504 #[default]
505 Native,
506 Weighted,
507}
508
509#[derive(Clone, Debug, PartialEq)]
514#[cfg_attr(feature = "serde", derive(serde::Serialize))]
515pub struct RerankExecutedPayload {
516 pub reranker: RerankerKind,
518 pub served_by_profile_id: Option<String>,
520 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
522 pub model_id: Option<Id128>,
523 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
525 pub query_id: Option<String>,
526 pub tiers: Vec<String>,
528 pub ignored_weights: Vec<String>,
530 pub unidentified_candidates: usize,
532 pub candidates: Vec<Id128>,
534 pub reranked: Vec<(Id128, Vec<(String, f32)>)>,
536 pub final_scores: Vec<(Id128, f32)>,
538 pub latency_us: u64,
540 pub hook_applied: bool,
542 pub hook_target_match: bool,
544}
545
546impl RerankExecutedPayload {
547 pub fn is_valid(&self) -> bool {
549 let reranked_ok = self
550 .reranked
551 .iter()
552 .all(|(_, scores)| scores.iter().all(|(_, s)| s.is_finite()));
553 let final_ok = self.final_scores.iter().all(|(_, s)| s.is_finite());
554 reranked_ok && final_ok
555 }
556}
557
558#[cfg(feature = "serde")]
559impl<'de> serde::Deserialize<'de> for RerankExecutedPayload {
560 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
561 where
562 D: serde::Deserializer<'de>,
563 {
564 #[derive(serde::Deserialize)]
565 struct Raw {
566 #[serde(default)]
567 reranker: RerankerKind,
568 served_by_profile_id: Option<String>,
569 model_id: Option<Id128>,
570 query_id: Option<String>,
571 #[serde(default)]
572 tiers: Vec<String>,
573 #[serde(default)]
574 ignored_weights: Vec<String>,
575 #[serde(default)]
576 unidentified_candidates: usize,
577 candidates: Vec<Id128>,
578 reranked: Vec<(Id128, Vec<(String, f32)>)>,
579 final_scores: Vec<(Id128, f32)>,
580 latency_us: u64,
581 hook_applied: bool,
582 hook_target_match: bool,
583 }
584
585 let raw = Raw::deserialize(deserializer)?;
586
587 for (_, score) in &raw.final_scores {
588 if !score.is_finite() {
589 return Err(serde::de::Error::custom(alloc::format!(
590 "RerankExecutedPayload final_scores must be finite, got {score}"
591 )));
592 }
593 }
594 for (_, sections) in &raw.reranked {
595 for (section_name, score) in sections {
596 if !score.is_finite() {
597 return Err(serde::de::Error::custom(alloc::format!(
598 "RerankExecutedPayload reranked section '{section_name}' score must be finite, got {score}"
599 )));
600 }
601 }
602 }
603
604 Ok(RerankExecutedPayload {
605 reranker: raw.reranker,
606 served_by_profile_id: raw.served_by_profile_id,
607 model_id: raw.model_id,
608 query_id: raw.query_id,
609 tiers: raw.tiers,
610 ignored_weights: raw.ignored_weights,
611 unidentified_candidates: raw.unidentified_candidates,
612 candidates: raw.candidates,
613 reranked: raw.reranked,
614 final_scores: raw.final_scores,
615 latency_us: raw.latency_us,
616 hook_applied: raw.hook_applied,
617 hook_target_match: raw.hook_target_match,
618 })
619 }
620}
621
622#[cfg(feature = "serde")]
624#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
625pub struct ProposalCreatedPayload {
626 pub proposal_id: Id128,
627 pub proposer: String,
628 pub title: String,
629 pub description: String,
630 pub changeset: ProposalChangeset,
631 pub reviewers: Vec<String>,
632 pub expiry: Option<crate::Timestamp>,
633 pub parent_id: Option<Id128>,
634}
635
636#[cfg(feature = "serde")]
641#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
642pub struct EntityDraft {
643 pub kind: String,
645 pub name: String,
647 #[serde(skip_serializing_if = "Option::is_none")]
649 pub description: Option<String>,
650 #[serde(skip_serializing_if = "Option::is_none")]
652 pub properties: Option<serde_json::Value>,
653 #[serde(default, skip_serializing_if = "Vec::is_empty")]
655 pub tags: Vec<String>,
656}
657
658#[cfg(feature = "serde")]
664#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
665pub struct ProposalEntityPatch {
666 #[serde(skip_serializing_if = "Option::is_none")]
667 pub name: Option<String>,
668 #[serde(
670 default,
671 skip_serializing_if = "Option::is_none",
672 with = "serde_opt_opt"
673 )]
674 pub description: Option<Option<String>>,
675 #[serde(skip_serializing_if = "Option::is_none")]
676 pub properties: Option<serde_json::Value>,
677 #[serde(skip_serializing_if = "Option::is_none")]
678 pub tags: Option<Vec<String>>,
679 #[serde(
683 default,
684 skip_serializing_if = "Option::is_none",
685 with = "serde_opt_opt"
686 )]
687 pub entity_type: Option<Option<String>>,
688}
689
690#[cfg(feature = "serde")]
694#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
695pub struct NoteDraft {
696 pub kind: String,
698 pub content: String,
700 #[serde(skip_serializing_if = "Option::is_none")]
702 pub name: Option<String>,
703 #[serde(skip_serializing_if = "Option::is_none")]
705 pub properties: Option<serde_json::Value>,
706}
707
708#[cfg(feature = "serde")]
710mod serde_opt_opt {
711 use serde::{Deserialize, Deserializer, Serialize, Serializer};
712
713 pub fn serialize<T, S>(val: &Option<Option<T>>, s: S) -> Result<S::Ok, S::Error>
714 where
715 T: Serialize,
716 S: Serializer,
717 {
718 match val {
719 None => unreachable!("skip_serializing_if guards the None case"),
720 Some(inner) => inner.serialize(s),
721 }
722 }
723
724 pub fn deserialize<'de, T, D>(d: D) -> Result<Option<Option<T>>, D::Error>
725 where
726 T: Deserialize<'de>,
727 D: Deserializer<'de>,
728 {
729 let opt: Option<T> = Option::deserialize(d)?;
730 Ok(Some(opt))
731 }
732}
733
734#[cfg(feature = "serde")]
736#[derive(Clone, Debug, PartialEq, serde::Serialize)]
737#[serde(tag = "kind", rename_all = "snake_case")]
738pub enum ProposalChangeset {
739 AddEntity {
741 entity: EntityDraft,
742 },
743 UpdateEntity {
746 id: Id128,
747 patch: ProposalEntityPatch,
748 },
749 AddEdge {
751 source: Id128,
752 target: Id128,
753 relation: crate::EdgeRelation,
754 weight: Option<f32>,
755 },
756 AddNote {
758 note: NoteDraft,
759 },
760 MergeEntities {
761 into: Id128,
762 from: Id128,
763 },
764 SupersedeEntity {
765 old: Id128,
766 new: Id128,
767 },
768 Canonical {
769 schema_version: u32,
770 delta: String,
771 },
772 Compound {
773 steps: Vec<ProposalChangeset>,
774 },
775}
776
777#[cfg(feature = "serde")]
778impl ProposalChangeset {
779 fn validate(&self) -> Result<(), alloc::string::String> {
780 match self {
781 Self::AddEdge { weight, .. } => {
782 if let Some(w) = weight {
783 if !w.is_finite() {
784 return Err(alloc::format!(
785 "ProposalChangeset AddEdge weight must be finite, got {w}"
786 ));
787 }
788 if !(*w >= 0.0 && *w <= 1.0) {
789 return Err(alloc::format!(
790 "ProposalChangeset AddEdge weight must be in [0.0, 1.0], got {w}"
791 ));
792 }
793 }
794 Ok(())
795 }
796 Self::Compound { steps } => {
797 for step in steps {
798 if matches!(step, Self::Canonical { .. }) {
799 return Err("Canonical proposal changesets must be top-level".into());
800 }
801 step.validate()?;
802 }
803 Ok(())
804 }
805 _ => Ok(()),
806 }
807 }
808}
809
810#[cfg(feature = "serde")]
811impl<'de> serde::Deserialize<'de> for ProposalChangeset {
812 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
813 where
814 D: serde::Deserializer<'de>,
815 {
816 #[derive(serde::Deserialize)]
817 #[serde(deny_unknown_fields)]
818 struct CanonicalFields {
819 schema_version: u32,
820 delta: String,
821 }
822
823 #[derive(serde::Deserialize)]
824 #[serde(tag = "kind", rename_all = "snake_case")]
825 enum ProposalChangesetRaw {
826 AddEntity {
827 entity: EntityDraft,
828 },
829 UpdateEntity {
830 id: Id128,
831 patch: ProposalEntityPatch,
832 },
833 AddEdge {
834 source: Id128,
835 target: Id128,
836 relation: crate::EdgeRelation,
837 weight: Option<f32>,
838 },
839 AddNote {
840 note: NoteDraft,
841 },
842 MergeEntities {
843 into: Id128,
844 from: Id128,
845 },
846 SupersedeEntity {
847 old: Id128,
848 new: Id128,
849 },
850 Canonical(CanonicalFields),
851 Compound {
852 steps: Vec<ProposalChangeset>,
853 },
854 }
855
856 let raw = ProposalChangesetRaw::deserialize(deserializer)?;
857 let cs = match raw {
858 ProposalChangesetRaw::AddEntity { entity } => Self::AddEntity { entity },
859 ProposalChangesetRaw::UpdateEntity { id, patch } => Self::UpdateEntity { id, patch },
860 ProposalChangesetRaw::AddEdge {
861 source,
862 target,
863 relation,
864 weight,
865 } => Self::AddEdge {
866 source,
867 target,
868 relation,
869 weight,
870 },
871 ProposalChangesetRaw::AddNote { note } => Self::AddNote { note },
872 ProposalChangesetRaw::MergeEntities { into, from } => {
873 Self::MergeEntities { into, from }
874 }
875 ProposalChangesetRaw::SupersedeEntity { old, new } => {
876 Self::SupersedeEntity { old, new }
877 }
878 ProposalChangesetRaw::Canonical(CanonicalFields {
879 schema_version,
880 delta,
881 }) => Self::Canonical {
882 schema_version,
883 delta,
884 },
885 ProposalChangesetRaw::Compound { steps } => Self::Compound { steps },
886 };
887 cs.validate().map_err(serde::de::Error::custom)?;
888 Ok(cs)
889 }
890}
891
892#[cfg(not(feature = "serde"))]
893#[derive(Clone, Debug, PartialEq)]
894pub enum ProposalChangeset {
895 AddEdge {
896 source: Id128,
897 target: Id128,
898 relation: crate::EdgeRelation,
899 weight: Option<f32>,
900 },
901 MergeEntities {
902 into: Id128,
903 from: Id128,
904 },
905 SupersedeEntity {
906 old: Id128,
907 new: Id128,
908 },
909 Canonical {
910 schema_version: u32,
911 delta: String,
912 },
913 Compound {
914 steps: Vec<ProposalChangeset>,
915 },
916}
917
918#[derive(Clone, Debug, PartialEq)]
920#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
921pub struct ProposalReviewedPayload {
922 pub proposal_id: Id128,
923 pub reviewer: String,
924 pub decision: ProposalDecision,
925 pub comment: Option<String>,
926}
927
928#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
930#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
931#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
932pub enum ProposalDecision {
933 Approve,
935 Reject,
937 Comment,
939 RequestChanges,
941}
942
943impl ProposalDecision {
944 pub fn as_str(self) -> &'static str {
949 match self {
950 Self::Approve => "approve",
951 Self::Reject => "reject",
952 Self::Comment => "comment",
953 Self::RequestChanges => "request_changes",
954 }
955 }
956}
957
958#[derive(Clone, Debug, PartialEq)]
960#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
961pub struct ProposalAppliedPayload {
962 pub proposal_id: Id128,
963 pub applied_at: crate::Timestamp,
964 pub applied_by: String,
965 pub result: ApplyResult,
966 #[cfg_attr(
967 feature = "serde",
968 serde(default, skip_serializing_if = "Option::is_none")
969 )]
970 pub preimage: Option<ProposalPreimageStatus>,
971}
972
973#[derive(Clone, Copy, Debug, PartialEq, Eq)]
975#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
976#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
977pub enum ProposalPreimageStatus {
978 None,
979 Captured,
980}
981
982#[derive(Clone, Debug, PartialEq)]
984#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
985#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
986pub enum ApplyResult {
987 Success {
988 created_records: Vec<Id128>,
989 },
990 Failed {
991 error: String,
992 applied_step_count: u32,
993 },
994}
995
996#[derive(Clone, Debug, PartialEq)]
998#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
999pub struct ProposalWithdrawnPayload {
1000 pub proposal_id: Id128,
1001 pub by: String,
1002 pub reason: Option<String>,
1003}
1004
1005pub struct EventBuilder {
1007 verb: String,
1008 substrate: SubstrateKind,
1009 actor: Option<String>,
1010 kind: EventKind,
1011 payload: EventPayload,
1012 payload_schema_version: u32,
1013 profile_state_version: Option<u64>,
1014 aggregate: Option<AggregateRef>,
1015}
1016
1017impl EventBuilder {
1018 pub fn new(
1020 verb: impl Into<String>,
1021 substrate: SubstrateKind,
1022 actor: impl Into<String>,
1023 ) -> Self {
1024 Self {
1025 verb: verb.into(),
1026 substrate,
1027 actor: Some(actor.into()),
1028 kind: EventKind::Audit,
1029 payload: EventPayload::default(),
1030 payload_schema_version: 1,
1031 profile_state_version: None,
1032 aggregate: None,
1033 }
1034 }
1035
1036 pub fn kind(mut self, kind: EventKind) -> Self {
1038 self.kind = kind;
1039 self
1040 }
1041
1042 pub fn payload(mut self, payload: EventPayload) -> Self {
1044 self.payload = payload;
1045 self
1046 }
1047
1048 pub fn payload_schema_version(mut self, version: u32) -> Self {
1050 self.payload_schema_version = version;
1051 self
1052 }
1053
1054 pub fn profile_state_version(mut self, version: u64) -> Self {
1056 self.profile_state_version = Some(version);
1057 self
1058 }
1059
1060 pub fn aggregate(mut self, aggregate: AggregateRef) -> Self {
1062 self.aggregate = Some(aggregate);
1063 self
1064 }
1065
1066 pub fn build(self, header: Header) -> Event {
1068 Event {
1069 header,
1070 verb: self.verb,
1071 substrate: self.substrate,
1072 actor: self.actor,
1073 kind: self.kind,
1074 payload: self.payload,
1075 payload_schema_version: self.payload_schema_version,
1076 profile_state_version: self.profile_state_version,
1077 aggregate: self.aggregate,
1078 }
1079 }
1080}
1081
1082#[cfg(test)]
1083#[path = "event_tests.rs"]
1084mod tests;
1085
1086#[cfg(all(test, feature = "serde"))]
1087#[path = "event_canonical_tests.rs"]
1088mod canonical_tests;