diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 7c40c96..d12c8f8 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1424,4 +1424,15 @@ target_link_libraries(step249_test PRIVATE tree_sitter_java tree_sitter_rust tree_sitter_go tree_sitter_org) +# Step 250: Structured diagnostic format +add_executable(step250_test tests/step250_test.cpp) +target_include_directories(step250_test PRIVATE src) +target_link_libraries(step250_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 5a8d430..215b4a5 100644 --- a/editor/src/AgentPermissionPolicy.h +++ b/editor/src/AgentPermissionPolicy.h @@ -46,7 +46,8 @@ struct AgentPermissionPolicy { method == "workspaceList" || method == "fileDiff" || method == "getASTSubtree" || - method == "getASTDiff") { + method == "getASTDiff" || + method == "getDiagnostics") { return true; } diff --git a/editor/src/HeadlessAgentRPCHandler.h b/editor/src/HeadlessAgentRPCHandler.h index 4fdc651..ed65f70 100644 --- a/editor/src/HeadlessAgentRPCHandler.h +++ b/editor/src/HeadlessAgentRPCHandler.h @@ -578,6 +578,33 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state, }); } + // --- getDiagnostics --- + if (method == "getDiagnostics") { + if (!AgentPermissionPolicy::canInvoke(role, method)) + return headlessRpcError(id, -32031, "Role not permitted"); + auto err = headlessRequireAST(state, id); + if (!err.is_null()) return err; + auto params = request.contains("params") ? request["params"] + : json::object(); + auto diags = collectAllDiagnostics(state.activeAST()); + // Optional severity filter + if (params.contains("severity")) { + std::string sevStr = params.value("severity", ""); + DiagnosticSeverity maxSev = severityFromStr(sevStr); + diags = filterBySeverity(diags, maxSev); + } + // Optional source filter + if (params.contains("source")) { + std::string src = params.value("source", ""); + diags = filterBySource(diags, src); + } + return headlessRpcResult(id, { + {"diagnostics", diagnosticsToJson(diags)}, + {"count", (int)diags.size()}, + {"version", state.active()->versionTracker.version} + }); + } + // --- getASTSubtree --- if (method == "getASTSubtree") { auto err = headlessRequireAST(state, id); diff --git a/editor/src/HeadlessEditorState.h b/editor/src/HeadlessEditorState.h index 6deb305..aaa4e42 100644 --- a/editor/src/HeadlessEditorState.h +++ b/editor/src/HeadlessEditorState.h @@ -24,6 +24,7 @@ #include "WorkflowRecorder.h" #include "FileOperations.h" #include "CompactAST.h" +#include "StructuredDiagnostics.h" #include "Orchestrator.h" #include "IncrementalOptimizer.h" #include "TextASTSync.h" diff --git a/editor/src/MCPServer.h b/editor/src/MCPServer.h index 544cda7..43d72bb 100644 --- a/editor/src/MCPServer.h +++ b/editor/src/MCPServer.h @@ -646,6 +646,30 @@ private: }; } + // --------------------------------------------------------------- + // Step 250: Register diagnostic tools + // --------------------------------------------------------------- + void registerDiagnosticTools() { + tools_.push_back({"whetstone_get_diagnostics", + "Get structured diagnostics for the active buffer. Combines " + "parse errors, annotation validation, and strategy violations " + "into one stream with error codes, nodeIds, and fix suggestions. " + "Filter by severity (error/warning/info/hint) or source " + "(parser/annotation/strategy).", + {{"type", "object"}, {"properties", { + {"severity", {{"type", "string"}, + {"enum", {"error", "warning", "info", "hint"}}, + {"description", "Maximum severity level to include"}}}, + {"source", {{"type", "string"}, + {"enum", {"parser", "annotation", "strategy"}}, + {"description", "Filter by diagnostic source"}}} + }}} + }); + toolHandlers_["whetstone_get_diagnostics"] = [this](const json& args) { + return callWhetstone("getDiagnostics", args); + }; + } + // --------------------------------------------------------------- // Register all tools // --------------------------------------------------------------- @@ -653,5 +677,6 @@ private: registerASTTools(); registerAnnotationTools(); registerFileTools(); + registerDiagnosticTools(); } }; diff --git a/editor/src/StructuredDiagnostics.h b/editor/src/StructuredDiagnostics.h new file mode 100644 index 0000000..15a1d83 --- /dev/null +++ b/editor/src/StructuredDiagnostics.h @@ -0,0 +1,290 @@ +#pragma once +// Step 250: Structured Diagnostics +// +// Unified diagnostic format for agent consumption. Merges tree-sitter +// parse errors, AnnotationValidator diagnostics, and StrategyValidator +// violations into a single stream with error codes, AST node references, +// and optional machine-applicable fix mutations. +// +// Error code scheme: +// E01xx — Parse errors (tree-sitter syntax) +// E02xx — Annotation validation (consistency) +// E03xx — Strategy validation (memory safety) + +#include +#include +#include +#include "ast/ASTNode.h" +#include "ast/Parser.h" +#include "AnnotationValidator.h" +#include "StrategyValidator.h" +#include "Pipeline.h" + +using json = nlohmann::json; + +// ----------------------------------------------------------------------- +// Severity levels (numeric for filtering, lower = more severe) +// ----------------------------------------------------------------------- +enum class DiagnosticSeverity { + Error = 1, + Warning = 2, + Info = 3, + Hint = 4 +}; + +inline const char* severityLabel(DiagnosticSeverity s) { + switch (s) { + case DiagnosticSeverity::Error: return "error"; + case DiagnosticSeverity::Warning: return "warning"; + case DiagnosticSeverity::Info: return "info"; + case DiagnosticSeverity::Hint: return "hint"; + } + return "info"; +} + +inline DiagnosticSeverity severityFromStr(const std::string& s) { + if (s == "error") return DiagnosticSeverity::Error; + if (s == "warning") return DiagnosticSeverity::Warning; + if (s == "hint") return DiagnosticSeverity::Hint; + return DiagnosticSeverity::Info; +} + +// ----------------------------------------------------------------------- +// StructuredDiagnostic — single unified diagnostic entry +// ----------------------------------------------------------------------- +struct StructuredDiagnostic { + std::string code; // e.g. "E0100", "E0201" + DiagnosticSeverity severity = DiagnosticSeverity::Error; + std::string nodeId; // AST node reference (empty for parse) + int line = 0; // 1-based + int col = 0; // 1-based + std::string message; + std::string source; // "parser", "annotation", "strategy" + json fix = nullptr; // optional mutation object +}; + +inline json diagnosticToJson(const StructuredDiagnostic& d) { + json j = { + {"code", d.code}, + {"severity", severityLabel(d.severity)}, + {"severityLevel", static_cast(d.severity)}, + {"nodeId", d.nodeId}, + {"line", d.line}, + {"col", d.col}, + {"message", d.message}, + {"source", d.source} + }; + if (!d.fix.is_null()) j["fix"] = d.fix; + return j; +} + +// ----------------------------------------------------------------------- +// Error code assignment +// ----------------------------------------------------------------------- + +// Parse errors: E0100 (generic syntax), E0101 (missing token) +inline std::string parseErrorCode(const std::string& message) { + if (message.find("missing") != std::string::npos || + message.find("Missing") != std::string::npos) + return "E0101"; + return "E0100"; +} + +// Annotation validation: E0200..E0203 +inline std::string annotationErrorCode(const std::string& message) { + if (message.find("Missing Intent") != std::string::npos) + return "E0200"; + if (message.find("aliased") != std::string::npos || + message.find("alias") != std::string::npos) + return "E0201"; + if (message.find("conflict") != std::string::npos || + message.find("Conflict") != std::string::npos) + return "E0202"; + return "E0203"; +} + +// Strategy validation: E0300..E0304 +inline std::string strategyErrorCode(const std::string& category) { + if (category == "use-after-free") return "E0300"; + if (category == "leak") return "E0301"; + if (category == "double-move") return "E0302"; + if (category == "aliasing") return "E0303"; + if (category == "destructor-unreachable") return "E0304"; + return "E0305"; +} + +// ----------------------------------------------------------------------- +// Fix suggestion builders +// ----------------------------------------------------------------------- + +// For annotation issues: suggest removing the problematic annotation +inline json buildAnnotationFix(const std::string& nodeId, + const std::string& message) { + if (message.find("Missing Intent") != std::string::npos) { + // Deallocate(Explicit) without dealloc point: suggest removing + return {{"type", "deleteNode"}, {"nodeId", nodeId}, + {"description", "Remove @Deallocate(Explicit) annotation"}}; + } + if (message.find("aliased") != std::string::npos) { + // Single ownership alias: suggest changing to Shared + return {{"type", "setProperty"}, {"nodeId", nodeId}, + {"property", "strategy"}, {"value", "Shared"}, + {"description", + "Change @Owner(Single) to @Owner(Shared)"}}; + } + return nullptr; +} + +// For strategy violations: suggest concrete fixes +inline json buildStrategyFix(const std::string& nodeId, + const std::string& category) { + if (category == "use-after-free") { + return {{"type", "deleteNode"}, {"nodeId", nodeId}, + {"description", + "Remove use of variable after deallocation"}}; + } + if (category == "leak") { + return {{"type", "insertNode"}, {"parentId", nodeId}, + {"role", "body"}, + {"description", + "Add deallocation call for leaked variable"}}; + } + return nullptr; +} + +// ----------------------------------------------------------------------- +// Collectors: convert each diagnostic source to unified format +// ----------------------------------------------------------------------- + +inline std::vector collectParseDiagnostics( + const std::vector& diags) { + std::vector out; + out.reserve(diags.size()); + for (const auto& d : diags) { + StructuredDiagnostic sd; + sd.code = parseErrorCode(d.message); + sd.severity = severityFromStr(d.severity); + sd.line = d.line; + sd.col = d.column; + sd.message = d.message; + sd.source = "parser"; + out.push_back(std::move(sd)); + } + return out; +} + +inline std::vector collectAnnotationDiagnostics( + const std::vector& diags) { + std::vector out; + out.reserve(diags.size()); + for (const auto& d : diags) { + StructuredDiagnostic sd; + sd.code = annotationErrorCode(d.message); + sd.severity = severityFromStr(d.severity); + sd.nodeId = d.nodeId; + sd.message = d.message; + sd.source = "annotation"; + sd.fix = buildAnnotationFix(d.nodeId, d.message); + out.push_back(std::move(sd)); + } + return out; +} + +inline std::vector collectStrategyDiagnostics( + const std::vector& violations) { + std::vector out; + out.reserve(violations.size()); + for (const auto& v : violations) { + StructuredDiagnostic sd; + sd.code = strategyErrorCode(v.category); + sd.severity = severityFromStr(v.severity); + sd.nodeId = v.nodeId; + sd.message = v.message; + sd.source = "strategy"; + sd.fix = buildStrategyFix(v.nodeId, v.category); + out.push_back(std::move(sd)); + } + return out; +} + +// ----------------------------------------------------------------------- +// collectAllDiagnostics — run the full validation pipeline on an AST +// ----------------------------------------------------------------------- +inline std::vector collectAllDiagnostics( + Module* ast) { + std::vector all; + if (!ast) return all; + + // Annotation validation + AnnotationValidator annoValidator; + auto annoDiags = annoValidator.validate(ast); + auto annoStructured = collectAnnotationDiagnostics(annoDiags); + all.insert(all.end(), annoStructured.begin(), annoStructured.end()); + + // Strategy validation (post-optimization invariants) + StrategyValidator stratValidator; + auto violations = stratValidator.validateInvariants(ast); + auto stratStructured = collectStrategyDiagnostics(violations); + all.insert(all.end(), stratStructured.begin(), stratStructured.end()); + + return all; +} + +// ----------------------------------------------------------------------- +// collectPipelineDiagnostics — from a PipelineResult +// ----------------------------------------------------------------------- +inline std::vector collectPipelineDiagnostics( + const Pipeline::PipelineResult& pr) { + std::vector all; + + auto parseDiags = collectParseDiagnostics(pr.parseDiags); + all.insert(all.end(), parseDiags.begin(), parseDiags.end()); + + auto annoDiags = collectAnnotationDiagnostics(pr.validationDiags); + all.insert(all.end(), annoDiags.begin(), annoDiags.end()); + + auto stratDiags = collectStrategyDiagnostics(pr.violations); + all.insert(all.end(), stratDiags.begin(), stratDiags.end()); + + return all; +} + +// ----------------------------------------------------------------------- +// diagnosticsToJson — convert array to JSON +// ----------------------------------------------------------------------- +inline json diagnosticsToJson( + const std::vector& diags) { + json arr = json::array(); + for (const auto& d : diags) + arr.push_back(diagnosticToJson(d)); + return arr; +} + +// ----------------------------------------------------------------------- +// filterBySeverity — keep only diagnostics at or above threshold +// ----------------------------------------------------------------------- +inline std::vector filterBySeverity( + const std::vector& diags, + DiagnosticSeverity maxSeverity) { + std::vector out; + for (const auto& d : diags) { + if (static_cast(d.severity) <= + static_cast(maxSeverity)) { + out.push_back(d); + } + } + return out; +} + +// ----------------------------------------------------------------------- +// filterBySource — keep only diagnostics from a specific source +// ----------------------------------------------------------------------- +inline std::vector filterBySource( + const std::vector& diags, + const std::string& source) { + std::vector out; + for (const auto& d : diags) { + if (d.source == source) out.push_back(d); + } + return out; +} diff --git a/editor/tests/step250_test.cpp b/editor/tests/step250_test.cpp new file mode 100644 index 0000000..834f64f --- /dev/null +++ b/editor/tests/step250_test.cpp @@ -0,0 +1,394 @@ +// Step 250 TDD Test: Structured Diagnostic Format +// +// Tests the unified diagnostic format: error codes, severity levels, +// nodeId references, fix suggestions, and filtering. Exercises parse +// diagnostics, annotation validation, and strategy validation through +// the getDiagnostics RPC method. +#include "HeadlessEditorState.h" +#include "MCPBridge.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; + } +} + +int main() { + int passed = 0; + int failed = 0; + + // --------------------------------------------------------------- + // Test 1: Valid Python → zero diagnostics + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.openBuffer("valid.py", + "def add(a, b):\n return a + b\n", "python"); + state.setAgentRole("s1", AgentRole::Refactor); + + auto diags = collectAllDiagnostics(state.activeAST()); + expect(diags.empty(), + "Valid Python produces zero diagnostics", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 2: Parse diagnostics from syntax errors have E01xx codes + // --------------------------------------------------------------- + { + Pipeline pipeline; + std::vector parseDiags; + auto mod = pipeline.parse("def broken(\n", "python", parseDiags); + + auto structured = collectParseDiagnostics(parseDiags); + bool allParse = true; + bool allHaveCode = true; + for (const auto& d : structured) { + if (d.source != "parser") allParse = false; + if (d.code.substr(0, 3) != "E01") allHaveCode = false; + } + expect(!structured.empty() && allParse && allHaveCode, + "Parse errors have E01xx codes and source='parser' (got " + + std::to_string(structured.size()) + ")", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 3: Parse diagnostics have line/col and severity + // --------------------------------------------------------------- + { + Pipeline pipeline; + std::vector parseDiags; + pipeline.parse("def broken(\n", "python", parseDiags); + + auto structured = collectParseDiagnostics(parseDiags); + bool hasLocation = !structured.empty() && + structured[0].line > 0; + bool hasSeverity = !structured.empty() && + structured[0].severity == DiagnosticSeverity::Error; + expect(hasLocation && hasSeverity, + "Parse diagnostics have line > 0 and severity=error", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 4: Annotation validation produces E02xx diagnostics + // --------------------------------------------------------------- + { + // Build AST with @Deallocate(Explicit) but no dealloc call + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.openBuffer("annotated.py", + "def cleanup(ptr):\n x = ptr\n", "python"); + state.setAgentRole("s1", AgentRole::Refactor); + + Module* ast = state.activeAST(); + // Add DeallocateAnnotation to the function node + ASTNode* fn = nullptr; + for (auto* c : ast->allChildren()) { + if (c->conceptType == "Function") { fn = c; break; } + } + if (fn) { + auto* anno = new DeallocateAnnotation(); + fn->addChild("annotations", anno); + } + + auto diags = collectAllDiagnostics(ast); + bool hasAnno = false; + bool hasE02 = false; + for (const auto& d : diags) { + if (d.source == "annotation") hasAnno = true; + if (d.code.substr(0, 3) == "E02") hasE02 = true; + } + expect(hasAnno && hasE02, + "Annotation validation produces E02xx diagnostics", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 5: Annotation diagnostics include nodeId + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.openBuffer("anno2.py", + "def cleanup(ptr):\n x = ptr\n", "python"); + state.setAgentRole("s1", AgentRole::Refactor); + + Module* ast = state.activeAST(); + ASTNode* fn = nullptr; + for (auto* c : ast->allChildren()) { + if (c->conceptType == "Function") { fn = c; break; } + } + if (fn) { + auto* anno = new DeallocateAnnotation(); + fn->addChild("annotations", anno); + } + + auto diags = collectAllDiagnostics(ast); + bool hasNodeId = false; + for (const auto& d : diags) { + if (d.source == "annotation" && !d.nodeId.empty()) + hasNodeId = true; + } + expect(hasNodeId, + "Annotation diagnostics include non-empty nodeId", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 6: Annotation diagnostics include fix suggestions + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.openBuffer("fix.py", + "def cleanup(ptr):\n x = ptr\n", "python"); + state.setAgentRole("s1", AgentRole::Refactor); + + Module* ast = state.activeAST(); + ASTNode* fn = nullptr; + for (auto* c : ast->allChildren()) { + if (c->conceptType == "Function") { fn = c; break; } + } + if (fn) { + auto* anno = new DeallocateAnnotation(); + fn->addChild("annotations", anno); + } + + auto diags = collectAllDiagnostics(ast); + bool hasFix = false; + for (const auto& d : diags) { + if (d.source == "annotation" && !d.fix.is_null()) { + hasFix = d.fix.contains("type") && + d.fix.contains("description"); + } + } + expect(hasFix, + "Annotation diagnostics include fix with type and description", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 7: getDiagnostics RPC method returns structured format + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.openBuffer("rpc.py", + "def cleanup(ptr):\n x = ptr\n", "python"); + state.setAgentRole("s1", AgentRole::Refactor); + + Module* ast = state.activeAST(); + ASTNode* fn = nullptr; + for (auto* c : ast->allChildren()) { + if (c->conceptType == "Function") { fn = c; break; } + } + if (fn) { + auto* anno = new DeallocateAnnotation(); + fn->addChild("annotations", anno); + } + + json request = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", "getDiagnostics"}, + {"params", json::object()}}; + json resp = state.processAgentRequest(request, "s1"); + + bool hasResult = resp.contains("result"); + bool hasDiags = hasResult && + resp["result"].contains("diagnostics") && + resp["result"]["diagnostics"].is_array(); + bool hasCount = hasResult && + resp["result"].contains("count"); + bool hasVersion = hasResult && + resp["result"].contains("version"); + int count = 0; + if (hasCount) count = resp["result"].value("count", 0); + + expect(hasDiags && hasCount && hasVersion && count > 0, + "getDiagnostics RPC returns diagnostics array with count=" + + std::to_string(count), + passed, failed); + } + + // --------------------------------------------------------------- + // Test 8: getDiagnostics with severity filter + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.openBuffer("filter.py", + "def cleanup(ptr):\n x = ptr\n", "python"); + state.setAgentRole("s1", AgentRole::Refactor); + + Module* ast = state.activeAST(); + ASTNode* fn = nullptr; + for (auto* c : ast->allChildren()) { + if (c->conceptType == "Function") { fn = c; break; } + } + if (fn) { + auto* anno = new DeallocateAnnotation(); + fn->addChild("annotations", anno); + } + + // Get all diagnostics + json reqAll = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", "getDiagnostics"}, + {"params", json::object()}}; + json respAll = state.processAgentRequest(reqAll, "s1"); + int allCount = 0; + if (respAll.contains("result")) + allCount = respAll["result"].value("count", 0); + + // Get only errors (hint filter would exclude nothing more) + json reqErr = {{"jsonrpc", "2.0"}, {"id", 2}, + {"method", "getDiagnostics"}, + {"params", {{"severity", "error"}}}}; + json respErr = state.processAgentRequest(reqErr, "s1"); + int errCount = 0; + if (respErr.contains("result")) + errCount = respErr["result"].value("count", 0); + + expect(allCount > 0 && errCount <= allCount, + "Severity filter works (all=" + std::to_string(allCount) + + " error=" + std::to_string(errCount) + ")", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 9: diagnosticToJson produces correct fields + // --------------------------------------------------------------- + { + StructuredDiagnostic d; + d.code = "E0200"; + d.severity = DiagnosticSeverity::Error; + d.nodeId = "func_1"; + d.line = 10; + d.col = 4; + d.message = "test message"; + d.source = "annotation"; + d.fix = {{"type", "deleteNode"}, {"nodeId", "func_1"}}; + + json j = diagnosticToJson(d); + bool hasAll = j.value("code", "") == "E0200" && + j.value("severity", "") == "error" && + j.value("severityLevel", 0) == 1 && + j.value("nodeId", "") == "func_1" && + j.value("line", 0) == 10 && + j.value("col", 0) == 4 && + j.value("message", "") == "test message" && + j.value("source", "") == "annotation" && + j.contains("fix") && + j["fix"].value("type", "") == "deleteNode"; + expect(hasAll, + "diagnosticToJson produces all required fields", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 10: MCP tools/list now includes whetstone_get_diagnostics + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.openBuffer("mcp.py", "x = 1\n", "python"); + state.setAgentRole("mcp-session", AgentRole::Refactor); + + MCPBridge bridge; + bridge.setRequestHandler([&state](const json& req) -> json { + return state.processAgentRequest(req, "mcp-session"); + }); + + json req = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", "tools/list"}}; + std::string resp = bridge.processMessage(req.dump()); + json r = json::parse(resp); + + bool found = false; + int toolCount = 0; + if (r.contains("result") && r["result"].contains("tools")) { + toolCount = (int)r["result"]["tools"].size(); + for (const auto& t : r["result"]["tools"]) { + if (t.value("name", "") == "whetstone_get_diagnostics") + found = true; + } + } + expect(found && toolCount >= 18, + "MCP tools/list includes whetstone_get_diagnostics (total " + + std::to_string(toolCount) + " tools)", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 11: collectPipelineDiagnostics merges all sources + // --------------------------------------------------------------- + { + Pipeline pipeline; + auto pr = pipeline.run( + "def broken(\n", "python", "cpp"); + auto diags = collectPipelineDiagnostics(pr); + bool hasParser = false; + for (const auto& d : diags) { + if (d.source == "parser") hasParser = true; + } + expect(hasParser, + "collectPipelineDiagnostics includes parser diagnostics", + passed, failed); + } + + // --------------------------------------------------------------- + // Test 12: getDiagnostics with source filter + // --------------------------------------------------------------- + { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.openBuffer("src.py", + "def cleanup(ptr):\n x = ptr\n", "python"); + state.setAgentRole("s1", AgentRole::Refactor); + + Module* ast = state.activeAST(); + ASTNode* fn = nullptr; + for (auto* c : ast->allChildren()) { + if (c->conceptType == "Function") { fn = c; break; } + } + if (fn) { + auto* anno = new DeallocateAnnotation(); + fn->addChild("annotations", anno); + } + + json req = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", "getDiagnostics"}, + {"params", {{"source", "annotation"}}}}; + json resp = state.processAgentRequest(req, "s1"); + bool allAnnotation = true; + if (resp.contains("result") && + resp["result"].contains("diagnostics")) { + for (const auto& d : resp["result"]["diagnostics"]) { + if (d.value("source", "") != "annotation") + allAnnotation = false; + } + } + int count = 0; + if (resp.contains("result")) + count = resp["result"].value("count", 0); + expect(allAnnotation && count > 0, + "Source filter returns only annotation diagnostics (count=" + + std::to_string(count) + ")", + passed, failed); + } + + std::cout << "\n=== Step 250 Results: " << passed << " passed, " + << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 5173ac8..5f2207b 100644 --- a/progress.md +++ b/progress.md @@ -140,3 +140,48 @@ discovery, AST queries, pipeline execution, resource reads, and file I/O. - Full MCP stack validated end-to-end: handshake → tools → resources → prompts → file ops - Compact AST achieves 97% token savings through MCP layer (3% of full size) - 17 tools, 5 resources, 4 prompts all verified with correct schemas + +--- + +## Phase 9b: Agent-Optimized Diagnostics + +### Step 250: Structured Diagnostic Format +**Status:** PASS (12/12 tests) + +Unified diagnostic format for agent consumption. Merges tree-sitter parse +errors, annotation validation diagnostics, and strategy validation violations +into a single stream with error codes, severity levels, AST node references, +and machine-applicable fix mutations. + +**Files created:** +- `editor/src/StructuredDiagnostics.h` — Unified diagnostic format: + - `StructuredDiagnostic` struct: code, severity, nodeId, line, col, message, + source, fix (optional mutation object) + - Error code scheme: E01xx (parser), E02xx (annotation), E03xx (strategy) + - Severity enum: Error(1), Warning(2), Info(3), Hint(4) + - Collectors: `collectParseDiagnostics`, `collectAnnotationDiagnostics`, + `collectStrategyDiagnostics`, `collectAllDiagnostics`, + `collectPipelineDiagnostics` + - Fix builders: suggest concrete mutations (deleteNode, setProperty, insertNode) + - Filters: `filterBySeverity`, `filterBySource` + - JSON serialization: `diagnosticToJson`, `diagnosticsToJson` +- `editor/tests/step250_test.cpp` — 12 test cases: valid code (zero diags), + parse error codes (E01xx), parse location/severity, annotation validation + (E02xx), nodeId presence, fix suggestions, getDiagnostics RPC, severity + filter, JSON field validation, MCP tool registration, pipeline diagnostics, + source filter + +**Files modified:** +- `editor/src/HeadlessEditorState.h` — include StructuredDiagnostics.h +- `editor/src/HeadlessAgentRPCHandler.h` — getDiagnostics RPC method with + optional severity and source filters +- `editor/src/AgentPermissionPolicy.h` — getDiagnostics as read-only +- `editor/src/MCPServer.h` — whetstone_get_diagnostics tool registration +- `editor/CMakeLists.txt` — step250_test target + +**Key design decisions:** +- Error codes are hierarchical: E01xx=parser, E02xx=annotation, E03xx=strategy +- Fix suggestions are concrete mutation objects (same format as applyMutation) +- Severity filtering uses numeric comparison (lower = more severe) +- Source filtering allows isolating parser vs annotation vs strategy diagnostics +- tools/list now returns 18 tools (was 17): +whetstone_get_diagnostics