Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/admin/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ async function adminFetch<T>(path: string, init?: RequestInit): Promise<T> {
}

// T is caller-verified via the typed adminFetch<T> call-sites above.
return res.json() as Promise<T>;
return res.json() as Promise<T>; // safe-cast: fetch boundary — caller provides T
}

// ─── Stats ────────────────────────────────────────────────────────────────────
Expand Down
1 change: 1 addition & 0 deletions apps/expo/app/(app)/current-pack/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
<ItemRow item={item as unknown as PackItem} index={index} />
)}
/>
Expand Down
12 changes: 12 additions & 0 deletions apps/expo/components/Icon/Icon.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof MaterialCommunityIcons>['name'];
Expand All @@ -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'];
Expand All @@ -39,13 +43,17 @@ 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'];
const { type: _type, name: _name, ...restProps } = materialIcon || {};
return <MaterialIcons {...restProps} name={iconName} size={size} color={color} />;
}

// 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<typeof MaterialCommunityIcons>['name'];
Expand All @@ -68,13 +76,17 @@ 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'];
const { type: _type, name: _name, ...restProps } = materialIcon || {};
return <MaterialIcons {...restProps} name={iconName} size={size} color={color} />;
}

// 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<typeof MaterialCommunityIcons>['name'];
Expand Down
8 changes: 8 additions & 0 deletions apps/expo/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof MaterialCommunityIcons>['name'];
Expand All @@ -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'];
Expand All @@ -35,13 +39,17 @@ 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'];
const { type: _type, name: _name, ...restProps } = materialIcon || {};
return <MaterialIcons {...restProps} name={iconName} size={size} color={color} />;
}

// 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<typeof MaterialCommunityIcons>['name'];
Expand Down
10 changes: 10 additions & 0 deletions apps/expo/components/Icon/get-icon-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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'],
};
}
Expand All @@ -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,
};
Expand All @@ -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'],
};
}
1 change: 1 addition & 0 deletions apps/expo/features/ai-packs/hooks/useGeneratedPacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { GenerationRequest } from '../types';
const generatePacks = async (request: GenerationRequest): Promise<Pack[]> => {
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[];
};

Expand Down
2 changes: 2 additions & 0 deletions apps/expo/features/ai/components/ChatBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
9 changes: 9 additions & 0 deletions apps/expo/features/ai/components/ToolInvocationRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <WebSearchGenerativeUI toolInvocation={toolInvocation as WebSearchTool} />;
case 'tool-getWeatherForLocation':
// safe-cast: case guard narrows type to discriminant literal
return <WeatherGenerativeUI toolInvocation={toolInvocation as WeatherTool} />;
case 'tool-getCatalogItems':
case 'tool-catalogVectorSearch':
// safe-cast: case guard narrows type to discriminant literal
return <CatalogItemsGenerativeUI toolInvocation={toolInvocation as CatalogItemsTool} />;
case 'tool-searchPackratOutdoorGuidesRAG':
// safe-cast: case guard narrows type to discriminant literal
return <GuidesRAGGenerativeUI toolInvocation={toolInvocation as GuidesRAGTool} />;
case 'tool-getPackDetails':
// safe-cast: case guard narrows type to discriminant literal
return <PackDetailsGenerativeUI toolInvocation={toolInvocation as PackDetailsTool} />;
case 'tool-getPackItemDetails':
// safe-cast: case guard narrows type to discriminant literal
return <PackItemDetailsGenerativeUI toolInvocation={toolInvocation as PackItemTool} />;
default:
return null;
Expand Down
1 change: 1 addition & 0 deletions apps/expo/features/ai/hooks/useReportContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
1 change: 1 addition & 0 deletions apps/expo/features/ai/hooks/useReportedContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type ReportedContentCount = {
export const getReportedContent = async (): Promise<ReportedContentResponse> => {
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;
};

Expand Down
7 changes: 7 additions & 0 deletions apps/expo/features/auth/hooks/useAuthActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>);
// 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<string, unknown>).error) || fallback;
}
return fallback;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion apps/expo/features/catalog/components/SimilarItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const SimilarItems: React.FC<SimilarItemsProps> = ({
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
data={data.items as SimilarItem[]}
data={data.items}
renderItem={({ item }) => <SimilarItemCard item={item} onPress={handleItemPress} />}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={{ paddingHorizontal: 16 }}
Expand Down
1 change: 1 addition & 0 deletions apps/expo/features/catalog/hooks/useSimilarItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
9 changes: 6 additions & 3 deletions apps/expo/features/catalog/screens/CatalogItemsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Set<number>>(new Set());
Expand Down
Loading
Loading