///|
/// Metadata supplied by the caller for a reproducible analysis run.
pub(all) struct AnalysisRequest {
shipment_id : String
source_name : String
generated_at : Int64
readings : Array[Reading]
diagnostics : Array[Diagnostic]
config : AnalysisConfig
} derive(Debug, Eq)
///|
/// Create a request with standard 2–8 °C behavior.
pub fn analysis_request(
shipment_id : String,
source_name : String,
generated_at : Int64,
readings : Array[Reading],
) -> AnalysisRequest {
{
shipment_id,
source_name,
generated_at,
readings,
diagnostics: [],
config: default_analysis_config(),
}
}
///|
/// Execute normalization, sensor health, excursions, metrics and risk.
pub fn analyze(request : AnalysisRequest) -> AnalysisReport {
let normalized = normalize_readings(request.readings, request.config)
let quality = analyze_sensor_quality(
normalized.readings,
normalized.gaps,
request.config,
)
let diagnostics = copy_array(request.diagnostics)
diagnostics.append(normalized.diagnostics)
diagnostics.append(quality.diagnostics)
if request.readings.length() == 0 {
diagnostics.push(
error_diagnostic(
"analysis.no_readings", "analysis input contains no readings",
),
)
}
let events = build_event_timeline(
quality.readings,
normalized.gaps,
request.config.policy,
)
let statistics = compute_all_statistics(
quality.readings,
request.config.policy,
)
let base_risk = assess_risk(
events,
normalized.gaps,
diagnostics,
quality.health,
)
let risk = if quality.readings.length() == 0 {
let reasons = copy_array(base_risk.reasons)
reasons.push("no usable readings were available")
{ ..base_risk, band: Indeterminate, confidence_percent: 0, reasons }
} else {
base_risk
}
{
schema: REPORT_SCHEMA,
shipment_id: request.shipment_id,
generated_at: request.generated_at,
source_name: request.source_name,
sensor_ids: unique_sensor_ids(quality.readings),
readings: quality.readings,
gaps: normalized.gaps,
events,
statistics,
health: quality.health,
risk,
diagnostics,
}
}
///|
/// Parse and analyze CSV in one library call.
pub fn analyze_csv(
shipment_id : String,
source_name : String,
generated_at : Int64,
input : String,
config? : AnalysisConfig = default_analysis_config(),
) -> AnalysisReport {
let batch = parse_readings_csv(input)
analyze({
shipment_id,
source_name,
generated_at,
readings: batch.readings,
diagnostics: batch.diagnostics,
config,
})
}
///|
/// Analyze a named simulation in one call.
pub fn analyze_simulation(
scenario_name : String,
generated_at? : Int64 = 1785916800L,
) -> AnalysisReport {
let simulation = simulate_named(scenario_name)
analyze({
shipment_id: "simulation-\{scenario_name}",
source_name: "deterministic simulation",
generated_at,
readings: simulation.readings,
diagnostics: simulation.diagnostics,
config: default_analysis_config(),
})
}
///|
/// Differences between two analysis runs.
pub(all) struct ReportComparison {
baseline_shipment_id : String
candidate_shipment_id : String
score_change : Int
confidence_change : Int
confirmed_event_change : Int
gap_change : Int
sensor_health_change : Int?
summary : String
} derive(Debug, Eq)
///|
/// Compare average health when both reports contain sensors.
fn average_health_change(
baseline : Array[SensorHealth],
candidate : Array[SensorHealth],
) -> Int? {
match (average_sensor_health(baseline), average_sensor_health(candidate)) {
(Some(left), Some(right)) => Some(right - left)
_ => None
}
}
///|
/// Compare two reports without re-running their analyses.
pub fn compare_reports(
baseline : AnalysisReport,
candidate : AnalysisReport,
) -> ReportComparison {
let score_change = candidate.risk.score - baseline.risk.score
let confidence_change = candidate.risk.confidence_percent -
baseline.risk.confidence_percent
let event_change = confirmed_events(candidate.events).length() -
confirmed_events(baseline.events).length()
let gap_change = candidate.gaps.length() - baseline.gaps.length()
let health_change = average_health_change(baseline.health, candidate.health)
let direction = if score_change > 0 {
"risk increased"
} else if score_change < 0 {
"risk decreased"
} else {
"risk was unchanged"
}
{
baseline_shipment_id: baseline.shipment_id,
candidate_shipment_id: candidate.shipment_id,
score_change,
confidence_change,
confirmed_event_change: event_change,
gap_change,
sensor_health_change: health_change,
summary: "\{direction} by \{abs(score_change)} points",
}
}
///|
/// Local integer absolute value for report comparison.
fn abs(value : Int) -> Int {
if value < 0 {
-value
} else {
value
}
}