diff --git a/cmd/bd/mol_show.go b/cmd/bd/mol_show.go index 676f50bf..5585121c 100644 --- a/cmd/bd/mol_show.go +++ b/cmd/bd/mol_show.go @@ -70,14 +70,27 @@ func showMolecule(subgraph *MoleculeSubgraph) { "issues": subgraph.Issues, "dependencies": subgraph.Dependencies, "variables": extractAllVariables(subgraph), + "is_compound": subgraph.Root.IsCompound(), + "bonded_from": subgraph.Root.BondedFrom, }) return } - fmt.Printf("\n%s Molecule: %s\n", ui.RenderAccent("๐Ÿงช"), subgraph.Root.Title) + // Determine molecule type label + moleculeType := "Molecule" + if subgraph.Root.IsCompound() { + moleculeType = "Compound" + } + + fmt.Printf("\n%s %s: %s\n", ui.RenderAccent("๐Ÿงช"), moleculeType, subgraph.Root.Title) fmt.Printf(" ID: %s\n", subgraph.Root.ID) fmt.Printf(" Steps: %d\n", len(subgraph.Issues)) + // Show compound bonding info if this is a compound molecule + if subgraph.Root.IsCompound() { + showCompoundBondingInfo(subgraph.Root) + } + vars := extractAllVariables(subgraph) if len(vars) > 0 { fmt.Printf("\n%s Variables:\n", ui.RenderWarn("๐Ÿ“")) @@ -91,6 +104,52 @@ func showMolecule(subgraph *MoleculeSubgraph) { fmt.Println() } +// showCompoundBondingInfo displays the bonding lineage for compound molecules +func showCompoundBondingInfo(root *types.Issue) { + if !root.IsCompound() { + return + } + + constituents := root.GetConstituents() + fmt.Printf("\n%s Bonded from:\n", ui.RenderAccent("๐Ÿ”—")) + + for i, ref := range constituents { + connector := "โ”œโ”€โ”€" + if i == len(constituents)-1 { + connector = "โ””โ”€โ”€" + } + + // Format bond type for display + bondTypeDisplay := formatBondType(ref.BondType) + + // Show source ID with bond type + if ref.BondPoint != "" { + fmt.Printf(" %s %s (%s, at %s)\n", connector, ref.SourceID, bondTypeDisplay, ref.BondPoint) + } else { + fmt.Printf(" %s %s (%s)\n", connector, ref.SourceID, bondTypeDisplay) + } + } +} + +// formatBondType returns a human-readable bond type description +func formatBondType(bondType string) string { + switch bondType { + case types.BondTypeSequential: + return "sequential" + case types.BondTypeParallel: + return "parallel" + case types.BondTypeConditional: + return "on-failure" + case types.BondTypeRoot: + return "root" + default: + if bondType == "" { + return "default" + } + return bondType + } +} + // ParallelInfo holds parallel analysis information for a step type ParallelInfo struct { StepID string `json:"step_id"` @@ -327,14 +386,27 @@ func showMoleculeWithParallel(subgraph *MoleculeSubgraph) { "dependencies": subgraph.Dependencies, "variables": extractAllVariables(subgraph), "parallel": analysis, + "is_compound": subgraph.Root.IsCompound(), + "bonded_from": subgraph.Root.BondedFrom, }) return } - fmt.Printf("\n%s Molecule: %s\n", ui.RenderAccent("๐Ÿงช"), subgraph.Root.Title) + // Determine molecule type label + moleculeType := "Molecule" + if subgraph.Root.IsCompound() { + moleculeType = "Compound" + } + + fmt.Printf("\n%s %s: %s\n", ui.RenderAccent("๐Ÿงช"), moleculeType, subgraph.Root.Title) fmt.Printf(" ID: %s\n", subgraph.Root.ID) fmt.Printf(" Steps: %d (%d ready)\n", analysis.TotalSteps, analysis.ReadySteps) + // Show compound bonding info if this is a compound molecule + if subgraph.Root.IsCompound() { + showCompoundBondingInfo(subgraph.Root) + } + // Show parallel groups summary if len(analysis.ParallelGroups) > 0 { fmt.Printf("\n%s Parallel Groups:\n", ui.RenderPass("โšก")) diff --git a/cmd/bd/mol_test.go b/cmd/bd/mol_test.go index 90572939..b284ec7f 100644 --- a/cmd/bd/mol_test.go +++ b/cmd/bd/mol_test.go @@ -2527,3 +2527,103 @@ func TestSpawnMoleculeFromFormulaEphemeral(t *testing.T) { } } } + +// TestCompoundMoleculeVisualization tests the compound molecule display in mol show +func TestCompoundMoleculeVisualization(t *testing.T) { + // Test IsCompound() and GetConstituents() + tests := []struct { + name string + bondedFrom []types.BondRef + isCompound bool + expectedCount int + }{ + { + name: "not a compound - no BondedFrom", + bondedFrom: nil, + isCompound: false, + expectedCount: 0, + }, + { + name: "not a compound - empty BondedFrom", + bondedFrom: []types.BondRef{}, + isCompound: false, + expectedCount: 0, + }, + { + name: "compound with one constituent", + bondedFrom: []types.BondRef{ + {SourceID: "proto-a", BondType: types.BondTypeSequential}, + }, + isCompound: true, + expectedCount: 1, + }, + { + name: "compound with two constituents - sequential bond", + bondedFrom: []types.BondRef{ + {SourceID: "proto-a", BondType: types.BondTypeSequential}, + {SourceID: "proto-b", BondType: types.BondTypeSequential}, + }, + isCompound: true, + expectedCount: 2, + }, + { + name: "compound with parallel bond", + bondedFrom: []types.BondRef{ + {SourceID: "proto-a", BondType: types.BondTypeParallel}, + {SourceID: "proto-b", BondType: types.BondTypeParallel}, + }, + isCompound: true, + expectedCount: 2, + }, + { + name: "compound with bond point", + bondedFrom: []types.BondRef{ + {SourceID: "proto-a", BondType: types.BondTypeSequential, BondPoint: "step-2"}, + }, + isCompound: true, + expectedCount: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + issue := &types.Issue{ + ID: "test-compound", + Title: "Test Compound Molecule", + BondedFrom: tt.bondedFrom, + } + + if got := issue.IsCompound(); got != tt.isCompound { + t.Errorf("IsCompound() = %v, want %v", got, tt.isCompound) + } + + constituents := issue.GetConstituents() + if len(constituents) != tt.expectedCount { + t.Errorf("GetConstituents() returned %d items, want %d", len(constituents), tt.expectedCount) + } + }) + } +} + +// TestFormatBondType tests the formatBondType helper function +func TestFormatBondType(t *testing.T) { + tests := []struct { + bondType string + expected string + }{ + {types.BondTypeSequential, "sequential"}, + {types.BondTypeParallel, "parallel"}, + {types.BondTypeConditional, "on-failure"}, + {types.BondTypeRoot, "root"}, + {"", "default"}, + {"custom-type", "custom-type"}, + } + + for _, tt := range tests { + t.Run(tt.bondType, func(t *testing.T) { + if got := formatBondType(tt.bondType); got != tt.expected { + t.Errorf("formatBondType(%q) = %q, want %q", tt.bondType, got, tt.expected) + } + }) + } +}