From 1c798f93233092a3b29ddd2f6f1a6376a391512e Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 09:37:03 -0700 Subject: [PATCH] Step 538: integrate phase 28a legal-choice execution flow --- editor/CMakeLists.txt | 9 ++ editor/src/Phase28aIntegration.h | 118 +++++++++++++++++++++++ editor/tests/step538_test.cpp | 154 +++++++++++++++++++++++++++++++ progress.md | 39 ++++++++ 4 files changed, 320 insertions(+) create mode 100644 editor/src/Phase28aIntegration.h create mode 100644 editor/tests/step538_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 65a2506..da5b5fc 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3721,4 +3721,13 @@ target_link_libraries(step537_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step538_test tests/step538_test.cpp) +target_include_directories(step538_test PRIVATE src) +target_link_libraries(step538_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) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/Phase28aIntegration.h b/editor/src/Phase28aIntegration.h new file mode 100644 index 0000000..570966e --- /dev/null +++ b/editor/src/Phase28aIntegration.h @@ -0,0 +1,118 @@ +#pragma once +// Step 538: Phase 28a Integration + +#include +#include +#include + +#include + +#include "ArgumentShapeValidator.h" +#include "ConstrainedExecutionTelemetry.h" +#include "OperationSelectorAPI.h" +#include "SymbolSelectorAPI.h" + +using json = nlohmann::json; + +struct Phase28aRequest { + TaskitemContract contract; + std::string nodeKind; + ScopeSnapshot scope; + std::string requestedOperation; + std::string requestedSymbol; + json args; + int constrainedTokenCount = 0; + int baselineTokenCount = 0; +}; + +struct Phase28aResult { + bool menuReady = false; + bool executed = false; + std::vector errors; + OperationSelectionResult operationMenu; + SymbolSelectionResult symbolMenu; + TelemetrySummary telemetrySummary; +}; + +class Phase28aIntegration { +public: + static Phase28aResult run(const Phase28aRequest& request, + const LegalOperationGraph& graph, + ConstrainedExecutionTelemetry& telemetry) { + Phase28aResult result; + + result.operationMenu = OperationSelectorAPI::select( + request.contract, request.nodeKind, request.scope, graph); + result.symbolMenu = SymbolSelectorAPI::select(request.contract, request.scope); + + result.menuReady = result.operationMenu.supported && result.symbolMenu.supported; + + TelemetryEvent event; + event.taskitemId = request.contract.id; + event.candidateOperationCount = (int)result.operationMenu.candidates.size(); + event.candidateSymbolCount = (int)result.symbolMenu.candidates.size(); + event.baselineTokenCount = request.baselineTokenCount; + event.constrainedTokenCount = request.constrainedTokenCount; + + if (!result.menuReady) { + result.errors.push_back("menu_unavailable"); + event.rejectionReasons.push_back("menu_unavailable"); + telemetry.record(event); + result.telemetrySummary = telemetry.summarize(); + return result; + } + + if (!hasOperation(result.operationMenu, request.requestedOperation)) { + result.errors.push_back("operation_not_in_legal_menu"); + event.rejectionReasons.push_back("operation_not_in_legal_menu"); + telemetry.record(event); + result.telemetrySummary = telemetry.summarize(); + return result; + } + + if (!hasSymbol(result.symbolMenu, request.requestedSymbol)) { + result.errors.push_back("symbol_not_in_legal_menu"); + event.rejectionReasons.push_back("symbol_not_in_legal_menu"); + telemetry.record(event); + result.telemetrySummary = telemetry.summarize(); + return result; + } + + auto shape = ArgumentShapeValidator::validate(request.contract, + request.requestedOperation, + request.requestedSymbol, + request.args); + if (!shape.valid) { + result.errors.insert(result.errors.end(), shape.errors.begin(), shape.errors.end()); + event.rejectionReasons.push_back("invalid_argument_shape"); + telemetry.record(event); + result.telemetrySummary = telemetry.summarize(); + return result; + } + + event.selectedOperation = request.requestedOperation; + event.success = true; + telemetry.record(event); + + result.executed = true; + result.telemetrySummary = telemetry.summarize(); + return result; + } + +private: + static bool hasOperation(const OperationSelectionResult& menu, + const std::string& operation) { + for (const auto& c : menu.candidates) { + if (c.operation == operation) return true; + } + return false; + } + + static bool hasSymbol(const SymbolSelectionResult& menu, + const std::string& symbol) { + for (const auto& c : menu.candidates) { + if (c.name == symbol) return true; + } + return false; + } +}; diff --git a/editor/tests/step538_test.cpp b/editor/tests/step538_test.cpp new file mode 100644 index 0000000..6e6dca5 --- /dev/null +++ b/editor/tests/step538_test.cpp @@ -0,0 +1,154 @@ +// Step 538: Phase 28a Integration (8 tests) + +#include "Phase28aIntegration.h" + +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +static TaskitemContract contract() { + TaskitemContract c; + c.id = "ti-538"; + c.nodeId = "n-1"; + c.language = "cpp"; + c.allowedTargets = {"Function"}; + c.allowedOps = {"rename", "update"}; + c.allowedSymbols = {"cursor", "count"}; + c.forbiddenSymbols = {"unsafeGlobal"}; + c.expectedDiagnosticsAdd = {"none"}; + return c; +} + +static ScopeSnapshot scope() { + ScopeSnapshot s; + s.supported = true; + s.candidates = { + {"cursor", "Variable", 3}, + {"count", "Parameter", 3}, + {"unsafeGlobal", "Variable", 0} + }; + return s; +} + +void test_valid_legal_choice_executes_successfully() { + TEST(valid_legal_choice_executes_successfully); + LegalOperationGraph graph; + ConstrainedExecutionTelemetry telemetry; + Phase28aRequest req{contract(), "Function", scope(), "rename", "cursor", + json{{"newName", "cursor2"}}, 20, 40}; + auto result = Phase28aIntegration::run(req, graph, telemetry); + CHECK(result.menuReady, "menu should be ready"); + CHECK(result.executed, "valid legal choice should execute"); + PASS(); +} + +void test_operation_not_in_menu_is_rejected() { + TEST(operation_not_in_menu_is_rejected); + LegalOperationGraph graph; + ConstrainedExecutionTelemetry telemetry; + Phase28aRequest req{contract(), "Function", scope(), "delete", "cursor", + json{{"confirm", true}}, 20, 40}; + auto result = Phase28aIntegration::run(req, graph, telemetry); + CHECK(!result.executed, "illegal op should not execute"); + CHECK(!result.errors.empty() && result.errors[0] == "operation_not_in_legal_menu", + "operation rejection expected"); + PASS(); +} + +void test_symbol_not_in_menu_is_rejected() { + TEST(symbol_not_in_menu_is_rejected); + LegalOperationGraph graph; + ConstrainedExecutionTelemetry telemetry; + Phase28aRequest req{contract(), "Function", scope(), "rename", "ghost", + json{{"newName", "ghost2"}}, 20, 40}; + auto result = Phase28aIntegration::run(req, graph, telemetry); + CHECK(!result.executed, "illegal symbol should not execute"); + CHECK(!result.errors.empty() && result.errors[0] == "symbol_not_in_legal_menu", + "symbol rejection expected"); + PASS(); +} + +void test_invalid_argument_shape_is_rejected() { + TEST(invalid_argument_shape_is_rejected); + LegalOperationGraph graph; + ConstrainedExecutionTelemetry telemetry; + Phase28aRequest req{contract(), "Function", scope(), "rename", "cursor", + json::object(), 20, 40}; + auto result = Phase28aIntegration::run(req, graph, telemetry); + CHECK(!result.executed, "invalid args should not execute"); + CHECK(!result.errors.empty(), "shape errors expected"); + PASS(); +} + +void test_menu_unavailable_when_scope_unsupported() { + TEST(menu_unavailable_when_scope_unsupported); + LegalOperationGraph graph; + ConstrainedExecutionTelemetry telemetry; + auto s = scope(); + s.supported = false; + Phase28aRequest req{contract(), "Function", s, "rename", "cursor", + json{{"newName", "cursor2"}}, 20, 40}; + auto result = Phase28aIntegration::run(req, graph, telemetry); + CHECK(!result.menuReady, "menu should be unavailable"); + CHECK(!result.executed, "execution should be blocked"); + PASS(); +} + +void test_telemetry_records_candidate_breadth() { + TEST(telemetry_records_candidate_breadth); + LegalOperationGraph graph; + ConstrainedExecutionTelemetry telemetry; + Phase28aRequest req{contract(), "Function", scope(), "rename", "cursor", + json{{"newName", "cursor2"}}, 20, 40}; + auto result = Phase28aIntegration::run(req, graph, telemetry); + CHECK(result.telemetrySummary.events == 1, "one telemetry event expected"); + CHECK(result.telemetrySummary.avgCandidateOperationCount > 0, + "operation candidate breadth expected"); + CHECK(result.telemetrySummary.avgCandidateSymbolCount > 0, + "symbol candidate breadth expected"); + PASS(); +} + +void test_telemetry_shows_token_savings() { + TEST(telemetry_shows_token_savings); + LegalOperationGraph graph; + ConstrainedExecutionTelemetry telemetry; + Phase28aRequest req{contract(), "Function", scope(), "rename", "cursor", + json{{"newName", "cursor2"}}, 18, 40}; + auto result = Phase28aIntegration::run(req, graph, telemetry); + CHECK(result.telemetrySummary.tokenSavings == 22, "token savings should be recorded"); + PASS(); +} + +void test_rejection_path_records_failure_telemetry() { + TEST(rejection_path_records_failure_telemetry); + LegalOperationGraph graph; + ConstrainedExecutionTelemetry telemetry; + Phase28aRequest req{contract(), "Function", scope(), "delete", "cursor", + json{{"confirm", true}}, 20, 40}; + auto result = Phase28aIntegration::run(req, graph, telemetry); + CHECK(result.telemetrySummary.failureCount == 1, "failure telemetry expected"); + CHECK(result.telemetrySummary.rejectionHistogram["operation_not_in_legal_menu"] == 1, + "rejection reason histogram mismatch"); + PASS(); +} + +int main() { + std::cout << "Step 538: Phase 28a Integration\n"; + + test_valid_legal_choice_executes_successfully(); // 1 + test_operation_not_in_menu_is_rejected(); // 2 + test_symbol_not_in_menu_is_rejected(); // 3 + test_invalid_argument_shape_is_rejected(); // 4 + test_menu_unavailable_when_scope_unsupported(); // 5 + test_telemetry_records_candidate_breadth(); // 6 + test_telemetry_shows_token_savings(); // 7 + test_rejection_path_records_failure_telemetry(); // 8 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 00dd239..d18d042 100644 --- a/progress.md +++ b/progress.md @@ -9110,3 +9110,42 @@ baselines. - `editor/src/ConstrainedExecutionTelemetry.h` within header-size limit (`75` <= `600`) - `editor/tests/step537_test.cpp` within test-file size guidance (`175` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 538: Phase 28a Integration +**Status:** PASS (8/8 tests) + +Integrates legal-choice operation/symbol menus, argument-shape validation, and +telemetry into a single constrained execution surface for agent-facing editing. + +**Files added:** +- `editor/src/Phase28aIntegration.h` - Phase 28a integration gate: + - builds operation and symbol legal-choice menus + - enforces selection membership (operation + symbol must come from legal menus) + - validates selected argument shape before execution + - records per-attempt telemetry for success and rejection paths + - returns integrated execution result + aggregate telemetry summary +- `editor/tests/step538_test.cpp` - 8 tests covering: + - valid legal-choice execution path + - illegal operation/symbol menu rejection paths + - argument-shape rejection path + - menu unavailability failure path + - telemetry candidate-breadth, token-savings, and rejection histogram behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step538_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step538_test step537_test` - PASS +- `./editor/build-native/step538_test` - PASS (8/8) +- `./editor/build-native/step537_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/Phase28aIntegration.h` within header-size limit (`118` <= `600`) +- `editor/tests/step538_test.cpp` within test-file size guidance (`154` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +**Phase 28a totals (534-538):** +- **Steps completed:** 5 +- **New tests in phase plan:** 56/56 passing +- **Legal-choice API + validation + telemetry integration:** PASS