This commit is contained in:
2026-07-23 16:37:12 +02:00
parent 99c345f2f2
commit 0da75c1311
2159 changed files with 230833 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
# file: kb_materializer_api/Cargo.toml
# version: 2
[package]
name = "kb_materializer_api"
version.workspace = true
edition.workspace = true
license.workspace = true
publish.workspace = true
[dependencies]
kb_core = { path = "../kb_core" }
kb_decoder_api = { path = "../kb_decoder_api" }
kb_model = { path = "../kb_model" }
serde.workspace = true
serde_json.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,25 @@
<!-- file: kb_materializer_api/README.md -->
<!-- version: 3 -->
# kb_materializer_api
Ce crate déclare l'interface commune des matérialisateurs métier.
## Rôle dans l'écosystème
Ce module fait partie du découpage strict de Khadhroony Bot2. Il doit conserver des dépendances limitées et ne pas contourner les interfaces communes du workspace.
## Règles locales
- Les commentaires de code restent en anglais.
- La documentation Markdown reste en français.
- Les exports publics sont contrôlés depuis `lib.rs` lorsque le crate expose une bibliothèque.
- Les binaires utilisent `main.rs` avec les attributs Rust obligatoires.
## Contrats `0.4.0`
Le trait `EventMaterializer` consomme des observations décodées stables et déclare les familles acceptées, lapplicabilité exacte par observation ainsi que la politique applicable aux transactions réussies ou échouées. Les sorties possèdent des clés idempotentes et un statut explicite `inserted`, `replaced`, `ignored`, `refused` ou `failed`.
La garde commune refuse toute matérialisation de trade, liquidité ou lifecycle supposée réussie depuis une observation échouée ou non commitée.
À partir de `0.4.1-pre.005`, le pipeline filtre une observation par surface et entrée avant le ledger et la politique. Deux événements de même famille ne sont donc plus automatiquement proposés au même matérialiseur.

View File

