Skip to content

fix(roadmap): resolve Feature nesting under Milestone/Epic#207

Open
xRiErOS wants to merge 8 commits into
hmans:mainfrom
xRiErOS:fix/beans-ti53-roadmap-nested-hierarchy
Open

fix(roadmap): resolve Feature nesting under Milestone/Epic#207
xRiErOS wants to merge 8 commits into
hmans:mainfrom
xRiErOS:fix/beans-ti53-roadmap-nested-hierarchy

Conversation

@xRiErOS

@xRiErOS xRiErOS commented Jul 17, 2026

Copy link
Copy Markdown

Summary

beans roadmap only walked one level of children per epic/milestone, so any bean parented under a feature (a documented, valid parent type — see pkg/bean/bean.go:159) never appeared in the output, and epics whose only open descendants sat below a feature vanished entirely.

This PR makes roadmap resolve the full Milestone → Epic → Feature → Leaf hierarchy:

  • Adds featureGroup to the roadmap data model (JSON + Markdown), resolved recursively via new collectLeafDescendants / buildFeatureGroup / buildEpicGroup / classifyFeatureChild helpers, applied uniformly to milestone-scoped, epic-scoped, unscheduled-epic and orphan-feature cases.
  • A feature with descendants now renders as a #### Feature: section with its nested leaves. A childless feature still renders as a flat line — parity with the previous behavior, no visibility regression.
  • Feature-less roadmaps are byte-identical to before (verified by diffing against a main baseline binary). The JSON schema change is additive only — features fields are omitempty.
  • Hardens collectLeafDescendants with a visited set so a hand-edited cyclic parent: chain can't stack-overflow roadmap generation (the CLI's ValidateParent/cycle detection already prevents this for tool-created data; beans are hand-editable markdown).

Pre-existing scope limit, unchanged by this PR: --status / --no-status filter milestone selection only; leaf/feature items are filtered by archive-status, never re-filtered by --status.

Test plan

  • New unit tests in internal/commands/roadmap_test.go covering: leaf under feature under epic under milestone; epic-dropped-if-only-feature-descendants regression; milestone-direct-feature inclusion line; unscheduled epic with feature nesting; orphan feature with children; childless feature renders flat under orphan / epic / milestone; feature with only-completed children edge; hand-edited parent-cycle safety; nested-orphan-feature double-render guard.
  • go test ./... green.
  • go vet ./... clean; gofmt clean on the changed file.
  • Diffed Markdown output for a feature-less fixture against a main baseline → byte-identical (no template whitespace regression).
  • Manually verified against a real multi-level beans repo — no crash/panic.

xRiErOS added 8 commits July 17, 2026 22:46
- Introduce featureGroup{Feature, Items} mirroring the existing
  epicGroup pattern.
- Wire Features []featureGroup (omitempty) into milestoneGroup,
  epicGroup, and unscheduledGroup so a later task can populate the
  Milestone -> Epic -> Feature -> Leaf chain.
- Struct-only change; no behavior change yet (all Features fields are
  nil/unused until Task 2/3 wire the resolution logic).

Refs: beans-ti53
…lpers

- splitByContainerType separates direct children into leafs vs feature-typed
- collectLeafDescendants recursively flattens leaf beans below a feature,
  transparently walking nested feature containers
- collectLeafDescendantsVisited guards against hand-authored parent cycles
  in markdown (CLI-side validation can't catch manual edits) via a visited map

Refs: beans-ti53
Epics/Milestones whose only open descendants sit below a Feature child
were silently dropped from 'beans roadmap' output because
buildMilestoneGroup only looked at direct children. Feature is now a
recognized container type, resolved recursively via buildFeatureGroup.

Refs: beans-ti53
Mutation-probe review (beans-d223) found the
`|| len(group.Features) > 0` inclusion check in buildRoadmap was
untested — every existing feature-nesting test also had an epic
sibling under the milestone, so reverting the clause left all tests
green. Add a milestone-with-only-a-feature-child case to close the
gap.

Refs: beans-ti53
…ng it

The feature-nesting fix (beans-en7i) treats every feature-typed bean as a
container: it only renders via buildFeatureGroup when it has >=1 leaf
descendant, and the orphan-items loop unconditionally skips Type==feature.
A feature with zero children therefore vanished from `beans roadmap`
entirely instead of falling back to a flat leaf line like it did before
the nesting fix.

Per D01 (PO decision, beans-n8zw): a feature is a container IFF
collectLeafDescendants (respecting includeDone) returns >=1 item. Add
classifyFeatureChild to resolve a feature child into either a
featureGroup (>=1 descendant, existing behavior) or the feature bean
itself as a leaf (0 descendants), applied consistently at all three
sites: buildEpicGroup, buildMilestoneGroup, and the orphan-items loop in
buildRoadmap. Childless leaves go through the same archive-status filter
as any other leaf, so a completed/archived childless feature is still
dropped under the default !includeDone view.

Refs: beans-ti53
Refs: beans-n8zw
…fyFeatureChild

Refs: beans-ti53
Refs: beans-xj5d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant