refactor(mana/web): consume shared AI Zod schemas via z.infer

Drops the hand-written MealAnalysisResult / AnalyzedFood / NutritionData
interfaces in nutriphi/{api,types}.ts and the IdentifyResult interface
in planta/api.ts. They are now type aliases that re-export the inferred
types from @mana/shared-types — same types the backend validates against
at the boundary, so frontend and backend can no longer drift.

Net result is end-to-end type safety: a field rename in the shared
schema lights up red in both apps/api routes and apps/mana/apps/web
consumers in the same tsc pass. No more interface duplication, no more
manual sync.

Storage shapes (LocalMeal, LocalGoal, LocalFavorite) stay module-local
because they compose the shared NutritionData / AnalyzedFood with
storage-specific BaseRecord fields (id, userId, _fieldTimestamps,
deletedAt, etc.) that have no place in the wire format.

Tests: 29/29 nutriphi + 20/20 planta still green — the shapes are
identical, only the source of the type aliases changed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-09 17:00:13 +02:00
parent 0c0e31d2f3
commit f9b83990c6
3 changed files with 19 additions and 42 deletions

View file

@ -9,7 +9,11 @@
import { authStore } from '$lib/stores/auth.svelte';
import { getManaApiUrl } from '$lib/api/config';
import type { NutritionData } from './types';
// Wire format is the single source of truth in @mana/shared-types —
// the backend validates AI responses with these same Zod schemas.
import type { MealAnalysis } from '@mana/shared-types';
export type MealAnalysisResult = MealAnalysis;
export interface UploadMealPhotoResult {
mediaId: string;
@ -18,21 +22,6 @@ export interface UploadMealPhotoResult {
storagePath: string;
}
export interface AnalyzedFood {
name: string;
quantity?: string;
calories?: number;
}
export interface MealAnalysisResult {
foods?: AnalyzedFood[];
totalNutrition?: NutritionData;
description?: string;
confidence?: number;
warnings?: string[];
suggestions?: string[];
}
async function authHeader(): Promise<Record<string, string>> {
const token = await authStore.getAccessToken();
return token ? { Authorization: `Bearer ${token}` } : {};

View file

@ -1,28 +1,21 @@
/**
* NutriPhi module types for the unified app.
*
* NutritionData and AnalyzedFood are re-exported from @mana/shared-types
* because they double as the AI wire format (same Zod schema lives in
* packages/shared-types/src/ai-schemas.ts and is used by the backend
* generateObject() validator). Module-local types like LocalMeal compose
* those shared shapes with storage-specific BaseRecord fields.
*/
import type { BaseRecord } from '@mana/local-store';
import type { NutritionData, AnalyzedFood } from '@mana/shared-types';
export type { NutritionData, AnalyzedFood };
export type MealType = 'breakfast' | 'lunch' | 'dinner' | 'snack';
export type InputType = 'photo' | 'text';
export interface NutritionData {
calories: number;
protein: number;
carbohydrates: number;
fat: number;
fiber: number;
sugar: number;
}
/** A single food item identified by Gemini Vision in a meal photo. */
export interface AnalyzedFood {
name: string;
quantity?: string | null;
calories?: number | null;
}
export interface LocalMeal extends BaseRecord {
date: string;
mealType: MealType;

View file

@ -8,6 +8,11 @@
import { authStore } from '$lib/stores/auth.svelte';
import { getManaApiUrl } from '$lib/api/config';
// Wire format is the single source of truth in @mana/shared-types —
// the backend validates AI responses with the same Zod schema.
import type { PlantIdentification } from '@mana/shared-types';
export type IdentifyResult = PlantIdentification;
export interface UploadPhotoResult {
storagePath: string;
@ -16,16 +21,6 @@ export interface UploadPhotoResult {
plantId: string | null;
}
export interface IdentifyResult {
scientificName?: string;
commonNames?: string[];
confidence?: number;
healthAssessment?: string;
wateringAdvice?: string;
lightAdvice?: string;
generalTips?: string[];
}
async function authHeader(): Promise<Record<string, string>> {
const token = await authStore.getAccessToken();
return token ? { Authorization: `Bearer ${token}` } : {};