diff --git a/README.md b/README.md index 4785ae8..3645abf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Plant Laboratory / Botanica -M0 provides a running React shell, Fastify API, PostgreSQL schema and repeatable bootstrap. Genetics and game workflows follow in M1–M4. +M0–M2 provide a running React laboratory, deterministic genetics, illustrated SVG renderer, Fastify API, PostgreSQL schema and repeatable bootstrap. Persistent plant creation and breeding follow in M3. ## Run @@ -34,8 +34,18 @@ docker compose run --rm bootstrap node dist/migrate.js docker compose run --rm bootstrap node dist/seed.js ``` -Migrations are checked-in SQL tracked in `schema_migrations`; seeds never overwrite existing records. `.env` is ignored by Git and Docker build contexts. Production containers run as non-root. The API returns safe uniform errors and health responds 503 if PostgreSQL is unavailable. Relative browser API URLs avoid container hostname leakage. `PUBLIC_BASE_URL` is validated and derived from origin plus port for localhost, or just origin behind a production reverse proxy. +Migrations are checked-in SQL tracked in `schema_migrations`; player seeds preserve existing records; the catalog seed synchronizes the current catalog definition without changing plants or ancestry. `.env` is ignored by Git and Docker build contexts. Production containers run as non-root. The API returns safe uniform errors and health responds 503 if PostgreSQL is unavailable. Relative browser API URLs avoid container hostname leakage. `PUBLIC_BASE_URL` is validated and derived from origin plus port for localhost, or just origin behind a production reverse proxy. -Routes: `/` redirects to `/lab`; `/lab`, `/breed`, `/gallery`, `/about` currently show clearly labeled milestone empty states. Original shell illustration is decorative; the deterministic species renderer is M2. No authentication: this sandbox is intended for local use. +Routes: `/` redirects to `/lab`, which contains curated rendering studies, four stages, cosmetic seed and manual golden-vein preview, derived trait inspection, and SVG/PNG export with warm or transparent background. `/gallery` is a proof atlas of 12 systematic variants plus a 100-seed rendering demo. `/breed` remains a clearly labeled M3 empty state; `/about` explains the fictional genetics model. These studies are not saved plants. No authentication: this sandbox is intended for local use. Read [the implementation plan](docs/IMPLEMENTATION_PLAN.md) and [progress log](docs/PROGRESS.md) for acceptance gates. + + +## Domain and artwork + +Genetics v1 uses fixed-locus diploid genomes, explicit dominance, seeded Mulberry32 inheritance, mutation logs and derived rarity. The catalog and phenotype preview APIs are available under `/api/v1`; see `docs/API_AND_DATA.md` for request examples. Pure packages use built JS exports; root build/typecheck/test/dev scripts first build package dependencies in order. + +Velvetleaf is composed from 15 original handcrafted SVG primitives and six curated palettes. `npm run assets:write` regenerates standalone primitive sources from the typed paths; tests reject source drift. `assets/README.md` documents CC0 artwork provenance. Geometry is deterministic and all exported assets are self-contained. + +For optional browser acceptance QA, install/provide Playwright and Chrome, then run `node scripts/verify-m2.cjs` against the running Compose app. `PLAYWRIGHT_MODULE` may point to an available Playwright package and `BROWSER_EXECUTABLE` to a browser binary. The script checks desktop/mobile overflow, 12/100 rendering studies, namespace collisions, seed invariance, real downloads, PNG alpha/background and pixel equivalence to the live preview. Screenshots and measurements are written to `artifacts/m2`. + diff --git a/apps/server/package.json b/apps/server/package.json index fb4dd30..133a613 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -16,7 +16,8 @@ "drizzle-orm": "^0.45.4", "pg": "^8.16.3", "@botanica/genetics": "0.0.0", - "@botanica/shared": "0.0.0" + "@botanica/shared": "0.0.0", + "@botanica/renderer": "0.0.0" }, "devDependencies": { "@types/pg": "^8.15.5", diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index a8a6eb1..74bc811 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -1,3 +1,4 @@ +import { registerPreviewRoute } from "./preview-route.js"; import { ZodError } from "zod"; import { GeneticsError } from "@botanica/genetics"; import { registerGeneticsRoutes } from "./genetics-routes.js"; @@ -21,6 +22,7 @@ export function createApp( } }); registerGeneticsRoutes(app); + registerPreviewRoute(app); app.setNotFoundHandler((_request, reply) => reply .code(404) @@ -28,18 +30,16 @@ export function createApp( ); app.setErrorHandler((error, request, reply) => { if (error instanceof ZodError) - return reply - .code(400) - .send({ - error: { - code: "INVALID_REQUEST", - message: "Request validation failed.", - details: error.issues.map((i) => ({ - path: i.path, - message: i.message, - })), - }, - }); + return reply.code(400).send({ + error: { + code: "INVALID_REQUEST", + message: "Request validation failed.", + details: error.issues.map((i) => ({ + path: i.path, + message: i.message, + })), + }, + }); if (error instanceof GeneticsError) return reply .code(error.code === "INCOMPATIBLE_SPECIES" ? 409 : 400) diff --git a/apps/server/src/preview-route.test.ts b/apps/server/src/preview-route.test.ts new file mode 100644 index 0000000..0b34035 --- /dev/null +++ b/apps/server/src/preview-route.test.ts @@ -0,0 +1,41 @@ +import { it, expect } from "vitest"; +import { exampleGenome, resolvePhenotype } from "@botanica/genetics"; +import { layoutPlant, renderSvg } from "@botanica/renderer"; +import { createApp } from "./app.js"; +it("returns the same safe serialized SVG as the client renderer and rejects invalid preview inputs", async () => { + const app = createApp(async () => {}); + try { + const payload = { + speciesId: "velvetleaf", + genome: exampleGenome, + visualSeed: 91, + growthStage: "flowering", + background: "warm", + }; + const result = await app.inject({ + method: "POST", + url: "/api/v1/preview", + payload, + }); + expect(result.statusCode).toBe(200); + expect(result.json().svg).toBe( + renderSvg( + layoutPlant( + "velvetleaf", + resolvePhenotype("velvetleaf", exampleGenome), + 91, + "flowering", + ), + { namespace: "preview", background: "warm" }, + ), + ); + const bad = await app.inject({ + method: "POST", + url: "/api/v1/preview", + payload: { ...payload, visualSeed: "