Steps 284-289: Phase 10e — environment layer, host boundary nodes, MCP env tools (200/200 tests)

Completes Sprint 10. Adds EnvironmentSpec AST node, capability validation
(E0501), annotation compatibility checks (E0502-E0505), env-aware lowering
hints, 3 host boundary AST nodes (HostCall, ScheduleTask, ModuleLoad),
and 4 MCP environment tools. 38 MCP tools total.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill
2026-02-13 05:56:02 +00:00
parent ed8618e49f
commit 8cbeef5af4
8 changed files with 1284 additions and 11 deletions

View File

@@ -1734,4 +1734,52 @@ target_link_libraries(step283_test PRIVATE
tree_sitter_java tree_sitter_rust tree_sitter_go
tree_sitter_org)
# Step 284: EnvironmentSpec schema & AST node
add_executable(step284_test tests/step284_test.cpp)
target_include_directories(step284_test PRIVATE src)
target_link_libraries(step284_test PRIVATE nlohmann_json::nlohmann_json)
# Step 285: Capability vocabulary & validation
add_executable(step285_test tests/step285_test.cpp)
target_include_directories(step285_test PRIVATE src)
target_link_libraries(step285_test PRIVATE nlohmann_json::nlohmann_json)
# Step 286: Environment-aware pipeline hooks
add_executable(step286_test tests/step286_test.cpp)
target_include_directories(step286_test PRIVATE src)
target_link_libraries(step286_test PRIVATE
nlohmann_json::nlohmann_json
unofficial::tree-sitter::tree-sitter
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go
tree_sitter_org)
# Step 287: Environment-aware lowering decisions
add_executable(step287_test tests/step287_test.cpp)
target_include_directories(step287_test PRIVATE src)
target_link_libraries(step287_test PRIVATE
nlohmann_json::nlohmann_json
unofficial::tree-sitter::tree-sitter
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go
tree_sitter_org)
# Step 288: Host boundary AST nodes
add_executable(step288_test tests/step288_test.cpp)
target_include_directories(step288_test PRIVATE src)
target_link_libraries(step288_test PRIVATE nlohmann_json::nlohmann_json)
# Step 289: Phase 10e integration tests
add_executable(step289_test tests/step289_test.cpp)
target_include_directories(step289_test PRIVATE src)
target_link_libraries(step289_test PRIVATE
nlohmann_json::nlohmann_json
unofficial::tree-sitter::tree-sitter
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go
tree_sitter_org)
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)

View File

@@ -1161,6 +1161,81 @@ private:
};
}
// ---------------------------------------------------------------
// Step 286: Environment layer tools
// ---------------------------------------------------------------
void registerEnvironmentTools() {
// whetstone_set_environment
tools_.push_back({"whetstone_set_environment",
"Set the environment spec on the active module. Defines "
"scheduler, memory model, capabilities, constraints, "
"exception model, and FFI style. Replaces any existing env.",
{{"type", "object"}, {"properties", {
{"envId", {{"type", "string"},
{"description", "Environment identifier (e.g. posix_process, browser, jvm)"}}},
{"scheduler", {{"type", "string"},
{"enum", {"event_loop", "threads", "fibers", "coroutines", "single_thread"}},
{"description", "Scheduling model"}}},
{"memory", {{"type", "string"},
{"enum", {"manual", "raii", "refcount", "tracing_gc", "region"}},
{"description", "Memory management model"}}},
{"capabilities", {{"type", "array"}, {"items", {{"type", "string"}}},
{"description", "Available capabilities (io.fs, io.net, threads, etc.)"}}},
{"constraints", {{"type", "array"}, {"items", {{"type", "string"}}},
{"description", "Environment constraints (no_jit, no_threads, etc.)"}}},
{"exceptions", {{"type", "string"},
{"enum", {"unwind", "checked", "typed", "untyped"}},
{"description", "Exception handling model"}}},
{"ffi", {{"type", "string"},
{"enum", {"c_abi", "dynamic_linking", "none"}},
{"description", "Foreign function interface style"}}}
}}, {"required", {"envId"}}}
});
toolHandlers_["whetstone_set_environment"] =
[this](const json& args) {
return callWhetstone("setEnvironment", args);
};
// whetstone_get_environment
tools_.push_back({"whetstone_get_environment",
"Get the current environment spec from the active module. "
"Returns hasEnvironment=false if none is set.",
{{"type", "object"}, {"properties", json::object()}}
});
toolHandlers_["whetstone_get_environment"] =
[this](const json& args) {
return callWhetstone("getEnvironment", args);
};
// whetstone_validate_environment
tools_.push_back({"whetstone_validate_environment",
"Validate the active module against its environment spec. "
"Checks capability requirements (E0501) and annotation "
"compatibility (E0502-E0505). Returns diagnostics array.",
{{"type", "object"}, {"properties", json::object()}}
});
toolHandlers_["whetstone_validate_environment"] =
[this](const json& args) {
return callWhetstone("validateEnvironment", args);
};
// whetstone_get_lowering_hints
tools_.push_back({"whetstone_get_lowering_hints",
"Get environment-aware lowering hints for a node. Returns "
"code generation patterns based on scheduler, memory, and "
"exception models (e.g. callback, std_async, try_catch).",
{{"type", "object"}, {"properties", {
{"nodeId", {{"type", "string"},
{"description",
"Node ID (optional, defaults to module root)"}}}
}}}
});
toolHandlers_["whetstone_get_lowering_hints"] =
[this](const json& args) {
return callWhetstone("getLoweringHints", args);
};
}
void registerWhetstoneTools() {
registerASTTools();
registerAnnotationTools();
@@ -1171,5 +1246,6 @@ private:
registerSaveUndoTools();
registerSidecarTools();
registerSemanticAnnotationTools();
registerEnvironmentTools();
}
};

View File

@@ -7,6 +7,9 @@
#include "ast/Function.h"
#include "ast/Serialization.h"
#include "EnvironmentSpec.h"
#include "ASTUtils.h"
#include "CompactAST.h"
#include "SidecarPersistence.h"
#include <nlohmann/json.hpp>
using json = nlohmann::json;

View File

@@ -0,0 +1,304 @@
// Step 286: Environment-Aware Pipeline Hooks (12 tests)
//
// Tests that validateEnvAnnotations detects incompatible annotations,
// that the RPC validateEnvironment method returns combined diagnostics,
// and that MCP environment tools are properly registered.
#include <cassert>
#include <iostream>
#include <string>
#include "ast/ASTNode.h"
#include "ast/Module.h"
#include "ast/Function.h"
#include "ast/Annotation.h"
#include "ast/Serialization.h"
#include "EnvironmentSpec.h"
#include "HeadlessEditorState.h"
#include "MCPServer.h"
#include <nlohmann/json.hpp>
using json = nlohmann::json;
static int passed = 0, failed = 0;
static void check(bool c, const std::string& n) {
if (c) { std::cout << " PASS: " << n << "\n"; ++passed; }
else { std::cout << " FAIL: " << n << "\n"; ++failed; }
}
// Helper: build a module with env + annotated function
static Module* makeEnvModule(const std::string& scheduler,
const std::string& memory) {
auto* mod = new Module(); mod->id = "mod1"; mod->name = "envtest";
auto* env = new EnvironmentSpec();
env->envId = "test_env";
env->scheduler = scheduler;
env->memory = memory;
mod->addChild("environment", env);
return mod;
}
// Test 1: @Parallel in single_thread → E0502
static void test_parallel_single_thread() {
auto* mod = makeEnvModule("single_thread", "manual");
auto* fn = new Function(); fn->id = "f1"; fn->name = "doWork";
auto* pa = new ParallelAnnotation();
pa->kind = "data";
fn->addChild("annotations", pa);
mod->addChild("functions", fn);
auto* env = static_cast<EnvironmentSpec*>(mod->getChildren("environment")[0]);
auto diags = validateEnvAnnotations(mod, env);
check(diags.size() == 1, "1 diagnostic for @Parallel in single_thread");
check(diags[0].message.find("E0502") != std::string::npos, "E0502 error code");
check(diags[0].severity == "error", "severity is error");
delete mod;
}
// Test 2: @ThreadModel in single_thread → E0503
static void test_threadmodel_single_thread() {
auto* mod = makeEnvModule("single_thread", "manual");
auto* fn = new Function(); fn->id = "f1"; fn->name = "spawn";
auto* tm = new ThreadModelAnnotation();
tm->model = "os";
fn->addChild("annotations", tm);
mod->addChild("functions", fn);
auto* env = static_cast<EnvironmentSpec*>(mod->getChildren("environment")[0]);
auto diags = validateEnvAnnotations(mod, env);
check(diags.size() == 1, "1 diagnostic for @ThreadModel in single_thread");
check(diags[0].message.find("E0503") != std::string::npos, "E0503 error code");
delete mod;
}
// Test 3: @Atomic in single_thread → E0504 warning
static void test_atomic_single_thread() {
auto* mod = makeEnvModule("single_thread", "manual");
auto* fn = new Function(); fn->id = "f1"; fn->name = "inc";
auto* at = new AtomicAnnotation();
at->consistency = "seq_cst";
fn->addChild("annotations", at);
mod->addChild("functions", fn);
auto* env = static_cast<EnvironmentSpec*>(mod->getChildren("environment")[0]);
auto diags = validateEnvAnnotations(mod, env);
check(diags.size() == 1, "1 diagnostic for @Atomic in single_thread");
check(diags[0].severity == "warning", "severity is warning (not error)");
check(diags[0].message.find("E0504") != std::string::npos, "E0504 code");
delete mod;
}
// Test 4: @Exec(async) in single_thread → E0505
static void test_exec_async_single_thread() {
auto* mod = makeEnvModule("single_thread", "manual");
auto* fn = new Function(); fn->id = "f1"; fn->name = "fetch";
auto* ea = new ExecAnnotation();
ea->mode = "async";
fn->addChild("annotations", ea);
mod->addChild("functions", fn);
auto* env = static_cast<EnvironmentSpec*>(mod->getChildren("environment")[0]);
auto diags = validateEnvAnnotations(mod, env);
check(diags.size() == 1, "1 diagnostic for @Exec(async) in single_thread");
check(diags[0].message.find("E0505") != std::string::npos, "E0505 code");
delete mod;
}
// Test 5: No diagnostics when scheduler is compatible
static void test_compatible_scheduler() {
auto* mod = makeEnvModule("threads", "manual");
auto* fn = new Function(); fn->id = "f1"; fn->name = "doWork";
auto* pa = new ParallelAnnotation();
pa->kind = "data";
fn->addChild("annotations", pa);
auto* ea = new ExecAnnotation();
ea->mode = "async";
fn->addChild("annotations", ea);
mod->addChild("functions", fn);
auto* env = static_cast<EnvironmentSpec*>(mod->getChildren("environment")[0]);
auto diags = validateEnvAnnotations(mod, env);
check(diags.empty(), "no diagnostics with threads scheduler");
delete mod;
}
// Test 6: Multiple incompatible annotations → multiple diagnostics
static void test_multiple_incompatible() {
auto* mod = makeEnvModule("single_thread", "manual");
auto* fn = new Function(); fn->id = "f1"; fn->name = "multi";
auto* pa = new ParallelAnnotation(); pa->kind = "task";
fn->addChild("annotations", pa);
auto* ea = new ExecAnnotation(); ea->mode = "async";
fn->addChild("annotations", ea);
auto* at = new AtomicAnnotation(); at->consistency = "relaxed";
fn->addChild("annotations", at);
mod->addChild("functions", fn);
auto* env = static_cast<EnvironmentSpec*>(mod->getChildren("environment")[0]);
auto diags = validateEnvAnnotations(mod, env);
check(diags.size() == 3, "3 diagnostics for 3 incompatible annotations");
delete mod;
}
// Test 7: validateEnvironment RPC combines cap + annotation diagnostics
static void test_rpc_validate_environment() {
HeadlessEditorState state;
state.agent.defaultRole = AgentRole::Refactor;
state.openBuffer("test.py",
"def worker():\n pass\n", "python");
// Set environment
json setReq = {{"jsonrpc", "2.0"}, {"id", 1},
{"method", "setEnvironment"},
{"params", {{"envId", "single_thread_env"},
{"scheduler", "single_thread"},
{"memory", "manual"},
{"capabilities", json::array({"io.fs"})}}}};
state.processAgentRequest(setReq, "s1");
// Add @Parallel annotation via mutation
Module* ast = state.activeAST();
auto fns = ast->getChildren("functions");
if (!fns.empty()) {
auto* pa = new ParallelAnnotation(); pa->kind = "data";
fns[0]->addChild("annotations", pa);
}
// Validate
json valReq = {{"jsonrpc", "2.0"}, {"id", 2},
{"method", "validateEnvironment"}};
json resp = state.processAgentRequest(valReq, "s1");
check(resp.contains("result"), "validateEnvironment returns result");
auto diagsArr = resp["result"]["diagnostics"];
check(diagsArr.is_array(), "diagnostics is array");
check(resp["result"]["count"].get<int>() >= 1,
"at least 1 diagnostic");
// Should have E0502 for @Parallel in single_thread
bool foundE0502 = false;
for (const auto& d : diagsArr) {
if (d["message"].get<std::string>().find("E0502") != std::string::npos)
foundE0502 = true;
}
check(foundE0502, "E0502 found in validateEnvironment response");
}
// Test 8: setEnvironment + getEnvironment RPC roundtrip
static void test_rpc_set_get_environment() {
HeadlessEditorState state;
state.agent.defaultRole = AgentRole::Refactor;
state.openBuffer("test.py", "x = 1\n", "python");
json setReq = {{"jsonrpc", "2.0"}, {"id", 1},
{"method", "setEnvironment"},
{"params", {{"envId", "browser"}, {"scheduler", "event_loop"},
{"memory", "tracing_gc"}, {"exceptions", "untyped"}}}};
json setResp = state.processAgentRequest(setReq, "s1");
check(setResp["result"]["success"] == true, "setEnvironment success");
check(setResp["result"]["envId"] == "browser", "envId in response");
json getReq = {{"jsonrpc", "2.0"}, {"id", 2},
{"method", "getEnvironment"}};
json getResp = state.processAgentRequest(getReq, "s1");
check(getResp["result"]["hasEnvironment"] == true, "hasEnvironment true");
check(getResp["result"]["envId"] == "browser", "retrieved envId");
check(getResp["result"]["scheduler"] == "event_loop", "scheduler matches");
}
// Test 9: getEnvironment with no env set
static void test_rpc_get_no_environment() {
HeadlessEditorState state;
state.agent.defaultRole = AgentRole::Refactor;
state.openBuffer("test.py", "x = 1\n", "python");
json req = {{"jsonrpc", "2.0"}, {"id", 1},
{"method", "getEnvironment"}};
json resp = state.processAgentRequest(req, "s1");
check(resp["result"]["hasEnvironment"] == false, "hasEnvironment false when no env");
}
// Test 10: Linter role can read but not write environment
static void test_linter_permission() {
HeadlessEditorState state;
state.agent.defaultRole = AgentRole::Linter;
state.openBuffer("test.py", "x = 1\n", "python");
// Linter cannot setEnvironment
json setReq = {{"jsonrpc", "2.0"}, {"id", 1},
{"method", "setEnvironment"},
{"params", {{"envId", "test"}}}};
json setResp = state.processAgentRequest(setReq, "s1");
check(setResp.contains("error"), "Linter denied setEnvironment");
// Linter can getEnvironment
json getReq = {{"jsonrpc", "2.0"}, {"id", 2},
{"method", "getEnvironment"}};
json getResp = state.processAgentRequest(getReq, "s1");
check(!getResp.contains("error") || getResp.contains("result"),
"Linter can getEnvironment");
}
// Test 11: MCP environment tools registered
static void test_mcp_env_tools() {
MCPServer mcp;
const auto& tools = mcp.getTools();
bool foundSetEnv = false, foundGetEnv = false,
foundValidate = false, foundLowering = false;
for (const auto& t : tools) {
if (t.name == "whetstone_set_environment") foundSetEnv = true;
if (t.name == "whetstone_get_environment") foundGetEnv = true;
if (t.name == "whetstone_validate_environment") foundValidate = true;
if (t.name == "whetstone_get_lowering_hints") foundLowering = true;
}
check(foundSetEnv, "whetstone_set_environment registered");
check(foundGetEnv, "whetstone_get_environment registered");
check(foundValidate, "whetstone_validate_environment registered");
check(foundLowering, "whetstone_get_lowering_hints registered");
}
// Test 12: setEnvironment replaces existing env
static void test_replace_environment() {
HeadlessEditorState state;
state.agent.defaultRole = AgentRole::Refactor;
state.openBuffer("test.py", "x = 1\n", "python");
// Set first env
json set1 = {{"jsonrpc", "2.0"}, {"id", 1},
{"method", "setEnvironment"},
{"params", {{"envId", "posix"}, {"scheduler", "threads"}}}};
state.processAgentRequest(set1, "s1");
// Replace with second env
json set2 = {{"jsonrpc", "2.0"}, {"id", 2},
{"method", "setEnvironment"},
{"params", {{"envId", "browser"}, {"scheduler", "event_loop"}}}};
state.processAgentRequest(set2, "s1");
// Verify only the new one exists
json getReq = {{"jsonrpc", "2.0"}, {"id", 3},
{"method", "getEnvironment"}};
json resp = state.processAgentRequest(getReq, "s1");
check(resp["result"]["envId"] == "browser", "env replaced to browser");
check(resp["result"]["scheduler"] == "event_loop",
"scheduler replaced to event_loop");
// Verify only 1 environment child
Module* ast = state.activeAST();
check(ast->getChildren("environment").size() == 1,
"only 1 EnvironmentSpec after replacement");
}
int main() {
std::cout << "=== Step 286: Environment-Aware Pipeline Hooks ===\n";
test_parallel_single_thread();
test_threadmodel_single_thread();
test_atomic_single_thread();
test_exec_async_single_thread();
test_compatible_scheduler();
test_multiple_incompatible();
test_rpc_validate_environment();
test_rpc_set_get_environment();
test_rpc_get_no_environment();
test_linter_permission();
test_mcp_env_tools();
test_replace_environment();
std::cout << "\nResults: " << passed << "/" << (passed+failed) << "\n";
return failed > 0 ? 1 : 0;
}

View File

@@ -0,0 +1,288 @@
// Step 287: Environment-Aware Lowering Decisions (12 tests)
//
// Tests getLoweringHints — environment-aware code generation patterns
// based on scheduler, memory, and exception models. Also tests
// the getLoweringHints RPC method through HeadlessEditorState.
#include <cassert>
#include <iostream>
#include <string>
#include "ast/ASTNode.h"
#include "ast/Module.h"
#include "ast/Function.h"
#include "ast/Annotation.h"
#include "ast/Serialization.h"
#include "EnvironmentSpec.h"
#include "HeadlessEditorState.h"
#include <nlohmann/json.hpp>
using json = nlohmann::json;
static int passed = 0, failed = 0;
static void check(bool c, const std::string& n) {
if (c) { std::cout << " PASS: " << n << "\n"; ++passed; }
else { std::cout << " FAIL: " << n << "\n"; ++failed; }
}
// Test 1: @Exec(async) + event_loop → callback pattern
static void test_async_event_loop() {
auto* fn = new Function(); fn->id = "f1"; fn->name = "fetch";
auto* ea = new ExecAnnotation(); ea->mode = "async";
fn->addChild("annotations", ea);
EnvironmentSpec env;
env.scheduler = "event_loop";
env.memory = "tracing_gc";
env.exceptions = "untyped";
auto hints = getLoweringHints(fn, &env);
bool foundCallback = false;
for (const auto& h : hints)
if (h.pattern == "callback") foundCallback = true;
check(foundCallback, "async + event_loop → callback pattern");
delete fn;
}
// Test 2: @Exec(async) + threads → std_async pattern
static void test_async_threads() {
auto* fn = new Function(); fn->id = "f1"; fn->name = "compute";
auto* ea = new ExecAnnotation(); ea->mode = "async";
fn->addChild("annotations", ea);
EnvironmentSpec env;
env.scheduler = "threads";
env.memory = "manual";
env.exceptions = "unwind";
auto hints = getLoweringHints(fn, &env);
bool foundStdAsync = false;
for (const auto& h : hints)
if (h.pattern == "std_async") foundStdAsync = true;
check(foundStdAsync, "async + threads → std_async pattern");
delete fn;
}
// Test 3: @Exec(async) + coroutines → coroutine pattern
static void test_async_coroutines() {
auto* fn = new Function(); fn->id = "f1"; fn->name = "stream";
auto* ea = new ExecAnnotation(); ea->mode = "async";
fn->addChild("annotations", ea);
EnvironmentSpec env;
env.scheduler = "coroutines";
env.memory = "raii";
env.exceptions = "typed";
auto hints = getLoweringHints(fn, &env);
bool foundCoroutine = false;
for (const auto& h : hints)
if (h.pattern == "coroutine") foundCoroutine = true;
check(foundCoroutine, "async + coroutines → coroutine pattern");
delete fn;
}
// Test 4: tracing_gc → suppress_dealloc
static void test_gc_suppress_dealloc() {
auto* fn = new Function(); fn->id = "f1"; fn->name = "alloc";
EnvironmentSpec env;
env.scheduler = "event_loop";
env.memory = "tracing_gc";
auto hints = getLoweringHints(fn, &env);
bool found = false;
for (const auto& h : hints)
if (h.pattern == "suppress_dealloc") found = true;
check(found, "tracing_gc → suppress_dealloc");
delete fn;
}
// Test 5: manual memory → explicit_delete
static void test_manual_explicit_delete() {
auto* fn = new Function(); fn->id = "f1"; fn->name = "alloc";
EnvironmentSpec env;
env.scheduler = "threads";
env.memory = "manual";
auto hints = getLoweringHints(fn, &env);
bool found = false;
for (const auto& h : hints)
if (h.pattern == "explicit_delete") found = true;
check(found, "manual → explicit_delete");
delete fn;
}
// Test 6: RAII → raii_cleanup
static void test_raii_cleanup() {
auto* fn = new Function(); fn->id = "f1"; fn->name = "resource";
EnvironmentSpec env;
env.scheduler = "threads";
env.memory = "raii";
auto hints = getLoweringHints(fn, &env);
bool found = false;
for (const auto& h : hints)
if (h.pattern == "raii_cleanup") found = true;
check(found, "raii → raii_cleanup");
delete fn;
}
// Test 7: unwind exceptions → try_catch
static void test_unwind_try_catch() {
auto* fn = new Function(); fn->id = "f1"; fn->name = "parse";
EnvironmentSpec env;
env.scheduler = "threads";
env.memory = "manual";
env.exceptions = "unwind";
auto hints = getLoweringHints(fn, &env);
bool found = false;
for (const auto& h : hints)
if (h.pattern == "try_catch") found = true;
check(found, "unwind → try_catch");
delete fn;
}
// Test 8: typed exceptions → expected
static void test_typed_expected() {
auto* fn = new Function(); fn->id = "f1"; fn->name = "safe";
EnvironmentSpec env;
env.scheduler = "threads";
env.memory = "raii";
env.exceptions = "typed";
auto hints = getLoweringHints(fn, &env);
bool found = false;
for (const auto& h : hints)
if (h.pattern == "expected") found = true;
check(found, "typed → expected (std::expected)");
delete fn;
}
// Test 9: Combined hints — async + manual + unwind
static void test_combined_hints() {
auto* fn = new Function(); fn->id = "f1"; fn->name = "combo";
auto* ea = new ExecAnnotation(); ea->mode = "async";
fn->addChild("annotations", ea);
EnvironmentSpec env;
env.scheduler = "threads";
env.memory = "manual";
env.exceptions = "unwind";
auto hints = getLoweringHints(fn, &env);
// Should get: std_async + explicit_delete + try_catch
check(hints.size() >= 3, "at least 3 hints for combined env");
bool hasAsync = false, hasDelete = false, hasTryCatch = false;
for (const auto& h : hints) {
if (h.pattern == "std_async") hasAsync = true;
if (h.pattern == "explicit_delete") hasDelete = true;
if (h.pattern == "try_catch") hasTryCatch = true;
}
check(hasAsync && hasDelete && hasTryCatch,
"all 3 patterns present in combined hints");
delete fn;
}
// Test 10: getLoweringHints RPC method
static void test_rpc_lowering_hints() {
HeadlessEditorState state;
state.agent.defaultRole = AgentRole::Refactor;
state.openBuffer("test.py",
"def worker():\n pass\n", "python");
// Set environment
json setReq = {{"jsonrpc", "2.0"}, {"id", 1},
{"method", "setEnvironment"},
{"params", {{"envId", "posix"}, {"scheduler", "threads"},
{"memory", "manual"}, {"exceptions", "unwind"}}}};
state.processAgentRequest(setReq, "s1");
// Add @Exec(async) to worker function
Module* ast = state.activeAST();
auto fns = ast->getChildren("functions");
if (!fns.empty()) {
auto* ea = new ExecAnnotation(); ea->mode = "async";
fns[0]->addChild("annotations", ea);
}
// Get lowering hints for the function
std::string fnId = fns.empty() ? "" : fns[0]->id;
json hintReq = {{"jsonrpc", "2.0"}, {"id", 2},
{"method", "getLoweringHints"},
{"params", {{"nodeId", fnId}}}};
json resp = state.processAgentRequest(hintReq, "s1");
check(resp.contains("result"), "getLoweringHints returns result");
check(resp["result"]["hints"].is_array(), "hints is array");
check(resp["result"]["count"].get<int>() >= 1, "at least 1 hint");
// Should include std_async for threads+async
bool foundStdAsync = false;
for (const auto& h : resp["result"]["hints"]) {
if (h["pattern"] == "std_async") foundStdAsync = true;
}
check(foundStdAsync, "RPC hints include std_async");
}
// Test 11: getLoweringHints with no nodeId → module-level hints
static void test_rpc_lowering_module_level() {
HeadlessEditorState state;
state.agent.defaultRole = AgentRole::Refactor;
state.openBuffer("test.py", "x = 1\n", "python");
json setReq = {{"jsonrpc", "2.0"}, {"id", 1},
{"method", "setEnvironment"},
{"params", {{"envId", "browser"}, {"scheduler", "event_loop"},
{"memory", "tracing_gc"}, {"exceptions", "untyped"}}}};
state.processAgentRequest(setReq, "s1");
// No nodeId → module-level
json hintReq = {{"jsonrpc", "2.0"}, {"id", 2},
{"method", "getLoweringHints"},
{"params", json::object()}};
json resp = state.processAgentRequest(hintReq, "s1");
check(resp.contains("result"), "module-level hints returns result");
// Should get suppress_dealloc from tracing_gc
bool foundSuppressDealloc = false;
for (const auto& h : resp["result"]["hints"]) {
if (h["pattern"] == "suppress_dealloc") foundSuppressDealloc = true;
}
check(foundSuppressDealloc, "module-level includes suppress_dealloc");
}
// Test 12: null safety
static void test_null_safety() {
auto hints1 = getLoweringHints(nullptr, nullptr);
check(hints1.empty(), "null node + null env → empty");
auto* fn = new Function(); fn->id = "f1";
auto hints2 = getLoweringHints(fn, nullptr);
check(hints2.empty(), "null env → empty");
EnvironmentSpec env;
auto hints3 = getLoweringHints(nullptr, &env);
check(hints3.empty(), "null node → empty");
delete fn;
}
int main() {
std::cout << "=== Step 287: Environment-Aware Lowering Decisions ===\n";
test_async_event_loop();
test_async_threads();
test_async_coroutines();
test_gc_suppress_dealloc();
test_manual_explicit_delete();
test_raii_cleanup();
test_unwind_try_catch();
test_typed_expected();
test_combined_hints();
test_rpc_lowering_hints();
test_rpc_lowering_module_level();
test_null_safety();
std::cout << "\nResults: " << passed << "/" << (passed+failed) << "\n";
return failed > 0 ? 1 : 0;
}

View File

@@ -0,0 +1,236 @@
// Step 288: Host Boundary AST Nodes (12 tests)
//
// Tests HostCall, ScheduleTask, ModuleLoad AST node construction,
// JSON roundtrip serialization, capability validation, and compact
// AST integration.
#include <cassert>
#include <iostream>
#include <string>
#include "ast/ASTNode.h"
#include "ast/Module.h"
#include "ast/Function.h"
#include "ast/HostBoundary.h"
#include "ast/Serialization.h"
#include "ASTUtils.h"
#include "CompactAST.h"
#include "EnvironmentSpec.h"
#include <nlohmann/json.hpp>
using json = nlohmann::json;
static int passed = 0, failed = 0;
static void check(bool c, const std::string& n) {
if (c) { std::cout << " PASS: " << n << "\n"; ++passed; }
else { std::cout << " FAIL: " << n << "\n"; ++failed; }
}
// Test 1: HostCall default construction
static void test_hostcall_default() {
HostCall hc;
check(hc.conceptType == "HostCall", "conceptType is HostCall");
check(hc.capability.empty(), "default capability empty");
check(hc.name.empty(), "default name empty");
}
// Test 2: HostCall parameterized construction
static void test_hostcall_params() {
HostCall hc("io.fs", "readFile");
check(hc.conceptType == "HostCall", "conceptType is HostCall");
check(hc.capability == "io.fs", "capability set to io.fs");
check(hc.name == "readFile", "name set to readFile");
}
// Test 3: HostCall JSON roundtrip
static void test_hostcall_roundtrip() {
auto* hc = new HostCall("io.net", "listen");
hc->id = "hc1";
json j = toJson(hc);
check(j["concept"] == "HostCall", "JSON concept");
check(j["properties"]["capability"] == "io.net", "JSON capability");
check(j["properties"]["name"] == "listen", "JSON name");
ASTNode* restored = fromJson(j);
check(restored != nullptr, "fromJson non-null");
check(restored->conceptType == "HostCall", "restored type");
auto* rhc = static_cast<HostCall*>(restored);
check(rhc->capability == "io.net", "roundtrip capability");
check(rhc->name == "listen", "roundtrip name");
delete hc;
delete restored;
}
// Test 4: ScheduleTask default construction
static void test_schedule_default() {
ScheduleTask st;
check(st.conceptType == "ScheduleTask", "conceptType is ScheduleTask");
check(st.queue.empty(), "default queue empty");
}
// Test 5: ScheduleTask JSON roundtrip
static void test_schedule_roundtrip() {
auto* st = new ScheduleTask();
st->id = "st1";
st->queue = "microtask";
json j = toJson(st);
check(j["concept"] == "ScheduleTask", "JSON concept");
check(j["properties"]["queue"] == "microtask", "JSON queue");
ASTNode* restored = fromJson(j);
check(restored != nullptr, "fromJson non-null");
auto* rst = static_cast<ScheduleTask*>(restored);
check(rst->queue == "microtask", "roundtrip queue");
delete st;
delete restored;
}
// Test 6: ModuleLoad default construction
static void test_moduleload_default() {
ModuleLoad ml;
check(ml.conceptType == "ModuleLoad", "conceptType is ModuleLoad");
check(ml.moduleName.empty(), "default moduleName empty");
check(ml.mode.empty(), "default mode empty");
}
// Test 7: ModuleLoad JSON roundtrip
static void test_moduleload_roundtrip() {
auto* ml = new ModuleLoad();
ml->id = "ml1";
ml->moduleName = "math";
ml->mode = "static";
json j = toJson(ml);
check(j["concept"] == "ModuleLoad", "JSON concept");
check(j["properties"]["moduleName"] == "math", "JSON moduleName");
check(j["properties"]["mode"] == "static", "JSON mode");
ASTNode* restored = fromJson(j);
check(restored != nullptr, "fromJson non-null");
auto* rml = static_cast<ModuleLoad*>(restored);
check(rml->moduleName == "math", "roundtrip moduleName");
check(rml->mode == "static", "roundtrip mode");
delete ml;
delete restored;
}
// Test 8: createNode for all host boundary types
static void test_create_nodes() {
ASTNode* hc = createNode("HostCall");
check(hc != nullptr && hc->conceptType == "HostCall",
"createNode HostCall");
delete hc;
ASTNode* st = createNode("ScheduleTask");
check(st != nullptr && st->conceptType == "ScheduleTask",
"createNode ScheduleTask");
delete st;
ASTNode* ml = createNode("ModuleLoad");
check(ml != nullptr && ml->conceptType == "ModuleLoad",
"createNode ModuleLoad");
delete ml;
}
// Test 9: HostCall in function body AST
static void test_hostcall_in_function() {
auto* mod = new Module(); mod->id = "mod1"; mod->name = "test";
auto* fn = new Function(); fn->id = "f1"; fn->name = "readConfig";
auto* hc = new HostCall("io.fs", "readFile");
hc->id = "hc1";
fn->addChild("body", hc);
mod->addChild("functions", fn);
json j = toJson(mod);
ASTNode* restored = fromJson(j);
auto fns = restored->getChildren("functions");
check(fns.size() == 1, "restored has 1 function");
auto body = fns[0]->getChildren("body");
check(body.size() == 1, "function has 1 body node");
check(body[0]->conceptType == "HostCall", "body node is HostCall");
auto* rhc = static_cast<HostCall*>(body[0]);
check(rhc->capability == "io.fs", "nested HostCall capability");
check(rhc->name == "readFile", "nested HostCall name");
delete mod;
delete restored;
}
// Test 10: Compact AST getNodeName for host boundary nodes
static void test_compact_node_names() {
auto* hc = new HostCall("io.fs", "readFile");
hc->id = "hc1";
std::string hcName = getNodeName(hc);
check(hcName == "readFile", "HostCall node name is readFile");
auto* st = new ScheduleTask();
st->id = "st1"; st->queue = "microtask";
std::string stName = getNodeName(st);
// ScheduleTask may return queue as name or empty — just check no crash
check(!stName.empty() || stName.empty(), "ScheduleTask getNodeName no crash");
auto* ml = new ModuleLoad();
ml->id = "ml1"; ml->moduleName = "math";
std::string mlName = getNodeName(ml);
check(mlName == "math", "ModuleLoad node name is math");
delete hc; delete st; delete ml;
}
// Test 11: HostCall capability validation against env
static void test_hostcall_cap_validation() {
auto* mod = new Module(); mod->id = "mod1"; mod->name = "test";
auto* env = new EnvironmentSpec();
env->envId = "restricted";
env->capabilities = {"io.fs"};
mod->addChild("environment", env);
auto* fn = new Function(); fn->id = "f1"; fn->name = "serve";
auto* cr = new CapabilityRequirement();
cr->capability = "io.net"; cr->required = true;
fn->addChild("annotations", cr);
mod->addChild("functions", fn);
auto diags = validateCapabilities(mod, env);
check(diags.size() == 1, "1 diag for missing io.net");
check(diags[0].message.find("E0501") != std::string::npos,
"E0501 for missing capability");
delete mod;
}
// Test 12: ScheduleTask with body children
static void test_schedule_with_body() {
auto* st = new ScheduleTask();
st->id = "st1";
st->queue = "thread";
auto* hc = new HostCall("io.fs", "writeFile");
hc->id = "hc1";
st->addChild("body", hc);
json j = toJson(st);
ASTNode* restored = fromJson(j);
check(restored != nullptr, "roundtrip non-null");
auto body = restored->getChildren("body");
check(body.size() == 1, "ScheduleTask has 1 body child");
check(body[0]->conceptType == "HostCall", "body child is HostCall");
delete st;
delete restored;
}
int main() {
std::cout << "=== Step 288: Host Boundary AST Nodes ===\n";
test_hostcall_default();
test_hostcall_params();
test_hostcall_roundtrip();
test_schedule_default();
test_schedule_roundtrip();
test_moduleload_default();
test_moduleload_roundtrip();
test_create_nodes();
test_hostcall_in_function();
test_compact_node_names();
test_hostcall_cap_validation();
test_schedule_with_body();
std::cout << "\nResults: " << passed << "/" << (passed+failed) << "\n";
return failed > 0 ? 1 : 0;
}

View File

@@ -0,0 +1,314 @@
// Step 289: Phase 10e Environment Layer Integration Tests (8 tests)
//
// End-to-end integration tests exercising the full environment layer:
// EnvironmentSpec, capability validation, annotation compatibility,
// lowering hints, host boundary nodes, and MCP tool counts.
#include <cassert>
#include <iostream>
#include <string>
#include "ast/ASTNode.h"
#include "ast/Module.h"
#include "ast/Function.h"
#include "ast/Annotation.h"
#include "ast/HostBoundary.h"
#include "ast/Serialization.h"
#include "EnvironmentSpec.h"
#include "ASTUtils.h"
#include "CompactAST.h"
#include "HeadlessEditorState.h"
#include "MCPServer.h"
#include <nlohmann/json.hpp>
using json = nlohmann::json;
static int passed = 0, failed = 0;
static void check(bool c, const std::string& n) {
if (c) { std::cout << " PASS: " << n << "\n"; ++passed; }
else { std::cout << " FAIL: " << n << "\n"; ++failed; }
}
// Test 1: Full workflow — setEnvironment → add annotations → validate → getLoweringHints
static void test_full_env_workflow() {
HeadlessEditorState state;
state.agent.defaultRole = AgentRole::Refactor;
state.openBuffer("app.py",
"def fetch_data():\n pass\n"
"def process():\n pass\n", "python");
// 1. Set environment
json setEnv = {{"jsonrpc", "2.0"}, {"id", 1},
{"method", "setEnvironment"},
{"params", {{"envId", "posix_server"}, {"scheduler", "threads"},
{"memory", "manual"}, {"exceptions", "unwind"},
{"capabilities", json::array({"io.fs", "io.net", "threads"})}}}};
json envResp = state.processAgentRequest(setEnv, "s1");
check(envResp["result"]["success"] == true, "environment set");
// 2. Add @Exec(async) annotation to fetch_data
Module* ast = state.activeAST();
auto fns = ast->getChildren("functions");
check(fns.size() == 2, "2 functions parsed");
auto* ea = new ExecAnnotation(); ea->mode = "async";
fns[0]->addChild("annotations", ea);
// 3. Validate environment
json valReq = {{"jsonrpc", "2.0"}, {"id", 2},
{"method", "validateEnvironment"}};
json valResp = state.processAgentRequest(valReq, "s1");
check(valResp["result"]["count"].get<int>() == 0,
"0 diagnostics (threads supports async)");
// 4. Get lowering hints for fetch_data
json hintReq = {{"jsonrpc", "2.0"}, {"id", 3},
{"method", "getLoweringHints"},
{"params", {{"nodeId", fns[0]->id}}}};
json hintResp = state.processAgentRequest(hintReq, "s1");
check(hintResp["result"]["count"].get<int>() >= 3,
"multiple hints (std_async + explicit_delete + try_catch)");
}
// Test 2: Incompatible env detects issues and changing env clears them
static void test_env_switch_clears_issues() {
HeadlessEditorState state;
state.agent.defaultRole = AgentRole::Refactor;
state.openBuffer("ui.py",
"def render():\n pass\n", "python");
// Set single_thread env
json setEnv1 = {{"jsonrpc", "2.0"}, {"id", 1},
{"method", "setEnvironment"},
{"params", {{"envId", "embedded"}, {"scheduler", "single_thread"},
{"memory", "manual"}}}};
state.processAgentRequest(setEnv1, "s1");
// Add @Parallel (incompatible with single_thread)
Module* ast = state.activeAST();
auto fns = ast->getChildren("functions");
auto* pa = new ParallelAnnotation(); pa->kind = "data";
fns[0]->addChild("annotations", pa);
// Validate — should fail
json valReq = {{"jsonrpc", "2.0"}, {"id", 2},
{"method", "validateEnvironment"}};
json val1 = state.processAgentRequest(valReq, "s1");
check(val1["result"]["count"].get<int>() >= 1,
"at least 1 diagnostic in single_thread");
// Switch to threads env
json setEnv2 = {{"jsonrpc", "2.0"}, {"id", 3},
{"method", "setEnvironment"},
{"params", {{"envId", "server"}, {"scheduler", "threads"},
{"memory", "manual"}}}};
state.processAgentRequest(setEnv2, "s1");
// Validate again — should pass
json val2 = state.processAgentRequest(valReq, "s1");
check(val2["result"]["count"].get<int>() == 0,
"0 diagnostics after switching to threads");
}
// Test 3: Host boundary nodes in a function body survive AST roundtrip
static void test_host_boundary_roundtrip() {
auto* mod = new Module(); mod->id = "mod1"; mod->name = "server";
auto* fn = new Function(); fn->id = "f1"; fn->name = "handler";
auto* hc = new HostCall("io.net", "accept");
hc->id = "hc1";
fn->addChild("body", hc);
auto* st = new ScheduleTask();
st->id = "st1"; st->queue = "thread";
fn->addChild("body", st);
auto* ml = new ModuleLoad();
ml->id = "ml1"; ml->moduleName = "crypto"; ml->mode = "dynamic";
fn->addChild("body", ml);
mod->addChild("functions", fn);
// Full AST roundtrip
json j = toJson(mod);
ASTNode* restored = fromJson(j);
auto rFns = restored->getChildren("functions");
check(rFns.size() == 1, "1 function restored");
auto body = rFns[0]->getChildren("body");
check(body.size() == 3, "3 body nodes restored");
check(body[0]->conceptType == "HostCall", "body[0] is HostCall");
check(body[1]->conceptType == "ScheduleTask", "body[1] is ScheduleTask");
check(body[2]->conceptType == "ModuleLoad", "body[2] is ModuleLoad");
delete mod;
delete restored;
}
// Test 4: CapabilityRequirement + EnvironmentSpec validation end-to-end
static void test_capability_validation_e2e() {
HeadlessEditorState state;
state.agent.defaultRole = AgentRole::Refactor;
state.openBuffer("net.py",
"def serve():\n pass\n", "python");
// Set env without io.net
json setEnv = {{"jsonrpc", "2.0"}, {"id", 1},
{"method", "setEnvironment"},
{"params", {{"envId", "no_net"}, {"scheduler", "threads"},
{"capabilities", json::array({"io.fs"})}}}};
state.processAgentRequest(setEnv, "s1");
// Add CapabilityRequirement for io.net
Module* ast = state.activeAST();
auto fns = ast->getChildren("functions");
auto* cr = new CapabilityRequirement();
cr->capability = "io.net"; cr->required = true;
fns[0]->addChild("annotations", cr);
// Validate
json valReq = {{"jsonrpc", "2.0"}, {"id", 2},
{"method", "validateEnvironment"}};
json resp = state.processAgentRequest(valReq, "s1");
bool foundE0501 = false;
for (const auto& d : resp["result"]["diagnostics"]) {
if (d["message"].get<std::string>().find("E0501") != std::string::npos)
foundE0501 = true;
}
check(foundE0501, "E0501 for unmet io.net capability");
}
// Test 5: Lowering hints change when environment changes
static void test_lowering_hints_change_with_env() {
HeadlessEditorState state;
state.agent.defaultRole = AgentRole::Refactor;
state.openBuffer("async.py",
"def fetch():\n pass\n", "python");
Module* ast = state.activeAST();
auto fns = ast->getChildren("functions");
auto* ea = new ExecAnnotation(); ea->mode = "async";
fns[0]->addChild("annotations", ea);
// Set event_loop env
json setEnv1 = {{"jsonrpc", "2.0"}, {"id", 1},
{"method", "setEnvironment"},
{"params", {{"envId", "browser"}, {"scheduler", "event_loop"},
{"memory", "tracing_gc"}}}};
state.processAgentRequest(setEnv1, "s1");
json hintReq = {{"jsonrpc", "2.0"}, {"id", 2},
{"method", "getLoweringHints"},
{"params", {{"nodeId", fns[0]->id}}}};
json hints1 = state.processAgentRequest(hintReq, "s1");
bool hasCallback = false;
for (const auto& h : hints1["result"]["hints"])
if (h["pattern"] == "callback") hasCallback = true;
check(hasCallback, "event_loop env → callback pattern");
// Switch to threads env
json setEnv2 = {{"jsonrpc", "2.0"}, {"id", 3},
{"method", "setEnvironment"},
{"params", {{"envId", "server"}, {"scheduler", "threads"},
{"memory", "manual"}}}};
state.processAgentRequest(setEnv2, "s1");
json hints2 = state.processAgentRequest(hintReq, "s1");
bool hasStdAsync = false;
for (const auto& h : hints2["result"]["hints"])
if (h["pattern"] == "std_async") hasStdAsync = true;
check(hasStdAsync, "threads env → std_async pattern");
}
// Test 6: Batch query with environment methods
static void test_batch_env_queries() {
HeadlessEditorState state;
state.agent.defaultRole = AgentRole::Refactor;
state.openBuffer("batch.py",
"def work():\n pass\n", "python");
// Set env
json setEnv = {{"jsonrpc", "2.0"}, {"id", 1},
{"method", "setEnvironment"},
{"params", {{"envId", "jvm"}, {"scheduler", "threads"},
{"memory", "tracing_gc"}, {"exceptions", "unwind"}}}};
state.processAgentRequest(setEnv, "s1");
// Batch query: getEnvironment + validateEnvironment + getLoweringHints
json batchReq = {{"jsonrpc", "2.0"}, {"id", 2},
{"method", "batchQuery"},
{"params", {{"queries", json::array({
{{"method", "getEnvironment"}},
{{"method", "validateEnvironment"}},
{{"method", "getLoweringHints"}, {"params", json::object()}}
})}}}};
json resp = state.processAgentRequest(batchReq, "s1");
check(resp.contains("result"), "batchQuery returns result");
auto results = resp["result"]["results"];
check(results.is_array(), "results is array");
check(results.size() == 3, "3 sub-query results");
// Check getEnvironment result
check(results[0]["result"]["envId"] == "jvm",
"batch[0] getEnvironment returns jvm");
// Check validateEnvironment result
check(results[1]["result"].contains("diagnostics"),
"batch[1] validateEnvironment has diagnostics");
// Check getLoweringHints result
check(results[2]["result"].contains("hints"),
"batch[2] getLoweringHints has hints");
}
// Test 7: MCP tool count includes environment tools
static void test_mcp_tool_count() {
MCPServer mcp;
const auto& tools = mcp.getTools();
// Previous: 34 tools from Sprint 9 + sidecar + annotation
// + 4 new environment tools = 38
int envToolCount = 0;
for (const auto& t : tools) {
if (t.name.find("environment") != std::string::npos ||
t.name.find("lowering") != std::string::npos)
envToolCount++;
}
check(envToolCount == 4, "4 environment MCP tools registered");
check((int)tools.size() >= 38, "at least 38 total MCP tools");
}
// Test 8: EnvironmentSpec compact AST integration
static void test_env_compact_ast() {
auto* mod = new Module(); mod->id = "mod1"; mod->name = "compact_test";
auto* env = new EnvironmentSpec();
env->id = "env1"; env->envId = "posix";
env->scheduler = "threads"; env->memory = "raii";
mod->addChild("environment", env);
auto* fn = new Function(); fn->id = "f1"; fn->name = "worker";
auto* hc = new HostCall("io.fs", "readFile");
hc->id = "hc1";
fn->addChild("body", hc);
mod->addChild("functions", fn);
// Compact AST should include the host boundary node
json compact = toJsonCompactTree(mod);
check(compact.is_array(), "compact tree is array");
check(compact.size() >= 3, "at least 3 nodes (mod + fn + hostcall)");
// Find the HostCall in compact output
bool foundHostCall = false;
for (const auto& node : compact) {
if (node.value("type", "") == "HostCall") {
foundHostCall = true;
check(node.value("name", "") == "readFile",
"compact HostCall name is readFile");
}
}
check(foundHostCall, "HostCall found in compact AST");
delete mod;
}
int main() {
std::cout << "=== Step 289: Phase 10e Integration Tests ===\n";
test_full_env_workflow();
test_env_switch_clears_issues();
test_host_boundary_roundtrip();
test_capability_validation_e2e();
test_lowering_hints_change_with_env();
test_batch_env_queries();
test_mcp_tool_count();
test_env_compact_ast();
std::cout << "\nResults: " << passed << "/" << (passed+failed) << "\n";
return failed > 0 ? 1 : 0;
}

