fix(plan): use structured alternatives objects in plan explain output per spec #9407
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user