Skip to content

Restaurant Menu AI Content Implementation Plan

For Codex: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Generate reviewable menu descriptions, RU/EN/SR translations, and dish-image candidates without inventing product facts or letting generated content reach a published menu without approval.

Architecture: Tenant-scoped generation jobs run through BullMQ and provider adapters. Text uses the existing LiteLLM gateway with strict structured output; images use an asynchronous HTTP provider adapter and the shared object store. Candidates preserve prompt/model/policy/cost lineage, consume an atomic tenant budget, and update only the current draft when an owner or admin explicitly approves them.

Tech Stack: NestJS, TypeORM/PostgreSQL with FORCE RLS, BullMQ/Redis, LiteLLM, configurable image-generation API, S3-compatible object storage, Next.js, Jest, Playwright.


Global Constraints

  • Run python3 .agent/tools/recall.py "restaurant menu AI descriptions translations images migration provider failing test" before implementation and surface the result.
  • Obtain approval before applying the migration or adding a provider SDK; prefer the existing HTTP and LiteLLM clients.
  • Owners configure a monthly tenant budget in euro cents; default is zero, so generation is disabled until explicitly enabled.
  • Use only approved menu fields as factual context. Never add ingredients, allergens, dietary labels, origin claims, health claims, or preparation methods not supplied by the operator.
  • Generated text and images are candidates, never draft content. Publishing consumes only separately approved draft state.
  • Log hashes and provider metadata, not complete prompts containing customer data.
  • Do not use menu-document originals as image-generation reference input in the first release.
  • Retain rejected image objects for seven days and accepted generation lineage for the lifetime of the referenced draft asset.

Produced and Consumed Interfaces

ts
export type GenerationKind = 'description' | 'translation' | 'image';
export type GenerationState =
  | 'queued' | 'running' | 'needs_review' | 'approved'
  | 'rejected' | 'failed' | 'cancelled';

export interface TextGenerationProvider {
  generate(input: {
    kind: 'description' | 'translation';
    locale: 'ru' | 'en' | 'sr';
    facts: Record<string, string | string[]>;
    policyVersion: string;
  }): Promise<{ text: string; providerRequestId: string; usage: GenerationUsage }>;
}

export interface ImageGenerationProvider {
  submit(input: {
    prompt: string;
    aspectRatio: '1:1' | '4:3';
    callbackUrl: string;
  }): Promise<{ providerJobId: string; estimatedCostMinor: number }>;
  poll(providerJobId: string): Promise<ImageProviderResult>;
}

export interface GeneratedCandidateV1 {
  schemaVersion: 1;
  jobId: string;
  kind: GenerationKind;
  target: { menuId: string; itemId: string; locale?: string };
  text?: string;
  assetId?: string;
  policyWarnings: string[];
}

The feature consumes the core MenuDraftService, ObjectStore, asset contracts, tenant context, and LiteLLM configuration. It produces approved field or asset changes plus immutable generation audit facts.

Task 1: Persist Jobs, Candidates, Usage, and Budgets

Files:

  • Create: apps/api/src/menu/generation/entities/menu-generation-job.entity.ts

  • Create: apps/api/src/menu/generation/entities/menu-generation-candidate.entity.ts

  • Create: apps/api/src/menu/generation/entities/menu-generation-usage.entity.ts

  • Create: apps/api/src/menu/generation/menu-generation.types.ts

  • Create: apps/api/src/db/migrations/1723600000000-MenuGenerationJobs.ts

  • Modify: apps/api/src/db/data-source.ts

  • Modify: apps/api/src/menu/menu.module.ts

  • Test: apps/api/src/menu/generation/menu-generation.entity.spec.ts

  • [ ] Step 1: Write failing entity and state-machine tests

Cover state transitions, terminal immutability, unique tenant idempotency keys, one approval per candidate, integer minor-unit costs, budget floor at zero, and FORCE RLS.

  • [ ] Step 2: Run and confirm failure
bash
bun test apps/api/src/menu/generation/menu-generation.entity.spec.ts
  • [ ] Step 3: Implement entities and migration

Create tenant-scoped job, candidate, and usage tables. Add generation_monthly_budget_minor and generation_monthly_spent_minor to tenant settings, a check that both are non-negative, and an atomic reservation query guarded by spent + estimate <= budget.

  • [ ] Step 4: Register runtime and CLI entities

  • [ ] Step 5: After migration approval, run tests and a migration round trip

