Step 115: wire orchestrator

This commit is contained in:
Bill
2026-02-09 12:49:14 -07:00
parent 1d2828f5de
commit b007fee31e
6 changed files with 145 additions and 22 deletions

View File

@@ -218,6 +218,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1
- [x] Step 112: **IMPLEMENTED** — Breadcrumb navigation with scope roles (1/1 tests pass)
- [x] Step 113: **IMPLEMENTED** — Project-wide search panel with regex and glob filters (3/3 tests pass)
- [x] Step 114: **IMPLEMENTED** — Go-to-line popup with :line:col parsing (4/4 tests pass)
- [x] Step 115: **IMPLEMENTED** — Orchestrator wired for structured mutations; Emacs config path setting (3/3 tests pass)
---
@@ -315,6 +316,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi
**Step 112:** Compile and pass (1/1)
**Step 113:** Compile and pass (3/3)
**Step 114:** Compile and pass (4/4)
**Step 115:** Compile and pass (3/3)
---
@@ -366,7 +368,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi
## What's Next
Sprint 4 in progress. Step 114 (go-to-line) done. Next: Step 115 (wire orchestrator).
Sprint 4 in progress. Step 115 (wire orchestrator) done. Next: Step 116 (project save/load).
---
@@ -442,3 +444,4 @@ Sprint 4 in progress. Step 114 (go-to-line) done. Next: Step 115 (wire orchestra
| 2026-02-09 | Codex | Step 112: Breadcrumb navigation with scope roles. 1/1 tests pass. |
| 2026-02-09 | Codex | Step 113: Project-wide search panel with regex and glob filters. 3/3 tests pass. |
| 2026-02-09 | Codex | Step 114: Go-to-line popup with :line:col parsing. 4/4 tests pass. |
| 2026-02-09 | Codex | Step 115: Orchestrator wired for structured mutations; Emacs config path setting. 3/3 tests pass. |

View File

@@ -618,6 +618,10 @@ add_executable(step114_test tests/step114_test.cpp)
target_include_directories(step114_test PRIVATE src)
target_link_libraries(step114_test PRIVATE nlohmann_json::nlohmann_json)
add_executable(step115_test tests/step115_test.cpp)
target_include_directories(step115_test PRIVATE src)
target_link_libraries(step115_test PRIVATE nlohmann_json::nlohmann_json)
find_package(SDL2 CONFIG REQUIRED)
find_package(OpenGL REQUIRED)
find_package(glad CONFIG REQUIRED)

View File

@@ -23,6 +23,9 @@ public:
const std::vector<LSPServerConfig>& getLSPServers() const { return lspServers_; }
std::vector<LSPServerConfig>& getLSPServersMutable() { return lspServers_; }
const std::string& getEmacsConfigPath() const { return emacsConfigPath_; }
void setEmacsConfigPath(const std::string& path) { emacsConfigPath_ = path; }
std::string& getEmacsConfigPathMutable() { return emacsConfigPath_; }
LSPServerConfig* getServer(const std::string& language) {
for (auto& s : lspServers_) {
@@ -63,6 +66,7 @@ public:
private:
std::vector<LSPServerConfig> lspServers_;
std::string emacsConfigPath_;
void loadDefaults() {
lspServers_.push_back(makeConfig("python", "pylsp", {}));
@@ -72,6 +76,14 @@ private:
lspServers_.push_back(makeConfig("javascript", "typescript-language-server", {"--stdio"}));
lspServers_.push_back(makeConfig("typescript", "typescript-language-server", {"--stdio"}));
lspServers_.push_back(makeConfig("java", "jdtls", {}));
const char* home = std::getenv("USERPROFILE");
if (!home) home = std::getenv("HOME");
if (home) {
emacsConfigPath_ = (std::filesystem::path(home) / ".emacs.d").string();
} else {
emacsConfigPath_ = ".emacs.d";
}
}
static LSPServerConfig makeConfig(const std::string& lang,

View File

@@ -45,6 +45,7 @@
#include "Breadcrumbs.h"
#include "ProjectSearch.h"
#include "GoToLine.h"
#include "Orchestrator.h"
#include "ast/Serialization.h"
#include "ast/Generator.h"
#include "ast/Annotation.h"
@@ -160,6 +161,8 @@ struct EditorState {
std::vector<EditorDiagnostic> whetstoneDiagnostics;
SettingsManager settings;
bool showLspSettings = false;
Orchestrator orchestrator;
bool orchestratorDirty = true;
ASTMutationAPI mutator;
std::vector<MemoryStrategyInference::Suggestion> suggestions;
bool showSuggestionPopup = false;
@@ -334,6 +337,7 @@ struct EditorState {
state->bufferMode = mode;
activeBuffer = state.get();
bufferStates[path] = std::move(state);
orchestratorDirty = true;
if (path.rfind("(untitled", 0) != 0) watcher.watch(path);
if (lsp && path.rfind("(untitled", 0) != 0) {
lsp->didOpen(toFileUri(path), language, content, activeBuffer->lspVersion);
@@ -420,6 +424,7 @@ struct EditorState {
if (!buffers.hasBuffer(path)) return;
buffers.switchToBuffer(path);
activeBuffer = bufferStates[path].get();
orchestratorDirty = true;
symbolsPending = true;
symbolsLastChange = ImGui::GetTime();
if (lsp) lsp->clearDocumentSymbols();
@@ -473,6 +478,35 @@ struct EditorState {
registerCommands();
}
void syncOrchestratorFromActive() {
if (!active()) return;
if (!isStructured()) return;
Module* ast = active()->sync.getAST();
if (!ast) return;
orchestrator.setAST(cloneModule(ast));
orchestratorDirty = false;
}
void applyOrchestratorToActive() {
if (!active()) return;
if (!isStructured()) return;
Module* ast = orchestrator.getAST();
if (!ast) return;
active()->sync.setAST(cloneModule(ast));
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
refreshActiveTextFromAST();
orchestratorDirty = false;
}
Module* mutationAST() {
if (!active()) return nullptr;
if (!isStructured()) return nullptr;
if (orchestratorDirty || !orchestrator.getAST()) {
syncOrchestratorFromActive();
}
return orchestrator.getAST();
}
void registerCommand(const std::string& id,
const std::string& label,
const std::string& shortcut,
@@ -591,6 +625,7 @@ struct EditorState {
active()->sync.syncNow();
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
}
orchestratorDirty = true;
active()->highlightsDirty = true;
}
@@ -603,6 +638,7 @@ struct EditorState {
active()->sync.syncNow();
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
}
orchestratorDirty = true;
active()->highlightsDirty = true;
active()->generatedHighlightsDirty = true;
active()->modified = true;
@@ -624,6 +660,7 @@ struct EditorState {
active()->sync.syncNow();
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
}
orchestratorDirty = true;
active()->highlightsDirty = true;
active()->generatedHighlightsDirty = true;
}
@@ -637,6 +674,7 @@ struct EditorState {
active()->sync.syncNow();
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
}
orchestratorDirty = true;
active()->highlightsDirty = true;
active()->generatedHighlightsDirty = true;
}
@@ -2114,6 +2152,10 @@ int main(int, char**) {
if (state.showLspSettings) {
ImGui::Begin("LSP Servers", &state.showLspSettings);
ImGui::PushFont(uiFont);
ImGui::TextUnformatted("Emacs Config");
ImGui::SetNextItemWidth(320);
InputTextStr("Config Path", &state.settings.getEmacsConfigPathMutable(), ImGuiInputTextFlags_None);
ImGui::Separator();
if (ImGui::Button("Auto-Detect")) {
state.settings.autoDetect();
}
@@ -2201,7 +2243,7 @@ int main(int, char**) {
ImGui::SameLine();
if (ImGui::Button("Apply")) {
state.refactorError.clear();
Module* ast = state.activeAST();
Module* ast = state.mutationAST();
RefactorPlan plan;
if (state.refactorAction == 1) {
plan = buildRenameVariablePlan(ast, state.refactorNameA, state.refactorNameB);
@@ -2221,7 +2263,7 @@ int main(int, char**) {
} else {
state.outputLog += "Refactor applied (" +
std::to_string(res.appliedCount) + " mutations).\n";
state.refreshActiveTextFromAST();
state.applyOrchestratorToActive();
state.showRefactorPopup = false;
}
}
@@ -2651,7 +2693,7 @@ int main(int, char**) {
ImGui::TextWrapped("%s", state.suggestionPopup.reason.c_str());
ImGui::Text("Confidence: %.2f", state.suggestionPopup.confidence);
if (ImGui::Button("Apply")) {
Module* ast = state.active() ? state.active()->sync.getAST() : nullptr;
Module* ast = state.mutationAST();
if (ast) {
ASTNode* target = findNodeById(ast, state.suggestionPopup.nodeId);
if (target) {
@@ -2665,6 +2707,7 @@ int main(int, char**) {
state.mutator.setRoot(ast);
auto res2 = state.mutator.insertNode(target->id, "annotations", anno);
if (!res2.error.empty()) state.outputLog += res2.error + "\n";
state.applyOrchestratorToActive();
}
}
}
@@ -2748,12 +2791,13 @@ int main(int, char**) {
}
if (ImGui::BeginPopupContextWindow("AnnotationContext", ImGuiPopupFlags_MouseButtonRight)) {
Module* ast = state.active() ? state.active()->sync.getAST() : nullptr;
Module* ast = state.mutationAST();
int lineZero = state.active() ? std::max(0, state.active()->cursorLine - 1) : 0;
ASTNode* target = ast ? findAnnotationTarget(ast, lineZero) : nullptr;
if (!target) {
ImGui::TextDisabled("No annotatable symbol on this line.");
} else {
bool mutated = false;
ImGui::Text("Target: %s", nodeDisplayName(target).c_str());
ImGui::Separator();
@@ -2766,6 +2810,7 @@ int main(int, char**) {
state.mutator.setRoot(ast);
auto res = state.mutator.insertNode(target->id, "annotations", anno);
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (res.error.empty()) mutated = true;
}
}
if (ImGui::MenuItem("@Owner(Single)")) {
@@ -2776,6 +2821,7 @@ int main(int, char**) {
state.mutator.setRoot(ast);
auto res = state.mutator.insertNode(target->id, "annotations", anno);
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (res.error.empty()) mutated = true;
}
}
if (ImGui::MenuItem("@Deallocate(Explicit)")) {
@@ -2786,6 +2832,7 @@ int main(int, char**) {
state.mutator.setRoot(ast);
auto res = state.mutator.insertNode(target->id, "annotations", anno);
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (res.error.empty()) mutated = true;
}
}
if (ImGui::MenuItem("@Lifetime(RAII)")) {
@@ -2796,6 +2843,7 @@ int main(int, char**) {
state.mutator.setRoot(ast);
auto res = state.mutator.insertNode(target->id, "annotations", anno);
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (res.error.empty()) mutated = true;
}
}
if (ImGui::MenuItem("@Allocate(Static)")) {
@@ -2806,6 +2854,7 @@ int main(int, char**) {
state.mutator.setRoot(ast);
auto res = state.mutator.insertNode(target->id, "annotations", anno);
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (res.error.empty()) mutated = true;
}
}
ImGui::EndMenu();
@@ -2819,27 +2868,37 @@ int main(int, char**) {
if (anno->conceptType == "ReclaimAnnotation") {
if (ImGui::MenuItem("Tracing")) {
state.mutator.setRoot(ast);
state.mutator.setProperty(anno->id, "strategy", "Tracing");
auto res = state.mutator.setProperty(anno->id, "strategy", "Tracing");
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (res.error.empty()) mutated = true;
}
} else if (anno->conceptType == "OwnerAnnotation") {
if (ImGui::MenuItem("Single")) {
state.mutator.setRoot(ast);
state.mutator.setProperty(anno->id, "strategy", "Single");
auto res = state.mutator.setProperty(anno->id, "strategy", "Single");
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (res.error.empty()) mutated = true;
}
} else if (anno->conceptType == "DeallocateAnnotation") {
if (ImGui::MenuItem("Explicit")) {
state.mutator.setRoot(ast);
state.mutator.setProperty(anno->id, "strategy", "Explicit");
auto res = state.mutator.setProperty(anno->id, "strategy", "Explicit");
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (res.error.empty()) mutated = true;
}
} else if (anno->conceptType == "LifetimeAnnotation") {
if (ImGui::MenuItem("RAII")) {
state.mutator.setRoot(ast);
state.mutator.setProperty(anno->id, "strategy", "RAII");
auto res = state.mutator.setProperty(anno->id, "strategy", "RAII");
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (res.error.empty()) mutated = true;
}
} else if (anno->conceptType == "AllocateAnnotation") {
if (ImGui::MenuItem("Static")) {
state.mutator.setRoot(ast);
state.mutator.setProperty(anno->id, "strategy", "Static");
auto res = state.mutator.setProperty(anno->id, "strategy", "Static");
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (res.error.empty()) mutated = true;
}
}
ImGui::EndMenu();
@@ -2855,6 +2914,7 @@ int main(int, char**) {
state.mutator.setRoot(ast);
auto res = state.mutator.deleteNode(anno->id);
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (res.error.empty()) mutated = true;
}
}
ImGui::EndMenu();
@@ -2877,6 +2937,7 @@ int main(int, char**) {
state.mutator.setRoot(ast);
auto res = state.mutator.deleteNode(lineConflict.childAnnoId);
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (res.error.empty()) mutated = true;
}
if (ImGui::MenuItem("Match parent strategy")) {
state.mutator.setRoot(ast);
@@ -2884,10 +2945,14 @@ int main(int, char**) {
"strategy",
lineConflict.parentStrategy);
if (!res.error.empty()) state.outputLog += res.error + "\n";
if (res.error.empty()) mutated = true;
}
}
ImGui::EndMenu();
}
if (mutated) {
state.applyOrchestratorToActive();
}
}
ImGui::EndPopup();
}
@@ -2986,7 +3051,7 @@ int main(int, char**) {
// Optimization controls
if (ImGui::BeginTabItem("Optimize")) {
ImGui::PushFont(uiFont);
Module* ast = state.active() ? state.active()->sync.getAST() : nullptr;
Module* ast = state.mutationAST();
if (!state.active()) {
ImGui::TextDisabled("(no active buffer)");
} else if (state.active()->bufferMode == BufferManager::BufferMode::Text) {
@@ -3045,7 +3110,7 @@ int main(int, char**) {
}
state.optimizeFoldSummary = summary;
state.outputLog += summary + "\n";
if (nodes > 0) state.refreshActiveTextFromAST();
if (nodes > 0) state.applyOrchestratorToActive();
std::string afterText = state.active()->editBuf;
state.openDiff(beforeText, afterText, false, 1, {tid});
}
@@ -3089,7 +3154,7 @@ int main(int, char**) {
}
state.optimizeDeadCodeSummary = summary;
state.outputLog += summary + "\n";
if (nodes > 0) state.refreshActiveTextFromAST();
if (nodes > 0) state.applyOrchestratorToActive();
std::string afterText = state.active()->editBuf;
state.openDiff(beforeText, afterText, false, 2, {tid});
}
@@ -3138,7 +3203,7 @@ int main(int, char**) {
}
state.optimizeApplySummary = summary;
state.outputLog += summary + "\n";
if (totalNodes > 0) state.refreshActiveTextFromAST();
if (totalNodes > 0) state.applyOrchestratorToActive();
std::string afterText = state.active()->editBuf;
state.openDiff(beforeText, afterText, false, 3, {tidFold, tidDce});
}
@@ -3157,7 +3222,7 @@ int main(int, char**) {
if (ImGui::BeginTabItem("Transforms")) {
ImGui::PushFont(uiFont);
auto* buf = state.active();
Module* ast = buf ? buf->sync.getAST() : nullptr;
Module* ast = state.mutationAST();
if (!buf) {
ImGui::TextDisabled("(no active buffer)");
} else if (buf->bufferMode == BufferManager::BufferMode::Text) {
@@ -3176,7 +3241,7 @@ int main(int, char**) {
while (inc.undoLast()) {
any = true;
}
if (any) state.refreshActiveTextFromAST();
if (any) state.applyOrchestratorToActive();
}
if (!hasHistory) ImGui::EndDisabled();
ImGui::Separator();
@@ -3193,7 +3258,7 @@ int main(int, char**) {
ImGui::SameLine();
if (ImGui::Button("Undo")) {
if (inc.undoTransform(h.transformId)) {
state.refreshActiveTextFromAST();
state.applyOrchestratorToActive();
}
}
ImGui::Text("Affected: %d", (int)h.affectedNodeIds.size());
@@ -3218,7 +3283,7 @@ int main(int, char**) {
if (ImGui::BeginTabItem("Diff")) {
ImGui::PushFont(uiFont);
auto* buf = state.active();
Module* ast = buf ? buf->sync.getAST() : nullptr;
Module* ast = state.mutationAST();
if (!buf) {
ImGui::TextDisabled("(no active buffer)");
} else if (buf->bufferMode == BufferManager::BufferMode::Text) {
@@ -3238,7 +3303,7 @@ int main(int, char**) {
} else {
state.outputLog += "Refactor applied (" +
std::to_string(res.appliedCount) + " mutations).\n";
state.refreshActiveTextFromAST();
state.applyOrchestratorToActive();
}
} else {
buf->incrementalOptimizer.setRoot(ast);
@@ -3250,7 +3315,7 @@ int main(int, char**) {
buf->incrementalOptimizer.applyTransform("constant-fold");
buf->incrementalOptimizer.applyTransform("dead-code-elim");
}
state.refreshActiveTextFromAST();
state.applyOrchestratorToActive();
}
}
state.diff.active = false;
@@ -3271,7 +3336,7 @@ int main(int, char**) {
it != state.diff.transformIds.rend(); ++it) {
buf->incrementalOptimizer.undoTransform(*it);
}
state.refreshActiveTextFromAST();
state.applyOrchestratorToActive();
}
state.diff.active = false;
}

View File

@@ -0,0 +1,39 @@
// Step 115 TDD Test: Orchestrator wiring + Emacs config setting
#include "Orchestrator.h"
#include "SettingsManager.h"
#include <iostream>
#include <memory>
static void expect(bool cond, const std::string& name, int& passed, int& failed) {
if (cond) {
std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n";
++passed;
} else {
std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n";
++failed;
}
}
int main() {
int passed = 0;
int failed = 0;
{
Orchestrator orch;
auto mod = std::make_unique<Module>("m1", "Main", "python");
orch.setAST(std::move(mod));
expect(orch.getAST() != nullptr && orch.getAST()->name == "Main", "orchestrator stores AST", passed, failed);
}
{
SettingsManager settings;
std::string path = settings.getEmacsConfigPath();
bool ok = !path.empty() && path.find(".emacs.d") != std::string::npos;
expect(ok, "emacs config default", passed, failed);
settings.setEmacsConfigPath("C:/custom/emacs");
expect(settings.getEmacsConfigPath() == "C:/custom/emacs", "emacs config set", passed, failed);
}
std::cout << "\n=== Step 115 Results: " << passed << " passed, " << failed << " failed ===\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -352,7 +352,7 @@ Whetstone-specific AST navigation.
Wire the orchestrator, serialization, session management, and prepare
AST extensions for Sprint 5.
- [ ] **Step 115: Wire Orchestrator**
- [x] **Step 115: Wire Orchestrator**
Connect `Orchestrator.h` to the GUI. All AST mutations go through the
orchestrator's undo/redo journal (replacing TextEditor's simple stack).
Orchestrator manages the canonical AST state. GUI reads from it.