Development -> main#2315
Conversation
Agent-Logs-Url: https://github.com/PackRat-AI/PackRat/sessions/ef4c31bb-e06b-4c3d-90d4-0756bb8ca977 Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
- Add useCFAccessIdentity() hook to cfAccess.ts backed by useQuery with staleTime/gcTime=Infinity and retry=false so a null result is treated as "not behind CF Access" rather than a transient failure - Create lib/queryKeys.ts as a centralised registry for all admin query keys (admin, platform, catalogAnalytics, cfAccessIdentity) - Refactor auth-guard.tsx: replace manual canceled-flag useEffect pattern with useCFAccessIdentity(); render null while pending or when neither CF session nor stored token exists - Refactor login/page.tsx: replace useState<boolean|null> + raw useEffect with useCFAccessIdentity(); Local dev badge now keyed on !cfPending && !cfIdentity - Update all dashboard pages (page, users, packs, catalog), edit-catalog-dialog, and analytics hooks to use queryKeys.* instead of inline string arrays
refactor(admin): migrate CF Access identity fetch to TanStack Query
refactor: use zod for CF Access payload validation
chore(api): remove HTMX admin routes
Uses the CF Workers built-in Rate Limiting API (TOKEN_RATE_LIMITER binding) so brute-force attempts against the Basic→JWT token exchange endpoint are blocked at the edge before credentials are evaluated. Binding is optional — absent in local dev/test so nothing breaks without a CF environment. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
… store - cfAccess.ts: suppress useMaxParams for verifyCFAccessRequest (3 semantically distinct params, no benefit to wrapping) - TripWeatherDetailsScreen: replace any with WeatherApiForecastResponse / HourWeather / ForecastDay from existing weather types - packWeightHistory: fix unused biome-ignore by extracting endpoint variable so suppress is adjacent to the actual any cast 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Adds unit tests for verifyCFAccessRequest (cfAccess.ts), integration tests for adminAuthGuard branching (CF Access configured vs. absent), and round-trip tests for issueAdminJwt/verifyAdminJwt via HTTP. Also fixes a pre-existing EvalError in the CF Workers vitest pool: Elysia 1.4+ uses new Function() (AOT compilation) at module init time, which workerd/QuickJS disallows outside a request handler. Fix mocks the Elysia constructor via Proxy to force aot:false and stubs CloudflareAdapter.beforeCompile to a no-op, unblocking all integration tests from the EvalError that had been affecting the full suite.
- Replace 9 raw fetch() calls in useAuthActions with typed Treaty calls
(login, register, google, apple, logout, forgot/reset-password,
verify-email, resend-verification). Removes the clientEnvs URL
dependency from auth hooks entirely; all auth flows now go through
the same authFetcher path as the rest of the app.
- Add extractAuthError helper to surface server error messages from
Treaty error.value (preserves user-facing messages from API responses).
- Fix analyze_pack_weight in MCP tools: replace inline 'as { items?:...'
cast with a named PackDetailResponse interface, making the shape
explicit and reusable.
https://claude.ai/code/session_01YNwXsXiw34xFb75bnWDcig
…arsing
Replace every `as object[] | null`, `as object | null`, `as any`, and
`as unknown as T` cast with validated Zod schema parsing sourced from the
API package's canonical schemas.
Schema changes (packages/api/src/schemas/):
- packs.ts: add localCreatedAt/localUpdatedAt/templateId to PackSchema;
export CreatePackBodySchema, UpdatePackBodySchema,
PackWeightHistoryResponseSchema, CreatePackWeightHistoryBodySchema
- trips.ts (new): TripSchema, TripLocationSchema, create/update body schemas
- trailConditions.ts (new): TrailConditionReportSchema with enum types
Legend-State stores — all `as object[]`/`as object | null` casts removed:
- packs.ts: explicit body mapping for create/update; parse with
PackWithWeightsSchema (fixes three `as any` casts)
- packItems.ts: parse list with PackWithWeightsSchema.array().flatMap();
parse create/update responses with PackItemSchema
- packWeightHistory.ts: explicit body mapping, parse with
PackWeightHistoryResponseSchema (fixes `as any` body cast)
- trips.ts: parse with TripSchema
- trailConditionReports.ts: parse with TrailConditionReportSchema
- packTemplates.ts: parse with PackTemplateWithItemsSchema / PackTemplateSchema
- packTemplateItems.ts: parse with PackTemplateWithItemsSchema.flatMap /
PackTemplateItemSchema (removes two `as unknown as PackTemplateItem` casts)
Hooks and services — all `as unknown as T` casts removed:
- usePackDetailsFromApi: PackWithWeightsSchema.parse
- usePackItemDetailsFromApi: PackItemSchema.parse
- weatherService: LocationSearchResponseSchema.parse + WeatherAPIForecastResponseSchema.parse;
drop local WeatherApiForecastResponse type in favour of schema-derived type
- useCatalogItems: CatalogItemsResponseSchema.parse (drops local PaginatedCatalogItemsResponse import)
- useWildlifeIdentification: zodGuard from @packrat/guards validates
{ results: unknown[] } shape before casting items to IdentificationResult[]
https://claude.ai/code/session_01YNwXsXiw34xFb75bnWDcig
- Move @packrat/api/schemas/packs import before expo-app/* in packItems.ts - Break long import lines in packTemplates.ts and packTemplateItems.ts - Break long line in useWildlifeIdentification.ts (error throw) - Break long line in useAuthActions.ts (register.post call) https://claude.ai/code/session_01YNwXsXiw34xFb75bnWDcig
- Break .array().parse().flatMap() chain in packItems and packTemplateItems (Biome wraps method chains that exceed 100 chars) - Reorder named imports in weatherService to alphabetical order by identifier (type WeatherAPIForecastResponse between L and WeatherAPIForecastResponseSchema) https://claude.ai/code/session_01YNwXsXiw34xFb75bnWDcig
Switch from PROVIDER_GOOGLE to platform-specific provider: Google Maps on Android, Apple Maps (default) on iOS. No package change needed — react-native-maps 1.20.1 is already the Expo SDK 54 pinned version.
Agent-Logs-Url: https://github.com/PackRat-AI/PackRat/sessions/7ceaa179-fb85-4c8e-a60c-3679aac36203 Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
feat(expo): use Apple Maps on iOS for trip map views
POST /api/alltrails/preview scrapes og:title, og:description, og:image from AllTrails URLs server-side. MCP tool preview_alltrails_url exposes this to agents for trip/pack enrichment.
13 tests covering request validation, OG metadata extraction, timeout (504), network failure (502), upstream 4xx (502), missing og:title (422), and redirect-outside-alltrails.com (400).
feat(api): AllTrails OG preview endpoint and MCP tool
…ypes-Kke59 refactor(admin): migrate CF Access identity fetch to TanStack Query
…stallation chore: switch Dependabot to native `bun` ecosystem
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 56 minutes and 0 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (52)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying packrat-guides with
|
| Latest commit: |
c2ffc98
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://6fd6bf7c.packrat-guides-6gq.pages.dev |
| Branch Preview URL: | https://development.packrat-guides-6gq.pages.dev |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
packrat-api-dev | c2ffc98 | Apr 26 2026, 09:02 PM |
Coverage Report for API Unit Tests Coverage (./packages/api)
File Coverage
|
||||||||||||||||||||||||||||||||||||||
Coverage Report for Expo Unit Tests Coverage (./apps/expo)
File CoverageNo changed files found. |
Deploying packrat-landing with
|
| Latest commit: |
c2ffc98
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://abb11c60.packrat-landing.pages.dev |
| Branch Preview URL: | https://development.packrat-landing.pages.dev |
There was a problem hiding this comment.
Pull request overview
Adds stronger admin authentication + rate limiting, introduces an AllTrails OpenGraph preview endpoint/tool, and refactors clients to use shared Zod schemas (plus substantial test infrastructure updates).
Changes:
- Add
/api/alltrails/previewroute + MCP tool to fetch AllTrails OG metadata, with validation and tests. - Harden admin auth by verifying Cloudflare Access JWTs, removing spoofable header reliance, adding token endpoint rate limiting, and adding integration/unit tests.
- Refactor Expo/Admin clients to rely more on shared
@packrat/apiZod schemas and centralized query keys; adjust test setup to avoid Elysia AOT/eval in workerd.
Reviewed changes
Copilot reviewed 52 out of 53 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/mcp/src/tools/trails.ts | Adds MCP tool for AllTrails preview endpoint. |
| packages/mcp/src/tools/packs.ts | Replaces inline response typing with a named response interface + typed API call. |
| packages/mcp/src/index.ts | Registers the new trail tools. |
| packages/api/wrangler.jsonc | Adds rate limiting binding configuration (incl. dev env). |
| packages/api/vitest.config.ts | Adds test alias stub for Elysia env/AOT behavior in workerd. |
| packages/api/test/utils/test-helpers.ts | Adds helper to build CF Access-like JWTs for tests. |
| packages/api/test/setup.ts | Adds global mocks to prevent eval/AOT + globally mocks CF Access verifier. |
| packages/api/test/alltrails.test.ts | Adds tests for AllTrails preview endpoint (validation, success, upstream errors). |
| packages/api/test/admin-jwt.test.ts | Adds round-trip tests for admin JWT issuance/verification via HTTP layer. |
| packages/api/test/admin-auth-guard.test.ts | Adds integration tests for adminAuthGuard branching (CF Access vs fallback). |
| packages/api/src/utils/env-validation.ts | Adds optional TOKEN_RATE_LIMITER binding to env validation/types. |
| packages/api/src/schemas/weather.ts | Tightens WeatherAPI schemas (lat/lon numbers, required fields). |
| packages/api/src/schemas/trips.ts | Adds shared Trip schemas/types for clients. |
| packages/api/src/schemas/trailConditions.ts | Adds shared Trail Condition Report schemas/types for clients. |
| packages/api/src/schemas/packs.ts | Extends pack schemas and adds request/response schemas for packs + weight history. |
| packages/api/src/routes/index.ts | Registers AllTrails routes. |
| packages/api/src/routes/alltrails.ts | Implements AllTrails OG preview scraping endpoint with redirect/domain checks. |
| packages/api/src/routes/admin/index.ts | Adds token rate limiting; removes HTML admin UI; adds search params for list endpoints. |
| packages/api/src/middleware/cfAccess.ts | Switches CF Access verification to schema-validated payload and stronger typing. |
| packages/api/src/middleware/tests/cfAccess.test.ts | Adds unit tests for CF Access JWT verification without network JWKS fetch. |
| packages/api/src/test-stubs/elysia-env.ts | Provides Elysia env stub to force ELYSIA_AOT=false in tests. |
| bun.lock | Bumps workspace package versions and adds guards dependency to admin app. |
| apps/expo/features/wildlife/hooks/useWildlifeIdentification.ts | Adds runtime response guard for wildlife identify endpoint. |
| apps/expo/features/weather/screens/LocationPreviewScreen.tsx | Uses formatted weather output but now relies on type assertions. |
| apps/expo/features/weather/lib/weatherService.ts | Parses API responses with shared schemas; adjusts formatting and icon typing. |
| apps/expo/features/weather/hooks/useLocationSearch.ts | Uses formatted weather output but now relies on type assertions. |
| apps/expo/features/weather/hooks/useLocationRefresh.ts | Uses formatted weather output but now relies on type assertions. |
| apps/expo/features/trips/store/trips.ts | Validates trip responses with shared TripSchema. |
| apps/expo/features/trips/screens/TripWeatherDetailsScreen.tsx | Replaces any weather state with shared API type. |
| apps/expo/features/trips/screens/TripDetailScreen.tsx | Removes explicit Google provider from maps. |
| apps/expo/features/trail-conditions/store/trailConditionReports.ts | Validates trail condition report responses with shared schema. |
| apps/expo/features/packs/store/packs.ts | Validates pack responses and explicitly shapes create/update request bodies. |
| apps/expo/features/packs/store/packWeightHistory.ts | Validates weight history responses and explicitly shapes create request body. |
| apps/expo/features/packs/store/packItems.ts | Validates pack item responses with shared schemas. |
| apps/expo/features/packs/hooks/usePackItemDetailsFromApi.ts | Validates pack item response with shared schema. |
| apps/expo/features/packs/hooks/usePackDetailsFromApi.ts | Validates pack details response with shared schema. |
| apps/expo/features/pack-templates/store/packTemplates.ts | Validates pack template responses with shared schemas. |
| apps/expo/features/pack-templates/store/packTemplateItems.ts | Validates pack template item responses with shared schemas. |
| apps/expo/features/catalog/hooks/useCatalogItems.ts | Validates catalog responses with shared schema. |
| apps/expo/features/auth/hooks/useAuthActions.ts | Refactors auth actions to use apiClient instead of manual fetch + adds error extraction. |
| apps/expo/app/(app)/trip/location-search.tsx | Removes explicit Google provider from maps. |
| apps/admin/lib/queryKeys.ts | Adds centralized TanStack Query key registry. |
| apps/admin/lib/cfAccess.ts | Adds Zod validation + TanStack Query hook for CF Access identity. |
| apps/admin/hooks/use-platform-analytics.ts | Migrates platform analytics hooks to centralized queryKeys. |
| apps/admin/hooks/use-catalog-analytics.ts | Migrates catalog analytics hooks to centralized queryKeys. |
| apps/admin/components/edit-catalog-dialog.tsx | Uses centralized queryKeys for cache invalidation. |
| apps/admin/components/auth-guard.tsx | Switches CF Access detection to TanStack Query hook. |
| apps/admin/app/login/page.tsx | Switches CF Access detection to TanStack Query hook. |
| apps/admin/app/dashboard/users/page.tsx | Uses centralized queryKeys (and adds search param-based keying). |
| apps/admin/app/dashboard/page.tsx | Uses centralized queryKeys for dashboard queries. |
| apps/admin/app/dashboard/packs/page.tsx | Uses centralized queryKeys (and adds search param-based keying). |
| apps/admin/app/dashboard/catalog/page.tsx | Uses centralized queryKeys (and adds search param-based keying). |
| .github/dependabot.yml | Switches Dependabot update entries from npm to bun ecosystems. |
Comments suppressed due to low confidence (1)
apps/expo/features/weather/lib/weatherService.ts:64
WeatherAPILocationSchema.localtimeis optional, butformatWeatherDataalways callstoLocaleTimeStringon a Date derived from it. Iflocaltimeis missing/empty,new Date(...)becomes an invalid date andtoLocaleTimeStringwill throw aRangeError. Either makelocaltimerequired in the shared schema (if the API guarantees it) or add a safe fallback (e.g., uselocaltime_epoch, ornew Date()when missing).
const localTime = new Date(location.localtime ?? '');
const formattedTime = localTime.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true,
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| mutationFn: (data: Parameters<typeof updateCatalogItem>[1]) => updateCatalogItem(item.id, data), | ||
| onSuccess: () => { | ||
| queryClient.invalidateQueries({ queryKey: ['admin', 'catalog'] }); | ||
| queryClient.invalidateQueries({ queryKey: queryKeys.admin.catalog() }); |
There was a problem hiding this comment.
This invalidation uses queryKeys.admin.catalog() which currently yields ['admin','catalog', undefined] and won't match catalog queries keyed with queryKeys.admin.catalog(q) / catalog(5). That can leave the list stale after edits. Prefer invalidating the root key (e.g. ['admin','catalog']) or make queryKeys.admin.catalog omit the third element when undefined.
| queryClient.invalidateQueries({ queryKey: queryKeys.admin.catalog() }); | |
| queryClient.invalidateQueries({ queryKey: ['admin', 'catalog'] }); |
| temp: Math.round(hour.temp_f), | ||
| icon: getIconNameFromCode(hour.condition.code, hour.is_day), | ||
| icon: getIconNameFromCode(hour.condition.code, hour.is_day) as string, | ||
| weatherCode: hour.condition.code, |
There was a problem hiding this comment.
getWeatherIconName already returns MaterialIconName, but these as string casts widen the type and make the formatWeatherData return value incompatible with WeatherLocation (which is why downstream code is forced to use as unknown as WeatherLocation). Dropping the casts and typing formatWeatherData to return WeatherLocation would let the compiler verify shape compatibility and eliminate the unsafe assertions in screens/hooks.
| const newLocation = formattedData as unknown as WeatherLocation; | ||
|
|
There was a problem hiding this comment.
This as unknown as WeatherLocation cast bypasses compile-time verification of the formatWeatherData output. If WeatherLocation is the intended shape, consider updating formatWeatherData to return WeatherLocation (and aligning any field/icon types) so this can be a normal assignment without assertions.
| const formattedData = formatWeatherData(weatherData); | ||
|
|
||
| updateLocation(locationId, { | ||
| temperature: formattedData.temperature, | ||
| condition: formattedData.condition, | ||
| time: formattedData.time, | ||
| highTemp: formattedData.highTemp, | ||
| lowTemp: formattedData.lowTemp, | ||
| alerts: formattedData.alerts, | ||
| details: formattedData.details, | ||
| hourlyForecast: formattedData.hourlyForecast, | ||
| dailyForecast: formattedData.dailyForecast, | ||
| }); | ||
| updateLocation(locationId, formattedData as unknown as Partial<WeatherLocation>); | ||
|
|
There was a problem hiding this comment.
formattedData as unknown as Partial<WeatherLocation> bypasses compile-time checking of the refresh payload shape. Prefer typing formatWeatherData to return WeatherLocation (or a well-defined subset) so updateLocation receives a type-safe object without assertions.
| mutationFn: () => deletePack(pack.id), | ||
| onSuccess: () => { | ||
| queryClient.invalidateQueries({ queryKey: ['admin', 'packs'] }); | ||
| queryClient.invalidateQueries({ queryKey: queryKeys.admin.packs() }); |
There was a problem hiding this comment.
This invalidation uses queryKeys.admin.packs() which currently yields ['admin','packs', undefined]. That won't invalidate queries keyed with queryKeys.admin.packs(q) (or the dashboard packs(5)), so pack lists can remain stale after deletion. Use a root key (e.g. ['admin','packs']) or adjust queryKeys.admin.packs to omit the third element when undefined.
| queryClient.invalidateQueries({ queryKey: queryKeys.admin.packs() }); | |
| queryClient.invalidateQueries({ queryKey: ['admin', 'packs'] }); |
| mutationFn: () => deleteCatalogItem(item.id), | ||
| onSuccess: () => { | ||
| queryClient.invalidateQueries({ queryKey: ['admin', 'catalog'] }); | ||
| queryClient.invalidateQueries({ queryKey: queryKeys.admin.catalog() }); |
There was a problem hiding this comment.
This invalidation uses queryKeys.admin.catalog() which currently yields ['admin','catalog', undefined]. That won't invalidate catalog queries keyed with queryKeys.admin.catalog(q) / catalog(5), so the table may not refresh after a delete. Use a root key (e.g. ['admin','catalog']) or adjust queryKeys.admin.catalog to omit the third element when undefined.
| queryClient.invalidateQueries({ queryKey: queryKeys.admin.catalog() }); | |
| queryClient.invalidateQueries({ queryKey: ['admin', 'catalog'] }); |
| setWeatherData(formattedData as unknown as WeatherLocation); | ||
|
|
There was a problem hiding this comment.
The as unknown as WeatherLocation assertion hides mismatches between formatWeatherData's return type and the WeatherLocation interface, so type drift here won't be caught at compile time. Prefer making formatWeatherData explicitly return WeatherLocation (and fixing any field/icon type mismatches) so this assignment is type-safe without assertions.
| const formattedData = formatWeatherData(weatherData); | ||
|
|
||
| updateLocation(location.id, { | ||
| temperature: formattedData.temperature, | ||
| condition: formattedData.condition, | ||
| time: formattedData.time, | ||
| highTemp: formattedData.highTemp, | ||
| lowTemp: formattedData.lowTemp, | ||
| alerts: formattedData.alerts, | ||
| details: formattedData.details, | ||
| hourlyForecast: formattedData.hourlyForecast, | ||
| dailyForecast: formattedData.dailyForecast, | ||
| }); | ||
| updateLocation(location.id, formattedData as unknown as Partial<WeatherLocation>); | ||
| } |
There was a problem hiding this comment.
Same as above: the as unknown as Partial<WeatherLocation> cast prevents the compiler from catching shape drift between formatWeatherData and what updateLocation expects. Consider returning/deriving a typed Partial<WeatherLocation> instead of asserting through unknown.
| users: (limitOrQuery?: number | string) => ['admin', 'users', limitOrQuery] as const, | ||
| packs: (limitOrQuery?: number | string) => ['admin', 'packs', limitOrQuery] as const, | ||
| catalog: (limitOrQuery?: number | string) => ['admin', 'catalog', limitOrQuery] as const, |
There was a problem hiding this comment.
users/packs/catalog query keys always include a third element, so calling queryKeys.admin.users() (or packs/catalog) produces ['admin','users', undefined]. That key will not match queries keyed as ['admin','users','search'] or ['admin','users',5] during invalidateQueries, so list pages can stay stale after mutations. Consider returning ['admin','users'] when the argument is undefined (or add separate usersRoot/packsRoot/catalogRoot constants for invalidation).
| users: (limitOrQuery?: number | string) => ['admin', 'users', limitOrQuery] as const, | |
| packs: (limitOrQuery?: number | string) => ['admin', 'packs', limitOrQuery] as const, | |
| catalog: (limitOrQuery?: number | string) => ['admin', 'catalog', limitOrQuery] as const, | |
| users: (limitOrQuery?: number | string) => | |
| limitOrQuery === undefined ? (['admin', 'users'] as const) : (['admin', 'users', limitOrQuery] as const), | |
| packs: (limitOrQuery?: number | string) => | |
| limitOrQuery === undefined ? (['admin', 'packs'] as const) : (['admin', 'packs', limitOrQuery] as const), | |
| catalog: (limitOrQuery?: number | string) => | |
| limitOrQuery === undefined ? (['admin', 'catalog'] as const) : (['admin', 'catalog', limitOrQuery] as const), |
| mutationFn: () => deleteUser(user.id), | ||
| onSuccess: () => { | ||
| queryClient.invalidateQueries({ queryKey: ['admin', 'users'] }); | ||
| queryClient.invalidateQueries({ queryKey: queryKeys.admin.users() }); |
There was a problem hiding this comment.
This invalidation uses queryKeys.admin.users() which currently yields ['admin','users', undefined]. That won't invalidate existing user list queries keyed with queryKeys.admin.users(q) (or the dashboard users(5)), so the UI may not refresh after deleting a user. Use a root key (e.g. ['admin','users']) or adjust queryKeys.admin.users to omit the third element when undefined.
| queryClient.invalidateQueries({ queryKey: queryKeys.admin.users() }); | |
| queryClient.invalidateQueries({ queryKey: ['admin', 'users'] }); |
Description
Closes #
Type of change
Area(s) affected
apps/expo)packages/api)apps/landing)apps/guides).github/)Testing
curlor Postman)Screenshots / recordings
Pre-merge checklist
bun format && bun lintpasses with no errorsbun check-typespasses with no errorsfeat:,fix:,chore:, etc.)