fix: mol run loads all hierarchical children + supports title lookup (bd-c8d5, bd-drcx)

Two issues fixed:

1. bd-c8d5: mol run only created partial children from proto
   - Root cause: children with missing/wrong dependencies were not loaded
   - Fix: loadDescendants now uses two strategies:
     - Strategy 1: Check dependency records for parent-child relationships
     - Strategy 2: Find hierarchical children by ID pattern (parent.N)
   - This catches children that may have broken dependency data

2. bd-drcx: mol run now supports proto lookup by title
   - Can use: bd mol run mol-polecat-work --var issue=gt-xxx
   - Or by ID: bd mol run gt-lwuu --var issue=gt-xxx
   - Title matching is case-insensitive and supports partial matches
   - Shows helpful error on ambiguous matches

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-24 23:33:14 -08:00
parent 81171ff237
commit c429405e0d
3 changed files with 495 additions and 8 deletions

View File

@@ -409,8 +409,14 @@ func loadTemplateSubgraph(ctx context.Context, s storage.Storage, templateID str
}
// loadDescendants recursively loads all child issues
// It uses two strategies to find children:
// 1. Check dependency records for parent-child relationships
// 2. Check for hierarchical IDs (parent.N) to catch children with missing/wrong deps (bd-c8d5)
func loadDescendants(ctx context.Context, s storage.Storage, subgraph *TemplateSubgraph, parentID string) error {
// GetDependents returns issues that depend on parentID
// Track children we've already added to avoid duplicates
addedChildren := make(map[string]bool)
// Strategy 1: GetDependents returns issues that depend on parentID
dependents, err := s.GetDependents(ctx, parentID)
if err != nil {
return fmt.Errorf("failed to get dependents of %s: %w", parentID, err)
@@ -443,6 +449,7 @@ func loadDescendants(ctx context.Context, s storage.Storage, subgraph *TemplateS
// Add to subgraph
subgraph.Issues = append(subgraph.Issues, dependent)
subgraph.IssueMap[dependent.ID] = dependent
addedChildren[dependent.ID] = true
// Recurse to get children of this child
if err := loadDescendants(ctx, s, subgraph, dependent.ID); err != nil {
@@ -450,9 +457,132 @@ func loadDescendants(ctx context.Context, s storage.Storage, subgraph *TemplateS
}
}
// Strategy 2: Find hierarchical children by ID pattern (bd-c8d5)
// This catches children that have missing or incorrect dependency types.
// Hierarchical IDs follow the pattern: parentID.N (e.g., "gt-abc.1", "gt-abc.2")
hierarchicalChildren, err := findHierarchicalChildren(ctx, s, parentID)
if err != nil {
// Non-fatal: continue with what we have
return nil
}
for _, child := range hierarchicalChildren {
if addedChildren[child.ID] {
continue // Already added via dependency
}
if _, exists := subgraph.IssueMap[child.ID]; exists {
continue // Already in subgraph
}
// Add to subgraph
subgraph.Issues = append(subgraph.Issues, child)
subgraph.IssueMap[child.ID] = child
addedChildren[child.ID] = true
// Recurse to get children of this child
if err := loadDescendants(ctx, s, subgraph, child.ID); err != nil {
return err
}
}
return nil
}
// findHierarchicalChildren finds issues with IDs that match the pattern parentID.N
// This catches hierarchical children that may be missing parent-child dependencies.
func findHierarchicalChildren(ctx context.Context, s storage.Storage, parentID string) ([]*types.Issue, error) {
// Look for issues with IDs starting with "parentID."
// We need to query by ID pattern, which requires listing issues
pattern := parentID + "."
// Use the storage's search capability with a filter
allIssues, err := s.SearchIssues(ctx, "", types.IssueFilter{})
if err != nil {
return nil, err
}
var children []*types.Issue
for _, issue := range allIssues {
// Check if ID starts with pattern and is a direct child (no further dots after the pattern)
if len(issue.ID) > len(pattern) && issue.ID[:len(pattern)] == pattern {
// Check it's a direct child, not a grandchild
// e.g., "parent.1" is a child, "parent.1.2" is a grandchild
remaining := issue.ID[len(pattern):]
if !strings.Contains(remaining, ".") {
children = append(children, issue)
}
}
}
return children, nil
}
// =============================================================================
// Proto Lookup Functions (bd-drcx)
// =============================================================================
// resolveProtoIDOrTitle resolves a proto by ID or title.
// It first tries to resolve as an ID (via ResolvePartialID).
// If that fails, it searches for protos with matching titles.
// Returns the proto ID if found, or an error if not found or ambiguous.
func resolveProtoIDOrTitle(ctx context.Context, s storage.Storage, input string) (string, error) {
// Strategy 1: Try to resolve as an ID
protoID, err := utils.ResolvePartialID(ctx, s, input)
if err == nil {
// Verify it's a proto (has template label)
issue, getErr := s.GetIssue(ctx, protoID)
if getErr == nil && issue != nil {
labels, _ := s.GetLabels(ctx, protoID)
for _, label := range labels {
if label == BeadsTemplateLabel {
return protoID, nil // Found a valid proto by ID
}
}
}
// ID resolved but not a proto - continue to title search
}
// Strategy 2: Search for protos by title
protos, err := s.GetIssuesByLabel(ctx, BeadsTemplateLabel)
if err != nil {
return "", fmt.Errorf("failed to search protos: %w", err)
}
var matches []*types.Issue
var exactMatch *types.Issue
for _, proto := range protos {
// Check for exact title match (case-insensitive)
if strings.EqualFold(proto.Title, input) {
exactMatch = proto
break
}
// Check for partial title match (case-insensitive)
if strings.Contains(strings.ToLower(proto.Title), strings.ToLower(input)) {
matches = append(matches, proto)
}
}
if exactMatch != nil {
return exactMatch.ID, nil
}
if len(matches) == 0 {
return "", fmt.Errorf("no proto found matching %q (by ID or title)", input)
}
if len(matches) == 1 {
return matches[0].ID, nil
}
// Multiple matches - show them all for disambiguation
var matchNames []string
for _, m := range matches {
matchNames = append(matchNames, fmt.Sprintf("%s: %s", m.ID, m.Title))
}
return "", fmt.Errorf("ambiguous: %q matches %d protos:\n %s\nUse the ID or a more specific title", input, len(matches), strings.Join(matchNames, "\n "))
}
// =============================================================================
// Daemon-compatible Template Functions
// =============================================================================