Phase 10e WIP: environment layer core + host boundary nodes

EnvironmentSpec, CapabilityRequirement, HostCall, ScheduleTask, ModuleLoad
classes created. All wired through Serialization.h, CompactAST.h,
SidecarPersistence.h, HeadlessAgentRPCHandler.h (setEnvironment,
getEnvironment, validateEnvironment, getLoweringHints RPCs).
Tests for steps 284-285 written. Steps 286-289 tests remaining.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill
2026-02-12 18:41:16 +00:00
parent 090320f165
commit ed8618e49f
10 changed files with 981 additions and 3 deletions

View File

@@ -60,7 +60,10 @@ struct AgentPermissionPolicy {
method == "loadAnnotatedAST" ||
method == "listAnnotatedFiles" ||
method == "getSemanticAnnotations" ||
method == "getUnannotatedNodes") {
method == "getUnannotatedNodes" ||
method == "getEnvironment" ||
method == "validateEnvironment" ||
method == "getLoweringHints") {
return true;
}
@@ -81,7 +84,8 @@ struct AgentPermissionPolicy {
method == "redo" ||
method == "saveAnnotatedAST" ||
method == "setSemanticAnnotation" ||
method == "removeSemanticAnnotation") {
method == "removeSemanticAnnotation" ||
method == "setEnvironment") {
return role == AgentRole::Refactor || role == AgentRole::Generator;
}

View File

@@ -7,6 +7,8 @@
#include "ast/ASTNode.h"
#include "ast/Serialization.h"
#include "ast/Annotation.h"
#include "ast/HostBoundary.h"
#include "EnvironmentSpec.h"
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
@@ -319,6 +321,14 @@ inline json extractSemanticSummary(const ASTNode* node) {
if (!da->selection.empty()) obj["selection"] = da->selection;
if (!obj.empty()) sem["decision"] = obj;
}
// Environment Layer (Step 285)
else if (a->conceptType == "CapabilityRequirement") {
auto* cr = static_cast<const CapabilityRequirement*>(a);
json obj;
if (!cr->capability.empty()) obj["capability"] = cr->capability;
obj["required"] = cr->required;
sem["capabilityReq"] = obj;
}
}
return sem.empty() ? json() : sem;
}
@@ -365,6 +375,13 @@ inline std::string getNodeName(const ASTNode* node) {
if (ct == "BooleanLiteral")
return static_cast<const BooleanLiteral*>(node)->value
? "true" : "false";
// Host Boundary (Step 288)
if (ct == "HostCall")
return static_cast<const HostCall*>(node)->name;
if (ct == "ModuleLoad")
return static_cast<const ModuleLoad*>(node)->moduleName;
if (ct == "ScheduleTask")
return static_cast<const ScheduleTask*>(node)->queue;
return "";
}

View File

