///|
pub fn format_money_micro(value : Int) -> String {
let sign = if value < 0 { "-" } else { "" }
let magnitude = absolute(value)
let whole = magnitude / 1000000
let cents = magnitude % 1000000 / 10000
let cents_text = if cents < 10 { "0\{cents}" } else { cents.to_string() }
"\{sign}\{whole}.\{cents_text}"
}
///|
pub fn format_energy_wh(value : Int) -> String {
if absolute(value) >= 1000 {
let whole = value / 1000
let decimal = absolute(value % 1000) / 100
"\{whole}.\{decimal} kWh"
} else {
"\{value} Wh"
}
}
///|
pub fn format_slot(slot : Int, slot_minutes : Int) -> String {
let total_minutes = slot * slot_minutes
let day = total_minutes / 1440
let in_day = total_minutes % 1440
let hour = in_day / 60
let minute = in_day % 60
let hour_text = if hour < 10 { "0\{hour}" } else { hour.to_string() }
let minute_text = if minute < 10 { "0\{minute}" } else { minute.to_string() }
let day_text = if day > 0 { "D\{day} " } else { "" }
"\{day_text}\{hour_text}:\{minute_text}"
}
///|
fn repeat_character(character : String, count : Int) -> String {
let buffer = StringBuilder()
for index = 0; index < count; index = index + 1 {
buffer <+ "\{character}"
}
buffer.to_string()
}
///|
fn bar(value : Int, maximum_value : Int, width : Int) -> String {
if maximum_value <= 0 || value <= 0 {
repeat_character(".", width)
} else {
let filled = clamp(value * width / maximum_value, 0, width)
repeat_character("#", filled) + repeat_character(".", width - filled)
}
}
///|
pub fn plan_timeline(result : PlanResult) -> String {
let buffer = StringBuilder()
buffer <+ "slot time load grid solar missing battery\n"
buffer <+ "---- ----- ------- ------- ------- ------- -------\n"
for slot = 0; slot < result.horizon_slots; slot = slot + 1 {
let battery = if slot + 1 < result.battery_state_wh.length() {
result.battery_state_wh[slot + 1]
} else {
0
}
let time = format_slot(slot, result.slot_minutes)
let load = result.load_w[slot]
let grid = result.grid_w[slot]
let solar = result.solar_used_w[slot]
let missing = result.unserved_w[slot]
buffer <+
"\{slot}\t\{time}\t\{load}\t\{grid}\t\{solar}\t\{missing}\t\{battery}\n"
}
buffer.to_string()
}
///|
pub fn plan_ascii_chart(result : PlanResult, width? : Int = 28) -> String {
let buffer = StringBuilder()
let maximum_load = result.load_w.fold(init=1, fn(current, value) {
maximum(current, value)
})
buffer <+ "Energy profile (each bar scaled to peak load)\n"
for slot = 0; slot < result.horizon_slots; slot = slot + 1 {
let time = format_slot(slot, result.slot_minutes)
let chart = bar(result.load_w[slot], maximum_load, width)
let load = result.load_w[slot]
buffer <+ "\{time} \{chart} \{load} W"
if result.unserved_w[slot] > 0 {
let missing = result.unserved_w[slot]
buffer <+ " ! missing \{missing} W"
}
buffer <+ "\n"
}
buffer.to_string()
}
///|
fn schedule_markdown(
schedule : Array[ScheduleEntry],
slot_minutes : Int,
) -> String {
let buffer = StringBuilder()
buffer <+ "| Task | Priority | Start | End | Power | Energy | Reason |\n"
buffer <+ "|---|---:|---:|---:|---:|---:|---|\n"
for entry in schedule {
let priority = entry.priority.label()
let start = format_slot(entry.start_slot, slot_minutes)
let end = format_slot(entry.end_slot, slot_minutes)
let energy = format_energy_wh(entry.energy_wh)
buffer <+
"| \{entry.task_name} | \{priority} | \{start} | \{end} | \{entry.power_w} W | \{energy} | \{entry.reason} |\n"
}
buffer.to_string()
}
///|
fn metrics_markdown(metrics : PlanMetrics) -> String {
let buffer = StringBuilder()
let cost = format_money_micro(metrics.net_cost_micro())
let imported = format_energy_wh(metrics.imported_energy_wh)
let solar = format_energy_wh(metrics.solar_used_wh)
let curtailed = format_energy_wh(metrics.solar_curtailed_wh)
let charged = format_energy_wh(metrics.battery_charged_wh)
let discharged = format_energy_wh(metrics.battery_discharged_wh)
let unserved = format_energy_wh(metrics.unserved_energy_wh)
let critical = format_energy_wh(metrics.critical_unserved_wh)
buffer <+ "| Metric | Value |\n"
buffer <+ "|---|---:|\n"
buffer <+ "| Net energy cost | \{cost} |\n"
buffer <+ "| Grid import | \{imported} |\n"
buffer <+ "| Solar used | \{solar} |\n"
buffer <+ "| Solar curtailed | \{curtailed} |\n"
buffer <+ "| Battery charged | \{charged} |\n"
buffer <+ "| Battery discharged | \{discharged} |\n"
buffer <+ "| Carbon | \{metrics.carbon_g} g |\n"
buffer <+ "| Peak grid power | \{metrics.peak_grid_w} W |\n"
buffer <+ "| Unserved energy | \{unserved} |\n"
buffer <+ "| Critical unserved | \{critical} |\n"
buffer <+ "| Resilience | \{metrics.resilience_permille} permille |\n"
buffer <+ "| Objective score | \{metrics.score} |\n"
buffer.to_string()
}
///|
fn explanation_markdown(explanations : Array[Explanation]) -> String {
let buffer = StringBuilder()
for item in explanations {
let slot_text = match item.slot {
Some(slot) => " (slot \{slot})"
None => ""
}
buffer <+
"- **\{item.severity} / \{item.code}** - \{item.subject}\{slot_text}: \{item.message}\n"
}
buffer.to_string()
}
///|
/// Render a complete review-friendly Markdown plan report.
pub fn plan_markdown(result : PlanResult) -> String {
let buffer = StringBuilder()
let status = result.status.label()
let metrics = metrics_markdown(result.metrics)
let schedule = schedule_markdown(result.schedule, result.slot_minutes)
let explanations = explanation_markdown(result.explanations)
buffer <+ "# \{result.title}\n\n"
buffer <+ "- Status: **\{status}**\n"
buffer <+
"- Horizon: \{result.horizon_slots} slots x \{result.slot_minutes} minutes\n"
buffer <+ "- Scheduled entries: \{result.schedule.length()}\n"
buffer <+ "- Skipped tasks: \{result.skipped_task_ids.length()}\n\n"
buffer <+ "## Metrics\n\n\{metrics}"
buffer <+ "\n## Schedule\n\n\{schedule}"
buffer <+ "\n## Decision explanations\n\n\{explanations}"
buffer.to_string()
}
///|
pub fn plan_csv(result : PlanResult) -> String {
let buffer = StringBuilder()
buffer <+ "slot,time,load_w,grid_w,solar_used_w,unserved_w,battery_state_wh\n"
for slot = 0; slot < result.horizon_slots; slot = slot + 1 {
let state = if slot + 1 < result.battery_state_wh.length() {
result.battery_state_wh[slot + 1]
} else {
0
}
let time = format_slot(slot, result.slot_minutes)
let load = result.load_w[slot]
let grid = result.grid_w[slot]
let solar = result.solar_used_w[slot]
let missing = result.unserved_w[slot]
buffer <+ "\{slot},\{time},\{load},\{grid},\{solar},\{missing},\{state}\n"
}
buffer.to_string()
}
///|
fn distribution_row(
name : String,
summary : DistributionSummary,
unit : String,
) -> String {
"| \{name} | \{summary.minimum} | \{summary.median} | \{summary.p90} | \{summary.p95} | \{summary.maximum} | \{summary.mean} | \{unit} |\n"
}
///|
pub fn simulation_markdown(summary : SimulationSummary) -> String {
let buffer = StringBuilder()
let risk = summary.risk_band.label()
let cost = distribution_row("Cost", summary.cost_micro, "micro")
let carbon = distribution_row("Carbon", summary.carbon_g, "g")
let unserved = distribution_row(
"Unserved energy",
summary.unserved_energy_wh,
"Wh",
)
let critical = distribution_row(
"Critical unserved",
summary.critical_unserved_wh,
"Wh",
)
let peak = distribution_row("Peak grid", summary.peak_grid_w, "W")
let resilience = distribution_row(
"Resilience",
summary.resilience_permille,
"permille",
)
buffer <+ "# \{summary.title}\n\n"
buffer <+ "- Risk band: **\{risk}**\n"
buffer <+ "- Completed runs: \{summary.completed_runs}\n"
buffer <+ "- Feasibility: \{summary.feasibility_permille()} permille\n"
buffer <+ "- Worst sample: \{summary.worst_run_index}\n\n"
buffer <+ "## Distributions\n\n"
buffer <+ "| Metric | Min | Median | P90 | P95 | Max | Mean | Unit |\n"
buffer <+ "|---|---:|---:|---:|---:|---:|---:|---|\n"
buffer <+ "\{cost}\{carbon}\{unserved}\{critical}\{peak}\{resilience}"
buffer <+ "\n## Recommendations\n\n"
for recommendation in summary.recommendations {
buffer <+ "- \{recommendation}\n"
}
buffer.to_string()
}
///|
pub fn comparison_markdown(comparison : PlanComparison) -> String {
let buffer = StringBuilder()
let baseline_cost = format_money_micro(
comparison.baseline.metrics.net_cost_micro(),
)
let optimized_cost = format_money_micro(
comparison.optimized.metrics.net_cost_micro(),
)
let saving = format_money_micro(comparison.cost_saving_micro)
buffer <+ "# Baseline comparison\n\n"
buffer <+ "| Metric | Preferred baseline | Optimized | Difference |\n"
buffer <+ "|---|---:|---:|---:|\n"
buffer <+
"| Net cost | \{baseline_cost} | \{optimized_cost} | \{saving} saved |\n"
buffer <+
"| Carbon | \{comparison.baseline.metrics.carbon_g} g | \{comparison.optimized.metrics.carbon_g} g | \{comparison.carbon_saving_g} g saved |\n"
buffer <+
"| Peak grid | \{comparison.baseline.metrics.peak_grid_w} W | \{comparison.optimized.metrics.peak_grid_w} W | \{comparison.peak_reduction_w} W reduced |\n"
buffer <+
"| Resilience | \{comparison.baseline.metrics.resilience_permille} | \{comparison.optimized.metrics.resilience_permille} | \{comparison.resilience_gain_permille} gain |\n\n"
buffer <+ "**Recommendation:** \{comparison.recommendation}\n"
buffer.to_string()
}
///|
pub fn validation_markdown(report : ValidationReport) -> String {
let buffer = StringBuilder()
buffer <+ "# Input validation\n\n"
buffer <+ "- Valid: **\{report.is_valid()}**\n"
buffer <+ "- Errors: \{report.error_count()}\n"
buffer <+ "- Warnings: \{report.warning_count()}\n"
buffer <+ "- Advice: \{report.advice_count()}\n\n"
for issue in report.issues {
let code = issue.code.label()
let level = issue.level.label()
buffer <+
"- `\{code}` **\{level}** at `\{issue.path}`: \{issue.message} _\{issue.hint}_\n"
}
buffer.to_string()
}