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:
Bill
2026-02-11 15:45:31 +00:00
parent fbc7b6bce2
commit fcccaeb866
8 changed files with 586 additions and 1 deletions

View File

@@ -48,6 +48,7 @@ struct AgentPermissionPolicy {
method == "getASTSubtree" ||
method == "getASTDiff" ||
method == "getDiagnostics" ||
method == "getDiagnosticsDelta" ||
method == "getQuickFixes") {
return true;
}

View File

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

View File

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

View File

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

View File

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