{
"feature": "f3-binding-generator",
"description": "Binding Generator — transforms ScannedCLITool into .binding.yaml files with module IDs, JSON Schemas, YAML serialization, and a CLI executor function",
"language": "rust",
"tasks": [
{
"id": "F3-T1",
"title": "Module ID generator",
"description": "Implement generate_module_id() that converts a tool name and command path into a canonical apcore module ID (e.g., git + [commit] -> cli.git.commit). Includes sanitize_segment() for hyphen replacement, digit-prefix handling, and empty-segment fallback.",
"test_first": "Write parameterized rstest cases: (git, [commit]) -> cli.git.commit; (docker, [container, ls]) -> cli.docker.container.ls; (my-tool, [sub-cmd]) -> cli.my_tool.sub_cmd; (3ds, []) -> cli.x3ds; empty tool -> cli.unknown. Also test that a 130-char path returns Err(ParseError) and that all generated IDs match the regex ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$.",
"implementation": "Create src/binding/module_id.rs with generate_module_id(tool_name, command_path) -> Result<String, ApexeError> and sanitize_segment(). Join segments with dots, prefix with 'cli.', validate against regex, enforce 128-char limit.",
"acceptance": "All parameterized test cases pass. Valid IDs match the regex. Oversized IDs return ParseError. Hyphens become underscores, digit-prefixed segments get 'x' prefix.",
"status": "pending",
"depends_on": []
},
{
"id": "F3-T2",
"title": "JSON Schema generator from ScannedCommand",
"description": "Implement SchemaGenerator with generate_input_schema() and generate_output_schema(). Maps ScannedFlag fields to JSON Schema properties (type, default, enum, array for repeatable) and ScannedArg to required properties. Output schema always includes stdout, stderr, exit_code; optionally json_output.",
"test_first": "Write tests for each flag-to-schema mapping: string required flag -> in required array with type string; boolean flag -> type boolean default false; integer flag with default '10' -> type integer default 10 (coerced); enum flag -> type string with enum array; repeatable flag -> type array with items; Path flag -> type string. Test positional args: required arg in required array; variadic arg -> type array. Test output schema with and without structured output support.",
"implementation": "Create src/binding/schema_gen.rs with SchemaGenerator struct. Implement value_type_to_json_schema() mapping, flag_to_schema() with default coercion and enum handling, arg_to_schema() with variadic support, generate_input_schema() combining flags and args, generate_output_schema() with conditional json_output.",
"acceptance": "All flag-to-schema mappings produce correct JSON Schema. Required fields appear in required array. Boolean defaults are false. Integer/float defaults are coerced from strings. Repeatable flags produce array schemas. Output schema includes json_output only when structured output is supported.",
"status": "pending",
"depends_on": []
},
{
"id": "F3-T3",
"title": "Binding YAML serializer",
"description": "Implement BindingYAMLWriter that serializes a GeneratedBindingFile to a .binding.yaml file on disk. Output includes the auto-generated header comment and serde_yaml serialization of the bindings array.",
"test_first": "Write a test that creates a GeneratedBindingFile with known bindings, calls write() to a tempdir, reads the file back, verifies: (1) file exists at expected path, (2) content starts with '# Auto-generated by apexe', (3) content is valid YAML parseable by serde_yaml::from_str, (4) deserialized data contains correct number of bindings with correct module_ids.",
"implementation": "Create src/binding/writer.rs with BindingYAMLWriter struct and write() method. Create output directory with create_dir_all, serialize bindings to YAML via serde_yaml::to_string, prepend header comment, write to {tool_name}.binding.yaml, return the file path.",
"acceptance": "Writer creates valid YAML files in the specified directory. Files are round-trippable through serde_yaml. File name matches {tool_name}.binding.yaml pattern. Directory is created if it doesn't exist.",
"status": "pending",
"depends_on": ["F3-T1", "F3-T2"]
},
{
"id": "F3-T4",
"title": "CLI executor function",
"description": "Implement execute_cli() that wraps std::process::Command. Takes binary name, command path, timeout, optional JSON flag, optional working directory, and kwargs map. Builds CLI arguments from kwargs (bool -> flag only if true, array -> repeated flag+value, string/int -> flag+value). Never invokes a shell.",
"test_first": "Write tests: (1) simple command — execute_cli with 'echo' binary and message kwarg returns stdout containing the message; (2) boolean true flag — {all: true} produces --all in args; (3) boolean false flag — {all: false} does NOT produce --all; (4) underscore-to-hyphen — key 'no_cache' becomes '--no-cache'; (5) array values — {include: ['a', 'b']} produces --include a --include b; (6) null values are skipped.",
"implementation": "Create src/executor.rs with execute_cli() function. Build cmd_args from apexe_command, iterate kwargs to build flags using key.replace('_', '-'), handle each JSON value type. Use Command::new(apexe_binary) with args (no shell). Capture stdout, stderr, exit_code into a result map. Implement json_value_to_string() helper.",
"acceptance": "Command executes without shell invocation. Boolean true flags appear, false flags are omitted. Array values produce repeated flag pairs. Underscore keys map to hyphenated flags. Null values are skipped. Output map contains stdout, stderr, exit_code.",
"status": "pending",
"depends_on": []
},
{
"id": "F3-T5",
"title": "Structured output capture",
"description": "Extend execute_cli() to capture stdout, stderr, and exit_code as structured JSON output. When apexe_json_flag is set and stdout contains valid JSON, parse it into a json_output field.",
"test_first": "Write tests: (1) stdout and stderr are captured as strings in the result map; (2) exit_code is captured as integer (0 for success); (3) non-zero exit code from a failing command is captured correctly; (4) when apexe_json_flag is Some and stdout is valid JSON, json_output field is present with parsed value; (5) when apexe_json_flag is Some but stdout is not valid JSON, json_output is absent; (6) when apexe_json_flag is None, json_output is never present even if stdout is valid JSON.",
"implementation": "In execute_cli(), after command execution: convert stdout/stderr via String::from_utf8_lossy, extract exit code via output.status.code().unwrap_or(-1), build result map. If apexe_json_flag is Some and stdout is non-empty, attempt serde_json::from_str and insert json_output on success.",
"acceptance": "All three fields (stdout, stderr, exit_code) always present. JSON parsing only attempted when json_flag is set. Parsed JSON stored as json_output. Invalid JSON silently ignored (no error).",
"status": "pending",
"depends_on": ["F3-T4"]
},
{
"id": "F3-T6",
"title": "JSON output detection and injection prevention",
"description": "Implement validate_no_injection() that rejects shell metacharacters (;|&$`\\'\"\\n\\r) in command arguments. Also handle JSON output flag injection: when apexe_json_flag is set, split it with shell_words and append to command args.",
"test_first": "Write tests: (1) semicolon in value returns Err(CommandInjection) with correct param_name and chars containing ';'; (2) pipe character blocked; (3) backtick blocked; (4) dollar sign blocked; (5) ampersand blocked; (6) newline blocked; (7) clean value passes validation; (8) JSON flag '--format json' is split and appended as two args; (9) JSON flag '--output=json' appended as single arg.",
"implementation": "Implement validate_no_injection() with SHELL_INJECTION_CHARS constant and HashSet-based check. Return ApexeError::CommandInjection with param name and found chars. In execute_cli(), call validate_no_injection for every non-bool, non-null kwarg value. Use shell_words::split for json_flag appending.",
"acceptance": "All shell metacharacters are rejected with descriptive error. Clean values pass. JSON flags are correctly split and appended. No false positives on normal alphanumeric/path values.",
"status": "pending",
"depends_on": ["F3-T4"]
},
{
"id": "F3-T7",
"title": "End-to-end: ScannedCLITool to .binding.yaml file",
"description": "Implement BindingGenerator that takes a ScannedCLITool, recursively generates bindings for all commands/subcommands, deduplicates module IDs, and produces a GeneratedBindingFile. Wire together module_id, schema_gen, and writer into the full pipeline.",
"test_first": "Write an end-to-end test: construct a ScannedCLITool for a mock 'git' tool with subcommands (status, commit, remote.add). Call BindingGenerator::generate() then BindingYAMLWriter::write() to a tempdir. Verify: (1) file git.binding.yaml exists; (2) contains bindings for cli.git.status, cli.git.commit, cli.git.remote.add; (3) each binding has correct input_schema, output_schema, target, tags, metadata fields; (4) metadata contains apexe_binary, apexe_command, apexe_timeout. Also test deduplication: two commands that would produce the same module ID get _2 suffix.",
"implementation": "Create src/binding/binding_gen.rs with GeneratedBinding, GeneratedBindingFile structs and BindingGenerator. Implement generate() that calls generate_command_bindings() recursively, building command_path for each level. Populate metadata (apexe_binary, apexe_command, apexe_timeout, apexe_json_flag). Call deduplicate_ids() to handle collisions. Create src/binding/mod.rs to re-export all binding submodules.",
"acceptance": "Full pipeline produces valid .binding.yaml from ScannedCLITool. All subcommands generate bindings with correct module IDs. Metadata is populated. Deduplication appends _2 suffix on collision. File is written with correct name and header.",
"status": "pending",
"depends_on": ["F3-T1", "F3-T2", "F3-T3"]
},
{
"id": "F3-T8",
"title": "Integration test: generated binding loads into apcore Registry",
"description": "Verify that a generated .binding.yaml file can be loaded by apcore's BindingLoader and that the resulting modules register correctly in the apcore Registry. This validates compatibility between apexe output and apcore input.",
"test_first": "Write an integration test: (1) construct a ScannedCLITool, generate and write binding to tempdir; (2) use apcore BindingLoader::load_bindings() to load the generated YAML file; (3) verify loading succeeds without errors; (4) create a Registry and register the loaded modules; (5) verify each expected module_id is present in the Registry; (6) verify the registered modules have correct input_schema and output_schema; (7) verify target is 'apexe::executor::execute_cli'.",
"implementation": "Create tests/integration/binding_registry.rs. Add apcore as a dev-dependency if not already present. Build the full pipeline (scan mock -> generate -> write -> load -> register). Assert on Registry contents matching the generated binding data.",
"acceptance": "Generated bindings load successfully via BindingLoader. All module_ids appear in Registry. Schemas match what was generated. No deserialization errors. This proves apexe output is compatible with the apcore ecosystem.",
"status": "pending",
"depends_on": ["F3-T7"]
}
]
}