feat(veans): add canonical status to bucket-title mapping

This commit is contained in:
Tink bot 2026-05-26 22:39:18 +02:00 committed by kolaente
parent 36fb0f0ace
commit 6b48a37710
2 changed files with 184 additions and 0 deletions

View File

@ -0,0 +1,123 @@
// Package status maps the five canonical veans statuses to Vikunja bucket
// IDs and the `done` flag. The mapping is canonical and reflected verbatim
// in the agent prompt (see internal/commands/prompt.tmpl).
package status
import (
"fmt"
"strings"
"code.vikunja.io/veans/internal/config"
)
// Status is the agent-facing state name.
type Status string
const (
Todo Status = "todo"
InProgress Status = "in-progress"
InReview Status = "in-review"
Completed Status = "completed"
Scrapped Status = "scrapped"
)
// All returns the canonical statuses in display order.
func All() []Status {
return []Status{Todo, InProgress, InReview, Completed, Scrapped}
}
// CanonicalBucketTitles is the strict-with-override list seeded by `init`.
var CanonicalBucketTitles = []string{
"Todo",
"In Progress",
"In Review",
"Done",
"Scrapped",
}
// BucketTitle returns the bucket name that backs each status.
func (s Status) BucketTitle() string {
switch s {
case Todo:
return "Todo"
case InProgress:
return "In Progress"
case InReview:
return "In Review"
case Completed:
return "Done"
case Scrapped:
return "Scrapped"
}
return ""
}
// Done reports whether tasks in this status should have done=true.
func (s Status) Done() bool {
return s == Completed || s == Scrapped
}
// Parse normalizes user input. Accepts the canonical hyphenated form, plus
// underscored/snake variants and a couple of natural-language synonyms.
func Parse(raw string) (Status, error) {
n := strings.TrimSpace(strings.ToLower(raw))
n = strings.ReplaceAll(n, "_", "-")
n = strings.ReplaceAll(n, " ", "-")
switch n {
case "todo":
return Todo, nil
case "in-progress", "wip", "doing":
return InProgress, nil
case "in-review", "review":
return InReview, nil
case "completed", "done":
return Completed, nil
case "scrapped", "cancelled", "canceled":
return Scrapped, nil
}
return "", fmt.Errorf("unknown status %q (expected one of: %s)",
raw, strings.Join(allStrings(), ", "))
}
// BucketID resolves a status to the bucket ID stored in .veans.yml.
func BucketID(s Status, b config.Buckets) (int64, error) {
switch s {
case Todo:
return b.Todo, nil
case InProgress:
return b.InProgress, nil
case InReview:
return b.InReview, nil
case Completed:
return b.Done, nil
case Scrapped:
return b.Scrapped, nil
}
return 0, fmt.Errorf("unknown status %q", s)
}
// FromBucketID is the inverse of BucketID — used by `list` to render the
// status of a task fetched from the API.
func FromBucketID(id int64, b config.Buckets) Status {
switch id {
case b.Todo:
return Todo
case b.InProgress:
return InProgress
case b.InReview:
return InReview
case b.Done:
return Completed
case b.Scrapped:
return Scrapped
}
return ""
}
func allStrings() []string {
out := make([]string, 0, 5)
for _, s := range All() {
out = append(out, string(s))
}
return out
}

View File

@ -0,0 +1,61 @@
package status
import (
"testing"
"code.vikunja.io/veans/internal/config"
)
func TestParse(t *testing.T) {
cases := map[string]Status{
"todo": Todo,
"TODO": Todo,
"in-progress": InProgress,
"in_progress": InProgress,
"in progress": InProgress,
"WIP": InProgress,
"doing": InProgress,
"in-review": InReview,
"review": InReview,
"completed": Completed,
"done": Completed,
"scrapped": Scrapped,
"cancelled": Scrapped,
"canceled": Scrapped,
}
for in, want := range cases {
got, err := Parse(in)
if err != nil {
t.Errorf("Parse(%q): %v", in, err)
continue
}
if got != want {
t.Errorf("Parse(%q): got %q, want %q", in, got, want)
}
}
if _, err := Parse("nope"); err == nil {
t.Errorf("Parse(\"nope\"): expected error")
}
}
func TestDoneFlag(t *testing.T) {
if !Completed.Done() || !Scrapped.Done() {
t.Fatal("Completed/Scrapped should be done")
}
if Todo.Done() || InProgress.Done() || InReview.Done() {
t.Fatal("Todo/InProgress/InReview should not be done")
}
}
func TestBucketIDRoundTrip(t *testing.T) {
b := config.Buckets{Todo: 11, InProgress: 12, InReview: 13, Done: 14, Scrapped: 15}
for _, s := range All() {
id, err := BucketID(s, b)
if err != nil {
t.Fatalf("BucketID(%q): %v", s, err)
}
if got := FromBucketID(id, b); got != s {
t.Errorf("FromBucketID(%d) = %q, want %q", id, got, s)
}
}
}