Files
pansy/CLAUDE.md
T
steveandClaude Opus 4.8 1716362981
Build image / build-and-push (push) Canceled after 11s
Add CLAUDE.md and a prominent vibe-coded disclosure in the README
pansy is written by an LLM with a human directing. That belongs near the top of
the README, not in a footnote — someone deciding whether to self-host this and
point it at their own data should know before they decide, not after. The
disclosure says what it actually means in practice (less scrutiny than a
hand-written project, read the code, back up the database) rather than a vague
"AI-assisted" hedge.

CLAUDE.md collects the operational details that otherwise get rediscovered every
session: GOWORK=off, the store/service/api seam and why rules go in the middle
layer, the two unit scales, version-guarded writes, ErrNotFound-as-existence-mask,
the branch/Gadfly/merge/deploy loop, and the env var names that aren't guessable.
It also makes the disclosure a standing rule: if the README is ever rewritten,
that section survives the rewrite, and the only acceptable edit is a clearer one.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
2026-07-21 00:50:37 -04:00

4.5 KiB

CLAUDE.md

Working notes for Claude Code on pansy. DESIGN.md is the architecture document and stays authoritative; this file is the operational stuff you'd otherwise rediscover every session.

This is a vibe-coded project — say so

pansy is written by an LLM, with a human directing. That is not a footnote, it is a property of the software people should know before they trust it with their garden plans.

Rules, not preferences:

  • The README carries a prominent, near-the-top disclosure. It does not get moved below the fold, softened into "AI-assisted", or quietly dropped in a rewrite.
  • If you rewrite the README, the disclosure survives the rewrite.
  • Anywhere else the project introduces itself (a landing page, a docs site, a package description), it says the same thing.

If you find yourself editing that section, the only acceptable direction is clearer and more honest, never quieter.

Build and test

pansy is a standalone Go module inside a parent workspace, so GOWORK=off is required or the build picks up sibling modules:

GOWORK=off go build ./...
GOWORK=off go test ./...
cd web && npx tsc --noEmit && npx vitest run && npm run build
make test          # both halves
make build         # web bundle → embed → CGO_ENABLED=0 static binary

gofmt -l internal/ before committing. Note internal/service/plants_test.go is already unformatted on main — leave it alone unless you're touching it, so the diff stays about your change.

Architecture in one paragraph

internal/store (hand-written SQL, modernc.org/sqlite, pure Go) → internal/service (the seam: every permission check and invariant) → internal/api (thin gin handlers: decode, call service, encode). Agent tools in internal/agent are equally thin adapters over the same service methods, so they inherit permission enforcement for free. If you are about to put a rule in a handler, put it in the service instead.

Frontend: React 19 + Vite + Tailwind 4 + TanStack Router/Query, built into internal/webdist/dist and embedded with embed.FS.

Conventions that bite if you miss them

  • Everything is centimeters, stored as SQLite REAL. Imperial is a display and entry concern only, in web/src/lib/units.ts. Two distinct scales live there: dimension (m/ft — gardens, objects, the garden grid) and spacing (cm/in — plant spacing, bed grid). Mixing them is what #47 was about.
  • Optimistic concurrency everywhere. Every mutable row has version; PATCH and DELETE carry it; the store returns (current row, ErrVersionConflict) on mismatch and the API answers 409 with the row under "current".
  • No-access is ErrNotFound, not ErrForbidden. Existence is masked deliberately. ErrForbidden means "you can see it but may not do that".
  • Plops (plantings) live in their parent object's local frame, origin at the object's center, -y is north. Moving or rotating a bed moves its plants free.
  • Soft removal: "clear bed" sets removed_at; the editor reads removed_at IS NULL. Hard delete is a different operation.
  • Migrations are numbered .sql files in internal/store/migrations/, run at startup, embedded. Never edit one that has shipped.
  • Every service mutation lands in history (#48). If you add one, record it — see internal/service/revisions.go. Multi-row operations pass all their changes to a single record call so they undo as one unit.

Workflow

Branch → PR → Gadfly reviews it automatically → consider every finding and fix what's real → merge when the pipeline is green. Do not grade Gadfly findings. A push to main builds the image and deploys to Komodo; the live instance at pansy.orgrimmar.dudenhoeffer.casa updates a few minutes later.

Workflow- and config-only changes (CI, this file, docs) go straight to main without the PR dance.

Planning happens in Gitea issues first: standalone issues under a tracking epic, implemented in later per-issue sessions.

Environment

PANSY_PORT, PANSY_DB, PANSY_BASE_URL, PANSY_REGISTRATION, PANSY_LOCAL_AUTH, PANSY_OIDC_* — note it's PANSY_DB and PANSY_PORT, not the _PATH/_ADDR names you might guess. Authentik is the primary IdP; OIDC-first with local passwords as fallback.

Agent config (v2): OLLAMA_CLOUD_API_KEY and PANSY_AGENT_MODEL (default ollama-cloud/glm-5.2:cloud), set in Komodo. Model strings pass verbatim to majordomo.Parse, so a comma-separated spec gives failover for free. majordomo and executus are sibling repos at ../.