diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index c1e007e..83d4036 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3136,4 +3136,13 @@ target_link_libraries(step472_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step473_test tests/step473_test.cpp) +target_include_directories(step473_test PRIVATE src) +target_link_libraries(step473_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/ArchitectTechStackSelector.h b/editor/src/ArchitectTechStackSelector.h new file mode 100644 index 0000000..4ed4317 --- /dev/null +++ b/editor/src/ArchitectTechStackSelector.h @@ -0,0 +1,156 @@ +#pragma once + +// Step 473: Technology Stack Selector +// Chooses per-module language/framework/database suggestions from requirements +// and module graph, with optional preference overrides and rationale. + +#include "ArchitectModuleDecomposer.h" + +#include +#include +#include +#include + +struct TechChoice { + std::string moduleName; + std::string language; + std::string framework; + std::string database; + std::string rationale; + float confidence = 0.0f; +}; + +struct TechStackDecision { + std::vector choices; + std::map preferencesApplied; + + bool hasChoiceFor(const std::string& moduleName) const { + for (const auto& c : choices) if (c.moduleName == moduleName) return true; + return false; + } + + TechChoice getChoice(const std::string& moduleName) const { + for (const auto& c : choices) if (c.moduleName == moduleName) return c; + return {}; + } +}; + +class ArchitectTechStackSelector { +public: + static TechStackDecision select(const StructuredRequirements& reqs, + const ModuleGraph& graph, + const std::map& preferences = {}) { + TechStackDecision out; + out.preferencesApplied = preferences; + + std::string preferredBackend = pref(preferences, "backend_language"); + std::string preferredDb = pref(preferences, "database"); + std::string preferredFrontend = pref(preferences, "frontend_language"); + + for (const auto& mod : graph.nodes) { + if (mod.crossCutting) continue; + TechChoice c; + c.moduleName = mod.name; + + if (mod.name == "ui") { + c.language = preferredFrontend.empty() ? "typescript" : preferredFrontend; + c.framework = pickFrontendFramework(reqs); + c.database = ""; + c.rationale = "UI module mapped to frontend language/framework"; + c.confidence = 0.92f; + } else if (mod.name == "data") { + c.language = preferredBackend.empty() ? pickBackendLanguage(reqs, mod) : preferredBackend; + c.framework = ""; + c.database = preferredDb.empty() ? pickDatabase(reqs) : preferredDb; + c.rationale = "Data module mapped to persistence stack"; + c.confidence = 0.90f; + } else if (mod.name == "api" || mod.name == "auth" || mod.name == "search" || + mod.name == "reporting" || mod.name == "notifications" || mod.name == "core") { + c.language = preferredBackend.empty() ? pickBackendLanguage(reqs, mod) : preferredBackend; + c.framework = pickBackendFramework(c.language); + c.database = (mod.name == "api" || mod.name == "auth" || mod.name == "core") + ? (preferredDb.empty() ? pickDatabase(reqs) : preferredDb) + : ""; + c.rationale = "Service/backend module mapped to backend stack"; + c.confidence = 0.88f; + } else if (mod.name == "integrations") { + c.language = preferredBackend.empty() ? "python" : preferredBackend; + c.framework = "http-client"; + c.database = ""; + c.rationale = "Integration module favors high-velocity adapter language"; + c.confidence = 0.85f; + } else { + c.language = preferredBackend.empty() ? "python" : preferredBackend; + c.framework = ""; + c.database = ""; + c.rationale = "Fallback language selection"; + c.confidence = 0.75f; + } + + out.choices.push_back(std::move(c)); + } + + // Ensure we always return at least one choice for minimal graphs. + if (out.choices.empty()) { + out.choices.push_back({"core", "python", "", pickDatabase(reqs), + "Fallback stack decision for empty graph", 0.70f}); + } + + return out; + } + +private: + static std::string pref(const std::map& p, const std::string& key) { + auto it = p.find(key); + return it == p.end() ? "" : it->second; + } + + static bool hasKeyword(const std::vector& items, const std::string& keyword) { + auto k = toLower(keyword); + for (const auto& it : items) { + if (toLower(it.text).find(k) != std::string::npos) return true; + } + return false; + } + + static std::string pickBackendLanguage(const StructuredRequirements& reqs, const ModuleNode& mod) { + bool perf = hasKeyword(reqs.nonFunctionalRequirements, "performance") || + hasKeyword(reqs.nonFunctionalRequirements, "latency") || + hasKeyword(reqs.nonFunctionalRequirements, "throughput"); + bool secure = hasKeyword(reqs.nonFunctionalRequirements, "secure") || + hasKeyword(reqs.nonFunctionalRequirements, "security"); + if (mod.name == "search" && perf) return "rust"; + if ((mod.name == "api" || mod.name == "auth" || mod.name == "core") && (perf || secure)) + return "rust"; + if (hasKeyword(reqs.platformConstraints, "embedded")) return "c"; + return "python"; + } + + static std::string pickBackendFramework(const std::string& language) { + if (language == "rust") return "axum"; + if (language == "python") return "fastapi"; + if (language == "go") return "gin"; + if (language == "java") return "spring"; + return ""; + } + + static std::string pickFrontendFramework(const StructuredRequirements& reqs) { + (void)reqs; + return "react"; + } + + static std::string pickDatabase(const StructuredRequirements& reqs) { + if (hasKeyword(reqs.integrationRequirements, "postgresql") || + hasKeyword(reqs.integrationRequirements, "postgres")) return "postgresql"; + if (hasKeyword(reqs.integrationRequirements, "mysql")) return "mysql"; + if (hasKeyword(reqs.integrationRequirements, "sqlite")) return "sqlite"; + if (hasKeyword(reqs.integrationRequirements, "redis")) return "redis"; + return "postgresql"; + } + + static std::string toLower(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c){ return static_cast(std::tolower(c)); }); + return s; + } +}; diff --git a/editor/tests/step473_test.cpp b/editor/tests/step473_test.cpp new file mode 100644 index 0000000..77af578 --- /dev/null +++ b/editor/tests/step473_test.cpp @@ -0,0 +1,168 @@ +// Step 473: Technology Stack Selector Tests (12 tests) + +#include "ArchitectTechStackSelector.h" + +#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 StructuredRequirements reqFrom(const std::string& s) { + return ArchitectProblemParser::parse(s); +} + +static ModuleGraph graphFrom(const std::string& s, DecompositionStrategy strategy = DecompositionStrategy::Layered) { + auto req = reqFrom(s); + return ArchitectModuleDecomposer::decompose(req, strategy); +} + +void test_selects_frontend_typescript_for_ui_module() { + TEST(selects_frontend_typescript_for_ui_module); + auto req = reqFrom("Build a web dashboard."); + auto g = ArchitectModuleDecomposer::decompose(req); + auto d = ArchitectTechStackSelector::select(req, g); + CHECK(d.hasChoiceFor("ui"), "missing ui choice"); + auto c = d.getChoice("ui"); + CHECK(c.language == "typescript", "expected typescript for ui"); + CHECK(c.framework == "react", "expected react framework"); + PASS(); +} + +void test_selects_rust_for_perf_critical_api() { + TEST(selects_rust_for_perf_critical_api); + auto req = reqFrom("Need a secure low-latency REST API."); + auto g = ArchitectModuleDecomposer::decompose(req); + auto d = ArchitectTechStackSelector::select(req, g); + auto c = d.getChoice("api"); + CHECK(c.language == "rust", "expected rust for perf/secure api"); + PASS(); +} + +void test_selects_python_backend_when_no_perf_constraints() { + TEST(selects_python_backend_when_no_perf_constraints); + auto req = reqFrom("Build a REST API for internal admin tooling."); + auto g = ArchitectModuleDecomposer::decompose(req); + auto d = ArchitectTechStackSelector::select(req, g); + auto c = d.getChoice("api"); + CHECK(c.language == "python", "expected python default backend"); + CHECK(c.framework == "fastapi", "expected fastapi framework"); + PASS(); +} + +void test_selects_postgresql_when_requirement_mentions_postgres() { + TEST(selects_postgresql_when_requirement_mentions_postgres); + auto req = reqFrom("API with PostgreSQL storage."); + auto g = ArchitectModuleDecomposer::decompose(req); + auto d = ArchitectTechStackSelector::select(req, g); + auto c = d.getChoice("data"); + CHECK(c.database == "postgresql", "expected postgresql database"); + PASS(); +} + +void test_selects_mysql_when_requirement_mentions_mysql() { + TEST(selects_mysql_when_requirement_mentions_mysql); + auto req = reqFrom("Service with MySQL backend."); + auto g = ArchitectModuleDecomposer::decompose(req); + auto d = ArchitectTechStackSelector::select(req, g); + auto c = d.getChoice("data"); + CHECK(c.database == "mysql", "expected mysql database"); + PASS(); +} + +void test_applies_backend_language_preference_override() { + TEST(applies_backend_language_preference_override); + auto req = reqFrom("Need a secure low-latency API."); + auto g = ArchitectModuleDecomposer::decompose(req); + auto d = ArchitectTechStackSelector::select(req, g, {{"backend_language", "go"}}); + auto c = d.getChoice("api"); + CHECK(c.language == "go", "expected backend preference override"); + PASS(); +} + +void test_applies_database_preference_override() { + TEST(applies_database_preference_override); + auto req = reqFrom("API with postgres."); + auto g = ArchitectModuleDecomposer::decompose(req); + auto d = ArchitectTechStackSelector::select(req, g, {{"database", "sqlite"}}); + auto c = d.getChoice("data"); + CHECK(c.database == "sqlite", "expected database preference override"); + PASS(); +} + +void test_applies_frontend_language_preference_override() { + TEST(applies_frontend_language_preference_override); + auto req = reqFrom("Build a web dashboard."); + auto g = ArchitectModuleDecomposer::decompose(req); + auto d = ArchitectTechStackSelector::select(req, g, {{"frontend_language", "elm"}}); + auto c = d.getChoice("ui"); + CHECK(c.language == "elm", "expected frontend preference override"); + PASS(); +} + +void test_assigns_integrations_module_to_adapter_stack() { + TEST(assigns_integrations_module_to_adapter_stack); + auto req = reqFrom("Integrate with Stripe and Salesforce APIs."); + auto g = ArchitectModuleDecomposer::decompose(req); + auto d = ArchitectTechStackSelector::select(req, g); + auto c = d.getChoice("integrations"); + CHECK(c.language == "python", "expected python for integration adapters"); + CHECK(c.framework == "http-client", "expected adapter framework"); + PASS(); +} + +void test_confidence_scores_in_reasonable_range() { + TEST(confidence_scores_in_reasonable_range); + auto req = reqFrom("Web dashboard with API and postgres."); + auto g = ArchitectModuleDecomposer::decompose(req); + auto d = ArchitectTechStackSelector::select(req, g); + for (const auto& c : d.choices) { + CHECK(c.confidence >= 0.0f && c.confidence <= 1.0f, "confidence out of range"); + } + PASS(); +} + +void test_every_non_crosscutting_module_has_choice() { + TEST(every_non_crosscutting_module_has_choice); + auto req = reqFrom("Web API with auth, search, and PostgreSQL."); + auto g = ArchitectModuleDecomposer::decompose(req); + auto d = ArchitectTechStackSelector::select(req, g); + for (const auto& n : g.nodes) { + if (n.crossCutting) continue; + CHECK(d.hasChoiceFor(n.name), "missing tech choice for module"); + } + PASS(); +} + +void test_fallback_choice_for_empty_graph() { + TEST(fallback_choice_for_empty_graph); + StructuredRequirements req; + ModuleGraph g; + auto d = ArchitectTechStackSelector::select(req, g); + CHECK(!d.choices.empty(), "expected fallback choice"); + CHECK(d.choices[0].moduleName == "core", "expected core fallback choice"); + PASS(); +} + +int main() { + std::cout << "Step 473: Technology Stack Selector Tests\n"; + + test_selects_frontend_typescript_for_ui_module(); // 1 + test_selects_rust_for_perf_critical_api(); // 2 + test_selects_python_backend_when_no_perf_constraints();// 3 + test_selects_postgresql_when_requirement_mentions_postgres(); // 4 + test_selects_mysql_when_requirement_mentions_mysql(); // 5 + test_applies_backend_language_preference_override(); // 6 + test_applies_database_preference_override(); // 7 + test_applies_frontend_language_preference_override(); // 8 + test_assigns_integrations_module_to_adapter_stack(); // 9 + test_confidence_scores_in_reasonable_range(); // 10 + test_every_non_crosscutting_module_has_choice(); // 11 + test_fallback_choice_for_empty_graph(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 29fb191..7987e5d 100644 --- a/progress.md +++ b/progress.md @@ -6505,3 +6505,36 @@ with module graph nodes/edges, cross-cutting concerns, and complexity scoring. **Architecture gate check:** - `editor/src/ArchitectModuleDecomposer.h` within header-size limit (`212` <= `600`) - `editor/tests/step472_test.cpp` within test-file size guidance (`156` lines) + +### Step 473: Technology Stack Selector +**Status:** PASS (12/12 tests) + +Implements per-module technology stack selection from requirements + module +graph, including language/framework/database choices, rationale, confidence, +and preference overrides. + +**Files added:** +- `editor/src/ArchitectTechStackSelector.h` — stack decision engine: + - output model: `TechChoice`, `TechStackDecision` + - strategy-informed module stack selection for UI/API/auth/data/integrations/etc. + - backend/frontend/database preference overrides + - language/framework/database heuristics with rationale/confidence + - fallback behavior for sparse graphs +- `editor/tests/step473_test.cpp` — 12 tests covering: + - UI/frontend stack selection + - performance/security-driven backend language selection + - database selection from integration requirements + - preference override behavior (backend/db/frontend) + - integration module adapter stack behavior + - confidence range checks + - per-module coverage and fallback decision behavior +- `editor/CMakeLists.txt` — `step473_test` target + +**Verification run:** +- `cmake --build editor/build-native --target step473_test` — PASS +- `./editor/build-native/step473_test` — PASS (12/12) +- `./editor/build-native/step472_test` — PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/ArchitectTechStackSelector.h` within header-size limit (`156` <= `600`) +- `editor/tests/step473_test.cpp` within test-file size guidance (`168` lines)