Add step 620 config auto-discovery from cwd

This commit is contained in:
Bill
2026-02-17 20:53:18 -07:00
parent 010507af44
commit 4cce735f16
5 changed files with 268 additions and 0 deletions

View File

@@ -4459,4 +4459,13 @@ target_link_libraries(step619_test PRIVATE
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go)
add_executable(step620_test tests/step620_test.cpp)
target_include_directories(step620_test PRIVATE src)
target_link_libraries(step620_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)

View File

@@ -18,6 +18,7 @@ struct MCPProjectConfigData {
struct MCPProjectConfigLoadResult {
bool found = false;
std::string sourceWorkspaceRoot;
MCPProjectConfigData config;
std::string error;
};
@@ -69,6 +70,7 @@ public:
out.error = "workspace_missing";
return out;
}
out.sourceWorkspaceRoot = workspaceRoot;
std::filesystem::path path = std::filesystem::path(workspaceRoot) / ".whetstone.json";
if (!std::filesystem::exists(path)) return out;
@@ -86,6 +88,30 @@ public:
return out;
}
static MCPProjectConfigLoadResult discoverFromCwd(const std::string& startDir = "") {
MCPProjectConfigLoadResult out;
std::filesystem::path current = startDir.empty()
? std::filesystem::current_path()
: std::filesystem::path(startDir);
if (!std::filesystem::exists(current)) {
out.error = "start_dir_not_found";
return out;
}
current = std::filesystem::weakly_canonical(current);
while (true) {
std::filesystem::path candidate = current / ".whetstone.json";
if (std::filesystem::exists(candidate)) {
return loadFromWorkspace(current.string());
}
if (current == current.root_path()) break;
std::filesystem::path parent = current.parent_path();
if (parent == current) break;
current = parent;
}
return out;
}
private:
static bool readOptionalString(const json& parsed,
const char* key,

View File

@@ -124,12 +124,19 @@ int main(int argc, char** argv) {
if (!configLoad.error.empty()) {
std::cerr << "[whetstone-mcp] Config load warning: " << configLoad.error << "\n";
}
} else {
configLoad = MCPProjectConfig::discoverFromCwd();
if (!configLoad.error.empty()) {
std::cerr << "[whetstone-mcp] Config discovery warning: " << configLoad.error << "\n";
}
}
if (configLoad.found && !configLoad.config.workspace.empty()) {
state.workspaceRoot = configLoad.config.workspace;
} else if (!workspace.empty()) {
state.workspaceRoot = workspace;
} else if (configLoad.found) {
state.workspaceRoot = configLoad.sourceWorkspaceRoot;
}
if (!language.empty()) {

View File

@@ -0,0 +1,191 @@
// Step 620: Config auto-discovery (walk up from CWD) (12 tests)
#include "MCPProjectConfig.h"
#include <filesystem>
#include <fstream>
#include <iostream>
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 std::filesystem::path tempRoot() {
std::filesystem::path root = std::filesystem::temp_directory_path() / "whetstone_step620";
std::filesystem::remove_all(root);
std::filesystem::create_directories(root);
return root;
}
static void writeFile(const std::filesystem::path& path, const std::string& content) {
std::ofstream out(path);
out << content;
}
void test_discover_from_exact_directory() {
TEST(discover_from_exact_directory);
auto root = tempRoot() / "exact";
std::filesystem::create_directories(root);
writeFile(root / ".whetstone.json", R"({"defaultLanguage":"go"})");
auto result = MCPProjectConfig::discoverFromCwd(root.string());
CHECK(result.found, "config should be found");
CHECK(result.sourceWorkspaceRoot == root.string(), "source workspace mismatch");
CHECK(result.config.defaultLanguage == "go", "language mismatch");
PASS();
}
void test_discover_walks_to_parent() {
TEST(discover_walks_to_parent);
auto root = tempRoot() / "parent";
auto child = root / "src" / "feature";
std::filesystem::create_directories(child);
writeFile(root / ".whetstone.json", R"({"defaultLanguage":"rust"})");
auto result = MCPProjectConfig::discoverFromCwd(child.string());
CHECK(result.found, "config should be found");
CHECK(result.sourceWorkspaceRoot == root.string(), "should resolve parent workspace");
PASS();
}
void test_discover_prefers_nearest_config() {
TEST(discover_prefers_nearest_config);
auto root = tempRoot() / "nearest";
auto mid = root / "apps";
auto leaf = mid / "api";
std::filesystem::create_directories(leaf);
writeFile(root / ".whetstone.json", R"({"mcpWorkspaceAlias":"root"})");
writeFile(mid / ".whetstone.json", R"({"mcpWorkspaceAlias":"mid"})");
auto result = MCPProjectConfig::discoverFromCwd(leaf.string());
CHECK(result.found, "config should be found");
CHECK(result.sourceWorkspaceRoot == mid.string(), "should choose nearest parent");
CHECK(result.config.mcpWorkspaceAlias == "mid", "alias should come from nearest file");
PASS();
}
void test_discover_returns_not_found_when_missing() {
TEST(discover_returns_not_found_when_missing);
auto root = tempRoot() / "missing";
auto child = root / "sub";
std::filesystem::create_directories(child);
auto result = MCPProjectConfig::discoverFromCwd(child.string());
CHECK(!result.found, "should not find config");
CHECK(result.error.empty(), "error should be empty");
PASS();
}
void test_discover_reports_invalid_start_directory() {
TEST(discover_reports_invalid_start_directory);
auto root = tempRoot() / "nope" / "does-not-exist";
auto result = MCPProjectConfig::discoverFromCwd(root.string());
CHECK(!result.found, "should not find config");
CHECK(result.error == "start_dir_not_found", "wrong error");
PASS();
}
void test_discover_reports_parse_error_from_nearest_file() {
TEST(discover_reports_parse_error_from_nearest_file);
auto root = tempRoot() / "bad";
auto child = root / "src";
std::filesystem::create_directories(child);
writeFile(root / ".whetstone.json", "{bad");
auto result = MCPProjectConfig::discoverFromCwd(child.string());
CHECK(result.found, "file exists so found should be true");
CHECK(result.error == "config_json_parse_failed", "wrong error");
PASS();
}
void test_discover_parses_workspace_override() {
TEST(discover_parses_workspace_override);
auto root = tempRoot() / "workspace-override";
std::filesystem::create_directories(root);
writeFile(root / ".whetstone.json", R"({"workspace":"/opt/project"})");
auto result = MCPProjectConfig::discoverFromCwd(root.string());
CHECK(result.found, "should find config");
CHECK(result.config.workspace == "/opt/project", "workspace override mismatch");
PASS();
}
void test_discover_parses_agent_role() {
TEST(discover_parses_agent_role);
auto root = tempRoot() / "role";
std::filesystem::create_directories(root);
writeFile(root / ".whetstone.json", R"({"agentRole":"generator"})");
auto result = MCPProjectConfig::discoverFromCwd(root.string());
CHECK(result.found, "should find config");
CHECK(result.config.agentRole == "generator", "agent role mismatch");
PASS();
}
void test_discover_parses_workspace_alias() {
TEST(discover_parses_workspace_alias);
auto root = tempRoot() / "alias";
std::filesystem::create_directories(root);
writeFile(root / ".whetstone.json", R"({"mcpWorkspaceAlias":"payments"})");
auto result = MCPProjectConfig::discoverFromCwd(root.string());
CHECK(result.found, "should find config");
CHECK(result.config.mcpWorkspaceAlias == "payments", "alias mismatch");
PASS();
}
void test_discover_with_no_argument_uses_current_path() {
TEST(discover_with_no_argument_uses_current_path);
auto root = tempRoot() / "cwd-default";
std::filesystem::create_directories(root);
writeFile(root / ".whetstone.json", R"({"defaultLanguage":"cpp"})");
auto previous = std::filesystem::current_path();
std::filesystem::current_path(root);
auto result = MCPProjectConfig::discoverFromCwd();
std::filesystem::current_path(previous);
CHECK(result.found, "should find config from cwd");
CHECK(result.config.defaultLanguage == "cpp", "language mismatch");
PASS();
}
void test_discover_ignores_grandparent_when_parent_has_invalid_file() {
TEST(discover_ignores_grandparent_when_parent_has_invalid_file);
auto root = tempRoot() / "invalid-nearest";
auto parent = root / "service";
auto leaf = parent / "api";
std::filesystem::create_directories(leaf);
writeFile(root / ".whetstone.json", R"({"defaultLanguage":"python"})");
writeFile(parent / ".whetstone.json", "{bad");
auto result = MCPProjectConfig::discoverFromCwd(leaf.string());
CHECK(result.found, "nearest file exists");
CHECK(result.error == "config_json_parse_failed", "should fail on nearest invalid file");
PASS();
}
void test_discover_preserves_source_workspace_root_for_nested_start() {
TEST(discover_preserves_source_workspace_root_for_nested_start);
auto root = tempRoot() / "source-root";
auto nested = root / "a" / "b" / "c";
std::filesystem::create_directories(nested);
writeFile(root / ".whetstone.json", R"({"defaultLanguage":"typescript"})");
auto result = MCPProjectConfig::discoverFromCwd(nested.string());
CHECK(result.found, "should find config");
CHECK(result.sourceWorkspaceRoot == root.string(), "source root mismatch");
PASS();
}
int main() {
std::cout << "Step 620: Config auto-discovery walk-up\n";
test_discover_from_exact_directory(); // 1
test_discover_walks_to_parent(); // 2
test_discover_prefers_nearest_config(); // 3
test_discover_returns_not_found_when_missing(); // 4
test_discover_reports_invalid_start_directory(); // 5
test_discover_reports_parse_error_from_nearest_file(); // 6
test_discover_parses_workspace_override(); // 7
test_discover_parses_agent_role(); // 8
test_discover_parses_workspace_alias(); // 9
test_discover_with_no_argument_uses_current_path(); // 10
test_discover_ignores_grandparent_when_parent_has_invalid_file();// 11
test_discover_preserves_source_workspace_root_for_nested_start();// 12
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -12252,3 +12252,38 @@ at startup when present.
- `editor/src/mcp_main.cpp` within main-size limit (`177` <= `1500`)
- `editor/tests/step619_test.cpp` within test-file size guidance (`169` lines)
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
### Step 620: Config auto-discovery (walk up from CWD)
**Status:** PASS (12/12 tests)
Adds upward-walk config discovery so `whetstone_mcp` can boot without an
explicit `--workspace` flag by finding the nearest `.whetstone.json` from CWD.
**Files modified:**
- `editor/src/MCPProjectConfig.h` - added:
- `discoverFromCwd()` walk-up discovery
- `sourceWorkspaceRoot` capture for discovered config location
- `editor/src/mcp_main.cpp` - startup now:
- uses discovery path when `--workspace` is omitted
- applies discovered workspace root when config file omits `workspace`
- emits discovery warnings on invalid start/config parse errors
- `editor/CMakeLists.txt` - `step620_test` target
**Files added:**
- `editor/tests/step620_test.cpp` - 12 tests covering:
- exact/parent/nearest config discovery behavior
- no-config and invalid-start directory cases
- parse-error propagation from nearest config file
- field parsing during discovery and source-root tracking
**Verification run:**
- `cmake -S editor -B editor/build-native` - PASS
- `cmake --build editor/build-native --target step620_test step619_test` - PASS
- `./editor/build-native/step620_test` - PASS (12/12)
- `./editor/build-native/step619_test` - PASS (12/12) regression coverage
**Architecture gate check:**
- `editor/src/MCPProjectConfig.h` within header-size limit (`128` <= `600`)
- `editor/src/mcp_main.cpp` within main-size limit (`184` <= `1500`)
- `editor/tests/step620_test.cpp` within test-file size guidance (`191` lines)
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`