Skip to main content

forge_pilot/
decide.rs

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
15/// Selects the next executable candidate from the scored target set.
16pub 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
38/// Builds the public decision-audit artifact for a selected candidate.
39pub 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}