diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 6b2425725a..7901ce003f 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -51,10 +51,8 @@ jobs: run: bun scripts/format/sort-package-json.ts --check - name: Custom lint rules (typeof guards, raw regex, process.env) run: bun lint:custom - # TODO: remove continue-on-error once the type-cast backlog (130) is cleared. - name: Check unsafe type casts run: bun check:casts:strict - continue-on-error: true - name: Check types run: bun check-types - name: Run Expo Doctor diff --git a/apps/admin/lib/api.ts b/apps/admin/lib/api.ts index 69e03a5801..eb195ea3b0 100644 --- a/apps/admin/lib/api.ts +++ b/apps/admin/lib/api.ts @@ -40,7 +40,7 @@ async function adminFetch(path: string, init?: RequestInit): Promise { } // T is caller-verified via the typed adminFetch call-sites above. - return res.json() as Promise; + return res.json() as Promise; // safe-cast: fetch boundary — caller provides T } // ─── Stats ──────────────────────────────────────────────────────────────────── diff --git a/apps/expo/app/(app)/current-pack/[id].tsx b/apps/expo/app/(app)/current-pack/[id].tsx index b202e46c71..5fd5954669 100644 --- a/apps/expo/app/(app)/current-pack/[id].tsx +++ b/apps/expo/app/(app)/current-pack/[id].tsx @@ -196,6 +196,7 @@ export default function CurrentPackScreen() { data={pack.items} keyExtractor={(_, index) => index.toString()} renderItem={(item, index) => ( + // safe-cast: Treaty response type has createdAt?: string but PackItem schema requires string )} /> diff --git a/apps/expo/components/Icon/Icon.ios.tsx b/apps/expo/components/Icon/Icon.ios.tsx index 8475589829..1472ff8ac1 100644 --- a/apps/expo/components/Icon/Icon.ios.tsx +++ b/apps/expo/components/Icon/Icon.ios.tsx @@ -23,6 +23,8 @@ function Icon({ const prefersMaterialCommunityIcons = materialIcon?.type === 'MaterialCommunityIcons'; if (prefersMaterialCommunityIcons) { + // safe-cast: string fallback chain produces a valid MaterialCommunityIcons name at runtime; + // the icon name union is too wide for TypeScript to verify statically. const iconName = (materialIcon?.name ?? iconNames.materialCommunityIcon ?? 'help') as ComponentProps['name']; @@ -31,6 +33,8 @@ function Icon({ } if (prefersMaterialIcons) { + // safe-cast: string fallback chain produces a valid MaterialIcons name at runtime; + // the icon name union is too wide for TypeScript to verify statically. const iconName = (materialIcon?.name ?? iconNames.materialIcon ?? 'help') as ComponentProps< typeof MaterialIcons >['name']; @@ -39,6 +43,8 @@ function Icon({ } if (iconNames.materialIcon) { + // safe-cast: string fallback chain produces a valid MaterialIcons name at runtime; + // the icon name union is too wide for TypeScript to verify statically. const iconName = (materialIcon?.name ?? iconNames.materialIcon ?? 'help') as ComponentProps< typeof MaterialIcons >['name']; @@ -46,6 +52,8 @@ function Icon({ return ; } + // safe-cast: string fallback chain produces a valid MaterialCommunityIcons name at runtime; + // the icon name union is too wide for TypeScript to verify statically. const iconName = (materialIcon?.name ?? iconNames.materialCommunityIcon ?? 'help') as ComponentProps['name']; @@ -68,6 +76,8 @@ function Icon({ // Fallback to Material icons when no SF Symbol mapping exists if (iconNames.materialIcon) { + // safe-cast: string fallback chain produces a valid MaterialIcons name at runtime; + // the icon name union is too wide for TypeScript to verify statically. const iconName = (materialIcon?.name ?? iconNames.materialIcon ?? 'help') as ComponentProps< typeof MaterialIcons >['name']; @@ -75,6 +85,8 @@ function Icon({ return ; } + // safe-cast: string fallback chain produces a valid MaterialCommunityIcons name at runtime; + // the icon name union is too wide for TypeScript to verify statically. const iconName = (materialIcon?.name ?? iconNames.materialCommunityIcon ?? 'help') as ComponentProps['name']; diff --git a/apps/expo/components/Icon/Icon.tsx b/apps/expo/components/Icon/Icon.tsx index 8170705372..7f5accd2ce 100644 --- a/apps/expo/components/Icon/Icon.tsx +++ b/apps/expo/components/Icon/Icon.tsx @@ -18,6 +18,8 @@ function Icon({ const prefersMaterialCommunityIcons = materialIcon?.type === 'MaterialCommunityIcons'; if (prefersMaterialCommunityIcons) { + // safe-cast: string fallback chain produces a valid MaterialCommunityIcons name at runtime; + // the icon name union is too wide for TypeScript to verify statically. const iconName = (materialIcon?.name ?? iconNames.materialCommunityIcon ?? 'help') as ComponentProps['name']; @@ -26,6 +28,8 @@ function Icon({ } if (prefersMaterialIcons) { + // safe-cast: string fallback chain produces a valid MaterialIcons name at runtime; + // the icon name union is too wide for TypeScript to verify statically. const iconName = (materialIcon?.name ?? iconNames.materialIcon ?? 'help') as ComponentProps< typeof MaterialIcons >['name']; @@ -35,6 +39,8 @@ function Icon({ // Prefer MaterialIcons if available, otherwise use MaterialCommunityIcons if (iconNames.materialIcon) { + // safe-cast: string fallback chain produces a valid MaterialIcons name at runtime; + // the icon name union is too wide for TypeScript to verify statically. const iconName = (materialIcon?.name ?? iconNames.materialIcon ?? 'help') as ComponentProps< typeof MaterialIcons >['name']; @@ -42,6 +48,8 @@ function Icon({ return ; } + // safe-cast: string fallback chain produces a valid MaterialCommunityIcons name at runtime; + // the icon name union is too wide for TypeScript to verify statically. const iconName = (materialIcon?.name ?? iconNames.materialCommunityIcon ?? 'help') as ComponentProps['name']; diff --git a/apps/expo/components/Icon/get-icon-names.ts b/apps/expo/components/Icon/get-icon-names.ts index a69ea709c6..782683d6b1 100644 --- a/apps/expo/components/Icon/get-icon-names.ts +++ b/apps/expo/components/Icon/get-icon-names.ts @@ -29,6 +29,8 @@ export function getIconNames(namingScheme: 'sfSymbol' | 'material', name?: strin ]; if (materialCommunityIcon) { return { + // safe-cast: caller passes name under sfSymbol naming scheme; SfSymbolName is a string + // union from expo-symbols and cannot be checked without a full lookup table. sfSymbol: name as SfSymbolName, materialIcon: null, materialCommunityIcon, @@ -39,6 +41,7 @@ export function getIconNames(namingScheme: 'sfSymbol' | 'material', name?: strin SF_SYMBOLS_TO_MATERIAL_ICONS[name as keyof typeof SF_SYMBOLS_TO_MATERIAL_ICONS]; if (materialIcon) { return { + // safe-cast: same as above — sfSymbol naming scheme, string is a valid SF Symbol name sfSymbol: name as SfSymbolName, materialIcon, materialCommunityIcon: null, @@ -47,6 +50,7 @@ export function getIconNames(namingScheme: 'sfSymbol' | 'material', name?: strin // No mapping found for SF Symbol return { + // safe-cast: same as above — sfSymbol naming scheme, string is a valid SF Symbol name sfSymbol: name as SfSymbolName, materialIcon: null, materialCommunityIcon: null, @@ -61,8 +65,11 @@ export function getIconNames(namingScheme: 'sfSymbol' | 'material', name?: strin ]; if (sfSymbolFromCommunity) { return { + // safe-cast: value comes from MATERIAL_COMMUNITY_ICONS_TO_SF_SYMBOLS lookup table, + // which contains valid SF Symbol names; the full union is not statically checkable. sfSymbol: sfSymbolFromCommunity as SfSymbolName, materialIcon: null, + // safe-cast: name is a key of the MaterialCommunityIcons lookup; string checked at call site materialCommunityIcon: name as MaterialCommunityIconsProps['name'], }; } @@ -71,7 +78,9 @@ export function getIconNames(namingScheme: 'sfSymbol' | 'material', name?: strin MATERIAL_ICONS_TO_SF_SYMBOLS[name as keyof typeof MATERIAL_ICONS_TO_SF_SYMBOLS]; if (sfSymbolFromMaterial) { return { + // safe-cast: value from MATERIAL_ICONS_TO_SF_SYMBOLS lookup; valid SF Symbol name sfSymbol: sfSymbolFromMaterial as SfSymbolName, + // safe-cast: name is a key of the MaterialIcons lookup; string checked at call site materialIcon: name as MaterialIconsProps['name'], materialCommunityIcon: null, }; @@ -81,6 +90,7 @@ export function getIconNames(namingScheme: 'sfSymbol' | 'material', name?: strin return { sfSymbol: null, materialIcon: null, + // safe-cast: no mapping found; assume name is a MaterialCommunityIcons name as fallback materialCommunityIcon: name as MaterialCommunityIconsProps['name'], }; } diff --git a/apps/expo/features/ai-packs/hooks/useGeneratedPacks.ts b/apps/expo/features/ai-packs/hooks/useGeneratedPacks.ts index 7332ad11e4..2e6c304fac 100644 --- a/apps/expo/features/ai-packs/hooks/useGeneratedPacks.ts +++ b/apps/expo/features/ai-packs/hooks/useGeneratedPacks.ts @@ -9,6 +9,7 @@ import type { GenerationRequest } from '../types'; const generatePacks = async (request: GenerationRequest): Promise => { const { data, error } = await apiClient.packs['generate-packs'].post(request); if (error) throw new Error(`Failed to generate packs: ${error.value}`); + // safe-cast: treaty response shape matches Pack[] as validated by the API schema return (data ?? []) as unknown as Pack[]; }; diff --git a/apps/expo/features/ai/components/ChatBubble.tsx b/apps/expo/features/ai/components/ChatBubble.tsx index 9dbe5c3a47..fd65130483 100644 --- a/apps/expo/features/ai/components/ChatBubble.tsx +++ b/apps/expo/features/ai/components/ChatBubble.tsx @@ -141,6 +141,8 @@ export const ChatBubble = React.memo(function ChatBubble({ ); if (isAI && part.type.startsWith('tool-')) { + // safe-cast: startsWith('tool-') guard confirms this part is a ToolUIPart; ai's union + // type is too wide for TypeScript to narrow statically on a string prefix check. const toolPart = part as ToolUIPart; const toolKey = keyIn(toolPart, 'toolCallId') ? toolPart.toolCallId : key; return ( diff --git a/apps/expo/features/ai/components/ToolInvocationRenderer.tsx b/apps/expo/features/ai/components/ToolInvocationRenderer.tsx index 5555c62582..ae4746c610 100644 --- a/apps/expo/features/ai/components/ToolInvocationRenderer.tsx +++ b/apps/expo/features/ai/components/ToolInvocationRenderer.tsx @@ -17,19 +17,28 @@ interface ToolInvocationRendererProps { } export function ToolInvocationRenderer({ toolInvocation }: ToolInvocationRendererProps) { + // safe-cast: each case branch narrows toolInvocation.type to the discriminant literal; the + // local tool types (WebSearchTool, etc.) extend ToolUIPart with that exact `type` field, so + // the cast is verified by the switch guard above each arm. switch (toolInvocation.type) { case 'tool-webSearchTool': + // safe-cast: case guard narrows type to discriminant; local tool types extend ToolUIPart with that exact `type` field return ; case 'tool-getWeatherForLocation': + // safe-cast: case guard narrows type to discriminant literal return ; case 'tool-getCatalogItems': case 'tool-catalogVectorSearch': + // safe-cast: case guard narrows type to discriminant literal return ; case 'tool-searchPackratOutdoorGuidesRAG': + // safe-cast: case guard narrows type to discriminant literal return ; case 'tool-getPackDetails': + // safe-cast: case guard narrows type to discriminant literal return ; case 'tool-getPackItemDetails': + // safe-cast: case guard narrows type to discriminant literal return ; default: return null; diff --git a/apps/expo/features/ai/hooks/useReportContent.ts b/apps/expo/features/ai/hooks/useReportContent.ts index 0ddd5928cf..6b0f792c52 100644 --- a/apps/expo/features/ai/hooks/useReportContent.ts +++ b/apps/expo/features/ai/hooks/useReportContent.ts @@ -24,6 +24,7 @@ export const reportContent = async ( ...(userComment != null ? { userComment } : {}), }); if (error) throw new Error(`Failed to report content: ${error.value}`); + // safe-cast: treaty response shape matches ReportContentResponse as validated by the API schema return data as unknown as ReportContentResponse; }; diff --git a/apps/expo/features/ai/hooks/useReportedContent.ts b/apps/expo/features/ai/hooks/useReportedContent.ts index 6506c5e999..204ea674a4 100644 --- a/apps/expo/features/ai/hooks/useReportedContent.ts +++ b/apps/expo/features/ai/hooks/useReportedContent.ts @@ -26,6 +26,7 @@ type ReportedContentCount = { export const getReportedContent = async (): Promise => { const { data, error } = await apiClient.chat.reports.get(); if (error) throw new Error(`Failed to fetch reported content: ${error.value}`); + // safe-cast: treaty response shape matches ReportedContentResponse as validated by the API schema return data as unknown as ReportedContentResponse; }; diff --git a/apps/expo/features/auth/hooks/useAuthActions.ts b/apps/expo/features/auth/hooks/useAuthActions.ts index f7585eb604..fe35e09079 100644 --- a/apps/expo/features/auth/hooks/useAuthActions.ts +++ b/apps/expo/features/auth/hooks/useAuthActions.ts @@ -28,12 +28,15 @@ function redirect(route: string) { const parsedRoute: Href = JSON.parse(route); return router.dismissTo(parsedRoute); } catch { + // safe-cast: route is a plain string path from redirectToAtom (atom); + // Expo Router's Href accepts string paths directly. router.dismissTo(route as Href); } } function extractAuthError(value: unknown, fallback: string): string { if (isObject(value) && 'error' in value) { + // safe-cast: value is an object (checked above); indexed access to extract error field return String((value as Record).error) || fallback; } return fallback; @@ -66,6 +69,7 @@ export function useAuthActions() { await setToken(data.accessToken); await setRefreshToken(data.refreshToken); + // safe-cast: Treaty response type differs from local User type; Zod-validated at API boundary userStore.set(data.user as unknown as User); setNeedsReauth(false); redirect(redirectTo); @@ -96,6 +100,7 @@ export function useAuthActions() { await setToken(data.accessToken); await setRefreshToken(data.refreshToken); + // safe-cast: Treaty response type differs from local User type; Zod-validated at API boundary userStore.set(data.user as unknown as User); setNeedsReauth(false); redirect(redirectTo); @@ -142,6 +147,7 @@ export function useAuthActions() { await setToken(data.accessToken); await setRefreshToken(data.refreshToken); + // safe-cast: Treaty response type differs from local User type; Zod-validated at API boundary userStore.set(data.user as unknown as User); setNeedsReauth(false); redirect(redirectTo); @@ -248,6 +254,7 @@ export function useAuthActions() { await Storage.setItem('refresh_token', data.refreshToken); await setToken(data.accessToken); await setRefreshToken(data.refreshToken); + // safe-cast: Treaty response type differs from local User type; Zod-validated at API boundary userStore.set(data.user as unknown as User); redirect(redirectTo); } diff --git a/apps/expo/features/catalog/components/CatalogBrowserModal.tsx b/apps/expo/features/catalog/components/CatalogBrowserModal.tsx index ef6688af04..38a40684fe 100644 --- a/apps/expo/features/catalog/components/CatalogBrowserModal.tsx +++ b/apps/expo/features/catalog/components/CatalogBrowserModal.tsx @@ -164,6 +164,7 @@ export function CatalogBrowserModal({ const { recentItems } = useRecentlyUsedCatalogItems(); const { data: popularData, isLoading: isPopularLoading } = usePopularCatalogItems(8); + // safe-cast: treaty response shape matches CatalogItem[] as validated by the API schema const popularItems = (popularData?.items ?? []) as CatalogItem[]; const { @@ -187,11 +188,12 @@ export function CatalogBrowserModal({ error: searchError, } = useVectorSearch({ query: debouncedSearchValue, limit: 20 }); + // safe-cast: treaty response shape matches CatalogItem[] as validated by the API schema const items = ( isSearching ? searchResult?.items || [] : paginatedData?.pages.flatMap((page) => page.items) || [] - ) as CatalogItem[]; + ) as CatalogItem[]; // safe-cast: treaty response shape matches CatalogItem[] const isLoading = isSearching ? isSearchLoading : isPaginatedLoading; const error = isSearching ? searchError : paginatedError; diff --git a/apps/expo/features/catalog/components/SimilarItems.tsx b/apps/expo/features/catalog/components/SimilarItems.tsx index 280ecb0714..6883533197 100644 --- a/apps/expo/features/catalog/components/SimilarItems.tsx +++ b/apps/expo/features/catalog/components/SimilarItems.tsx @@ -124,7 +124,7 @@ export const SimilarItems: React.FC = ({ } keyExtractor={(item) => item.id.toString()} contentContainerStyle={{ paddingHorizontal: 16 }} diff --git a/apps/expo/features/catalog/hooks/useSimilarItems.ts b/apps/expo/features/catalog/hooks/useSimilarItems.ts index abd4f97ca0..430fe66173 100644 --- a/apps/expo/features/catalog/hooks/useSimilarItems.ts +++ b/apps/expo/features/catalog/hooks/useSimilarItems.ts @@ -29,6 +29,7 @@ export const getSimilarCatalogItems = async ( }, }); if (error) throw new Error(`Failed to fetch similar catalog items: ${error.value}`); + // safe-cast: treaty response shape matches SimilarItemsResponse as validated by the API schema return data as unknown as SimilarItemsResponse; }; diff --git a/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx b/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx index 6513d6fb23..4fe67890bf 100644 --- a/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx +++ b/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx @@ -67,11 +67,14 @@ function CatalogItemsScreen() { isLoading: isVectorLoading, error: vectorError, } = useVectorSearch({ query: trimmedQuery, limit: 10 }); + // safe-cast: treaty response shape matches CatalogItem[] as validated by the API schema const searchResults: CatalogItem[] = (vectorResult?.items ?? []) as unknown as CatalogItem[]; - const paginatedItems: CatalogItem[] = ( - (paginatedData?.pages.flatMap((page) => page.items) ?? []) as CatalogItem[] - ).filter((item) => Boolean(item?.id)); + const paginatedItems: CatalogItem[] = + // safe-cast: treaty response shape matches CatalogItem[] as validated by the API schema + ((paginatedData?.pages.flatMap((page) => page.items) ?? []) as CatalogItem[]).filter((item) => + Boolean(item?.id), + ); const totalItems = paginatedData?.pages[0]?.totalCount ?? 0; diff --git a/apps/expo/features/pack-templates/hooks/useGenerateTemplateFromOnlineContent.ts b/apps/expo/features/pack-templates/hooks/useGenerateTemplateFromOnlineContent.ts index a285c75c60..4276262235 100644 --- a/apps/expo/features/pack-templates/hooks/useGenerateTemplateFromOnlineContent.ts +++ b/apps/expo/features/pack-templates/hooks/useGenerateTemplateFromOnlineContent.ts @@ -58,12 +58,14 @@ export function useGenerateTemplateFromOnlineContent() { // server returns a structured object (e.g. { code, existingTemplateId }) // we extract it onto the thrown ImportError so callers can branch on // the duplicate-detection path. + // safe-cast: treaty surfaces error.value as unknown; we probe its shape before use const value = error.value as | { error?: string; code?: string; existingTemplateId?: string } | string | null | undefined; const message = isObject(value) && value?.error ? value.error : (value ?? 'Import failed'); + // safe-cast: augmenting the base Error with ImportError fields assigned immediately below const importError = new Error(String(message)) as ImportError; importError.status = error.status; if (isObject(value)) { @@ -72,6 +74,7 @@ export function useGenerateTemplateFromOnlineContent() { } throw importError; } + // safe-cast: treaty response shape matches GeneratedTemplate as validated by the API schema return data as unknown as GeneratedTemplate; }, onSuccess: (data) => { diff --git a/apps/expo/features/pack-templates/hooks/usePackTemplateSummary.ts b/apps/expo/features/pack-templates/hooks/usePackTemplateSummary.ts index f3e874881c..56a92edf6a 100644 --- a/apps/expo/features/pack-templates/hooks/usePackTemplateSummary.ts +++ b/apps/expo/features/pack-templates/hooks/usePackTemplateSummary.ts @@ -30,8 +30,7 @@ export function usePackTemplateSummary( for (const item of Object.values(items)) { if (item.packTemplateId !== templateId || item.deleted) continue; itemCount++; - const itemWeightInGrams = - convertToGrams(item.weight, item.weightUnit as WeightUnit) * item.quantity; + const itemWeightInGrams = convertToGrams(item.weight, item.weightUnit) * item.quantity; totalWeightGrams += itemWeightInGrams; if (!item.consumable && !item.worn) { baseWeightGrams += itemWeightInGrams; @@ -73,8 +72,7 @@ export function usePackTemplateSummaries( const bucket = grams[item.packTemplateId]; if (!bucket) continue; bucket.count++; - const itemWeightInGrams = - convertToGrams(item.weight, item.weightUnit as WeightUnit) * item.quantity; + const itemWeightInGrams = convertToGrams(item.weight, item.weightUnit) * item.quantity; bucket.total += itemWeightInGrams; if (!item.consumable && !item.worn) { bucket.base += itemWeightInGrams; diff --git a/apps/expo/features/pack-templates/screens/ItemsScanScreen.tsx b/apps/expo/features/pack-templates/screens/ItemsScanScreen.tsx index ff5cb8f454..2ea880be38 100644 --- a/apps/expo/features/pack-templates/screens/ItemsScanScreen.tsx +++ b/apps/expo/features/pack-templates/screens/ItemsScanScreen.tsx @@ -23,6 +23,7 @@ export function ItemsScanScreen() { const { t } = useTranslation(); const { packTemplateId, ...fileInfo } = useLocalSearchParams(); const [hasRunInitialScanOnMount, setHasRunInitialScanOnMount] = useState(false); + // safe-cast: expo-router query params are untyped strings; fileInfo carries uri/fileName/type const { selectedImage, pickImage, takePhoto } = useImagePicker(fileInfo as SelectedImage); const { showActionSheetWithOptions } = useActionSheet(); const [selectedCatalogItems, setSelectedCatalogItems] = useState>(new Set()); diff --git a/apps/expo/features/pack-templates/store/packTemplateItems.ts b/apps/expo/features/pack-templates/store/packTemplateItems.ts index 3435127cdf..239c556c6c 100644 --- a/apps/expo/features/pack-templates/store/packTemplateItems.ts +++ b/apps/expo/features/pack-templates/store/packTemplateItems.ts @@ -14,9 +14,12 @@ import type { PackTemplateItem } from '../types'; const listAllPackTemplateItems = async (): Promise => { const { data, error } = await apiClient['pack-templates'].get(); if (error) throw new Error(`Failed to list PackTemplateItems: ${error.value}`); - return PackTemplateWithItemsSchema.array() - .parse(data) - .flatMap((template) => template.items) as unknown as PackTemplateItem[]; + return ( + PackTemplateWithItemsSchema.array() + .parse(data) + // safe-cast: Zod parse validates the shape; PackTemplateItem extends the Zod-inferred type + .flatMap((template) => template.items) as unknown as PackTemplateItem[] + ); }; const createPackTemplateItem = async (item: PackTemplateItem): Promise => { @@ -36,6 +39,7 @@ const createPackTemplateItem = async (item: PackTemplateItem): Promise => { const { data, error } = await apiClient['pack-templates'].get(); if (error) throw new Error(`Failed to list pack templates: ${error.value}`); + // safe-cast: Zod parse validates the shape; PackTemplateInStore extends the Zod-inferred type return PackTemplateWithItemsSchema.array().parse(data) as unknown as PackTemplateInStore[]; }; @@ -34,6 +35,7 @@ const createPackTemplate = async ( localUpdatedAt: templateData.localUpdatedAt ?? new Date().toISOString(), }); if (error) throw new Error(`Failed to create pack template: ${error.value}`); + // safe-cast: Zod parse validates the shape; PackTemplateInStore extends the Zod-inferred type return PackTemplateSchema.parse(data) as unknown as PackTemplateInStore; }; @@ -57,6 +59,7 @@ const updatePackTemplate = async ({ ...(data.localUpdatedAt ? { localUpdatedAt: data.localUpdatedAt } : {}), }); if (error) throw new Error(`Failed to update pack template: ${error.value}`); + // safe-cast: Zod parse validates the shape; PackTemplateInStore extends the Zod-inferred type return PackTemplateSchema.parse(result) as unknown as PackTemplateInStore; }; diff --git a/apps/expo/features/packs/components/PackCard.tsx b/apps/expo/features/packs/components/PackCard.tsx index 31e8d5279b..449b1954e7 100644 --- a/apps/expo/features/packs/components/PackCard.tsx +++ b/apps/expo/features/packs/components/PackCard.tsx @@ -31,9 +31,8 @@ export function PackCard({ const insets = useSafeAreaInsets(); const isOwnedByUser = usePackOwnershipCheck(packArg.id); const packFromStore = usePackDetailsFromStore(packArg.id); // Use pack from store if it's owned by the current user so that component observe changes to it and thus update properly. - // packFromStore is always Pack (with computed weights); packArg may be Pack | PackInStore - // Cast: PackInStore lacks computed weight fields (baseWeight, totalWeight) that Pack has. - // We guard access to those fields with 'in' checks; the cast is safe at runtime. + // safe-cast: when isOwnedByUser, packFromStore is a full Pack with computed weights; + // when not owned, packArg is also accepted as Pack (caller must supply the full shape). const pack = (isOwnedByUser ? packFromStore : packArg) as Pack; const handleActionsPress = () => { diff --git a/apps/expo/features/packs/components/SimilarItemsForPackItem.tsx b/apps/expo/features/packs/components/SimilarItemsForPackItem.tsx index 81b913fa8a..0d4d25aa93 100644 --- a/apps/expo/features/packs/components/SimilarItemsForPackItem.tsx +++ b/apps/expo/features/packs/components/SimilarItemsForPackItem.tsx @@ -123,6 +123,7 @@ export const SimilarItemsForPackItem: React.FC = ( } keyExtractor={(item) => item.id.toString()} diff --git a/apps/expo/features/packs/hooks/useDuplicatePack.ts b/apps/expo/features/packs/hooks/useDuplicatePack.ts index d26df7ca08..7570426363 100644 --- a/apps/expo/features/packs/hooks/useDuplicatePack.ts +++ b/apps/expo/features/packs/hooks/useDuplicatePack.ts @@ -21,6 +21,7 @@ export function useDuplicatePack() { queryFn: async () => { const { data, error } = await apiClient.packs({ packId }).get(); if (error) throw new Error(`Failed to fetch pack: ${error.value}`); + // safe-cast: treaty response shape matches Pack as validated by the API schema return data as unknown as Pack; }, }); diff --git a/apps/expo/features/packs/hooks/useImageDetection.ts b/apps/expo/features/packs/hooks/useImageDetection.ts index 5bc89bdac2..89b0b48ec7 100644 --- a/apps/expo/features/packs/hooks/useImageDetection.ts +++ b/apps/expo/features/packs/hooks/useImageDetection.ts @@ -38,6 +38,7 @@ export function useImageDetection() { matchLimit, }); if (error) throw new Error(`Failed to analyze image: ${error.value}`); + // safe-cast: treaty response shape matches AnalyzeImageResponse as validated by the API schema return data as unknown as AnalyzeImageResponse; }, }); diff --git a/apps/expo/features/packs/hooks/usePackGapAnalysis.ts b/apps/expo/features/packs/hooks/usePackGapAnalysis.ts index 55afdc220d..3387d24278 100644 --- a/apps/expo/features/packs/hooks/usePackGapAnalysis.ts +++ b/apps/expo/features/packs/hooks/usePackGapAnalysis.ts @@ -28,6 +28,7 @@ export const analyzePackGaps = async ( ): Promise => { const { data, error } = await apiClient.packs({ packId })['gap-analysis'].post(context ?? {}); if (error) throw new Error(`Failed to analyze pack gaps: ${error.value}`); + // safe-cast: treaty response shape matches GapAnalysisResponse as validated by the API schema return data as unknown as GapAnalysisResponse; }; diff --git a/apps/expo/features/packs/hooks/usePackItemDetailsFromApi.ts b/apps/expo/features/packs/hooks/usePackItemDetailsFromApi.ts index d58b459de5..8229888c23 100644 --- a/apps/expo/features/packs/hooks/usePackItemDetailsFromApi.ts +++ b/apps/expo/features/packs/hooks/usePackItemDetailsFromApi.ts @@ -7,6 +7,7 @@ import type { PackItem } from '../types'; export async function fetchPackItemById(id: string) { const { data, error } = await apiClient.packs.items({ itemId: id }).get(); if (error) throw new Error(`Failed to fetch pack item: ${error.value}`); + // safe-cast: Zod parse validates the shape; TypeScript types diverge from Zod-inferred type return PackItemSchema.parse(data) as unknown as PackItem; } diff --git a/apps/expo/features/packs/hooks/useSeasonSuggestions.ts b/apps/expo/features/packs/hooks/useSeasonSuggestions.ts index 7adcee2c8c..4aa43cbf17 100644 --- a/apps/expo/features/packs/hooks/useSeasonSuggestions.ts +++ b/apps/expo/features/packs/hooks/useSeasonSuggestions.ts @@ -23,6 +23,7 @@ const generateSeasonSuggestions = async ( ): Promise => { const { data: result, error } = await apiClient['season-suggestions'].post(data); if (error) throw new Error(`Failed to generate season suggestions: ${error.value}`); + // safe-cast: treaty response shape matches SeasonSuggestionsResponse as validated by the API schema return result as unknown as SeasonSuggestionsResponse; }; diff --git a/apps/expo/features/packs/screens/ItemsScanScreen.tsx b/apps/expo/features/packs/screens/ItemsScanScreen.tsx index d3e2ef5ae1..4f6093118f 100644 --- a/apps/expo/features/packs/screens/ItemsScanScreen.tsx +++ b/apps/expo/features/packs/screens/ItemsScanScreen.tsx @@ -23,6 +23,7 @@ export function ItemsScanScreen() { const { colors } = useColorScheme(); const { packId, ...fileInfo } = useLocalSearchParams(); const [hasRunInitialScanOnMount, setHasRunInitialScanOnMount] = useState(false); + // safe-cast: expo-router query params are untyped strings; fileInfo carries uri/fileName/type const { selectedImage, pickImage, takePhoto } = useImagePicker(fileInfo as SelectedImage); const { showActionSheetWithOptions } = useActionSheet(); const [selectedCatalogItems, setSelectedCatalogItems] = useState>(new Set()); diff --git a/apps/expo/features/packs/screens/PackDetailScreen.tsx b/apps/expo/features/packs/screens/PackDetailScreen.tsx index 4bf25ddd24..0ed7252c7f 100644 --- a/apps/expo/features/packs/screens/PackDetailScreen.tsx +++ b/apps/expo/features/packs/screens/PackDetailScreen.tsx @@ -69,8 +69,8 @@ export function PackDetailScreen() { enabled: !isOwnedByUser, }); - // Cast: TypeScript can't track narrowing through closures defined before the - // early-return guard at line ~415; the guard ensures pack is defined at render time. + // safe-cast: pack is guaranteed non-undefined by the early-return guard below; + // TypeScript cannot track narrowing across the closure boundary. const pack = (isOwnedByUser ? packFromStore : packFromApi) as Pack; const { colors } = useColorScheme(); diff --git a/apps/expo/features/packs/store/packItems.ts b/apps/expo/features/packs/store/packItems.ts index 30ec530a87..847a3b60ee 100644 --- a/apps/expo/features/packs/store/packItems.ts +++ b/apps/expo/features/packs/store/packItems.ts @@ -13,9 +13,12 @@ import { uploadImage } from '../utils'; const listAllPackItems = async (): Promise => { const { data, error } = await apiClient.packs.get({ query: { includePublic: 0 } }); if (error) throw new Error(`Failed to list packitems: ${error.value}`); - return PackWithWeightsSchema.array() - .parse(data) - .flatMap((pack) => pack.items ?? []) as unknown as PackItem[]; + return ( + PackWithWeightsSchema.array() + .parse(data) + // safe-cast: Zod parse validates the shape; PackItem extends the Zod-inferred type + .flatMap((pack) => pack.items ?? []) as unknown as PackItem[] + ); }; const createPackItem = async ({ packId, ...data }: PackItem): Promise => { @@ -26,6 +29,7 @@ const createPackItem = async ({ packId, ...data }: PackItem): Promise => { const { data, error } = await apiClient.packs.get({ query: { includePublic: 0 } }); if (error) throw new Error(`Failed to list packs: ${error.value}`); + // safe-cast: Zod parse validates the shape; PackInStore extends the Zod-inferred type with local store fields return PackWithWeightsSchema.array().parse(data) as unknown as PackInStore[]; }; @@ -27,6 +28,7 @@ const createPack = async (packData: PackInStore): Promise => localUpdatedAt: packData.localUpdatedAt ?? new Date().toISOString(), }); if (error) throw new Error(`Failed to create pack: ${error.value}`); + // safe-cast: Zod parse validates the shape; PackInStore extends the Zod-inferred type with local store fields return PackWithWeightsSchema.parse(data) as unknown as PackInStore; }; @@ -42,6 +44,7 @@ const updatePack = async ({ id, ...data }: Partial): Promise HAZARD_LABELS[h.toLowerCase()] ?? h); diff --git a/apps/expo/features/trail-conditions/hooks/useTrailConditionReports.ts b/apps/expo/features/trail-conditions/hooks/useTrailConditionReports.ts index f49cd533b5..f0887a9dad 100644 --- a/apps/expo/features/trail-conditions/hooks/useTrailConditionReports.ts +++ b/apps/expo/features/trail-conditions/hooks/useTrailConditionReports.ts @@ -34,6 +34,7 @@ async function readCachedReports(opts: { }): Promise { try { const raw = await AsyncStorage.getItem(cacheKey(opts.userId, opts.trailName)); + // safe-cast: JSON.parse returns unknown; data was written as TrailConditionReport[] earlier if (raw) return JSON.parse(raw) as TrailConditionReport[]; } catch { // Corrupt or missing cache — ignore @@ -51,6 +52,7 @@ export const fetchTrailConditionReports = async ( console.error('Failed to fetch trail condition reports:', error.value); throw new Error(`Failed to fetch trail condition reports: ${error.value}`); } + // safe-cast: treaty response shape matches TrailConditionReport[] as validated by the API schema return (data ?? []) as unknown as TrailConditionReport[]; }; @@ -68,6 +70,7 @@ export function useTrailConditionReports(trailName?: string) { // Read locally-stored reports (user's own, offline-persisted) as fallback const localReports = useSelector(() => { const store = trailConditionReportsStore.get(); + // safe-cast: Legend-State observable record values are typed as TrailConditionReport return Object.values(store).filter((r) => !r.deleted) as TrailConditionReport[]; }); diff --git a/apps/expo/features/trips/components/TripForm.tsx b/apps/expo/features/trips/components/TripForm.tsx index 3d75468bde..c361f768e2 100644 --- a/apps/expo/features/trips/components/TripForm.tsx +++ b/apps/expo/features/trips/components/TripForm.tsx @@ -109,6 +109,8 @@ export const TripForm = ({ trip }: { trip?: Trip }) => { startDate: formatDate(trip?.startDate || ''), endDate: formatDate(trip?.endDate || ''), packId: trip?.packId, + // safe-cast: defaultValues object matches TripFormValues shape; useForm generic infers + // narrower literal types from the object literal without the cast. } as TripFormValues, validators: { onChange: tripFormSchema }, onSubmit: async ({ value }) => { diff --git a/apps/expo/features/trips/screens/TripDetailScreen.tsx b/apps/expo/features/trips/screens/TripDetailScreen.tsx index a717196390..e7ffa7da87 100644 --- a/apps/expo/features/trips/screens/TripDetailScreen.tsx +++ b/apps/expo/features/trips/screens/TripDetailScreen.tsx @@ -22,6 +22,8 @@ export function TripDetailScreen() { const [showConditionReport, setShowConditionReport] = useState(false); + // safe-cast: trip may be undefined before the store is hydrated; the guard at line ~38 handles + // the undefined case and returns early, ensuring trip is non-null at render time below. const trip = useTripDetailsFromStore(id as string) as Trip; const packs = useDetailedPacks(); diff --git a/apps/expo/features/weather/hooks/useLocationRefresh.ts b/apps/expo/features/weather/hooks/useLocationRefresh.ts index 3106482a5a..c9ad3a8e92 100644 --- a/apps/expo/features/weather/hooks/useLocationRefresh.ts +++ b/apps/expo/features/weather/hooks/useLocationRefresh.ts @@ -18,6 +18,7 @@ export function useLocationRefresh() { if (weatherData) { const formattedData = formatWeatherData(weatherData); + // safe-cast: formattedData is shaped by weatherService which guarantees WeatherLocation structure updateLocation(locationId, formattedData as unknown as Partial); return true; @@ -47,6 +48,7 @@ export function useLocationRefresh() { if (weatherData) { const formattedData = formatWeatherData(weatherData); + // safe-cast: formattedData is shaped by weatherService which guarantees WeatherLocation structure updateLocation(location.id, formattedData as unknown as Partial); } } catch (error) { diff --git a/apps/expo/features/weather/hooks/useLocationSearch.ts b/apps/expo/features/weather/hooks/useLocationSearch.ts index 523b036792..36574bf651 100644 --- a/apps/expo/features/weather/hooks/useLocationSearch.ts +++ b/apps/expo/features/weather/hooks/useLocationSearch.ts @@ -65,6 +65,7 @@ export function useLocationSearch() { const formattedData = formatWeatherData(weatherData); // Create new location with weather data + // safe-cast: formattedData is shaped by weatherService which guarantees WeatherLocation structure const newLocation = formattedData as unknown as WeatherLocation; addLocation(newLocation); diff --git a/apps/expo/features/weather/hooks/useWeatherAlert.ts b/apps/expo/features/weather/hooks/useWeatherAlert.ts index e5a64c00bc..1a462f2ad2 100644 --- a/apps/expo/features/weather/hooks/useWeatherAlert.ts +++ b/apps/expo/features/weather/hooks/useWeatherAlert.ts @@ -247,6 +247,8 @@ export function useWeatherAlerts() { try { const data = await getWeatherData(locationId); + // safe-cast: getWeatherData returns WeatherApiForecastResponse; WeatherApiData is a + // structural subset of that type used only by this alert generator. const formatted = generateAlerts(data as unknown as WeatherApiData, activeLocation); setAlerts(formatted); } catch (err) { diff --git a/apps/expo/features/weather/screens/LocationPreviewScreen.tsx b/apps/expo/features/weather/screens/LocationPreviewScreen.tsx index c6b8affe81..b3ea514a48 100644 --- a/apps/expo/features/weather/screens/LocationPreviewScreen.tsx +++ b/apps/expo/features/weather/screens/LocationPreviewScreen.tsx @@ -56,6 +56,7 @@ export default function LocationPreviewScreen() { const data = await getWeatherData(locationId); if (data) { const formattedData = formatWeatherData(data); + // safe-cast: formattedData is shaped by weatherService which guarantees WeatherLocation structure setWeatherData(formattedData as unknown as WeatherLocation); // Update gradient colors based on weather condition diff --git a/apps/expo/features/wildlife/hooks/useWildlifeIdentification.ts b/apps/expo/features/wildlife/hooks/useWildlifeIdentification.ts index 9c9090eb68..0dc6794346 100644 --- a/apps/expo/features/wildlife/hooks/useWildlifeIdentification.ts +++ b/apps/expo/features/wildlife/hooks/useWildlifeIdentification.ts @@ -26,6 +26,7 @@ async function identifyOnline(selectedImage: SelectedImage): Promise(store: Observable>, id: string): Observable { + // safe-cast: Legend-State v3 uses JavaScript Proxy for deep reactive access via store[id]; + // TypeScript resolves store[id] to T rather than Observable, so we bridge with a single cast. const observable = (store as unknown as Record>)[id]; assertDefined(observable); return observable; diff --git a/apps/guides/app/dev/generate/page.tsx b/apps/guides/app/dev/generate/page.tsx index 5d3c411b12..875c2887af 100644 --- a/apps/guides/app/dev/generate/page.tsx +++ b/apps/guides/app/dev/generate/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { guideEnv } from '@packrat/env/next'; +import { assertEnum } from '@packrat/guards'; import { Badge } from '@packrat/web-ui/components/badge'; import { Button } from '@packrat/web-ui/components/button'; import { @@ -284,18 +285,20 @@ export default function GeneratePage() {
- {Object.entries(CATEGORY_DISPLAY_NAMES).map(([key, name]) => ( -
- handleCategoryToggle(key as ContentCategory)} - /> - -
- ))} + {(Object.entries(CATEGORY_DISPLAY_NAMES) as [ContentCategory, string][]).map( + ([key, name]) => ( +
+ handleCategoryToggle(key)} + /> + +
+ ), + )}
@@ -303,7 +306,10 @@ export default function GeneratePage() {