bash
bun test apps/api/src/menu/generation/menu-generation.entity.spec.ts
bun run test:db:up
DATABASE_URL=postgres://app:app@localhost:54332/checklist_test bun run --cwd apps/api migration:run
DATABASE_URL=postgres://app:app@localhost:54332/checklist_test bun run --cwd apps/api migration:revert
DATABASE_URL=postgres://app:app@localhost:54332/checklist_test bun run --cwd apps/api migration:run
bun run test:db:down
  • [ ] Step 6: Commit
bash
git add apps/api/src/menu/generation apps/api/src/db/data-source.ts apps/api/src/db/migrations/1723600000000-MenuGenerationJobs.ts apps/api/src/menu/menu.module.ts
git commit -m "feat(menu): persist AI generation jobs"

Task 2: Generate Policy-Bounded Descriptions

Files:

  • Create: apps/api/src/menu/generation/text/text-generation-provider.ts

  • Create: apps/api/src/menu/generation/text/litellm-text-generation.adapter.ts

  • Create: apps/api/src/menu/generation/text/fake-text-generation.adapter.ts

  • Create: apps/api/src/menu/generation/text/menu-description-policy.ts

  • Test: apps/api/src/menu/generation/text/text-generation.contract.spec.ts

  • Test: apps/api/src/menu/generation/text/menu-description-policy.spec.ts

  • [ ] Step 1: Write provider contract and policy tests

Test strict JSON output, length by locale, hostile item names, missing facts, forbidden medical/dietary claims, unsupported ingredients, provider timeout, malformed output, and deterministic fake behavior.

  • [ ] Step 2: Run and confirm failure
bash
bun test apps/api/src/menu/generation/text/text-generation.contract.spec.ts apps/api/src/menu/generation/text/menu-description-policy.spec.ts
  • [ ] Step 3: Implement the LiteLLM adapter

Call the configured /v1/chat/completions endpoint with schema { text: string }, temperature 0.2, a 20-second timeout, and no tool access. Treat menu facts as quoted untrusted data and validate the result independently of the prompt.

  • [ ] Step 4: Implement the description policy

Reject new ingredient/allergen/dietary tokens when they are not present in approved facts, reject control characters and URLs, and enforce configured character bounds per locale.

  • [ ] Step 5: Run tests
bash
bun test apps/api/src/menu/generation/text/text-generation.contract.spec.ts apps/api/src/menu/generation/text/menu-description-policy.spec.ts
  • [ ] Step 6: Commit
bash
git add apps/api/src/menu/generation/text
git commit -m "feat(menu): generate bounded descriptions"

Task 3: Generate Reviewable RU, EN, and SR Translations

Files:

  • Create: apps/api/src/menu/generation/text/menu-translation.service.ts

  • Create: apps/api/src/menu/generation/text/menu-translation.schema.ts

  • Test: apps/api/src/menu/generation/text/menu-translation.spec.ts

  • Test: apps/api/src/menu/generation/fixtures/translations.expected.json

  • [ ] Step 1: Write failing golden tests

Cover protected brand names, units, decimal separators, currencies, empty descriptions, Serbian Latin output, source-language equality, and preservation of explicit allergen statements without adding new ones.

  • [ ] Step 2: Run and confirm failure
bash
bun test apps/api/src/menu/generation/text/menu-translation.spec.ts
  • [ ] Step 3: Implement translation generation

Require { name, description }, preserve tagged protected spans, require Serbian Latin output when locale is sr, and create one candidate per target locale. Do not overwrite a non-empty translation during approval unless the request includes its current draft revision.

  • [ ] Step 4: Run golden tests
bash
bun test apps/api/src/menu/generation/text/menu-translation.spec.ts
  • [ ] Step 5: Commit
bash
git add apps/api/src/menu/generation/text apps/api/src/menu/generation/fixtures/translations.expected.json
git commit -m "feat(menu): generate menu translations"

Task 4: Generate and Validate Image Candidates

Files:

  • Create: apps/api/src/menu/generation/image/image-generation-provider.ts

  • Create: apps/api/src/menu/generation/image/http-image-generation.adapter.ts

  • Create: apps/api/src/menu/generation/image/fake-image-generation.adapter.ts

  • Create: apps/api/src/menu/generation/image/image-candidate.service.ts

  • Modify: apps/api/src/config/env.schema.ts

  • Test: apps/api/src/menu/generation/image/image-generation.contract.spec.ts

  • Test: apps/api/src/menu/generation/image/image-candidate.spec.ts

  • [ ] Step 1: Write failing provider and validation tests