@@ -0,0 +1,237 @@
#pragma once
// Step 284-286: Environment Layer
//
// EnvironmentSpec — first-class environment model attached to modules.
// Capability vocabulary, validation, and environment-aware diagnostics.
#include "ast/ASTNode.h"
#include "ast/Annotation.h"
#include "ast/Serialization.h"
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
#include <set>
using json = nlohmann::json;
// --- EnvironmentSpec AST Node ---
class EnvironmentSpec : public ASTNode {
public:
std::string envId; // "posix_process", "browser", "jvm", etc.
std::string envVersion;
std::vector<std::string> capabilities;
std::vector<std::string> constraints;
std::string scheduler; // "event_loop"|"threads"|"fibers"|"coroutines"|"single_thread"
std::string memory; // "manual"|"raii"|"refcount"|"tracing_gc"|"region"
json bindingTimes; // per-feature: "compile"|"link"|"load"|"runtime"
std::string exceptions; // "unwind"|"checked"|"typed"|"untyped"
std::string ffi; // "c_abi"|"dynamic_linking"|"none"
EnvironmentSpec() {
conceptType = "EnvironmentSpec";
bindingTimes = json::object();
}
};
// --- CapabilityRequirement Annotation ---
class CapabilityRequirement : public Annotation {
public:
std::string capability;
bool required = true;
CapabilityRequirement() { conceptType = "CapabilityRequirement"; }
};
// --- Capability Vocabulary ---
inline const std::set<std::string>& knownCapabilities() {
static const std::set<std::string> caps = {
"io.fs", "io.net", "io.stdinout",
"clock.time", "timers",
"event_loop", "threads", "process", "signals",
"modules", "dynamic_loading", "reflection",
"gc", "jit",
"ui.dom", "editor.emacs", "host.imgui"
};
return caps;
}
inline bool isKnownCapability(const std::string& cap) {
return knownCapabilities().count(cap) > 0;
}
// --- Capability Validation ---
struct CapabilityDiagnostic {
std::string severity; // "error" | "warning"
std::string message;
std::string nodeId;
std::string capability;
};
inline std::vector<CapabilityDiagnostic> validateCapabilities(
const ASTNode* root, const EnvironmentSpec* env)
{
std::vector<CapabilityDiagnostic> diags;
if (!root || !env) return diags;
std::set<std::string> envCaps(env->capabilities.begin(), env->capabilities.end());
// Collect all CapabilityRequirements from the tree
std::vector<const ASTNode*> toVisit = {root};
while (!toVisit.empty()) {
const ASTNode* node = toVisit.back();
toVisit.pop_back();
for (const auto* child : node->allChildren()) {
if (child->conceptType == "CapabilityRequirement") {
auto* cr = static_cast<const CapabilityRequirement*>(child);
if (cr->required && envCaps.find(cr->capability) == envCaps.end()) {
diags.push_back({
"error",
"E0501: Unmet capability requirement: " + cr->capability,
node->id,
cr->capability
});
}
}
toVisit.push_back(child);
}
}
return diags;
}
// --- Environment-Incompatible Annotation Diagnostics (Step 286) ---
inline std::vector<CapabilityDiagnostic> validateEnvAnnotations(
const ASTNode* root, const EnvironmentSpec* env)
{
std::vector<CapabilityDiagnostic> diags;
if (!root || !env) return diags;
std::vector<const ASTNode*> toVisit = {root};
while (!toVisit.empty()) {
const ASTNode* node = toVisit.back();
toVisit.pop_back();
for (const auto* child : node->allChildren()) {
// ParallelAnnotation(data) in single_thread env
if (child->conceptType == "ParallelAnnotation" &&
env->scheduler == "single_thread") {
auto* pa = static_cast<const ParallelAnnotation*>(child);
diags.push_back({
"error",
"E0502: @Parallel(" + pa->kind + ") incompatible with single_thread scheduler",
node->id, ""
});
}
// ThreadModelAnnotation(os) in single_thread env
if (child->conceptType == "ThreadModelAnnotation" &&
env->scheduler == "single_thread") {
diags.push_back({
"error",
"E0503: @ThreadModel incompatible with single_thread scheduler",
node->id, ""
});
}
// AtomicAnnotation in single_thread env (warning, not error)
if (child->conceptType == "AtomicAnnotation" &&
env->scheduler == "single_thread") {
diags.push_back({
"warning",
"E0504: @Atomic unnecessary in single_thread environment",
node->id, ""
});
}
// ExecAnnotation(async) requires event_loop, threads, fibers, or coroutines
if (child->conceptType == "ExecAnnotation") {
auto* ea = static_cast<const ExecAnnotation*>(child);
if (ea->mode == "async" && env->scheduler == "single_thread") {
diags.push_back({
"error",
"E0505: @Exec(async) incompatible with single_thread scheduler",
node->id, ""
});
}
}
toVisit.push_back(child);
}
}
return diags;
}
// --- Environment-Aware Lowering Hints (Step 287) ---
struct LoweringHint {
std::string pattern; // e.g. "callback", "std_async", "try_catch"
std::string description;
};
inline std::vector<LoweringHint> getLoweringHints(
const ASTNode* node, const EnvironmentSpec* env)
{
std::vector<LoweringHint> hints;
if (!node || !env) return hints;
for (const auto* child : node->getChildren("annotations")) {
if (child->conceptType == "ExecAnnotation") {
auto* ea = static_cast<const ExecAnnotation*>(child);
if (ea->mode == "async") {
if (env->scheduler == "event_loop")
hints.push_back({"callback", "async → callback-based pattern"});
else if (env->scheduler == "threads")
hints.push_back({"std_async", "async → std::async/std::future"});
else if (env->scheduler == "coroutines")
hints.push_back({"coroutine", "async → co_await coroutine"});
}
}
}
// Memory model hints
if (env->memory == "tracing_gc")
hints.push_back({"suppress_dealloc", "GC env → suppress explicit delete"});
else if (env->memory == "manual")
hints.push_back({"explicit_delete", "manual env → inject explicit delete"});
else if (env->memory == "raii")
hints.push_back({"raii_cleanup", "RAII env → destructor-based cleanup"});
// Exception model hints
if (env->exceptions == "unwind")
hints.push_back({"try_catch", "unwind → try/catch blocks"});
else if (env->exceptions == "typed")
hints.push_back({"expected", "typed → std::expected return types"});
else if (env->exceptions == "checked")
hints.push_back({"checked_throw", "checked → explicit throw declarations"});
return hints;
}
// --- EnvironmentSpec JSON serialization helpers ---
inline json envSpecToJson(const EnvironmentSpec* env) {
if (!env) return json();
json j;
j["envId"] = env->envId;
if (!env->envVersion.empty()) j["envVersion"] = env->envVersion;
if (!env->capabilities.empty()) j["capabilities"] = env->capabilities;
if (!env->constraints.empty()) j["constraints"] = env->constraints;
if (!env->scheduler.empty()) j["scheduler"] = env->scheduler;
if (!env->memory.empty()) j["memory"] = env->memory;
if (!env->bindingTimes.empty()) j["bindingTimes"] = env->bindingTimes;
if (!env->exceptions.empty()) j["exceptions"] = env->exceptions;
if (!env->ffi.empty()) j["ffi"] = env->ffi;
return j;
}
inline void envSpecFromJson(EnvironmentSpec* env, const json& j) {
if (!env) return;
if (j.contains("envId")) env->envId = j["envId"].get<std::string>();
if (j.contains("envVersion")) env->envVersion = j["envVersion"].get<std::string>();
if (j.contains("capabilities") && j["capabilities"].is_array()) {
env->capabilities.clear();
for (const auto& c : j["capabilities"])
if (c.is_string()) env->capabilities.push_back(c.get<std::string>());
}
if (j.contains("constraints") && j["constraints"].is_array()) {
env->constraints.clear();
for (const auto& c : j["constraints"])
if (c.is_string()) env->constraints.push_back(c.get<std::string>());
}
if (j.contains("scheduler")) env->scheduler = j["scheduler"].get<std::string>();
if (j.contains("memory")) env->memory = j["memory"].get<std::string>();
if (j.contains("bindingTimes")) env->bindingTimes = j["bindingTimes"];
if (j.contains("exceptions")) env->exceptions = j["exceptions"].get<std::string>();
if (j.contains("ffi")) env->ffi = j["ffi"].get<std::string>();
}

