2026-02-11 06:29:17 +00:00
|
|
|
#pragma once
|
|
|
|
|
// Step 248: Compact AST response format
|
|
|
|
|
//
|
|
|
|
|
// Token-efficient AST serialization for agent consumption.
|
|
|
|
|
// Provides compact mode, subtree extraction, and AST diff support.
|
|
|
|
|
|
|
|
|
|
#include "ast/ASTNode.h"
|
|
|
|
|
#include "ast/Serialization.h"
|
Steps 266-268: Phase 10a — semantic annotation core + sidecar persistence (32/32 tests)
Step 266: 5 semantic annotation AST types (Intent, Complexity, Risk,
Contract, SemanticTag) with JSON roundtrip and compact AST integration.
Step 267: Sidecar AST persistence (.whetstone/<file>.ast.json) with
save/load/list RPC methods, MCP tools, and permission enforcement.
Step 268: Phase 10a integration tests — multi-file sidecar workflow,
all 5 types in compact view, idempotent roundtrip, source isolation.
Also includes Sprint 10 plan, GUI/installer polish, and Emacs integration fixes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:01:56 +00:00
|
|
|
#include "ast/Annotation.h"
|
2026-02-11 06:29:17 +00:00
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <vector>
|
|
|
|
|
#include <map>
|
|
|
|
|
|
|
|
|
|
using json = nlohmann::json;
|
|
|
|
|
|
Steps 266-268: Phase 10a — semantic annotation core + sidecar persistence (32/32 tests)
Step 266: 5 semantic annotation AST types (Intent, Complexity, Risk,
Contract, SemanticTag) with JSON roundtrip and compact AST integration.
Step 267: Sidecar AST persistence (.whetstone/<file>.ast.json) with
save/load/list RPC methods, MCP tools, and permission enforcement.
Step 268: Phase 10a integration tests — multi-file sidecar workflow,
all 5 types in compact view, idempotent roundtrip, source isolation.
Also includes Sprint 10 plan, GUI/installer polish, and Emacs integration fixes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:01:56 +00:00
|
|
|
// --- Extract semantic annotation summary from a node's annotations ---
|
|
|
|
|
// Returns a compact JSON object with semantic fields, or null if none exist.
|
|
|
|
|
inline json extractSemanticSummary(const ASTNode* node) {
|
|
|
|
|
if (!node) return json();
|
|
|
|
|
auto annos = node->getChildren("annotations");
|
|
|
|
|
if (annos.empty()) return json();
|
|
|
|
|
json sem;
|
|
|
|
|
for (const auto* a : annos) {
|
|
|
|
|
if (a->conceptType == "IntentAnnotation") {
|
|
|
|
|
auto* ia = static_cast<const IntentAnnotation*>(a);
|
|
|
|
|
json obj;
|
|
|
|
|
if (!ia->summary.empty()) obj["summary"] = ia->summary;
|
|
|
|
|
if (!ia->category.empty()) obj["category"] = ia->category;
|
|
|
|
|
if (!obj.empty()) sem["intent"] = obj;
|
|
|
|
|
}
|
|
|
|
|
else if (a->conceptType == "ComplexityAnnotation") {
|
|
|
|
|
auto* ca = static_cast<const ComplexityAnnotation*>(a);
|
|
|
|
|
json obj;
|
|
|
|
|
if (!ca->timeComplexity.empty()) obj["time"] = ca->timeComplexity;
|
|
|
|
|
if (ca->cognitiveComplexity > 0) obj["cognitive"] = ca->cognitiveComplexity;
|
|
|
|
|
if (ca->linesOfLogic > 0) obj["lines"] = ca->linesOfLogic;
|
|
|
|
|
if (!obj.empty()) sem["complexity"] = obj;
|
|
|
|
|
}
|
|
|
|
|
else if (a->conceptType == "RiskAnnotation") {
|
|
|
|
|
auto* ra = static_cast<const RiskAnnotation*>(a);
|
|
|
|
|
json obj;
|
|
|
|
|
if (!ra->level.empty()) obj["level"] = ra->level;
|
|
|
|
|
if (!ra->reason.empty()) obj["reason"] = ra->reason;
|
|
|
|
|
if (ra->dependentCount > 0) obj["dependents"] = ra->dependentCount;
|
|
|
|
|
if (!obj.empty()) sem["risk"] = obj;
|
|
|
|
|
}
|
|
|
|
|
else if (a->conceptType == "ContractAnnotation") {
|
|
|
|
|
auto* ca = static_cast<const ContractAnnotation*>(a);
|
|
|
|
|
json obj;
|
|
|
|
|
if (!ca->preconditions.empty()) obj["pre"] = ca->preconditions;
|
|
|
|
|
if (!ca->postconditions.empty()) obj["post"] = ca->postconditions;
|
|
|
|
|
if (!ca->returnShape.empty()) obj["returns"] = ca->returnShape;
|
|
|
|
|
if (!ca->sideEffects.empty()) obj["sideEffects"] = ca->sideEffects;
|
|
|
|
|
if (!obj.empty()) sem["contract"] = obj;
|
|
|
|
|
}
|
|
|
|
|
else if (a->conceptType == "SemanticTagAnnotation") {
|
|
|
|
|
auto* ta = static_cast<const SemanticTagAnnotation*>(a);
|
|
|
|
|
if (!ta->tags.empty()) sem["tags"] = ta->tags;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return sem.empty() ? json() : sem;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 06:29:17 +00:00
|
|
|
// --- Extract a human-readable name from any AST node ---
|
|
|
|
|
inline std::string getNodeName(const ASTNode* node) {
|
|
|
|
|
if (!node) return "";
|
|
|
|
|
const auto& ct = node->conceptType;
|
|
|
|
|
if (ct == "Module")
|
|
|
|
|
return static_cast<const Module*>(node)->name;
|
|
|
|
|
if (ct == "Function")
|
|
|
|
|
return static_cast<const Function*>(node)->name;
|
|
|
|
|
if (ct == "Variable")
|
|
|
|
|
return static_cast<const Variable*>(node)->name;
|
|
|
|
|
if (ct == "Parameter")
|
|
|
|
|
return static_cast<const Parameter*>(node)->name;
|
|
|
|
|
if (ct == "FunctionCall")
|
|
|
|
|
return static_cast<const FunctionCall*>(node)->functionName;
|
|
|
|
|
if (ct == "VariableReference")
|
|
|
|
|
return static_cast<const VariableReference*>(node)->variableName;
|
|
|
|
|
if (ct == "BinaryOperation")
|
|
|
|
|
return static_cast<const BinaryOperation*>(node)->op;
|
|
|
|
|
if (ct == "UnaryOperation")
|
|
|
|
|
return static_cast<const UnaryOperation*>(node)->op;
|
|
|
|
|
if (ct == "StringLiteral")
|
|
|
|
|
return static_cast<const StringLiteral*>(node)->value;
|
|
|
|
|
if (ct == "Import")
|
|
|
|
|
return static_cast<const Import*>(node)->moduleName;
|
|
|
|
|
if (ct == "ExternalModule")
|
|
|
|
|
return static_cast<const ExternalModule*>(node)->name;
|
|
|
|
|
if (ct == "PrimitiveType")
|
|
|
|
|
return static_cast<const PrimitiveType*>(node)->kind;
|
|
|
|
|
if (ct == "CustomType")
|
|
|
|
|
return static_cast<const CustomType*>(node)->typeName;
|
|
|
|
|
if (ct == "MemberAccess")
|
|
|
|
|
return static_cast<const MemberAccess*>(node)->memberName;
|
|
|
|
|
if (ct == "TypeSignature")
|
|
|
|
|
return static_cast<const TypeSignature*>(node)->name;
|
|
|
|
|
if (ct == "ForLoop")
|
|
|
|
|
return static_cast<const ForLoop*>(node)->iteratorName;
|
|
|
|
|
if (ct == "IntegerLiteral")
|
|
|
|
|
return std::to_string(
|
|
|
|
|
static_cast<const IntegerLiteral*>(node)->value);
|
|
|
|
|
if (ct == "BooleanLiteral")
|
|
|
|
|
return static_cast<const BooleanLiteral*>(node)->value
|
|
|
|
|
? "true" : "false";
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Compact AST serialization ---
|
|
|
|
|
// Returns: {id, type, name, line, childCount, children: [child_ids]}
|
|
|
|
|
// Uses short keys and omits empty fields for minimal token usage.
|
|
|
|
|
inline json toJsonCompact(const ASTNode* node) {
|
|
|
|
|
if (!node) return json();
|
|
|
|
|
json j;
|
|
|
|
|
j["id"] = node->id;
|
|
|
|
|
j["type"] = node->conceptType;
|
|
|
|
|
std::string name = getNodeName(node);
|
|
|
|
|
if (!name.empty()) j["name"] = name;
|
|
|
|
|
if (node->hasSpan()) j["line"] = node->spanStartLine;
|
|
|
|
|
|
Steps 266-268: Phase 10a — semantic annotation core + sidecar persistence (32/32 tests)
Step 266: 5 semantic annotation AST types (Intent, Complexity, Risk,
Contract, SemanticTag) with JSON roundtrip and compact AST integration.
Step 267: Sidecar AST persistence (.whetstone/<file>.ast.json) with
save/load/list RPC methods, MCP tools, and permission enforcement.
Step 268: Phase 10a integration tests — multi-file sidecar workflow,
all 5 types in compact view, idempotent roundtrip, source isolation.
Also includes Sprint 10 plan, GUI/installer polish, and Emacs integration fixes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:01:56 +00:00
|
|
|
// Include semantic summary if annotations exist
|
|
|
|
|
json sem = extractSemanticSummary(node);
|
|
|
|
|
if (!sem.empty()) j["semantic"] = sem;
|
|
|
|
|
|
2026-02-11 06:29:17 +00:00
|
|
|
auto kids = node->allChildren();
|
|
|
|
|
if (!kids.empty()) {
|
|
|
|
|
j["childCount"] = (int)kids.size();
|
|
|
|
|
json childIds = json::array();
|
|
|
|
|
for (const auto* child : kids)
|
|
|
|
|
childIds.push_back(child->id);
|
|
|
|
|
j["children"] = childIds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return j;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compact summary: top-level nodes only (Module + direct children).
|
|
|
|
|
// Deeper nodes omitted — use getASTSubtree for detail.
|
|
|
|
|
inline json toJsonCompactSummary(const ASTNode* root) {
|
|
|
|
|
if (!root) return json::array();
|
|
|
|
|
json nodes = json::array();
|
|
|
|
|
// Root node
|
|
|
|
|
json rootJ;
|
|
|
|
|
rootJ["id"] = root->id;
|
|
|
|
|
rootJ["type"] = root->conceptType;
|
|
|
|
|
std::string rname = getNodeName(root);
|
|
|
|
|
if (!rname.empty()) rootJ["name"] = rname;
|
|
|
|
|
if (root->hasSpan()) rootJ["line"] = root->spanStartLine;
|
|
|
|
|
auto rootKids = root->allChildren();
|
|
|
|
|
if (!rootKids.empty()) rootJ["childCount"] = (int)rootKids.size();
|
|
|
|
|
nodes.push_back(rootJ);
|
|
|
|
|
// Direct children (depth 1 only — functions, imports, etc.)
|
|
|
|
|
for (const auto* child : rootKids) {
|
|
|
|
|
json cj;
|
|
|
|
|
cj["id"] = child->id;
|
|
|
|
|
cj["type"] = child->conceptType;
|
|
|
|
|
std::string cname = getNodeName(child);
|
|
|
|
|
if (!cname.empty()) cj["name"] = cname;
|
|
|
|
|
if (child->hasSpan()) cj["line"] = child->spanStartLine;
|
Steps 266-268: Phase 10a — semantic annotation core + sidecar persistence (32/32 tests)
Step 266: 5 semantic annotation AST types (Intent, Complexity, Risk,
Contract, SemanticTag) with JSON roundtrip and compact AST integration.
Step 267: Sidecar AST persistence (.whetstone/<file>.ast.json) with
save/load/list RPC methods, MCP tools, and permission enforcement.
Step 268: Phase 10a integration tests — multi-file sidecar workflow,
all 5 types in compact view, idempotent roundtrip, source isolation.
Also includes Sprint 10 plan, GUI/installer polish, and Emacs integration fixes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:01:56 +00:00
|
|
|
json csem = extractSemanticSummary(child);
|
|
|
|
|
if (!csem.empty()) cj["semantic"] = csem;
|
2026-02-11 06:29:17 +00:00
|
|
|
auto grandkids = child->allChildren();
|
|
|
|
|
if (!grandkids.empty())
|
|
|
|
|
cj["childCount"] = (int)grandkids.size();
|
|
|
|
|
nodes.push_back(cj);
|
|
|
|
|
}
|
|
|
|
|
return nodes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Collect all nodes in compact format (flat list)
|
|
|
|
|
inline json toJsonCompactTree(const ASTNode* node) {
|
|
|
|
|
if (!node) return json::array();
|
|
|
|
|
json nodes = json::array();
|
|
|
|
|
nodes.push_back(toJsonCompact(node));
|
|
|
|
|
for (const auto* child : node->allChildren()) {
|
|
|
|
|
json childNodes = toJsonCompactTree(child);
|
|
|
|
|
for (auto& cn : childNodes)
|
|
|
|
|
nodes.push_back(std::move(cn));
|
|
|
|
|
}
|
|
|
|
|
return nodes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Subtree extraction ---
|
|
|
|
|
// Returns full JSON for the subtree rooted at nodeId
|
|
|
|
|
inline json toJsonSubtree(ASTNode* root, const std::string& nodeId) {
|
|
|
|
|
ASTNode* target = findNodeById(root, nodeId);
|
|
|
|
|
if (!target) return json();
|
|
|
|
|
return toJson(target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Token estimate ---
|
|
|
|
|
// Rough estimate: characters / 4 (approximates LLM tokens)
|
|
|
|
|
inline int tokenEstimate(const json& j) {
|
|
|
|
|
std::string s = j.dump();
|
|
|
|
|
return (int)s.size() / 4;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- AST version tracking ---
|
|
|
|
|
// Stored in HeadlessBufferState, records which node IDs changed per version.
|
|
|
|
|
struct ASTVersionTracker {
|
|
|
|
|
int version = 0;
|
|
|
|
|
// version -> list of affected node IDs
|
|
|
|
|
std::map<int, std::vector<std::string>> changes;
|
|
|
|
|
|
|
|
|
|
void recordMutation(const std::vector<std::string>& affectedIds) {
|
|
|
|
|
++version;
|
|
|
|
|
changes[version] = affectedIds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get all node IDs that changed since a given version
|
|
|
|
|
std::vector<std::string> changedSince(int sinceVersion) const {
|
|
|
|
|
std::vector<std::string> result;
|
|
|
|
|
for (const auto& [v, ids] : changes) {
|
|
|
|
|
if (v > sinceVersion) {
|
|
|
|
|
for (const auto& id : ids)
|
|
|
|
|
result.push_back(id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build a diff response: full JSON for changed nodes only
|
|
|
|
|
json buildDiff(ASTNode* root, int sinceVersion) const {
|
|
|
|
|
auto changedIds = changedSince(sinceVersion);
|
|
|
|
|
json nodes = json::array();
|
|
|
|
|
for (const auto& id : changedIds) {
|
|
|
|
|
ASTNode* node = findNodeById(root, id);
|
|
|
|
|
if (node)
|
|
|
|
|
nodes.push_back(toJson(node));
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
{"sinceVersion", sinceVersion},
|
|
|
|
|
{"currentVersion", version},
|
|
|
|
|
{"changedCount", (int)nodes.size()},
|
|
|
|
|
{"nodes", nodes}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prune old entries to prevent unbounded growth
|
|
|
|
|
void pruneOlderThan(int keepVersion) {
|
|
|
|
|
auto it = changes.begin();
|
|
|
|
|
while (it != changes.end() && it->first < keepVersion)
|
|
|
|
|
it = changes.erase(it);
|
|
|
|
|
}
|
|
|
|
|
};
|