Address #127 review: quote the garden name, validate rectangles, require plantId
Build image / build-and-push (push) Successful in 17s

- The plan-name line of the system prompt interpolates the garden's name
  with %q like the rest of the prompt: any editor can rename a garden, and a
  name with a newline in it must not read as an instruction.
- fill_region refuses an inverted rectangle with its corners named, and a
  rectangle that misses the bed (or only touches its edge) is an error from
  the service rather than a successful fill of nothing.
- remove_plantings requires plantId; omitted it would remove plant 0 and
  report success.
- historyEntry.Undo → UndoOf (it holds the reverted change set's id).
- remove_planting's description names list_plantings as an id source.
- RemovePlanting takes the removal date itself; the dateless wrapper had no
  callers left.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-08-23 00:27:48 -04:00
co-authored by Claude Fable 5
parent a1baf4b871
commit d3d7238259
6 changed files with 53 additions and 21 deletions
+8
View File
@@ -276,6 +276,14 @@ func (s *Service) fillLoaded(ctx context.Context, actorID int64, o *domain.Garde
}
region = region.clampTo(o.WidthCM/2, o.HeightCM/2)
if region.MaxX <= region.MinX || region.MaxY <= region.MinY {
// An explicit rectangle that misses the object, or only touches its edge.
// Planting nothing and reporting success would read as "done" to a caller
// that aimed at the wrong coordinates (typically the agent mixing up the
// garden frame and the object's local one) — and a rectangle clamped to a
// line would get hexCenters' one-plop-in-the-middle rule, on the edge.
return nil, fmt.Errorf("%w: the region lies outside the object", domain.ErrInvalidInput)
}
centers, total := hexCenters(region, radius, edgeInset(radius, spacing, layout), maxFillPlops)
if total > maxFillPlops {
return nil, domain.ErrInvalidInput // region too large for this spacing; ask for less
+11
View File
@@ -797,4 +797,15 @@ func TestFillByRectangleAttributesSeed(t *testing.T) {
t.Errorf("empty rectangle %+v: err = %v, want ErrInvalidInput", r, err)
}
}
// A rectangle that misses the bed (it is 240 wide, so ±120) — or only
// touches its edge — is an error, not a successful fill of nothing.
for _, r := range []Region{{MinX: 200, MinY: -10, MaxX: 300, MaxY: 10}, {MinX: 120, MinY: -10, MaxX: 200, MaxY: 10}} {
if _, err := s.Fill(ctx, owner, bed.ID, FillSpec{Region: r, PlantID: garlic.ID}); !errors.Is(err, domain.ErrInvalidInput) {
t.Errorf("off-bed rectangle %+v: err = %v, want ErrInvalidInput", r, err)
}
}
// Partly outside is fine: the part inside gets planted.
if created, err := s.Fill(ctx, owner, bed.ID, FillSpec{Region: Region{MinX: 80, MinY: -10, MaxX: 300, MaxY: 10}, PlantID: garlic.ID}); err != nil || len(created) == 0 {
t.Errorf("overhanging rectangle: %d plops, %v; want some", len(created), err)
}
}
+6 -12
View File
@@ -186,18 +186,12 @@ func (s *Service) UpdatePlanting(ctx context.Context, actorID, plantingID int64,
}
// RemovePlanting soft-removes a single plop — the one-plop counterpart to
// ClearObject. It stamps removed_at from the service clock (s.now()), the same
// UTC day ClearObject and the fill path default to; RemovePlantingOn is the
// form for a caller that knows the gardener's local day. Both delegate to
// UpdatePlanting for the editor-role check, version guard and history record.
func (s *Service) RemovePlanting(ctx context.Context, actorID, plantingID, version int64) (*domain.Planting, error) {
return s.RemovePlantingOn(ctx, actorID, plantingID, version, nil)
}
// RemovePlantingOn is RemovePlanting with the removal date supplied (YYYY-MM-DD)
// by a caller that knows the person's local day. nil keeps the service clock's
// UTC today.
func (s *Service) RemovePlantingOn(ctx context.Context, actorID, plantingID, version int64, removedAt *string) (*domain.Planting, error) {
// ClearObject, used by the agent's remove_planting tool. removedAt (YYYY-MM-DD)
// is the day the caller knows it happened — the gardener's local day; nil
// stamps the service clock's UTC today, the same default ClearObject and the
// fill path use. Delegates to UpdatePlanting for the editor-role check, version
// guard and history record.
func (s *Service) RemovePlanting(ctx context.Context, actorID, plantingID, version int64, removedAt *string) (*domain.Planting, error) {
if !validDatePtr(removedAt) {
return nil, fmt.Errorf("%w: removedAt must be a YYYY-MM-DD date", domain.ErrInvalidInput)
}