use crate::diagnostics::MaxDiagnostic;
use lsp_types_max::{Diagnostic, DiagnosticSeverity, NumberOrString};
use serde::{Deserialize, Serialize};
pub const METHOD_PIPELINE_SEARCH: &str = "max/pipelineSearch";
pub const METHOD_PIPELINE_EVALUATE: &str = "max/pipelineEvaluate";
pub const METHOD_PIPELINE_BREEDS: &str = "max/pipelineBreeds";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineSearchParams {
pub generations: Option<usize>,
pub population_size: Option<usize>,
pub ocel_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineSearchResultMsg {
pub status: String,
pub best_breeds: Vec<String>,
pub best_fitness: f64,
pub generations_run: usize,
pub evaluations: usize,
pub summary: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineEvaluateParams {
pub breed_id: String,
pub ocel_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineEvaluateResultMsg {
pub status: String,
pub breed_id: String,
pub fitness: f64,
pub summary: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineBreedsParams {
pub filter: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineBreedsResultMsg {
pub status: String,
pub breeds: Vec<String>,
pub pool_size: usize,
pub summary: String,
}
pub const TPOT2_NONCONVERGENCE: &str = "TPOT2-NONCONVERGENCE";
pub const TPOT2_EMPTY_POOL: &str = "TPOT2-EMPTY-POOL";
pub const TPOT2_OCEL_MISSING: &str = "TPOT2-OCEL-MISSING";
pub const STATUS_ADMITTED: &str = "ADMITTED";
pub const STATUS_PARTIAL: &str = "PARTIAL";
pub const STATUS_UNKNOWN: &str = "UNKNOWN";
pub const STATUS_REFUSED: &str = "REFUSED";
fn tpot2_diagnostic(code: &str, severity: DiagnosticSeverity, message: String) -> MaxDiagnostic {
let lsp = Diagnostic {
severity: Some(severity),
code: Some(NumberOrString::String(code.to_string())),
source: Some("lsp-max:tpot2".to_string()),
message,
..Default::default()
};
MaxDiagnostic {
lsp,
diagnostic_id: code.to_string(),
law_id: code.to_string(),
law_axis: crate::conformance::LawAxis::Domain,
violated_axes: vec![crate::conformance::LawAxis::Domain.to_string()],
..Default::default()
}
}
pub fn diagnostics_for_search(
status: &str,
best_fitness: f64,
admission_threshold: f64,
) -> Vec<MaxDiagnostic> {
match status {
STATUS_REFUSED => vec![tpot2_diagnostic(
TPOT2_EMPTY_POOL,
DiagnosticSeverity::ERROR,
"breed pool is empty; status REFUSED — no breeds to evaluate".to_string(),
)],
STATUS_UNKNOWN => vec![
tpot2_diagnostic(
TPOT2_OCEL_MISSING,
DiagnosticSeverity::INFORMATION,
"OCEL observation source absent; status UNKNOWN — admissibility \
cannot be determined and is not coerced to REFUSED or ADMITTED"
.to_string(),
),
tpot2_diagnostic(
TPOT2_NONCONVERGENCE,
DiagnosticSeverity::INFORMATION,
format!(
"pipeline search did not reach admission threshold {admission_threshold}; \
status UNKNOWN — best observed fitness {best_fitness}"
),
),
],
STATUS_PARTIAL if best_fitness < admission_threshold => vec![tpot2_diagnostic(
TPOT2_NONCONVERGENCE,
DiagnosticSeverity::WARNING,
format!(
"pipeline search ended below admission threshold {admission_threshold}; \
status PARTIAL — best observed fitness {best_fitness}"
),
)],
_ => Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn codes(diags: &[MaxDiagnostic]) -> Vec<&str> {
diags
.iter()
.filter_map(|d| match &d.lsp.code {
Some(NumberOrString::String(s)) => Some(s.as_str()),
_ => None,
})
.collect()
}
#[test]
fn partial_below_threshold_emits_nonconvergence() {
let diags = diagnostics_for_search(STATUS_PARTIAL, 0.40, 0.80);
assert_eq!(codes(&diags), vec![TPOT2_NONCONVERGENCE]);
assert_eq!(diags[0].lsp.severity, Some(DiagnosticSeverity::WARNING));
assert_eq!(diags[0].law_axis, crate::conformance::LawAxis::Domain);
}
#[test]
fn refused_empty_pool_emits_empty_pool_error() {
let diags = diagnostics_for_search(STATUS_REFUSED, 0.0, 0.80);
assert_eq!(codes(&diags), vec![TPOT2_EMPTY_POOL]);
assert_eq!(diags[0].lsp.severity, Some(DiagnosticSeverity::ERROR));
}
#[test]
fn admitted_outcome_emits_no_diagnostics() {
let diags = diagnostics_for_search(STATUS_ADMITTED, 0.95, 0.80);
assert!(
diags.is_empty(),
"an admitted outcome must not emit any TPOT2 diagnostic"
);
}
#[test]
fn unknown_path_stays_unknown_and_is_not_coerced() {
let diags = diagnostics_for_search(STATUS_UNKNOWN, 0.0, 0.80);
let emitted = codes(&diags);
assert!(emitted.contains(&TPOT2_OCEL_MISSING));
assert!(emitted.contains(&TPOT2_NONCONVERGENCE));
assert!(
!emitted.contains(&TPOT2_EMPTY_POOL),
"UNKNOWN must never be coerced into the REFUSED (empty-pool) outcome"
);
assert!(
diags
.iter()
.all(|d| d.lsp.severity != Some(DiagnosticSeverity::ERROR)),
"UNKNOWN path must not surface a REFUSED-polarity (Error) diagnostic"
);
}
}