Add semantic hash table tools, lock scaffolding, and H2 dual-hash migration scaffold

This commit is contained in:
Bill
2026-02-24 11:13:31 -07:00
parent 1a761c62e5
commit f4f3633b11
12 changed files with 665 additions and 6 deletions

View File

@@ -200,6 +200,7 @@
std::string type = params.value("type", "");
bool preferImports = params.value("preferImports", false);
bool strictMode = params.value("strictMode", false);
std::string hashLockMode = params.value("hashLockMode", "warn"); // warn|error
Module* ast = state.mutationAST();
if (!ast) return headlessRpcError(id, -32001, "AST unavailable");
ASTMutationAPI mut;
@@ -207,9 +208,22 @@
ASTMutationAPI::MutationResult res;
LibraryPolicyResult policy;
std::vector<std::string> affectedIds;
std::string hashLockWarning;
auto handleHashLock = [&](ASTNode* n, const std::string& context) -> json {
if (!n) return json();
std::string w = ASTMutationAPI::semanticHashLockWarning(n);
if (w.empty()) return json();
hashLockWarning = w;
if (hashLockMode == "error") {
return headlessRpcError(id, -32012, "Semantic hash lock violation (" + context + "): " + w);
}
return json();
};
if (type == "setProperty") {
std::string nodeId = params.value("nodeId", "");
if (!nodeId.empty()) affectedIds.push_back(nodeId);
json lockErr = handleHashLock(findNodeById(ast, nodeId), "setProperty");
if (!lockErr.is_null()) return lockErr;
res = mut.setProperty(params.value("nodeId", ""),
params.value("property", ""),
params.value("value", ""));
@@ -221,10 +235,14 @@
}
std::string nodeId = params.value("nodeId", "");
if (!nodeId.empty()) affectedIds.push_back(nodeId);
json lockErr = handleHashLock(findNodeById(ast, nodeId), "updateNode");
if (!lockErr.is_null()) return lockErr;
res = mut.updateNode(params.value("nodeId", ""), props);
} else if (type == "deleteNode") {
std::string nodeId = params.value("nodeId", "");
if (!nodeId.empty()) affectedIds.push_back(nodeId);
json lockErr = handleHashLock(findNodeById(ast, nodeId), "deleteNode");
if (!lockErr.is_null()) return lockErr;
res = mut.deleteNode(params.value("nodeId", ""));
} else if (type == "insertNode") {
ASTNode* node = nullptr;
@@ -241,6 +259,11 @@
return headlessRpcError(id, -32011, policy.error);
}
}
json lockErr = handleHashLock(findNodeById(ast, params.value("parentId", "")), "insertNode");
if (!lockErr.is_null()) {
if (node) deleteTree(node);
return lockErr;
}
res = mut.insertNode(params.value("parentId", ""),
params.value("role", ""), node);
if (!res.success && node) {
@@ -267,6 +290,7 @@
}
return headlessRpcResult(id, {
{"success", true}, {"warning", res.warning},
{"hashLockWarning", hashLockWarning},
{"libraryWarning", policy.warning},
{"unknownFunctions", policy.unknownFunctions},
{"version", state.active()
@@ -283,6 +307,7 @@
Module* ast = state.mutationAST();
auto params = request.contains("params") ? request["params"]
: json::object();
std::string hashLockMode = params.value("hashLockMode", "warn"); // warn|error
if (!params.contains("mutations") ||
!params["mutations"].is_array())
return headlessRpcError(id, -32602,
@@ -291,6 +316,7 @@
batch.setRoot(ast);
std::vector<BatchMutationAPI::Mutation> mutations;
std::vector<ASTNode*> ownedNodes;
std::vector<std::string> hashLockWarnings;
for (const auto& m : params["mutations"]) {
BatchMutationAPI::Mutation mut;
mut.type = m.value("type", "");
@@ -303,8 +329,19 @@
mut.newNode = fromJson(m["node"]);
if (mut.newNode) ownedNodes.push_back(mut.newNode);
}
ASTNode* guard = nullptr;
if (mut.type == "insertNode") guard = findNodeById(ast, mut.parentId);
else guard = findNodeById(ast, mut.nodeId);
std::string w = ASTMutationAPI::semanticHashLockWarning(guard);
if (!w.empty()) hashLockWarnings.push_back(w);
mutations.push_back(mut);
}
if (!hashLockWarnings.empty() && hashLockMode == "error") {
for (auto* n : ownedNodes) {
if (n->parent == nullptr) deleteTree(n);
}
return headlessRpcError(id, -32012, "Semantic hash lock violation in batch: " + hashLockWarnings.front());
}
auto batchRes = batch.applySequence(mutations);
if (!batchRes.success) {
for (auto* n : ownedNodes) {
@@ -329,6 +366,7 @@
}
return headlessRpcResult(id,
{{"success", true}, {"appliedCount", batchRes.appliedCount},
{"hashLockWarnings", hashLockWarnings},
{"version", state.active()
? state.active()->versionTracker.version : 0}});
}

View File

@@ -96,7 +96,9 @@
return headlessRpcResult(id,
{{"success", true},
{"sidecarPath", res.sidecarPath},
{"annotationCount", res.annotationCount}});
{"annotationCount", res.annotationCount},
{"semanticHashTablePath", res.semanticHashTablePath},
{"semanticHashCount", res.semanticHashCount}});
}
// --- loadAnnotatedAST ---
@@ -130,6 +132,107 @@
{{"files", files}, {"count", (int)files.size()}});
}
// --- saveSemanticHashTable ---
if (method == "saveSemanticHashTable") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
auto params = request.contains("params") ? request["params"] : json::object();
std::string path = params.value("path", "");
if (path.empty() && state.activeBuffer) path = state.activeBuffer->path;
auto it = state.bufferStates.find(path);
if (it == state.bufferStates.end())
return headlessRpcError(id, -32602, "No buffer: " + path);
Module* ast = it->second->sync.getAST();
auto res = saveSemanticHashTable(state.workspaceRoot, path, ast);
if (!res.success) return headlessRpcError(id, -32010, res.error);
return headlessRpcResult(id, {
{"success", true},
{"semanticHashTablePath", res.path},
{"semanticHashCount", res.entryCount}
});
}
// --- getSemanticHashTable ---
if (method == "getSemanticHashTable") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
auto params = request.contains("params") ? request["params"] : json::object();
std::string path = params.value("path", "");
if (path.empty() && state.activeBuffer) path = state.activeBuffer->path;
bool refresh = params.value("refresh", false);
auto it = state.bufferStates.find(path);
if (it == state.bufferStates.end())
return headlessRpcError(id, -32602, "No buffer: " + path);
if (refresh) {
Module* ast = it->second->sync.getAST();
auto save = saveSemanticHashTable(state.workspaceRoot, path, ast);
if (!save.success) return headlessRpcError(id, -32010, save.error);
}
auto res = loadSemanticHashTable(state.workspaceRoot, path);
if (!res.success) return headlessRpcError(id, -32010, res.error);
return headlessRpcResult(id, {
{"success", true},
{"semanticHashTablePath", res.path},
{"table", res.table}
});
}
// --- listSemanticHashTables ---
if (method == "listSemanticHashTables") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
auto files = listSemanticHashTables(state.workspaceRoot);
return headlessRpcResult(id, {
{"files", files},
{"count", (int)files.size()}
});
}
// --- setSemanticHashLock ---
if (method == "setSemanticHashLock") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
auto params = request.contains("params") ? request["params"] : json::object();
std::string nodeId = params.value("nodeId", "");
if (nodeId.empty())
return headlessRpcError(id, -32602, "nodeId required");
bool locked = params.value("locked", true);
std::string reason = params.value("reason", "");
Module* ast = state.activeAST();
if (!ast) return headlessRpcError(id, -32602, "No AST available");
ASTNode* node = findNodeById(ast, nodeId);
if (!node) return headlessRpcError(id, -32602, "Node not found: " + nodeId);
node->semanticHashLockState = locked ? "locked" : "unlocked";
node->semanticHashLockReason = reason;
return headlessRpcResult(id, {
{"success", true},
{"nodeId", node->id},
{"semanticHash", node->semanticHash},
{"semanticHashLockState", node->semanticHashLockState},
{"semanticHashLockReason", node->semanticHashLockReason}
});
}
// --- getSemanticHashLock ---
if (method == "getSemanticHashLock") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
auto params = request.contains("params") ? request["params"] : json::object();
std::string nodeId = params.value("nodeId", "");
if (nodeId.empty())
return headlessRpcError(id, -32602, "nodeId required");
Module* ast = state.activeAST();
if (!ast) return headlessRpcError(id, -32602, "No AST available");
ASTNode* node = findNodeById(ast, nodeId);
if (!node) return headlessRpcError(id, -32602, "Node not found: " + nodeId);
return headlessRpcResult(id, {
{"nodeId", node->id},
{"semanticHash", node->semanticHash},
{"semanticHashLockState", node->semanticHashLockState.empty() ? "unlocked" : node->semanticHashLockState},
{"semanticHashLockReason", node->semanticHashLockReason}
});
}
// --- setSemanticAnnotation ---
if (method == "setSemanticAnnotation") {
if (!AgentPermissionPolicy::canInvoke(role, method))
@@ -138,6 +241,7 @@
: json::object();
std::string nodeId = params.value("nodeId", "");
std::string type = params.value("type", "");
std::string hashLockMode = params.value("hashLockMode", "warn"); // warn|error
json fields = params.contains("fields") ? params["fields"]
: json::object();
if (nodeId.empty() || type.empty())
@@ -150,6 +254,10 @@
ASTNode* target = findNodeById(ast, nodeId);
if (!target)
return headlessRpcError(id, -32602, "Node not found: " + nodeId);
std::string hashLockWarning = ASTMutationAPI::semanticHashLockWarning(target);
if (!hashLockWarning.empty() && hashLockMode == "error") {
return headlessRpcError(id, -32012, "Semantic hash lock violation: " + hashLockWarning);
}
// Map type string to conceptType
std::string conceptType;
@@ -246,7 +354,11 @@
return headlessRpcError(id, -32010, "Failed to create annotation");
target->addChild("annotations", newAnno);
return headlessRpcResult(id, {{"success", true}, {"type", type}});
return headlessRpcResult(id, {
{"success", true},
{"type", type},
{"hashLockWarning", hashLockWarning}
});
}
// --- getSemanticAnnotations ---