The JSON output from bd show now includes the dependency_type field for both dependencies and dependents, enabling programmatic differentiation between dependency types (blocks, related, parent-child, discovered-from). Implementation approach: - Added IssueWithDependencyMetadata type with embedded Issue and DependencyType field - Extended GetDependenciesWithMetadata and GetDependentsWithMetadata to include dependency type from SQL JOIN - Made GetDependencies and GetDependents wrap the WithMetadata methods for backward compatibility - Added scanIssuesWithDependencyType helper to handle scanning with dependency type field - Updated bd show --json to use WithMetadata methods Tests added: - TestGetDependenciesWithMetadata - basic functionality - TestGetDependentsWithMetadata - dependent retrieval - TestGetDependenciesWithMetadataEmpty - edge case handling - TestGetDependenciesWithMetadataMultipleTypes - multiple types Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Happy <yesreply@happy.engineering>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
{"id":"bd-07af","content_hash":"227fcbcef36e3d6d31cdf78434fdb7198bf877dd6d3ca7a585239e3e20ee7461","title":"Need comprehensive daemon health check command (bd daemon doctor?)","description":"","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-10-31T21:08:09.092473-07:00","updated_at":"2025-11-01T20:10:41.957435-07:00","closed_at":"2025-11-01T20:10:41.957435-07:00","dependencies":[{"issue_id":"bd-07af","depends_on_id":"bd-2752a7a2","type":"discovered-from","created_at":"2025-10-31T21:08:09.093276-07:00","created_by":"stevey"}]}
|
||||
{"id":"bd-08e556f2","content_hash":"cd9e7cc106b733dc4893e92a75feae3331b422238f261a7c738c21a18e29719f","title":"Remove Cache Configuration Docs","description":"Remove documentation of deprecated cache env vars","acceptance_criteria":"- Documentation doesn't reference removed env vars\n- CHANGELOG documents breaking change\n- No mentions of storage cache except in CHANGELOG\n\nFiles to update:\n- ADVANCED.md (remove cache configuration section)\n- commands/daemons.md (remove cache env vars)\n- integrations/beads-mcp/SETUP_DAEMON.md (remove cache tuning)\n- CHANGELOG.md (add removal entry)\n\nDeprecated env vars:\n- BEADS_DAEMON_MAX_CACHE_SIZE\n- BEADS_DAEMON_CACHE_TTL\n- BEADS_DAEMON_MEMORY_THRESHOLD_MB","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-28T10:50:15.125488-07:00","updated_at":"2025-10-30T17:12:58.216329-07:00","closed_at":"2025-10-28T10:48:20.606979-07:00"}
|
||||
{"id":"bd-09b5f2f5","content_hash":"2cf0ab565f49aaa39cc7128cef964f99b37f14a948fbad3c617f9397df1e2541","title":"Daemon fails to auto-import after git pull updates JSONL","description":"After git pull updates .beads/issues.jsonl, daemon doesn't automatically re-import changes, causing stale data to be shown until next sync cycle (up to 5 minutes).\n\nReproduction:\n1. Repo A: Close issues, export, commit, push\n2. Repo B: git pull (successfully updates .beads/issues.jsonl)\n3. bd show \u003cissue\u003e shows OLD status from daemon's SQLite db\n4. JSONL on disk has correct new status\n\nRoot cause: Daemon sync cycle runs on timer (5min). When user manually runs git pull, daemon doesn't detect JSONL was updated externally and continues serving stale data from SQLite.\n\nImpact:\n- High for AI agents using beads in git workflows\n- Breaks fundamental git-as-source-of-truth model\n- Confusing UX: git log shows commit, bd shows old state\n- Data consistency issues between JSONL and daemon\n\nSee WYVERN_SYNC_ISSUE.md for full analysis.","design":"Three possible solutions:\n\nOption 1: Auto-detect and re-import (recommended)\n- Before serving any bd command, check if .beads/issues.jsonl mtime \u003e last import time\n- If newer, auto-import before processing request\n- Fast check, minimal overhead\n\nOption 2: File watcher in daemon\n- Daemon watches .beads/issues.jsonl for mtime changes\n- Auto-imports when file changes\n- More complex, requires file watching infrastructure\n\nOption 3: Explicit sync command\n- User runs `bd sync` after git pull\n- Manual, error-prone, defeats automation\n\nRecommended: Option 1 (auto-detect) + Option 3 (explicit sync) as fallback.","acceptance_criteria":"1. After git pull updates .beads/issues.jsonl, next bd command sees fresh data\n2. No manual import or daemon restart required\n3. Performance impact \u003c 10ms per command (mtime check is fast)\n4. Works in both daemon and non-daemon modes\n5. Test: Two repo clones, update in one, pull in other, verify immediate sync","notes":"**Fixed in v0.21.2!**\n\nThe daemon auto-import is fully implemented:\n- internal/autoimport package handles staleness detection\n- internal/importer package provides shared import logic (used by both CLI and daemon)\n- daemon's checkAndAutoImportIfStale() calls autoimport.AutoImportIfNewer()\n- importFunc uses importer.ImportIssues() with auto-rename enabled\n- All tests passing\n\nThe critical data corruption bug is FIXED:\n✅ After git pull, daemon detects JSONL is newer (mtime check)\n✅ Daemon auto-imports before serving requests\n✅ No stale data served\n✅ No data loss in multi-agent workflows\n\nVerification needed: Run two-repo test to confirm end-to-end behavior.","status":"closed","priority":0,"issue_type":"epic","created_at":"2025-10-25T23:13:12.270766-07:00","updated_at":"2025-11-01T16:52:50.931197-07:00","closed_at":"2025-11-01T16:52:50.931197-07:00"}
|
||||
{"id":"bd-0a90","content_hash":"d04ded2e8194db5c42c27641f68e52b553ff500e9101861e87e7956ec012dc74","title":"bd show --json doesn't include dependency type field","description":"Fix GitHub issue #202. The JSON output from bd show and bd list commands should include the dependency type field (and optionally created_at, created_by) to match internal storage format and enable better tooling integration.","notes":"PR #203 updated with cleaner implementation: https://github.com/steveyegge/beads/pull/203\n\n## Final Implementation\n\nCleanest possible approach - no internal helper methods needed:\n\n**Design:**\n- `GetDependenciesWithMetadata()` / `GetDependentsWithMetadata()` - canonical implementations with full SQL query\n- `GetDependencies()` / `GetDependents()` - thin wrappers that strip metadata for backward compat\n- `scanIssuesWithDependencyType()` - shared helper for scanning rows with dependency type\n\n**Benefits:**\n- Single source of truth - the `...WithMetadata()` methods ARE the implementation\n- Eliminated ~139 lines of duplicated SQL and scanning code\n- All tests passing (14 dependency-related tests)\n- Backward compatible\n- dependency_type field appears correctly in JSON output\n\n**Note on scan helpers:**\nThe duplication between `scanIssues()` and `scanIssuesWithDependencyType()` is necessary because they handle different SQL result shapes (16 vs 17 columns). This is justified as they serve fundamentally different purposes based on query structure.","status":"in_progress","priority":2,"issue_type":"bug","created_at":"2025-11-02T09:42:08.712725096Z","updated_at":"2025-11-02T11:08:42.796241364Z","external_ref":"https://github.com/steveyegge/beads/issues/202"}
|
||||
{"id":"bd-0dcea000","content_hash":"a6fc218b07d270e3498957525c39a869f7c850d687339b6d758a246be20c9591","title":"Add tests for internal/importer package","description":"Currently 0.0% coverage. Need tests for JSONL import logic including collision detection and resolution.","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-29T14:06:21.071024-07:00","updated_at":"2025-10-30T17:12:58.183211-07:00","dependencies":[{"issue_id":"bd-0dcea000","depends_on_id":"bd-cbed9619.5","type":"blocks","created_at":"2025-10-29T19:52:05.531279-07:00","created_by":"import-remap"},{"issue_id":"bd-0dcea000","depends_on_id":"bd-cbed9619.4","type":"blocks","created_at":"2025-10-29T19:52:05.53166-07:00","created_by":"import-remap"}]}
|
||||
{"id":"bd-0e1f2b1b","content_hash":"c0b1677fe3f4aa3f395ae4d79bff5362632d5db26477bf571c09f9177b8741ef","title":"Event-driven daemon architecture","description":"Replace 5-second polling sync loop with event-driven architecture that reacts instantly to changes. Eliminates stale data issues while reducing CPU ~60%. Key components: FileWatcher (fsnotify), Debouncer (500ms), RPC mutation events, optional git hooks. Target latency: \u003c500ms (vs 5000ms). See event_driven_daemon.md for full design.","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-10-28T16:20:02.430479-07:00","updated_at":"2025-10-30T17:12:58.221424-07:00","closed_at":"2025-10-28T16:30:26.631191-07:00"}
|
||||
{"id":"bd-11e0","content_hash":"591db3f5674cf7f79b1f50d6b8d83fe60e495f02a21e181c9c63d8da88308d58","title":"Database import silently fails when daemon version != CLI version","description":"","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-10-31T21:08:09.096749-07:00","updated_at":"2025-11-01T19:29:35.267817-07:00","closed_at":"2025-11-01T19:29:35.267817-07:00"}
|
||||
@@ -104,6 +105,7 @@
|
||||
{"id":"bd-833559b3","content_hash":"9082c986207b9df7a7a4dc87a53007849e2b9f6e92f3bea41e22d6a14f1f6f42","title":"bd validate - Comprehensive health check","description":"Run all validation checks in one command.\n\nChecks:\n- Duplicates\n- Orphaned dependencies\n- Test pollution\n- Git conflicts\n\nSupports --fix-all for auto-repair.\n\nDepends on bd-cbed9619.1, bd-0dcea000, bd-2752a7a2, bd-9826b69a.\n\nFiles: cmd/bd/validate.go (new)","status":"open","priority":1,"issue_type":"task","created_at":"2025-10-29T20:02:47.957692-07:00","updated_at":"2025-10-30T17:12:58.219095-07:00"}
|
||||
{"id":"bd-83f0bb64","content_hash":"c7be091ee7e713dd9c8ec0f9a498a9ae12adb09f8b7510a5ec10a815a05322e1","title":"Platform tests: Linux, macOS, Windows","description":"Test event-driven mode on all platforms. Verify inotify (Linux), FSEvents (macOS), ReadDirectoryChangesW (Windows). Test fallback behavior on each.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-29T19:42:29.857419-07:00","updated_at":"2025-10-31T12:00:43.197445-07:00","closed_at":"2025-10-31T12:00:43.197445-07:00"}
|
||||
{"id":"bd-85487065","content_hash":"637cbd56af122b175ff060b4df050871fe86124c5d883ba7f8a17f2f95479613","title":"Add tests for internal/autoimport package","description":"Currently 0.0% coverage. Need tests for auto-import functionality that detects and imports updated JSONL files.","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-29T14:06:18.154805-07:00","updated_at":"2025-10-30T17:12:58.182987-07:00"}
|
||||
{"id":"bd-879d","content_hash":"2f291ca2adead5ee3fb7fade39c088165b5467780599f9a719dcaebc87455ae3","title":"Test issue 1","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-02T09:44:12.538697729Z","updated_at":"2025-11-02T09:45:20.76214671Z","closed_at":"2025-11-02T09:45:20.76214671Z","dependencies":[{"issue_id":"bd-879d","depends_on_id":"bd-d3e5","type":"discovered-from","created_at":"2025-11-02T09:44:22.103468321Z","created_by":"mrdavidlaing"}]}
|
||||
{"id":"bd-8900f145","content_hash":"4a07f36a9e5d24aaffb092c89e2273cb58f9de357d24eeb01fcde6a4079ba775","title":"Testing event-driven mode!","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-29T15:28:33.564871-07:00","updated_at":"2025-10-30T17:12:58.186325-07:00","closed_at":"2025-10-29T19:12:54.43368-07:00"}
|
||||
{"id":"bd-89e2","content_hash":"ddf4626e586440f379ff19872eac29941cecb925d0a4aae8a6f9c08c969ca05d","title":"Daemon race condition: stale export overwrites recent DB changes","description":"**Symptom:**\nMerged bd-fc2d into bd-fb05 in ~/src/beads (commit ce4d756), pushed to remote. The ~/src/fred/beads daemon then exported its stale DB state and committed (8cc1bb4), reverting bd-fc2d back to \"open\" status.\n\n**Timeline:**\n1. 21:45:12 - Merge committed from ~/src/beads (ce4d756): bd-fc2d closed\n2. 21:49:42 - Daemon in ~/src/fred/beads exported stale state (8cc1bb4): bd-fc2d open again\n\n**Root cause:**\nThe fred/beads daemon had a stale database (bd-fc2d still open) and didn't auto-import the newer JSONL before exporting. When it exported, it overwrote the merge with its stale state.\n\n**Expected behavior:**\nDaemon should detect that JSONL is newer than its last export and import before exporting.\n\n**Actual behavior:**\nDaemon exported stale DB state, creating a conflicting commit that reverted upstream changes.\n\n**Impact:**\nMulti-workspace setups with daemons can silently lose changes if one daemon has stale state and exports.","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-01T21:53:07.930819-07:00","updated_at":"2025-11-01T22:01:25.54126-07:00","closed_at":"2025-11-01T22:01:25.54126-07:00"}
|
||||
{"id":"bd-89f89fc0","content_hash":"235c3bdeb45e3069167f81e7b4e798fc98547478bb16df40556100478c5e505a","title":"Remove unreachable RPC methods","description":"Several RPC server and client methods are unreachable and should be removed:\n\nServer methods (internal/rpc/server.go):\n- `Server.GetLastImportTime` (line 2116)\n- `Server.SetLastImportTime` (line 2123)\n- `Server.findJSONLPath` (line 2255)\n\nClient methods (internal/rpc/client.go):\n- `Client.Import` (line 311) - RPC import not used (daemon uses autoimport)\n\nEvidence:\n```bash\ngo run golang.org/x/tools/cmd/deadcode@latest -test ./...\n```\n\nImpact: Removes ~80 LOC of unused RPC code","acceptance_criteria":"- Remove the 4 unreachable methods (~80 LOC total)\n- Verify no callers: `grep -r \"GetLastImportTime\\|SetLastImportTime\\|findJSONLPath\" .`\n- All tests pass: `go test ./internal/rpc/...`\n- Daemon functionality works: test daemon start/stop/operations","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-28T16:20:02.432202-07:00","updated_at":"2025-10-30T17:12:58.222655-07:00"}
|
||||
@@ -156,6 +158,7 @@
|
||||
{"id":"bd-cf349eb3","content_hash":"1b42289a0cb1da0626a69c6f004bf62fc9ba6e3a0f8eb70159c5f1446497020b","title":"Update LINTING.md with current baseline","description":"After cleanup, document the remaining acceptable baseline in LINTING.md so we can track regression.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-27T23:20:10.39272-07:00","updated_at":"2025-10-30T17:12:58.215471-07:00","closed_at":"2025-10-27T23:05:31.945614-07:00"}
|
||||
{"id":"bd-d33c","content_hash":"0c3eb277be0ec16edae305156aa8824b6bc9c37fbd6151477f235e859e9b6181","title":"Separate process/lock/PID concerns into process.go","description":"Create internal/daemonrunner/process.go with: acquireDaemonLock, PID file read/write, stopDaemon, isDaemonRunning, getPIDFilePath, socket path helpers, version check.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-01T11:41:14.871122-07:00","updated_at":"2025-11-01T22:56:08.368079-07:00","closed_at":"2025-11-01T22:56:08.368079-07:00"}
|
||||
{"id":"bd-d355a07d","content_hash":"b4f98403e209eadf33dd4913660c1538fd922c89339a9ed034ef504aac358662","title":"Import validation falsely reports data loss on collision resolution","description":"## Problem\n\nPost-import validation reports 'data loss detected!' when import count reduces due to legitimate collision resolution.\n\n## Example\n\n```\nImport complete: 1 created, 8 updated, 142 unchanged, 19 skipped, 1 issues remapped\nPost-import validation failed: import reduced issue count: 165 → 164 (data loss detected!)\n```\n\nThis was actually successful collision resolution (bd-70419816 duplicated → remapped to-70419816), not data loss.\n\n## Impact\n\n- False alarms waste investigation time\n- Undermines confidence in import validation\n- Confuses users/agents about sync health\n\n## Solution\n\nImprove validation to distinguish:\n- Collision-resolution merges (expected count reduction)\n- Actual data loss (unexpected disappearance)\n\nTrack remapped issue count and adjust expected post-import count accordingly.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-10-29T23:15:00.815227-07:00","updated_at":"2025-10-31T19:38:09.19996-07:00"}
|
||||
{"id":"bd-d3e5","content_hash":"16f978c58b9988457aeb1eaff37fb17f12e91325549b38be10362a08923e9a2d","title":"Test issue 2","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-02T09:44:17.116768539Z","updated_at":"2025-11-02T09:45:20.780838695Z","closed_at":"2025-11-02T09:45:20.780838695Z"}
|
||||
{"id":"bd-d4ec5a82","content_hash":"872448809bfa26d39d68ba6cac5071379756c30bcd3b08dc75de6da56c133956","title":"Add MCP functions for repair commands","description":"Add repair commands to beads-mcp for agent access:\n- beads_resolve_conflicts()\n- beads_find_duplicates()\n- beads_detect_pollution()\n- beads_validate()\n\nFiles: integrations/beads-mcp/src/beads_mcp/server.py","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-28T14:48:29.071495-07:00","updated_at":"2025-10-30T17:12:58.219499-07:00"}
|
||||
{"id":"bd-d68f","content_hash":"4b5b5340749fba1c419c22f9937717b363ee8a49e4c5e0a5e0066a24b652a936","title":"Add tests for Comments API (AddIssueComment, GetIssueComments)","description":"Comments API currently has 0% coverage. Need tests for:\n- AddIssueComment - adding comments to issues\n- GetIssueComments - retrieving comments\n- Comment ordering and pagination\n- Edge cases (non-existent issues, empty comments)","status":"closed","priority":3,"issue_type":"task","created_at":"2025-11-01T22:40:58.980688-07:00","updated_at":"2025-11-01T22:53:42.124391-07:00","closed_at":"2025-11-01T22:53:42.124391-07:00"}
|
||||
{"id":"bd-d7e88238","content_hash":"b69ec861618b03129fad7807b085ee6365860cfd2e9901b49eb846e192b95a0d","title":"Rapid 3","description":"","status":"open","priority":3,"issue_type":"task","created_at":"2025-10-29T19:11:57.459655-07:00","updated_at":"2025-10-30T17:12:58.189494-07:00"}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
@@ -197,18 +198,33 @@ var showCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
// Include labels, dependencies, and comments in JSON output
|
||||
// Include labels, dependencies (with metadata), dependents (with metadata), and comments in JSON output
|
||||
type IssueDetails struct {
|
||||
*types.Issue
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
Dependencies []*types.Issue `json:"dependencies,omitempty"`
|
||||
Dependents []*types.Issue `json:"dependents,omitempty"`
|
||||
Comments []*types.Comment `json:"comments,omitempty"`
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
Dependencies []*types.IssueWithDependencyMetadata `json:"dependencies,omitempty"`
|
||||
Dependents []*types.IssueWithDependencyMetadata `json:"dependents,omitempty"`
|
||||
Comments []*types.Comment `json:"comments,omitempty"`
|
||||
}
|
||||
details := &IssueDetails{Issue: issue}
|
||||
details.Labels, _ = store.GetLabels(ctx, issue.ID)
|
||||
details.Dependencies, _ = store.GetDependencies(ctx, issue.ID)
|
||||
details.Dependents, _ = store.GetDependents(ctx, issue.ID)
|
||||
|
||||
// Get dependencies with metadata (type, created_at, created_by)
|
||||
if sqliteStore, ok := store.(*sqlite.SQLiteStorage); ok {
|
||||
details.Dependencies, _ = sqliteStore.GetDependenciesWithMetadata(ctx, issue.ID)
|
||||
details.Dependents, _ = sqliteStore.GetDependentsWithMetadata(ctx, issue.ID)
|
||||
} else {
|
||||
// Fallback to regular methods without metadata for other storage backends
|
||||
deps, _ := store.GetDependencies(ctx, issue.ID)
|
||||
for _, dep := range deps {
|
||||
details.Dependencies = append(details.Dependencies, &types.IssueWithDependencyMetadata{Issue: *dep})
|
||||
}
|
||||
dependents, _ := store.GetDependents(ctx, issue.ID)
|
||||
for _, dependent := range dependents {
|
||||
details.Dependents = append(details.Dependents, &types.IssueWithDependencyMetadata{Issue: *dependent})
|
||||
}
|
||||
}
|
||||
|
||||
details.Comments, _ = store.GetIssueComments(ctx, issue.ID)
|
||||
allDetails = append(allDetails, details)
|
||||
continue
|
||||
|
||||
@@ -199,42 +199,74 @@ func (s *SQLiteStorage) RemoveDependency(ctx context.Context, issueID, dependsOn
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// GetDependencies returns issues that this issue depends on
|
||||
func (s *SQLiteStorage) GetDependencies(ctx context.Context, issueID string) ([]*types.Issue, error) {
|
||||
// GetDependenciesWithMetadata returns issues that this issue depends on, including dependency type
|
||||
func (s *SQLiteStorage) GetDependenciesWithMetadata(ctx context.Context, issueID string) ([]*types.IssueWithDependencyMetadata, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT i.id, i.content_hash, i.title, i.description, i.design, i.acceptance_criteria, i.notes,
|
||||
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
|
||||
i.created_at, i.updated_at, i.closed_at, i.external_ref
|
||||
i.created_at, i.updated_at, i.closed_at, i.external_ref,
|
||||
d.type
|
||||
FROM issues i
|
||||
JOIN dependencies d ON i.id = d.depends_on_id
|
||||
WHERE d.issue_id = ?
|
||||
ORDER BY i.priority ASC
|
||||
`, issueID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get dependencies: %w", err)
|
||||
return nil, fmt.Errorf("failed to get dependencies with metadata: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
return s.scanIssues(ctx, rows)
|
||||
return s.scanIssuesWithDependencyType(ctx, rows)
|
||||
}
|
||||
|
||||
// GetDependents returns issues that depend on this issue
|
||||
func (s *SQLiteStorage) GetDependents(ctx context.Context, issueID string) ([]*types.Issue, error) {
|
||||
// GetDependentsWithMetadata returns issues that depend on this issue, including dependency type
|
||||
func (s *SQLiteStorage) GetDependentsWithMetadata(ctx context.Context, issueID string) ([]*types.IssueWithDependencyMetadata, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT i.id, i.content_hash, i.title, i.description, i.design, i.acceptance_criteria, i.notes,
|
||||
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
|
||||
i.created_at, i.updated_at, i.closed_at, i.external_ref
|
||||
i.created_at, i.updated_at, i.closed_at, i.external_ref,
|
||||
d.type
|
||||
FROM issues i
|
||||
JOIN dependencies d ON i.id = d.issue_id
|
||||
WHERE d.depends_on_id = ?
|
||||
ORDER BY i.priority ASC
|
||||
`, issueID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get dependents: %w", err)
|
||||
return nil, fmt.Errorf("failed to get dependents with metadata: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
return s.scanIssues(ctx, rows)
|
||||
return s.scanIssuesWithDependencyType(ctx, rows)
|
||||
}
|
||||
|
||||
// GetDependencies returns issues that this issue depends on
|
||||
func (s *SQLiteStorage) GetDependencies(ctx context.Context, issueID string) ([]*types.Issue, error) {
|
||||
issuesWithMeta, err := s.GetDependenciesWithMetadata(ctx, issueID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert to plain Issue slice for backward compatibility
|
||||
issues := make([]*types.Issue, len(issuesWithMeta))
|
||||
for i, iwm := range issuesWithMeta {
|
||||
issues[i] = &iwm.Issue
|
||||
}
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
// GetDependents returns issues that depend on this issue
|
||||
func (s *SQLiteStorage) GetDependents(ctx context.Context, issueID string) ([]*types.Issue, error) {
|
||||
issuesWithMeta, err := s.GetDependentsWithMetadata(ctx, issueID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert to plain Issue slice for backward compatibility
|
||||
issues := make([]*types.Issue, len(issuesWithMeta))
|
||||
for i, iwm := range issuesWithMeta {
|
||||
issues[i] = &iwm.Issue
|
||||
}
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
// GetDependencyCounts returns dependency and dependent counts for multiple issues in a single query
|
||||
@@ -673,3 +705,60 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
|
||||
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
// Helper function to scan issues with dependency type from rows
|
||||
func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *sql.Rows) ([]*types.IssueWithDependencyMetadata, error) {
|
||||
var results []*types.IssueWithDependencyMetadata
|
||||
for rows.Next() {
|
||||
var issue types.Issue
|
||||
var contentHash sql.NullString
|
||||
var closedAt sql.NullTime
|
||||
var estimatedMinutes sql.NullInt64
|
||||
var assignee sql.NullString
|
||||
var externalRef sql.NullString
|
||||
var depType types.DependencyType
|
||||
|
||||
err := rows.Scan(
|
||||
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
||||
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
||||
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef,
|
||||
&depType,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan issue with dependency type: %w", err)
|
||||
}
|
||||
|
||||
if contentHash.Valid {
|
||||
issue.ContentHash = contentHash.String
|
||||
}
|
||||
if closedAt.Valid {
|
||||
issue.ClosedAt = &closedAt.Time
|
||||
}
|
||||
if estimatedMinutes.Valid {
|
||||
mins := int(estimatedMinutes.Int64)
|
||||
issue.EstimatedMinutes = &mins
|
||||
}
|
||||
if assignee.Valid {
|
||||
issue.Assignee = assignee.String
|
||||
}
|
||||
if externalRef.Valid {
|
||||
issue.ExternalRef = &externalRef.String
|
||||
}
|
||||
|
||||
// Fetch labels for this issue
|
||||
labels, err := s.GetLabels(ctx, issue.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get labels for issue %s: %w", issue.ID, err)
|
||||
}
|
||||
issue.Labels = labels
|
||||
|
||||
result := &types.IssueWithDependencyMetadata{
|
||||
Issue: issue,
|
||||
DependencyType: depType,
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -1008,3 +1008,210 @@ func TestGetDependencyCountsNonexistent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDependenciesWithMetadata(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create issues
|
||||
issue1 := &types.Issue{Title: "Foundation", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
|
||||
issue2 := &types.Issue{Title: "Feature A", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
|
||||
issue3 := &types.Issue{Title: "Feature B", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
|
||||
|
||||
store.CreateIssue(ctx, issue1, "test-user")
|
||||
store.CreateIssue(ctx, issue2, "test-user")
|
||||
store.CreateIssue(ctx, issue3, "test-user")
|
||||
|
||||
// Add dependencies with different types
|
||||
// issue2 depends on issue1 (blocks)
|
||||
store.AddDependency(ctx, &types.Dependency{
|
||||
IssueID: issue2.ID,
|
||||
DependsOnID: issue1.ID,
|
||||
Type: types.DepBlocks,
|
||||
}, "test-user")
|
||||
|
||||
// issue3 depends on issue1 (discovered-from)
|
||||
store.AddDependency(ctx, &types.Dependency{
|
||||
IssueID: issue3.ID,
|
||||
DependsOnID: issue1.ID,
|
||||
Type: types.DepDiscoveredFrom,
|
||||
}, "test-user")
|
||||
|
||||
// Get dependencies with metadata for issue2
|
||||
deps, err := store.GetDependenciesWithMetadata(ctx, issue2.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
|
||||
}
|
||||
|
||||
if len(deps) != 1 {
|
||||
t.Fatalf("Expected 1 dependency, got %d", len(deps))
|
||||
}
|
||||
|
||||
// Verify the dependency includes type metadata
|
||||
dep := deps[0]
|
||||
if dep.ID != issue1.ID {
|
||||
t.Errorf("Expected dependency on %s, got %s", issue1.ID, dep.ID)
|
||||
}
|
||||
if dep.DependencyType != types.DepBlocks {
|
||||
t.Errorf("Expected dependency type 'blocks', got %s", dep.DependencyType)
|
||||
}
|
||||
if dep.Title != "Foundation" {
|
||||
t.Errorf("Expected title 'Foundation', got %s", dep.Title)
|
||||
}
|
||||
|
||||
// Get dependencies with metadata for issue3
|
||||
deps3, err := store.GetDependenciesWithMetadata(ctx, issue3.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
|
||||
}
|
||||
|
||||
if len(deps3) != 1 {
|
||||
t.Fatalf("Expected 1 dependency, got %d", len(deps3))
|
||||
}
|
||||
|
||||
// Verify the dependency type is discovered-from
|
||||
if deps3[0].DependencyType != types.DepDiscoveredFrom {
|
||||
t.Errorf("Expected dependency type 'discovered-from', got %s", deps3[0].DependencyType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDependentsWithMetadata(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create issues: issue2 and issue3 both depend on issue1
|
||||
issue1 := &types.Issue{Title: "Foundation", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
|
||||
issue2 := &types.Issue{Title: "Feature A", Status: types.StatusOpen, Priority: 2, IssueType: types.TypeTask}
|
||||
issue3 := &types.Issue{Title: "Feature B", Status: types.StatusOpen, Priority: 3, IssueType: types.TypeTask}
|
||||
|
||||
store.CreateIssue(ctx, issue1, "test-user")
|
||||
store.CreateIssue(ctx, issue2, "test-user")
|
||||
store.CreateIssue(ctx, issue3, "test-user")
|
||||
|
||||
// Add dependencies with different types
|
||||
store.AddDependency(ctx, &types.Dependency{
|
||||
IssueID: issue2.ID,
|
||||
DependsOnID: issue1.ID,
|
||||
Type: types.DepBlocks,
|
||||
}, "test-user")
|
||||
|
||||
store.AddDependency(ctx, &types.Dependency{
|
||||
IssueID: issue3.ID,
|
||||
DependsOnID: issue1.ID,
|
||||
Type: types.DepRelated,
|
||||
}, "test-user")
|
||||
|
||||
// Get dependents of issue1 with metadata
|
||||
dependents, err := store.GetDependentsWithMetadata(ctx, issue1.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDependentsWithMetadata failed: %v", err)
|
||||
}
|
||||
|
||||
if len(dependents) != 2 {
|
||||
t.Fatalf("Expected 2 dependents, got %d", len(dependents))
|
||||
}
|
||||
|
||||
// Verify dependents are ordered by priority (issue2=P1 before issue3=P2)
|
||||
if dependents[0].ID != issue2.ID {
|
||||
t.Errorf("Expected first dependent to be %s, got %s", issue2.ID, dependents[0].ID)
|
||||
}
|
||||
if dependents[0].DependencyType != types.DepBlocks {
|
||||
t.Errorf("Expected first dependent type 'blocks', got %s", dependents[0].DependencyType)
|
||||
}
|
||||
|
||||
if dependents[1].ID != issue3.ID {
|
||||
t.Errorf("Expected second dependent to be %s, got %s", issue3.ID, dependents[1].ID)
|
||||
}
|
||||
if dependents[1].DependencyType != types.DepRelated {
|
||||
t.Errorf("Expected second dependent type 'related', got %s", dependents[1].DependencyType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDependenciesWithMetadataEmpty(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create issue with no dependencies
|
||||
issue := &types.Issue{Title: "Standalone", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
|
||||
store.CreateIssue(ctx, issue, "test-user")
|
||||
|
||||
// Get dependencies with metadata
|
||||
deps, err := store.GetDependenciesWithMetadata(ctx, issue.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
|
||||
}
|
||||
|
||||
if len(deps) != 0 {
|
||||
t.Errorf("Expected 0 dependencies, got %d", len(deps))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDependenciesWithMetadataMultipleTypes(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create issues
|
||||
base := &types.Issue{Title: "Base", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
|
||||
blocks := &types.Issue{Title: "Blocker", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
|
||||
related := &types.Issue{Title: "Related", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
|
||||
discovered := &types.Issue{Title: "Discovered", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
|
||||
|
||||
store.CreateIssue(ctx, base, "test-user")
|
||||
store.CreateIssue(ctx, blocks, "test-user")
|
||||
store.CreateIssue(ctx, related, "test-user")
|
||||
store.CreateIssue(ctx, discovered, "test-user")
|
||||
|
||||
// Add dependencies of different types
|
||||
store.AddDependency(ctx, &types.Dependency{
|
||||
IssueID: base.ID,
|
||||
DependsOnID: blocks.ID,
|
||||
Type: types.DepBlocks,
|
||||
}, "test-user")
|
||||
|
||||
store.AddDependency(ctx, &types.Dependency{
|
||||
IssueID: base.ID,
|
||||
DependsOnID: related.ID,
|
||||
Type: types.DepRelated,
|
||||
}, "test-user")
|
||||
|
||||
store.AddDependency(ctx, &types.Dependency{
|
||||
IssueID: base.ID,
|
||||
DependsOnID: discovered.ID,
|
||||
Type: types.DepDiscoveredFrom,
|
||||
}, "test-user")
|
||||
|
||||
// Get all dependencies with metadata
|
||||
deps, err := store.GetDependenciesWithMetadata(ctx, base.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
|
||||
}
|
||||
|
||||
if len(deps) != 3 {
|
||||
t.Fatalf("Expected 3 dependencies, got %d", len(deps))
|
||||
}
|
||||
|
||||
// Create a map of dependency types
|
||||
typeMap := make(map[string]types.DependencyType)
|
||||
for _, dep := range deps {
|
||||
typeMap[dep.ID] = dep.DependencyType
|
||||
}
|
||||
|
||||
// Verify all types are correctly returned
|
||||
if typeMap[blocks.ID] != types.DepBlocks {
|
||||
t.Errorf("Expected blocks dependency type 'blocks', got %s", typeMap[blocks.ID])
|
||||
}
|
||||
if typeMap[related.ID] != types.DepRelated {
|
||||
t.Errorf("Expected related dependency type 'related', got %s", typeMap[related.ID])
|
||||
}
|
||||
if typeMap[discovered.ID] != types.DepDiscoveredFrom {
|
||||
t.Errorf("Expected discovered dependency type 'discovered-from', got %s", typeMap[discovered.ID])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,13 @@ type DependencyCounts struct {
|
||||
DependentCount int `json:"dependent_count"` // Number of issues that depend on this issue
|
||||
}
|
||||
|
||||
// IssueWithDependencyMetadata extends Issue with dependency relationship type
|
||||
// Note: We explicitly include all Issue fields to ensure proper JSON marshaling
|
||||
type IssueWithDependencyMetadata struct {
|
||||
Issue
|
||||
DependencyType DependencyType `json:"dependency_type"`
|
||||
}
|
||||
|
||||
// IssueWithCounts extends Issue with dependency relationship counts
|
||||
type IssueWithCounts struct {
|
||||
*Issue
|
||||
|
||||
Reference in New Issue
Block a user