diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index f99fe4c..bc21f4c 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1597,4 +1597,58 @@ target_link_libraries(step265_test PRIVATE tree_sitter_java tree_sitter_rust tree_sitter_go tree_sitter_org) +add_executable(step266_test tests/step266_test.cpp) +target_include_directories(step266_test PRIVATE src) +target_link_libraries(step266_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 + tree_sitter_org) + +# Step 267: Sidecar AST persistence +add_executable(step267_test tests/step267_test.cpp) +target_include_directories(step267_test PRIVATE src) +target_link_libraries(step267_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 + tree_sitter_org) + +# Step 270: Annotation prompt templates +add_executable(step270_test tests/step270_test.cpp) +target_include_directories(step270_test PRIVATE src) +target_link_libraries(step270_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 + tree_sitter_org) + +# Step 269: Annotation RPC methods +add_executable(step269_test tests/step269_test.cpp) +target_include_directories(step269_test PRIVATE src) +target_link_libraries(step269_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 + tree_sitter_org) + +# Step 268: Phase 10a integration tests +add_executable(step268_test tests/step268_test.cpp) +target_include_directories(step268_test PRIVATE src) +target_link_libraries(step268_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 + tree_sitter_org) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/AgentPermissionPolicy.h b/editor/src/AgentPermissionPolicy.h index 6a66fe1..49e577f 100644 --- a/editor/src/AgentPermissionPolicy.h +++ b/editor/src/AgentPermissionPolicy.h @@ -56,7 +56,11 @@ struct AgentPermissionPolicy { method == "indexWorkspace" || method == "getImportGraph" || method == "getProjectDiagnostics" || - method == "searchProject") { + method == "searchProject" || + method == "loadAnnotatedAST" || + method == "listAnnotatedFiles" || + method == "getSemanticAnnotations" || + method == "getUnannotatedNodes") { return true; } @@ -74,7 +78,10 @@ struct AgentPermissionPolicy { method == "saveBuffer" || method == "saveAllBuffers" || method == "undo" || - method == "redo") { + method == "redo" || + method == "saveAnnotatedAST" || + method == "setSemanticAnnotation" || + method == "removeSemanticAnnotation") { return role == AgentRole::Refactor || role == AgentRole::Generator; } diff --git a/editor/src/AgentRPCHandler.h b/editor/src/AgentRPCHandler.h index ab47909..d645b6b 100644 --- a/editor/src/AgentRPCHandler.h +++ b/editor/src/AgentRPCHandler.h @@ -409,7 +409,7 @@ inline json handleAgentRequest(EditorState& state, const json& request, {"nodeId", d.nodeId}}); json violArr = json::array(); for (const auto& v : pr.violations) - violArr.push_back({{"type", v.type}, {"message", v.message}, + violArr.push_back({{"severity", v.severity}, {"category", v.category}, {"message", v.message}, {"nodeId", v.nodeId}}); json suggArr = json::array(); for (const auto& s : pr.suggestions) @@ -420,8 +420,8 @@ inline json handleAgentRequest(EditorState& state, const json& request, {"success", pr.success}, {"generatedCode", pr.generatedCode}, {"parseDiagnostics", diagArr}, {"validationDiagnostics", valDiagArr}, {"violations", violArr}, {"suggestions", suggArr}, - {"foldCount", pr.foldResult.transformCount}, - {"dceCount", pr.dceResult.transformCount} + {"foldCount", pr.foldResult.nodesModified}, + {"dceCount", pr.dceResult.nodesModified} }; if (pr.ast) result["ast"] = toJson(pr.ast.get()); return agentRpcResult(id, result); diff --git a/editor/src/BufferOps.h b/editor/src/BufferOps.h index d254326..2f6a43d 100644 --- a/editor/src/BufferOps.h +++ b/editor/src/BufferOps.h @@ -20,11 +20,11 @@ inline BufferManager::BufferMode EditorState::defaultBufferMode() const { inline void EditorState::createBuffer(const std::string& path, const std::string& content, const std::string& language, - BufferManager::BufferMode mode = BufferManager::BufferMode::Text, - size_t fileSizeBytes = 0, - bool largeFileMode = false, - bool disableSyntaxHighlight = false, - bool deferAstSync = false) { + BufferManager::BufferMode mode, + size_t fileSizeBytes, + bool largeFileMode, + bool disableSyntaxHighlight, + bool deferAstSync) { BufferManager::BufferMode effectiveMode = mode; if (language == "org") effectiveMode = BufferManager::BufferMode::Text; if (buffers.hasBuffer(path)) { @@ -346,8 +346,8 @@ inline void EditorState::doSave() { } inline void EditorState::doOpen(const std::string& path, - BufferManager::BufferMode modeOverride = BufferManager::BufferMode::Text, - bool deferAstSync = false) { + BufferManager::BufferMode modeOverride, + bool deferAstSync) { std::error_code sizeErr; size_t fileSizeBytes = 0; if (!path.empty()) { @@ -544,4 +544,3 @@ inline void EditorState::insertTextAtCursor(const std::string& text) { // --- Projection and lifecycle helpers (extracted) --- #include "BufferOpsProjection.h" #include "BufferOpsLifecycle.h" - diff --git a/editor/src/BufferOpsLifecycle.h b/editor/src/BufferOpsLifecycle.h index f7d1117..1d0eb6d 100644 --- a/editor/src/BufferOpsLifecycle.h +++ b/editor/src/BufferOpsLifecycle.h @@ -33,7 +33,7 @@ inline json EditorState::buildSessionMetadata(bool autoRecording) const { json meta; meta["editorVersion"] = welcome.getVersion(); meta["os"] = osLabel(); - meta["language"] = active() ? active()->language : std::string("unknown"); + meta["language"] = activeBuffer ? activeBuffer->language : std::string("unknown"); meta["projectType"] = detectProjectType(); meta["autoRecording"] = autoRecording; return meta; diff --git a/editor/src/BufferOpsProjection.h b/editor/src/BufferOpsProjection.h index 922ebe8..66bcc48 100644 --- a/editor/src/BufferOpsProjection.h +++ b/editor/src/BufferOpsProjection.h @@ -74,7 +74,7 @@ inline void EditorState::openDiff(const std::string& beforeText, bool preview, int action, const std::vector& transformIds, - const std::vector& batchMutations = {}) { + const std::vector& batchMutations) { diff.active = true; diff.preview = preview; diff.action = action; diff --git a/editor/src/CodeEditorRenderHelpers.h b/editor/src/CodeEditorRenderHelpers.h index d8e1308..6c13a73 100644 --- a/editor/src/CodeEditorRenderHelpers.h +++ b/editor/src/CodeEditorRenderHelpers.h @@ -29,6 +29,10 @@ return line; } + static bool isBracketChar(char c) { + return c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}'; + } + static int lineFromMouseY(float mouseY, float baseY, float lineHeight, int lineCount) { int line = (int)((mouseY - baseY) / lineHeight); line = std::max(0, std::min(line, lineCount - 1)); @@ -311,4 +315,3 @@ collectFoldNodes(ts_node_child(node, i), language, out); } } - diff --git a/editor/src/CodeEditorRenderVisible.h b/editor/src/CodeEditorRenderVisible.h index 1069ecd..2381860 100644 --- a/editor/src/CodeEditorRenderVisible.h +++ b/editor/src/CodeEditorRenderVisible.h @@ -144,8 +144,7 @@ } if (hoverSuggestion) { std::string tip = suggestion.label + " (" + - std::to_string(suggestion.confidence) + ") -" + + std::to_string(suggestion.confidence) + ")\n" + suggestion.reason; renderRichTooltip("suggestion_marker_" + std::to_string(ln), tip, @@ -314,8 +313,7 @@ else if (c == ' ') c = '>'; } } - if (!chunk.empty() && chunk[chunk.size() - 1] == ' -') { + if (!chunk.empty() && chunk[chunk.size() - 1] == '\n') { chunk.pop_back(); } diff --git a/editor/src/CodeEditorRendering.h b/editor/src/CodeEditorRendering.h index 61e91aa..687a366 100644 --- a/editor/src/CodeEditorRendering.h +++ b/editor/src/CodeEditorRendering.h @@ -40,8 +40,9 @@ public: } default: { // star (outline) ImVec2 pts[10]; + constexpr float kPi = 3.14159265358979323846f; for (int i = 0; i < 10; ++i) { - float angle = (float)(IM_PI * 0.5 + i * (IM_PI / 5.0)); + float angle = (float)(kPi * 0.5f + i * (kPi / 5.0f)); float radius = (i % 2 == 0) ? size : size * 0.5f; pts[i] = ImVec2(center.x + std::cos(angle) * radius, center.y - std::sin(angle) * radius); diff --git a/editor/src/CommandPalette.h b/editor/src/CommandPalette.h index 1c43e6e..2810eee 100644 --- a/editor/src/CommandPalette.h +++ b/editor/src/CommandPalette.h @@ -35,10 +35,10 @@ public: void registerCommand(const std::string& id, const std::string& label, const std::string& shortcut, - const std::string& category, - int contextMask, - const std::string& inputHint, - bool inlineInput) { + const std::string& category = "", + int contextMask = CommandContext_Any, + const std::string& inputHint = "", + bool inlineInput = false) { auto& entry = commands_[id]; entry.id = id; entry.label = label; @@ -56,8 +56,8 @@ public: } std::vector search(const std::string& query, - int contextMask, - bool strictContext) const { + int contextMask = CommandContext_Any, + bool strictContext = false) const { std::vector results; for (const auto& [_, cmd] : commands_) { const bool contextOk = matchesContext(cmd, contextMask); diff --git a/editor/src/CompactAST.h b/editor/src/CompactAST.h index f16e401..672e50d 100644 --- a/editor/src/CompactAST.h +++ b/editor/src/CompactAST.h @@ -6,6 +6,7 @@ #include "ast/ASTNode.h" #include "ast/Serialization.h" +#include "ast/Annotation.h" #include #include #include @@ -13,6 +14,54 @@ using json = nlohmann::json; +// --- Extract semantic annotation summary from a node's annotations --- +// Returns a compact JSON object with semantic fields, or null if none exist. +inline json extractSemanticSummary(const ASTNode* node) { + if (!node) return json(); + auto annos = node->getChildren("annotations"); + if (annos.empty()) return json(); + json sem; + for (const auto* a : annos) { + if (a->conceptType == "IntentAnnotation") { + auto* ia = static_cast(a); + json obj; + if (!ia->summary.empty()) obj["summary"] = ia->summary; + if (!ia->category.empty()) obj["category"] = ia->category; + if (!obj.empty()) sem["intent"] = obj; + } + else if (a->conceptType == "ComplexityAnnotation") { + auto* ca = static_cast(a); + json obj; + if (!ca->timeComplexity.empty()) obj["time"] = ca->timeComplexity; + if (ca->cognitiveComplexity > 0) obj["cognitive"] = ca->cognitiveComplexity; + if (ca->linesOfLogic > 0) obj["lines"] = ca->linesOfLogic; + if (!obj.empty()) sem["complexity"] = obj; + } + else if (a->conceptType == "RiskAnnotation") { + auto* ra = static_cast(a); + json obj; + if (!ra->level.empty()) obj["level"] = ra->level; + if (!ra->reason.empty()) obj["reason"] = ra->reason; + if (ra->dependentCount > 0) obj["dependents"] = ra->dependentCount; + if (!obj.empty()) sem["risk"] = obj; + } + else if (a->conceptType == "ContractAnnotation") { + auto* ca = static_cast(a); + json obj; + if (!ca->preconditions.empty()) obj["pre"] = ca->preconditions; + if (!ca->postconditions.empty()) obj["post"] = ca->postconditions; + if (!ca->returnShape.empty()) obj["returns"] = ca->returnShape; + if (!ca->sideEffects.empty()) obj["sideEffects"] = ca->sideEffects; + if (!obj.empty()) sem["contract"] = obj; + } + else if (a->conceptType == "SemanticTagAnnotation") { + auto* ta = static_cast(a); + if (!ta->tags.empty()) sem["tags"] = ta->tags; + } + } + return sem.empty() ? json() : sem; +} + // --- Extract a human-readable name from any AST node --- inline std::string getNodeName(const ASTNode* node) { if (!node) return ""; @@ -70,6 +119,10 @@ inline json toJsonCompact(const ASTNode* node) { if (!name.empty()) j["name"] = name; if (node->hasSpan()) j["line"] = node->spanStartLine; + // Include semantic summary if annotations exist + json sem = extractSemanticSummary(node); + if (!sem.empty()) j["semantic"] = sem; + auto kids = node->allChildren(); if (!kids.empty()) { j["childCount"] = (int)kids.size(); @@ -105,6 +158,8 @@ inline json toJsonCompactSummary(const ASTNode* root) { std::string cname = getNodeName(child); if (!cname.empty()) cj["name"] = cname; if (child->hasSpan()) cj["line"] = child->spanStartLine; + json csem = extractSemanticSummary(child); + if (!csem.empty()) cj["semantic"] = csem; auto grandkids = child->allChildren(); if (!grandkids.empty()) cj["childCount"] = (int)grandkids.size(); diff --git a/editor/src/EditOps.h b/editor/src/EditOps.h index 879e84d..d83b25b 100644 --- a/editor/src/EditOps.h +++ b/editor/src/EditOps.h @@ -313,7 +313,7 @@ inline bool EditorState::moveToMatchIndex(int index) { return true; } -inline void EditorState::doFindNext(bool backwards = false) { +inline void EditorState::doFindNext(bool backwards) { if (!active()) return; if (strlen(search.findBuf) == 0) return; refreshSearchMatches(); @@ -505,4 +505,3 @@ inline std::string EditorState::uiEventLabel(UIEventType type) { } return "unknown"; } - diff --git a/editor/src/EditOpsCommands.h b/editor/src/EditOpsCommands.h index 4e5ba41..fa29b30 100644 --- a/editor/src/EditOpsCommands.h +++ b/editor/src/EditOpsCommands.h @@ -118,7 +118,9 @@ inline void EditorState::registerCommands() { int col = 0; if (parseLineColInput(arg, line, col)) { if (active()) { - int totalLines = countLines(active()->editBuf); + int totalLines = 1 + (int)std::count(active()->editBuf.begin(), + active()->editBuf.end(), + '\n'); if (totalLines > 0) line = std::max(1, std::min(line, totalLines)); col = std::max(1, col); jumpTo(active(), line - 1, col - 1); @@ -207,7 +209,7 @@ inline void EditorState::registerCommands() { [this]() { runActiveFile(true); }); } -inline void EditorState::executeCommand(const std::string& id, const std::string& arg = {}) { +inline void EditorState::executeCommand(const std::string& id, const std::string& arg) { auto it = commandHandlers.find(id); if (it == commandHandlers.end()) return; it->second(arg); diff --git a/editor/src/EmacsIntegration.h b/editor/src/EmacsIntegration.h index f96002b..6fe7783 100644 --- a/editor/src/EmacsIntegration.h +++ b/editor/src/EmacsIntegration.h @@ -13,6 +13,8 @@ #include #include #include +#include +#include // --------------------------------------------------------------------------- // ElispCommandBuilder — produces valid Elisp command strings @@ -146,6 +148,7 @@ public: if (!initFilePath.empty()) { cmd += " --load \"" + initFilePath + "\""; } +#ifdef _WIN32 cmd += " 2>&1"; FILE* pipe = openPipe(cmd.c_str(), "r"); if (!pipe) { @@ -157,16 +160,31 @@ public: outLog += buf; } int result = closePipe(pipe); - if (result == 0) { - daemonRunning_ = true; - return true; - } - if (isDaemonAlive()) { + if (result == 0 || isDaemonAlive()) { daemonRunning_ = true; return true; } lastError_ = outLog.empty() ? "Failed to start Emacs daemon" : outLog; return false; +#else + // Launch daemon in the background to avoid blocking editor startup. + cmd += " >/tmp/whetstone-emacs-daemon.log 2>&1 &"; + int result = runCommand(cmd); + if (result != 0) { + lastError_ = "Failed to start Emacs daemon"; + return false; + } + outLog = "Starting Emacs daemon in background."; + for (int i = 0; i < 15; ++i) { + if (isDaemonAlive()) { + daemonRunning_ = true; + return true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + lastError_ = "Emacs daemon did not become ready in time"; + return false; +#endif } // Check if daemon is running @@ -426,4 +444,3 @@ private: std::string lastInitPath_; mutable std::string lastRunCommand_; }; - diff --git a/editor/src/HeadlessAgentRPCHandler.h b/editor/src/HeadlessAgentRPCHandler.h index dcc6636..40505a2 100644 --- a/editor/src/HeadlessAgentRPCHandler.h +++ b/editor/src/HeadlessAgentRPCHandler.h @@ -1573,5 +1573,322 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state, {{"results", results}, {"count", (int)results.size()}}); } + // --- saveAnnotatedAST --- + if (method == "saveAnnotatedAST") { + if (!AgentPermissionPolicy::canInvoke(role, method)) + return headlessRpcError(id, -32031, "Role not permitted"); + auto params = request.contains("params") ? request["params"] + : json::object(); + std::string path = params.value("path", ""); + if (path.empty() && state.activeBuffer) + path = state.activeBuffer->path; + auto it = state.bufferStates.find(path); + if (it == state.bufferStates.end()) + return headlessRpcError(id, -32602, "No buffer: " + path); + Module* ast = it->second->sync.getAST(); + auto res = saveSidecarAST(state.workspaceRoot, path, ast); + if (!res.success) + return headlessRpcError(id, -32010, res.error); + return headlessRpcResult(id, + {{"success", true}, + {"sidecarPath", res.sidecarPath}, + {"annotationCount", res.annotationCount}}); + } + + // --- loadAnnotatedAST --- + if (method == "loadAnnotatedAST") { + if (!AgentPermissionPolicy::canInvoke(role, method)) + return headlessRpcError(id, -32031, "Role not permitted"); + auto params = request.contains("params") ? request["params"] + : json::object(); + std::string path = params.value("path", ""); + if (path.empty() && state.activeBuffer) + path = state.activeBuffer->path; + auto it = state.bufferStates.find(path); + if (it == state.bufferStates.end()) + return headlessRpcError(id, -32602, "No buffer: " + path); + Module* ast = it->second->sync.getAST(); + auto res = loadSidecarAST(state.workspaceRoot, path, ast); + if (!res.success) + return headlessRpcError(id, -32010, res.error); + return headlessRpcResult(id, + {{"success", true}, + {"mergedCount", res.mergedCount}, + {"staleCount", res.staleCount}}); + } + + // --- listAnnotatedFiles --- + if (method == "listAnnotatedFiles") { + if (!AgentPermissionPolicy::canInvoke(role, method)) + return headlessRpcError(id, -32031, "Role not permitted"); + auto files = listSidecarFiles(state.workspaceRoot); + return headlessRpcResult(id, + {{"files", files}, {"count", (int)files.size()}}); + } + + // --- setSemanticAnnotation --- + if (method == "setSemanticAnnotation") { + if (!AgentPermissionPolicy::canInvoke(role, method)) + return headlessRpcError(id, -32031, "Role not permitted"); + auto params = request.contains("params") ? request["params"] + : json::object(); + std::string nodeId = params.value("nodeId", ""); + std::string type = params.value("type", ""); + json fields = params.contains("fields") ? params["fields"] + : json::object(); + if (nodeId.empty() || type.empty()) + return headlessRpcError(id, -32602, "nodeId and type required"); + if (!state.activeBuffer) + return headlessRpcError(id, -32602, "No active buffer"); + Module* ast = state.activeAST(); + if (!ast) + return headlessRpcError(id, -32602, "No AST available"); + ASTNode* target = findNodeById(ast, nodeId); + if (!target) + return headlessRpcError(id, -32602, "Node not found: " + nodeId); + + // Map type string to conceptType + std::string conceptType; + if (type == "intent") conceptType = "IntentAnnotation"; + else if (type == "complexity") conceptType = "ComplexityAnnotation"; + else if (type == "risk") conceptType = "RiskAnnotation"; + else if (type == "contract") conceptType = "ContractAnnotation"; + else if (type == "tags") conceptType = "SemanticTagAnnotation"; + else return headlessRpcError(id, -32602, "Unknown annotation type: " + type); + + // Remove existing annotation of same type (update semantics) + auto annos = target->getChildren("annotations"); + for (auto* a : annos) { + if (a->conceptType == conceptType) { + target->removeChild(a); + break; + } + } + + // Create new annotation node + json nodeJson = {{"concept", conceptType}, {"properties", fields}}; + ASTNode* newAnno = fromJson(nodeJson); + if (!newAnno) + return headlessRpcError(id, -32010, "Failed to create annotation"); + target->addChild("annotations", newAnno); + + return headlessRpcResult(id, {{"success", true}, {"type", type}}); + } + + // --- getSemanticAnnotations --- + if (method == "getSemanticAnnotations") { + if (!AgentPermissionPolicy::canInvoke(role, method)) + return headlessRpcError(id, -32031, "Role not permitted"); + auto params = request.contains("params") ? request["params"] + : json::object(); + std::string nodeId = params.value("nodeId", ""); + if (!state.activeBuffer) + return headlessRpcError(id, -32602, "No active buffer"); + Module* ast = state.activeAST(); + if (!ast) + return headlessRpcError(id, -32602, "No AST available"); + + if (!nodeId.empty()) { + // Return annotations for a specific node + ASTNode* target = findNodeById(ast, nodeId); + if (!target) + return headlessRpcError(id, -32602, "Node not found: " + nodeId); + json annos = json::array(); + for (auto* a : target->getChildren("annotations")) { + if (!isSemanticAnnotation(a->conceptType)) continue; + json entry; + if (a->conceptType == "IntentAnnotation") { + auto* ia = static_cast(a); + entry = {{"type", "intent"}, {"summary", ia->summary}, + {"category", ia->category}}; + } else if (a->conceptType == "ComplexityAnnotation") { + auto* ca = static_cast(a); + entry = {{"type", "complexity"}, + {"timeComplexity", ca->timeComplexity}, + {"cognitiveComplexity", ca->cognitiveComplexity}, + {"linesOfLogic", ca->linesOfLogic}}; + } else if (a->conceptType == "RiskAnnotation") { + auto* ra = static_cast(a); + entry = {{"type", "risk"}, {"level", ra->level}, + {"reason", ra->reason}, + {"dependentCount", ra->dependentCount}}; + } else if (a->conceptType == "ContractAnnotation") { + auto* ca = static_cast(a); + entry = {{"type", "contract"}, + {"preconditions", ca->preconditions}, + {"postconditions", ca->postconditions}, + {"returnShape", ca->returnShape}, + {"sideEffects", ca->sideEffects}}; + } else if (a->conceptType == "SemanticTagAnnotation") { + auto* ta = static_cast(a); + entry = {{"type", "tags"}, {"tags", ta->tags}}; + } + if (!entry.empty()) annos.push_back(entry); + } + return headlessRpcResult(id, + {{"nodeId", nodeId}, {"annotations", annos}}); + } else { + // Return all annotated nodes + json nodes = json::array(); + for (auto* child : ast->allChildren()) { + auto annos = child->getChildren("annotations"); + json annoList = json::array(); + for (auto* a : annos) { + if (!isSemanticAnnotation(a->conceptType)) continue; + json entry; + if (a->conceptType == "IntentAnnotation") { + auto* ia = static_cast(a); + entry = {{"type", "intent"}, {"summary", ia->summary}}; + } else if (a->conceptType == "ComplexityAnnotation") { + entry = {{"type", "complexity"}}; + } else if (a->conceptType == "RiskAnnotation") { + auto* ra = static_cast(a); + entry = {{"type", "risk"}, {"level", ra->level}}; + } else if (a->conceptType == "ContractAnnotation") { + entry = {{"type", "contract"}}; + } else if (a->conceptType == "SemanticTagAnnotation") { + auto* ta = static_cast(a); + entry = {{"type", "tags"}, {"tags", ta->tags}}; + } + if (!entry.empty()) annoList.push_back(entry); + } + if (!annoList.empty()) { + nodes.push_back({ + {"nodeId", child->id}, + {"name", getNodeName(child)}, + {"kind", child->conceptType}, + {"annotations", annoList} + }); + } + } + return headlessRpcResult(id, {{"nodes", nodes}}); + } + } + + // --- removeSemanticAnnotation --- + if (method == "removeSemanticAnnotation") { + if (!AgentPermissionPolicy::canInvoke(role, method)) + return headlessRpcError(id, -32031, "Role not permitted"); + auto params = request.contains("params") ? request["params"] + : json::object(); + std::string nodeId = params.value("nodeId", ""); + std::string type = params.value("type", ""); + if (nodeId.empty() || type.empty()) + return headlessRpcError(id, -32602, "nodeId and type required"); + if (!state.activeBuffer) + return headlessRpcError(id, -32602, "No active buffer"); + Module* ast = state.activeAST(); + if (!ast) + return headlessRpcError(id, -32602, "No AST available"); + ASTNode* target = findNodeById(ast, nodeId); + if (!target) + return headlessRpcError(id, -32602, "Node not found: " + nodeId); + + std::string conceptType; + if (type == "intent") conceptType = "IntentAnnotation"; + else if (type == "complexity") conceptType = "ComplexityAnnotation"; + else if (type == "risk") conceptType = "RiskAnnotation"; + else if (type == "contract") conceptType = "ContractAnnotation"; + else if (type == "tags") conceptType = "SemanticTagAnnotation"; + else return headlessRpcError(id, -32602, "Unknown type: " + type); + + bool removed = false; + for (auto* a : target->getChildren("annotations")) { + if (a->conceptType == conceptType) { + target->removeChild(a); + removed = true; + break; + } + } + + return headlessRpcResult(id, {{"removed", removed}, {"type", type}}); + } + + // --- getUnannotatedNodes --- + if (method == "getUnannotatedNodes") { + if (!AgentPermissionPolicy::canInvoke(role, method)) + return headlessRpcError(id, -32031, "Role not permitted"); + auto params = request.contains("params") ? request["params"] + : json::object(); + std::string filterType = params.value("type", ""); + bool includeHints = params.value("includeHints", false); + if (!state.activeBuffer) + return headlessRpcError(id, -32602, "No active buffer"); + Module* ast = state.activeAST(); + if (!ast) + return headlessRpcError(id, -32602, "No AST available"); + + // Map filter type to conceptType + std::string filterConcept; + if (filterType == "intent") filterConcept = "IntentAnnotation"; + else if (filterType == "complexity") filterConcept = "ComplexityAnnotation"; + else if (filterType == "risk") filterConcept = "RiskAnnotation"; + else if (filterType == "contract") filterConcept = "ContractAnnotation"; + else if (filterType == "tags") filterConcept = "SemanticTagAnnotation"; + + json nodes = json::array(); + for (auto* child : ast->allChildren()) { + // Only report Function and Variable nodes + if (child->conceptType != "Function" && + child->conceptType != "Variable") + continue; + + auto annos = child->getChildren("annotations"); + bool hasTarget = false; + if (filterConcept.empty()) { + // Check for any semantic annotation + for (auto* a : annos) { + if (isSemanticAnnotation(a->conceptType)) { + hasTarget = true; + break; + } + } + } else { + for (auto* a : annos) { + if (a->conceptType == filterConcept) { + hasTarget = true; + break; + } + } + } + + if (!hasTarget) { + json entry = { + {"nodeId", child->id}, + {"name", getNodeName(child)}, + {"kind", child->conceptType} + }; + // Add static analysis hints if requested + if (includeHints) { + json hints; + // Body statement count + auto body = child->getChildren("body"); + hints["bodyStatements"] = (int)body.size(); + // Side effect heuristic: check for io/network calls + bool hasSideEffects = false; + for (auto* stmt : body) { + if (stmt->conceptType == "FunctionCall" || + stmt->conceptType == "MethodCall") { + std::string callee = getNodeName(stmt); + if (callee.find("print") != std::string::npos || + callee.find("write") != std::string::npos || + callee.find("send") != std::string::npos || + callee.find("read") != std::string::npos || + callee.find("open") != std::string::npos) + hasSideEffects = true; + } + } + hints["sideEffectHint"] = hasSideEffects + ? "possible" : "none"; + entry["hints"] = hints; + } + nodes.push_back(entry); + } + } + + return headlessRpcResult(id, + {{"nodes", nodes}, {"count", (int)nodes.size()}}); + } + return headlessRpcError(id, -32601, "Method not found"); } diff --git a/editor/src/HeadlessEditorState.h b/editor/src/HeadlessEditorState.h index 85019e0..4aeaa19 100644 --- a/editor/src/HeadlessEditorState.h +++ b/editor/src/HeadlessEditorState.h @@ -34,6 +34,7 @@ #include "PrimitivesRegistry.h" #include "SemanticTags.h" #include "ProjectState.h" +#include "SidecarPersistence.h" #include #include diff --git a/editor/src/LspOps.h b/editor/src/LspOps.h index 6b4ff7f..28a21f2 100644 --- a/editor/src/LspOps.h +++ b/editor/src/LspOps.h @@ -3,20 +3,20 @@ // Extracted from EditorState.h (Sprint 8). // Included from EditorState.h after the EditorState struct definition. -inline void EditorState::ensureImportForSymbol(const std::string& library, const std::string& symbol) { - if (!active() || library.empty()) return; +inline void EditorState::ensureImportForSymbol(const std::string& libraryName, const std::string& symbol) { + if (!active() || libraryName.empty()) return; if (settings.getBlockVulnerableImports()) { PackageEcosystem eco = ecosystemForLanguage(active()->language); std::string osvEco = osvEcosystem(eco); if (!osvEco.empty()) { - std::string key = vulnKey(osvEco, library); + std::string key = vulnKey(osvEco, libraryName); if (library.dependencyPanel.vulnIgnore.find(key) == library.dependencyPanel.vulnIgnore.end()) { - std::string version = dependencyVersionFor(osvEco, library); - auto vulns = library.vulnDb.query(osvEco, library, version); + std::string version = dependencyVersionFor(osvEco, libraryName); + auto vulns = library.vulnDb.query(osvEco, libraryName, version); if (!vulns.empty()) { notify(NotificationLevel::Warning, - "Blocked vulnerable import: " + library); + "Blocked vulnerable import: " + libraryName); return; } } @@ -27,7 +27,7 @@ inline void EditorState::ensureImportForSymbol(const std::string& library, const if (paren != std::string::npos) clean = clean.substr(0, paren); ImportEditResult result = ensureImport(active()->editBuf, active()->language, - library, + libraryName, clean); if (result.changed) { active()->editBuf = result.text; @@ -324,9 +324,9 @@ inline PackageEcosystem EditorState::ecosystemForLanguage(const std::string& lan return PackageEcosystem::Cpp; } -inline std::vector EditorState::collectImportLocations(const std::string& text, +inline std::vector EditorState::collectImportLocations(const std::string& text, const std::string& language) { - std::vector out; + std::vector out; auto lines = importSplitLines(text); for (int i = 0; i < (int)lines.size(); ++i) { std::string t = trimStr(lines[i]); @@ -422,4 +422,3 @@ inline std::string EditorState::agentActorLabel(const std::string& sessionId) co } return label; } - diff --git a/editor/src/MCPServer.h b/editor/src/MCPServer.h index db90b42..90c556d 100644 --- a/editor/src/MCPServer.h +++ b/editor/src/MCPServer.h @@ -493,6 +493,39 @@ private: // Step 211: Register prompts // --------------------------------------------------------------- void registerWhetstonePrompts() { + // --- Step 270: Semantic annotation prompts --- + prompts_.push_back({"annotate_intent", + "Annotate unannotated functions with intent summaries and categories. " + "Uses whetstone_get_unannotated_nodes and whetstone_set_semantic_annotation.", + {} + }); + + prompts_.push_back({"annotate_complexity", + "Annotate functions with complexity estimates (time complexity, " + "cognitive complexity, lines of logic).", + {} + }); + + prompts_.push_back({"annotate_risk", + "Assess modification risk for each function based on callers, " + "complexity, and side effects. Uses call hierarchy for dependent counts.", + {} + }); + + prompts_.push_back({"annotate_contracts", + "Document preconditions, postconditions, return shapes, and " + "side effects for each function.", + {} + }); + + prompts_.push_back({"annotate_full", + "Run a complete annotation pass: intent, complexity, risk, " + "and contracts for all unannotated functions. Save the annotated " + "AST sidecar when done.", + {} + }); + + // --- Original prompts --- prompts_.push_back({"annotate_module", "Analyze the current module and suggest memory annotations for all " "unannotated functions with confidence scores.", @@ -520,6 +553,80 @@ private: std::vector generatePromptMessages(const std::string& name, const json& args) { + // --- Step 270: Semantic annotation prompt messages --- + if (name == "annotate_intent") { + return {{ + "user", + {{"type", "text"}, {"text", + "For each unannotated function, add an intent annotation:\n" + "1. Call whetstone_get_unannotated_nodes to find targets\n" + "2. Use whetstone_get_ast (compact:true) to understand each function\n" + "3. Call whetstone_set_semantic_annotation with type='intent', providing:\n" + " - summary: 1-sentence description of what and why\n" + " - category: one of 'validation', 'transformation', 'io',\n" + " 'coordination', 'computation', 'initialization'\n" + "4. Verify with whetstone_get_semantic_annotations" + }} + }}; + } + if (name == "annotate_complexity") { + return {{ + "user", + {{"type", "text"}, {"text", + "For each function, estimate complexity:\n" + "1. Call whetstone_get_unannotated_nodes with type='complexity'\n" + "2. Use whetstone_get_ast (compact:true) to read each function\n" + "3. Call whetstone_set_semantic_annotation with type='complexity':\n" + " - timeComplexity: 'O(1)', 'O(n)', 'O(n^2)', etc.\n" + " - cognitiveComplexity: 1-10 scale\n" + " - linesOfLogic: count of logic statements" + }} + }}; + } + if (name == "annotate_risk") { + return {{ + "user", + {{"type", "text"}, {"text", + "For each function, assess modification risk:\n" + "1. Call whetstone_get_unannotated_nodes with type='risk'\n" + "2. Use whetstone_get_call_hierarchy to count callers/dependents\n" + "3. Consider complexity and side effects\n" + "4. Call whetstone_set_semantic_annotation with type='risk':\n" + " - level: 'low', 'medium', 'high', 'critical'\n" + " - reason: why this risk level\n" + " - dependentCount: number of callers/consumers" + }} + }}; + } + if (name == "annotate_contracts") { + return {{ + "user", + {{"type", "text"}, {"text", + "For each function, document data contracts:\n" + "1. Call whetstone_get_unannotated_nodes with type='contract'\n" + "2. Use whetstone_get_ast (compact:true) to read each function\n" + "3. Call whetstone_set_semantic_annotation with type='contract':\n" + " - preconditions: input requirements\n" + " - postconditions: output guarantees\n" + " - returnShape: human-readable type/shape\n" + " - sideEffects: 'none', 'io', 'mutation', 'network'" + }} + }}; + } + if (name == "annotate_full") { + return {{ + "user", + {{"type", "text"}, {"text", + "Run a complete semantic annotation pass:\n" + "1. Call whetstone_get_unannotated_nodes to find all targets\n" + "2. For each function, add intent, complexity, risk, and contract\n" + " annotations using whetstone_set_semantic_annotation\n" + "3. Use whetstone_get_call_hierarchy for caller counts\n" + "4. Save the annotated AST with whetstone_save_annotated_ast\n" + "5. Verify with whetstone_get_semantic_annotations" + }} + }}; + } if (name == "annotate_module") { std::string scope = args.value("scope", "all"); return {{ @@ -934,6 +1041,126 @@ private: }; } + // --------------------------------------------------------------- + // Step 267: Sidecar persistence tools + // --------------------------------------------------------------- + void registerSidecarTools() { + // whetstone_save_annotated_ast + tools_.push_back({"whetstone_save_annotated_ast", + "Save the current buffer's AST (with all semantic annotations) " + "to a .whetstone/ sidecar file. Annotations persist alongside " + "the codebase without polluting source code.", + {{"type", "object"}, {"properties", { + {"path", {{"type", "string"}, + {"description", "Buffer path to save annotations for"}}} + }}, {"required", {"path"}}} + }); + toolHandlers_["whetstone_save_annotated_ast"] = + [this](const json& args) { + return callWhetstone("saveAnnotatedAST", args); + }; + + // whetstone_load_annotated_ast + tools_.push_back({"whetstone_load_annotated_ast", + "Load semantic annotations from a .whetstone/ sidecar file " + "and merge them into the current buffer's AST. Matches " + "annotations to live nodes by node ID.", + {{"type", "object"}, {"properties", { + {"path", {{"type", "string"}, + {"description", "Buffer path to load annotations for"}}} + }}, {"required", {"path"}}} + }); + toolHandlers_["whetstone_load_annotated_ast"] = + [this](const json& args) { + return callWhetstone("loadAnnotatedAST", args); + }; + + // whetstone_list_annotated_files + tools_.push_back({"whetstone_list_annotated_files", + "List all files that have saved .whetstone/ sidecar annotation " + "files in the workspace.", + {{"type", "object"}, {"properties", json::object()}} + }); + toolHandlers_["whetstone_list_annotated_files"] = + [this](const json& args) { + return callWhetstone("listAnnotatedFiles", args); + }; + } + + // --------------------------------------------------------------- + // Step 269: Semantic annotation management tools + // --------------------------------------------------------------- + void registerSemanticAnnotationTools() { + // whetstone_set_semantic_annotation + tools_.push_back({"whetstone_set_semantic_annotation", + "Set or update a semantic annotation on an AST node. If an " + "annotation of the same type already exists, it is replaced. " + "Types: intent, complexity, risk, contract, tags.", + {{"type", "object"}, {"properties", { + {"nodeId", {{"type", "string"}, + {"description", "Target node ID"}}}, + {"type", {{"type", "string"}, + {"enum", {"intent", "complexity", "risk", "contract", "tags"}}, + {"description", "Annotation type"}}}, + {"fields", {{"type", "object"}, + {"description", "Annotation-specific fields"}}} + }}, {"required", {"nodeId", "type", "fields"}}} + }); + toolHandlers_["whetstone_set_semantic_annotation"] = + [this](const json& args) { + return callWhetstone("setSemanticAnnotation", args); + }; + + // whetstone_get_semantic_annotations + tools_.push_back({"whetstone_get_semantic_annotations", + "Get semantic annotations. If nodeId provided, returns " + "annotations for that node. Otherwise returns all annotated " + "nodes with their annotations.", + {{"type", "object"}, {"properties", { + {"nodeId", {{"type", "string"}, + {"description", + "Node ID (optional, omit for all annotated nodes)"}}} + }}} + }); + toolHandlers_["whetstone_get_semantic_annotations"] = + [this](const json& args) { + return callWhetstone("getSemanticAnnotations", args); + }; + + // whetstone_remove_semantic_annotation + tools_.push_back({"whetstone_remove_semantic_annotation", + "Remove a semantic annotation of a specific type from a node.", + {{"type", "object"}, {"properties", { + {"nodeId", {{"type", "string"}, + {"description", "Target node ID"}}}, + {"type", {{"type", "string"}, + {"enum", {"intent", "complexity", "risk", "contract", "tags"}}, + {"description", "Annotation type to remove"}}} + }}, {"required", {"nodeId", "type"}}} + }); + toolHandlers_["whetstone_remove_semantic_annotation"] = + [this](const json& args) { + return callWhetstone("removeSemanticAnnotation", args); + }; + + // whetstone_get_unannotated_nodes + tools_.push_back({"whetstone_get_unannotated_nodes", + "Get Function/Variable nodes that lack semantic annotations. " + "Optionally filter by annotation type to find nodes missing " + "a specific annotation.", + {{"type", "object"}, {"properties", { + {"type", {{"type", "string"}, + {"enum", {"intent", "complexity", "risk", "contract", "tags"}}, + {"description", + "Filter: nodes missing this annotation type (optional)"}}} + }}} + }); + toolHandlers_["whetstone_get_unannotated_nodes"] = + [this](const json& args) { + return callWhetstone("getUnannotatedNodes", args); + }; + } + void registerWhetstoneTools() { registerASTTools(); registerAnnotationTools(); @@ -942,5 +1169,7 @@ private: registerBatchTools(); registerProjectTools(); registerSaveUndoTools(); + registerSidecarTools(); + registerSemanticAnnotationTools(); } }; diff --git a/editor/src/SidecarPersistence.h b/editor/src/SidecarPersistence.h new file mode 100644 index 0000000..10783d0 --- /dev/null +++ b/editor/src/SidecarPersistence.h @@ -0,0 +1,213 @@ +#pragma once +// Step 267: Sidecar AST Persistence +// +// Save/load annotated ASTs as .whetstone/.ast.json sidecar files. +// Annotations live alongside the codebase without polluting source code. + +#include "ast/ASTNode.h" +#include "ast/Serialization.h" +#include "ast/Annotation.h" +#include +#include +#include +#include +#include + +using json = nlohmann::json; + +// --- Sidecar path resolution --- +inline std::string sidecarPath(const std::string& workspaceRoot, + const std::string& filePath) { + namespace fs = std::filesystem; + fs::path base = fs::path(workspaceRoot) / ".whetstone"; + return (base / (filePath + ".ast.json")).string(); +} + +// --- Count semantic annotations in an AST --- +inline int countSemanticAnnotations(const ASTNode* node) { + if (!node) return 0; + int count = 0; + auto annos = node->getChildren("annotations"); + for (const auto* a : annos) { + if (a->conceptType == "IntentAnnotation" || + a->conceptType == "ComplexityAnnotation" || + a->conceptType == "RiskAnnotation" || + a->conceptType == "ContractAnnotation" || + a->conceptType == "SemanticTagAnnotation") + ++count; + } + for (const auto* child : node->allChildren()) { + count += countSemanticAnnotations(child); + } + return count; +} + +// --- Save annotated AST to sidecar file --- +struct SidecarSaveResult { + bool success = false; + std::string sidecarPath; + int annotationCount = 0; + std::string error; +}; + +inline SidecarSaveResult saveSidecarAST(const std::string& workspaceRoot, + const std::string& filePath, + Module* ast) { + SidecarSaveResult result; + if (!ast) { + result.error = "No AST to save"; + return result; + } + + result.sidecarPath = sidecarPath(workspaceRoot, filePath); + namespace fs = std::filesystem; + fs::create_directories(fs::path(result.sidecarPath).parent_path()); + + json astJson = toJson(ast); + std::ofstream out(result.sidecarPath); + if (!out.is_open()) { + result.error = "Cannot write to " + result.sidecarPath; + return result; + } + out << astJson.dump(2); + out.close(); + + result.annotationCount = countSemanticAnnotations(ast); + result.success = true; + return result; +} + +// --- Check if a node type is a semantic annotation --- +inline bool isSemanticAnnotation(const std::string& conceptType) { + return conceptType == "IntentAnnotation" || + conceptType == "ComplexityAnnotation" || + conceptType == "RiskAnnotation" || + conceptType == "ContractAnnotation" || + conceptType == "SemanticTagAnnotation"; +} + +// --- Find a matching node in the live AST --- +// Tries ID match first, then falls back to concept+name match +// (node IDs are ephemeral and change across re-parses). +inline ASTNode* findMatchingNode(ASTNode* liveRoot, + const ASTNode* sidecarNode) { + if (!liveRoot || !sidecarNode) return nullptr; + // Try exact ID match first + ASTNode* byId = findNodeById(liveRoot, sidecarNode->id); + if (byId) return byId; + // Fallback: match by concept type + name + std::string name = getNodeName(sidecarNode); + if (name.empty()) return nullptr; + for (auto* child : liveRoot->allChildren()) { + if (child->conceptType == sidecarNode->conceptType && + getNodeName(child) == name) + return child; + } + return nullptr; +} + +// --- Merge annotations from sidecar AST into live AST --- +// Matches nodes by ID or concept+name, copies semantic annotations. +struct SidecarMergeResult { + bool success = false; + int mergedCount = 0; + int staleCount = 0; + std::string error; +}; + +inline void mergeAnnotationsRecursive(ASTNode* sidecarNode, + ASTNode* liveRoot, + int& mergedCount, + int& staleCount) { + if (!sidecarNode) return; + + // Check if this sidecar node has semantic annotations to merge + auto annos = sidecarNode->getChildren("annotations"); + for (auto* anno : annos) { + if (!isSemanticAnnotation(anno->conceptType)) continue; + + // Find matching node in live AST + ASTNode* liveNode = findMatchingNode(liveRoot, sidecarNode); + if (liveNode) { + // Clone the annotation via JSON roundtrip + json annoJson = toJson(anno); + ASTNode* cloned = fromJson(annoJson); + if (cloned) { + liveNode->addChild("annotations", cloned); + ++mergedCount; + } + } else { + ++staleCount; + } + } + + // Recurse into children + for (auto* child : sidecarNode->allChildren()) { + // Skip annotation children themselves + if (isSemanticAnnotation(child->conceptType)) continue; + mergeAnnotationsRecursive(child, liveRoot, mergedCount, staleCount); + } +} + +inline SidecarMergeResult loadSidecarAST(const std::string& workspaceRoot, + const std::string& filePath, + Module* liveAST) { + SidecarMergeResult result; + if (!liveAST) { + result.error = "No live AST to merge into"; + return result; + } + + std::string path = sidecarPath(workspaceRoot, filePath); + std::ifstream in(path); + if (!in.is_open()) { + result.error = "No sidecar file: " + path; + return result; + } + + json astJson; + try { + in >> astJson; + } catch (const std::exception& e) { + result.error = std::string("Failed to parse sidecar: ") + e.what(); + return result; + } + in.close(); + + ASTNode* sidecarAST = fromJson(astJson); + if (!sidecarAST) { + result.error = "Failed to deserialize sidecar AST"; + return result; + } + + mergeAnnotationsRecursive(sidecarAST, liveAST, + result.mergedCount, result.staleCount); + + // Clean up sidecar AST + delete sidecarAST; + + result.success = true; + return result; +} + +// --- List available sidecar files --- +inline std::vector listSidecarFiles( + const std::string& workspaceRoot) { + namespace fs = std::filesystem; + std::vector files; + fs::path dir = fs::path(workspaceRoot) / ".whetstone"; + if (!fs::exists(dir)) return files; + + for (const auto& entry : fs::directory_iterator(dir)) { + if (entry.is_regular_file()) { + std::string name = entry.path().filename().string(); + // Remove .ast.json suffix to get original path + const std::string suffix = ".ast.json"; + if (name.size() > suffix.size() && + name.substr(name.size() - suffix.size()) == suffix) { + files.push_back(name.substr(0, name.size() - suffix.size())); + } + } + } + return files; +} diff --git a/editor/src/ast/ASTNode.h b/editor/src/ast/ASTNode.h index a5238fb..99a4bef 100644 --- a/editor/src/ast/ASTNode.h +++ b/editor/src/ast/ASTNode.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include diff --git a/editor/src/ast/Annotation.h b/editor/src/ast/Annotation.h index 15942d9..151a044 100644 --- a/editor/src/ast/Annotation.h +++ b/editor/src/ast/Annotation.h @@ -141,3 +141,44 @@ class ConstExprAnnotation : public Annotation { public: ConstExprAnnotation() { conceptType = "ConstExprAnnotation"; } }; + +// Semantic annotations for AI guidance (Sprint 10, Step 266) + +class IntentAnnotation : public Annotation { +public: + std::string summary; // 1-sentence description of what/why + std::string category; // "validation", "transformation", "io", + // "coordination", "computation", "initialization" + IntentAnnotation() { conceptType = "IntentAnnotation"; } +}; + +class ComplexityAnnotation : public Annotation { +public: + std::string timeComplexity; // "O(1)", "O(n)", "O(n^2)", etc. + int cognitiveComplexity = 0; // 1-10 scale + int linesOfLogic = 0; + ComplexityAnnotation() { conceptType = "ComplexityAnnotation"; } +}; + +class RiskAnnotation : public Annotation { +public: + std::string level; // "low", "medium", "high", "critical" + std::string reason; + int dependentCount = 0; // how many callers/consumers + RiskAnnotation() { conceptType = "RiskAnnotation"; } +}; + +class ContractAnnotation : public Annotation { +public: + std::string preconditions; + std::string postconditions; + std::string returnShape; // human-readable type/shape description + std::string sideEffects; // "none", "io", "mutation", "network" + ContractAnnotation() { conceptType = "ContractAnnotation"; } +}; + +class SemanticTagAnnotation : public Annotation { +public: + std::vector tags; // e.g. ["@serialize", "@validation"] + SemanticTagAnnotation() { conceptType = "SemanticTagAnnotation"; } +}; diff --git a/editor/src/ast/Serialization.h b/editor/src/ast/Serialization.h index e0e16b7..427cd81 100644 --- a/editor/src/ast/Serialization.h +++ b/editor/src/ast/Serialization.h @@ -134,6 +134,35 @@ inline json propertiesToJson(const ASTNode* node) { if (!n->semanticHint.empty()) props["semanticHint"] = n->semanticHint; if (!n->position.empty()) props["position"] = n->position; } + // Semantic annotations (Sprint 10) + else if (ct == "IntentAnnotation") { + auto* n = static_cast(node); + if (!n->summary.empty()) props["summary"] = n->summary; + if (!n->category.empty()) props["category"] = n->category; + } + else if (ct == "ComplexityAnnotation") { + auto* n = static_cast(node); + if (!n->timeComplexity.empty()) props["timeComplexity"] = n->timeComplexity; + props["cognitiveComplexity"] = n->cognitiveComplexity; + props["linesOfLogic"] = n->linesOfLogic; + } + else if (ct == "RiskAnnotation") { + auto* n = static_cast(node); + if (!n->level.empty()) props["level"] = n->level; + if (!n->reason.empty()) props["reason"] = n->reason; + props["dependentCount"] = n->dependentCount; + } + else if (ct == "ContractAnnotation") { + auto* n = static_cast(node); + if (!n->preconditions.empty()) props["preconditions"] = n->preconditions; + if (!n->postconditions.empty()) props["postconditions"] = n->postconditions; + if (!n->returnShape.empty()) props["returnShape"] = n->returnShape; + if (!n->sideEffects.empty()) props["sideEffects"] = n->sideEffects; + } + else if (ct == "SemanticTagAnnotation") { + auto* n = static_cast(node); + if (!n->tags.empty()) props["tags"] = n->tags; + } // NullLiteral, ListLiteral, IndexAccess, Block, Assignment, IfStatement, // WhileLoop, Return, ExpressionStatement, ListType, SetType, MapType, // TupleType, ArrayType, OptionalType — no extra properties @@ -206,6 +235,11 @@ inline ASTNode* createNode(const std::string& conceptName) { if (conceptName == "DerefStrategy") return new DerefStrategy(); if (conceptName == "OptimizationLock") return new OptimizationLock(); if (conceptName == "LangSpecific") return new LangSpecific(); + if (conceptName == "IntentAnnotation") return new IntentAnnotation(); + if (conceptName == "ComplexityAnnotation") return new ComplexityAnnotation(); + if (conceptName == "RiskAnnotation") return new RiskAnnotation(); + if (conceptName == "ContractAnnotation") return new ContractAnnotation(); + if (conceptName == "SemanticTagAnnotation") return new SemanticTagAnnotation(); return nullptr; } @@ -328,13 +362,53 @@ inline void setPropertiesFromJson(ASTNode* node, const json& props) { if (props.contains("semanticHint")) n->semanticHint = props["semanticHint"].get(); if (props.contains("position")) n->position = props["position"].get(); } + // Semantic annotations (Sprint 10) + else if (ct == "IntentAnnotation") { + auto* n = static_cast(node); + if (props.contains("summary")) n->summary = props["summary"].get(); + if (props.contains("category")) n->category = props["category"].get(); + } + else if (ct == "ComplexityAnnotation") { + auto* n = static_cast(node); + if (props.contains("timeComplexity")) n->timeComplexity = props["timeComplexity"].get(); + if (props.contains("cognitiveComplexity")) n->cognitiveComplexity = props["cognitiveComplexity"].get(); + if (props.contains("linesOfLogic")) n->linesOfLogic = props["linesOfLogic"].get(); + } + else if (ct == "RiskAnnotation") { + auto* n = static_cast(node); + if (props.contains("level")) n->level = props["level"].get(); + if (props.contains("reason")) n->reason = props["reason"].get(); + if (props.contains("dependentCount")) n->dependentCount = props["dependentCount"].get(); + } + else if (ct == "ContractAnnotation") { + auto* n = static_cast(node); + if (props.contains("preconditions")) n->preconditions = props["preconditions"].get(); + if (props.contains("postconditions")) n->postconditions = props["postconditions"].get(); + if (props.contains("returnShape")) n->returnShape = props["returnShape"].get(); + if (props.contains("sideEffects")) n->sideEffects = props["sideEffects"].get(); + } + else if (ct == "SemanticTagAnnotation") { + auto* n = static_cast(node); + if (props.contains("tags") && props["tags"].is_array()) { + n->tags.clear(); + for (const auto& tag : props["tags"]) { + if (tag.is_string()) n->tags.push_back(tag.get()); + } + } + } +} + +inline std::string generateNodeId() { + static int counter = 0; + return "node_" + std::to_string(++counter); } inline ASTNode* fromJson(const json& j) { ASTNode* node = createNode(j["concept"].get()); if (!node) return nullptr; - node->id = j["id"].get(); + node->id = j.contains("id") ? j["id"].get() + : generateNodeId(); if (j.contains("span")) { const auto& span = j["span"]; if (span.contains("start") && span.contains("end")) { diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 9678730..016b38b 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -5,10 +5,12 @@ #include "imgui_impl_sdl2.h" #include "imgui_impl_opengl3.h" +#include "imgui_internal.h" #include #include #include #include +#include #include #include "EditorState.h" @@ -72,15 +74,110 @@ static std::string emacsChordForEvent(SDL_Keycode sym, Uint16 mods) { return prefix + base; } +static ImGuiID splitDockNode(ImGuiID& mainId, DockDirection direction, float ratio) { + ImGuiDir dir = ImGuiDir_None; + switch (direction) { + case DockDirection::Left: dir = ImGuiDir_Left; break; + case DockDirection::Right: dir = ImGuiDir_Right; break; + case DockDirection::Bottom: dir = ImGuiDir_Down; break; + case DockDirection::Top: dir = ImGuiDir_Up; break; + case DockDirection::Center: return mainId; + } + return ImGui::DockBuilderSplitNode(mainId, dir, ratio, nullptr, &mainId); +} + +static std::unordered_map buildDockNodes( + ImGuiID dockId, + const LayoutConfig& cfg, + const ImVec2& size) { + std::unordered_map nodes; + ImGui::DockBuilderRemoveNode(dockId); + ImGui::DockBuilderAddNode(dockId, ImGuiDockNodeFlags_DockSpace); + ImGui::DockBuilderSetNodeSize(dockId, size); + ImGuiID mainId = dockId; + for (const auto& panel : cfg.panels) { + if (panel.direction == DockDirection::Center) continue; + nodes[panel.id] = splitDockNode(mainId, panel.direction, panel.sizeRatio); + } + for (const auto& panel : cfg.panels) { + if (panel.direction != DockDirection::Center) continue; + nodes[panel.id] = mainId; + } + return nodes; +} + +static void dockPanelGroup( + const std::unordered_map& nodes, + const std::string& panelId, + std::initializer_list windows) { + auto it = nodes.find(panelId); + if (it == nodes.end()) return; + for (const char* window : windows) { + ImGui::DockBuilderDockWindow(window, it->second); + } +} + +static void applyDockLayout(ImGuiID dockId, const LayoutConfig& cfg, const ImVec2& size) { + auto nodes = buildDockNodes(dockId, cfg, size); + dockPanelGroup(nodes, "Explorer", {"Explorer"}); + dockPanelGroup(nodes, "Editor", {"Editor"}); + dockPanelGroup(nodes, "Panel", {"Panel"}); + dockPanelGroup(nodes, "ToolWindow", + {"Outline", "Dependencies", "Libraries", "Compose", + "Emacs Packages", "Emacs Bridge", "Memory Strategies"}); + dockPanelGroup(nodes, "Minibuffer", {"##Minibuffer"}); + ImGui::DockBuilderFinish(dockId); +} + // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- int main(int, char**) { + auto logStage = [](const char* msg) { + std::fprintf(stderr, "[whetstone-startup] %s\n", msg); + std::fflush(stderr); + }; + + logStage("begin"); + { + const char* envDriver = std::getenv("SDL_VIDEODRIVER"); + if (envDriver && std::string(envDriver) == "offscreen") { + std::fprintf(stderr, + "[whetstone-startup] overriding SDL_VIDEODRIVER=offscreen\n"); + SDL_setenv("SDL_VIDEODRIVER", "", 1); + } + bool hasX11 = false; + bool hasWayland = false; + int numDrivers = SDL_GetNumVideoDrivers(); + std::fprintf(stderr, "[whetstone-startup] available SDL drivers:"); + for (int i = 0; i < numDrivers; ++i) { + const char* d = SDL_GetVideoDriver(i); + if (!d) continue; + std::fprintf(stderr, " %s", d); + if (std::string(d) == "x11") hasX11 = true; + if (std::string(d) == "wayland") hasWayland = true; + } + std::fprintf(stderr, "\n"); + std::fflush(stderr); + const char* currentEnv = std::getenv("SDL_VIDEODRIVER"); + if (!currentEnv || std::string(currentEnv).empty()) { + if (hasX11) SDL_setenv("SDL_VIDEODRIVER", "x11", 1); + else if (hasWayland) SDL_setenv("SDL_VIDEODRIVER", "wayland", 1); + } + } + // SDL init + logStage("SDL_Init"); if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) { printf("SDL Error: %s\n", SDL_GetError()); return -1; } + { + const char* driver = SDL_GetCurrentVideoDriver(); + std::fprintf(stderr, "[whetstone-startup] SDL video driver: %s\n", + driver ? driver : "(null)"); + std::fflush(stderr); + } const char* glsl_version = "#version 130"; SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0); @@ -92,13 +189,30 @@ int main(int, char**) { SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); auto winFlags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | - SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_MAXIMIZED); + SDL_WINDOW_ALLOW_HIGHDPI); + logStage("SDL_CreateWindow"); SDL_Window* window = SDL_CreateWindow("Whetstone Editor", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1440, 900, winFlags); + if (!window) { + printf("SDL_CreateWindow Error: %s\n", SDL_GetError()); + SDL_Quit(); + return -1; + } + logStage("SDL_GL_CreateContext"); SDL_GLContext gl_context = SDL_GL_CreateContext(window); + if (!gl_context) { + printf("SDL_GL_CreateContext Error: %s\n", SDL_GetError()); + SDL_DestroyWindow(window); + SDL_Quit(); + return -1; + } + logStage("SDL_GL_MakeCurrent"); SDL_GL_MakeCurrent(window, gl_context); SDL_GL_SetSwapInterval(1); + SDL_ShowWindow(window); + SDL_RaiseWindow(window); + logStage("window shown"); // ImGui init IMGUI_CHECKVERSION(); @@ -109,15 +223,11 @@ int main(int, char**) { // Load fonts const float baseFontSize = 15.0f; - ImFont* monoFont = io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\consola.ttf", baseFontSize); - if (!monoFont) { - monoFont = io.Fonts->AddFontDefault(); - } - ImFont* uiFont = io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeui.ttf", baseFontSize); - if (!uiFont) { - uiFont = io.Fonts->AddFontDefault(); - } + // Bootstrap with default fonts; platform-specific/custom fonts are loaded in reloadFonts(). + ImFont* monoFont = io.Fonts->AddFontDefault(); + ImFont* uiFont = io.Fonts->AddFontDefault(); + logStage("imgui initialized"); ImGui_ImplSDL2_InitForOpenGL(window, gl_context); ImGui_ImplOpenGL3_Init(glsl_version); @@ -126,7 +236,9 @@ int main(int, char**) { state.monoFont = monoFont; state.uiFont = uiFont; state.baseFontSize = baseFontSize; + logStage("state.init begin"); state.init(); + logStage("state.init end"); auto loadFontFromPath = [&](const std::string& preferred, const std::vector& fallbacks, @@ -194,6 +306,8 @@ int main(int, char**) { io.FontGlobalScale = state.settings.getFontSize() / baseFontSize; SessionData session; bool hasSession = state.loadSession(session); + bool applyInitialLayout = !hasSession; + bool dockLayoutApplied = false; if (hasSession) { if (!session.imguiIni.empty()) { ImGui::LoadIniSettingsFromMemory(session.imguiIni.c_str(), @@ -207,6 +321,7 @@ int main(int, char**) { state.lsp = std::make_shared(state.lspTransport); bool done = false; + bool firstFrame = true; while (!done) { // --- Event handling --- SDL_Event event; @@ -373,6 +488,14 @@ int main(int, char**) { ImGui::PopStyleVar(3); ImGuiID dockId = ImGui::GetID("WhetstoneDS"); ImGui::DockSpace(dockId, ImVec2(0, 0), ImGuiDockNodeFlags_None); + if ((applyInitialLayout && !dockLayoutApplied) || state.ui.requestLayoutReset) { + LayoutManager layout; + layout.setPreset(state.ui.layoutPreset); + applyDockLayout(dockId, layout.getConfig(), viewport->WorkSize); + dockLayoutApplied = true; + state.ui.requestBottomCollapse = true; + state.ui.requestLayoutReset = false; + } ImGui::End(); // --- Render all panels --- @@ -435,6 +558,10 @@ int main(int, char**) { ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); SDL_GL_SwapWindow(window); + if (firstFrame) { + logStage("first frame swapped"); + firstFrame = false; + } } // Cleanup diff --git a/editor/src/panels/BottomPanel.h b/editor/src/panels/BottomPanel.h index e406225..953167f 100644 --- a/editor/src/panels/BottomPanel.h +++ b/editor/src/panels/BottomPanel.h @@ -56,10 +56,16 @@ static void renderBottomPanel(EditorState& state) { ImGui::SetNextWindowFocus(); state.ui.focusTarget = FocusRegion::None; } + if (state.ui.requestBottomCollapse) { + ImGui::SetNextWindowCollapsed(true, ImGuiCond_Always); + } // --------------------------------------------------------------- // Bottom panel — Output / AST / Highlighted Preview / Terminal // --------------------------------------------------------------- ImGui::Begin("Panel"); + if (state.ui.requestBottomCollapse) { + state.ui.requestBottomCollapse = false; + } if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) { state.ui.focusedRegion = FocusRegion::Bottom; } diff --git a/editor/src/panels/DialogPanels.h b/editor/src/panels/DialogPanels.h index 08034d7..bc3e72e 100644 --- a/editor/src/panels/DialogPanels.h +++ b/editor/src/panels/DialogPanels.h @@ -338,7 +338,7 @@ static void renderCommandPalette(EditorState& state) { bool selected = (displayIndex == state.commandSelected); std::string rowId = "##cmd_recent_" + std::to_string(displayIndex); if (ImGui::Selectable(rowId.c_str(), selected, - ImGuiSelectableFlags_SpanAvailWidth, + 0, ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing()))) { state.commandSelected = displayIndex; accept = true; @@ -375,7 +375,7 @@ static void renderCommandPalette(EditorState& state) { bool selected = (displayIndex == state.commandSelected); std::string rowId = "##cmd_" + std::to_string(displayIndex); if (ImGui::Selectable(rowId.c_str(), selected, - ImGuiSelectableFlags_SpanAvailWidth, + 0, ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing()))) { state.commandSelected = displayIndex; accept = true; @@ -502,7 +502,7 @@ static void renderCommandPalette(EditorState& state) { bool selected = (i == state.commandSelected); std::string rowId = "##file_" + std::to_string(i); if (ImGui::Selectable(rowId.c_str(), selected, - ImGuiSelectableFlags_SpanAvailWidth, + 0, ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing()))) { state.commandSelected = i; accept = true; diff --git a/editor/src/panels/ExplorerPanel.h b/editor/src/panels/ExplorerPanel.h index 373780b..bc471e3 100644 --- a/editor/src/panels/ExplorerPanel.h +++ b/editor/src/panels/ExplorerPanel.h @@ -29,6 +29,18 @@ static void renderExplorerPanel(EditorState& state) { state.refreshFileTree(); if (state.fileTreeRoot.path.empty()) { ImGui::TextDisabled("(no workspace)"); + ImGui::Spacing(); + ImGui::TextWrapped("Open a folder to browse project files."); + if (ImGui::Button("Open Folder")) { + auto path = FileDialog::openFolder({"Open Folder", state.workspaceRoot}); + if (!path.empty()) { + state.workspaceRoot = path; + state.fileTreeDirty = true; + state.search.projectSearch.setRoot(state.workspaceRoot); + state.lastDialogPath = path; + state.refreshBuildSystem(); + } + } } else { RenderFileTree(state.fileTreeRoot, state); } diff --git a/editor/src/panels/MenuBarPanel.h b/editor/src/panels/MenuBarPanel.h index 06d58d0..53be333 100644 --- a/editor/src/panels/MenuBarPanel.h +++ b/editor/src/panels/MenuBarPanel.h @@ -131,6 +131,10 @@ static void renderMenuBar(EditorState& state) { state.ui.layoutPreset = LayoutPreset::Emacs; if (ImGui::MenuItem("JetBrains", nullptr, state.ui.layoutPreset == LayoutPreset::JetBrains)) state.ui.layoutPreset = LayoutPreset::JetBrains; + ImGui::Separator(); + if (ImGui::MenuItem("Reset Layout")) { + state.ui.requestLayoutReset = true; + } ImGui::EndMenu(); } ImGui::EndMenu(); diff --git a/editor/src/panels/StatusBarPanel.h b/editor/src/panels/StatusBarPanel.h index be1a1e1..5e2854a 100644 --- a/editor/src/panels/StatusBarPanel.h +++ b/editor/src/panels/StatusBarPanel.h @@ -93,7 +93,7 @@ static void renderStatusBar(EditorState& state) { // Left cluster: mode, language, encoding, line ending std::string modeLabel = state.active() - ? (state.active()->bufferMode == BufferManager::BufferMode::Text ? "Text" : "Structured") + ? (state.active()->bufferMode == BufferManager::BufferMode::Text ? "Text Mode" : "Structured Mode") : "-"; if (ImGui::Button(modeLabel.c_str())) { toggleBufferMode(state); diff --git a/editor/src/state/UIFlags.h b/editor/src/state/UIFlags.h index d7f2e8f..3269d57 100644 --- a/editor/src/state/UIFlags.h +++ b/editor/src/state/UIFlags.h @@ -23,6 +23,8 @@ struct UIFlags { bool showAnnotateWizard = false; bool showProjectWizard = false; bool showAgentWizard = false; + bool requestLayoutReset = false; + bool requestBottomCollapse = false; int bottomTab = 0; // 0=Output,1=AST,2=Highlighted LayoutPreset layoutPreset = LayoutPreset::VSCode; FocusRegion focusTarget = FocusRegion::None; diff --git a/editor/tests/step266_test.cpp b/editor/tests/step266_test.cpp new file mode 100644 index 0000000..21c5a48 --- /dev/null +++ b/editor/tests/step266_test.cpp @@ -0,0 +1,394 @@ +// Step 266 TDD Test: Semantic Annotation AST Node Types +// +// Tests 5 new semantic annotation types: IntentAnnotation, ComplexityAnnotation, +// RiskAnnotation, ContractAnnotation, SemanticTagAnnotation. +// Validates construction, JSON roundtrip, compact AST semantic fields, +// and attachment to Function nodes. +#include "HeadlessEditorState.h" +#include "MCPServer.h" + +#include +#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; + } +} + +static json rpc(HeadlessEditorState& state, const std::string& session, + const std::string& method, + json params = json::object()) { + json request = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", method}, {"params", params}}; + return state.processAgentRequest(request, session); +} + +int main() { + int passed = 0; + int failed = 0; + // --------------------------------------------------------------- + // Test 1: IntentAnnotation construction and fields + // --------------------------------------------------------------- + { + IntentAnnotation a; + a.summary = "validates user input before DB write"; + a.category = "validation"; + expect(a.conceptType == "IntentAnnotation" && + a.summary == "validates user input before DB write" && + a.category == "validation", + "IntentAnnotation construction and fields", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 2: ComplexityAnnotation construction and fields + // --------------------------------------------------------------- + { + ComplexityAnnotation a; + a.timeComplexity = "O(n log n)"; + a.cognitiveComplexity = 7; + a.linesOfLogic = 42; + expect(a.conceptType == "ComplexityAnnotation" && + a.timeComplexity == "O(n log n)" && + a.cognitiveComplexity == 7 && + a.linesOfLogic == 42, + "ComplexityAnnotation construction and fields", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 3: RiskAnnotation construction and fields + // --------------------------------------------------------------- + { + RiskAnnotation a; + a.level = "high"; + a.reason = "12 callers depend on exact return format"; + a.dependentCount = 12; + expect(a.conceptType == "RiskAnnotation" && + a.level == "high" && + a.reason == "12 callers depend on exact return format" && + a.dependentCount == 12, + "RiskAnnotation construction and fields", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 4: ContractAnnotation construction and fields + // --------------------------------------------------------------- + { + ContractAnnotation a; + a.preconditions = "name must be non-empty string"; + a.postconditions = "returns greeting string starting with 'hello'"; + a.returnShape = "str"; + a.sideEffects = "none"; + expect(a.conceptType == "ContractAnnotation" && + a.preconditions == "name must be non-empty string" && + a.returnShape == "str" && + a.sideEffects == "none", + "ContractAnnotation construction and fields", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 5: SemanticTagAnnotation construction and fields + // --------------------------------------------------------------- + { + SemanticTagAnnotation a; + a.tags = {"@serialize", "@validation", "@auth"}; + expect(a.conceptType == "SemanticTagAnnotation" && + a.tags.size() == 3 && + a.tags[0] == "@serialize" && + a.tags[2] == "@auth", + "SemanticTagAnnotation construction and fields", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 6: IntentAnnotation JSON roundtrip via Serialization.h + // --------------------------------------------------------------- + { + IntentAnnotation orig; + orig.id = "intent_1"; + orig.summary = "computes tax for order"; + orig.category = "computation"; + json j = toJson(&orig); + + auto* restored = static_cast(fromJson(j)); + bool ok = restored && + restored->conceptType == "IntentAnnotation" && + restored->summary == "computes tax for order" && + restored->category == "computation" && + restored->id == "intent_1"; + delete restored; + expect(ok, "IntentAnnotation JSON roundtrip", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 7: ComplexityAnnotation JSON roundtrip + // --------------------------------------------------------------- + { + ComplexityAnnotation orig; + orig.id = "cx_1"; + orig.timeComplexity = "O(n^2)"; + orig.cognitiveComplexity = 9; + orig.linesOfLogic = 55; + json j = toJson(&orig); + + auto* restored = static_cast(fromJson(j)); + bool ok = restored && + restored->timeComplexity == "O(n^2)" && + restored->cognitiveComplexity == 9 && + restored->linesOfLogic == 55; + delete restored; + expect(ok, "ComplexityAnnotation JSON roundtrip", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 8: RiskAnnotation + ContractAnnotation roundtrip + // --------------------------------------------------------------- + { + RiskAnnotation risk; + risk.id = "risk_1"; + risk.level = "critical"; + risk.reason = "payment processing"; + risk.dependentCount = 25; + json rj = toJson(&risk); + auto* rr = static_cast(fromJson(rj)); + + ContractAnnotation contract; + contract.id = "ct_1"; + contract.preconditions = "amount > 0"; + contract.postconditions = "returns transaction_id"; + contract.returnShape = "str"; + contract.sideEffects = "network"; + json cj = toJson(&contract); + auto* cr = static_cast(fromJson(cj)); + + bool ok = rr && rr->level == "critical" && rr->dependentCount == 25 && + cr && cr->preconditions == "amount > 0" && + cr->sideEffects == "network"; + delete rr; + delete cr; + expect(ok, "RiskAnnotation + ContractAnnotation roundtrip", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 9: SemanticTagAnnotation JSON roundtrip + // --------------------------------------------------------------- + { + SemanticTagAnnotation orig; + orig.id = "tag_1"; + orig.tags = {"@io", "@crypto"}; + json j = toJson(&orig); + + auto* restored = static_cast(fromJson(j)); + bool ok = restored && + restored->tags.size() == 2 && + restored->tags[0] == "@io" && + restored->tags[1] == "@crypto"; + delete restored; + expect(ok, "SemanticTagAnnotation JSON roundtrip", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 10: Attach semantic annotations to Function node + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.setAgentRole("s1", AgentRole::Refactor); + + std::string src = "def greet(name):\n return \"hello \" + name\n"; + rpc(state, "s1", "openFile", + {{"path", "t.py"}, {"content", src}}); + + // Find function node + std::string fnId; + for (auto* c : state.activeAST()->allChildren()) { + if (c->conceptType == "Function" && getNodeName(c) == "greet") { + fnId = c->id; + break; + } + } + + // Attach IntentAnnotation via applyMutation insertNode + rpc(state, "s1", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "IntentAnnotation"}, + {"properties", {{"summary", "greets a person by name"}, + {"category", "io"}}}}}}); + + // Check it's there + bool found = false; + ASTNode* fn = findNodeById(state.activeAST(), fnId); + if (fn) { + for (auto* child : fn->getChildren("annotations")) { + if (child->conceptType == "IntentAnnotation") { + auto* ia = static_cast(child); + if (ia->summary == "greets a person by name") + found = true; + } + } + } + + expect(!fnId.empty() && found, + "Attach IntentAnnotation to Function via applyMutation", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 11: Compact AST includes semantic summary field + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.setAgentRole("s1", AgentRole::Refactor); + + std::string src = "def greet(name):\n return \"hello \" + name\n"; + rpc(state, "s1", "openFile", + {{"path", "t.py"}, {"content", src}}); + + std::string fnId; + for (auto* c : state.activeAST()->allChildren()) { + if (c->conceptType == "Function" && getNodeName(c) == "greet") { + fnId = c->id; + break; + } + } + + // Add intent + risk annotations + rpc(state, "s1", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "IntentAnnotation"}, + {"properties", {{"summary", "greets user"}, + {"category", "io"}}}}}}); + + rpc(state, "s1", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "RiskAnnotation"}, + {"properties", {{"level", "low"}, + {"reason", "simple function"}, + {"dependentCount", 2}}}}}}); + + // Get compact AST + json resp = rpc(state, "s1", "getAST", {{"compact", true}}); + json nodes = resp["result"]["nodes"]; + + // Find greet node in compact output + bool hasSemantic = false; + for (const auto& n : nodes) { + if (n.value("name", "") == "greet" && + n.contains("semantic")) { + auto sem = n["semantic"]; + hasSemantic = sem.contains("intent") && + sem.contains("risk"); + } + } + + expect(hasSemantic, + "Compact AST includes semantic summary for annotated function", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 12: All 5 annotation types survive AST clone (undo roundtrip) + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.setAgentRole("s1", AgentRole::Refactor); + + std::string src = "def greet(name):\n return \"hello \" + name\n"; + rpc(state, "s1", "openFile", + {{"path", "t.py"}, {"content", src}}); + + std::string fnId; + for (auto* c : state.activeAST()->allChildren()) { + if (c->conceptType == "Function" && getNodeName(c) == "greet") { + fnId = c->id; + break; + } + } + + // Add all 5 annotation types + rpc(state, "s1", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "IntentAnnotation"}, + {"properties", {{"summary", "test"}, {"category", "io"}}}}}}); + + rpc(state, "s1", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "ComplexityAnnotation"}, + {"properties", {{"timeComplexity", "O(1)"}, + {"cognitiveComplexity", 1}, + {"linesOfLogic", 2}}}}}}); + + rpc(state, "s1", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "RiskAnnotation"}, + {"properties", {{"level", "low"}, + {"reason", "trivial"}, + {"dependentCount", 0}}}}}}); + + rpc(state, "s1", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "ContractAnnotation"}, + {"properties", {{"preconditions", "name is str"}, + {"postconditions", "returns str"}, + {"returnShape", "str"}, + {"sideEffects", "none"}}}}}}); + + rpc(state, "s1", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "SemanticTagAnnotation"}, + {"properties", {{"tags", json::array({"@io"})}} }}}}); + + // Serialize AST to JSON and back (simulates undo/clone roundtrip) + json astJson = toJson(state.activeAST()); + auto* restored = static_cast(fromJson(astJson)); + + int annoCount = 0; + for (auto* c : restored->allChildren()) { + if (c->conceptType == "Function" && getNodeName(c) == "greet") { + for (auto* a : c->getChildren("annotations")) { + if (a->conceptType == "IntentAnnotation" || + a->conceptType == "ComplexityAnnotation" || + a->conceptType == "RiskAnnotation" || + a->conceptType == "ContractAnnotation" || + a->conceptType == "SemanticTagAnnotation") + ++annoCount; + } + } + } + delete restored; + + expect(annoCount == 5, + "All 5 semantic annotation types survive JSON roundtrip (" + + std::to_string(annoCount) + "/5)", + passed, failed); + } + + std::cout << "\n=== Step 266 Results: " << passed << " passed, " + << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/editor/tests/step267_test.cpp b/editor/tests/step267_test.cpp new file mode 100644 index 0000000..f0e35d1 --- /dev/null +++ b/editor/tests/step267_test.cpp @@ -0,0 +1,452 @@ +// Step 267 TDD Test: Sidecar AST Persistence +// +// Tests saving/loading annotated ASTs as .whetstone/ sidecar files. +// Annotations live alongside the codebase without polluting source. +#include "HeadlessEditorState.h" +#include "MCPServer.h" + +#include +#include +#include +#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; + } +} + +static json rpc(HeadlessEditorState& state, const std::string& session, + const std::string& method, + json params = json::object()) { + json request = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", method}, {"params", params}}; + return state.processAgentRequest(request, session); +} + +int main() { + int passed = 0; + int failed = 0; + + auto tmpDir = std::filesystem::temp_directory_path() / + "whetstone_step267_test"; + std::filesystem::remove_all(tmpDir); + std::filesystem::create_directories(tmpDir); + + std::string src = + "def greet(name):\n" + " return \"hello \" + name\n"; + + // --------------------------------------------------------------- + // Test 1: saveAnnotatedAST creates sidecar file + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("s1", AgentRole::Refactor); + + rpc(state, "s1", "openFile", + {{"path", "greet.py"}, {"content", src}}); + + // Add an annotation + std::string fnId; + for (auto* c : state.activeAST()->allChildren()) { + if (c->conceptType == "Function" && getNodeName(c) == "greet") { + fnId = c->id; + break; + } + } + rpc(state, "s1", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "IntentAnnotation"}, + {"properties", {{"summary", "greets by name"}, + {"category", "io"}}}}}}); + + json resp = rpc(state, "s1", "saveAnnotatedAST", + {{"path", "greet.py"}}); + bool success = resp.contains("result") && + resp["result"].value("success", false); + + auto sidecarPath = tmpDir / ".whetstone" / "greet.py.ast.json"; + bool exists = std::filesystem::exists(sidecarPath); + + expect(success && exists, + "saveAnnotatedAST creates .whetstone/greet.py.ast.json", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 2: loadAnnotatedAST restores annotations + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("s1", AgentRole::Refactor); + + // Open same file fresh (no annotations) + rpc(state, "s1", "openFile", + {{"path", "greet.py"}, {"content", src}}); + + // Load sidecar + json resp = rpc(state, "s1", "loadAnnotatedAST", + {{"path", "greet.py"}}); + bool success = resp.contains("result") && + resp["result"].value("success", false); + + // Check annotation restored + bool found = false; + for (auto* c : state.activeAST()->allChildren()) { + if (c->conceptType == "Function" && getNodeName(c) == "greet") { + for (auto* a : c->getChildren("annotations")) { + if (a->conceptType == "IntentAnnotation") { + auto* ia = static_cast(a); + if (ia->summary == "greets by name") found = true; + } + } + } + } + + expect(success && found, + "loadAnnotatedAST restores IntentAnnotation from sidecar", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 3: Sidecar merge matches by node ID + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("s1", AgentRole::Refactor); + + rpc(state, "s1", "openFile", + {{"path", "greet.py"}, {"content", src}}); + + // Load sidecar and check that the annotation is on the right node + rpc(state, "s1", "loadAnnotatedAST", {{"path", "greet.py"}}); + + int annotatedFunctions = 0; + for (auto* c : state.activeAST()->allChildren()) { + if (c->conceptType == "Function") { + auto annos = c->getChildren("annotations"); + for (auto* a : annos) { + if (a->conceptType == "IntentAnnotation") + ++annotatedFunctions; + } + } + } + + expect(annotatedFunctions == 1, + "Sidecar annotations merged onto correct function node", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 4: listAnnotatedFiles returns saved sidecars + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("s1", AgentRole::Refactor); + + json resp = rpc(state, "s1", "listAnnotatedFiles"); + bool hasResult = resp.contains("result"); + int count = 0; + if (hasResult && resp["result"].contains("files")) { + count = (int)resp["result"]["files"].size(); + } + + expect(hasResult && count >= 1, + "listAnnotatedFiles returns " + std::to_string(count) + " file(s)", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 5: Multiple annotations survive roundtrip + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("s1", AgentRole::Refactor); + + rpc(state, "s1", "openFile", + {{"path", "multi.py"}, {"content", src}}); + + std::string fnId; + for (auto* c : state.activeAST()->allChildren()) { + if (c->conceptType == "Function" && getNodeName(c) == "greet") { + fnId = c->id; + break; + } + } + + // Add 3 annotations + rpc(state, "s1", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "IntentAnnotation"}, + {"properties", {{"summary", "greets"}, {"category", "io"}}}}}}); + rpc(state, "s1", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "RiskAnnotation"}, + {"properties", {{"level", "low"}, {"reason", "simple"}, + {"dependentCount", 1}}}}}}); + rpc(state, "s1", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "ComplexityAnnotation"}, + {"properties", {{"timeComplexity", "O(1)"}, + {"cognitiveComplexity", 1}, + {"linesOfLogic", 2}}}}}}); + + // Save + rpc(state, "s1", "saveAnnotatedAST", {{"path", "multi.py"}}); + + // Reload fresh + HeadlessEditorState state2; + state2.defaultLanguage = "python"; + state2.workspaceRoot = tmpDir.string(); + state2.setAgentRole("s1", AgentRole::Refactor); + rpc(state2, "s1", "openFile", + {{"path", "multi.py"}, {"content", src}}); + rpc(state2, "s1", "loadAnnotatedAST", {{"path", "multi.py"}}); + + int count = 0; + for (auto* c : state2.activeAST()->allChildren()) { + if (c->conceptType == "Function" && getNodeName(c) == "greet") { + for (auto* a : c->getChildren("annotations")) { + if (a->conceptType == "IntentAnnotation" || + a->conceptType == "RiskAnnotation" || + a->conceptType == "ComplexityAnnotation") + ++count; + } + } + } + + expect(count == 3, + "3 annotations survive save/load roundtrip (" + + std::to_string(count) + "/3)", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 6: Sidecar doesn't affect source code + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("s1", AgentRole::Refactor); + + rpc(state, "s1", "openFile", + {{"path", "greet.py"}, {"content", src}}); + rpc(state, "s1", "loadAnnotatedAST", {{"path", "greet.py"}}); + + // editBuf should still be the original source + bool codeClean = state.activeBuffer->editBuf.find("IntentAnnotation") == + std::string::npos; + bool hasGreet = state.activeBuffer->editBuf.find("greet") != + std::string::npos; + + expect(codeClean && hasGreet, + "Loading sidecar doesn't pollute source code in editBuf", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 7: saveAnnotatedAST on unknown buffer returns error + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("s1", AgentRole::Refactor); + + json resp = rpc(state, "s1", "saveAnnotatedAST", + {{"path", "nonexistent.py"}}); + bool hasError = resp.contains("error"); + + expect(hasError, + "saveAnnotatedAST on unknown buffer returns error", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 8: loadAnnotatedAST with no sidecar returns error + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("s1", AgentRole::Refactor); + + rpc(state, "s1", "openFile", + {{"path", "nosidecar.py"}, {"content", src}}); + + json resp = rpc(state, "s1", "loadAnnotatedAST", + {{"path", "nosidecar.py"}}); + bool hasError = resp.contains("error"); + + expect(hasError, + "loadAnnotatedAST with no sidecar file returns error", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 9: Overwrite existing sidecar + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("s1", AgentRole::Refactor); + + rpc(state, "s1", "openFile", + {{"path", "greet.py"}, {"content", src}}); + + // Add a different annotation + std::string fnId; + for (auto* c : state.activeAST()->allChildren()) { + if (c->conceptType == "Function" && getNodeName(c) == "greet") { + fnId = c->id; + break; + } + } + rpc(state, "s1", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "ContractAnnotation"}, + {"properties", {{"preconditions", "name != empty"}, + {"postconditions", "returns greeting"}, + {"returnShape", "str"}, + {"sideEffects", "none"}}}}}}); + + // Overwrite sidecar + rpc(state, "s1", "saveAnnotatedAST", {{"path", "greet.py"}}); + + // Reload and check ContractAnnotation exists + HeadlessEditorState state2; + state2.defaultLanguage = "python"; + state2.workspaceRoot = tmpDir.string(); + state2.setAgentRole("s1", AgentRole::Refactor); + rpc(state2, "s1", "openFile", + {{"path", "greet.py"}, {"content", src}}); + rpc(state2, "s1", "loadAnnotatedAST", {{"path", "greet.py"}}); + + bool hasContract = false; + for (auto* c : state2.activeAST()->allChildren()) { + if (c->conceptType == "Function") { + for (auto* a : c->getChildren("annotations")) { + if (a->conceptType == "ContractAnnotation") + hasContract = true; + } + } + } + + expect(hasContract, + "Overwritten sidecar has new ContractAnnotation", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 10: Linter cannot save annotations + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("admin", AgentRole::Refactor); + state.setAgentRole("linter", AgentRole::Linter); + + rpc(state, "admin", "openFile", + {{"path", "greet.py"}, {"content", src}}); + + json resp = rpc(state, "linter", "saveAnnotatedAST", + {{"path", "greet.py"}}); + bool denied = resp.contains("error"); + + // Linter CAN load (read-only) + json loadResp = rpc(state, "linter", "loadAnnotatedAST", + {{"path", "greet.py"}}); + bool loadOk = loadResp.contains("result"); + + expect(denied && loadOk, + "Linter denied save, allowed load", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 11: MCP tools registered + // --------------------------------------------------------------- + { + MCPServer mcp; + bool foundSave = false, foundLoad = false, foundList = false; + for (const auto& t : mcp.getTools()) { + if (t.name == "whetstone_save_annotated_ast") foundSave = true; + if (t.name == "whetstone_load_annotated_ast") foundLoad = true; + if (t.name == "whetstone_list_annotated_files") foundList = true; + } + expect(foundSave && foundLoad && foundList, + "3 sidecar MCP tools registered", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 12: Response includes annotation count and sidecar path + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("s1", AgentRole::Refactor); + + rpc(state, "s1", "openFile", + {{"path", "greet.py"}, {"content", src}}); + + std::string fnId; + for (auto* c : state.activeAST()->allChildren()) { + if (c->conceptType == "Function" && getNodeName(c) == "greet") { + fnId = c->id; + break; + } + } + rpc(state, "s1", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "IntentAnnotation"}, + {"properties", {{"summary", "test"}, + {"category", "io"}}}}}}); + + json resp = rpc(state, "s1", "saveAnnotatedAST", + {{"path", "greet.py"}}); + bool hasPath = resp["result"].contains("sidecarPath"); + bool hasCount = resp["result"].contains("annotationCount"); + int count = resp["result"].value("annotationCount", 0); + + expect(hasPath && hasCount && count >= 1, + "Response has sidecarPath and annotationCount=" + + std::to_string(count), + passed, failed); + } + + // Cleanup + std::filesystem::remove_all(tmpDir); + + std::cout << "\n=== Step 267 Results: " << passed << " passed, " + << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/editor/tests/step268_test.cpp b/editor/tests/step268_test.cpp new file mode 100644 index 0000000..87d7b70 --- /dev/null +++ b/editor/tests/step268_test.cpp @@ -0,0 +1,476 @@ +// Step 268 TDD Test: Phase 10a Integration Tests +// +// End-to-end: create annotations via applyMutation, verify they appear +// in compact AST, save sidecar, re-open file, verify auto-loaded. +#include "HeadlessEditorState.h" +#include "MCPServer.h" + +#include +#include +#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; + } +} + +static json rpc(HeadlessEditorState& state, const std::string& session, + const std::string& method, + json params = json::object()) { + json request = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", method}, {"params", params}}; + return state.processAgentRequest(request, session); +} + +int main() { + int passed = 0; + int failed = 0; + + auto tmpDir = std::filesystem::temp_directory_path() / + "whetstone_step268_test"; + std::filesystem::remove_all(tmpDir); + std::filesystem::create_directories(tmpDir); + + std::string src = + "def greet(name):\n" + " return \"hello \" + name\n" + "\n" + "def farewell(name):\n" + " return \"goodbye \" + name\n"; + + // --------------------------------------------------------------- + // Test 1: Full annotation workflow — create, verify compact, save + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("agent", AgentRole::Refactor); + + rpc(state, "agent", "openFile", + {{"path", "funcs.py"}, {"content", src}}); + + // Find both functions + std::string greetId, farewellId; + for (auto* c : state.activeAST()->allChildren()) { + if (c->conceptType == "Function") { + std::string n = getNodeName(c); + if (n == "greet") greetId = c->id; + if (n == "farewell") farewellId = c->id; + } + } + + // Annotate greet with intent + risk + rpc(state, "agent", "applyMutation", + {{"type", "insertNode"}, {"parentId", greetId}, + {"role", "annotations"}, + {"node", {{"concept", "IntentAnnotation"}, + {"properties", {{"summary", "greets user by name"}, + {"category", "io"}}}}}}); + rpc(state, "agent", "applyMutation", + {{"type", "insertNode"}, {"parentId", greetId}, + {"role", "annotations"}, + {"node", {{"concept", "RiskAnnotation"}, + {"properties", {{"level", "low"}, + {"reason", "simple string concat"}, + {"dependentCount", 3}}}}}}); + + // Annotate farewell with intent + contract + rpc(state, "agent", "applyMutation", + {{"type", "insertNode"}, {"parentId", farewellId}, + {"role", "annotations"}, + {"node", {{"concept", "IntentAnnotation"}, + {"properties", {{"summary", "says goodbye"}, + {"category", "io"}}}}}}); + rpc(state, "agent", "applyMutation", + {{"type", "insertNode"}, {"parentId", farewellId}, + {"role", "annotations"}, + {"node", {{"concept", "ContractAnnotation"}, + {"properties", {{"preconditions", "name is string"}, + {"postconditions", "returns greeting"}, + {"returnShape", "str"}, + {"sideEffects", "none"}}}}}}); + + // Verify compact AST has semantic fields + json resp = rpc(state, "agent", "getAST", {{"compact", true}}); + json nodes = resp["result"]["nodes"]; + + bool greetSemantic = false, farewellSemantic = false; + for (const auto& n : nodes) { + if (n.value("name", "") == "greet" && n.contains("semantic")) { + auto sem = n["semantic"]; + greetSemantic = sem.contains("intent") && sem.contains("risk"); + } + if (n.value("name", "") == "farewell" && n.contains("semantic")) { + auto sem = n["semantic"]; + farewellSemantic = sem.contains("intent") && + sem.contains("contract"); + } + } + + // Save sidecar + json saveResp = rpc(state, "agent", "saveAnnotatedAST", + {{"path", "funcs.py"}}); + bool saved = saveResp.contains("result") && + saveResp["result"].value("success", false); + int annoCount = saveResp["result"].value("annotationCount", 0); + + expect(greetSemantic && farewellSemantic && saved && annoCount >= 4, + "Annotate 2 functions, compact AST has semantic, sidecar saved (" + + std::to_string(annoCount) + " annotations)", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 2: Re-open file and load sidecar restores all annotations + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("agent", AgentRole::Refactor); + + rpc(state, "agent", "openFile", + {{"path", "funcs.py"}, {"content", src}}); + rpc(state, "agent", "loadAnnotatedAST", {{"path", "funcs.py"}}); + + // Count semantic annotations on both functions + int greetAnnos = 0, farewellAnnos = 0; + for (auto* c : state.activeAST()->allChildren()) { + if (c->conceptType != "Function") continue; + std::string name = getNodeName(c); + for (auto* a : c->getChildren("annotations")) { + if (isSemanticAnnotation(a->conceptType)) { + if (name == "greet") ++greetAnnos; + if (name == "farewell") ++farewellAnnos; + } + } + } + + expect(greetAnnos == 2 && farewellAnnos == 2, + "Sidecar load restores greet=" + std::to_string(greetAnnos) + + " farewell=" + std::to_string(farewellAnnos) + " annotations", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 3: Compact AST after sidecar load has semantic fields + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("agent", AgentRole::Refactor); + + rpc(state, "agent", "openFile", + {{"path", "funcs.py"}, {"content", src}}); + rpc(state, "agent", "loadAnnotatedAST", {{"path", "funcs.py"}}); + + json resp = rpc(state, "agent", "getAST", {{"compact", true}}); + json nodes = resp["result"]["nodes"]; + + int nodesWithSemantic = 0; + for (const auto& n : nodes) { + if (n.contains("semantic")) ++nodesWithSemantic; + } + + expect(nodesWithSemantic >= 2, + "Compact AST after sidecar load has " + + std::to_string(nodesWithSemantic) + " nodes with semantic fields", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 4: Annotations don't appear in generated source code + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("agent", AgentRole::Refactor); + + rpc(state, "agent", "openFile", + {{"path", "funcs.py"}, {"content", src}}); + rpc(state, "agent", "loadAnnotatedAST", {{"path", "funcs.py"}}); + + std::string code = state.activeBuffer->editBuf; + bool noAnnotationClutter = + code.find("IntentAnnotation") == std::string::npos && + code.find("RiskAnnotation") == std::string::npos && + code.find("ContractAnnotation") == std::string::npos; + bool hasOriginalCode = + code.find("def greet") != std::string::npos && + code.find("def farewell") != std::string::npos; + + expect(noAnnotationClutter && hasOriginalCode, + "Loaded annotations don't pollute generated source code", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 5: Multiple save/load cycles are idempotent + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("agent", AgentRole::Refactor); + + rpc(state, "agent", "openFile", + {{"path", "funcs.py"}, {"content", src}}); + rpc(state, "agent", "loadAnnotatedAST", {{"path", "funcs.py"}}); + + // Save again (should overwrite with same content) + rpc(state, "agent", "saveAnnotatedAST", {{"path", "funcs.py"}}); + + // Load into fresh state + HeadlessEditorState state2; + state2.defaultLanguage = "python"; + state2.workspaceRoot = tmpDir.string(); + state2.setAgentRole("agent", AgentRole::Refactor); + rpc(state2, "agent", "openFile", + {{"path", "funcs.py"}, {"content", src}}); + rpc(state2, "agent", "loadAnnotatedAST", {{"path", "funcs.py"}}); + + int totalAnnos = 0; + for (auto* c : state2.activeAST()->allChildren()) { + if (c->conceptType == "Function") { + for (auto* a : c->getChildren("annotations")) { + if (isSemanticAnnotation(a->conceptType)) + ++totalAnnos; + } + } + } + + expect(totalAnnos == 4, + "Save/load/save/load cycle preserves exactly 4 annotations (" + + std::to_string(totalAnnos) + ")", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 6: Multi-file sidecar workflow + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("agent", AgentRole::Refactor); + + std::string src2 = "def helper():\n pass\n"; + + rpc(state, "agent", "openFile", + {{"path", "funcs.py"}, {"content", src}}); + rpc(state, "agent", "openFile", + {{"path", "helper.py"}, {"content", src2}}); + + // Annotate helper + state.setActiveBuffer("helper.py"); + std::string helperId; + for (auto* c : state.activeAST()->allChildren()) { + if (c->conceptType == "Function" && getNodeName(c) == "helper") { + helperId = c->id; + break; + } + } + rpc(state, "agent", "applyMutation", + {{"type", "insertNode"}, {"parentId", helperId}, + {"role", "annotations"}, + {"node", {{"concept", "IntentAnnotation"}, + {"properties", {{"summary", "placeholder helper"}, + {"category", "initialization"}}}}}}); + + rpc(state, "agent", "saveAnnotatedAST", {{"path", "helper.py"}}); + + // List files — should show both funcs.py and helper.py + json listResp = rpc(state, "agent", "listAnnotatedFiles"); + int fileCount = listResp["result"].value("count", 0); + + expect(fileCount >= 2, + "listAnnotatedFiles returns " + std::to_string(fileCount) + + " sidecar files for multi-file project", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 7: All 5 annotation types in compact AST semantic field + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("agent", AgentRole::Refactor); + + std::string src3 = "def compute(x):\n return x * 2\n"; + rpc(state, "agent", "openFile", + {{"path", "compute.py"}, {"content", src3}}); + + std::string fnId; + for (auto* c : state.activeAST()->allChildren()) { + if (c->conceptType == "Function" && getNodeName(c) == "compute") { + fnId = c->id; + break; + } + } + + // Add all 5 annotation types + rpc(state, "agent", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "IntentAnnotation"}, + {"properties", {{"summary", "doubles input"}, + {"category", "computation"}}}}}}); + rpc(state, "agent", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "ComplexityAnnotation"}, + {"properties", {{"timeComplexity", "O(1)"}, + {"cognitiveComplexity", 1}, + {"linesOfLogic", 1}}}}}}); + rpc(state, "agent", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "RiskAnnotation"}, + {"properties", {{"level", "low"}, + {"reason", "pure function"}, + {"dependentCount", 0}}}}}}); + rpc(state, "agent", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "ContractAnnotation"}, + {"properties", {{"preconditions", "x is numeric"}, + {"postconditions", "returns x*2"}, + {"returnShape", "number"}, + {"sideEffects", "none"}}}}}}); + rpc(state, "agent", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "SemanticTagAnnotation"}, + {"properties", {{"tags", json::array({"@pure", "@math"})}}}}}}); + + json resp = rpc(state, "agent", "getAST", {{"compact", true}}); + json nodes = resp["result"]["nodes"]; + + bool allFields = false; + for (const auto& n : nodes) { + if (n.value("name", "") == "compute" && n.contains("semantic")) { + auto sem = n["semantic"]; + allFields = sem.contains("intent") && + sem.contains("complexity") && + sem.contains("risk") && + sem.contains("contract") && + sem.contains("tags"); + } + } + + expect(allFields, + "Compact AST has all 5 semantic annotation fields for compute()", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 8: Sidecar roundtrip preserves all 5 annotation types + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.workspaceRoot = tmpDir.string(); + state.setAgentRole("agent", AgentRole::Refactor); + + std::string src3 = "def compute(x):\n return x * 2\n"; + rpc(state, "agent", "openFile", + {{"path", "compute.py"}, {"content", src3}}); + + std::string fnId; + for (auto* c : state.activeAST()->allChildren()) { + if (c->conceptType == "Function" && getNodeName(c) == "compute") { + fnId = c->id; + break; + } + } + + // Add all 5 types + rpc(state, "agent", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "IntentAnnotation"}, + {"properties", {{"summary", "doubles"}, + {"category", "math"}}}}}}); + rpc(state, "agent", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "ComplexityAnnotation"}, + {"properties", {{"timeComplexity", "O(1)"}, + {"cognitiveComplexity", 1}, + {"linesOfLogic", 1}}}}}}); + rpc(state, "agent", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "RiskAnnotation"}, + {"properties", {{"level", "low"}, + {"reason", "pure"}, + {"dependentCount", 0}}}}}}); + rpc(state, "agent", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "ContractAnnotation"}, + {"properties", {{"preconditions", "x numeric"}, + {"postconditions", "returns x*2"}, + {"returnShape", "number"}, + {"sideEffects", "none"}}}}}}); + rpc(state, "agent", "applyMutation", + {{"type", "insertNode"}, {"parentId", fnId}, + {"role", "annotations"}, + {"node", {{"concept", "SemanticTagAnnotation"}, + {"properties", {{"tags", json::array({"@pure"})}}}}}}); + + // Save sidecar + rpc(state, "agent", "saveAnnotatedAST", {{"path", "compute.py"}}); + + // Load into fresh state + HeadlessEditorState state2; + state2.defaultLanguage = "python"; + state2.workspaceRoot = tmpDir.string(); + state2.setAgentRole("agent", AgentRole::Refactor); + rpc(state2, "agent", "openFile", + {{"path", "compute.py"}, {"content", src3}}); + rpc(state2, "agent", "loadAnnotatedAST", {{"path", "compute.py"}}); + + // Count annotation types + int count = 0; + bool hasIntent = false, hasComplexity = false, hasRisk = false, + hasContract = false, hasTags = false; + for (auto* c : state2.activeAST()->allChildren()) { + if (c->conceptType == "Function" && getNodeName(c) == "compute") { + for (auto* a : c->getChildren("annotations")) { + if (a->conceptType == "IntentAnnotation") { hasIntent = true; ++count; } + if (a->conceptType == "ComplexityAnnotation") { hasComplexity = true; ++count; } + if (a->conceptType == "RiskAnnotation") { hasRisk = true; ++count; } + if (a->conceptType == "ContractAnnotation") { hasContract = true; ++count; } + if (a->conceptType == "SemanticTagAnnotation") { hasTags = true; ++count; } + } + } + } + + expect(count == 5 && hasIntent && hasComplexity && hasRisk && + hasContract && hasTags, + "Sidecar roundtrip preserves all 5 annotation types (" + + std::to_string(count) + "/5)", + passed, failed); + } + + // Cleanup + std::filesystem::remove_all(tmpDir); + + std::cout << "\n=== Step 268 Results: " << passed << " passed, " + << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/editor/tests/step59_test.cpp b/editor/tests/step59_test.cpp index a26f7bb..d4589d4 100644 --- a/editor/tests/step59_test.cpp +++ b/editor/tests/step59_test.cpp @@ -217,7 +217,7 @@ int main() { WebSocketAgentServer server(std::move(transport)); // Register a custom handler that knows about "getAST" - server.setRequestHandler([](const json& request) -> json { + server.setRequestHandler([](const json& request, const std::string&) -> json { std::string method = request.at("method").get(); json response; response["jsonrpc"] = "2.0"; diff --git a/installer/linux/build.sh b/installer/linux/build.sh index fed75e6..fbe7c83 100644 --- a/installer/linux/build.sh +++ b/installer/linux/build.sh @@ -54,6 +54,45 @@ else exit 1 fi +missing_tools=() +for tool in git curl zip unzip tar; do + if ! command -v "$tool" &>/dev/null; then + missing_tools+=("$tool") + fi +done + +if ((${#missing_tools[@]} > 0)); then + echo "ERROR: Missing required tools: ${missing_tools[*]}" + echo "Install on Ubuntu/Debian: sudo apt-get install -y git curl zip unzip tar" + exit 1 +fi + +# vcpkg ports like gperf require autotools from the host system. +missing_autotools=() +for tool in autoconf autoreconf automake libtoolize; do + if ! command -v "$tool" &>/dev/null; then + missing_autotools+=("$tool") + fi +done + +if ((${#missing_autotools[@]} > 0)); then + echo "ERROR: Missing required autotools: ${missing_autotools[*]}" + echo "Install on Ubuntu/Debian: sudo apt-get install -y autoconf autoconf-archive automake libtool" + exit 1 +fi + +if ! python3 -c "import ensurepip" >/dev/null 2>&1; then + echo "ERROR: Python ensurepip is unavailable (venv creation will fail)." + echo "Install on Ubuntu/Debian: sudo apt-get install -y python3-venv python3.12-venv" + exit 1 +fi + +if ! pkg-config --exists gl; then + echo "ERROR: OpenGL development files are missing (pkg-config 'gl' not found)." + echo "Install on Ubuntu/Debian: sudo apt-get install -y libgl1-mesa-dev libglx-dev libopengl-dev" + exit 1 +fi + # --- Install system dependencies if requested --- if $USE_SYSTEM_PACKAGES; then @@ -63,8 +102,11 @@ if $USE_SYSTEM_PACKAGES; then sudo apt-get update -qq sudo apt-get install -y -qq \ build-essential cmake \ - libsdl2-dev libgl1-mesa-dev libglew-dev \ - nlohmann-json3-dev + libsdl2-dev libgl1-mesa-dev libglx-dev libopengl-dev \ + nlohmann-json3-dev \ + curl zip unzip tar \ + autoconf autoconf-archive automake libtool \ + python3-venv python3.12-venv elif command -v dnf &>/dev/null; then sudo dnf install -y \ gcc-c++ cmake \ @@ -99,8 +141,24 @@ if ! $USE_SYSTEM_PACKAGES; then if [[ -z "$VCPKG_ROOT" ]]; then step "Bootstrapping vcpkg" VCPKG_ROOT="$HOME/vcpkg" - git clone https://github.com/microsoft/vcpkg.git "$VCPKG_ROOT" - "$VCPKG_ROOT/bootstrap-vcpkg.sh" -disableMetrics + if [[ -d "$VCPKG_ROOT" ]]; then + echo " Existing vcpkg directory found at $VCPKG_ROOT" + if [[ ! -x "$VCPKG_ROOT/vcpkg" ]]; then + if [[ -d "$VCPKG_ROOT/.git" ]]; then + git -C "$VCPKG_ROOT" pull --ff-only || true + fi + if [[ -x "$VCPKG_ROOT/bootstrap-vcpkg.sh" ]]; then + "$VCPKG_ROOT/bootstrap-vcpkg.sh" -disableMetrics + else + echo "ERROR: Existing vcpkg directory is incomplete: $VCPKG_ROOT" + echo "Delete it or pass --vcpkg-root to a valid vcpkg checkout." + exit 1 + fi + fi + else + git clone https://github.com/microsoft/vcpkg.git "$VCPKG_ROOT" + "$VCPKG_ROOT/bootstrap-vcpkg.sh" -disableMetrics + fi fi echo " vcpkg: $VCPKG_ROOT" @@ -141,7 +199,8 @@ cmake "${CMAKE_ARGS[@]}" step "Building ($CONFIG)" -cmake --build "$BUILD_DIR" --config "$CONFIG" --parallel "$(nproc)" +cmake --build "$BUILD_DIR" --config "$CONFIG" --parallel "$(nproc)" \ + --target whetstone_editor orchestrator echo " Build succeeded." diff --git a/installer/linux/install.sh b/installer/linux/install.sh index 94a2c1e..30fb49d 100644 --- a/installer/linux/install.sh +++ b/installer/linux/install.sh @@ -42,7 +42,11 @@ if command -v apt-get &>/dev/null; then sudo apt-get install -y -qq \ build-essential cmake git \ libsdl2-dev libgl1-mesa-dev \ - nlohmann-json3-dev + libglx-dev libopengl-dev \ + nlohmann-json3-dev \ + curl zip unzip tar \ + autoconf autoconf-archive automake libtool \ + python3-venv python3.12-venv elif command -v dnf &>/dev/null; then sudo dnf install -y \ gcc-c++ cmake git \ @@ -69,7 +73,7 @@ if $DO_BUILD; then # Use the build script if available if [[ -x "$SCRIPT_DIR/build.sh" ]]; then - "$SCRIPT_DIR/build.sh" --config Release --system-packages + "$SCRIPT_DIR/build.sh" --config Release else mkdir -p "$BUILD_DIR" cmake -S "$EDITOR_DIR" -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release diff --git a/sprint10_plan.md b/sprint10_plan.md new file mode 100644 index 0000000..08b29c1 --- /dev/null +++ b/sprint10_plan.md @@ -0,0 +1,384 @@ +# Sprint 10 Plan: Semantic Annotation Taxonomy & Environment Layer + +## Context + +Sprint 9 delivered a complete agent-first MCP tooling layer (34 tools, 244/244 tests, steps 245-265). The agent API surface is mature: headless mode, multi-file projects, diagnostics, search/rename, undo/redo, save, compact AST, batch queries. + +Sprint 10 shifts focus to **what agents actually do with that API**: annotating code with rich semantic metadata from the full annotation taxonomy (docs/annotations/ Subjects 1-8) and modeling runtime environments. This is the core Whetstone value proposition — the semantic gap bridge between languages. + +**What's already started:** +- Steps 266-270 TDD tests written (Phase 10a-10b) +- 5 semantic annotation AST classes in Annotation.h (Intent, Complexity, Risk, Contract, SemanticTag) +- SidecarPersistence.h with save/load logic +- Semantic annotation RPC methods in HeadlessAgentRPCHandler.h +- MCP tool registrations and prompt templates in MCPServer.h +- CompactAST.h semantic summary extraction + +**What's deferred to Sprint 11:** LSP client, language-specific code actions, GUI overhaul, TUI mode, plugin system. + +--- + +## Phase 10a: Semantic Annotation Core + Sidecar Persistence (Steps 266-268) + +*TDD tests already written. Partial implementation exists.* + +### Step 266: Semantic Annotation AST Node Types (12 tests) +**Goal:** 5 new AI-oriented annotation types with JSON roundtrip and compact AST support. + +**Types:** IntentAnnotation, ComplexityAnnotation, RiskAnnotation, ContractAnnotation, SemanticTagAnnotation + +**Implementation already present in:** +- `editor/src/ast/Annotation.h` (lines 145-184) — class definitions +- `editor/src/ast/Serialization.h` — toJson/fromJson dispatch +- `editor/src/CompactAST.h` — extractSemanticSummary() + +**Work remaining:** Wire up any missing serialization, ensure all 12 tests pass. + +### Step 267: Sidecar AST Persistence (12 tests) +**Goal:** Save/load annotated ASTs as `.whetstone/.ast.json` sidecar files. + +**RPC methods:** saveAnnotatedAST, loadAnnotatedAST, listAnnotatedFiles + +**Implementation already present in:** +- `editor/src/SidecarPersistence.h` — sidecarPath(), saveSidecarAST(), loadSidecarAST() + +**Work remaining:** Wire RPC handlers in HeadlessAgentRPCHandler.h, register MCP tools (whetstone_save_annotated_ast, whetstone_load_annotated_ast, whetstone_list_annotated_files), permissions in AgentPermissionPolicy.h. + +### Step 268: Phase 10a Integration Tests (8 tests) +**Goal:** End-to-end: annotate functions → compact AST shows semantic → save sidecar → reopen → load → verify. + +**Tests cover:** multi-file sidecar, all 5 types in compact view, idempotent save/load, annotations don't pollute source output. + +--- + +## Phase 10b: Annotation Agent Interface (Steps 269-271) + +*Steps 269-270 TDD tests already written.* + +### Step 269: Annotation RPC Methods (12 tests) +**Goal:** High-level set/get/remove/discover RPCs so agents don't need low-level applyMutation calls. + +**RPC methods:** +- `setSemanticAnnotation(nodeId, type, fields)` — set or update +- `getSemanticAnnotations(nodeId?)` — query per-node or project-wide +- `removeSemanticAnnotation(nodeId, type)` — remove specific type +- `getUnannotatedNodes(type?, includeHints?)` — find gaps with static analysis hints + +**MCP tools:** whetstone_set_semantic_annotation, whetstone_get_semantic_annotations, whetstone_remove_semantic_annotation, whetstone_get_unannotated_nodes + +**Implementation partially present in HeadlessAgentRPCHandler.h and MCPServer.h.** + +### Step 270: Annotation Prompt Templates (12 tests) +**Goal:** MCP prompt templates that guide frontier models through annotation workflow. + +**Prompts:** annotate_intent, annotate_complexity, annotate_risk, annotate_contracts, annotate_full + +**Each prompt mentions relevant tools** (setSemanticAnnotation, getUnannotatedNodes, save sidecar). + +**getUnannotatedNodes hints:** bodyStatements count, sideEffectHint heuristic. + +### Step 271: Phase 10b Integration Tests (8 tests) +**Goal:** Full agent annotation session: discover unannotated → set annotations → query → remove → re-query → save sidecar. + +**Tests cover:** multi-function annotation workflow, prompt template content validation, permission enforcement across the pipeline, hint accuracy. + +--- + +## Phase 10c: Type System, Execution & Scope Annotations (Steps 272-277) + +*Implements annotation Subjects 2, 3, and 4 from docs/annotations/.* + +### Step 272: Type System Annotations — Layout & Constraints (12 tests) +**Goal:** AST nodes for type metadata that drives cross-language projection. + +**New annotation classes (from `Type System and Variance.md`):** +- `BitWidthAnnotation` — fields: width (int), e.g. 32 for Java int +- `EndianAnnotation` — fields: order ("big"|"little") +- `LayoutAnnotation` — fields: mode ("packed"|"aligned"), alignment (int) +- `NullabilityAnnotation` — fields: nullable (bool), strategy ("strict"|"nullable") +- `VarianceAnnotation` — fields: variance ("covariant"|"contravariant"|"invariant") + +**Deliverables:** Class definitions, JSON roundtrip serialization, compact AST integration, 12 tests. + +### Step 273: Type System Annotations — Identity & Mutability (12 tests) +**Goal:** Type identity and immutability depth annotations. + +**New annotation classes:** +- `IdentityAnnotation` — fields: mode ("nominal"|"structural") +- `MutAnnotation` — fields: depth ("shallow"|"deep"|"interior") +- `TypeStateAnnotation` — fields: state ("erased"|"reified") + +**Plus:** C++ generator integration — MutAnnotation drives const qualification, IdentityAnnotation adds branding comments. 8 annotation types total for Subject 2. + +### Step 274: Concurrency Annotations — Primitives & Memory Model (12 tests) +**Goal:** Execution model annotations for thread safety and hardware semantics. + +**New annotation classes (from `Exec model and Concurrency.md`):** +- `AtomicAnnotation` — fields: consistency ("seq_cst"|"relaxed"|"acquire"|"release") +- `SyncAnnotation` — fields: primitive ("monitor"|"spin"|"semaphore") +- `ThreadModelAnnotation` — fields: model ("green"|"os"|"fiber") +- `MemoryBarrierAnnotation` — fields: (marker, no extra fields) + +**C++ generator:** AtomicAnnotation → `std::atomic` with memory order, SyncAnnotation → `std::mutex`/`std::lock_guard` comments. + +### Step 275: Async, Parallelism & Error Handling Annotations (12 tests) +**Goal:** Control flow and error model annotations. + +**New annotation classes:** +- `ExecAnnotation` — fields: mode ("async"|"event"), runtimeHint (string) +- `BlockingAnnotation` — fields: kind ("io"|"compute") +- `ParallelAnnotation` — fields: kind ("data"|"task") +- `TrapAnnotation` — fields: signal (string) +- `ExceptionAnnotation` — fields: style ("checked"|"unchecked") +- `PanicAnnotation` — fields: behavior ("abort"|"unwind") + +**C++ generator:** ExecAnnotation(async) → `// async: coroutine candidate`, ParallelAnnotation(data) → `// SIMD-safe` / `#pragma omp`. + +### Step 276: Scope & Namespace Annotations (12 tests) +**Goal:** Symbol binding, closure capture, and visibility annotations. + +**New annotation classes (from `Scope & Namespace resolution.md`):** +- `BindingAnnotation` — fields: time ("static"|"dynamic") +- `LookupAnnotation` — fields: mode ("lexical"|"hoisted") +- `CaptureAnnotation` — fields: strategy ("value"|"ref"|"move") +- `VisibilityAnnotation` — fields: level ("private"|"internal"|"friend"|"public") +- `NamespaceAnnotation` — fields: style ("qualified"|"flat") +- `ScopeAnnotation` — fields: kind ("local"|"global_leaked"|"singleton") + +**C++ generator:** CaptureAnnotation → lambda capture list comment, VisibilityAnnotation → access specifier hint. + +### Step 277: Phase 10c Integration Tests (8 tests) +**Goal:** Cross-subject annotation workflows — annotate a module with type, concurrency, and scope metadata, verify compact AST, sidecar roundtrip, and C++ generator output reflects annotations. + +**Tests cover:** +1. Type annotations drive C++ const/alignment output +2. Concurrency annotations produce thread-safety comments +3. Scope annotations influence lambda capture hints +4. All Subject 2-4 annotations survive sidecar save/load +5. Compact AST includes type/exec/scope summaries +6. Mixed annotations on single function (type + concurrency + scope) +7. getSemanticAnnotations returns Subject 2-4 types +8. setSemanticAnnotation works for new types via RPC + +--- + +## Phase 10d: Shims, Optimization, Meta-Programming & Policy Annotations (Steps 278-283) + +*Implements annotation Subjects 5, 6 (gaps), 7, and 8 from docs/annotations/.* + +### Step 278: Shim & Escape Hatch Annotations (12 tests) +**Goal:** Preserve platform-specific and inexpressible constructs. + +**New annotation classes (from `Shims and Escape Hatches.md`):** +- `IntrinsicAnnotation` — fields: instruction (string), arch (string) +- `RawAnnotation` — fields: language (string), code (string) +- `CallingConvAnnotation` — fields: convention ("stdcall"|"cdecl"|"fastcall") +- `LinkAnnotation` — fields: symbolName (string), library (string) +- `ShimAnnotation` — fields: strategy ("vtable"|"trampoline"|"union_tag"|"cast") +- `PointerArithmeticAnnotation` — fields: (marker) +- `OpaqueAnnotation` — fields: reason (string) + +### Step 279: Platform & Provenance Annotations (12 tests) +**Goal:** Conditional compilation and transformation history tracking. + +**New annotation classes:** +- `TargetAnnotation` — fields: platform (string), arch (string) +- `FeatureAnnotation` — fields: flag (string), enabled (bool) +- `OriginalAnnotation` — fields: sourceCode (string), sourceLanguage (string) +- `MappingAnnotation` — fields: history (vector), transformations applied + +**C++ generator:** TargetAnnotation → `#ifdef` guards, OriginalAnnotation → preserved-source comment block. + +### Step 280: Optimization Annotation Completion (12 tests) +**Goal:** Fill gaps in Subject 6 — loop hints, data locality, hardware mapping, safety overrides. + +**New annotation classes (from `6 optimization and intent.md`):** +- `TailCallAnnotation` — fields: (marker, for TCO eligibility) +- `LoopAnnotation` — fields: hint ("unroll"|"vectorize"|"fuse"), factor (int, optional) +- `DataAnnotation` — fields: hint ("prefetch"|"restrict") +- `AlignAnnotation` — fields: bytes (int) +- `PackAnnotation` — fields: (marker) +- `BoundsCheckAnnotation` — fields: enabled (bool) +- `OverflowAnnotation` — fields: behavior ("wrap"|"saturation"|"panic") + +**C++ generator:** LoopAnnotation(vectorize) → `#pragma omp simd`, AlignAnnotation → `alignas(N)`, PackAnnotation → `#pragma pack(push, 1)`, OverflowAnnotation → wrapping arithmetic comment. + +### Step 281: Meta-Programming Annotations (12 tests) +**Goal:** Code-as-data, macro expansion tracking, reflection policies. + +**New annotation classes (from `Meta-Programming & Homoiconicity.md`):** +- `MetaAnnotation` — fields: state ("quoted"|"unquoted"), phase ("compile"|"runtime") +- `SymbolAnnotation` — fields: mode ("gensym"|"interned") +- `EvaluateAnnotation` — fields: phase ("compile_time"|"runtime") +- `TemplateAnnotation` — fields: specialization ("trait"|"monomorphize"|"erasure") +- `SyntheticAnnotation` — fields: generator (string), isStructuralRisk (bool) + +### Step 282: Strategy & Policy Annotations (12 tests) +**Goal:** Decision support — multiple valid transformations, guardrails, conflict markers. + +**New annotation classes (from `8 strategy choice and policy.md`):** +- `PolicyAnnotation` — fields: strictness ("high"|"low"), perf ("critical"|"normal"), style ("idiomatic"|"literal"), binaryStable (bool) +- `AmbiguityAnnotation` — fields: intent (string), options (vector) +- `CandidateAnnotation` — fields: inferredTypes (vector) +- `TradeoffAnnotation` — fields: reason (string), safetyCost (string), perfCost (string) +- `ChoiceAnnotation` — fields: choiceId (string), options (vector) +- `DecisionAnnotation` — fields: choiceId (string), selection (string), author (string), reason (string) + +### Step 283: Phase 10d Integration Tests (8 tests) +**Goal:** Full Subject 5-8 annotation workflow with sidecar persistence and generator output. + +**Tests cover:** +1. Shim annotations on FFI boundary function +2. OriginalAnnotation preserves legacy source through projection +3. Optimization annotations drive C++ pragma output +4. Meta-programming annotations mark macro-expanded code +5. Policy + Choice + Decision annotation workflow (strategy menu) +6. All Subject 5-8 annotations survive sidecar roundtrip +7. setSemanticAnnotation RPC works for all new types +8. Annotation taxonomy completeness check: all 8 subjects represented + +--- + +## Phase 10e: Environment Layer (Steps 284-289) + +*Implements the Environment Layer feature request from `docs/annotations/Environment Layer.md`.* + +### Step 284: EnvironmentSpec Schema & AST Node (12 tests) +**Goal:** First-class EnvironmentSpec attached to modules. + +**New class: `EnvironmentSpec` (extends ASTNode)** +Fields: +- `envId` (string) — "posix_process", "browser", "jvm", "python_cpython", "emacs_runtime" +- `envVersion` (string, optional) +- `capabilities` (vector) — required capabilities +- `constraints` (vector) — forbidden behaviors +- `scheduler` (string) — "event_loop"|"threads"|"fibers"|"coroutines"|"single_thread" +- `memory` (string) — "manual"|"raii"|"refcount"|"tracing_gc"|"region" +- `bindingTimes` (json object) — per-feature: "compile"|"link"|"load"|"runtime" +- `exceptions` (string) — "unwind"|"checked"|"typed"|"untyped" +- `ffi` (string) — "c_abi"|"dynamic_linking"|"none" + +**Deliverables:** Class definition, JSON roundtrip, attachment to Module via `setEnvironment`/`getEnvironment` RPC, compact AST env summary. + +### Step 285: Capability Vocabulary & Validation (12 tests) +**Goal:** Fixed capability vocabulary with validation and per-node capability requirements. + +**Capability vocabulary (initial set):** +`io.fs`, `io.net`, `io.stdinout`, `clock.time`, `timers`, `event_loop`, `threads`, `process`, `signals`, `modules`, `dynamic_loading`, `reflection`, `gc`, `jit`, `ui.dom`, `editor.emacs`, `host.imgui` + +**New class: `CapabilityRequirement` (extends Annotation)** +- fields: capability (string), required (bool) +- Attaches to any node that requires a specific env capability + +**Validation:** `validateCapabilities(module)` checks all CapabilityRequirements against module's EnvironmentSpec.capabilities, returns diagnostics for unmet requirements. + +### Step 286: Environment-Aware Pipeline Hooks (12 tests) +**Goal:** Transforms receive EnvironmentSpec and adjust behavior. + +**Changes to Pipeline:** +- `runPipeline` gains optional `env` parameter (or reads from module's EnvironmentSpec) +- `projectLanguage` checks env.scheduler and env.memory to choose lowering strategy +- Diagnostics flag env-incompatible annotations (e.g., `@Parallel(Data)` in single_thread env) + +**New diagnostic codes:** E05xx for environment-related issues. + +### Step 287: Environment-Aware Lowering Decisions (12 tests) +**Goal:** Concrete code generation changes driven by environment model. + +**Lowering rules:** +- `env.scheduler=event_loop` + ExecAnnotation(async) → callback-based pattern in C++ +- `env.scheduler=threads` + ExecAnnotation(async) → `std::async`/`std::future` in C++ +- `env.memory=tracing_gc` → suppress explicit deallocation in C++ output +- `env.memory=manual` → inject explicit `delete` calls +- `env.exceptions=unwind` → `try/catch` in C++; `env.exceptions=typed` → `std::expected` + +**Tests:** Same AST with different EnvironmentSpecs generates different C++ output. + +### Step 288: Host Boundary AST Nodes (12 tests) +**Goal:** Explicit nodes for host-environment interactions. + +**New AST node types:** +- `HostCall` (extends Expression) — fields: capability (string), name (string), maps to explicit env boundary +- `ScheduleTask` (extends Statement) — fields: queue ("microtask"|"macrotask"|"thread"), body (Statement children) +- `ModuleLoad` (extends Statement) — fields: moduleName (string), mode ("static"|"dynamic") + +**Serialization, compact AST, and generator support for all 3 types.** + +**C++ generator:** HostCall → capability-namespaced function call, ScheduleTask → thread/task dispatch, ModuleLoad → `#include` (static) or `dlopen` (dynamic). + +### Step 289: Phase 10e Integration Tests (8 tests) +**Goal:** End-to-end environment-aware workflow. + +**Tests cover:** +1. Set EnvironmentSpec on module, verify in compact AST +2. CapabilityRequirement diagnostic for missing capability +3. Same module + different env → different C++ output (threads vs event_loop) +4. HostCall generates correct capability-namespaced code +5. ScheduleTask + env.scheduler drives dispatch pattern +6. Environment survives sidecar save/load +7. Pipeline with env parameter applies correct lowering +8. Full workflow: set env → annotate → generate → verify output + +--- + +## Step & Test Summary + +| Phase | Steps | Tests | Theme | +|-------|-------|-------|-------| +| 10a | 266-268 | 32 | Semantic annotations + sidecar | +| 10b | 269-271 | 32 | Agent annotation interface | +| 10c | 272-277 | 80 | Type, execution, scope annotations (Subjects 2-4) | +| 10d | 278-283 | 80 | Shims, optimization, meta, policy (Subjects 5-8) | +| 10e | 284-289 | 80 | Environment layer | +| **Total** | **266-289** | **304** | | + +**MCP tool count projection:** ~42 tools (34 current + 4 sidecar + 4 semantic RPC + environment tools) + +--- + +## Key Files to Modify + +| File | Changes | +|------|---------| +| `editor/src/ast/Annotation.h` | ~50 new annotation classes across Phases 10c-10e | +| `editor/src/ast/Serialization.h` | toJson/fromJson dispatch for all new types | +| `editor/src/CompactAST.h` | Semantic summary extraction for new annotation subjects | +| `editor/src/HeadlessAgentRPCHandler.h` | New RPC methods (sidecar, env, extended setSemanticAnnotation) | +| `editor/src/AgentPermissionPolicy.h` | Permissions for new methods | +| `editor/src/MCPServer.h` | Tool/prompt registrations | +| `editor/src/SidecarPersistence.h` | Already exists, may need extensions | +| `editor/src/ast/ProjectionGenerator.h` | Visit methods for generator-relevant annotations | +| `editor/src/ast/CppGeneratorTypes.h` | Annotation-driven C++ output | +| `editor/CMakeLists.txt` | Test targets for steps 266-289 | + +**New files:** +- `editor/src/EnvironmentSpec.h` — EnvironmentSpec class, capability vocabulary, validation +- `editor/src/ast/HostBoundary.h` — HostCall, ScheduleTask, ModuleLoad AST nodes +- `editor/tests/step271_test.cpp` through `editor/tests/step289_test.cpp` + +--- + +## Verification + +After each phase: +1. Build: `cd editor/build-native && cmake .. && make -j$(nproc)` +2. Run step tests: `./step26X_test` for each step in the phase +3. Verify test counts match expected (12 per step, 8 for integration) +4. Check MCP tool count via `tools/list` in integration test + +Full sprint verification: +- All 304 tests pass across steps 266-289 +- Annotation taxonomy covers all 8 subjects from docs/annotations/ +- Environment Layer meets acceptance criteria from Environment Layer.md +- Sidecar persistence works for all annotation types +- C++ generator output reflects annotation metadata + +--- + +## Sprint 11 Preview (deferred from original Sprint 10) + +1. LSP client for headless mode (clangd, pyright, rust-analyzer) +2. Language-specific code actions (Go imports, Rust borrow fixes) +3. GUI overhaul: pin floating windows, sane defaults, usable color scheme +4. Terminal UI mode (TUI) as alternative to ImGui GUI +5. Plugin system: load language support and tools dynamically