feat: Add tracks relation type for convoy tracking (bd-3roq)

Adds non-blocking tracks dependency type for convoy to issue relationships:
- Non-blocking: does not affect ready work calculation
- Cross-prefix capable: convoys in hq-* can track issues in gt-*, bd-*
- Reverse lookup: bd dep list <id> --direction=up -t tracks

Also adds bd dep list command with direction and type filtering for
querying dependencies/dependents.

🤖 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-29 21:04:28 -08:00
parent 2b90f51d0c
commit b8a5ee162b
5 changed files with 189 additions and 4 deletions

View File

@@ -695,6 +695,49 @@ func (m *MemoryStorage) GetDependents(ctx context.Context, issueID string) ([]*t
return results, nil
}
// GetDependenciesWithMetadata gets issues that this issue depends on, with dependency type
func (m *MemoryStorage) GetDependenciesWithMetadata(ctx context.Context, issueID string) ([]*types.IssueWithDependencyMetadata, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var results []*types.IssueWithDependencyMetadata
for _, dep := range m.dependencies[issueID] {
if issue, exists := m.issues[dep.DependsOnID]; exists {
issueCopy := *issue
results = append(results, &types.IssueWithDependencyMetadata{
Issue: issueCopy,
DependencyType: dep.Type,
})
}
}
return results, nil
}
// GetDependentsWithMetadata gets issues that depend on this issue, with dependency type
func (m *MemoryStorage) GetDependentsWithMetadata(ctx context.Context, issueID string) ([]*types.IssueWithDependencyMetadata, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var results []*types.IssueWithDependencyMetadata
for id, deps := range m.dependencies {
for _, dep := range deps {
if dep.DependsOnID == issueID {
if issue, exists := m.issues[id]; exists {
issueCopy := *issue
results = append(results, &types.IssueWithDependencyMetadata{
Issue: issueCopy,
DependencyType: dep.Type,
})
}
break
}
}
}
return results, nil
}
// GetDependencyCounts returns dependency and dependent counts for multiple issues
func (m *MemoryStorage) GetDependencyCounts(ctx context.Context, issueIDs []string) (map[string]*types.DependencyCounts, error) {
m.mu.RLock()

View File

@@ -92,6 +92,8 @@ type Storage interface {
RemoveDependency(ctx context.Context, issueID, dependsOnID string, actor string) error
GetDependencies(ctx context.Context, issueID string) ([]*types.Issue, error)
GetDependents(ctx context.Context, issueID string) ([]*types.Issue, error)
GetDependenciesWithMetadata(ctx context.Context, issueID string) ([]*types.IssueWithDependencyMetadata, error)
GetDependentsWithMetadata(ctx context.Context, issueID string) ([]*types.IssueWithDependencyMetadata, error)
GetDependencyRecords(ctx context.Context, issueID string) ([]*types.Dependency, error)
GetAllDependencyRecords(ctx context.Context) (map[string][]*types.Dependency, error)
GetDependencyCounts(ctx context.Context, issueIDs []string) (map[string]*types.DependencyCounts, error)

View File

@@ -519,6 +519,9 @@ const (
DepAuthoredBy DependencyType = "authored-by" // Creator relationship
DepAssignedTo DependencyType = "assigned-to" // Assignment relationship
DepApprovedBy DependencyType = "approved-by" // Approval relationship
// Convoy tracking (non-blocking cross-project references)
DepTracks DependencyType = "tracks" // Convoy → issue tracking (non-blocking)
)
// IsValid checks if the dependency type value is valid.
@@ -534,7 +537,7 @@ func (d DependencyType) IsWellKnown() bool {
switch d {
case DepBlocks, DepParentChild, DepConditionalBlocks, DepWaitsFor, DepRelated, DepDiscoveredFrom,
DepRepliesTo, DepRelatesTo, DepDuplicates, DepSupersedes,
DepAuthoredBy, DepAssignedTo, DepApprovedBy:
DepAuthoredBy, DepAssignedTo, DepApprovedBy, DepTracks:
return true
}
return false