use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::api::{
EvidenceEnvelope, EvidenceLevel, FailureClassification, ReplayBundle, ScenarioClass,
ScenarioManifest, ScenarioOutcome, scope,
};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AlloyContractRecord {
pub scope: String,
pub replay_bundle_artifact_name: String,
pub evidence_artifact_name: String,
pub evidence_report_line: String,
pub manifest_summary: String,
pub permits_release: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ManifestPreconditions {
pub dataset_family: String,
pub plan_profile: String,
pub cache_mode: String,
pub transport: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ReplayManifest {
pub scenario_id: String,
pub title: String,
pub phase: String,
pub maturity_tier: String,
pub scenario_class: ScenarioClass,
pub producer_surfaces: Vec<String>,
pub preconditions: ManifestPreconditions,
pub stimulus: String,
pub assertions: Vec<String>,
pub evidence_level: EvidenceLevel,
pub required_artifacts: Vec<String>,
pub failure_classifications: Vec<FailureClassification>,
pub reproduction_steps: Vec<String>,
pub notes: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AssertionResultRecord {
pub assertion: String,
pub passed: bool,
pub detail: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct FileArtifactRef {
pub kind: String,
pub path: String,
pub description: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ProducerEvidenceRecord {
pub producer_surface: String,
pub dataset_family: String,
pub workload_id: String,
pub supporting_artifacts: Vec<FileArtifactRef>,
pub metadata: BTreeMap<String, String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RunSummary {
pub result: ScenarioOutcome,
pub assertion_results: Vec<AssertionResultRecord>,
pub producer_evidence: Vec<ProducerEvidenceRecord>,
pub reproduction_steps: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct TimelineEntry {
pub sequence: u32,
pub label: String,
pub detail: Option<String>,
pub connection_id: Option<String>,
pub stream_id: Option<String>,
pub fault_class: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct TraceSummary {
pub trace_id: String,
pub summary: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct MetricsSnapshot {
pub counters: BTreeMap<String, u64>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ReplayArtifact {
pub artifact_id: String,
pub schema_version: String,
pub producing_repo: String,
pub producing_commit: String,
pub scenario_id: String,
pub scenario_class: ScenarioClass,
pub maturity_tier: String,
pub dataset_family: String,
pub workload_id: String,
pub result: ScenarioOutcome,
pub evidence_level: EvidenceLevel,
pub failure_classification: Option<FailureClassification>,
pub started_at: String,
pub finished_at: String,
pub supporting_artifacts: Vec<FileArtifactRef>,
pub alloy_contract: AlloyContractRecord,
pub manifest: ReplayManifest,
pub run_summary: RunSummary,
pub timeline: Vec<TimelineEntry>,
pub trace_summary: Option<TraceSummary>,
pub metrics_snapshot: Option<MetricsSnapshot>,
pub attachments: Vec<FileArtifactRef>,
}
impl ReplayArtifact {
#[must_use]
pub fn validate_phase_1_requirements(&self) -> Phase1ValidationReport {
let mut missing_sections = Vec::new();
if self.failure_classification.is_none() {
missing_sections.push("failure_classification".to_owned());
}
if self.timeline.is_empty() {
missing_sections.push("timeline".to_owned());
}
match self.scenario_class {
ScenarioClass::RestartRecovery => {
if self.trace_summary.is_none() {
missing_sections.push("trace_summary".to_owned());
}
if self.metrics_snapshot.is_none() {
missing_sections.push("metrics_snapshot".to_owned());
}
}
ScenarioClass::CapabilityRejection => {
if self.trace_summary.is_none() {
missing_sections.push("trace_summary".to_owned());
}
}
ScenarioClass::CacheTransition => {
if self.metrics_snapshot.is_none() {
missing_sections.push("metrics_snapshot".to_owned());
}
}
ScenarioClass::TransportFault => {
if self.trace_summary.is_none() {
missing_sections.push("trace_summary".to_owned());
}
if !self.timeline.iter().any(|entry| {
entry.connection_id.is_some()
&& entry.stream_id.is_some()
&& entry.fault_class.is_some()
}) {
missing_sections.push("timeline.transport_correlation".to_owned());
}
}
}
Phase1ValidationReport {
scenario_class: self.scenario_class,
missing_sections,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Phase1ValidationReport {
pub scenario_class: ScenarioClass,
pub missing_sections: Vec<String>,
}
impl Phase1ValidationReport {
#[must_use]
pub fn is_valid(&self) -> bool {
self.missing_sections.is_empty()
}
}
#[must_use]
pub fn canonical_alloy_contract_record(
replay_bundle: &ReplayBundle<'_>,
manifest: &ScenarioManifest<'_>,
envelope: &EvidenceEnvelope<'_>,
) -> AlloyContractRecord {
AlloyContractRecord {
scope: scope().to_owned(),
replay_bundle_artifact_name: replay_bundle.artifact_name(),
evidence_artifact_name: envelope.artifact_name(),
evidence_report_line: envelope.report_line(),
manifest_summary: manifest.summary_line(),
permits_release: envelope.permits_release(),
}
}