///|
/// JSON string escaping for stable dependency-free reports.
pub fn json_escape(value : String) -> String {
let builder = StringBuilder::new()
for char in value {
match char {
'"' => builder.write_string("\\\"")
'\\' => builder.write_string("\\\\")
'\n' => builder.write_string("\\n")
'\r' => builder.write_string("\\r")
'\t' => builder.write_string("\\t")
'\b' => builder.write_string("\\b")
'\u{0C}' => builder.write_string("\\f")
_ => builder.write_char(char)
}
}
builder.to_string()
}
///|
/// Write a quoted JSON string.
fn write_json_string(builder : StringBuilder, value : String) -> Unit {
builder.write_char('"')
builder.write_string(json_escape(value))
builder.write_char('"')
}
///|
/// Write an optional JSON string.
fn write_optional_string(builder : StringBuilder, value : String?) -> Unit {
match value {
Some(text) => write_json_string(builder, text)
None => builder.write_string("null")
}
}
///|
/// Write an optional double.
fn write_optional_double(builder : StringBuilder, value : Double?) -> Unit {
match value {
Some(number) => builder.write_object(number)
None => builder.write_string("null")
}
}
///|
/// Write an optional timestamp in ISO-8601 form.
fn write_optional_timestamp(builder : StringBuilder, value : Int64?) -> Unit {
match value {
Some(timestamp) => write_json_string(builder, format_iso8601(timestamp))
None => builder.write_string("null")
}
}
///|
/// Serialize a diagnostic as JSON.
fn write_diagnostic_json(
builder : StringBuilder,
diagnostic : Diagnostic,
) -> Unit {
builder.write_string("{\"level\":")
write_json_string(builder, diagnostic_level_name(diagnostic.level))
builder.write_string(",\"code\":")
write_json_string(builder, diagnostic.code)
builder.write_string(",\"message\":")
write_json_string(builder, diagnostic.message)
builder.write_string(",\"line\":")
match diagnostic.line {
Some(line) => builder.write_object(line)
None => builder.write_string("null")
}
builder.write_string(",\"sensor_id\":")
write_optional_string(builder, diagnostic.sensor_id)
builder.write_char('}')
}
///|
/// Serialize one event as JSON.
fn write_event_json(builder : StringBuilder, event : ExcursionEvent) -> Unit {
builder.write_string("{\"event_id\":")
write_json_string(builder, event.event_id)
builder.write_string(",\"sensor_id\":")
write_json_string(builder, event.sensor_id)
builder.write_string(",\"kind\":")
write_json_string(builder, excursion_kind_name(event.kind))
builder.write_string(",\"status\":")
write_json_string(
builder,
match event.status {
Candidate => "candidate"
Confirmed => "confirmed"
Suppressed => "suppressed"
Closed => "closed"
},
)
builder.write_string(",\"started_at\":")
write_json_string(builder, format_iso8601(event.started_at))
builder.write_string(",\"ended_at\":")
write_json_string(builder, format_iso8601(event.ended_at))
builder.write_string(",\"duration_seconds\":")
builder.write_object(event.duration_seconds)
builder.write_string(",\"sample_count\":")
builder.write_object(event.sample_count)
builder.write_string(",\"minimum_c\":")
write_optional_double(builder, event.minimum_c)
builder.write_string(",\"maximum_c\":")
write_optional_double(builder, event.maximum_c)
builder.write_string(",\"mean_c\":")
write_optional_double(builder, event.mean_c)
builder.write_string(",\"degree_seconds\":")
builder.write_object(event.degree_seconds)
builder.write_string(",\"peak_deviation_c\":")
builder.write_object(event.peak_deviation_c)
builder.write_string(",\"reason\":")
write_json_string(builder, event.reason)
builder.write_char('}')
}
///|
/// Serialize one statistics record as JSON.
fn write_statistics_json(
builder : StringBuilder,
statistics : SensorStatistics,
) -> Unit {
builder.write_string("{\"sensor_id\":")
write_json_string(builder, statistics.sensor_id)
builder.write_string(",\"sample_count\":")
builder.write_object(statistics.sample_count)
builder.write_string(",\"usable_count\":")
builder.write_object(statistics.usable_count)
builder.write_string(",\"first_timestamp\":")
write_optional_timestamp(builder, statistics.first_timestamp)
builder.write_string(",\"last_timestamp\":")
write_optional_timestamp(builder, statistics.last_timestamp)
builder.write_string(",\"minimum_c\":")
write_optional_double(builder, statistics.minimum_c)
builder.write_string(",\"maximum_c\":")
write_optional_double(builder, statistics.maximum_c)
builder.write_string(",\"mean_c\":")
write_optional_double(builder, statistics.mean_c)
builder.write_string(",\"time_weighted_mean_c\":")
write_optional_double(builder, statistics.time_weighted_mean_c)
builder.write_string(",\"mean_kinetic_temperature_c\":")
write_optional_double(builder, statistics.mean_kinetic_temperature_c)
builder.write_string(",\"standard_deviation_c\":")
write_optional_double(builder, statistics.standard_deviation_c)
builder.write_string(",\"median_c\":")
write_optional_double(builder, statistics.median_c)
builder.write_string(",\"p05_c\":")
write_optional_double(builder, statistics.p05_c)
builder.write_string(",\"p95_c\":")
write_optional_double(builder, statistics.p95_c)
builder.write_string(",\"in_range_seconds\":")
builder.write_object(statistics.in_range_seconds)
builder.write_string(",\"low_seconds\":")
builder.write_object(statistics.low_seconds)
builder.write_string(",\"high_seconds\":")
builder.write_object(statistics.high_seconds)
builder.write_string(",\"unknown_seconds\":")
builder.write_object(statistics.unknown_seconds)
builder.write_string(",\"low_degree_seconds\":")
builder.write_object(statistics.low_degree_seconds)
builder.write_string(",\"high_degree_seconds\":")
builder.write_object(statistics.high_degree_seconds)
builder.write_char('}')
}
///|
/// Serialize one sensor health summary as JSON.
fn write_health_json(builder : StringBuilder, health : SensorHealth) -> Unit {
builder.write_string("{\"sensor_id\":")
write_json_string(builder, health.sensor_id)
builder.write_string(",\"score\":")
builder.write_object(health.score)
builder.write_string(",\"stuck_runs\":")
builder.write_object(health.stuck_runs)
builder.write_string(",\"noisy_steps\":")
builder.write_object(health.noisy_steps)
builder.write_string(",\"drift_windows\":")
builder.write_object(health.drift_windows)
builder.write_string(",\"gaps\":")
builder.write_object(health.gaps)
builder.write_string(",\"conflicts\":")
builder.write_object(health.conflicts)
builder.write_string(",\"low_battery_samples\":")
builder.write_object(health.low_battery_samples)
builder.write_string(",\"flagged_samples\":")
builder.write_object(health.flagged_samples)
builder.write_string(",\"observations\":[")
for index = 0; index < health.observations.length(); index = index + 1 {
if index > 0 {
builder.write_char(',')
}
write_json_string(builder, health.observations[index])
}
builder.write_string("]}")
}
///|
/// Serialize risk details as JSON.
fn write_risk_json(builder : StringBuilder, risk : RiskAssessment) -> Unit {
builder.write_string("{\"score\":")
builder.write_object(risk.score)
builder.write_string(",\"band\":")
write_json_string(builder, risk_band_name(risk.band))
builder.write_string(",\"confidence_percent\":")
builder.write_object(risk.confidence_percent)
builder.write_string(",\"temperature_component\":")
builder.write_object(risk.temperature_component)
builder.write_string(",\"duration_component\":")
builder.write_object(risk.duration_component)
builder.write_string(",\"data_quality_component\":")
builder.write_object(risk.data_quality_component)
builder.write_string(",\"sensor_health_component\":")
builder.write_object(risk.sensor_health_component)
builder.write_string(",\"reasons\":[")
for index = 0; index < risk.reasons.length(); index = index + 1 {
if index > 0 {
builder.write_char(',')
}
write_json_string(builder, risk.reasons[index])
}
builder.write_string("]}")
}
///|
/// Stable complete JSON report.
pub fn report_to_json(report : AnalysisReport) -> String {
let builder = StringBuilder::new()
builder.write_string("{\"schema\":")
write_json_string(builder, report.schema)
builder.write_string(",\"shipment_id\":")
write_json_string(builder, report.shipment_id)
builder.write_string(",\"source_name\":")
write_json_string(builder, report.source_name)
builder.write_string(",\"generated_at\":")
write_json_string(builder, format_iso8601(report.generated_at))
builder.write_string(",\"sensor_ids\":[")
for index = 0; index < report.sensor_ids.length(); index = index + 1 {
if index > 0 {
builder.write_char(',')
}
write_json_string(builder, report.sensor_ids[index])
}
builder.write_string("],\"summary\":{\"reading_count\":")
builder.write_object(report.readings.length())
builder.write_string(",\"gap_count\":")
builder.write_object(report.gaps.length())
builder.write_string(",\"event_count\":")
builder.write_object(report.events.length())
builder.write_string("},\"risk\":")
write_risk_json(builder, report.risk)
builder.write_string(",\"statistics\":[")
for index = 0; index < report.statistics.length(); index = index + 1 {
if index > 0 {
builder.write_char(',')
}
write_statistics_json(builder, report.statistics[index])
}
builder.write_string("],\"health\":[")
for index = 0; index < report.health.length(); index = index + 1 {
if index > 0 {
builder.write_char(',')
}
write_health_json(builder, report.health[index])
}
builder.write_string("],\"events\":[")
for index = 0; index < report.events.length(); index = index + 1 {
if index > 0 {
builder.write_char(',')
}
write_event_json(builder, report.events[index])
}
builder.write_string("],\"diagnostics\":[")
for index = 0; index < report.diagnostics.length(); index = index + 1 {
if index > 0 {
builder.write_char(',')
}
write_diagnostic_json(builder, report.diagnostics[index])
}
builder.write_string("]}")
builder.to_string()
}
///|
/// Compact terminal summary.
pub fn report_summary(report : AnalysisReport) -> String {
let builder = StringBuilder::new()
builder.write_string("ThermoTrail analysis for ")
builder.write_string(report.shipment_id)
builder.write_char('\n')
builder.write_string("Risk: ")
builder.write_string(risk_band_name(report.risk.band))
builder.write_string(" (")
builder.write_object(report.risk.score)
builder.write_string("/100, confidence ")
builder.write_object(report.risk.confidence_percent)
builder.write_string("%)\nSensors: ")
builder.write_object(report.sensor_ids.length())
builder.write_string(", readings: ")
builder.write_object(report.readings.length())
builder.write_string(", events: ")
builder.write_object(report.events.length())
builder.write_string(", gaps: ")
builder.write_object(report.gaps.length())
builder.write_char('\n')
for reason in report.risk.reasons {
builder.write_string("- ")
builder.write_string(reason)
builder.write_char('\n')
}
builder.to_string()
}
///|
/// Human-reviewable Markdown report.
pub fn report_to_markdown(report : AnalysisReport) -> String {
let builder = StringBuilder::new()
builder.write_string("# ThermoTrail 冷链分析报告\n\n")
builder.write_string("- 运输编号:")
builder.write_string(report.shipment_id)
builder.write_string("\n- 数据来源:")
builder.write_string(report.source_name)
builder.write_string("\n- 生成时间:")
builder.write_string(format_iso8601(report.generated_at))
builder.write_string("\n- 报告格式:")
builder.write_string(report.schema)
builder.write_string("\n\n## 风险结论\n\n")
builder.write_string("- 风险等级:**")
builder.write_string(risk_band_name(report.risk.band))
builder.write_string("**\n- 风险分数:")
builder.write_object(report.risk.score)
builder.write_string("/100\n- 数据置信度:")
builder.write_object(report.risk.confidence_percent)
builder.write_string("%\n\n")
for reason in report.risk.reasons {
builder.write_string("- ")
builder.write_string(reason)
builder.write_char('\n')
}
builder.write_string("\n## 传感器统计\n\n")
builder.write_string(
"| 传感器 | 样本 | 最低温 | 最高温 | 平均温 | MKT | 范围内时长 |\n",
)
builder.write_string("|---|---:|---:|---:|---:|---:|---:|\n")
for item in report.statistics {
builder.write_string("| ")
builder.write_string(item.sensor_id)
builder.write_string(" | ")
builder.write_object(item.sample_count)
builder.write_string(" | ")
match item.minimum_c {
Some(value) => builder.write_object(value)
None => builder.write_string("—")
}
builder.write_string(" | ")
match item.maximum_c {
Some(value) => builder.write_object(value)
None => builder.write_string("—")
}
builder.write_string(" | ")
match item.mean_c {
Some(value) => builder.write_object(value)
None => builder.write_string("—")
}
builder.write_string(" | ")
match item.mean_kinetic_temperature_c {
Some(value) => builder.write_object(value)
None => builder.write_string("—")
}
builder.write_string(" | ")
builder.write_string(format_duration(item.in_range_seconds))
builder.write_string(" |\n")
}
builder.write_string("\n## 事件时间线\n\n")
if report.events.length() == 0 {
builder.write_string("未检测到温度偏差或数据缺口事件。\n")
} else {
builder.write_string(
"| 开始 | 传感器 | 类型 | 状态 | 时长 | 峰值偏差 |\n",
)
builder.write_string("|---|---|---|---|---:|---:|\n")
for event in report.events {
builder.write_string("| ")
builder.write_string(format_iso8601(event.started_at))
builder.write_string(" | ")
builder.write_string(event.sensor_id)
builder.write_string(" | ")
builder.write_string(excursion_kind_name(event.kind))
builder.write_string(" | ")
builder.write_string(
match event.status {
Candidate => "candidate"
Confirmed => "confirmed"
Suppressed => "suppressed"
Closed => "closed"
},
)
builder.write_string(" | ")
builder.write_string(format_duration(event.duration_seconds))
builder.write_string(" | ")
builder.write_object(event.peak_deviation_c)
builder.write_string(" |\n")
}
}
builder.write_string("\n## 传感器健康\n\n")
for health in report.health {
builder.write_string("### ")
builder.write_string(health.sensor_id)
builder.write_string("(")
builder.write_object(health.score)
builder.write_string("/100)\n\n")
for observation in health.observations {
builder.write_string("- ")
builder.write_string(observation)
builder.write_char('\n')
}
builder.write_char('\n')
}
builder.write_string("## 使用限制\n\n")
builder.write_string(
"本报告用于离线风险筛查和技术分析,不替代医疗、食品或监管机构的最终合规判断。\n",
)
builder.to_string()
}
///|
/// Machine-friendly event CSV.
pub fn events_to_csv(events : Array[ExcursionEvent]) -> String {
let builder = StringBuilder::new()
builder.write_string(
"event_id,sensor_id,kind,status,started_at,ended_at,duration_seconds,sample_count,minimum_c,maximum_c,mean_c,degree_seconds,peak_deviation_c,reason\n",
)
for event in events {
builder.write_string(csv_escape(event.event_id))
builder.write_char(',')
builder.write_string(csv_escape(event.sensor_id))
builder.write_char(',')
builder.write_string(excursion_kind_name(event.kind))
builder.write_char(',')
builder.write_string(
match event.status {
Candidate => "candidate"
Confirmed => "confirmed"
Suppressed => "suppressed"
Closed => "closed"
},
)
builder.write_char(',')
builder.write_string(format_iso8601(event.started_at))
builder.write_char(',')
builder.write_string(format_iso8601(event.ended_at))
builder.write_char(',')
builder.write_object(event.duration_seconds)
builder.write_char(',')
builder.write_object(event.sample_count)
builder.write_char(',')
match event.minimum_c {
Some(value) => builder.write_object(value)
None => ()
}
builder.write_char(',')
match event.maximum_c {
Some(value) => builder.write_object(value)
None => ()
}
builder.write_char(',')
match event.mean_c {
Some(value) => builder.write_object(value)
None => ()
}
builder.write_char(',')
builder.write_object(event.degree_seconds)
builder.write_char(',')
builder.write_object(event.peak_deviation_c)
builder.write_char(',')
builder.write_string(csv_escape(event.reason))
builder.write_char('\n')
}
builder.to_string()
}