Imported from gencau/test-practices-agent-configurations (
dataset/repos/oss-apps§split-pro/AGENTS.md). Install upstream withnpx skills add gencau/test-practices-agent-configurations --skill oss-apps§split-pro. Copyright stays with the author.
AGENTS.md - Coding Agent Guidelines for SplitPro
Build/Lint/Test Commands
Development
pnpm dev # Start dev server with Turbopack
pnpm d # Full dev setup (install, docker, migrate, dev)
pnpm dx # Setup dependencies (install, docker up, migrate dev)
pnpm dx:up # Start Docker containers
pnpm dx:down # Stop Docker containers
Worktrees
- when using git worktrees, store them in the .worktrees directory in the root of the project folder
- when creating a worktree, copy
.envfrom the root project into the worktree - update worktree-local app urls/port to avoid conflicts with the main workspace:
- set
NEXTAUTH_URLandNEXTAUTH_URL_INTERNALto a unique localhost port - run dev server with that same port (for example
pnpm dev --port 3174)
- set
- use a separate postgres instance per worktree (different host port + db name)
- set unique values in the worktree
.env:POSTGRES_PORT,POSTGRES_DB,POSTGRES_CONTAINER_NAME,DATABASE_URL - start dedicated db container, for example:
docker run -d --name <unique-container-name> -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=strong-password -e POSTGRES_DB=<worktree-db> -p <worktree-port>:5432 ossapps/postgres:17.7-trixie postgres -c shared_preload_libraries=pg_cron -c cron.database_name=<worktree-db> -c cron.timezone=UTC
- enable pg_cron extension once:
psql "postgresql://postgres:strong-password@localhost:<worktree-port>/<worktree-db>" -c "CREATE EXTENSION IF NOT EXISTS pg_cron;" - initialize schema and data:
pnpm exec prisma migrate reset --force --skip-seed && pnpm db:seed
- set unique values in the worktree
Database
pnpm db:push # Push Prisma schema to database
pnpm db:studio # Open Prisma Studio
pnpm db:dev # Run Prisma migrations (dev)
pnpm db:seed # Seed the database
pnpm generate # Generate Prisma client
Testing
pnpm test # Run all tests
pnpm test:watch # Run tests in watch mode
pnpm test src/tests/simplify.test.ts # Run a specific test file
pnpm test -- -t "test name pattern" # Run tests matching pattern
Linting & Formatting
pnpm lint # Run oxlint with type-aware checking
pnpm prettier --write . # Format all files
pnpm prettier --check . # Check formatting without changes
pnpm tsgo --noEmit # Type check (used in pre-commit)
Building
pnpm build # Build for production
CI Pipeline (runs on PRs)
pnpm prettier --check .- Format checkpnpm lint- Lintingpnpm tsgo --noEmit- Type checkingpnpm test- Unit testspnpm build --no-lint- Build verification
Code Style Guidelines
Formatting (Prettier)
- Semicolons: required
- Quotes: single quotes
- Trailing commas: all
- Print width: 100 characters
- Tailwind CSS classes are auto-sorted
TypeScript Configuration
- Strict mode enabled
- Target: ES2022
noUncheckedIndexedAccess: true- handle undefined for index access- Path aliases:
~/*maps to./src/*@/*maps to./*
Import Organization
- External packages first (react, next, third-party libs)
- Internal imports using
~/alias - Relative imports for nearby files
- Use
typekeyword for type-only imports
import { HeartHandshakeIcon, X } from 'lucide-react';
import { useTranslation } from 'next-i18next';
import React, { useCallback } from 'react';
import { type CurrencyCode } from '~/lib/currency';
import { useAddExpenseStore } from '~/store/addStore';
import { api } from '~/utils/api';
import { Button } from '../ui/button';
Naming Conventions
- Components: PascalCase (
AddExpensePage.tsx,Button.tsx) - Utility files: camelCase (
utils.ts,currency.ts) - Test files:
*.test.tspattern - Pages: kebab-case for routes (
import-splitwise.tsx) - Dynamic routes:
[paramName].tsx - Functions: camelCase (
calculateParticipantSplit) - Constants: UPPER_SNAKE_CASE (
DEFAULT_CATEGORY) - Types/Interfaces: PascalCase (
AddExpenseState) - Hooks: camelCase with
useprefix (useAddExpenseStore)
Yoda Conditions
Use literal on the left side for comparisons:
if ('authenticated' === status) { ... }
if (0n === amount) { ... }
if ('' === description) { ... }
BigInt Usage
- All monetary values are BigInt (cents/smallest unit) to prevent rounding errors
- Use
nsuffix for BigInt literals:0n,1n,10000n - Use
BigMathhelper (src/utils/numbers.ts) for arithmetic:abs,sign,min,max,roundDiv - Use
getCurrencyHelpers({ currency, locale })for display/parsing
Error Handling
tRPC API errors:
if (!group) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Group not found' });
}
Client-side errors:
try {
await mutation.mutateAsync(data);
} catch (error) {
toast.error(error instanceof Error ? error.message : 'An unexpected error occurred');
}
Promise chains:
router.push('/path').catch(console.error);
Functions and Iteration
-
Prefer arrow functions over function declarations:
// Good const calculateTotal = (items: Item[]) => items.reduce((sum, item) => sum + item.amount, 0n); // Avoid function calculateTotal(items: Item[]) { ... } -
Prefer iterator methods over for loops (
map,filter,reduce,forEach,find,some,every):// Good const totals = expenses.filter(e => !e.deletedAt).map(e => e.amount); // Avoid for (const expense of expenses) { ... } -
Prefer early returns to reduce nesting. Put the shorter/simpler case first:
// Good const processItem = (item: Item) => { if (!item.needsProcessing) { return item; } // Complex processing logic here... return processedItem; }; // Avoid const processItem = (item: Item) => { if (item.needsProcessing) { // Complex processing logic here... return processedItem; } else { return item; } };
React Patterns
- Functional components with TypeScript interfaces
useCallbackfor memoized callbacks,useMemofor computed values- Zustand for global state, tRPC/React Query for server state
cn()utility for conditional Tailwind classes- Use
as constandsatisfiesfor fixed arrays/objects
Test Structure
describe('functionName', () => {
describe('ScenarioGroup', () => {
it('should do something specific', () => {
const result = functionName(input);
expect(result).toBe(expected);
});
});
});
Linting Rules (oxlint)
no-unused-vars: warnno-console: warn (allows info, warn, error, debug, trace in JSX/TSX)sort-imports: error- Ignored paths:
prisma/seed.ts,src/components/ui/*
Tech Stack
- Framework: Next.js 15 (Pages Router)
- Database: PostgreSQL + Prisma ORM
- API: tRPC
- State: Zustand + React Query
- Styling: Tailwind CSS v4
- UI: Radix UI primitives
- Auth: NextAuth.js
- Validation: Zod
- i18n: next-i18next
- Package Manager: pnpm
Key Architecture Notes
- Double-entry balance accounting: Every expense creates two balance records (bidirectional)
- Transaction batching: Use
db.$transaction(operations)for expense mutations - Split types:
EQUAL,PERCENTAGE,SHARE,EXACT,ADJUSTMENT,SETTLEMENT,CURRENCY_CONVERSION - Schema typo:
firendId(notfriendId) in GroupBalance - maintain for consistency
Typescript Migrations
Data migrations too complex for SQL queries run automatically during server startup via src/instrumentation.ts.
Architecture
src/migrations/index.ts- Migration runner that checksAppMetadata.schema_versionand runs pending migrationssrc/migrations/{name}.ts- Individual migration files exporting an async function
Adding New Migrations
- Create a new file
src/migrations/{description}.ts:
import { db } from '~/server/db';
export async function myMigrationFunction(): Promise<void> {
// Migration logic using the shared db connection
const records = await db.someModel.findMany({ ... });
for (const record of records) {
await db.$transaction(async (tx) => {
// Atomic operations
});
}
console.log('Migration completed');
}
- Register the migration in
src/migrations/index.tsby calling it in its respective case block.
Version Tracking
- Schema version is stored in
AppMetadatatable with keyschema_version - Versions are compared lexicographically (semver format:
"2.0.0","2.1.0", etc.) - Missing version (pre-2.0.0 databases) is treated as needing all migrations
Key Points
- Migrations run on every server start but are idempotent (version check)
- Use the shared
dbconnection from~/server/db - Wrap related operations in
db.$transaction()for atomicity - Log progress for visibility during deployment
Localization
- Only create English translation keys when adding features
- Translations are handled via Weblate by the community
- Translation files:
public/locales/{lang}/*.json
Pre-commit Hooks
Husky runs on commit:
- Prettier formatting
- oxlint with auto-fix
- Prisma format (for .prisma files)
- Type checking with tsgo
Override with git commit --no-verify if needed.