diff --git a/features/plan_explain.feature b/features/plan_explain.feature index aa95a778c..4a88e0580 100644 --- a/features/plan_explain.feature +++ b/features/plan_explain.feature @@ -13,7 +13,7 @@ Feature: Plan explain and decision tree CLI commands And the explain dict should contain key "question" And the explain dict should contain key "chosen" And the explain dict should contain key "type" - And the explain dict should contain key "alternatives_considered" + And the explain dict should contain key "alternatives" And the explain dict should not contain key "rationale" And the explain dict should not contain key "context_snapshot" @@ -45,8 +45,10 @@ Feature: Plan explain and decision tree CLI commands Scenario: Explain includes alternatives by default Given a test decision with alternatives for explain When I build the explain dict with default options - Then the explain dict should contain key "alternatives_considered" + Then the explain dict should contain key "alternatives" And the alternatives list should have 2 items + And each alternative should have keys "index", "description", and "chosen" + And exactly one alternative should have chosen set to true # ------------------------------------------------------------------ # plan explain - json format diff --git a/features/plan_explain_cli_coverage.feature b/features/plan_explain_cli_coverage.feature index f9d2b87db..24c983ea3 100644 --- a/features/plan_explain_cli_coverage.feature +++ b/features/plan_explain_cli_coverage.feature @@ -49,7 +49,7 @@ Feature: Plan explain and tree CLI command coverage Given pec a mock DecisionService returning a decision with alternatives When pec I invoke "explain" with the decision id Then pec the exit code should be 0 - And pec the output should contain "alternatives_considered" + And pec the output should contain "alternatives" # ------------------------------------------------------------------ # tree_decisions_cmd - rich tree format diff --git a/features/steps/plan_explain_steps.py b/features/steps/plan_explain_steps.py index 02a5eee3f..ad5d31ed4 100644 --- a/features/steps/plan_explain_steps.py +++ b/features/steps/plan_explain_steps.py @@ -82,6 +82,7 @@ def step_test_decision_with_reasoning(context: Context) -> None: @given("a test decision with alternatives for explain") def step_test_decision_with_alternatives(context: Context) -> None: context.pe_decision = _make_decision( + chosen_option="GraphQL API", alternatives_considered=["GraphQL API", "gRPC service"], ) @@ -322,10 +323,35 @@ def step_snapshot_has_key(context: Context, key: str) -> None: @then("the alternatives list should have {count:d} items") def step_alternatives_count(context: Context, count: int) -> None: - alts = context.pe_explain_dict["alternatives_considered"] + alts = context.pe_explain_dict["alternatives"] assert len(alts) == count, f"Expected {count} alternatives, got {len(alts)}" +@then('each alternative should have keys "index", "description", and "chosen"') +def step_alternatives_have_required_keys(context: Context) -> None: + alts = context.pe_explain_dict["alternatives"] + for i, alt in enumerate(alts): + assert isinstance(alt, dict), f"Alternative {i} is not a dict: {alt!r}" + assert "index" in alt, ( + f"Alternative {i} missing 'index' key: {list(alt.keys())}" + ) + assert "description" in alt, ( + f"Alternative {i} missing 'description' key: {list(alt.keys())}" + ) + assert "chosen" in alt, ( + f"Alternative {i} missing 'chosen' key: {list(alt.keys())}" + ) + + +@then("exactly one alternative should have chosen set to true") +def step_exactly_one_chosen(context: Context) -> None: + alts = context.pe_explain_dict["alternatives"] + chosen_count = sum(1 for alt in alts if alt.get("chosen") is True) + assert chosen_count == 1, ( + f"Expected exactly 1 chosen alternative, got {chosen_count}" + ) + + @then('the json output should contain "{text}"') def step_json_contains(context: Context, text: str) -> None: assert text in context.pe_json_output, f"Expected '{text}' in JSON output" diff --git a/robot/helper_plan_explain.py b/robot/helper_plan_explain.py index 215940953..0a416f38a 100644 --- a/robot/helper_plan_explain.py +++ b/robot/helper_plan_explain.py @@ -49,7 +49,8 @@ def _test_explain_format() -> None: assert "decision_id" in data assert "context_snapshot" in data assert "rationale" in data - assert "alternatives_considered" in data + assert "alternatives" in data + assert isinstance(data["alternatives"], list) assert data["question"] == "What to build?" print("plan-explain-ok") diff --git a/src/cleveragents/cli/commands/plan.py b/src/cleveragents/cli/commands/plan.py index 3cead2b5f..13d0e3116 100644 --- a/src/cleveragents/cli/commands/plan.py +++ b/src/cleveragents/cli/commands/plan.py @@ -4253,7 +4253,14 @@ def _build_explain_dict( "is_correction": decision.is_correction, "superseded": decision.is_superseded, "created_at": decision.created_at.isoformat(), - "alternatives_considered": decision.alternatives_considered, + "alternatives": [ + { + "index": i + 1, + "description": alt, + "chosen": alt == decision.chosen_option, + } + for i, alt in enumerate(decision.alternatives_considered) + ], } if show_reasoning: data["rationale"] = decision.rationale