Step 250: structured diagnostic format (error codes, nodeIds, fix suggestions)
Unified diagnostic stream merging parse errors (E01xx), annotation validation (E02xx), and strategy violations (E03xx) with severity levels, AST node references, and machine-applicable fix mutations. New getDiagnostics RPC method with severity/source filtering. 18 MCP tools total. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1424,4 +1424,15 @@ target_link_libraries(step249_test PRIVATE
|
|||||||
tree_sitter_java tree_sitter_rust tree_sitter_go
|
tree_sitter_java tree_sitter_rust tree_sitter_go
|
||||||
tree_sitter_org)
|
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)
|
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ struct AgentPermissionPolicy {
|
|||||||
method == "workspaceList" ||
|
method == "workspaceList" ||
|
||||||
method == "fileDiff" ||
|
method == "fileDiff" ||
|
||||||
method == "getASTSubtree" ||
|
method == "getASTSubtree" ||
|
||||||
method == "getASTDiff") {
|
method == "getASTDiff" ||
|
||||||
|
method == "getDiagnostics") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 ---
|
// --- getASTSubtree ---
|
||||||
if (method == "getASTSubtree") {
|
if (method == "getASTSubtree") {
|
||||||
auto err = headlessRequireAST(state, id);
|
auto err = headlessRequireAST(state, id);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include "WorkflowRecorder.h"
|
#include "WorkflowRecorder.h"
|
||||||
#include "FileOperations.h"
|
#include "FileOperations.h"
|
||||||
#include "CompactAST.h"
|
#include "CompactAST.h"
|
||||||
|
#include "StructuredDiagnostics.h"
|
||||||
#include "Orchestrator.h"
|
#include "Orchestrator.h"
|
||||||
#include "IncrementalOptimizer.h"
|
#include "IncrementalOptimizer.h"
|
||||||
#include "TextASTSync.h"
|
#include "TextASTSync.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
|
// Register all tools
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
@@ -653,5 +677,6 @@ private:
|
|||||||
registerASTTools();
|
registerASTTools();
|
||||||
registerAnnotationTools();
|
registerAnnotationTools();
|
||||||
registerFileTools();
|
registerFileTools();
|
||||||
|
registerDiagnosticTools();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
290
editor/src/StructuredDiagnostics.h
Normal file
290
editor/src/StructuredDiagnostics.h
Normal file
@@ -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 <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#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<int>(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<StructuredDiagnostic> collectParseDiagnostics(
|
||||||
|
const std::vector<ParseDiagnostic>& diags) {
|
||||||
|
std::vector<StructuredDiagnostic> 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<StructuredDiagnostic> collectAnnotationDiagnostics(
|
||||||
|
const std::vector<AnnotationValidator::Diagnostic>& diags) {
|
||||||
|
std::vector<StructuredDiagnostic> 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<StructuredDiagnostic> collectStrategyDiagnostics(
|
||||||
|
const std::vector<StrategyValidator::Violation>& violations) {
|
||||||
|
std::vector<StructuredDiagnostic> 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<StructuredDiagnostic> collectAllDiagnostics(
|
||||||
|
Module* ast) {
|
||||||
|
std::vector<StructuredDiagnostic> 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<StructuredDiagnostic> collectPipelineDiagnostics(
|
||||||
|
const Pipeline::PipelineResult& pr) {
|
||||||
|
std::vector<StructuredDiagnostic> 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<StructuredDiagnostic>& 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<StructuredDiagnostic> filterBySeverity(
|
||||||
|
const std::vector<StructuredDiagnostic>& diags,
|
||||||
|
DiagnosticSeverity maxSeverity) {
|
||||||
|
std::vector<StructuredDiagnostic> out;
|
||||||
|
for (const auto& d : diags) {
|
||||||
|
if (static_cast<int>(d.severity) <=
|
||||||
|
static_cast<int>(maxSeverity)) {
|
||||||
|
out.push_back(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// filterBySource — keep only diagnostics from a specific source
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
inline std::vector<StructuredDiagnostic> filterBySource(
|
||||||
|
const std::vector<StructuredDiagnostic>& diags,
|
||||||
|
const std::string& source) {
|
||||||
|
std::vector<StructuredDiagnostic> out;
|
||||||
|
for (const auto& d : diags) {
|
||||||
|
if (d.source == source) out.push_back(d);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
394
editor/tests/step250_test.cpp
Normal file
394
editor/tests/step250_test.cpp
Normal file
@@ -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 <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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<ParseDiagnostic> 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<ParseDiagnostic> 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;
|
||||||
|
}
|
||||||
45
progress.md
45
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
|
- 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)
|
- Compact AST achieves 97% token savings through MCP layer (3% of full size)
|
||||||
- 17 tools, 5 resources, 4 prompts all verified with correct schemas
|
- 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
|
||||||
|
|||||||
Reference in New Issue
Block a user