///|
pub(all) struct AuditSummary {
valid_problem : Bool
complete : Bool
hard_errors : Int
warnings : Int
coverage_percent : Int
deterministic_fingerprint : String
notes : Array[String]
} derive(Debug, Eq)
///|
fn mix_hash(value : Int, next : Int) -> Int {
((value * 16777619) ^ next) & 0x7fffffff
}
///|
pub fn schedule_fingerprint(assignments : Array[Assignment]) -> String {
let values = assignments.copy()
values.sort_by((a, b) => {
let by_shift = a.shift_id.compare(b.shift_id)
if by_shift != 0 {
by_shift
} else if a.slot != b.slot {
a.slot - b.slot
} else {
a.person_id.compare(b.person_id)
}
})
let mut hash = 216613626
for item in values {
for char in item.shift_id {
hash = mix_hash(hash, char.to_int())
}
hash = mix_hash(hash, item.slot)
for char in item.person_id {
hash = mix_hash(hash, char.to_int())
}
hash = mix_hash(hash, if item.locked { 1 } else { 0 })
}
"sw-\{hash}-\{values.length()}"
}
///|
pub fn audit_schedule(
problem : ScheduleProblem,
assignments : Array[Assignment],
) -> AuditSummary {
let problem_diagnostics = validate_problem(problem)
let assignment_diagnostics = validate_assignments(problem, assignments)
let findings = run_all_policies(problem, assignments)
let coverage = coverage_summary(problem, assignments)
let assignment_error = if has_errors(assignment_diagnostics) { 1 } else { 0 }
let notes : Array[String] = []
if problem_diagnostics.length() > 0 {
notes.push("Problem definition contains validation diagnostics.")
}
if coverage.uncovered_slots > 0 {
notes.push("Schedule is incomplete.")
}
if maximum_workload_gap(problem, assignments) > 480 {
notes.push("Workload distribution has a large gap.")
}
if preference_score(problem, assignments) < 0 {
notes.push("Net preference score is negative.")
}
{
valid_problem: !has_errors(problem_diagnostics),
complete: coverage.uncovered_slots == 0,
hard_errors: count_policy_errors(findings) + assignment_error,
warnings: count_policy_warnings(findings),
coverage_percent: coverage.coverage_percent,
deterministic_fingerprint: schedule_fingerprint(assignments),
notes,
}
}
///|
pub fn compare_schedules(
problem : ScheduleProblem,
left : Array[Assignment],
right : Array[Assignment],
) -> String {
let lm = evaluate_schedule(problem, left)
let rm = evaluate_schedule(problem, right)
let mut text = "# Schedule comparison\n\n"
text = text + "| Metric | Left | Right | Delta |\n|---|---:|---:|---:|\n"
text = text +
"| Objective | \{lm.objective} | \{rm.objective} | \{rm.objective - lm.objective} |\n"
text = text +
"| Uncovered | \{lm.uncovered_slots} | \{rm.uncovered_slots} | \{rm.uncovered_slots - lm.uncovered_slots} |\n"
text = text +
"| Preference | \{lm.preference_score} | \{rm.preference_score} | \{rm.preference_score - lm.preference_score} |\n"
text = text +
"| Fairness penalty | \{lm.fairness_penalty} | \{rm.fairness_penalty} | \{rm.fairness_penalty - lm.fairness_penalty} |\n"
text = text +
"| Disruptions | \{lm.disruption_count} | \{rm.disruption_count} | \{rm.disruption_count - lm.disruption_count} |\n"
text
}