View File

@@ -1716,6 +1716,8 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
else if (type == "tradeoff") conceptType = "TradeoffAnnotation";
else if (type == "choice") conceptType = "ChoiceAnnotation";
else if (type == "decision") conceptType = "DecisionAnnotation";
// Environment Layer (Step 285)
else if (type == "capabilityRequirement") conceptType = "CapabilityRequirement";
else return headlessRpcError(id, -32602, "Unknown annotation type: " + type);
// Remove existing annotation of same type (update semantics)
@@ -1970,5 +1972,118 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
{{"nodes", nodes}, {"count", (int)nodes.size()}});
}
// --- setEnvironment ---
if (method == "setEnvironment") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
auto params = request.contains("params") ? request["params"]
: json::object();
if (!state.activeBuffer)
return headlessRpcError(id, -32602, "No active buffer");
Module* ast = state.activeAST();
if (!ast)
return headlessRpcError(id, -32602, "No AST available");
// Remove existing EnvironmentSpec if any
for (auto* child : ast->getChildren("environment")) {
ast->removeChild(child);
break;
}
auto* env = new EnvironmentSpec();
envSpecFromJson(env, params);
ast->addChild("environment", env);
return headlessRpcResult(id, {{"success", true},
{"envId", env->envId}});
}
// --- getEnvironment ---
if (method == "getEnvironment") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
if (!state.activeBuffer)
return headlessRpcError(id, -32602, "No active buffer");
Module* ast = state.activeAST();
if (!ast)
return headlessRpcError(id, -32602, "No AST available");
auto envChildren = ast->getChildren("environment");
if (envChildren.empty())
return headlessRpcResult(id, {{"hasEnvironment", false}});
auto* env = static_cast<EnvironmentSpec*>(envChildren[0]);
json envJson = envSpecToJson(env);
envJson["hasEnvironment"] = true;
return headlessRpcResult(id, envJson);
}
// --- validateEnvironment ---
if (method == "validateEnvironment") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
if (!state.activeBuffer)
return headlessRpcError(id, -32602, "No active buffer");
Module* ast = state.activeAST();
if (!ast)
return headlessRpcError(id, -32602, "No AST available");
auto envChildren = ast->getChildren("environment");
if (envChildren.empty())
return headlessRpcError(id, -32602, "No EnvironmentSpec set on module");
auto* env = static_cast<EnvironmentSpec*>(envChildren[0]);
auto capDiags = validateCapabilities(ast, env);
auto annoDiags = validateEnvAnnotations(ast, env);
json diagsJson = json::array();
for (const auto& d : capDiags) {
diagsJson.push_back({{"severity", d.severity},
{"message", d.message}, {"nodeId", d.nodeId},
{"capability", d.capability}});
}
for (const auto& d : annoDiags) {
diagsJson.push_back({{"severity", d.severity},
{"message", d.message}, {"nodeId", d.nodeId}});
}
return headlessRpcResult(id, {
{"diagnostics", diagsJson},
{"count", (int)diagsJson.size()}
});
}
// --- getLoweringHints ---
if (method == "getLoweringHints") {
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 (!state.activeBuffer)
return headlessRpcError(id, -32602, "No active buffer");
Module* ast = state.activeAST();
if (!ast)
return headlessRpcError(id, -32602, "No AST available");
auto envChildren = ast->getChildren("environment");
if (envChildren.empty())
return headlessRpcError(id, -32602, "No EnvironmentSpec set");
auto* env = static_cast<EnvironmentSpec*>(envChildren[0]);
ASTNode* target = nodeId.empty() ? ast : findNodeById(ast, nodeId);
if (!target)
return headlessRpcError(id, -32602, "Node not found: " + nodeId);
auto hints = getLoweringHints(target, env);
json hintsJson = json::array();
for (const auto& h : hints) {
hintsJson.push_back({{"pattern", h.pattern},
{"description", h.description}});
}
return headlessRpcResult(id, {{"hints", hintsJson},
{"count", (int)hintsJson.size()}});
}
return headlessRpcError(id, -32601, "Method not found");
}

