shiftweave

    Explainable workforce scheduling and minimal-disruption repair engine

    scheduling
    constraint-solving
    roster
    optimization
    Download zip
    Author
    Version
    0.1.1
    License
    Apache-2.0
    Last updated
    2 months ago
    Downloads
    35

    No dependencies

    #ShiftWeave

    ShiftWeave is an explainable workforce scheduling and minimal-disruption repair engine written in MoonBit. It assigns qualified and available people to shift slots, validates hard constraints, scores fairness and preferences, explains infeasible rosters, and repairs an existing roster after leave or demand changes.

    The repository contains more than 4,000 lines of effective MoonBit source, runnable scenarios, tests, CI, and an independently verifiable reporting pipeline.

    #Why ShiftWeave

    Small teams often schedule with spreadsheets. A spreadsheet can display a roster, but it rarely explains why a shift cannot be filled or how to repair a roster without changing everyone else's assignment. ShiftWeave treats scheduling as a constrained search problem and keeps evidence for every rejection.

    The project is deliberately domain-facing rather than a MoonBit language tool. Typical uses include retail shifts, laboratory equipment duty, volunteer events, and small service teams.

    #Quick start

    moon run cmd/main -- demo moon test

    The demo solves a seven-shift retail roster and prints two Markdown reports: the assignment result and an operational risk review. The latter identifies uncovered duties, replacement margin, rest-sensitive handovers, and concrete follow-up actions.

    The current CLI intentionally exposes the built-in demonstration only; the reusable library API provides solving, repair, parsing, and reporting. examples/store.roster demonstrates the interchange format and can be parsed through parse_roster when embedded in an application.

    #Install and use as a library

    moon add SUIKKKA/shiftweave@0.1.1

    let problem = @shiftweave.store_scenario()
    let result = @shiftweave.solve(problem)
    println(@shiftweave.render_markdown(problem, result))

    For a disrupted roster, construct a RepairRequest and call repair. Search uses one deterministic global node budget; ScheduleResult reports the real explored-node and propagated-value counts and distinguishes an exhausted budget from a proven infeasible problem.

    #Core capabilities

    • Validates identities, time spans, availability, skills, slots, overlap, rest, workload, and consecutive-duty limits.
    • Propagates forced assignments before deterministic bounded backtracking.
    • Scores coverage, fairness, preferences, and disruption separately.
    • Repairs an existing roster while preserving unaffected locked assignments.
    • Explains candidate rejection and suggests changes for infeasible slots.
    • Produces Markdown, CSV, and JSON output plus stable audit fingerprints.
    • Reviews daily load, shift resilience, standby margin, and handover risk.
    • Includes retail, laboratory, and volunteer-event scenarios.

    #Architecture

    The domain model is independent from the solver. validation.mbt and policy.mbt form the verification boundary; candidate.mbt, propagation.mbt, and solver.mbt implement search; scoring.mbt keeps optimization policy explicit; repair.mbt handles minimal-change replanning. Reporting and operational review consume only public problem/result values, so their evidence can be checked independently.

    The text interchange format is line-oriented and pipe-delimited. Comments begin with #; records describe settings, people, skills, availability, preferences, shifts, requirements, and existing assignments. See examples/store.roster for a compact example and text_input.mbt for the parser and round-trip serializer.

    #Testing and reproducibility

    moon fmt --check moon check --deny-warn moon test --deny-warn moon build moon run cmd/main -- demo moon publish --dry-run

    Tests cover validation failures, deterministic solving, policy evaluation, candidate explanations, repair impact, serializers, built-in scenarios, audit comparison, and operational risk. GitHub Actions runs formatting, checking, the native test suite, and the demo on every push and pull request.

    #Development

    moon check moon test moon fmt

    #Project boundaries and originality

    • Offline core and CLI; no cloud account or attendance tracking.
    • Configurable work/rest rules; no jurisdiction-specific legal advice.
    • Deterministic results for the same input and search budget.
    • A verifier checks every emitted roster independently of the solver.

    ShiftWeave is an original project implemented for this hackathon. It does not port third-party source code. Dependencies are limited to the MoonBit core/toolchain; repository contents are released under Apache-2.0. AI assistance and source provenance are documented in PROVENANCE.md.

    #License

    Apache-2.0. See LICENSE.

    PersonId

    type PersonId = String

    ShiftId

    type ShiftId = String

    Assignment

    pub(all) struct Assignment {
    shift_id : String
    slot : Int
    person_id : String
    locked : Bool
    } derive(Eq,
    Debug
    )

    AuditSummary

    pub(all) struct AuditSummary {
    valid_problem : Bool
    complete : Bool
    hard_errors : Int
    warnings : Int
    coverage_percent : Int
    deterministic_fingerprint : String
    notes : Array[String]
    } derive(Eq,
    Debug
    )

    CandidateDomain

    pub(all) struct CandidateDomain {
    key : SlotKey
    people : Array[String]
    rejected : Array[Diagnostic]
    } derive(Eq,
    Debug
    )

    CandidateExplanation

    pub(all) struct CandidateExplanation {
    person_id : String
    eligible : Bool
    incremental_cost : Int
    reasons : Array[String]
    } derive(Eq,
    Debug
    )

    CoverageSummary

    pub(all) struct CoverageSummary {
    total_slots : Int
    covered_slots : Int
    uncovered_slots : Int
    coverage_percent : Int
    people_used : Int
    total_minutes : Int
    } derive(Eq,
    Debug
    )

    DayOperation

    pub(all) struct DayOperation {
    day : Int
    shift_count : Int
    required_slots : Int
    assigned_slots : Int
    uncovered_slots : Int
    people_used : Int
    scheduled_minutes : Int
    average_load : Int
    risk_score : Int
    } derive(Eq,
    Debug
    )

    Day-level summary for dashboards and post-run review.

    Diagnostic

    pub(all) struct Diagnostic {
    code : DiagnosticCode
    message : String
    person_id : String?
    shift_id : String?
    slot : Int?
    severity : Int
    } derive(Eq,
    Debug
    )

    DiagnosticCode

    pub(all) enum DiagnosticCode {
    InvalidTimeSpan
    DuplicatePerson
    DuplicateShift
    UnknownPerson
    UnknownShift
    InvalidSlot
    DuplicateAssignment
    OverlappingAssignment
    OutsideAvailability
    SkillMismatch
    MaximumMinutesExceeded
    InsufficientRest
    ConsecutiveLimitExceeded
    UncoveredSlot
    EmptyCandidateDomain
    SearchBudgetExceeded
    InvalidConfiguration
    } derive(Eq,
    Debug
    )

    FindingLevel

    pub(all) enum FindingLevel {
    Info
    Warning
    Error
    } derive(Eq,
    Debug
    )

    Handover

    pub(all) struct Handover {
    person_id : String
    from_shift : String
    to_shift : String
    gap_minutes : Int
    same_group : Bool
    risk_score : Int
    } derive(Eq,
    Debug
    )

    A transition between two adjacent duties performed by one person.

    OperationalAction

    pub(all) struct OperationalAction {
    priority : Int
    category : String
    subject : String
    message : String
    } derive(Eq,
    Debug
    )

    A concrete operational action generated from schedule evidence.

    OperationalReview

    pub(all) struct OperationalReview {
    shifts : Array[ShiftOperation]
    days : Array[DayOperation]
    handovers : Array[Handover]
    actions : Array[OperationalAction]
    total_risk : Int
    } derive(Eq,
    Debug
    )

    Full operational view; it remains useful even for a partial roster.

    ParseIssue

    pub(all) struct ParseIssue {
    line : Int
    column : Int
    message : String
    source : String
    } derive(Eq,
    Debug
    )

    ParsedRoster

    pub(all) struct ParsedRoster {
    problem : ScheduleProblem
    issues : Array[ParseIssue]
    } derive(Eq,
    Debug
    )

    Person

    pub(all) struct Person {
    id : String
    name : String
    skills : Array[Skill]
    availability : Array[TimeSpan]
    preferences : Array[ShiftPreference]
    target_minutes : Int
    maximum_minutes : Int
    maximum_consecutive_shifts : Int
    } derive(Eq,
    Debug
    )

    PersonWorkload

    pub(all) struct PersonWorkload {
    person_id : String
    assigned_minutes : Int
    target_minutes : Int
    difference : Int
    assignment_count : Int
    preference_score : Int
    } derive(Eq,
    Debug
    )

    PolicyFinding

    pub(all) struct PolicyFinding {
    rule : String
    level : FindingLevel
    message : String
    person_id : String?
    shift_id : String?
    measured : Int
    expected : Int
    } derive(Eq,
    Debug
    )

    PropagationResult

    pub(all) struct PropagationResult {
    assignments : Array[Assignment]
    diagnostics : Array[Diagnostic]
    forced_count : Int
    feasible : Bool
    } derive(Eq,
    Debug
    )

    RecordKind

    pub(all) enum RecordKind {
    PersonRecord
    SkillRecord
    AvailabilityRecord
    PreferenceRecord
    ShiftRecord
    RequirementRecord
    AssignmentRecord
    RuleRecord
    CommentRecord
    BlankRecord
    UnknownRecord
    } derive(Eq,
    Debug
    )

    RepairRequest

    pub(all) struct RepairRequest {
    unavailable_people : Array[String]
    removed_shifts : Array[String]
    unlocked_shifts : Array[String]
    preserve_unaffected : Bool
    } derive(Eq,
    Debug
    )

    RepairRequest::empty

    ScheduleMetrics

    pub(all) struct ScheduleMetrics {
    hard_violations : Int
    uncovered_slots : Int
    preference_score : Int
    fairness_penalty : Int
    disruption_count : Int
    total_minutes : Int
    objective : Int
    } derive(Eq,
    Debug
    )

    ScheduleMetrics::empty

    ScheduleProblem

    pub(all) struct ScheduleProblem {
    people : Array[Person]
    shifts : Array[Shift]
    existing : Array[Assignment]
    minimum_rest_minutes : Int
    fairness_weight : Int
    preference_weight : Int
    disruption_weight : Int
    } derive(Eq,
    Debug
    )

    ScheduleResult

    pub(all) struct ScheduleResult {
    status : SolveStatus
    assignments : Array[Assignment]
    diagnostics : Array[Diagnostic]
    metrics : ScheduleMetrics
    explored_nodes : Int
    propagated_values : Int
    } derive(Eq,
    Debug
    )

    SearchOptions

    pub(all) struct SearchOptions {
    maximum_nodes : Int
    maximum_solutions : Int
    stop_at_first_feasible : Bool
    keep_explanations : Bool
    } derive(Eq,
    Debug
    )

    SearchOptions::default

    fn SearchOptions::default() -> SearchOptions

    Shift

    pub(all) struct Shift {
    id : String
    label : String
    time : TimeSpan
    required_people : Int
    requirements : Array[SkillRequirement]
    priority : Int
    group : String
    } derive(Eq,
    Debug
    )

    ShiftOperation

    pub(all) struct ShiftOperation {
    shift_id : String
    label : String
    group : String
    assigned : Int
    required : Int
    qualified : Int
    standby : Int
    coverage_percent : Int
    risk_score : Int
    risk_level : String
    } derive(Eq,
    Debug
    )

    Operational analysis derived independently from the search algorithm.

    ShiftPreference

    pub(all) struct ShiftPreference {
    shift_id : String
    weight : Int
    } derive(Eq,
    Debug
    )

    Skill

    pub(all) struct Skill {
    name : String
    level : Int
    } derive(Eq,
    Debug
    )

    SkillRequirement

    pub(all) struct SkillRequirement {
    skill : String
    minimum_level : Int
    } derive(Eq,
    Debug
    )

    SlotExplanation

    pub(all) struct SlotExplanation {
    shift_id : String
    slot : Int
    candidates : Array[CandidateExplanation]
    eligible_count : Int
    rejected_count : Int
    forced : Bool
    } derive(Eq,
    Debug
    )

    SlotKey

    pub(all) struct SlotKey {
    shift_id : String
    slot : Int
    } derive(Eq,
    Debug
    )

    SolveStatus

    pub(all) enum SolveStatus {
    Solved
    Partial
    Infeasible
    BudgetExceeded
    } derive(Eq,
    Debug
    )

    TimeSpan

    pub(all) struct TimeSpan {
    start : Int
    end : Int
    } derive(Eq,
    Debug
    )

    TimeSpan::contains

    fn TimeSpan::contains(self : TimeSpan, other : TimeSpan) -> Bool

    TimeSpan::duration

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

    TimeSpan::gap_to

    fn TimeSpan::gap_to(self : TimeSpan, other : TimeSpan) -> Int

    TimeSpan::is_valid

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

    TimeSpan::overlaps

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

    absolute

    fn absolute(value : Int) -> Int

    all_workloads

    fn all_workloads(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[PersonWorkload]

    analyze_days

    fn analyze_days(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[DayOperation]

    Generate summaries only for days that contain at least one shift.

    analyze_handovers

    fn analyze_handovers(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[Handover]

    Analyze every adjacent duty transition.

    analyze_shift_operations

    fn analyze_shift_operations(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[ShiftOperation]

    Analyze every shift, including uncovered ones.

    apply_repair_request

    fn apply_repair_request(problem : ScheduleProblem, request : RepairRequest) -> ScheduleProblem

    assigned_minutes

    fn assigned_minutes(problem : ScheduleProblem, assignments : Array[Assignment], person_id : String) -> Int

    assignments_for_person

    fn assignments_for_person(assignments : Array[Assignment], person_id : String) -> Array[Assignment]

    assignments_for_shift

    fn assignments_for_shift(assignments : Array[Assignment], shift_id : String) -> Array[Assignment]

    audit_schedule

    fn audit_schedule(problem : ScheduleProblem, assignments : Array[Assignment]) -> AuditSummary

    average_minutes

    fn average_minutes(problem : ScheduleProblem, assignments : Array[Assignment]) -> Int

    better_metrics

    fn better_metrics(a : ScheduleMetrics, b : ScheduleMetrics) -> Bool

    build_domain

    fn build_domain(problem : ScheduleProblem, assignments : Array[Assignment], shift : Shift, slot : Int) -> CandidateDomain

    build_domains

    fn build_domains(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[CandidateDomain]

    build_operational_actions

    fn build_operational_actions(shifts : Array[ShiftOperation], handovers : Array[Handover]) -> Array[OperationalAction]

    Turn evidence into a short, deterministic action queue.

    busiest_person

    fn busiest_person(problem : ScheduleProblem, assignments : Array[Assignment]) -> String?

    can_assign

    fn can_assign(problem : ScheduleProblem, assignments : Array[Assignment], shift : Shift, slot : Int, person : Person) -> Diagnostic?

    changed_assignments

    fn changed_assignments(before : Array[Assignment], after : Array[Assignment]) -> Array[Assignment]

    check_availability_policy

    fn check_availability_policy(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[PolicyFinding]

    check_availability_supply_policy

    fn check_availability_supply_policy(problem : ScheduleProblem) -> Array[PolicyFinding]

    check_capacity_policy

    fn check_capacity_policy(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[PolicyFinding]

    check_coverage_policy

    fn check_coverage_policy(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[PolicyFinding]

    check_duplicate_person_in_shift_policy

    fn check_duplicate_person_in_shift_policy(assignments : Array[Assignment]) -> Array[PolicyFinding]

    check_duplicate_slot_policy

    fn check_duplicate_slot_policy(assignments : Array[Assignment]) -> Array[PolicyFinding]

    check_empty_skill_policy

    fn check_empty_skill_policy(problem : ScheduleProblem) -> Array[PolicyFinding]

    check_fairness_gap_policy

    fn check_fairness_gap_policy(problem : ScheduleProblem, assignments : Array[Assignment], maximum_gap? : Int) -> Array[PolicyFinding]

    check_locked_assignment_policy

    fn check_locked_assignment_policy(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[PolicyFinding]

    check_maximum_minutes_policy

    fn check_maximum_minutes_policy(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[PolicyFinding]

    check_overlap_policy

    fn check_overlap_policy(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[PolicyFinding]

    check_preference_policy

    fn check_preference_policy(problem : ScheduleProblem, assignments : Array[Assignment], threshold? : Int) -> Array[PolicyFinding]

    check_priority_policy

    fn check_priority_policy(problem : ScheduleProblem, assignments : Array[Assignment], minimum_priority? : Int) -> Array[PolicyFinding]

    check_rest_policy

    fn check_rest_policy(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[PolicyFinding]

    check_skill_policy

    fn check_skill_policy(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[PolicyFinding]

    check_target_minutes_policy

    fn check_target_minutes_policy(problem : ScheduleProblem, assignments : Array[Assignment], tolerance? : Int) -> Array[PolicyFinding]

    compare_schedules

    fn compare_schedules(problem : ScheduleProblem, left : Array[Assignment], right : Array[Assignment]) -> String

    count_policy_errors

    fn count_policy_errors(findings : Array[PolicyFinding]) -> Int

    count_policy_warnings

    fn count_policy_warnings(findings : Array[PolicyFinding]) -> Int

    count_uncovered

    fn count_uncovered(problem : ScheduleProblem, assignments : Array[Assignment]) -> Int

    coverage_summary

    fn coverage_summary(problem : ScheduleProblem, assignments : Array[Assignment]) -> CoverageSummary

    disruption_count

    fn disruption_count(existing : Array[Assignment], proposed : Array[Assignment]) -> Int

    domain_contains

    fn domain_contains(domain : CandidateDomain, person_id : String) -> Bool

    evaluate_schedule

    fn evaluate_schedule(problem : ScheduleProblem, assignments : Array[Assignment]) -> ScheduleMetrics

    expand_days

    fn expand_days(base : ScheduleProblem, copies : Int) -> ScheduleProblem

    explain_candidate

    fn explain_candidate(problem : ScheduleProblem, assignments : Array[Assignment], shift : Shift, slot : Int, person : Person) -> CandidateExplanation

    explain_problem

    fn explain_problem(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[SlotExplanation]

    explain_slot

    fn explain_slot(problem : ScheduleProblem, assignments : Array[Assignment], shift : Shift, slot : Int) -> SlotExplanation

    fairness_penalty

    fn fairness_penalty(problem : ScheduleProblem, assignments : Array[Assignment]) -> Int

    find_person

    fn find_person(problem : ScheduleProblem, id : String) -> Person?

    find_shift

    fn find_shift(problem : ScheduleProblem, id : String) -> Shift?

    forced_slots

    fn forced_slots(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[SlotExplanation]

    has_errors

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

    impossible_slots

    fn impossible_slots(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[SlotExplanation]

    incremental_cost

    fn incremental_cost(problem : ScheduleProblem, assignments : Array[Assignment], shift : Shift, person : Person) -> Int

    is_available

    fn is_available(person : Person, shift : Shift) -> Bool

    laboratory_scenario

    fn laboratory_scenario() -> ScheduleProblem

    make_person

    fn make_person(id : String, name : String, skills : Array[Skill], availability : Array[TimeSpan], target : Int, maximum : Int, preferences? : Array[ShiftPreference]) -> Person

    make_shift

    fn make_shift(id : String, label : String, start : Int, end : Int, required : Int, requirements : Array[SkillRequirement], priority? : Int, group? : String) -> Shift

    maximum_workload_gap

    fn maximum_workload_gap(problem : ScheduleProblem, assignments : Array[Assignment]) -> Int

    meets_requirements

    fn meets_requirements(person : Person, shift : Shift) -> Bool

    minutes_to_clock

    fn minutes_to_clock(minutes : Int) -> String

    most_constrained_slot

    fn most_constrained_slot(problem : ScheduleProblem, assignments : Array[Assignment]) -> SlotExplanation?

    parse_integer

    fn parse_integer(text : String) -> Int?

    parse_roster

    fn parse_roster(text : String) -> ParsedRoster

    person_minutes

    fn person_minutes(problem : ScheduleProblem, assignments : Array[Assignment], person_id : String) -> Int

    preference_for

    fn preference_for(person : Person, shift_id : String) -> Int

    preference_score

    fn preference_score(problem : ScheduleProblem, assignments : Array[Assignment]) -> Int

    propagate

    fn propagate(problem : ScheduleProblem, initial : Array[Assignment]) -> PropagationResult

    record_kind

    fn record_kind(line : String) -> RecordKind

    removed_assignments

    fn removed_assignments(before : Array[Assignment], after : Array[Assignment]) -> Array[Assignment]

    render_csv

    fn render_csv(problem : ScheduleProblem, result : ScheduleResult) -> String

    render_json

    fn render_json(_problem : ScheduleProblem, result : ScheduleResult) -> String

    render_markdown

    fn render_markdown(problem : ScheduleProblem, result : ScheduleResult) -> String

    render_operations_markdown

    fn render_operations_markdown(review : OperationalReview) -> String

    Render a compact Markdown operations section.

    render_problem_explanation

    fn render_problem_explanation(problem : ScheduleProblem, assignments : Array[Assignment]) -> String

    render_roster

    fn render_roster(problem : ScheduleProblem) -> String

    render_slot_explanation

    fn render_slot_explanation(problem : ScheduleProblem, explanation : SlotExplanation) -> String

    repair

    fn repair(problem : ScheduleProblem, request : RepairRequest, options? : SearchOptions) -> ScheduleResult

    review_operations

    fn review_operations(problem : ScheduleProblem, assignments : Array[Assignment]) -> OperationalReview

    Build the full operational review in one call.

    risk_level

    fn risk_level(score : Int) -> String

    Convert a numeric risk into a stable label for reports.

    run_all_policies

    fn run_all_policies(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[PolicyFinding]

    same_assignment

    fn same_assignment(a : Assignment, b : Assignment) -> Bool

    same_day

    fn same_day(a : TimeSpan, b : TimeSpan) -> Bool

    scenario_by_name

    fn scenario_by_name(name : String) -> ScheduleProblem?

    scenario_names

    fn scenario_names() -> Array[String]

    schedule_fingerprint

    fn schedule_fingerprint(assignments : Array[Assignment]) -> String

    skill_level

    fn skill_level(person : Person, name : String) -> Int

    smallest_domain_index

    fn smallest_domain_index(domains : Array[CandidateDomain]) -> Int

    solve

    fn solve(problem : ScheduleProblem, options? : SearchOptions) -> ScheduleResult

    split_fields

    fn split_fields(line : String) -> Array[String]

    store_scenario

    fn store_scenario() -> ScheduleProblem

    suggested_actions

    fn suggested_actions(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[String]

    summarize_infeasibility

    fn summarize_infeasibility(problem : ScheduleProblem, assignments : Array[Assignment]) -> Array[String]

    total_scheduled_minutes

    fn total_scheduled_minutes(problem : ScheduleProblem, assignments : Array[Assignment]) -> Int

    trim_ascii

    fn trim_ascii(text : String) -> String

    validate_assignments

    fn validate_assignments(problem : ScheduleProblem, assignments : Array[Assignment], require_complete? : Bool) -> Array[Diagnostic]

    validate_problem

    fn validate_problem(problem : ScheduleProblem) -> Array[Diagnostic]

    volunteer_scenario

    fn volunteer_scenario() -> ScheduleProblem

    workload_for

    fn workload_for(problem : ScheduleProblem, assignments : Array[Assignment], person : Person) -> PersonWorkload