Compare commits

...

1 Commits

Author SHA1 Message Date
HAL9000 5db2265cc1 fix(plan): use structured alternatives objects in plan explain output per spec
Convert alternatives_considered list of strings to structured objects with
index (1-based), description, and chosen fields in _build_explain_dict().
Rename output field from alternatives_considered to alternatives.
Update BDD tests in plan_explain.feature, plan_explain_cli_coverage.feature,
and plan_explain_steps.py to validate the new structured format.

ISSUES CLOSED: #9166
2026-05-08 14:00:35 +00:00
7 changed files with 45 additions and 7 deletions
+5
View File
@@ -102,6 +102,11 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
`session show`, `session delete`, and `session export`, all of which require the full
26-character identifier. The full ULID is now displayed in all output formats (Rich, plain,
JSON, YAML, table).
- **`plan explain` output uses structured alternatives objects** (#9166): The `alternatives_considered`
field is replaced with a structured `alternatives` array where each element contains
an `"index"` (1-based), `"description"`, and `"chosen"` (boolean) key. This satisfies the
plan explain specification requiring per-alternative metadata rather than a flat list of
decision strings.
- **Suppress passing BDD scenario output in `unit_tests` by default** (#10987): Implemented
`PassSuppressFormatter`, a custom Behave formatter (in `scripts/behave_pass_suppress_formatter.py`)
+1
View File
@@ -36,3 +36,4 @@ Below are some of the specific details of various contributions.
* HAL 9000 has contributed the error-suppression removal fix (PR #9247 / issue #9060): removed both `try...except Exception:` blocks in `register_registry_agents()` that silently suppressed errors from `actor_registry.list_actors()` and the route bridge refresh, enabling exceptions to propagate per CONTRIBUTING.md fail-fast policy. Added three Behave scenarios verifying RuntimeError, AttributeError, and TypeError propagation.
* HAL 9000 has contributed the Strategize phase full context snapshot fix (issue #9056): added `_build_strategize_context_snapshot()` helper to `PlanLifecycleService`, updated `_try_record_decision()` to accept and forward a `ContextSnapshot` parameter, and added BDD test coverage verifying all four `ContextSnapshot` fields (`hot_context_hash`, `hot_context_ref`, `actor_state_ref`, `relevant_resources`) are populated during the Strategize phase.
* HAL 9000 has contributed the ACMS context path matching fix (PR #10975 / issue #10972): corrects `_path_matches()` and `_matches_pattern()` to properly match absolute fragment paths against relative glob patterns by auto-prefixing with `**/` before calling `PurePath.full_match()`, preventing silent inefficacy of include/exclude filters for absolute paths in fragment metadata.
* HAL 9000 has contributed the `plan explain` structured alternatives output fix (#9407 / #9166): replaced the flat `alternatives_considered` list with structured `alternatives` objects containing `index`, `description`, and `chosen` fields per plan explain specification, updated BDD step definitions and Robot integration tests accordingly.
+4 -3
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,9 @@ 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"
And the alternatives list should have 2 items
Then the explain dict should contain key "alternatives"
And each alternative must have keys "index", "description", and "chosen"
And exactly one alternative has chosen=true with 2 total alternatives
# ------------------------------------------------------------------
# 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
+24 -1
View File
@@ -322,10 +322,33 @@ 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 must have keys "{keys_csv}"'
)
def step_alternatives_have_required_keys(context: Context, keys_csv: str) -> None:
"""Verify all alternatives contain the specified keys."""
keys = [k.strip() for k in keys_csv.split(",")]
alts = context.pe_explain_dict["alternatives"]
for alt in alts:
for key in keys:
assert key in alt, f"Expected key '{key}' in alternative {alt}"
@then("exactly one alternative has chosen=true with {total:d} total alternatives")
def step_exactly_one_chosen(context: Context, total: int) -> None:
"""Verify exactly one alternative is marked as chosen."""
alts = context.pe_explain_dict["alternatives"]
assert len(alts) == total, f"Expected {total} alternatives, got {len(alts)}"
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
@@ -3909,7 +3909,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