// file: kb-lib/src/materializer/api/contracts.rs // version: 7 //! Backend-neutral decoded observation materialization contracts. /// Stable identity of one decoded event materializer. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct MaterializerIdentity { /// Stable lower snake case processor name. pub name: std::string::String, /// Semantic or deterministic implementation version. pub version: std::string::String, } impl MaterializerIdentity { /// Builds a validated materializer identity. pub fn new( name: impl std::convert::Into, version: impl std::convert::Into, ) -> kb_core::Result { let value = Self { name: name.into(), version: version.into(), }; if value.name.trim().is_empty() || value.version.trim().is_empty() { return std::result::Result::Err(kb_core::Error::invalid_state( "materializer identity fields must not be empty", )); } return std::result::Result::Ok(value); } } /// Explicit policy applied to successful and failed source transactions. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub enum MaterializationTransactionPolicy { /// Accept committed observations from successful transactions only. SuccessfulCommittedOnly, /// Accept successful observations and failed audit-only observations. SuccessfulOrFailedAudit, /// Accept failed observations for a specifically declared non-mutating family. ExplicitFailedObservation, } /// Terminal materializer result status. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub enum MaterializerOutcomeStatus { /// One or more new stable outputs were inserted. Inserted, /// One or more processor-owned outputs were deterministically replaced. Replaced, /// The decoded observation was intentionally ignored. Ignored, /// The decoded observation was rejected by the declared policy. Refused, /// Materialization failed and diagnostics were produced. Failed, } /// Structured materializer diagnostic without generic error wrappers. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct MaterializerDiagnostic { /// Stable machine-readable diagnostic code. pub code: std::string::String, /// Human-readable diagnostic message. pub message: std::string::String, /// Whether retrying the same version and input may succeed. pub retriable: bool, } impl MaterializerDiagnostic { /// Builds a validated materializer diagnostic. pub fn new( code: impl std::convert::Into, message: impl std::convert::Into, retriable: bool, ) -> kb_core::Result { let value = Self { code: code.into(), message: message.into(), retriable, }; if value.code.trim().is_empty() || value.message.trim().is_empty() { return std::result::Result::Err(kb_core::Error::invalid_state( "materializer diagnostic code and message must not be empty", )); } return std::result::Result::Ok(value); } } /// Stable processor-owned business output derived from one decoded observation. #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct MaterializedOutput { /// Stable processor-owned output key within one decoded observation. pub output_key: std::string::String, /// Business output family. pub family: crate::MaterializedEventFamily, /// Typed business payload serialized as deterministic JSON. pub payload_json: serde_json::Value, } impl MaterializedOutput { /// Validates stable output identity. pub fn validate(&self) -> kb_core::Result<()> { if self.output_key.trim().is_empty() { return std::result::Result::Err(kb_core::Error::invalid_state( "materialized output key must not be empty", )); } return std::result::Result::Ok(()); } } /// Complete explicit result produced by one materializer invocation. #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct MaterializerExecutionResult { /// Terminal materializer status. pub status: MaterializerOutcomeStatus, /// Stable business outputs. pub outputs: std::vec::Vec, /// Structured diagnostics. pub diagnostics: std::vec::Vec, } impl MaterializerExecutionResult { /// Builds a policy refusal result. pub fn refused(code: &str, message: &str) -> Self { return Self { status: MaterializerOutcomeStatus::Refused, outputs: std::vec::Vec::new(), diagnostics: std::vec![MaterializerDiagnostic { code: code.to_string(), message: message.to_string(), retriable: false, }], }; } /// Builds an ignored result. pub fn ignored() -> Self { return Self { status: MaterializerOutcomeStatus::Ignored, outputs: std::vec::Vec::new(), diagnostics: std::vec::Vec::new(), }; } /// Validates status and output consistency. pub fn validate(&self) -> kb_core::Result<()> { let output_status = self.status == MaterializerOutcomeStatus::Inserted || self.status == MaterializerOutcomeStatus::Replaced; if output_status && self.outputs.is_empty() { return std::result::Result::Err(kb_core::Error::invalid_state( "inserted or replaced materializer status requires outputs", )); } if !output_status && !self.outputs.is_empty() { return std::result::Result::Err(kb_core::Error::invalid_state( "ignored, refused or failed materializer status must not contain outputs", )); } if self.status == MaterializerOutcomeStatus::Failed && self.diagnostics.is_empty() { return std::result::Result::Err(kb_core::Error::invalid_state( "failed materializer status requires at least one diagnostic", )); } for output in &self.outputs { let validation_result = output.validate(); if let std::result::Result::Err(error) = validation_result { return std::result::Result::Err(error); } } return std::result::Result::Ok(()); } } /// Stable decoded observation materializer contract used by the common pipeline. pub trait EventMaterializer: std::marker::Send + std::marker::Sync { /// Returns the stable materializer identity. fn identity(&self) -> MaterializerIdentity; /// Returns every decoded event family explicitly accepted by this materializer. fn accepted_families(&self) -> &'static [crate::EventFamily]; /// Returns whether this materializer accepts one exact decoded observation. fn accepts_observation(&self, observation: &crate::DecodedObservation) -> bool { let family = observation.event.event_family; return self.accepted_families().iter().any(|accepted| return *accepted == family); } /// Returns the failed/successful transaction policy for one accepted family. fn transaction_policy(&self, family: crate::EventFamily) -> MaterializationTransactionPolicy; /// Materializes one validated decoded observation. fn materialize(&self, observation: &crate::DecodedObservation) -> MaterializerExecutionResult; } /// Returns true when a materializer explicitly accepts one decoded family. pub fn materializer_accepts_family( materializer: &dyn EventMaterializer, family: crate::EventFamily, ) -> bool { return materializer .accepted_families() .iter() .any(|accepted| return *accepted == family); } /// Returns true when a materializer accepts one exact decoded observation. pub fn materializer_accepts_observation( materializer: &dyn EventMaterializer, observation: &crate::DecodedObservation, ) -> bool { return materializer.accepts_observation(observation); } /// Applies the mandatory source transaction policy before materialization. pub fn validate_materialization_policy( materializer: &dyn EventMaterializer, observation: &crate::DecodedObservation, ) -> std::result::Result<(), MaterializerExecutionResult> { let family = observation.event.event_family; if !crate::materializer_accepts_observation(materializer, observation) { return std::result::Result::Err(MaterializerExecutionResult::refused( "unsupported_decoded_observation", "materializer does not accept the decoded observation surface and entry", )); } let policy = materializer.transaction_policy(family); if observation.transaction_failed || !observation.observation_committed { if family == crate::EventFamily::Trade || family == crate::EventFamily::Liquidity || family == crate::EventFamily::Lifecycle { return std::result::Result::Err(MaterializerExecutionResult::refused( "failed_transaction_mutation_refused", "failed or uncommitted observations cannot create successful mutable business outputs", )); } if policy == MaterializationTransactionPolicy::SuccessfulCommittedOnly { return std::result::Result::Err(MaterializerExecutionResult::refused( "failed_transaction_policy_refused", "materializer accepts committed observations from successful transactions only", )); } if policy == MaterializationTransactionPolicy::SuccessfulOrFailedAudit && family != crate::EventFamily::Audit && family != crate::EventFamily::ComplianceAudit && family != crate::EventFamily::Risk { return std::result::Result::Err(MaterializerExecutionResult::refused( "failed_transaction_non_audit_refused", "failed transaction materialization is limited to declared audit or risk families", )); } } return std::result::Result::Ok(()); } /// Rejects successful mutable business outputs derived from failed or uncommitted observations. pub fn validate_materialized_output_policy( observation: &crate::DecodedObservation, result: &MaterializerExecutionResult, ) -> std::result::Result<(), MaterializerExecutionResult> { if !observation.transaction_failed && observation.observation_committed { return std::result::Result::Ok(()); } let forbidden_output = result.outputs.iter().any(|output| { return output.family != crate::MaterializedEventFamily::ComplianceAudit && output.family != crate::MaterializedEventFamily::TokenMetadataRisk && output.family != crate::MaterializedEventFamily::Risk; }); if forbidden_output { return std::result::Result::Err(MaterializerExecutionResult::refused( "failed_transaction_output_refused", "failed or uncommitted observations may only produce audit or risk outputs", )); } return std::result::Result::Ok(()); } #[cfg(test)] mod tests { struct TradeMaterializer; impl crate::EventMaterializer for TradeMaterializer { fn identity(&self) -> crate::MaterializerIdentity { return crate::MaterializerIdentity { name: "trade_materializer".to_string(), version: "1".to_string(), }; } fn accepted_families(&self) -> &'static [crate::EventFamily] { return &[crate::EventFamily::Trade]; } fn transaction_policy( &self, _family: crate::EventFamily, ) -> crate::MaterializationTransactionPolicy { return crate::MaterializationTransactionPolicy::SuccessfulCommittedOnly; } fn materialize( &self, _observation: &crate::DecodedObservation, ) -> crate::MaterializerExecutionResult { return crate::MaterializerExecutionResult::ignored(); } } fn failed_trade_observation() -> crate::DecodedObservation { return crate::DecodedObservation { event_key: "trade".to_string(), event: crate::DecodedProtocolEvent { signature: crate::Signature("signature".to_string()), slot: crate::Slot(42), instruction_path: crate::InstructionPath("0".to_string()), program_id: crate::ProgramId("program".to_string()), protocol_code: crate::ProtocolCode("protocol".to_string()), surface_code: crate::SurfaceCode("surface".to_string()), event_code: crate::EventCode("trade".to_string()), event_name: crate::EventName("trade".to_string()), event_family: crate::EventFamily::Trade, source_kind: crate::EventSourceKind::Instruction, confidence: crate::DecoderConfidence::Exact, }, payload_json: serde_json::json!({}), transaction_failed: true, transaction_error: std::option::Option::Some( serde_json::json!({"InstructionError": [0, "Custom"]}), ), observation_committed: false, proof: crate::DecoderProof { kind: crate::DecoderProofKind::ExactLayout, confidence: crate::DecoderConfidence::Exact, evidence: std::vec!["layout".to_string()], }, }; } #[test] fn failed_trade_is_refused_before_materialization() { let result = crate::validate_materialization_policy(&TradeMaterializer, &failed_trade_observation()); assert!(result.is_err()); let refusal = match result { std::result::Result::Ok(()) => panic!("failed trade unexpectedly accepted"), std::result::Result::Err(value) => value, }; assert_eq!(refusal.status, crate::MaterializerOutcomeStatus::Refused); assert_eq!(refusal.diagnostics[0].code, "failed_transaction_mutation_refused"); } struct FailedAuditToTradeMaterializer; impl crate::EventMaterializer for FailedAuditToTradeMaterializer { fn identity(&self) -> crate::MaterializerIdentity { return crate::MaterializerIdentity { name: "failed_audit_to_trade".to_string(), version: "1".to_string(), }; } fn accepted_families(&self) -> &'static [crate::EventFamily] { return &[crate::EventFamily::Audit]; } fn transaction_policy( &self, _family: crate::EventFamily, ) -> crate::MaterializationTransactionPolicy { return crate::MaterializationTransactionPolicy::SuccessfulOrFailedAudit; } fn materialize( &self, _observation: &crate::DecodedObservation, ) -> crate::MaterializerExecutionResult { return crate::MaterializerExecutionResult { status: crate::MaterializerOutcomeStatus::Inserted, outputs: std::vec![crate::MaterializedOutput { output_key: "trade".to_string(), family: crate::MaterializedEventFamily::Trade, payload_json: serde_json::json!({}), }], diagnostics: std::vec::Vec::new(), }; } } fn failed_audit_observation() -> crate::DecodedObservation { let mut observation = failed_trade_observation(); observation.event.event_family = crate::EventFamily::Audit; return observation; } #[test] fn failed_audit_cannot_emit_successful_trade_output() { let materializer = FailedAuditToTradeMaterializer; let observation = failed_audit_observation(); let policy_result = crate::validate_materialization_policy(&materializer, &observation); assert!(policy_result.is_ok()); let materialized = crate::EventMaterializer::materialize(&materializer, &observation); let output_result = crate::validate_materialized_output_policy(&observation, &materialized); assert!(output_result.is_err()); let refusal = match output_result { std::result::Result::Ok(()) => panic!("failed audit unexpectedly emitted a trade"), std::result::Result::Err(value) => value, }; assert_eq!(refusal.status, crate::MaterializerOutcomeStatus::Refused); assert_eq!(refusal.diagnostics[0].code, "failed_transaction_output_refused"); } }