From 94a1a0f56562749d6807e8c8991334760049945f Mon Sep 17 00:00:00 2001 From: Ghost69 Date: Tue, 21 Jul 2026 20:14:49 +0100 Subject: [PATCH 1/2] docs: upgrade Tai positioning and fix CI formatting --- README.md | 110 +++++++++++++++--- .../api/src/concierge/concierge.controller.ts | 7 +- apps/api/src/concierge/concierge.service.ts | 5 +- .../app/api/concierge/chat/route.ts | 7 +- apps/storefront/app/page.tsx | 4 +- apps/storefront/app/studio-guide/page.tsx | 7 +- .../concierge/concierge-launcher.tsx | 5 +- .../components/concierge/concierge-panel.tsx | 34 ++---- .../concierge/concierge-provider.tsx | 3 +- .../product/product-configurator.tsx | 6 +- .../components/studio/design-studio.tsx | 3 +- apps/storefront/lib/concierge/intent.ts | 16 ++- .../lib/concierge/knowledge/corpus.ts | 13 +-- apps/storefront/lib/concierge/orchestrator.ts | 46 ++++++-- .../lib/concierge/providers/fallback.ts | 9 +- apps/storefront/lib/concierge/tools/cart.ts | 16 ++- .../storefront/lib/concierge/tools/catalog.ts | 17 +-- apps/storefront/lib/concierge/tools/orders.ts | 35 +++++- .../storefront/lib/concierge/tools/tickets.ts | 14 ++- .../lib/studio-artwork-folders.spec.ts | 11 +- docs/AI_ANALYTICS_DEFINITIONS.md | 38 +++--- docs/AI_CUSTOMER_CARE_AUDIT.md | 96 ++++++++------- docs/AI_CUSTOMER_CARE_SPEC.md | 21 ++-- docs/AI_KNOWLEDGE_SOURCES.md | 26 ++--- docs/AI_TEST_MATRIX.md | 52 ++++----- 25 files changed, 373 insertions(+), 228 deletions(-) diff --git a/README.md b/README.md index 4a6612e..99925c7 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,61 @@ -# Tai Manic Studios +# F.A.T.U / Tai Manic Studios -Art-first commerce platform for publishing original artwork across approved garment configurations. +An art-first commerce platform for publishing original artwork across approved garment configurations, selling finished designs, and operating the workflow through a dedicated administration system. -## Workspace +> **Status:** Active collaborative development. The repository contains a storefront, admin application, modular API, background worker, shared packages, and infrastructure definitions. + +![F.A.T.U storefront at desktop width](./apps/storefront/tests/__screenshots__/visual/showcase.spec.ts/home-desktop-1440.png) + +## Product model -This repository is a pnpm/Turborepo monorepo. The backend is a modular TypeScript system with a NestJS API, a background worker, PostgreSQL/Prisma persistence, Redis/BullMQ jobs, and S3-compatible storage. +Artwork is the primary creative entity. Garments are approved canvases for immutable artwork versions—not the root catalogue entity. + +The platform supports: + +- editorial artwork and garment discovery; +- front/back garment configuration and artwork placement; +- saved designs and stable sharing; +- carts, promotions, checkout, payment-provider abstraction, and order snapshots; +- inventory reservations and append-only stock movement; +- customer identity and account workflows; +- administration, media ingestion, derivative processing, and operational review; +- a customer-facing concierge with configurable provider fallback. + +## Workspace ```text -apps/api Versioned HTTP API and OpenAPI document -apps/worker Background processing entry point -apps/storefront Frontend-owned storefront location -apps/admin Frontend-owned administration location -packages/* Shared contracts and platform packages -infra Local infrastructure -docs Product, backend, contract, and coordination records +apps/api Versioned NestJS API and OpenAPI document +apps/worker BullMQ background-processing entry point +apps/storefront Next.js customer storefront and Design Studio +apps/admin Next.js administration application +packages/* Contracts, database, UI, media, email, and shared tooling +infra Local PostgreSQL, Redis, and object-storage infrastructure +docs Product, testing, contract, coordination, and handoff records ``` -## Prerequisites +OpenAPI is the backend-to-frontend source of truth. Shared contracts, frontend integrations, and backend handlers must stay aligned. + +## Stack + +| Layer | Technology | +| --------- | --------------------------------------------------------------- | +| Workspace | pnpm, Turborepo, TypeScript | +| Frontend | Next.js 16, React 19, Tailwind CSS | +| API | NestJS 11, OpenAPI, class-validator, Helmet | +| Data | PostgreSQL, Prisma | +| Jobs | Redis, BullMQ | +| Media | S3-compatible storage, Sharp | +| Quality | ESLint, Prettier, Vitest, Playwright, Testing Library, axe-core | + +## Local setup + +### Prerequisites - Node.js 22.18 or newer - pnpm 10.20 or newer -- Docker with Compose for local services +- Docker with Compose -## Quick start +### Install and run ```bash cp .env.example .env @@ -33,8 +66,51 @@ pnpm build pnpm dev ``` -The API listens on `http://localhost:4000`, with OpenAPI UI at `/api/docs` and liveness/readiness under `/api/v1/health`. +Default local services: + +- Storefront: `http://localhost:3000` +- Admin: `http://localhost:3001` +- API: `http://localhost:4000` +- OpenAPI: `http://localhost:4000/api/docs` +- Health: `http://localhost:4000/api/v1/health` + +Use [`.env.example`](./.env.example) as the variable-name reference. Never commit real database, Redis, storage, email, payment, or AI-provider credentials. + +## Commands + +| Command | Purpose | +| ------------------- | ---------------------------------------- | +| `pnpm dev` | Start workspace services in parallel | +| `pnpm build` | Build all packages and applications | +| `pnpm format:check` | Verify formatting | +| `pnpm lint` | Run workspace lint tasks | +| `pnpm typecheck` | Run TypeScript checks | +| `pnpm test` | Run unit and integration tests | +| `pnpm db:validate` | Validate the Prisma schema | +| `pnpm check` | Run the complete repository quality gate | + +The storefront also exposes its Playwright suite through the package-specific scripts documented in [`apps/storefront/README.md`](./apps/storefront/README.md). + +## Documentation map + +- [`docs/backend/TESTING.md`](./docs/backend/TESTING.md) — backend verification +- [`docs/backend/SECURITY.md`](./docs/backend/SECURITY.md) — security model and expectations +- [`docs/frontend/README.md`](./docs/frontend/README.md) — frontend guidance +- [`docs/coordination/`](./docs/coordination/) — cross-boundary contract notes +- [`docs/handoffs/`](./docs/handoffs/) — implementation handoffs +- [`AGENTS.md`](./AGENTS.md) — repository ownership and completion rules + +## Security and operations + +- Keep price, availability, inventory, and order state server-authoritative. +- Validate media type, size, and ownership before ingestion; store derivatives under controlled keys. +- Protect admin routes with explicit role checks and audited mutations. +- Treat payment callbacks as untrusted until signature and idempotency checks pass. +- Do not log credentials, payment payloads, private customer data, or raw provider responses. +- Apply reviewed migrations and validate backup/restore behavior before production rollout. + +## Contribution context and licence -## Quality checks +This is a collaborative repository. Application areas have explicit ownership recorded in [`AGENTS.md`](./AGENTS.md); use commit and pull-request history when describing individual contributions. -Run `pnpm check` before opening a pull request. See `docs/backend/TESTING.md` and `AGENTS.md` for ownership and coordination rules. +No open-source licence is currently granted. Public visibility does not by itself permit reuse, modification, or redistribution. diff --git a/apps/api/src/concierge/concierge.controller.ts b/apps/api/src/concierge/concierge.controller.ts index 6d48a92..311d3d9 100644 --- a/apps/api/src/concierge/concierge.controller.ts +++ b/apps/api/src/concierge/concierge.controller.ts @@ -10,12 +10,7 @@ import { UnauthorizedException, UseGuards, } from '@nestjs/common'; -import { - ApiExtraModels, - ApiOkResponse, - ApiOperation, - ApiTags, -} from '@nestjs/swagger'; +import { ApiExtraModels, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import type { ApiResponse } from '@tms/contracts'; import type { Request } from 'express'; diff --git a/apps/api/src/concierge/concierge.service.ts b/apps/api/src/concierge/concierge.service.ts index 200000f..5920828 100644 --- a/apps/api/src/concierge/concierge.service.ts +++ b/apps/api/src/concierge/concierge.service.ts @@ -136,10 +136,7 @@ export class ConciergeService { intent?: string; pagePath?: string; }) { - const conversation = await this.ensureConversation( - input.conversationPublicId, - input.pagePath, - ); + const conversation = await this.ensureConversation(input.conversationPublicId, input.pagePath); if (input.intent) { await this.database.client.aiConversation.update({ where: { id: conversation.id }, diff --git a/apps/storefront/app/api/concierge/chat/route.ts b/apps/storefront/app/api/concierge/chat/route.ts index 5992f60..aa8c6b3 100644 --- a/apps/storefront/app/api/concierge/chat/route.ts +++ b/apps/storefront/app/api/concierge/chat/route.ts @@ -19,7 +19,12 @@ function clientKey(request: Request): string { export async function POST(request: Request): Promise { if (process.env.AI_ENABLED === 'false') { return Response.json( - { error: { code: 'INTEGRATION_UNAVAILABLE', message: 'The Concierge is temporarily unavailable.' } }, + { + error: { + code: 'INTEGRATION_UNAVAILABLE', + message: 'The Concierge is temporarily unavailable.', + }, + }, { status: 503 }, ); } diff --git a/apps/storefront/app/page.tsx b/apps/storefront/app/page.tsx index 23de2ef..cf2bcfa 100644 --- a/apps/storefront/app/page.tsx +++ b/apps/storefront/app/page.tsx @@ -57,7 +57,9 @@ export default async function HomePage() { // Prefer the curated order first, then fill from the catalogue — one plate per slide, no dupes. for (const slug of [ ...HERO_SLUG_ORDER, - ...artworks.map((a) => a.slug).filter((s) => !(HERO_SLUG_ORDER as readonly string[]).includes(s)), + ...artworks + .map((a) => a.slug) + .filter((s) => !(HERO_SLUG_ORDER as readonly string[]).includes(s)), ]) { if (heroSlides.length >= PILLARS.length) break; if (HERO_EXCLUDED_SLUGS.has(slug)) continue; diff --git a/apps/storefront/app/studio-guide/page.tsx b/apps/storefront/app/studio-guide/page.tsx index 46fd506..34ee61c 100644 --- a/apps/storefront/app/studio-guide/page.tsx +++ b/apps/storefront/app/studio-guide/page.tsx @@ -19,10 +19,9 @@ export default function StudioGuidePage() { Studio Guide - The site-wide{' '} - F.A.T.U Concierge (launcher on every - page) is the primary assistant. This page keeps the original Studio Guide preview chat. - For a specific order, our{' '} + The site-wide F.A.T.U Concierge{' '} + (launcher on every page) is the primary assistant. This page keeps the original Studio + Guide preview chat. For a specific order, our{' '} {assistantName} {panel === 'minimised' ? ( - + ) : null} ); diff --git a/apps/storefront/components/concierge/concierge-panel.tsx b/apps/storefront/components/concierge/concierge-panel.tsx index 1a28be9..d585391 100644 --- a/apps/storefront/components/concierge/concierge-panel.tsx +++ b/apps/storefront/components/concierge/concierge-panel.tsx @@ -1,14 +1,7 @@ 'use client'; import { buttonVariants, cn, Price } from '@tms/ui'; -import { - ArrowRight, - Minimize2, - RotateCw, - Send, - ShieldCheck, - X, -} from 'lucide-react'; +import { ArrowRight, Minimize2, RotateCw, Send, ShieldCheck, X } from 'lucide-react'; import Link from 'next/link'; import { useEffect, useId, useRef } from 'react'; import { BrandLogo } from '@/components/site/brand-logo'; @@ -71,12 +64,7 @@ export function ConciergePanel() { )} >
- +

