Agent: catalog and garden tools, and a ready date on every describe group
- update_seed_lot / delete_seed_lot: correct or drop a recorded purchase
("it was three packets, not two"); the plant a lot is for stays fixed.
- delete_plant: remove a duplicate from the user's catalog. The service
already refuses while plantings (past seasons included) or a lot reference
it; the tool turns that sentinel into words the model can pass on, and
tells it not to clear those references to get its way.
- create_garden: a new place, with the service's defaults; the prompt says a
plan is still a copy_garden.
- describe_garden groups carry readyAround — planting date plus days to
maturity for the plops still in the ground — so "what can I pick this
week?" is a lookup rather than arithmetic the model got wrong live.
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"log/slog"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.stevedudenhoeffer.com/steve/pansy/internal/domain"
|
||||
)
|
||||
@@ -614,6 +615,11 @@ type DescribeGroup struct {
|
||||
// DaysToMaturity is the plant's, when the catalog knows it — with PlantedAt,
|
||||
// enough to say when the harvest is due.
|
||||
DaysToMaturity *int `json:"daysToMaturity,omitempty"`
|
||||
// ReadyAround is that arithmetic done: planting date plus days to maturity
|
||||
// for the plops still in the ground, as one date or "first…last". Absent
|
||||
// when the catalog has no days for the plant or nothing is dated. The
|
||||
// model was asked "what can I pick this week?" and got the sums wrong.
|
||||
ReadyAround string `json:"readyAround,omitempty"`
|
||||
// Removed counts the plops in the group that have been pulled, and RemovedAt
|
||||
// is when ("first…last" when they differ). Only a season view lists pulled
|
||||
// plops, so both are absent from a describe of what is growing now.
|
||||
@@ -746,6 +752,15 @@ func describeGroups(o *domain.GardenObject, plops []domain.Planting, plantByID m
|
||||
DaysToMaturity: plant.DaysToMaturity,
|
||||
RemovedAt: dateRangeOf(members, func(pl domain.Planting) *string { return pl.RemovedAt }),
|
||||
}
|
||||
if plant.DaysToMaturity != nil {
|
||||
days := *plant.DaysToMaturity
|
||||
g.ReadyAround = dateRangeOf(members, func(pl domain.Planting) *string {
|
||||
if pl.RemovedAt != nil {
|
||||
return nil // pulled already; its harvest is not ahead of us
|
||||
}
|
||||
return readyDate(pl.PlantedAt, days)
|
||||
})
|
||||
}
|
||||
for _, pl := range members {
|
||||
g.Plants += effectiveCount(pl)
|
||||
if pl.RemovedAt != nil {
|
||||
@@ -778,6 +793,20 @@ func describePlanting(pl domain.Planting, plantName string) DescribePlanting {
|
||||
return d
|
||||
}
|
||||
|
||||
// readyDate is plantedAt plus days to maturity, or nil when the plop is undated
|
||||
// (or its date is not one the store should have accepted).
|
||||
func readyDate(plantedAt *string, days int) *string {
|
||||
if plantedAt == nil || *plantedAt == "" {
|
||||
return nil
|
||||
}
|
||||
t, err := time.Parse(dateLayout, *plantedAt)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
d := t.AddDate(0, 0, days).Format(dateLayout)
|
||||
return &d
|
||||
}
|
||||
|
||||
// effectiveCount is the plant count a plop stands for: its explicit count, else
|
||||
// the one derived from its area and the plant's spacing.
|
||||
func effectiveCount(pl domain.Planting) int {
|
||||
|
||||
@@ -920,3 +920,67 @@ func TestDescribeGardenByYear(t *testing.T) {
|
||||
t.Errorf("describe(20026) err = %v, want ErrInvalidInput", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDescribeGroupSaysWhenReady — "what can I pick this week?" is a lookup
|
||||
// when the group carries the date, and a sum the model gets wrong when it
|
||||
// doesn't. Planting date plus days to maturity, for the plops still in the
|
||||
// ground; nothing for a plant the catalog has no days for.
|
||||
func TestDescribeGroupSaysWhenReady(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newTestService(t, openConfig())
|
||||
owner := seedUser(t, s, "[email protected]")
|
||||
g, err := s.CreateGarden(ctx, owner, GardenInput{Name: "Harvest", WidthCM: 2000, HeightCM: 2000})
|
||||
if err != nil {
|
||||
t.Fatalf("garden: %v", err)
|
||||
}
|
||||
bed := seedFillBed(t, s, owner, g.ID, 400, 400)
|
||||
sixty := 60
|
||||
radish, err := s.CreatePlant(ctx, owner, PlantInput{Name: "Radish", Category: domain.CategoryVegetable, SpacingCM: 5, Color: "#c33", Icon: "🌱", DaysToMaturity: &sixty})
|
||||
if err != nil {
|
||||
t.Fatalf("radish: %v", err)
|
||||
}
|
||||
mint := seedNamedPlant(t, s, owner, "Mint", 30) // no days to maturity
|
||||
|
||||
plant := func(plantID int64, x float64, on string) *domain.Planting {
|
||||
t.Helper()
|
||||
pl, err := s.CreatePlanting(ctx, owner, bed.ID, PlantingInput{PlantID: plantID, XCM: x, YCM: 0, RadiusCM: 10, PlantedAt: &on})
|
||||
if err != nil {
|
||||
t.Fatalf("plant: %v", err)
|
||||
}
|
||||
return pl
|
||||
}
|
||||
plant(radish.ID, -100, "2026-05-01")
|
||||
plant(radish.ID, 0, "2026-05-11")
|
||||
pulled := plant(radish.ID, 100, "2026-03-01")
|
||||
on := "2026-04-20"
|
||||
if _, err := s.RemovePlanting(ctx, owner, pulled.ID, pulled.Version, &on); err != nil {
|
||||
t.Fatalf("pull: %v", err)
|
||||
}
|
||||
plant(mint.ID, 150, "2026-05-01")
|
||||
|
||||
groups := func(year *int) map[string]DescribeGroup {
|
||||
t.Helper()
|
||||
desc, err := s.DescribeGarden(ctx, owner, g.ID, year)
|
||||
if err != nil {
|
||||
t.Fatalf("describe: %v", err)
|
||||
}
|
||||
out := map[string]DescribeGroup{}
|
||||
for _, gr := range desc.Objects[0].Plantings {
|
||||
out[gr.Plant] = gr
|
||||
}
|
||||
return out
|
||||
}
|
||||
now := groups(nil)
|
||||
if got := now["Radish"].ReadyAround; got != "2026-06-30…2026-07-10" {
|
||||
t.Errorf("radish readyAround = %q, want %q", got, "2026-06-30…2026-07-10")
|
||||
}
|
||||
if got := now["Mint"].ReadyAround; got != "" {
|
||||
t.Errorf("mint has no days to maturity but readyAround = %q", got)
|
||||
}
|
||||
// The season view lists the pulled radish too, but its harvest is behind
|
||||
// us: the range is still the two still growing.
|
||||
y := 2026
|
||||
if got := groups(&y)["Radish"]; got.Removed != 1 || got.ReadyAround != "2026-06-30…2026-07-10" {
|
||||
t.Errorf("2026 radish = removed %d, readyAround %q; want 1 and the live plops' range", got.Removed, got.ReadyAround)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user