Address #129 review: one date path, ordered years, trimmed dates
Build image / build-and-push (push) Successful in 8s
Build image / build-and-push (push) Successful in 8s
- Every dated tool argument now goes through day() → parseDay, so a prose date on remove_planting / remove_plantings / clear_object (and place, fill, journal) is refused with the same message as update_planting's. - parseDay's trimmed value is what gets stored, not the raw argument. - list_years re-sorts after adding the gardener's year instead of prepending it: newest first holds when their year is the oldest. - ClearSeedLot matches its JSON tag; the label-clearing branch says why nil. - The prompt says the notes are facts to plan with, not instructions. Left as is: update_garden's read-then-overlay merge. UpdateGarden is whole-row by design (the REST PATCH sends every field too), and a service GardenPatch would duplicate gardenFromInput's validation for one caller. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
+71
-32
@@ -3,6 +3,7 @@ package agent
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -241,16 +242,21 @@ func (a *adapter) lastRevert() *int64 {
|
||||
}
|
||||
|
||||
// day is the date a tool stamps: the one the model passed, else the gardener's
|
||||
// local today, else nil for the service's UTC default.
|
||||
func (a *adapter) day(explicit string) *string {
|
||||
if d := strings.TrimSpace(explicit); d != "" {
|
||||
return &d
|
||||
// local today, else nil for the service's UTC default. A date the model typed
|
||||
// is checked here, so every dated tool refuses a prose date the same way.
|
||||
func (a *adapter) day(explicit string) (*string, error) {
|
||||
if strings.TrimSpace(explicit) != "" {
|
||||
d, err := parseDay(explicit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &d, nil
|
||||
}
|
||||
if a.today != "" {
|
||||
d := a.today
|
||||
return &d
|
||||
return &d, nil
|
||||
}
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (a *adapter) listGardens(ctx context.Context, _ struct{}) (any, error) {
|
||||
@@ -272,7 +278,9 @@ func (a *adapter) listYears(ctx context.Context, args struct {
|
||||
return nil, err
|
||||
}
|
||||
// The service pads the list with ITS current year (UTC); the gardener's may
|
||||
// differ around New Year. Theirs is the one "this year" means to them.
|
||||
// differ around New Year. Theirs is the one "this year" means to them — and
|
||||
// it is not always the newest, so the list is re-sorted rather than
|
||||
// prepended to.
|
||||
if y, ok := yearOf(a.today); ok {
|
||||
present := false
|
||||
for _, have := range years {
|
||||
@@ -282,7 +290,8 @@ func (a *adapter) listYears(ctx context.Context, args struct {
|
||||
}
|
||||
}
|
||||
if !present {
|
||||
years = append([]int{y}, years...)
|
||||
years = append(years, y)
|
||||
sort.Sort(sort.Reverse(sort.IntSlice(years)))
|
||||
}
|
||||
}
|
||||
return map[string]any{"years": years}, nil
|
||||
@@ -343,9 +352,13 @@ func (a *adapter) placePlanting(ctx context.Context, args struct {
|
||||
PlantedAt string `json:"plantedAt" description:"optional planting date, YYYY-MM-DD; defaults to today"`
|
||||
SeedLotID *int64 `json:"seedLotId" description:"optional seed lot (from list_seed_lots) this planting uses, so the lot counts it as used"`
|
||||
}) (any, error) {
|
||||
on, err := a.day(args.PlantedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.svc.CreatePlanting(ctx, a.actor, args.ObjectID, service.PlantingInput{
|
||||
PlantID: args.PlantID, XCM: args.XCM, YCM: args.YCM, RadiusCM: args.RadiusCM, Count: args.Count,
|
||||
PlantedAt: a.day(args.PlantedAt), SeedLotID: args.SeedLotID,
|
||||
PlantedAt: on, SeedLotID: args.SeedLotID,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -362,9 +375,13 @@ func (a *adapter) fillRegion(ctx context.Context, args struct {
|
||||
PlantedAt string `json:"plantedAt" description:"optional planting date for every plop, YYYY-MM-DD; defaults to today"`
|
||||
SeedLotID *int64 `json:"seedLotId" description:"optional seed lot (from list_seed_lots) this fill uses, so the lot counts it as used"`
|
||||
}) (any, error) {
|
||||
on, err := a.day(args.PlantedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spec := service.FillSpec{
|
||||
RegionName: args.Region, PlantID: args.PlantID, SpacingOverride: args.SpacingOverride,
|
||||
Layout: service.FillLayout(args.Mode), PlantedAt: a.day(args.PlantedAt), SeedLotID: args.SeedLotID,
|
||||
Layout: service.FillLayout(args.Mode), PlantedAt: on, SeedLotID: args.SeedLotID,
|
||||
}
|
||||
rect := []*float64{args.X0CM, args.Y0CM, args.X1CM, args.Y1CM}
|
||||
given := 0
|
||||
@@ -450,8 +467,12 @@ func (a *adapter) addJournalEntry(ctx context.Context, args struct {
|
||||
Body string `json:"body" description:"what happened, in plain words"`
|
||||
ObservedAt string `json:"observedAt" description:"optional date it happened, YYYY-MM-DD; defaults to today"`
|
||||
}) (any, error) {
|
||||
on, err := a.day(args.ObservedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.svc.CreateJournalEntry(ctx, a.actor, args.GardenID, service.JournalInput{
|
||||
ObjectID: args.ObjectID, Body: args.Body, ObservedAt: a.day(args.ObservedAt),
|
||||
ObjectID: args.ObjectID, Body: args.Body, ObservedAt: on,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -459,7 +480,11 @@ func (a *adapter) clearObject(ctx context.Context, args struct {
|
||||
ObjectID int64 `json:"objectId" description:"object to remove all plants from"`
|
||||
RemovedAt string `json:"removedAt" description:"optional date the plants came out, YYYY-MM-DD; defaults to today"`
|
||||
}) (any, error) {
|
||||
n, err := a.svc.ClearPlantings(ctx, a.actor, args.ObjectID, service.ClearOptions{RemovedAt: a.day(args.RemovedAt)})
|
||||
on, err := a.day(args.RemovedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n, err := a.svc.ClearPlantings(ctx, a.actor, args.ObjectID, service.ClearOptions{RemovedAt: on})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -475,8 +500,12 @@ func (a *adapter) removePlantings(ctx context.Context, args struct {
|
||||
// Left out, it would "remove" plant 0 — nothing — and report success.
|
||||
return nil, fmt.Errorf("%w: plantId is required — say which plant to remove, or use clear_object for all of them", domain.ErrInvalidInput)
|
||||
}
|
||||
on, err := a.day(args.RemovedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n, err := a.svc.ClearPlantings(ctx, a.actor, args.ObjectID,
|
||||
service.ClearOptions{PlantID: &args.PlantID, RemovedAt: a.day(args.RemovedAt)})
|
||||
service.ClearOptions{PlantID: &args.PlantID, RemovedAt: on})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -484,22 +513,23 @@ func (a *adapter) removePlantings(ctx context.Context, args struct {
|
||||
}
|
||||
|
||||
func (a *adapter) updatePlanting(ctx context.Context, args struct {
|
||||
PlantingID int64 `json:"plantingId" description:"plop to correct (its id from describe_garden or list_plantings)"`
|
||||
Version int64 `json:"version" description:"the plop's current version"`
|
||||
PlantedAt *string `json:"plantedAt" description:"optional corrected planting date, YYYY-MM-DD"`
|
||||
Count *int `json:"count" description:"optional explicit plant count for the plop"`
|
||||
ClearCount bool `json:"clearCount" description:"optional: drop an explicit count and derive it from area and spacing again"`
|
||||
Label *string `json:"label" description:"optional label for the plop; empty clears it"`
|
||||
RadiusCM *float64 `json:"radiusCm" description:"optional new radius in cm"`
|
||||
SeedLotID *int64 `json:"seedLotId" description:"optional seed lot (from list_seed_lots) to attribute it to"`
|
||||
ClearLot bool `json:"clearSeedLot" description:"optional: detach it from its seed lot"`
|
||||
PlantingID int64 `json:"plantingId" description:"plop to correct (its id from describe_garden or list_plantings)"`
|
||||
Version int64 `json:"version" description:"the plop's current version"`
|
||||
PlantedAt *string `json:"plantedAt" description:"optional corrected planting date, YYYY-MM-DD"`
|
||||
Count *int `json:"count" description:"optional explicit plant count for the plop"`
|
||||
ClearCount bool `json:"clearCount" description:"optional: drop an explicit count and derive it from area and spacing again"`
|
||||
Label *string `json:"label" description:"optional label for the plop; empty clears it"`
|
||||
RadiusCM *float64 `json:"radiusCm" description:"optional new radius in cm"`
|
||||
SeedLotID *int64 `json:"seedLotId" description:"optional seed lot (from list_seed_lots) to attribute it to"`
|
||||
ClearSeedLot bool `json:"clearSeedLot" description:"optional: detach it from its seed lot"`
|
||||
}) (any, error) {
|
||||
patch := service.PlantingPatch{RadiusCM: args.RadiusCM}
|
||||
if args.PlantedAt != nil {
|
||||
if _, err := parseDay(*args.PlantedAt); err != nil {
|
||||
on, err := parseDay(*args.PlantedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patch.SetPlantedAt, patch.PlantedAt = true, args.PlantedAt
|
||||
patch.SetPlantedAt, patch.PlantedAt = true, &on
|
||||
}
|
||||
switch {
|
||||
case args.ClearCount && args.Count != nil:
|
||||
@@ -509,16 +539,18 @@ func (a *adapter) updatePlanting(ctx context.Context, args struct {
|
||||
case args.Count != nil:
|
||||
patch.SetCount, patch.Count = true, args.Count
|
||||
}
|
||||
// SetLabel with a nil Label clears it: an empty string from the model means
|
||||
// "no label", not a label that happens to be empty.
|
||||
if args.Label != nil {
|
||||
patch.SetLabel = true
|
||||
if strings.TrimSpace(*args.Label) != "" {
|
||||
patch.Label = args.Label
|
||||
if l := strings.TrimSpace(*args.Label); l != "" {
|
||||
patch.Label = &l
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case args.ClearLot && args.SeedLotID != nil:
|
||||
case args.ClearSeedLot && args.SeedLotID != nil:
|
||||
return nil, fmt.Errorf("%w: give a seedLotId or clearSeedLot, not both", domain.ErrInvalidInput)
|
||||
case args.ClearLot:
|
||||
case args.ClearSeedLot:
|
||||
patch.SetSeedLotID = true
|
||||
case args.SeedLotID != nil:
|
||||
patch.SetSeedLotID, patch.SeedLotID = true, args.SeedLotID
|
||||
@@ -567,13 +599,16 @@ func (a *adapter) updateJournalEntry(ctx context.Context, args struct {
|
||||
if args.Body == nil && args.ObservedAt == nil {
|
||||
return nil, fmt.Errorf("%w: say what to change — the body, the date, or both", domain.ErrInvalidInput)
|
||||
}
|
||||
if args.ObservedAt != nil {
|
||||
if _, err := parseDay(*args.ObservedAt); err != nil {
|
||||
observed := args.ObservedAt
|
||||
if observed != nil {
|
||||
on, err := parseDay(*observed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
observed = &on
|
||||
}
|
||||
return a.svc.UpdateJournalEntry(ctx, a.actor, args.EntryID,
|
||||
service.JournalPatch{Body: args.Body, ObservedAt: args.ObservedAt}, args.Version)
|
||||
service.JournalPatch{Body: args.Body, ObservedAt: observed}, args.Version)
|
||||
}
|
||||
|
||||
func (a *adapter) deleteJournalEntry(ctx context.Context, args struct {
|
||||
@@ -714,7 +749,11 @@ func (a *adapter) removePlanting(ctx context.Context, args struct {
|
||||
// Soft-remove via the service, dated the day the gardener said, else their
|
||||
// local day like every other tool here (the service clock's UTC day when
|
||||
// that isn't known).
|
||||
return a.svc.RemovePlanting(ctx, a.actor, args.PlantingID, args.Version, a.day(args.RemovedAt))
|
||||
on, err := a.day(args.RemovedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.svc.RemovePlanting(ctx, a.actor, args.PlantingID, args.Version, on)
|
||||
}
|
||||
|
||||
func (a *adapter) listSeedLots(ctx context.Context, args struct {
|
||||
|
||||
Reference in New Issue
Block a user