diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 2c17627..4aec9dd 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1921,4 +1921,9 @@ add_executable(step320_test tests/step320_test.cpp) target_include_directories(step320_test PRIVATE src) target_link_libraries(step320_test PRIVATE nlohmann_json::nlohmann_json) +# Step 321: TaskQueue — Priority Queue with Dependencies +add_executable(step321_test tests/step321_test.cpp) +target_include_directories(step321_test PRIVATE src) +target_link_libraries(step321_test PRIVATE nlohmann_json::nlohmann_json) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/TaskQueue.h b/editor/src/TaskQueue.h new file mode 100644 index 0000000..1cb0978 --- /dev/null +++ b/editor/src/TaskQueue.h @@ -0,0 +1,200 @@ +#pragma once +// Step 321: TaskQueue — Priority Queue with Dependencies +// +// Ordered queue that respects both priority levels and dependency chains. +// Items whose dependencies are unsatisfied stay blocked; ready items are +// ordered by priority then creation time. + +#include "WorkItem.h" +#include +#include +#include +#include + +class TaskQueue { +public: + // Add a work item to the queue + void enqueue(WorkItem item) { + // Set status based on whether dependencies exist + if (item.dependencies.empty()) { + if (item.status == WI_PENDING) { + transitionWorkItem(item, WI_READY); + } + } + // else stays pending until dependencies complete + items_.push_back(std::move(item)); + } + + // Returns items whose dependencies are all complete, ordered by priority then createdAt + std::vector getReady() const { + std::vector ready; + for (const auto& item : items_) { + if (item.status == WI_READY) { + ready.push_back(item); + } + } + sortByPriority(ready); + return ready; + } + + // Next ready item without removing + std::optional peek() const { + auto ready = getReady(); + if (ready.empty()) return std::nullopt; + return ready.front(); + } + + // Remove and return next ready item, transition to assigned + std::optional dequeue() { + auto ready = getReady(); + if (ready.empty()) return std::nullopt; + + std::string id = ready.front().id; + for (auto& item : items_) { + if (item.id == id) { + transitionWorkItem(item, WI_ASSIGNED); + return item; + } + } + return std::nullopt; + } + + // Mark complete, trigger dependency re-evaluation + bool complete(const std::string& itemId) { + auto* item = findItem(itemId); + if (!item) return false; + + // Transition through necessary states to reach complete + if (item->status == WI_IN_PROGRESS || item->status == WI_REVIEW) { + transitionWorkItem(*item, WI_COMPLETE); + } else { + return false; + } + + // Re-evaluate blocked items + resolveDependencies(); + return true; + } + + // Mark rejected, re-enqueue as ready with feedback + bool reject(const std::string& itemId, const std::string& feedback) { + auto* item = findItem(itemId); + if (!item) return false; + + if (item->status != WI_REVIEW) return false; + + transitionWorkItem(*item, WI_REJECTED); + item->result.reasoning = feedback; + + // Re-enqueue: rejected → ready + transitionWorkItem(*item, WI_READY); + return true; + } + + // Items waiting on dependencies + std::vector getBlocked() const { + std::vector blocked; + for (const auto& item : items_) { + if (item.status == WI_PENDING && !item.dependencies.empty()) { + blocked.push_back(item); + } + } + return blocked; + } + + // Filter by lifecycle status + std::vector getByStatus(const std::string& status) const { + std::vector result; + for (const auto& item : items_) { + if (item.status == status) { + result.push_back(item); + } + } + return result; + } + + int size() const { return static_cast(items_.size()); } + + int readyCount() const { + int count = 0; + for (const auto& item : items_) { + if (item.status == WI_READY) ++count; + } + return count; + } + + int blockedCount() const { + int count = 0; + for (const auto& item : items_) { + if (item.status == WI_PENDING && !item.dependencies.empty()) ++count; + } + return count; + } + + // Lookup by ID + std::optional getItem(const std::string& itemId) const { + for (const auto& item : items_) { + if (item.id == itemId) return item; + } + return std::nullopt; + } + + // Replace item (for external status updates) + bool updateItem(const std::string& itemId, const WorkItem& updated) { + for (auto& item : items_) { + if (item.id == itemId) { + item = updated; + return true; + } + } + return false; + } + +private: + std::vector items_; + + WorkItem* findItem(const std::string& id) { + for (auto& item : items_) { + if (item.id == id) return &item; + } + return nullptr; + } + + // Check if a given item ID is complete + bool isComplete(const std::string& id) const { + for (const auto& item : items_) { + if (item.id == id) return item.status == WI_COMPLETE; + } + // Dependency not in queue — treat as satisfied + return true; + } + + // Scan blocked items and promote to ready if all deps complete + void resolveDependencies() { + for (auto& item : items_) { + if (item.status != WI_PENDING) continue; + if (item.dependencies.empty()) continue; + + bool allComplete = true; + for (const auto& dep : item.dependencies) { + if (!isComplete(dep)) { + allComplete = false; + break; + } + } + if (allComplete) { + transitionWorkItem(item, WI_READY); + } + } + } + + // Sort by priority (lower int = higher priority), then by createdAt + static void sortByPriority(std::vector& items) { + std::sort(items.begin(), items.end(), [](const WorkItem& a, const WorkItem& b) { + int pa = priorityToInt(a.priority); + int pb = priorityToInt(b.priority); + if (pa != pb) return pa < pb; + return a.createdAt < b.createdAt; + }); + } +}; diff --git a/editor/tests/step321_test.cpp b/editor/tests/step321_test.cpp new file mode 100644 index 0000000..9ab4344 --- /dev/null +++ b/editor/tests/step321_test.cpp @@ -0,0 +1,301 @@ +// Step 321: TaskQueue — Priority Queue with Dependencies (12 tests) +// Tests enqueue/dequeue ordering, dependency blocking, reject/re-enqueue, +// size tracking, completion cascading, mixed priorities with dependencies. + +#include "TaskQueue.h" +#include +#include +#include +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +// Helper: create a WorkItem with given id, priority, and optional dependencies +static WorkItem makeItem(const std::string& id, const std::string& priority, + const std::vector& deps = {}) { + WorkItem wi; + wi.id = id; + wi.nodeId = "node_" + id; + wi.nodeName = "func_" + id; + wi.nodeType = "Function"; + wi.bufferId = "buf"; + wi.contextWidth = "local"; + wi.workerType = "llm"; + wi.priority = priority; + wi.dependencies = deps; + wi.status = WI_PENDING; + wi.createdAt = workItemTimestamp(); + return wi; +} + +// 1. Enqueue/dequeue basic ordering +void test_enqueue_dequeue() { + TEST(enqueue_dequeue); + TaskQueue q; + q.enqueue(makeItem("a", "medium")); + q.enqueue(makeItem("b", "medium")); + + CHECK(q.size() == 2, "size=2"); + auto item = q.dequeue(); + CHECK(item.has_value(), "dequeue returns item"); + CHECK(item->id == "a", "FIFO for same priority: got " + item->id); + PASS(); +} + +// 2. Priority ordering — critical before low +void test_priority_ordering() { + TEST(priority_ordering); + TaskQueue q; + q.enqueue(makeItem("low1", "low")); + q.enqueue(makeItem("crit1", "critical")); + q.enqueue(makeItem("med1", "medium")); + q.enqueue(makeItem("high1", "high")); + + auto ready = q.getReady(); + CHECK(ready.size() == 4, "4 ready"); + CHECK(ready[0].id == "crit1", "first=critical"); + CHECK(ready[1].id == "high1", "second=high"); + CHECK(ready[2].id == "med1", "third=medium"); + CHECK(ready[3].id == "low1", "fourth=low"); + PASS(); +} + +// 3. Dependency blocking — B blocked by A, A completes → B becomes ready +void test_dependency_blocking() { + TEST(dependency_blocking); + TaskQueue q; + q.enqueue(makeItem("A", "medium")); + q.enqueue(makeItem("B", "medium", {"A"})); + + CHECK(q.readyCount() == 1, "1 ready (A)"); + CHECK(q.blockedCount() == 1, "1 blocked (B)"); + + // Dequeue A, progress it through to complete + auto a = q.dequeue(); + CHECK(a.has_value(), "dequeued A"); + // Manually transition: assigned → in-progress → complete + auto aItem = q.getItem("A"); + CHECK(aItem.has_value(), "A still in queue"); + WorkItem updated = *aItem; + transitionWorkItem(updated, WI_IN_PROGRESS); + q.updateItem("A", updated); + q.complete("A"); + + CHECK(q.readyCount() == 1, "B now ready"); + CHECK(q.blockedCount() == 0, "0 blocked"); + auto b = q.peek(); + CHECK(b.has_value() && b->id == "B", "B is next"); + PASS(); +} + +// 4. Reject and re-enqueue +void test_reject_reenqueue() { + TEST(reject_reenqueue); + TaskQueue q; + q.enqueue(makeItem("X", "high")); + + auto x = q.dequeue(); + CHECK(x.has_value(), "dequeued X"); + + // Move to in-progress → review + WorkItem updated = *q.getItem("X"); + transitionWorkItem(updated, WI_IN_PROGRESS); + q.updateItem("X", updated); + updated = *q.getItem("X"); + transitionWorkItem(updated, WI_REVIEW); + q.updateItem("X", updated); + + bool rejected = q.reject("X", "needs better error handling"); + CHECK(rejected, "reject succeeded"); + + auto xNow = q.getItem("X"); + CHECK(xNow.has_value(), "X still exists"); + CHECK(xNow->status == WI_READY, "X is ready again"); + CHECK(xNow->result.reasoning == "needs better error handling", "feedback preserved"); + PASS(); +} + +// 5. getReady returns only unblocked items +void test_get_ready_filtering() { + TEST(get_ready_filtering); + TaskQueue q; + q.enqueue(makeItem("r1", "medium")); + q.enqueue(makeItem("r2", "medium")); + q.enqueue(makeItem("b1", "medium", {"r1"})); + q.enqueue(makeItem("b2", "medium", {"r2"})); + + auto ready = q.getReady(); + CHECK(ready.size() == 2, "2 ready, got " + std::to_string(ready.size())); + // blocked items should not appear + for (const auto& r : ready) { + CHECK(r.id == "r1" || r.id == "r2", "only r1/r2 in ready"); + } + PASS(); +} + +// 6. Empty queue behavior +void test_empty_queue() { + TEST(empty_queue); + TaskQueue q; + CHECK(q.size() == 0, "empty size"); + CHECK(q.readyCount() == 0, "empty readyCount"); + CHECK(q.blockedCount() == 0, "empty blockedCount"); + CHECK(!q.peek().has_value(), "peek returns nullopt"); + CHECK(!q.dequeue().has_value(), "dequeue returns nullopt"); + CHECK(!q.getItem("nonexistent").has_value(), "getItem returns nullopt"); + PASS(); +} + +// 7. getByStatus filtering +void test_get_by_status() { + TEST(get_by_status); + TaskQueue q; + q.enqueue(makeItem("s1", "medium")); + q.enqueue(makeItem("s2", "medium")); + q.enqueue(makeItem("s3", "medium", {"s1"})); + + auto ready = q.getByStatus(WI_READY); + CHECK(ready.size() == 2, "2 ready"); + auto pending = q.getByStatus(WI_PENDING); + CHECK(pending.size() == 1, "1 pending"); + + q.dequeue(); // assign s1 + auto assigned = q.getByStatus(WI_ASSIGNED); + CHECK(assigned.size() == 1, "1 assigned"); + PASS(); +} + +// 8. Size tracking after operations +void test_size_tracking() { + TEST(size_tracking); + TaskQueue q; + q.enqueue(makeItem("t1", "low")); + q.enqueue(makeItem("t2", "high")); + q.enqueue(makeItem("t3", "medium", {"t1"})); + + CHECK(q.size() == 3, "size=3"); + CHECK(q.readyCount() == 2, "readyCount=2"); + CHECK(q.blockedCount() == 1, "blockedCount=1"); + + q.dequeue(); // dequeue t2 (highest priority) + CHECK(q.readyCount() == 1, "readyCount=1 after dequeue"); + PASS(); +} + +// 9. Completion cascading (A → B → C chain) +void test_completion_cascade() { + TEST(completion_cascade); + TaskQueue q; + q.enqueue(makeItem("c1", "high")); + q.enqueue(makeItem("c2", "high", {"c1"})); + q.enqueue(makeItem("c3", "high", {"c2"})); + + CHECK(q.readyCount() == 1, "only c1 ready"); + CHECK(q.blockedCount() == 2, "c2, c3 blocked"); + + // Complete c1 + q.dequeue(); // assign c1 + WorkItem u = *q.getItem("c1"); + transitionWorkItem(u, WI_IN_PROGRESS); + q.updateItem("c1", u); + q.complete("c1"); + + CHECK(q.readyCount() == 1, "c2 now ready"); + CHECK(q.blockedCount() == 1, "c3 still blocked"); + + // Complete c2 + q.dequeue(); // assign c2 + u = *q.getItem("c2"); + transitionWorkItem(u, WI_IN_PROGRESS); + q.updateItem("c2", u); + q.complete("c2"); + + CHECK(q.readyCount() == 1, "c3 now ready"); + CHECK(q.blockedCount() == 0, "none blocked"); + PASS(); +} + +// 10. Mixed priorities with dependencies +void test_mixed_priorities_deps() { + TEST(mixed_priorities_deps); + TaskQueue q; + q.enqueue(makeItem("lo", "low")); + q.enqueue(makeItem("hi", "critical", {"lo"})); + + // hi is critical but blocked, lo is low but ready + auto ready = q.getReady(); + CHECK(ready.size() == 1, "1 ready"); + CHECK(ready[0].id == "lo", "lo is ready despite lower priority"); + + // Complete lo → hi becomes ready + q.dequeue(); + WorkItem u = *q.getItem("lo"); + transitionWorkItem(u, WI_IN_PROGRESS); + q.updateItem("lo", u); + q.complete("lo"); + + ready = q.getReady(); + CHECK(ready.size() == 1, "hi now ready"); + CHECK(ready[0].id == "hi", "hi unblocked"); + PASS(); +} + +// 11. Peek does not remove +void test_peek_no_remove() { + TEST(peek_no_remove); + TaskQueue q; + q.enqueue(makeItem("p1", "medium")); + + auto first = q.peek(); + CHECK(first.has_value(), "peek returns value"); + auto second = q.peek(); + CHECK(second.has_value(), "peek still returns value"); + CHECK(first->id == second->id, "same item"); + CHECK(q.readyCount() == 1, "still 1 ready"); + PASS(); +} + +// 12. updateItem replaces item data +void test_update_item() { + TEST(update_item); + TaskQueue q; + q.enqueue(makeItem("u1", "medium")); + + auto item = *q.getItem("u1"); + item.assignee = "worker-42"; + item.workerType = "deterministic"; + q.updateItem("u1", item); + + auto updated = q.getItem("u1"); + CHECK(updated.has_value(), "item exists"); + CHECK(updated->assignee == "worker-42", "assignee updated"); + CHECK(updated->workerType == "deterministic", "workerType updated"); + PASS(); +} + +int main() { + std::cout << "=== Step 321: TaskQueue Priority Queue with Dependencies ===\n"; + try { + test_enqueue_dequeue(); + test_priority_ordering(); + test_dependency_blocking(); + test_reject_reenqueue(); + test_get_ready_filtering(); + test_empty_queue(); + test_get_by_status(); + test_size_tracking(); + test_completion_cascade(); + test_mixed_priorities_deps(); + test_peek_no_remove(); + test_update_item(); + } catch (const std::exception& e) { + std::cout << "EXCEPTION: " << e.what() << "\n"; + ++failed; + } + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed > 0 ? 1 : 0; +} diff --git a/progress.md b/progress.md index 8b18f47..49ca594 100644 --- a/progress.md +++ b/progress.md @@ -1226,6 +1226,25 @@ work that flows through the routing engine. **State machine:** pending→ready→assigned→in-progress→(review→complete|rejected OR complete); rejected→ready +### Step 321: TaskQueue — Priority Queue with Dependencies +**Status:** PASS (12/12 tests) + +Ordered queue that respects both priority levels and dependency chains. +Items whose dependencies are unsatisfied stay blocked; ready items are ordered +by priority (critical first) then creation time. + +**Files created:** +- `editor/src/TaskQueue.h` — TaskQueue class with enqueue, dequeue, peek, + complete, reject, getReady, getBlocked, getByStatus, getItem, updateItem; + dependency resolution on completion, priority-based sorting +- `editor/tests/step321_test.cpp` — 12 tests: enqueue/dequeue ordering, + priority ordering, dependency blocking, reject/re-enqueue, getReady + filtering, empty queue, getByStatus, size tracking, completion cascading + (A→B→C chain), mixed priorities with deps, peek no-remove, updateItem + +**Files modified:** +- `editor/CMakeLists.txt` — step321_test target + --- # Roadmap Planning — Sprints 12-25+