Rank captured tasks by what he actually said (#129)
Ordering is computed, not generated. Asking a 1.7B which of his tasks
matters most produces a fluent opinion with no basis in anything, and a
confidently wrong priority is worse than none — same posture as the
behaviour profile in internal/memory, which counts instead of summarising.
internal/tasks is a pure package (no ipc, no store, no cgo) holding the
score, the order and the Russian rendering, so the spoken list and the
/tasks page cannot drift. Four signals, all of them things he stated:
deadline (overdue > today > tomorrow > this week), stated urgency, age
with a cap so nothing rots at the bottom, and confirmed work always
ahead of mail-derived candidates. A task with no due date and no weight
scores nothing and carries no reason string — inventing a "потому что"
about a priority he never set is the failure mode this avoids.
Capture now picks up urgency he says out loud ("добавь в задачи срочно
оплатить интернет"), stripping the marker from the task text, and the web
add form offers the same three rungs. Ranking is a read: it sorts and
renders, never writes, schedules or announces.
This commit is contained in:
+51
-4
@@ -38,11 +38,33 @@ var taskCapturePrefixes = []string{
|
||||
"new task",
|
||||
}
|
||||
|
||||
// TaskCapture — a parsed capture: the task itself, plus the importance he
|
||||
// stated out loud if he stated one (Vikunja #129). Weight 0 means he said
|
||||
// nothing about importance, which the ranker treats as exactly that — no
|
||||
// urgency is inferred from the wording.
|
||||
type TaskCapture struct {
|
||||
Text string
|
||||
Weight int
|
||||
}
|
||||
|
||||
// urgencyMarkers — the words that set a weight, strongest first. Only these
|
||||
// two rungs: "срочно" is a deadline he has not named, "важно" is a preference,
|
||||
// and a third shade of urgent would be a distinction he never makes out loud.
|
||||
var urgencyMarkers = []struct {
|
||||
word string
|
||||
weight int
|
||||
}{
|
||||
{"срочно", 3},
|
||||
{"urgent", 3},
|
||||
{"важно", 2},
|
||||
{"important", 2},
|
||||
}
|
||||
|
||||
// ParseTaskCapture reports whether an utterance explicitly files a task, and
|
||||
// returns the task text with the marker stripped. A marker with nothing after it
|
||||
// is not a capture (there is no task in "добавь в задачи") — the caller falls
|
||||
// through to whatever it would otherwise have done with the turn.
|
||||
func ParseTaskCapture(text string) (string, bool) {
|
||||
func ParseTaskCapture(text string) (TaskCapture, bool) {
|
||||
trimmed := strings.TrimSpace(text)
|
||||
lower := strings.ToLower(trimmed)
|
||||
best := ""
|
||||
@@ -52,7 +74,7 @@ func ParseTaskCapture(text string) (string, bool) {
|
||||
}
|
||||
}
|
||||
if best == "" {
|
||||
return "", false
|
||||
return TaskCapture{}, false
|
||||
}
|
||||
// Cut on the rune length of the matched prefix. ToLower does not change the
|
||||
// byte length of Russian or English letters, so the index carries over.
|
||||
@@ -60,10 +82,35 @@ func ParseTaskCapture(text string) (string, bool) {
|
||||
rest = strings.TrimLeft(rest, ":—- ")
|
||||
rest = strings.TrimSpace(rest)
|
||||
rest = strings.TrimRight(rest, ".!")
|
||||
rest, weight := stripUrgency(rest)
|
||||
if rest == "" {
|
||||
return "", false
|
||||
return TaskCapture{}, false
|
||||
}
|
||||
return rest, true
|
||||
return TaskCapture{Text: rest, Weight: weight}, true
|
||||
}
|
||||
|
||||
// stripUrgency pulls a leading or trailing urgency word out of the task text
|
||||
// and returns the weight it implies. Only at the edges: "срочно оплатить
|
||||
// интернет" and "оплатить интернет срочно" are the same instruction, while
|
||||
// "позвонить в срочную помощь" is a task whose text happens to contain the
|
||||
// stem, and cutting a word out of the middle of it would mangle the task.
|
||||
//
|
||||
// The word is removed from the text, because the list should read "оплатить
|
||||
// интернет (важно)" and not "важно оплатить интернет (важно)".
|
||||
func stripUrgency(text string) (string, int) {
|
||||
for _, m := range urgencyMarkers {
|
||||
lower := strings.ToLower(text)
|
||||
switch {
|
||||
case strings.HasPrefix(lower, m.word+" "):
|
||||
return strings.TrimSpace(text[len(m.word):]), m.weight
|
||||
case strings.HasSuffix(lower, " "+m.word):
|
||||
return strings.TrimSpace(text[:len(text)-len(m.word)]), m.weight
|
||||
case lower == m.word:
|
||||
// Nothing but the marker — no task in it.
|
||||
return "", 0
|
||||
}
|
||||
}
|
||||
return text, 0
|
||||
}
|
||||
|
||||
// taskListWords — the nouns that make a question be about the task list.
|
||||
|
||||
Reference in New Issue
Block a user