@@ -0,0 +1,416 @@
// file: kb_materializer_api/src/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<std::string::String>,
version: impl std::convert::Into<std::string::String>,
) -> kb_core::Result<Self> {
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<std::string::String>,
message: impl std::convert::Into<std::string::String>,
retriable: bool,
) -> kb_core::Result<Self> {
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: kb_model::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<MaterializedOutput>,
/// Structured diagnostics.
pub diagnostics: std::vec::Vec<MaterializerDiagnostic>,
}
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 [kb_model::EventFamily];
/// Returns whether this materializer accepts one exact decoded observation.
fn accepts_observation(&self, observation: &kb_decoder_api::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: kb_model::EventFamily)
-> MaterializationTransactionPolicy;
/// Materializes one validated decoded observation.
fn materialize(
&self,
observation: &kb_decoder_api::DecodedObservation,
) -> MaterializerExecutionResult;
}
/// Returns true when a materializer explicitly accepts one decoded family.
pub fn materializer_accepts_family(
materializer: &dyn EventMaterializer,
family: kb_model::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: &kb_decoder_api::DecodedObservation,
) -> bool {
return materializer.accepts_observation(observation);
}
/// Applies the mandatory source transaction policy before materialization.
pub fn validate_materialization_policy(
materializer: &dyn EventMaterializer,
observation: &kb_decoder_api::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 == kb_model::EventFamily::Trade
|| family == kb_model::EventFamily::Liquidity
|| family == kb_model::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 != kb_model::EventFamily::Audit
&& family != kb_model::EventFamily::ComplianceAudit
&& family != kb_model::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: &kb_decoder_api::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 != kb_model::MaterializedEventFamily::ComplianceAudit
&& output.family != kb_model::MaterializedEventFamily::TokenMetadataRisk
&& output.family != kb_model::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 [kb_model::EventFamily] {
return &[kb_model::EventFamily::Trade];
}
fn transaction_policy(
&self,
_family: kb_model::EventFamily,
) -> crate::MaterializationTransactionPolicy {
return crate::MaterializationTransactionPolicy::SuccessfulCommittedOnly;
}
fn materialize(
&self,
_observation: &kb_decoder_api::DecodedObservation,
) -> crate::MaterializerExecutionResult {
return crate::MaterializerExecutionResult::ignored();
}
}
fn failed_trade_observation() -> kb_decoder_api::DecodedObservation {
return kb_decoder_api::DecodedObservation {
event_key: "trade".to_string(),
event: kb_model::DecodedProtocolEvent {
signature: kb_model::Signature("signature".to_string()),
slot: kb_model::Slot(42),
instruction_path: kb_model::InstructionPath("0".to_string()),
program_id: kb_model::ProgramId("program".to_string()),
protocol_code: kb_model::ProtocolCode("protocol".to_string()),
surface_code: kb_model::SurfaceCode("surface".to_string()),
event_code: kb_model::EventCode("trade".to_string()),
event_name: kb_model::EventName("trade".to_string()),
event_family: kb_model::EventFamily::Trade,
source_kind: kb_model::EventSourceKind::Instruction,
confidence: kb_model::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: kb_decoder_api::DecoderProof {
kind: kb_decoder_api::DecoderProofKind::ExactLayout,
confidence: kb_model::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 [kb_model::EventFamily] {
return &[kb_model::EventFamily::Audit];
}
fn transaction_policy(
&self,
_family: kb_model::EventFamily,
) -> crate::MaterializationTransactionPolicy {
return crate::MaterializationTransactionPolicy::SuccessfulOrFailedAudit;
}
fn materialize(
&self,
_observation: &kb_decoder_api::DecodedObservation,
) -> crate::MaterializerExecutionResult {
return crate::MaterializerExecutionResult {
status: crate::MaterializerOutcomeStatus::Inserted,
outputs: std::vec![crate::MaterializedOutput {
output_key: "trade".to_string(),
family: kb_model::MaterializedEventFamily::Trade,
payload_json: serde_json::json!({}),
}],
diagnostics: std::vec::Vec::new(),
};
}
}
fn failed_audit_observation() -> kb_decoder_api::DecodedObservation {
let mut observation = failed_trade_observation();
observation.event.event_family = kb_model::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");
}
}

View File

@@ -0,0 +1,35 @@
// file: kb_materializer_api/src/lib.rs
// version: 4
//! Materializer API shared by business materializer crates.
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
mod contracts;
mod materializer;
/// Stable decoded observation materializer contract used by the common pipeline.
pub use crate::contracts::EventMaterializer;
/// Explicit policy applied to successful and failed source transactions.
pub use crate::contracts::MaterializationTransactionPolicy;
/// Stable processor-owned business output derived from one decoded observation.
pub use crate::contracts::MaterializedOutput;
/// Structured materializer diagnostic without generic error wrappers.
pub use crate::contracts::MaterializerDiagnostic;
/// Complete explicit result produced by one materializer invocation.
pub use crate::contracts::MaterializerExecutionResult;
/// Stable identity of one decoded event materializer.
pub use crate::contracts::MaterializerIdentity;
/// Terminal materializer result status.
pub use crate::contracts::MaterializerOutcomeStatus;
/// Returns true when a materializer explicitly accepts one decoded family.
pub use crate::contracts::materializer_accepts_family;
/// Returns true when a materializer accepts one exact decoded observation.
pub use crate::contracts::materializer_accepts_observation;
/// Applies the mandatory source transaction policy before materialization.
pub use crate::contracts::validate_materialization_policy;
/// Rejects mutable business outputs derived from failed or uncommitted observations.
pub use crate::contracts::validate_materialized_output_policy;
/// Exposes the legacy common business materializer trait.
pub use crate::materializer::Materializer;

View File

@@ -0,0 +1,22 @@
// file: kb_materializer_api/src/materializer.rs
// version: 2
//! Materializer API contract used by business materializer crates.
/// Common contract implemented by all business materializer crates.
pub trait Materializer {
/// Returns the stable materializer name.
fn materializer_name(&self) -> &'static str;
/// Returns the materializer version.
fn materializer_version(&self) -> &'static str;
/// Tests whether this materializer accepts a decoded event.
fn accepts_event(&self, event: &kb_model::DecodedProtocolEvent) -> bool;
/// Materializes a decoded event into zero or more business events.
fn materialize_event(
&self,
event: &kb_model::DecodedProtocolEvent,
) -> kb_core::Result<std::vec::Vec<kb_model::MaterializedEvent>>;
}