From bb0a0ea703a30a9c46ed7194fe7418902208b9bc Mon Sep 17 00:00:00 2001 From: beads/crew/grip Date: Fri, 2 Jan 2026 13:43:07 -0800 Subject: [PATCH] feat(mol): add compound visualization in bd mol show (bd-iw4z) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add visualization for compound molecules (those created by bonding protos): - Detect compound molecules via IsCompound() check on root issue - Display "Bonded from:" section showing constituent protos with bond types - Show bond point when specified (attachment site within molecule) - Format bond types as human-readable: sequential, parallel, on-failure, root - Include is_compound and bonded_from in JSON output Example output for compound molecules: ๐Ÿงช Compound: proto-feature-with-tests ID: bd-abc123 Steps: 5 ๐Ÿ”— Bonded from: โ”œโ”€โ”€ proto-feature (sequential) โ””โ”€โ”€ proto-testing (sequential, at step-2) Added unit tests for compound detection and bond type formatting. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/mol_show.go | 76 +++++++++++++++++++++++++++++++++- cmd/bd/mol_test.go | 100 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 2 deletions(-) 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) + } + }) + } +}