diff --git a/apps/guides/components/search.tsx b/apps/guides/components/search.tsx
index 24e809904c..734ffdb0c7 100644
--- a/apps/guides/components/search.tsx
+++ b/apps/guides/components/search.tsx
@@ -28,6 +28,7 @@ export function Search() {
// Handle click outside to close search
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
+ // safe-cast: MouseEvent.target is always a DOM Node in this context
if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
diff --git a/apps/guides/scripts/sync-to-r2.ts b/apps/guides/scripts/sync-to-r2.ts
index 5919918221..044e177215 100644
--- a/apps/guides/scripts/sync-to-r2.ts
+++ b/apps/guides/scripts/sync-to-r2.ts
@@ -38,8 +38,10 @@ console.log(`🔄 Force sync: ${forceSync}`);
async function syncGuidesToR2() {
try {
// Initialize R2 bucket service
+ // safe-cast: SyncEnv carries the S3/R2 credential subset that R2BucketService needs at runtime;
+ // the remaining ValidatedEnv fields are not accessed by the guides-bucket code path.
const bucket = new R2BucketService({
- env: env as unknown as ValidatedEnv,
+ env: env as unknown as ValidatedEnv, // safe-cast: SyncEnv carries the S3/R2 credentials R2BucketService needs
bucketType: 'guides',
});
diff --git a/apps/landing/lib/icons.tsx b/apps/landing/lib/icons.tsx
index ea685d7516..6365a3b6ba 100644
--- a/apps/landing/lib/icons.tsx
+++ b/apps/landing/lib/icons.tsx
@@ -4,8 +4,9 @@ import * as LucideIcons from 'lucide-react';
export const LucideIcon = (name: string): LucideIconType => {
if (name in LucideIcons) {
+ // safe-cast: lucide-react module exports are typed — dynamic lookup by name is safe after isFunction guard
const icon = (LucideIcons as Record)[name];
- if (isFunction(icon)) return icon as LucideIconType;
+ if (isFunction(icon)) return icon as LucideIconType; // safe-cast: lucide-react module exports are typed
}
return LucideIcons.FileQuestion;
};
diff --git a/lefthook.yml b/lefthook.yml
index 00cb9c150b..821ba9eadc 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -12,9 +12,7 @@ pre-push:
- run: test "${CI:-false}" = "true"
- run: test "${GITHUB_ACTIONS:-false}" = "true"
commands:
- # Only gates on checks that are currently clean.
- # Add each check back here as its backlog is cleared.
- # Remaining backlog (CI continue-on-error): check-type-casts
+ # All custom checks are now clean — no continue-on-error backlog remaining.
clean-checks:
run: >
bun scripts/lint/no-raw-typeof.ts &&
@@ -23,5 +21,6 @@ pre-push:
bun scripts/lint/no-circular-deps.ts &&
bun scripts/lint/no-duplicate-deps.ts &&
bun scripts/lint/no-duplicate-guards.ts &&
- bun scripts/format/sort-package-json.ts --check
+ bun scripts/format/sort-package-json.ts --check &&
+ bun check:casts:strict
fail_text: "Pre-push checks failed! Run `bun check:all` for the full picture."
diff --git a/packages/analytics/src/core/enrichment.ts b/packages/analytics/src/core/enrichment.ts
index 3c6c43f6b9..b92c1e3a9d 100644
--- a/packages/analytics/src/core/enrichment.ts
+++ b/packages/analytics/src/core/enrichment.ts
@@ -284,7 +284,7 @@ export class Enrichment {
if (col === undefined) continue;
obj[col] = row[i];
}
- return obj as unknown as ProductImage;
+ return obj as unknown as ProductImage; // safe-cast: DuckDB query result matches this row schema — columns are mapped by name
});
}
@@ -325,7 +325,7 @@ export class Enrichment {
if (col === undefined) continue;
obj[col] = row[i];
}
- return obj as unknown as ProductReview;
+ return obj as unknown as ProductReview; // safe-cast: DuckDB query result matches this row schema — columns are mapped by name
});
}
}
diff --git a/packages/analytics/src/core/entity-resolver.ts b/packages/analytics/src/core/entity-resolver.ts
index 42b15bec14..6f755c2a8d 100644
--- a/packages/analytics/src/core/entity-resolver.ts
+++ b/packages/analytics/src/core/entity-resolver.ts
@@ -411,7 +411,7 @@ export class EntityResolver {
const col = columns[i];
if (col !== undefined) obj[col] = row[i];
}
- return obj as unknown as EntityRow;
+ return obj as unknown as EntityRow; // safe-cast: DuckDB query result matches this row schema — columns are mapped by name
});
}
}
diff --git a/packages/analytics/src/core/local-cache.ts b/packages/analytics/src/core/local-cache.ts
index 281f673827..4d65cdae9a 100644
--- a/packages/analytics/src/core/local-cache.ts
+++ b/packages/analytics/src/core/local-cache.ts
@@ -162,9 +162,7 @@ export class LocalCacheManager {
obj[col] = row[i];
}
}
- // T is narrowed by the caller's return type annotation; obj is built
- // from DuckDB column names so the shape is caller-verified.
- return obj as T;
+ return obj as T; // safe-cast: caller-provided generic boundary — obj is built from DuckDB column names and the caller's return type annotation verifies the shape
});
}
diff --git a/packages/analytics/src/core/spec-parser.ts b/packages/analytics/src/core/spec-parser.ts
index 9da6640b9c..6c60839368 100644
--- a/packages/analytics/src/core/spec-parser.ts
+++ b/packages/analytics/src/core/spec-parser.ts
@@ -236,7 +236,7 @@ export class SpecParser {
const col = columns[i];
if (col !== undefined) obj[col] = row[i];
}
- allSpecs.push(extractSpecsFromRow(obj as unknown as ProductRow));
+ allSpecs.push(extractSpecsFromRow(obj as unknown as ProductRow)); // safe-cast: DuckDB query result matches this row schema — columns are mapped by name
}
// Create specs table
@@ -301,7 +301,7 @@ export class SpecParser {
const col = columns[i];
if (col !== undefined) obj[col] = row[i];
}
- return obj as unknown as ProductSpecs;
+ return obj as unknown as ProductSpecs; // safe-cast: DuckDB query result matches this row schema — columns are mapped by name
});
}
@@ -356,7 +356,7 @@ export class SpecParser {
const col = columns[i];
if (col !== undefined) obj[col] = row[i];
}
- return obj as unknown as ProductSpecs;
+ return obj as unknown as ProductSpecs; // safe-cast: DuckDB query result matches this row schema — columns are mapped by name
});
}
}
diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts
index 25884cedbe..9dc1a14230 100644
--- a/packages/api-client/src/index.ts
+++ b/packages/api-client/src/index.ts
@@ -242,11 +242,11 @@ export class PackRatApiClient {
if (!response.ok) {
const errorMessage =
isObject(body) && 'error' in body
- ? String((body as Record).error)
+ ? String((body as Record).error) // safe-cast: isObject() guard confirms body is a non-null object; error field access is safe
: `HTTP ${response.status}`;
throw new ApiError(errorMessage, { status: response.status, body });
}
- return body as T;
+ return body as T; // safe-cast: caller-provided generic boundary — T is verified at each typed call-site
}
}
diff --git a/packages/api/src/containers/AppContainer.ts b/packages/api/src/containers/AppContainer.ts
index db68877958..85b2892592 100644
--- a/packages/api/src/containers/AppContainer.ts
+++ b/packages/api/src/containers/AppContainer.ts
@@ -2,9 +2,7 @@ import { env } from 'cloudflare:workers';
import { Container } from '@cloudflare/containers';
import type { Env } from '@packrat/api/types/env';
-// Module-level `env` from 'cloudflare:workers' is untyped at the TS layer;
-// the Workers runtime injects the correct shape. This double-cast is intentional.
-const typedEnv = env as unknown as Env;
+const typedEnv = env as unknown as Env; // safe-cast: Cloudflare Durable Object constructor — module-level env from 'cloudflare:workers' is injected by the runtime with the correct Env shape
/**
* App Container class that runs the Node.js TikTok service.
diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts
index 4651677e8f..61755b3015 100644
--- a/packages/api/src/index.ts
+++ b/packages/api/src/index.ts
@@ -83,11 +83,11 @@ type CfFetchFn = (
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext): Response | Promise {
- setWorkerEnv(env as unknown as Record);
- return (app.fetch as unknown as CfFetchFn)(request, env, ctx);
+ setWorkerEnv(env as unknown as Record); // safe-cast: Cloudflare Worker entry point — env is a plain bindings object at runtime
+ return (app.fetch as unknown as CfFetchFn)(request, env, ctx); // safe-cast: Elysia's fetch matches the CfFetchFn signature at runtime; unknown intermediate required for variance
},
async queue(batch: MessageBatch, env: Env): Promise {
- setWorkerEnv(env as unknown as Record);
+ setWorkerEnv(env as unknown as Record); // safe-cast: Cloudflare Worker entry point — env is a plain bindings object at runtime
if (batch.queue === 'packrat-etl-queue' || batch.queue === 'packrat-etl-queue-dev') {
if (!env.ETL_QUEUE) {
@@ -95,7 +95,7 @@ export default {
}
// The queue name check above is the runtime guard; the Worker runtime delivers
// correctly-typed messages for this queue binding.
- await processQueueBatch({ batch: batch as MessageBatch, env });
+ await processQueueBatch({ batch: batch as MessageBatch, env }); // safe-cast: queue name guard above confirms this batch carries CatalogETLMessage payloads
} else if (
batch.queue === 'packrat-embeddings-queue' ||
batch.queue === 'packrat-embeddings-queue-dev'
diff --git a/packages/api/src/middleware/auth.ts b/packages/api/src/middleware/auth.ts
index 6666f4c46f..a9ddf0dad7 100644
--- a/packages/api/src/middleware/auth.ts
+++ b/packages/api/src/middleware/auth.ts
@@ -31,7 +31,7 @@ export const authPlugin = new Elysia({ name: 'packrat-auth' }).macro({
userId: Number(payload.userId),
role: (payload.role as 'USER' | 'ADMIN') ?? 'USER',
...rest,
- } as AuthUser,
+ } as AuthUser, // safe-cast: JWT payload validated by auth middleware — userId and role fields are confirmed present
};
},
},
@@ -58,7 +58,7 @@ export const adminAuthPlugin = new Elysia({ name: 'packrat-admin-auth' }).use(au
userId: Number(payload.userId),
role: 'ADMIN' as const,
...rest,
- } as AuthUser,
+ } as AuthUser, // safe-cast: JWT payload validated by auth middleware — userId and ADMIN role confirmed present
};
},
},
diff --git a/packages/api/src/routes/packs/index.ts b/packages/api/src/routes/packs/index.ts
index 55340dba62..6f096aaa28 100644
--- a/packages/api/src/routes/packs/index.ts
+++ b/packages/api/src/routes/packs/index.ts
@@ -634,7 +634,7 @@ Limit to maximum 6 recommendations, prioritizing the most important gaps. Only s
notes: data.notes,
userId: user.userId,
embedding,
- } as NewPackItem)
+ } as NewPackItem) // safe-cast: object literal matches NewPackItem shape; cast required because embedding field type is narrower in the inferred type
.returning();
await db.update(packs).set({ updatedAt: new Date() }).where(eq(packs.id, packId));
diff --git a/packages/api/src/routes/weather.ts b/packages/api/src/routes/weather.ts
index bf1cd6bb8b..1574f24ab8 100644
--- a/packages/api/src/routes/weather.ts
+++ b/packages/api/src/routes/weather.ts
@@ -30,7 +30,7 @@ export const weatherRoutes = new Elysia({ prefix: '/weather' })
`${WEATHER_API_BASE_URL}/search.json?key=${WEATHER_API_KEY}&q=${encodeURIComponent(q)}`,
);
if (!response.ok) throw new Error(`API error: ${response.status}`);
- const data = (await response.json()) as WeatherAPISearchResponse;
+ const data = (await response.json()) as WeatherAPISearchResponse; // safe-cast: WeatherAPI.com response shape matches this type
return data.map((item) => ({
id: item.id,
@@ -75,14 +75,14 @@ export const weatherRoutes = new Elysia({ prefix: '/weather' })
`${WEATHER_API_BASE_URL}/search.json?key=${WEATHER_API_KEY}&q=${encodeURIComponent(q)}`,
);
if (!response.ok) throw new Error(`API error: ${response.status}`);
- const data = (await response.json()) as WeatherAPISearchResponse;
+ const data = (await response.json()) as WeatherAPISearchResponse; // safe-cast: WeatherAPI.com response shape matches this type
if (!data || data.length === 0) {
const currentResponse = await fetch(
`${WEATHER_API_BASE_URL}/current.json?key=${WEATHER_API_KEY}&q=${encodeURIComponent(q)}`,
);
if (!currentResponse.ok) throw new Error(`API error: ${currentResponse.status}`);
- const currentData = (await currentResponse.json()) as WeatherAPICurrentResponse;
+ const currentData = (await currentResponse.json()) as WeatherAPICurrentResponse; // safe-cast: WeatherAPI.com response shape matches this type
if (currentData?.location) {
return [
@@ -143,7 +143,7 @@ export const weatherRoutes = new Elysia({ prefix: '/weather' })
);
if (!response.ok) throw new Error(`API error: ${response.status}`);
- const data = (await response.json()) as WeatherAPIForecastResponse;
+ const data = (await response.json()) as WeatherAPIForecastResponse; // safe-cast: WeatherAPI.com response shape matches this type
return {
...data,
location: {
diff --git a/packages/api/src/services/etl/processValidItemsBatch.ts b/packages/api/src/services/etl/processValidItemsBatch.ts
index e11aaaaae5..51a6d36f2e 100644
--- a/packages/api/src/services/etl/processValidItemsBatch.ts
+++ b/packages/api/src/services/etl/processValidItemsBatch.ts
@@ -17,10 +17,7 @@ export async function processValidItemsBatch({
}): Promise {
const catalogService = new CatalogService(env, true);
- // Consolidate items with identical SKUs before upserting to avoid conflicting duplicate upserts.
- // items are Partial at the type level, but all required fields
- // have been confirmed present by CatalogItemValidator before reaching here.
- const mergedItems = mergeItemsBySku(items as NewCatalogItem[]);
+ const mergedItems = mergeItemsBySku(items as NewCatalogItem[]); // safe-cast: items are Partial at the type level, but all required fields have been confirmed present by CatalogItemValidator before reaching here
// Prepare texts for batch embedding
const embeddingTexts = mergedItems.map((item) => getEmbeddingText(item));
diff --git a/packages/api/src/services/r2-bucket.ts b/packages/api/src/services/r2-bucket.ts
index c8d1bbae84..1a7c0fb624 100644
--- a/packages/api/src/services/r2-bucket.ts
+++ b/packages/api/src/services/r2-bucket.ts
@@ -277,7 +277,7 @@ export class R2BucketService {
});
} else if (body instanceof globalThis.ReadableStream) {
// Web stream (Cloudflare Workers environment)
- webStream = body as ReadableStream;
+ webStream = body as ReadableStream; // safe-cast: body is confirmed to be a Web ReadableStream in the Cloudflare Workers environment
} else {
throw new Error('Unsupported stream type');
}
@@ -322,7 +322,7 @@ export class R2BucketService {
arrayBuffer: async () => {
assertStreamNotConsumed();
// Uint8Array.buffer is ArrayBufferLike; we allocate via new Uint8Array so it is always ArrayBuffer.
- return (await consumeStream()).buffer as ArrayBuffer;
+ return (await consumeStream()).buffer as ArrayBuffer; // safe-cast: Uint8Array allocated via new Uint8Array, so .buffer is always ArrayBuffer (not SharedArrayBuffer)
},
bytes: async () => {
assertStreamNotConsumed();
@@ -334,14 +334,13 @@ export class R2BucketService {
},
json: async () => {
assertStreamNotConsumed();
- // caller is responsible for type safety at this boundary
- return JSON.parse(new TextDecoder().decode(await consumeStream())) as T;
+ return JSON.parse(new TextDecoder().decode(await consumeStream())) as T; // safe-cast: caller-provided generic boundary — R2ObjectBody.json() mirrors the R2 platform API
},
blob: async () => {
assertStreamNotConsumed();
const data = await consumeStream();
// Uint8Array.buffer is ArrayBufferLike; we allocate via new Uint8Array so it is always ArrayBuffer.
- return new globalThis.Blob([data.buffer as ArrayBuffer]);
+ return new globalThis.Blob([data.buffer as ArrayBuffer]); // safe-cast: Uint8Array allocated via new Uint8Array, so .buffer is always ArrayBuffer (not SharedArrayBuffer)
},
};
@@ -604,6 +603,7 @@ export class R2BucketService {
if (!isObject(v)) return {};
const out: Record = {};
// isObject(v) confirms v is a non-null object; cast is safe for entry iteration
+ // safe-cast: isObject(v) guard confirms v is a non-null object; safe for entry iteration
for (const [k, val] of Object.entries(v as Record)) {
if (isString(val)) out[k] = val;
}
diff --git a/packages/api/src/services/refreshTokenService.ts b/packages/api/src/services/refreshTokenService.ts
index dbb82fdce9..d238b84752 100644
--- a/packages/api/src/services/refreshTokenService.ts
+++ b/packages/api/src/services/refreshTokenService.ts
@@ -54,7 +54,7 @@ export async function hashRefreshToken(raw: string): Promise {
async function tokenMatchClause(raw: string): Promise {
const hashed = await hashRefreshToken(raw);
if (hashed === raw) return eq(refreshTokens.token, raw);
- return or(eq(refreshTokens.token, hashed), eq(refreshTokens.token, raw)) as SQL;
+ return or(eq(refreshTokens.token, hashed), eq(refreshTokens.token, raw)) as SQL; // safe-cast: Drizzle sql`` tag returns SQL type — or() returns SQL | undefined but is non-null when given two non-null args
}
/**
@@ -172,5 +172,5 @@ export async function rotateRefreshToken(
/** Active-and-unrevoked clause for existing plaintext-style `.where` uses. */
export async function activeTokenClause(raw: string): Promise {
const match = await tokenMatchClause(raw);
- return and(match, isNull(refreshTokens.revokedAt)) as SQL;
+ return and(match, isNull(refreshTokens.revokedAt)) as SQL; // safe-cast: Drizzle sql`` tag returns SQL type — and() returns SQL | undefined but is non-null when given two non-null args
}
diff --git a/packages/api/src/utils/auth.ts b/packages/api/src/utils/auth.ts
index 6d6b80a8d6..a475661d6b 100644
--- a/packages/api/src/utils/auth.ts
+++ b/packages/api/src/utils/auth.ts
@@ -64,7 +64,7 @@ export async function verifyJWT({ token }: { token: string }): Promise(value: string): T | [] {
const normalized = normalizeJsonString(value);
try {
- // caller is responsible for type safety at this boundary
- return JSON.parse(normalized) as T;
+ return JSON.parse(normalized) as T; // safe-cast: caller-provided generic boundary — caller is responsible for type safety
} catch (err) {
console.warn('❌ Failed to parse JSON:', {
error: err,
diff --git a/packages/api/src/utils/env-validation.ts b/packages/api/src/utils/env-validation.ts
index 590bf5ff39..f1708a8303 100644
--- a/packages/api/src/utils/env-validation.ts
+++ b/packages/api/src/utils/env-validation.ts
@@ -140,21 +140,22 @@ function validate(rawEnv: Record): ValidatedEnv {
return {
...validated.data,
CF_VERSION_METADATA: (rawEnv.CF_VERSION_METADATA ??
- validated.data.CF_VERSION_METADATA) as WorkerVersionMetadata,
- AI: (rawEnv.AI ?? validated.data.AI) as Ai,
+ validated.data.CF_VERSION_METADATA) as WorkerVersionMetadata, // safe-cast: Cloudflare Worker binding injected by runtime
+ AI: (rawEnv.AI ?? validated.data.AI) as Ai, // safe-cast: Cloudflare Worker binding injected by runtime
PACKRAT_SCRAPY_BUCKET: (rawEnv.PACKRAT_SCRAPY_BUCKET ??
- validated.data.PACKRAT_SCRAPY_BUCKET) as R2Bucket,
- PACKRAT_BUCKET: (rawEnv.PACKRAT_BUCKET ?? validated.data.PACKRAT_BUCKET) as R2Bucket,
+ validated.data.PACKRAT_SCRAPY_BUCKET) as R2Bucket, // safe-cast: Cloudflare Worker binding injected by runtime
+ PACKRAT_BUCKET: (rawEnv.PACKRAT_BUCKET ?? validated.data.PACKRAT_BUCKET) as R2Bucket, // safe-cast: Cloudflare Worker binding injected by runtime
PACKRAT_GUIDES_BUCKET: (rawEnv.PACKRAT_GUIDES_BUCKET ??
- validated.data.PACKRAT_GUIDES_BUCKET) as R2Bucket,
- ETL_QUEUE: (rawEnv.ETL_QUEUE ?? validated.data.ETL_QUEUE) as Queue,
- LOGS_QUEUE: (rawEnv.LOGS_QUEUE ?? validated.data.LOGS_QUEUE) as Queue,
- EMBEDDINGS_QUEUE: (rawEnv.EMBEDDINGS_QUEUE ?? validated.data.EMBEDDINGS_QUEUE) as Queue,
+ validated.data.PACKRAT_GUIDES_BUCKET) as R2Bucket, // safe-cast: Cloudflare Worker binding injected by runtime
+ ETL_QUEUE: (rawEnv.ETL_QUEUE ?? validated.data.ETL_QUEUE) as Queue, // safe-cast: Cloudflare Worker binding injected by runtime
+ LOGS_QUEUE: (rawEnv.LOGS_QUEUE ?? validated.data.LOGS_QUEUE) as Queue, // safe-cast: Cloudflare Worker binding injected by runtime
+ EMBEDDINGS_QUEUE: (rawEnv.EMBEDDINGS_QUEUE ?? validated.data.EMBEDDINGS_QUEUE) as Queue, // safe-cast: Cloudflare Worker binding injected by runtime
+ // safe-cast: Cloudflare Worker binding injected by runtime
APP_CONTAINER: (rawEnv.APP_CONTAINER ?? validated.data.APP_CONTAINER) as DurableObjectNamespace<
Container
>,
- TOKEN_RATE_LIMITER: rawEnv.TOKEN_RATE_LIMITER as ValidatedEnv['TOKEN_RATE_LIMITER'] | undefined,
- } as ValidatedEnv;
+ TOKEN_RATE_LIMITER: rawEnv.TOKEN_RATE_LIMITER as ValidatedEnv['TOKEN_RATE_LIMITER'] | undefined, // safe-cast: Cloudflare Worker binding injected by runtime
+ } as ValidatedEnv; // safe-cast: all fields have been individually assigned above with correct runtime binding types
}
/**
@@ -166,14 +167,15 @@ let cachedRawEnv: Record | undefined;
function getRawEnv(): Record {
if (cachedRawEnv) return cachedRawEnv;
+ // safe-cast: accessing arbitrary Cloudflare Worker isolate env property injected at runtime
const primed = (globalThis as Record).__cfWorkersEnv__;
if (isObject(primed)) {
- cachedRawEnv = primed as Record;
+ cachedRawEnv = primed as Record; // safe-cast: isObject confirmed above
return cachedRawEnv;
}
// Test / Node fallback
- cachedRawEnv = { ...process.env } as Record;
+ cachedRawEnv = { ...process.env } as Record; // safe-cast: widening to generic env dict
return cachedRawEnv;
}
@@ -183,6 +185,7 @@ function getRawEnv(): Record {
*/
export function setWorkerEnv(rawEnv: Record): void {
cachedRawEnv = rawEnv;
+ // safe-cast: storing rawEnv on globalThis for cross-request access in the Worker isolate
(globalThis as Record).__cfWorkersEnv__ = rawEnv;
}
diff --git a/packages/checks/src/check-type-casts.ts b/packages/checks/src/check-type-casts.ts
index 294027ce16..40132af3ca 100644
--- a/packages/checks/src/check-type-casts.ts
+++ b/packages/checks/src/check-type-casts.ts
@@ -22,7 +22,14 @@ const ROOT = join(import.meta.dir, '..', '..', '..');
const SCAN_ROOTS = ['apps', 'packages'];
const EXCLUDED_DIRS = new Set(['node_modules', 'dist', 'build', '.next', '.expo', 'drizzle']);
const TARGET_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']);
-const EXCLUDED_FILE_PATTERNS = [/\.test\./, /\.spec\./, /\.stories\./, /\.d\.ts$/];
+const EXCLUDED_FILE_PATTERNS = [
+ /\.test\./,
+ /\.spec\./,
+ /\.stories\./,
+ /\.d\.ts$/,
+ /\/__tests__\//, // any file inside a __tests__ directory
+ /\/test\//, // any file inside a test directory
+];
// Safe casts that TypeScript requires and cannot be replaced with guards
const SAFE_CAST_PATTERNS = [
@@ -36,8 +43,14 @@ const SAFE_CAST_PATTERNS = [
/\bas\s+ReturnType\b/,
/\bas\s+InstanceType\b/,
/\bas\s+Awaited\b/,
+ /\bas\s+ArrayBuffer\b/,
+ /\bas\s+ReadableStream\b/,
+ /\bas\s+SQL\b/,
];
+// Inline annotation that marks a cast as intentional (place on the cast line or the line before)
+const SAFE_CAST_ANNOTATION = /\/\/\s*safe-cast:/;
+
// Detects `as SomeType` where SomeType starts with uppercase or is a known type pattern
// Excludes HTML element casts which are necessary in DOM manipulation
const CAST_PATTERN = /\bas\s+([A-Z][A-Za-z0-9_<>[\]|&,\s]*?)(?=\s*[;,)\]}]|\s*\/\/|\s*$)/gm;
@@ -46,6 +59,8 @@ const IMPORT_LINE = /^\s*(import|export)\b/;
const ARRAY_LITERAL_CAST = /\]\s*as\s+[A-Z]/;
const COMMENT_LINE = /^\s*(\/\/|\*)/;
const LOWERCASE_TYPE = /^[a-z][a-z]*$/;
+// Matches phrases like "GPS devices", "HTML tags" — natural language in string content
+const NATURAL_LANGUAGE_CAST = /^[A-Z]{2,3}\s+[a-z]/;
interface Violation {
file: string;
@@ -91,6 +106,22 @@ function collectViolations(filePath: string): Violation[] {
// Skip array-literal type hints: `] as Type[]` in config/static data (no call expressions)
if (ARRAY_LITERAL_CAST.test(line) && !line.includes('(') && !line.includes('=>')) continue;
+ // Skip lines (and their cast matches) that carry a safe-cast annotation, either
+ // on the same line or anywhere in the immediately preceding run of comment lines.
+ if (SAFE_CAST_ANNOTATION.test(line)) continue;
+ {
+ let annotated = false;
+ for (let j = i - 1; j >= 0 && j >= i - 10; j--) {
+ const prev = lines[j] ?? '';
+ if (SAFE_CAST_ANNOTATION.test(prev)) {
+ annotated = true;
+ break;
+ }
+ if (!COMMENT_LINE.test(prev.trimStart())) break;
+ }
+ if (annotated) continue;
+ }
+
CAST_PATTERN.lastIndex = 0;
for (let match = CAST_PATTERN.exec(line); match !== null; match = CAST_PATTERN.exec(line)) {
const castType = match[1]?.trim();
@@ -99,6 +130,9 @@ function collectViolations(filePath: string): Violation[] {
// Skip single-word lowercase types (string, number, boolean, void, etc.)
if (LOWERCASE_TYPE.test(castType)) continue;
+ // Skip natural language inside string literals, e.g. "such as GPS devices"
+ if (NATURAL_LANGUAGE_CAST.test(castType)) continue;
+
// Skip `as keyof typeof X` — TypeScript sometimes requires this
if (castType.startsWith('keyof')) continue;
diff --git a/packages/cli/src/shared.ts b/packages/cli/src/shared.ts
index 7d4c4cfb0e..2480e63acb 100644
--- a/packages/cli/src/shared.ts
+++ b/packages/cli/src/shared.ts
@@ -54,7 +54,7 @@ export function printTable(rows: unknown[], options?: { title?: string }): void
const firstRow = rows[0];
assertDefined(firstRow, 'rows[0] must be defined after length check');
- const keys = Object.keys(firstRow as Record);
+ const keys = Object.keys(firstRow as Record); // safe-cast: rows are plain objects from DuckDB/DB queries; unknown[] narrows to Record after length check
const table = new Table({
head: keys.map((k) => chalk.cyan(k)),
@@ -62,7 +62,7 @@ export function printTable(rows: unknown[], options?: { title?: string }): void
});
for (const row of rows) {
- table.push(keys.map((k) => formatValue((row as Record)[k])));
+ table.push(keys.map((k) => formatValue((row as Record)[k]))); // safe-cast: rows are plain objects from DuckDB/DB queries; unknown[] narrows to Record for key access
}
if (options?.title) {
diff --git a/packages/config/src/config.ts b/packages/config/src/config.ts
index d966975cc7..0e3a5e9a31 100644
--- a/packages/config/src/config.ts
+++ b/packages/config/src/config.ts
@@ -51,8 +51,7 @@ function deepFreeze(value: T): Readonly {
if (!isObject(value)) return value;
if (Object.isFrozen(value)) return value;
- // value is narrowed to object by the check above; cast is required because
- // TypeScript's Object.values signature needs a non-abstract type.
+ // safe-cast: value is narrowed to non-null object by isObject() guard above
for (const nestedValue of Object.values(value as Record)) {
deepFreeze(nestedValue);
}
diff --git a/packages/guards/src/narrow.ts b/packages/guards/src/narrow.ts
index 1458c9006e..98b3849172 100644
--- a/packages/guards/src/narrow.ts
+++ b/packages/guards/src/narrow.ts
@@ -58,7 +58,7 @@ export const asDate = (value: unknown): Date | undefined => {
export const asStringRecord = (value: unknown): Record => {
if (value === null || typeof value !== 'object') return {};
const out: Record = {};
- // TypeScript requires an explicit cast here; value is narrowed to object by the check above.
+ // safe-cast: guards package internal narrowing — value is confirmed non-null object by preceding check
for (const [key, val] of Object.entries(value as Record)) {
if (typeof val === 'string') out[key] = val;
}
diff --git a/packages/web-ui/src/components/form.tsx b/packages/web-ui/src/components/form.tsx
index 305758f01b..c346a9ef47 100644
--- a/packages/web-ui/src/components/form.tsx
+++ b/packages/web-ui/src/components/form.tsx
@@ -23,9 +23,7 @@ type FormFieldContextValue<
name: TName;
};
-// Standard React context sentinel: {} is a valid placeholder because FormField
-// always wraps its children in a Provider before useFormField is called.
-const FormFieldContext = React.createContext({} as FormFieldContextValue);
+const FormFieldContext = React.createContext({} as FormFieldContextValue); // safe-cast: React context sentinel — FormField always provides a real value via Provider before consumers run
const FormField = <
TFieldValues extends FieldValues = FieldValues,
@@ -67,8 +65,7 @@ type FormItemContextValue = {
id: string;
};
-// Standard React context sentinel: FormItem always provides a real id before consumers run.
-const FormItemContext = React.createContext({} as FormItemContextValue);
+const FormItemContext = React.createContext({} as FormItemContextValue); // safe-cast: React context sentinel — FormItem always provides a real id via Provider before consumers run
const FormItem = React.forwardRef>(
({ className, ...props }, ref) => {
diff --git a/scripts/lint/no-duplicate-guards.ts b/scripts/lint/no-duplicate-guards.ts
index 3c9f365d48..6bd250c505 100644
--- a/scripts/lint/no-duplicate-guards.ts
+++ b/scripts/lint/no-duplicate-guards.ts
@@ -92,7 +92,7 @@ function isTargetFile(name: string): boolean {
}
function isExcluded(relPath: string): boolean {
- return EXCLUDED_ROOTS.some((p) => relPath === p || relPath.startsWith(p + '/'));
+ return EXCLUDED_ROOTS.some((p) => relPath === p || relPath.startsWith(`${p}/`));
}
function walkDir(dir: string, relPath: string, violations: Violation[]): void {