# z3ed Command Abstraction Layer Guide **Created**: October 11, 2025 **Status**: Implementation Complete ## Overview This guide documents the new command abstraction layer for z3ed CLI commands. The abstraction layer eliminates ~500+ lines of duplicated code across tool commands and provides a consistent, maintainable architecture for future command development. ## Problem Statement ### Before Abstraction The original `tool_commands.cc` (1549 lines) had severe code duplication: 1. **ROM Loading**: Every command had 20-30 lines of identical ROM loading logic 2. **Argument Parsing**: Each command manually parsed `--format`, `--rom`, `--type`, etc. 3. **Output Formatting**: JSON vs text formatting was duplicated across every command 4. **Label Initialization**: Resource label loading was repeated in every handler 5. **Error Handling**: Inconsistent error messages and validation patterns ### Code Duplication Example ```cpp // Repeated in EVERY command (30+ times): Rom rom_storage; Rom* rom = nullptr; if (rom_context != nullptr && rom_context->is_loaded()) { rom = rom_context; } else { auto rom_or = LoadRomFromFlag(); if (!rom_or.ok()) { return rom_or.status(); } rom_storage = std::move(rom_or.value()); rom = &rom_storage; } // Initialize labels (repeated in every command that needs labels) if (rom->resource_label()) { if (!rom->resource_label()->labels_loaded_) { core::YazeProject project; project.use_embedded_labels = true; auto labels_status = project.InitializeEmbeddedLabels(); // ... more boilerplate ... } } // Manual argument parsing (repeated everywhere) std::string format = "json"; for (size_t i = 0; i < arg_vec.size(); ++i) { const std::string& token = arg_vec[i]; if (token == "--format") { if (i + 1 >= arg_vec.size()) { return absl::InvalidArgumentError("--format requires a value."); } format = arg_vec[++i]; } else if (absl::StartsWith(token, "--format=")) { format = token.substr(9); } } // Manual output formatting (repeated everywhere) if (format == "json") { std::cout << "{\n"; std::cout << " \"field\": \"value\",\n"; std::cout << "}\n"; } else { std::cout << "Field: value\n"; } ``` ## Solution Architecture ### Three-Layer Abstraction 1. **CommandContext** - ROM loading, context management 2. **ArgumentParser** - Unified argument parsing 3. **OutputFormatter** - Consistent output formatting 4. **CommandHandler** (Optional) - Base class for structured commands ### File Structure ``` src/cli/service/resources/ ├── command_context.h # Context management ├── command_context.cc ├── command_handler.h # Base handler class ├── command_handler.cc └── (existing files...) src/cli/handlers/agent/ ├── tool_commands.cc # Original (to be refactored) ├── tool_commands_refactored.cc # Example refactored commands └── (other handlers...) ``` ## Core Components ### 1. CommandContext Encapsulates ROM loading and common context: ```cpp // Create context CommandContext::Config config; config.external_rom_context = rom_context; // Optional: use existing ROM config.rom_path = "/path/to/rom.sfc"; // Optional: override ROM path config.use_mock_rom = false; // Optional: use mock for testing config.format = "json"; CommandContext context(config); // Get ROM (auto-loads if needed) ASSIGN_OR_RETURN(Rom* rom, context.GetRom()); // Ensure labels loaded RETURN_IF_ERROR(context.EnsureLabelsLoaded(rom)); ``` **Benefits**: - Single location for ROM loading logic - Automatic error handling - Mock ROM support for testing - Label management abstraction ### 2. ArgumentParser Unified argument parsing with type safety: ```cpp ArgumentParser parser(arg_vec); // String arguments auto type = parser.GetString("type"); // Returns std::optional auto format = parser.GetString("format").value_or("json"); // Integer arguments (supports hex with 0x prefix) ASSIGN_OR_RETURN(int room_id, parser.GetInt("room")); // Hex-only arguments ASSIGN_OR_RETURN(int tile_id, parser.GetHex("tile")); // Flags if (parser.HasFlag("verbose")) { // ... } // Validation RETURN_IF_ERROR(parser.RequireArgs({"type", "query"})); ``` **Benefits**: - Consistent argument parsing across all commands - Type-safe with proper error handling - Supports both `--arg=value` and `--arg value` forms - Built-in hex parsing for ROM addresses ### 3. OutputFormatter Consistent JSON/text output: ```cpp ASSIGN_OR_RETURN(auto formatter, OutputFormatter::FromString("json")); formatter.BeginObject("Room Information"); formatter.AddField("room_id", "0x12"); formatter.AddHexField("address", 0x1234, 4); // Formats as "0x1234" formatter.AddField("sprite_count", 5); formatter.BeginArray("sprites"); formatter.AddArrayItem("Sprite 1"); formatter.AddArrayItem("Sprite 2"); formatter.EndArray(); formatter.EndObject(); formatter.Print(); ``` **Output (JSON)**: ```json { "room_id": "0x12", "address": "0x1234", "sprite_count": 5, "sprites": [ "Sprite 1", "Sprite 2" ] } ``` **Output (Text)**: ``` === Room Information === room_id : 0x12 address : 0x1234 sprite_count : 5 sprites: - Sprite 1 - Sprite 2 ``` **Benefits**: - No manual JSON escaping - Consistent formatting rules - Easy to switch between JSON and text - Proper indentation handling ### 4. CommandHandler (Optional Base Class) For more complex commands, use the base class pattern: ```cpp class MyCommandHandler : public CommandHandler { protected: std::string GetUsage() const override { return "agent my-command --required [--format ]"; } absl::Status ValidateArgs(const ArgumentParser& parser) override { return parser.RequireArgs({"required"}); } absl::Status Execute(Rom* rom, const ArgumentParser& parser, OutputFormatter& formatter) override { auto value = parser.GetString("required").value(); // Business logic here formatter.AddField("result", value); return absl::OkStatus(); } bool RequiresLabels() const override { return true; } }; // Usage: absl::Status HandleMyCommand(const std::vector& args, Rom* rom) { MyCommandHandler handler; return handler.Run(args, rom); } ``` **Benefits**: - Enforces consistent structure - Automatic context setup and teardown - Built-in error handling - Easy to test individual components ## Migration Guide ### Step-by-Step Refactoring #### Before (80 lines): ```cpp absl::Status HandleResourceListCommand( const std::vector& arg_vec, Rom* rom_context) { std::string type; std::string format = "table"; // Manual argument parsing (20 lines) for (size_t i = 0; i < arg_vec.size(); ++i) { const std::string& token = arg_vec[i]; if (token == "--type") { if (i + 1 >= arg_vec.size()) { return absl::InvalidArgumentError("--type requires a value."); } type = arg_vec[++i]; } else if (absl::StartsWith(token, "--type=")) { type = token.substr(7); } // ... repeat for --format ... } if (type.empty()) { return absl::InvalidArgumentError("Usage: ..."); } // ROM loading (30 lines) Rom rom_storage; Rom* rom = nullptr; if (rom_context != nullptr && rom_context->is_loaded()) { rom = rom_context; } else { auto rom_or = LoadRomFromFlag(); if (!rom_or.ok()) { return rom_or.status(); } rom_storage = std::move(rom_or.value()); rom = &rom_storage; } // Label initialization (15 lines) if (rom->resource_label()) { if (!rom->resource_label()->labels_loaded_) { core::YazeProject project; project.use_embedded_labels = true; auto labels_status = project.InitializeEmbeddedLabels(); if (labels_status.ok()) { rom->resource_label()->labels_ = project.resource_labels; rom->resource_label()->labels_loaded_ = true; } } } // Business logic ResourceContextBuilder context_builder(rom); auto labels_or = context_builder.GetLabels(type); if (!labels_or.ok()) { return labels_or.status(); } auto labels = std::move(labels_or.value()); // Manual output formatting (15 lines) if (format == "json") { std::cout << "{\n"; for (const auto& [key, value] : labels) { std::cout << " \"" << key << "\": \"" << value << "\",\n"; } std::cout << "}\n"; } else { for (const auto& [key, value] : labels) { std::cout << key << ": " << value << "\n"; } } return absl::OkStatus(); } ``` #### After (30 lines): ```cpp absl::Status HandleResourceListCommand( const std::vector& arg_vec, Rom* rom_context) { // Parse arguments ArgumentParser parser(arg_vec); auto type = parser.GetString("type"); auto format_str = parser.GetString("format").value_or("table"); if (!type.has_value()) { return absl::InvalidArgumentError( "Usage: agent resource-list --type [--format ]"); } // Create formatter ASSIGN_OR_RETURN(auto formatter, OutputFormatter::FromString(format_str)); // Setup context CommandContext::Config config; config.external_rom_context = rom_context; CommandContext context(config); // Get ROM and labels ASSIGN_OR_RETURN(Rom* rom, context.GetRom()); RETURN_IF_ERROR(context.EnsureLabelsLoaded(rom)); // Execute business logic ResourceContextBuilder builder(rom); ASSIGN_OR_RETURN(auto labels, builder.GetLabels(*type)); // Format output formatter.BeginObject("Labels"); for (const auto& [key, value] : labels) { formatter.AddField(key, value); } formatter.EndObject(); formatter.Print(); return absl::OkStatus(); } ``` **Savings**: 50+ lines eliminated, clearer intent, easier to maintain ### Commands to Refactor Priority order for refactoring (based on duplication level): 1. **High Priority** (Heavy duplication): - `HandleResourceListCommand` - Example provided ✓ - `HandleResourceSearchCommand` - Example provided ✓ - `HandleDungeonDescribeRoomCommand` - 80 lines → ~35 lines - `HandleOverworldDescribeMapCommand` - 100 lines → ~40 lines - `HandleOverworldListWarpsCommand` - 120 lines → ~45 lines 2. **Medium Priority** (Moderate duplication): - `HandleDungeonListSpritesCommand` - `HandleOverworldFindTileCommand` - `HandleOverworldListSpritesCommand` - `HandleOverworldGetEntranceCommand` - `HandleOverworldTileStatsCommand` 3. **Low Priority** (Simple commands, less duplication): - `HandleMessageListCommand` (delegates to message handler) - `HandleMessageReadCommand` (delegates to message handler) - `HandleMessageSearchCommand` (delegates to message handler) ### Estimated Impact | Metric | Before | After | Savings | |--------|--------|-------|---------| | Lines of code (tool_commands.cc) | 1549 | ~800 | **48%** | | Duplicated ROM loading | ~600 lines | 0 | **600 lines** | | Duplicated arg parsing | ~400 lines | 0 | **400 lines** | | Duplicated formatting | ~300 lines | 0 | **300 lines** | | **Total Duplication Removed** | | | **~1300 lines** | ## Testing Strategy ### Unit Testing ```cpp TEST(CommandContextTest, LoadsRomFromConfig) { CommandContext::Config config; config.rom_path = "test.sfc"; CommandContext context(config); auto rom_or = context.GetRom(); ASSERT_OK(rom_or); EXPECT_TRUE(rom_or.value()->is_loaded()); } TEST(ArgumentParserTest, ParsesStringArguments) { std::vector args = {"--type=dungeon", "--format", "json"}; ArgumentParser parser(args); EXPECT_EQ(parser.GetString("type").value(), "dungeon"); EXPECT_EQ(parser.GetString("format").value(), "json"); } TEST(OutputFormatterTest, GeneratesValidJson) { auto formatter = OutputFormatter::FromString("json").value(); formatter.BeginObject("Test"); formatter.AddField("key", "value"); formatter.EndObject(); std::string output = formatter.GetOutput(); EXPECT_THAT(output, HasSubstr("\"key\": \"value\"")); } ``` ### Integration Testing ```cpp TEST(ResourceListCommandTest, ListsDungeons) { std::vector args = {"--type=dungeon", "--format=json"}; Rom rom; rom.LoadFromFile("test.sfc"); auto status = HandleResourceListCommand(args, &rom); EXPECT_OK(status); } ``` ## Benefits Summary ### For Developers 1. **Less Code to Write**: New commands take 30-40 lines instead of 80-120 2. **Consistent Patterns**: All commands follow the same structure 3. **Better Error Handling**: Standardized error messages and validation 4. **Easier Testing**: Each component can be tested independently 5. **Self-Documenting**: Clear separation of concerns ### For Maintainability 1. **Single Source of Truth**: ROM loading logic in one place 2. **Easy to Update**: Change all commands by updating one class 3. **Consistent Behavior**: All commands handle errors the same way 4. **Reduced Bugs**: Less duplication = fewer places for bugs ### For AI Integration 1. **Predictable Structure**: AI can generate commands using templates 2. **Type Safety**: ArgumentParser prevents common errors 3. **Consistent Output**: AI can reliably parse JSON responses 4. **Easy to Extend**: New tool types follow existing patterns ## Next Steps ### Immediate (Current PR) 1. Create abstraction layer (CommandContext, ArgumentParser, OutputFormatter) 2. Add CommandHandler base class 3. Provide refactored examples 4. Update build system 5. Document architecture ### Phase 2 (Next PR) 1. Refactor high-priority commands (5 commands) 2. Add comprehensive unit tests 3. Update AI tool dispatcher to use new patterns 4. Create command generator templates for AI ### Phase 3 (Future) 1. Refactor remaining commands 2. Remove old helper functions 3. Add performance benchmarks 4. Create VS Code snippets for command development ## Migration Checklist For each command being refactored: - [ ] Replace manual argument parsing with ArgumentParser - [ ] Replace ROM loading with CommandContext - [ ] Replace label initialization with context.EnsureLabelsLoaded() - [ ] Replace manual formatting with OutputFormatter - [ ] Update error messages to use GetUsage() - [ ] Add unit tests for the command - [ ] Update documentation - [ ] Test with both JSON and text output - [ ] Test with missing/invalid arguments - [ ] Test with mock ROM ## References - Implementation: `src/cli/service/resources/command_context.{h,cc}` - Examples: `src/cli/handlers/agent/tool_commands_refactored.cc` - Base class: `src/cli/service/resources/command_handler.{h,cc}` - Build config: `src/cli/agent.cmake` ## Questions & Answers **Q: Should I refactor all commands at once?** A: No. Refactor in phases to minimize risk. Start with 2-3 commands as proof of concept. **Q: What if my command needs custom argument handling?** A: ArgumentParser is flexible. You can still access raw args or add custom parsing logic. **Q: Can I use both old and new patterns temporarily?** A: Yes. The new abstraction layer works alongside existing code. Migrate gradually. **Q: Will this affect AI tool calling?** A: No breaking changes. The command interfaces remain the same. Internal implementation improves. **Q: How do I test commands with the new abstractions?** A: Use CommandContext with mock ROM, or pass external rom_context in tests. --- **Last Updated**: October 11, 2025 **Author**: AI Assistant **Review Status**: Ready for Implementation