ticket

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 24, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package ticket provides core types and operations for ticket management.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AddDep

func AddDep(t *Ticket, depID string) error

AddDep adds depID to the ticket's deps list. Returns error if it would create a self-dependency.

func AddLink(a, b *Ticket)

AddLink adds a symmetric link between two tickets.

func BlockingDeps

func BlockingDeps(store Store, t *Ticket) []string

BlockingDeps returns the IDs of dependencies that are not done/closed.

func FindTicketsDir

func FindTicketsDir(startDir string) (string, bool)

FindTicketsDir walks up from startDir looking for a .tickets/ subdirectory. startDir should be an absolute path. Returns the path and true if found, or empty string and false.

func FormatNamespacedID

func FormatNamespacedID(project, ticketID string) string

FormatNamespacedID combines a project name and ticket ID into a namespaced ID ("project/ticket-id"). If project is empty, returns the bare ticket ID.

func GenerateID

func GenerateID(title string) string

GenerateID creates a ticket ID from the title and a hash. Format: slug-hash where slug is up to 3 meaningful words from the title, and hash is 4 hex chars derived from PID + timestamp.

func GenerateIDFrom

func GenerateIDFrom(title string, t time.Time) string

GenerateIDFrom creates a ticket ID from explicit inputs (testable).

func IsBlocked

func IsBlocked(store Store, t *Ticket) bool

IsBlocked returns true if any of the ticket's dependencies are not done/closed.

func IsReady

func IsReady(store Store, t *Ticket) bool

IsReady returns true if the ticket is actionable: not terminal, not backlog, all deps done, and parent chain is active.

func IsReadyOpen

func IsReadyOpen(store Store, t *Ticket) bool

IsReadyOpen is like IsReady but bypasses parent gating. Shows all unblocked non-terminal tickets regardless of epic status.

func IsReservedKey

func IsReservedKey(key string) bool

IsReservedKey reports whether key is a known frontmatter field name.

func ParseNamespacedID

func ParseNamespacedID(id string) (project, ticketID string)

ParseNamespacedID splits a namespaced ticket ID ("project/ticket-id") into its project and ticket ID components. If the ID contains no slash, the project is empty and the full string is the ticket ID.

func PropagateStatusUp

func PropagateStatusUp(store Store, child *Ticket) error

PropagateStatusUp bumps a parent epic's status in response to a child's status change. Rules:

  • child → ready: parent epic backlog → ready (no-op if parent already ready, open, done, or closed).
  • child → open: parent epic backlog or ready → open (no-op if parent already open, done, or closed).
  • child → done: if every child of the parent is now terminal (done or closed) and the parent is not already terminal, parent → done.

Changes cascade up nested epic chains via store.Update, which calls this function again on each write.

func RemoveDep

func RemoveDep(t *Ticket, depID string)

RemoveDep removes depID from the ticket's deps list. Matching is tolerant of namespace-prefix mismatches between the stored dep ID and the caller's argument.

func RemoveLink(a, b *Ticket)

RemoveLink removes a symmetric link between two tickets. Matching is tolerant of namespace-prefix mismatches.

func Serialize

func Serialize(t *Ticket) ([]byte, error)

Serialize writes a ticket to canonical markdown+YAML format.

func SortByPriorityID

func SortByPriorityID(tickets []*Ticket)

SortByPriorityID sorts by priority then ID. Used when grouping by status.

func SortByStatusPriorityID

func SortByStatusPriorityID(tickets []*Ticket)

SortByStatusPriorityID sorts tickets by status order, then priority (asc), then ID. This is the default sort for ls/ready/blocked.

func StatusOrder

func StatusOrder(s Status) int

StatusOrder returns the sort rank for a status (lower = earlier in display).

func TypeOrder

func TypeOrder(t TicketType) int

TypeOrder returns the sort rank for a ticket type.

func UpdateSection

func UpdateSection(body, heading, content string) string

UpdateSection replaces or inserts a markdown section in the body. If heading is empty, replaces the description (text before first structural heading).

func ValidateExtraKey

func ValidateExtraKey(key string) error

ValidateExtraKey checks that key is a valid extra field name. Keys must be non-empty identifiers: letters, digits, hyphens, underscores.

func ValidateExtraValue

func ValidateExtraValue(value string) error

