- UpdatePlanting: only re-check the object-bounds when the position is actually
being moved. A plop orphaned outside its object by a later resize stays
editable/removable instead of becoming a row you can't fix or delete.
- finalizePlanting: reject removed_at before planted_at.
- derivedCount: guard Inf (not just NaN) and cap the result at maxExplicitCount
(1e6) — the same ceiling a manual override honors — so a huge radius / tiny
spacing can't overflow a 32-bit int or return an absurd value.
- Refresh stale docs that referenced #14 as not-yet-landed (FullGarden,
ListActivePlantingsForGarden, ListReferencedPlants) and note the date-only
layout beside timeLayout.
Deliberately did NOT add a plantable re-check to Update/Delete: existing plops
must stay editable/removable even if their object was later marked non-plantable
(same trap as the bounds case).
Tests: derived-count cap, removed-before-planted rejection, and edit/move of a
plop orphaned by an object shrink.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
- domain: Planting gains a computed (non-persisted) DerivedCount field.
- store/plantings.go: Get/Create/Update (version-guarded)/Delete alongside the
existing /full read helper.
- service/plantings.go: place/move/resize/soft-remove a plop; editor role on the
object's garden; object must be plantable; plant_id must be visible to the
actor (built-in or own) else ErrInvalidInput; center must sit within the
object's unrotated local bounds (radius may overhang); planted_at defaults to
today. derivedCount = max(1, round(π·r²/spacing²)) — one unit-tested helper,
reused by /full (via a spacing map, no N+1) and single responses.
- api: POST /objects/:id/plantings, PATCH/DELETE /plantings/:id; nullable
count/label/plantedAt/removedAt use RawMessage so null (clear) is distinct
from absent (unchanged). removedAt is the soft-remove / "clear bed" seam.
- /full now enriches each active plop with its derivedCount.
Service tests: formula edge cases (tiny radius → 1), defaults + derived, count
override, move/resize + clear override, non-plantable rejection, foreign/unknown
plant rejection, bounds, soft-remove leaves /full, version conflict, cross-user
masking, delete. Plus an API-level create/patch/full/delete flow.
GOWORK=off go build/vet/test ./internal/... green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi