fix: KB tree builder demotes decisions with < 2 branches to actions

Decision nodes with fewer than 2 buildable child targets now get
demoted to action nodes instead of creating invalid tree structures.
Also adds id fields to option objects (required by tree editor).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-11 02:31:17 -04:00
parent 416bb230e3
commit c1fb2f180c

View File

@@ -667,26 +667,60 @@ def _build_troubleshooting_tree(nodes: list[KBImportNode]) -> dict:
# question/decision type — recursively build children
options = content.get("options", [])
children = []
# Count how many options point to buildable (not-yet-placed) targets
buildable_targets = []
for opt in options:
next_id = opt.get("next_node_id")
if next_id and next_id in original_id_map and next_id not in placed:
buildable_targets.append(next_id)
# Decision nodes MUST have at least 2 branches to pass validation.
# If fewer than 2 buildable targets, demote to action node.
if len(buildable_targets) < 2:
demoted: dict = {
"id": node_id,
"type": "action",
"title": question_text,
"description": content.get("description", ""),
}
if buildable_targets:
demoted["next_node_id"] = buildable_targets[0]
elif options:
# All targets already placed; reference first option's target
first_next = options[0].get("next_node_id")
if first_next:
demoted["next_node_id"] = first_next
return demoted
# Build children for decision node
children = []
built_options = []
for opt in options:
next_id = opt.get("next_node_id")
opt_id = opt.get("id", f"opt-{node_id}-{len(built_options)}")
if next_id and next_id in original_id_map:
child_node = _build_node(original_id_map[next_id])
if child_node is not None:
children.append(child_node)
# If the child is an action with a next_node_id, also
# build that target as a sibling (the tree editor
# expects reachable nodes nested under the decision)
_collect_action_chain(child_node, children)
built_options.append({
"id": opt_id,
"label": opt.get("label", ""),
"next_node_id": next_id,
})
else:
built_options.append({
"id": opt_id,
"label": opt.get("label", ""),
"next_node_id": next_id or "",
})
return {
"id": node_id,
"type": "decision",
"question": question_text,
"options": [
{"label": opt.get("label", ""), "next_node_id": opt.get("next_node_id", "")}
for opt in options
],
"options": built_options,
"children": children,
}