diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 60cdf67..c1e007e 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3127,4 +3127,13 @@ target_link_libraries(step471_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step472_test tests/step472_test.cpp) +target_include_directories(step472_test PRIVATE src) +target_link_libraries(step472_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/ArchitectModuleDecomposer.h b/editor/src/ArchitectModuleDecomposer.h new file mode 100644 index 0000000..64f92be --- /dev/null +++ b/editor/src/ArchitectModuleDecomposer.h @@ -0,0 +1,212 @@ +#pragma once + +// Step 472: Module Decomposition Engine +// Produces a module graph from structured requirements with strategy-specific +// decomposition behavior (layered/monolith/microservice). + +#include "ArchitectProblemParser.h" + +#include +#include +#include +#include +#include + +enum class DecompositionStrategy { + Layered, + Monolith, + Microservice +}; + +struct ModuleNode { + std::string name; + std::vector responsibilities; + int complexity = 1; // 1-10 heuristic + bool crossCutting = false; +}; + +struct ModuleEdge { + std::string from; + std::string to; + std::string reason; +}; + +struct ModuleGraph { + std::vector nodes; + std::vector edges; + DecompositionStrategy strategy = DecompositionStrategy::Layered; + + bool hasModule(const std::string& name) const { + for (const auto& n : nodes) if (n.name == name) return true; + return false; + } + + int dependencyCountFor(const std::string& moduleName) const { + int count = 0; + for (const auto& e : edges) + if (e.from == moduleName || e.to == moduleName) ++count; + return count; + } +}; + +class ArchitectModuleDecomposer { +public: + static ModuleGraph decompose(const StructuredRequirements& reqs, + DecompositionStrategy strategy = DecompositionStrategy::Layered) { + ModuleGraph g; + g.strategy = strategy; + + // Core modules inferred from functional/integration requirements + maybeAddAuth(reqs, g); + maybeAddApi(reqs, g); + maybeAddUi(reqs, g); + maybeAddData(reqs, g); + maybeAddSearch(reqs, g); + maybeAddReporting(reqs, g); + maybeAddNotifications(reqs, g); + maybeAddIntegration(reqs, g); + + if (strategy == DecompositionStrategy::Monolith) { + addModule(g, "core", {"application orchestration", "module coordination"}, 5); + } + + if (g.nodes.empty()) { + g.nodes.push_back({"core", {"core application behavior"}, 3, false}); + } + + addCrossCutting(reqs, g); + addEdges(strategy, g); + normalizeComplexity(g); + return g; + } + +private: + static void addModule(ModuleGraph& g, + const std::string& name, + const std::vector& responsibilities, + int complexity, + bool crossCutting = false) { + if (g.hasModule(name)) return; + g.nodes.push_back({name, responsibilities, complexity, crossCutting}); + } + + static bool hasReqKeyword(const std::vector& items, const std::string& key) { + auto lk = toLower(key); + for (const auto& it : items) { + if (toLower(it.text).find(lk) != std::string::npos) return true; + } + return false; + } + + static bool hasAny(const StructuredRequirements& r, + const std::vector& keys) { + for (const auto& k : keys) { + if (hasReqKeyword(r.functionalRequirements, k)) return true; + if (hasReqKeyword(r.integrationRequirements, k)) return true; + if (hasReqKeyword(r.platformConstraints, k)) return true; + } + return false; + } + + static void maybeAddAuth(const StructuredRequirements& r, ModuleGraph& g) { + if (hasAny(r, {"auth", "login", "signup", "oauth", "jwt"})) + addModule(g, "auth", {"identity", "session", "authorization"}, 6); + } + + static void maybeAddApi(const StructuredRequirements& r, ModuleGraph& g) { + if (hasAny(r, {"api", "rest", "endpoint", "crud"})) + addModule(g, "api", {"http routing", "request validation", "response mapping"}, 6); + } + + static void maybeAddUi(const StructuredRequirements& r, ModuleGraph& g) { + if (hasAny(r, {"web", "mobile", "frontend", "dashboard", "ios", "android"})) + addModule(g, "ui", {"presentation", "interaction", "client state"}, 5); + } + + static void maybeAddData(const StructuredRequirements& r, ModuleGraph& g) { + if (hasAny(r, {"postgres", "postgresql", "mysql", "sqlite", "redis", "data"})) + addModule(g, "data", {"persistence", "queries", "transactions"}, 7); + } + + static void maybeAddSearch(const StructuredRequirements& r, ModuleGraph& g) { + if (hasAny(r, {"search"})) + addModule(g, "search", {"indexing", "query parsing"}, 5); + } + + static void maybeAddReporting(const StructuredRequirements& r, ModuleGraph& g) { + if (hasAny(r, {"reporting"})) + addModule(g, "reporting", {"aggregation", "export", "analytics"}, 5); + } + + static void maybeAddNotifications(const StructuredRequirements& r, ModuleGraph& g) { + if (hasAny(r, {"notification"})) + addModule(g, "notifications", {"event fanout", "delivery"}, 4); + } + + static void maybeAddIntegration(const StructuredRequirements& r, ModuleGraph& g) { + if (hasAny(r, {"stripe", "salesforce", "ldap", "kafka", "s3"})) + addModule(g, "integrations", {"external api clients", "adapters"}, 6); + } + + static void addCrossCutting(const StructuredRequirements& r, ModuleGraph& g) { + bool secure = hasReqKeyword(r.nonFunctionalRequirements, "secure") || + hasReqKeyword(r.nonFunctionalRequirements, "security"); + bool scalable = hasReqKeyword(r.nonFunctionalRequirements, "scalable") || + hasReqKeyword(r.nonFunctionalRequirements, "performance"); + + addModule(g, "logging", {"structured logs", "trace context"}, 2, true); + addModule(g, "config", {"env config", "feature flags"}, 2, true); + addModule(g, "error_handling", {"error taxonomy", "mapping"}, 2, true); + if (secure) addModule(g, "security", {"policy", "hardening"}, 4, true); + if (scalable) addModule(g, "observability", {"metrics", "alerts"}, 3, true); + } + + static void addEdges(DecompositionStrategy strategy, ModuleGraph& g) { + auto edge = [&g](const std::string& a, const std::string& b, const std::string& reason) { + if (!g.hasModule(a) || !g.hasModule(b)) return; + g.edges.push_back({a, b, reason}); + }; + + if (strategy == DecompositionStrategy::Layered) { + edge("ui", "api", "ui consumes api"); + edge("api", "auth", "endpoint auth checks"); + edge("api", "data", "persistence access"); + edge("api", "search", "search endpoints"); + edge("api", "reporting", "report generation"); + } else if (strategy == DecompositionStrategy::Microservice) { + edge("api", "auth", "auth service call"); + edge("api", "data", "data service call"); + edge("api", "integrations", "integration gateway"); + edge("notifications", "integrations", "delivery integrations"); + } else { // Monolith + edge("core", "data", "monolith persistence"); + edge("core", "auth", "monolith auth"); + edge("core", "ui", "monolith ui"); + } + + // Cross-cutting edges + std::vector cross; + for (const auto& n : g.nodes) if (n.crossCutting) cross.push_back(n.name); + for (const auto& n : g.nodes) { + if (n.crossCutting) continue; + for (const auto& c : cross) edge(n.name, c, "cross-cutting concern"); + } + } + + static void normalizeComplexity(ModuleGraph& g) { + std::map incoming; + for (const auto& e : g.edges) incoming[e.to]++; + for (auto& n : g.nodes) { + int c = n.complexity + incoming[n.name]; + if (c < 1) c = 1; + if (c > 10) c = 10; + n.complexity = c; + } + } + + 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/step472_test.cpp b/editor/tests/step472_test.cpp new file mode 100644 index 0000000..1a0fde2 --- /dev/null +++ b/editor/tests/step472_test.cpp @@ -0,0 +1,156 @@ +// Step 472: Module Decomposition Engine Tests (12 tests) + +#include "ArchitectModuleDecomposer.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 reqFromText(const std::string& s) { + return ArchitectProblemParser::parse(s); +} + +void test_decomposes_auth_api_data_modules() { + TEST(decomposes_auth_api_data_modules); + auto req = reqFromText("Build a REST API with auth and PostgreSQL."); + auto g = ArchitectModuleDecomposer::decompose(req); + CHECK(g.hasModule("auth"), "missing auth module"); + CHECK(g.hasModule("api"), "missing api module"); + CHECK(g.hasModule("data"), "missing data module"); + PASS(); +} + +void test_decomposes_ui_module_from_web_mobile_constraints() { + TEST(decomposes_ui_module_from_web_mobile_constraints); + auto req = reqFromText("Create a web and mobile dashboard."); + auto g = ArchitectModuleDecomposer::decompose(req); + CHECK(g.hasModule("ui"), "missing ui module"); + PASS(); +} + +void test_decomposes_search_reporting_modules() { + TEST(decomposes_search_reporting_modules); + auto req = reqFromText("App requires search and reporting features."); + auto g = ArchitectModuleDecomposer::decompose(req); + CHECK(g.hasModule("search"), "missing search module"); + CHECK(g.hasModule("reporting"), "missing reporting module"); + PASS(); +} + +void test_adds_integration_module_for_external_systems() { + TEST(adds_integration_module_for_external_systems); + auto req = reqFromText("Integrate Stripe and Salesforce APIs."); + auto g = ArchitectModuleDecomposer::decompose(req); + CHECK(g.hasModule("integrations"), "missing integrations module"); + PASS(); +} + +void test_cross_cutting_modules_are_present() { + TEST(cross_cutting_modules_are_present); + auto req = reqFromText("Build a REST API."); + auto g = ArchitectModuleDecomposer::decompose(req); + CHECK(g.hasModule("logging"), "missing logging"); + CHECK(g.hasModule("config"), "missing config"); + CHECK(g.hasModule("error_handling"), "missing error_handling"); + PASS(); +} + +void test_security_nonfunctional_adds_security_module() { + TEST(security_nonfunctional_adds_security_module); + auto req = reqFromText("System must be secure and reliable."); + auto g = ArchitectModuleDecomposer::decompose(req); + CHECK(g.hasModule("security"), "missing security module"); + PASS(); +} + +void test_layered_strategy_generates_expected_dependencies() { + TEST(layered_strategy_generates_expected_dependencies); + auto req = reqFromText("Web REST API with auth and PostgreSQL."); + auto g = ArchitectModuleDecomposer::decompose(req, DecompositionStrategy::Layered); + bool sawUiApi = false; + bool sawApiData = false; + for (const auto& e : g.edges) { + if (e.from == "ui" && e.to == "api") sawUiApi = true; + if (e.from == "api" && e.to == "data") sawApiData = true; + } + CHECK(sawUiApi, "missing ui->api edge"); + CHECK(sawApiData, "missing api->data edge"); + PASS(); +} + +void test_microservice_strategy_changes_edge_shape() { + TEST(microservice_strategy_changes_edge_shape); + auto req = reqFromText("REST API with auth and stripe integration."); + auto g = ArchitectModuleDecomposer::decompose(req, DecompositionStrategy::Microservice); + bool sawApiIntegrations = false; + for (const auto& e : g.edges) { + if (e.from == "api" && e.to == "integrations") sawApiIntegrations = true; + } + CHECK(sawApiIntegrations, "missing api->integrations edge in microservice strategy"); + PASS(); +} + +void test_monolith_strategy_uses_core_hub() { + TEST(monolith_strategy_uses_core_hub); + auto req = reqFromText("simple app with auth and web ui"); + auto g = ArchitectModuleDecomposer::decompose(req, DecompositionStrategy::Monolith); + bool sawCoreDep = false; + for (const auto& e : g.edges) { + if (e.from == "core") sawCoreDep = true; + } + CHECK(g.hasModule("core"), "missing core module"); + CHECK(sawCoreDep, "expected core dependencies in monolith strategy"); + PASS(); +} + +void test_complexity_scores_are_clamped() { + TEST(complexity_scores_are_clamped); + auto req = reqFromText("web mobile api auth search reporting notification postgres stripe secure scalable"); + auto g = ArchitectModuleDecomposer::decompose(req); + for (const auto& n : g.nodes) { + CHECK(n.complexity >= 1 && n.complexity <= 10, "complexity out of range"); + } + PASS(); +} + +void test_dependency_count_helper_reports_nonzero() { + TEST(dependency_count_helper_reports_nonzero); + auto req = reqFromText("REST API with auth and PostgreSQL."); + auto g = ArchitectModuleDecomposer::decompose(req); + CHECK(g.dependencyCountFor("api") > 0, "expected api dependencies"); + PASS(); +} + +void test_fallback_core_module_when_sparse_requirements() { + TEST(fallback_core_module_when_sparse_requirements); + StructuredRequirements req; + req.functionalRequirements = {{"core application behavior", 0.55f}}; + auto g = ArchitectModuleDecomposer::decompose(req, DecompositionStrategy::Monolith); + CHECK(g.hasModule("core"), "missing fallback core module"); + PASS(); +} + +int main() { + std::cout << "Step 472: Module Decomposition Engine Tests\n"; + + test_decomposes_auth_api_data_modules(); // 1 + test_decomposes_ui_module_from_web_mobile_constraints(); // 2 + test_decomposes_search_reporting_modules(); // 3 + test_adds_integration_module_for_external_systems(); // 4 + test_cross_cutting_modules_are_present(); // 5 + test_security_nonfunctional_adds_security_module(); // 6 + test_layered_strategy_generates_expected_dependencies(); // 7 + test_microservice_strategy_changes_edge_shape(); // 8 + test_monolith_strategy_uses_core_hub(); // 9 + test_complexity_scores_are_clamped(); // 10 + test_dependency_count_helper_reports_nonzero(); // 11 + test_fallback_core_module_when_sparse_requirements(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 3b0ddec..29fb191 100644 --- a/progress.md +++ b/progress.md @@ -6469,3 +6469,39 @@ natural-language problem descriptions with per-item confidence scores. **Architecture gate check:** - `editor/src/ArchitectProblemParser.h` within header-size limit (`114` <= `600`) - `editor/tests/step471_test.cpp` within test-file size guidance (`157` lines) + +### Step 472: Module Decomposition Engine +**Status:** PASS (12/12 tests) + +Implements strategy-aware module decomposition from structured requirements, +with module graph nodes/edges, cross-cutting concerns, and complexity scoring. + +**Files added:** +- `editor/src/ArchitectModuleDecomposer.h` — module decomposition engine: + - decomposition strategies: + - `Layered` + - `Monolith` + - `Microservice` + - module graph model (`ModuleNode`, `ModuleEdge`, `ModuleGraph`) + - inferred modules from requirements (auth/api/ui/data/search/reporting/notifications/integrations) + - cross-cutting module insertion (logging/config/error/security/observability) + - strategy-specific dependency edge shaping + - normalized complexity scoring with dependency influence + - helper APIs (`hasModule`, dependency counting) +- `editor/tests/step472_test.cpp` — 12 tests covering: + - module inference from requirement categories + - strategy-specific edge behavior + - cross-cutting concern insertion + - complexity clamping + - monolith core-hub behavior + - sparse-input fallback behavior +- `editor/CMakeLists.txt` — `step472_test` target + +**Verification run:** +- `cmake --build editor/build-native --target step472_test` — PASS +- `./editor/build-native/step472_test` — PASS (12/12) +- `./editor/build-native/step471_test` — PASS (12/12) regression coverage + +**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)