{assistantName}

@@ -131,7 +119,10 @@ export function ConciergePanel() { )} {pending ? ( -

+
Working…
@@ -158,13 +149,7 @@ export function ConciergePanel() { ); } -function AssistantBubble({ - message, - onRetry, -}: { - message: ConciergeMessage; - onRetry: () => void; -}) { +function AssistantBubble({ message, onRetry }: { message: ConciergeMessage; onRetry: () => void }) { return (
@@ -195,7 +180,10 @@ function AssistantBubble({

) : null}
- + ))} diff --git a/apps/storefront/components/concierge/concierge-provider.tsx b/apps/storefront/components/concierge/concierge-provider.tsx index 4ec3673..78c6554 100644 --- a/apps/storefront/components/concierge/concierge-provider.tsx +++ b/apps/storefront/components/concierge/concierge-provider.tsx @@ -141,8 +141,7 @@ export function ConciergeProvider({ const pageContext = useMemo(() => { const pageType = pageTypeFromPath(pathname); - const artworkSlug = - pageType === 'artwork' ? pathname.split('/')[2]?.split('?')[0] : undefined; + const artworkSlug = pageType === 'artwork' ? pathname.split('/')[2]?.split('?')[0] : undefined; const collectionSlug = pageType === 'collection' ? pathname.split('/')[2]?.split('?')[0] : undefined; return { diff --git a/apps/storefront/components/product/product-configurator.tsx b/apps/storefront/components/product/product-configurator.tsx index 63db6f1..146fd8a 100644 --- a/apps/storefront/components/product/product-configurator.tsx +++ b/apps/storefront/components/product/product-configurator.tsx @@ -103,11 +103,7 @@ export function ProductConfigurator({ product }: { product: ProductDetail }) { garment={product.garment} colour={colour ?? selectedColour?.hex} view={view} - artwork={ - print && view === 'front' - ? { src: print, area: 'front', alt: '' } - : null - } + artwork={print && view === 'front' ? { src: print, area: 'front', alt: '' } : null} priority={view === 'front'} className="p-4 sm:p-6" sizes="(min-width: 1024px) 40vw, 90vw" diff --git a/apps/storefront/components/studio/design-studio.tsx b/apps/storefront/components/studio/design-studio.tsx index e19b4c8..ecc094c 100644 --- a/apps/storefront/components/studio/design-studio.tsx +++ b/apps/storefront/components/studio/design-studio.tsx @@ -111,7 +111,8 @@ export function DesignStudio({ ); const artworkFolders = useMemo(() => groupArtworksForStudio(artworks), [artworks]); const [openFolders, setOpenFolders] = useState>( - () => new Set([defaultOpenStudioFolderId(groupArtworksForStudio(artworks), initialConfig.artwork)]), + () => + new Set([defaultOpenStudioFolderId(groupArtworksForStudio(artworks), initialConfig.artwork)]), ); const [copied, setCopied] = useState(false); const [status, setStatus] = useState(null); diff --git a/apps/storefront/lib/concierge/intent.ts b/apps/storefront/lib/concierge/intent.ts index 8f6ed46..0bb5505 100644 --- a/apps/storefront/lib/concierge/intent.ts +++ b/apps/storefront/lib/concierge/intent.ts @@ -47,7 +47,9 @@ export function routeIntent(message: string, context?: ConciergePageContext): In } } - if (/\b(human|agent|someone|real person|speak to|talk to (a |the )?person|escalat)\b/.test(text)) { + if ( + /\b(human|agent|someone|real person|speak to|talk to (a |the )?person|escalat)\b/.test(text) + ) { return { intent: 'human_handoff', allowedTools: [TOOLS.escalate, TOOLS.ticket], @@ -67,7 +69,9 @@ export function routeIntent(message: string, context?: ConciergePageContext): In }; } - if (/\b(my order|order status|where.*(order)|track|tracking|order number|order ref)\b/.test(text)) { + if ( + /\b(my order|order status|where.*(order)|track|tracking|order number|order ref)\b/.test(text) + ) { return { intent: 'order_support', allowedTools: [TOOLS.orders, TOOLS.orderStatus, TOOLS.ticket, TOOLS.knowledge], @@ -98,7 +102,13 @@ export function routeIntent(message: string, context?: ConciergePageContext): In if (/\b(design studio|placement|compatible|mockup|customi[sz]e)\b/.test(text)) { return { intent: 'design_studio', - allowedTools: [TOOLS.studio, TOOLS.validateStudio, TOOLS.deepLink, TOOLS.artwork, TOOLS.knowledge], + allowedTools: [ + TOOLS.studio, + TOOLS.validateStudio, + TOOLS.deepLink, + TOOLS.artwork, + TOOLS.knowledge, + ], confidence: 'high', }; } diff --git a/apps/storefront/lib/concierge/knowledge/corpus.ts b/apps/storefront/lib/concierge/knowledge/corpus.ts index aa186eb..b17155d 100644 --- a/apps/storefront/lib/concierge/knowledge/corpus.ts +++ b/apps/storefront/lib/concierge/knowledge/corpus.ts @@ -2,14 +2,7 @@ import { createHash } from 'node:crypto'; import type { ConciergeCitation } from '@tms/contracts'; export type KnowledgeSourceType = - | 'product' - | 'artwork' - | 'collection' - | 'policy' - | 'story' - | 'faq' - | 'page' - | 'article'; + 'product' | 'artwork' | 'collection' | 'policy' | 'story' | 'faq' | 'page' | 'article'; export interface KnowledgeRecord { sourceType: KnowledgeSourceType; @@ -155,9 +148,7 @@ export const KNOWLEDGE_CORPUS: readonly KnowledgeRecord[] = [ function tokenize(text: string): string[] { const normalized = text.toLowerCase().replace(/f\.?\s*a\.?\s*t\.?\s*u/g, 'fatu'); - return normalized - .split(/[^a-z0-9₦]+/i) - .filter((t) => t.length > 2); + return normalized.split(/[^a-z0-9₦]+/i).filter((t) => t.length > 2); } /** diff --git a/apps/storefront/lib/concierge/orchestrator.ts b/apps/storefront/lib/concierge/orchestrator.ts index 601ff07..0856186 100644 --- a/apps/storefront/lib/concierge/orchestrator.ts +++ b/apps/storefront/lib/concierge/orchestrator.ts @@ -67,8 +67,18 @@ export async function runConciergeTurn( if (intentResult.intent === 'greeting') { text = `I’m the ${assistantName()}. I can help you discover artwork, choose a garment and size, understand delivery, or check an order. What would you like to do?`; citations = [ - { kind: 'catalogue', label: 'Artworks', description: 'Browse the gallery', href: '/artworks' }, - { kind: 'studio', label: 'Design Studio', description: 'Build a piece', href: '/design-studio' }, + { + kind: 'catalogue', + label: 'Artworks', + description: 'Browse the gallery', + href: '/artworks', + }, + { + kind: 'studio', + label: 'Design Studio', + description: 'Build a piece', + href: '/design-studio', + }, { kind: 'support', label: 'Contact', description: 'Speak to the studio', href: '/contact' }, ]; } else if (intentResult.intent === 'human_handoff' || allowed.has('escalate_to_human')) { @@ -84,9 +94,7 @@ export async function runConciergeTurn( }, opts.cookie, ); - text = ticket.ok - ? `I’ll connect you with the studio team. ${ticket.text}` - : ticket.text; + text = ticket.ok ? `I’ll connect you with the studio team. ${ticket.text}` : ticket.text; ticketReference = ticket.ticketReference; citations = ticket.citations; } else if (intentResult.intent === 'complaint') { @@ -133,7 +141,12 @@ export async function runConciergeTurn( text = 'I can add a piece once the artwork, garment, colour, size, and placement are confirmed as an approved combination. Tell me those details, or open the Design Studio / product page to add it yourself — I will never substitute options silently.'; citations = [ - { kind: 'studio', label: 'Design Studio', description: 'Confirm and add', href: '/design-studio' }, + { + kind: 'studio', + label: 'Design Studio', + description: 'Confirm and add', + href: '/design-studio', + }, { kind: 'catalogue', label: 'Your bag', description: 'Review bag', href: '/cart' }, ]; guarded = true; @@ -144,15 +157,16 @@ export async function runConciergeTurn( } } else if (intentResult.intent === 'design_studio') { const slug = - request.context.artworkSlug || - message.match(/artwork\s+([a-z0-9-]+)/i)?.[1] || - undefined; + request.context.artworkSlug || message.match(/artwork\s+([a-z0-9-]+)/i)?.[1] || undefined; const validation = validateDesignConfiguration({ artworkSlug: slug }); const link = createDesignStudioDeepLink({ artworkSlug: slug }); text = `${validation.text} ${link.text}`; citations = mergeCitations(validation.citations, link.citations); } else if (intentResult.intent === 'product_discovery') { - if (request.context.artworkSlug && /\b(this artwork|about this|tell me about)\b/i.test(message)) { + if ( + request.context.artworkSlug && + /\b(this artwork|about this|tell me about)\b/i.test(message) + ) { const art = await getArtworkTool(request.context.artworkSlug); text = art.text; cards = art.cards; @@ -177,7 +191,12 @@ export async function runConciergeTurn( if (cards.length === 0) { text += ' Try a different mood, city, or budget, or browse the gallery.'; citations = mergeCitations(citations, [ - { kind: 'catalogue', label: 'Artworks', description: 'Browse gallery', href: '/artworks' }, + { + kind: 'catalogue', + label: 'Artworks', + description: 'Browse gallery', + href: '/artworks', + }, ]); } } @@ -198,7 +217,10 @@ export async function runConciergeTurn( text = top.content; citations = knowledge.citations; // Price/stock deflection stays honest even if knowledge mentions timelines. - if (/\b(price|how much|in stock|sold out)\b/i.test(message) && intentResult.intent !== 'policy') { + if ( + /\b(price|how much|in stock|sold out)\b/i.test(message) && + intentResult.intent !== 'policy' + ) { guarded = true; text = 'I only quote live prices and availability from catalogue tools or the product page — not from memory. Open the artwork or shop page for the current figure.'; diff --git a/apps/storefront/lib/concierge/providers/fallback.ts b/apps/storefront/lib/concierge/providers/fallback.ts index ff8d07e..d13e5c6 100644 --- a/apps/storefront/lib/concierge/providers/fallback.ts +++ b/apps/storefront/lib/concierge/providers/fallback.ts @@ -34,9 +34,7 @@ function parseProviderOrder(): AiProviderId[] { .map((s) => s.trim().toLowerCase()) .filter(Boolean); const allowed: AiProviderId[] = ['openai', 'anthropic', 'gemini']; - const ordered = raw.filter((id): id is AiProviderId => - (allowed as string[]).includes(id), - ); + const ordered = raw.filter((id): id is AiProviderId => (allowed as string[]).includes(id)); return ordered.length > 0 ? ordered : [...allowed]; } @@ -134,7 +132,10 @@ async function callGemini(input: PolishInput): Promise { const json = (await response.json()) as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }>; }; - const text = json.candidates?.[0]?.content?.parts?.map((p) => p.text ?? '').join('').trim(); + const text = json.candidates?.[0]?.content?.parts + ?.map((p) => p.text ?? '') + .join('') + .trim(); return text ? { text, provider: 'gemini', model } : null; } diff --git a/apps/storefront/lib/concierge/tools/cart.ts b/apps/storefront/lib/concierge/tools/cart.ts index aac85ce..1f780b7 100644 --- a/apps/storefront/lib/concierge/tools/cart.ts +++ b/apps/storefront/lib/concierge/tools/cart.ts @@ -17,7 +17,7 @@ export async function getCartTool(cookie?: string): Promise<{ } try { const cart = await fetchCart(cookie); - const count = cart.items.reduce((n, line) => n + line.quantity, 0); + const count = cart.items.reduce((n, line) => n + line.quantity, 0); return { ok: true, text: `Your bag has ${count} item${count === 1 ? '' : 's'}. Subtotal is resolved server-side — I will not invent a price. ${cart.hasIssues ? 'Some lines have availability issues and are excluded from checkout until fixed.' : 'No availability issues reported on the last read.'}`, @@ -51,7 +51,12 @@ export async function addToCartTool(input: AddCartItemInput): Promise<{ ok: false, text: 'I cannot add to a live bag while the catalogue is in preview mode. Open the product or Design Studio to add it yourself.', citations: [ - { kind: 'studio', label: 'Design Studio', description: 'Build and add', href: '/design-studio' }, + { + kind: 'studio', + label: 'Design Studio', + description: 'Build and add', + href: '/design-studio', + }, { kind: 'catalogue', label: 'Shop', description: 'Browse products', href: '/shop' }, ], }; @@ -62,7 +67,12 @@ export async function addToCartTool(input: AddCartItemInput): Promise<{ ok: true, text: 'Added to your bag. Open the bag to confirm sizes and quantities before checkout.', citations: [ - { kind: 'catalogue', label: 'Your bag', description: 'Confirm the addition', href: '/cart' }, + { + kind: 'catalogue', + label: 'Your bag', + description: 'Confirm the addition', + href: '/cart', + }, ], }; } catch { diff --git a/apps/storefront/lib/concierge/tools/catalog.ts b/apps/storefront/lib/concierge/tools/catalog.ts index 20b19f2..e30dbda 100644 --- a/apps/storefront/lib/concierge/tools/catalog.ts +++ b/apps/storefront/lib/concierge/tools/catalog.ts @@ -12,14 +12,14 @@ export async function searchCatalog(query: string): Promise<{ summary: string; }> { const q = query.trim(); - const items = q ? await dataProvider.searchArtworks(q, 8) : (await dataProvider.listArtworks({ limit: 8 })).items; + const items = q + ? await dataProvider.searchArtworks(q, 8) + : (await dataProvider.listArtworks({ limit: 8 })).items; // Preference heuristics from free text (budget / mood) without inventing popularity. // "under 20000" / "under ₦20,000" → treat as naira major units, convert to minor. const budgetMatch = q.match(/under\s*₦?\s*([\d,]+)/i); - const budgetCeiling = budgetMatch?.[1] - ? Number(budgetMatch[1].replace(/,/g, '')) * 100 - : null; + const budgetCeiling = budgetMatch?.[1] ? Number(budgetMatch[1].replace(/,/g, '')) * 100 : null; let filtered = items; if (budgetCeiling != null && !Number.isNaN(budgetCeiling)) { @@ -161,10 +161,11 @@ export function createDesignStudioDeepLink(input: { }; } -export function validateDesignConfiguration(input: { - artworkSlug?: string; - garment?: string; -}): { valid: boolean; text: string; citations: ConciergeCitation[] } { +export function validateDesignConfiguration(input: { artworkSlug?: string; garment?: string }): { + valid: boolean; + text: string; + citations: ConciergeCitation[]; +} { // Without a live studio validation API domain, we refuse to invent validity. // Point the customer at the studio which only lists approved combinations. if (!input.artworkSlug) { diff --git a/apps/storefront/lib/concierge/tools/orders.ts b/apps/storefront/lib/concierge/tools/orders.ts index 9f51131..bd24166 100644 --- a/apps/storefront/lib/concierge/tools/orders.ts +++ b/apps/storefront/lib/concierge/tools/orders.ts @@ -27,7 +27,12 @@ export async function getCustomerOrdersTool(cookie?: string): Promise<{ ok: true, text: 'I do not see any orders on this signed-in account yet.', citations: [ - { kind: 'support', label: 'Your orders', description: 'Account orders', href: '/account/orders' }, + { + kind: 'support', + label: 'Your orders', + description: 'Account orders', + href: '/account/orders', + }, ], }; } @@ -39,7 +44,12 @@ export async function getCustomerOrdersTool(cookie?: string): Promise<{ ok: true, text: `Here are recent orders on your account:\n${lines}\nStatuses come from the order service — I will not invent tracking or dispatch dates.`, citations: [ - { kind: 'support', label: 'Your orders', description: 'Full order history', href: '/account/orders' }, + { + kind: 'support', + label: 'Your orders', + description: 'Full order history', + href: '/account/orders', + }, ], }; } catch (error) { @@ -58,7 +68,12 @@ export async function getCustomerOrdersTool(cookie?: string): Promise<{ ok: false, text: 'I could not reach the order service. Please try again, check Your orders, or contact the studio.', citations: [ - { kind: 'support', label: 'Your orders', description: 'Account orders', href: '/account/orders' }, + { + kind: 'support', + label: 'Your orders', + description: 'Account orders', + href: '/account/orders', + }, { kind: 'support', label: 'Contact', description: 'Studio support', href: '/contact' }, ], }; @@ -77,7 +92,12 @@ export async function getOrderStatusTool( ok: false, text: 'Please share a valid order reference from your confirmation email.', citations: [ - { kind: 'support', label: 'Your orders', description: 'Find references', href: '/account/orders' }, + { + kind: 'support', + label: 'Your orders', + description: 'Find references', + href: '/account/orders', + }, ], }; } @@ -105,7 +125,12 @@ export async function getOrderStatusTool( ok: false, text: 'I could not find an order with that reference for this account. Double-check the reference or contact the studio.', citations: [ - { kind: 'support', label: 'Your orders', description: 'Account orders', href: '/account/orders' }, + { + kind: 'support', + label: 'Your orders', + description: 'Account orders', + href: '/account/orders', + }, { kind: 'support', label: 'Contact', description: 'Studio support', href: '/contact' }, ], }; diff --git a/apps/storefront/lib/concierge/tools/tickets.ts b/apps/storefront/lib/concierge/tools/tickets.ts index 6b1b652..a37a61c 100644 --- a/apps/storefront/lib/concierge/tools/tickets.ts +++ b/apps/storefront/lib/concierge/tools/tickets.ts @@ -25,7 +25,12 @@ export interface CreatedTicket { export async function createSupportTicketTool( input: CreateTicketInput, cookie?: string, -): Promise<{ ok: boolean; text: string; ticketReference?: string; citations: ConciergeCitation[] }> { +): Promise<{ + ok: boolean; + text: string; + ticketReference?: string; + citations: ConciergeCitation[]; +}> { try { const ticket = await apiFetch('/api/v1/concierge/tickets', { method: 'POST', @@ -55,7 +60,12 @@ export async function createSupportTicketTool( ok: false, text: 'I could not create a support ticket right now, so I cannot claim the studio has received this yet. Please use the contact page and include your order reference if you have one.', citations: [ - { kind: 'support', label: 'Contact the studio', description: 'Email the team directly', href: '/contact' }, + { + kind: 'support', + label: 'Contact the studio', + description: 'Email the team directly', + href: '/contact', + }, ], }; } diff --git a/apps/storefront/lib/studio-artwork-folders.spec.ts b/apps/storefront/lib/studio-artwork-folders.spec.ts index 28c5271..d4d2eae 100644 --- a/apps/storefront/lib/studio-artwork-folders.spec.ts +++ b/apps/storefront/lib/studio-artwork-folders.spec.ts @@ -31,17 +31,18 @@ describe('groupArtworksForStudio', () => { title: 'Africa United Heritage Duo', collection: 'Africa United', }), - art({ slug: 'resilience-hands-high', title: 'Resilience Hands High', collection: 'Resilience' }), + art({ + slug: 'resilience-hands-high', + title: 'Resilience Hands High', + collection: 'Resilience', + }), art({ slug: 'studio-cap-muse', title: 'Studio Cap Muse', collection: 'Studio Muses' }), ]; const groups = groupArtworksForStudio(items); expect(groups).toHaveLength(2); expect(groups.map((g) => g.folder.id)).toEqual(['signature', 'studio-collections']); - expect(groups[0]!.artworks.map((a) => a.slug)).toEqual([ - 'midnight-in-lagos', - 'paper-tigers', - ]); + expect(groups[0]!.artworks.map((a) => a.slug)).toEqual(['midnight-in-lagos', 'paper-tigers']); expect(groups[1]!.artworks.map((a) => a.slug)).toEqual([ 'africa-united-heritage-duo', 'resilience-hands-high', diff --git a/docs/AI_ANALYTICS_DEFINITIONS.md b/docs/AI_ANALYTICS_DEFINITIONS.md index 3c7f53c..8dd72fd 100644 --- a/docs/AI_ANALYTICS_DEFINITIONS.md +++ b/docs/AI_ANALYTICS_DEFINITIONS.md @@ -2,30 +2,30 @@ ## Attribution rules -| Term | Definition | -| --- | --- | -| **Direct** | Customer completed an action (add-to-cart, checkout) in the same conversation turn after an explicit Concierge tool confirmation | -| **Influenced** | Customer opened Concierge within the session and later purchased; chat alone is **not** causation | -| **Unaffected** | Purchase with no Concierge interaction in the session | +| Term | Definition | +| -------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| **Direct** | Customer completed an action (add-to-cart, checkout) in the same conversation turn after an explicit Concierge tool confirmation | +| **Influenced** | Customer opened Concierge within the session and later purchased; chat alone is **not** causation | +| **Unaffected** | Purchase with no Concierge interaction in the session | Opening the Concierge launcher never counts as causing a sale. ## Event types (`AiAnalyticsEvent.eventType`) -| Event | Meaning | -| --- | --- | -| `conversation_started` | First message in a conversation | -| `intent_classified` | Intent router result | -| `recommendation_shown` | Product cards returned | -| `recommendation_clicked` | Customer followed a card link (client beacon — future) | -| `assisted_add_to_cart` | Cart tool returned success | -| `assisted_checkout` | Checkout completed with prior assisted_add_to_cart in session | -| `support_ticket_created` | Ticket persisted | -| `human_handoff` | Escalation path taken | -| `feedback_submitted` | Thumbs / score | -| `provider_error` | Model/provider failure | -| `rate_limited` | Abuse protection tripped | -| `unanswered` | Low-confidence / escalated without answer | +| Event | Meaning | +| ------------------------ | ------------------------------------------------------------- | +| `conversation_started` | First message in a conversation | +| `intent_classified` | Intent router result | +| `recommendation_shown` | Product cards returned | +| `recommendation_clicked` | Customer followed a card link (client beacon — future) | +| `assisted_add_to_cart` | Cart tool returned success | +| `assisted_checkout` | Checkout completed with prior assisted_add_to_cart in session | +| `support_ticket_created` | Ticket persisted | +| `human_handoff` | Escalation path taken | +| `feedback_submitted` | Thumbs / score | +| `provider_error` | Model/provider failure | +| `rate_limited` | Abuse protection tripped | +| `unanswered` | Low-confidence / escalated without answer | ## Core metrics diff --git a/docs/AI_CUSTOMER_CARE_AUDIT.md b/docs/AI_CUSTOMER_CARE_AUDIT.md index c121509..32444aa 100644 --- a/docs/AI_CUSTOMER_CARE_AUDIT.md +++ b/docs/AI_CUSTOMER_CARE_AUDIT.md @@ -17,21 +17,21 @@ The monorepo already contains a production-shaped commerce stack (NestJS API + P ## 2. Existing architecture -| Layer | Technology | Location | -| --- | --- | --- | -| Package manager | pnpm 10.20 / Turbo 2.10 | root `package.json` | -| Storefront | Next.js 16.2 App Router, React 19 | `apps/storefront` | -| Admin | Next.js 16.2 App Router | `apps/admin` | -| API | NestJS 11, Express, Pino | `apps/api` | -| Worker | BullMQ consumer | `apps/worker` | -| Database | Prisma 7 + PostgreSQL (Supabase-compatible) | `packages/database` | -| Contracts | Shared TypeScript DTOs | `packages/contracts` | -| UI / tokens | `@tms/ui`, Tailwind v4, Space Grotesk + IBM Plex Sans | `packages/ui` | -| Config | Zod-validated env (`loadEnvironment`) | `packages/configuration` | -| Payments | Flutterwave + mock provider | `apps/api/src/payments` | -| Email | `@tms/email` | `packages/email` | -| Analytics package | Present (lightweight) | `packages/analytics` | -| Tests | Vitest everywhere; Playwright e2e on storefront | various | +| Layer | Technology | Location | +| ----------------- | ----------------------------------------------------- | ------------------------ | +| Package manager | pnpm 10.20 / Turbo 2.10 | root `package.json` | +| Storefront | Next.js 16.2 App Router, React 19 | `apps/storefront` | +| Admin | Next.js 16.2 App Router | `apps/admin` | +| API | NestJS 11, Express, Pino | `apps/api` | +| Worker | BullMQ consumer | `apps/worker` | +| Database | Prisma 7 + PostgreSQL (Supabase-compatible) | `packages/database` | +| Contracts | Shared TypeScript DTOs | `packages/contracts` | +| UI / tokens | `@tms/ui`, Tailwind v4, Space Grotesk + IBM Plex Sans | `packages/ui` | +| Config | Zod-validated env (`loadEnvironment`) | `packages/configuration` | +| Payments | Flutterwave + mock provider | `apps/api/src/payments` | +| Email | `@tms/email` | `packages/email` | +| Analytics package | Present (lightweight) | `packages/analytics` | +| Tests | Vitest everywhere; Playwright e2e on storefront | various | **Data-source policy (critical):** storefront uses per-domain composition in `apps/storefront/lib/data/index.ts`. API-capable today: `artworks`, `collections`, `drops`, `stories`. Still mock: `studio`, `products`, `passport`, `reviews`, `community`, `loyalty`, `delivery`. Cart/auth/orders use separate clients and `DATA_SOURCE=api` when the API is reachable. Default builds stay on mock so CI does not require a live API. @@ -39,18 +39,18 @@ The monorepo already contains a production-shaped commerce stack (NestJS API + P ## 3. Reusable systems for the Concierge -| Capability | Reuse | -| --- | --- | -| Brand / policy copy | `/about`, `/faq`, `/delivery`, `/returns`, `/size-guide`, `/care`, `/privacy`, `/terms`, `/cookies`, `/contact`, `/studio-guide` | -| Catalogue | `dataProvider` + NestJS catalogue/artwork/garment modules | -| Design Studio rules | Garment compatibility APIs + storefront studio (ADR-013: approved placements only) | -| Cart | `lib/cart-api.ts` → `POST/PATCH/DELETE /api/v1/cart/*` (never send prices) | -| Auth | `tms_session` HttpOnly cookie; `AuthProvider` | -| Orders / payment | NestJS order state machine + Flutterwave handoff | -| Admin shell / RBAC | `AdminShell`, admin session + MFA + permission guards | -| Studio Guide UX patterns | `StudioGuideChat` — identity, suggestions, references, tool-error + retry, a11y log | -| Observability | Pino + correlation IDs; extend with concierge traces | -| Rate limiting pattern | `AuthRateLimiterService` — mirror for chat | +| Capability | Reuse | +| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | +| Brand / policy copy | `/about`, `/faq`, `/delivery`, `/returns`, `/size-guide`, `/care`, `/privacy`, `/terms`, `/cookies`, `/contact`, `/studio-guide` | +| Catalogue | `dataProvider` + NestJS catalogue/artwork/garment modules | +| Design Studio rules | Garment compatibility APIs + storefront studio (ADR-013: approved placements only) | +| Cart | `lib/cart-api.ts` → `POST/PATCH/DELETE /api/v1/cart/*` (never send prices) | +| Auth | `tms_session` HttpOnly cookie; `AuthProvider` | +| Orders / payment | NestJS order state machine + Flutterwave handoff | +| Admin shell / RBAC | `AdminShell`, admin session + MFA + permission guards | +| Studio Guide UX patterns | `StudioGuideChat` — identity, suggestions, references, tool-error + retry, a11y log | +| Observability | Pino + correlation IDs; extend with concierge traces | +| Rate limiting pattern | `AuthRateLimiterService` — mirror for chat | --- @@ -73,17 +73,17 @@ The monorepo already contains a production-shaped commerce stack (NestJS API + P ## 5. Data sources (source-of-truth map) -| Domain | Source of truth | Concierge access | -| --- | --- | --- | -| Artwork / collections / drops / stories | Postgres via API (when `DATA_SOURCE=api`) else mock seeds | Live tools; never hardcode titles/prices | -| Products (artwork×garment) | Composed on storefront; pricing from approved pairs (ADR-015) | Tool must resolve from catalogue services | -| Studio options | Mock until FBR studio gap closes; API has compatible-garments | Validate via real compatibility endpoints when available | -| Cart | NestJS cart service | Mutating tools only with explicit customer intent | -| Orders / payments | NestJS order + payment modules | Ownership-checked tools only | -| Delivery fees / VAT | Checkout path; delivery domain still mock on storefront | Cite policy pages + checkout totals; do not invent fees | -| Policies / FAQ | Static Next.js pages (editorial) | Indexed knowledge corpus + citations | -| Site CMS | `packages/site-content` Prisma models exist | Prefer when admin CMS is wired; otherwise page corpus | -| Promotions | `Promotion` model | Only when status ACTIVE | +| Domain | Source of truth | Concierge access | +| --------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------------- | +| Artwork / collections / drops / stories | Postgres via API (when `DATA_SOURCE=api`) else mock seeds | Live tools; never hardcode titles/prices | +| Products (artwork×garment) | Composed on storefront; pricing from approved pairs (ADR-015) | Tool must resolve from catalogue services | +| Studio options | Mock until FBR studio gap closes; API has compatible-garments | Validate via real compatibility endpoints when available | +| Cart | NestJS cart service | Mutating tools only with explicit customer intent | +| Orders / payments | NestJS order + payment modules | Ownership-checked tools only | +| Delivery fees / VAT | Checkout path; delivery domain still mock on storefront | Cite policy pages + checkout totals; do not invent fees | +| Policies / FAQ | Static Next.js pages (editorial) | Indexed knowledge corpus + citations | +| Site CMS | `packages/site-content` Prisma models exist | Prefer when admin CMS is wired; otherwise page corpus | +| Promotions | `Promotion` model | Only when status ACTIVE | --- @@ -142,22 +142,26 @@ Customer → F.A.T.U Concierge UI (storefront) ## 10. Exact files expected to change / add ### Documentation + - `docs/AI_CUSTOMER_CARE_*.md`, `docs/AI_*.md` (this suite) - `docs/coordination/BACKEND_TO_FRONTEND.md` (new AI endpoints) - `docs/coordination/FRONTEND_TO_BACKEND.md` (TMS-FBR-009 fulfilment notes) - `.env.example` ### Database / contracts / config + - `packages/database/prisma/schema.prisma` + migration - `packages/contracts/src/index.ts` (concierge DTOs) - `packages/configuration/src/index.ts` (`AI_*` vars) ### API + - `apps/api/src/concierge/**` (module, controllers, services) - `apps/api/src/app.module.ts` - Optional worker job for knowledge sync ### Storefront + - `apps/storefront/app/api/concierge/**` - `apps/storefront/lib/concierge/**` - `apps/storefront/components/concierge/**` @@ -165,24 +169,26 @@ Customer → F.A.T.U Concierge UI (storefront) - Evolve `/studio-guide` to share Concierge identity ### Admin + - `apps/admin/app/concierge/**` - `apps/admin/components/admin-shell.tsx` (nav) - `apps/admin/lib/concierge/**` ### Tests + - Unit/integration under each app; Playwright scenarios for chat shell --- ## 11. Assumptions still requiring verification -| Assumption | Status | -| --- | --- | -| Production `AI_API_KEY` / provider choice | **Unverified** — env placeholders only | -| pgvector availability on Supabase | **Unverified** — lexical retrieval first | -| Exact VAT rate / delivery fee tables | **Partial** — checkout calculates; storefront delivery domain mock | -| Guest order OTP flow | **Missing** — design verification before claiming guest tracking | -| Legal finalisation of privacy retention copy | **Draft** — document + admin retention setting | +| Assumption | Status | +| --------------------------------------------------- | ------------------------------------------------------------------ | +| Production `AI_API_KEY` / provider choice | **Unverified** — env placeholders only | +| pgvector availability on Supabase | **Unverified** — lexical retrieval first | +| Exact VAT rate / delivery fee tables | **Partial** — checkout calculates; storefront delivery domain mock | +| Guest order OTP flow | **Missing** — design verification before claiming guest tracking | +| Legal finalisation of privacy retention copy | **Draft** — document + admin retention setting | | Vercel project `tai-admin` maps 1:1 to `apps/admin` | **Likely** via monorepo; confirm in deployment docs when deploying | --- diff --git a/docs/AI_CUSTOMER_CARE_SPEC.md b/docs/AI_CUSTOMER_CARE_SPEC.md index 6f5dda5..cd27a88 100644 --- a/docs/AI_CUSTOMER_CARE_SPEC.md +++ b/docs/AI_CUSTOMER_CARE_SPEC.md @@ -20,12 +20,12 @@ Ship a production-grade customer-care and shopping concierge that: ## 2. Roles -| Role | Behaviour | -| --- | --- | -| Customer-care agent | Policies, sizing, delivery, returns, payments, orders, escalation | -| Shopping assistant | Preference discovery, 2–4 recommendations with reasons + cards | -| Design Studio guide | Explain studio, validate combos, deep-link when possible | -| Business intelligence | Anonymised intents, gaps, assisted conversion metrics | +| Role | Behaviour | +| --------------------- | ----------------------------------------------------------------- | +| Customer-care agent | Policies, sizing, delivery, returns, payments, orders, escalation | +| Shopping assistant | Preference discovery, 2–4 recommendations with reasons + cards | +| Design Studio guide | Explain studio, validate combos, deep-link when possible | +| Business intelligence | Anonymised intents, gaps, assisted conversion metrics | --- @@ -104,7 +104,14 @@ Greeting / simple FAQ must not call expensive order or cart tools. ```ts { - sourceType: 'product' | 'artwork' | 'collection' | 'policy' | 'story' | 'faq' | 'page' | 'article'; + sourceType: 'product' | + 'artwork' | + 'collection' | + 'policy' | + 'story' | + 'faq' | + 'page' | + 'article'; sourceId: string; title: string; canonicalUrl: string; diff --git a/docs/AI_KNOWLEDGE_SOURCES.md b/docs/AI_KNOWLEDGE_SOURCES.md index f97b14d..cbfee9c 100644 --- a/docs/AI_KNOWLEDGE_SOURCES.md +++ b/docs/AI_KNOWLEDGE_SOURCES.md @@ -13,19 +13,19 @@ Located in `apps/storefront/lib/concierge/knowledge/corpus.ts`. -| sourceId | Type | URL | Update mechanism | -| --- | --- | --- | --- | -| about | page | `/about` | Manual seed + knowledge sync | -| faq-made-to-order | faq | `/faq` | Manual seed + knowledge sync | -| delivery | policy | `/delivery` | Manual seed + knowledge sync | -| returns | policy | `/returns` | Manual seed + knowledge sync | -| size-guide | policy | `/size-guide` | Manual seed + knowledge sync | -| faq-payment | faq | `/faq` | Manual seed + knowledge sync | -| faq-combinations | faq | `/faq` | Manual seed + knowledge sync | -| studio-guide | page | `/design-studio` | Manual seed + knowledge sync | -| care | page | `/care` | Manual seed + knowledge sync | -| contact | page | `/contact` | Manual seed + knowledge sync | -| privacy | policy | `/privacy` | Manual seed + knowledge sync | +| sourceId | Type | URL | Update mechanism | +| ----------------- | ------ | ---------------- | ---------------------------- | +| about | page | `/about` | Manual seed + knowledge sync | +| faq-made-to-order | faq | `/faq` | Manual seed + knowledge sync | +| delivery | policy | `/delivery` | Manual seed + knowledge sync | +| returns | policy | `/returns` | Manual seed + knowledge sync | +| size-guide | policy | `/size-guide` | Manual seed + knowledge sync | +| faq-payment | faq | `/faq` | Manual seed + knowledge sync | +| faq-combinations | faq | `/faq` | Manual seed + knowledge sync | +| studio-guide | page | `/design-studio` | Manual seed + knowledge sync | +| care | page | `/care` | Manual seed + knowledge sync | +| contact | page | `/contact` | Manual seed + knowledge sync | +| privacy | policy | `/privacy` | Manual seed + knowledge sync | Checksum = SHA-256 prefix of content. Obsolete sources should be unpublished via admin/API. diff --git a/docs/AI_TEST_MATRIX.md b/docs/AI_TEST_MATRIX.md index 96ef7ac..6385de7 100644 --- a/docs/AI_TEST_MATRIX.md +++ b/docs/AI_TEST_MATRIX.md @@ -1,31 +1,31 @@ # AI Concierge Test Matrix -| # | Scenario | Layer | Status | -| --- | --- | --- | --- | -| 1 | What is F.A.T.U? | unit (`concierge.spec`) | covered | -| 2 | Made-to-order explanation | unit | covered | -| 3 | Product under budget | unit (search heuristics) | partial — extend | -| 4 | Bold Lagos artwork | unit | covered | -| 5 | Current price of artwork | unit/tool | via cards when provider has price | -| 6 | Unavailable size | integration | pending API stock domain | -| 7 | Garment combinations for artwork | unit/studio | deep-link guidance | -| 8 | Invalid Design Studio combo | unit | refuses invention | -| 9 | Add valid item to bag | integration | requires `DATA_SOURCE=api` | -| 10 | Delivery timing | knowledge | covered via corpus | -| 11 | Change-of-mind return | unit | covered | -| 12 | Wrong item received | ticket classification | covered | -| 13 | Signed-in order track | integration | requires API + session | -| 14 | Access another person’s order | API ownership | existing order e2e | -| 15 | Payment taken, no order | complaint URGENT | covered classify | -| 16 | Request human | handoff | covered intent | -| 17 | Provider unavailable | mock fallback | default path | -| 18 | Catalog service fails | tool error text | pending dedicated test | -| 19 | Malicious system prompt reveal | unit | covered | -| 20 | Retrieved doc with instructions | precedence + prompt | covered by design | -| 21 | Mobile open/minimise/restore | e2e | pending Playwright | -| 22 | Keyboard-only conversation | e2e | pending Playwright | -| 23 | Admin policy update → answer | sync | pending live admin article CRUD | -| 24 | Unconfident → escalate | unit | unknown path | +| # | Scenario | Layer | Status | +| --- | -------------------------------- | ------------------------ | --------------------------------- | +| 1 | What is F.A.T.U? | unit (`concierge.spec`) | covered | +| 2 | Made-to-order explanation | unit | covered | +| 3 | Product under budget | unit (search heuristics) | partial — extend | +| 4 | Bold Lagos artwork | unit | covered | +| 5 | Current price of artwork | unit/tool | via cards when provider has price | +| 6 | Unavailable size | integration | pending API stock domain | +| 7 | Garment combinations for artwork | unit/studio | deep-link guidance | +| 8 | Invalid Design Studio combo | unit | refuses invention | +| 9 | Add valid item to bag | integration | requires `DATA_SOURCE=api` | +| 10 | Delivery timing | knowledge | covered via corpus | +| 11 | Change-of-mind return | unit | covered | +| 12 | Wrong item received | ticket classification | covered | +| 13 | Signed-in order track | integration | requires API + session | +| 14 | Access another person’s order | API ownership | existing order e2e | +| 15 | Payment taken, no order | complaint URGENT | covered classify | +| 16 | Request human | handoff | covered intent | +| 17 | Provider unavailable | mock fallback | default path | +| 18 | Catalog service fails | tool error text | pending dedicated test | +| 19 | Malicious system prompt reveal | unit | covered | +| 20 | Retrieved doc with instructions | precedence + prompt | covered by design | +| 21 | Mobile open/minimise/restore | e2e | pending Playwright | +| 22 | Keyboard-only conversation | e2e | pending Playwright | +| 23 | Admin policy update → answer | sync | pending live admin article CRUD | +| 24 | Unconfident → escalate | unit | unknown path | ## Commands From b12a9c785e6e324010b1fe1e1f89af8c9ffeab9d Mon Sep 17 00:00:00 2001 From: Ghost69 Date: Tue, 21 Jul 2026 20:18:37 +0100 Subject: [PATCH 2/2] test: account for AI concierge migration --- packages/database/src/identity-foundation.integration.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/database/src/identity-foundation.integration.spec.ts b/packages/database/src/identity-foundation.integration.spec.ts index 7ec3e7b..e169ac3 100644 --- a/packages/database/src/identity-foundation.integration.spec.ts +++ b/packages/database/src/identity-foundation.integration.spec.ts @@ -153,7 +153,7 @@ describe.sequential('backend persistence PostgreSQL integration', () => { 'SELECT count(*)::int AS count FROM permissions', ); - expect(migrationResult.rows[0]?.count).toBe(13); + expect(migrationResult.rows[0]?.count).toBe(14); expect(permissionResult.rows[0]?.count).toBe(14); expect(roleResult.rows).toEqual([ { code: 'ANALYST', is_system: true, grant_count: 3 },