Cover submit/poll/webhook convergence, signed webhook rejection, duplicate delivery, timeout, unsafe-provider result, wrong MIME, pixel/dimension bounds, corrupt output, metadata stripping, object-store failure, and cost reconciliation.

  • [ ] Step 2: Run and confirm failure
bash
bun test apps/api/src/menu/generation/image/image-generation.contract.spec.ts apps/api/src/menu/generation/image/image-candidate.spec.ts
  • [ ] Step 3: Implement the configurable HTTP adapter

Require MENU_IMAGE_PROVIDER_BASE_URL, MENU_IMAGE_PROVIDER_TOKEN, MENU_IMAGE_WEBHOOK_SECRET, and explicit timeouts. Authenticate callbacks with HMAC over the raw body and timestamp, rejecting timestamps older than five minutes.

  • [ ] Step 4: Validate and store output

Accept only clean JPEG, PNG, or WebP between 768 and 4096 pixels per side and at most 12 MiB. Re-encode through the approved image processor, remove metadata, store an immutable candidate object, and create an asset only upon approval.

  • [ ] Step 5: Run tests
bash
bun test apps/api/src/menu/generation/image/image-generation.contract.spec.ts apps/api/src/menu/generation/image/image-candidate.spec.ts
  • [ ] Step 6: Commit
bash
git add apps/api/src/menu/generation/image apps/api/src/config/env.schema.ts
git commit -m "feat(menu): generate image candidates"

Task 5: Orchestrate Jobs, Budget, and Idempotency

Files:

  • Create: apps/api/src/menu/generation/menu-generation.controller.ts

  • Create: apps/api/src/menu/generation/menu-generation.service.ts

  • Create: apps/api/src/menu/generation/workers/menu-generation.processor.ts

  • Create: apps/api/src/menu/generation/menu-generation-budget.service.ts

  • Test: apps/api/src/menu/generation/menu-generation.service.spec.ts

  • [ ] Step 1: Write failing lifecycle tests

Cover create/list/get/cancel, owner/admin/member authorization, atomic budget reservation under concurrency, estimated versus actual cost, retry without double spend, cancellation after provider submit, and daily tenant quota.

  • [ ] Step 2: Run and confirm failure
bash
bun test apps/api/src/menu/generation/menu-generation.service.spec.ts
  • [ ] Step 3: Implement endpoints and worker
text
POST /menus/:menuId/items/:itemId/generations
GET  /menus/:menuId/generations
GET  /menus/:menuId/generations/:jobId
POST /menus/:menuId/generations/:jobId/cancel

Reserve estimated cost before enqueue, use (tenant_id, idempotency_key) for convergence, reconcile actual cost once, release unused reservation, and persist provider/model/policy versions.

  • [ ] Step 4: Run lifecycle tests
bash
bun test apps/api/src/menu/generation/menu-generation.service.spec.ts
  • [ ] Step 5: Commit
bash
git add apps/api/src/menu/generation
git commit -m "feat(menu): orchestrate AI generation"

Task 6: Approve or Reject Candidates

Files:

  • Create: apps/api/src/menu/generation/menu-candidate-review.dto.ts

  • Create: apps/api/src/menu/generation/menu-candidate-review.service.ts

  • Modify: apps/api/src/menu/generation/menu-generation.controller.ts

  • Test: apps/api/src/menu/generation/menu-candidate-review.spec.ts

  • [ ] Step 1: Write failing review tests

Cover approve/reject, edited approval, stale draft revision, duplicate approval, wrong tenant, role denial, deleted item, image asset linkage, and transaction rollback.

  • [ ] Step 2: Run and confirm failure
bash
bun test apps/api/src/menu/generation/menu-candidate-review.spec.ts
  • [ ] Step 3: Implement review endpoints

POST /menus/:menuId/generations/:jobId/approve accepts { draftRevision, editedText? }; reject accepts a bounded reason code. Approval updates the target draft and audit log in one transaction and never publishes.

  • [ ] Step 4: Run review tests
bash
bun test apps/api/src/menu/generation/menu-candidate-review.spec.ts
  • [ ] Step 5: Commit
