Step 252: diagnostic delta streaming (getDiagnosticsDelta, version tracking)
DiagnosticVersionTracker records snapshots and computes added/removed deltas between versions. Agents can efficiently check if their fix resolved an error without re-fetching all diagnostics. 21 MCP tools total. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1446,4 +1446,15 @@ target_link_libraries(step251_test PRIVATE
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go
|
||||
tree_sitter_org)
|
||||
|
||||
# Step 252: Diagnostic delta streaming
|
||||
add_executable(step252_test tests/step252_test.cpp)
|
||||
target_include_directories(step252_test PRIVATE src)
|
||||
target_link_libraries(step252_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)
|
||||
|
||||
@@ -48,6 +48,7 @@ struct AgentPermissionPolicy {
|
||||
method == "getASTSubtree" ||
|
||||
method == "getASTDiff" ||
|
||||
method == "getDiagnostics" ||
|
||||
method == "getDiagnosticsDelta" ||
|
||||
method == "getQuickFixes") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -598,13 +598,32 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
|
||||
std::string src = params.value("source", "");
|
||||
diags = filterBySource(diags, src);
|
||||
}
|
||||
// Record snapshot for delta tracking
|
||||
auto allDiagsUnfiltered = collectAllDiagnostics(state.activeAST());
|
||||
state.active()->diagTracker.recordSnapshot(allDiagsUnfiltered);
|
||||
return headlessRpcResult(id, {
|
||||
{"diagnostics", diagnosticsToJson(diags)},
|
||||
{"count", (int)diags.size()},
|
||||
{"version", state.active()->versionTracker.version}
|
||||
{"version", state.active()->diagTracker.version}
|
||||
});
|
||||
}
|
||||
|
||||
// --- getDiagnosticsDelta ---
|
||||
if (method == "getDiagnosticsDelta") {
|
||||
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();
|
||||
int sinceVersion = params.value("sinceVersion", 0);
|
||||
auto currentDiags = collectAllDiagnostics(state.activeAST());
|
||||
state.active()->diagTracker.recordSnapshot(currentDiags);
|
||||
auto delta = state.active()->diagTracker.getDelta(
|
||||
currentDiags, sinceVersion);
|
||||
return headlessRpcResult(id, deltaToJson(delta));
|
||||
}
|
||||
|
||||
// --- getQuickFixes ---
|
||||
if (method == "getQuickFixes") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
|
||||
@@ -67,6 +67,7 @@ struct HeadlessBufferState {
|
||||
Orchestrator orchestrator;
|
||||
IncrementalOptimizer incrementalOptimizer;
|
||||
ASTVersionTracker versionTracker;
|
||||
DiagnosticVersionTracker diagTracker;
|
||||
std::string language = "python";
|
||||
std::string path = "(untitled)";
|
||||
std::string editBuf;
|
||||
|
||||
@@ -669,6 +669,23 @@ private:
|
||||
return callWhetstone("getDiagnostics", args);
|
||||
};
|
||||
|
||||
// whetstone_get_diagnostics_delta
|
||||
tools_.push_back({"whetstone_get_diagnostics_delta",
|
||||
"Get only the diagnostics that changed since a given version. "
|
||||
"Returns added and removed diagnostics for efficient "
|
||||
"mutate-then-check loops. Use the version from a previous "
|
||||
"getDiagnostics or getDiagnosticsDelta response.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"sinceVersion", {{"type", "integer"},
|
||||
{"description",
|
||||
"Version number from a previous diagnostics response"}}}
|
||||
}}, {"required", {"sinceVersion"}}}
|
||||
});
|
||||
toolHandlers_["whetstone_get_diagnostics_delta"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("getDiagnosticsDelta", args);
|
||||
};
|
||||
|
||||
// whetstone_get_quick_fixes
|
||||
tools_.push_back({"whetstone_get_quick_fixes",
|
||||
"Get all applicable quick-fix actions for a node or the entire "
|
||||
|
||||
@@ -409,3 +409,115 @@ inline QuickFix findQuickFix(Module* ast, const std::string& diagCode,
|
||||
}
|
||||
return {}; // empty fix (mutation will be null)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Step 252: DiagnosticVersionTracker — track diagnostic changes
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// Key that identifies a unique diagnostic instance
|
||||
struct DiagnosticKey {
|
||||
std::string code;
|
||||
std::string nodeId;
|
||||
int line = 0;
|
||||
|
||||
bool operator==(const DiagnosticKey& o) const {
|
||||
return code == o.code && nodeId == o.nodeId && line == o.line;
|
||||
}
|
||||
bool operator<(const DiagnosticKey& o) const {
|
||||
if (code != o.code) return code < o.code;
|
||||
if (nodeId != o.nodeId) return nodeId < o.nodeId;
|
||||
return line < o.line;
|
||||
}
|
||||
};
|
||||
|
||||
inline DiagnosticKey keyFromDiagnostic(const StructuredDiagnostic& d) {
|
||||
return {d.code, d.nodeId, d.line};
|
||||
}
|
||||
|
||||
struct DiagnosticDelta {
|
||||
std::vector<StructuredDiagnostic> added;
|
||||
std::vector<StructuredDiagnostic> removed;
|
||||
int version = 0;
|
||||
};
|
||||
|
||||
class DiagnosticVersionTracker {
|
||||
public:
|
||||
int version = 0;
|
||||
|
||||
// Record current diagnostics snapshot and bump version
|
||||
void recordSnapshot(const std::vector<StructuredDiagnostic>& diags) {
|
||||
previousDiags_ = currentDiags_;
|
||||
previousVersion_ = version;
|
||||
currentDiags_ = diags;
|
||||
++version;
|
||||
}
|
||||
|
||||
// Compute delta: what changed since the given version
|
||||
DiagnosticDelta getDelta(
|
||||
const std::vector<StructuredDiagnostic>& currentDiags,
|
||||
int sinceVersion) const {
|
||||
DiagnosticDelta delta;
|
||||
delta.version = version;
|
||||
|
||||
if (sinceVersion >= version) {
|
||||
// No changes since that version
|
||||
return delta;
|
||||
}
|
||||
|
||||
// Build key sets for previous and current
|
||||
std::set<DiagnosticKey> prevKeys;
|
||||
std::map<DiagnosticKey, StructuredDiagnostic> prevMap;
|
||||
for (const auto& d : previousDiags_) {
|
||||
auto key = keyFromDiagnostic(d);
|
||||
prevKeys.insert(key);
|
||||
prevMap[key] = d;
|
||||
}
|
||||
|
||||
std::set<DiagnosticKey> currKeys;
|
||||
std::map<DiagnosticKey, StructuredDiagnostic> currMap;
|
||||
for (const auto& d : currentDiags) {
|
||||
auto key = keyFromDiagnostic(d);
|
||||
currKeys.insert(key);
|
||||
currMap[key] = d;
|
||||
}
|
||||
|
||||
// Added: in current but not in previous
|
||||
for (const auto& key : currKeys) {
|
||||
if (prevKeys.find(key) == prevKeys.end()) {
|
||||
delta.added.push_back(currMap[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Removed: in previous but not in current
|
||||
for (const auto& key : prevKeys) {
|
||||
if (currKeys.find(key) == currKeys.end()) {
|
||||
delta.removed.push_back(prevMap[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return delta;
|
||||
}
|
||||
|
||||
// Reset tracker
|
||||
void reset() {
|
||||
version = 0;
|
||||
previousVersion_ = 0;
|
||||
currentDiags_.clear();
|
||||
previousDiags_.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
int previousVersion_ = 0;
|
||||
std::vector<StructuredDiagnostic> currentDiags_;
|
||||
std::vector<StructuredDiagnostic> previousDiags_;
|
||||
};
|
||||
|
||||
inline json deltaToJson(const DiagnosticDelta& delta) {
|
||||
return {
|
||||
{"added", diagnosticsToJson(delta.added)},
|
||||
{"removed", diagnosticsToJson(delta.removed)},
|
||||
{"addedCount", (int)delta.added.size()},
|
||||
{"removedCount", (int)delta.removed.size()},
|
||||
{"version", delta.version}
|
||||
};
|
||||
}
|
||||
|
||||
392
editor/tests/step252_test.cpp
Normal file
392
editor/tests/step252_test.cpp
Normal file
@@ -0,0 +1,392 @@
|
||||
// Step 252 TDD Test: Diagnostic Delta Streaming
|
||||
//
|
||||
// Tests getDiagnosticsDelta RPC method: version tracking, added/removed
|
||||
// diagnostics after mutations, efficient delta computation, and MCP tool.
|
||||
#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: getDiagnostics returns version number
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
HeadlessEditorState state;
|
||||
state.defaultLanguage = "python";
|
||||
state.openBuffer("v.py",
|
||||
"def add(a, b):\n return a + b\n", "python");
|
||||
state.setAgentRole("s1", AgentRole::Refactor);
|
||||
|
||||
json req = {{"jsonrpc", "2.0"}, {"id", 1},
|
||||
{"method", "getDiagnostics"},
|
||||
{"params", json::object()}};
|
||||
json resp = state.processAgentRequest(req, "s1");
|
||||
|
||||
bool hasVersion = resp.contains("result") &&
|
||||
resp["result"].contains("version");
|
||||
int ver = 0;
|
||||
if (hasVersion) ver = resp["result"].value("version", -1);
|
||||
expect(hasVersion && ver >= 1,
|
||||
"getDiagnostics returns version >= 1 (got " +
|
||||
std::to_string(ver) + ")",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 2: getDiagnosticsDelta with same version → empty delta
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
HeadlessEditorState state;
|
||||
state.defaultLanguage = "python";
|
||||
state.openBuffer("d.py",
|
||||
"def add(a, b):\n return a + b\n", "python");
|
||||
state.setAgentRole("s1", AgentRole::Refactor);
|
||||
|
||||
// Get initial diagnostics to set version
|
||||
json diagReq = {{"jsonrpc", "2.0"}, {"id", 1},
|
||||
{"method", "getDiagnostics"},
|
||||
{"params", json::object()}};
|
||||
json diagResp = state.processAgentRequest(diagReq, "s1");
|
||||
int ver = diagResp["result"].value("version", 0);
|
||||
|
||||
// Get delta with same version → no changes
|
||||
json deltaReq = {{"jsonrpc", "2.0"}, {"id", 2},
|
||||
{"method", "getDiagnosticsDelta"},
|
||||
{"params", {{"sinceVersion", ver}}}};
|
||||
json deltaResp = state.processAgentRequest(deltaReq, "s1");
|
||||
|
||||
bool hasResult = deltaResp.contains("result");
|
||||
int addedCount = 0, removedCount = 0;
|
||||
if (hasResult) {
|
||||
addedCount = deltaResp["result"].value("addedCount", -1);
|
||||
removedCount = deltaResp["result"].value("removedCount", -1);
|
||||
}
|
||||
expect(hasResult && addedCount == 0 && removedCount == 0,
|
||||
"Delta with current version returns empty (added=0, removed=0)",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 3: Introduce annotation error → delta shows addition
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
HeadlessEditorState state;
|
||||
state.defaultLanguage = "python";
|
||||
state.openBuffer("intro.py",
|
||||
"def cleanup(ptr):\n x = ptr\n", "python");
|
||||
state.setAgentRole("s1", AgentRole::Refactor);
|
||||
|
||||
// Get initial diagnostics (clean state)
|
||||
json req1 = {{"jsonrpc", "2.0"}, {"id", 1},
|
||||
{"method", "getDiagnostics"},
|
||||
{"params", json::object()}};
|
||||
json resp1 = state.processAgentRequest(req1, "s1");
|
||||
int ver1 = resp1["result"].value("version", 0);
|
||||
int count1 = resp1["result"].value("count", 0);
|
||||
|
||||
// Add annotation to introduce an error
|
||||
Module* ast = state.activeAST();
|
||||
for (auto* c : ast->allChildren()) {
|
||||
if (c->conceptType == "Function") {
|
||||
auto* anno = new DeallocateAnnotation();
|
||||
c->addChild("annotations", anno);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Get delta → should show additions
|
||||
json deltaReq = {{"jsonrpc", "2.0"}, {"id", 2},
|
||||
{"method", "getDiagnosticsDelta"},
|
||||
{"params", {{"sinceVersion", ver1}}}};
|
||||
json deltaResp = state.processAgentRequest(deltaReq, "s1");
|
||||
|
||||
int addedCount = 0;
|
||||
if (deltaResp.contains("result"))
|
||||
addedCount = deltaResp["result"].value("addedCount", 0);
|
||||
expect(addedCount > 0,
|
||||
"Delta after introducing error shows additions (added=" +
|
||||
std::to_string(addedCount) + ")",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 4: Fix annotation error → delta shows removal
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
HeadlessEditorState state;
|
||||
state.defaultLanguage = "python";
|
||||
state.openBuffer("fix.py",
|
||||
"def cleanup(ptr):\n x = ptr\n", "python");
|
||||
state.setAgentRole("s1", AgentRole::Refactor);
|
||||
|
||||
// Add annotation to create an error
|
||||
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 diagnostics with the error (establishes baseline)
|
||||
json req1 = {{"jsonrpc", "2.0"}, {"id", 1},
|
||||
{"method", "getDiagnostics"},
|
||||
{"params", json::object()}};
|
||||
json resp1 = state.processAgentRequest(req1, "s1");
|
||||
int ver1 = resp1["result"].value("version", 0);
|
||||
int errCount = resp1["result"].value("count", 0);
|
||||
|
||||
// Remove the annotation to fix the error
|
||||
if (fn) {
|
||||
auto annos = fn->getChildren("annotations");
|
||||
for (auto* a : annos) {
|
||||
if (a->conceptType == "DeallocateAnnotation") {
|
||||
fn->removeChild(a);
|
||||
delete a;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get delta → should show removals
|
||||
json deltaReq = {{"jsonrpc", "2.0"}, {"id", 2},
|
||||
{"method", "getDiagnosticsDelta"},
|
||||
{"params", {{"sinceVersion", ver1}}}};
|
||||
json deltaResp = state.processAgentRequest(deltaReq, "s1");
|
||||
|
||||
int removedCount = 0;
|
||||
if (deltaResp.contains("result"))
|
||||
removedCount = deltaResp["result"].value("removedCount", 0);
|
||||
expect(errCount > 0 && removedCount > 0,
|
||||
"Delta after fix shows removals (had=" +
|
||||
std::to_string(errCount) + " removed=" +
|
||||
std::to_string(removedCount) + ")",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 5: Delta response has correct structure
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
HeadlessEditorState state;
|
||||
state.defaultLanguage = "python";
|
||||
state.openBuffer("struct.py",
|
||||
"def foo():\n pass\n", "python");
|
||||
state.setAgentRole("s1", AgentRole::Refactor);
|
||||
|
||||
json req = {{"jsonrpc", "2.0"}, {"id", 1},
|
||||
{"method", "getDiagnostics"},
|
||||
{"params", json::object()}};
|
||||
state.processAgentRequest(req, "s1");
|
||||
|
||||
json deltaReq = {{"jsonrpc", "2.0"}, {"id", 2},
|
||||
{"method", "getDiagnosticsDelta"},
|
||||
{"params", {{"sinceVersion", 0}}}};
|
||||
json resp = state.processAgentRequest(deltaReq, "s1");
|
||||
|
||||
bool hasAdded = resp.contains("result") &&
|
||||
resp["result"].contains("added") &&
|
||||
resp["result"]["added"].is_array();
|
||||
bool hasRemoved = resp.contains("result") &&
|
||||
resp["result"].contains("removed") &&
|
||||
resp["result"]["removed"].is_array();
|
||||
bool hasVersion = resp.contains("result") &&
|
||||
resp["result"].contains("version");
|
||||
bool hasCounts = resp.contains("result") &&
|
||||
resp["result"].contains("addedCount") &&
|
||||
resp["result"].contains("removedCount");
|
||||
|
||||
expect(hasAdded && hasRemoved && hasVersion && hasCounts,
|
||||
"Delta has added, removed, version, addedCount, removedCount",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 6: Version increments on each getDiagnostics call
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
HeadlessEditorState state;
|
||||
state.defaultLanguage = "python";
|
||||
state.openBuffer("ver.py",
|
||||
"def foo():\n pass\n", "python");
|
||||
state.setAgentRole("s1", AgentRole::Refactor);
|
||||
|
||||
json req = {{"jsonrpc", "2.0"}, {"id", 1},
|
||||
{"method", "getDiagnostics"},
|
||||
{"params", json::object()}};
|
||||
json r1 = state.processAgentRequest(req, "s1");
|
||||
int v1 = r1["result"].value("version", 0);
|
||||
|
||||
json r2 = state.processAgentRequest(req, "s1");
|
||||
int v2 = r2["result"].value("version", 0);
|
||||
|
||||
expect(v2 == v1 + 1,
|
||||
"Version increments: " + std::to_string(v1) +
|
||||
" → " + std::to_string(v2),
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 7: DiagnosticKey comparison works correctly
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
DiagnosticKey a{"E0200", "func_1", 10};
|
||||
DiagnosticKey b{"E0200", "func_1", 10};
|
||||
DiagnosticKey c{"E0201", "func_1", 10};
|
||||
DiagnosticKey d{"E0200", "func_2", 10};
|
||||
|
||||
expect(a == b && !(a == c) && !(a == d) && a < c && a < d,
|
||||
"DiagnosticKey equality and ordering",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 8: DiagnosticVersionTracker reset clears state
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
DiagnosticVersionTracker tracker;
|
||||
StructuredDiagnostic d;
|
||||
d.code = "E0100"; d.source = "parser"; d.line = 1;
|
||||
tracker.recordSnapshot({d});
|
||||
expect(tracker.version == 1, "version=1 before reset",
|
||||
passed, failed);
|
||||
// (extra check after reset will be in test 9)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 9: Tracker reset returns to version 0
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
DiagnosticVersionTracker tracker;
|
||||
StructuredDiagnostic d;
|
||||
d.code = "E0100"; d.source = "parser"; d.line = 1;
|
||||
tracker.recordSnapshot({d});
|
||||
tracker.reset();
|
||||
expect(tracker.version == 0,
|
||||
"Tracker reset returns to version 0",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 10: MCP tools/list includes diagnostics delta tool
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
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_delta")
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
expect(found && toolCount >= 21,
|
||||
"MCP tools/list has diagnostics_delta (total " +
|
||||
std::to_string(toolCount) + ")",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 11: Delta added diagnostics have structured format
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
HeadlessEditorState state;
|
||||
state.defaultLanguage = "python";
|
||||
state.openBuffer("fmt.py",
|
||||
"def cleanup(ptr):\n x = ptr\n", "python");
|
||||
state.setAgentRole("s1", AgentRole::Refactor);
|
||||
|
||||
// Baseline (no errors)
|
||||
json req1 = {{"jsonrpc", "2.0"}, {"id", 1},
|
||||
{"method", "getDiagnostics"},
|
||||
{"params", json::object()}};
|
||||
state.processAgentRequest(req1, "s1");
|
||||
|
||||
// Add annotation error
|
||||
for (auto* c : state.activeAST()->allChildren()) {
|
||||
if (c->conceptType == "Function") {
|
||||
c->addChild("annotations", new DeallocateAnnotation());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
json deltaReq = {{"jsonrpc", "2.0"}, {"id", 2},
|
||||
{"method", "getDiagnosticsDelta"},
|
||||
{"params", {{"sinceVersion", 0}}}};
|
||||
json resp = state.processAgentRequest(deltaReq, "s1");
|
||||
|
||||
bool validFormat = false;
|
||||
if (resp.contains("result") &&
|
||||
!resp["result"]["added"].empty()) {
|
||||
const auto& d = resp["result"]["added"][0];
|
||||
validFormat = d.contains("code") &&
|
||||
d.contains("severity") &&
|
||||
d.contains("message") &&
|
||||
d.contains("source");
|
||||
}
|
||||
expect(validFormat,
|
||||
"Delta added diagnostics have structured format",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 12: Linter role can access getDiagnosticsDelta
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
HeadlessEditorState state;
|
||||
state.defaultLanguage = "python";
|
||||
state.openBuffer("perm.py",
|
||||
"def foo():\n pass\n", "python");
|
||||
state.setAgentRole("linter", AgentRole::Linter);
|
||||
|
||||
json req = {{"jsonrpc", "2.0"}, {"id", 1},
|
||||
{"method", "getDiagnosticsDelta"},
|
||||
{"params", {{"sinceVersion", 0}}}};
|
||||
json resp = state.processAgentRequest(req, "linter");
|
||||
|
||||
bool hasResult = resp.contains("result");
|
||||
expect(hasResult,
|
||||
"Linter role can access getDiagnosticsDelta",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
std::cout << "\n=== Step 252 Results: " << passed << " passed, "
|
||||
<< failed << " failed ===\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
32
progress.md
32
progress.md
@@ -220,3 +220,35 @@ diagnostic cleared.
|
||||
- After applying, re-runs diagnostics to report if the error cleared
|
||||
- tools/list now returns 20 tools (was 18): +whetstone_get_quick_fixes,
|
||||
+whetstone_apply_quick_fix
|
||||
|
||||
### Step 252: Diagnostic Delta Streaming
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Efficient delta computation so agents only see what changed since their
|
||||
last diagnostic query. `getDiagnosticsDelta` returns added/removed diagnostics
|
||||
based on a version number, enabling fast mutate → check loops.
|
||||
|
||||
**Files created:**
|
||||
- `editor/tests/step252_test.cpp` — 12 test cases: version tracking,
|
||||
empty delta, addition detection, removal detection, response structure,
|
||||
version increment, DiagnosticKey comparison, tracker reset, MCP tool,
|
||||
structured format in delta, Linter role access
|
||||
|
||||
**Files modified:**
|
||||
- `editor/src/StructuredDiagnostics.h` — DiagnosticKey (code, nodeId, line),
|
||||
DiagnosticDelta (added, removed, version), DiagnosticVersionTracker
|
||||
(recordSnapshot, getDelta, reset), deltaToJson
|
||||
- `editor/src/HeadlessEditorState.h` — DiagnosticVersionTracker in
|
||||
HeadlessBufferState
|
||||
- `editor/src/HeadlessAgentRPCHandler.h` — getDiagnosticsDelta RPC method,
|
||||
getDiagnostics now records snapshots for delta tracking
|
||||
- `editor/src/AgentPermissionPolicy.h` — getDiagnosticsDelta as read-only
|
||||
- `editor/src/MCPServer.h` — whetstone_get_diagnostics_delta tool
|
||||
- `editor/CMakeLists.txt` — step252_test target
|
||||
|
||||
**Key design decisions:**
|
||||
- DiagnosticKey tuple (code, nodeId, line) uniquely identifies a diagnostic
|
||||
- Version bumps on each getDiagnostics/getDiagnosticsDelta call
|
||||
- Delta computed by set difference between previous and current snapshots
|
||||
- Response includes addedCount/removedCount for quick checks without parsing
|
||||
- tools/list now returns 21 tools (was 20): +whetstone_get_diagnostics_delta
|
||||
|
||||
Reference in New Issue
Block a user