diff --git a/internal/types/types.go b/internal/types/types.go index bba71af1..496044b9 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -420,6 +420,37 @@ func (t IssueType) IsValid() bool { return false } +// RequiredSection describes a recommended section for an issue type. +// Used by bd lint and bd create --validate for template validation. +type RequiredSection struct { + Heading string // Markdown heading, e.g., "## Steps to Reproduce" + Hint string // Guidance for what to include +} + +// RequiredSections returns the recommended sections for this issue type. +// Returns nil for types with no specific section requirements. +func (t IssueType) RequiredSections() []RequiredSection { + switch t { + case TypeBug: + return []RequiredSection{ + {Heading: "## Steps to Reproduce", Hint: "Describe how to reproduce the bug"}, + {Heading: "## Acceptance Criteria", Hint: "Define criteria to verify the fix"}, + } + case TypeTask, TypeFeature: + return []RequiredSection{ + {Heading: "## Acceptance Criteria", Hint: "Define criteria to verify completion"}, + } + case TypeEpic: + return []RequiredSection{ + {Heading: "## Success Criteria", Hint: "Define high-level success criteria"}, + } + default: + // Chore, message, molecule, gate, agent, role, convoy, event, merge-request + // have no required sections + return nil + } +} + // AgentState represents the self-reported state of an agent type AgentState string diff --git a/internal/types/types_test.go b/internal/types/types_test.go index 2f2be8da..1f3895bb 100644 --- a/internal/types/types_test.go +++ b/internal/types/types_test.go @@ -414,6 +414,42 @@ func TestIssueTypeIsValid(t *testing.T) { } } +func TestIssueTypeRequiredSections(t *testing.T) { + tests := []struct { + issueType IssueType + expectCount int + expectHeading string // First heading if any + }{ + {TypeBug, 2, "## Steps to Reproduce"}, + {TypeFeature, 1, "## Acceptance Criteria"}, + {TypeTask, 1, "## Acceptance Criteria"}, + {TypeEpic, 1, "## Success Criteria"}, + {TypeChore, 0, ""}, + {TypeMessage, 0, ""}, + {TypeMolecule, 0, ""}, + {TypeGate, 0, ""}, + {TypeAgent, 0, ""}, + {TypeRole, 0, ""}, + {TypeConvoy, 0, ""}, + {TypeEvent, 0, ""}, + {TypeMergeRequest, 0, ""}, + } + + for _, tt := range tests { + t.Run(string(tt.issueType), func(t *testing.T) { + sections := tt.issueType.RequiredSections() + if len(sections) != tt.expectCount { + t.Errorf("IssueType(%q).RequiredSections() returned %d sections, want %d", + tt.issueType, len(sections), tt.expectCount) + } + if tt.expectCount > 0 && sections[0].Heading != tt.expectHeading { + t.Errorf("IssueType(%q).RequiredSections()[0].Heading = %q, want %q", + tt.issueType, sections[0].Heading, tt.expectHeading) + } + }) + } +} + func TestAgentStateIsValid(t *testing.T) { cases := []struct { name string