Files
khadhroony-bot3/kb-pipeline/src/decode_replay.rs
2026-07-28 18:41:30 +02:00

2263 lines
109 KiB
Rust

// file: kb-pipeline/src/decode_replay.rs
// version: 2
//! Common contextual instruction decode and optional materialization pipeline.
use futures_util::StreamExt; // rust-rules: trait-import
use tracing::Instrument; // rust-rules: trait-import
static DECODE_CAMPAIGN_SEQUENCE: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(1);
/// Stable processing ledger stage used by contextual instruction decoders.
pub const INSTRUCTION_DECODE_STAGE: &str = "instruction_decode";
/// Stable processing ledger stage used by decoded event materializers.
pub const EVENT_MATERIALIZATION_STAGE: &str = "event_materialization";
/// Current common decode pipeline orchestration version.
pub const DECODE_PIPELINE_VERSION: &str = "1";
/// Deterministic policy used when several decoders recognize one input.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecodeDispatchPolicy {
/// Run only the highest-ranked compatible decoder.
HighestPriority,
/// Run every compatible decoder in deterministic rank order.
AllCompatible,
}
/// Complete bounded contextual decode replay request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DecodeReplayRequest {
/// Stable caller-provided campaign identifier used by structured tracing.
pub campaign_id: std::string::String,
/// Bounded backend-neutral input selection.
pub selection: kb_store::DecodeSelectionFilter,
/// Explicit decoder names to enable, or every supplied decoder when empty.
pub decoder_names: std::vec::Vec<std::string::String>,
/// Deterministic multi-decoder dispatch policy.
pub dispatch_policy: crate::DecodeDispatchPolicy,
/// Maximum concurrent contextual input executions.
pub max_concurrent_inputs: u32,
/// Replaces processor-owned outputs for the selected version and input.
pub force_replay: bool,
/// Explicitly authorizes a bounded force replay without exact signatures.
pub force_replay_all_matching: bool,
/// Runs compatible materializers after decoded observations are committed.
pub materialize_after_decode: bool,
}
impl DecodeReplayRequest {
/// Validates campaign bounds and explicit decoder names.
pub fn validate(&self) -> kb_core::Result<()> {
if self.campaign_id.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"decode replay campaign id must not be empty",
));
}
if self.max_concurrent_inputs == 0 {
return std::result::Result::Err(kb_core::Error::config(
"decode replay concurrency must be greater than zero",
));
}
if self.force_replay
&& self.selection.signatures.is_empty()
&& !self.force_replay_all_matching
{
return std::result::Result::Err(kb_core::Error::config(
"force replay requires exact signatures or explicit all-matching authorization",
));
}
if self.force_replay_all_matching && !self.force_replay {
return std::result::Result::Err(kb_core::Error::config(
"all-matching replay authorization requires force replay",
));
}
if self.force_replay_all_matching && !self.selection.signatures.is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"all-matching replay authorization cannot be combined with exact signatures",
));
}
if self.decoder_names.iter().any(|name| return name.trim().is_empty()) {
return std::result::Result::Err(kb_core::Error::config(
"decode replay decoder names must not be empty",
));
}
return std::result::Result::Ok(());
}
}
/// Decode replay progress severity.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecodeReplayProgressLevel {
/// Diagnostic detail.
Debug,
/// Normal campaign information.
Info,
/// Recoverable issue or cancellation.
Warning,
/// Decode or persistence failure.
Error,
}
impl DecodeReplayProgressLevel {
/// Returns the stable lowercase level code.
pub fn code(&self) -> &'static str {
return match self {
crate::DecodeReplayProgressLevel::Debug => "debug",
crate::DecodeReplayProgressLevel::Info => "info",
crate::DecodeReplayProgressLevel::Warning => "warn",
crate::DecodeReplayProgressLevel::Error => "error",
};
}
}
/// One operator-visible decode replay progress event.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DecodeReplayProgressEvent {
/// UTC timestamp rendered in RFC 3339.
pub timestamp: std::string::String,
/// Severity.
pub level: crate::DecodeReplayProgressLevel,
/// Human-readable message.
pub message: std::string::String,
/// Number of terminal contextual inputs.
pub completed: u64,
/// Total selected contextual inputs.
pub total: u64,
}
impl DecodeReplayProgressEvent {
fn new(
level: crate::DecodeReplayProgressLevel,
message: impl std::convert::Into<std::string::String>,
completed: u64,
total: u64,
) -> Self {
return Self {
timestamp: chrono::Utc::now().to_rfc3339(),
level,
message: message.into(),
completed,
total,
};
}
}
/// Progress and cooperative cancellation contract implemented by applications.
pub trait DecodeReplayObserver: Sync {
/// Receives one progress event.
fn on_progress(&self, event: &crate::DecodeReplayProgressEvent);
/// Returns true when cooperative cancellation was requested.
fn is_cancelled(&self) -> bool;
}
/// Aggregated terminal counters for one decoder version.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DecodeProcessorSummary {
/// Stable decoder name.
pub processor_name: std::string::String,
/// Stable decoder version.
pub processor_version: std::string::String,
/// Number of compatible dispatches.
pub dispatched: u64,
/// Number skipped because version and contextual input hash already succeeded.
pub skipped: u64,
/// Number producing decoded observations.
pub decoded: u64,
/// Number intentionally ignored.
pub ignored: u64,
/// Number recognized but unsupported.
pub unsupported: u64,
/// Number returning an explicit decoder failure outcome.
pub failed: u64,
/// Number interrupted by orchestration, storage or other processing errors.
pub processing_errors: u64,
/// Number of materialized outputs committed.
pub materialized_outputs: u64,
/// Number of materializer policy refusals committed to the ledger.
pub materialization_refused: u64,
}
/// Final counters for one bounded contextual decode replay campaign.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DecodeReplaySummary {
/// Stable process-local campaign identifier used by structured tracing.
pub campaign_id: std::string::String,
/// Common orchestration implementation version.
pub pipeline_version: std::string::String,
/// Number of selected contextual inputs.
pub selected: u64,
/// Number admitted to execution.
pub started: u64,
/// Number reaching a terminal input result.
pub completed: u64,
/// Number with no compatible enabled decoder.
pub unmatched: u64,
/// Number never decoded because cancellation was already requested.
pub not_started: u64,
/// Number of inputs returning an explicit decoder failure outcome.
pub failed_inputs: u64,
/// Number of inputs interrupted by orchestration, storage or other processing errors.
pub processing_error_inputs: u64,
/// Whether cooperative cancellation was observed.
pub cancelled: bool,
/// Per-decoder counters ordered by name and version.
pub processors: std::vec::Vec<crate::DecodeProcessorSummary>,
/// Campaign start timestamp.
pub started_at: std::string::String,
/// Campaign finish timestamp.
pub finished_at: std::string::String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct DecodeItemOutcome {
signature: std::string::String,
slot: u64,
instruction_path: std::string::String,
program_id: std::string::String,
started: bool,
unmatched: bool,
decode_failed: bool,
processing_error: bool,
cancelled: bool,
processors: std::vec::Vec<crate::DecodeProcessorSummary>,
}
struct RankedDecoder<'decoder> {
decoder: &'decoder dyn kb_lib::DcApiInstructionDecoder,
identity: kb_lib::DcApiDecoderIdentity,
recognition: kb_lib::DcApiDecoderRecognition,
}
/// Executes one bounded contextual instruction decode and optional materialization campaign.
pub async fn execute_decode_replay<S, O>(
store: &S,
request: &crate::DecodeReplayRequest,
decoders: &[std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>],
materializers: &[std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>],
observer: &O,
) -> kb_core::Result<crate::DecodeReplaySummary>
where
S: kb_store::DecodePipelineStore + Sync,
O: crate::DecodeReplayObserver,
{
let campaign_id = request.campaign_id.clone();
let campaign_span = tracing::debug_span!(
target: crate::TRACING_TARGET,
"decode_replay_campaign",
campaign_id = %campaign_id,
pipeline_version = crate::DECODE_PIPELINE_VERSION
);
return execute_decode_replay_campaign(
store,
request,
decoders,
materializers,
observer,
campaign_id,
)
.instrument(campaign_span)
.await;
}
async fn execute_decode_replay_campaign<S, O>(
store: &S,
request: &crate::DecodeReplayRequest,
decoders: &[std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>],
materializers: &[std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>],
observer: &O,
campaign_id: std::string::String,
) -> kb_core::Result<crate::DecodeReplaySummary>
where
S: kb_store::DecodePipelineStore + Sync,
O: crate::DecodeReplayObserver,
{
let signature_sample = text_sample(request.selection.signatures.as_slice(), 5);
tracing::debug!(
target: crate::TRACING_TARGET,
action = "execute_campaign",
campaign_id = %campaign_id,
signature_count = request.selection.signatures.len(),
signature_sample = ?signature_sample,
processing_states = ?request.selection.processing_states,
min_slot = ?request.selection.min_slot,
max_slot = ?request.selection.max_slot,
program_ids = ?request.selection.program_ids,
instruction_paths = ?request.selection.instruction_paths,
limit = request.selection.limit,
decoder_names = ?request.decoder_names,
dispatch_policy = ?request.dispatch_policy,
max_concurrent_inputs = request.max_concurrent_inputs,
force_replay = request.force_replay,
force_replay_all_matching = request.force_replay_all_matching,
materialize_after_decode = request.materialize_after_decode,
supplied_decoder_count = decoders.len(),
supplied_materializer_count = materializers.len(),
"received contextual decode replay campaign"
);
let validation_result = request.validate();
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let enabled = enabled_decoders(request, decoders);
if enabled.is_empty() {
tracing::error!(target: crate::TRACING_TARGET, action = "resolve_decoders", campaign_id = %campaign_id, requested_decoder_names = ?request.decoder_names, supplied_decoder_count = decoders.len(), "no contextual decoder is enabled");
return std::result::Result::Err(kb_core::Error::config(
"decode replay requires at least one enabled decoder",
));
}
let enabled_decoder_identities = enabled
.iter()
.map(|decoder| {
let identity = decoder.identity();
return format!("{}@{}", identity.name, identity.version);
})
.collect::<std::vec::Vec<_>>();
tracing::debug!(target: crate::TRACING_TARGET, action = "resolve_decoders", campaign_id = %campaign_id, enabled_decoders = ?enabled_decoder_identities, "resolved contextual decode replay decoders");
let decoder_registry_result = validate_decoder_registry(&enabled);
if let std::result::Result::Err(error) = decoder_registry_result {
return std::result::Result::Err(error);
}
let effective_selection_result = effective_selection(request, enabled.as_slice());
let effective_selection = match effective_selection_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
tracing::debug!(
target: crate::TRACING_TARGET,
action = "resolve_selection",
campaign_id = %campaign_id,
requested_program_ids = ?request.selection.program_ids,
effective_program_ids = ?effective_selection.program_ids,
requested_processing_states = ?request.selection.processing_states,
effective_processing_states = ?effective_selection.processing_states,
force_replay = request.force_replay,
explicit_signature_selection = !effective_selection.signatures.is_empty(),
processing_state_filter_bypassed = request.force_replay
&& (!effective_selection.signatures.is_empty() || request.force_replay_all_matching),
force_replay_all_matching = request.force_replay_all_matching,
"resolved contextual decode replay selection"
);
let ordered_materializers_result = ordered_materializers(materializers);
let ordered_materializers = match ordered_materializers_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let ordered_materializer_identities = ordered_materializers
.iter()
.map(|materializer| {
let identity = materializer.identity();
return format!("{}@{}", identity.name, identity.version);
})
.collect::<std::vec::Vec<_>>();
if request.materialize_after_decode && ordered_materializers.is_empty() {
tracing::error!(target: crate::TRACING_TARGET, action = "resolve_materializers", campaign_id = %campaign_id, requested = true, available_count = 0_usize, "materialization requested without available materializer");
return std::result::Result::Err(kb_core::Error::config(
"decode replay materialization requires at least one available materializer",
));
}
tracing::debug!(target: crate::TRACING_TARGET, action = "resolve_materializers", campaign_id = %campaign_id, materializers = ?ordered_materializer_identities, "resolved contextual decode replay materializers");
for decoder in &enabled {
let identity = decoder.identity();
let declarations = coverage_declaration_inserts(decoder.as_ref());
tracing::debug!(target: crate::TRACING_TARGET, action = "persist_coverage_declarations", campaign_id = %campaign_id, processor_name = %identity.name, processor_version = %identity.version, declaration_count = declarations.len(), "persist decoder coverage declarations");
let persist_result = store.persist_decode_coverage_declarations(&declarations).await;
match persist_result {
std::result::Result::Ok(outcome) => {
tracing::debug!(target: crate::TRACING_TARGET, action = "persist_coverage_declarations", campaign_id = %campaign_id, processor_name = %identity.name, processor_version = %identity.version, outcome = ?outcome, "decoder coverage declarations persisted");
},
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "persist_coverage_declarations", campaign_id = %campaign_id, processor_name = %identity.name, processor_version = %identity.version, error = %error, "decoder coverage declaration persistence failed");
return std::result::Result::Err(error);
},
}
}
tracing::debug!(target: crate::TRACING_TARGET, action = "select_inputs", campaign_id = %campaign_id, signature_count = effective_selection.signatures.len(), processing_states = ?effective_selection.processing_states, min_slot = ?effective_selection.min_slot, max_slot = ?effective_selection.max_slot, program_ids = ?effective_selection.program_ids, instruction_paths = ?effective_selection.instruction_paths, limit = effective_selection.limit, "select contextual decode replay inputs");
let inputs_result = store.list_decode_inputs(&effective_selection).await;
let inputs = match inputs_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "select_inputs", campaign_id = %campaign_id, error = %error, "contextual decode replay input selection failed");
return std::result::Result::Err(error);
},
};
tracing::debug!(target: crate::TRACING_TARGET, action = "select_inputs", campaign_id = %campaign_id, selected_count = inputs.len(), "selected contextual decode replay inputs");
for input in &inputs {
tracing::debug!(
target: crate::TRACING_TARGET,
action = "selected_input",
campaign_id = %campaign_id,
signature = %input.signature,
slot = input.slot,
instruction_path = %input.instruction_path,
program_id = %input.program_id,
input_key = %input.replay_input_key,
transaction_failed = input.transaction_failed,
surface_code_hint = ?input.surface_code_hint,
payload_hash = ?input.instruction_payload_hash,
"selected contextual decode replay input"
);
}
let started_at = chrono::Utc::now().to_rfc3339();
let selected_result = u64::try_from(inputs.len());
let selected = match selected_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"decode selected count conversion failed: {error}"
)));
},
};
observer.on_progress(&crate::DecodeReplayProgressEvent::new(
crate::DecodeReplayProgressLevel::Info,
format!(
"decode replay campaign {campaign_id} selected {selected} contextual inputs for {} decoders",
enabled.len()
),
0,
selected,
));
let concurrency_result = usize::try_from(request.max_concurrent_inputs);
let concurrency = match concurrency_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"decode concurrency conversion failed: {error}"
)));
},
};
let enabled_ref = enabled.as_slice();
let materializers_ref = ordered_materializers.as_slice();
let campaign_id_ref = campaign_id.as_str();
let stream = futures_util::stream::iter(inputs.into_iter().map(|input| {
return async move {
let input_span = tracing::debug_span!(
target: crate::TRACING_TARGET,
"decode_replay_input",
campaign_id = %campaign_id_ref,
signature = %input.signature,
slot = input.slot,
instruction_path = %input.instruction_path,
program_id = %input.program_id,
input_key = %input.replay_input_key
);
return execute_decode_input(
store,
request,
enabled_ref,
materializers_ref,
observer,
campaign_id_ref,
&input,
)
.instrument(input_span)
.await;
};
}))
.buffer_unordered(concurrency);
futures_util::pin_mut!(stream);
let mut outcomes = std::vec::Vec::new();
let mut settled = 0_u64;
while let std::option::Option::Some(outcome) = stream.next().await {
settled += 1;
let level = if outcome.decode_failed || outcome.processing_error {
crate::DecodeReplayProgressLevel::Error
} else if outcome.cancelled {
crate::DecodeReplayProgressLevel::Warning
} else {
crate::DecodeReplayProgressLevel::Debug
};
observer.on_progress(&crate::DecodeReplayProgressEvent::new(
level,
format!(
"decode replay input terminal signature={} slot={} instruction_path={} program_id={} unmatched={} decode_failed={} processing_error={} cancelled={} processors={:?}",
outcome.signature,
outcome.slot,
outcome.instruction_path,
outcome.program_id,
outcome.unmatched,
outcome.decode_failed,
outcome.processing_error,
outcome.cancelled,
outcome.processors
),
settled,
selected,
));
outcomes.push(outcome);
}
let mut summary = crate::DecodeReplaySummary {
campaign_id: campaign_id.clone(),
pipeline_version: crate::DECODE_PIPELINE_VERSION.to_string(),
selected,
started: 0,
completed: 0,
unmatched: 0,
not_started: 0,
failed_inputs: 0,
processing_error_inputs: 0,
cancelled: observer.is_cancelled(),
processors: std::vec::Vec::new(),
started_at,
finished_at: chrono::Utc::now().to_rfc3339(),
};
for outcome in outcomes {
if outcome.started {
summary.started += 1;
summary.completed += 1;
}
if outcome.unmatched {
summary.unmatched += 1;
}
if outcome.decode_failed {
summary.failed_inputs += 1;
}
if outcome.processing_error {
summary.processing_error_inputs += 1;
}
if outcome.cancelled && !outcome.started {
summary.not_started += 1;
}
for processor in outcome.processors {
merge_processor_summary(&mut summary.processors, processor);
}
}
summary.processors.sort_by(|left, right| {
return left
.processor_name
.cmp(&right.processor_name)
.then(left.processor_version.cmp(&right.processor_version));
});
if summary.unmatched > 0 || summary.failed_inputs > 0 || summary.processing_error_inputs > 0 {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_campaign", campaign_id = %summary.campaign_id, selected = summary.selected, started = summary.started, completed = summary.completed, unmatched = summary.unmatched, not_started = summary.not_started, failed_inputs = summary.failed_inputs, processing_error_inputs = summary.processing_error_inputs, cancelled = summary.cancelled, processors = ?summary.processors, started_at = %summary.started_at, finished_at = %summary.finished_at, "contextual decode replay completed with failures");
} else if summary.cancelled {
tracing::warn!(target: crate::TRACING_TARGET, action = "execute_campaign", campaign_id = %summary.campaign_id, selected = summary.selected, started = summary.started, completed = summary.completed, unmatched = summary.unmatched, not_started = summary.not_started, failed_inputs = summary.failed_inputs, processing_error_inputs = summary.processing_error_inputs, cancelled = summary.cancelled, processors = ?summary.processors, started_at = %summary.started_at, finished_at = %summary.finished_at, "contextual decode replay cancelled");
} else {
tracing::info!(target: crate::TRACING_TARGET, action = "execute_campaign", campaign_id = %summary.campaign_id, selected = summary.selected, started = summary.started, completed = summary.completed, unmatched = summary.unmatched, not_started = summary.not_started, failed_inputs = summary.failed_inputs, processing_error_inputs = summary.processing_error_inputs, cancelled = summary.cancelled, processors = ?summary.processors, started_at = %summary.started_at, finished_at = %summary.finished_at, "contextual decode replay completed");
}
return std::result::Result::Ok(summary);
}
async fn execute_decode_input<S, O>(
store: &S,
request: &crate::DecodeReplayRequest,
decoders: &[std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>],
materializers: &[std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>],
observer: &O,
campaign_id: &str,
input: &kb_lib::MdCoreInstructionReplayInput,
) -> DecodeItemOutcome
where
S: kb_store::DecodePipelineStore + Sync,
O: crate::DecodeReplayObserver,
{
tracing::debug!(target: crate::TRACING_TARGET, action = "admit_input", campaign_id = %campaign_id, signature = %input.signature, slot = input.slot, instruction_path = %input.instruction_path, program_id = %input.program_id, input_key = %input.replay_input_key, transaction_failed = input.transaction_failed, "consider contextual decode replay input");
if observer.is_cancelled() {
tracing::debug!(target: crate::TRACING_TARGET, action = "admit_input", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, admitted = false, reason = "cancelled_before_admission", "contextual decode replay input not admitted");
return DecodeItemOutcome {
signature: input.signature.clone(),
slot: input.slot,
instruction_path: input.instruction_path.clone(),
program_id: input.program_id.clone(),
started: false,
unmatched: false,
decode_failed: false,
processing_error: false,
cancelled: true,
processors: std::vec::Vec::new(),
};
}
let ranked = rank_decoders(campaign_id, input, decoders);
if ranked.is_empty() {
tracing::error!(target: crate::TRACING_TARGET, action = "dispatch_input", campaign_id = %campaign_id, signature = %input.signature, slot = input.slot, instruction_path = %input.instruction_path, program_id = %input.program_id, matched_decoder_count = 0_usize, error_code = "decode_input_unmatched", "no contextual decoder matched selected input");
return DecodeItemOutcome {
signature: input.signature.clone(),
slot: input.slot,
instruction_path: input.instruction_path.clone(),
program_id: input.program_id.clone(),
started: true,
unmatched: true,
decode_failed: false,
processing_error: false,
cancelled: false,
processors: std::vec::Vec::new(),
};
}
let selected = match request.dispatch_policy {
crate::DecodeDispatchPolicy::HighestPriority => ranked.into_iter().take(1).collect(),
crate::DecodeDispatchPolicy::AllCompatible => ranked,
};
let selected_decoder_identities = selected
.iter()
.map(|ranked_decoder: &RankedDecoder<'_>| {
return format!("{}@{}", ranked_decoder.identity.name, ranked_decoder.identity.version);
})
.collect::<std::vec::Vec<_>>();
tracing::debug!(target: crate::TRACING_TARGET, action = "dispatch_input", campaign_id = %campaign_id, signature = %input.signature, slot = input.slot, instruction_path = %input.instruction_path, program_id = %input.program_id, dispatch_policy = ?request.dispatch_policy, selected_decoders = ?selected_decoder_identities, "dispatch contextual decode replay input");
let mut processors = std::vec::Vec::new();
let mut decode_failed = false;
let mut processing_error = false;
for ranked_decoder in selected {
if observer.is_cancelled() {
break;
}
tracing::debug!(target: crate::TRACING_TARGET, action = "invoke_decoder", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, program_id = %input.program_id, processor_name = %ranked_decoder.identity.name, processor_version = %ranked_decoder.identity.version, recognition = ?ranked_decoder.recognition, "invoke contextual instruction decoder");
let processor_span = tracing::debug_span!(
target: crate::TRACING_TARGET,
"decode_replay_processor",
campaign_id = %campaign_id,
signature = %input.signature,
instruction_path = %input.instruction_path,
program_id = %input.program_id,
processor_name = %ranked_decoder.identity.name,
processor_version = %ranked_decoder.identity.version
);
let processor_result =
execute_decoder(store, request, campaign_id, &ranked_decoder, materializers, input)
.instrument(processor_span)
.await;
match processor_result {
std::result::Result::Ok(value) => {
tracing::debug!(target: crate::TRACING_TARGET, action = "invoke_decoder", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, program_id = %input.program_id, processor_name = %value.processor_name, processor_version = %value.processor_version, summary = ?value, "contextual instruction decoder completed");
if value.failed > 0 {
decode_failed = true;
}
processors.push(value);
},
std::result::Result::Err(error) => {
processing_error = true;
processors.push(crate::DecodeProcessorSummary {
processor_name: ranked_decoder.identity.name.clone(),
processor_version: ranked_decoder.identity.version.clone(),
dispatched: 1,
skipped: 0,
decoded: 0,
ignored: 0,
unsupported: 0,
failed: 0,
processing_errors: 1,
materialized_outputs: 0,
materialization_refused: 0,
});
tracing::error!(
target: crate::TRACING_TARGET,
action = "invoke_decoder", campaign_id = %campaign_id,
signature = %input.signature,
slot = input.slot,
instruction_path = %input.instruction_path,
program_id = %input.program_id,
processor_name = %ranked_decoder.identity.name,
processor_version = %ranked_decoder.identity.version,
input_key = %input.replay_input_key,
error = %error,
"contextual instruction decode failed"
);
},
}
}
let cancelled = observer.is_cancelled();
tracing::debug!(target: crate::TRACING_TARGET, action = "complete_input", campaign_id = %campaign_id, signature = %input.signature, slot = input.slot, instruction_path = %input.instruction_path, program_id = %input.program_id, decode_failed, processing_error, cancelled, processors = ?processors, "contextual decode replay input completed");
return DecodeItemOutcome {
signature: input.signature.clone(),
slot: input.slot,
instruction_path: input.instruction_path.clone(),
program_id: input.program_id.clone(),
started: true,
unmatched: false,
decode_failed,
processing_error,
cancelled,
processors,
};
}
async fn execute_decoder<S>(
store: &S,
request: &crate::DecodeReplayRequest,
campaign_id: &str,
ranked: &RankedDecoder<'_>,
materializers: &[std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>],
input: &kb_lib::MdCoreInstructionReplayInput,
) -> kb_core::Result<crate::DecodeProcessorSummary>
where
S: kb_store::DecodePipelineStore + Sync,
{
let input_hash_result = kb_lib::decoder_api_contextual_input_hash(input);
let input_hash = match input_hash_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let ledger_result = kb_store::ProcessingLedgerIdentity::new(
crate::INSTRUCTION_DECODE_STAGE,
ranked.identity.name.clone(),
ranked.identity.version.clone(),
input.replay_input_key.clone(),
input_hash.clone(),
);
let ledger_identity = match ledger_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
tracing::debug!(
target: crate::TRACING_TARGET,
action = "execute_decoder",
campaign_id = %campaign_id,
signature = %input.signature,
slot = input.slot,
instruction_path = %input.instruction_path,
program_id = %input.program_id,
input_key = %input.replay_input_key,
input_hash = %input_hash,
processor_name = %ranked.identity.name,
processor_version = %ranked.identity.version,
recognition = ?ranked.recognition,
force_replay = request.force_replay,
materialize_after_decode = request.materialize_after_decode,
"execute contextual instruction decoder"
);
let mut summary = crate::DecodeProcessorSummary {
processor_name: ranked.identity.name.clone(),
processor_version: ranked.identity.version.clone(),
dispatched: 1,
skipped: 0,
decoded: 0,
ignored: 0,
unsupported: 0,
failed: 0,
processing_errors: 0,
materialized_outputs: 0,
materialization_refused: 0,
};
if !request.force_replay && !request.selection.incomplete_signatures {
tracing::debug!(target: crate::TRACING_TARGET, action = "check_decode_ledger", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, processor_name = %ranked.identity.name, processor_version = %ranked.identity.version, input_key = %ledger_identity.input_key, input_hash = %ledger_identity.input_hash, "check current contextual decode ledger identity");
let current_result = store.is_decode_current(&ledger_identity).await;
match current_result {
std::result::Result::Ok(true) => {
summary.skipped = 1;
tracing::debug!(target: crate::TRACING_TARGET, action = "skip_decoder", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, program_id = %input.program_id, processor_name = %ranked.identity.name, processor_version = %ranked.identity.version, input_key = %ledger_identity.input_key, input_hash = %ledger_identity.input_hash, reason = "same_processor_version_and_input_hash", "skip current contextual instruction decode");
return std::result::Result::Ok(summary);
},
std::result::Result::Ok(false) => {
tracing::debug!(target: crate::TRACING_TARGET, action = "check_decode_ledger", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, processor_name = %ranked.identity.name, processor_version = %ranked.identity.version, current = false, "contextual decode ledger identity is not current");
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
} else {
tracing::debug!(target: crate::TRACING_TARGET, action = "check_decode_ledger", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, processor_name = %ranked.identity.name, processor_version = %ranked.identity.version, force_replay = request.force_replay, incomplete_signatures = request.selection.incomplete_signatures, ledger_check_bypassed = true, "selected replay scope bypasses contextual decode skip check");
}
tracing::debug!(target: crate::TRACING_TARGET, action = "call_decoder", campaign_id = %campaign_id, signature = %input.signature, slot = input.slot, instruction_path = %input.instruction_path, program_id = %input.program_id, processor_name = %ranked.identity.name, processor_version = %ranked.identity.version, transaction_failed = input.transaction_failed, "call contextual instruction decoder implementation");
let decode_result = ranked.decoder.decode(input);
tracing::debug!(target: crate::TRACING_TARGET, action = "call_decoder", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, processor_name = %ranked.identity.name, processor_version = %ranked.identity.version, status = ?decode_result.status, recognized_entry_code = ?decode_result.recognized_entry_code, observation_count = decode_result.observations.len(), diagnostics = ?decode_result.diagnostics, "contextual instruction decoder returned");
if matches!(
decode_result.status,
kb_lib::DcApiDecoderOutcomeStatus::Failed | kb_lib::DcApiDecoderOutcomeStatus::Unsupported
) {
tracing::error!(
target: crate::TRACING_TARGET,
action = "decoder_outcome_failure",
campaign_id = %campaign_id,
signature = %input.signature,
slot = input.slot,
instruction_path = %input.instruction_path,
program_id = %input.program_id,
processor_name = %ranked.identity.name,
processor_version = %ranked.identity.version,
input_key = %input.replay_input_key,
status = ?decode_result.status,
recognized_entry_code = ?decode_result.recognized_entry_code,
diagnostics = ?decode_result.diagnostics,
"contextual instruction was not decoded successfully"
);
}
let validation_result = decode_result.validate();
if let std::result::Result::Err(error) = validation_result {
tracing::error!(target: crate::TRACING_TARGET, action = "validate_decoder_result", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, processor_name = %ranked.identity.name, processor_version = %ranked.identity.version, error = %error, "contextual decoder returned an invalid result");
let failure = kb_store::DecodeFailure {
ledger_identity,
signature: input.signature.clone(),
instruction_path: input.instruction_path.clone(),
error_code: "invalid_decoder_result".to_string(),
error_message: error.to_string(),
};
let persist_failure_result = store.mark_decode_failed(&failure).await;
if let std::result::Result::Err(persist_error) = persist_failure_result {
return std::result::Result::Err(persist_error);
}
return std::result::Result::Err(error);
}
let observation_inserts_result = observation_inserts(
&ranked.identity,
input,
input_hash.as_str(),
&decode_result.observations,
);
let observation_inserts = match observation_inserts_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let status = decoder_status_code(decode_result.status).to_string();
let decoded_count_result = u32::try_from(decode_result.observations.len());
let decoded_count = match decoded_count_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"decoded observation count conversion failed: {error}"
)));
},
};
let error_count_result = u32::try_from(decode_result.diagnostics.len());
let error_count = match error_count_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"decoder diagnostic count conversion failed: {error}"
)));
},
};
let coverage = kb_store::DecodeCoverageObservationInsert {
processor_name: ranked.identity.name.clone(),
processor_version: ranked.identity.version.clone(),
input_key: input.replay_input_key.clone(),
input_hash: input_hash.clone(),
signature: input.signature.clone(),
slot: input.slot,
instruction_path: input.instruction_path.clone(),
program_id: input.program_id.clone(),
surface_code: ranked.recognition.surface_code.clone(),
entry_code: decode_result
.recognized_entry_code
.clone()
.or_else(|| return ranked.recognition.entry_code.clone()),
discriminator_hex: ranked.recognition.discriminator_hex.clone(),
status: status.clone(),
recognized: ranked.recognition.compatible,
decoded_count,
materialized_count: 0,
error_count,
transaction_failed: input.transaction_failed,
};
let decode_error = if decode_result.status == kb_lib::DcApiDecoderOutcomeStatus::Failed {
decode_result.diagnostics.first()
} else {
std::option::Option::None
};
let bundle = kb_store::DecodePersistenceBundle {
ledger_identity,
signature: input.signature.clone(),
instruction_path: input.instruction_path.clone(),
status: status.clone(),
error_code: decode_error.map(|diagnostic| return diagnostic.code.clone()),
error_message: decode_error.map(|diagnostic| return diagnostic.message.clone()),
observations: observation_inserts,
coverage,
};
tracing::debug!(target: crate::TRACING_TARGET, action = "persist_decode_result", campaign_id = %campaign_id, signature = %input.signature, slot = input.slot, instruction_path = %input.instruction_path, program_id = %input.program_id, processor_name = %ranked.identity.name, processor_version = %ranked.identity.version, input_key = %bundle.ledger_identity.input_key, input_hash = %bundle.ledger_identity.input_hash, status = %bundle.status, observation_count = bundle.observations.len(), force_replay = request.force_replay, "persist contextual decode result");
let persist_result = store.persist_decode_result(&bundle, request.force_replay).await;
let persist_outcome = match persist_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "persist_decode_result", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, processor_name = %ranked.identity.name, processor_version = %ranked.identity.version, error = %error, "contextual decode result persistence failed");
return std::result::Result::Err(error);
},
};
tracing::debug!(target: crate::TRACING_TARGET, action = "persist_decode_result", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, processor_name = %ranked.identity.name, processor_version = %ranked.identity.version, outcome = ?persist_outcome, "contextual decode result persisted");
match decode_result.status {
kb_lib::DcApiDecoderOutcomeStatus::Decoded => summary.decoded = 1,
kb_lib::DcApiDecoderOutcomeStatus::Ignored => summary.ignored = 1,
kb_lib::DcApiDecoderOutcomeStatus::Unsupported => summary.unsupported = 1,
kb_lib::DcApiDecoderOutcomeStatus::Failed => summary.failed = 1,
}
if request.materialize_after_decode
&& decode_result.status == kb_lib::DcApiDecoderOutcomeStatus::Decoded
{
tracing::debug!(target: crate::TRACING_TARGET, action = "materialize_after_decode", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, processor_name = %ranked.identity.name, processor_version = %ranked.identity.version, observation_count = decode_result.observations.len(), materializer_count = materializers.len(), "run materializers after contextual decode");
for observation in &decode_result.observations {
let materialization_result = execute_materializers(
store,
request.force_replay,
campaign_id,
&ranked.identity,
input,
observation,
materializers,
)
.await;
let materialization = match materialization_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
summary.materialized_outputs += materialization.0;
summary.materialization_refused += materialization.1;
}
} else {
tracing::debug!(target: crate::TRACING_TARGET, action = "materialize_after_decode", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, processor_name = %ranked.identity.name, processor_version = %ranked.identity.version, requested = request.materialize_after_decode, decode_status = ?decode_result.status, executed = false, "materialization not executed for contextual decode result");
}
tracing::debug!(target: crate::TRACING_TARGET, action = "execute_decoder", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, processor_name = %ranked.identity.name, processor_version = %ranked.identity.version, summary = ?summary, "contextual instruction decoder execution completed");
return std::result::Result::Ok(summary);
}
async fn execute_materializers<S>(
store: &S,
force_replay: bool,
campaign_id: &str,
decoder_identity: &kb_lib::DcApiDecoderIdentity,
input: &kb_lib::MdCoreInstructionReplayInput,
observation: &kb_lib::DcApiDecodedObservation,
materializers: &[std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>],
) -> kb_core::Result<(u64, u64)>
where
S: kb_store::DecodePipelineStore + Sync,
{
tracing::debug!(target: crate::TRACING_TARGET, action = "execute_materializers", campaign_id = %campaign_id, signature = %input.signature, slot = input.slot, instruction_path = %input.instruction_path, program_id = %input.program_id, source_decoder_name = %decoder_identity.name, source_decoder_version = %decoder_identity.version, event_key = %observation.event_key, event_family = ?observation.event.event_family, materializer_count = materializers.len(), force_replay, "evaluate materializers for decoded observation");
let mut output_count = 0_u64;
let mut refusal_count = 0_u64;
for materializer in materializers {
let identity = materializer.identity();
if !kb_lib::materializer_api_accepts_observation(materializer.as_ref(), observation) {
tracing::debug!(target: crate::TRACING_TARGET, action = "select_materializer", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, event_key = %observation.event_key, materializer_name = %identity.name, materializer_version = %identity.version, accepted = false, event_family = ?observation.event.event_family, surface_code = %observation.event.surface_code.0.as_str(), entry_code = %observation.event.event_name.0.as_str(), "materializer does not accept decoded observation");
continue;
}
tracing::debug!(target: crate::TRACING_TARGET, action = "select_materializer", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, event_key = %observation.event_key, materializer_name = %identity.name, materializer_version = %identity.version, accepted = true, event_family = ?observation.event.event_family, surface_code = %observation.event.surface_code.0.as_str(), entry_code = %observation.event.event_name.0.as_str(), "materializer accepts decoded observation");
let materializer_input_key =
materializer_input_key(input, decoder_identity, observation.event_key.as_str());
let serialize_result = serde_json::to_value(observation);
let mut observation_json = match serialize_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::json(format!(
"cannot serialize decoded observation for materialization hash: {error}"
)));
},
};
let hash_result = kb_lib::decoder_api_deterministic_json_hash(&mut observation_json);
let input_hash = match hash_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let ledger_result = kb_store::ProcessingLedgerIdentity::new(
crate::EVENT_MATERIALIZATION_STAGE,
identity.name.clone(),
identity.version.clone(),
materializer_input_key.clone(),
input_hash.clone(),
);
let ledger_identity = match ledger_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !force_replay {
tracing::debug!(target: crate::TRACING_TARGET, action = "check_materialization_ledger", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, event_key = %observation.event_key, materializer_name = %identity.name, materializer_version = %identity.version, input_key = %ledger_identity.input_key, input_hash = %ledger_identity.input_hash, "check current materialization ledger identity");
let current_result = store.is_decode_current(&ledger_identity).await;
match current_result {
std::result::Result::Ok(true) => {
tracing::debug!(target: crate::TRACING_TARGET, action = "skip_materializer", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, event_key = %observation.event_key, materializer_name = %identity.name, materializer_version = %identity.version, reason = "same_processor_version_and_input_hash", "skip current materialization");
continue;
},
std::result::Result::Ok(false) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
} else {
tracing::debug!(target: crate::TRACING_TARGET, action = "check_materialization_ledger", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, event_key = %observation.event_key, materializer_name = %identity.name, materializer_version = %identity.version, force_replay = true, ledger_check_bypassed = true, "force replay bypasses materialization skip check");
}
let policy_result = kb_lib::materializer_api_validate_materialization_policy(
materializer.as_ref(),
observation,
);
let mut execution = match policy_result {
std::result::Result::Ok(()) => materializer.materialize(observation),
std::result::Result::Err(refusal) => refusal,
};
let output_policy_result =
kb_lib::materializer_api_validate_materialized_output_policy(observation, &execution);
if let std::result::Result::Err(refusal) = output_policy_result {
execution = refusal;
}
if execution.status == kb_lib::MtApiMaterializerOutcomeStatus::Refused {
refusal_count += 1;
}
tracing::debug!(target: crate::TRACING_TARGET, action = "call_materializer", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, event_key = %observation.event_key, materializer_name = %identity.name, materializer_version = %identity.version, status = ?execution.status, output_count = execution.outputs.len(), diagnostics = ?execution.diagnostics, "materializer returned");
if execution.status == kb_lib::MtApiMaterializerOutcomeStatus::Failed {
tracing::error!(
target: crate::TRACING_TARGET,
action = "materializer_outcome_failure",
campaign_id = %campaign_id,
signature = %input.signature,
slot = input.slot,
instruction_path = %input.instruction_path,
program_id = %input.program_id,
event_key = %observation.event_key,
materializer_name = %identity.name,
materializer_version = %identity.version,
surface_code = %observation.event.surface_code.0.as_str(),
entry_code = %observation.event.event_name.0.as_str(),
diagnostics = ?execution.diagnostics,
"decoded observation materialization failed"
);
}
let validation_result = execution.validate();
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let mut outputs = std::vec::Vec::with_capacity(execution.outputs.len());
for output in &execution.outputs {
outputs.push(kb_store::MaterializedOutputInsert {
processor_name: identity.name.clone(),
processor_version: identity.version.clone(),
input_key: materializer_input_key.clone(),
input_hash: input_hash.clone(),
output_key: output.output_key.clone(),
source_event_key: observation.event_key.clone(),
signature: input.signature.clone(),
slot: input.slot,
materialized_family: materialized_family_code(output.family).to_string(),
payload_json: output.payload_json.clone(),
});
}
let materialization_error =
if execution.status == kb_lib::MtApiMaterializerOutcomeStatus::Failed {
execution.diagnostics.first()
} else {
std::option::Option::None
};
let bundle = kb_store::MaterializationPersistenceBundle {
ledger_identity,
source_decoder_name: decoder_identity.name.clone(),
source_decoder_version: decoder_identity.version.clone(),
source_decode_input_key: input.replay_input_key.clone(),
signature: input.signature.clone(),
instruction_path: input.instruction_path.clone(),
status: materializer_status_code(execution.status).to_string(),
error_code: materialization_error.map(|diagnostic| return diagnostic.code.clone()),
error_message: materialization_error
.map(|diagnostic| return diagnostic.message.clone()),
outputs,
};
tracing::debug!(target: crate::TRACING_TARGET, action = "persist_materialization_result", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, event_key = %observation.event_key, materializer_name = %identity.name, materializer_version = %identity.version, input_key = %bundle.ledger_identity.input_key, input_hash = %bundle.ledger_identity.input_hash, status = %bundle.status, output_count = bundle.outputs.len(), force_replay, "persist materialization result");
let persist_result = store.persist_materialization_result(&bundle, force_replay).await;
let persist_outcome = match persist_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "persist_materialization_result", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, event_key = %observation.event_key, materializer_name = %identity.name, materializer_version = %identity.version, error = %error, "materialization result persistence failed");
return std::result::Result::Err(error);
},
};
tracing::debug!(target: crate::TRACING_TARGET, action = "persist_materialization_result", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, event_key = %observation.event_key, materializer_name = %identity.name, materializer_version = %identity.version, outcome = ?persist_outcome, "materialization result persisted");
let count_result = u64::try_from(execution.outputs.len());
let count = match count_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"materialized output count conversion failed: {error}"
)));
},
};
output_count += count;
}
tracing::debug!(target: crate::TRACING_TARGET, action = "execute_materializers", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, event_key = %observation.event_key, output_count, refusal_count, "materializer evaluation completed");
return std::result::Result::Ok((output_count, refusal_count));
}
fn materializer_input_key(
input: &kb_lib::MdCoreInstructionReplayInput,
decoder_identity: &kb_lib::DcApiDecoderIdentity,
event_key: &str,
) -> std::string::String {
return format!(
"{}:{}:{}:{}",
input.replay_input_key, decoder_identity.name, decoder_identity.version, event_key
);
}
fn validate_decoder_registry(
decoders: &[std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>],
) -> kb_core::Result<()> {
let mut identities = std::collections::BTreeSet::new();
for decoder in decoders {
let identity = decoder.identity();
if identity.name.trim().is_empty() || identity.version.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"decoder registry contains an empty processor identity",
));
}
if !identities.insert((identity.name, identity.version)) {
return std::result::Result::Err(kb_core::Error::config(
"decoder registry contains a duplicate processor identity",
));
}
}
return std::result::Result::Ok(());
}
fn ordered_materializers(
materializers: &[std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>],
) -> kb_core::Result<std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>>> {
let mut output = materializers.to_vec();
output.sort_by(|left, right| {
let left_identity = left.identity();
let right_identity = right.identity();
return left_identity
.name
.cmp(&right_identity.name)
.then(left_identity.version.cmp(&right_identity.version));
});
let mut previous = std::option::Option::None;
for materializer in &output {
let identity = materializer.identity();
if identity.name.trim().is_empty() || identity.version.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"materializer registry contains an empty processor identity",
));
}
let key = (identity.name, identity.version);
if previous.as_ref() == std::option::Option::Some(&key) {
return std::result::Result::Err(kb_core::Error::config(
"materializer registry contains a duplicate processor identity",
));
}
previous = std::option::Option::Some(key);
}
return std::result::Result::Ok(output);
}
/// Creates one stable process-local contextual decode campaign identifier.
pub fn new_decode_campaign_id() -> std::string::String {
let sequence = DECODE_CAMPAIGN_SEQUENCE.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return format!("decode-{}-{sequence}", chrono::Utc::now().timestamp_micros());
}
fn text_sample(values: &[std::string::String], limit: usize) -> std::vec::Vec<&str> {
return values.iter().take(limit).map(std::string::String::as_str).collect();
}
fn effective_selection(
request: &crate::DecodeReplayRequest,
decoders: &[std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>],
) -> kb_core::Result<kb_store::DecodeSelectionFilter> {
let mut supported_program_ids = std::collections::BTreeSet::<std::string::String>::new();
for decoder in decoders {
for surface in decoder.surfaces() {
supported_program_ids.insert(surface.program_id.to_string());
}
}
if supported_program_ids.is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"enabled decoders declare no exact program ids",
));
}
let effective_program_ids = if request.selection.program_ids.is_empty() {
supported_program_ids.iter().cloned().collect::<std::vec::Vec<_>>()
} else {
let mut requested = std::collections::BTreeSet::<std::string::String>::new();
let mut unsupported = std::vec::Vec::<std::string::String>::new();
for program_id in &request.selection.program_ids {
if supported_program_ids.contains(program_id) {
requested.insert(program_id.clone());
} else {
unsupported.push(program_id.clone());
}
}
if !unsupported.is_empty() {
unsupported.sort();
return std::result::Result::Err(kb_core::Error::config(format!(
"selected program ids are not supported by enabled decoders: {}",
unsupported.join(", ")
)));
}
requested.into_iter().collect::<std::vec::Vec<_>>()
};
let bypass_processing_states = request.force_replay
&& (!request.selection.signatures.is_empty() || request.force_replay_all_matching);
let processing_states = if bypass_processing_states {
std::vec::Vec::new()
} else {
request.selection.processing_states.clone()
};
return kb_store::DecodeSelectionFilter::new(
request.selection.signatures.clone(),
processing_states,
request.selection.min_slot,
request.selection.max_slot,
effective_program_ids,
request.selection.instruction_paths.clone(),
request.selection.incomplete_signatures,
request.selection.limit,
);
}
fn enabled_decoders(
request: &crate::DecodeReplayRequest,
decoders: &[std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>],
) -> std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> {
let mut output = decoders
.iter()
.filter(|decoder| {
if request.decoder_names.is_empty() {
return true;
}
let identity = decoder.identity();
return request.decoder_names.iter().any(|name| return *name == identity.name);
})
.cloned()
.collect::<std::vec::Vec<_>>();
output.sort_by(|left, right| {
let left_identity = left.identity();
let right_identity = right.identity();
return left_identity
.name
.cmp(&right_identity.name)
.then(left_identity.version.cmp(&right_identity.version));
});
return output;
}
fn rank_decoders<'decoder>(
campaign_id: &str,
input: &kb_lib::MdCoreInstructionReplayInput,
decoders: &'decoder [std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>],
) -> std::vec::Vec<RankedDecoder<'decoder>> {
let mut output = std::vec::Vec::new();
for decoder in decoders {
let identity = decoder.identity();
if !kb_lib::decoder_api_decoder_handles_program_id(
decoder.as_ref(),
input.program_id.as_str(),
) {
tracing::debug!(target: crate::TRACING_TARGET, action = "recognize_input", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, program_id = %input.program_id, processor_name = %identity.name, processor_version = %identity.version, handles_program_id = false, recognize_called = false, "decoder rejected by exact program id before recognition");
continue;
}
tracing::debug!(target: crate::TRACING_TARGET, action = "recognize_input", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, program_id = %input.program_id, processor_name = %identity.name, processor_version = %identity.version, handles_program_id = true, recognize_called = true, "call decoder recognition");
let recognition = decoder.recognize(input);
if !recognition.compatible {
tracing::debug!(target: crate::TRACING_TARGET, action = "recognize_input", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, program_id = %input.program_id, processor_name = %identity.name, processor_version = %identity.version, recognition = ?recognition, compatible = false, "decoder recognition rejected contextual input");
continue;
}
tracing::debug!(target: crate::TRACING_TARGET, action = "recognize_input", campaign_id = %campaign_id, signature = %input.signature, instruction_path = %input.instruction_path, program_id = %input.program_id, processor_name = %identity.name, processor_version = %identity.version, recognition = ?recognition, compatible = true, "decoder recognition accepted contextual input");
output.push(RankedDecoder {
decoder: decoder.as_ref(),
identity,
recognition,
});
}
output.sort_by(|left, right| {
return right
.recognition
.exact
.cmp(&left.recognition.exact)
.then(right.recognition.priority.cmp(&left.recognition.priority))
.then(left.identity.name.cmp(&right.identity.name))
.then(left.identity.version.cmp(&right.identity.version));
});
return output;
}
fn coverage_declaration_inserts(
decoder: &dyn kb_lib::DcApiInstructionDecoder,
) -> std::vec::Vec<kb_store::DecodeCoverageDeclarationInsert> {
let identity = decoder.identity();
return decoder
.coverage()
.into_iter()
.map(|entry| {
return kb_store::DecodeCoverageDeclarationInsert {
processor_name: identity.name.clone(),
processor_version: identity.version.clone(),
program_id: entry.program_id,
surface_code: entry.surface_code,
entry_kind: coverage_entry_kind_code(entry.entry_kind).to_string(),
entry_code: entry.entry_code,
discriminator_hex: entry.discriminator_hex,
historical: entry.historical,
};
})
.collect();
}
fn observation_inserts(
identity: &kb_lib::DcApiDecoderIdentity,
input: &kb_lib::MdCoreInstructionReplayInput,
input_hash: &str,
observations: &[kb_lib::DcApiDecodedObservation],
) -> kb_core::Result<std::vec::Vec<kb_store::DecodeObservationInsert>> {
let mut output = std::vec::Vec::with_capacity(observations.len());
for observation in observations {
if observation.event.signature.0 != input.signature
|| observation.event.slot.0 != input.slot
|| observation.event.instruction_path.0 != input.instruction_path
|| observation.event.program_id.0 != input.program_id
|| observation.transaction_failed != input.transaction_failed
|| observation.transaction_error != input.transaction_err_json
{
return std::result::Result::Err(kb_core::Error::invalid_state(
"decoded observation context does not match contextual input",
));
}
let proof_result = serde_json::to_value(&observation.proof);
let proof_json = match proof_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::json(format!(
"cannot serialize decoder proof: {error}"
)));
},
};
output.push(kb_store::DecodeObservationInsert {
processor_name: identity.name.clone(),
processor_version: identity.version.clone(),
input_key: input.replay_input_key.clone(),
input_hash: input_hash.to_string(),
event_key: observation.event_key.clone(),
signature: observation.event.signature.0.clone(),
slot: observation.event.slot.0,
instruction_path: observation.event.instruction_path.0.clone(),
program_id: observation.event.program_id.0.clone(),
protocol_code: observation.event.protocol_code.0.clone(),
surface_code: observation.event.surface_code.0.clone(),
event_code: observation.event.event_code.0.clone(),
event_name: observation.event.event_name.0.clone(),
event_family: event_family_code(observation.event.event_family).to_string(),
source_kind: event_source_kind_code(observation.event.source_kind).to_string(),
confidence: decoder_confidence_code(observation.event.confidence).to_string(),
proof_kind: proof_kind_code(observation.proof.kind).to_string(),
proof_json,
payload_json: observation.payload_json.clone(),
transaction_failed: observation.transaction_failed,
transaction_error: observation.transaction_error.clone(),
observation_committed: observation.observation_committed,
});
}
return std::result::Result::Ok(output);
}
fn merge_processor_summary(
summaries: &mut std::vec::Vec<crate::DecodeProcessorSummary>,
value: crate::DecodeProcessorSummary,
) {
for summary in summaries.iter_mut() {
if summary.processor_name == value.processor_name
&& summary.processor_version == value.processor_version
{
summary.dispatched += value.dispatched;
summary.skipped += value.skipped;
summary.decoded += value.decoded;
summary.ignored += value.ignored;
summary.unsupported += value.unsupported;
summary.failed += value.failed;
summary.processing_errors += value.processing_errors;
summary.materialized_outputs += value.materialized_outputs;
summary.materialization_refused += value.materialization_refused;
return;
}
}
summaries.push(value);
}
fn decoder_status_code(status: kb_lib::DcApiDecoderOutcomeStatus) -> &'static str {
return match status {
kb_lib::DcApiDecoderOutcomeStatus::Decoded => "decoded",
kb_lib::DcApiDecoderOutcomeStatus::Ignored => "ignored",
kb_lib::DcApiDecoderOutcomeStatus::Unsupported => "unsupported",
kb_lib::DcApiDecoderOutcomeStatus::Failed => "failed",
};
}
fn materializer_status_code(status: kb_lib::MtApiMaterializerOutcomeStatus) -> &'static str {
return match status {
kb_lib::MtApiMaterializerOutcomeStatus::Inserted => "inserted",
kb_lib::MtApiMaterializerOutcomeStatus::Replaced => "replaced",
kb_lib::MtApiMaterializerOutcomeStatus::Ignored => "ignored",
kb_lib::MtApiMaterializerOutcomeStatus::Refused => "refused",
kb_lib::MtApiMaterializerOutcomeStatus::Failed => "failed",
};
}
fn coverage_entry_kind_code(kind: kb_lib::DcApiDecoderCoverageEntryKind) -> &'static str {
return match kind {
kb_lib::DcApiDecoderCoverageEntryKind::Instruction => "instruction",
kb_lib::DcApiDecoderCoverageEntryKind::Event => "event",
kb_lib::DcApiDecoderCoverageEntryKind::Discriminator => "discriminator",
};
}
fn proof_kind_code(kind: kb_lib::DcApiDecoderProofKind) -> &'static str {
return match kind {
kb_lib::DcApiDecoderProofKind::ExactDiscriminator => "exact_discriminator",
kb_lib::DcApiDecoderProofKind::ExactLayout => "exact_layout",
kb_lib::DcApiDecoderProofKind::Idl => "idl",
kb_lib::DcApiDecoderProofKind::Manual => "manual",
kb_lib::DcApiDecoderProofKind::LogCorrelation => "log_correlation",
kb_lib::DcApiDecoderProofKind::BalanceDelta => "balance_delta",
kb_lib::DcApiDecoderProofKind::Heuristic => "heuristic",
kb_lib::DcApiDecoderProofKind::Audit => "audit",
kb_lib::DcApiDecoderProofKind::Unknown => "unknown",
};
}
fn decoder_confidence_code(confidence: kb_lib::MdDecoderConfidence) -> &'static str {
return match confidence {
kb_lib::MdDecoderConfidence::Exact => "exact",
kb_lib::MdDecoderConfidence::IdlExact => "idl_exact",
kb_lib::MdDecoderConfidence::ManualExact => "manual_exact",
kb_lib::MdDecoderConfidence::Inferred => "inferred",
kb_lib::MdDecoderConfidence::Unsafe => "unsafe",
kb_lib::MdDecoderConfidence::AuditOnly => "audit_only",
kb_lib::MdDecoderConfidence::Unknown => "unknown",
};
}
fn event_source_kind_code(kind: kb_lib::MdEventSourceKind) -> &'static str {
return match kind {
kb_lib::MdEventSourceKind::Instruction => "instruction",
kb_lib::MdEventSourceKind::InnerInstruction => "inner_instruction",
kb_lib::MdEventSourceKind::Log => "log",
kb_lib::MdEventSourceKind::AnchorEvent => "anchor_event",
kb_lib::MdEventSourceKind::AnchorSelfCpiEvent => "anchor_self_cpi_event",
kb_lib::MdEventSourceKind::BalanceDelta => "balance_delta",
kb_lib::MdEventSourceKind::Synthetic => "synthetic",
kb_lib::MdEventSourceKind::Inferred => "inferred",
kb_lib::MdEventSourceKind::Audit => "audit",
};
}
fn event_family_code(family: kb_lib::MdEventFamily) -> &'static str {
return match family {
kb_lib::MdEventFamily::Trade => "trade",
kb_lib::MdEventFamily::Liquidity => "liquidity",
kb_lib::MdEventFamily::Lifecycle => "lifecycle",
kb_lib::MdEventFamily::Fee => "fee",
kb_lib::MdEventFamily::Admin => "admin",
kb_lib::MdEventFamily::Reward => "reward",
kb_lib::MdEventFamily::Orderbook => "orderbook",
kb_lib::MdEventFamily::TokenAccount => "token_account",
kb_lib::MdEventFamily::TokenMint => "token_mint",
kb_lib::MdEventFamily::TokenBurn => "token_burn",
kb_lib::MdEventFamily::Nft => "nft",
kb_lib::MdEventFamily::Metadata => "metadata",
kb_lib::MdEventFamily::Oracle => "oracle",
kb_lib::MdEventFamily::Lending => "lending",
kb_lib::MdEventFamily::Staking => "staking",
kb_lib::MdEventFamily::Governance => "governance",
kb_lib::MdEventFamily::Bridge => "bridge",
kb_lib::MdEventFamily::Perpetuals => "perpetuals",
kb_lib::MdEventFamily::Vault => "vault",
kb_lib::MdEventFamily::Routing => "routing",
kb_lib::MdEventFamily::ComplianceAudit => "compliance_audit",
kb_lib::MdEventFamily::TokenMetadataRisk => "token_metadata_risk",
kb_lib::MdEventFamily::Risk => "risk",
kb_lib::MdEventFamily::Audit => "audit",
kb_lib::MdEventFamily::Unknown => "unknown",
};
}
fn materialized_family_code(family: kb_lib::MdMaterializedEventFamily) -> &'static str {
return match family {
kb_lib::MdMaterializedEventFamily::Trade => "trade",
kb_lib::MdMaterializedEventFamily::Liquidity => "liquidity",
kb_lib::MdMaterializedEventFamily::Lifecycle => "lifecycle",
kb_lib::MdMaterializedEventFamily::Fee => "fee",
kb_lib::MdMaterializedEventFamily::Admin => "admin",
kb_lib::MdMaterializedEventFamily::TokenAccount => "token_account",
kb_lib::MdMaterializedEventFamily::PoolState => "pool_state",
kb_lib::MdMaterializedEventFamily::Orderbook => "orderbook",
kb_lib::MdMaterializedEventFamily::Reward => "reward",
kb_lib::MdMaterializedEventFamily::Nft => "nft",
kb_lib::MdMaterializedEventFamily::Metadata => "metadata",
kb_lib::MdMaterializedEventFamily::Oracle => "oracle",
kb_lib::MdMaterializedEventFamily::Lending => "lending",
kb_lib::MdMaterializedEventFamily::Staking => "staking",
kb_lib::MdMaterializedEventFamily::Governance => "governance",
kb_lib::MdMaterializedEventFamily::Bridge => "bridge",
kb_lib::MdMaterializedEventFamily::Perpetuals => "perpetuals",
kb_lib::MdMaterializedEventFamily::Vault => "vault",
kb_lib::MdMaterializedEventFamily::Routing => "routing",
kb_lib::MdMaterializedEventFamily::ComplianceAudit => "compliance_audit",
kb_lib::MdMaterializedEventFamily::TokenMetadataRisk => "token_metadata_risk",
kb_lib::MdMaterializedEventFamily::Risk => "risk",
kb_lib::MdMaterializedEventFamily::TransactionAnnotation => "transaction_annotation",
kb_lib::MdMaterializedEventFamily::Unknown => "unknown",
};
}
#[cfg(test)]
mod tests {
fn lock_or_panic<T>(mutex: &std::sync::Mutex<T>) -> std::sync::MutexGuard<'_, T> {
return match mutex.lock() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("test mutex is poisoned: {error}"),
};
}
fn result_or_panic<T, E>(result: std::result::Result<T, E>) -> T
where
E: std::fmt::Display,
{
return match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("test operation failed: {error}"),
};
}
static SURFACES: &[kb_lib::DcApiDecoderSurface] = &[kb_lib::DcApiDecoderSurface {
program_id: "program_a",
surface_code: "surface_a",
priority: 10,
}];
struct TestDecoder {
version: &'static str,
exact: bool,
priority: u16,
}
impl kb_lib::DcApiInstructionDecoder for TestDecoder {
fn identity(&self) -> kb_lib::DcApiDecoderIdentity {
return kb_lib::DcApiDecoderIdentity {
name: "test_decoder".to_string(),
version: self.version.to_string(),
};
}
fn surfaces(&self) -> &'static [kb_lib::DcApiDecoderSurface] {
return SURFACES;
}
fn coverage(&self) -> std::vec::Vec<kb_lib::DcApiDecoderCoverageDeclaration> {
return std::vec![kb_lib::DcApiDecoderCoverageDeclaration {
program_id: "program_a".to_string(),
surface_code: std::option::Option::Some("surface_a".to_string()),
entry_kind: kb_lib::DcApiDecoderCoverageEntryKind::Instruction,
entry_code: "attempt".to_string(),
discriminator_hex: std::option::Option::None,
historical: false,
}];
}
fn recognize(
&self,
input: &kb_lib::MdCoreInstructionReplayInput,
) -> kb_lib::DcApiDecoderRecognition {
if input.program_id != "program_a" {
return kb_lib::DcApiDecoderRecognition::incompatible();
}
return kb_lib::DcApiDecoderRecognition::compatible(
self.exact,
self.priority,
std::option::Option::Some("surface_a".to_string()),
std::option::Option::Some("attempt".to_string()),
kb_lib::decoder_api_discriminator_8_hex(input),
);
}
fn decode(
&self,
input: &kb_lib::MdCoreInstructionReplayInput,
) -> kb_lib::DcApiDecoderExecutionResult {
let observation = kb_lib::DcApiDecodedObservation {
event_key: "attempt".to_string(),
event: kb_lib::MdDecodedProtocolEvent {
signature: kb_lib::MdSignature(input.signature.clone()),
slot: kb_lib::MdSlot(input.slot),
instruction_path: kb_lib::MdInstructionPath(input.instruction_path.clone()),
program_id: kb_lib::MdProgramId(input.program_id.clone()),
protocol_code: kb_lib::MdProtocolCode("test".to_string()),
surface_code: kb_lib::MdSurfaceCode("surface_a".to_string()),
event_code: kb_lib::MdEventCode("attempt".to_string()),
event_name: kb_lib::MdEventName("attempt".to_string()),
event_family: kb_lib::MdEventFamily::Audit,
source_kind: kb_lib::MdEventSourceKind::Instruction,
confidence: kb_lib::MdDecoderConfidence::Exact,
},
payload_json: serde_json::json!({"attempted": true}),
transaction_failed: input.transaction_failed,
transaction_error: input.transaction_err_json.clone(),
observation_committed: !input.transaction_failed,
proof: kb_lib::DcApiDecoderProof {
kind: kb_lib::DcApiDecoderProofKind::ExactLayout,
confidence: kb_lib::MdDecoderConfidence::Exact,
evidence: std::vec!["test".to_string()],
},
};
return kb_lib::DcApiDecoderExecutionResult {
status: kb_lib::DcApiDecoderOutcomeStatus::Decoded,
recognized_entry_code: std::option::Option::Some("attempt".to_string()),
observations: std::vec![observation],
diagnostics: std::vec::Vec::new(),
};
}
}
struct SelectiveAuditMaterializer;
impl kb_lib::MtApiEventMaterializer for SelectiveAuditMaterializer {
fn identity(&self) -> kb_lib::MtApiMaterializerIdentity {
return kb_lib::MtApiMaterializerIdentity {
name: "selective_audit_materializer".to_string(),
version: "1".to_string(),
};
}
fn accepted_families(&self) -> &'static [kb_lib::MdEventFamily] {
return &[kb_lib::MdEventFamily::Audit];
}
fn accepts_observation(&self, observation: &kb_lib::DcApiDecodedObservation) -> bool {
return observation.event.surface_code.0.as_str() == "accepted_surface";
}
fn transaction_policy(
&self,
_family: kb_lib::MdEventFamily,
) -> kb_lib::MtApiMaterializationTransactionPolicy {
return kb_lib::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly;
}
fn materialize(
&self,
_observation: &kb_lib::DcApiDecodedObservation,
) -> kb_lib::MtApiMaterializerExecutionResult {
return kb_lib::MtApiMaterializerExecutionResult {
status: kb_lib::MtApiMaterializerOutcomeStatus::Failed,
outputs: std::vec::Vec::new(),
diagnostics: std::vec![kb_lib::MtApiMaterializerDiagnostic {
code: "unexpected_materializer_call".to_string(),
message:
"selective materializer must not be called for an unaccepted observation"
.to_string(),
retriable: false,
}],
};
}
}
struct TestObserver {
cancelled: std::sync::atomic::AtomicBool,
}
impl crate::DecodeReplayObserver for TestObserver {
fn on_progress(&self, _event: &crate::DecodeReplayProgressEvent) {}
fn is_cancelled(&self) -> bool {
return self.cancelled.load(std::sync::atomic::Ordering::SeqCst);
}
}
#[derive(Default)]
struct TestStore {
inputs: std::sync::Mutex<std::vec::Vec<kb_lib::MdCoreInstructionReplayInput>>,
filters: std::sync::Mutex<std::vec::Vec<kb_store::DecodeSelectionFilter>>,
current: std::sync::Mutex<std::collections::BTreeSet<std::string::String>>,
decode_bundles: std::sync::Mutex<std::vec::Vec<kb_store::DecodePersistenceBundle>>,
materialization_bundles:
std::sync::Mutex<std::vec::Vec<kb_store::MaterializationPersistenceBundle>>,
declarations: std::sync::Mutex<std::vec::Vec<kb_store::DecodeCoverageDeclarationInsert>>,
}
#[async_trait::async_trait]
impl kb_store::DecodePipelineStore for TestStore {
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn list_decode_inputs(
&self,
filter: &kb_store::DecodeSelectionFilter,
) -> kb_core::Result<std::vec::Vec<kb_lib::MdCoreInstructionReplayInput>> {
super::tests::lock_or_panic(&self.filters).push(filter.clone());
let limit_result = usize::try_from(filter.limit);
let limit = match limit_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_error) => usize::MAX,
};
let inputs = super::tests::lock_or_panic(&self.inputs)
.iter()
.filter(|input| {
return (filter.signatures.is_empty()
|| filter.signatures.contains(&input.signature))
&& (filter.program_ids.is_empty()
|| filter.program_ids.contains(&input.program_id))
&& (filter.instruction_paths.is_empty()
|| filter.instruction_paths.contains(&input.instruction_path));
})
.take(limit)
.cloned()
.collect::<std::vec::Vec<_>>();
return std::result::Result::Ok(inputs);
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn is_decode_current(
&self,
identity: &kb_store::ProcessingLedgerIdentity,
) -> kb_core::Result<bool> {
let key = format!(
"{}:{}:{}:{}:{}",
identity.stage,
identity.processor_name,
identity.processor_version,
identity.input_key,
identity.input_hash
);
return std::result::Result::Ok(
super::tests::lock_or_panic(&self.current).contains(&key),
);
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn persist_decode_coverage_declarations(
&self,
declarations: &[kb_store::DecodeCoverageDeclarationInsert],
) -> kb_core::Result<kb_store::InsertOutcome> {
super::tests::lock_or_panic(&self.declarations).extend_from_slice(declarations);
return std::result::Result::Ok(kb_store::InsertOutcome::new(
declarations.len() as u64,
0,
0,
));
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn persist_decode_result(
&self,
bundle: &kb_store::DecodePersistenceBundle,
force_replay: bool,
) -> kb_core::Result<kb_store::InsertOutcome> {
let key = format!(
"{}:{}:{}:{}:{}",
bundle.ledger_identity.stage,
bundle.ledger_identity.processor_name,
bundle.ledger_identity.processor_version,
bundle.ledger_identity.input_key,
bundle.ledger_identity.input_hash
);
super::tests::lock_or_panic(&self.current).insert(key);
let mut bundles = super::tests::lock_or_panic(&self.decode_bundles);
if force_replay {
bundles.retain(|existing| {
return existing.ledger_identity.processor_name
!= bundle.ledger_identity.processor_name
|| existing.ledger_identity.processor_version
!= bundle.ledger_identity.processor_version
|| existing.ledger_identity.input_key != bundle.ledger_identity.input_key;
});
}
bundles.push(bundle.clone());
return std::result::Result::Ok(kb_store::InsertOutcome::new(1, 1, 0));
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn mark_decode_failed(
&self,
_failure: &kb_store::DecodeFailure,
) -> kb_core::Result<kb_store::InsertOutcome> {
return std::result::Result::Ok(kb_store::InsertOutcome::new(0, 1, 0));
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn persist_materialization_result(
&self,
bundle: &kb_store::MaterializationPersistenceBundle,
force_replay: bool,
) -> kb_core::Result<kb_store::InsertOutcome> {
let key = format!(
"{}:{}:{}:{}:{}",
bundle.ledger_identity.stage,
bundle.ledger_identity.processor_name,
bundle.ledger_identity.processor_version,
bundle.ledger_identity.input_key,
bundle.ledger_identity.input_hash
);
super::tests::lock_or_panic(&self.current).insert(key);
let mut bundles = super::tests::lock_or_panic(&self.materialization_bundles);
if force_replay {
bundles.retain(|existing| {
return existing.ledger_identity.processor_name
!= bundle.ledger_identity.processor_name
|| existing.ledger_identity.processor_version
!= bundle.ledger_identity.processor_version
|| existing.ledger_identity.input_key != bundle.ledger_identity.input_key;
});
}
bundles.push(bundle.clone());
let count_result = u64::try_from(bundle.outputs.len());
let count = match count_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"test materialization output count conversion failed: {error}"
)));
},
};
return std::result::Result::Ok(kb_store::InsertOutcome::new(count, 1, 0));
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn list_decode_coverage_summary(
&self,
_processor_name: std::option::Option<&str>,
_processor_version: std::option::Option<&str>,
_limit: u32,
) -> kb_core::Result<std::vec::Vec<kb_store::DecodeCoverageSummaryRow>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn list_materialized_events(
&self,
_filter: &kb_store::MaterializedEventFilter,
) -> kb_core::Result<std::vec::Vec<kb_store::MaterializedEventQueryRow>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
fn replay_input(program_id: &str, failed: bool) -> kb_lib::MdCoreInstructionReplayInput {
let result = kb_lib::MdCoreInstructionReplayInput::new(
"signature:0",
"signature",
42,
"0",
program_id,
failed,
if failed {
std::option::Option::Some(serde_json::json!({"InstructionError": [0, "Custom"]}))
} else {
std::option::Option::None
},
serde_json::json!([]),
serde_json::json!([]),
std::option::Option::Some(serde_json::json!({"dataBase64": "AQIDBAUGBwg="})),
std::option::Option::Some("payload-hash".to_string()),
serde_json::json!([]),
serde_json::json!([]),
serde_json::json!([]),
serde_json::json!([]),
);
return super::tests::result_or_panic(result);
}
fn request(force_replay: bool) -> crate::DecodeReplayRequest {
return crate::DecodeReplayRequest {
campaign_id: "decode-test-campaign".to_string(),
selection: super::tests::result_or_panic(kb_store::DecodeSelectionFilter::actionable(
10,
)),
decoder_names: std::vec::Vec::new(),
dispatch_policy: crate::DecodeDispatchPolicy::HighestPriority,
max_concurrent_inputs: 2,
force_replay,
force_replay_all_matching: force_replay,
materialize_after_decode: false,
};
}
fn memo_replay_input(failed: bool) -> kb_lib::MdCoreInstructionReplayInput {
let result = kb_lib::MdCoreInstructionReplayInput::new(
"memo-signature:0",
"memo-signature",
84,
"0",
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
failed,
if failed {
std::option::Option::Some(serde_json::json!({
"InstructionError": [0, "InvalidInstructionData"]
}))
} else {
std::option::Option::None
},
serde_json::json!([]),
serde_json::json!([]),
std::option::Option::Some(serde_json::json!({
"dataBase64": "ZGVtbyBhbm5vdGF0aW9u"
})),
std::option::Option::Some("memo-payload-hash".to_string()),
serde_json::json!([]),
serde_json::json!([]),
serde_json::json!([]),
serde_json::json!([]),
);
return super::tests::result_or_panic(result);
}
#[test]
fn dispatch_is_exact_by_program_and_rank() {
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> = std::vec![
std::sync::Arc::new(TestDecoder {
version: "1",
exact: false,
priority: 100
}),
std::sync::Arc::new(TestDecoder { version: "2", exact: true, priority: 1 }),
];
let ranked =
super::rank_decoders("test-campaign", &replay_input("program_a", false), &decoders);
assert_eq!(ranked.len(), 2);
assert_eq!(ranked[0].identity.version, "2");
assert!(
super::rank_decoders("test-campaign", &replay_input("unknown", false), &decoders)
.is_empty()
);
}
#[test]
fn decoded_observation_context_must_match_core_input() {
let input = replay_input("program_a", true);
let decoder = TestDecoder { version: "1", exact: true, priority: 1 };
let mut execution = kb_lib::DcApiInstructionDecoder::decode(&decoder, &input);
execution.observations[0].event.slot = kb_lib::MdSlot(input.slot + 1);
let identity = kb_lib::DcApiInstructionDecoder::identity(&decoder);
let result =
super::observation_inserts(&identity, &input, "input-hash", &execution.observations);
assert!(result.is_err());
}
#[test]
fn duplicate_decoder_identity_is_rejected() {
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> = std::vec![
std::sync::Arc::new(TestDecoder { version: "1", exact: true, priority: 1 }),
std::sync::Arc::new(TestDecoder { version: "1", exact: false, priority: 2 }),
];
let result = super::validate_decoder_registry(&decoders);
assert!(result.is_err());
}
#[test]
fn materializer_input_key_changes_with_decoder_version() {
let input = replay_input("program_a", false);
let version_one = kb_lib::DcApiDecoderIdentity {
name: "test_decoder".to_string(),
version: "1".to_string(),
};
let version_two = kb_lib::DcApiDecoderIdentity {
name: "test_decoder".to_string(),
version: "2".to_string(),
};
let first = super::materializer_input_key(&input, &version_one, "attempt");
let second = super::materializer_input_key(&input, &version_two, "attempt");
assert_ne!(first, second);
}
#[test]
fn effective_selection_uses_enabled_decoder_programs() {
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(TestDecoder { version: "1", exact: true, priority: 1 })];
let selection = super::tests::result_or_panic(super::effective_selection(
&request(false),
decoders.as_slice(),
));
assert_eq!(selection.program_ids, std::vec!["program_a".to_string()]);
assert_eq!(selection.processing_states.len(), 3);
}
#[test]
fn force_replay_with_explicit_signatures_bypasses_state_filter() {
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(TestDecoder { version: "1", exact: true, priority: 1 })];
let mut replay_request = request(true);
replay_request.force_replay_all_matching = false;
replay_request.selection.signatures = std::vec!["signature".to_string()];
let selection = super::tests::result_or_panic(super::effective_selection(
&replay_request,
decoders.as_slice(),
));
assert!(selection.processing_states.is_empty());
}
#[test]
fn force_replay_without_explicit_scope_is_rejected() {
let mut replay_request = request(false);
replay_request.force_replay = true;
let result = replay_request.validate();
assert!(result.is_err());
}
#[test]
fn all_matching_authorization_rejects_explicit_signatures() {
let mut replay_request = super::tests::request(true);
replay_request.selection.signatures = std::vec!["signature".to_string()];
let result = replay_request.validate();
assert!(result.is_err());
}
#[test]
fn force_replay_all_matching_bypasses_state_filter() {
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(TestDecoder { version: "1", exact: true, priority: 1 })];
let replay_request = request(true);
let selection = super::tests::result_or_panic(super::effective_selection(
&replay_request,
decoders.as_slice(),
));
assert!(selection.processing_states.is_empty());
}
#[tokio::test]
async fn observation_level_materializer_filter_avoids_false_refusals() {
let store = TestStore::default();
super::tests::lock_or_panic(&store.inputs).push(replay_input("program_a", false));
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(TestDecoder { version: "1", exact: true, priority: 1 })];
let materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> =
std::vec![std::sync::Arc::new(SelectiveAuditMaterializer)];
let observer = TestObserver {
cancelled: std::sync::atomic::AtomicBool::new(false),
};
let mut replay_request = request(false);
replay_request.materialize_after_decode = true;
let summary = super::tests::result_or_panic(
crate::execute_decode_replay(
&store,
&replay_request,
&decoders,
&materializers,
&observer,
)
.await,
);
assert_eq!(summary.processors[0].decoded, 1);
assert_eq!(summary.processors[0].materialized_outputs, 0);
assert_eq!(summary.processors[0].materialization_refused, 0);
}
#[tokio::test]
async fn materialization_without_materializer_is_rejected() {
let store = TestStore::default();
super::tests::lock_or_panic(&store.inputs).push(replay_input("program_a", false));
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(TestDecoder { version: "1", exact: true, priority: 1 })];
let observer = TestObserver {
cancelled: std::sync::atomic::AtomicBool::new(false),
};
let mut replay_request = request(false);
replay_request.materialize_after_decode = true;
let result =
crate::execute_decode_replay(&store, &replay_request, &decoders, &[], &observer).await;
assert!(result.is_err());
}
#[test]
fn explicit_unsupported_program_is_rejected_before_selection() {
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(TestDecoder { version: "1", exact: true, priority: 1 })];
let mut replay_request = request(false);
replay_request.selection.program_ids = std::vec!["unrelated".to_string()];
let result = super::effective_selection(&replay_request, decoders.as_slice());
assert!(result.is_err());
}
#[tokio::test]
async fn automatic_program_scope_prevents_unrelated_unmatched_inputs() {
let store = TestStore::default();
{
let mut inputs = super::tests::lock_or_panic(&store.inputs);
inputs.push(replay_input("program_a", false));
inputs.push(replay_input("unrelated", false));
}
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(TestDecoder { version: "1", exact: true, priority: 1 })];
let observer = TestObserver {
cancelled: std::sync::atomic::AtomicBool::new(false),
};
let summary = super::tests::result_or_panic(
crate::execute_decode_replay(&store, &request(false), &decoders, &[], &observer).await,
);
assert_eq!(summary.selected, 1);
assert_eq!(summary.unmatched, 0);
assert_eq!(summary.processors[0].decoded, 1);
}
#[tokio::test]
async fn same_version_and_hash_is_skipped() {
let store = TestStore::default();
super::tests::lock_or_panic(&store.inputs).push(replay_input("program_a", false));
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(TestDecoder { version: "1", exact: true, priority: 1 })];
let observer = TestObserver {
cancelled: std::sync::atomic::AtomicBool::new(false),
};
let first = super::tests::result_or_panic(
crate::execute_decode_replay(&store, &request(false), &decoders, &[], &observer).await,
);
let second = super::tests::result_or_panic(
crate::execute_decode_replay(&store, &request(false), &decoders, &[], &observer).await,
);
assert_eq!(first.processors[0].decoded, 1);
assert_eq!(second.processors[0].skipped, 1);
}
#[tokio::test]
async fn version_change_replays_and_force_replaces_target_version_only() {
let store = TestStore::default();
super::tests::lock_or_panic(&store.inputs).push(replay_input("program_a", false));
let observer = TestObserver {
cancelled: std::sync::atomic::AtomicBool::new(false),
};
let version_one: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(TestDecoder { version: "1", exact: true, priority: 1 })];
let version_two: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(TestDecoder { version: "2", exact: true, priority: 1 })];
super::tests::result_or_panic(
crate::execute_decode_replay(&store, &request(false), &version_one, &[], &observer)
.await,
);
super::tests::result_or_panic(
crate::execute_decode_replay(&store, &request(false), &version_two, &[], &observer)
.await,
);
super::tests::result_or_panic(
crate::execute_decode_replay(&store, &request(true), &version_two, &[], &observer)
.await,
);
let bundles = super::tests::lock_or_panic(&store.decode_bundles);
assert_eq!(bundles.len(), 2);
assert!(
bundles
.iter()
.any(|bundle| return bundle.ledger_identity.processor_version == "1")
);
assert!(
bundles
.iter()
.any(|bundle| return bundle.ledger_identity.processor_version == "2")
);
}
#[tokio::test]
async fn failed_transaction_observation_is_persisted_uncommitted() {
let store = TestStore::default();
super::tests::lock_or_panic(&store.inputs).push(replay_input("program_a", true));
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(TestDecoder { version: "1", exact: true, priority: 1 })];
let observer = TestObserver {
cancelled: std::sync::atomic::AtomicBool::new(false),
};
super::tests::result_or_panic(
crate::execute_decode_replay(&store, &request(false), &decoders, &[], &observer).await,
);
let bundles = super::tests::lock_or_panic(&store.decode_bundles);
assert!(bundles[0].observations[0].transaction_failed);
assert!(!bundles[0].observations[0].observation_committed);
}
#[tokio::test]
async fn concrete_memo_replay_materializes_only_committed_annotations_idempotently() {
let store = TestStore::default();
super::tests::lock_or_panic(&store.inputs).push(memo_replay_input(false));
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(kb_lib::DcSplMemoDecoder)];
let materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> =
std::vec![std::sync::Arc::new(kb_lib::MtTransactionAnnotationMaterializer,)];
let observer = TestObserver {
cancelled: std::sync::atomic::AtomicBool::new(false),
};
let mut replay_request = request(false);
replay_request.materialize_after_decode = true;
let first = super::tests::result_or_panic(
crate::execute_decode_replay(
&store,
&replay_request,
&decoders,
&materializers,
&observer,
)
.await,
);
let second = super::tests::result_or_panic(
crate::execute_decode_replay(
&store,
&replay_request,
&decoders,
&materializers,
&observer,
)
.await,
);
assert_eq!(first.processors[0].decoded, 1);
assert_eq!(first.processors[0].materialized_outputs, 1);
assert_eq!(second.processors[0].skipped, 1);
{
let bundles = super::tests::lock_or_panic(&store.materialization_bundles);
assert_eq!(bundles.len(), 1);
assert_eq!(bundles[0].outputs.len(), 1);
assert_eq!(bundles[0].outputs[0].materialized_family, "transaction_annotation");
assert_eq!(bundles[0].outputs[0].payload_json["text"], "demo annotation");
}
let failed_store = TestStore::default();
super::tests::lock_or_panic(&failed_store.inputs).push(memo_replay_input(true));
let failed = super::tests::result_or_panic(
crate::execute_decode_replay(
&failed_store,
&replay_request,
&decoders,
&materializers,
&observer,
)
.await,
);
assert_eq!(failed.processors[0].decoded, 1);
assert_eq!(failed.processors[0].materialized_outputs, 0);
assert_eq!(failed.processors[0].materialization_refused, 1);
}
#[tokio::test]
async fn declared_and_observed_coverage_are_recorded() {
let store = TestStore::default();
super::tests::lock_or_panic(&store.inputs).push(replay_input("program_a", false));
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(TestDecoder { version: "1", exact: true, priority: 1 })];
let observer = TestObserver {
cancelled: std::sync::atomic::AtomicBool::new(false),
};
super::tests::result_or_panic(
crate::execute_decode_replay(&store, &request(false), &decoders, &[], &observer).await,
);
let declarations = super::tests::lock_or_panic(&store.declarations);
assert_eq!(declarations.len(), 1);
assert_eq!(declarations[0].entry_code, "attempt");
let bundles = super::tests::lock_or_panic(&store.decode_bundles);
assert_eq!(bundles.len(), 1);
assert_eq!(bundles[0].coverage.entry_code.as_deref(), std::option::Option::Some("attempt"));
assert!(bundles[0].coverage.recognized);
assert_eq!(bundles[0].coverage.decoded_count, 1);
assert_eq!(bundles[0].coverage.status, "decoded");
}
#[tokio::test]
async fn cancellation_leaves_candidates_not_started() {
let store = TestStore::default();
super::tests::lock_or_panic(&store.inputs).push(replay_input("program_a", false));
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(TestDecoder { version: "1", exact: true, priority: 1 })];
let observer = TestObserver {
cancelled: std::sync::atomic::AtomicBool::new(true),
};
let summary = super::tests::result_or_panic(
crate::execute_decode_replay(&store, &request(false), &decoders, &[], &observer).await,
);
assert_eq!(summary.selected, 1);
assert_eq!(summary.not_started, 1);
assert_eq!(summary.started, 0);
assert_eq!(summary.completed, 0);
}
}