Skip to content

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…") — the commit-msg hook 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:tokenscheck:identity. Two structural contracts gate the build:
    • Theme tokens (scripts/check-theme-tokens.mjs) — fails if src/ 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 under src/ constructs a controlled-vocabulary identity from a label: composing an attributeId:value ref, deriving an id, recomputing the frozen tier_ladder.tier, or reading the dropped attribute_enum_values.value. Resolve an incoming string through the alias tables (resolveEnumValueId / enumIdsByLabel) instead.
  • npm test = vitest, all unit tests. Visual specs: npm run test:visual (Playwright).
  • npm run build runs gen first (regenerates worker-configuration.d.ts); after editing wrangler.jsonc, re-run npm run gen.
  • Deploys go through npm run deploy — the gate (deploy:gate) is a hard stop, never bypass it. Promotion ceiling is external: 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, then db:apply:local / db:apply:remote).
  • Seeding is npm-script-driven, not per-file: npm run seed:local (build-seed → one seed/seed.sql → catalog fragments → media objects). Both seed:local and seed:remote apply seed/catalog-fragments.txt through 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 whole seed/sql/ directory alphabetically (the dev-lifecycle fragments are not in the catalog set). dev-up's own seed step is deliberately a no-op (see dev.json).
  • Local dev DB wedged → npm run db:reset:local (destroys local state only).

Admin & auth

  • /admin is 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.dev host 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 is src/lib/deeplink.ts.
  • Dev server runs on port 6650 (dev-up / npm run dev -- --port 6650); health endpoint at /health.