- 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