← back to Wallco Ai
CLAUDE.md
105 lines
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
`wallco.ai` — customer-facing storefront for curated-original wallpaper + murals. Every pattern is AI-generated, never repeated. Live at https://wallco.ai (Kamatera, pm2 process `wallco-ai-9878`, port 9905 → behind nginx).
Single-process Express app: `server.js` (~42k lines including the giant in-process route table). State lives in PostgreSQL (`dw_unified` database, `spoon_all_designs` table) + a static snapshot at `data/designs.json` that the server caches in memory on boot.
## Common commands
```bash
npm start # boots server.js on PORT=9905
node -c server.js # syntax check before edits — server.js is 42k lines and a typo costs a deploy
./deploy-kamatera.sh # canonical deploy — DO NOT do surgical rsync (see "Deploy" below)
# Generation pipeline
node scripts/generate_designs.js # SDXL via Replicate or ComfyUI per GEN_BACKEND env
node scripts/fix-design.js <id> # opacity-snap + tile-edge heal on an existing design (no AI cost)
python scripts/fix-seam.py <id> # PIL shift-and-heal for mid-seam defects
python scripts/edges-scan.py # 6-lens seamless-tile defect scanner
# Ghost-layer pipeline
node scripts/audit-ghost-sample.js # Gemini vision pass — flags multi-opacity defects
node scripts/triage-ghost-flagged.js # interactive triage of ghost-flagged rows
# Database snapshot ↔ static JSON
node scripts/sync-designs-json-product-line.js # PG → data/designs.json (the server reads from this on boot)
curl -X POST http://127.0.0.1:9905/admin/reload-designs # hot-reload data/designs.json (localhost-gated, no auth needed)
```
`npm test` is a placeholder (no test suite). Validation happens via the publish-gate stack (settlement + edges + ghost + seamless detectors) before `is_published=true`.
## Architecture in one paragraph
`server.js` is the entire web layer — every route, all admin gates, all in-process caches — read straight from `data/designs.json` at boot. `lib/` is the detection toolkit (ghost / seamless / bad-aesthetic / composition / subject / color / repeat-prompt). `scripts/` (183 files) holds every generator, every fixer, every one-off pipeline — generators emit PNGs into `data/generated/`, write rows to PG `spoon_all_designs`, sync to `data/designs.json` only when explicitly invoked. `src/` holds modular admin surfaces mounted into the server (`admin-gate.js`, `library.js`, `marketplace/`, `review.js`, etc.). The default backend is `GEN_BACKEND=comfy` pointing at `COMFY_URL=http://100.94.103.98:8188` (Mac1 ComfyUI). `GEN_BACKEND=replicate` falls back to Replicate API.
## Deploy — the only rule
**Always use `./deploy-kamatera.sh`. Never surgical rsync+pm2-reload.** The script does pre-flight lint, full rsync (with the carefully-tuned exclude list in `.deploy.conf`), pm2 reload, and HTTP smoke test. Surgical deploys skip the incremental rsync of `data/generated/` and locally-generated PNGs never reach prod — the catalog goes blank.
**`data/designs.json` IS shipped by the deploy — it is NOT excluded.** It's the Mac2-built catalog artifact: the publish flow is (1) flip `is_published` in Mac2's `dw_unified.spoon_all_designs`, (2) `python3 scripts/refresh_designs_snapshot.py` rebuilds `data/designs.json` on Mac2, (3) `./deploy-kamatera.sh` rsyncs it to prod + pm2 reload. Prod's *own* `dw_unified` is near-empty (~350 rows) and is NOT the catalog source. The real hazard is the reverse direction: a **prod-side** process regenerating `designs.json` against the sparse prod DB clobbers the live catalog (the 2026-05-26 "157 designs" incident) — `refresh_designs_snapshot.py` has a catastrophe guard (refuse a >50% shrink) for exactly that. See `reference`/memory for the full flow.
Critical excludes (already in `.deploy.conf`) — these files ARE genuinely PROD-AUTHORED, never push local copies over them:
- `data/generated/`, `data/images/`, `data/rooms/` — generators run on prod and write here
- `data/marketplace/events.jsonl` — live event log
- `data/ghost-scan-*.jsonl`, `data/ghost-purge.jsonl`, `data/ghost-labels.jsonl`, `data/bad-aesthetic-patterns.jsonl` — prod scanner writes these; the 2026-05-24 incident wiped fresh prod flags by rsyncing stale local copies
## Hard rules baked into this project
- **Round-1 outputs are sacred.** Generators emit one shot per prompt. Don't retry-until-clean — feed defects through `scripts/fix-design.js` / `fix-seam.py` / `fix-frame-overlay.js` to preserve composition. Re-rolling discards the artist intent. See `~/.claude/skills/fix-design/` for the fixer playbook.
- **Solid screen-print colors only.** No gradients, no transparency, no ghost layers unless the prompt explicitly asks for them. Anti-prompt enforcement in `scripts/generate_designs.js` plus a Gemini vision pre-publish gate.
- **Always paint on a REAL natural wallcovering texture.** When generating a pattern, the ground MUST be anchored to an actual DW natural-material SKU — **grasscloth, cork, raffia, mica, real linen, silk, sisal, abaca, seagrass, jute, hemp, arrowroot, paperweave, bamboo, reed** — never a flat/hallucinated "ivory paper" ground. The motif is painted *on top of the real textile* so the weave/slubs/flecks show through. Mechanism: `scripts/gen-luxe.js` `pickTextureBrief()` pulls a random real SKU from `shopify_products` (matched by material regex) and injects `Ground: <family> <material> (matched DW SKU <name>, dominant #hex)` into the prompt; the snapshot namer then reads that ground hex for the colorway (`refresh_designs_snapshot.py` `ground_hex()`). DW has real SKUs for all of these (mica 292, cork 384, raffia 381, grasscloth 1978, linen 1857, silk 1478, +2862 other-natural). If you add a new generator, copy this real-texture-anchor pattern — a flat paper ground is a regression.
- **Settlement gate before publish.** `~/.claude/skills/settlement/` orchestrates 5 detectors (a1 directional leaves, a2 open space, a3 multiple colors, b prohibited elements, acceptable elements) + verdict combinator. Never set `is_published=true` without the gate returning `OK`.
- **TIFs are lazily-built print masters — NOT a publish prerequisite.** A `tif_path` (150 dpi, up to 36000 × 19800 px, ~2 GB) is the full-size archive for *physical-print* fulfillment. **Publish = catalog-visible; it does NOT require a TIF.** TIF generation is opt-in/deferred because each file is ~2 GB and disk runs hot (`scripts/export-etsy-digital.js`: "NO TIF gen by default"), so a large share of the live catalog is published without one. The digital license download (`/api/design/:id/download`) serves an on-demand Real-ESRGAN upscale, **not** the TIF — so a NULL `tif_path` does not block purchase. Any hard "must-have-TIF" requirement belongs at *physical-print fulfillment*, not at catalog publish. The no-TIF backlog is browsable at `/admin/needs-tif`. Full analysis + the rejected hard-gate options: `docs/tif-publish-gate-review.md`. (Historical note: earlier docs/comments claimed "the publish gate refuses NULL `tif_path`" — that gate was never implemented, contradicted ~73% of the live catalog, and is retired. DTD verdict 2026-06-02.)
- **No vendor names in public UI.** 30-vendor denylist enforced in PG bulk rename + server.js dropdown filter. `wallco.ai/library` is admin-only.
- **No "FLIEPAPER" word in any customer-facing surface.** Internal scaffold names (`src/fliepaper-bugs.js`) are exempt; rendered URLs / image filenames / category slugs go through redaction.
- **`/admin` routes are admin-gated.** `src/admin-gate.js` enforces basic-auth on every `/admin/*` and `/api/admin/*` route. Don't add a new admin route without the gate.
- **`data/designs.json` MUST NOT include `local_path`.** That column leaks `/Users/stevestudio2/…` via the public `/api/designs` endpoint. Use `localPathForDesign(d)` in server.js (filename-only basename) instead. See commit `6dbeb3b` reverting `a1dfbdd`.
- **Mural categories are NEVER tileable.** Rows in `monterey-mural`, `cactus-pine-scenic`, `cactus-11ft-mural`, `cactus-accent-mural`, `tree-mural`, `mural-scenic` must have `kind='mural_panel'` (or `'mural'` for cactus-11ft-mural). Never `seamless_tile`. A misclassified row renders as a tiny repeating tile preview on /designs grid + PDP and looks broken. Canonical list + `assertMuralKind(kind, category)` helper live in `lib/mural-categories.js`. 2026-05-27 audit fixed 12 misclassified rows (10 monterey-mural + 2 cactus-pine-scenic).
## Generation backend
```
GEN_BACKEND=comfy # default — Mac1 ComfyUI
COMFY_URL=http://100.94.103.98:8188
COMFY_MODEL=sd_xl_base_1.0.safetensors
COMFY_WIDTH=1024
COMFY_HEIGHT=1024
SEAMLESS_TOLERANCE=12 # edges-scan ΔE threshold
```
For batch jobs: `scripts/refiner_compare.js` supports `CMP_SHARD` / `CMP_SHARDS` to fan generations across Mac1+Mac2 simultaneously. Mac2 has its own ComfyUI at `127.0.0.1:8188` once started (`cd ~/Projects/ComfyUI && ./venv/bin/python main.py --listen 127.0.0.1 --port 8188`).
## Production secrets guard
`server.js:resolveSecret()` refuses to boot in `NODE_ENV=production` if any of these strings appear in env:
- `wallco-dev-secret-change-for-prod`
- `wallco-trade-dev-secret-CHANGE-IN-PROD`
- `wallco-marketplace-dev-secret-rotate-me`
Rotate via secrets-manager (`~/Projects/secrets-manager/cli.js`) which writes to `/root/public-projects/wallco-ai/.env` on Kamatera.
## Common gotchas
- **Backticks in JS comments inside a template literal silently close it.** server.js is heavy on `res.send(\`<html>...\`)` blocks; a comment with inline-code backticks 60 lines upstream once closed the outer literal and pointed the SyntaxError at unrelated downstream code (321 pm2 restarts before catching). Always run `node -c server.js` before deploying.
- **Grid-card wrapper must be `<div>`, not `<a>`.** A `<button>` inside an `<a>` triggers Chrome's Adoption Agency Algorithm which splits one card into three sibling grid items. Canonical shape: `<div class="design-card">` wrapping inner `<a class="card-link">` with badges as siblings of the link.
- **Auto-crop top 8-12% of any user-facing image.** Hero/product/room/mockup/mailer/IG/reels — every customer-facing surface uses `object-fit:cover; object-position:center 12%` to hide watermarks/headers/banners.
- **PG schema drift between local and prod.** Prod is the source of truth (Kamatera dw_admin user owns the table). Local migrations are advisory until applied on prod via `sudo -u postgres psql -c "ALTER TABLE ..."` followed by a pm2 restart to refresh the in-memory `DESIGNS` cache.
## TODO + backlog
- `TODO.md` — open work for next session, surfaced to CNCP at `http://localhost:3333` via the cncp `/api/tasks` reader
- `YOLO_BACKLOG.md` — Yuri-runnable refinement tasks (user-facing only; security backlog parked separately)
## Skills that own pieces of this codebase
- `~/.claude/skills/fix-design/` — clean defects without regenerating
- `~/.claude/skills/edges-agent/` — 6-lens seamless-tile scanner + verdict
- `~/.claude/skills/elements/` — curated "solid opaque middle" reference library for new generations
- `~/.claude/skills/settlement/` — 5-detector publish gate (orchestrator + 7 sub-skills)
- `~/.claude/skills/luxe-curator/` — 3-variant hot-or-not picker for converting roots into luxury heritage variants (replaces the failed blanket roll-luxe pattern)
- `~/.claude/skills/yolo-runner/` — Yuri queue for autonomous overnight work on this codebase