diff --git a/cmd/bd/comments.go b/cmd/bd/comments.go index 594b1cf563..2426b176ae 100644 --- a/cmd/bd/comments.go +++ b/cmd/bd/comments.go @@ -44,7 +44,13 @@ Examples: } issueID = fullID - comments, err := store.GetIssueComments(ctx, issueID) + commentType, _ := cmd.Flags().GetString("type") + var comments []*types.Comment + if commentType != "" { + comments, err = store.GetIssueCommentsByType(ctx, issueID, commentType) + } else { + comments, err = store.GetIssueComments(ctx, issueID) + } if err != nil { FatalErrorRespectJSON("getting comments: %v", err) } @@ -74,8 +80,16 @@ Examples: fmt.Printf("[%s] at %s\n", comment.Author, ts.Format("2006-01-02 15:04")) rendered := ui.RenderMarkdown(comment.Text) // TrimRight removes trailing newlines that Glamour adds, preventing extra blank lines - for _, line := range strings.Split(strings.TrimRight(rendered, "\n"), "\n") { - fmt.Printf(" %s\n", line) + lines := strings.Split(strings.TrimRight(rendered, "\n"), "\n") + if comment.Type != "" { + fmt.Printf(" [%s] %s\n", comment.Type, lines[0]) + for _, line := range lines[1:] { + fmt.Printf(" %s\n", line) + } + } else { + for _, line := range lines { + fmt.Printf(" %s\n", line) + } } fmt.Println() } @@ -134,7 +148,8 @@ Examples: } issueID = fullID - comment, err := store.AddIssueComment(ctx, issueID, author, commentText) + commentType, _ := cmd.Flags().GetString("type") + comment, err := store.AddIssueComment(ctx, issueID, author, commentText, commentType) if err != nil { FatalErrorRespectJSON("adding comment: %v", err) } @@ -151,8 +166,10 @@ Examples: func init() { commentsCmd.AddCommand(commentsAddCmd) commentsCmd.Flags().Bool("local-time", false, "Show timestamps in local time instead of UTC") + commentsCmd.Flags().String("type", "", "Filter by comment type (decision, handoff, note)") commentsAddCmd.Flags().StringP("file", "f", "", "Read comment text from file") commentsAddCmd.Flags().StringP("author", "a", "", "Add author to comment") + commentsAddCmd.Flags().String("type", "", "Comment type (decision, handoff, note)") // Issue ID completions commentsCmd.ValidArgsFunction = issueIDCompletion diff --git a/cmd/bd/comments_test.go b/cmd/bd/comments_test.go index e542849c97..a560166298 100644 --- a/cmd/bd/comments_test.go +++ b/cmd/bd/comments_test.go @@ -7,6 +7,7 @@ import ( "fmt" "path/filepath" "testing" + "time" "github.com/steveyegge/beads/internal/types" ) @@ -35,7 +36,7 @@ func TestCommentsSuite(t *testing.T) { } t.Run("add comment", func(t *testing.T) { - comment, err := s.AddIssueComment(ctx, issue.ID, testUserAlice, "This is a test comment") + comment, err := s.AddIssueComment(ctx, issue.ID, testUserAlice, "This is a test comment", "") if err != nil { t.Fatalf("Failed to add comment: %v", err) } @@ -67,7 +68,7 @@ func TestCommentsSuite(t *testing.T) { }) t.Run("multiple comments", func(t *testing.T) { - _, err := s.AddIssueComment(ctx, issue.ID, "bob", "Second comment") + _, err := s.AddIssueComment(ctx, issue.ID, "bob", "Second comment", "") if err != nil { t.Fatalf("Failed to add second comment: %v", err) } @@ -108,7 +109,7 @@ func TestCommentsSuite(t *testing.T) { } t.Run("comment added via storage API works", func(t *testing.T) { - comment, err := s.AddIssueComment(ctx, issue.ID, testUserAlice, "Test comment") + comment, err := s.AddIssueComment(ctx, issue.ID, testUserAlice, "Test comment", "") if err != nil { t.Fatalf("Failed to add comment: %v", err) } @@ -129,6 +130,78 @@ func TestCommentsSuite(t *testing.T) { }) } +func TestTypedComments(t *testing.T) { + t.Parallel() + ctx := context.Background() + tmpDir := t.TempDir() + testDB := filepath.Join(tmpDir, ".beads", "beads.db") + s := newTestStore(t, testDB) + + issue := &types.Issue{ + Title: "Typed Comment Test", + Status: types.StatusOpen, + Priority: 2, + IssueType: types.TypeTask, + CreatedAt: time.Now(), + } + if err := s.CreateIssue(ctx, issue, "test"); err != nil { + t.Fatalf("Failed to create issue: %v", err) + } + + t.Run("AddTypedComment", func(t *testing.T) { + comment, err := s.AddIssueComment(ctx, issue.ID, "alice", "Use JWT over sessions", types.CommentTypeDecision) + if err != nil { + t.Fatalf("AddIssueComment failed: %v", err) + } + if comment.Type != types.CommentTypeDecision { + t.Errorf("comment.Type = %q, want %q", comment.Type, types.CommentTypeDecision) + } + }) + + t.Run("AddUntypedComment", func(t *testing.T) { + comment, err := s.AddIssueComment(ctx, issue.ID, "bob", "General update", "") + if err != nil { + t.Fatalf("AddIssueComment failed: %v", err) + } + if comment.Type != "" { + t.Errorf("comment.Type = %q, want empty", comment.Type) + } + }) + + t.Run("GetCommentsByType", func(t *testing.T) { + comments, err := s.GetIssueCommentsByType(ctx, issue.ID, types.CommentTypeDecision) + if err != nil { + t.Fatalf("GetIssueCommentsByType failed: %v", err) + } + if len(comments) != 1 { + t.Errorf("got %d comments, want 1", len(comments)) + } + if len(comments) > 0 && comments[0].Type != types.CommentTypeDecision { + t.Errorf("comment.Type = %q, want %q", comments[0].Type, types.CommentTypeDecision) + } + }) + + t.Run("GetAllCommentsReturnsType", func(t *testing.T) { + comments, err := s.GetIssueComments(ctx, issue.ID) + if err != nil { + t.Fatalf("GetIssueComments failed: %v", err) + } + if len(comments) < 2 { + t.Fatalf("got %d comments, want >= 2", len(comments)) + } + found := false + for _, c := range comments { + if c.Type == types.CommentTypeDecision { + found = true + break + } + } + if !found { + t.Error("decision comment type not found in GetIssueComments results") + } + }) +} + func TestIsUnknownOperationError(t *testing.T) { t.Parallel() tests := []struct { diff --git a/cmd/bd/human.go b/cmd/bd/human.go index 27615a5a56..77a3cd2489 100644 --- a/cmd/bd/human.go +++ b/cmd/bd/human.go @@ -229,7 +229,7 @@ Examples: // Add comment using AddIssueComment (issueID, author, text) commentText := fmt.Sprintf("Response: %s", response) - _, err = targetStore.AddIssueComment(ctx, resolvedID, actor, commentText) + _, err = targetStore.AddIssueComment(ctx, resolvedID, actor, commentText, "") if err != nil { FatalErrorRespectJSON("adding comment: %v", err) } diff --git a/examples/library-usage/main.go b/examples/library-usage/main.go index 3ca2c36aa0..88c3e6a858 100644 --- a/examples/library-usage/main.go +++ b/examples/library-usage/main.go @@ -90,7 +90,7 @@ func main() { // Example 5: Add a comment fmt.Println("\n=== Adding Comment ===") - comment, err := store.AddIssueComment(ctx, newIssue.ID, "library-example", "This is a programmatic comment") + comment, err := store.AddIssueComment(ctx, newIssue.ID, "library-example", "This is a programmatic comment", "") if err != nil { log.Fatalf("Failed to add comment: %v", err) } diff --git a/internal/beads/beads_integration_test.go b/internal/beads/beads_integration_test.go index 0e5f9c75ab..34325285f1 100644 --- a/internal/beads/beads_integration_test.go +++ b/internal/beads/beads_integration_test.go @@ -94,7 +94,7 @@ func (h *integrationTestHelper) addLabel(id, label string) { } func (h *integrationTestHelper) addComment(id, user, text string) *types.Comment { - comment, err := h.store.AddIssueComment(h.ctx, id, user, text) + comment, err := h.store.AddIssueComment(h.ctx, id, user, text, "") if err != nil { h.t.Fatalf("AddIssueComment failed: %v", err) } diff --git a/internal/jira/tracker_test.go b/internal/jira/tracker_test.go index ac4dce825b..5849309633 100644 --- a/internal/jira/tracker_test.go +++ b/internal/jira/tracker_test.go @@ -581,12 +581,15 @@ func (s *configStore) GetBlockedIssues(_ context.Context, _ types.WorkFilter) ([ func (s *configStore) GetEpicsEligibleForClosure(_ context.Context) ([]*types.EpicStatus, error) { return nil, nil } -func (s *configStore) AddIssueComment(_ context.Context, _, _, _ string) (*types.Comment, error) { +func (s *configStore) AddIssueComment(_ context.Context, _, _, _, _ string) (*types.Comment, error) { return nil, nil } func (s *configStore) GetIssueComments(_ context.Context, _ string) ([]*types.Comment, error) { return nil, nil } +func (s *configStore) GetIssueCommentsByType(_ context.Context, _, _ string) ([]*types.Comment, error) { + return nil, nil +} func (s *configStore) GetEvents(_ context.Context, _ string, _ int) ([]*types.Event, error) { return nil, nil } diff --git a/internal/storage/annotation_queries.go b/internal/storage/annotation_queries.go index 04231e56cb..076b17ce46 100644 --- a/internal/storage/annotation_queries.go +++ b/internal/storage/annotation_queries.go @@ -10,7 +10,7 @@ import ( // AnnotationStore provides comment and label operations, including bulk queries. type AnnotationStore interface { AddComment(ctx context.Context, issueID, actor, comment string) error - ImportIssueComment(ctx context.Context, issueID, author, text string, createdAt time.Time) (*types.Comment, error) + ImportIssueComment(ctx context.Context, issueID, author, text, commentType string, createdAt time.Time) (*types.Comment, error) GetCommentCounts(ctx context.Context, issueIDs []string) (map[string]int, error) GetCommentsForIssues(ctx context.Context, issueIDs []string) (map[string][]*types.Comment, error) GetLabelsForIssues(ctx context.Context, issueIDs []string) (map[string][]string, error) diff --git a/internal/storage/dolt/dolt_test.go b/internal/storage/dolt/dolt_test.go index 6f5c015093..daf7f01d8d 100644 --- a/internal/storage/dolt/dolt_test.go +++ b/internal/storage/dolt/dolt_test.go @@ -977,7 +977,7 @@ func TestDoltStoreComments(t *testing.T) { } // Add comments - comment1, err := store.AddIssueComment(ctx, issue.ID, "user1", "First comment") + comment1, err := store.AddIssueComment(ctx, issue.ID, "user1", "First comment", "") if err != nil { t.Fatalf("failed to add first comment: %v", err) } @@ -985,7 +985,7 @@ func TestDoltStoreComments(t *testing.T) { t.Error("expected comment ID to be generated") } - _, err = store.AddIssueComment(ctx, issue.ID, "user2", "Second comment") + _, err = store.AddIssueComment(ctx, issue.ID, "user2", "Second comment", "") if err != nil { t.Fatalf("failed to add second comment: %v", err) } diff --git a/internal/storage/dolt/events.go b/internal/storage/dolt/events.go index 308d2e176a..4067d3ad43 100644 --- a/internal/storage/dolt/events.go +++ b/internal/storage/dolt/events.go @@ -80,13 +80,13 @@ func (s *DoltStore) GetAllEventsSince(ctx context.Context, sinceID int64) ([]*ty } // AddIssueComment adds a comment to an issue (structured comment) -func (s *DoltStore) AddIssueComment(ctx context.Context, issueID, author, text string) (*types.Comment, error) { - return s.ImportIssueComment(ctx, issueID, author, text, time.Now().UTC()) +func (s *DoltStore) AddIssueComment(ctx context.Context, issueID, author, text, commentType string) (*types.Comment, error) { + return s.ImportIssueComment(ctx, issueID, author, text, commentType, time.Now().UTC()) } // ImportIssueComment adds a comment during import, preserving the original timestamp. // This prevents comment timestamp drift across import/export cycles. -func (s *DoltStore) ImportIssueComment(ctx context.Context, issueID, author, text string, createdAt time.Time) (*types.Comment, error) { +func (s *DoltStore) ImportIssueComment(ctx context.Context, issueID, author, text, commentType string, createdAt time.Time) (*types.Comment, error) { // Verify issue exists — route to wisps table for active wisps issueTable := "issues" commentTable := "comments" @@ -110,9 +110,9 @@ func (s *DoltStore) ImportIssueComment(ctx context.Context, issueID, author, tex createdAt = createdAt.UTC() //nolint:gosec // G201: table is hardcoded result, err := s.execContext(ctx, fmt.Sprintf(` - INSERT INTO %s (issue_id, author, text, created_at) - VALUES (?, ?, ?, ?) - `, commentTable), issueID, author, text, createdAt) + INSERT INTO %s (issue_id, author, text, type, created_at) + VALUES (?, ?, ?, ?, ?) + `, commentTable), issueID, author, text, commentType, createdAt) if err != nil { return nil, fmt.Errorf("failed to add comment: %w", err) } @@ -127,6 +127,7 @@ func (s *DoltStore) ImportIssueComment(ctx context.Context, issueID, author, tex IssueID: issueID, Author: author, Text: text, + Type: commentType, CreatedAt: createdAt, }, nil } @@ -140,7 +141,7 @@ func (s *DoltStore) GetIssueComments(ctx context.Context, issueID string) ([]*ty //nolint:gosec // G201: table is hardcoded rows, err := s.queryContext(ctx, fmt.Sprintf(` - SELECT id, issue_id, author, text, created_at + SELECT id, issue_id, author, text, type, created_at FROM %s WHERE issue_id = ? ORDER BY created_at ASC @@ -153,6 +154,28 @@ func (s *DoltStore) GetIssueComments(ctx context.Context, issueID string) ([]*ty return scanComments(rows) } +// GetIssueCommentsByType retrieves comments for an issue filtered by type. +func (s *DoltStore) GetIssueCommentsByType(ctx context.Context, issueID, commentType string) ([]*types.Comment, error) { + table := "comments" + if s.isActiveWisp(ctx, issueID) { + table = "wisp_comments" + } + + //nolint:gosec // G201: table is hardcoded + rows, err := s.queryContext(ctx, fmt.Sprintf(` + SELECT id, issue_id, author, text, type, created_at + FROM %s + WHERE issue_id = ? AND type = ? + ORDER BY created_at ASC + `, table), issueID, commentType) + if err != nil { + return nil, fmt.Errorf("failed to get comments by type: %w", err) + } + defer rows.Close() + + return scanComments(rows) +} + // GetCommentsForIssues retrieves comments for multiple issues func (s *DoltStore) GetCommentsForIssues(ctx context.Context, issueIDs []string) (map[string][]*types.Comment, error) { if len(issueIDs) == 0 { @@ -192,7 +215,7 @@ func (s *DoltStore) getCommentsForIDsInto(ctx context.Context, table string, ids //nolint:gosec // G201: table is hardcoded, placeholders contains only ? markers query := fmt.Sprintf(` - SELECT id, issue_id, author, text, created_at + SELECT id, issue_id, author, text, type, created_at FROM %s WHERE issue_id IN (%s) ORDER BY issue_id, created_at ASC @@ -205,7 +228,7 @@ func (s *DoltStore) getCommentsForIDsInto(ctx context.Context, table string, ids for rows.Next() { var c types.Comment - if err := rows.Scan(&c.ID, &c.IssueID, &c.Author, &c.Text, &c.CreatedAt); err != nil { + if err := rows.Scan(&c.ID, &c.IssueID, &c.Author, &c.Text, &c.Type, &c.CreatedAt); err != nil { _ = rows.Close() return fmt.Errorf("failed to scan comment: %w", err) } @@ -317,7 +340,7 @@ func scanComments(rows *sql.Rows) ([]*types.Comment, error) { var comments []*types.Comment for rows.Next() { var c types.Comment - if err := rows.Scan(&c.ID, &c.IssueID, &c.Author, &c.Text, &c.CreatedAt); err != nil { + if err := rows.Scan(&c.ID, &c.IssueID, &c.Author, &c.Text, &c.Type, &c.CreatedAt); err != nil { return nil, fmt.Errorf("failed to scan comment: %w", err) } comments = append(comments, &c) diff --git a/internal/storage/dolt/migrations.go b/internal/storage/dolt/migrations.go index dbe4d9b00a..9065cf1182 100644 --- a/internal/storage/dolt/migrations.go +++ b/internal/storage/dolt/migrations.go @@ -28,6 +28,7 @@ var migrationsList = []Migration{ {"infra_to_wisps", migrations.MigrateInfraToWisps}, {"wisp_dep_type_index", migrations.MigrateWispDepTypeIndex}, {"cleanup_autopush_metadata", migrations.MigrateCleanupAutopushMetadata}, + {"comment_type_column", migrations.MigrateCommentTypeColumn}, } // RunMigrations executes all registered Dolt migrations in order. diff --git a/internal/storage/dolt/migrations/010_comment_type_column.go b/internal/storage/dolt/migrations/010_comment_type_column.go new file mode 100644 index 0000000000..a7e5bbc5ae --- /dev/null +++ b/internal/storage/dolt/migrations/010_comment_type_column.go @@ -0,0 +1,38 @@ +package migrations + +import "database/sql" + +// MigrateCommentTypeColumn adds a `type` column to comments and wisp_comments +// tables for categorizing comments (decision, handoff, note). +// Existing comments get empty string (untyped) — backwards compatible. +func MigrateCommentTypeColumn(db *sql.DB) error { + for _, table := range []string{"comments", "wisp_comments"} { + exists, err := columnExists(db, table, "type") + if err != nil { + // Table may not exist (e.g., wisp_comments on fresh installs + // before wisp migration runs). Skip gracefully. + if isTableNotFoundError(err) { + continue + } + return err + } + if exists { + continue + } + + //nolint:gosec // G202: table name comes from hardcoded list above + if _, err := db.Exec("ALTER TABLE `" + table + "` ADD COLUMN `type` VARCHAR(32) NOT NULL DEFAULT ''"); err != nil { + return err + } + + // Composite index for filtering comments by type within an issue + idxName := "idx_" + table + "_issue_type" + if !indexExists(db, table, idxName) { + //nolint:gosec // G202: table/index names from internal constants + if _, err := db.Exec("CREATE INDEX `" + idxName + "` ON `" + table + "` (issue_id, type)"); err != nil { + return err + } + } + } + return nil +} diff --git a/internal/storage/dolt/transaction.go b/internal/storage/dolt/transaction.go index b6bc25c019..d23d251d4b 100644 --- a/internal/storage/dolt/transaction.go +++ b/internal/storage/dolt/transaction.go @@ -768,7 +768,7 @@ func (t *doltTransaction) GetMetadata(ctx context.Context, key string) (string, return value, wrapQueryError("get metadata in tx", err) } -func (t *doltTransaction) ImportIssueComment(ctx context.Context, issueID, author, text string, createdAt time.Time) (*types.Comment, error) { +func (t *doltTransaction) ImportIssueComment(ctx context.Context, issueID, author, text, commentType string, createdAt time.Time) (*types.Comment, error) { _, err := t.GetIssue(ctx, issueID) if err != nil { return nil, err @@ -782,9 +782,9 @@ func (t *doltTransaction) ImportIssueComment(ctx context.Context, issueID, autho createdAt = createdAt.UTC() //nolint:gosec // G201: table is hardcoded res, err := t.tx.ExecContext(ctx, fmt.Sprintf(` - INSERT INTO %s (issue_id, author, text, created_at) - VALUES (?, ?, ?, ?) - `, table), issueID, author, text, createdAt) + INSERT INTO %s (issue_id, author, text, type, created_at) + VALUES (?, ?, ?, ?, ?) + `, table), issueID, author, text, commentType, createdAt) if err != nil { return nil, fmt.Errorf("failed to add comment: %w", err) } @@ -793,7 +793,7 @@ func (t *doltTransaction) ImportIssueComment(ctx context.Context, issueID, autho return nil, fmt.Errorf("failed to get comment id: %w", err) } - return &types.Comment{ID: id, IssueID: issueID, Author: author, Text: text, CreatedAt: createdAt}, nil + return &types.Comment{ID: id, IssueID: issueID, Author: author, Text: text, Type: commentType, CreatedAt: createdAt}, nil } func (t *doltTransaction) GetIssueComments(ctx context.Context, issueID string) ([]*types.Comment, error) { @@ -804,7 +804,7 @@ func (t *doltTransaction) GetIssueComments(ctx context.Context, issueID string) //nolint:gosec // G201: table is hardcoded rows, err := t.tx.QueryContext(ctx, fmt.Sprintf(` - SELECT id, issue_id, author, text, created_at + SELECT id, issue_id, author, text, type, created_at FROM %s WHERE issue_id = ? ORDER BY created_at ASC @@ -816,7 +816,7 @@ func (t *doltTransaction) GetIssueComments(ctx context.Context, issueID string) var comments []*types.Comment for rows.Next() { var c types.Comment - if err := rows.Scan(&c.ID, &c.IssueID, &c.Author, &c.Text, &c.CreatedAt); err != nil { + if err := rows.Scan(&c.ID, &c.IssueID, &c.Author, &c.Text, &c.Type, &c.CreatedAt); err != nil { return nil, wrapScanError("get comments in tx", err) } comments = append(comments, &c) diff --git a/internal/storage/embeddeddolt/store.go b/internal/storage/embeddeddolt/store.go index fdc7de02b3..01fb08a652 100644 --- a/internal/storage/embeddeddolt/store.go +++ b/internal/storage/embeddeddolt/store.go @@ -226,7 +226,7 @@ func (s *EmbeddedDoltStore) GetEpicsEligibleForClosure(ctx context.Context) ([]* panic("embeddeddolt: GetEpicsEligibleForClosure not implemented") } -func (s *EmbeddedDoltStore) AddIssueComment(ctx context.Context, issueID, author, text string) (*types.Comment, error) { +func (s *EmbeddedDoltStore) AddIssueComment(ctx context.Context, issueID, author, text, commentType string) (*types.Comment, error) { panic("embeddeddolt: AddIssueComment not implemented") } @@ -234,6 +234,10 @@ func (s *EmbeddedDoltStore) GetIssueComments(ctx context.Context, issueID string panic("embeddeddolt: GetIssueComments not implemented") } +func (s *EmbeddedDoltStore) GetIssueCommentsByType(ctx context.Context, issueID, commentType string) ([]*types.Comment, error) { + panic("embeddeddolt: GetIssueCommentsByType not implemented") +} + func (s *EmbeddedDoltStore) GetEvents(ctx context.Context, issueID string, limit int) ([]*types.Event, error) { panic("embeddeddolt: GetEvents not implemented") } @@ -509,7 +513,7 @@ func (s *EmbeddedDoltStore) AddComment(ctx context.Context, issueID, actor, comm panic("embeddeddolt: AddComment not implemented") } -func (s *EmbeddedDoltStore) ImportIssueComment(ctx context.Context, issueID, author, text string, createdAt time.Time) (*types.Comment, error) { +func (s *EmbeddedDoltStore) ImportIssueComment(ctx context.Context, issueID, author, text, commentType string, createdAt time.Time) (*types.Comment, error) { panic("embeddeddolt: ImportIssueComment not implemented") } diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 875f2ed412..da642ded32 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -63,8 +63,9 @@ type Storage interface { GetEpicsEligibleForClosure(ctx context.Context) ([]*types.EpicStatus, error) // Comments and events - AddIssueComment(ctx context.Context, issueID, author, text string) (*types.Comment, error) + AddIssueComment(ctx context.Context, issueID, author, text, commentType string) (*types.Comment, error) GetIssueComments(ctx context.Context, issueID string) ([]*types.Comment, error) + GetIssueCommentsByType(ctx context.Context, issueID, commentType string) ([]*types.Comment, error) GetEvents(ctx context.Context, issueID string, limit int) ([]*types.Event, error) GetAllEventsSince(ctx context.Context, sinceID int64) ([]*types.Event, error) @@ -162,6 +163,6 @@ type Transaction interface { // Comment operations AddComment(ctx context.Context, issueID, actor, comment string) error - ImportIssueComment(ctx context.Context, issueID, author, text string, createdAt time.Time) (*types.Comment, error) + ImportIssueComment(ctx context.Context, issueID, author, text, commentType string, createdAt time.Time) (*types.Comment, error) GetIssueComments(ctx context.Context, issueID string) ([]*types.Comment, error) } diff --git a/internal/telemetry/storage.go b/internal/telemetry/storage.go index 5a64cba8e9..657a38da7c 100644 --- a/internal/telemetry/storage.go +++ b/internal/telemetry/storage.go @@ -309,13 +309,13 @@ func (s *InstrumentedStorage) GetEpicsEligibleForClosure(ctx context.Context) ([ // ── Comments & events ──────────────────────────────────────────────────────── -func (s *InstrumentedStorage) AddIssueComment(ctx context.Context, issueID, author, text string) (*types.Comment, error) { +func (s *InstrumentedStorage) AddIssueComment(ctx context.Context, issueID, author, text, commentType string) (*types.Comment, error) { attrs := []attribute.KeyValue{ attribute.String("bd.issue.id", issueID), attribute.String("bd.actor", author), } ctx, span, t := s.op(ctx, "AddIssueComment", attrs...) - v, err := s.inner.AddIssueComment(ctx, issueID, author, text) + v, err := s.inner.AddIssueComment(ctx, issueID, author, text, commentType) s.done(ctx, span, t, err, attrs...) return v, err } @@ -328,6 +328,17 @@ func (s *InstrumentedStorage) GetIssueComments(ctx context.Context, issueID stri return v, err } +func (s *InstrumentedStorage) GetIssueCommentsByType(ctx context.Context, issueID, commentType string) ([]*types.Comment, error) { + attrs := []attribute.KeyValue{ + attribute.String("bd.issue.id", issueID), + attribute.String("bd.comment.type", commentType), + } + ctx, span, t := s.op(ctx, "GetIssueCommentsByType", attrs...) + v, err := s.inner.GetIssueCommentsByType(ctx, issueID, commentType) + s.done(ctx, span, t, err, attrs...) + return v, err +} + func (s *InstrumentedStorage) GetEvents(ctx context.Context, issueID string, limit int) ([]*types.Event, error) { attrs := []attribute.KeyValue{attribute.String("bd.issue.id", issueID)} ctx, span, t := s.op(ctx, "GetEvents", attrs...) diff --git a/internal/types/types.go b/internal/types/types.go index e3ad71f85f..413244a288 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -826,12 +826,19 @@ type Label struct { Label string `json:"label"` } +const ( + CommentTypeDecision = "decision" + CommentTypeHandoff = "handoff" + CommentTypeNote = "note" +) + // Comment represents a comment on an issue type Comment struct { ID int64 `json:"id"` IssueID string `json:"issue_id"` Author string `json:"author"` Text string `json:"text"` + Type string `json:"type,omitempty"` CreatedAt time.Time `json:"created_at"` }