Skip to content

Ticket Ordering Architecture

This document describes the technical implementation of Gira's ticket ordering system, which allows manual arrangement of tickets within status columns.

Overview

Gira implements a flexible ordering system that allows users to manually arrange tickets within their status columns. This provides visual prioritization on the board while maintaining Git-friendly storage.

Design Principles

  1. Column-Scoped: Order is specific to each status column
  2. Gap-Based: Uses numeric values with gaps for efficient insertions
  3. Auto-Rebalancing: Automatically redistributes values when gaps are exhausted
  4. Git-Friendly: Order changes result in minimal file modifications
  5. Stable Defaults: Unordered tickets (order=0) sort by ID for consistency

Implementation Details

Order Field

Each ticket has an optional order field:

class Ticket(TimestampedModel):
    # ... other fields ...
    order: int = Field(default=0, description="Manual ordering within status column")

Order Values

  • Default: New tickets have order=0 (unordered)
  • Ordered: Positive integers with gaps (10, 20, 30...)
  • Gap Size: Default gap of 10 between sequential tickets
  • Maximum: No hard limit, uses Python's arbitrary precision integers

Ordering Algorithm

Position-Based Ordering

When setting a specific position (1-based):

def calculate_order_for_position(position: int, existing_tickets: List[Ticket]) -> int:
    if position == 1:
        return 10  # First position
    elif position > len(existing_tickets):
        # Place at end
        max_order = max(t.order for t in existing_tickets if t.order > 0)
        return max_order + 10
    else:
        # Find gap between positions
        prev_order = existing_tickets[position-2].order if position > 1 else 0
        next_order = existing_tickets[position-1].order
        return (prev_order + next_order) // 2

Relative Ordering

When placing before/after another ticket:

def calculate_relative_order(
    target_ticket: Ticket, 
    reference_ticket: Ticket,
    all_tickets: List[Ticket],
    placement: str  # "before" or "after"
) -> int:
    sorted_tickets = sorted(all_tickets, key=lambda t: (t.order or float('inf'), t.id))
    ref_index = sorted_tickets.index(reference_ticket)

    if placement == "before":
        if ref_index == 0:
            return max(1, reference_ticket.order - 10)
        else:
            prev_ticket = sorted_tickets[ref_index - 1]
            return (prev_ticket.order + reference_ticket.order) // 2
    else:  # after
        if ref_index == len(sorted_tickets) - 1:
            return reference_ticket.order + 10
        else:
            next_ticket = sorted_tickets[ref_index + 1]
            return (reference_ticket.order + next_ticket.order) // 2

Auto-Rebalancing

When gaps between tickets are exhausted:

def renumber_tickets(tickets: List[Ticket]) -> None:
    """Renumber all tickets with proper spacing."""
    sorted_tickets = sorted(tickets, key=lambda t: (t.order or float('inf'), t.id))

    for i, ticket in enumerate(sorted_tickets):
        ticket.order = (i + 1) * 10
        ticket.save()

Rebalancing triggers when: - Calculated order ≤ previous ticket's order - No gap available for insertion - Manual trigger via maintenance command

Sorting Logic

Tickets are sorted using a compound key:

def sort_tickets(tickets: List[Ticket]) -> List[Ticket]:
    return sorted(tickets, key=lambda t: (
        t.order if t.order > 0 else float('inf'),  # Ordered tickets first
        t.id  # Stable sort by ID for same order values
    ))

Storage Considerations

File Organization

Order is stored in each ticket's JSON file:

{
  "id": "PROJ-123",
  "title": "Example ticket",
  "status": "todo",
  "order": 20,
  "created_at": "2024-01-15T10:00:00Z",
  "updated_at": "2024-01-15T14:30:00Z"
}

Git Implications

  • Minimal Changes: Only affected ticket files are modified
  • Merge-Friendly: Order conflicts are rare due to gap strategy
  • Atomic Updates: Each order change is a single file write

Performance Characteristics

Time Complexity

  • View Ordered List: O(n log n) - sorting tickets
  • Order Single Ticket: O(n) - read all tickets in status
  • Rebalance Column: O(n) - renumber all tickets

Space Complexity

  • Memory: O(n) - load tickets for sorting
  • Storage: No additional files, order stored in ticket JSON

Edge Cases

Concurrent Modifications

When multiple users reorder simultaneously: - Last write wins for individual tickets - May trigger auto-rebalance on next operation - No data loss, only order preference conflicts

Large Columns

For status columns with many tickets: - Rebalancing becomes more expensive - Consider pagination for display - Order operations remain O(n)

Order Value Overflow

Extremely unlikely but handled: - Python's arbitrary precision integers - Rebalancing prevents runaway growth - No practical limit on reorderings

Best Practices

For Users

  1. Regular Grooming: Reorder during planning sessions
  2. Meaningful Positions: Top = highest priority
  3. Batch Operations: Reorder multiple tickets together
  4. Avoid Conflicts: Coordinate ordering in shared projects

For Developers

  1. Preserve Order: Maintain order when moving tickets
  2. Lazy Rebalancing: Only rebalance when necessary
  3. Atomic Operations: Complete order changes in single transaction
  4. Clear Errors: Provide actionable error messages

Future Enhancements

Planned Improvements

  1. Drag-and-Drop API: Endpoints for TUI/GUI interfaces
  2. Bulk Ordering: Order multiple tickets in one command
  3. Order Templates: Save and apply ordering patterns
  4. Smart Suggestions: AI-based order recommendations

Potential Optimizations

  1. Order Index: Separate index file for large projects
  2. Incremental Updates: Only load affected tickets
  3. Background Rebalancing: Async rebalancing for large columns
  4. Order History: Track ordering changes over time