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 GrantChange,
182}
183
184impl EventKind {
185 pub const ALL: [Self; 43] = [
187 Self::Audit,
188 Self::RecallExecuted,
189 Self::RerankExecuted,
190 Self::SearchExecuted,
191 Self::ToolCheckDecided,
192 Self::LinkCreated,
193 Self::EntityCreated,
194 Self::EntityUpdated,
195 Self::EntityDeleted,
196 Self::EntityMerged,
197 Self::NoteMerged,
198 Self::NoteCreated,
199 Self::NoteUpdated,
200 Self::NoteDeleted,
201 Self::EdgeUpdated,
202 Self::EdgeDeleted,
203 Self::TaskTransitioned,
204 Self::FeedbackExplicit,
205 Self::FeedbackUnjudged,
206 Self::ProfileResolutionRecommended,
207 Self::ProfileMerged,
208 Self::EmbeddingModelChanged,
209 Self::EmbeddingMigrationCompleted,
210 Self::EmbeddingMigrationFailed,
211 Self::EmbeddingDriftDetected,
212 Self::EmbedderInitialized,
213 Self::ProposalCreated,
214 Self::ProposalReviewed,
215 Self::ProposalApplied,
216 Self::ProposalWithdrawn,
217 Self::ChannelPollStarted,
218 Self::ChannelPollSucceeded,
219 Self::ChannelPollFailed,
220 Self::ChannelBackoffArmed,
221 Self::ChannelBackoffReset,
222 Self::ChannelHeartbeatPersistFailed,
223 Self::ConfigLocked,
224 Self::CheckpointOutcomeRecorded,
225 Self::PhaseStarted,
226 Self::PhaseCompleted,
227 Self::PhaseCancelled,
228 Self::Refusal,
229 Self::GrantChange,
230 ];
231
232 pub const fn sink(self) -> EventSink {
237 match self {
238 Self::Audit => EventSink::CallerEventStore,
239 Self::RecallExecuted => EventSink::CallerEventStore,
240 Self::RerankExecuted => EventSink::CallerEventStore,
241 Self::SearchExecuted => EventSink::CallerEventStore,
242 Self::ToolCheckDecided => EventSink::CallerEventStore,
243 Self::LinkCreated => EventSink::CallerEventStore,
244 Self::EntityCreated => EventSink::CallerEventStore,
245 Self::EntityUpdated => EventSink::CallerEventStore,
246 Self::EntityDeleted => EventSink::CallerEventStore,
247 Self::EntityMerged => EventSink::CallerEventStore,
248 Self::NoteMerged => EventSink::CallerEventStore,
249 Self::NoteCreated => EventSink::CallerEventStore,
250 Self::NoteUpdated => EventSink::CallerEventStore,
251 Self::NoteDeleted => EventSink::CallerEventStore,
252 Self::EdgeUpdated => EventSink::CallerEventStore,
253 Self::EdgeDeleted => EventSink::CallerEventStore,
254 Self::TaskTransitioned => EventSink::CallerEventStore,
255 Self::FeedbackExplicit => EventSink::CallerEventStore,
256 Self::FeedbackUnjudged => EventSink::CallerEventStore,
257 Self::ProfileResolutionRecommended => EventSink::CallerEventStore,
258 Self::ProfileMerged => EventSink::CallerEventStore,
259 Self::EmbeddingModelChanged => EventSink::CallerEventStore,
260 Self::EmbeddingMigrationCompleted => EventSink::CallerEventStore,
261 Self::EmbeddingMigrationFailed => EventSink::CallerEventStore,
262 Self::EmbeddingDriftDetected => EventSink::CallerEventStore,
263 Self::EmbedderInitialized => EventSink::CallerEventStore,
264 Self::ProposalCreated => EventSink::CallerEventStore,
265 Self::ProposalReviewed => EventSink::CallerEventStore,
266 Self::ProposalApplied => EventSink::CallerEventStore,
267 Self::ProposalWithdrawn => EventSink::CallerEventStore,
268 Self::ChannelPollStarted => EventSink::CallerEventStore,
269 Self::ChannelPollSucceeded => EventSink::CallerEventStore,
270 Self::ChannelPollFailed => EventSink::CallerEventStore,
271 Self::ChannelBackoffArmed => EventSink::CallerEventStore,
272 Self::ChannelBackoffReset => EventSink::CallerEventStore,
273 Self::ChannelHeartbeatPersistFailed => EventSink::CallerEventStore,
274 Self::ConfigLocked => EventSink::CallerEventStore,
275 Self::CheckpointOutcomeRecorded => EventSink::CallerEventStore,
276 Self::PhaseStarted => EventSink::CallerEventStore,
277 Self::PhaseCompleted => EventSink::CallerEventStore,
278 Self::PhaseCancelled => EventSink::CallerEventStore,
279 Self::Refusal => EventSink::CallerEventStore,
280 Self::GrantChange => EventSink::CallerEventStore,
281 }
282 }
283
284 pub const fn name(self) -> &'static str {
286 match self {
287 Self::Audit => "audit",
288 Self::RecallExecuted => "recall_executed",
289 Self::RerankExecuted => "rerank_executed",
290 Self::SearchExecuted => "search_executed",
291 Self::ToolCheckDecided => "tool_check_decided",
292 Self::LinkCreated => "link_created",
293 Self::EntityCreated => "entity_created",
294 Self::EntityUpdated => "entity_updated",
295 Self::EntityDeleted => "entity_deleted",
296 Self::EntityMerged => "entity_merged",
297 Self::NoteMerged => "note_merged",
298 Self::NoteCreated => "note_created",
299 Self::NoteUpdated => "note_updated",
300 Self::NoteDeleted => "note_deleted",
301 Self::EdgeUpdated => "edge_updated",
302 Self::EdgeDeleted => "edge_deleted",
303 Self::TaskTransitioned => "task_transitioned",
304 Self::FeedbackExplicit => "feedback_explicit",
305 Self::FeedbackUnjudged => "feedback_unjudged",
306 Self::ProfileResolutionRecommended => "profile_resolution_recommended",
307 Self::ProfileMerged => "profile_merged",
308 Self::EmbeddingModelChanged => "embedding_model_changed",
309 Self::EmbeddingMigrationCompleted => "embedding_migration_completed",
310 Self::EmbeddingMigrationFailed => "embedding_migration_failed",
311 Self::EmbeddingDriftDetected => "embedding_drift_detected",
312 Self::EmbedderInitialized => "embedder_initialized",
313 Self::ProposalCreated => "proposal_created",
314 Self::ProposalReviewed => "proposal_reviewed",
315 Self::ProposalApplied => "proposal_applied",
316 Self::ProposalWithdrawn => "proposal_withdrawn",
317 Self::ChannelPollStarted => "channel_poll_started",
318 Self::ChannelPollSucceeded => "channel_poll_succeeded",
319 Self::ChannelPollFailed => "channel_poll_failed",
320 Self::ChannelBackoffArmed => "channel_backoff_armed",
321 Self::ChannelBackoffReset => "channel_backoff_reset",
322 Self::ChannelHeartbeatPersistFailed => "channel_heartbeat_persist_failed",
323 Self::ConfigLocked => "config_locked",
324 Self::CheckpointOutcomeRecorded => "checkpoint_outcome_recorded",
325 Self::PhaseStarted => "phase_started",
326 Self::PhaseCompleted => "phase_completed",
327 Self::PhaseCancelled => "phase_cancelled",
328 Self::Refusal => "refusal",
329 Self::GrantChange => "grant_change",
330 }
331 }
332}
333
334impl fmt::Display for EventKind {
335 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336 f.write_str(self.name())
337 }
338}
339
340const EVENT_KIND_VALID: &[&str] = &[
341 "audit",
342 "recall_executed",
343 "rerank_executed",
344 "search_executed",
345 "tool_check_decided",
346 "link_created",
347 "entity_created",
348 "entity_updated",
349 "entity_deleted",
350 "entity_merged",
351 "note_merged",
352 "note_created",
353 "note_updated",
354 "note_deleted",
355 "edge_updated",
356 "edge_deleted",
357 "task_transitioned",
358 "feedback_explicit",
359 "feedback_unjudged",
360 "profile_resolution_recommended",
361 "profile_merged",
362 "embedding_model_changed",
363 "embedding_migration_completed",
364 "embedding_migration_failed",
365 "embedding_drift_detected",
366 "embedder_initialized",
367 "proposal_created",
368 "proposal_reviewed",
369 "proposal_applied",
370 "proposal_withdrawn",
371 "channel_poll_started",
372 "channel_poll_succeeded",
373 "channel_poll_failed",
374 "channel_backoff_armed",
375 "channel_backoff_reset",
376 "channel_heartbeat_persist_failed",
377 "config_locked",
378 "checkpoint_outcome_recorded",
379 "phase_started",
380 "phase_completed",
381 "phase_cancelled",
382 "refusal",
383 "grant_change",
384];
385
386impl core::str::FromStr for EventKind {
387 type Err = crate::error::UnknownVariant;
388
389 fn from_str(s: &str) -> Result<Self, Self::Err> {
390 match s.trim().to_ascii_lowercase().as_str() {
391 "audit" => Ok(Self::Audit),
392 "recall_executed" => Ok(Self::RecallExecuted),
393 "rerank_executed" => Ok(Self::RerankExecuted),
394 "search_executed" => Ok(Self::SearchExecuted),
395 "tool_check_decided" => Ok(Self::ToolCheckDecided),
396 "link_created" => Ok(Self::LinkCreated),
397 "entity_created" => Ok(Self::EntityCreated),
398 "entity_updated" => Ok(Self::EntityUpdated),
399 "entity_deleted" => Ok(Self::EntityDeleted),
400 "entity_merged" => Ok(Self::EntityMerged),
401 "note_merged" => Ok(Self::NoteMerged),
402 "note_created" => Ok(Self::NoteCreated),
403 "note_updated" => Ok(Self::NoteUpdated),
404 "note_deleted" => Ok(Self::NoteDeleted),
405 "edge_updated" => Ok(Self::EdgeUpdated),
406 "edge_deleted" => Ok(Self::EdgeDeleted),
407 "task_transitioned" => Ok(Self::TaskTransitioned),
408 "feedback_explicit" => Ok(Self::FeedbackExplicit),
409 "feedback_unjudged" => Ok(Self::FeedbackUnjudged),
410 "profile_resolution_recommended" => Ok(Self::ProfileResolutionRecommended),
411 "profile_merged" => Ok(Self::ProfileMerged),
412 "embedding_model_changed" => Ok(Self::EmbeddingModelChanged),
413 "embedding_migration_completed" => Ok(Self::EmbeddingMigrationCompleted),
414 "embedding_migration_failed" => Ok(Self::EmbeddingMigrationFailed),
415 "embedding_drift_detected" => Ok(Self::EmbeddingDriftDetected),
416 "embedder_initialized" => Ok(Self::EmbedderInitialized),
417 "proposal_created" => Ok(Self::ProposalCreated),
418 "proposal_reviewed" => Ok(Self::ProposalReviewed),
419 "proposal_applied" => Ok(Self::ProposalApplied),
420 "proposal_withdrawn" => Ok(Self::ProposalWithdrawn),
421 "channel_poll_started" => Ok(Self::ChannelPollStarted),
422 "channel_poll_succeeded" => Ok(Self::ChannelPollSucceeded),
423 "channel_poll_failed" => Ok(Self::ChannelPollFailed),
424 "channel_backoff_armed" => Ok(Self::ChannelBackoffArmed),
425 "channel_backoff_reset" => Ok(Self::ChannelBackoffReset),
426 "channel_heartbeat_persist_failed" => Ok(Self::ChannelHeartbeatPersistFailed),
427 "config_locked" => Ok(Self::ConfigLocked),
428 "checkpoint_outcome_recorded" => Ok(Self::CheckpointOutcomeRecorded),
429 "phase_started" => Ok(Self::PhaseStarted),
430 "phase_completed" => Ok(Self::PhaseCompleted),
431 "phase_cancelled" => Ok(Self::PhaseCancelled),
432 "refusal" => Ok(Self::Refusal),
433 "grant_change" => Ok(Self::GrantChange),
434 other => Err(crate::error::UnknownVariant::new(
435 "event_kind",
436 other,
437 EVENT_KIND_VALID,
438 )),
439 }
440 }
441}
442
443#[derive(Clone, Debug, PartialEq, Eq)]
448#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
449pub struct AggregateRef {
450 pub kind: String,
452 pub id: Id128,
454}
455
456#[derive(Clone, Debug, PartialEq)]
462#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
463#[cfg_attr(
464 feature = "serde",
465 serde(tag = "kind", content = "payload", rename_all = "snake_case")
466)]
467pub enum EventPayload {
468 Json(String),
470 RerankExecuted(RerankExecutedPayload),
472 ToolCheckDecided(ToolCheckDecidedPayload),
474 #[cfg(feature = "serde")]
476 ProposalCreated(ProposalCreatedPayload),
477 ProposalReviewed(ProposalReviewedPayload),
479 ProposalApplied(ProposalAppliedPayload),
481 ProposalWithdrawn(ProposalWithdrawnPayload),
483}
484
485impl Default for EventPayload {
486 fn default() -> Self {
487 Self::Json("{}".into())
488 }
489}
490
491#[derive(Clone, Debug, PartialEq, Eq)]
493#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
494pub struct ToolCheckDecidedPayload {
495 pub actor: String,
496 pub tool: String,
497 pub registered: bool,
498 pub decision: String,
499 pub source: String,
500 pub id: Option<String>,
501 pub scope: Option<String>,
502 pub caller_verb: String,
503}
504
505#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
508#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
509#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
510pub enum RerankerKind {
511 #[default]
512 Native,
513 Weighted,
514}
515
516#[derive(Clone, Debug, PartialEq)]
521#[cfg_attr(feature = "serde", derive(serde::Serialize))]
522pub struct RerankExecutedPayload {
523 pub reranker: RerankerKind,
525 pub served_by_profile_id: Option<String>,
527 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
529 pub model_id: Option<Id128>,
530 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
532 pub query_id: Option<String>,
533 pub tiers: Vec<String>,
535 pub ignored_weights: Vec<String>,
537 pub unidentified_candidates: usize,
539 pub candidates: Vec<Id128>,
541 pub reranked: Vec<(Id128, Vec<(String, f32)>)>,
543 pub final_scores: Vec<(Id128, f32)>,
545 pub latency_us: u64,
547 pub hook_applied: bool,
549 pub hook_target_match: bool,
551}
552
553impl RerankExecutedPayload {
554 pub fn is_valid(&self) -> bool {
556 let reranked_ok = self
557 .reranked
558 .iter()
559 .all(|(_, scores)| scores.iter().all(|(_, s)| s.is_finite()));
560 let final_ok = self.final_scores.iter().all(|(_, s)| s.is_finite());
561 reranked_ok && final_ok
562 }
563}
564
565#[cfg(feature = "serde")]
566impl<'de> serde::Deserialize<'de> for RerankExecutedPayload {
567 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
568 where
569 D: serde::Deserializer<'de>,
570 {
571 #[derive(serde::Deserialize)]
572 struct Raw {
573 #[serde(default)]
574 reranker: RerankerKind,
575 served_by_profile_id: Option<String>,
576 model_id: Option<Id128>,
577 query_id: Option<String>,
578 #[serde(default)]
579 tiers: Vec<String>,
580 #[serde(default)]
581 ignored_weights: Vec<String>,
582 #[serde(default)]
583 unidentified_candidates: usize,
584 candidates: Vec<Id128>,
585 reranked: Vec<(Id128, Vec<(String, f32)>)>,
586 final_scores: Vec<(Id128, f32)>,
587 latency_us: u64,
588 hook_applied: bool,
589 hook_target_match: bool,
590 }
591
592 let raw = Raw::deserialize(deserializer)?;
593
594 for (_, score) in &raw.final_scores {
595 if !score.is_finite() {
596 return Err(serde::de::Error::custom(alloc::format!(
597 "RerankExecutedPayload final_scores must be finite, got {score}"
598 )));
599 }
600 }
601 for (_, sections) in &raw.reranked {
602 for (section_name, score) in sections {
603 if !score.is_finite() {
604 return Err(serde::de::Error::custom(alloc::format!(
605 "RerankExecutedPayload reranked section '{section_name}' score must be finite, got {score}"
606 )));
607 }
608 }
609 }
610
611 Ok(RerankExecutedPayload {
612 reranker: raw.reranker,
613 served_by_profile_id: raw.served_by_profile_id,
614 model_id: raw.model_id,
615 query_id: raw.query_id,
616 tiers: raw.tiers,
617 ignored_weights: raw.ignored_weights,
618 unidentified_candidates: raw.unidentified_candidates,
619 candidates: raw.candidates,
620 reranked: raw.reranked,
621 final_scores: raw.final_scores,
622 latency_us: raw.latency_us,
623 hook_applied: raw.hook_applied,
624 hook_target_match: raw.hook_target_match,
625 })
626 }
627}
628
629#[cfg(feature = "serde")]
631#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
632pub struct ProposalCreatedPayload {
633 pub proposal_id: Id128,
634 pub proposer: String,
635 pub title: String,
636 pub description: String,
637 pub changeset: ProposalChangeset,
638 pub reviewers: Vec<String>,
639 pub expiry: Option<crate::Timestamp>,
640 pub parent_id: Option<Id128>,
641}
642
643#[cfg(feature = "serde")]
648#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
649pub struct EntityDraft {
650 pub kind: String,
652 pub name: String,
654 #[serde(skip_serializing_if = "Option::is_none")]
656 pub description: Option<String>,
657 #[serde(skip_serializing_if = "Option::is_none")]
659 pub properties: Option<serde_json::Value>,
660 #[serde(default, skip_serializing_if = "Vec::is_empty")]
662 pub tags: Vec<String>,
663}
664
665#[cfg(feature = "serde")]
671#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
672pub struct ProposalEntityPatch {
673 #[serde(skip_serializing_if = "Option::is_none")]
674 pub name: Option<String>,
675 #[serde(
677 default,
678 skip_serializing_if = "Option::is_none",
679 with = "serde_opt_opt"
680 )]
681 pub description: Option<Option<String>>,
682 #[serde(skip_serializing_if = "Option::is_none")]
683 pub properties: Option<serde_json::Value>,
684 #[serde(skip_serializing_if = "Option::is_none")]
685 pub tags: Option<Vec<String>>,
686 #[serde(
690 default,
691 skip_serializing_if = "Option::is_none",
692 with = "serde_opt_opt"
693 )]
694 pub entity_type: Option<Option<String>>,
695}
696
697#[cfg(feature = "serde")]
701#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
702pub struct NoteDraft {
703 pub kind: String,
705 pub content: String,
707 #[serde(skip_serializing_if = "Option::is_none")]
709 pub name: Option<String>,
710 #[serde(skip_serializing_if = "Option::is_none")]
712 pub properties: Option<serde_json::Value>,
713}
714
715#[cfg(feature = "serde")]
717mod serde_opt_opt {
718 use serde::{Deserialize, Deserializer, Serialize, Serializer};
719
720 pub fn serialize<T, S>(val: &Option<Option<T>>, s: S) -> Result<S::Ok, S::Error>
721 where
722 T: Serialize,
723 S: Serializer,
724 {
725 match val {
726 None => unreachable!("skip_serializing_if guards the None case"),
727 Some(inner) => inner.serialize(s),
728 }
729 }
730
731 pub fn deserialize<'de, T, D>(d: D) -> Result<Option<Option<T>>, D::Error>
732 where
733 T: Deserialize<'de>,
734 D: Deserializer<'de>,
735 {
736 let opt: Option<T> = Option::deserialize(d)?;
737 Ok(Some(opt))
738 }
739}
740
741#[cfg(feature = "serde")]
743#[derive(Clone, Debug, PartialEq, serde::Serialize)]
744#[serde(tag = "kind", rename_all = "snake_case")]
745pub enum ProposalChangeset {
746 AddEntity {
748 entity: EntityDraft,
749 },
750 UpdateEntity {
753 id: Id128,
754 patch: ProposalEntityPatch,
755 },
756 AddEdge {
758 source: Id128,
759 target: Id128,
760 relation: crate::EdgeRelation,
761 weight: Option<f32>,
762 },
763 AddNote {
765 note: NoteDraft,
766 },
767 MergeEntities {
768 into: Id128,
769 from: Id128,
770 },
771 SupersedeEntity {
772 old: Id128,
773 new: Id128,
774 },
775 Canonical {
776 schema_version: u32,
777 delta: String,
778 },
779 Compound {
780 steps: Vec<ProposalChangeset>,
781 },
782}
783
784#[cfg(feature = "serde")]
785impl ProposalChangeset {
786 fn validate(&self) -> Result<(), alloc::string::String> {
787 match self {
788 Self::AddEdge { weight, .. } => {
789 if let Some(w) = weight {
790 if !w.is_finite() {
791 return Err(alloc::format!(
792 "ProposalChangeset AddEdge weight must be finite, got {w}"
793 ));
794 }
795 if !crate::validate_edge_weight(f64::from(*w)) {
796 return Err(alloc::format!(
797 "ProposalChangeset AddEdge weight must be in [0.0, 1.0], got {w}"
798 ));
799 }
800 }
801 Ok(())
802 }
803 Self::Compound { steps } => {
804 for step in steps {
805 if matches!(step, Self::Canonical { .. }) {
806 return Err("Canonical proposal changesets must be top-level".into());
807 }
808 step.validate()?;
809 }
810 Ok(())
811 }
812 _ => Ok(()),
813 }
814 }
815}
816
817#[cfg(feature = "serde")]
818impl<'de> serde::Deserialize<'de> for ProposalChangeset {
819 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
820 where
821 D: serde::Deserializer<'de>,
822 {
823 #[derive(serde::Deserialize)]
824 #[serde(deny_unknown_fields)]
825 struct CanonicalFields {
826 schema_version: u32,
827 delta: String,
828 }
829
830 #[derive(serde::Deserialize)]
831 #[serde(tag = "kind", rename_all = "snake_case")]
832 enum ProposalChangesetRaw {
833 AddEntity {
834 entity: EntityDraft,
835 },
836 UpdateEntity {
837 id: Id128,
838 patch: ProposalEntityPatch,
839 },
840 AddEdge {
841 source: Id128,
842 target: Id128,
843 relation: crate::EdgeRelation,
844 weight: Option<f32>,
845 },
846 AddNote {
847 note: NoteDraft,
848 },
849 MergeEntities {
850 into: Id128,
851 from: Id128,
852 },
853 SupersedeEntity {
854 old: Id128,
855 new: Id128,
856 },
857 Canonical(CanonicalFields),
858 Compound {
859 steps: Vec<ProposalChangeset>,
860 },
861 }
862
863 let raw = ProposalChangesetRaw::deserialize(deserializer)?;
864 let cs = match raw {
865 ProposalChangesetRaw::AddEntity { entity } => Self::AddEntity { entity },
866 ProposalChangesetRaw::UpdateEntity { id, patch } => Self::UpdateEntity { id, patch },
867 ProposalChangesetRaw::AddEdge {
868 source,
869 target,
870 relation,
871 weight,
872 } => Self::AddEdge {
873 source,
874 target,
875 relation,
876 weight,
877 },
878 ProposalChangesetRaw::AddNote { note } => Self::AddNote { note },
879 ProposalChangesetRaw::MergeEntities { into, from } => {
880 Self::MergeEntities { into, from }
881 }
882 ProposalChangesetRaw::SupersedeEntity { old, new } => {
883 Self::SupersedeEntity { old, new }
884 }
885 ProposalChangesetRaw::Canonical(CanonicalFields {
886 schema_version,
887 delta,
888 }) => Self::Canonical {
889 schema_version,
890 delta,
891 },
892 ProposalChangesetRaw::Compound { steps } => Self::Compound { steps },
893 };
894 cs.validate().map_err(serde::de::Error::custom)?;
895 Ok(cs)
896 }
897}
898
899#[cfg(not(feature = "serde"))]
900#[derive(Clone, Debug, PartialEq)]
901pub enum ProposalChangeset {
902 AddEdge {
903 source: Id128,
904 target: Id128,
905 relation: crate::EdgeRelation,
906 weight: Option<f32>,
907 },
908 MergeEntities {
909 into: Id128,
910 from: Id128,
911 },
912 SupersedeEntity {
913 old: Id128,
914 new: Id128,
915 },
916 Canonical {
917 schema_version: u32,
918 delta: String,
919 },
920 Compound {
921 steps: Vec<ProposalChangeset>,
922 },
923}
924
925#[derive(Clone, Debug, PartialEq)]
927#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
928pub struct ProposalReviewedPayload {
929 pub proposal_id: Id128,
930 pub reviewer: String,
931 pub decision: ProposalDecision,
932 pub comment: Option<String>,
933}
934
935#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
937#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
938#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
939pub enum ProposalDecision {
940 Approve,
942 Reject,
944 Comment,
946 RequestChanges,
948}
949
950impl ProposalDecision {
951 pub fn as_str(self) -> &'static str {
956 match self {
957 Self::Approve => "approve",
958 Self::Reject => "reject",
959 Self::Comment => "comment",
960 Self::RequestChanges => "request_changes",
961 }
962 }
963}
964
965#[derive(Clone, Debug, PartialEq)]
967#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
968pub struct ProposalAppliedPayload {
969 pub proposal_id: Id128,
970 pub applied_at: crate::Timestamp,
971 pub applied_by: String,
972 pub result: ApplyResult,
973 #[cfg_attr(
974 feature = "serde",
975 serde(default, skip_serializing_if = "Option::is_none")
976 )]
977 pub preimage: Option<ProposalPreimageStatus>,
978}
979
980#[derive(Clone, Copy, Debug, PartialEq, Eq)]
982#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
983#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
984pub enum ProposalPreimageStatus {
985 None,
986 Captured,
987}
988
989#[derive(Clone, Debug, PartialEq)]
991#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
992#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
993pub enum ApplyResult {
994 Success {
995 created_records: Vec<Id128>,
996 },
997 Failed {
998 error: String,
999 applied_step_count: u32,
1000 },
1001}
1002
1003#[derive(Clone, Debug, PartialEq)]
1005#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1006pub struct ProposalWithdrawnPayload {
1007 pub proposal_id: Id128,
1008 pub by: String,
1009 pub reason: Option<String>,
1010}
1011
1012pub struct EventBuilder {
1014 verb: String,
1015 substrate: SubstrateKind,
1016 actor: Option<String>,
1017 kind: EventKind,
1018 payload: EventPayload,
1019 payload_schema_version: u32,
1020 profile_state_version: Option<u64>,
1021 aggregate: Option<AggregateRef>,
1022}
1023
1024impl EventBuilder {
1025 pub fn new(
1027 verb: impl Into<String>,
1028 substrate: SubstrateKind,
1029 actor: impl Into<String>,
1030 ) -> Self {
1031 Self {
1032 verb: verb.into(),
1033 substrate,
1034 actor: Some(actor.into()),
1035 kind: EventKind::Audit,
1036 payload: EventPayload::default(),
1037 payload_schema_version: 1,
1038 profile_state_version: None,
1039 aggregate: None,
1040 }
1041 }
1042
1043 pub fn kind(mut self, kind: EventKind) -> Self {
1045 self.kind = kind;
1046 self
1047 }
1048
1049 pub fn payload(mut self, payload: EventPayload) -> Self {
1051 self.payload = payload;
1052 self
1053 }
1054
1055 pub fn payload_schema_version(mut self, version: u32) -> Self {
1057 self.payload_schema_version = version;
1058 self
1059 }
1060
1061 pub fn profile_state_version(mut self, version: u64) -> Self {
1063 self.profile_state_version = Some(version);
1064 self
1065 }
1066
1067 pub fn aggregate(mut self, aggregate: AggregateRef) -> Self {
1069 self.aggregate = Some(aggregate);
1070 self
1071 }
1072
1073 pub fn build(self, header: Header) -> Event {
1075 Event {
1076 header,
1077 verb: self.verb,
1078 substrate: self.substrate,
1079 actor: self.actor,
1080 kind: self.kind,
1081 payload: self.payload,
1082 payload_schema_version: self.payload_schema_version,
1083 profile_state_version: self.profile_state_version,
1084 aggregate: self.aggregate,
1085 }
1086 }
1087}
1088
1089#[cfg(test)]
1090#[path = "event_tests.rs"]
1091mod tests;
1092
1093#[cfg(all(test, feature = "serde"))]
1094#[path = "event_canonical_tests.rs"]
1095mod canonical_tests;