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>
This commit is contained in:
Bill
2026-02-12 16:01:56 +00:00
parent c36fd92045
commit 976161dc4a
36 changed files with 2999 additions and 69 deletions

View File

@@ -0,0 +1,394 @@
// Step 266 TDD Test: Semantic Annotation AST Node Types
//
// Tests 5 new semantic annotation types: IntentAnnotation, ComplexityAnnotation,
// RiskAnnotation, ContractAnnotation, SemanticTagAnnotation.
// Validates construction, JSON roundtrip, compact AST semantic fields,
// and attachment to Function nodes.
#include "HeadlessEditorState.h"
#include "MCPServer.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;
}
}
static json rpc(HeadlessEditorState& state, const std::string& session,
const std::string& method,
json params = json::object()) {
json request = {{"jsonrpc", "2.0"}, {"id", 1},
{"method", method}, {"params", params}};
return state.processAgentRequest(request, session);
}
int main() {
int passed = 0;
int failed = 0;
// ---------------------------------------------------------------
// Test 1: IntentAnnotation construction and fields
// ---------------------------------------------------------------
{
IntentAnnotation a;
a.summary = "validates user input before DB write";
a.category = "validation";
expect(a.conceptType == "IntentAnnotation" &&
a.summary == "validates user input before DB write" &&
a.category == "validation",
"IntentAnnotation construction and fields",
passed, failed);
}
// ---------------------------------------------------------------
// Test 2: ComplexityAnnotation construction and fields
// ---------------------------------------------------------------
{
ComplexityAnnotation a;
a.timeComplexity = "O(n log n)";
a.cognitiveComplexity = 7;
a.linesOfLogic = 42;
expect(a.conceptType == "ComplexityAnnotation" &&
a.timeComplexity == "O(n log n)" &&
a.cognitiveComplexity == 7 &&
a.linesOfLogic == 42,
"ComplexityAnnotation construction and fields",
passed, failed);
}
// ---------------------------------------------------------------
// Test 3: RiskAnnotation construction and fields
// ---------------------------------------------------------------
{
RiskAnnotation a;
a.level = "high";
a.reason = "12 callers depend on exact return format";
a.dependentCount = 12;
expect(a.conceptType == "RiskAnnotation" &&
a.level == "high" &&
a.reason == "12 callers depend on exact return format" &&
a.dependentCount == 12,
"RiskAnnotation construction and fields",
passed, failed);
}
// ---------------------------------------------------------------
// Test 4: ContractAnnotation construction and fields
// ---------------------------------------------------------------
{
ContractAnnotation a;
a.preconditions = "name must be non-empty string";
a.postconditions = "returns greeting string starting with 'hello'";
a.returnShape = "str";
a.sideEffects = "none";
expect(a.conceptType == "ContractAnnotation" &&
a.preconditions == "name must be non-empty string" &&
a.returnShape == "str" &&
a.sideEffects == "none",
"ContractAnnotation construction and fields",
passed, failed);
}
// ---------------------------------------------------------------
// Test 5: SemanticTagAnnotation construction and fields
// ---------------------------------------------------------------
{
SemanticTagAnnotation a;
a.tags = {"@serialize", "@validation", "@auth"};
expect(a.conceptType == "SemanticTagAnnotation" &&
a.tags.size() == 3 &&
a.tags[0] == "@serialize" &&
a.tags[2] == "@auth",
"SemanticTagAnnotation construction and fields",
passed, failed);
}
// ---------------------------------------------------------------
// Test 6: IntentAnnotation JSON roundtrip via Serialization.h
// ---------------------------------------------------------------
{
IntentAnnotation orig;
orig.id = "intent_1";
orig.summary = "computes tax for order";
orig.category = "computation";
json j = toJson(&orig);
auto* restored = static_cast<IntentAnnotation*>(fromJson(j));
bool ok = restored &&
restored->conceptType == "IntentAnnotation" &&
restored->summary == "computes tax for order" &&
restored->category == "computation" &&
restored->id == "intent_1";
delete restored;
expect(ok, "IntentAnnotation JSON roundtrip",
passed, failed);
}
// ---------------------------------------------------------------
// Test 7: ComplexityAnnotation JSON roundtrip
// ---------------------------------------------------------------
{
ComplexityAnnotation orig;
orig.id = "cx_1";
orig.timeComplexity = "O(n^2)";
orig.cognitiveComplexity = 9;
orig.linesOfLogic = 55;
json j = toJson(&orig);
auto* restored = static_cast<ComplexityAnnotation*>(fromJson(j));
bool ok = restored &&
restored->timeComplexity == "O(n^2)" &&
restored->cognitiveComplexity == 9 &&
restored->linesOfLogic == 55;
delete restored;
expect(ok, "ComplexityAnnotation JSON roundtrip",
passed, failed);
}
// ---------------------------------------------------------------
// Test 8: RiskAnnotation + ContractAnnotation roundtrip
// ---------------------------------------------------------------
{
RiskAnnotation risk;
risk.id = "risk_1";
risk.level = "critical";
risk.reason = "payment processing";
risk.dependentCount = 25;
json rj = toJson(&risk);
auto* rr = static_cast<RiskAnnotation*>(fromJson(rj));
ContractAnnotation contract;
contract.id = "ct_1";
contract.preconditions = "amount > 0";
contract.postconditions = "returns transaction_id";
contract.returnShape = "str";
contract.sideEffects = "network";
json cj = toJson(&contract);
auto* cr = static_cast<ContractAnnotation*>(fromJson(cj));
bool ok = rr && rr->level == "critical" && rr->dependentCount == 25 &&
cr && cr->preconditions == "amount > 0" &&
cr->sideEffects == "network";
delete rr;
delete cr;
expect(ok, "RiskAnnotation + ContractAnnotation roundtrip",
passed, failed);
}
// ---------------------------------------------------------------
// Test 9: SemanticTagAnnotation JSON roundtrip
// ---------------------------------------------------------------
{
SemanticTagAnnotation orig;
orig.id = "tag_1";
orig.tags = {"@io", "@crypto"};
json j = toJson(&orig);
auto* restored = static_cast<SemanticTagAnnotation*>(fromJson(j));
bool ok = restored &&
restored->tags.size() == 2 &&
restored->tags[0] == "@io" &&
restored->tags[1] == "@crypto";
delete restored;
expect(ok, "SemanticTagAnnotation JSON roundtrip",
passed, failed);
}
// ---------------------------------------------------------------
// Test 10: Attach semantic annotations to Function node
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.setAgentRole("s1", AgentRole::Refactor);
std::string src = "def greet(name):\n return \"hello \" + name\n";
rpc(state, "s1", "openFile",
{{"path", "t.py"}, {"content", src}});
// Find function node
std::string fnId;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" && getNodeName(c) == "greet") {
fnId = c->id;
break;
}
}
// Attach IntentAnnotation via applyMutation insertNode
rpc(state, "s1", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "IntentAnnotation"},
{"properties", {{"summary", "greets a person by name"},
{"category", "io"}}}}}});
// Check it's there
bool found = false;
ASTNode* fn = findNodeById(state.activeAST(), fnId);
if (fn) {
for (auto* child : fn->getChildren("annotations")) {
if (child->conceptType == "IntentAnnotation") {
auto* ia = static_cast<IntentAnnotation*>(child);
if (ia->summary == "greets a person by name")
found = true;
}
}
}
expect(!fnId.empty() && found,
"Attach IntentAnnotation to Function via applyMutation",
passed, failed);
}
// ---------------------------------------------------------------
// Test 11: Compact AST includes semantic summary field
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.setAgentRole("s1", AgentRole::Refactor);
std::string src = "def greet(name):\n return \"hello \" + name\n";
rpc(state, "s1", "openFile",
{{"path", "t.py"}, {"content", src}});
std::string fnId;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" && getNodeName(c) == "greet") {
fnId = c->id;
break;
}
}
// Add intent + risk annotations
rpc(state, "s1", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "IntentAnnotation"},
{"properties", {{"summary", "greets user"},
{"category", "io"}}}}}});
rpc(state, "s1", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "RiskAnnotation"},
{"properties", {{"level", "low"},
{"reason", "simple function"},
{"dependentCount", 2}}}}}});
// Get compact AST
json resp = rpc(state, "s1", "getAST", {{"compact", true}});
json nodes = resp["result"]["nodes"];
// Find greet node in compact output
bool hasSemantic = false;
for (const auto& n : nodes) {
if (n.value("name", "") == "greet" &&
n.contains("semantic")) {
auto sem = n["semantic"];
hasSemantic = sem.contains("intent") &&
sem.contains("risk");
}
}
expect(hasSemantic,
"Compact AST includes semantic summary for annotated function",
passed, failed);
}
// ---------------------------------------------------------------
// Test 12: All 5 annotation types survive AST clone (undo roundtrip)
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.setAgentRole("s1", AgentRole::Refactor);
std::string src = "def greet(name):\n return \"hello \" + name\n";
rpc(state, "s1", "openFile",
{{"path", "t.py"}, {"content", src}});
std::string fnId;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" && getNodeName(c) == "greet") {
fnId = c->id;
break;
}
}
// Add all 5 annotation types
rpc(state, "s1", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "IntentAnnotation"},
{"properties", {{"summary", "test"}, {"category", "io"}}}}}});
rpc(state, "s1", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "ComplexityAnnotation"},
{"properties", {{"timeComplexity", "O(1)"},
{"cognitiveComplexity", 1},
{"linesOfLogic", 2}}}}}});
rpc(state, "s1", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "RiskAnnotation"},
{"properties", {{"level", "low"},
{"reason", "trivial"},
{"dependentCount", 0}}}}}});
rpc(state, "s1", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "ContractAnnotation"},
{"properties", {{"preconditions", "name is str"},
{"postconditions", "returns str"},
{"returnShape", "str"},
{"sideEffects", "none"}}}}}});
rpc(state, "s1", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "SemanticTagAnnotation"},
{"properties", {{"tags", json::array({"@io"})}} }}}});
// Serialize AST to JSON and back (simulates undo/clone roundtrip)
json astJson = toJson(state.activeAST());
auto* restored = static_cast<Module*>(fromJson(astJson));
int annoCount = 0;
for (auto* c : restored->allChildren()) {
if (c->conceptType == "Function" && getNodeName(c) == "greet") {
for (auto* a : c->getChildren("annotations")) {
if (a->conceptType == "IntentAnnotation" ||
a->conceptType == "ComplexityAnnotation" ||
a->conceptType == "RiskAnnotation" ||
a->conceptType == "ContractAnnotation" ||
a->conceptType == "SemanticTagAnnotation")
++annoCount;
}
}
}
delete restored;
expect(annoCount == 5,
"All 5 semantic annotation types survive JSON roundtrip (" +
std::to_string(annoCount) + "/5)",
passed, failed);
}
std::cout << "\n=== Step 266 Results: " << passed << " passed, "
<< failed << " failed ===\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -0,0 +1,452 @@
// Step 267 TDD Test: Sidecar AST Persistence
//
// Tests saving/loading annotated ASTs as .whetstone/ sidecar files.
// Annotations live alongside the codebase without polluting source.
#include "HeadlessEditorState.h"
#include "MCPServer.h"
#include <iostream>
#include <string>
#include <fstream>
#include <filesystem>
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;
}
}
static json rpc(HeadlessEditorState& state, const std::string& session,
const std::string& method,
json params = json::object()) {
json request = {{"jsonrpc", "2.0"}, {"id", 1},
{"method", method}, {"params", params}};
return state.processAgentRequest(request, session);
}
int main() {
int passed = 0;
int failed = 0;
auto tmpDir = std::filesystem::temp_directory_path() /
"whetstone_step267_test";
std::filesystem::remove_all(tmpDir);
std::filesystem::create_directories(tmpDir);
std::string src =
"def greet(name):\n"
" return \"hello \" + name\n";
// ---------------------------------------------------------------
// Test 1: saveAnnotatedAST creates sidecar file
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "greet.py"}, {"content", src}});
// Add an annotation
std::string fnId;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" && getNodeName(c) == "greet") {
fnId = c->id;
break;
}
}
rpc(state, "s1", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "IntentAnnotation"},
{"properties", {{"summary", "greets by name"},
{"category", "io"}}}}}});
json resp = rpc(state, "s1", "saveAnnotatedAST",
{{"path", "greet.py"}});
bool success = resp.contains("result") &&
resp["result"].value("success", false);
auto sidecarPath = tmpDir / ".whetstone" / "greet.py.ast.json";
bool exists = std::filesystem::exists(sidecarPath);
expect(success && exists,
"saveAnnotatedAST creates .whetstone/greet.py.ast.json",
passed, failed);
}
// ---------------------------------------------------------------
// Test 2: loadAnnotatedAST restores annotations
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("s1", AgentRole::Refactor);
// Open same file fresh (no annotations)
rpc(state, "s1", "openFile",
{{"path", "greet.py"}, {"content", src}});
// Load sidecar
json resp = rpc(state, "s1", "loadAnnotatedAST",
{{"path", "greet.py"}});
bool success = resp.contains("result") &&
resp["result"].value("success", false);
// Check annotation restored
bool found = false;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" && getNodeName(c) == "greet") {
for (auto* a : c->getChildren("annotations")) {
if (a->conceptType == "IntentAnnotation") {
auto* ia = static_cast<IntentAnnotation*>(a);
if (ia->summary == "greets by name") found = true;
}
}
}
}
expect(success && found,
"loadAnnotatedAST restores IntentAnnotation from sidecar",
passed, failed);
}
// ---------------------------------------------------------------
// Test 3: Sidecar merge matches by node ID
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "greet.py"}, {"content", src}});
// Load sidecar and check that the annotation is on the right node
rpc(state, "s1", "loadAnnotatedAST", {{"path", "greet.py"}});
int annotatedFunctions = 0;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function") {
auto annos = c->getChildren("annotations");
for (auto* a : annos) {
if (a->conceptType == "IntentAnnotation")
++annotatedFunctions;
}
}
}
expect(annotatedFunctions == 1,
"Sidecar annotations merged onto correct function node",
passed, failed);
}
// ---------------------------------------------------------------
// Test 4: listAnnotatedFiles returns saved sidecars
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("s1", AgentRole::Refactor);
json resp = rpc(state, "s1", "listAnnotatedFiles");
bool hasResult = resp.contains("result");
int count = 0;
if (hasResult && resp["result"].contains("files")) {
count = (int)resp["result"]["files"].size();
}
expect(hasResult && count >= 1,
"listAnnotatedFiles returns " + std::to_string(count) + " file(s)",
passed, failed);
}
// ---------------------------------------------------------------
// Test 5: Multiple annotations survive roundtrip
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "multi.py"}, {"content", src}});
std::string fnId;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" && getNodeName(c) == "greet") {
fnId = c->id;
break;
}
}
// Add 3 annotations
rpc(state, "s1", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "IntentAnnotation"},
{"properties", {{"summary", "greets"}, {"category", "io"}}}}}});
rpc(state, "s1", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "RiskAnnotation"},
{"properties", {{"level", "low"}, {"reason", "simple"},
{"dependentCount", 1}}}}}});
rpc(state, "s1", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "ComplexityAnnotation"},
{"properties", {{"timeComplexity", "O(1)"},
{"cognitiveComplexity", 1},
{"linesOfLogic", 2}}}}}});
// Save
rpc(state, "s1", "saveAnnotatedAST", {{"path", "multi.py"}});
// Reload fresh
HeadlessEditorState state2;
state2.defaultLanguage = "python";
state2.workspaceRoot = tmpDir.string();
state2.setAgentRole("s1", AgentRole::Refactor);
rpc(state2, "s1", "openFile",
{{"path", "multi.py"}, {"content", src}});
rpc(state2, "s1", "loadAnnotatedAST", {{"path", "multi.py"}});
int count = 0;
for (auto* c : state2.activeAST()->allChildren()) {
if (c->conceptType == "Function" && getNodeName(c) == "greet") {
for (auto* a : c->getChildren("annotations")) {
if (a->conceptType == "IntentAnnotation" ||
a->conceptType == "RiskAnnotation" ||
a->conceptType == "ComplexityAnnotation")
++count;
}
}
}
expect(count == 3,
"3 annotations survive save/load roundtrip (" +
std::to_string(count) + "/3)",
passed, failed);
}
// ---------------------------------------------------------------
// Test 6: Sidecar doesn't affect source code
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "greet.py"}, {"content", src}});
rpc(state, "s1", "loadAnnotatedAST", {{"path", "greet.py"}});
// editBuf should still be the original source
bool codeClean = state.activeBuffer->editBuf.find("IntentAnnotation") ==
std::string::npos;
bool hasGreet = state.activeBuffer->editBuf.find("greet") !=
std::string::npos;
expect(codeClean && hasGreet,
"Loading sidecar doesn't pollute source code in editBuf",
passed, failed);
}
// ---------------------------------------------------------------
// Test 7: saveAnnotatedAST on unknown buffer returns error
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("s1", AgentRole::Refactor);
json resp = rpc(state, "s1", "saveAnnotatedAST",
{{"path", "nonexistent.py"}});
bool hasError = resp.contains("error");
expect(hasError,
"saveAnnotatedAST on unknown buffer returns error",
passed, failed);
}
// ---------------------------------------------------------------
// Test 8: loadAnnotatedAST with no sidecar returns error
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "nosidecar.py"}, {"content", src}});
json resp = rpc(state, "s1", "loadAnnotatedAST",
{{"path", "nosidecar.py"}});
bool hasError = resp.contains("error");
expect(hasError,
"loadAnnotatedAST with no sidecar file returns error",
passed, failed);
}
// ---------------------------------------------------------------
// Test 9: Overwrite existing sidecar
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "greet.py"}, {"content", src}});
// Add a different annotation
std::string fnId;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" && getNodeName(c) == "greet") {
fnId = c->id;
break;
}
}
rpc(state, "s1", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "ContractAnnotation"},
{"properties", {{"preconditions", "name != empty"},
{"postconditions", "returns greeting"},
{"returnShape", "str"},
{"sideEffects", "none"}}}}}});
// Overwrite sidecar
rpc(state, "s1", "saveAnnotatedAST", {{"path", "greet.py"}});
// Reload and check ContractAnnotation exists
HeadlessEditorState state2;
state2.defaultLanguage = "python";
state2.workspaceRoot = tmpDir.string();
state2.setAgentRole("s1", AgentRole::Refactor);
rpc(state2, "s1", "openFile",
{{"path", "greet.py"}, {"content", src}});
rpc(state2, "s1", "loadAnnotatedAST", {{"path", "greet.py"}});
bool hasContract = false;
for (auto* c : state2.activeAST()->allChildren()) {
if (c->conceptType == "Function") {
for (auto* a : c->getChildren("annotations")) {
if (a->conceptType == "ContractAnnotation")
hasContract = true;
}
}
}
expect(hasContract,
"Overwritten sidecar has new ContractAnnotation",
passed, failed);
}
// ---------------------------------------------------------------
// Test 10: Linter cannot save annotations
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("admin", AgentRole::Refactor);
state.setAgentRole("linter", AgentRole::Linter);
rpc(state, "admin", "openFile",
{{"path", "greet.py"}, {"content", src}});
json resp = rpc(state, "linter", "saveAnnotatedAST",
{{"path", "greet.py"}});
bool denied = resp.contains("error");
// Linter CAN load (read-only)
json loadResp = rpc(state, "linter", "loadAnnotatedAST",
{{"path", "greet.py"}});
bool loadOk = loadResp.contains("result");
expect(denied && loadOk,
"Linter denied save, allowed load",
passed, failed);
}
// ---------------------------------------------------------------
// Test 11: MCP tools registered
// ---------------------------------------------------------------
{
MCPServer mcp;
bool foundSave = false, foundLoad = false, foundList = false;
for (const auto& t : mcp.getTools()) {
if (t.name == "whetstone_save_annotated_ast") foundSave = true;
if (t.name == "whetstone_load_annotated_ast") foundLoad = true;
if (t.name == "whetstone_list_annotated_files") foundList = true;
}
expect(foundSave && foundLoad && foundList,
"3 sidecar MCP tools registered",
passed, failed);
}
// ---------------------------------------------------------------
// Test 12: Response includes annotation count and sidecar path
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("s1", AgentRole::Refactor);
rpc(state, "s1", "openFile",
{{"path", "greet.py"}, {"content", src}});
std::string fnId;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" && getNodeName(c) == "greet") {
fnId = c->id;
break;
}
}
rpc(state, "s1", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "IntentAnnotation"},
{"properties", {{"summary", "test"},
{"category", "io"}}}}}});
json resp = rpc(state, "s1", "saveAnnotatedAST",
{{"path", "greet.py"}});
bool hasPath = resp["result"].contains("sidecarPath");
bool hasCount = resp["result"].contains("annotationCount");
int count = resp["result"].value("annotationCount", 0);
expect(hasPath && hasCount && count >= 1,
"Response has sidecarPath and annotationCount=" +
std::to_string(count),
passed, failed);
}
// Cleanup
std::filesystem::remove_all(tmpDir);
std::cout << "\n=== Step 267 Results: " << passed << " passed, "
<< failed << " failed ===\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -0,0 +1,476 @@
// Step 268 TDD Test: Phase 10a Integration Tests
//
// End-to-end: create annotations via applyMutation, verify they appear
// in compact AST, save sidecar, re-open file, verify auto-loaded.
#include "HeadlessEditorState.h"
#include "MCPServer.h"
#include <iostream>
#include <string>
#include <filesystem>
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;
}
}
static json rpc(HeadlessEditorState& state, const std::string& session,
const std::string& method,
json params = json::object()) {
json request = {{"jsonrpc", "2.0"}, {"id", 1},
{"method", method}, {"params", params}};
return state.processAgentRequest(request, session);
}
int main() {
int passed = 0;
int failed = 0;
auto tmpDir = std::filesystem::temp_directory_path() /
"whetstone_step268_test";
std::filesystem::remove_all(tmpDir);
std::filesystem::create_directories(tmpDir);
std::string src =
"def greet(name):\n"
" return \"hello \" + name\n"
"\n"
"def farewell(name):\n"
" return \"goodbye \" + name\n";
// ---------------------------------------------------------------
// Test 1: Full annotation workflow — create, verify compact, save
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("agent", AgentRole::Refactor);
rpc(state, "agent", "openFile",
{{"path", "funcs.py"}, {"content", src}});
// Find both functions
std::string greetId, farewellId;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function") {
std::string n = getNodeName(c);
if (n == "greet") greetId = c->id;
if (n == "farewell") farewellId = c->id;
}
}
// Annotate greet with intent + risk
rpc(state, "agent", "applyMutation",
{{"type", "insertNode"}, {"parentId", greetId},
{"role", "annotations"},
{"node", {{"concept", "IntentAnnotation"},
{"properties", {{"summary", "greets user by name"},
{"category", "io"}}}}}});
rpc(state, "agent", "applyMutation",
{{"type", "insertNode"}, {"parentId", greetId},
{"role", "annotations"},
{"node", {{"concept", "RiskAnnotation"},
{"properties", {{"level", "low"},
{"reason", "simple string concat"},
{"dependentCount", 3}}}}}});
// Annotate farewell with intent + contract
rpc(state, "agent", "applyMutation",
{{"type", "insertNode"}, {"parentId", farewellId},
{"role", "annotations"},
{"node", {{"concept", "IntentAnnotation"},
{"properties", {{"summary", "says goodbye"},
{"category", "io"}}}}}});
rpc(state, "agent", "applyMutation",
{{"type", "insertNode"}, {"parentId", farewellId},
{"role", "annotations"},
{"node", {{"concept", "ContractAnnotation"},
{"properties", {{"preconditions", "name is string"},
{"postconditions", "returns greeting"},
{"returnShape", "str"},
{"sideEffects", "none"}}}}}});
// Verify compact AST has semantic fields
json resp = rpc(state, "agent", "getAST", {{"compact", true}});
json nodes = resp["result"]["nodes"];
bool greetSemantic = false, farewellSemantic = false;
for (const auto& n : nodes) {
if (n.value("name", "") == "greet" && n.contains("semantic")) {
auto sem = n["semantic"];
greetSemantic = sem.contains("intent") && sem.contains("risk");
}
if (n.value("name", "") == "farewell" && n.contains("semantic")) {
auto sem = n["semantic"];
farewellSemantic = sem.contains("intent") &&
sem.contains("contract");
}
}
// Save sidecar
json saveResp = rpc(state, "agent", "saveAnnotatedAST",
{{"path", "funcs.py"}});
bool saved = saveResp.contains("result") &&
saveResp["result"].value("success", false);
int annoCount = saveResp["result"].value("annotationCount", 0);
expect(greetSemantic && farewellSemantic && saved && annoCount >= 4,
"Annotate 2 functions, compact AST has semantic, sidecar saved (" +
std::to_string(annoCount) + " annotations)",
passed, failed);
}
// ---------------------------------------------------------------
// Test 2: Re-open file and load sidecar restores all annotations
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("agent", AgentRole::Refactor);
rpc(state, "agent", "openFile",
{{"path", "funcs.py"}, {"content", src}});
rpc(state, "agent", "loadAnnotatedAST", {{"path", "funcs.py"}});
// Count semantic annotations on both functions
int greetAnnos = 0, farewellAnnos = 0;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType != "Function") continue;
std::string name = getNodeName(c);
for (auto* a : c->getChildren("annotations")) {
if (isSemanticAnnotation(a->conceptType)) {
if (name == "greet") ++greetAnnos;
if (name == "farewell") ++farewellAnnos;
}
}
}
expect(greetAnnos == 2 && farewellAnnos == 2,
"Sidecar load restores greet=" + std::to_string(greetAnnos) +
" farewell=" + std::to_string(farewellAnnos) + " annotations",
passed, failed);
}
// ---------------------------------------------------------------
// Test 3: Compact AST after sidecar load has semantic fields
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("agent", AgentRole::Refactor);
rpc(state, "agent", "openFile",
{{"path", "funcs.py"}, {"content", src}});
rpc(state, "agent", "loadAnnotatedAST", {{"path", "funcs.py"}});
json resp = rpc(state, "agent", "getAST", {{"compact", true}});
json nodes = resp["result"]["nodes"];
int nodesWithSemantic = 0;
for (const auto& n : nodes) {
if (n.contains("semantic")) ++nodesWithSemantic;
}
expect(nodesWithSemantic >= 2,
"Compact AST after sidecar load has " +
std::to_string(nodesWithSemantic) + " nodes with semantic fields",
passed, failed);
}
// ---------------------------------------------------------------
// Test 4: Annotations don't appear in generated source code
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("agent", AgentRole::Refactor);
rpc(state, "agent", "openFile",
{{"path", "funcs.py"}, {"content", src}});
rpc(state, "agent", "loadAnnotatedAST", {{"path", "funcs.py"}});
std::string code = state.activeBuffer->editBuf;
bool noAnnotationClutter =
code.find("IntentAnnotation") == std::string::npos &&
code.find("RiskAnnotation") == std::string::npos &&
code.find("ContractAnnotation") == std::string::npos;
bool hasOriginalCode =
code.find("def greet") != std::string::npos &&
code.find("def farewell") != std::string::npos;
expect(noAnnotationClutter && hasOriginalCode,
"Loaded annotations don't pollute generated source code",
passed, failed);
}
// ---------------------------------------------------------------
// Test 5: Multiple save/load cycles are idempotent
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("agent", AgentRole::Refactor);
rpc(state, "agent", "openFile",
{{"path", "funcs.py"}, {"content", src}});
rpc(state, "agent", "loadAnnotatedAST", {{"path", "funcs.py"}});
// Save again (should overwrite with same content)
rpc(state, "agent", "saveAnnotatedAST", {{"path", "funcs.py"}});
// Load into fresh state
HeadlessEditorState state2;
state2.defaultLanguage = "python";
state2.workspaceRoot = tmpDir.string();
state2.setAgentRole("agent", AgentRole::Refactor);
rpc(state2, "agent", "openFile",
{{"path", "funcs.py"}, {"content", src}});
rpc(state2, "agent", "loadAnnotatedAST", {{"path", "funcs.py"}});
int totalAnnos = 0;
for (auto* c : state2.activeAST()->allChildren()) {
if (c->conceptType == "Function") {
for (auto* a : c->getChildren("annotations")) {
if (isSemanticAnnotation(a->conceptType))
++totalAnnos;
}
}
}
expect(totalAnnos == 4,
"Save/load/save/load cycle preserves exactly 4 annotations (" +
std::to_string(totalAnnos) + ")",
passed, failed);
}
// ---------------------------------------------------------------
// Test 6: Multi-file sidecar workflow
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("agent", AgentRole::Refactor);
std::string src2 = "def helper():\n pass\n";
rpc(state, "agent", "openFile",
{{"path", "funcs.py"}, {"content", src}});
rpc(state, "agent", "openFile",
{{"path", "helper.py"}, {"content", src2}});
// Annotate helper
state.setActiveBuffer("helper.py");
std::string helperId;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" && getNodeName(c) == "helper") {
helperId = c->id;
break;
}
}
rpc(state, "agent", "applyMutation",
{{"type", "insertNode"}, {"parentId", helperId},
{"role", "annotations"},
{"node", {{"concept", "IntentAnnotation"},
{"properties", {{"summary", "placeholder helper"},
{"category", "initialization"}}}}}});
rpc(state, "agent", "saveAnnotatedAST", {{"path", "helper.py"}});
// List files — should show both funcs.py and helper.py
json listResp = rpc(state, "agent", "listAnnotatedFiles");
int fileCount = listResp["result"].value("count", 0);
expect(fileCount >= 2,
"listAnnotatedFiles returns " + std::to_string(fileCount) +
" sidecar files for multi-file project",
passed, failed);
}
// ---------------------------------------------------------------
// Test 7: All 5 annotation types in compact AST semantic field
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("agent", AgentRole::Refactor);
std::string src3 = "def compute(x):\n return x * 2\n";
rpc(state, "agent", "openFile",
{{"path", "compute.py"}, {"content", src3}});
std::string fnId;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" && getNodeName(c) == "compute") {
fnId = c->id;
break;
}
}
// Add all 5 annotation types
rpc(state, "agent", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "IntentAnnotation"},
{"properties", {{"summary", "doubles input"},
{"category", "computation"}}}}}});
rpc(state, "agent", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "ComplexityAnnotation"},
{"properties", {{"timeComplexity", "O(1)"},
{"cognitiveComplexity", 1},
{"linesOfLogic", 1}}}}}});
rpc(state, "agent", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "RiskAnnotation"},
{"properties", {{"level", "low"},
{"reason", "pure function"},
{"dependentCount", 0}}}}}});
rpc(state, "agent", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "ContractAnnotation"},
{"properties", {{"preconditions", "x is numeric"},
{"postconditions", "returns x*2"},
{"returnShape", "number"},
{"sideEffects", "none"}}}}}});
rpc(state, "agent", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "SemanticTagAnnotation"},
{"properties", {{"tags", json::array({"@pure", "@math"})}}}}}});
json resp = rpc(state, "agent", "getAST", {{"compact", true}});
json nodes = resp["result"]["nodes"];
bool allFields = false;
for (const auto& n : nodes) {
if (n.value("name", "") == "compute" && n.contains("semantic")) {
auto sem = n["semantic"];
allFields = sem.contains("intent") &&
sem.contains("complexity") &&
sem.contains("risk") &&
sem.contains("contract") &&
sem.contains("tags");
}
}
expect(allFields,
"Compact AST has all 5 semantic annotation fields for compute()",
passed, failed);
}
// ---------------------------------------------------------------
// Test 8: Sidecar roundtrip preserves all 5 annotation types
// ---------------------------------------------------------------
{
HeadlessEditorState state;
state.defaultLanguage = "python";
state.workspaceRoot = tmpDir.string();
state.setAgentRole("agent", AgentRole::Refactor);
std::string src3 = "def compute(x):\n return x * 2\n";
rpc(state, "agent", "openFile",
{{"path", "compute.py"}, {"content", src3}});
std::string fnId;
for (auto* c : state.activeAST()->allChildren()) {
if (c->conceptType == "Function" && getNodeName(c) == "compute") {
fnId = c->id;
break;
}
}
// Add all 5 types
rpc(state, "agent", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "IntentAnnotation"},
{"properties", {{"summary", "doubles"},
{"category", "math"}}}}}});
rpc(state, "agent", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "ComplexityAnnotation"},
{"properties", {{"timeComplexity", "O(1)"},
{"cognitiveComplexity", 1},
{"linesOfLogic", 1}}}}}});
rpc(state, "agent", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "RiskAnnotation"},
{"properties", {{"level", "low"},
{"reason", "pure"},
{"dependentCount", 0}}}}}});
rpc(state, "agent", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "ContractAnnotation"},
{"properties", {{"preconditions", "x numeric"},
{"postconditions", "returns x*2"},
{"returnShape", "number"},
{"sideEffects", "none"}}}}}});
rpc(state, "agent", "applyMutation",
{{"type", "insertNode"}, {"parentId", fnId},
{"role", "annotations"},
{"node", {{"concept", "SemanticTagAnnotation"},
{"properties", {{"tags", json::array({"@pure"})}}}}}});
// Save sidecar
rpc(state, "agent", "saveAnnotatedAST", {{"path", "compute.py"}});
// Load into fresh state
HeadlessEditorState state2;
state2.defaultLanguage = "python";
state2.workspaceRoot = tmpDir.string();
state2.setAgentRole("agent", AgentRole::Refactor);
rpc(state2, "agent", "openFile",
{{"path", "compute.py"}, {"content", src3}});
rpc(state2, "agent", "loadAnnotatedAST", {{"path", "compute.py"}});
// Count annotation types
int count = 0;
bool hasIntent = false, hasComplexity = false, hasRisk = false,
hasContract = false, hasTags = false;
for (auto* c : state2.activeAST()->allChildren()) {
if (c->conceptType == "Function" && getNodeName(c) == "compute") {
for (auto* a : c->getChildren("annotations")) {
if (a->conceptType == "IntentAnnotation") { hasIntent = true; ++count; }
if (a->conceptType == "ComplexityAnnotation") { hasComplexity = true; ++count; }
if (a->conceptType == "RiskAnnotation") { hasRisk = true; ++count; }
if (a->conceptType == "ContractAnnotation") { hasContract = true; ++count; }
if (a->conceptType == "SemanticTagAnnotation") { hasTags = true; ++count; }
}
}
}
expect(count == 5 && hasIntent && hasComplexity && hasRisk &&
hasContract && hasTags,
"Sidecar roundtrip preserves all 5 annotation types (" +
std::to_string(count) + "/5)",
passed, failed);
}
// Cleanup
std::filesystem::remove_all(tmpDir);
std::cout << "\n=== Step 268 Results: " << passed << " passed, "
<< failed << " failed ===\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -217,7 +217,7 @@ int main() {
WebSocketAgentServer server(std::move(transport));
// Register a custom handler that knows about "getAST"
server.setRequestHandler([](const json& request) -> json {
server.setRequestHandler([](const json& request, const std::string&) -> json {
std::string method = request.at("method").get<std::string>();
json response;
response["jsonrpc"] = "2.0";