From 5e52198bb002c465d77859967f2efaecb3535f24 Mon Sep 17 00:00:00 2001 From: MohammadSyed18 <79668964+MohammadSyed18@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:51:07 -0500 Subject: [PATCH] Enforce scheduling constraints --- .gitignore | 2 +- README.md | 3 +-- include/planner.hpp | 2 +- src/planner.cpp | 33 ++++++++++++++++++++++++++++----- tests/planner_tests.cpp | 20 ++++++++++++++++++-- 5 files changed, 49 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 8dd5068..2573c4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -build/ +/build/ *.o planner diff --git a/README.md b/README.md index 97925b5..6c4f5b8 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,10 @@ PlannerAI is a C++ scheduling engine that turns tasks, deadlines, dependencies, available time blocks, and an hourly energy curve into a realistic work plan. The optimizer favors urgent/high-value work, avoids prerequisite violations, and can split long tasks across multiple blocks. -Current state: CSV input, constraint checks, scoring, scheduling, JSON output, a demo dataset, and core tests are working. Calendar import and interactive replanning are still in progress. +Current state: CSV input, dependency and non-overlap validation, hard deadline enforcement, energy-aware scoring, explainable unscheduled work, JSON output, a demo dataset, and core tests are working. Calendar import and interactive replanning are still in progress. ```bash cmake -S . -B build && cmake --build build ./build/planner data/tasks.csv data/availability.csv ctest --test-dir build --output-on-failure ``` - diff --git a/include/planner.hpp b/include/planner.hpp index 764fea9..83df428 100644 --- a/include/planner.hpp +++ b/include/planner.hpp @@ -38,6 +38,7 @@ struct ScheduledBlock { struct Plan { std::vector blocks; std::unordered_map unscheduled_minutes; + std::unordered_map unscheduled_reasons; double objective{}; }; @@ -56,4 +57,3 @@ std::string plan_to_json(const Plan& plan); std::string minute_to_clock(int minute); } // namespace plannerai - diff --git a/src/planner.cpp b/src/planner.cpp index 9efdf0b..0a2256e 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -63,6 +63,7 @@ void Scheduler::validate(const std::vector& tasks, const std::vector 5) throw std::invalid_argument("priority must be 1..5"); if (task.cognitive_load < 0 || task.cognitive_load > 1) throw std::invalid_argument("cognitive load must be 0..1"); + if (task.deadline_minute <= 0) throw std::invalid_argument("deadline must be positive"); } for (const auto& task : tasks) { for (const auto& dependency : task.dependencies) { @@ -70,9 +71,15 @@ void Scheduler::validate(const std::vector& tasks, const std::vector ordered = availability; + std::sort(ordered.begin(), ordered.end(), [](const auto& left, const auto& right) { + return left.start_minute < right.start_minute; + }); + for (std::size_t index = 0; index < ordered.size(); ++index) { + const auto& slot = ordered[index]; if (slot.end_minute <= slot.start_minute) throw std::invalid_argument("availability must have positive length"); if (slot.energy < 0 || slot.energy > 1) throw std::invalid_argument("energy must be 0..1"); + if (index && slot.start_minute < ordered[index - 1].end_minute) throw std::invalid_argument("availability blocks must not overlap"); } } @@ -107,11 +114,12 @@ Plan Scheduler::build(std::vector tasks, std::vector availab double best_score = -1e9; for (const auto& task : tasks) { if (remaining[task.id] <= 0) continue; + if (cursor >= task.deadline_minute) continue; const bool ready = std::all_of(task.dependencies.begin(), task.dependencies.end(), [&](const auto& dep) { return completed.contains(dep); }); if (!ready) continue; - const int free = slot.end_minute - cursor; + const int free = std::min(slot.end_minute, task.deadline_minute) - cursor; if (free < std::min(task.minimum_chunk, remaining[task.id])) continue; const double candidate = score(task, slot, cursor, remaining[task.id]); if (candidate > best_score || (candidate == best_score && task.deadline_minute < best->deadline_minute)) { @@ -120,7 +128,7 @@ Plan Scheduler::build(std::vector tasks, std::vector availab } } if (!best) break; - const int chunk = std::min(remaining[best->id], slot.end_minute - cursor); + const int chunk = std::min({remaining[best->id], slot.end_minute - cursor, best->deadline_minute - cursor}); plan.blocks.push_back({best->id, best->title, cursor, cursor + chunk, best_score}); plan.objective += best_score * chunk / 60.0; remaining[best->id] -= chunk; @@ -128,7 +136,18 @@ Plan Scheduler::build(std::vector tasks, std::vector availab if (remaining[best->id] == 0) completed.insert(best->id); } } - for (const auto& [id, minutes] : remaining) if (minutes > 0) plan.unscheduled_minutes[id] = minutes; + const int horizon = availability.empty() ? 0 : std::max_element(availability.begin(), availability.end(), [](const auto& left, const auto& right) { + return left.end_minute < right.end_minute; + })->end_minute; + for (const auto& [id, minutes] : remaining) if (minutes > 0) { + plan.unscheduled_minutes[id] = minutes; + const auto& task = by_id.at(id); + const bool blocked = std::any_of(task.dependencies.begin(), task.dependencies.end(), [&](const auto& dependency) { + return remaining.at(dependency) > 0; + }); + plan.unscheduled_reasons[id] = blocked ? "dependency_incomplete" + : task.deadline_minute <= horizon ? "deadline_capacity" : "insufficient_capacity"; + } return plan; } @@ -188,9 +207,13 @@ std::string plan_to_json(const Plan& plan) { for (const auto& [id, minutes] : plan.unscheduled_minutes) { out << (index++ ? ", " : "") << "\"" << escape_json(id) << "\": " << minutes; } + out << "},\n \"unscheduled_reasons\": {"; + index = 0; + for (const auto& [id, reason] : plan.unscheduled_reasons) { + out << (index++ ? ", " : "") << "\"" << escape_json(id) << "\": \"" << escape_json(reason) << "\""; + } out << "}\n}\n"; return out.str(); } } // namespace plannerai - diff --git a/tests/planner_tests.cpp b/tests/planner_tests.cpp index a1328bc..c4b71dd 100644 --- a/tests/planner_tests.cpp +++ b/tests/planner_tests.cpp @@ -33,10 +33,26 @@ void cycles_are_rejected() { assert(threw); } +void hard_deadlines_are_never_crossed() { + std::vector tasks{{"due", "Due soon", 90, 5, 540, 30, .8, {}}}; + Plan plan = Scheduler().build(tasks, {{510, 600, .8}}); + assert(plan.blocks.size() == 1); + assert(plan.blocks[0].end_minute == 540); + assert(plan.unscheduled_minutes.at("due") == 60); + assert(plan.unscheduled_reasons.at("due") == "deadline_capacity"); +} + +void overlapping_availability_is_rejected() { + bool threw = false; + try { Scheduler().build({}, {{480, 540, .5}, {530, 600, .7}}); } catch (const std::invalid_argument&) { threw = true; } + assert(threw); +} + int main() { dependency_order_is_preserved(); impossible_capacity_is_reported(); cycles_are_rejected(); - std::cout << "3 planner tests passed\n"; + hard_deadlines_are_never_crossed(); + overlapping_availability_is_rejected(); + std::cout << "5 planner tests passed\n"; } -