refactor(deps): upgrade go-lunatask, drop deeplink

Amolith created

Delete internal/deeplink/ package and simplify internal/validate/ by
delegating to new go-lunatask v0.1.0-rc10 functions: ParseDeepLink,
BuildDeepLink, ParseTaskStatus, ParseMotivation,
ParseRelationshipStrength.

Removes ~250 lines of code now provided upstream.

Assisted-by: Claude Sonnet 4 via Crush

Change summary

AGENTS.md                          |  10 +-
cmd/area/show.go                   |   6 
cmd/goal/show.go                   |   4 
cmd/init/ui.go                     |  16 ---
cmd/init/ui_test.go                |   2 
cmd/note/show.go                   |   3 
cmd/person/show.go                 |   3 
cmd/task/show.go                   |   3 
go.mod                             |   8 +
go.sum                             |   4 
go.sum.license                     |   3 
internal/deeplink/deeplink.go      |  92 -----------------------
internal/deeplink/deeplink_test.go | 128 --------------------------------
internal/validate/validate.go      |  53 +++----------
14 files changed, 41 insertions(+), 294 deletions(-)

Detailed changes

AGENTS.md 🔗

@@ -51,10 +51,9 @@ internal/
   config/            → TOML config at ~/.config/lune/config.toml
   completion/        → Shell completion helpers (config-based + static values)
   dateutil/          → Natural language date parsing via go-dateparser
-  deeplink/          → Parse/build lunatask:// URLs
   stats/             → Usage statistics helpers
   ui/                → Lipgloss styles (Success, Warning, Error, H1, H2, FormatDate)
-  validate/          → Input validation (UUID, enums, deep links → UUID)
+  validate/          → Input validation (delegates to go-lunatask Parse* functions)
 ```
 
 **Command flow**: `fang.Execute()` wraps Cobra with version/commit info and
@@ -135,8 +134,9 @@ The implementations differ: name uses `bufio.Scanner` (single line), note uses
 ### Deep link support
 
 Commands accepting IDs also accept `lunatask://` deep links. Use
-`validate.Reference()` to normalize either format to a UUID. The `deeplink`
-package handles parsing and building these URLs.
+`validate.Reference()` to normalize either format to a UUID. Deep link
+parsing and building use `lunatask.ParseDeepLink()` and `lunatask.BuildDeepLink()`
+from go-lunatask.
 
 ### Linter exclusions for cmd/
 
@@ -176,7 +176,7 @@ colors.
 ## Testing
 
 Table-driven tests with `t.Parallel()`. Use `_test` package suffix for black-box
-testing (see `deeplink_test.go`). When testing commands, use `cmd.SetOut()` /
+testing (see `cmd/init/ui_test.go`). When testing commands, use `cmd.SetOut()` /
 `cmd.SetErr()` to capture output.
 
 ### Init wizard

cmd/area/show.go 🔗

