be62660be9
Step 5 of the board build. internal/tasks/stall.go counts three shapes — overdue, sitting longer than StallDays, waiting for confirmation — and states nothing about what any of them means. That is the line internal/memory/behavior.go already drew for habits, and the reason is the same: a 1.7B asked to judge will agree fluently and launder a guess into a decision. A test asserts the wording carries no assessment. Sitting is measured from created_ts, the only clock a live row carries: the store stamps resolved_ts and nothing else. So "no state change in eleven days" is exactly "captured eleven days ago and still live", which is narrower than the plan's wording and is the claim the data supports. A candidate is never counted as overdue, because its due date is Maven's reading of a mail rather than a deadline he set. Not a nag. No tick rule reads the counts; they go on /tasks and into the list reply when he asks, and tickLoop.dayPlan still does not read tasks at all. The empty case renders as nothing: "ничего не залежалось" appended to every list read is a nag with a friendly face. Three say entries, so the page and the spoken list cannot word it differently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
100 lines
3.7 KiB
Go
100 lines
3.7 KiB
Go
package tasks
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/say"
|
|
)
|
|
|
|
// Counted stall shapes (Vikunja #512, step 5 of docs/plans/15-board-surface.md).
|
|
//
|
|
// She may count. She may not assess. Every shape here is arithmetic over rows he
|
|
// can see — how many, how long, how many undated — and none of it says whether a
|
|
// task matters, whether a blocker is real, or whether something should be
|
|
// dropped. That is the line internal/memory/behavior.go already drew for habits
|
|
// and the reason is the same: a 1.7B asked to judge will agree fluently and
|
|
// launder a guess into a decision.
|
|
//
|
|
// Not a nag either. Nothing here is read by the tick loop; the counts go on
|
|
// /tasks and into the answer when he asks for the list. tickLoop.dayPlan
|
|
// deliberately does not read tasks — keep it that way.
|
|
|
|
// StallDays — how long a live task has to have sat before it is counted as
|
|
// sitting. Ten days rather than a week, because a task captured on a Friday and
|
|
// still open the next Friday is an ordinary week, not a stall.
|
|
//
|
|
// Measured from Created, which is the only clock a live row carries: the store
|
|
// stamps resolved_ts and nothing else, so "no state change in eleven days" is
|
|
// exactly "captured eleven days ago and still live". That is a narrower claim
|
|
// than the plan's wording and it is the one the data supports.
|
|
const StallDays = 10
|
|
|
|
// Stall — one counted shape: how many tasks, and the sentence that says what
|
|
// they have in common. N is always ≥ 1; a shape with no tasks in it is not
|
|
// returned, because "нет просроченных" is a reassurance nobody asked for.
|
|
type Stall struct {
|
|
N int
|
|
Line string
|
|
}
|
|
|
|
// Stalls counts the shapes present in a set of live tasks, in a fixed order:
|
|
// overdue first, then sitting, then unconfirmed. Fixed because the order is what
|
|
// he reads first, and sorting by count would move the sections around every time
|
|
// one number changed.
|
|
//
|
|
// Resolved tasks are not passed in and would not be counted if they were: this
|
|
// is a statement about outstanding work.
|
|
func Stalls(items []Item, now time.Time) []Stall {
|
|
var overdue, sitting, unconfirmed int
|
|
for _, it := range items {
|
|
if it.Status == StatusCandidate {
|
|
unconfirmed++
|
|
// A candidate is Maven's reading of something she read. Counting it as
|
|
// overdue would put her own guess about a deadline in a number he is
|
|
// meant to act on.
|
|
continue
|
|
}
|
|
if it.Due != nil && it.Due.Before(now) {
|
|
overdue++
|
|
}
|
|
if now.Sub(it.Created) >= StallDays*24*time.Hour {
|
|
sitting++
|
|
}
|
|
}
|
|
var out []Stall
|
|
add := func(n int, key string, args map[string]string) {
|
|
if n == 0 {
|
|
return
|
|
}
|
|
out = append(out, Stall{N: n, Line: say.S(key, args)})
|
|
}
|
|
add(overdue, say.StallOverdue, map[string]string{
|
|
"n": fmt.Sprint(overdue), "word": say.CountWord(overdue, "задача", "задачи", "задач"),
|
|
})
|
|
add(sitting, say.StallSitting, map[string]string{
|
|
"n": fmt.Sprint(sitting), "word": say.CountWord(sitting, "задача", "задачи", "задач"),
|
|
"days": fmt.Sprint(StallDays), "dayword": say.Days(StallDays),
|
|
})
|
|
add(unconfirmed, say.StallUnconfirmed, map[string]string{
|
|
"n": fmt.Sprint(unconfirmed), "word": say.CountWord(unconfirmed, "задача", "задачи", "задач"),
|
|
})
|
|
return out
|
|
}
|
|
|
|
// StallsRU joins the shapes into one sentence for the spoken list, or "" when
|
|
// there are none. Empty on purpose: the list read is the answer to his question,
|
|
// and appending "ничего не залежалось" to it every time is a nag with a friendly
|
|
// face.
|
|
func StallsRU(stalls []Stall) string {
|
|
if len(stalls) == 0 {
|
|
return ""
|
|
}
|
|
parts := make([]string, 0, len(stalls))
|
|
for _, s := range stalls {
|
|
parts = append(parts, s.Line)
|
|
}
|
|
return strings.Join(parts, "; ") + "."
|
|
}
|