diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 62924ab..2441be2 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3784,4 +3784,13 @@ target_link_libraries(step544_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step545_test tests/step545_test.cpp) +target_include_directories(step545_test PRIVATE src) +target_link_libraries(step545_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/ContextBundleMinimizer.h b/editor/src/ContextBundleMinimizer.h new file mode 100644 index 0000000..298a302 --- /dev/null +++ b/editor/src/ContextBundleMinimizer.h @@ -0,0 +1,88 @@ +#pragma once +// Step 545: Context Bundle Minimizer + +#include +#include +#include +#include + +struct ContextBundleInput { + std::string taskitemId; + std::vector contractTargets; + std::vector contractSymbols; + std::vector contractOps; + std::vector fileContext; + std::vector projectContext; + std::vector dependencyContext; + bool narrowOperation = true; +}; + +struct ContextBundleOutput { + std::vector minimalContext; + std::vector removedContext; +}; + +class ContextBundleMinimizer { +public: + static ContextBundleOutput minimize(const ContextBundleInput& input) { + ContextBundleOutput out; + + auto mustKeep = seedMustKeep(input); + appendFiltered(input.fileContext, mustKeep, out.minimalContext, out.removedContext); + + if (!input.narrowOperation) { + appendFiltered(input.projectContext, mustKeep, out.minimalContext, out.removedContext); + appendFiltered(input.dependencyContext, mustKeep, out.minimalContext, out.removedContext); + } else { + appendFiltered(input.projectContext, mustKeep, out.minimalContext, out.removedContext, + /*strict=*/true); + appendFiltered(input.dependencyContext, mustKeep, out.minimalContext, out.removedContext, + /*strict=*/true); + } + + dedupe(out.minimalContext); + dedupe(out.removedContext); + return out; + } + +private: + static std::set seedMustKeep(const ContextBundleInput& input) { + std::set keys; + for (const auto& t : input.contractTargets) keys.insert(t); + for (const auto& s : input.contractSymbols) keys.insert(s); + for (const auto& o : input.contractOps) keys.insert(o); + return keys; + } + + static void appendFiltered(const std::vector& context, + const std::set& keys, + std::vector& keep, + std::vector& drop, + bool strict = false) { + for (const auto& item : context) { + bool relevant = containsAny(item, keys); + if (relevant || (!strict && looksStructural(item))) { + keep.push_back(item); + } else { + drop.push_back(item); + } + } + } + + static bool containsAny(const std::string& item, + const std::set& keys) { + for (const auto& k : keys) { + if (!k.empty() && item.find(k) != std::string::npos) return true; + } + return false; + } + + static bool looksStructural(const std::string& item) { + return item.find("file:") == 0 || item.find("ast:") == 0 || item.find("diag:") == 0; + } + + static void dedupe(std::vector& items) { + std::sort(items.begin(), items.end()); + items.erase(std::unique(items.begin(), items.end()), items.end()); + } +}; diff --git a/editor/tests/step545_test.cpp b/editor/tests/step545_test.cpp new file mode 100644 index 0000000..03e43d6 --- /dev/null +++ b/editor/tests/step545_test.cpp @@ -0,0 +1,169 @@ +// Step 545: Context Bundle Minimizer (12 tests) + +#include "ContextBundleMinimizer.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 bool contains(const std::vector& v, const std::string& needle) { + for (const auto& x : v) if (x == needle) return true; + return false; +} + +static ContextBundleInput sampleInput(bool narrow = true) { + ContextBundleInput i; + i.taskitemId = "ti-545"; + i.contractTargets = {"Function"}; + i.contractSymbols = {"cursor", "EditorState"}; + i.contractOps = {"rename"}; + i.fileContext = { + "file:main.cpp", + "ast:Function cursor update block", + "diag:none", + "note:unrelated_ui_theme" + }; + i.projectContext = { + "module:EditorState contract cursor", + "module:Theme purple gradient", + "module:Function signatures" + }; + i.dependencyContext = { + "dep:tree_sitter Function", + "dep:sdl2 window", + "dep:cursor_utils" + }; + i.narrowOperation = narrow; + return i; +} + +void test_narrow_mode_keeps_relevant_file_context() { + TEST(narrow_mode_keeps_relevant_file_context); + auto out = ContextBundleMinimizer::minimize(sampleInput(true)); + CHECK(contains(out.minimalContext, "ast:Function cursor update block"), "relevant file context should remain"); + PASS(); +} + +void test_narrow_mode_drops_unrelated_project_context() { + TEST(narrow_mode_drops_unrelated_project_context); + auto out = ContextBundleMinimizer::minimize(sampleInput(true)); + CHECK(contains(out.removedContext, "module:Theme purple gradient"), "unrelated project context should drop"); + PASS(); +} + +void test_narrow_mode_drops_unrelated_dependency_context() { + TEST(narrow_mode_drops_unrelated_dependency_context); + auto out = ContextBundleMinimizer::minimize(sampleInput(true)); + CHECK(contains(out.removedContext, "dep:sdl2 window"), "unrelated dependency context should drop"); + PASS(); +} + +void test_nonnarrow_mode_keeps_structural_project_context() { + TEST(nonnarrow_mode_keeps_structural_project_context); + auto in = sampleInput(false); + auto out = ContextBundleMinimizer::minimize(in); + CHECK(contains(out.minimalContext, "module:Theme purple gradient") == false, + "non-structural unrelated module should still drop"); + CHECK(contains(out.minimalContext, "module:Function signatures"), + "relevant project module should remain"); + PASS(); +} + +void test_nonnarrow_mode_keeps_structural_file_entries() { + TEST(nonnarrow_mode_keeps_structural_file_entries); + auto in = sampleInput(false); + auto out = ContextBundleMinimizer::minimize(in); + CHECK(contains(out.minimalContext, "file:main.cpp"), "file structural entry should remain"); + CHECK(contains(out.minimalContext, "diag:none"), "diag structural entry should remain"); + PASS(); +} + +void test_contract_symbols_drive_relevance() { + TEST(contract_symbols_drive_relevance); + auto in = sampleInput(true); + in.projectContext.push_back("module:EditorState snapshots"); + auto out = ContextBundleMinimizer::minimize(in); + CHECK(contains(out.minimalContext, "module:EditorState snapshots"), "symbol-linked module should remain"); + PASS(); +} + +void test_contract_ops_drive_relevance() { + TEST(contract_ops_drive_relevance); + auto in = sampleInput(true); + in.projectContext.push_back("module:rename strategy docs"); + auto out = ContextBundleMinimizer::minimize(in); + CHECK(contains(out.minimalContext, "module:rename strategy docs"), "op-linked module should remain"); + PASS(); +} + +void test_dedupe_applies_to_minimal_context() { + TEST(dedupe_applies_to_minimal_context); + auto in = sampleInput(true); + in.fileContext.push_back("ast:Function cursor update block"); + auto out = ContextBundleMinimizer::minimize(in); + int count = 0; + for (const auto& x : out.minimalContext) if (x == "ast:Function cursor update block") ++count; + CHECK(count == 1, "minimal context should be deduped"); + PASS(); +} + +void test_dedupe_applies_to_removed_context() { + TEST(dedupe_applies_to_removed_context); + auto in = sampleInput(true); + in.projectContext.push_back("module:Theme purple gradient"); + auto out = ContextBundleMinimizer::minimize(in); + int count = 0; + for (const auto& x : out.removedContext) if (x == "module:Theme purple gradient") ++count; + CHECK(count == 1, "removed context should be deduped"); + PASS(); +} + +void test_empty_context_inputs_return_empty_outputs() { + TEST(empty_context_inputs_return_empty_outputs); + ContextBundleInput in; + auto out = ContextBundleMinimizer::minimize(in); + CHECK(out.minimalContext.empty(), "minimal should be empty"); + CHECK(out.removedContext.empty(), "removed should be empty"); + PASS(); +} + +void test_irrelevant_file_note_dropped_even_if_nonnarrow() { + TEST(irrelevant_file_note_dropped_even_if_nonnarrow); + auto in = sampleInput(false); + auto out = ContextBundleMinimizer::minimize(in); + CHECK(contains(out.removedContext, "note:unrelated_ui_theme"), "irrelevant file note should drop"); + PASS(); +} + +void test_dependency_relevance_by_symbol_match() { + TEST(dependency_relevance_by_symbol_match); + auto in = sampleInput(true); + in.dependencyContext.push_back("dep:EditorState_helpers"); + auto out = ContextBundleMinimizer::minimize(in); + CHECK(contains(out.minimalContext, "dep:EditorState_helpers"), "symbol-matched dependency should remain"); + PASS(); +} + +int main() { + std::cout << "Step 545: Context Bundle Minimizer\n"; + + test_narrow_mode_keeps_relevant_file_context(); // 1 + test_narrow_mode_drops_unrelated_project_context(); // 2 + test_narrow_mode_drops_unrelated_dependency_context(); // 3 + test_nonnarrow_mode_keeps_structural_project_context(); // 4 + test_nonnarrow_mode_keeps_structural_file_entries(); // 5 + test_contract_symbols_drive_relevance(); // 6 + test_contract_ops_drive_relevance(); // 7 + test_dedupe_applies_to_minimal_context(); // 8 + test_dedupe_applies_to_removed_context(); // 9 + test_empty_context_inputs_return_empty_outputs(); // 10 + test_irrelevant_file_note_dropped_even_if_nonnarrow(); // 11 + test_dependency_relevance_by_symbol_match(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 1f497e4..ba4b13a 100644 --- a/progress.md +++ b/progress.md @@ -9396,3 +9396,40 @@ post-apply failure escalation. - `editor/src/ConstrainedRoutingRulesetExtension.h` within header-size limit (`60` <= `600`) - `editor/tests/step544_test.cpp` within test-file size guidance (`126` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 545: Context Bundle Minimizer +**Status:** PASS (12/12 tests) + +Implements minimal-sufficient context bundling for constrained taskitems, +retaining contract-relevant context while dropping unrelated project and +dependency noise for narrow operations. + +**Files added:** +- `editor/src/ContextBundleMinimizer.h` - context minimization module: + - seeds relevance keys from contract targets/symbols/ops + - filters file/project/dependency context by relevance and structural heuristics + - applies stricter pruning for narrow operations + - returns explicit kept vs removed context sets + - deterministic dedupe normalization for outputs +- `editor/tests/step545_test.cpp` - 12 tests covering: + - narrow-mode relevance retention/drop behavior + - non-narrow structural keep behavior + - symbol/op-driven relevance behavior + - dedupe behavior for kept/removed sets + - empty-input edge case + - irrelevant-note dropping behavior + - dependency relevance by symbol-match behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step545_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step545_test step544_test` - PASS +- `./editor/build-native/step545_test` - PASS (12/12) +- `./editor/build-native/step544_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/ContextBundleMinimizer.h` within header-size limit (`88` <= `600`) +- `editor/tests/step545_test.cpp` within test-file size guidance (`169` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`