fix(plan): use structured alternatives objects in plan explain output per spec #9407

Merged
HAL9000 merged 5 commits from timeline/day-104-2026-04-14-auto-time-2 into master 2026-06-13 19:07:22 +00:00
5 changed files with 42 additions and 6 deletions
+4 -2
View File
@@ -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
+1 -1
View File
@@ -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
+27 -1
View File
@@ -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"
+2 -1
View File
@@ -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")
+8 -1
View File
@@ -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