1use crate::config::LoopConfig;
2use crate::history::PilotHistory;
3use crate::orient::TargetCandidate;
4use crate::types::{BlockedStepRecord, DecisionAudit, LawfulStepKind, PlannedStep};
5use semantic_memory_forge::ExactnessLevelV1;
6use verification_control::{CheapCheckKindV1, CheapCheckStatusV1};
7
8#[derive(Debug, Clone)]
9pub struct Decision {
10 pub selected: Option<TargetCandidate>,
11 pub exhausted_candidate_count: usize,
12 pub audit: Option<DecisionAudit>,
13}
14
15pub fn select_candidate(
17 mut candidates: Vec<TargetCandidate>,
18 history: &PilotHistory,
19 config: &LoopConfig,
20) -> Decision {
21 let exhausted_candidate_count = candidates
22 .iter()
23 .filter(|candidate| history.is_exhausted(&candidate.stable_key))
24 .count();
25 candidates.retain(|candidate| !history.is_exhausted(&candidate.stable_key));
26 let selected = candidates
27 .into_iter()
28 .find(|candidate| candidate.urgency >= config.halt_urgency_threshold);
29 let audit = selected.as_ref().map(build_decision_audit);
30
31 Decision {
32 selected,
33 exhausted_candidate_count,
34 audit,
35 }
36}
37
38pub fn build_decision_audit(candidate: &TargetCandidate) -> DecisionAudit {
40 let mut fallback_steps = Vec::new();
41 let mut blocked_steps = Vec::new();
42 let mut cheapest_admissible = None;
43
44 for (index, step_kind) in candidate
45 .target
46 .lawful_step_ladder()
47 .into_iter()
48 .enumerate()
49 {
50 if let Some(reason) = block_reason(candidate, step_kind) {
51 blocked_steps.push(BlockedStepRecord { step_kind, reason });
52 continue;
53 }
54
55 let planned = PlannedStep {
56 step_kind,
57 cost_rank: index as u8,
58 required_artifact_families: required_artifacts(step_kind),
59 };
60 if cheapest_admissible.is_none() {
61 cheapest_admissible = Some(step_kind);
62 } else {
63 fallback_steps.push(planned);
64 }
65 }
66
67 let advisory_only = cheapest_admissible.is_none()
68 || matches!(
69 candidate.plan,
70 crate::act::PlanKind::AdvisoryOnlyVerificationPlan(_)
71 );
72
73 DecisionAudit {
74 stable_target_key: candidate.stable_key.clone(),
75 canonical_case_class: candidate.normalization.canonical_case_class,
76 budget_class: candidate.normalization.budget_class,
77 target_exactness: if candidate.rationale.contains("degradation active") {
78 ExactnessLevelV1::Conservative
79 } else {
80 ExactnessLevelV1::Exact
81 },
82 cheapest_admissible,
83 fallback_steps,
84 blocked_steps,
85 cheap_check_ladder: cheap_check_ladder(candidate),
86 advisory_only,
87 }
88}
89
90fn cheap_check_ladder(candidate: &TargetCandidate) -> Vec<CheapCheckStatusV1> {
91 vec![
92 CheapCheckStatusV1 {
93 check: CheapCheckKindV1::SchemaValidation,
94 satisfied: true,
95 detail: Some("target normalized into canonical verification case".into()),
96 },
97 CheapCheckStatusV1 {
98 check: CheapCheckKindV1::ProvenanceAudit,
99 satisfied: !candidate.rationale.contains("missing provenance"),
100 detail: Some(candidate.rationale.clone()),
101 },
102 CheapCheckStatusV1 {
103 check: CheapCheckKindV1::ReplayPreflight,
104 satisfied: !candidate.rationale.contains("degradation active"),
105 detail: Some(if candidate.rationale.contains("degradation active") {
106 "degraded observation blocks exact replay path".into()
107 } else {
108 "exact replay path remains admissible".into()
109 }),
110 },
111 ]
112}
113
114fn block_reason(candidate: &TargetCandidate, step_kind: LawfulStepKind) -> Option<String> {
115 if candidate.normalization.missing_falsifier
116 && matches!(step_kind, LawfulStepKind::MinimalPerturbationRefuter)
117 {
118 return Some("missing falsifier artifact".into());
119 }
120
121 if candidate.normalization.comparability_required
122 && matches!(
123 step_kind,
124 LawfulStepKind::PairedComparativeCheck | LawfulStepKind::NuisanceComparabilityAudit
125 )
126 && !candidate
127 .check_hints
128 .iter()
129 .any(|hint| hint.contains("comparability"))
130 {
131 return Some("comparability witness set unavailable".into());
132 }
133
134 if candidate.rationale.contains("degradation active")
135 && matches!(
136 step_kind,
137 LawfulStepKind::ExactOracleSlice
138 | LawfulStepKind::ExactReplay
139 | LawfulStepKind::CanonicalExportRequest
140 | LawfulStepKind::CanonicalImportRequest
141 )
142 {
143 return Some("degraded observation blocks exact path".into());
144 }
145
146 None
147}
148
149fn required_artifacts(step_kind: LawfulStepKind) -> Vec<String> {
150 match step_kind {
151 LawfulStepKind::ContractSchemaCheck => vec!["VerificationPlanV1".into()],
152 LawfulStepKind::ProvenanceReceiptAudit => {
153 vec!["RuntimeQueryProvenanceV1".into(), "ControlReceipt".into()]
154 }
155 LawfulStepKind::TemporalConsistencyCheck => vec!["ExecutionContextV1".into()],
156 LawfulStepKind::ExactReplay => vec!["ReplayArtifact".into(), "ExecutionContextV1".into()],
157 LawfulStepKind::PairedComparativeCheck | LawfulStepKind::NuisanceComparabilityAudit => {
158 vec!["ComparableWitnessSet".into()]
159 }
160 LawfulStepKind::ExactOracleSlice | LawfulStepKind::ConservativeOracleSlice => {
161 vec!["RuntimeQueryProvenanceV1".into()]
162 }
163 LawfulStepKind::MinimalPerturbationRefuter => vec!["FalsifierWitness".into()],
164 LawfulStepKind::HumanReviewRequest => Vec::new(),
165 LawfulStepKind::CanonicalExportRequest | LawfulStepKind::CanonicalImportRequest => {
166 vec!["EpisodeBundleV1".into()]
167 }
168 }
169}