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 Compound {
769 steps: Vec<ProposalChangeset>,
770 },
771}
772
773#[cfg(feature = "serde")]
774impl ProposalChangeset {
775 fn validate(&self) -> Result<(), alloc::string::String> {
776 match self {
777 Self::AddEdge { weight, .. } => {
778 if let Some(w) = weight {
779 if !w.is_finite() {
780 return Err(alloc::format!(
781 "ProposalChangeset AddEdge weight must be finite, got {w}"
782 ));
783 }
784 if !(*w >= 0.0 && *w <= 1.0) {
785 return Err(alloc::format!(
786 "ProposalChangeset AddEdge weight must be in [0.0, 1.0], got {w}"
787 ));
788 }
789 }
790 Ok(())
791 }
792 Self::Compound { steps } => {
793 for step in steps {
794 step.validate()?;
795 }
796 Ok(())
797 }
798 _ => Ok(()),
799 }
800 }
801}
802
803#[cfg(feature = "serde")]
804impl<'de> serde::Deserialize<'de> for ProposalChangeset {
805 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
806 where
807 D: serde::Deserializer<'de>,
808 {
809 #[derive(serde::Deserialize)]
810 #[serde(tag = "kind", rename_all = "snake_case")]
811 enum ProposalChangesetRaw {
812 AddEntity {
813 entity: EntityDraft,
814 },
815 UpdateEntity {
816 id: Id128,
817 patch: ProposalEntityPatch,
818 },
819 AddEdge {
820 source: Id128,
821 target: Id128,
822 relation: crate::EdgeRelation,
823 weight: Option<f32>,
824 },
825 AddNote {
826 note: NoteDraft,
827 },
828 MergeEntities {
829 into: Id128,
830 from: Id128,
831 },
832 SupersedeEntity {
833 old: Id128,
834 new: Id128,
835 },
836 Compound {
837 steps: Vec<ProposalChangeset>,
838 },
839 }
840
841 let raw = ProposalChangesetRaw::deserialize(deserializer)?;
842 let cs = match raw {
843 ProposalChangesetRaw::AddEntity { entity } => Self::AddEntity { entity },
844 ProposalChangesetRaw::UpdateEntity { id, patch } => Self::UpdateEntity { id, patch },
845 ProposalChangesetRaw::AddEdge {
846 source,
847 target,
848 relation,
849 weight,
850 } => Self::AddEdge {
851 source,
852 target,
853 relation,
854 weight,
855 },
856 ProposalChangesetRaw::AddNote { note } => Self::AddNote { note },
857 ProposalChangesetRaw::MergeEntities { into, from } => {
858 Self::MergeEntities { into, from }
859 }
860 ProposalChangesetRaw::SupersedeEntity { old, new } => {
861 Self::SupersedeEntity { old, new }
862 }
863 ProposalChangesetRaw::Compound { steps } => Self::Compound { steps },
864 };
865 cs.validate().map_err(serde::de::Error::custom)?;
866 Ok(cs)
867 }
868}
869
870#[cfg(not(feature = "serde"))]
871#[derive(Clone, Debug, PartialEq)]
872pub enum ProposalChangeset {
873 AddEdge {
874 source: Id128,
875 target: Id128,
876 relation: crate::EdgeRelation,
877 weight: Option<f32>,
878 },
879 MergeEntities {
880 into: Id128,
881 from: Id128,
882 },
883 SupersedeEntity {
884 old: Id128,
885 new: Id128,
886 },
887 Compound {
888 steps: Vec<ProposalChangeset>,
889 },
890}
891
892#[derive(Clone, Debug, PartialEq)]
894#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
895pub struct ProposalReviewedPayload {
896 pub proposal_id: Id128,
897 pub reviewer: String,
898 pub decision: ProposalDecision,
899 pub comment: Option<String>,
900}
901
902#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
904#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
905#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
906pub enum ProposalDecision {
907 Approve,
909 Reject,
911 Comment,
913 RequestChanges,
915}
916
917impl ProposalDecision {
918 pub fn as_str(self) -> &'static str {
923 match self {
924 Self::Approve => "approve",
925 Self::Reject => "reject",
926 Self::Comment => "comment",
927 Self::RequestChanges => "request_changes",
928 }
929 }
930}
931
932#[derive(Clone, Debug, PartialEq)]
934#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
935pub struct ProposalAppliedPayload {
936 pub proposal_id: Id128,
937 pub applied_at: crate::Timestamp,
938 pub applied_by: String,
939 pub result: ApplyResult,
940}
941
942#[derive(Clone, Debug, PartialEq)]
944#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
945#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
946pub enum ApplyResult {
947 Success {
948 created_records: Vec<Id128>,
949 },
950 Failed {
951 error: String,
952 applied_step_count: u32,
953 },
954}
955
956#[derive(Clone, Debug, PartialEq)]
958#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
959pub struct ProposalWithdrawnPayload {
960 pub proposal_id: Id128,
961 pub by: String,
962 pub reason: Option<String>,
963}
964
965pub struct EventBuilder {
967 verb: String,
968 substrate: SubstrateKind,
969 actor: Option<String>,
970 kind: EventKind,
971 payload: EventPayload,
972 payload_schema_version: u32,
973 profile_state_version: Option<u64>,
974 aggregate: Option<AggregateRef>,
975}
976
977impl EventBuilder {
978 pub fn new(
980 verb: impl Into<String>,
981 substrate: SubstrateKind,
982 actor: impl Into<String>,
983 ) -> Self {
984 Self {
985 verb: verb.into(),
986 substrate,
987 actor: Some(actor.into()),
988 kind: EventKind::Audit,
989 payload: EventPayload::default(),
990 payload_schema_version: 1,
991 profile_state_version: None,
992 aggregate: None,
993 }
994 }
995
996 pub fn kind(mut self, kind: EventKind) -> Self {
998 self.kind = kind;
999 self
1000 }
1001
1002 pub fn payload(mut self, payload: EventPayload) -> Self {
1004 self.payload = payload;
1005 self
1006 }
1007
1008 pub fn payload_schema_version(mut self, version: u32) -> Self {
1010 self.payload_schema_version = version;
1011 self
1012 }
1013
1014 pub fn profile_state_version(mut self, version: u64) -> Self {
1016 self.profile_state_version = Some(version);
1017 self
1018 }
1019
1020 pub fn aggregate(mut self, aggregate: AggregateRef) -> Self {
1022 self.aggregate = Some(aggregate);
1023 self
1024 }
1025
1026 pub fn build(self, header: Header) -> Event {
1028 Event {
1029 header,
1030 verb: self.verb,
1031 substrate: self.substrate,
1032 actor: self.actor,
1033 kind: self.kind,
1034 payload: self.payload,
1035 payload_schema_version: self.payload_schema_version,
1036 profile_state_version: self.profile_state_version,
1037 aggregate: self.aggregate,
1038 }
1039 }
1040}
1041
1042#[cfg(test)]
1043#[path = "event_tests.rs"]
1044mod tests;