ValidateExtraValue checks that value is safe for unquoted YAML serialization. Values allow printable ASCII: letters, digits, spaces, and common punctuation. YAML indicator characters (% ! & * @ ` : # [ ] { } | > ' ") and control characters are rejected to prevent parse failures in writeField output.

func ValidatePriority

func ValidatePriority(p int) error

ValidatePriority returns an error if p is outside the range 0-4.

func ValidateStateTransition

func ValidateStateTransition(store Store, t *Ticket) error

ValidateStateTransition rejects saving an epic as done while any of its children are still non-terminal (not done, not closed). Only checks when t.Type == TypeEpic and t.Status == StatusDone; otherwise it is a no-op.

The returned error names the offending children and suggests remediation, so CLI and MCP callers (and LLM agents) can self-correct.

func ValidateStatus

func ValidateStatus(s Status) error

ValidateStatus returns an error if s is not a recognized status.

func ValidateType

func ValidateType(t TicketType) error

ValidateType returns an error if t is not a recognized ticket type.

Types

type ActionKind

type ActionKind string

ActionKind describes what type of action is needed on a ticket.

const (
	ActionWork    ActionKind = "work"
	ActionBlocked ActionKind = "blocked"
	ActionReady   ActionKind = "ready"
)

type Cycle

type Cycle struct {
	IDs []string
}

Cycle represents a dependency cycle as an ordered list of ticket IDs.

func FindCycles

func FindCycles(store Store) ([]Cycle, error)

FindCycles detects dependency cycles among open (non-done/closed) tickets. Uses DFS with white(0)/gray(1)/black(2) coloring.

func (Cycle) String

func (c Cycle) String() string

type DepNode

type DepNode struct {
	ID     string
	Title  string
	Status Status
	Depth  int
}

DepNode represents one entry in a dependency tree.

func DepTree

func DepTree(store Store, id string, full bool) ([]DepNode, error)

DepTree walks the dependency graph for the given ticket ID. With full=false (default), each node appears only once (dedup). With full=true, shows the full tree with repeated subtrees.

type FileStore

type FileStore struct {
	Dir string
}

FileStore provides filesystem-backed CRUD operations for tickets.

func NewFileStore

func NewFileStore(dir string) *FileStore

NewFileStore creates a FileStore rooted at the given directory.

func (*FileStore) Create

func (s *FileStore) Create(t *Ticket) error

Create writes a new ticket to disk. The ticket must already have an ID. If the ID collides with an existing ticket, a new ID is generated and the ticket is retried (up to 5 attempts).

func (*FileStore) Delete

func (s *FileStore) Delete(id string) error

Delete removes a ticket file by exact or partial ID.

func (*FileStore) EnsureDir

func (s *FileStore) EnsureDir() error

EnsureDir creates the tickets directory if it doesn't exist.

func (*FileStore) Get

func (s *FileStore) Get(id string) (*Ticket, error)

Get retrieves a ticket by exact or partial ID.

func (*FileStore) List

func (s *FileStore) List() ([]*Ticket, error)

List reads all tickets from the directory.

func (*FileStore) Resolve

func (s *FileStore) Resolve(id string) (string, error)

Resolve finds the full file path for an exact or partial ticket ID. Returns an error if the ID is ambiguous (multiple matches) or not found.

func (*FileStore) Update

func (s *FileStore) Update(t *Ticket) error

Update writes a ticket back to disk in canonical format. When t.Status has changed, the write is validated (e.g. epics can't be marked done while children are still open) and the parent epic chain is updated via PropagateStatusUp.

type InboxItem

type InboxItem struct {
	Ticket *Ticket
	Action ActionKind
	Detail string    // Human-readable description of what's needed.
	Since  time.Time // When this action became pending.
}

InboxItem represents a ticket needing attention, with context about what's needed.

func Inbox

func Inbox(store Store) ([]InboxItem, error)

Inbox returns actionable tickets (ready or open), sorted by priority then age.

func NextAction

func NextAction(t *Ticket) InboxItem

NextAction computes the next action needed for a single ticket.

type ListOptions

type ListOptions struct {
	Status   Status
	Type     TicketType
	Priority int // -1 means no filter
	Tag      string
	Parent   string
}

ListOptions carries filter parameters for listing tickets.

func DefaultListOptions

func DefaultListOptions() ListOptions

DefaultListOptions returns options with no filters applied.

type MoveResult

type MoveResult struct {
	OldID         string
	NewID         string
	StrippedDeps  []string
	StrippedLinks []string
}

MoveResult describes a single ticket move operation.

func MoveTicket

func MoveTicket(src, dst *FileStore, id string, recursive bool) ([]MoveResult, error)

MoveTicket moves a single ticket from src store to dst store. The ticket is closed in src with a note, and created in dst with a new ID.

type MultiStore

type MultiStore struct {
	// contains filtered or unexported fields
}

MultiStore provides multi-project ticket storage by wrapping a FileStore per project subdirectory under a shared root. Ticket IDs are namespaced as "project/ticket-id" for cross-project disambiguation.

func NewMultiStore

func NewMultiStore(rootDir string) *MultiStore

NewMultiStore creates a MultiStore rooted at the given directory. Each subdirectory of rootDir is treated as a project with its own tickets.

func (*MultiStore) Create

func (m *MultiStore) Create(t *Ticket) error

Create writes a new ticket. The ticket ID must be namespaced ("project/id").

func (*MultiStore) Delete

func (m *MultiStore) Delete(id string) error

Delete removes a ticket by namespaced or bare ID. Bare IDs are resolved across all projects.

func (*MultiStore) Get

func (m *MultiStore) Get(id string) (*Ticket, error)

Get retrieves a ticket by namespaced ("project/id") or bare ID. Bare IDs are resolved across all projects; ambiguous matches return an error.

func (*MultiStore) List

func (m *MultiStore) List() ([]*Ticket, error)

List returns all tickets from all projects with namespaced IDs.

func (*MultiStore) Update

func (m *MultiStore) Update(t *Ticket) error

Update writes a ticket back to disk. Accepts namespaced or bare IDs. Bare IDs are resolved across all projects.

type Note

type Note struct {
	Timestamp time.Time
	Text      string
}

Note is a timestamped comment appended to a ticket.

type ProjectSummary

type ProjectSummary struct {
	Epic            *Ticket
	Total           int
	StatusBreakdown map[Status]int
	NextActions     []InboxItem
	CompletionPct   float64
}

ProjectSummary aggregates progress for an epic/parent ticket.

func Projects

func Projects(store Store) ([]ProjectSummary, error)

Projects returns active epics with their child progress, sorted by priority then completeness (least complete first).

type Status

type Status string

Status represents the lifecycle state of a ticket.

const (
	StatusBacklog Status = "backlog"
	StatusReady   Status = "ready"
	StatusOpen    Status = "open"
	StatusDone    Status = "done"
	StatusClosed  Status = "closed"
)

type Store

type Store interface {
	Get(id string) (*Ticket, error)
	List() ([]*Ticket, error)
	Create(t *Ticket) error
	Update(t *Ticket) error
	Delete(id string) error
}

Store defines the interface for ticket storage backends.

func ResolveStoreForRepo

func ResolveStoreForRepo(repoDir string) (Store, string, error)

ResolveStoreForRepo opens the ticket Store configured for the given repo directory. Reads ~/.ticket/config.yaml plus the shared <central_root>/config.yaml, resolves the project name via explicit path mapping, git remote, or directory basename, and returns a FileStore rooted at <central_root>/tickets/<name>.

Returns an error if no config is present or no project name resolves. The returned FileStore may wrap a directory that does not yet exist; List() on a missing directory returns (nil, nil) so callers can treat that as an empty project without special-casing.

type Ticket

type Ticket struct {
	ID          string     `yaml:"id"`
	Status      Status     `yaml:"status"`
	Type        TicketType `yaml:"type"`
	Priority    int        `yaml:"priority"`
	Parent      string     `yaml:"parent,omitempty"`
	Deps        []string   `yaml:"deps,flow"`
	Links       []string   `yaml:"links,flow"`
	Tags        []string   `yaml:"tags,omitempty,flow"`
	ExternalRef string     `yaml:"external-ref,omitempty"`
	Branch      string     `yaml:"branch,omitempty"`
	Created     time.Time  `yaml:"created"`

	// Custom key/value pairs, handled manually in format.go.
	Extra map[string]string `yaml:"-"`

	// Parsed from markdown, not stored in frontmatter.
	Title string `yaml:"-"`
	Body  string `yaml:"-"`
	Notes []Note `yaml:"-"`
}

Ticket is the core data structure representing a work item. YAML frontmatter fields are mapped via yaml tags. Title and body content are parsed from the markdown outside the frontmatter.

func BlockedTickets

func BlockedTickets(store Store) ([]*Ticket, error)

BlockedTickets returns all non-terminal, non-backlog tickets with unresolved deps.

func Filter

func Filter(tickets []*Ticket, opts ListOptions) []*Ticket

Filter returns tickets matching all non-zero fields in opts.

func Parse

func Parse(r io.Reader) (*Ticket, error)

Parse reads a ticket from markdown with YAML frontmatter.

func ReadyTickets

func ReadyTickets(store Store) ([]*Ticket, error)

ReadyTickets returns all tickets that pass the IsReady check.

func ReadyTicketsOpen

func ReadyTicketsOpen(store Store) ([]*Ticket, error)

ReadyTicketsOpen returns all unblocked tickets, bypassing parent gating.

func (*Ticket) Validate

func (t *Ticket) Validate() error

Validate checks all fields for consistency. Returns the first error found.

type TicketType

type TicketType string

TicketType represents the kind of work a ticket tracks.

const (
	TypeFeature TicketType = "feature"
	TypeBug     TicketType = "bug"
	TypeEpic    TicketType = "epic"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL