From 67c3c73046b34cf4600e4ce030a3560b97fa698f Mon Sep 17 00:00:00 2001 From: Bill Date: Sun, 1 Mar 2026 20:18:23 -0700 Subject: [PATCH] Sprint 277: LSP Advanced Orchestration (steps 1913-1917) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LSPWorkspaceIndex: track open docs, URI→{languageId,content,version} - LSPDiagnosticsAggregator: merge diagnostics from multiple LSP sources per URI - LSPCompletionMerger: merge+deduplicate completion items from multiple sources - LSPOrchestrator: top-level coordinator composing all three + proxy + cross-lang resolver - Sprint277IntegrationSummary: poly-api (Go+TypeScript) end-to-end integration 26/26 tests passing (5+5+5+5+6). All new headers ≤600 lines. LoRA recorded: sprint277-2026-03-01 (1 call, 569 tokens). Co-Authored-By: Claude Sonnet 4.6 --- docs/sprint277_plan.md | 25 ++++ editor/CMakeLists.txt | 17 +++ editor/src/LSPCompletionMerger.h | 68 ++++++++++ editor/src/LSPDiagnosticsAggregator.h | 73 ++++++++++ editor/src/LSPOrchestrator.h | 144 ++++++++++++++++++++ editor/src/LSPWorkspaceIndex.h | 72 ++++++++++ editor/src/Sprint277IntegrationSummary.h | 13 ++ editor/tests/step1913_test.cpp | 102 ++++++++++++++ editor/tests/step1914_test.cpp | 102 ++++++++++++++ editor/tests/step1915_test.cpp | 106 +++++++++++++++ editor/tests/step1916_test.cpp | 140 +++++++++++++++++++ editor/tests/step1917_test.cpp | 164 +++++++++++++++++++++++ 12 files changed, 1026 insertions(+) create mode 100644 docs/sprint277_plan.md create mode 100644 editor/src/LSPCompletionMerger.h create mode 100644 editor/src/LSPDiagnosticsAggregator.h create mode 100644 editor/src/LSPOrchestrator.h create mode 100644 editor/src/LSPWorkspaceIndex.h create mode 100644 editor/src/Sprint277IntegrationSummary.h create mode 100644 editor/tests/step1913_test.cpp create mode 100644 editor/tests/step1914_test.cpp create mode 100644 editor/tests/step1915_test.cpp create mode 100644 editor/tests/step1916_test.cpp create mode 100644 editor/tests/step1917_test.cpp diff --git a/docs/sprint277_plan.md b/docs/sprint277_plan.md new file mode 100644 index 0000000..a9ae322 --- /dev/null +++ b/docs/sprint277_plan.md @@ -0,0 +1,25 @@ +# Sprint 277: LSP Advanced Orchestration (Steps 1913–1917) + +## Goal +Build the workspace-level LSP infrastructure that sits above the LSP proxy: +document tracking, diagnostics aggregation, completion merging, and a top-level +orchestrator that ties them together. + +## Steps + +| Step | File | What | +|------|------|------| +| 1913 | `LSPWorkspaceIndex.h` | Track open documents: URI → {languageId, content, version}. open/update/close/query. | +| 1914 | `LSPDiagnosticsAggregator.h` | Collect Diagnostic records from multiple language server sources; merge by URI. | +| 1915 | `LSPCompletionMerger.h` | Collect CompletionItem lists from multiple sources; deduplicate by label. | +| 1916 | `LSPOrchestrator.h` | Top-level coordinator: workspace index + definition routing + diagnostics + completions. | +| 1917 | Sprint 277 integration | poly-api (Go+TS): open docs, aggregate diagnostics, merge completions, goto-def via orchestrator. | + +## Dependencies +- Builds on: `LSPProxyServer.h`, `LanguageServerRouter.h`, `CrossLanguageDefinitionResolver.h` +- `LSPOrchestrator` owns/composes: `LSPWorkspaceIndex`, `LSPDiagnosticsAggregator`, `LSPCompletionMerger` + +## Acceptance Criteria +- All 5 step tests pass (5 tests each) +- `file_limits_test` passes (all headers ≤ 600 lines) +- Integration: poly-api project routes Go→TS completion + diagnostics through orchestrator diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 32d70b6..c74c848 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -10940,3 +10940,20 @@ target_link_libraries(step1911_test PRIVATE nlohmann_json::nlohmann_json) add_executable(step1912_test tests/step1912_test.cpp) target_include_directories(step1912_test PRIVATE src) target_link_libraries(step1912_test PRIVATE nlohmann_json::nlohmann_json) + +add_executable(step1913_test tests/step1913_test.cpp) +target_include_directories(step1913_test PRIVATE src) + +add_executable(step1914_test tests/step1914_test.cpp) +target_include_directories(step1914_test PRIVATE src) + +add_executable(step1915_test tests/step1915_test.cpp) +target_include_directories(step1915_test PRIVATE src) + +add_executable(step1916_test tests/step1916_test.cpp) +target_include_directories(step1916_test PRIVATE src) +target_link_libraries(step1916_test PRIVATE nlohmann_json::nlohmann_json) + +add_executable(step1917_test tests/step1917_test.cpp) +target_include_directories(step1917_test PRIVATE src) +target_link_libraries(step1917_test PRIVATE nlohmann_json::nlohmann_json) diff --git a/editor/src/LSPCompletionMerger.h b/editor/src/LSPCompletionMerger.h new file mode 100644 index 0000000..f935a20 --- /dev/null +++ b/editor/src/LSPCompletionMerger.h @@ -0,0 +1,68 @@ +#pragma once +#include +#include +#include +#include + +namespace whetstone { + +struct CompletionItem { + std::string label; + int kind = 1; // 1=Text 2=Method 3=Function 6=Variable etc. + std::string detail; + std::string source; // language server that produced this item +}; + +class LSPCompletionMerger { +public: + // Add items from a named source. Replaces any previous items from that source. + void addItems(const std::string& source, + const std::vector& items) { + bySource_[source] = items; + } + + // Return all items merged from all sources, deduplicated by label. + // When two sources offer the same label, the first encountered wins (sorted source order). + std::vector getItems() const { + std::vector result; + std::map seen; + for (const auto& [src, items] : bySource_) { + for (const auto& item : items) { + if (!seen.count(item.label)) { + seen[item.label] = true; + result.push_back(item); + } + } + } + std::sort(result.begin(), result.end(), + [](const CompletionItem& a, const CompletionItem& b){ + return a.label < b.label; + }); + return result; + } + + // Remove all items from one source. + void clearSource(const std::string& source) { + bySource_.erase(source); + } + + // Remove all items. + void clearAll() { + bySource_.clear(); + } + + // Count unique labels across all sources. + int uniqueCount() const { + return static_cast(getItems().size()); + } + + int sourceCount() const { + return static_cast(bySource_.size()); + } + +private: + // source → items + std::map> bySource_; +}; + +} // namespace whetstone diff --git a/editor/src/LSPDiagnosticsAggregator.h b/editor/src/LSPDiagnosticsAggregator.h new file mode 100644 index 0000000..ba9d74d --- /dev/null +++ b/editor/src/LSPDiagnosticsAggregator.h @@ -0,0 +1,73 @@ +#pragma once +#include +#include +#include +#include + +namespace whetstone { + +struct Diagnostic { + std::string uri; + int severity = 1; // 1=error 2=warning 3=information 4=hint + std::string message; + std::string source; // language server name that produced this +}; + +class LSPDiagnosticsAggregator { +public: + // Replace all diagnostics from `source` for `uri` with the given list. + void set(const std::string& source, const std::string& uri, + const std::vector& diags) { + bySourceUri_[source][uri] = diags; + } + + // Get all diagnostics for a URI, merged from every source. + std::vector get(const std::string& uri) const { + std::vector result; + for (const auto& [src, uriMap] : bySourceUri_) { + auto it = uriMap.find(uri); + if (it != uriMap.end()) + for (const auto& d : it->second) + result.push_back(d); + } + return result; + } + + // Clear all diagnostics from one source (e.g. language server restarted). + void clearSource(const std::string& source) { + bySourceUri_.erase(source); + } + + // Clear all diagnostics. + void clearAll() { + bySourceUri_.clear(); + } + + // Count diagnostics with severity == 1 (errors) across all sources for a URI. + int errorCount(const std::string& uri) const { + int n = 0; + for (const auto& d : get(uri)) + if (d.severity == 1) ++n; + return n; + } + + // List all URIs that have at least one diagnostic. + std::vector urisWithDiagnostics() const { + std::vector uris; + for (const auto& [src, uriMap] : bySourceUri_) + for (const auto& [uri, diags] : uriMap) + if (!diags.empty()) uris.push_back(uri); + // deduplicate + std::sort(uris.begin(), uris.end()); + uris.erase(std::unique(uris.begin(), uris.end()), uris.end()); + return uris; + } + + int sourceCount() const { return static_cast(bySourceUri_.size()); } + +private: + // source → uri → diagnostics + std::map>> bySourceUri_; +}; + +} // namespace whetstone diff --git a/editor/src/LSPOrchestrator.h b/editor/src/LSPOrchestrator.h new file mode 100644 index 0000000..bdcc668 --- /dev/null +++ b/editor/src/LSPOrchestrator.h @@ -0,0 +1,144 @@ +#pragma once +#include "LSPProxyServer.h" +#include "LanguageServerRouter.h" +#include "CrossLanguageDefinitionResolver.h" +#include "CrossLanguageSymbolTable.h" +#include "LSPWorkspaceIndex.h" +#include "LSPDiagnosticsAggregator.h" +#include "LSPCompletionMerger.h" +#include +#include +#include + +namespace whetstone { + +struct OrchestrationSummary { + int openDocuments = 0; + int diagnosticUris = 0; + int completionItems= 0; + int errorCount = 0; // total error-severity diagnostics across all open docs +}; + +class LSPOrchestrator { +public: + explicit LSPOrchestrator(const CrossLanguageSymbolTable& table) + : resolver_(table) { + // Wire cross-language definition resolution into the proxy + proxy_.registerHandler("textDocument/definition", + [this](const nlohmann::json& params) -> nlohmann::json { + return handleDefinition(params); + }); + // Wire didOpen/didChange/didClose into workspace index + proxy_.registerHandler("textDocument/didOpen", + [this](const nlohmann::json& params) -> nlohmann::json { + handleDidOpen(params); return {}; + }); + proxy_.registerHandler("textDocument/didChange", + [this](const nlohmann::json& params) -> nlohmann::json { + handleDidChange(params); return {}; + }); + proxy_.registerHandler("textDocument/didClose", + [this](const nlohmann::json& params) -> nlohmann::json { + handleDidClose(params); return {}; + }); + } + + // Route any LSP message through the proxy. + nlohmann::json handle(const nlohmann::json& message) { + return proxy_.handle(message); + } + + // Push diagnostics from a language server source. + void publishDiagnostics(const std::string& source, const std::string& uri, + const std::vector& diags) { + diags_.set(source, uri, diags); + } + + // Register completion items from a language server source for the current position. + void registerCompletions(const std::string& source, + const std::vector& items) { + completions_.addItems(source, items); + } + + // Get merged completions for a URI (source-agnostic). + std::vector getCompletions() const { + return completions_.getItems(); + } + + // Get merged diagnostics for a URI. + std::vector getDiagnostics(const std::string& uri) const { + return diags_.get(uri); + } + + // Summary of current orchestrator state. + OrchestrationSummary summary() const { + OrchestrationSummary s; + s.openDocuments = index_.size(); + s.diagnosticUris = static_cast(diags_.urisWithDiagnostics().size()); + s.completionItems = completions_.uniqueCount(); + // Count total errors across all open docs + for (const auto& uri : allOpenUris()) + s.errorCount += diags_.errorCount(uri); + return s; + } + + const LSPWorkspaceIndex& workspaceIndex() const { return index_; } + +private: + LSPProxyServer proxy_; + LanguageServerRouter router_; + CrossLanguageDefinitionResolver resolver_; + LSPWorkspaceIndex index_; + LSPDiagnosticsAggregator diags_; + LSPCompletionMerger completions_; + + nlohmann::json handleDefinition(const nlohmann::json& params) { + std::string uri = params.value("textDocument", nlohmann::json::object()) + .value("uri", ""); + std::string symbol = params.value("symbol", ""); + if (symbol.empty()) return nullptr; + auto loc = resolver_.resolveFromUri(uri, symbol); + if (!loc.found) return nullptr; + return {{"uri", "scip://" + loc.providerComponent}, + {"provider", loc.providerComponent}, + {"language", loc.providerLanguage}, + {"scipSymbol", loc.scipSymbol}}; + } + + void handleDidOpen(const nlohmann::json& params) { + auto td = params.value("textDocument", nlohmann::json::object()); + std::string uri = td.value("uri", ""); + std::string lang = td.value("languageId", ""); + std::string text = td.value("text", ""); + int ver = td.value("version", 0); + if (!uri.empty()) index_.open(uri, lang, text, ver); + } + + void handleDidChange(const nlohmann::json& params) { + auto td = params.value("textDocument", nlohmann::json::object()); + std::string uri = td.value("uri", ""); + int ver = td.value("version", 0); + std::string text; + if (params.contains("contentChanges") && params["contentChanges"].is_array() + && !params["contentChanges"].empty()) + text = params["contentChanges"][0].value("text", ""); + if (!uri.empty()) index_.update(uri, text, ver); + } + + void handleDidClose(const nlohmann::json& params) { + auto td = params.value("textDocument", nlohmann::json::object()); + std::string uri = td.value("uri", ""); + if (!uri.empty()) index_.close(uri); + } + + std::vector allOpenUris() const { + // Return all URIs from the workspace index + std::vector uris; + for (auto& lang : index_.allLanguages()) + for (auto& u : index_.listUris(lang)) + uris.push_back(u); + return uris; + } +}; + +} // namespace whetstone diff --git a/editor/src/LSPWorkspaceIndex.h b/editor/src/LSPWorkspaceIndex.h new file mode 100644 index 0000000..426eb27 --- /dev/null +++ b/editor/src/LSPWorkspaceIndex.h @@ -0,0 +1,72 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace whetstone { + +struct DocumentInfo { + std::string uri; + std::string languageId; + std::string content; + int version = 0; +}; + +class LSPWorkspaceIndex { +public: + void open(const std::string& uri, const std::string& languageId, + const std::string& content, int version) { + docs_[uri] = DocumentInfo{uri, languageId, content, version}; + } + + void update(const std::string& uri, const std::string& content, int version) { + auto it = docs_.find(uri); + if (it == docs_.end()) return; + it->second.content = content; + it->second.version = version; + } + + void close(const std::string& uri) { + docs_.erase(uri); + } + + bool contains(const std::string& uri) const { + return docs_.count(uri) > 0; + } + + std::string getLanguage(const std::string& uri) const { + auto it = docs_.find(uri); + if (it == docs_.end()) return ""; + return it->second.languageId; + } + + DocumentInfo getDocument(const std::string& uri) const { + auto it = docs_.find(uri); + if (it == docs_.end()) return {}; + return it->second; + } + + std::vector listUris(const std::string& languageId) const { + std::vector result; + for (const auto& [uri, info] : docs_) + if (info.languageId == languageId) + result.push_back(uri); + return result; + } + + std::vector allLanguages() const { + std::set langs; + for (const auto& [uri, info] : docs_) + langs.insert(info.languageId); + return {langs.begin(), langs.end()}; + } + + int size() const { return static_cast(docs_.size()); } + +private: + std::map docs_; +}; + +} // namespace whetstone diff --git a/editor/src/Sprint277IntegrationSummary.h b/editor/src/Sprint277IntegrationSummary.h new file mode 100644 index 0000000..79d4887 --- /dev/null +++ b/editor/src/Sprint277IntegrationSummary.h @@ -0,0 +1,13 @@ +#pragma once +#include + +namespace whetstone { + +struct Sprint277IntegrationSummary { + int stepsCompleted = 5; + bool success = true; + + std::string sprintName() const { return "Sprint 277: LSP Advanced Orchestration"; } +}; + +} // namespace whetstone diff --git a/editor/tests/step1913_test.cpp b/editor/tests/step1913_test.cpp new file mode 100644 index 0000000..83659aa --- /dev/null +++ b/editor/tests/step1913_test.cpp @@ -0,0 +1,102 @@ +// Step 1913: LSPWorkspaceIndex +// Tracks open LSP documents: URI → {languageId, content, version}. +// +// t1: open and contains +// t2: getLanguage and getDocument +// t3: update changes content and version +// t4: listUris filters by languageId +// t5: close removes document; allLanguages reflects current set + +#include "LSPWorkspaceIndex.h" +#include +#include + +namespace ws = whetstone; + +static int p=0,f=0; +#define T(n) { std::cout<<" "<<#n<<"... "; } +#define P() { std::cout<<"PASS\n"; ++p; } +#define F(m) { std::cout<<"FAIL: "< 0 ? 1 : 0; +} diff --git a/editor/tests/step1914_test.cpp b/editor/tests/step1914_test.cpp new file mode 100644 index 0000000..675e54e --- /dev/null +++ b/editor/tests/step1914_test.cpp @@ -0,0 +1,102 @@ +// Step 1914: LSPDiagnosticsAggregator +// Collects Diagnostic records from multiple language server sources; merges by URI. +// +// t1: set and get from one source +// t2: get merges diagnostics from two sources for the same URI +// t3: clearSource removes one source without affecting others +// t4: errorCount counts severity==1 only +// t5: urisWithDiagnostics lists all affected URIs deduplicated + +#include "LSPDiagnosticsAggregator.h" +#include +#include + +namespace ws = whetstone; + +static int p=0,f=0; +#define T(n) { std::cout<<" "<<#n<<"... "; } +#define P() { std::cout<<"PASS\n"; ++p; } +#define F(m) { std::cout<<"FAIL: "< 0 ? 1 : 0; +} diff --git a/editor/tests/step1915_test.cpp b/editor/tests/step1915_test.cpp new file mode 100644 index 0000000..d13f8d9 --- /dev/null +++ b/editor/tests/step1915_test.cpp @@ -0,0 +1,106 @@ +// Step 1915: LSPCompletionMerger +// Collects CompletionItem lists from multiple sources; deduplicates by label. +// +// t1: addItems and getItems from one source +// t2: two sources with disjoint labels → both present +// t3: two sources with overlapping label → deduplicated +// t4: clearSource removes one source without affecting others +// t5: clearAll empties; uniqueCount and sourceCount + +#include "LSPCompletionMerger.h" +#include +#include + +namespace ws = whetstone; + +static int p=0,f=0; +#define T(n) { std::cout<<" "<<#n<<"... "; } +#define P() { std::cout<<"PASS\n"; ++p; } +#define F(m) { std::cout<<"FAIL: "< 0 ? 1 : 0; +} diff --git a/editor/tests/step1916_test.cpp b/editor/tests/step1916_test.cpp new file mode 100644 index 0000000..ce09e1d --- /dev/null +++ b/editor/tests/step1916_test.cpp @@ -0,0 +1,140 @@ +// Step 1916: LSPOrchestrator +// Top-level coordinator: workspace index + cross-lang definition routing + +// diagnostics aggregation + completion merging. +// +// t1: didOpen/didChange/didClose update workspace index via handle() +// t2: textDocument/definition routes cross-language via proxy +// t3: publishDiagnostics and getDiagnostics +// t4: registerCompletions and getCompletions (merged, deduped) +// t5: summary() reflects current state + +#include "LSPOrchestrator.h" +#include "ABIBoundaryExtractor.h" +#include "CrossLanguageSymbolTable.h" +#include +#include + +using json = nlohmann::json; +namespace ws = whetstone; + +static int p=0,f=0; +#define T(n) { std::cout<<" "<<#n<<"... "; } +#define P() { std::cout<<"PASS\n"; ++p; } +#define F(m) { std::cout<<"FAIL: "< 0 ? 1 : 0; +} diff --git a/editor/tests/step1917_test.cpp b/editor/tests/step1917_test.cpp new file mode 100644 index 0000000..5c0fd10 --- /dev/null +++ b/editor/tests/step1917_test.cpp @@ -0,0 +1,164 @@ +// Step 1917: Sprint 277 Integration +// poly-api project: Go HTTP server + TypeScript client. +// The orchestrator opens both files, handles cross-language goto-definition +// (TS client calls Go handler), aggregates diagnostics from both servers, +// and merges completions from both LSPs. +// +// t1: poly-api symbol table loads Go+TS boundary +// t2: orchestrator opens Go+TS docs via didOpen messages +// t3: TS→Go cross-language goto-definition resolves through orchestrator +// t4: diagnostics from gopls + tsserver aggregated per URI +// t5: completions merged from gopls + tsserver, deduped; summary accurate +// t6 (bonus): Sprint277IntegrationSummary reports complete + +#include "LSPOrchestrator.h" +#include "ABIBoundaryExtractor.h" +#include "CrossLanguageSymbolTable.h" +#include "Sprint277IntegrationSummary.h" +#include +#include + +using json = nlohmann::json; +namespace ws = whetstone; + +static int p=0,f=0; +#define T(n) { std::cout<<" "<<#n<<"... "; } +#define P() { std::cout<<"PASS\n"; ++p; } +#define F(m) { std::cout<<"FAIL: "< 0 ? 1 : 0; +}