fix: Address code review issues in bd mol bond formula support

Fixes from review of gt-8tmz.25 implementation:

1. Dry-run no longer cooks formulas - added resolveOrDescribe() for
   dry-run mode that checks if operand exists without cooking

2. Ephemeral protos now cleaned up after successful bond, not just
   on error

3. Unique proto IDs to avoid collision - ephemeral protos use format
   "_ephemeral-<formula>-<timestamp>" instead of formula name

4. Removed unused vars parameter from resolveOrCookFormula

5. Added informative output showing formulas will be cooked and
   cleaned up

🤖 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-25 16:46:53 -08:00
parent 6bb11c4297
commit 3b3f844484

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"strings"
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/formula"
@@ -134,44 +135,52 @@ func runMolBond(cmd *cobra.Command, args []string) {
vars[parts[0]] = parts[1]
}
// Resolve both operands - can be issue IDs or formula names
// Formula names are cooked inline to ephemeral protos (gt-8tmz.25)
issueA, cookedA, err := resolveOrCookFormula(ctx, store, args[0], vars, actor)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
issueB, cookedB, err := resolveOrCookFormula(ctx, store, args[1], vars, actor)
if err != nil {
// Clean up first cooked formula if second one fails
if cookedA {
_ = deleteProtoSubgraph(ctx, store, issueA.ID)
}
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// Track if we cooked formulas for cleanup on error
cleanupCooked := func() {
if cookedA {
_ = deleteProtoSubgraph(ctx, store, issueA.ID)
}
if cookedB {
_ = deleteProtoSubgraph(ctx, store, issueB.ID)
}
}
idA := issueA.ID
idB := issueB.ID
// Determine operand types
aIsProto := isProto(issueA)
bIsProto := isProto(issueB)
// For dry-run, just check if operands can be resolved (don't cook)
if dryRun {
issueA, formulaA, err := resolveOrDescribe(ctx, store, args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
issueB, formulaB, err := resolveOrDescribe(ctx, store, args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
idA := args[0]
idB := args[1]
aIsProto := false
bIsProto := false
if issueA != nil {
idA = issueA.ID
aIsProto = isProto(issueA)
}
if issueB != nil {
idB = issueB.ID
bIsProto = isProto(issueB)
}
// Formulas are treated as protos for dry-run display
if formulaA != "" {
aIsProto = true
}
if formulaB != "" {
bIsProto = true
}
fmt.Printf("\nDry run: bond %s + %s\n", idA, idB)
fmt.Printf(" A: %s (%s)\n", issueA.Title, operandType(aIsProto))
fmt.Printf(" B: %s (%s)\n", issueB.Title, operandType(bIsProto))
if formulaA != "" {
fmt.Printf(" A: %s (formula → will cook as proto)\n", formulaA)
} else if issueA != nil {
fmt.Printf(" A: %s (%s)\n", issueA.Title, operandType(aIsProto))
}
if formulaB != "" {
fmt.Printf(" B: %s (formula → will cook as proto)\n", formulaB)
} else if issueB != nil {
fmt.Printf(" B: %s (%s)\n", issueB.Title, operandType(bIsProto))
}
fmt.Printf(" Bond type: %s\n", bondType)
if wisp {
fmt.Printf(" Phase override: vapor (--wisp)\n")
@@ -187,35 +196,51 @@ func runMolBond(cmd *cobra.Command, args []string) {
if customTitle != "" {
fmt.Printf(" Custom title: %s\n", customTitle)
}
if wisp || pour {
fmt.Printf(" Note: phase flags ignored for proto+proto (templates stay in permanent storage)\n")
}
if childRef != "" {
fmt.Printf(" Note: --ref ignored for proto+proto (use when bonding to molecule)\n")
}
} else if aIsProto || bIsProto {
fmt.Printf(" Result: spawn proto, attach to molecule\n")
if !wisp && !pour {
fmt.Printf(" Phase: follows target's phase\n")
}
if childRef != "" {
// Determine parent molecule
parentID := idB
if bIsProto {
parentID = idA
}
resolvedRef := substituteVariables(childRef, vars)
fmt.Printf(" Bonded ID: %s.%s\n", parentID, resolvedRef)
}
} else {
fmt.Printf(" Result: compound molecule\n")
if childRef != "" {
fmt.Printf(" Note: --ref ignored for mol+mol (use when bonding proto to molecule)\n")
}
}
if formulaA != "" || formulaB != "" {
fmt.Printf("\n Note: Cooked formulas are ephemeral and deleted after bonding.\n")
}
return
}
// Resolve both operands - can be issue IDs or formula names
// Formula names are cooked inline to ephemeral protos (gt-8tmz.25)
issueA, cookedA, err := resolveOrCookFormula(ctx, store, args[0], actor)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
issueB, cookedB, err := resolveOrCookFormula(ctx, store, args[1], actor)
if err != nil {
// Clean up first cooked formula if second one fails
if cookedA {
_ = deleteProtoSubgraph(ctx, store, issueA.ID)
}
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// Track cooked formulas for cleanup (ephemeral protos deleted after use)
cleanupCooked := func() {
if cookedA {
_ = deleteProtoSubgraph(ctx, store, issueA.ID)
}
if cookedB {
_ = deleteProtoSubgraph(ctx, store, issueB.ID)
}
}
idA := issueA.ID
idB := issueB.ID
// Determine operand types
aIsProto := isProto(issueA)
bIsProto := isProto(issueB)
// Dispatch based on operand types
// All operations use the main store; wisp flag determines ephemeral vs persistent
var result *BondResult
@@ -240,6 +265,10 @@ func runMolBond(cmd *cobra.Command, args []string) {
// Schedule auto-flush - wisps are in main DB now, but JSONL export skips them
markDirtyAndScheduleFlush()
// Clean up ephemeral protos after successful bond
// These were only needed to get the proto structure; the spawned issues persist
cleanupCooked()
if jsonOutput {
outputJSON(result)
return
@@ -255,6 +284,9 @@ func runMolBond(cmd *cobra.Command, args []string) {
} else if pour {
fmt.Printf(" Phase: liquid (persistent, Wisp=false)\n")
}
if cookedA || cookedB {
fmt.Printf(" Ephemeral protos cleaned up after use.\n")
}
}
// isProto checks if an issue is a proto (has the template label)
@@ -504,12 +536,40 @@ func minPriority(a, b int) int {
return b
}
// resolveOrDescribe checks if an operand is an issue or formula without cooking.
// Used for dry-run mode. Returns (issue, formulaName, error).
// If it's an issue, issue is set. If it's a formula, formulaName is set.
func resolveOrDescribe(ctx context.Context, s storage.Storage, operand string) (*types.Issue, string, error) {
// First, try to resolve as an existing issue
id, err := utils.ResolvePartialID(ctx, s, operand)
if err == nil {
issue, err := s.GetIssue(ctx, id)
if err == nil {
return issue, "", nil
}
}
// Not found as issue - check if it looks like a formula name
if !looksLikeFormulaName(operand) {
return nil, "", fmt.Errorf("'%s' not found (not an issue ID or formula name)", operand)
}
// Try to load the formula (but don't cook it)
parser := formula.NewParser()
f, err := parser.LoadByName(operand)
if err != nil {
return nil, "", fmt.Errorf("'%s' not found as issue or formula: %w", operand, err)
}
return nil, f.Formula, nil
}
// resolveOrCookFormula tries to resolve an operand as an issue ID.
// If not found and it looks like a formula name, cooks the formula inline.
// Returns the issue, whether it was cooked (ephemeral proto), and any error.
//
// This implements gt-8tmz.25: formula names are cooked inline as ephemeral protos.
func resolveOrCookFormula(ctx context.Context, s storage.Storage, operand string, vars map[string]string, actorName string) (*types.Issue, bool, error) {
func resolveOrCookFormula(ctx context.Context, s storage.Storage, operand string, actorName string) (*types.Issue, bool, error) {
// First, try to resolve as an existing issue
id, err := utils.ResolvePartialID(ctx, s, operand)
if err == nil {
@@ -575,8 +635,9 @@ func resolveOrCookFormula(ctx context.Context, s storage.Storage, operand string
}
// Cook the formula to create an ephemeral proto
// Use formula name as proto ID for clarity
protoID := resolved.Formula
// Use a unique ID to avoid collision with existing protos
// Format: _ephemeral-<formula>-<timestamp> (underscore prefix marks it as ephemeral)
protoID := fmt.Sprintf("_ephemeral-%s-%d", resolved.Formula, time.Now().UnixNano())
result, err := cookFormula(ctx, s, resolved, protoID)
if err != nil {
return nil, false, fmt.Errorf("cooking formula '%s': %w", operand, err)