View File

@@ -91,7 +91,9 @@ inline bool isSemanticAnnotation(const std::string& conceptType) {
conceptType == "CandidateAnnotation" ||
conceptType == "TradeoffAnnotation" ||
conceptType == "ChoiceAnnotation" ||
conceptType == "DecisionAnnotation";
conceptType == "DecisionAnnotation" ||
// Environment Layer (Step 285)
conceptType == "CapabilityRequirement";
}
// --- Count semantic annotations in an AST ---

View File

@@ -0,0 +1,39 @@
#pragma once
// Step 288: Host Boundary AST Nodes
//
// Explicit nodes for host-environment interactions:
// HostCall, ScheduleTask, ModuleLoad
#include "ASTNode.h"
#include "Expression.h"
#include "Statement.h"
#include <string>
// HostCall — capability-namespaced host function call
class HostCall : public Expression {
public:
std::string capability; // e.g. "io.fs", "io.net"
std::string name; // e.g. "readFile", "listen"
HostCall() { conceptType = "HostCall"; }
HostCall(const std::string& cap, const std::string& n)
: capability(cap), name(n) { conceptType = "HostCall"; }
};
// ScheduleTask — dispatches work to a queue
class ScheduleTask : public Statement {
public:
std::string queue; // "microtask" | "macrotask" | "thread"
ScheduleTask() { conceptType = "ScheduleTask"; }
// Body statements via children role "body"
};
// ModuleLoad — static or dynamic module import
class ModuleLoad : public Statement {
public:
std::string moduleName;
std::string mode; // "static" | "dynamic"
ModuleLoad() { conceptType = "ModuleLoad"; }
};

View File

