1use serde::{Deserialize, Serialize};
10use std::path::PathBuf;
11
12use crate::error::ForgeResult;
13use crate::experiment::{ExperimentDiff, TypedLocatedEffect};
14use crate::lab::evaluate::ScoreVector;
15use stack_ids::{AttemptId, ClaimVersionId, RelationVersionId, TrialId};
16
17const LOCAL_AUTHORING_METADATA_KEY: &str = "living_memory_authoring";
18
19#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum ClaimStrength {
27 #[default]
29 ProvisionalSinglePair,
30}
31
32impl std::fmt::Display for ClaimStrength {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 Self::ProvisionalSinglePair => {
36 write!(f, "provisional local attribution from one paired intervention on one fixed workload slice")
37 }
38 }
39 }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct BundleScope {
47 pub workload_id: String,
49 pub backend_family: String,
51 pub selected_checks: Vec<String>,
53 pub timeout_class: String,
55 pub config_flags: Vec<String>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct Covariates {
64 pub env_fingerprint: String,
66 pub dependency_fingerprint: Option<String>,
68 pub config_flags: Vec<String>,
70 pub workload_id: String,
72 pub selected_checks: Vec<String>,
74 pub adjacent_edits: bool,
76 #[serde(default)]
78 pub adjacent_edit_signatures: Vec<String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct Treatment {
86 pub kind: String,
88 pub patch_hash: String,
90 pub patch_summary: String,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct ReceiptRef {
99 pub receipt_id: String,
101 pub kind: ReceiptKind,
103 pub storage: ReceiptStorage,
105 pub content_hash: String,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub trace_id: Option<String>,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub replay_handle: Option<String>,
113}
114
115impl ReceiptRef {
116 pub fn verify_content(&self, content: &[u8]) -> bool {
118 let hash = blake3::hash(content).to_hex().to_string();
119 hash == self.content_hash
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125#[serde(rename_all = "snake_case")]
126pub enum ReceiptKind {
127 TrialLog,
128 TrialMetrics,
129 CheckResult,
130 PatchApplicationRecord,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum ReceiptStorage {
137 Inline(String),
139 StoreRow { table: String, key: String },
141 ArtifactPath(String),
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct HypothesisEdge {
153 pub edge_id: String,
155 pub source_edit: String,
157 pub target_effect: String,
159 pub kind: HypothesisEdgeKind,
161 pub status: HypothesisStatus,
163 pub confidence: f64,
165 pub evidence_ids: Vec<String>,
167 pub contradiction_ids: Vec<String>,
169 pub verification_status: VerificationState,
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
175#[serde(rename_all = "snake_case")]
176pub enum HypothesisEdgeKind {
177 CausesRegression,
179 FixesFailure,
181 AssociatedWithStableFailure,
183}
184
185#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(rename_all = "snake_case")]
188pub enum VerificationState {
189 #[default]
190 Unverified,
191 PlanGenerated,
192 VerificationPending,
193 Verified,
194 VerificationFailed,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
201#[serde(rename_all = "snake_case")]
202pub enum BaselineOrPatch {
203 Baseline,
205 Patched,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct VerificationTrial {
212 pub trial_id: TrialId,
214 pub attempt_id: AttemptId,
216 pub baseline_or_patch: BaselineOrPatch,
218 pub completed: bool,
220 #[serde(default)]
222 pub receipts: Vec<String>,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
227#[serde(rename_all = "snake_case")]
228pub enum RefutationArtifactOutcome {
229 Passed,
231 Failed {
233 reason: String,
235 },
236 Inconclusive {
238 reason: String,
240 },
241 Skipped {
243 reason: String,
245 },
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
250#[serde(rename_all = "snake_case")]
251pub enum RefutationArtifactType {
252 Placebo,
254 DummyOutcome,
256 SubsampleStability,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct RefutationArtifact {
263 pub artifact_id: String,
265 pub artifact_type: RefutationArtifactType,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub trial_id: Option<TrialId>,
270 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub attempt_id: Option<AttemptId>,
273 pub outcome: RefutationArtifactOutcome,
275 #[serde(default, skip_serializing_if = "Option::is_none")]
277 pub estimate_delta: Option<f64>,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub details: Option<String>,
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
285#[serde(rename_all = "snake_case")]
286pub enum EffectRelationLineageSource {
287 PrimaryEffect,
289 AllEffect,
291 ExperimentDiff,
293}
294
295impl EffectRelationLineageSource {
296 pub fn export_source_key(self) -> &'static str {
298 match self {
299 Self::PrimaryEffect => "primary_effect",
300 Self::AllEffect => "all_effect",
301 Self::ExperimentDiff => "experiment_diff",
302 }
303 }
304}
305
306#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
308pub struct EffectRelationLineageHint {
309 pub source: EffectRelationLineageSource,
311 pub kind: crate::experiment::EffectKind,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub file: Option<PathBuf>,
316 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub line: Option<u32>,
319 pub message: String,
321 pub in_baseline: bool,
323 pub in_patched: bool,
325 pub supersedes_relation_version_id: RelationVersionId,
327}
328
329impl EffectRelationLineageHint {
330 fn matches(
331 &self,
332 source: EffectRelationLineageSource,
333 effect: &crate::experiment::TypedLocatedEffect,
334 ) -> bool {
335 self.source == source
336 && self.kind == effect.kind
337 && self.file == effect.file
338 && self.line == effect.line
339 && self.message == effect.message
340 && self.in_baseline == effect.in_baseline
341 && self.in_patched == effect.in_patched
342 }
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
347pub struct HypothesisRelationLineageHint {
348 pub edge_id: String,
350 pub supersedes_relation_version_id: RelationVersionId,
352}
353
354impl HypothesisRelationLineageHint {
355 fn matches(&self, edge: &HypothesisEdge) -> bool {
356 self.edge_id == edge.edge_id
357 }
358}
359
360#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
362pub struct VerificationTrialRelationLineageHint {
363 pub trial_id: TrialId,
365 pub baseline_or_patch: BaselineOrPatch,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub attempt_id: Option<AttemptId>,
370 pub supersedes_relation_version_id: RelationVersionId,
372}
373
374impl VerificationTrialRelationLineageHint {
375 fn matches(&self, trial: &VerificationTrial) -> bool {
376 self.trial_id == trial.trial_id
377 && self.baseline_or_patch == trial.baseline_or_patch
378 && match self.attempt_id.as_ref() {
379 Some(attempt_id) => attempt_id == &trial.attempt_id,
380 None => true,
381 }
382 }
383}
384
385#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
387pub struct RefutationRelationLineageHint {
388 pub artifact_id: String,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub artifact_type: Option<RefutationArtifactType>,
393 pub supersedes_relation_version_id: RelationVersionId,
395}
396
397impl RefutationRelationLineageHint {
398 fn matches(&self, artifact: &RefutationArtifact) -> bool {
399 self.artifact_id == artifact.artifact_id
400 && match self.artifact_type {
401 Some(artifact_type) => artifact_type == artifact.artifact_type,
402 None => true,
403 }
404 }
405}
406
407#[derive(Debug, Clone, Default, Serialize, Deserialize)]
409pub struct RelationLineageHints {
410 #[serde(default, skip_serializing_if = "Vec::is_empty")]
412 pub effect_relations: Vec<EffectRelationLineageHint>,
413 #[serde(default, skip_serializing_if = "Vec::is_empty")]
415 pub hypothesis_relations: Vec<HypothesisRelationLineageHint>,
416 #[serde(default, skip_serializing_if = "Vec::is_empty")]
418 pub verification_trial_relations: Vec<VerificationTrialRelationLineageHint>,
419 #[serde(default, skip_serializing_if = "Vec::is_empty")]
421 pub refutation_relations: Vec<RefutationRelationLineageHint>,
422}
423
424impl RelationLineageHints {
425 pub fn is_empty(&self) -> bool {
427 self.effect_relations.is_empty()
428 && self.hypothesis_relations.is_empty()
429 && self.verification_trial_relations.is_empty()
430 && self.refutation_relations.is_empty()
431 }
432
433 fn effect_supersedes_relation_version_id(
434 &self,
435 source: EffectRelationLineageSource,
436 effect: &crate::experiment::TypedLocatedEffect,
437 ) -> Option<RelationVersionId> {
438 self.effect_relations
439 .iter()
440 .find(|hint| hint.matches(source, effect))
441 .map(|hint| hint.supersedes_relation_version_id.clone())
442 }
443
444 fn hypothesis_supersedes_relation_version_id(
445 &self,
446 edge: &HypothesisEdge,
447 ) -> Option<RelationVersionId> {
448 self.hypothesis_relations
449 .iter()
450 .find(|hint| hint.matches(edge))
451 .map(|hint| hint.supersedes_relation_version_id.clone())
452 }
453
454 fn verification_trial_supersedes_relation_version_id(
455 &self,
456 trial: &VerificationTrial,
457 ) -> Option<RelationVersionId> {
458 self.verification_trial_relations
459 .iter()
460 .find(|hint| hint.matches(trial))
461 .map(|hint| hint.supersedes_relation_version_id.clone())
462 }
463
464 fn refutation_supersedes_relation_version_id(
465 &self,
466 artifact: &RefutationArtifact,
467 ) -> Option<RelationVersionId> {
468 self.refutation_relations
469 .iter()
470 .find(|hint| hint.matches(artifact))
471 .map(|hint| hint.supersedes_relation_version_id.clone())
472 }
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct ExperimentEvidenceBundle {
487 pub bundle_id: String,
489 pub candidate_id: String,
491 pub eval_id: String,
493 pub version_id: String,
495 #[serde(default, skip_serializing_if = "Option::is_none")]
500 pub supersedes_claim_version_id: Option<ClaimVersionId>,
501 #[serde(default, skip_serializing_if = "RelationLineageHints::is_empty")]
506 pub relation_lineage_hints: RelationLineageHints,
507 pub scores: ScoreVector,
509 pub hypotheses: Vec<CausalHypothesis>,
511 pub verification: Option<VerificationPlan>,
513 pub trace_id: Option<String>,
515 #[serde(default, skip_serializing_if = "Option::is_none")]
517 pub experiment_diff: Option<ExperimentDiff>,
518 #[serde(default, skip_serializing_if = "Option::is_none")]
520 pub attribution_json: Option<String>,
521 #[serde(default, skip_serializing_if = "Option::is_none")]
523 pub assessment: Option<EvidenceAssessment>,
524 #[serde(default)]
526 pub warnings: Vec<String>,
527 #[serde(default = "default_bundle_created_at")]
529 pub created_at: String,
530
531 #[serde(default, skip_serializing_if = "Option::is_none")]
534 pub run_id: Option<String>,
535 #[serde(default, skip_serializing_if = "Option::is_none")]
537 pub attempt_id: Option<String>,
538 #[serde(default, skip_serializing_if = "Option::is_none")]
540 pub causal_question: Option<String>,
541 #[serde(default, skip_serializing_if = "Option::is_none")]
543 pub unit_definition: Option<String>,
544 #[serde(default, skip_serializing_if = "Option::is_none")]
546 pub bundle_scope: Option<BundleScope>,
547 #[serde(default, skip_serializing_if = "Option::is_none")]
549 pub pair_comparability: Option<PairComparability>,
550 #[serde(default)]
552 pub claim_strength: ClaimStrength,
553 #[serde(default, skip_serializing_if = "Option::is_none")]
555 pub identification_rationale: Option<String>,
556 #[serde(default)]
558 pub known_threats: Vec<String>,
559 #[serde(default, skip_serializing_if = "Option::is_none")]
561 pub patch_hash: Option<String>,
562 #[serde(default, skip_serializing_if = "Option::is_none")]
564 pub treatment: Option<Treatment>,
565 #[serde(default, skip_serializing_if = "Option::is_none")]
567 pub outcome: Option<String>,
568 #[serde(default, skip_serializing_if = "Option::is_none")]
570 pub covariates: Option<Covariates>,
571 #[serde(default, skip_serializing_if = "Option::is_none")]
573 pub promotion_state: Option<semantic_memory_forge::PromotionState>,
574 #[serde(default, skip_serializing_if = "Option::is_none")]
576 pub primary_effect: Option<TypedLocatedEffect>,
577 #[serde(default)]
579 pub all_effects: Vec<TypedLocatedEffect>,
580 #[serde(default)]
582 pub hypothesis_edges: Vec<HypothesisEdge>,
583 #[serde(default)]
585 pub receipts: Vec<ReceiptRef>,
586 #[serde(default)]
588 pub verification_trials: Vec<VerificationTrial>,
589 #[serde(default)]
591 pub refutation_artifacts: Vec<RefutationArtifact>,
592 #[serde(default)]
594 pub sealed: bool,
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize)]
598struct LocalEvidenceBundleAuthoring {
599 candidate_id: String,
600 eval_id: String,
601 version_id: String,
602 supersedes_claim_version_id: Option<ClaimVersionId>,
603 relation_lineage_hints: RelationLineageHints,
604 scores: ScoreVector,
605 hypotheses: Vec<CausalHypothesis>,
606 verification: Option<VerificationPlan>,
607 experiment_diff: Option<ExperimentDiff>,
608 attribution_json: Option<String>,
609 assessment: Option<EvidenceAssessment>,
610 warnings: Vec<String>,
611 run_id: Option<String>,
612 attempt_id: Option<String>,
613 pair_comparability: Option<PairComparability>,
614 claim_strength: ClaimStrength,
615 known_threats: Vec<String>,
616 patch_hash: Option<String>,
617 treatment: Option<Treatment>,
618 covariates: Option<Covariates>,
619 primary_effect: Option<TypedLocatedEffect>,
620 all_effects: Vec<TypedLocatedEffect>,
621 hypothesis_edges: Vec<HypothesisEdge>,
622 receipts: Vec<ReceiptRef>,
623 verification_trials: Vec<VerificationTrial>,
624 refutation_artifacts: Vec<RefutationArtifact>,
625 sealed: bool,
626}
627
628impl LocalEvidenceBundleAuthoring {
629 fn from_bundle(bundle: &ExperimentEvidenceBundle) -> Self {
630 Self {
631 candidate_id: bundle.candidate_id.clone(),
632 eval_id: bundle.eval_id.clone(),
633 version_id: bundle.version_id.clone(),
634 supersedes_claim_version_id: bundle.supersedes_claim_version_id.clone(),
635 relation_lineage_hints: bundle.relation_lineage_hints.clone(),
636 scores: bundle.scores.clone(),
637 hypotheses: bundle.hypotheses.clone(),
638 verification: bundle.verification.clone(),
639 experiment_diff: bundle.experiment_diff.clone(),
640 attribution_json: bundle.attribution_json.clone(),
641 assessment: bundle.assessment.clone(),
642 warnings: bundle.warnings.clone(),
643 run_id: bundle.run_id.clone(),
644 attempt_id: bundle.attempt_id.clone(),
645 pair_comparability: bundle.pair_comparability.clone(),
646 claim_strength: bundle.claim_strength,
647 known_threats: bundle.known_threats.clone(),
648 patch_hash: bundle.patch_hash.clone(),
649 treatment: bundle.treatment.clone(),
650 covariates: bundle.covariates.clone(),
651 primary_effect: bundle.primary_effect.clone(),
652 all_effects: bundle.all_effects.clone(),
653 hypothesis_edges: bundle.hypothesis_edges.clone(),
654 receipts: bundle.receipts.clone(),
655 verification_trials: bundle.verification_trials.clone(),
656 refutation_artifacts: bundle.refutation_artifacts.clone(),
657 sealed: bundle.sealed,
658 }
659 }
660}
661
662impl ExperimentEvidenceBundle {
663 pub fn seal(&mut self) -> ForgeResult<()> {
666 if self.sealed {
667 return Err(crate::error::ForgeError::Other(
668 "bundle is already sealed".into(),
669 ));
670 }
671 self.sealed = true;
672 Ok(())
673 }
674
675 pub fn check_not_sealed(&self) -> ForgeResult<()> {
677 if self.sealed {
678 return Err(crate::error::ForgeError::Other(
679 "cannot mutate sealed bundle".into(),
680 ));
681 }
682 Ok(())
683 }
684
685 pub fn to_canonical_evidence_bundle(&self) -> semantic_memory_forge::EvidenceBundle {
692 let question = semantic_memory_forge::CausalQuestion {
693 description: self.causal_question.clone().unwrap_or_else(|| {
694 let treatment = self
695 .treatment
696 .as_ref()
697 .map(|t| t.patch_summary.as_str())
698 .unwrap_or("applied patch");
699 let outcome = self.outcome.as_deref().unwrap_or("recorded outcome");
700 let workload = self
701 .bundle_scope
702 .as_ref()
703 .map(|scope| scope.workload_id.as_str())
704 .unwrap_or("unspecified workload");
705 let scope_summary = self
706 .bundle_scope
707 .as_ref()
708 .map(|scope| scope.backend_family.as_str())
709 .unwrap_or("unspecified scope");
710 Self::render_causal_question(treatment, outcome, workload, scope_summary)
711 }),
712 unit_definition: self
713 .unit_definition
714 .clone()
715 .unwrap_or_else(|| "paired experiment run".into()),
716 };
717
718 let treatment = semantic_memory_forge::TreatmentSpec {
719 description: self
720 .treatment
721 .as_ref()
722 .map(|t| format!("{} [{}]", t.patch_summary, t.patch_hash))
723 .unwrap_or_else(|| "patch applied".into()),
724 baseline_description: "baseline workspace without patch".into(),
725 paired_trials: self.experiment_diff.is_some() || !self.verification_trials.is_empty(),
726 };
727
728 let outcome = semantic_memory_forge::OutcomeSpec {
729 description: self
730 .outcome
731 .clone()
732 .unwrap_or_else(|| "bundle outcome summary".into()),
733 measurement_method: if self.verification_trials.is_empty() {
734 "single_pair_execution_observation".into()
735 } else {
736 "paired_verification_trials".into()
737 },
738 outcome_type: if self.primary_effect.is_some() {
739 "typed_effect".into()
740 } else {
741 "bundle_summary".into()
742 },
743 };
744
745 let mut covariates = Vec::new();
746 if let Some(bundle_scope) = &self.bundle_scope {
747 covariates.push(format!("workload:{}", bundle_scope.workload_id));
748 covariates.push(format!("backend:{}", bundle_scope.backend_family));
749 covariates.push(format!("timeout_class:{}", bundle_scope.timeout_class));
750 covariates.extend(
751 bundle_scope
752 .selected_checks
753 .iter()
754 .map(|check| format!("check:{check}")),
755 );
756 covariates.extend(
757 bundle_scope
758 .config_flags
759 .iter()
760 .map(|flag| format!("config_flag:{flag}")),
761 );
762 }
763 if let Some(covariates_meta) = &self.covariates {
764 covariates.push(format!(
765 "env_fingerprint:{}",
766 covariates_meta.env_fingerprint
767 ));
768 covariates.push(format!("workload:{}", covariates_meta.workload_id));
769 covariates.extend(
770 covariates_meta
771 .selected_checks
772 .iter()
773 .map(|check| format!("selected_check:{check}")),
774 );
775 covariates.extend(
776 covariates_meta
777 .config_flags
778 .iter()
779 .map(|flag| format!("covariate_flag:{flag}")),
780 );
781 if let Some(dependency_fingerprint) = &covariates_meta.dependency_fingerprint {
782 covariates.push(format!("dependency_fingerprint:{dependency_fingerprint}"));
783 }
784 if covariates_meta.adjacent_edits {
785 covariates.push("adjacent_edits:true".into());
786 }
787 covariates.extend(
788 covariates_meta
789 .adjacent_edit_signatures
790 .iter()
791 .map(|sig| format!("adjacent_edit:{sig}")),
792 );
793 }
794 covariates.extend(
795 self.known_threats
796 .iter()
797 .map(|threat| format!("known_threat:{threat}")),
798 );
799
800 let refutations = self
801 .refutation_artifacts
802 .iter()
803 .map(|artifact| semantic_memory_forge::RefutationAttempt {
804 method: format!("{:?}", artifact.artifact_type).to_lowercase(),
805 result: match &artifact.outcome {
806 RefutationArtifactOutcome::Passed => {
807 semantic_memory_forge::RefutationResult::Passed {
808 estimate_change: artifact.estimate_delta,
809 }
810 }
811 RefutationArtifactOutcome::Failed { reason } => {
812 semantic_memory_forge::RefutationResult::Failed {
813 reason: reason.clone(),
814 estimate_change: artifact.estimate_delta,
815 }
816 }
817 RefutationArtifactOutcome::Inconclusive { reason } => {
818 semantic_memory_forge::RefutationResult::Inconclusive {
819 reason: reason.clone(),
820 }
821 }
822 RefutationArtifactOutcome::Skipped { reason } => {
823 semantic_memory_forge::RefutationResult::Skipped {
824 reason: reason.clone(),
825 }
826 }
827 },
828 estimator_kind: Some("living_memory_phase5_scorevector".into()),
829 parameters: artifact.details.as_ref().map(|details| {
830 serde_json::json!({
831 "artifact_id": artifact.artifact_id,
832 "details": details,
833 })
834 }),
835 })
836 .collect();
837
838 let refutation_artifacts = self
839 .refutation_artifacts
840 .iter()
841 .map(|artifact| semantic_memory_forge::RefutationArtifactRecord {
842 artifact_id: artifact.artifact_id.clone(),
843 artifact_type: Self::debug_name_to_snake_case(&format!(
844 "{:?}",
845 artifact.artifact_type
846 )),
847 trial_id: artifact.trial_id.clone(),
848 attempt_id: artifact.attempt_id.clone(),
849 result: match &artifact.outcome {
850 RefutationArtifactOutcome::Passed => {
851 semantic_memory_forge::RefutationResult::Passed {
852 estimate_change: artifact.estimate_delta,
853 }
854 }
855 RefutationArtifactOutcome::Failed { reason } => {
856 semantic_memory_forge::RefutationResult::Failed {
857 reason: reason.clone(),
858 estimate_change: artifact.estimate_delta,
859 }
860 }
861 RefutationArtifactOutcome::Inconclusive { reason } => {
862 semantic_memory_forge::RefutationResult::Inconclusive {
863 reason: reason.clone(),
864 }
865 }
866 RefutationArtifactOutcome::Skipped { reason } => {
867 semantic_memory_forge::RefutationResult::Skipped {
868 reason: reason.clone(),
869 }
870 }
871 },
872 estimate_delta: artifact.estimate_delta,
873 details: artifact.details.clone(),
874 })
875 .collect::<Vec<_>>();
876
877 let verification_trials = self
878 .verification_trials
879 .iter()
880 .map(|trial| semantic_memory_forge::VerificationTrialRecord {
881 trial_id: trial.trial_id.clone(),
882 attempt_id: trial.attempt_id.clone(),
883 side: match trial.baseline_or_patch {
884 BaselineOrPatch::Baseline => {
885 semantic_memory_forge::VerificationTrialSide::Baseline
886 }
887 BaselineOrPatch::Patched => {
888 semantic_memory_forge::VerificationTrialSide::Patched
889 }
890 },
891 completed: trial.completed,
892 receipt_handles: trial.receipts.clone(),
893 })
894 .collect::<Vec<_>>();
895
896 let comparability_snapshot =
897 self.bundle_scope
898 .as_ref()
899 .map(|scope| semantic_memory_forge::ComparabilitySnapshot {
900 workload_id: scope.workload_id.clone(),
901 backend_family: scope.backend_family.clone(),
902 selected_checks: scope.selected_checks.clone(),
903 timeout_class: scope.timeout_class.clone(),
904 config_flags: scope.config_flags.clone(),
905 comparable: self.pair_comparability.as_ref().map(|pair| pair.valid),
906 violations: self
907 .pair_comparability
908 .as_ref()
909 .map(|pair| pair.violations.clone())
910 .unwrap_or_default(),
911 });
912
913 let raw_receipt_handle = self.receipts.first().map(|receipt| match &receipt.storage {
914 ReceiptStorage::Inline(_) => format!("receipt:inline:{}", receipt.receipt_id),
915 ReceiptStorage::StoreRow { table, key } => {
916 format!("receipt:store:{table}:{key}")
917 }
918 ReceiptStorage::ArtifactPath(path) => format!("receipt:path:{path}"),
919 });
920
921 let confidence = self
922 .assessment
923 .as_ref()
924 .map(|assessment| match assessment.sample_support {
925 SampleSupport::Sufficient => 0.9,
926 SampleSupport::Marginal => 0.6,
927 SampleSupport::Insufficient => 0.3,
928 })
929 .unwrap_or_else(|| self.scores.weighted_total.clamp(0.0, 1.0) as f32);
930
931 let completed_trial_count = self
932 .verification_trials
933 .iter()
934 .filter(|trial| trial.completed)
935 .count() as u32;
936 let passed_refutation_count = self
937 .refutation_artifacts
938 .iter()
939 .filter(|artifact| matches!(artifact.outcome, RefutationArtifactOutcome::Passed))
940 .count() as u32;
941 let failed_refutation_count = self
942 .refutation_artifacts
943 .iter()
944 .filter(|artifact| matches!(artifact.outcome, RefutationArtifactOutcome::Failed { .. }))
945 .count() as u32;
946 let lifecycle_state = if self.assessment.as_ref().is_some_and(|assessment| {
947 assessment.contradiction_state == ContradictionState::HasContradictions
948 }) || failed_refutation_count > 0
949 {
950 semantic_memory_forge::VerificationLifecycleState::Contradicted
951 } else if completed_trial_count > 0
952 || self.assessment.as_ref().is_some_and(|assessment| {
953 matches!(
954 assessment.reproducibility,
955 AssessmentCategory::Strong | AssessmentCategory::Adequate
956 )
957 })
958 {
959 semantic_memory_forge::VerificationLifecycleState::Verified
960 } else {
961 semantic_memory_forge::VerificationLifecycleState::Unverified
962 };
963
964 let derived_promotion_state = self.promotion_state.clone().unwrap_or_else(|| {
965 if let Some(assessment) = self.assessment.as_ref() {
966 if assessment.contradiction_state == ContradictionState::HasContradictions {
967 semantic_memory_forge::PromotionState::Blocked {
968 reason: "contradictions_present".into(),
969 }
970 } else if assessment.sample_support == SampleSupport::Insufficient {
971 semantic_memory_forge::PromotionState::Blocked {
972 reason: "sample_support_insufficient".into(),
973 }
974 } else if assessment.isolation != AssessmentCategory::Strong {
975 semantic_memory_forge::PromotionState::Blocked {
976 reason: "isolation_not_strong".into(),
977 }
978 } else if matches!(
979 assessment.reproducibility,
980 AssessmentCategory::Strong | AssessmentCategory::Adequate
981 ) {
982 semantic_memory_forge::PromotionState::Eligible
983 } else {
984 semantic_memory_forge::PromotionState::NotPromoted
985 }
986 } else {
987 semantic_memory_forge::PromotionState::NotPromoted
988 }
989 });
990
991 let mut verification_notes = self.warnings.clone();
992 if self.supersedes_claim_version_id.is_some() {
993 verification_notes.push("supersedes_prior_claim_version".into());
994 }
995
996 let estimator_meta = Some(semantic_memory_forge::EstimatorMeta {
997 kind: semantic_memory_forge::EstimatorKind::Custom(
998 "living_memory_phase5_scorevector".into(),
999 ),
1000 version: self.version_id.clone(),
1001 parameters: serde_json::json!({
1002 "weighted_total": self.scores.weighted_total,
1003 "correctness": self.scores.correctness,
1004 "novelty": self.scores.novelty,
1005 "stability": self.scores.stability,
1006 "claim_strength": format!("{}", self.claim_strength),
1007 }),
1008 random_seed: None,
1009 environment: self.covariates.as_ref().map(|covariates| {
1010 semantic_memory_forge::EnvironmentFingerprint {
1011 python_version: None,
1012 package_versions: serde_json::json!({
1013 "selected_checks": covariates.selected_checks,
1014 "config_flags": covariates.config_flags,
1015 }),
1016 platform: None,
1017 env_hash: Some(covariates.env_fingerprint.clone()),
1018 }
1019 }),
1020 timeout_secs: None,
1021 failure_mode: None,
1022 request_schema_version: Some("living_memory.phase5.bundle.v1".into()),
1023 response_schema_version: Some("semantic_memory_forge.evidence_bundle.v3".into()),
1024 });
1025
1026 semantic_memory_forge::EvidenceBundle {
1027 id: semantic_memory_forge::EvidenceBundleId::new(self.bundle_id.clone()),
1028 question,
1029 treatment,
1030 outcome,
1031 covariates,
1032 identification_rationale: self
1033 .identification_rationale
1034 .clone()
1035 .unwrap_or_else(|| "phase5_local_bundle_adapter".into()),
1036 estimator_kind: "living_memory_phase5_scorevector".into(),
1037 estimator_version: self.version_id.clone(),
1038 estimator_meta,
1039 estimate: self.scores.weighted_total,
1040 estimate_uncertainty: None,
1041 confidence,
1042 trial_count: self.verification_trials.len().max(1) as u32,
1043 variance_aware: self.verification_trials.len() > 1,
1044 verification_trials,
1045 comparability_snapshot,
1046 refutations,
1047 refutation_artifacts,
1048 verification_summary: Some(semantic_memory_forge::VerificationSummary {
1049 lifecycle_state,
1050 promotion_state: derived_promotion_state.clone(),
1051 completed_trial_count,
1052 passed_refutation_count,
1053 failed_refutation_count,
1054 comparability_snapshot_version: self.bundle_scope.as_ref().map(|scope| {
1055 format!(
1056 "{}:{}:{}",
1057 scope.workload_id, scope.backend_family, scope.timeout_class
1058 )
1059 }),
1060 notes: verification_notes,
1061 }),
1062 raw_receipt_handle,
1063 trace_ctx: self
1064 .trace_id
1065 .as_ref()
1066 .map(|trace_id| stack_ids::TraceCtx::from_trace_id(trace_id.as_str())),
1067 attempt_id: self
1068 .verification_trials
1069 .first()
1070 .map(|trial| trial.attempt_id.clone())
1071 .or_else(|| {
1072 self.attempt_id
1073 .as_ref()
1074 .map(|attempt| AttemptId::new(attempt.clone()))
1075 }),
1076 trial_id: self
1077 .verification_trials
1078 .first()
1079 .map(|trial| trial.trial_id.clone()),
1080 replay_handle: self
1081 .receipts
1082 .iter()
1083 .find_map(|receipt| receipt.replay_handle.clone()),
1084 source_envelope_id: None,
1085 claim_ids: Vec::new(),
1086 created_at: self.created_at.clone(),
1087 comparability_snapshot_version: self.bundle_scope.as_ref().map(|scope| {
1088 format!(
1089 "{}:{}:{}",
1090 scope.workload_id, scope.backend_family, scope.timeout_class
1091 )
1092 }),
1093 metadata: Some(serde_json::json!({
1094 "bundle_id": self.bundle_id,
1095 "candidate_id": self.candidate_id,
1096 "eval_id": self.eval_id,
1097 "version_id": self.version_id,
1098 "claim_strength": format!("{}", self.claim_strength),
1099 "assessment": self.assessment,
1100 "pair_comparability": self.pair_comparability,
1101 "promotion_state": derived_promotion_state,
1102 "receipt_count": self.receipts.len(),
1103 "verification_trial_count": self.verification_trials.len(),
1104 "refutation_artifact_count": self.refutation_artifacts.len(),
1105 "canonical_owner": "semantic_memory_forge::EvidenceBundle",
1106 LOCAL_AUTHORING_METADATA_KEY: LocalEvidenceBundleAuthoring::from_bundle(self),
1107 })),
1108 }
1109 }
1110
1111 pub fn from_canonical_evidence_bundle(
1117 canonical: &semantic_memory_forge::EvidenceBundle,
1118 ) -> ForgeResult<Self> {
1119 let authoring = canonical
1120 .metadata
1121 .as_ref()
1122 .and_then(|metadata| metadata.get(LOCAL_AUTHORING_METADATA_KEY))
1123 .cloned()
1124 .map(serde_json::from_value::<LocalEvidenceBundleAuthoring>)
1125 .transpose()?;
1126
1127 let bundle_scope = canonical
1128 .comparability_snapshot
1129 .as_ref()
1130 .map(|snapshot| BundleScope {
1131 workload_id: snapshot.workload_id.clone(),
1132 backend_family: snapshot.backend_family.clone(),
1133 selected_checks: snapshot.selected_checks.clone(),
1134 timeout_class: snapshot.timeout_class.clone(),
1135 config_flags: snapshot.config_flags.clone(),
1136 });
1137
1138 let pair_comparability = authoring
1139 .as_ref()
1140 .and_then(|authoring| authoring.pair_comparability.clone())
1141 .or_else(|| {
1142 canonical
1143 .comparability_snapshot
1144 .as_ref()
1145 .map(|snapshot| PairComparability {
1146 valid: snapshot.comparable.unwrap_or(false),
1147 violations: snapshot.violations.clone(),
1148 })
1149 });
1150
1151 let treatment = authoring
1152 .as_ref()
1153 .and_then(|authoring| authoring.treatment.clone())
1154 .or_else(|| {
1155 if canonical.treatment.description.is_empty() {
1156 None
1157 } else {
1158 Some(Treatment {
1159 kind: "patch_applied".into(),
1160 patch_hash: String::new(),
1161 patch_summary: canonical.treatment.description.clone(),
1162 })
1163 }
1164 });
1165
1166 let covariates = authoring
1167 .as_ref()
1168 .and_then(|authoring| authoring.covariates.clone());
1169
1170 let score_fallback = ScoreVector {
1171 correctness: canonical.estimate,
1172 novelty: 0.0,
1173 stability: canonical.confidence as f64,
1174 weighted_total: canonical.estimate,
1175 cea_confidence: None,
1176 cea_predicted_correctness: None,
1177 };
1178
1179 Ok(Self {
1180 bundle_id: canonical.id.as_str().to_string(),
1181 candidate_id: authoring
1182 .as_ref()
1183 .map(|authoring| authoring.candidate_id.clone())
1184 .or_else(|| {
1185 canonical.metadata.as_ref().and_then(|metadata| {
1186 metadata
1187 .get("candidate_id")
1188 .and_then(serde_json::Value::as_str)
1189 .map(ToOwned::to_owned)
1190 })
1191 })
1192 .unwrap_or_else(|| "unknown_candidate".into()),
1193 eval_id: authoring
1194 .as_ref()
1195 .map(|authoring| authoring.eval_id.clone())
1196 .or_else(|| {
1197 canonical.metadata.as_ref().and_then(|metadata| {
1198 metadata
1199 .get("eval_id")
1200 .and_then(serde_json::Value::as_str)
1201 .map(ToOwned::to_owned)
1202 })
1203 })
1204 .unwrap_or_else(|| "unknown_eval".into()),
1205 version_id: authoring
1206 .as_ref()
1207 .map(|authoring| authoring.version_id.clone())
1208 .or_else(|| {
1209 canonical.metadata.as_ref().and_then(|metadata| {
1210 metadata
1211 .get("version_id")
1212 .and_then(serde_json::Value::as_str)
1213 .map(ToOwned::to_owned)
1214 })
1215 })
1216 .unwrap_or_else(|| canonical.estimator_version.clone()),
1217 supersedes_claim_version_id: authoring
1218 .as_ref()
1219 .and_then(|authoring| authoring.supersedes_claim_version_id.clone()),
1220 relation_lineage_hints: authoring
1221 .as_ref()
1222 .map(|authoring| authoring.relation_lineage_hints.clone())
1223 .unwrap_or_default(),
1224 scores: authoring
1225 .as_ref()
1226 .map(|authoring| authoring.scores.clone())
1227 .unwrap_or(score_fallback),
1228 hypotheses: authoring
1229 .as_ref()
1230 .map(|authoring| authoring.hypotheses.clone())
1231 .unwrap_or_default(),
1232 verification: authoring
1233 .as_ref()
1234 .and_then(|authoring| authoring.verification.clone()),
1235 trace_id: canonical
1236 .trace_ctx
1237 .as_ref()
1238 .map(|trace_ctx| trace_ctx.trace_id.clone()),
1239 experiment_diff: authoring
1240 .as_ref()
1241 .and_then(|authoring| authoring.experiment_diff.clone()),
1242 attribution_json: authoring
1243 .as_ref()
1244 .and_then(|authoring| authoring.attribution_json.clone()),
1245 assessment: authoring
1246 .as_ref()
1247 .and_then(|authoring| authoring.assessment.clone()),
1248 warnings: authoring
1249 .as_ref()
1250 .map(|authoring| authoring.warnings.clone())
1251 .unwrap_or_default(),
1252 created_at: canonical.created_at.clone(),
1253 run_id: authoring
1254 .as_ref()
1255 .and_then(|authoring| authoring.run_id.clone()),
1256 attempt_id: canonical
1257 .attempt_id
1258 .as_ref()
1259 .map(|attempt_id| attempt_id.as_str().to_string())
1260 .or_else(|| {
1261 authoring
1262 .as_ref()
1263 .and_then(|authoring| authoring.attempt_id.clone())
1264 }),
1265 causal_question: Some(canonical.question.description.clone()),
1266 unit_definition: Some(canonical.question.unit_definition.clone()),
1267 bundle_scope,
1268 pair_comparability,
1269 claim_strength: authoring
1270 .as_ref()
1271 .map(|authoring| authoring.claim_strength)
1272 .unwrap_or_default(),
1273 identification_rationale: Some(canonical.identification_rationale.clone()),
1274 known_threats: authoring
1275 .as_ref()
1276 .map(|authoring| authoring.known_threats.clone())
1277 .unwrap_or_default(),
1278 patch_hash: authoring
1279 .as_ref()
1280 .and_then(|authoring| authoring.patch_hash.clone()),
1281 treatment,
1282 outcome: Some(canonical.outcome.description.clone()),
1283 covariates,
1284 promotion_state: canonical
1285 .verification_summary
1286 .as_ref()
1287 .map(|summary| summary.promotion_state.clone()),
1288 primary_effect: authoring
1289 .as_ref()
1290 .and_then(|authoring| authoring.primary_effect.clone()),
1291 all_effects: authoring
1292 .as_ref()
1293 .map(|authoring| authoring.all_effects.clone())
1294 .unwrap_or_default(),
1295 hypothesis_edges: authoring
1296 .as_ref()
1297 .map(|authoring| authoring.hypothesis_edges.clone())
1298 .unwrap_or_default(),
1299 receipts: authoring
1300 .as_ref()
1301 .map(|authoring| authoring.receipts.clone())
1302 .unwrap_or_default(),
1303 verification_trials: if let Some(authoring) = authoring.as_ref() {
1304 authoring.verification_trials.clone()
1305 } else {
1306 canonical
1307 .verification_trials
1308 .iter()
1309 .map(|trial| VerificationTrial {
1310 trial_id: trial.trial_id.clone(),
1311 attempt_id: trial.attempt_id.clone(),
1312 baseline_or_patch: match trial.side {
1313 semantic_memory_forge::VerificationTrialSide::Baseline => {
1314 BaselineOrPatch::Baseline
1315 }
1316 semantic_memory_forge::VerificationTrialSide::Patched => {
1317 BaselineOrPatch::Patched
1318 }
1319 },
1320 completed: trial.completed,
1321 receipts: trial.receipt_handles.clone(),
1322 })
1323 .collect()
1324 },
1325 refutation_artifacts: if let Some(authoring) = authoring.as_ref() {
1326 authoring.refutation_artifacts.clone()
1327 } else {
1328 canonical
1329 .refutation_artifacts
1330 .iter()
1331 .map(|artifact| RefutationArtifact {
1332 artifact_id: artifact.artifact_id.clone(),
1333 artifact_type: match artifact.artifact_type.as_str() {
1334 "placebo" => RefutationArtifactType::Placebo,
1335 "dummy_outcome" => RefutationArtifactType::DummyOutcome,
1336 _ => RefutationArtifactType::SubsampleStability,
1337 },
1338 trial_id: artifact.trial_id.clone(),
1339 attempt_id: artifact.attempt_id.clone(),
1340 outcome: match &artifact.result {
1341 semantic_memory_forge::RefutationResult::Passed { .. } => {
1342 RefutationArtifactOutcome::Passed
1343 }
1344 semantic_memory_forge::RefutationResult::Failed { reason, .. } => {
1345 RefutationArtifactOutcome::Failed {
1346 reason: reason.clone(),
1347 }
1348 }
1349 semantic_memory_forge::RefutationResult::Inconclusive { reason } => {
1350 RefutationArtifactOutcome::Inconclusive {
1351 reason: reason.clone(),
1352 }
1353 }
1354 semantic_memory_forge::RefutationResult::Skipped { reason } => {
1355 RefutationArtifactOutcome::Skipped {
1356 reason: reason.clone(),
1357 }
1358 }
1359 },
1360 estimate_delta: artifact.estimate_delta,
1361 details: artifact.details.clone(),
1362 })
1363 .collect()
1364 },
1365 sealed: authoring
1366 .as_ref()
1367 .map(|authoring| authoring.sealed)
1368 .unwrap_or(false),
1369 })
1370 }
1371
1372 pub fn render_causal_question(
1374 patch_summary: &str,
1375 outcome_summary: &str,
1376 workload_id: &str,
1377 scope_summary: &str,
1378 ) -> String {
1379 format!(
1380 "Did patch {} change outcome {} on workload {} under scope {}?",
1381 patch_summary, outcome_summary, workload_id, scope_summary
1382 )
1383 }
1384
1385 fn debug_name_to_snake_case(name: &str) -> String {
1386 let mut rendered = String::with_capacity(name.len() + 4);
1387 for (idx, ch) in name.chars().enumerate() {
1388 if ch.is_uppercase() {
1389 if idx > 0 {
1390 rendered.push('_');
1391 }
1392 for lower in ch.to_lowercase() {
1393 rendered.push(lower);
1394 }
1395 } else {
1396 rendered.push(ch);
1397 }
1398 }
1399 rendered
1400 }
1401
1402 pub fn superseded_effect_relation_version_id(
1404 &self,
1405 source: EffectRelationLineageSource,
1406 effect: &crate::experiment::TypedLocatedEffect,
1407 ) -> Option<RelationVersionId> {
1408 self.relation_lineage_hints
1409 .effect_supersedes_relation_version_id(source, effect)
1410 }
1411
1412 pub fn superseded_hypothesis_relation_version_id(
1414 &self,
1415 edge: &HypothesisEdge,
1416 ) -> Option<RelationVersionId> {
1417 self.relation_lineage_hints
1418 .hypothesis_supersedes_relation_version_id(edge)
1419 }
1420
1421 pub fn superseded_verification_trial_relation_version_id(
1423 &self,
1424 trial: &VerificationTrial,
1425 ) -> Option<RelationVersionId> {
1426 self.relation_lineage_hints
1427 .verification_trial_supersedes_relation_version_id(trial)
1428 }
1429
1430 pub fn superseded_refutation_relation_version_id(
1432 &self,
1433 artifact: &RefutationArtifact,
1434 ) -> Option<RelationVersionId> {
1435 self.relation_lineage_hints
1436 .refutation_supersedes_relation_version_id(artifact)
1437 }
1438
1439 pub fn to_episode_meta(&self) -> serde_json::Value {
1441 serde_json::json!({
1442 "type": "forge_evidence",
1443 "bundle_id": self.bundle_id,
1444 "candidate_id": self.candidate_id,
1445 "eval_id": self.eval_id,
1446 "version_id": self.version_id,
1447 "supersedes_claim_version_id": self.supersedes_claim_version_id,
1448 "trace_id": self.trace_id,
1449 "hypothesis_count": self.hypotheses.len(),
1450 "hypothesis_edge_count": self.hypothesis_edges.len(),
1451 "has_verification": self.verification.is_some(),
1452 "verification_trial_count": self.verification_trials.len(),
1453 "refutation_artifact_count": self.refutation_artifacts.len(),
1454 "claim_strength": format!("{}", self.claim_strength),
1455 "run_id": self.run_id,
1456 "attempt_id": self.attempt_id,
1457 "treatment": self.treatment,
1458 "outcome": self.outcome,
1459 "confounders": self.known_threats,
1460 "identification_rationale": self.identification_rationale,
1461 "patch_hash": self.patch_hash,
1462 "estimator_metadata": serde_json::json!({
1463 "weighted_total": self.scores.weighted_total,
1464 "cea_confidence": self.scores.cea_confidence,
1465 "cea_predicted_correctness": self.scores.cea_predicted_correctness,
1466 }),
1467 "sealed": self.sealed,
1468 })
1469 }
1470
1471 pub fn to_episode_content(&self) -> String {
1477 let mut parts = Vec::new();
1478
1479 parts.push(format!(
1481 "Evidence bundle {} for candidate {} (eval {})",
1482 self.bundle_id, self.candidate_id, self.eval_id
1483 ));
1484
1485 parts.push(format!("Claim strength: {}", self.claim_strength));
1487
1488 if let Some(ref outcome) = self.outcome {
1489 parts.push(format!("Outcome: {outcome}"));
1490 }
1491
1492 if let Some(ref rationale) = self.identification_rationale {
1493 parts.push(format!("Identification rationale: {rationale}"));
1494 }
1495
1496 if let Some(ref q) = self.causal_question {
1498 parts.push(format!("Causal question: {q}"));
1499 }
1500
1501 if let Some(ref t) = self.treatment {
1503 parts.push(format!(
1504 "Treatment: kind={kind}, patch_hash={patch_hash}, summary={summary}",
1505 kind = t.kind,
1506 patch_hash = t.patch_hash,
1507 summary = t.patch_summary
1508 ));
1509 }
1510
1511 if let Some(ref eff) = self.primary_effect {
1513 parts.push(format!("Primary effect: {:?} - {}", eff.kind, eff.message));
1514 }
1515
1516 parts.push(format!(
1518 "Scores: correctness={:.2}, novelty={:.2}, stability={:.2}, total={:.2}",
1519 self.scores.correctness,
1520 self.scores.novelty,
1521 self.scores.stability,
1522 self.scores.weighted_total,
1523 ));
1524
1525 if let Some(ref cov) = self.covariates {
1527 parts.push(format!("Workload: {}", cov.workload_id));
1528 parts.push(format!("Checks: {}", cov.selected_checks.join(", ")));
1529 if cov.adjacent_edits {
1530 parts.push("Adjacent edits present".to_string());
1531 }
1532 if !cov.adjacent_edit_signatures.is_empty() {
1533 parts.push(format!(
1534 "Adjacent edits: {}",
1535 cov.adjacent_edit_signatures.join(", ")
1536 ));
1537 }
1538 }
1539
1540 if !self.known_threats.is_empty() {
1541 parts.push(format!(
1542 "Confounders / threats: {}",
1543 self.known_threats.join(", ")
1544 ));
1545 }
1546
1547 for edge in &self.hypothesis_edges {
1549 parts.push(format!(
1550 "Edge {}: {:?} {:?} (confidence={:.2})",
1551 edge.edge_id, edge.kind, edge.status, edge.confidence
1552 ));
1553 }
1554
1555 for h in &self.hypotheses {
1557 parts.push(format!(
1558 "Hypothesis {}: {:?} (confidence={:.2}, support={}, contradictions={})",
1559 h.hypothesis_id, h.status, h.confidence, h.support_count, h.contradiction_count
1560 ));
1561 }
1562
1563 for trial in &self.verification_trials {
1565 parts.push(format!(
1566 "Trial {} / attempt {}: {:?} completed={}",
1567 trial.trial_id, trial.attempt_id, trial.baseline_or_patch, trial.completed
1568 ));
1569 }
1570
1571 for artifact in &self.refutation_artifacts {
1573 let outcome = match &artifact.outcome {
1574 RefutationArtifactOutcome::Passed => "passed".to_string(),
1575 RefutationArtifactOutcome::Failed { reason } => format!("failed: {reason}"),
1576 RefutationArtifactOutcome::Inconclusive { reason } => {
1577 format!("inconclusive: {reason}")
1578 }
1579 RefutationArtifactOutcome::Skipped { reason } => format!("skipped: {reason}"),
1580 };
1581 parts.push(format!(
1582 "Refutation {:?} {}",
1583 artifact.artifact_type, outcome
1584 ));
1585 }
1586
1587 if let Some(ref plan) = self.verification {
1589 let required_count = plan
1590 .steps
1591 .iter()
1592 .filter(|s| s.requirement == StepRequirement::Required)
1593 .count();
1594 parts.push(format!(
1595 "Verification: {} steps ({} required)",
1596 plan.steps.len(),
1597 required_count,
1598 ));
1599 if let Some(ref budget) = plan.budget {
1600 parts.push(format!(
1601 "Plan budget: max {} steps, est. {}s",
1602 budget.max_steps, budget.estimated_duration_secs
1603 ));
1604 }
1605 }
1606
1607 parts.join("\n")
1608 }
1609}
1610
1611fn default_bundle_created_at() -> String {
1612 chrono::Utc::now().to_rfc3339()
1613}
1614
1615#[path = "evidence_analysis.rs"]
1616mod evidence_analysis;
1617
1618#[allow(deprecated)]
1619pub use evidence_analysis::{
1620 build_hypothesis_edges, compute_assessment, compute_confidence, derive_status,
1621 generate_verification_plan, local_hypothesis_support_confidence, update_hypotheses_from_diff,
1622 AssessmentCategory, CausalHypothesis, ContradictionState, DroppedStep, EvidenceAssessment,
1623 ExperimentRunner, HypothesisStatus, LocalExperimentRunner, PairComparability, PlanBudget,
1624 SampleSupport, StepRequirement, VerificationPlan, VerificationPolicy, VerificationStep,
1625 VerificationType,
1626};