fairshift

    Explainable fair workforce rostering and resilience analysis in MoonBit

    scheduling
    rostering
    optimization
    fairness
    workforce
    Download zip
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    2 months ago
    Downloads
    30

    No dependencies

    #FairShift

    FairShift 是一个使用 MoonBit 实现的可解释公平排班优化引擎。它面向小型团队、志愿活动、社区诊所和运维轮值等真实场景,在满足人员可用性、技能覆盖、休息时间和工时上限等硬约束的同时,尽量平衡工作量、夜班、周末班和个人偏好。

    #主要能力

    • 类型化人员、技能、时间区间、班次、需求与偏好模型;
    • 可用时间、重复分配、班次重叠、最短休息、工时上限和连续工作限制;
    • 班次人数与分级技能覆盖约束;
    • 确定性贪心构造与局部搜索优化;
    • 工作量、夜班、周末班、偏好和加班的可解释评分;
    • 候选分配拒绝原因和最终方案解释;
    • CSV 输入、CSV/JSON/文本报告输出;
    • 社区诊所、志愿活动、运维轮值三个可运行场景;
    • 求解审计轨迹、方案比较、容量分析和人员缺席敏感性分析;
    • 18 个自动化测试与 GitHub Actions 持续集成。

    #快速开始

    需要 MoonBit 工具链。确认安装后执行:

    git clone https://github.com/zhou506403/zhou.git cd zhou moon check moon test moon run cmd/main

    CLI 默认运行三日社区志愿活动场景,输出问题名称、可行性、覆盖率、目标值、工作量差距和逐班分配。

    #库使用示例

    let problem = @fairshift.volunteer_scenario()
    let config = @fairshift.SolverConfig::default()
    .with_iterations(400)
    .with_seed(202608)
    let solution = @fairshift.solve_with_config(problem, config)
    println(@fairshift.render_text_report(problem, solution))

    完整流程:

    1. 使用 PlanningProblem 添加人员、技能和班次;
    2. 调用 problem.validate() 检查输入;
    3. 调用 solve 或 solve_with_config;
    4. 使用 explain_solution、summarize_schedule 或导出函数检查结果;
    5. 使用 analyze_capacity 和 analyze_sensitivity 评估排班韧性。

    #约束与目标

    硬约束包括引用有效性、重复分配、人员可用性、班次重叠、最短休息、总工时、连续工作、最低人数和技能数量。软目标综合与目标工时的偏差、工作量离散、夜班与周末班离散、个人偏好和加班成本。ScoreBreakdown 保留每一项,目标值越低越好。

    #CSV 输入

    人员表包含 8 列:

    id,name,min_minutes,target_minutes,max_minutes,max_consecutive,min_rest,skills ada,Ada,0,960,1440,5,600,first-aid:4|lead:3 grace,Grace,0,960,1440,5,600,first-aid:2

    班次表包含 9 列:

    id,label,start,end,required,preferred,kind,weekend,requirements day-1,Day 1,480,960,1,1,day,false,first-aid:2:1 day-2,Day 2,1920,2400,1,1,day,false,

    调用 parse_problem_csv 获得 Parsed(problem) 或带行列信息的 ParseFailed(issues)。当前解析器定位为轻量无依赖入口,字段中不能包含未转义的逗号。

    #输出与分析

    • render_text_report:终端和验收演示;
    • schedule_to_csv、workload_to_csv:结果交换;
    • solution_to_json:结构化方案摘要;
    • solve_audited:方案和审计事件;
    • compare_solutions:比较目标、覆盖率、工作量与变更;
    • analyze_capacity:识别班次及技能容量瓶颈;
    • analyze_sensitivity:逐人模拟缺席并识别单点风险;
    • capacity_recommendations:生成补员、备份和交叉培训建议。

    #开发与验证

    moon fmt --check moon check moon test moon run cmd/main moon info

    项目包含超过 4,000 行非空、非注释 MoonBit 实现。CI 在每次推送和拉取请求时执行格式、检查、测试和示例运行。

    #功能边界

    FairShift 针对数十名人员和数十至数百个班次的小型排班。它不是大型混合整数规划器,也不保证全局最优;目标是在无外部求解器依赖下快速产生确定性、可解释、可维护的方案。医疗、航空等受监管场景必须由专业人员审核。

    #开源与原创声明

    本项目为原创 MoonBit 实现,不移植第三方代码。算法使用通用的约束检查、贪心构造、局部移动搜索和敏感性分析思想。项目不包含第三方素材或数据集,采用 Apache-2.0 许可证。

    #路线图

    • v0.1:模型、约束、求解、解释、CSV、报告与韧性分析;
    • v0.2:完整 RFC 4180 CSV、排班版本和更多局部搜索邻域;
    • v0.3:浏览器 WASM 可视化界面与交互式约束编辑;
    • v1.0:稳定公共 API、性能基准和兼容策略。

    #English

    FairShift is an explainable workforce rostering engine written in MoonBit. It builds deterministic schedules under availability, overlap, rest, workload, staffing and skill constraints, then reports fairness costs and reasons for rejected assignments. The repository includes runnable scenarios, CSV ingestion, text/CSV/JSON reports, audit trails, capacity analysis, absence sensitivity analysis, tests and CI.

    Run moon test and moon run cmd/main to verify the project. See DESIGN.md for architecture details.

    #License

    Apache-2.0. See LICENSE.

    AbsenceImpact

    pub struct AbsenceImpact {
    worker_id : String
    baseline_feasible : Bool
    absence_feasible : Bool
    baseline_score : Int
    absence_score : Int
    score_increase : Int
    baseline_coverage : Int
    absence_coverage : Int
    coverage_loss : Int
    newly_critical_shifts : Int
    severity : RiskLevel
    } derive(Eq, Show, ToJson)

    Assignment

    pub struct Assignment {
    worker_id : String
    shift_id : String
    locked : Bool
    } derive(Eq, Show, ToJson)

    Assignment::locked

    fn Assignment::locked(worker_id : String, shift_id : String) -> Assignment

    Assignment::new

    fn Assignment::new(worker_id : String, shift_id : String) -> Assignment

    AuditEvent

    pub struct AuditEvent {
    sequence : Int
    kind : AuditEventKind
    message : String
    worker_id : String?
    shift_id : String?
    score_before : Int?
    score_after : Int?
    } derive(Eq, Show, ToJson)

    AuditEvent::for_assignment

    fn AuditEvent::for_assignment(self : AuditEvent, worker_id : String, shift_id : String) -> AuditEvent

    AuditEvent::new

    fn AuditEvent::new(sequence : Int, kind : AuditEventKind, message : String) -> AuditEvent

    AuditEvent::with_scores

    fn AuditEvent::with_scores(self : AuditEvent, before : Int, after : Int) -> AuditEvent

    AuditEventKind

    pub(all) enum AuditEventKind {
    ProblemValidated
    CandidateConsidered
    CandidateRejected
    AssignmentAdded
    AssignmentMoved
    ImprovementAccepted
    CoverageChecked
    SolutionCompleted
    } derive(Eq, Show, ToJson)

    AuditTrail

    pub struct AuditTrail {
    events : Array[AuditEvent]
    } derive(Eq, Show, ToJson)

    AuditTrail::empty

    fn AuditTrail::empty() -> AuditTrail

    AuditTrail::record

    fn AuditTrail::record(self : AuditTrail, kind : AuditEventKind, message : String) -> AuditTrail

    AuditTrail::record_assignment

    fn AuditTrail::record_assignment(self : AuditTrail, kind : AuditEventKind, message : String, worker_id : String, shift_id : String) -> AuditTrail

    AuditedSolution

    pub struct AuditedSolution {
    solution : Solution
    audit : AuditTrail
    } derive(Eq, Show, ToJson)

    Candidate

    pub struct Candidate {
    worker_id : String
    shift_id : String
    delta : Int
    tie_breaker : Int
    } derive(Eq, Show, ToJson)

    CapacityRecommendation

    pub struct CapacityRecommendation {
    code : String
    title : String
    detail : String
    priority : Int
    worker_id : String?
    shift_id : String?
    skill_id : String?
    } derive(Eq, Show, ToJson)

    CapacityReport

    pub struct CapacityReport {
    shifts : Array[ShiftCapacity]
    skills : Array[SkillCapacity]
    critical_shifts : Int
    critical_skills : Int
    total_required_slots : Int
    total_eligible_instances : Int
    resilience_score : Int
    } derive(Eq, Show, ToJson)

    ConstraintReport

    pub struct ConstraintReport {
    violations : Array[ConstraintViolation]
    hard_count : Int
    soft_count : Int
    } derive(Eq, Show, ToJson)

    ConstraintReport::add

    ConstraintReport::append

    ConstraintReport::empty

    ConstraintReport::feasible

    fn ConstraintReport::feasible(self : ConstraintReport) -> Bool

    ConstraintViolation

    pub struct ConstraintViolation {
    kind : ViolationKind
    message : String
    worker_id : String?
    shift_id : String?
    hard : Bool
    magnitude : Int
    } derive(Eq, Show, ToJson)

    ConstraintViolation::for_shift

    fn ConstraintViolation::for_shift(self : ConstraintViolation, shift_id : String) -> ConstraintViolation

    ConstraintViolation::for_worker

    fn ConstraintViolation::for_worker(self : ConstraintViolation, worker_id : String) -> ConstraintViolation

    ConstraintViolation::new

    fn ConstraintViolation::new(kind : ViolationKind, message : String, hard : Bool) -> ConstraintViolation

    ConstraintViolation::with_magnitude

    fn ConstraintViolation::with_magnitude(self : ConstraintViolation, magnitude : Int) -> ConstraintViolation

    CsvIssue

    pub struct CsvIssue {
    source : String
    line : Int
    column : Int
    code : String
    message : String
    } derive(Eq, Show, ToJson)

    CsvIssue::new

    fn CsvIssue::new(source : String, line : Int, column : Int, code : String, message : String) -> CsvIssue

    DeterministicRandom

    type DeterministicRandom

    Diagnostic

    pub struct Diagnostic {
    code : String
    severity : DiagnosticSeverity
    message : String
    worker_id : String?
    shift_id : String?
    } derive(Eq, Show, ToJson)

    Diagnostic::for_shift

    fn Diagnostic::for_shift(self : Diagnostic, shift_id : String) -> Diagnostic

    Diagnostic::for_worker

    fn Diagnostic::for_worker(self : Diagnostic, worker_id : String) -> Diagnostic

    Diagnostic::new

    fn Diagnostic::new(code : String, severity : DiagnosticSeverity, message : String) -> Diagnostic

    DiagnosticSeverity

    pub(all) enum DiagnosticSeverity {
    Info
    Warning
    Error
    } derive(Eq, Show, ToJson)

    Explanation

    pub struct Explanation {
    kind : ExplanationKind
    title : String
    detail : String
    worker_id : String?
    shift_id : String?
    priority : Int
    } derive(Eq, Show, ToJson)

    Explanation::for_shift

    fn Explanation::for_shift(self : Explanation, shift_id : String) -> Explanation

    Explanation::for_worker

    fn Explanation::for_worker(self : Explanation, worker_id : String) -> Explanation

    Explanation::new

    fn Explanation::new(kind : ExplanationKind, title : String, detail : String, priority : Int) -> Explanation

    ExplanationKind

    pub(all) enum ExplanationKind {
    AssignmentAccepted
    AssignmentRejected
    CoverageFailure
    SkillCoverageFailure
    WorkloadObservation
    PreferenceObservation
    ImprovementSuggestion
    } derive(Eq, Show, ToJson)

    FairnessWeights

    pub struct FairnessWeights {
    workload : Int
    night : Int
    weekend : Int
    preference : Int
    understaffing : Int
    overtime : Int
    } derive(Eq, Show, ToJson)

    FairnessWeights::balanced

    Minute

    pub struct Minute {
    value_ : Int
    } derive(Compare, Eq, Show, ToJson)

    A minute offset from the beginning of the planning horizon.

    Minute::add

    fn Minute::add(self : Minute, delta : Int) -> Minute

    Minute::distance_to

    fn Minute::distance_to(self : Minute, other : Minute) -> Int

    Minute::new

    fn Minute::new(value : Int) -> Minute

    Minute::value

    fn Minute::value(self : Minute) -> Int

    PlanningProblem

    pub struct PlanningProblem {
    name : String
    workers : Array[Worker]
    skills : Array[Skill]
    shifts : Array[Shift]
    locked_assignments : Array[Assignment]
    weights : FairnessWeights
    seed : Int
    } derive(Eq, Show, ToJson)

    PlanningProblem::add_shift

    fn PlanningProblem::add_shift(self : PlanningProblem, shift : Shift) -> PlanningProblem

    PlanningProblem::add_skill

    fn PlanningProblem::add_skill(self : PlanningProblem, skill : Skill) -> PlanningProblem

    PlanningProblem::add_worker

    fn PlanningProblem::add_worker(self : PlanningProblem, worker : Worker) -> PlanningProblem

    PlanningProblem::lock

    fn PlanningProblem::lock(self : PlanningProblem, worker_id : String, shift_id : String) -> PlanningProblem

    PlanningProblem::new

    fn PlanningProblem::new(name : String) -> PlanningProblem

    PlanningProblem::shift

    fn PlanningProblem::shift(self : PlanningProblem, id : String) -> Shift?

    PlanningProblem::validate

    PlanningProblem::worker

    fn PlanningProblem::worker(self : PlanningProblem, id : String) -> Worker?

    PreferenceKind

    pub(all) enum PreferenceKind {
    Prefer
    Avoid
    Require
    Unavailable
    } derive(Eq, Show, ToJson)

    ProblemParseResult

    pub(all) enum ProblemParseResult {
    Parsed(PlanningProblem)
    ParseFailed(Array[CsvIssue])
    } derive(Eq, Show, ToJson)

    RiskLevel

    pub(all) enum RiskLevel {
    LowRisk
    ModerateRisk
    HighRisk
    CriticalRisk
    } derive(Compare, Eq, Show, ToJson)

    Schedule

    pub struct Schedule {
    assignments : Array[Assignment]
    } derive(Eq, Show, ToJson)

    Schedule::add

    fn Schedule::add(self : Schedule, assignment : Assignment) -> Schedule

    Schedule::contains

    fn Schedule::contains(self : Schedule, worker_id : String, shift_id : String) -> Bool

    Schedule::empty

    fn Schedule::empty() -> Schedule

    Schedule::remove

    fn Schedule::remove(self : Schedule, worker_id : String, shift_id : String) -> Schedule

    Schedule::shifts_for

    fn Schedule::shifts_for(self : Schedule, worker_id : String) -> Array[String]

    Schedule::workers_for

    fn Schedule::workers_for(self : Schedule, shift_id : String) -> Array[String]

    ScheduleSummary

    pub struct ScheduleSummary {
    problem_name : String
    worker_count : Int
    shift_count : Int
    assignment_count : Int
    coverage_per_mille : Int
    workload_spread : Int
    night_spread : Int
    weekend_spread : Int
    feasible : Bool
    score : ScoreBreakdown
    workers : Array[WorkerSummary]
    shifts : Array[ShiftSummary]
    } derive(Eq, Show, ToJson)

    ScoreBreakdown

    pub struct ScoreBreakdown {
    total : Int
    understaffing : Int
    skill_shortage : Int
    workload_imbalance : Int
    night_imbalance : Int
    weekend_imbalance : Int
    preference_cost : Int
    overtime_cost : Int
    } derive(Eq, Show, ToJson)

    ScoreBreakdown::zero

    SensitivityReport

    pub struct SensitivityReport {
    impacts : Array[AbsenceImpact]
    critical_workers : Array[String]
    high_risk_workers : Array[String]
    most_critical_worker : String?
    worst_coverage_loss : Int
    worst_score_increase : Int
    } derive(Eq, Show, ToJson)

    Shift

    pub struct Shift {
    id : String
    label : String
    range : TimeRange
    kind : ShiftKind
    required_workers : Int
    preferred_workers : Int
    requirements : Array[SkillRequirement]
    tags : Array[String]
    day_index : Int
    weekend : Bool
    } derive(Eq, Show, ToJson)

    Shift::add_requirement

    fn Shift::add_requirement(self : Shift, requirement : SkillRequirement) -> Shift

    Shift::add_tag

    fn Shift::add_tag(self : Shift, tag : String) -> Shift

    Shift::new

    fn Shift::new(id : String, label : String, start : Int, end : Int, required_workers : Int) -> Shift

    Shift::with_kind

    fn Shift::with_kind(self : Shift, kind : ShiftKind) -> Shift

    Shift::with_preferred_workers

    fn Shift::with_preferred_workers(self : Shift, count : Int) -> Shift

    Shift::with_weekend

    fn Shift::with_weekend(self : Shift, weekend : Bool) -> Shift

    ShiftCapacity

    pub struct ShiftCapacity {
    shift_id : String
    required_workers : Int
    available_workers : Int
    eligible_workers : Int
    capacity_margin : Int
    risk : RiskLevel
    } derive(Eq, Show, ToJson)

    ShiftKind

    pub(all) enum ShiftKind {
    Morning
    Day
    Evening
    Night
    OnCall
    Custom(String)
    } derive(Eq, Show, ToJson)

    ShiftKind::is_night

    fn ShiftKind::is_night(self : ShiftKind) -> Bool

    ShiftPreference

    pub struct ShiftPreference {
    shift_id : String
    kind : PreferenceKind
    weight : Int
    } derive(Eq, Show, ToJson)

    ShiftPreference::new

    fn ShiftPreference::new(shift_id : String, kind : PreferenceKind, weight : Int) -> ShiftPreference

    ShiftSummary

    pub struct ShiftSummary {
    shift_id : String
    label : String
    assigned : Int
    required : Int
    qualified_requirements : Int
    total_requirements : Int
    covered : Bool
    } derive(Eq, Show, ToJson)

    Skill

    pub struct Skill {
    id : String
    name : String
    } derive(Eq, Show, ToJson)

    Skill::new

    fn Skill::new(id : String, name : String) -> Skill

    SkillCapacity

    pub struct SkillCapacity {
    skill_id : String
    required_slots : Int
    qualified_workers : Int
    qualification_instances : Int
    margin : Int
    risk : RiskLevel
    } derive(Eq, Show, ToJson)

    SkillLevel

    pub(all) enum SkillLevel {
    Beginner
    Qualified
    Advanced
    Expert
    } derive(Compare, Eq, Show, ToJson)

    SkillLevel::rank

    fn SkillLevel::rank(self : SkillLevel) -> Int

    SkillRequirement

    pub struct SkillRequirement {
    skill_id : String
    minimum_level : SkillLevel
    count : Int
    } derive(Eq, Show, ToJson)

    SkillRequirement::new

    fn SkillRequirement::new(skill_id : String, minimum_level : SkillLevel, count : Int) -> SkillRequirement

    Solution

    pub struct Solution {
    schedule : Schedule
    score : ScoreBreakdown
    diagnostics : Array[Diagnostic]
    feasible : Bool
    stats : SolveStats
    } derive(Eq, Show, ToJson)

    SolutionComparison

    pub struct SolutionComparison {
    left_score : Int
    right_score : Int
    score_delta : Int
    left_coverage : Int
    right_coverage : Int
    coverage_delta : Int
    left_workload_spread : Int
    right_workload_spread : Int
    workload_spread_delta : Int
    changed_assignments : Int
    recommendation : String
    } derive(Eq, Show, ToJson)

    SolveStats

    pub struct SolveStats {
    explored_nodes : Int
    pruned_nodes : Int
    improvements : Int
    elapsed_steps : Int
    } derive(Eq, Show, ToJson)

    SolverConfig

    pub struct SolverConfig {
    max_iterations : Int
    candidate_limit : Int
    allow_partial : Bool
    improve : Bool
    seed : Int
    } derive(Eq, Show, ToJson)

    SolverConfig::default

    fn SolverConfig::default() -> SolverConfig

    SolverConfig::with_iterations

    fn SolverConfig::with_iterations(self : SolverConfig, max_iterations : Int) -> SolverConfig

    SolverConfig::with_seed

    fn SolverConfig::with_seed(self : SolverConfig, seed : Int) -> SolverConfig

    TimeRange

    pub struct TimeRange {
    start : Minute
    end : Minute
    } derive(Eq, Show, ToJson)

    TimeRange::duration

    fn TimeRange::duration(self : TimeRange) -> Int

    TimeRange::gap_after

    fn TimeRange::gap_after(self : TimeRange, previous : TimeRange) -> Int

    TimeRange::is_valid

    fn TimeRange::is_valid(self : TimeRange) -> Bool

    TimeRange::new

    fn TimeRange::new(start : Int, end : Int) -> TimeRange

    TimeRange::overlaps

    fn TimeRange::overlaps(self : TimeRange, other : TimeRange) -> Bool

    ViolationKind

    pub(all) enum ViolationKind {
    UnknownWorker
    UnknownShift
    DuplicateAssignment
    WorkerUnavailable
    ExplicitlyUnavailable
    MissingRequiredPreference
    OverlappingShift
    InsufficientRest
    WeeklyMinuteLimit
    ConsecutiveShiftLimit
    MissingSkill
    Understaffed
    SkillShortage
    } derive(Eq, Show, ToJson)

    Worker

    pub struct Worker {
    id : String
    name : String
    skills : Array[WorkerSkill]
    availability : Array[TimeRange]
    preferences : Array[ShiftPreference]
    min_minutes : Int
    target_minutes : Int
    max_minutes : Int
    max_consecutive_shifts : Int
    min_rest_minutes : Int
    } derive(Eq, Show, ToJson)

    Worker::add_availability

    fn Worker::add_availability(self : Worker, range : TimeRange) -> Worker

    Worker::add_preference

    fn Worker::add_preference(self : Worker, preference : ShiftPreference) -> Worker

    Worker::add_skill

    fn Worker::add_skill(self : Worker, skill : WorkerSkill) -> Worker

    Worker::has_skill

    fn Worker::has_skill(self : Worker, skill_id : String, minimum : SkillLevel) -> Bool

    Worker::is_available

    fn Worker::is_available(self : Worker, range : TimeRange) -> Bool

    Worker::new

    fn Worker::new(id : String, name : String) -> Worker

    Worker::preference_for

    fn Worker::preference_for(self : Worker, shift_id : String) -> ShiftPreference?

    Worker::skill_level

    fn Worker::skill_level(self : Worker, skill_id : String) -> SkillLevel?

    Worker::with_limits

    fn Worker::with_limits(self : Worker, min_minutes : Int, target_minutes : Int, max_minutes : Int) -> Worker

    Worker::with_rest_rules

    fn Worker::with_rest_rules(self : Worker, max_consecutive_shifts : Int, min_rest_minutes : Int) -> Worker

    WorkerSkill

    pub struct WorkerSkill {
    skill_id : String
    level : SkillLevel
    } derive(Eq, Show, ToJson)

    WorkerSkill::new

    fn WorkerSkill::new(skill_id : String, level : SkillLevel) -> WorkerSkill

    WorkerSummary

    pub struct WorkerSummary {
    worker_id : String
    assigned_minutes : Int
    target_minutes : Int
    night_shifts : Int
    weekend_shifts : Int
    preference_cost : Int
    } derive(Eq, Show, ToJson)

    analyze_absence

    fn analyze_absence(problem : PlanningProblem, worker_id : String, config : SolverConfig) -> AbsenceImpact?

    analyze_capacity

    fn analyze_capacity(problem : PlanningProblem) -> CapacityReport

    analyze_sensitivity

    fn analyze_sensitivity(problem : PlanningProblem, config : SolverConfig) -> SensitivityReport

    assignment_count_for_shift

    fn assignment_count_for_shift(schedule : Schedule, shift_id : String) -> Int

    assignment_delta

    fn assignment_delta(problem : PlanningProblem, schedule : Schedule, worker_id : String, shift_id : String) -> Int

    capacity_recommendations

    fn capacity_recommendations(problem : PlanningProblem, capacity : CapacityReport, sensitivity : SensitivityReport) -> Array[CapacityRecommendation]

    check_candidate

    fn check_candidate(problem : PlanningProblem, schedule : Schedule, worker_id : String, shift_id : String) -> ConstraintReport

    clinic_scenario

    fn clinic_scenario() -> PlanningProblem

    compare_solutions

    fn compare_solutions(problem : PlanningProblem, left : Solution, right : Solution) -> SolutionComparison

    coverage_ratio_per_mille

    fn coverage_ratio_per_mille(problem : PlanningProblem, schedule : Schedule) -> Int

    diagnostics_have_errors

    fn diagnostics_have_errors(diagnostics : Array[Diagnostic]) -> Bool

    eligible_workers

    fn eligible_workers(problem : PlanningProblem, schedule : Schedule, shift_id : String) -> Array[String]

    evaluate_constraints

    fn evaluate_constraints(problem : PlanningProblem, schedule : Schedule) -> ConstraintReport

    explain_candidate

    fn explain_candidate(problem : PlanningProblem, schedule : Schedule, worker_id : String, shift_id : String) -> Array[Explanation]

    explain_solution

    fn explain_solution(problem : PlanningProblem, solution : Solution) -> Array[Explanation]

    operations_scenario

    fn operations_scenario() -> PlanningProblem

    parse_problem_csv

    fn parse_problem_csv(name : String, workers_csv : String, shifts_csv : String) -> ProblemParseResult

    render_audit

    fn render_audit(audit : AuditTrail) -> String

    render_capacity_report

    fn render_capacity_report(problem : PlanningProblem, capacity : CapacityReport) -> String

    render_text_report

    fn render_text_report(problem : PlanningProblem, solution : Solution) -> String

    repair_schedule

    fn repair_schedule(problem : PlanningProblem, existing : Schedule, config : SolverConfig) -> Solution

    scenario_by_name

    fn scenario_by_name(name : String) -> PlanningProblem?

    scenario_names

    fn scenario_names() -> Array[String]

    schedule_to_csv

    fn schedule_to_csv(problem : PlanningProblem, solution : Solution) -> String

    score_schedule

    fn score_schedule(problem : PlanningProblem, schedule : Schedule) -> ScoreBreakdown

    shift_capacity

    fn shift_capacity(problem : PlanningProblem, shift_id : String) -> ShiftCapacity?

    shifts_csv_template

    fn shifts_csv_template() -> String

    skill_capacity

    fn skill_capacity(problem : PlanningProblem, skill_id : String) -> SkillCapacity

    solution_to_json

    fn solution_to_json(problem : PlanningProblem, solution : Solution) -> String

    solve

    fn solve(problem : PlanningProblem) -> Solution

    solve_audited

    fn solve_audited(problem : PlanningProblem, config : SolverConfig) -> AuditedSolution

    solve_with_config

    fn solve_with_config(problem : PlanningProblem, config : SolverConfig) -> Solution

    summarize_schedule

    fn summarize_schedule(problem : PlanningProblem, solution : Solution) -> ScheduleSummary

    summarize_shift

    fn summarize_shift(problem : PlanningProblem, schedule : Schedule, shift_id : String) -> ShiftSummary?

    summarize_worker

    fn summarize_worker(problem : PlanningProblem, schedule : Schedule, worker_id : String) -> WorkerSummary?

    volunteer_scenario

    fn volunteer_scenario() -> PlanningProblem

    worker_assigned_minutes

    fn worker_assigned_minutes(problem : PlanningProblem, schedule : Schedule, worker_id : String) -> Int

    worker_night_count

    fn worker_night_count(problem : PlanningProblem, schedule : Schedule, worker_id : String) -> Int

    worker_weekend_count

    fn worker_weekend_count(problem : PlanningProblem, schedule : Schedule, worker_id : String) -> Int

    workers_csv_template

    fn workers_csv_template() -> String

    workload_range

    fn workload_range(problem : PlanningProblem, schedule : Schedule) -> Int

    workload_to_csv

    fn workload_to_csv(problem : PlanningProblem, solution : Solution) -> String