Phase 3e complete: Agent API Extend (Steps 59, 61-63)
Step 59: WebSocketAgentServer with session management, JSON-RPC routing, MockWebSocketTransport (10/10 tests) Step 61: ASTMutationAPI with setProperty, updateNode, deleteNode, insertNode, journal, OptimizationLock warning (6/6 tests) Step 62: ContextAPI with getInScopeSymbols, getCallHierarchy, getDependencyGraph (6/6 tests) Step 63: BatchMutationAPI with atomic applySequence and reverse-order rollback (5/5 tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
215
editor/src/ASTMutationAPI.h
Normal file
215
editor/src/ASTMutationAPI.h
Normal file
@@ -0,0 +1,215 @@
|
||||
#pragma once
|
||||
// Step 61: AST mutation API (extend)
|
||||
//
|
||||
// ASTMutationAPI: provides validated AST mutations with journal recording
|
||||
// and OptimizationLock awareness.
|
||||
//
|
||||
// Methods:
|
||||
// setProperty — set a single property on a node
|
||||
// updateNode — bulk property update
|
||||
// deleteNode — remove a node from its parent
|
||||
// insertNode — add a node as child of a parent in a given role
|
||||
//
|
||||
// All mutations check for OptimizationLock annotations on the target
|
||||
// node or its ancestors. If a lock is found the mutation still succeeds
|
||||
// but a warning is returned (per spec: "warn, don't reject").
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include "ast/ASTNode.h"
|
||||
#include "ast/Module.h"
|
||||
#include "ast/Function.h"
|
||||
#include "ast/Variable.h"
|
||||
#include "ast/Parameter.h"
|
||||
#include "ast/Expression.h"
|
||||
#include "ast/Annotation.h"
|
||||
|
||||
class ASTMutationAPI {
|
||||
public:
|
||||
struct MutationResult {
|
||||
bool success = false;
|
||||
std::string warning; // non-empty if a warning was produced
|
||||
std::string error; // non-empty if the mutation failed
|
||||
};
|
||||
|
||||
void setRoot(ASTNode* root) { root_ = root; }
|
||||
|
||||
// --- mutations ---
|
||||
|
||||
MutationResult setProperty(const std::string& nodeId,
|
||||
const std::string& property,
|
||||
const std::string& value) {
|
||||
MutationResult res;
|
||||
ASTNode* node = findById(root_, nodeId);
|
||||
if (!node) {
|
||||
res.error = "Node not found: " + nodeId;
|
||||
return res;
|
||||
}
|
||||
|
||||
// Lock check
|
||||
std::string lockWarning = checkLock(node);
|
||||
|
||||
if (!applyProperty(node, property, value)) {
|
||||
res.error = "Cannot set property '" + property +
|
||||
"' on " + node->conceptType;
|
||||
return res;
|
||||
}
|
||||
|
||||
// Record in journal
|
||||
journal_.push_back("setProperty " + nodeId + "." + property +
|
||||
" = " + value);
|
||||
|
||||
res.success = true;
|
||||
res.warning = lockWarning;
|
||||
return res;
|
||||
}
|
||||
|
||||
MutationResult updateNode(const std::string& nodeId,
|
||||
const std::map<std::string, std::string>& properties) {
|
||||
MutationResult res;
|
||||
ASTNode* node = findById(root_, nodeId);
|
||||
if (!node) {
|
||||
res.error = "Node not found: " + nodeId;
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string lockWarning = checkLock(node);
|
||||
|
||||
for (const auto& [prop, val] : properties) {
|
||||
if (!applyProperty(node, prop, val)) {
|
||||
res.error = "Cannot set property '" + prop +
|
||||
"' on " + node->conceptType;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
journal_.push_back("updateNode " + nodeId + " (" +
|
||||
std::to_string(properties.size()) + " properties)");
|
||||
|
||||
res.success = true;
|
||||
res.warning = lockWarning;
|
||||
return res;
|
||||
}
|
||||
|
||||
MutationResult deleteNode(const std::string& nodeId) {
|
||||
MutationResult res;
|
||||
ASTNode* node = findById(root_, nodeId);
|
||||
if (!node) {
|
||||
res.error = "Node not found: " + nodeId;
|
||||
return res;
|
||||
}
|
||||
if (!node->parent) {
|
||||
res.error = "Cannot delete root node";
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string lockWarning = checkLock(node);
|
||||
|
||||
ASTNode* par = node->parent;
|
||||
if (!par->removeChild(node)) {
|
||||
res.error = "Failed to remove node from parent";
|
||||
return res;
|
||||
}
|
||||
|
||||
journal_.push_back("deleteNode " + nodeId);
|
||||
|
||||
res.success = true;
|
||||
res.warning = lockWarning;
|
||||
return res;
|
||||
}
|
||||
|
||||
MutationResult insertNode(const std::string& parentId,
|
||||
const std::string& role,
|
||||
ASTNode* node) {
|
||||
MutationResult res;
|
||||
ASTNode* parent = findById(root_, parentId);
|
||||
if (!parent) {
|
||||
res.error = "Parent not found: " + parentId;
|
||||
return res;
|
||||
}
|
||||
if (!node) {
|
||||
res.error = "Node is null";
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string lockWarning = checkLock(parent);
|
||||
|
||||
parent->addChild(role, node);
|
||||
|
||||
journal_.push_back("insertNode " + node->id + " into " +
|
||||
parentId + "." + role);
|
||||
|
||||
res.success = true;
|
||||
res.warning = lockWarning;
|
||||
return res;
|
||||
}
|
||||
|
||||
// --- journal ---
|
||||
|
||||
std::vector<std::string> getJournalEntries() const { return journal_; }
|
||||
void clearJournal() { journal_.clear(); }
|
||||
|
||||
private:
|
||||
// Recursive node lookup
|
||||
ASTNode* findById(ASTNode* node, const std::string& nodeId) const {
|
||||
if (!node) return nullptr;
|
||||
if (node->id == nodeId) return node;
|
||||
for (auto* child : node->allChildren()) {
|
||||
auto* found = findById(child, nodeId);
|
||||
if (found) return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Walk up the ancestor chain looking for an OptimizationLock annotation.
|
||||
// Returns a warning string if locked, empty string otherwise.
|
||||
std::string checkLock(const ASTNode* node) const {
|
||||
const ASTNode* cur = node;
|
||||
while (cur) {
|
||||
for (const auto* anno : cur->getChildren("annotations")) {
|
||||
if (anno->conceptType == "OptimizationLock") {
|
||||
auto* lock = static_cast<const OptimizationLock*>(anno);
|
||||
return "Node is under OptimizationLock (locked by " +
|
||||
lock->lockedBy + ": " + lock->lockReason + ")";
|
||||
}
|
||||
}
|
||||
cur = cur->parent;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Apply a string property to a node. Returns false if the property
|
||||
// is not recognised for the node's concept type.
|
||||
bool applyProperty(ASTNode* node, const std::string& property,
|
||||
const std::string& value) const {
|
||||
const auto& ct = node->conceptType;
|
||||
if (property == "name") {
|
||||
if (ct == "Function") { static_cast<Function*>(node)->name = value; return true; }
|
||||
if (ct == "Variable") { static_cast<Variable*>(node)->name = value; return true; }
|
||||
if (ct == "Parameter") { static_cast<Parameter*>(node)->name = value; return true; }
|
||||
if (ct == "Module") { static_cast<Module*>(node)->name = value; return true; }
|
||||
}
|
||||
if (property == "strategy") {
|
||||
if (ct == "DerefStrategy") { static_cast<DerefStrategy*>(node)->strategy = value; return true; }
|
||||
if (ct == "DeallocateAnnotation") { static_cast<DeallocateAnnotation*>(node)->strategy = value; return true; }
|
||||
if (ct == "LifetimeAnnotation") { static_cast<LifetimeAnnotation*>(node)->strategy = value; return true; }
|
||||
if (ct == "ReclaimAnnotation") { static_cast<ReclaimAnnotation*>(node)->strategy = value; return true; }
|
||||
if (ct == "OwnerAnnotation") { static_cast<OwnerAnnotation*>(node)->strategy = value; return true; }
|
||||
if (ct == "AllocateAnnotation") { static_cast<AllocateAnnotation*>(node)->strategy = value; return true; }
|
||||
}
|
||||
if (property == "op") {
|
||||
if (ct == "BinaryOperation") { static_cast<BinaryOperation*>(node)->op = value; return true; }
|
||||
}
|
||||
if (property == "variableName") {
|
||||
if (ct == "VariableReference") { static_cast<VariableReference*>(node)->variableName = value; return true; }
|
||||
}
|
||||
if (property == "targetLanguage") {
|
||||
if (ct == "Module") { static_cast<Module*>(node)->targetLanguage = value; return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ASTNode* root_ = nullptr;
|
||||
std::vector<std::string> journal_;
|
||||
};
|
||||
237
editor/src/BatchMutationAPI.h
Normal file
237
editor/src/BatchMutationAPI.h
Normal file
@@ -0,0 +1,237 @@
|
||||
#pragma once
|
||||
// Step 63: Batch operations (extend)
|
||||
//
|
||||
// BatchMutationAPI: atomic all-or-nothing mutation sequences.
|
||||
//
|
||||
// applySequence(mutations) tries each mutation in order. If any
|
||||
// mutation fails, every preceding mutation is rolled back in reverse
|
||||
// order so the AST returns to its pre-batch state.
|
||||
//
|
||||
// Supported mutation types: "setProperty", "insertNode", "deleteNode".
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <algorithm>
|
||||
#include "ast/ASTNode.h"
|
||||
#include "ast/Module.h"
|
||||
#include "ast/Function.h"
|
||||
#include "ast/Variable.h"
|
||||
#include "ast/Parameter.h"
|
||||
#include "ast/Expression.h"
|
||||
|
||||
class BatchMutationAPI {
|
||||
public:
|
||||
struct Mutation {
|
||||
std::string type; // "setProperty", "insertNode", "deleteNode"
|
||||
std::string nodeId;
|
||||
std::string property; // for setProperty
|
||||
std::string value; // for setProperty
|
||||
std::string parentId; // for insertNode
|
||||
std::string role; // for insertNode
|
||||
ASTNode* newNode = nullptr; // for insertNode
|
||||
};
|
||||
|
||||
struct BatchResult {
|
||||
bool success = false;
|
||||
int appliedCount = 0;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
void setRoot(ASTNode* root) { root_ = root; }
|
||||
|
||||
BatchResult applySequence(const std::vector<Mutation>& mutations) {
|
||||
BatchResult res;
|
||||
std::vector<UndoAction> undoStack;
|
||||
|
||||
for (size_t i = 0; i < mutations.size(); ++i) {
|
||||
const auto& mut = mutations[i];
|
||||
std::string err;
|
||||
|
||||
if (mut.type == "setProperty") {
|
||||
err = applySetProperty(mut, undoStack);
|
||||
} else if (mut.type == "insertNode") {
|
||||
err = applyInsertNode(mut, undoStack);
|
||||
} else if (mut.type == "deleteNode") {
|
||||
err = applyDeleteNode(mut, undoStack);
|
||||
} else {
|
||||
err = "Unknown mutation type: " + mut.type;
|
||||
}
|
||||
|
||||
if (!err.empty()) {
|
||||
// Roll back everything applied so far
|
||||
rollback(undoStack);
|
||||
res.success = false;
|
||||
res.appliedCount = static_cast<int>(i);
|
||||
res.error = err;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// All succeeded — record in journal
|
||||
for (const auto& undo : undoStack) {
|
||||
journal_.push_back(undo.description);
|
||||
}
|
||||
|
||||
res.success = true;
|
||||
res.appliedCount = static_cast<int>(mutations.size());
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<std::string> getJournalEntries() const { return journal_; }
|
||||
void clearJournal() { journal_.clear(); }
|
||||
|
||||
private:
|
||||
// An undo action captures how to reverse a single mutation.
|
||||
struct UndoAction {
|
||||
std::function<void()> undo;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
// --- mutation implementations ---
|
||||
|
||||
std::string applySetProperty(const Mutation& mut,
|
||||
std::vector<UndoAction>& undoStack) {
|
||||
ASTNode* node = findById(root_, mut.nodeId);
|
||||
if (!node)
|
||||
return "Node not found: " + mut.nodeId;
|
||||
|
||||
// Read old value
|
||||
std::string oldValue = getStringProperty(node, mut.property);
|
||||
|
||||
if (!setStringProperty(node, mut.property, mut.value))
|
||||
return "Cannot set property '" + mut.property +
|
||||
"' on " + node->conceptType;
|
||||
|
||||
// Capture undo
|
||||
std::string nid = mut.nodeId;
|
||||
std::string prop = mut.property;
|
||||
ASTNode** rootPtr = &root_;
|
||||
undoStack.push_back({
|
||||
[node, prop, oldValue, this]() {
|
||||
setStringProperty(node, prop, oldValue);
|
||||
},
|
||||
"setProperty " + mut.nodeId + "." + mut.property + " = " + mut.value
|
||||
});
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string applyInsertNode(const Mutation& mut,
|
||||
std::vector<UndoAction>& undoStack) {
|
||||
ASTNode* parent = findById(root_, mut.parentId);
|
||||
if (!parent)
|
||||
return "Parent not found: " + mut.parentId;
|
||||
if (!mut.newNode)
|
||||
return "Node is null for insertNode";
|
||||
|
||||
parent->addChild(mut.role, mut.newNode);
|
||||
|
||||
ASTNode* inserted = mut.newNode;
|
||||
undoStack.push_back({
|
||||
[parent, inserted]() {
|
||||
parent->removeChild(inserted);
|
||||
},
|
||||
"insertNode " + inserted->id + " into " + mut.parentId + "." + mut.role
|
||||
});
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string applyDeleteNode(const Mutation& mut,
|
||||
std::vector<UndoAction>& undoStack) {
|
||||
ASTNode* node = findById(root_, mut.nodeId);
|
||||
if (!node)
|
||||
return "Node not found: " + mut.nodeId;
|
||||
if (!node->parent)
|
||||
return "Cannot delete root node";
|
||||
|
||||
ASTNode* parent = node->parent;
|
||||
std::string role = findRole(parent, node);
|
||||
if (role.empty())
|
||||
return "Could not determine role for node " + mut.nodeId;
|
||||
|
||||
if (!parent->removeChild(node))
|
||||
return "Failed to remove node from parent";
|
||||
|
||||
undoStack.push_back({
|
||||
[parent, node, role]() {
|
||||
parent->addChild(role, node);
|
||||
},
|
||||
"deleteNode " + mut.nodeId
|
||||
});
|
||||
return "";
|
||||
}
|
||||
|
||||
// --- rollback ---
|
||||
|
||||
static void rollback(std::vector<UndoAction>& undoStack) {
|
||||
for (auto it = undoStack.rbegin(); it != undoStack.rend(); ++it) {
|
||||
it->undo();
|
||||
}
|
||||
undoStack.clear();
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
ASTNode* findById(ASTNode* node, const std::string& id) const {
|
||||
if (!node) return nullptr;
|
||||
if (node->id == id) return node;
|
||||
for (auto* child : node->allChildren()) {
|
||||
auto* found = findById(child, id);
|
||||
if (found) return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Find which role a child occupies in its parent
|
||||
static std::string findRole(const ASTNode* parent, const ASTNode* child) {
|
||||
for (const auto& role : parent->childRoles()) {
|
||||
for (const auto* kid : parent->getChildren(role)) {
|
||||
if (kid == child) return role;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static std::string getStringProperty(const ASTNode* node,
|
||||
const std::string& prop) {
|
||||
const auto& ct = node->conceptType;
|
||||
if (prop == "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 == "Module") return static_cast<const Module*>(node)->name;
|
||||
}
|
||||
if (prop == "op" && ct == "BinaryOperation")
|
||||
return static_cast<const BinaryOperation*>(node)->op;
|
||||
if (prop == "variableName" && ct == "VariableReference")
|
||||
return static_cast<const VariableReference*>(node)->variableName;
|
||||
if (prop == "targetLanguage" && ct == "Module")
|
||||
return static_cast<const Module*>(node)->targetLanguage;
|
||||
return "";
|
||||
}
|
||||
|
||||
bool setStringProperty(ASTNode* node, const std::string& prop,
|
||||
const std::string& value) const {
|
||||
const auto& ct = node->conceptType;
|
||||
if (prop == "name") {
|
||||
if (ct == "Function") { static_cast<Function*>(node)->name = value; return true; }
|
||||
if (ct == "Variable") { static_cast<Variable*>(node)->name = value; return true; }
|
||||
if (ct == "Parameter") { static_cast<Parameter*>(node)->name = value; return true; }
|
||||
if (ct == "Module") { static_cast<Module*>(node)->name = value; return true; }
|
||||
}
|
||||
if (prop == "op" && ct == "BinaryOperation") {
|
||||
static_cast<BinaryOperation*>(node)->op = value; return true;
|
||||
}
|
||||
if (prop == "variableName" && ct == "VariableReference") {
|
||||
static_cast<VariableReference*>(node)->variableName = value; return true;
|
||||
}
|
||||
if (prop == "targetLanguage" && ct == "Module") {
|
||||
static_cast<Module*>(node)->targetLanguage = value; return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ASTNode* root_ = nullptr;
|
||||
std::vector<std::string> journal_;
|
||||
};
|
||||
261
editor/src/ContextAPI.h
Normal file
261
editor/src/ContextAPI.h
Normal file
@@ -0,0 +1,261 @@
|
||||
#pragma once
|
||||
// Step 62: Context API
|
||||
//
|
||||
// ContextAPI: provides scope, call-hierarchy, and data-flow queries
|
||||
// over the SemAnno AST.
|
||||
//
|
||||
// Methods:
|
||||
// getInScopeSymbols — variables/functions/parameters visible at a node
|
||||
// getCallHierarchy — callers and callees of a function
|
||||
// getDependencyGraph — data-flow dependencies of a node (via VariableRef)
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include "ast/ASTNode.h"
|
||||
#include "ast/Module.h"
|
||||
#include "ast/Function.h"
|
||||
#include "ast/Variable.h"
|
||||
#include "ast/Parameter.h"
|
||||
#include "ast/Expression.h"
|
||||
|
||||
class ContextAPI {
|
||||
public:
|
||||
struct Symbol {
|
||||
std::string name;
|
||||
std::string kind; // "function", "variable", "parameter"
|
||||
std::string nodeId;
|
||||
};
|
||||
|
||||
struct CallInfo {
|
||||
std::string functionId;
|
||||
std::string functionName;
|
||||
std::vector<std::string> callerIds; // functions that call this one
|
||||
std::vector<std::string> calleeIds; // functions called by this one
|
||||
};
|
||||
|
||||
void setRoot(ASTNode* root) { root_ = root; }
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// getInScopeSymbols — walk up from nodeId, collecting symbols
|
||||
// ---------------------------------------------------------------
|
||||
std::vector<Symbol> getInScopeSymbols(const std::string& nodeId) const {
|
||||
ASTNode* node = findById(root_, nodeId);
|
||||
if (!node) return {};
|
||||
|
||||
std::vector<Symbol> symbols;
|
||||
|
||||
// Walk up the ancestor chain (including node itself)
|
||||
const ASTNode* cur = node;
|
||||
while (cur) {
|
||||
if (cur->conceptType == "Function") {
|
||||
auto* fn = static_cast<const Function*>(cur);
|
||||
// Parameters
|
||||
for (auto* p : fn->getChildren("parameters")) {
|
||||
if (p->conceptType == "Parameter") {
|
||||
auto* param = static_cast<const Parameter*>(p);
|
||||
addSymbolUnique(symbols, {param->name, "parameter", param->id});
|
||||
}
|
||||
}
|
||||
// Body variables
|
||||
for (auto* child : fn->getChildren("body")) {
|
||||
if (child->conceptType == "Variable") {
|
||||
auto* v = static_cast<const Variable*>(child);
|
||||
addSymbolUnique(symbols, {v->name, "variable", v->id});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (cur->conceptType == "Module") {
|
||||
auto* mod = static_cast<const Module*>(cur);
|
||||
// Module-level variables
|
||||
for (auto* child : mod->getChildren("variables")) {
|
||||
if (child->conceptType == "Variable") {
|
||||
auto* v = static_cast<const Variable*>(child);
|
||||
addSymbolUnique(symbols, {v->name, "variable", v->id});
|
||||
}
|
||||
}
|
||||
// Module-level functions
|
||||
for (auto* child : mod->getChildren("functions")) {
|
||||
if (child->conceptType == "Function") {
|
||||
auto* fn = static_cast<const Function*>(child);
|
||||
addSymbolUnique(symbols, {fn->name, "function", fn->id});
|
||||
}
|
||||
}
|
||||
}
|
||||
cur = cur->parent;
|
||||
}
|
||||
|
||||
return symbols;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// getCallHierarchy
|
||||
// ---------------------------------------------------------------
|
||||
CallInfo getCallHierarchy(const std::string& functionId) const {
|
||||
CallInfo info;
|
||||
ASTNode* fnNode = findById(root_, functionId);
|
||||
if (!fnNode || fnNode->conceptType != "Function") return info;
|
||||
|
||||
auto* fn = static_cast<Function*>(fnNode);
|
||||
info.functionId = fn->id;
|
||||
info.functionName = fn->name;
|
||||
|
||||
// Collect ALL FunctionCall nodes in the entire tree
|
||||
std::vector<ASTNode*> allCalls;
|
||||
collectByType(root_, "FunctionCall", allCalls);
|
||||
|
||||
// callerIds: functions that contain a call to fn->name
|
||||
// calleeIds: functions called from within fn
|
||||
for (auto* callNode : allCalls) {
|
||||
auto* fc = static_cast<FunctionCall*>(callNode);
|
||||
|
||||
if (fc->functionName == fn->name) {
|
||||
// Someone calls our function — find the enclosing Function
|
||||
Function* caller = enclosingFunction(callNode);
|
||||
if (caller && caller->id != fn->id) {
|
||||
addUnique(info.callerIds, caller->id);
|
||||
}
|
||||
}
|
||||
|
||||
// Is this call inside our function?
|
||||
if (isDescendantOf(callNode, fnNode)) {
|
||||
// Resolve callee function ID by name
|
||||
std::string calleeId = resolveFunctionId(fc->functionName);
|
||||
if (!calleeId.empty()) {
|
||||
addUnique(info.calleeIds, calleeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// getDependencyGraph — data-flow deps via VariableReference
|
||||
// ---------------------------------------------------------------
|
||||
std::vector<std::string> getDependencyGraph(const std::string& nodeId) const {
|
||||
ASTNode* node = findById(root_, nodeId);
|
||||
if (!node) return {};
|
||||
|
||||
// Collect all VariableReference nodes in the subtree
|
||||
std::vector<ASTNode*> refs;
|
||||
collectByType(node, "VariableReference", refs);
|
||||
|
||||
std::vector<std::string> deps;
|
||||
for (auto* refNode : refs) {
|
||||
auto* vr = static_cast<VariableReference*>(refNode);
|
||||
// Resolve to the declaration node (Variable or Parameter)
|
||||
std::string declId = resolveSymbolId(vr->variableName, refNode);
|
||||
if (!declId.empty()) {
|
||||
addUnique(deps, declId);
|
||||
}
|
||||
}
|
||||
return deps;
|
||||
}
|
||||
|
||||
private:
|
||||
// --- helpers ---
|
||||
|
||||
ASTNode* findById(ASTNode* node, const std::string& id) const {
|
||||
if (!node) return nullptr;
|
||||
if (node->id == id) return node;
|
||||
for (auto* child : node->allChildren()) {
|
||||
auto* found = findById(child, id);
|
||||
if (found) return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void collectByType(ASTNode* node, const std::string& type,
|
||||
std::vector<ASTNode*>& out) {
|
||||
if (!node) return;
|
||||
if (node->conceptType == type) out.push_back(node);
|
||||
for (auto* child : node->allChildren()) {
|
||||
collectByType(child, type, out);
|
||||
}
|
||||
}
|
||||
|
||||
// Walk up to find the nearest enclosing Function
|
||||
static Function* enclosingFunction(ASTNode* node) {
|
||||
ASTNode* cur = node->parent;
|
||||
while (cur) {
|
||||
if (cur->conceptType == "Function")
|
||||
return static_cast<Function*>(cur);
|
||||
cur = cur->parent;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check if child is a descendant of ancestor
|
||||
static bool isDescendantOf(const ASTNode* child, const ASTNode* ancestor) {
|
||||
const ASTNode* cur = child->parent;
|
||||
while (cur) {
|
||||
if (cur == ancestor) return true;
|
||||
cur = cur->parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve a function name to its node ID (searching module-level functions)
|
||||
std::string resolveFunctionId(const std::string& name) const {
|
||||
if (!root_) return "";
|
||||
// Find the module (root or first module child)
|
||||
const ASTNode* mod = root_;
|
||||
if (mod->conceptType != "Module") return "";
|
||||
|
||||
for (auto* child : mod->getChildren("functions")) {
|
||||
if (child->conceptType == "Function") {
|
||||
auto* fn = static_cast<const Function*>(child);
|
||||
if (fn->name == name) return fn->id;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Resolve a variable/parameter name to its declaration ID,
|
||||
// searching up the scope chain from refNode.
|
||||
std::string resolveSymbolId(const std::string& name,
|
||||
const ASTNode* refNode) const {
|
||||
const ASTNode* cur = refNode;
|
||||
while (cur) {
|
||||
if (cur->conceptType == "Function") {
|
||||
auto* fn = static_cast<const Function*>(cur);
|
||||
for (auto* p : fn->getChildren("parameters")) {
|
||||
if (p->conceptType == "Parameter" &&
|
||||
static_cast<const Parameter*>(p)->name == name)
|
||||
return p->id;
|
||||
}
|
||||
for (auto* child : fn->getChildren("body")) {
|
||||
if (child->conceptType == "Variable" &&
|
||||
static_cast<const Variable*>(child)->name == name)
|
||||
return child->id;
|
||||
}
|
||||
}
|
||||
else if (cur->conceptType == "Module") {
|
||||
for (auto* child : cur->getChildren("variables")) {
|
||||
if (child->conceptType == "Variable" &&
|
||||
static_cast<const Variable*>(child)->name == name)
|
||||
return child->id;
|
||||
}
|
||||
}
|
||||
cur = cur->parent;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static void addSymbolUnique(std::vector<Symbol>& symbols,
|
||||
const Symbol& sym) {
|
||||
for (const auto& s : symbols) {
|
||||
if (s.nodeId == sym.nodeId) return;
|
||||
}
|
||||
symbols.push_back(sym);
|
||||
}
|
||||
|
||||
static void addUnique(std::vector<std::string>& vec,
|
||||
const std::string& val) {
|
||||
if (std::find(vec.begin(), vec.end(), val) == vec.end())
|
||||
vec.push_back(val);
|
||||
}
|
||||
|
||||
ASTNode* root_ = nullptr;
|
||||
};
|
||||
359
editor/src/WebSocketServer.h
Normal file
359
editor/src/WebSocketServer.h
Normal file
@@ -0,0 +1,359 @@
|
||||
#pragma once
|
||||
// Step 59: WebSocket agent endpoint
|
||||
//
|
||||
// WebSocketAgentServer: manages agent sessions and routes JSON-RPC messages.
|
||||
// Agents connect via WebSocket and exchange JSON-RPC over the connection.
|
||||
//
|
||||
// Architecture:
|
||||
// WebSocketTransport (abstract) — handles raw WebSocket I/O
|
||||
// WebSocketAgentServer — session management + JSON-RPC dispatch
|
||||
// MockWebSocketTransport — for testing without real sockets
|
||||
//
|
||||
// The server runs alongside the existing stdin/stdout JSON-RPC in
|
||||
// orchestrator_main.cpp. Both share the same processRequest() handler
|
||||
// via the setRequestHandler() callback.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AgentSession — represents one connected agent
|
||||
// ---------------------------------------------------------------------------
|
||||
struct AgentSession {
|
||||
std::string sessionId;
|
||||
std::string agentName; // optional, set via setAgentName RPC
|
||||
bool connected = false;
|
||||
uint64_t connectedAtMs = 0;
|
||||
uint64_t lastMessageAtMs = 0;
|
||||
int messageCount = 0;
|
||||
};
|
||||
|
||||
// Callback types
|
||||
using JsonRpcHandler = std::function<json(const json& request)>;
|
||||
using SessionEventCallback =
|
||||
std::function<void(const AgentSession& session, const std::string& event)>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WebSocketTransport — abstract transport interface
|
||||
// ---------------------------------------------------------------------------
|
||||
class WebSocketTransport {
|
||||
public:
|
||||
virtual ~WebSocketTransport() = default;
|
||||
|
||||
virtual bool start(int port) = 0;
|
||||
virtual void stop() = 0;
|
||||
virtual bool isRunning() const = 0;
|
||||
|
||||
// Send a text message to a connected session
|
||||
virtual bool sendMessage(const std::string& sessionId,
|
||||
const std::string& message) = 0;
|
||||
|
||||
// --- handler registration (called by WebSocketAgentServer) ---
|
||||
|
||||
void setMessageHandler(
|
||||
std::function<void(const std::string& sessionId,
|
||||
const std::string& message)> handler) {
|
||||
messageHandler_ = std::move(handler);
|
||||
}
|
||||
|
||||
void setConnectHandler(
|
||||
std::function<void(const std::string& sessionId)> handler) {
|
||||
connectHandler_ = std::move(handler);
|
||||
}
|
||||
|
||||
void setDisconnectHandler(
|
||||
std::function<void(const std::string& sessionId)> handler) {
|
||||
disconnectHandler_ = std::move(handler);
|
||||
}
|
||||
|
||||
protected:
|
||||
std::function<void(const std::string&, const std::string&)> messageHandler_;
|
||||
std::function<void(const std::string&)> connectHandler_;
|
||||
std::function<void(const std::string&)> disconnectHandler_;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WebSocketAgentServer
|
||||
// ---------------------------------------------------------------------------
|
||||
class WebSocketAgentServer {
|
||||
public:
|
||||
explicit WebSocketAgentServer(std::unique_ptr<WebSocketTransport> transport)
|
||||
: transport_(std::move(transport))
|
||||
{
|
||||
transport_->setMessageHandler(
|
||||
[this](const std::string& sid, const std::string& msg) {
|
||||
onMessage(sid, msg);
|
||||
});
|
||||
transport_->setConnectHandler(
|
||||
[this](const std::string& sid) { onConnect(sid); });
|
||||
transport_->setDisconnectHandler(
|
||||
[this](const std::string& sid) { onDisconnect(sid); });
|
||||
}
|
||||
|
||||
bool start(int port) {
|
||||
port_ = port;
|
||||
return transport_->start(port);
|
||||
}
|
||||
|
||||
void stop() { transport_->stop(); }
|
||||
|
||||
bool isRunning() const { return transport_->isRunning(); }
|
||||
int getPort() const { return port_; }
|
||||
|
||||
// Register a fallback JSON-RPC handler for methods the server doesn't
|
||||
// handle itself (e.g. the orchestrator's processRequest).
|
||||
void setRequestHandler(JsonRpcHandler handler) {
|
||||
requestHandler_ = std::move(handler);
|
||||
}
|
||||
|
||||
// Register a callback for session lifecycle events ("connected" /
|
||||
// "disconnected").
|
||||
void setSessionEventCallback(SessionEventCallback cb) {
|
||||
sessionEventCallback_ = std::move(cb);
|
||||
}
|
||||
|
||||
// --- session queries ---
|
||||
|
||||
const AgentSession* getSession(const std::string& sessionId) const {
|
||||
auto it = sessions_.find(sessionId);
|
||||
return it != sessions_.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
std::vector<AgentSession> getActiveSessions() const {
|
||||
std::vector<AgentSession> result;
|
||||
for (const auto& [id, s] : sessions_) {
|
||||
if (s.connected) result.push_back(s);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t getActiveSessionCount() const {
|
||||
size_t n = 0;
|
||||
for (const auto& [id, s] : sessions_) {
|
||||
if (s.connected) ++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private:
|
||||
// --- transport callbacks ---
|
||||
|
||||
void onConnect(const std::string& sessionId) {
|
||||
AgentSession s;
|
||||
s.sessionId = sessionId;
|
||||
s.connected = true;
|
||||
s.connectedAtMs = nowMs();
|
||||
s.lastMessageAtMs = s.connectedAtMs;
|
||||
sessions_[sessionId] = s;
|
||||
|
||||
if (sessionEventCallback_)
|
||||
sessionEventCallback_(s, "connected");
|
||||
}
|
||||
|
||||
void onDisconnect(const std::string& sessionId) {
|
||||
auto it = sessions_.find(sessionId);
|
||||
if (it != sessions_.end()) {
|
||||
it->second.connected = false;
|
||||
if (sessionEventCallback_)
|
||||
sessionEventCallback_(it->second, "disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
void onMessage(const std::string& sessionId, const std::string& message) {
|
||||
// Update session stats
|
||||
auto it = sessions_.find(sessionId);
|
||||
if (it != sessions_.end()) {
|
||||
it->second.lastMessageAtMs = nowMs();
|
||||
it->second.messageCount++;
|
||||
}
|
||||
|
||||
// Parse + dispatch
|
||||
json response;
|
||||
try {
|
||||
json request = json::parse(message);
|
||||
response = processRequest(request, sessionId);
|
||||
} catch (const std::exception& e) {
|
||||
response = {
|
||||
{"jsonrpc", "2.0"},
|
||||
{"id", nullptr},
|
||||
{"error", {{"code", -32700},
|
||||
{"message", std::string("Parse error: ") + e.what()}}}
|
||||
};
|
||||
}
|
||||
|
||||
transport_->sendMessage(sessionId, response.dump());
|
||||
}
|
||||
|
||||
// Route a JSON-RPC request. Built-in methods are handled first;
|
||||
// everything else is forwarded to the registered handler.
|
||||
json processRequest(const json& request, const std::string& sessionId) {
|
||||
json response;
|
||||
response["jsonrpc"] = "2.0";
|
||||
response["id"] = request.contains("id") ? request["id"] : json(nullptr);
|
||||
|
||||
try {
|
||||
std::string method = request.at("method").get<std::string>();
|
||||
|
||||
if (method == "ping") {
|
||||
response["result"] = json{
|
||||
{"message", "pong"},
|
||||
{"sessionId", sessionId}
|
||||
};
|
||||
}
|
||||
else if (method == "getSessionInfo") {
|
||||
auto* sess = getSession(sessionId);
|
||||
if (sess) {
|
||||
response["result"] = {
|
||||
{"sessionId", sess->sessionId},
|
||||
{"agentName", sess->agentName},
|
||||
{"connected", sess->connected},
|
||||
{"messageCount", sess->messageCount}
|
||||
};
|
||||
} else {
|
||||
response["error"] = {
|
||||
{"code", -32603},
|
||||
{"message", "Session not found"}
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (method == "setAgentName") {
|
||||
std::string name =
|
||||
request.at("params").at("name").get<std::string>();
|
||||
auto sit = sessions_.find(sessionId);
|
||||
if (sit != sessions_.end()) {
|
||||
sit->second.agentName = name;
|
||||
response["result"] = true;
|
||||
} else {
|
||||
response["error"] = {
|
||||
{"code", -32603},
|
||||
{"message", "Session not found"}
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (method == "listSessions") {
|
||||
json arr = json::array();
|
||||
for (const auto& [id, s] : sessions_) {
|
||||
if (s.connected) {
|
||||
arr.push_back({
|
||||
{"sessionId", s.sessionId},
|
||||
{"agentName", s.agentName},
|
||||
{"messageCount", s.messageCount}
|
||||
});
|
||||
}
|
||||
}
|
||||
response["result"] = arr;
|
||||
}
|
||||
else if (requestHandler_) {
|
||||
// Delegate to the orchestrator's processRequest
|
||||
response = requestHandler_(request);
|
||||
// Ensure jsonrpc and id are set
|
||||
response["jsonrpc"] = "2.0";
|
||||
if (!response.contains("id"))
|
||||
response["id"] = request.contains("id")
|
||||
? request["id"] : json(nullptr);
|
||||
}
|
||||
else {
|
||||
response["error"] = {
|
||||
{"code", -32601},
|
||||
{"message", "Method not found: " + method}
|
||||
};
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
response["error"] = {
|
||||
{"code", -32600},
|
||||
{"message", std::string("Invalid request: ") + e.what()}
|
||||
};
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static uint64_t nowMs() {
|
||||
using namespace std::chrono;
|
||||
return static_cast<uint64_t>(
|
||||
duration_cast<milliseconds>(
|
||||
steady_clock::now().time_since_epoch())
|
||||
.count());
|
||||
}
|
||||
|
||||
std::unique_ptr<WebSocketTransport> transport_;
|
||||
std::map<std::string, AgentSession> sessions_;
|
||||
JsonRpcHandler requestHandler_;
|
||||
SessionEventCallback sessionEventCallback_;
|
||||
int port_ = 0;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MockWebSocketTransport — for testing without real sockets
|
||||
// ---------------------------------------------------------------------------
|
||||
class MockWebSocketTransport : public WebSocketTransport {
|
||||
public:
|
||||
bool start(int port) override {
|
||||
port_ = port;
|
||||
running_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void stop() override { running_ = false; }
|
||||
|
||||
bool isRunning() const override { return running_; }
|
||||
|
||||
bool sendMessage(const std::string& sessionId,
|
||||
const std::string& message) override {
|
||||
sentMessages_[sessionId].push_back(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- test helpers: simulate agent behaviour ---
|
||||
|
||||
// Simulate an agent opening a WebSocket connection
|
||||
std::string simulateConnect() {
|
||||
std::string sessionId = "agent_" + std::to_string(nextId_++);
|
||||
if (connectHandler_) connectHandler_(sessionId);
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
// Simulate an agent closing its connection
|
||||
void simulateDisconnect(const std::string& sessionId) {
|
||||
if (disconnectHandler_) disconnectHandler_(sessionId);
|
||||
}
|
||||
|
||||
// Simulate an agent sending a text message
|
||||
void simulateMessage(const std::string& sessionId,
|
||||
const std::string& message) {
|
||||
if (messageHandler_) messageHandler_(sessionId, message);
|
||||
}
|
||||
|
||||
// Get all messages the server sent back to a session
|
||||
std::vector<std::string> getSentMessages(
|
||||
const std::string& sessionId) const {
|
||||
auto it = sentMessages_.find(sessionId);
|
||||
return it != sentMessages_.end()
|
||||
? it->second
|
||||
: std::vector<std::string>{};
|
||||
}
|
||||
|
||||
// Get the most recent message sent to a session
|
||||
std::string getLastSentMessage(const std::string& sessionId) const {
|
||||
auto it = sentMessages_.find(sessionId);
|
||||
if (it != sentMessages_.end() && !it->second.empty())
|
||||
return it->second.back();
|
||||
return "";
|
||||
}
|
||||
|
||||
// Clear recorded messages (useful between test cases)
|
||||
void clearSentMessages() { sentMessages_.clear(); }
|
||||
|
||||
private:
|
||||
bool running_ = false;
|
||||
int port_ = 0;
|
||||
int nextId_ = 1;
|
||||
std::map<std::string, std::vector<std::string>> sentMessages_;
|
||||
};
|
||||
@@ -63,6 +63,19 @@ public:
|
||||
return result;
|
||||
}
|
||||
|
||||
// Remove a child from whatever role it belongs to
|
||||
bool removeChild(ASTNode* child) {
|
||||
for (auto& [role, kids] : children_) {
|
||||
auto it = std::find(kids.begin(), kids.end(), child);
|
||||
if (it != kids.end()) {
|
||||
(*it)->parent = nullptr;
|
||||
kids.erase(it);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::string, std::vector<ASTNode*>> children_;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user