Files
gems/opal-task/internal/engine/urgency.go
T
joakim 8f6db4672a Implement urgency system with TaskWarrior-inspired calculation
- Add urgency calculation based on multiple factors:
  * Due date (linear scale: overdue=12.0, today=10.0, week=6.0, 2weeks=2.0)
  * Priority (H=6.0, M=3.9, D=1.8, L=0.0)
  * Age (0-2.0 over 365 days)
  * Active status (+4.0 boost)
  * Waiting status (-3.0 penalty)
  * Tags (+1.0 with count modifier)
  * Project assignment (+1.0)
  * Configurable urgent tag (default 'next', +15.0)

- Replace priority column with urgency in all reports
  * Display as decimal with 1 decimal place
  * 4-tier color coding: ≥10 (bright red), ≥5 (red), ≥2 (yellow), <2 (cyan)
  * Minimal format color-coded by urgency

- Add default urgency sorting to all reports
  * list, minimal, active, ready, overdue reports sort by urgency
  * newest/oldest keep date-based sorting

- Implement 'next' report
  * Shows most urgent ready tasks
  * Configurable limit (default 5)
  * Only includes tasks ready to work on (no future wait/scheduled)

- Add urgency display to info command
  * Shows urgency score alongside priority

- All urgency coefficients configurable via config
  * Adjusted defaults for Opal's simpler model (no blocking/annotations)
  * Configurable urgent tag name (not hardcoded to 'next')

Priority order maintained: High > Medium > Default > Low
2026-01-06 14:32:44 +01:00

191 lines
4.7 KiB
Go

package engine
// UrgencyCoefficients holds the configurable coefficients for urgency calculation
type UrgencyCoefficients struct {
Due float64
PriorityH float64
PriorityM float64
PriorityD float64
PriorityL float64
Active float64
Age float64
AgeMax int
Tags float64
Project float64
Waiting float64
UrgentTag string // Configurable urgent tag name (default: "next")
UrgentCoeff float64 // Coefficient for urgent tag
}
// CalculateUrgency computes the urgency score for a task
func (t *Task) CalculateUrgency(coeffs *UrgencyCoefficients) float64 {
urgency := 0.0
// Due date contribution
urgency += t.urgencyDue(coeffs.Due)
// Priority contribution
urgency += t.urgencyPriority(coeffs)
// Age contribution
urgency += t.urgencyAge(coeffs.Age, coeffs.AgeMax)
// Active task contribution
urgency += t.urgencyActive(coeffs.Active)
// Waiting task penalty
urgency += t.urgencyWaiting(coeffs.Waiting)
// Tags contribution
urgency += t.urgencyTags(coeffs)
// Project contribution
urgency += t.urgencyProject(coeffs.Project)
return urgency
}
// urgencyDue calculates urgency contribution from due date
// Linear scale: overdue=12.0, today=10.0, 7 days=6.0, 14+ days=2.0
func (t *Task) urgencyDue(coeff float64) float64 {
if t.Due == nil {
return 0.0
}
now := timeNow()
due := *t.Due
// Calculate days until due (negative if overdue)
duration := due.Sub(now)
daysUntil := duration.Hours() / 24.0
var scale float64
if daysUntil < 0 {
// Overdue - maximum urgency
scale = 12.0
} else if daysUntil < 1 {
// Due today
scale = 10.0
} else if daysUntil <= 7 {
// Due within a week - linear from 10.0 to 6.0
scale = 10.0 - (daysUntil * 0.571) // (10-6)/7 = 0.571
} else if daysUntil <= 14 {
// Due within two weeks - linear from 6.0 to 2.0
scale = 6.0 - ((daysUntil - 7.0) * 0.571) // (6-2)/7 = 0.571
} else {
// Due further out - low urgency
scale = 2.0
}
return scale * coeff
}
// urgencyPriority calculates urgency contribution from priority
// Order: High > Medium > Default > Low
func (t *Task) urgencyPriority(coeffs *UrgencyCoefficients) float64 {
switch t.Priority {
case PriorityHigh:
return coeffs.PriorityH
case PriorityMedium:
return coeffs.PriorityM
case PriorityDefault:
return coeffs.PriorityD
case PriorityLow:
return coeffs.PriorityL
default:
return 0.0
}
}
// urgencyAge calculates urgency contribution from task age
// Age increases linearly up to maxDays, then caps
func (t *Task) urgencyAge(coeff float64, maxDays int) float64 {
now := timeNow()
ageDuration := now.Sub(t.Created)
ageDays := int(ageDuration.Hours() / 24.0)
if ageDays > maxDays {
ageDays = maxDays
}
ageRatio := float64(ageDays) / float64(maxDays)
return ageRatio * coeff
}
// urgencyActive calculates urgency contribution for started tasks
func (t *Task) urgencyActive(coeff float64) float64 {
if t.Start != nil {
return coeff
}
return 0.0
}
// urgencyWaiting calculates urgency penalty for waiting tasks
func (t *Task) urgencyWaiting(coeff float64) float64 {
if t.Wait != nil {
now := timeNow()
if t.Wait.After(now) {
return coeff // Should be negative coefficient
}
}
return 0.0
}
// urgencyTags calculates urgency contribution from tags
// Special urgent tag (configurable, default "next") has high coefficient
// Regular tags contribute with diminishing returns (0.8 for 1, 0.9 for 2, 1.0 for 3+)
func (t *Task) urgencyTags(coeffs *UrgencyCoefficients) float64 {
tagCount := len(t.Tags)
if tagCount == 0 {
return 0.0
}
// Check for special urgent tag first
for _, tag := range t.Tags {
if tag == coeffs.UrgentTag {
return coeffs.UrgentCoeff
}
}
// Apply general tag coefficient with multiplier based on count
var multiplier float64
switch tagCount {
case 1:
multiplier = 0.8
case 2:
multiplier = 0.9
default:
multiplier = 1.0
}
return multiplier * coeffs.Tags
}
// urgencyProject calculates urgency contribution from having a project
func (t *Task) urgencyProject(coeff float64) float64 {
if t.Project != nil {
return coeff
}
return 0.0
}
// BuildUrgencyCoefficients creates UrgencyCoefficients from config
func BuildUrgencyCoefficients(cfg *Config) *UrgencyCoefficients {
return &UrgencyCoefficients{
Due: cfg.UrgencyDue,
PriorityH: cfg.UrgencyPriorityH,
PriorityM: cfg.UrgencyPriorityM,
PriorityD: cfg.UrgencyPriorityD,
PriorityL: cfg.UrgencyPriorityL,
Active: cfg.UrgencyActive,
Age: cfg.UrgencyAge,
AgeMax: cfg.UrgencyAgeMax,
Tags: cfg.UrgencyTags,
Project: cfg.UrgencyProject,
Waiting: cfg.UrgencyWaiting,
UrgentTag: cfg.UrgencyUrgentTag,
UrgentCoeff: cfg.UrgencyUrgentCoeff,
}
}