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:
Bill
2026-02-11 15:36:24 +00:00
parent f858925911
commit 14c2fc218a
8 changed files with 795 additions and 1 deletions

View File

@@ -46,7 +46,8 @@ struct AgentPermissionPolicy {
method == "workspaceList" ||
method == "fileDiff" ||
method == "getASTSubtree" ||
method == "getASTDiff") {
method == "getASTDiff" ||
method == "getDiagnostics") {
return true;
}

View File

@@ -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);

View File

@@ -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"

View File

@@ -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();
}
};

View 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;
}