From ab7b8a862b5c827dc8aa30aea118f12ce48f6d36 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 16 Feb 2026 17:20:11 -0700 Subject: [PATCH] Step 433: add inline code annotation badges model --- editor/CMakeLists.txt | 9 ++ editor/src/CodeAnnotationBadges.h | 88 +++++++++++++++++++ editor/tests/step433_test.cpp | 141 ++++++++++++++++++++++++++++++ progress.md | 46 ++++++++++ 4 files changed, 284 insertions(+) create mode 100644 editor/src/CodeAnnotationBadges.h create mode 100644 editor/tests/step433_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 1a91144..ba2aa54 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2776,4 +2776,13 @@ target_link_libraries(step432_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step433_test tests/step433_test.cpp) +target_include_directories(step433_test PRIVATE src) +target_link_libraries(step433_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/CodeAnnotationBadges.h b/editor/src/CodeAnnotationBadges.h new file mode 100644 index 0000000..7edaaf2 --- /dev/null +++ b/editor/src/CodeAnnotationBadges.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include +#include + +struct AnnotationBadgeInput { + int line = 0; + std::string type; + std::string detail; + std::string nodeId; +}; + +struct InlineBadge { + int line = 0; + std::string type; + std::string icon; + std::string colorHex; + std::string detail; + std::string nodeId; +}; + +struct LineBadgeStack { + int line = 0; + std::vector badges; +}; + +class CodeAnnotationBadges { +public: + static std::vector buildBadges(const std::vector& in) { + std::vector out; + for (const auto& a : in) { + InlineBadge b; + b.line = a.line; + b.type = a.type; + b.icon = iconForType(a.type); + b.colorHex = colorForType(a.type); + b.detail = a.detail; + b.nodeId = a.nodeId; + out.push_back(b); + } + std::sort(out.begin(), out.end(), + [](const InlineBadge& a, const InlineBadge& b) { + if (a.line != b.line) return a.line < b.line; + return a.type < b.type; + }); + return out; + } + + static std::vector stackByLine(const std::vector& badges) { + std::map> grouped; + for (const auto& b : badges) grouped[b.line].push_back(b); + std::vector out; + for (auto& [line, lineBadges] : grouped) { + out.push_back({line, lineBadges}); + } + std::sort(out.begin(), out.end(), + [](const LineBadgeStack& a, const LineBadgeStack& b) { + return a.line < b.line; + }); + return out; + } + + static std::string expandDetail(const InlineBadge& badge) { + return badge.type + " [" + badge.nodeId + "]: " + badge.detail; + } + + static std::string iconForType(const std::string& type) { + if (type.find("Intent") != std::string::npos) return "I"; + if (type.find("Complexity") != std::string::npos) return "C"; + if (type.find("Risk") != std::string::npos) return "R"; + if (type.find("Contract") != std::string::npos) return "K"; + if (type.find("Review") != std::string::npos) return "V"; + if (type.find("Priority") != std::string::npos) return "P"; + return "A"; + } + + static std::string colorForType(const std::string& type) { + if (type.find("Intent") != std::string::npos) return "#1ABC9C"; + if (type.find("Complexity") != std::string::npos) return "#F39C12"; + if (type.find("Risk") != std::string::npos) return "#E74C3C"; + if (type.find("Contract") != std::string::npos) return "#3498DB"; + if (type.find("Review") != std::string::npos) return "#9B59B6"; + if (type.find("Priority") != std::string::npos) return "#2ECC71"; + return "#95A5A6"; + } +}; diff --git a/editor/tests/step433_test.cpp b/editor/tests/step433_test.cpp new file mode 100644 index 0000000..12c1041 --- /dev/null +++ b/editor/tests/step433_test.cpp @@ -0,0 +1,141 @@ +// Step 433: Inline Annotation Badges Tests (12 tests) + +#include "CodeAnnotationBadges.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 std::vector sampleInputs() { + return { + {10, "IntentAnnotation", "Parse auth tokens", "n1"}, + {10, "RiskAnnotation", "High external exposure", "n1"}, + {20, "ComplexityAnnotation", "Cyclomatic=12", "n2"}, + {30, "ReviewAnnotation", "Human review required", "n3"} + }; +} + +void test_build_badges_count_matches_input() { + TEST(build_badges_count_matches_input); + auto badges = CodeAnnotationBadges::buildBadges(sampleInputs()); + CHECK(badges.size() == sampleInputs().size(), "badge count mismatch"); + PASS(); +} + +void test_badges_sorted_by_line_then_type() { + TEST(badges_sorted_by_line_then_type); + auto badges = CodeAnnotationBadges::buildBadges({ + {30, "RiskAnnotation", "r", "n3"}, + {10, "IntentAnnotation", "i", "n1"}, + {10, "ComplexityAnnotation", "c", "n1"} + }); + CHECK(badges[0].line == 10 && badges[1].line == 10 && badges[2].line == 30, + "line sort mismatch"); + PASS(); +} + +void test_icon_mapping_for_known_types() { + TEST(icon_mapping_for_known_types); + CHECK(CodeAnnotationBadges::iconForType("IntentAnnotation") == "I", "intent icon"); + CHECK(CodeAnnotationBadges::iconForType("ComplexityAnnotation") == "C", "complexity icon"); + CHECK(CodeAnnotationBadges::iconForType("RiskAnnotation") == "R", "risk icon"); + PASS(); +} + +void test_color_mapping_for_known_types() { + TEST(color_mapping_for_known_types); + CHECK(CodeAnnotationBadges::colorForType("IntentAnnotation") == "#1ABC9C", "intent color"); + CHECK(CodeAnnotationBadges::colorForType("RiskAnnotation") == "#E74C3C", "risk color"); + PASS(); +} + +void test_unknown_type_fallback_icon_and_color() { + TEST(unknown_type_fallback_icon_and_color); + CHECK(CodeAnnotationBadges::iconForType("UnknownAnnotation") == "A", "fallback icon"); + CHECK(CodeAnnotationBadges::colorForType("UnknownAnnotation") == "#95A5A6", "fallback color"); + PASS(); +} + +void test_stack_by_line_groups_multiple_badges_same_line() { + TEST(stack_by_line_groups_multiple_badges_same_line); + auto badges = CodeAnnotationBadges::buildBadges(sampleInputs()); + auto stacks = CodeAnnotationBadges::stackByLine(badges); + int line10Count = 0; + for (const auto& s : stacks) if (s.line == 10) line10Count = (int)s.badges.size(); + CHECK(line10Count == 2, "line 10 should have two stacked badges"); + PASS(); +} + +void test_stack_by_line_sorted_by_line() { + TEST(stack_by_line_sorted_by_line); + auto badges = CodeAnnotationBadges::buildBadges(sampleInputs()); + auto stacks = CodeAnnotationBadges::stackByLine(badges); + CHECK(stacks.size() >= 3, "expected three lines"); + CHECK(stacks[0].line < stacks[1].line, "stacks should be sorted by line"); + PASS(); +} + +void test_expand_detail_contains_type_node_and_detail() { + TEST(expand_detail_contains_type_node_and_detail); + auto badges = CodeAnnotationBadges::buildBadges(sampleInputs()); + auto text = CodeAnnotationBadges::expandDetail(badges[0]); + CHECK(text.find(badges[0].type) != std::string::npos, "missing type"); + CHECK(text.find(badges[0].nodeId) != std::string::npos, "missing node"); + CHECK(text.find(badges[0].detail) != std::string::npos, "missing detail"); + PASS(); +} + +void test_badge_preserves_line_and_node_id() { + TEST(badge_preserves_line_and_node_id); + auto badges = CodeAnnotationBadges::buildBadges(sampleInputs()); + CHECK(badges[0].line > 0, "line missing"); + CHECK(!badges[0].nodeId.empty(), "node id missing"); + PASS(); +} + +void test_empty_input_returns_empty_badges() { + TEST(empty_input_returns_empty_badges); + auto badges = CodeAnnotationBadges::buildBadges({}); + CHECK(badges.empty(), "expected empty badges"); + PASS(); +} + +void test_empty_badges_return_empty_stacks() { + TEST(empty_badges_return_empty_stacks); + auto stacks = CodeAnnotationBadges::stackByLine({}); + CHECK(stacks.empty(), "expected empty stacks"); + PASS(); +} + +void test_review_annotation_has_review_icon() { + TEST(review_annotation_has_review_icon); + auto badges = CodeAnnotationBadges::buildBadges({{5, "ReviewAnnotation", "review me", "n1"}}); + CHECK(!badges.empty(), "badge missing"); + CHECK(badges[0].icon == "V", "review icon mismatch"); + PASS(); +} + +int main() { + std::cout << "Step 433: Inline Annotation Badges Tests\n"; + + test_build_badges_count_matches_input(); // 1 + test_badges_sorted_by_line_then_type(); // 2 + test_icon_mapping_for_known_types(); // 3 + test_color_mapping_for_known_types(); // 4 + test_unknown_type_fallback_icon_and_color(); // 5 + test_stack_by_line_groups_multiple_badges_same_line();// 6 + test_stack_by_line_sorted_by_line(); // 7 + test_expand_detail_contains_type_node_and_detail(); // 8 + test_badge_preserves_line_and_node_id(); // 9 + test_empty_input_returns_empty_badges(); // 10 + test_empty_badges_return_empty_stacks(); // 11 + test_review_annotation_has_review_icon(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index a899914..b4f3483 100644 --- a/progress.md +++ b/progress.md @@ -4994,6 +4994,52 @@ changes and review lifecycle transitions. - `editor/src/MCPServer.h` (`1940` > `600`) - `editor/src/HeadlessAgentRPCHandler.h` (`2768` > `600`) +## Phase 19b: Code Annotation Overlay + +### Step 433: Inline Annotation Badges +**Status:** PASS (12/12 tests) + +Added an inline-annotation badge model for editor gutter overlays, including +type-specific icon/color mapping, stacked line badges, and expandable detail text. + +**Files created:** +- `editor/src/CodeAnnotationBadges.h` — inline badge support: + - annotation input -> badge projection + - type->icon mapping (intent/complexity/risk/contract/review/priority) + - type->color mapping for visual encoding + - deterministic line/type sorting + - line-based stacking for multiple annotations on one line + - expandable detail formatter +- `editor/tests/step433_test.cpp` — 12 tests covering: + 1. input->badge count preservation + 2. sort stability by line/type + 3. known icon mapping + 4. known color mapping + 5. unknown-type fallback icon/color + 6. multi-badge same-line stacking + 7. stack ordering by line + 8. expanded detail payload content + 9. line/node identity preservation + 10. empty input edge case + 11. empty stack edge case + 12. review-annotation icon mapping + +**Files modified:** +- `editor/CMakeLists.txt` — `step433_test` target + +**Verification run:** +- `step433_test` — PASS (12/12) new step coverage +- `step432_test` — PASS (8/8) regression coverage +- `step431_test` — PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/CodeAnnotationBadges.h` within header-size limit (`88` <= `600`) +- `editor/tests/step433_test.cpp` within test-file size guidance (`141` lines) +- Legacy oversized headers persist: + - `editor/src/ast/Serialization.h` (`1427` > `600`) + - `editor/src/MCPServer.h` (`1940` > `600`) + - `editor/src/HeadlessAgentRPCHandler.h` (`2768` > `600`) + # Roadmap Planning — Sprints 12-25+ ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)