bash
git add apps/api/src/menu/generation
git commit -m "feat(menu): review generated candidates"

Task 7: Build the AI Studio UI

Files:

  • Create: apps/web/app/[locale]/app/menus/[menuId]/ai/page.tsx

  • Create: apps/web/ui/app/menus/ai/GenerationForm.tsx

  • Create: apps/web/ui/app/menus/ai/CandidateReviewCard.tsx

  • Create: apps/web/ui/app/menus/ai/BudgetMeter.tsx

  • Create: apps/web/ui/app/menus/ai/menuGenerationsClient.ts

  • Test: apps/web/ui/app/menus/ai/CandidateReviewCard.test.tsx

  • Test: apps/web/e2e/menu-ai.spec.ts

  • Modify: apps/web/messages/ru.json

  • Modify: apps/web/messages/en.json

  • Modify: apps/web/messages/sr.json

  • [ ] Step 1: Write failing UI and journey tests

Cover kind/locale selection, immutable fact preview, cost estimate, zero/exhausted budget, progress, warning display, edit-before-approve, rejection, image alt text, keyboard flow, and RU/EN/SR strings.

  • [ ] Step 2: Run and confirm failure
bash
bun test --cwd apps/web ui/app/menus/ai/CandidateReviewCard.test.tsx
  • [ ] Step 3: Implement AI Studio

Show source facts beside candidates, disclose AI generation, require a confirmation action for approval, and display budget in tenant currency without floating-point arithmetic.

  • [ ] Step 4: Run UI tests
bash
bun test --cwd apps/web ui/app/menus/ai/CandidateReviewCard.test.tsx
bun apps/web/node_modules/playwright/cli.js test apps/web/e2e/menu-ai.spec.ts
  • [ ] Step 5: Commit
bash
git add apps/web/app apps/web/ui/app/menus/ai apps/web/e2e/menu-ai.spec.ts apps/web/messages
git commit -m "feat(menu): add AI content studio"

Task 8: Prove Safety, Cost Control, and Operations

Files:

  • Create: apps/api/src/menu/generation/workers/menu-generation-retention.processor.ts

  • Create: test/menu-generation.e2e.test.ts

  • Create: test/menu-generation-policy.e2e.test.ts

  • Create: docs-internal/menu-ai-content-operations.md

  • [ ] Step 1: Write failing end-to-end safety tests

Prove prompt-injection resistance, no unapproved draft mutation, no invented restricted facts, concurrent budget enforcement, duplicate callback convergence, rejected-object retention, tenant isolation, and secret/content redaction.

  • [ ] Step 2: Implement retention, metrics, and alerts

Emit queue age, provider latency, failure code, policy rejection, estimated/actual cost, and remaining tenant budget; alert on callback-signature failures and spend reconciliation mismatch.

  • [ ] Step 3: Write the operations runbook

Document provider credentials, model/policy rollout, budget changes, callback rotation, cancellation, incident disable switch, cost reconciliation, candidate retention, and support diagnostics.

  • [ ] Step 4: Run the acceptance suite
bash
bun test apps/api/src/menu/generation
bun apps/web/node_modules/playwright/cli.js test apps/web/e2e/menu-ai.spec.ts
bun test test/menu-generation.e2e.test.ts test/menu-generation-policy.e2e.test.ts
  • [ ] Step 5: Commit
bash
git add apps/api/src/menu/generation/workers/menu-generation-retention.processor.ts test/menu-generation.e2e.test.ts test/menu-generation-policy.e2e.test.ts docs-internal/menu-ai-content-operations.md
git commit -m "test(menu): prove AI content safeguards"

Workstream Acceptance Checklist

  • [ ] Tenant generation starts disabled and cannot exceed its atomic monthly budget.
  • [ ] Text providers return schema-valid, policy-bounded candidates only.
  • [ ] Descriptions and translations do not invent restricted facts.
  • [ ] Image callbacks are authenticated, convergent, scanned, normalized, and cost-reconciled.
  • [ ] No candidate changes a draft until explicit owner/admin approval.
  • [ ] Approval is tenant-scoped, revision-checked, transactional, idempotent, and audited.
  • [ ] Rejected assets expire; accepted lineage remains traceable.
  • [ ] RU/EN/SR UI, accessibility, safety, resilience, cost, and runbook checks pass.