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¶
- Column-Scoped: Order is specific to each status column
- Gap-Based: Uses numeric values with gaps for efficient insertions
- Auto-Rebalancing: Automatically redistributes values when gaps are exhausted
- Git-Friendly: Order changes result in minimal file modifications
- 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¶
- Regular Grooming: Reorder during planning sessions
- Meaningful Positions: Top = highest priority
- Batch Operations: Reorder multiple tickets together
- Avoid Conflicts: Coordinate ordering in shared projects
For Developers¶
- Preserve Order: Maintain order when moving tickets
- Lazy Rebalancing: Only rebalance when necessary
- Atomic Operations: Complete order changes in single transaction
- Clear Errors: Provide actionable error messages
Future Enhancements¶
Planned Improvements¶
- Drag-and-Drop API: Endpoints for TUI/GUI interfaces
- Bulk Ordering: Order multiple tickets in one command
- Order Templates: Save and apply ordering patterns
- Smart Suggestions: AI-based order recommendations
Potential Optimizations¶
- Order Index: Separate index file for large projects
- Incremental Updates: Only load affected tickets
- Background Rebalancing: Async rebalancing for large columns
- Order History: Track ordering changes over time
Related Documentation¶
- Ticket Management - User guide for ordering
- CLI Reference - Command reference
- Architecture Overview - System architecture