@@ -9,6 +9,8 @@
#include "Expression.h"
#include "Type.h"
#include "Annotation.h"
#include "HostBoundary.h"
#include "../EnvironmentSpec.h"
#include "Import.h"
#include "ExternalModule.h"
#include "TypeSignature.h"
@@ -395,6 +397,27 @@ inline json propertiesToJson(const ASTNode* node) {
if (!n->author.empty()) props["author"] = n->author;
if (!n->reason.empty()) props["reason"] = n->reason;
}
// Host Boundary (Step 288)
else if (ct == "HostCall") {
auto* n = static_cast<const HostCall*>(node);
if (!n->capability.empty()) props["capability"] = n->capability;
if (!n->name.empty()) props["name"] = n->name;
}
else if (ct == "ScheduleTask") {
auto* n = static_cast<const ScheduleTask*>(node);
if (!n->queue.empty()) props["queue"] = n->queue;
}
else if (ct == "ModuleLoad") {
auto* n = static_cast<const ModuleLoad*>(node);
if (!n->moduleName.empty()) props["moduleName"] = n->moduleName;
if (!n->mode.empty()) props["mode"] = n->mode;
}
// Environment Layer (Step 284-285)
else if (ct == "CapabilityRequirement") {
auto* n = static_cast<const CapabilityRequirement*>(node);
if (!n->capability.empty()) props["capability"] = n->capability;
props["required"] = n->required;
}
// NullLiteral, ListLiteral, IndexAccess, Block, Assignment, IfStatement,
// WhileLoop, Return, ExpressionStatement, ListType, SetType, MapType,
// TupleType, ArrayType, OptionalType — no extra properties
@@ -535,6 +558,13 @@ inline ASTNode* createNode(const std::string& conceptName) {
if (conceptName == "TradeoffAnnotation") return new TradeoffAnnotation();
if (conceptName == "ChoiceAnnotation") return new ChoiceAnnotation();
if (conceptName == "DecisionAnnotation") return new DecisionAnnotation();
// Host Boundary (Step 288)
if (conceptName == "HostCall") return new HostCall();
if (conceptName == "ScheduleTask") return new ScheduleTask();
if (conceptName == "ModuleLoad") return new ModuleLoad();
// Environment Layer (Step 284-285)
if (conceptName == "EnvironmentSpec") return new EnvironmentSpec();
if (conceptName == "CapabilityRequirement") return new CapabilityRequirement();
return nullptr;
}
@@ -939,6 +969,27 @@ inline void setPropertiesFromJson(ASTNode* node, const json& props) {
if (props.contains("author")) n->author = props["author"].get<std::string>();
if (props.contains("reason")) n->reason = props["reason"].get<std::string>();
}
// Host Boundary (Step 288)
else if (ct == "HostCall") {
auto* n = static_cast<HostCall*>(node);
if (props.contains("capability")) n->capability = props["capability"].get<std::string>();
if (props.contains("name")) n->name = props["name"].get<std::string>();
}
else if (ct == "ScheduleTask") {
auto* n = static_cast<ScheduleTask*>(node);
if (props.contains("queue")) n->queue = props["queue"].get<std::string>();
}
else if (ct == "ModuleLoad") {
auto* n = static_cast<ModuleLoad*>(node);
if (props.contains("moduleName")) n->moduleName = props["moduleName"].get<std::string>();
if (props.contains("mode")) n->mode = props["mode"].get<std::string>();
}
// Environment Layer (Step 284-285)
else if (ct == "CapabilityRequirement") {
auto* n = static_cast<CapabilityRequirement*>(node);
if (props.contains("capability")) n->capability = props["capability"].get<std::string>();
if (props.contains("required")) n->required = props["required"].get<bool>();
}
}
inline std::string generateNodeId() {

View File

@@ -0,0 +1,220 @@
// Step 284: EnvironmentSpec Schema & AST Node (12 tests)
#include <cassert>
#include <iostream>
#include <string>
#include "ast/ASTNode.h"
#include "ast/Module.h"
#include "ast/Serialization.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: EnvironmentSpec default construction
static void test_env_defaults() {
EnvironmentSpec env;
check(env.conceptType == "EnvironmentSpec", "conceptType is EnvironmentSpec");
check(env.envId.empty(), "envId default empty");
check(env.capabilities.empty(), "capabilities default empty");
check(env.scheduler.empty(), "scheduler default empty");
check(env.memory.empty(), "memory default empty");
check(env.bindingTimes.is_object(), "bindingTimes default is json object");
}
// Test 2: EnvironmentSpec field assignment
static void test_env_fields() {
EnvironmentSpec env;
env.envId = "posix_process";
env.envVersion = "4.18";
env.capabilities = {"io.fs", "io.net", "threads"};
env.constraints = {"no_jit"};
env.scheduler = "threads";
env.memory = "manual";
env.exceptions = "unwind";
env.ffi = "c_abi";
env.bindingTimes = {{"types", "compile"}, {"dispatch", "runtime"}};
check(env.envId == "posix_process", "envId set");
check(env.capabilities.size() == 3, "3 capabilities");
check(env.scheduler == "threads", "scheduler set");
check(env.memory == "manual", "memory set");
check(env.exceptions == "unwind", "exceptions set");
check(env.ffi == "c_abi", "ffi set");
}
// Test 3: EnvironmentSpec JSON roundtrip via envSpecToJson/envSpecFromJson
static void test_env_json_roundtrip() {
EnvironmentSpec env1;
env1.envId = "browser";
env1.envVersion = "es2022";
env1.capabilities = {"ui.dom", "io.net", "event_loop"};
env1.scheduler = "event_loop";
env1.memory = "tracing_gc";
env1.exceptions = "untyped";
env1.ffi = "none";
env1.bindingTimes = {{"modules", "load"}};
json j = envSpecToJson(&env1);
check(j["envId"] == "browser", "JSON envId");
check(j["scheduler"] == "event_loop", "JSON scheduler");
check(j["capabilities"].size() == 3, "JSON capabilities count");
EnvironmentSpec env2;
envSpecFromJson(&env2, j);
check(env2.envId == "browser", "roundtrip envId");
check(env2.scheduler == "event_loop", "roundtrip scheduler");
check(env2.memory == "tracing_gc", "roundtrip memory");
check(env2.capabilities.size() == 3, "roundtrip capabilities count");
}
// Test 4: EnvironmentSpec attachment to Module
static void test_env_attach_to_module() {
auto* mod = new Module(); mod->id = "mod1"; mod->name = "test";
auto* env = new EnvironmentSpec();
env->envId = "jvm";
env->scheduler = "threads";
mod->addChild("environment", env);
auto envChildren = mod->getChildren("environment");
check(envChildren.size() == 1, "module has 1 environment child");
check(envChildren[0]->conceptType == "EnvironmentSpec", "child is EnvironmentSpec");
auto* retrieved = static_cast<EnvironmentSpec*>(envChildren[0]);
check(retrieved->envId == "jvm", "retrieved envId matches");
delete mod;
}
// Test 5: EnvironmentSpec createNode
static void test_env_create_node() {
ASTNode* node = createNode("EnvironmentSpec");
check(node != nullptr, "createNode returns non-null");
check(node->conceptType == "EnvironmentSpec", "created node type");
delete node;
}
// Test 6: EnvironmentSpec via AST JSON roundtrip (toJson/fromJson)
static void test_env_ast_roundtrip() {
auto* mod = new Module(); mod->id = "mod1"; mod->name = "envtest";
auto* env = new EnvironmentSpec();
env->id = "env1";
env->envId = "python_cpython";
env->scheduler = "event_loop";
env->memory = "tracing_gc";
mod->addChild("environment", env);
json j = toJson(mod);
check(j["children"].contains("environment"), "JSON has environment children");
check(j["children"]["environment"].size() == 1, "1 environment in JSON");
ASTNode* restored = fromJson(j);
check(restored != nullptr, "fromJson returns non-null");
auto envKids = restored->getChildren("environment");
check(envKids.size() == 1, "restored has 1 environment");
check(envKids[0]->conceptType == "EnvironmentSpec", "restored type correct");
delete mod;
delete restored;
}
// Test 7: envSpecToJson omits empty fields
static void test_env_json_sparse() {
EnvironmentSpec env;
env.envId = "minimal";
json j = envSpecToJson(&env);
check(j.contains("envId"), "has envId");
check(!j.contains("scheduler"), "no scheduler when empty");
check(!j.contains("memory"), "no memory when empty");
check(!j.contains("capabilities"), "no capabilities when empty");
}
// Test 8: envSpecFromJson handles missing keys gracefully
static void test_env_json_partial() {
json j = {{"envId", "partial"}, {"scheduler", "fibers"}};
EnvironmentSpec env;
envSpecFromJson(&env, j);
check(env.envId == "partial", "partial envId");
check(env.scheduler == "fibers", "partial scheduler");
check(env.memory.empty(), "missing memory stays empty");
check(env.capabilities.empty(), "missing capabilities stays empty");
}
// Test 9: Multiple EnvironmentSpec values
static void test_env_values() {
std::vector<std::string> schedulers = {"event_loop", "threads", "fibers", "coroutines", "single_thread"};
std::vector<std::string> memories = {"manual", "raii", "refcount", "tracing_gc", "region"};
int ok = 0;
for (const auto& s : schedulers) {
EnvironmentSpec env;
env.scheduler = s;
json j = envSpecToJson(&env);
if (j["scheduler"] == s) ++ok;
}
for (const auto& m : memories) {
EnvironmentSpec env;
env.memory = m;
json j = envSpecToJson(&env);
if (j["memory"] == m) ++ok;
}
check(ok == 10, "all scheduler/memory values roundtrip");
}
// Test 10: Binding times JSON object
static void test_binding_times() {
EnvironmentSpec env;
env.envId = "bt_test";
env.bindingTimes = {{"types", "compile"}, {"dispatch", "runtime"}, {"generics", "link"}};
json j = envSpecToJson(&env);
check(j["bindingTimes"].size() == 3, "3 binding times");
check(j["bindingTimes"]["types"] == "compile", "types binding time");
EnvironmentSpec env2;
envSpecFromJson(&env2, j);
check(env2.bindingTimes["dispatch"] == "runtime", "roundtrip dispatch binding");
}
// Test 11: envSpecToJson null safety
static void test_null_safety() {
json j = envSpecToJson(nullptr);
check(j.is_null(), "null env → null json");
EnvironmentSpec env;
envSpecFromJson(nullptr, {{"envId", "x"}});
envSpecFromJson(&env, json());
check(env.envId.empty(), "empty json → no change");
}
// Test 12: EnvironmentSpec properties via propertiesToJson
static void test_env_properties() {
// EnvironmentSpec uses its own envSpecToJson, but propertiesToJson
// should at least not crash (returns empty for unknown types handled by the EnvironmentSpec's own serialization)
EnvironmentSpec env;
env.envId = "test";
// propertiesToJson won't have a specific case for EnvironmentSpec (uses envSpecToJson instead)
// but it shouldn't crash
json props = propertiesToJson(&env);
// EnvironmentSpec is not in propertiesToJson dispatch, so props will be empty — that's OK
// The test verifies no crash and that envSpecToJson works as the canonical path
json envJ = envSpecToJson(&env);
check(envJ["envId"] == "test", "envSpecToJson canonical path works");
}
int main() {
std::cout << "=== Step 284: EnvironmentSpec Schema & AST Node ===\n";
test_env_defaults();
test_env_fields();
test_env_json_roundtrip();
test_env_attach_to_module();
test_env_create_node();
test_env_ast_roundtrip();
test_env_json_sparse();
test_env_json_partial();
test_env_values();
test_binding_times();
test_null_safety();
test_env_properties();
std::cout << "\nResults: " << passed << "/" << (passed+failed) << "\n";
return failed > 0 ? 1 : 0;
}

View File

@@ -0,0 +1,206 @@
// Step 285: Capability Vocabulary & Validation (12 tests)
#include <cassert>
#include <iostream>
#include <string>
#include "ast/ASTNode.h"
#include "ast/Module.h"
#include "ast/Function.h"
#include "ast/Serialization.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: knownCapabilities returns expected set
static void test_known_capabilities() {
const auto& caps = knownCapabilities();
check(caps.count("io.fs") > 0, "io.fs is known");
check(caps.count("io.net") > 0, "io.net is known");
check(caps.count("threads") > 0, "threads is known");
check(caps.count("event_loop") > 0, "event_loop is known");
check(caps.count("gc") > 0, "gc is known");
check(caps.count("ui.dom") > 0, "ui.dom is known");
check(caps.size() >= 17, "at least 17 capabilities");
}
// Test 2: isKnownCapability checks
static void test_is_known_capability() {
check(isKnownCapability("io.fs"), "io.fs recognized");
check(isKnownCapability("threads"), "threads recognized");
check(!isKnownCapability("quantum_computing"), "quantum_computing not known");
check(!isKnownCapability(""), "empty string not known");
}
// Test 3: CapabilityRequirement construction
static void test_cap_requirement() {
CapabilityRequirement cr;
check(cr.conceptType == "CapabilityRequirement", "conceptType");
check(cr.required == true, "default required is true");
check(cr.capability.empty(), "default capability empty");
cr.capability = "io.fs";
cr.required = true;
check(cr.capability == "io.fs", "capability set");
}
// Test 4: CapabilityRequirement createNode
static void test_cap_requirement_create_node() {
ASTNode* node = createNode("CapabilityRequirement");
check(node != nullptr, "createNode non-null");
check(node->conceptType == "CapabilityRequirement", "correct type");
delete node;
}
// Test 5: CapabilityRequirement JSON roundtrip
static void test_cap_requirement_roundtrip() {
auto* cr = new CapabilityRequirement();
cr->id = "cr1";
cr->capability = "io.net";
cr->required = true;
json j = toJson(cr);
check(j["concept"] == "CapabilityRequirement", "JSON concept");
check(j["properties"]["capability"] == "io.net", "JSON capability");
check(j["properties"]["required"] == true, "JSON required");
ASTNode* restored = fromJson(j);
check(restored != nullptr, "fromJson non-null");
auto* rcr = static_cast<CapabilityRequirement*>(restored);
check(rcr->capability == "io.net", "roundtrip capability");
check(rcr->required == true, "roundtrip required");
delete cr;
delete restored;
}
// Test 6: validateCapabilities — all met
static void test_validate_all_met() {
auto* mod = new Module(); mod->id = "m1"; mod->name = "test";
auto* fn = new Function(); fn->id = "f1"; fn->name = "readFile";
auto* cr = new CapabilityRequirement();
cr->capability = "io.fs"; cr->required = true;
fn->addChild("annotations", cr);
mod->addChild("functions", fn);
EnvironmentSpec env;
env.envId = "posix";
env.capabilities = {"io.fs", "io.net", "threads"};
auto diags = validateCapabilities(mod, &env);
check(diags.empty(), "no diagnostics when all caps met");
delete mod;
}
// Test 7: validateCapabilities — unmet requirement
static void test_validate_unmet() {
auto* mod = new Module(); mod->id = "m1"; mod->name = "test";
auto* fn = new Function(); fn->id = "f1"; fn->name = "useGPU";
auto* cr = new CapabilityRequirement();
cr->capability = "gpu.compute"; cr->required = true;
fn->addChild("annotations", cr);
mod->addChild("functions", fn);
EnvironmentSpec env;
env.envId = "basic";
env.capabilities = {"io.fs"};
auto diags = validateCapabilities(mod, &env);
check(diags.size() == 1, "1 diagnostic for unmet cap");
check(diags[0].severity == "error", "severity is error");
check(diags[0].message.find("E0501") != std::string::npos, "E0501 code");
check(diags[0].capability == "gpu.compute", "diagnostic has capability");
delete mod;
}
// Test 8: validateCapabilities — multiple unmet
static void test_validate_multiple_unmet() {
auto* mod = new Module(); mod->id = "m1"; mod->name = "test";
auto* fn1 = new Function(); fn1->id = "f1"; fn1->name = "serve";
auto* cr1 = new CapabilityRequirement(); cr1->capability = "io.net";
fn1->addChild("annotations", cr1);
auto* fn2 = new Function(); fn2->id = "f2"; fn2->name = "reflectTypes";
auto* cr2 = new CapabilityRequirement(); cr2->capability = "reflection";
fn2->addChild("annotations", cr2);
mod->addChild("functions", fn1);
mod->addChild("functions", fn2);
EnvironmentSpec env;
env.envId = "minimal";
env.capabilities = {};
auto diags = validateCapabilities(mod, &env);
check(diags.size() == 2, "2 diagnostics for 2 unmet caps");
delete mod;
}
// Test 9: validateCapabilities — non-required is OK
static void test_validate_optional() {
auto* mod = new Module(); mod->id = "m1"; mod->name = "test";
auto* fn = new Function(); fn->id = "f1"; fn->name = "maybeNet";
auto* cr = new CapabilityRequirement();
cr->capability = "io.net"; cr->required = false;
fn->addChild("annotations", cr);
mod->addChild("functions", fn);
EnvironmentSpec env;
env.envId = "basic";
env.capabilities = {};
auto diags = validateCapabilities(mod, &env);
check(diags.empty(), "optional requirement → no diagnostic");
delete mod;
}
// Test 10: validateCapabilities null safety
static void test_validate_null() {
auto diags1 = validateCapabilities(nullptr, nullptr);
check(diags1.empty(), "null root → empty");
auto* mod = new Module(); mod->id = "m1";
auto diags2 = validateCapabilities(mod, nullptr);
check(diags2.empty(), "null env → empty");
delete mod;
}
// Test 11: CapabilityRequirement in isSemanticAnnotation
static void test_cap_is_semantic() {
check(isSemanticAnnotation("CapabilityRequirement"),
"CapabilityRequirement is semantic annotation");
}
// Test 12: CapabilityRequirement in compact AST semantic summary
static void test_cap_compact_summary() {
auto* fn = new Function(); fn->id = "f1"; fn->name = "netOp";
auto* cr = new CapabilityRequirement();
cr->capability = "io.net"; cr->required = true;
fn->addChild("annotations", cr);
json sem = extractSemanticSummary(fn);
check(sem.contains("capabilityReq"), "semantic has capabilityReq");
check(sem["capabilityReq"]["capability"] == "io.net", "capabilityReq.capability");
check(sem["capabilityReq"]["required"] == true, "capabilityReq.required");
delete fn;
}
int main() {
std::cout << "=== Step 285: Capability Vocabulary & Validation ===\n";
test_known_capabilities();
test_is_known_capability();
test_cap_requirement();
test_cap_requirement_create_node();
test_cap_requirement_roundtrip();
test_validate_all_met();
test_validate_unmet();
test_validate_multiple_unmet();
test_validate_optional();
test_validate_null();
test_cap_is_semantic();
test_cap_compact_summary();
std::cout << "\nResults: " << passed << "/" << (passed+failed) << "\n";
return failed > 0 ? 1 : 0;
}

View File

@@ -682,3 +682,90 @@ and redos, and that saveAllBuffers respects mixed undo states across buffers.
- Save + undo/redo integration validated end-to-end
- 34 MCP tools total
- Sprint 9 complete: all 5 phases pass (244/244 tests across steps 245265)
---
# Sprint 10 Progress — Semantic Annotation Taxonomy & Environment Layer
## Phase 10a: Semantic Annotation Core + Sidecar Persistence (Steps 266-268)
**Status:** PASS (32/32 tests) — committed as `976161d`
5 semantic annotation types (IntentAnnotation, ComplexityAnnotation, RiskAnnotation, ContractAnnotation, SemanticTagAnnotation) with JSON roundtrip, compact AST semantic summary, and sidecar persistence (.whetstone/<file>.ast.json).
**Key files:**
- `editor/src/ast/Annotation.h` — 5 annotation class definitions
- `editor/src/ast/Serialization.h` — propertiesToJson/createNode/setPropertiesFromJson
- `editor/src/CompactAST.h` — extractSemanticSummary()
- `editor/src/SidecarPersistence.h` — sidecarPath, save/load, isSemanticAnnotation
---
## Phase 10b: Annotation Agent Interface (Steps 269-271)
**Status:** PASS (32/32 tests) — committed as `a049d60`
High-level RPC methods for agent annotation workflow: setSemanticAnnotation, getSemanticAnnotations, removeSemanticAnnotation, getUnannotatedNodes. MCP prompt templates for annotation guidance.
**Key additions:**
- `HeadlessAgentRPCHandler.h` — 4 new RPC methods
- `MCPServer.h` — 4 MCP tools + 5 annotation prompt templates
- `AgentPermissionPolicy.h` — read/write permission mapping
---
## Phase 10c: Type System, Execution & Scope Annotations (Steps 272-277)
**Status:** PASS (171/171 tests) — committed as `e2d1872`
24 new annotation classes across Subjects 2-4:
- **Subject 2 (Type System):** BitWidth, Endian, Layout, Nullability, Variance, Identity, Mut, TypeState
- **Subject 3 (Concurrency):** Atomic, Sync, ThreadModel, MemoryBarrier, Exec, Blocking, Parallel, Trap, Exception, Panic
- **Subject 4 (Scope):** Binding, Lookup, Capture, Visibility, Namespace, Scope
All wired through Serialization.h (3 dispatch points), CompactAST.h, SidecarPersistence.h, and HeadlessAgentRPCHandler.h setSemanticAnnotation type mapping.
---
## Phase 10d: Shims, Optimization, Meta-Programming & Policy Annotations (Steps 278-283)
**Status:** PASS (117/117 tests) — committed as `090320f`
29 new annotation classes across Subjects 5-8:
- **Subject 5 (Shims):** Intrinsic, Raw, CallingConv, Link, Shim, PointerArithmetic, Opaque, Target, Feature, Original, Mapping
- **Subject 6 (Optimization):** TailCall, Loop, Data, Align, Pack, BoundsCheck, Overflow
- **Subject 7 (Meta-Programming):** Meta, Symbol, Evaluate, Template, Synthetic
- **Subject 8 (Policy):** Policy, Ambiguity, Candidate, Tradeoff, Choice, Decision
Generic fallback added to getSemanticAnnotations RPC for extensible annotation type support.
---
## Phase 10e: Environment Layer (Steps 284-289)
**Status:** IN PROGRESS
### Completed so far:
**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
- `editor/src/ast/HostBoundary.h` — HostCall (Expression), ScheduleTask (Statement), ModuleLoad (Statement)
**Core files updated:**
- `Serialization.h` — propertiesToJson/createNode/setPropertiesFromJson for HostCall, ScheduleTask, ModuleLoad, EnvironmentSpec, CapabilityRequirement
- `CompactAST.h` — CapabilityRequirement in extractSemanticSummary, host boundary nodes in getNodeName
- `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)
**Test files written:**
- `step284_test.cpp` — 12 tests (EnvironmentSpec schema, JSON roundtrip, module attachment)
- `step285_test.cpp` — 12 tests (capability vocabulary, validation, CapabilityRequirement)
### 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