Complete step472 module decomposition engine with tests
This commit is contained in:
@@ -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)
|
||||
|
||||
212
editor/src/ArchitectModuleDecomposer.h
Normal file
212
editor/src/ArchitectModuleDecomposer.h
Normal file
@@ -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 <algorithm>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
enum class DecompositionStrategy {
|
||||
Layered,
|
||||
Monolith,
|
||||
Microservice
|
||||
};
|
||||
|
||||
struct ModuleNode {
|
||||
std::string name;
|
||||
std::vector<std::string> 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<ModuleNode> nodes;
|
||||
std::vector<ModuleEdge> 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<std::string>& 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<ExtractedItem>& 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<std::string>& 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<std::string> 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<std::string, int> 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<char>(std::tolower(c)); });
|
||||
return s;
|
||||
}
|
||||
};
|
||||
156
editor/tests/step472_test.cpp
Normal file
156
editor/tests/step472_test.cpp
Normal file
@@ -0,0 +1,156 @@
|
||||
// Step 472: Module Decomposition Engine Tests (12 tests)
|
||||
|
||||
#include "ArchitectModuleDecomposer.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
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;
|
||||
}
|
||||
36
progress.md
36
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)
|
||||
|
||||
Reference in New Issue
Block a user