wip: external dep resolution helper (bd-zmmy)
This commit is contained in:
129
internal/storage/sqlite/external_deps.go
Normal file
129
internal/storage/sqlite/external_deps.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// Package sqlite provides external dependency resolution for cross-project blocking.
|
||||
//
|
||||
// External dependencies use the format: external:<project>:<capability>
|
||||
// They are satisfied when:
|
||||
// - The project is configured in external_projects config
|
||||
// - The project's beads database has a closed issue with provides:<capability> label
|
||||
//
|
||||
// Resolution happens lazily at query time (GetReadyWork) rather than during
|
||||
// cache rebuild, to keep cache rebuilds fast and avoid holding multiple DB connections.
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/steveyegge/beads/internal/config"
|
||||
"github.com/steveyegge/beads/internal/configfile"
|
||||
)
|
||||
|
||||
// ExternalDepStatus represents whether an external dependency is satisfied
|
||||
type ExternalDepStatus struct {
|
||||
Ref string // The full external reference (external:project:capability)
|
||||
Project string // Parsed project name
|
||||
Capability string // Parsed capability name
|
||||
Satisfied bool // Whether the dependency is satisfied
|
||||
Reason string // Human-readable reason if not satisfied
|
||||
}
|
||||
|
||||
// CheckExternalDep checks if a single external dependency is satisfied.
|
||||
// Returns status information about the dependency.
|
||||
func CheckExternalDep(ctx context.Context, ref string) *ExternalDepStatus {
|
||||
status := &ExternalDepStatus{
|
||||
Ref: ref,
|
||||
Satisfied: false,
|
||||
}
|
||||
|
||||
// Parse external:project:capability
|
||||
if !strings.HasPrefix(ref, "external:") {
|
||||
status.Reason = "not an external reference"
|
||||
return status
|
||||
}
|
||||
|
||||
parts := strings.SplitN(ref, ":", 3)
|
||||
if len(parts) != 3 {
|
||||
status.Reason = "invalid format (expected external:project:capability)"
|
||||
return status
|
||||
}
|
||||
|
||||
status.Project = parts[1]
|
||||
status.Capability = parts[2]
|
||||
|
||||
if status.Project == "" || status.Capability == "" {
|
||||
status.Reason = "missing project or capability"
|
||||
return status
|
||||
}
|
||||
|
||||
// Look up project path from config
|
||||
projectPath := config.ResolveExternalProjectPath(status.Project)
|
||||
if projectPath == "" {
|
||||
status.Reason = "project not configured in external_projects"
|
||||
return status
|
||||
}
|
||||
|
||||
// Find the beads database in the project
|
||||
beadsDir := filepath.Join(projectPath, ".beads")
|
||||
cfg, err := configfile.Load(beadsDir)
|
||||
if err != nil || cfg == nil {
|
||||
status.Reason = "project has no beads database"
|
||||
return status
|
||||
}
|
||||
|
||||
dbPath := cfg.DatabasePath(beadsDir)
|
||||
|
||||
// Open the external database (read-only)
|
||||
db, err := sql.Open("sqlite3", dbPath+"?mode=ro")
|
||||
if err != nil {
|
||||
status.Reason = "cannot open project database"
|
||||
return status
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
// Check for a closed issue with provides:<capability> label
|
||||
providesLabel := "provides:" + status.Capability
|
||||
var count int
|
||||
err = db.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*) FROM issues i
|
||||
JOIN labels l ON i.id = l.issue_id
|
||||
WHERE i.status = 'closed'
|
||||
AND l.label = ?
|
||||
`, providesLabel).Scan(&count)
|
||||
|
||||
if err != nil {
|
||||
status.Reason = "database query failed"
|
||||
return status
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
status.Reason = "capability not shipped (no closed issue with provides:" + status.Capability + " label)"
|
||||
return status
|
||||
}
|
||||
|
||||
status.Satisfied = true
|
||||
status.Reason = "capability shipped"
|
||||
return status
|
||||
}
|
||||
|
||||
// CheckExternalDeps checks multiple external dependencies.
|
||||
// Returns a map of ref -> status.
|
||||
func CheckExternalDeps(ctx context.Context, refs []string) map[string]*ExternalDepStatus {
|
||||
results := make(map[string]*ExternalDepStatus)
|
||||
for _, ref := range refs {
|
||||
results[ref] = CheckExternalDep(ctx, ref)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// GetUnsatisfiedExternalDeps returns external dependencies that are not satisfied.
|
||||
func GetUnsatisfiedExternalDeps(ctx context.Context, refs []string) []string {
|
||||
var unsatisfied []string
|
||||
for _, ref := range refs {
|
||||
status := CheckExternalDep(ctx, ref)
|
||||
if !status.Satisfied {
|
||||
unsatisfied = append(unsatisfied, ref)
|
||||
}
|
||||
}
|
||||
return unsatisfied
|
||||
}
|
||||
Reference in New Issue
Block a user