diff --git a/PROGRESS.md b/PROGRESS.md index 90d4290..c7c5360 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -382,7 +382,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi ## What's Next -Sprint 5 in progress. Step 135 (library-aware completion) done. Next: Step 136 (agent library-aware mode). +Sprint 5 in progress. Step 136 (agent library-aware mode) done. Next: Step 137 (function composition builder). --- @@ -481,5 +481,6 @@ Sprint 5 in progress. Step 135 (library-aware completion) done. Next: Step 136 ( | 2026-02-09 | Codex | Step 133: Import statement generation + unused import warnings across Python/JS/Rust/Go/Elisp with auto-insert on library symbol use. 7/7 tests pass. | | 2026-02-09 | Codex | Step 134: PrimitivesRegistry aggregating builtins, imports, and in-scope symbols. 6/6 tests pass. | | 2026-02-09 | Codex | Step 135: Library-aware completion ordering with auto-import hints for non-primitive symbols. 5/5 tests pass. | +| 2026-02-09 | Codex | Step 136: Agent preferImports/strictMode policy checks on mutations with warnings or blocking. 4/4 tests pass. | | 2026-02-09 | Codex | Added FEATURE_REQUESTS.md to track security vulnerability awareness and semantic library annotations for future sprints. | | 2026-02-09 | Codex | Added LLM tooling + MCP bridge feature request to FEATURE_REQUESTS.md. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 172345e..cf4c2b8 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -758,6 +758,10 @@ add_executable(step135_test tests/step135_test.cpp) target_include_directories(step135_test PRIVATE src) target_link_libraries(step135_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step136_test tests/step136_test.cpp) +target_include_directories(step136_test PRIVATE src) +target_link_libraries(step136_test PRIVATE nlohmann_json::nlohmann_json) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/AgentLibraryPolicy.h b/editor/src/AgentLibraryPolicy.h new file mode 100644 index 0000000..11572b2 --- /dev/null +++ b/editor/src/AgentLibraryPolicy.h @@ -0,0 +1,104 @@ +#pragma once +#include "ast/Module.h" +#include "ast/Function.h" +#include "ast/ExternalModule.h" +#include "ast/TypeSignature.h" +#include "ast/Expression.h" +#include +#include +#include +#include + +struct LibraryPolicyResult { + bool ok = true; + std::string warning; + std::string error; + std::vector unknownFunctions; +}; + +static inline void collectFunctionCalls(ASTNode* node, std::vector& out) { + if (!node) return; + if (node->conceptType == "FunctionCall") { + auto* call = static_cast(node); + if (!call->functionName.empty()) out.push_back(call->functionName); + } + for (auto* child : node->allChildren()) { + collectFunctionCalls(child, out); + } +} + +static inline bool matchesAllowed(const std::unordered_set& allowed, + const std::string& name) { + if (allowed.count(name) > 0) return true; + for (const auto& item : allowed) { + if (item.size() > name.size()) { + if (item.rfind("." + name) == item.size() - name.size() - 1) return true; + if (item.rfind("::" + name) == item.size() - name.size() - 2) return true; + } + } + return false; +} + +static inline std::vector collectAllowedFunctions(Module* ast) { + std::vector out; + if (!ast) return out; + for (auto* fnNode : ast->getChildren("functions")) { + if (fnNode->conceptType != "Function") continue; + auto* fn = static_cast(fnNode); + if (!fn->name.empty()) out.push_back(fn->name); + } + for (auto* extNode : ast->getChildren("externalModules")) { + if (extNode->conceptType != "ExternalModule") continue; + auto* ext = static_cast(extNode); + for (auto* sigNode : ext->getChildren("signatures")) { + if (sigNode->conceptType != "TypeSignature") continue; + auto* sig = static_cast(sigNode); + if (!sig->name.empty()) out.push_back(sig->name); + } + } + return out; +} + +static inline LibraryPolicyResult checkMutationLibraryPolicy(ASTNode* node, + Module* ast, + bool preferImports, + bool strictMode) { + LibraryPolicyResult res; + if (!preferImports) return res; + auto allowedList = collectAllowedFunctions(ast); + std::unordered_set allowed(allowedList.begin(), allowedList.end()); + + std::vector calls; + collectFunctionCalls(node, calls); + std::vector unknown; + for (const auto& name : calls) { + if (!matchesAllowed(allowed, name)) { + if (std::find(unknown.begin(), unknown.end(), name) == unknown.end()) { + unknown.push_back(name); + } + } + } + + if (!unknown.empty()) { + res.unknownFunctions = unknown; + std::string alt; + for (size_t i = 0; i < std::min(allowedList.size(), 5); ++i) { + if (!alt.empty()) alt += ", "; + alt += allowedList[i]; + } + std::string message = "References functions outside imports: "; + for (size_t i = 0; i < unknown.size(); ++i) { + if (i > 0) message += ", "; + message += unknown[i]; + } + if (!alt.empty()) message += " | available: " + alt; + if (strictMode) { + res.ok = false; + res.error = message; + } else { + res.warning = message; + } + } + + return res; +} diff --git a/editor/src/EditorState.h b/editor/src/EditorState.h index 4412f04..e8c70c8 100644 --- a/editor/src/EditorState.h +++ b/editor/src/EditorState.h @@ -48,6 +48,7 @@ #include "LibraryBrowserPanel.h" #include "ImportManager.h" #include "PrimitivesRegistry.h" +#include "AgentLibraryPolicy.h" #include "IncrementalOptimizer.h" #include "ast/Serialization.h" #include "ast/Generator.h" @@ -989,6 +990,8 @@ struct EditorState { } auto params = request.contains("params") ? request["params"] : json::object(); std::string type = params.value("type", ""); + bool preferImports = params.value("preferImports", false); + bool strictMode = params.value("strictMode", false); Module* ast = mutationAST(); if (!ast) { response["error"] = {{"code", -32001}, {"message", "AST unavailable"}}; @@ -997,6 +1000,7 @@ struct EditorState { ASTMutationAPI mut; mut.setRoot(ast); ASTMutationAPI::MutationResult res; + LibraryPolicyResult policy; if (type == "setProperty") { res = mut.setProperty(params.value("nodeId", ""), @@ -1017,6 +1021,14 @@ struct EditorState { if (params.contains("node")) { node = fromJson(params["node"]); } + if (node) { + policy = checkMutationLibraryPolicy(node, ast, preferImports, strictMode); + if (!policy.ok) { + response["error"] = {{"code", -32011}, {"message", policy.error}}; + deleteTree(node); + return response; + } + } res = mut.insertNode(params.value("parentId", ""), params.value("role", ""), node); if (!res.success && node) { @@ -1035,7 +1047,9 @@ struct EditorState { applyOrchestratorToActive(); response["result"] = { {"success", true}, - {"warning", res.warning} + {"warning", res.warning}, + {"libraryWarning", policy.warning}, + {"unknownFunctions", policy.unknownFunctions} }; return response; } diff --git a/editor/tests/step136_test.cpp b/editor/tests/step136_test.cpp new file mode 100644 index 0000000..2004298 --- /dev/null +++ b/editor/tests/step136_test.cpp @@ -0,0 +1,49 @@ +// Step 136 TDD Test: Agent library-aware mode policy +#include "AgentLibraryPolicy.h" +#include "ast/Function.h" +#include "ast/ExternalModule.h" +#include "ast/TypeSignature.h" +#include "ast/Expression.h" +#include + +static void expect(bool cond, const std::string& name, int& passed, int& failed) { + if (cond) { + std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n"; + ++passed; + } else { + std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n"; + ++failed; + } +} + +int main() { + int passed = 0; + int failed = 0; + + Module mod("m1", "Module", "python"); + auto* fn = new Function("fn1", "user_fn"); + mod.addChild("functions", fn); + auto* ext = new ExternalModule("ext_numpy", "numpy", "python", "1.0"); + ext->addChild("signatures", new TypeSignature("sig_1", "numpy.array")); + mod.addChild("externalModules", ext); + + auto* callKnown = new FunctionCall(); + callKnown->functionName = "numpy.array"; + auto resKnown = checkMutationLibraryPolicy(callKnown, &mod, true, false); + expect(resKnown.warning.empty(), "known function no warning", passed, failed); + + auto* callUnknown = new FunctionCall(); + callUnknown->functionName = "mystery"; + auto resWarn = checkMutationLibraryPolicy(callUnknown, &mod, true, false); + expect(!resWarn.warning.empty(), "unknown function warning", passed, failed); + expect(!resWarn.unknownFunctions.empty(), "unknown list set", passed, failed); + + auto resStrict = checkMutationLibraryPolicy(callUnknown, &mod, true, true); + expect(!resStrict.ok, "strict mode blocks unknown", passed, failed); + + delete callKnown; + delete callUnknown; + + std::cout << "\n=== Step 136 Results: " << passed << " passed, " << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/sprint5_plan.md b/sprint5_plan.md index 5815347..8faa919 100644 --- a/sprint5_plan.md +++ b/sprint5_plan.md @@ -120,7 +120,7 @@ what's already available. parameter types from `TypeSignature`. *Modifies:* `CodeEditorWidget.h`, `LSPClient.h` -- [ ] **Step 136: Agent library-aware mode** +- [x] **Step 136: Agent library-aware mode** New agent capability flag: `preferImports`. When set, the agent (via WebSocket API) **prefers** inserting code that references symbols in `PrimitivesRegistry`, and warns (not rejects) when using symbols outside