diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index d05da85..1f94ead 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1956,4 +1956,9 @@ target_link_libraries(step325_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +# Step 326: RoutingEngine — Annotation-to-Dispatch Logic +add_executable(step326_test tests/step326_test.cpp) +target_include_directories(step326_test PRIVATE src) +target_link_libraries(step326_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/RoutingEngine.h b/editor/src/RoutingEngine.h new file mode 100644 index 0000000..739c695 --- /dev/null +++ b/editor/src/RoutingEngine.h @@ -0,0 +1,192 @@ +#pragma once +// Step 326: RoutingEngine — Annotation-to-Dispatch Logic +// +// Given a WorkItem with routing annotations, produces a RoutingDecision +// that says what kind of worker should handle it and how much context +// to provide. Rules follow a priority cascade from explicit annotations +// through complexity and context width heuristics to pattern defaults. + +#include "WorkItem.h" +#include +#include +#include + +// --- RoutingDecision --- + +struct RoutingDecision { + std::string workerType; // "deterministic" | "template" | "slm" | "llm" | "human" + std::string contextWidth; // "local" | "file" | "project" | "cross-project" + int contextBudgetTokens = 0; // estimated tokens for context window + bool reviewRequired = false; // whether result needs human review + float confidence = 0.0f; // engine's confidence in this routing (0.0-1.0) + std::string reasoning; // human-readable explanation + std::string agentRole; // "linter" | "refactor" | "generator" + + json toJson() const { + return json{ + {"workerType", workerType}, + {"contextWidth", contextWidth}, + {"contextBudgetTokens", contextBudgetTokens}, + {"reviewRequired", reviewRequired}, + {"confidence", confidence}, + {"reasoning", reasoning}, + {"agentRole", agentRole} + }; + } + + static RoutingDecision fromJson(const json& j) { + RoutingDecision d; + if (j.contains("workerType")) d.workerType = j["workerType"].get(); + if (j.contains("contextWidth")) d.contextWidth = j["contextWidth"].get(); + if (j.contains("contextBudgetTokens")) d.contextBudgetTokens = j["contextBudgetTokens"].get(); + if (j.contains("reviewRequired")) d.reviewRequired = j["reviewRequired"].get(); + if (j.contains("confidence")) d.confidence = j["confidence"].get(); + if (j.contains("reasoning")) d.reasoning = j["reasoning"].get(); + if (j.contains("agentRole")) d.agentRole = j["agentRole"].get(); + return d; + } +}; + +// --- Context budget estimation --- + +inline int estimateContextBudget(const std::string& contextWidth) { + if (contextWidth == "local") return 500; + if (contextWidth == "file") return 2000; + if (contextWidth == "project") return 8000; + if (contextWidth == "cross-project") return 16000; + return 500; // default to local +} + +// --- Agent role from worker type --- + +inline std::string agentRoleForWorker(const std::string& workerType) { + if (workerType == "deterministic" || workerType == "template") return "refactor"; + if (workerType == "human") return "generator"; + return "generator"; // slm, llm +} + +// --- Pattern detection --- + +inline bool isGetterSetterPattern(const std::string& nodeName) { + if (nodeName.size() < 4) return false; + std::string lower = nodeName; + std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); + return lower.substr(0, 3) == "get" || lower.substr(0, 3) == "set" || + lower.substr(0, 2) == "is"; +} + +// --- RoutingEngine --- + +class RoutingEngine { +public: + // Main entry point: route a single work item + RoutingDecision route(const WorkItem& item) const { + RoutingDecision decision; + decision.contextWidth = item.contextWidth.empty() ? "local" : item.contextWidth; + decision.reviewRequired = item.reviewRequired; + decision.contextBudgetTokens = estimateContextBudget(decision.contextWidth); + + // Track how many annotations contributed to the decision + int annotationSignals = 0; + + // Rule 1: Explicit @Automatability override + if (!item.workerType.empty() && item.workerType != "llm") { + // Non-default workerType means architect explicitly set it + decision.workerType = item.workerType; + decision.reasoning = "Explicit @Automatability(" + item.workerType + ")"; + decision.confidence = 0.95f; + annotationSignals++; + // Human routing always requires review + if (item.workerType == "human") { + decision.reviewRequired = true; + decision.confidence = 0.9f; + decision.reasoning = "Routed to human (high ambiguity or explicit)"; + } + decision.agentRole = agentRoleForWorker(decision.workerType); + return decision; + } + + // Rule 3: Review gate + if (item.reviewRequired) { + annotationSignals++; + } + + // Rule 5: Context width escalation (evaluated before complexity + // since we may not have complexity annotations) + if (decision.contextWidth == "cross-project") { + decision.workerType = "llm"; + decision.reasoning = "Cross-project context requires LLM"; + decision.confidence = 0.8f; + annotationSignals++; + decision.agentRole = "generator"; + return decision; + } + + if (decision.contextWidth == "project") { + decision.workerType = "llm"; + decision.reasoning = "Project-wide context favors LLM"; + decision.confidence = 0.75f; + annotationSignals++; + decision.agentRole = "generator"; + return decision; + } + + // Rule 6: Pattern defaults (no complexity annotations available + // through WorkItem, so use heuristics) + + // Getter/setter/accessor → deterministic + if (isGetterSetterPattern(item.nodeName)) { + decision.workerType = "deterministic"; + decision.reasoning = "Getter/setter pattern → deterministic"; + decision.confidence = 0.9f; + decision.agentRole = "refactor"; + return decision; + } + + // Context-based defaults + if (decision.contextWidth == "local") { + // Local context: simple enough for template or SLM + decision.workerType = "slm"; + decision.reasoning = "Local context, default to SLM"; + decision.confidence = 0.6f; + decision.agentRole = "generator"; + } else if (decision.contextWidth == "file") { + decision.workerType = "slm"; + decision.reasoning = "File context, default to SLM"; + decision.confidence = 0.55f; + decision.agentRole = "generator"; + } else { + // Fallback + decision.workerType = "llm"; + decision.reasoning = "Default routing to LLM"; + decision.confidence = 0.5f; + decision.agentRole = "generator"; + } + + // Boost confidence if we have annotation signals + if (annotationSignals > 0) { + decision.confidence = std::min(0.95f, + decision.confidence + 0.1f * annotationSignals); + } + + return decision; + } + + // Batch routing + std::vector routeBatch(const std::vector& items) const { + std::vector decisions; + decisions.reserve(items.size()); + for (const auto& item : items) { + decisions.push_back(route(item)); + } + return decisions; + } + + // Route and apply: updates the WorkItem's workerType + reviewRequired + void routeAndApply(WorkItem& item) const { + auto decision = route(item); + item.workerType = decision.workerType; + item.reviewRequired = decision.reviewRequired; + item.contextWidth = decision.contextWidth; + } +}; diff --git a/editor/tests/step326_test.cpp b/editor/tests/step326_test.cpp new file mode 100644 index 0000000..98c4417 --- /dev/null +++ b/editor/tests/step326_test.cpp @@ -0,0 +1,190 @@ +// Step 326: RoutingEngine — Annotation-to-Dispatch Logic (12 tests) +// Tests routing rules: explicit override, ambiguity, context width, +// pattern defaults, batch routing, confidence, context budget. + +#include "RoutingEngine.h" +#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 {} + +static WorkItem makeItem(const std::string& name, + const std::string& workerType = "", + const std::string& contextWidth = "local", + bool reviewRequired = false) { + WorkItem wi; + wi.id = "wi-" + name; + wi.nodeId = "n-" + name; + wi.nodeName = name; + wi.nodeType = "Function"; + wi.bufferId = "buf"; + wi.workerType = workerType; + wi.contextWidth = contextWidth; + wi.reviewRequired = reviewRequired; + wi.priority = "medium"; + wi.createdAt = workItemTimestamp(); + return wi; +} + +RoutingEngine engine; + +// 1. Explicit @Automatability overrides inferred routing +void test_explicit_override() { + TEST(explicit_override); + auto wi = makeItem("compute", "deterministic", "project"); + auto d = engine.route(wi); + CHECK(d.workerType == "deterministic", "explicit deterministic honored"); + CHECK(d.confidence > 0.9f, "high confidence for explicit"); + CHECK(d.reasoning.find("Explicit") != std::string::npos, "reasoning mentions explicit"); + PASS(); +} + +// 2. High ambiguity → human +void test_ambiguity_human() { + TEST(ambiguity_human); + auto wi = makeItem("ambiguous", "human", "local"); + auto d = engine.route(wi); + CHECK(d.workerType == "human", "routed to human"); + CHECK(d.reviewRequired, "review required for human"); + PASS(); +} + +// 3. Low complexity + local → deterministic (getter pattern) +void test_getter_deterministic() { + TEST(getter_deterministic); + auto wi = makeItem("getName", "", "local"); + auto d = engine.route(wi); + CHECK(d.workerType == "deterministic", "getter → deterministic"); + CHECK(d.agentRole == "refactor", "agent role = refactor"); + PASS(); +} + +// 4. Setter pattern → deterministic +void test_setter_deterministic() { + TEST(setter_deterministic); + auto wi = makeItem("setValue", "", "local"); + auto d = engine.route(wi); + CHECK(d.workerType == "deterministic", "setter → deterministic"); + PASS(); +} + +// 5. Cross-project → LLM +void test_cross_project_llm() { + TEST(cross_project_llm); + auto wi = makeItem("orchestrate", "", "cross-project"); + auto d = engine.route(wi); + CHECK(d.workerType == "llm", "cross-project → llm"); + CHECK(d.contextBudgetTokens == 16000, "16000 token budget"); + PASS(); +} + +// 6. Project context → LLM +void test_project_llm() { + TEST(project_llm); + auto wi = makeItem("buildGraph", "", "project"); + auto d = engine.route(wi); + CHECK(d.workerType == "llm", "project → llm"); + CHECK(d.contextBudgetTokens == 8000, "8000 token budget"); + PASS(); +} + +// 7. Review annotation sets reviewRequired +void test_review_annotation() { + TEST(review_annotation); + auto wi = makeItem("sensitive", "", "local", true); + auto d = engine.route(wi); + CHECK(d.reviewRequired, "review preserved from annotation"); + // Should also boost confidence slightly + CHECK(d.confidence > 0.5f, "confidence boosted by annotation signal"); + PASS(); +} + +// 8. Batch routing +void test_batch_routing() { + TEST(batch_routing); + std::vector items = { + makeItem("getName", "", "local"), + makeItem("compute", "", "project"), + makeItem("transform", "human", "local"), + }; + auto decisions = engine.routeBatch(items); + CHECK(decisions.size() == 3, "3 decisions"); + CHECK(decisions[0].workerType == "deterministic", "first=deterministic"); + CHECK(decisions[1].workerType == "llm", "second=llm"); + CHECK(decisions[2].workerType == "human", "third=human"); + PASS(); +} + +// 9. No-annotation defaults (local context, no patterns) +void test_default_local() { + TEST(default_local); + auto wi = makeItem("processData", "", "local"); + auto d = engine.route(wi); + CHECK(d.workerType == "slm", "local default → slm"); + CHECK(d.contextBudgetTokens == 500, "500 token budget"); + PASS(); +} + +// 10. File context default +void test_default_file() { + TEST(default_file); + auto wi = makeItem("handleRequest", "", "file"); + auto d = engine.route(wi); + CHECK(d.workerType == "slm", "file default → slm"); + CHECK(d.contextBudgetTokens == 2000, "2000 token budget"); + PASS(); +} + +// 11. Confidence reflects annotation coverage +void test_confidence_levels() { + TEST(confidence_levels); + // Explicit annotation: highest confidence + auto wi1 = makeItem("f1", "template", "local"); + auto d1 = engine.route(wi1); + + // No annotations, just defaults: lower confidence + auto wi2 = makeItem("f2", "", "local"); + auto d2 = engine.route(wi2); + + CHECK(d1.confidence > d2.confidence, + "explicit has higher confidence than default"); + PASS(); +} + +// 12. Context budget reasonable for each width +void test_context_budgets() { + TEST(context_budgets); + CHECK(estimateContextBudget("local") == 500, "local=500"); + CHECK(estimateContextBudget("file") == 2000, "file=2000"); + CHECK(estimateContextBudget("project") == 8000, "project=8000"); + CHECK(estimateContextBudget("cross-project") == 16000, "cross-project=16000"); + CHECK(estimateContextBudget("unknown") == 500, "unknown defaults to 500"); + PASS(); +} + +int main() { + std::cout << "=== Step 326: RoutingEngine Annotation-to-Dispatch Logic ===\n"; + try { + test_explicit_override(); + test_ambiguity_human(); + test_getter_deterministic(); + test_setter_deterministic(); + test_cross_project_llm(); + test_project_llm(); + test_review_annotation(); + test_batch_routing(); + test_default_local(); + test_default_file(); + test_confidence_levels(); + test_context_budgets(); + } 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 4a73213..11544d4 100644 --- a/progress.md +++ b/progress.md @@ -1333,6 +1333,27 @@ rejection flow, persistence roundtrip, stats accuracy, and a combined **Phase 12a complete:** 6/6 steps (320-325), 68/68 tests passing +## Phase 12b: Routing Engine + Workers + +### Step 326: RoutingEngine — Annotation-to-Dispatch Logic +**Status:** PASS (12/12 tests) + +Given a WorkItem with routing annotations, produces a RoutingDecision with +worker type, context width, token budget, review requirements, confidence, +and reasoning. Priority cascade: explicit override → ambiguity gate → +context width escalation → getter/setter patterns → defaults. + +**Files created:** +- `editor/src/RoutingEngine.h` — RoutingDecision struct (with JSON helpers), + estimateContextBudget, agentRoleForWorker, isGetterSetterPattern, RoutingEngine + class with route, routeBatch, routeAndApply +- `editor/tests/step326_test.cpp` — 12 tests: explicit override, ambiguity→human, + getter/setter→deterministic, cross-project→llm, project→llm, review annotation, + batch routing, default local/file, confidence levels, context budgets + +**Files modified:** +- `editor/CMakeLists.txt` — step326_test target + --- # Roadmap Planning — Sprints 12-25+