Appearance
NanaSelect — project conventions for Claude
NanaSelect is a standalone SvelteKit app on Cloudflare Workers (D1, R2, Vectorize, Workers AI) that guides buyers to the right NanaWall system. It augments — never replaces — the legacy Drupal site at nanawall.com: product data flows in from Drupal dumps/JSON; NanaSelect owns its own product DB.
Operational levers (dev-up, deploy, logs, recovery): docs/runbook.md. Docs are Diátaxis-organized under docs/ and published at docs.nanaselect.iwpi.com.
Ground rules
- Never commit on
main— branch first. A hook blocks direct merges/pushes to main outside the sanctioned flow. - No AI-attribution trailers in commit messages (
Co-Authored-By: Claude, "Generated with…") — thecommit-msghook strips them; don't add them. - Conventional-commit subjects referencing the issue:
feat(#NN): …,fix(#NN): …,docs(#NN): …. - Work is tracked as GitHub issues (BoB conventions); no local PM files.
- Two-tier degradation policy (decision, #1585/#569): public routes degrade, admin fails loud. A buyer-facing surface never shows an error for a recoverable gap — it falls back (deterministic retrieval, advisor hand-off, row silently absent, fail-open gates) because a degraded answer beats a broken funnel. Admin/operator surfaces do the opposite — throw/fail(4xx/503) visibly — because a silent admin fallback hides real breakage from the one audience that can fix it. Follow the tier when adding failure handling.
Build, test, verify
npm run check= wrangler types gen → svelte-check (0 errors expected) →check:tokens→check:identity. Two structural contracts gate the build:- Theme tokens (
scripts/check-theme-tokens.mjs) — fails ifsrc/uses colors outside the ~70 contract tokens or an AA-failing pair. Style with the tokens, don't hardcode. - Vocabulary identity (
scripts/check-vocabulary-identity.mjs, #1851) — fails if any code undersrc/constructs a controlled-vocabulary identity from a label: composing anattributeId:valueref, deriving an id, recomputing the frozentier_ladder.tier, or reading the droppedattribute_enum_values.value. Resolve an incoming string through the alias tables (resolveEnumValueId/enumIdsByLabel) instead.
- Theme tokens (
npm test= vitest, all unit tests. Visual specs:npm run test:visual(Playwright).npm run buildrunsgenfirst (regeneratesworker-configuration.d.ts); after editingwrangler.jsonc, re-runnpm run gen.- Deploys go through
npm run deploy— the gate (deploy:gate) is a hard stop, never bypass it. Promotion ceiling isexternal: merges land on main, deployment is a separate, deliberate step.
Database & seeding
- Schema = Drizzle; migrations live in
drizzle/and are append-only (npm run db:generate, thendb:apply:local/db:apply:remote). - Seeding is npm-script-driven, not per-file:
npm run seed:local(build-seed → oneseed/seed.sql→ catalog fragments → media objects). Bothseed:localandseed:remoteapplyseed/catalog-fragments.txtthrough the same runner (scripts/seed-fragments.mjs) — keep it that way, the two drifting apart was bug #1885. Fragment numbers encode FK order: a fragment must sort after anything it references, and never run the wholeseed/sql/directory alphabetically (the dev-lifecycle fragments are not in the catalog set). dev-up's own seed step is deliberately a no-op (seedev.json). - Local dev DB wedged →
npm run db:reset:local(destroys local state only).
Admin & auth
/adminis gated by Cloudflare Access (edge JWT) + a D1 roster (admin_users) — a two-part grant; capability checks (edit_data/edit_rules/view_audit/manage_users), never role-name checks. See docs/how-to/admin-access.md.- Local dev auto-signs-in as
admin@test.local(dev bypass — compile-time impossible in the built Worker). Other roles via.dev.vars(ADMIN_DEV_EMAIL). - Admin UI uses the shared kit at
src/lib/admin/ui/(Button, Card, Badge, …,admin-ui.css) — extend the kit rather than writing bespoke per-page components/CSS.
Intelligence features (RAG / media tags / similarity)
All ship behind flags that are OFF by default (src/lib/server/intelligence-flags.ts). A flag flips on only after its eval reports GO and Paul approves — never flip one as a side effect. Rollback is one flag + redeploy: docs/how-to/deploy-and-activate-intelligence.md.
Hosting facts that surprise people
- Prod-ish host: nanaselect.iwpi.com (custom domain, whole site behind one Access app — one app, one AUD). The
*.workers.devhost is disabled on purpose (Access can't gate workers.dev) — don't re-enable it. - Deep links to nanawall.com derive from the system id (slug = id), except PDP paths (Drupal
field_url) — the seam issrc/lib/deeplink.ts. - Dev server runs on port 6650 (
dev-up/npm run dev -- --port 6650); health endpoint at/health.