View File

@@ -743,9 +743,7 @@ Generic fallback added to getSemanticAnnotations RPC for extensible annotation t
## Phase 10e: Environment Layer (Steps 284-289)
**Status:** IN PROGRESS
### Completed so far:
**Status:** PASS (200/200 checks across steps 284289)
**New files created:**
- `editor/src/EnvironmentSpec.h` — EnvironmentSpec AST node (envId, envVersion, capabilities, constraints, scheduler, memory, bindingTimes, exceptions, ffi), CapabilityRequirement annotation, capability vocabulary (17 known capabilities), validateCapabilities() (E0501 diagnostics), validateEnvAnnotations() (E0502-E0505 diagnostics), getLoweringHints() (env-aware lowering patterns), envSpecToJson/envSpecFromJson helpers
@@ -757,15 +755,21 @@ Generic fallback added to getSemanticAnnotations RPC for extensible annotation t
- `SidecarPersistence.h` — CapabilityRequirement in isSemanticAnnotation
- `HeadlessAgentRPCHandler.h` — capabilityRequirement type in setSemanticAnnotation, 4 new RPCs: setEnvironment, getEnvironment, validateEnvironment, getLoweringHints
- `AgentPermissionPolicy.h` — getEnvironment/validateEnvironment/getLoweringHints (read-only), setEnvironment (mutation)
- `MCPServer.h` — registerEnvironmentTools() with 4 MCP tool definitions (whetstone_set_environment, whetstone_get_environment, whetstone_validate_environment, whetstone_get_lowering_hints)
**Test files written:**
**Test files:**
- `step284_test.cpp` — 12 tests (EnvironmentSpec schema, JSON roundtrip, module attachment)
- `step285_test.cpp` — 12 tests (capability vocabulary, validation, CapabilityRequirement)
- `step286_test.cpp` — 12 tests (validateEnvAnnotations E0502-E0505, RPC validate/set/get, Linter permissions, MCP tool registration, env replacement)
- `step287_test.cpp` — 12 tests (getLoweringHints: callback/std_async/coroutine, suppress_dealloc/explicit_delete/raii_cleanup, try_catch/expected, combined hints, RPC method, null safety)
- `step288_test.cpp` — 12 tests (HostCall/ScheduleTask/ModuleLoad construction, JSON roundtrip, createNode, compact AST names, capability validation, nested body children)
- `step289_test.cpp` — 8 integration tests (full env workflow, env switch clears issues, host boundary roundtrip, capability validation e2e, lowering hints change with env, batch env queries, MCP tool count 38+, compact AST with host boundary)
### Remaining:
- Write step286_test.cpp (environment-aware pipeline hooks, 12 tests)
- Write step287_test.cpp (environment-aware lowering decisions, 12 tests)
- Write step288_test.cpp (host boundary AST nodes, 12 tests)
- Write step289_test.cpp (Phase 10e integration tests, 8 tests)
- Add CMake targets for steps 284-289
- Build, test, fix, commit
**Key results:**
- Phase 10e complete: all 6 steps pass (200/200 checks across steps 284289)
- 4 environment RPCs: setEnvironment, getEnvironment, validateEnvironment, getLoweringHints
- 5 environment diagnostics: E0501 (unmet capability), E0502E0505 (annotation/scheduler incompatibility)
- 3 host boundary AST nodes: HostCall, ScheduleTask, ModuleLoad
- Lowering hint patterns: callback, std_async, coroutine, suppress_dealloc, explicit_delete, raii_cleanup, try_catch, expected, checked_throw
- 38 MCP tools total (was 34): +whetstone_set_environment, +whetstone_get_environment, +whetstone_validate_environment, +whetstone_get_lowering_hints
- Sprint 10 complete: all 5 phases pass (phases 10a10e)