@@ -7,10 +7,10 @@ package area
 import (
 	"fmt"
 
+	"git.secluded.site/go-lunatask"
 	"git.secluded.site/lune/internal/client"
 	"git.secluded.site/lune/internal/completion"
 	"git.secluded.site/lune/internal/config"
-	"git.secluded.site/lune/internal/deeplink"
 	"git.secluded.site/lune/internal/stats"
 	"git.secluded.site/lune/internal/ui"
 	"github.com/spf13/cobra"
@@ -52,7 +52,7 @@ func runShow(cmd *cobra.Command, args []string) error {
 }
 
 func printAreaDetails(cmd *cobra.Command, area *config.Area, counter *stats.TaskCounter) error {
-	link, _ := deeplink.Build(deeplink.Area, area.ID)
+	link, _ := lunatask.BuildDeepLink(lunatask.ResourceArea, area.ID)
 
 	fmt.Fprintf(cmd.OutOrStdout(), "%s (%s)\n", ui.H1.Render(area.Name), area.Key)
 	fmt.Fprintf(cmd.OutOrStdout(), "  ID:   %s\n", area.ID)
@@ -83,7 +83,7 @@ func printAreaDetails(cmd *cobra.Command, area *config.Area, counter *stats.Task
 }
 
 func printGoalSummary(cmd *cobra.Command, goal *config.Goal, counter *stats.TaskCounter) error {
-	goalLink, _ := deeplink.Build(deeplink.Goal, goal.ID)
+	goalLink, _ := lunatask.BuildDeepLink(lunatask.ResourceGoal, goal.ID)
 
 	fmt.Fprintf(cmd.OutOrStdout(), "  %s (%s)\n", ui.H2.Render(goal.Name), goal.Key)
 	fmt.Fprintf(cmd.OutOrStdout(), "    ID:   %s\n", goal.ID)

cmd/goal/show.go 🔗

@@ -9,10 +9,10 @@ import (
 	"fmt"
 	"strings"
 
+	"git.secluded.site/go-lunatask"
 	"git.secluded.site/lune/internal/client"
 	"git.secluded.site/lune/internal/completion"
 	"git.secluded.site/lune/internal/config"
-	"git.secluded.site/lune/internal/deeplink"
 	"git.secluded.site/lune/internal/stats"
 	"git.secluded.site/lune/internal/ui"
 	"github.com/spf13/cobra"
@@ -101,7 +101,7 @@ func resolveGoal(cfg *config.Config, goalKey, areaKey string) (*config.GoalMatch
 }
 
 func printGoalDetails(cmd *cobra.Command, goal *config.Goal, area *config.Area, counter *stats.TaskCounter) error {
-	link, _ := deeplink.Build(deeplink.Goal, goal.ID)
+	link, _ := lunatask.BuildDeepLink(lunatask.ResourceGoal, goal.ID)
 
 	fmt.Fprintf(cmd.OutOrStdout(), "%s (%s)\n", ui.H1.Render(goal.Name), goal.Key)
 	fmt.Fprintf(cmd.OutOrStdout(), "  ID:   %s\n", goal.ID)

cmd/init/ui.go 🔗

@@ -15,8 +15,8 @@ import (
 	"github.com/charmbracelet/huh"
 	"github.com/spf13/cobra"
 
+	"git.secluded.site/go-lunatask"
 	"git.secluded.site/lune/internal/config"
-	"git.secluded.site/lune/internal/deeplink"
 	"git.secluded.site/lune/internal/ui"
 )
 
@@ -161,22 +161,12 @@ func validateKeyFormat(input string) error {
 	return nil
 }
 
-func validateReference(input string, supportsDeepLink bool) (string, error) {
+func validateReference(input string, _ bool) (string, error) {
 	if input == "" {
 		return "", errRefRequired
 	}
 
-	if supportsDeepLink {
-		id, err := deeplink.ParseID(input)
-		if err != nil {
-			return "", errRefFormat
-		}
-
-		return id, nil
-	}
-
-	// Habits don't support deep links, only raw UUIDs
-	id, err := deeplink.ParseID(input)
+	_, id, err := lunatask.ParseDeepLink(input)
 	if err != nil {
 		return "", errRefFormat
 	}

cmd/init/ui_test.go 🔗

@@ -79,7 +79,7 @@ func TestValidateReference_Valid(t *testing.T) {
 		wantID           string
 	}{
 		{"valid UUID lowercase", uuidLower, false, uuidLower},
-		{"valid UUID uppercase", "123E4567-E89B-12D3-A456-426614174000", false, uuidLower},
+		{"valid UUID uppercase", "123E4567-E89B-12D3-A456-426614174000", false, "123E4567-E89B-12D3-A456-426614174000"},
 		{"deep link area", "lunatask://areas/" + uuidArea, true, uuidArea},
 		{"deep link goal", "lunatask://goals/" + uuidGoal, true, uuidGoal},
 		{"deep link note", "lunatask://notes/" + uuidNote, true, uuidNote},

cmd/note/show.go 🔗

@@ -11,7 +11,6 @@ import (
 	"git.secluded.site/go-lunatask"
 	"git.secluded.site/lune/internal/client"
 	"git.secluded.site/lune/internal/config"
-	"git.secluded.site/lune/internal/deeplink"
 	"git.secluded.site/lune/internal/ui"
 	"git.secluded.site/lune/internal/validate"
 	"github.com/spf13/cobra"
@@ -70,7 +69,7 @@ func outputNoteJSON(cmd *cobra.Command, note *lunatask.Note) error {
 }
 
 func printNoteDetails(cmd *cobra.Command, note *lunatask.Note) error {
-	link, _ := deeplink.Build(deeplink.Note, note.ID)
+	link, _ := lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
 
 	fmt.Fprintf(cmd.OutOrStdout(), "%s\n", ui.H1.Render("Note"))
 	fmt.Fprintf(cmd.OutOrStdout(), "  ID:   %s\n", note.ID)

cmd/person/show.go 🔗

@@ -10,7 +10,6 @@ import (
 
 	"git.secluded.site/go-lunatask"
 	"git.secluded.site/lune/internal/client"
-	"git.secluded.site/lune/internal/deeplink"
 	"git.secluded.site/lune/internal/ui"
 	"git.secluded.site/lune/internal/validate"
 	"github.com/spf13/cobra"
@@ -69,7 +68,7 @@ func outputPersonJSON(cmd *cobra.Command, person *lunatask.Person) error {
 }
 
 func printPersonDetails(cmd *cobra.Command, person *lunatask.Person) error {
-	link, _ := deeplink.Build(deeplink.Person, person.ID)
+	link, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
 
 	fmt.Fprintf(cmd.OutOrStdout(), "%s\n", ui.H1.Render("Person"))
 	fmt.Fprintf(cmd.OutOrStdout(), "  ID:   %s\n", person.ID)

cmd/task/show.go 🔗

@@ -11,7 +11,6 @@ import (
 	"git.secluded.site/go-lunatask"
 	"git.secluded.site/lune/internal/client"
 	"git.secluded.site/lune/internal/config"
-	"git.secluded.site/lune/internal/deeplink"
 	"git.secluded.site/lune/internal/ui"
 	"git.secluded.site/lune/internal/validate"
 	"github.com/spf13/cobra"
@@ -70,7 +69,7 @@ func outputTaskJSON(cmd *cobra.Command, task *lunatask.Task) error {
 }
 
 func printTaskDetails(cmd *cobra.Command, task *lunatask.Task) error {
-	link, _ := deeplink.Build(deeplink.Task, task.ID)
+	link, _ := lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
 
 	status := "unknown"
 	if task.Status != nil {

go.mod 🔗

@@ -1,15 +1,18 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: CC0-1.0
+
 module git.secluded.site/lune
 
 go 1.25.5
 
 require (
-	git.secluded.site/go-lunatask v0.1.0-rc9
+	git.secluded.site/go-lunatask v0.1.0-rc10
 	github.com/BurntSushi/toml v1.6.0
 	github.com/charmbracelet/fang v0.4.4
 	github.com/charmbracelet/huh v0.8.0
 	github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3
 	github.com/charmbracelet/lipgloss v1.1.0
-	github.com/google/uuid v1.6.0
 	github.com/klauspost/lctime v0.1.0
 	github.com/markusmobius/go-dateparser v1.2.4
 	github.com/spf13/cobra v1.10.2
@@ -40,6 +43,7 @@ require (
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
 	github.com/godbus/dbus/v5 v5.1.0 // indirect
+	github.com/google/uuid v1.6.0 // indirect
 	github.com/hablullah/go-hijri v1.0.2 // indirect
 	github.com/hablullah/go-juliandays v1.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect

go.sum 🔗

@@ -2,8 +2,8 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXy
 al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
 charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k=
 charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
-git.secluded.site/go-lunatask v0.1.0-rc9 h1:ri8PDl7Xzg3mGStvHBxvL5PKOlBSZGxKDBzkqurLpEw=
-git.secluded.site/go-lunatask v0.1.0-rc9/go.mod h1:sWUQxme1z7qfsfS59nU5hqPvsRCt+HBmT/yBeIn6Fmc=
+git.secluded.site/go-lunatask v0.1.0-rc10 h1:KKkYNs/cipNjIlRPXAvpPm5QcWSuA3REcG8XZ8sALk4=
+git.secluded.site/go-lunatask v0.1.0-rc10/go.mod h1:rxps7BBqF+BkY8VN5E7J9zSOzSbtZ1hDmLEOHxjTHZQ=
 github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
 github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
 github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=

go.sum.license 🔗

@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+
+SPDX-License-Identifier: CC0-1.0

internal/deeplink/deeplink.go 🔗

@@ -1,92 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-// Package deeplink parses and builds Lunatask deep links.
-package deeplink
-
-import (
-	"errors"
-	"fmt"
-	"strings"
-
-	"github.com/google/uuid"
-)
-
-// Resource represents a Lunatask resource type that supports deep linking.
-type Resource string
-
-// Supported Lunatask resource types for deep linking.
-const (
-	Area     Resource = "areas"
-	Goal     Resource = "goals"
-	Task     Resource = "tasks"
-	Note     Resource = "notes"
-	Person   Resource = "people"
-	Notebook Resource = "notebooks"
-)
-
-const (
-	scheme            = "lunatask://"
-	deepLinkPartCount = 2
-)
-
-// ErrInvalidReference indicates the input is neither a valid UUID nor deep link.
-var ErrInvalidReference = errors.New("invalid reference: expected UUID or lunatask:// deep link")
-
-// ErrUnsupportedResource indicates the deep link resource type is not recognised.
-var ErrUnsupportedResource = errors.New("unsupported resource type in deep link")
-
-// ParseID extracts a UUID from either a raw UUID string or a Lunatask deep link.
-// Accepts formats:
-//   - UUID: "3bbf1923-64ae-4bcf-96a9-9bb86c799dab"
-//   - Deep link: "lunatask://areas/3bbf1923-64ae-4bcf-96a9-9bb86c799dab"
-func ParseID(input string) (string, error) {
-	input = strings.TrimSpace(input)
-
-	// Try parsing as UUID first
-	if parsed, err := uuid.Parse(input); err == nil {
-		return parsed.String(), nil
-	}
-
-	// Try parsing as deep link
-	if !strings.HasPrefix(input, scheme) {
-		return "", fmt.Errorf("%w: %s", ErrInvalidReference, input)
-	}
-
-	path := strings.TrimPrefix(input, scheme)
-	parts := strings.Split(path, "/")
-
-	if len(parts) != deepLinkPartCount {
-		return "", fmt.Errorf("%w: %s", ErrInvalidReference, input)
-	}
-
-	resource := parts[0]
-	id := parts[1]
-
-	// Validate resource type
-	switch Resource(resource) {
-	case Area, Goal, Task, Note, Person, Notebook:
-		// Valid resource
-	default:
-		return "", fmt.Errorf("%w: %s", ErrUnsupportedResource, resource)
-	}
-
-	// Validate and normalise the UUID
-	parsed, err := uuid.Parse(id)
-	if err != nil {
-		return "", fmt.Errorf("%w: %s", ErrInvalidReference, input)
-	}
-
-	return parsed.String(), nil
-}
-
-// Build constructs a Lunatask deep link for the given resource and ID.
-func Build(resource Resource, id string) (string, error) {
-	parsed, err := uuid.Parse(id)
-	if err != nil {
-		return "", fmt.Errorf("%w: %s", ErrInvalidReference, id)
-	}
-
-	return fmt.Sprintf("%s%s/%s", scheme, resource, parsed.String()), nil
-}
@@ -1,128 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package deeplink_test
-
-import (
-	"errors"
-	"testing"
-
-	"git.secluded.site/lune/internal/deeplink"
-)
-
-func TestParseID_Valid(t *testing.T) {
-	t.Parallel()
-
-	const (
-		uuidLower = "123e4567-e89b-12d3-a456-426614174000"
-		uuidArea  = "3bbf1923-64ae-4bcf-96a9-9bb86c799dab"
-		uuidGoal  = "9d79e922-9ca8-4b8c-9aa5-dd98bb2492b2"
-	)
-
-	tests := []struct {
-		name   string
-		input  string
-		wantID string
-	}{
-		{"UUID lowercase", uuidLower, uuidLower},
-		{"UUID uppercase", "123E4567-E89B-12D3-A456-426614174000", uuidLower},
-		{"deep link areas", "lunatask://areas/" + uuidArea, uuidArea},
-		{"deep link goals", "lunatask://goals/" + uuidGoal, uuidGoal},
-		{"deep link tasks", "lunatask://tasks/" + uuidArea, uuidArea},
-		{"deep link notes", "lunatask://notes/" + uuidArea, uuidArea},
-		{"deep link people", "lunatask://people/" + uuidArea, uuidArea},
-		{"deep link notebooks", "lunatask://notebooks/" + uuidArea, uuidArea},
-		{"with whitespace", "  " + uuidLower + "  ", uuidLower},
-	}
-
-	for _, testCase := range tests {
-		t.Run(testCase.name, func(t *testing.T) {
-			t.Parallel()
-
-			id, err := deeplink.ParseID(testCase.input)
-			if err != nil {
-				t.Errorf("ParseID(%q) error = %v, want nil", testCase.input, err)
-			}
-
-			if id != testCase.wantID {
-				t.Errorf("ParseID(%q) = %q, want %q", testCase.input, id, testCase.wantID)
-			}
-		})
-	}
-}
-
-func TestParseID_Invalid(t *testing.T) {
-	t.Parallel()
-
-	tests := []struct {
-		name    string
-		input   string
-		wantErr error
-	}{
-		{"empty", "", deeplink.ErrInvalidReference},
-		{"random text", "not-a-uuid", deeplink.ErrInvalidReference},
-		{"invalid UUID", "123e4567-e89b-12d3-a456-42661417zzzz", deeplink.ErrInvalidReference},
-		{"wrong scheme", "http://areas/123e4567-e89b-12d3-a456-426614174000", deeplink.ErrInvalidReference},
-		{"invalid resource", "lunatask://habits/123e4567-e89b-12d3-a456-426614174000", deeplink.ErrUnsupportedResource},
-		{"missing path", "lunatask://", deeplink.ErrInvalidReference},
-		{"extra path", "lunatask://areas/foo/bar", deeplink.ErrInvalidReference},
-	}
-
-	for _, testCase := range tests {
-		t.Run(testCase.name, func(t *testing.T) {
-			t.Parallel()
-
-			_, err := deeplink.ParseID(testCase.input)
-			if !errors.Is(err, testCase.wantErr) {
-				t.Errorf("ParseID(%q) error = %v, want %v", testCase.input, err, testCase.wantErr)
-			}
-		})
-	}
-}
-
-func TestBuild(t *testing.T) {
-	t.Parallel()
-
-	const uuid = "3bbf1923-64ae-4bcf-96a9-9bb86c799dab"
-
-	tests := []struct {
-		name     string
-		resource deeplink.Resource
-		id       string
-		want     string
-	}{
-		{"area", deeplink.Area, uuid, "lunatask://areas/" + uuid},
-		{"goal", deeplink.Goal, uuid, "lunatask://goals/" + uuid},
-		{"task", deeplink.Task, uuid, "lunatask://tasks/" + uuid},
-		{"note", deeplink.Note, uuid, "lunatask://notes/" + uuid},
-		{"person", deeplink.Person, uuid, "lunatask://people/" + uuid},
-		{"notebook", deeplink.Notebook, uuid, "lunatask://notebooks/" + uuid},
-	}
-
-	for _, testCase := range tests {
-		t.Run(testCase.name, func(t *testing.T) {
-			t.Parallel()
-
-			got, err := deeplink.Build(testCase.resource, testCase.id)
-			if err != nil {
-				t.Errorf("Build(%v, %q) error = %v, want nil",
-					testCase.resource, testCase.id, err)
-			}
-
-			if got != testCase.want {
-				t.Errorf("Build(%v, %q) = %q, want %q",
-					testCase.resource, testCase.id, got, testCase.want)
-			}
-		})
-	}
-}
-
-func TestBuild_InvalidID(t *testing.T) {
-	t.Parallel()
-
-	_, err := deeplink.Build(deeplink.Area, "not-a-uuid")
-	if !errors.Is(err, deeplink.ErrInvalidReference) {
-		t.Errorf("Build(Area, \"not-a-uuid\") error = %v, want %v", err, deeplink.ErrInvalidReference)
-	}
-}

internal/validate/validate.go 🔗

@@ -8,35 +8,19 @@ package validate
 import (
 	"errors"
 	"fmt"
-	"strings"
 
 	"git.secluded.site/go-lunatask"
-	"github.com/google/uuid"
-
-	"git.secluded.site/lune/internal/deeplink"
 )
 
-// ErrInvalidID indicates an ID is not a valid UUID.
-var ErrInvalidID = errors.New("invalid ID: expected UUID format")
-
 // ErrInvalidReference indicates the input is not a valid UUID or deep link.
 var ErrInvalidReference = errors.New("invalid reference: expected UUID or lunatask:// deep link")
 
-// ID validates that the given string is a valid Lunatask ID (UUID).
-func ID(id string) error {
-	if _, err := uuid.Parse(id); err != nil {
-		return fmt.Errorf("%w: %s", ErrInvalidID, id)
-	}
-
-	return nil
-}
-
 // Reference parses a UUID or Lunatask deep link and returns the normalised UUID.
 // Accepts formats:
 //   - UUID: "3bbf1923-64ae-4bcf-96a9-9bb86c799dab"
 //   - Deep link: "lunatask://areas/3bbf1923-64ae-4bcf-96a9-9bb86c799dab"
 func Reference(input string) (string, error) {
-	id, err := deeplink.ParseID(input)
+	_, id, err := lunatask.ParseDeepLink(input)
 	if err != nil {
 		return "", fmt.Errorf("%w: %s", ErrInvalidReference, input)
 	}
@@ -50,15 +34,12 @@ var ErrInvalidStatus = errors.New("invalid status")
 // TaskStatus validates and normalizes a task status string.
 // Returns the corresponding lunatask.TaskStatus or an error if invalid.
 func TaskStatus(input string) (lunatask.TaskStatus, error) {
-	status := lunatask.TaskStatus(strings.ToLower(input))
-
-	switch status {
-	case lunatask.StatusLater, lunatask.StatusNext, lunatask.StatusStarted,
-		lunatask.StatusWaiting, lunatask.StatusCompleted:
-		return status, nil
-	default:
+	status, err := lunatask.ParseTaskStatus(input)
+	if err != nil {
 		return "", fmt.Errorf("%w: %s", ErrInvalidStatus, input)
 	}
+
+	return status, nil
 }
 
 // ErrInvalidMotivation indicates the motivation value is not recognized.
@@ -67,15 +48,12 @@ var ErrInvalidMotivation = errors.New("invalid motivation")
 // Motivation validates and normalizes a motivation string.
 // Returns the corresponding lunatask.Motivation or an error if invalid.
 func Motivation(input string) (lunatask.Motivation, error) {
-	motivation := lunatask.Motivation(strings.ToLower(input))
-
-	switch motivation {
-	case lunatask.MotivationUnknown, lunatask.MotivationMust,
-		lunatask.MotivationShould, lunatask.MotivationWant:
-		return motivation, nil
-	default:
+	motivation, err := lunatask.ParseMotivation(input)
+	if err != nil {
 		return "", fmt.Errorf("%w: %s", ErrInvalidMotivation, input)
 	}
+
+	return motivation, nil
 }
 
 // ErrInvalidRelationship indicates the relationship strength is not recognized.
@@ -84,15 +62,10 @@ var ErrInvalidRelationship = errors.New("invalid relationship strength")
 // RelationshipStrength validates and normalizes a relationship strength string.
 // Returns the corresponding lunatask.RelationshipStrength or an error if invalid.
 func RelationshipStrength(input string) (lunatask.RelationshipStrength, error) {
-	rel := lunatask.RelationshipStrength(strings.ToLower(input))
-
-	switch rel {
-	case lunatask.RelationshipFamily, lunatask.RelationshipIntimateFriend,
-		lunatask.RelationshipCloseFriend, lunatask.RelationshipCasualFriend,
-		lunatask.RelationshipAcquaintance, lunatask.RelationshipBusiness,
-		lunatask.RelationshipAlmostStranger:
-		return rel, nil
-	default:
+	rel, err := lunatask.ParseRelationshipStrength(input)
+	if err != nil {
 		return "", fmt.Errorf("%w: %s", ErrInvalidRelationship, input)
 	}
+
+	return rel, nil
 }